diff --git a/.claude/commands/claude-health.md b/.claude/commands/claude-health.md new file mode 100644 index 0000000..dedc2ce --- /dev/null +++ b/.claude/commands/claude-health.md @@ -0,0 +1,138 @@ +--- +description: Maintain coherence across Claude Code agentic capabilities (CLAUDE.md, rules, skills, commands, hooks) +allowed-tools: Bash(ls *) Bash(find *) Bash(wc *) Bash(head *) Bash(cat *) Bash(jq *) Bash(python3 *) +--- + +# /claude-health + +Audit the project's Claude Code configuration and surface inconsistencies. + +## 1. Inventory check + +Verify all expected Claude Code files exist: + +```! +ls -la .claude/rules/*.md .claude/commands/*.md .claude/settings.json 2>/dev/null | head -40 +``` + +```! +find .claude/skills -name "SKILL.md" 2>/dev/null +``` + +```! +ls -la CLAUDE.md AGENTS.md 2>/dev/null +``` + +## 2. Size validation + +Keep individual rules and commands tight. CLAUDE.md should stay under ~200 lines for best adherence. + +```! +wc -l CLAUDE.md AGENTS.md 2>/dev/null +``` + +```! +wc -c .claude/rules/*.md +``` + +```! +wc -c .claude/commands/*.md +``` + +```! +wc -c .claude/skills/*/SKILL.md +``` + +## 3. Cross-reference audit + +Confirm these alignments: + +| Source | Must reference | Check | +| ------------------------------------- | --------------------- | ------------------------------------ | +| `.claude/rules/python-style.md` | Pydantic, type hints | Matches `src/dppvalidator/` patterns | +| `.claude/rules/dpp-domain.md` | ESPR, CIRPASS, DPP | Matches domain model structure | +| `.claude/rules/commits.md` | Conventional commits | Consistent with gitflow workflows | +| `.claude/skills/validate-dpp/` | Validation logic | References correct validator paths | +| `.claude/skills/pypi-publish/` | Publishing steps | Matches `pyproject.toml` config | +| Root `AGENTS.md` (imported by CLAUDE) | Tech stack, structure | Reflects current project state | + +## 4. Command consistency + +Verify command descriptions match their content: + +```! +head -5 .claude/commands/*.md +``` + +Check cross-references between commands: + +- `/release` should reference `/lint` and `/test`. +- `/feature` and `/hotfix` follow gitflow. +- `/fix-lint` complements `/lint`. + +## 5. Hooks validation + +```! +cat .claude/settings.json | python3 -m json.tool > /dev/null && echo ".claude/settings.json valid JSON" +``` + +Verify hook commands are valid: + +- Commands use correct tool paths (`uv run ruff`, etc.). +- Hook scripts under `.claude/hooks/` are executable. +- Use the `$CLAUDE_PROJECT_DIR` env var for project-relative script paths. + +```! +ls -l .claude/hooks/ 2>/dev/null +``` + +## 6. CLAUDE.md / AGENTS.md consistency + +Verify root context covers: + +- [ ] Project overview and purpose +- [ ] Tech stack (Python 3.10+, Pydantic v2, uv, ruff, ty) +- [ ] Directory structure +- [ ] Development workflow (gitflow) +- [ ] Code principles (SOLID, DRY) + +Check for conflicts between: + +- Root `AGENTS.md` ↔ `.claude/rules/*.md` +- Skills ↔ Commands (e.g. `/pypi-publish` skill vs `/release` command) +- `CLAUDE.md` ↔ `AGENTS.md` (CLAUDE.md should `@AGENTS.md`, not duplicate) + +```! +head -3 CLAUDE.md +``` + +## 7. Skill / command frontmatter + +Spot-check that skill frontmatter is well-formed: + +```! +for f in .claude/skills/*/SKILL.md; do echo "=== $f ==="; awk '/^---$/{c++; if(c==2) exit} c==1' "$f"; done +``` + +```! +for f in .claude/commands/*.md; do echo "=== $f ==="; awk '/^---$/{c++; if(c==2) exit} c==1' "$f"; done +``` + +## 8. Report and fix + +Document any misalignments found: + +- [ ] Missing files → create from templates. +- [ ] Oversized files → trim or split into path-scoped rules / supporting files. +- [ ] Stale references → update paths. +- [ ] Outdated tech stack → update `AGENTS.md`. +- [ ] Conflicting guidance → resolve in favor of `AGENTS.md` and `CLAUDE.md`. + +If changes were made, commit: + +```bash +git add .claude/ CLAUDE.md AGENTS.md \ + && git commit -m "chore: align claude-code agentic capabilities" +``` + +**Run this workflow monthly or after major refactors.** diff --git a/.claude/commands/code-health.md b/.claude/commands/code-health.md new file mode 100644 index 0000000..7c56ec4 --- /dev/null +++ b/.claude/commands/code-health.md @@ -0,0 +1,92 @@ +--- +description: Maintain code coherence, remove inconsistencies, improve readability (DRY, SOLID, SOTA) +allowed-tools: Bash(uv run ruff *) Bash(uv run ty *) Bash(uv run pytest *) Bash(uv run coverage *) +--- + +# /code-health + +## 1. Static analysis + +```! +uv run ruff check src/dppvalidator/ tests/ --fix +``` + +```! +uv run ruff format src/dppvalidator/ tests/ +``` + +## 2. Type check + +```! +uv run ty check src/dppvalidator/ +``` + +## 3. Review checklist + +### DRY (don't repeat yourself) + +- [ ] No duplicate code blocks across modules. +- [ ] Shared logic extracted to utility functions. +- [ ] Constants defined centrally (e.g. in config or constants module). +- [ ] Common test fixtures in `tests/conftest.py`. + +### SOLID principles + +- [ ] **S**ingle responsibility: each validator/model has one purpose. +- [ ] **O**pen/closed: new validators extend patterns, don't modify base. +- [ ] **L**iskov substitution: subclasses honor parent contracts. +- [ ] **I**nterface segregation: no forced unused dependencies. +- [ ] **D**ependency inversion: use protocols/ABCs for abstractions. + +### Consistency (codebase-specific) + +- [ ] All modules use Pydantic v2 patterns. +- [ ] All public functions have type hints (ty enforced). +- [ ] All models inherit from appropriate Pydantic base. +- [ ] Import order: stdlib → third-party → local. + +### Readability + +- [ ] Function names describe what they do. +- [ ] Variables have meaningful names (no `x`, `temp`, `data`). +- [ ] Complex logic has inline comments explaining *why*. +- [ ] Max function length ~50 lines; split if larger. + +## 4. Find code smells + +```! +uv run ruff check src/dppvalidator/ --select=C901,PLR0912,PLR0915 --statistics +``` + +This checks for: + +- `C901`: complex functions (cyclomatic complexity) +- `PLR0912`: too many branches +- `PLR0915`: too many statements + +## 5. Test coverage + +```! +uv run pytest tests/ --cov=src/dppvalidator --cov-report=term-missing --cov-fail-under=80 +``` + +## 6. Final verification + +```! +uv run pytest tests/ -q +``` + +______________________________________________________________________ + +## Quick reference: common refactorings + +| Smell | Refactoring | +| ------------------- | --------------------------------------------- | +| Duplicate code | Extract to shared utility module | +| Long function | Split into smaller functions | +| God class | Decompose into focused classes | +| Primitive obsession | Create Pydantic models in `models/` | +| Long parameter list | Use Pydantic config or dataclass | +| Magic strings | Use `Literal` types or `Enum` | +| Nested validation | Use Pydantic validators and `model_validator` | +| Repeated schemas | Extract base models, use inheritance | diff --git a/.claude/commands/dev-setup.md b/.claude/commands/dev-setup.md new file mode 100644 index 0000000..6794100 --- /dev/null +++ b/.claude/commands/dev-setup.md @@ -0,0 +1,42 @@ +--- +description: Set up the development environment with miniforge and uv +allowed-tools: Bash(uv *) Bash(curl *) Bash(pip install uv) Bash(conda *) +--- + +# /dev-setup + +Set up a local development environment. + +1. Create a miniforge environment: + + ```bash + conda create -n dppvalidator python=3.12 -y && conda activate dppvalidator + ``` + +1. Install the `uv` package manager (skip if already installed): + + ```! + command -v uv || curl -LsSf https://astral.sh/uv/install.sh | sh + ``` + + Alternative: `pip install uv` + +1. Install project dependencies: + + ```! + uv sync --dev + ``` + +1. Install pre-commit hooks: + + ```! + uv run pre-commit install + ``` + +1. Verify installation: + + ```! + uv run python -c "import dppvalidator; print('Setup complete!')" + ``` + +After setup, run `/lint` and `/test` to verify everything works. diff --git a/.claude/commands/docs-health.md b/.claude/commands/docs-health.md new file mode 100644 index 0000000..1f8100b --- /dev/null +++ b/.claude/commands/docs-health.md @@ -0,0 +1,122 @@ +--- +description: Check documentation consistency; ensure README.md and mkdocs are aligned with the codebase +allowed-tools: Bash(grep *) Bash(head *) Bash(ls *) Bash(test *) Bash(find *) +--- + +# /docs-health + +## 1. Verify mkdocs nav files exist + +```! +for f in $(grep -oE '[a-z/-]+\.md' mkdocs.yml); do [ -f "docs/$f" ] || echo "MISSING: docs/$f"; done +``` + +## 2. Core documentation consistency + +Compare key information across documentation sources: + +| Source | Check | +| ---------------- | -------------------------------------------------- | +| `README.md` | Accurate project description, install instructions | +| `docs/index.md` | Matches README features and quick start | +| `AGENTS.md` | Tech stack reflects current dependencies | +| `pyproject.toml` | Version, description matches docs | + +```! +head -20 README.md +``` + +```! +grep -E "^(name|version|description)" pyproject.toml | head -5 +``` + +## 3. Validate public API documentation + +```! +grep -r "^from dppvalidator" docs/reference/ 2>/dev/null || echo "Check API docs manually" +``` + +```! +grep -E "^(class|def) " src/dppvalidator/__init__.py 2>/dev/null | head -10 +``` + +## 4. Check code examples + +Verify code examples in docs use current API patterns: + +- Import paths match actual module structure. +- Class/function names exist in codebase. +- Examples use Pydantic v2 syntax (not v1). + +```! +grep -rh "from dppvalidator" docs/*.md docs/**/*.md 2>/dev/null | sort -u | head -10 +``` + +```! +grep -rh "import dppvalidator" docs/*.md docs/**/*.md 2>/dev/null | sort -u | head -5 +``` + +## 5. Version consistency + +```! +grep -E "version|0\.[0-9]" pyproject.toml docs/index.md mkdocs.yml CHANGELOG.md 2>/dev/null | head -15 +``` + +Verify version numbers are consistent across: + +- [ ] `pyproject.toml` → package version +- [ ] `docs/index.md` → schema version references +- [ ] `CHANGELOG.md` → latest release matches `pyproject.toml` + +## 6. Changelog sync + +```! +head -30 CHANGELOG.md +``` + +```! +[ -f docs/changelog.md ] && head -5 docs/changelog.md || echo "docs/changelog.md missing" +``` + +Ensure `docs/changelog.md` references or includes root `CHANGELOG.md`. + +## 7. Links validation + +```! +grep -rohE '\[.*\]\([^)]+\.md[^)]*\)' docs/*.md docs/**/*.md 2>/dev/null | head -20 +``` + +Spot-check external links manually: + +- PyPI badge links +- GitHub repo links +- UNTP / ESPR reference links + +## 8. Schema reference accuracy + +```! +ls -la src/dppvalidator/schemas/*.json 2>/dev/null || echo "Check schema location" +``` + +```! +grep -r "0\.6\." docs/ src/ 2>/dev/null | head -10 +``` + +## 9. Report and fix + +Document any inconsistencies found: + +- [ ] Missing nav files → create placeholder or remove from nav +- [ ] Stale code examples → update to current API +- [ ] Version mismatch → sync versions +- [ ] Broken links → fix paths +- [ ] Missing API docs → document public exports + +If changes were made, commit: + +```bash +git add README.md docs/ CHANGELOG.md \ + && git commit -m "docs: sync documentation with codebase" +``` + +Run this workflow before releases and after major API changes. diff --git a/.claude/commands/feature.md b/.claude/commands/feature.md new file mode 100644 index 0000000..fc03af7 --- /dev/null +++ b/.claude/commands/feature.md @@ -0,0 +1,50 @@ +--- +description: Start a new feature branch following gitflow +argument-hint: "" +disable-model-invocation: true +allowed-tools: Bash(git *) Bash(uv run pytest *) Bash(uv run ruff *) Bash(gh *) +--- + +# /feature + +Create a feature branch for `$ARGUMENTS` and walk through the gitflow loop. + +1. Ensure `develop` is up to date: + + ```bash + git checkout develop && git pull origin develop + ``` + +1. Create the feature branch: + + ```bash + git checkout -b feature/$ARGUMENTS + ``` + +1. **Implement the feature** — write code and tests. + +1. Run tests: + + ```bash + uv run pytest tests/ -v + ``` + +1. Run lint: + + ```bash + uv run ruff check src/ tests/ + ``` + +1. Commit changes (conventional commits — see `.claude/rules/commits.md`): + + ```bash + git add . && git commit -m "feat: $ARGUMENTS" + ``` + +1. Push the feature branch: + + ```bash + git push -u origin feature/$ARGUMENTS + ``` + +1. Open the PR against `develop` via `gh pr create` or the GitHub UI. diff --git a/.claude/commands/fix-lint.md b/.claude/commands/fix-lint.md new file mode 100644 index 0000000..486c84c --- /dev/null +++ b/.claude/commands/fix-lint.md @@ -0,0 +1,20 @@ +--- +description: Auto-fix linting and formatting issues with ruff +allowed-tools: Bash(uv run ruff *) +--- + +# /fix-lint + +Auto-fix what can be fixed, then re-verify. Commit only after issues are resolved. + +```! +uv run ruff check --fix src/ tests/ +``` + +```! +uv run ruff format src/ tests/ +``` + +```! +uv run ruff check src/ tests/ +``` diff --git a/.claude/commands/hotfix.md b/.claude/commands/hotfix.md new file mode 100644 index 0000000..0672d5c --- /dev/null +++ b/.claude/commands/hotfix.md @@ -0,0 +1,150 @@ +--- +description: Create a hotfix for production following gitflow; includes PyPI release-failure runbook +argument-hint: "" +disable-model-invocation: true +allowed-tools: Bash(git *) Bash(uv *) Bash(uv run *) Bash(gh *) +--- + +# /hotfix + +## Standard hotfix workflow + +1. Create the hotfix branch from `main`: + + ```bash + git checkout main && git pull && git checkout -b hotfix/$ARGUMENTS + ``` + +1. **Fix the issue** — implement the minimal fix. + +1. Add a regression test for the fix. + +1. Run tests: + + ```bash + uv run pytest tests/ -v + ``` + +1. Bump the patch version: + + ```bash + uv version patch + ``` + +1. Commit the fix: + + ```bash + git add . && git commit -m "fix: $ARGUMENTS" + ``` + +1. Merge to `main`: + + ```bash + git checkout main && git merge --no-ff hotfix/$ARGUMENTS + ``` + +1. Tag the release: + + ```bash + git tag -a v$(uv version --short) -m "Hotfix v$(uv version --short)" + ``` + +1. Merge to `develop`: + + ```bash + git checkout develop && git merge --no-ff hotfix/$ARGUMENTS + ``` + +1. Push and publish: + + ```bash + git push origin main develop --tags + ``` + +1. Delete the hotfix branch: + + ```bash + git branch -d hotfix/$ARGUMENTS + ``` + +______________________________________________________________________ + +## PyPI release-failure runbook + +Use this runbook when `verify-pypi` fails or users report installation issues. + +### Step 1: assess the failure + +Check the GitHub Actions workflow run to identify the issue: + +- **Import failure**: missing module or dependency issue. +- **CLI failure**: entry-point misconfiguration. +- **Validation failure**: core functionality broken. + +### Step 2: yank the release (only if necessary) + +Yank only if the release causes installation failures or breaks functionality. + +1. Open . +1. Find the affected version. +1. Click **Options → Yank release**. +1. Reason: "Installation/functionality issue - hotfix pending". + +Yanked releases remain downloadable via explicit version, but won't be installed by default. Reversible. + +### Step 3: cut a hotfix release + +1. Branch: + + ```bash + git checkout main && git pull && git checkout -b hotfix/v + ``` + +1. Fix the identified issue. + +1. Add a regression test. + +1. Full test suite: + + ```bash + uv run pytest tests/ -v + ``` + +1. Bump the patch version: `uv version patch`. + +1. Update `CHANGELOG.md`: + + ```text + ## [X.Y.Z] - YYYY-MM-DD + + ### Fixed + - Fixed [description] that caused [symptom] + ``` + +1. Commit, merge, tag: + + ```bash + git add . + git commit -m "fix: [description]" + git checkout main && git merge --no-ff hotfix/v + git tag -a v$(uv version --short) -m "Hotfix v$(uv version --short)" + ``` + +1. Merge to `develop` and push: + + ```bash + git checkout develop && git merge --no-ff main + git push origin main develop --tags + ``` + +### Step 4: verify the hotfix + +1. Wait for CI/CD to complete. +1. Confirm `smoke-test` passes. +1. Confirm `verify-pypi` passes. +1. Test installation manually: `pip install dppvalidator==`. + +### Step 5: communicate (if public release) + +- Update the GitHub Release notes with hotfix information. +- If users were affected, consider a brief announcement. diff --git a/.claude/commands/lint.md b/.claude/commands/lint.md new file mode 100644 index 0000000..78ec2c2 --- /dev/null +++ b/.claude/commands/lint.md @@ -0,0 +1,20 @@ +--- +description: Run linting and type checking with ruff and ty +allowed-tools: Bash(uv run ruff *) Bash(uv run ty *) +--- + +# /lint + +Run the project's static checks. On any failure, run `/fix-lint` to auto-fix what can be fixed and report the rest. + +```! +uv run ruff check src/ tests/ +``` + +```! +uv run ruff format --check src/ tests/ +``` + +```! +uv run ty check src/ +``` diff --git a/.claude/commands/pr-review.md b/.claude/commands/pr-review.md new file mode 100644 index 0000000..2102129 --- /dev/null +++ b/.claude/commands/pr-review.md @@ -0,0 +1,45 @@ +--- +description: Review and address PR comments +argument-hint: "" +disable-model-invocation: true +allowed-tools: Bash(gh *) Bash(git *) Bash(uv run pytest *) +--- + +# /pr-review + +Address feedback on PR `#$ARGUMENTS`. + +1. Fetch and check out the PR branch: + + ```bash + gh pr checkout $ARGUMENTS + ``` + +1. Get PR comments: + + ```bash + gh pr view $ARGUMENTS --comments + ``` + +1. Run tests to ensure current state is green: + + ```bash + uv run pytest tests/ -v + ``` + +1. For **each comment**: + + 1. Read and understand the feedback. + 1. Implement the requested change. + 1. Run relevant tests. + 1. Commit with reference: `git commit -m "fix: address PR feedback - "`. + +1. Push changes: + + ```bash + git push + ``` + +1. Reply to comments on GitHub indicating addressed items. + +Use `gh pr comment $ARGUMENTS --body "Addressed in latest push"` to notify reviewers. diff --git a/.claude/commands/release.md b/.claude/commands/release.md new file mode 100644 index 0000000..71a8840 --- /dev/null +++ b/.claude/commands/release.md @@ -0,0 +1,82 @@ +--- +description: Create a new release following gitflow and publish to PyPI +argument-hint: "[patch|minor|major]" +disable-model-invocation: true +allowed-tools: Bash(git *) Bash(uv *) Bash(uv run *) Bash(gh *) +--- + +# /release + +Cut a release. Bump kind defaults to `patch` if `$ARGUMENTS` is empty. + +1. Ensure you're on `develop`: + + ```bash + git checkout develop && git pull origin develop + ``` + +1. Run linting: + + ```bash + uv run ruff check src/ tests/ + ``` + +1. Run the full test suite: + + ```bash + uv run pytest tests/ -v + ``` + +1. Bump the version: + + ```bash + uv version $ARGUMENTS # patch | minor | major + ``` + +1. Create the release branch: + + ```bash + git checkout -b release/v$(uv version --short) + ``` + +1. Update `CHANGELOG.md` with release notes. + +1. Commit the version bump: + + ```bash + git add pyproject.toml CHANGELOG.md \ + && git commit -m "chore: bump version to $(uv version --short)" + ``` + +1. Merge to `main`: + + ```bash + git checkout main && git pull \ + && git merge --no-ff release/v$(uv version --short) + ``` + +1. Tag the release: + + ```bash + git tag -a v$(uv version --short) -m "Release v$(uv version --short)" + ``` + +1. Merge back to `develop`: + + ```bash + git checkout develop && git merge --no-ff main + ``` + +1. Push all branches and tags: + + ```bash + git push origin main develop --tags + ``` + +1. Build and publish to PyPI (the `/pypi-publish` skill walks through this in detail): + + ```bash + uv build && uv publish + ``` + +Ensure `PYPI_API_TOKEN` is configured in environment or `.pypirc`. diff --git a/.claude/commands/test.md b/.claude/commands/test.md new file mode 100644 index 0000000..a99edb1 --- /dev/null +++ b/.claude/commands/test.md @@ -0,0 +1,83 @@ +______________________________________________________________________ + +## description: Run the test suite with coverage analysis and quality checks argument-hint: "[pytest args]" allowed-tools: Bash(uv run pytest \*) Bash(uv run coverage \*) Bash(uv run mutmut \*) + +# /test + +Tests must validate **library behavior**, not implementation details: + +- Avoid mocking internal components unless testing integration boundaries. +- Test real Pydantic validation, not mocked validators. +- Verify actual JSON-LD output, not mocked exporters. +- Use fixtures with realistic DPP data. +- Coverage target: **95%** (protocols excluded via `pyproject.toml`). + +## Quick test (default) + +```! +uv run pytest tests/ -v --cov=src/dppvalidator --cov-report=term-missing --cov-fail-under=95 $ARGUMENTS +``` + +## Comprehensive test suite + +```! +uv run pytest tests/unit/ -v --cov=src/dppvalidator --cov-report=term-missing +``` + +```! +uv run pytest tests/property/ -v --hypothesis-show-statistics +``` + +```! +uv run pytest tests/fuzz/ -v +``` + +```! +uv run pytest tests/integration/ -v 2>/dev/null || echo "No integration tests yet" +``` + +## Coverage analysis + +```! +uv run pytest tests/ --cov=src/dppvalidator --cov-report=html --cov-report=term-missing +``` + +```! +uv run coverage report --show-missing --skip-covered +``` + +## Mutation testing (verify test quality) + +```bash +uv run mutmut run --paths-to-mutate=src/dppvalidator --tests-dir=tests +uv run mutmut results +``` + +## Debugging specific tests + +```bash +# single test file +uv run pytest tests/unit/test_.py -v + +# specific test function +uv run pytest tests/unit/test_.py::test_ -v -s + +# tests matching a pattern +uv run pytest tests/ -v -k "" +``` + +## Test quality checklist + +- [ ] Tests validate **behavior**, not mocked internals +- [ ] Edge cases covered (empty inputs, invalid data, boundary values, etc.) +- [ ] Error messages are meaningful and tested +- [ ] Property tests cover model invariants +- [ ] Fixtures use realistic DPP data from `tests/fixtures/` +- [ ] No over-mocking (real Pydantic validation, real exports) + +**On failure**: + +- Check test output for specific failures. +- Use `uv run pytest tests/path/to/test.py::test_name -v -s` to debug. +- Run `/lint` to check for code issues. +- Review coverage gaps with `uv run coverage html && open htmlcov/index.html`. diff --git a/.claude/commands/untp-bump.md b/.claude/commands/untp-bump.md new file mode 100644 index 0000000..026cd6a --- /dev/null +++ b/.claude/commands/untp-bump.md @@ -0,0 +1,102 @@ +--- +description: Bootstrap support for a new UNTP version (vendors upstream artefacts, registers the version, scaffolds models, opens a feature branch). Reads the canonical playbook from the untp-migrate skill. +argument-hint: "" +disable-model-invocation: true +allowed-tools: Bash(git *) Bash(curl *) Bash(shasum *) Bash(python3 *) Bash(uv run *) Bash(jq *) Bash(mkdir *) Bash(cp *) +--- + +# /untp-bump + +Bootstrap dppvalidator support for UNTP `$ARGUMENTS`. This is the executable form of the recipe in [.claude/skills/untp-migrate/SKILL.md](../skills/untp-migrate/SKILL.md). Read that skill for the full operating principles before running this; it must be loaded into context. + +## Preconditions + +- `git status` is clean. +- You're on `develop` (gitflow). +- `$ARGUMENTS` is a valid SemVer (e.g. `0.7.0`, `0.7.1`, `0.8.0`). + +## Steps + +### 1. Branch + +```bash +git checkout develop && git pull origin develop +git checkout -b feature/untp-$ARGUMENTS +``` + +### 2. Vendor upstream artefacts + +```bash +VER="$ARGUMENTS" +mkdir -p tests/fixtures/upstream/v$VER +BASE="https://opensource.unicc.org/un/unece/uncefact/spec-untp/-/raw/v$VER/artefacts" +curl -sL "$BASE/schema/v$VER/dpp/DigitalProductPassport.json" -o tests/fixtures/upstream/v$VER/dpp-schema.json +curl -sL "$BASE/contexts/v$VER/untp-context.jsonld" -o tests/fixtures/upstream/v$VER/context.jsonld +curl -sL "$BASE/samples/v$VER/dpp/DigitalProductPassport_instance.json" -o tests/fixtures/upstream/v$VER/sample.json +shasum -a 256 tests/fixtures/upstream/v$VER/* +``` + +If any of those download as zero bytes, the upstream layout has shifted — stop and re-read the [migration plan](../../docs/plans/UNTP_0.7.0_MIGRATION.md) §2.6. + +### 3. Drop into bundled paths + +```bash +cp tests/fixtures/upstream/v$VER/dpp-schema.json src/dppvalidator/schemas/data/untp-dpp-schema-$VER.json +cp tests/fixtures/upstream/v$VER/context.jsonld src/dppvalidator/vocabularies/data/untp-context-$VER.jsonld +``` + +### 4. Diff against the previous version + +```bash +PREV=$(python3 -c "from dppvalidator.schemas.registry import SCHEMA_REGISTRY; \ + vs=sorted(SCHEMA_REGISTRY.keys()); print(vs[-1])") +python3 .claude/skills/untp-migrate/scripts/diff_schema.py \ + src/dppvalidator/schemas/data/untp-dpp-schema-$PREV.json \ + src/dppvalidator/schemas/data/untp-dpp-schema-$VER.json +``` + +Paste the output into a new `docs/plans/UNTP_${VER}_MIGRATION.md` (use the 0.7.0 plan as a template). + +### 5. Register the version + +You must edit (Claude does this — these aren't shell commands): + +- [src/dppvalidator/schemas/registry.py](../../src/dppvalidator/schemas/registry.py) — append a `SchemaVersion` entry with the SHA-256 from step 2. +- [src/dppvalidator/exporters/contexts.py](../../src/dppvalidator/exporters/contexts.py) — append a `ContextDefinition` entry. +- [src/dppvalidator/validators/detection.py](../../src/dppvalidator/validators/detection.py) — extend `_CONTEXT_URL_PATTERN` if the new URL shape isn't already covered. +- `src/dppvalidator/schemas/data/MANIFEST.json` — add the new artefact entries. + +### 6. Scaffold models + +Create the `src/dppvalidator/models/v_/` package with one file per top-level `$def` from the new schema. Stay strictly inside Pydantic v2 patterns (see `.claude/rules/dpp-domain.md`). + +### 7. Wire the dispatch + +Add the new version to `_MODEL_BY_VERSION` in `src/dppvalidator/validators/model.py`. Do not branch on the version literal anywhere else — the no-version-literals guard test will catch you. + +### 8. Verify + +```bash +uv run pytest tests/ -q +uv run ruff check src/ tests/ +uv run ty check src/ +``` + +### 9. Commit and push + +```bash +git add tests/fixtures/upstream/v$VER \ + src/dppvalidator/schemas/data/untp-dpp-schema-$VER.json \ + src/dppvalidator/vocabularies/data/untp-context-$VER.jsonld \ + src/dppvalidator/schemas/data/MANIFEST.json \ + src/dppvalidator/schemas/registry.py \ + src/dppvalidator/exporters/contexts.py \ + src/dppvalidator/validators/detection.py \ + src/dppvalidator/models/v* \ + src/dppvalidator/validators/model.py \ + docs/plans/UNTP_${VER}_MIGRATION.md +git commit -m "feat(untp): vendor and register UNTP $ARGUMENTS" +git push -u origin feature/untp-$ARGUMENTS +``` + +The shim, version-matrix tests, default-flip, deprecation, and removal happen in **separate PRs** — see the plan's phase split. Do not bundle them into this branch. diff --git a/.claude/hooks/ruff-fix.sh b/.claude/hooks/ruff-fix.sh new file mode 100755 index 0000000..37795f2 --- /dev/null +++ b/.claude/hooks/ruff-fix.sh @@ -0,0 +1,45 @@ +#!/usr/bin/env bash +# PostToolUse hook: auto-fix Python files edited by Claude with ruff. +# +# Reads the Claude Code hook event JSON from stdin, extracts the file path +# of the Edit/Write/MultiEdit tool call, and runs `uv run ruff check --fix` +# on it. Silent on success; never blocks Claude (exit 0 always). + +set -u + +# Hook input is JSON on stdin per Claude Code hooks contract. +input="$(cat)" + +# Extract file_path from tool_input. PostToolUse fires for Edit, Write, +# MultiEdit, NotebookEdit. Each surfaces the path as tool_input.file_path +# (or notebook_path for NotebookEdit). +file_path="$(printf '%s' "$input" | python3 -c ' +import json, sys +try: + data = json.load(sys.stdin) +except Exception: + sys.exit(0) +ti = data.get("tool_input", {}) or {} +path = ti.get("file_path") or ti.get("notebook_path") or "" +print(path) +' 2>/dev/null)" + +# Bail if no path or not a Python file we manage. +case "$file_path" in + *.py) ;; + *) exit 0 ;; +esac + +# Only auto-fix files inside the project tree. +project_dir="${CLAUDE_PROJECT_DIR:-$(pwd)}" +case "$file_path" in + "$project_dir"/*) ;; + /*) exit 0 ;; + *) file_path="$project_dir/$file_path" ;; +esac + +# Run ruff fix; never block, even on failure. +cd "$project_dir" || exit 0 +uv run ruff check --fix "$file_path" >/dev/null 2>&1 || true + +exit 0 diff --git a/.claude/rules/commits.md b/.claude/rules/commits.md new file mode 100644 index 0000000..9445c1c --- /dev/null +++ b/.claude/rules/commits.md @@ -0,0 +1,29 @@ +# Conventional Commits + +Use conventional commit format for every commit message: + +``` +(): + +[optional body] +[optional footer] +``` + +**Types:** + +- **feat**: new feature +- **fix**: bug fix +- **docs**: documentation changes +- **style**: formatting (no code change) +- **refactor**: code restructuring +- **test**: adding/modifying tests +- **chore**: maintenance tasks +- **perf**: performance improvements +- **ci**: CI/CD changes + +**Examples:** + +- `feat(validator): add JSON-LD export support` +- `fix(material): correct percentage validation logic` +- `docs(readme): add installation instructions` +- `test(dpp): add unit tests for passport validation` diff --git a/.claude/rules/dpp-domain.md b/.claude/rules/dpp-domain.md new file mode 100644 index 0000000..6181c56 --- /dev/null +++ b/.claude/rules/dpp-domain.md @@ -0,0 +1,37 @@ +______________________________________________________________________ + +paths: + +- "src/\*\*/\*.py" +- "tests/\*\*/\*.py" + +______________________________________________________________________ + +# DPP Domain Guidelines + +## Domain knowledge + +- DPP = Digital Product Passport (EU ESPR regulation). +- Use CIRPASS and UNECE ontologies as reference. +- Material codes follow ISO 2076 (e.g. `CO`=Cotton, `EL`=Elastane). +- Country codes use ISO 3166-1 alpha-2. +- Product IDs use GTIN-13 or equivalent. + +## Validation rules + +- Material percentages must sum to 100%. +- All mandatory ESPR fields must be present. +- URIs must be valid and follow semantic web standards. +- Supply chain nodes must have valid roles: `Manufacturer`, `Supplier`, `Recycler`. + +## Pydantic v2 patterns (do not regress to v1) + +- Use `Field()` with `description=` for documentation. +- Use `@field_validator` decorator with `@classmethod` (NOT v1 `@validator`). +- Use `@model_validator(mode="after")` for cross-field validation (NOT v1 `@root_validator`). +- Use `model_dump()` instead of v1 `.dict()`. +- Use `model_dump_json()` instead of v1 `.json()`. +- Use `model_validate()` instead of v1 `.parse_obj()`. +- Use `X | None` type syntax instead of `Optional[X]`. +- Use `ConfigDict` class attribute instead of inner `Config` class. +- Export to JSON-LD with `@context` and `@type`. diff --git a/.claude/rules/plugin-licenses.md b/.claude/rules/plugin-licenses.md new file mode 100644 index 0000000..7ea2b08 --- /dev/null +++ b/.claude/rules/plugin-licenses.md @@ -0,0 +1,50 @@ +______________________________________________________________________ + +paths: + +- "plugins/\*\*/\*" +- "src/dppvalidator/\*\*/\*.py" + +______________________________________________________________________ + +# Plugin License Rules + +The `plugins/` directory contains separately-licensed packages. Follow these rules strictly. + +## License isolation + +- **Core package** (`src/dppvalidator/`): MIT licensed. +- **Plugin packages** (`plugins/*/`): may have different licenses (e.g. GPL-3.0). + +## Critical rules + +1. **No reverse imports**: core MUST NOT import from any plugin. + + - `src/dppvalidator/` cannot contain `from dppvalidator_textiles import ...` + - Plugins depend on core, never the reverse. + +1. **Separate `pyproject.toml`**: each plugin has its own with: + + - Its own `license` field. + - Dependency on `dppvalidator>=X.Y.Z`. + - Its own entry-points registration. + +1. **LICENSE file per plugin**: each plugin directory must have its own LICENSE file. + +1. **No code copying**: do not copy GPL-licensed code into MIT-licensed core. + + - Extend via inheritance, not duplication. + - Use entry-points for plugin discovery. + +## Current plugins + +| Plugin | Path | License | Upstream | +| -------- | ------------------- | ---------------- | ---------- | +| textiles | `plugins/textiles/` | GPL-3.0-or-later | spec-unttp | + +## When adding new plugins + +1. Check upstream license compatibility. +1. Create `plugins//LICENSE` with appropriate license. +1. Set `license` in `plugins//pyproject.toml`. +1. Document in this file. diff --git a/.claude/rules/python-style.md b/.claude/rules/python-style.md new file mode 100644 index 0000000..f3b306d --- /dev/null +++ b/.claude/rules/python-style.md @@ -0,0 +1,37 @@ +______________________________________________________________________ + +paths: + +- "\*\*/\*.py" + +______________________________________________________________________ + +# Python Code Style + +## Coding standards + +- Use type hints for all function parameters and return values. +- Follow PEP 8 naming: `snake_case` for functions/variables, `PascalCase` for classes. +- Use Pydantic v2 models for data validation. +- Prefer early returns to reduce nesting. +- Keep functions focused and under ~50 lines. +- Use dataclasses or Pydantic models instead of plain dicts for structured data. + +## Imports + +- Group imports: stdlib, third-party, local (separated by blank lines). +- Use absolute imports over relative imports. +- Import specific items rather than entire modules when practical. + +## Error handling + +- Use specific exception types, not bare `except:`. +- Validate input at boundaries using Pydantic. +- Raise descriptive exceptions with context. + +## Testing companion + +- Each module should have corresponding tests in `tests/`. +- Use pytest fixtures for shared test setup. +- Test both happy path and error cases. +- Use parametrized tests for multiple input variations. diff --git a/.claude/rules/testing.md b/.claude/rules/testing.md new file mode 100644 index 0000000..2393db8 --- /dev/null +++ b/.claude/rules/testing.md @@ -0,0 +1,65 @@ +______________________________________________________________________ + +paths: + +- "tests/\*\*/\*.py" +- "\*\*/test\_\*.py" +- "\*\*/\*\_test.py" + +______________________________________________________________________ + +# Testing Guidelines + +## Coverage requirements + +- Target **≥95% code coverage** for all modules. +- Protocol classes (`typing.Protocol`) may be excluded from coverage as they cannot be tested directly. +- Use `# pragma: no cover` sparingly and only for genuinely untestable code. + +## Testing philosophy + +- Test **intended behavior**, not literal implementation details. +- Tests should validate what the code is supposed to do, not how it does it. +- Avoid brittle tests that break when refactoring internals. +- Focus on public API contracts and observable outcomes. + +## Test types + +- **Unit tests**: isolate individual functions/classes, mock external dependencies. +- **Integration tests**: verify components work together correctly. +- **Property-based tests**: use Hypothesis for fuzz testing with generated inputs. +- **Fixtures**: use pytest fixtures for reusable test setup and teardown. + +## pytest best practices + +- Organize fixtures in `conftest.py` files at appropriate directory levels. +- Use `@pytest.fixture` with appropriate scope (function, class, module, session). +- Use `@pytest.mark.parametrize` for testing multiple inputs. +- Use `@pytest.mark.integration` to tag integration tests. +- Use Hypothesis `@given` decorators for property-based testing. + +## Example structure + +```python +import pytest +from hypothesis import given, strategies as st + + +@pytest.fixture +def sample_data(): + """Reusable test fixture.""" + return {...} + + +def test_behavior_not_implementation(sample_data): + """Test what it does, not how.""" + result = function_under_test(sample_data) + assert result.is_valid # behavior check + + +@given(st.text(), st.integers()) +def test_property_based(text, number): + """Fuzz test with generated inputs.""" + result = process(text, number) + assert invariant_holds(result) +``` diff --git a/.claude/rules/untp-versioning.md b/.claude/rules/untp-versioning.md new file mode 100644 index 0000000..cf5232a --- /dev/null +++ b/.claude/rules/untp-versioning.md @@ -0,0 +1,61 @@ +--- +paths: + - "src/dppvalidator/schemas/**" + - "src/dppvalidator/exporters/contexts.py" + - "src/dppvalidator/exporters/jsonld.py" + - "src/dppvalidator/exporters/eudpp_jsonld.py" + - "src/dppvalidator/validators/detection.py" + - "src/dppvalidator/validators/model.py" + - "src/dppvalidator/validators/jsonld_semantic.py" + - "src/dppvalidator/validators/semantic.py" + - "src/dppvalidator/models/**" + - "src/dppvalidator/cli/commands/**" + - "src/dppvalidator/compat/**" +--- + +# UNTP Versioning Rules + +These files form the version-aware spine of the validator. Read carefully before editing — they have stricter rules than the rest of the codebase. + +## Cardinal rules + +1. **No bare UNTP version literals.** A string like `"0.6.1"` or `"0.7.0"` may only appear in `src/dppvalidator/schemas/registry.py` and `src/dppvalidator/exporters/contexts.py`. Everywhere else: look it up via `SchemaRegistry`, `ContextManager`, or `dppvalidator.compat.active_version()`. The `tests/unit/test_no_version_literals.py` guard will fail your PR otherwise. + +2. **Models are version-namespaced.** Pydantic classes for UNTP data live in `src/dppvalidator/models/v0_6/`, `…v0_7/`, etc. Never edit a `v0_X` package to absorb behaviour from a different version. To support a new version, add a `v0_Y/` package — do not graft fields onto the previous one. + +3. **Detection is centralised.** `validators/detection.py` is the only place that decides what version a payload is. New URL/namespace shapes get added to `_CONTEXT_URL_PATTERN` and `_SCHEMA_URL_PATTERN` there, nowhere else. + +4. **Bundled artefacts have a manifest.** Every JSON Schema and JSON-LD context vendored under `src/dppvalidator/schemas/data/` or `src/dppvalidator/vocabularies/data/` MUST appear in `src/dppvalidator/schemas/data/MANIFEST.json` with version, source URL, SHA-256, and pull date. CI verifies the hashes. + +5. **Coexist before you cut.** When a new version lands, the previous version must keep working in the same release. Removing a version is its own minor release with its own deprecation warning lead-time. + +## Adding a UNTP version: short version + +Use the `/untp-bump ` slash command. It runs the recipe documented in [`.claude/skills/untp-migrate/SKILL.md`](../skills/untp-migrate/SKILL.md). Read that skill in full before improvising. + +## Adding a UNTP version: minimum touch list + +When you add `vX.Y.Z`, you must touch: + +- `src/dppvalidator/schemas/registry.py` — one `SchemaVersion` entry. +- `src/dppvalidator/exporters/contexts.py` — one `ContextDefinition` entry. +- `src/dppvalidator/schemas/data/MANIFEST.json` — manifest entries for the new schema and context files. +- `src/dppvalidator/schemas/data/untp-dpp-schema-X.Y.Z.json` — vendored schema. +- `src/dppvalidator/vocabularies/data/untp-context-X.Y.Z.jsonld` — vendored context. +- `src/dppvalidator/models/vX_Y/` — new Pydantic model package. +- `src/dppvalidator/validators/model.py` — add to `_MODEL_BY_VERSION`. +- `src/dppvalidator/validators/detection.py` — extend URL pattern if the namespace shape changed. +- `src/dppvalidator/compat/upgrade__to_.py` — input shim. +- `tests/fixtures/upstream/vX.Y.Z/` — vendored upstream samples + schema. +- `tests/integration/test_version_matrix.py` — add the new version to the matrix. +- `docs/plans/UNTP_X.Y.Z_MIGRATION.md` — full migration doc. + +If you touched more than this list, you're either fixing an unrelated bug (split the PR) or going around the version-aware spine (don't). + +## Anti-patterns + +- Hardcoding `"0.6.1"` or `"0.7.0"` as a default in a function signature. +- Branching on `if version == "0.7.0":` outside `validators/model.py`'s `_MODEL_BY_VERSION` table. +- Adding a `Optional[Union[Old, New]]` typed field to a model to "support both". +- Fetching a schema or context from the network during validation. +- Editing a vendored schema or context to "fix" something — that breaks the SHA-256 manifest. Either upgrade to a new upstream version or open an upstream issue. diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..8c452cd --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,46 @@ +{ + "$schema": "https://json.schemastore.org/claude-code-settings.json", + "hooks": { + "PostToolUse": [ + { + "matcher": "Edit|Write|MultiEdit", + "hooks": [ + { + "type": "command", + "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/ruff-fix.sh", + "timeout": 30 + } + ] + } + ] + }, + "permissions": { + "allow": [ + "Bash(uv run pytest *)", + "Bash(uv run ruff *)", + "Bash(uv run ty *)", + "Bash(uv run coverage *)", + "Bash(uv run mutmut *)", + "Bash(uv run pre-commit *)", + "Bash(uv sync *)", + "Bash(uv version *)", + "Bash(uv build)", + "Bash(git status)", + "Bash(git diff *)", + "Bash(git log *)", + "Bash(git branch *)", + "Bash(git show *)", + "Bash(gh pr view *)", + "Bash(gh pr diff *)", + "Bash(gh pr list *)", + "Bash(gh issue view *)", + "Bash(gh issue list *)" + ], + "deny": [ + "Bash(rm -rf /)", + "Bash(rm -rf ~/*)", + "Bash(git push --force *)", + "Bash(git push -f *)" + ] + } +} diff --git a/.claude/skills/pypi-publish/SKILL.md b/.claude/skills/pypi-publish/SKILL.md new file mode 100644 index 0000000..589c12f --- /dev/null +++ b/.claude/skills/pypi-publish/SKILL.md @@ -0,0 +1,68 @@ +______________________________________________________________________ + +## name: pypi-publish description: Guide publishing dppvalidator to PyPI with proper versioning, smoke checks, and GitHub release. Use when the user asks to release, publish, or cut a version of the package. disable-model-invocation: true argument-hint: "[patch|minor|major]" allowed-tools: Bash(uv run pytest \*) Bash(uv run ruff \*) Bash(uv run ty \*) Bash(uv build \*) Bash(uv publish \*) Bash(uv version \*) Bash(git \*) + +# pypi-publish + +Publish a release of `dppvalidator` to PyPI. Bump type defaults to `patch` if `$ARGUMENTS` is empty. + +## 1. Pre-publishing checklist + +```bash +uv run pytest tests/ -v +uv run ruff check src/ +uv run ty check src/ +``` + +- [ ] All tests pass +- [ ] Lint clean +- [ ] Type check clean +- [ ] Version bumped in `pyproject.toml` +- [ ] `CHANGELOG.md` updated + +## 2. Bump the version + +```bash +# patch (0.1.0 -> 0.1.1) +uv version patch + +# minor (0.1.0 -> 0.2.0) +uv version minor + +# major (0.1.0 -> 1.0.0) +uv version major +``` + +## 3. Build and publish + +```bash +# Build distribution +uv build + +# Publish to TestPyPI first +uv publish --repository testpypi + +# Verify install from TestPyPI +uv pip install --index-url https://test.pypi.org/simple/ dppvalidator + +# Publish to PyPI +uv publish +``` + +## 4. Authentication + +Set `PYPI_API_TOKEN` in the environment or configure `~/.pypirc`: + +```ini +[pypi] +username = __token__ +password = pypi-YOUR_API_TOKEN +``` + +## 5. Post-publish + +1. Create a GitHub release with the tag. +1. Update documentation. +1. Announce the release. + +If anything fails after publish, use the `/hotfix` workflow for the PyPI release-failure runbook. diff --git a/.claude/skills/untp-migrate/SKILL.md b/.claude/skills/untp-migrate/SKILL.md new file mode 100644 index 0000000..44e5101 --- /dev/null +++ b/.claude/skills/untp-migrate/SKILL.md @@ -0,0 +1,125 @@ +--- +name: untp-migrate +description: Plan, scaffold, and execute a UNTP DPP version bump (e.g. 0.6.1 → 0.7.0). Use when the user asks about adding a new UNTP version, migrating fixtures or models between versions, drifting away from a hardcoded version, or when working in src/dppvalidator/{schemas,exporters,models,validators}/ during a known migration window. +allowed-tools: Read Edit Write Grep Glob Bash(uv run pytest *) Bash(uv run ruff *) Bash(uv run ty *) Bash(curl *) Bash(python3 *) Bash(shasum *) Bash(jq *) +--- + +# untp-migrate + +Companion skill to [docs/plans/UNTP_0.7.0_MIGRATION.md](../../docs/plans/UNTP_0.7.0_MIGRATION.md). Use it for any UNTP version bump, not only 0.7.0. + +## When to invoke + +- Adding a new UNTP minor (0.7.0 today, 0.7.x or 0.8.0 next). +- Touching anything under `src/dppvalidator/schemas/`, `src/dppvalidator/exporters/contexts.py`, `src/dppvalidator/validators/detection.py`, `src/dppvalidator/models/v*/`. +- Helping users migrate their own DPP payloads between versions. + +## Operating principles (load these into your head before editing) + +1. **No bare version literals** outside `schemas/registry.py` and `exporters/contexts.py`. If you find a `"0.6.1"` literal anywhere else, replace it with a registry lookup. +2. **Models are version-namespaced** under `dppvalidator.models.v0_6/`, `…v0_7/`, etc. Never edit a `v0_*` package to "fix" a bug introduced by a different version — branch the version. +3. **Bundled artefacts have a manifest.** Every JSON Schema, JSON-LD context, and ontology lives under `src/dppvalidator/schemas/data/` or `src/dppvalidator/vocabularies/data/`, with SHA-256 in `MANIFEST.json`. Updating an artefact means updating its hash. +4. **Detect, don't guess.** `validators/detection.py` is the only place that decides what version a payload is. Add new URL patterns there. +5. **Coexist before you cut.** Two adjacent versions (N-1 and N) must work simultaneously for one full minor release before N-1 is removed. + +## Reference: 0.6.1 → 0.7.0 surface deltas + +The migration plan has the full table. The deltas you trip over most often: + +- Namespace moved from `test.uncefact.org/vocabulary/untp/dpp/X.Y.Z/` to `vocabulary.uncefact.org/untp/X.Y.Z/context/`. +- `credentialSubject` is now a `Product` directly, not a `ProductPassport` envelope. +- `EmissionsPerformance`, `CircularityPerformance`, `TraceabilityPerformance`, `Metric` collapse into `Claim.claimedPerformance: Performance[]` keyed by `conformityTopic`. +- `serialNumber → itemNumber`; `producedByParty → relatedParty[0]`; `materialsProvenance → materialProvenance`; `Classification.schemeID → schemeId`. +- New required envelope fields: `validFrom`, `name`, `credentialSubject`. + +## The bump recipe (mirrors `/untp-bump`) + +For each new version `vX.Y.Z`: + +### 1. Vendor upstream artefacts + +```bash +mkdir -p tests/fixtures/upstream/v$VER +BASE="https://opensource.unicc.org/un/unece/uncefact/spec-untp/-/raw/v$VER/artefacts" +curl -sL $BASE/schema/v$VER/dpp/DigitalProductPassport.json -o tests/fixtures/upstream/v$VER/dpp-schema.json +curl -sL $BASE/contexts/v$VER/untp-context.jsonld -o tests/fixtures/upstream/v$VER/context.jsonld +curl -sL $BASE/samples/v$VER/dpp/DigitalProductPassport_instance.json -o tests/fixtures/upstream/v$VER/sample.json +shasum -a 256 tests/fixtures/upstream/v$VER/* +``` + +Record the GitLab tag SHA and pull date in `tests/fixtures/upstream/SOURCES.md`. + +### 2. Drop them under the bundled paths + +```bash +cp tests/fixtures/upstream/v$VER/dpp-schema.json src/dppvalidator/schemas/data/untp-dpp-schema-$VER.json +cp tests/fixtures/upstream/v$VER/context.jsonld src/dppvalidator/vocabularies/data/untp-context-$VER.jsonld +``` + +Then update `src/dppvalidator/schemas/data/MANIFEST.json` with version, source URL, SHA-256, and pull date. + +### 3. Register the version + +Add entries to: + +- [src/dppvalidator/schemas/registry.py](../../src/dppvalidator/schemas/registry.py) — `SCHEMA_REGISTRY[VER] = SchemaVersion(...)` +- [src/dppvalidator/exporters/contexts.py](../../src/dppvalidator/exporters/contexts.py) — `CONTEXTS[VER] = ContextDefinition(...)` +- [src/dppvalidator/validators/detection.py](../../src/dppvalidator/validators/detection.py) — extend `_CONTEXT_URL_PATTERN` if the namespace shape changed + +### 4. Diff against the previous version + +Run the bundled helper to print a deltas table you can paste into the migration plan: + +```bash +python3 ${CLAUDE_SKILL_DIR}/scripts/diff_schema.py \ + src/dppvalidator/schemas/data/untp-dpp-schema-.json \ + src/dppvalidator/schemas/data/untp-dpp-schema-.json +``` + +### 5. Scaffold the model package + +Create `src/dppvalidator/models/v/` with one file per top-level `$def`. Use Pydantic v2 patterns from `.claude/rules/dpp-domain.md`. Cross-field invariants live in `@model_validator(mode="after")`. Re-export the new models from `src/dppvalidator/models/__init__.py` only after the default version flips. + +### 6. Wire the model dispatch + +In [src/dppvalidator/validators/model.py](../../src/dppvalidator/validators/model.py), add the new version to `_MODEL_BY_VERSION`. Do not branch on version anywhere else. + +### 7. Write the upgrade shim + +Add `dppvalidator/compat/upgrade__to_.py`. The shim takes a dict and returns a dict; lossy mappings emit warnings, never silently drop. Property-based tests round-trip every previous-version fixture through it. + +### 8. Tests and fixtures + +- Add `tests/fixtures/valid/untp-dpp-instance-$VER.json`. +- Parametrise version-relevant tests with `@pytest.mark.parametrize("version", [...])`. +- Add `tests/integration/test_version_matrix.py` cases for the new pair. + +### 9. Verify + +```bash +uv run pytest tests/ -q +uv run ruff check src/ tests/ +uv run ty check src/ +``` + +The `tests/unit/test_no_version_literals.py` regex guard must stay green. + +### 10. Docs + +- Add `docs/guides/migration--to-.md` (use the 0.6→0.7 doc as the template). +- Update `docs/concepts/untp-versions.md` table. +- Refresh CHANGELOG. + +## Anti-patterns to refuse + +- "Just bump the default to the new version" without writing the upgrade shim — breaks every pinned downstream user. +- "Edit the existing model class in place" — produces an Either-typed schema and unstable JSON-LD output. +- "Fetch the schema from the network at validate time" — destroys offline-first behaviour and supply-chain integrity. +- "Bypass the registry with a one-off literal because it's a test fixture" — defeats the no-literals guard test. + +## Pointers + +- Migration plan: [docs/plans/UNTP_0.7.0_MIGRATION.md](../../docs/plans/UNTP_0.7.0_MIGRATION.md) +- Upstream source: (tag `v0.7.0`) +- Hosted context: +- Versioning rule: [.claude/rules/untp-versioning.md](../../.claude/rules/untp-versioning.md) diff --git a/.claude/skills/untp-migrate/scripts/diff_schema.py b/.claude/skills/untp-migrate/scripts/diff_schema.py new file mode 100755 index 0000000..a0415e8 --- /dev/null +++ b/.claude/skills/untp-migrate/scripts/diff_schema.py @@ -0,0 +1,82 @@ +#!/usr/bin/env python3 +"""Diff two UNTP DPP JSON Schema versions and emit a markdown table. + +Usage: + python3 diff_schema.py + +Prints sections matching the format used in the migration plan: + - Root field diff + - Required-field diff + - $defs class diff (added / removed / shared) + - Per-shared-class property diff + +No external dependencies; standard library only. +""" + +from __future__ import annotations + +import json +import sys +from collections.abc import Mapping +from pathlib import Path +from typing import Any + + +def _props(spec: Mapping[str, Any]) -> dict[str, list[str]]: + out: dict[str, list[str]] = {} + for name, body in (spec.get("$defs") or {}).items(): + out[name] = sorted((body.get("properties") or {}).keys()) + out["__root__"] = sorted((spec.get("properties") or {}).keys()) + return out + + +def _print_section(title: str) -> None: + print() + print(f"### {title}") + print() + + +def main(old_path: str, new_path: str) -> int: + old = json.loads(Path(old_path).read_text(encoding="utf-8")) + new = json.loads(Path(new_path).read_text(encoding="utf-8")) + + old_props = _props(old) + new_props = _props(new) + + print(f"# Schema diff: `{Path(old_path).name}` → `{Path(new_path).name}`") + + _print_section("Root field diff") + ra, rb = set(old_props["__root__"]), set(new_props["__root__"]) + print(f"- removed: {sorted(ra - rb)}") + print(f"- added: {sorted(rb - ra)}") + print(f"- shared: {sorted(ra & rb)}") + + _print_section("Required-field diff") + print(f"- old required: {old.get('required')}") + print(f"- new required: {new.get('required')}") + + _print_section("$defs class diff") + da = set(old_props) - {"__root__"} + db = set(new_props) - {"__root__"} + print(f"- removed defs: {sorted(da - db)}") + print(f"- added defs: {sorted(db - da)}") + print(f"- shared defs: {sorted(da & db)}") + + _print_section("Per-shared-class property diff") + for cls in sorted(da & db): + pa, pb = set(old_props[cls]), set(new_props[cls]) + if pa == pb: + continue + print(f"#### `{cls}`") + print(f"- removed: {sorted(pa - pb)}") + print(f"- added: {sorted(pb - pa)}") + print() + + return 0 + + +if __name__ == "__main__": + if len(sys.argv) != 3: + print(__doc__, file=sys.stderr) + sys.exit(2) + sys.exit(main(sys.argv[1], sys.argv[2])) diff --git a/.claude/skills/validate-dpp/SKILL.md b/.claude/skills/validate-dpp/SKILL.md new file mode 100644 index 0000000..560409b --- /dev/null +++ b/.claude/skills/validate-dpp/SKILL.md @@ -0,0 +1,67 @@ +______________________________________________________________________ + +## name: validate-dpp description: Implement Digital Product Passport validation features following EU ESPR/CIRPASS standards. Use when adding a new validator, Pydantic model for a DPP entity, JSON-LD export, or any work that touches src/dppvalidator/models/ or src/dppvalidator/validators/. allowed-tools: Read Edit Write Grep Glob Bash(uv run pytest \*) Bash(uv run ruff \*) Bash(uv run ty \*) + +# validate-dpp + +Implement DPP validation features following EU ESPR regulations and CIRPASS ontologies. + +## Implementation steps + +### 1. Define the Pydantic v2 model + +```python +from pydantic import BaseModel, Field, field_validator, model_validator + + +class YourModel(BaseModel): + field_name: str = Field(..., description="Description for docs") + optional_field: str | None = None # use X | None, not Optional[X] + + @field_validator("field_name") + @classmethod + def validate_field(cls, v: str) -> str: + # validation logic + return v +``` + +Anchor decisions to `.claude/rules/dpp-domain.md` (Pydantic v2 patterns, ESPR/CIRPASS reference data). + +### 2. Wire it into the validation engine + +- Add the model under `src/dppvalidator/models/`. +- Register it in the validation engine. +- Add JSON-LD export support (`@context`, `@type`). + +### 3. Write tests + +```python +import pytest +from dppvalidator.models import YourModel + + +def test_valid_model() -> None: + model = YourModel(field_name="value") + assert model.field_name == "value" + + +def test_invalid_model() -> None: + with pytest.raises(ValueError): + YourModel(field_name="invalid") +``` + +### 4. Verify + +```bash +uv run pytest tests/ -q +uv run ruff check src/ tests/ +uv run ty check src/ +``` + +## Reference standards + +- **ISO 2076**: textile fiber codes +- **ISO 3166-1**: country codes +- **GS1 GTIN-13**: product identifiers +- **JSON-LD**: linked data format +- **CIRPASS / UNECE**: DPP ontologies diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 70832d5..0417155 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -51,9 +51,19 @@ jobs: - name: Run ty type checker run: uv run ty check src/dppvalidator/ - # Matches pre-commit: pip-audit (pre-push stage) + # Matches pre-commit: pip-audit (pre-push stage). + # + # We audit a frozen requirements file (the actually-installed + # non-editable packages) rather than ``pip-audit --skip-editable`` + # alone. The plain ``uv run pip-audit`` ships its own pip 25.3 in + # the tool environment, which surfaces pip-the-installer CVEs + # unrelated to anything dppvalidator imports or ships. ``pip + # freeze --exclude-editable`` doesn't emit ``pip`` itself, so the + # audit is scoped to runtime + dev dependencies only. - name: Run security scan (pip-audit) - run: uv run pip-audit --skip-editable + run: | + uv pip freeze --exclude-editable > /tmp/audit-requirements.txt + uv run pip-audit --requirement /tmp/audit-requirements.txt --strict # Check error documentation coverage - name: Check error documentation diff --git a/.gitignore b/.gitignore index 2905e03..92e83b4 100644 --- a/.gitignore +++ b/.gitignore @@ -211,6 +211,13 @@ __marimo__/ .windsurf/ docs/windsurf/ +# Claude Code +# .claude/ itself is committed (settings.json, rules, commands, skills, hooks), +# but personal overrides and credentials must not be. +.claude/settings.local.json +.claude/.credentials.json +CLAUDE.local.md + # Internal planning documents (keep locally, exclude from repo) docs/IMPLEMENTATION_PLAN.md docs/IMPROVEMENT_ROADMAP.md diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 06c0620..a1b6d97 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -3,9 +3,20 @@ repos: rev: v6.0.0 hooks: - id: check-yaml - args: [--unsafe] + args: [ --unsafe ] + # ``end-of-file-fixer`` and ``trailing-whitespace`` mutate files — + # which is incompatible with the SHA-pinned vendored artefacts + # under ``src/dppvalidator/{schemas,vocabularies}/data/``. Those + # files are byte-pinned in + # ``src/dppvalidator/schemas/data/MANIFEST.json`` and verified by + # ``tests/unit/test_manifest_integrity.py``. Adding a trailing + # newline silently invalidates the SHA. The exclusion guards + # both that contract and the upstream-vendored fixtures under + # ``tests/fixtures/upstream/`` (also SHA-recorded in SOURCES.md). - id: end-of-file-fixer + exclude: ^(src/dppvalidator/(schemas|vocabularies)/data/|tests/fixtures/upstream/).*$ - id: trailing-whitespace + exclude: ^(src/dppvalidator/(schemas|vocabularies)/data/|tests/fixtures/upstream/).*$ - id: detect-private-key - id: debug-statements - id: check-added-large-files @@ -19,45 +30,47 @@ repos: - mdformat-gfm - mdformat-black - mdformat-admon - exclude: ^\.windsurf/(rules|skills|workflows)/.*\.md$ + exclude: ^(\.windsurf/(rules|skills|workflows)|\.claude|docs/plans|tests/fixtures/upstream)/.*\.md$ - repo: https://github.com/hadialqattan/pycln rev: v2.6.0 hooks: - id: pycln - args: [--config=pyproject.toml] + args: [ --config=pyproject.toml ] - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.14.14 hooks: # bandit (security) - exclude notebooks (example code uses random) + # ``scripts/`` is excluded too: it holds dev-only utilities (e.g. the + # functional smoke test) that legitimately drive ``subprocess.run`` + # with controlled, env-overridable binary paths — not user input. - id: ruff - types_or: [python, pyi] - args: ["--fix", "--select=S", "--ignore=S101,S110"] - exclude: ^(tests/|mutants/|examples/) + types_or: [ python, pyi ] + args: [ "--fix", "--select=S", "--ignore=S101,S110" ] + exclude: ^(tests/|mutants/|examples/|scripts/) # isort - id: ruff - types_or: [python, pyi, jupyter] - args: [--fix, "--select=I"] + types_or: [ python, pyi, jupyter ] + args: [ --fix, "--select=I" ] # type annotations - exclude notebooks (educational examples) - id: ruff - types_or: [python, pyi] - args: - ["--select", "ANN", "--ignore", "ANN101,ANN102,ANN401,ANN002,ANN003"] - exclude: ^(tests/|mutants/|benchmarks/|examples/).*$ + types_or: [ python, pyi ] + args: [ "--select", "ANN", "--ignore", "ANN101,ANN102,ANN401,ANN002,ANN003" ] + exclude: ^(tests/|mutants/|benchmarks/|examples/|scripts/).*$ # Replace %s statements with f-string syntax - id: ruff - types_or: [python, pyi, jupyter] - args: ["--select=FLY002"] + types_or: [ python, pyi, jupyter ] + args: [ "--select=FLY002" ] # Comprehensive ruff check (no auto-fix) - only on src code - id: ruff name: ruff-check-all - types_or: [python, pyi] - args: ["--output-format=github"] - exclude: ^(tests/|mutants/|benchmarks/|examples/) + types_or: [ python, pyi ] + args: [ "--output-format=github" ] + exclude: ^(tests/|mutants/|benchmarks/|examples/|scripts/) # formatting - id: ruff-format - types_or: [python, pyi, jupyter] + types_or: [ python, pyi, jupyter ] # ty type checking via uv (uses project's dev dependencies) - repo: local @@ -66,11 +79,17 @@ repos: name: ty entry: uv run ty check src/dppvalidator/ language: system - types: [python] + types: [ python ] pass_filenames: false + # Audit the actually-installed non-editable packages rather than + # plain ``uv run pip-audit --skip-editable``. The bare command + # ships its own pip 25.3 in the tool environment, which surfaces + # pip-the-installer CVEs unrelated to anything dppvalidator + # imports or ships. ``pip freeze --exclude-editable`` excludes + # both pip itself and our editable package. - id: pip-audit name: pip-audit - entry: uv run pip-audit --skip-editable + entry: bash -c 'uv pip freeze --exclude-editable > /tmp/audit-requirements.txt && uv run pip-audit --requirement /tmp/audit-requirements.txt --strict' language: system pass_filenames: false - stages: [pre-push] + stages: [ pre-push ] diff --git a/AGENTS.md b/AGENTS.md index 1196559..6a6e093 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -19,23 +19,52 @@ ```text src/dppvalidator/ # Main package ├── models/ # Pydantic models for DPP entities -├── validators/ # Validation logic +│ ├── v0_6/ # UNTP 0.6.x models (ProductPassport envelope) +│ ├── v0_7/ # UNTP 0.7.0 models (Product as credentialSubject) +│ └── … # 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 +│ └── … +├── compat/ # Cross-version compat shims (Phase 4) ├── 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 +├── exporters/ # JSON-LD and EU DPP export formats +├── schemas/ # JSON Schema loading + version registry +├── vocabularies/ # Controlled vocabulary loading + EU DPP ontology mapping +├── cli/ # Command-line interface (validate, migrate, schema, …) +├── plugins/ # Plugin system (entry-points discovery) └── __init__.py tests/ # Test suite ├── unit/ # Unit tests -├── integration/ # Integration tests +├── integration/ # Integration tests (incl. version matrix, plugin) ├── property/ # Property-based tests (Hypothesis) ├── fuzz/ # Fuzz tests └── fixtures/ # Test data + ├── valid/ # Per-version happy-path fixtures + ├── invalid/0.7.0/ # v0.7-specific failure fixtures + └── upstream/ # SHA-pinned upstream samples ``` +## UNTP version handling + +dppvalidator supports **UNTP DPP 0.6.x and 0.7.0** 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 + code instead of hardcoding the literal. +- Adding a new version: see + [`.claude/rules/untp-versioning.md`](.claude/rules/untp-versioning.md) + for the cardinal rules and the minimum touch list. Use + `/untp-bump ` (Claude Code). +- v0.6 → v0.7 upgrade: `dppvalidator.compat.upgrade_0_6_to_0_7.upgrade` + ships the 17-step shim with structured warnings; CLI surface is + `dppvalidator migrate` and `dppvalidator validate --upgrade-from`. +- Documentation: [`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). + ## Development Workflow 1. Use **gitflow**: `develop` → `feature/*` → PR → `develop` → `release/*` → `main` @@ -49,3 +78,6 @@ tests/ # Test suite - Validate at boundaries with Pydantic - Type hint all public APIs - Document public functions with docstrings +- **Cardinal versioning rules** in + [`.claude/rules/untp-versioning.md`](.claude/rules/untp-versioning.md) + apply to every change in `src/dppvalidator/{schemas,exporters,models,validators,compat}/`. diff --git a/CHANGELOG.md b/CHANGELOG.md index ca1974e..f7d9a1c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,113 @@ 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.4.0] - 2026-05-08 + +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); +each phase has its own implementation log there. + +### Added + +- **UNTP DPP 0.7.0 schema, context, and Pydantic models**. Vendored + upstream artefacts under `src/dppvalidator/{schemas,vocabularies}/data/` + with SHA-256 pins. New version-namespaced model package + `dppvalidator.models.v0_7.*` (envelope, product, materials, claims, + identifiers, primitives) covering every required field per the + upstream schema. The existing top-level `dppvalidator.models.*` + imports continue to resolve to v0.6 for back-compat. +- **Per-version validator dispatch**. New `_MODEL_BY_VERSION` + (model layer), `ALL_RULES_BY_VERSION` (semantic-rule layer), + `LINK_PATHS_BY_VERSION` (deep validator) tables; `ValidationEngine` + selects the right artefact set per detected version. +- **VER001 version-mismatch fail-fast**. Engine raises `VER001` when + `schema_version` is pinned and the payload's declared version + conflicts. +- **Compat shim 0.6.x → 0.7.0**. New `dppvalidator.compat` package + exporting `upgrade(data, *, country_lookup=None) -> (dict, list[UpgradeWarning])`, + plus `active_version()` / `is_version()` helpers and four warning + codes (`UPG001`–`UPG004`). Implements all 17 transformation steps + from §Phase 4 of the migration plan. +- **`dppvalidator migrate` CLI**. Writes the upgraded JSON to + `-o` / `--in-place` / stdout; refuses on warnings unless + `--accept-warnings`; always emits a sidecar + `.warnings.json` when blocking warnings fire. +- **`dppvalidator validate --upgrade-from `**. Runs the shim + before validating; surfaces upgrade + validation issues in one + report. +- **Per-version EU DPP exporter mapping**. `TermMapping` extended + with `untp_v0_6` / `untp_v0_7` columns + a `TERM_REMOVED` sentinel; + `EUDPPJsonLDExporter(schema_version=…)` dispatches per-call; + auto-detect resolves the source version from the passport's class. +- **Plugin version-awareness**. New + `BrandNameRuleV07` in the example plugin demonstrates the + `applies_to_versions` opt-in pattern; the example plugin is now a + CI-tested target via `tests/integration/test_example_plugin.py`. +- **Manifest integrity test**. + `tests/unit/test_manifest_integrity.py` SHA-256-verifies every + vendored `.json` / `.jsonld` artefact and includes a drift-catch + for un-manifested files. +- **Sample classification test**. + `tests/unit/test_samples_classification.py` pins + `detect_schema_version()` per real-world sample under + `tests/fixtures/samples/`. +- **Production-URL split per artefact**. `SchemaVersion` and + `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); + refreshed schema, JSON-LD, validation, CLI, FAQ, and index pages + with both v0.6 and v0.7 examples. + +### Changed + +- `dppvalidator schema list` reports all three registered versions + (0.6.0, 0.6.1, 0.7.0). +- `valid_dpp_data` pytest fixture is now parametrised over both + matrix versions; tests pin to a single version with + `@pytest.mark.dpp_version("X.Y.Z")`. +- v0.6 model files were relocated to `dppvalidator.models.v0_6/` + with thin re-export shims at the top level — no callers should + notice. +- v0.6 semantic rules likewise relocated to + `dppvalidator.validators.rules.v0_6/`; new + `dppvalidator.validators.rules.v0_7/` carries the v0.7 ports. +- `EUDPPJsonLDExporter` no longer hardcodes a single mapping table; + the new auto-detection reads the passport's module path. +- The `Characteristics` `$def` quirk in the upstream UNTP 0.7.0 DPP + schema (empty `properties`, description copy-pasted from `Claim`) + is documented in `MANIFEST.json` and + `docs/concepts/cirpass-implementation.md`. + +### Deprecated + +- Hardcoded version literals (`"0.6.1"` / `"0.7.0"`) in feature code + outside `schemas/registry.py` and `exporters/contexts.py`. The + `tests/unit/test_no_version_literals.py` guard rejects new + occurrences. Feature code should call + `dppvalidator.compat.active_version()` instead. + +### Fixed + +- Credential verifier: `proofValue` decoding no longer misroutes + base64-encoded Ed25519 signatures whose first character happens to + be `z` (~1.5% of random signatures). The verifier now treats the + leading `z` as a multibase base58btc hint and falls back to base64 + if base58 decode fails, instead of raising and returning `None`. + +### Tests + +- 2019 passing, 13 skipped (by-design via the dpp_version marker), + 1 xfailed; coverage 92.20 % (above the 90 % gate). +- Net new tests added across the migration: ~150+ unit cases, the + 17-case version matrix, the 13-case manifest integrity, the + 27-case samples classification, the 17-case plugin integration, + the 50-case compat shim, the 10-case CLI migrate, the 4-case + round-trip integration, and 10 production-URL pins. + ## [0.3.2] - 2026-02-01 ### Added diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..ca42a4a --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,36 @@ +@AGENTS.md + +## Working with Claude Code in this repo + +This repository is configured for Claude Code under `.claude/`: + +- `.claude/CLAUDE.md` — extra project instructions (this file imports it implicitly via `./CLAUDE.md`) +- `.claude/rules/` — path-scoped rules that load when Claude reads matching files +- `.claude/skills/` — invocable skills (`/validate-dpp`, `/pypi-publish`) +- `.claude/commands/` — slash commands for workflows (`/lint`, `/test`, `/feature`, `/release`, `/hotfix`, `/pr-review`, `/code-health`, `/docs-health`, `/dev-setup`, `/fix-lint`, `/claude-health`) +- `.claude/settings.json` — hooks and other shared settings (committed) +- `.claude/settings.local.json` — personal overrides (gitignored) + +## Conventions specific to Claude Code sessions + +- Always use `uv run ` (not bare `pytest`/`ruff`/`ty`); the project pins versions through `uv`. +- Prefer the `Edit` tool for changes to existing files; reserve `Write` for new files. +- When editing Python under `src/dppvalidator/` or `tests/`, the `PostToolUse` hook auto-runs `uv run ruff check --fix` on the touched file. If a fix is applied, re-read the file before subsequent edits. +- Follow conventional commits (`feat:`, `fix:`, `docs:`, `refactor:`, `test:`, `chore:`, `ci:`, `perf:`). See `.claude/rules/commits.md`. +- Do not import plugin code from `src/dppvalidator/` (one-way dependency only). See `.claude/rules/plugin-licenses.md`. + +## Quick orientation + +- Public package: `src/dppvalidator/` (MIT) +- Plugin packages: `plugins/*/` (separately licensed; e.g. `plugins/textiles/` is GPL-3.0) +- Tests: `tests/{unit,integration,property,fuzz}/` with shared fixtures in `tests/fixtures/` +- Docs site: `mkdocs.yml` + `docs/` +- CLI entry: defined in `pyproject.toml` +- Versioned models: `src/dppvalidator/models/v0_6/`, `…/v0_7/`. + Top-level imports re-export v0.6 for back-compat. +- Compat shim 0.6 → 0.7: + `src/dppvalidator/compat/upgrade_0_6_to_0_7.py` (CLI: + `dppvalidator migrate` and `validate --upgrade-from`). +- Versioning cardinal rules: `.claude/rules/untp-versioning.md`. + Adding a UNTP version: `/untp-bump `. + Migration plan archive: `docs/plans/UNTP_0.7.0_MIGRATION.md`. diff --git a/README.md b/README.md index 1015387..2e23b75 100644 --- a/README.md +++ b/README.md @@ -35,14 +35,18 @@ ______________________________________________________________________ ## Why dppvalidator? + + | Challenge | Solution | | --------------------------------- | ----------------------------------------------------------------------------------------------------------------------- | | Complex JSON Schema validation | **Seven-layer validation** catches errors at schema, model, semantic, JSON-LD, vocabulary, plugin, and signature levels | -| Evolving UNTP specifications | **Built-in schema support** for UNTP DPP 0.6.1 with easy version switching | +| Evolving UNTP specifications | **Both UNTP DPP 0.6.x and 0.7.0** — auto-detected; `dppvalidator migrate` upgrades 0.6 → 0.7 | | Integration with existing systems | **CLI + Python API** for pipelines, CI/CD, and application integration | | Custom business rules | **Plugin system** for domain-specific validators and exporters | | Interoperability requirements | **JSON-LD export** for W3C Verifiable Credentials compliance | + + ## Installation ``` @@ -171,6 +175,35 @@ jsonld_output = exporter.export(passport) # Ready for W3C Verifiable Credentials ecosystem ``` +## Supported versions + +dppvalidator supports both UNTP DPP wire formats in the same release. +The version is auto-detected from the payload's `@context` / +`$schema` URLs; pin explicitly with `--schema-version` (CLI) or +`schema_version=` (Python). + + + +| UNTP DPP | 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`. | + + + +A compat shim upgrades v0.6.x payloads to v0.7.0 shape: + +```bash +dppvalidator migrate passport-v06.json -o passport-v07.json +dppvalidator validate passport-v06.json --upgrade-from 0.6.1 --schema-version 0.7.0 +``` + +The full version-handling story is documented in +[`docs/concepts/untp-versions.md`](docs/concepts/untp-versions.md); +the field rename table and warning codes are in +[`docs/guides/migration-0-6-to-0-7.md`](docs/guides/migration-0-6-to-0-7.md). + ## Features ### Seven-Layer Validation Architecture diff --git a/docs/concepts/cirpass-implementation.md b/docs/concepts/cirpass-implementation.md index aea1ea1..29fe1cf 100644 --- a/docs/concepts/cirpass-implementation.md +++ b/docs/concepts/cirpass-implementation.md @@ -131,6 +131,52 @@ vocabularies/data/schemas/ └── cirpass_dpp.xsd # XML Schema (reference) ``` +## UNTP 0.7.0 schema notes + +The bundled UNTP 0.7.0 DPP schema lives at +`schemas/data/untp-dpp-schema-0.7.0.json` and is byte-for-byte identical +to two upstream-published copies: + +- **Production:** + +- **SHA-pinned source:** + + +Both URLs are recorded in `MANIFEST.json` (`production_url` and +`source_url` respectively); the SHA-256 pin is enforced by +`tests/unit/test_manifest_integrity.py`. + +### Known upstream quirk: `Characteristics` `$def` + +The UN/CEFACT split layout publishes the Product schema as a separate +file +(), +whose 17 `$defs` are also embedded into the bundled +`DigitalProductPassport.json`. Verified on 2026-05-08, **the embedded +`$defs.Characteristics` differs from the standalone version**: + +**Bundled `DPP.$defs.Characteristics`** carries an empty +`"properties": {}` and a description that was clearly copy-pasted from +`$defs.Claim` ("A declaration of conformance with one or more +criteria…"). + +**Standalone `Product.$defs.Characteristics`** has the canonical +shape: a documented `@context` field in `properties` for JSON-LD +vocabulary scoping, plus the correct Characteristics-specific +description. + +dppvalidator models `Characteristics` as `extra="allow"` (in both +[`v0_6/product.py`](https://github.com/artiso-ai/dppvalidator/blob/main/src/dppvalidator/models/v0_6/product.py) +and +[`v0_7/primitives.py`](https://github.com/artiso-ai/dppvalidator/blob/main/src/dppvalidator/models/v0_7/primitives.py)), +so behaviour is identical to the standalone Product.json: arbitrary +extension fields flow through. The discrepancy is purely documentary +and is documented in `MANIFEST.json` (notes field on the +`untp-dpp-schema@0.7.0` entry) for future readers. Vendoring +`Product.json` was considered but rejected because the validator +already runs against the bundled file and a second copy would create +silent-divergence risk if upstream ever fixes one without the other. + ## CLI Usage Validate with CIRPASS schema from command line: diff --git a/docs/concepts/eudpp-ontology-alignment.md b/docs/concepts/eudpp-ontology-alignment.md index d458b15..b8f9a2a 100644 --- a/docs/concepts/eudpp-ontology-alignment.md +++ b/docs/concepts/eudpp-ontology-alignment.md @@ -109,11 +109,33 @@ engine = ValidationEngine() result = engine.validate(dpp_data) if result.valid and result.passport: - # Export to EU DPP format + # Export to EU DPP format (auto-detects UNTP version from the + # passport's class — works for both v0.6 and v0.7 inputs). exporter = EUDPPJsonLDExporter() eudpp_jsonld = exporter.export(result.passport) ``` +### Per-version mapping (Phase 3c) + +The exporter is **version-aware**. UNTP v0.6 and v0.7 use different +source-side spellings (`serialNumber` vs `itemNumber`, +`producedByParty` vs `relatedParty`, …) but most map to the same EU +DPP target URI. The mapping table in +[`vocabularies/ontology.py:TermMapping`](https://github.com/artiso-ai/dppvalidator/blob/main/src/dppvalidator/vocabularies/ontology.py) +carries `untp_v0_6` / `untp_v0_7` columns; the exporter reads the +right one per call. + +- **`schema_version=None` (default)** — auto-detect from the passport + class's module path (`dppvalidator.models.v0_X.*`). +- **`schema_version="0.6.1"` or `"0.7.0"`** — pin explicitly. Useful + for downstream-compat scenarios (e.g. forcing v0.6 mapping on a + v0.7 passport). + +Terms removed in a given version (the `TERM_REMOVED` sentinel — +currently `gtin` for v0.7) drop out of that version's mapper index. +The full mapping table and per-version usage examples are in the +[EU DPP export guide](../guides/eudpp-export.md). + ## Ontology Data Files Bundled Turtle files for offline validation: diff --git a/docs/concepts/untp-schema.md b/docs/concepts/untp-schema.md index e534145..d5a7908 100644 --- a/docs/concepts/untp-schema.md +++ b/docs/concepts/untp-schema.md @@ -1,3 +1,5 @@ + + # UNTP DPP Schema The UN Trade Facilitation and Electronic Business Centre (UN/CEFACT) has developed the United Nations Transparency Protocol (UNTP) for Digital Product Passports. @@ -8,7 +10,17 @@ The UNTP Digital Product Passport (DPP) is a standardized format for sharing pro ## Schema Version -dppvalidator currently supports UNTP DPP Schema version **0.6.1**. +dppvalidator supports UNTP DPP Schema versions **0.6.0**, **0.6.1** +(default), and **0.7.0**. Both wire shapes are first-class and +coexist in the same release. See [UNTP DPP versions](untp-versions.md) +for the full version-handling story; this page describes the schema +itself. + +The structure diagram below covers the **v0.6.x** shape — the wire +format used by the engine when no `$schema` or `@context` URL pins a +different version. The v0.7.0 shape is summarised at the end of this +page; the canonical v0.7.0 reference is the upstream DPP schema at +. ## Schema Structure @@ -85,19 +97,22 @@ Core product information: ## JSON Schema -The schema is available at: +Bundled SHA-pinned copies live under +[`src/dppvalidator/schemas/data/`](https://github.com/artiso-ai/dppvalidator/blob/main/src/dppvalidator/schemas/data/README.md); +the upstream production URLs are: -```text -https://vocabulary.uncefact.org/untp/dpp/0.6.1/schema.json -``` +| Version | Production URL | +| ------- | -------------------------------------------------------------------------------- | +| 0.6.1 | | +| 0.7.0 | | -## Example +## v0.6.x example ```json { "@context": [ "https://www.w3.org/ns/credentials/v2", - "https://vocabulary.uncefact.org/untp/dpp/0.6.1" + "https://test.uncefact.org/vocabulary/untp/dpp/0.6.1/" ], "type": ["DigitalProductPassport", "VerifiableCredential"], "id": "https://example.com/dpp/battery-001", @@ -109,6 +124,7 @@ https://vocabulary.uncefact.org/untp/dpp/0.6.1/schema.json "validUntil": "2029-01-01T00:00:00Z", "credentialSubject": { "id": "https://example.com/product/battery-001", + "type": ["ProductPassport"], "product": { "id": "https://example.com/product/battery-001", "name": "EV Battery Pack", @@ -118,6 +134,66 @@ https://vocabulary.uncefact.org/untp/dpp/0.6.1/schema.json } ``` +## v0.7.0 example + +The structural shift in v0.7.0: `credentialSubject` IS the +`Product` directly, the `ProductPassport` envelope is gone, and the +top-level `name` field is required. Full v0.7.0-required Product +fields: `id`, `name`, `idScheme`, `idGranularity`, `productCategory`, +`producedAtFacility`, `countryOfProduction`. + +```json +{ + "@context": [ + "https://www.w3.org/ns/credentials/v2", + "https://vocabulary.uncefact.org/untp/0.7.0/context/" + ], + "type": ["DigitalProductPassport", "VerifiableCredential"], + "id": "https://example.com/dpp/battery-001", + "name": "EV Battery Pack DPP", + "issuer": { + "type": ["CredentialIssuer"], + "id": "did:web:example.com:manufacturer", + "name": "Battery Manufacturer Inc." + }, + "validFrom": "2024-01-01T00:00:00Z", + "validUntil": "2029-01-01T00:00:00Z", + "credentialSubject": { + "type": ["Product"], + "id": "https://example.com/product/battery-001", + "name": "EV Battery Pack", + "description": "High-capacity lithium-ion battery", + "idScheme": { + "type": ["IdentifierScheme"], + "id": "https://example.com/schemes/internal", + "name": "Manufacturer internal scheme" + }, + "idGranularity": "model", + "productCategory": [ + { + "type": ["Classification"], + "schemeId": "https://unstats.un.org/unsd/classifications/Econ/cpc/", + "schemeName": "UN Central Product Classification", + "code": "46410", + "name": "Primary cells and primary batteries" + } + ], + "producedAtFacility": { + "type": ["Facility"], + "id": "https://example.com/facilities/001", + "name": "Manufacturer facility" + }, + "countryOfProduction": { + "countryCode": "DE", + "countryName": "Germany" + } + } +} +``` + +For a full field-by-field migration table from v0.6.x, see the +[migration guide](../guides/migration-0-6-to-0-7.md). + ## Related Standards - **W3C Verifiable Credentials** — Credential format @@ -126,5 +202,5 @@ https://vocabulary.uncefact.org/untp/dpp/0.6.1/schema.json ## Next Steps -- [Seven-Layer Validation](validation-layers.md) — How dppvalidator validates DPPs +- [Three-Layer Validation](validation-layers.md) — How dppvalidator validates DPPs - [Validation Guide](../guides/validation.md) — Using the validation engine diff --git a/docs/concepts/untp-versions.md b/docs/concepts/untp-versions.md new file mode 100644 index 0000000..3f3a7a0 --- /dev/null +++ b/docs/concepts/untp-versions.md @@ -0,0 +1,194 @@ + + +# UNTP DPP versions + +dppvalidator supports more than one UNTP DPP wire format. This page is the +single authoritative explanation of how versions are detected, which one +is the default, how to pick one explicitly, and how a new one would be +added. + +For a side-by-side migration walkthrough between specific versions, +see the [migration guide](../guides/migration-0-6-to-0-7.md). + +## Supported versions + +| 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. | + +Future releases may flip the default. The flip is a separate minor +release with deprecation warnings — see *Adding a new version* below. + +## How a payload's version is detected + +`dppvalidator.validators.detection.detect_schema_version(data)` is the +**only** place where this decision is made. The detection rules apply +in order: + +1. **`$schema` URL** — if the payload carries a `$schema` field whose + URL matches a vendored schema basename + (`untp-dpp-schema-X.Y.Z.json` or `…/vX.Y.Z/.../DigitalProductPassport.json`), + that wins. +1. **`@context` URLs** — if a context entry matches a registered + pattern, the version is read from there. Two URL conventions are + recognised: + - Legacy (0.6.x): `https://test.uncefact.org/vocabulary/untp/dpp/X.Y.Z/` + - Modern (0.7.0+): `https://vocabulary.uncefact.org/untp/X.Y.Z/context/` +1. **Type marker** — if `"DigitalProductPassport"` appears in the + `type` array, the payload is recognised as a DPP and the + `DEFAULT_SCHEMA_VERSION` is used. +1. **Fallback** — `DEFAULT_SCHEMA_VERSION`. + +False positives are guarded by a registry-membership check at the call +sites: a `/X.Y.Z/` URL segment that doesn't appear in +`SCHEMA_REGISTRY` is rejected. Adding a new URL convention means +appending a pattern in [validators/detection.py](https://github.com/artiso-ai/dppvalidator/blob/main/src/dppvalidator/validators/detection.py), +not anywhere else. + +## Picking a version explicitly + +Three layers can pin a version. Pick the one that matches your +intent. + +### Engine-wide pin + +```python +from dppvalidator.validators import ValidationEngine + +# Pin the engine; auto-detection is bypassed. +engine = ValidationEngine(schema_version="0.7.0") +result = engine.validate(payload) +``` + +When `schema_version` and the payload's declared version disagree, +the engine emits a **VER001 version mismatch** error and refuses to +validate. This fail-fast behaviour catches accidental version +mixing in pipelines. + +### CLI pin + +```bash +# Validate as 0.7.0 regardless of the payload's declared version. +dppvalidator validate passport.json --schema-version 0.7.0 + +# Validate as 0.6.1 (current default; the flag is optional). +dppvalidator validate passport.json --schema-version 0.6.1 + +# List every version the registry knows about. +dppvalidator schema list +``` + +### Programmatic registry lookup + +```python +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' + +# The SHA-pinned upstream URL the bundled bytes came from. +print(reg.get_schema_url("0.7.0")) + +# The canonical production URL (when set; e.g. untp.unece.org for v0.7). +print(reg.get_production_url("0.7.0")) + +# JSON-LD context URLs paired with this version. +print(reg.get_context_urls("0.7.0")) +``` + +## Default version + +`dppvalidator.schemas.registry.DEFAULT_SCHEMA_VERSION` is the single +source of truth. Application code that needs to refer to "the active +default" version SHOULD use +`dppvalidator.compat.active_version()` instead of importing the +constant directly — both return the same string, but +`active_version()` keeps your call site outside the +no-version-literals guard's allow-list. + +```python +from dppvalidator.compat import active_version, is_version + +if is_version("0.7.0"): + # we're on a build whose default is v0.7.0 + ... +``` + +## Coexistence with v0.6.x + +v0.6.x and v0.7.0 coexist in the same release. Every validator layer, +every exporter, and every model package is version-aware: + +| Surface | v0.6 entry point | v0.7 entry point | +| ----------------------------- | -------------------------------------------------------------- | ---------------------------------------------------------- | +| Pydantic model package | `dppvalidator.models.v0_6.*` | `dppvalidator.models.v0_7.*` | +| Top-level shim (back-compat) | `dppvalidator.models.passport.DigitalProductPassport` (= v0.6) | `dppvalidator.models.v0_7.envelope.DigitalProductPassport` | +| Semantic-rule registry | `dppvalidator.validators.rules.v0_6` | `dppvalidator.validators.rules.v0_7` | +| Deep-link path table | `LINK_PATHS_BY_VERSION["0.6.1"]` | `LINK_PATHS_BY_VERSION["0.7.0"]` | +| EU DPP exporter mapping table | `TermMapping.untp_v0_6` | `TermMapping.untp_v0_7` | +| Engine model dispatch | `_MODEL_BY_VERSION["0.6.1"]` | `_MODEL_BY_VERSION["0.7.0"]` | +| Compat shim (input upgrade) | n/a | `dppvalidator.compat.upgrade_0_6_to_0_7.upgrade(data)` | + +Plugin authors targeting a specific version should set +`applies_to_versions: tuple[str, ...] = ("0.7.0",)` on their rule +class and gate their attribute access on the wire shape they expect. +See the [example plugin](https://github.com/artiso-ai/dppvalidator/tree/main/examples/dppvalidator_example_plugin) +for a worked v0.6 vs v0.7 sibling pair. + +## Migrating between versions + +v0.6.x payloads can be upgraded to v0.7.0 shape via the bundled +compat shim: + +```bash +# Upgrade a single payload, write to stdout. +dppvalidator migrate passport.json + +# Upgrade in place, accepting any warnings. +dppvalidator migrate passport.json --in-place --accept-warnings + +# Validate the v0.6 payload against v0.7 (runs the shim, then validates). +dppvalidator validate passport.json --upgrade-from 0.6.1 --schema-version 0.7.0 +``` + +The shim emits structured warnings (`UPG001`–`UPG004`) for lossy or +synthesised values; a sidecar `.warnings.json` is always +written when blocking warnings fire. See the +[migration guide](../guides/migration-0-6-to-0-7.md) for the field +rename table and known shim limitations. + +## Adding a new UNTP version + +The full recipe lives in +[`.claude/skills/untp-migrate/SKILL.md`](https://github.com/artiso-ai/dppvalidator/blob/main/.claude/skills/untp-migrate/SKILL.md) +(invocable as `/untp-bump ` in Claude Code) and the +authoritative cardinal-rules document is +[`.claude/rules/untp-versioning.md`](https://github.com/artiso-ai/dppvalidator/blob/main/.claude/rules/untp-versioning.md). +The minimum touch list: + +1. `src/dppvalidator/schemas/registry.py` — one new `SchemaVersion` entry. +1. `src/dppvalidator/exporters/contexts.py` — one new `ContextDefinition` entry. +1. `src/dppvalidator/schemas/data/MANIFEST.json` — manifest entries for the new schema and context (with SHA-256, source URL, production URL). +1. `src/dppvalidator/schemas/data/untp-dpp-schema-X.Y.Z.json` — vendored schema bytes. +1. `src/dppvalidator/vocabularies/data/untp-context-X.Y.Z.jsonld` — vendored context bytes. +1. `src/dppvalidator/models/vX_Y/` — new Pydantic model package. +1. `src/dppvalidator/validators/model.py` — add to `_MODEL_BY_VERSION`. +1. `src/dppvalidator/validators/detection.py` — extend URL pattern if the namespace shape changed. +1. `src/dppvalidator/compat/upgrade__to_.py` — input shim from the previous version. +1. `tests/fixtures/upstream/vX.Y.Z/` — vendored upstream samples + schema. +1. `tests/integration/test_version_matrix.py` — add the new version to the matrix. +1. `docs/plans/UNTP_X.Y.Z_MIGRATION.md` — full migration doc. + +If your change touches more than this list, you're either fixing an +unrelated bug (split the PR) or going around the version-aware +spine (don't). + +## See also + +- [Migration guide: 0.6.x → 0.7.0](../guides/migration-0-6-to-0-7.md) +- [Validation pipeline](validation-layers.md) — how the version flows through the validator layers. +- [EU DPP ontology alignment](eudpp-ontology-alignment.md) — how UNTP versions map onto EU DPP / CIRPASS-2 ontology terms. +- [Migration plan archive](https://github.com/artiso-ai/dppvalidator/blob/main/docs/plans/UNTP_0.7.0_MIGRATION.md) — phased history of the v0.7.0 migration. diff --git a/docs/concepts/validation-layers.md b/docs/concepts/validation-layers.md index 4e64224..7a3d4a7 100644 --- a/docs/concepts/validation-layers.md +++ b/docs/concepts/validation-layers.md @@ -61,21 +61,53 @@ Automatically detects the DPP schema version from the input document. **Detection priority:** -1. `$schema` URL pattern (e.g., `untp-dpp-schema-0.6.1.json`) -1. `@context` URLs (e.g., `/untp/dpp/0.6.1/`) +1. `$schema` URL pattern (e.g., `untp-dpp-schema-0.6.1.json` or + `…/v0.7.0/.../DigitalProductPassport.json`) +1. `@context` URLs: + - Legacy (0.6.x): `https://test.uncefact.org/vocabulary/untp/dpp/X.Y.Z/` + - Modern (0.7.0+): `https://vocabulary.uncefact.org/untp/X.Y.Z/context/` 1. `type` array presence → default version -1. Fallback to default (0.6.1) +1. Fallback to `dppvalidator.schemas.registry.DEFAULT_SCHEMA_VERSION` + (currently `0.6.1`) ```python from dppvalidator import ValidationEngine -# Auto-detection is the default -engine = ValidationEngine() # schema_version="auto" +# Auto-detection (default) +engine = ValidationEngine() -# Or explicit version for deterministic behavior +# 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. +engine = ValidationEngine(schema_version="0.7.0") ``` +The full version-handling story (detection internals, default-version +constant, adding a new UNTP version) lives in +[UNTP DPP versions](untp-versions.md). + +### Per-version layer dispatch + +Layers 1–3 below dispatch through version-keyed tables — the engine +selects the right model / rule set / link paths for the detected +version. The dispatch is centralised in three tables: + + + +| Table | Module | Layer it powers | +| ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------- | +| `_MODEL_BY_VERSION` | [`validators/model.py`](https://github.com/artiso-ai/dppvalidator/blob/main/src/dppvalidator/validators/model.py) | Model layer (Layer 2) | +| `ALL_RULES_BY_VERSION` | [`validators/rules/__init__.py`](https://github.com/artiso-ai/dppvalidator/blob/main/src/dppvalidator/validators/rules/__init__.py) | Semantic layer (Layer 4) | +| `LINK_PATHS_BY_VERSION` | [`validators/deep.py`](https://github.com/artiso-ai/dppvalidator/blob/main/src/dppvalidator/validators/deep.py) | Deep validator (separate from layers 1–5) | + + + +Plugin authors can opt into version-aware dispatch by setting +`applies_to_versions = ("0.7.0",)` on their rule class — see the +[plugin development guide](../guides/plugins.md#writing-a-version-aware-rule). + ## Layer 1: Schema Validation Validates JSON structure against the UNTP DPP JSON Schema. diff --git a/docs/dpp_validator_description.md b/docs/dpp_validator_description.md new file mode 100644 index 0000000..ad4521e --- /dev/null +++ b/docs/dpp_validator_description.md @@ -0,0 +1,13 @@ +## 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/MDL098.md b/docs/errors/MDL098.md new file mode 100644 index 0000000..991646c --- /dev/null +++ b/docs/errors/MDL098.md @@ -0,0 +1,63 @@ +# MDL098 - No Model Registered for Schema Version + +## Description + +The model layer was asked to validate against an +`schema_version` for which no Pydantic model is registered in +`_MODEL_BY_VERSION` (see +[`validators/model.py`](https://github.com/artiso-ai/dppvalidator/blob/main/src/dppvalidator/validators/model.py)). +The engine fails fast rather than silently coercing to the default +model — silent coercion would mask the misconfiguration and produce +misleading downstream errors. + +## Category + +Model Errors + +## Severity + +`error` + +## Common Causes + +- The caller passed an `schema_version` string the engine doesn't + know about (e.g. a typo, a future-version pin, or a CI environment + that wasn't refreshed after a UNTP upgrade). +- A custom build of dppvalidator removed a model registration + without also removing the version from `SCHEMA_REGISTRY`. + +## How to Fix + +1. **Check the registered versions** — `dppvalidator schema list` + prints every version `_MODEL_BY_VERSION` knows about. The error + message also lists them under the `available` field. +1. **Pin a registered version** when constructing the engine, or + omit `schema_version` entirely to let auto-detection pick from + the payload's `@context`. +1. **If you need a new version**, follow the recipe in + [`.claude/rules/untp-versioning.md`](https://github.com/artiso-ai/dppvalidator/blob/main/.claude/rules/untp-versioning.md) + (or the `/untp-bump ` slash command). Adding a version + requires touching `_MODEL_BY_VERSION`, `SCHEMA_REGISTRY`, and a + handful of related dispatch tables — see the rule for the full + minimum touch list. + +## Example + +```python +from dppvalidator.validators import ValidationEngine + +# Wrong — "0.5.0" isn't registered. +engine = ValidationEngine(schema_version="0.5.0") +result = engine.validate(payload) +# result.errors[0].code == "MDL098" +# result.errors[0].context["requested_version"] == "0.5.0" + +# Right — register one of the supported versions, or auto-detect. +engine = ValidationEngine(schema_version="0.7.0") +``` + +## See Also + +- [VER001 - UNTP version mismatch](VER001.md) — the engine-vs-payload version mismatch (different failure mode, same family). +- [UNTP DPP versions](../concepts/untp-versions.md) +- [Error Overview](index.md) diff --git a/docs/errors/UPG001.md b/docs/errors/UPG001.md new file mode 100644 index 0000000..5635c22 --- /dev/null +++ b/docs/errors/UPG001.md @@ -0,0 +1,49 @@ +# UPG001 - Lossy upgrade transformation + +## Description + +The v0.6 → v0.7 compat shim +(`dppvalidator.compat.upgrade_0_6_to_0_7.upgrade`) encountered a +field that has no v0.7.0 equivalent and dropped it. The +transformation is structurally complete but lossy — the source +information is no longer represented in the upgraded payload. + +## Category + +Compat shim (Phase 4 of `docs/plans/UNTP_0.7.0_MIGRATION.md`). + +## Severity + +`warning` (or `info` for sentinel placeholders that were already +non-data, e.g. `"undefined"` in `Material.symbol`). + +## Common causes + +| Source field | Why it's dropped | +| ------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------- | +| `Product.registeredId` | The field's home moved from `Product` to `Party` in v0.7. Re-attach to `Product.relatedParty[*].party.registeredId` on the appropriate party. | +| `Material.symbol` containing a non-base64 string | v0.7's `Image` type expects a real base64 payload + `mediaType`. Sentinel placeholders are dropped. | +| Whole conformityClaim / scorecard fold-down | The fold from v0.6's typed claim/scorecard hierarchy into a single `Claim` shape is best-effort; review for fidelity. | + +## How to fix + +The shim emits a JSONPath-style `path` on the warning so you know +exactly what was dropped: + +```text +[UPG001] (warning) credentialSubject.registeredId: v0.6 Product.registeredId +has no v0.7 equivalent on Product — the field has moved to Party. The value +was dropped; if required, re-attach it to +Product.relatedParty[*].party.registeredId manually. +``` + +When the loss is acceptable (e.g. you don't need that registeredId +in the upgraded payload), pass `--accept-warnings` to +`dppvalidator migrate` to write the upgraded file anyway. A +sidecar `.warnings.json` always records the full warning +list so you can audit later. + +## See also + +- [Migration guide: 0.6 → 0.7](../guides/migration-0-6-to-0-7.md) — full field rename / shape-change table and the documented limitations. +- [Error Overview](index.md) diff --git a/docs/errors/UPG002.md b/docs/errors/UPG002.md new file mode 100644 index 0000000..60e1a5b --- /dev/null +++ b/docs/errors/UPG002.md @@ -0,0 +1,57 @@ +# UPG002 - Synthesised value during upgrade + +## Description + +The v0.6 → v0.7 compat shim filled a missing v0.7-required field +from a related v0.6 source rather than failing. The upgraded value +is a best-effort guess and should be reviewed before publishing. + +## Category + +Compat shim (Phase 4 of `docs/plans/UNTP_0.7.0_MIGRATION.md`). + +## Severity + +`warning` for envelope-level synthesis; `info` for synthesis where +the missing field is optional in v0.7 (e.g. `Country.countryName`). + +## Common causes + +| What was synthesised | Source | +| ------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | +| Top-level envelope `name` | Copied from `credentialSubject.product.name` (the v0.6 product name). | +| `Material.symbol` upgraded to `Image` | Real base64 bytes preserved; `name="Material symbol"` and `mediaType="image/png"` synthesised. | +| `Country.countryName` not populated | The shim only sets the name when the caller passes a `country_lookup` mapping or the code is in the bundled list. | + +## How to fix + +Most synthesised values are reasonable defaults. To exercise the +review: + +1. **Read the sidecar warnings file** that the `migrate` CLI + produces alongside the upgraded payload. Every UPG002 carries a + JSONPath locator and the synthesised value's source. + +1. **Override synthesised values** before validation: + + ```python + from dppvalidator.compat import upgrade + + upgraded, warnings = upgrade( + payload_v06, + country_lookup={"DE": "Germany", "AU": "Australia"}, + ) + # Replace the auto-synthesised envelope name with a richer one. + upgraded["name"] = "Battery DPP — EV-300 series" + ``` + +1. **Avoid synthesis** by providing the field at v0.6: set + `credentialSubject.product.name` (synthesises envelope `name`) + and a top-level `name` directly to skip the warning entirely. + +## See also + +- [Migration guide: 0.6 → 0.7](../guides/migration-0-6-to-0-7.md) — full warning code reference. +- [`UPG003`](UPG003.md) — country-specific UPG variant. +- [`UPG004`](UPG004.md) — when synthesis is *not* possible. +- [Error Overview](index.md) diff --git a/docs/errors/UPG003.md b/docs/errors/UPG003.md new file mode 100644 index 0000000..21e287c --- /dev/null +++ b/docs/errors/UPG003.md @@ -0,0 +1,52 @@ +# UPG003 - Unmapped country code + +## Description + +The v0.6 → v0.7 compat shim wrapped a scalar country code (e.g. +`"XX"`) into the v0.7 `Country` shape, but the code is not in the +bundled ISO-3166-1 alpha-2 list. The structural rewrite still +happens (`{countryCode: "XX"}`), but the resulting payload will +fail v0.7 model validation downstream. + +## Category + +Compat shim (Phase 4 of `docs/plans/UNTP_0.7.0_MIGRATION.md`). + +## Severity + +`warning` + +## Common causes + +- The source data uses a typo'd country code (e.g. `"GE"` for + Germany when the correct alpha-2 is `"DE"`). +- The source data uses an alpha-3 code (`"DEU"`) where v0.7 + requires alpha-2. +- The source uses a non-ISO regional code (e.g. an internal + manufacturing-region code) that should never have been written + into `originCountry` in the first place. + +## How to fix + +1. **Fix the source data** — the field expects ISO-3166-1 + alpha-2. Examples: `"DE"` for Germany, `"AU"` for Australia, + `"ZM"` for Zambia. + +1. **If the source uses alpha-3**, convert to alpha-2 in your + producer pipeline before invoking the shim. + +1. **If the value is genuinely a non-country region**, move it to a + different field — `Material.originCountry` and + `Product.countryOfProduction` are both ISO-pinned in v0.7. + +The shim still produces the upgraded structure, so callers can +choose to pass `--accept-warnings` to `dppvalidator migrate` if +they intend to fix the country code in a follow-up step. The +upgraded payload will not validate against the v0.7 schema until +the country code is corrected. + +## See also + +- [Migration guide: 0.6 → 0.7](../guides/migration-0-6-to-0-7.md) — Country / Material / Classification shape changes. +- [`VOC001`](VOC001.md) — the runtime validator's equivalent invalid-country error (fires after the shim, when the upgraded payload is validated). +- [Error Overview](index.md) diff --git a/docs/errors/UPG004.md b/docs/errors/UPG004.md new file mode 100644 index 0000000..482b89d --- /dev/null +++ b/docs/errors/UPG004.md @@ -0,0 +1,66 @@ +# UPG004 - Required v0.7 field missing + +## Description + +The v0.6 → v0.7 compat shim encountered a v0.7-required field that +is missing from the source v0.6 payload AND cannot be synthesised +from any related v0.6 field. The caller MUST provide the value +manually before the upgraded payload validates against v0.7. + +## Category + +Compat shim (Phase 4 of `docs/plans/UNTP_0.7.0_MIGRATION.md`). + +## Severity + +`error` — this is the only UPG code that ships at error severity. +The `migrate` CLI refuses to write the upgraded file when any +UPG004 fires, regardless of `--accept-warnings`, because the +result is guaranteed not to validate. + +## Common causes + +| Missing v0.7-required field | Synthesisable from v0.6? | Notes | +| --------------------------- | ----------------------------- | -------------------------------------------------------------------------------------------------- | +| Envelope `name` | only if `Product.name` is set | Otherwise UPG004; provide a top-level `name` manually. | +| Envelope `validFrom` | no | Cannot fabricate a date. | +| `Material.materialType` | no | v0.7 requires a `Classification` object; v0.6 made it optional. Supply a real classification code. | +| `Material.massFraction` | no | v0.7 requires a numeric value in `[0, 1]`; v0.6 made it optional. | + +## How to fix + +The shim emits one UPG004 per missing field with a JSONPath +locator. Patch the source payload at exactly those paths before +re-running the shim: + +```python +from dppvalidator.compat import upgrade + +# 1. First pass: collect the gaps. +upgraded, warnings = upgrade(payload_v06) +gaps = [w for w in warnings if w.code == "UPG004"] +for w in gaps: + print(f"{w.path}: needs manual value — {w.message}") + +# 2. Patch the source. Example: fill missing materialType. +payload_v06["credentialSubject"]["materialsProvenance"][0]["materialType"] = { + "type": ["Classification"], + "schemeId": "https://unstats.un.org/unsd/classifications/Econ/cpc/", + "schemeName": "UN Central Product Classification", + "code": "27310", + "name": "Steel basic shapes", +} + +# 3. Second pass: should produce no UPG004s. +upgraded, warnings = upgrade(payload_v06) +assert not any(w.code == "UPG004" for w in warnings) +``` + +For pipelines, treat UPG004 as a hard signal: failing data should +go back to the producer, not be force-fed into the upgrade. + +## See also + +- [Migration guide: 0.6 → 0.7](../guides/migration-0-6-to-0-7.md) — the limitations table lists every UPG004-emitting field with worked examples. +- [`UPG002`](UPG002.md) — when synthesis *is* possible (warning, not error). +- [Error Overview](index.md) diff --git a/docs/errors/VER001.md b/docs/errors/VER001.md new file mode 100644 index 0000000..2680ebd --- /dev/null +++ b/docs/errors/VER001.md @@ -0,0 +1,57 @@ +# VER001 - UNTP version mismatch + +## Description + +The `ValidationEngine` was constructed with an explicit +`schema_version`, but the payload declares a different UNTP version +in its `$schema` URL or `@context`. The engine refuses to validate +mismatched payloads — silently coercing across versions would mask +genuine errors (e.g. a v0.6 payload incorrectly tagged as v0.7). + +## Category + +UNTP version dispatch (Phase 3.3 of +`docs/plans/UNTP_0.7.0_MIGRATION.md`). + +## Severity + +`error` + +## Common Causes + +- The engine is pinned to one version (e.g. + `ValidationEngine(schema_version="0.7.0")`) and a v0.6.x payload + is fed in (or vice-versa). +- A pipeline upgraded the engine but didn't upgrade the + payload-producing service at the same time. +- A payload's `@context` was hand-edited to reference v0.7 without + the corresponding shape changes. + +## How to fix + +1. **Run the compat shim** if you have v0.6.x payloads and want + v0.7.0 validation: + + ```bash + dppvalidator validate passport.json \ + --upgrade-from 0.6.1 \ + --schema-version 0.7.0 + ``` + +1. **Drop the explicit pin** and let the engine auto-detect: + + ```python + engine = ValidationEngine() # no schema_version= + ``` + +1. **Match the engine pin to the payload version**: + + ```python + engine = ValidationEngine(schema_version="0.6.1") + ``` + +## See also + +- [UNTP DPP versions](../concepts/untp-versions.md) — version handling overview. +- [Migration guide: 0.6 → 0.7](../guides/migration-0-6-to-0-7.md) — the compat shim. +- [Error Overview](index.md) diff --git a/docs/errors/index.md b/docs/errors/index.md index 16b14cb..10b89a7 100644 --- a/docs/errors/index.md +++ b/docs/errors/index.md @@ -5,15 +5,21 @@ produce across its seven-layer validation architecture. ## Error Code Categories -| Prefix | Layer | Description | -| ------ | ---------- | ------------------------------------ | -| SCH | Schema | JSON Schema structural validation | -| PRS | Parsing | Input parsing and file handling | -| MOD | Model | Pydantic model type validation | -| JLD | JSON-LD | Context and term resolution errors | -| SEM | Semantic | Business logic and cross-field rules | -| VOC | Vocabulary | Controlled vocabulary and code lists | -| SIG | Signature | VC signature verification errors | + + +| Prefix | Layer | Description | +| ------ | ------------ | --------------------------------------------------------------------- | +| SCH | Schema | JSON Schema structural validation | +| PRS | Parsing | Input parsing and file handling | +| MOD | Model | Pydantic model type validation | +| JLD | JSON-LD | Context and term resolution errors | +| SEM | Semantic | Business logic and cross-field rules | +| VOC | Vocabulary | Controlled vocabulary and code lists | +| SIG | Signature | VC signature verification errors | +| VER | Version | UNTP version mismatch (engine pin vs declared payload version) | +| UPG | Upgrade shim | v0.6.x → v0.7.0 compat-shim warnings (lossy / synthesised / required) | + + ## Schema Rules (SCH) @@ -60,3 +66,31 @@ produce across its seven-layer validation architecture. | [VOC003](VOC003.md) | Warning | Material code must be valid per UNECE Rec 46 | | [VOC004](VOC004.md) | Warning | HS code must be valid for product category | | [VOC005](VOC005.md) | Error | GTIN must have valid check digit | + +## Version Rules (VER) + + + +| Code | Severity | Description | +| ------------------- | -------- | ------------------------------------------------------------------ | +| [VER001](VER001.md) | Error | UNTP version mismatch — engine `schema_version` vs payload version | + + + +## Upgrade-shim Rules (UPG) + +Emitted by `dppvalidator.compat.upgrade_0_6_to_0_7.upgrade` and the +`dppvalidator migrate` / `dppvalidator validate --upgrade-from` +CLI surfaces. See the [migration guide](../guides/migration-0-6-to-0-7.md) +for the full field rename / shape-change context. + + + +| Code | Severity | Description | +| ------------------- | -------------- | ------------------------------------------------------------------------------------------------- | +| [UPG001](UPG001.md) | Warning / Info | Lossy — a v0.6 field has no v0.7 equivalent and was dropped (e.g. `Product.registeredId`). | +| [UPG002](UPG002.md) | Warning / Info | Synthesised — a v0.7-required field was filled from a related v0.6 source and should be reviewed. | +| [UPG003](UPG003.md) | Warning | Unmapped country code — wrapped structurally but the value will fail v0.7 validation. | +| [UPG004](UPG004.md) | Error | Required v0.7 field is missing from the v0.6 source AND cannot be synthesised; provide manually. | + + diff --git a/docs/faq.md b/docs/faq.md index c26b8db..01fe309 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -84,14 +84,49 @@ Optional extras: Currently supported: -- **UNTP DPP 0.6.1** (default) +- **UNTP DPP 0.6.0** — supported (legacy) +- **UNTP DPP 0.6.1** — default +- **UNTP DPP 0.7.0** — fully supported -Schema version is auto-detected from `@context` or `$schema` fields. You can also specify it explicitly: +Schema version is auto-detected from `@context` or `$schema` fields. +You can also specify it explicitly: ```python -engine = ValidationEngine(schema_version="0.6.1") +engine = ValidationEngine(schema_version="0.6.1") # current default +engine = ValidationEngine(schema_version="0.7.0") # opt in to v0.7 ``` +For the full version-handling story see +[UNTP DPP versions](concepts/untp-versions.md). + +### Can I migrate v0.6.x payloads to v0.7.0? + +Yes — `dppvalidator` ships a compat shim that rewrites v0.6.x +payloads into v0.7.0 shape with structured warnings for anything it +can't fully translate: + +```bash +# Upgrade and write to a new file +dppvalidator migrate passport-v06.json -o passport-v07.json + +# Validate-after-upgrade in one shot +dppvalidator validate passport-v06.json \ + --upgrade-from 0.6.1 \ + --schema-version 0.7.0 +``` + +```python +from dppvalidator.compat import upgrade + +upgraded, warnings = upgrade(payload_v06, country_lookup={"DE": "Germany"}) +``` + +The shim emits four warning codes (`UPG001`–`UPG004`) covering +lossy transformations, synthesised values, unmapped country codes, +and required-field gaps. See the +[migration guide](guides/migration-0-6-to-0-7.md) for the field +rename table and the documented limitations. + ______________________________________________________________________ ## Validation Questions diff --git a/docs/getting-started/quickstart.md b/docs/getting-started/quickstart.md index 8788bf7..8f781ae 100644 --- a/docs/getting-started/quickstart.md +++ b/docs/getting-started/quickstart.md @@ -111,10 +111,53 @@ jsonld = exporter.export(passport) print(jsonld) ``` +## 7. Working with UNTP v0.7.0 + +dppvalidator supports both UNTP DPP **v0.6.x** (the wire shape used +above — `credentialSubject` wraps a `Product`) and **v0.7.0** +(`credentialSubject` IS the `Product` directly, with new required +fields). The engine auto-detects the version from the payload's +`@context` URL. + +### Validate a v0.7.0 payload + +```bash +# Auto-detected from the payload's @context URL. +dppvalidator validate passport-v07.json + +# Pin explicitly when you want VER001 fail-fast on mismatched payloads. +dppvalidator validate passport-v07.json --schema-version 0.7.0 +``` + +### Upgrade a v0.6.x payload to v0.7.0 + +```bash +# Write the upgraded payload to a new file. +dppvalidator migrate passport.json -o passport-v07.json + +# Validate-after-upgrade in one shot. +dppvalidator validate passport.json \ + --upgrade-from 0.6.1 \ + --schema-version 0.7.0 +``` + +The shim emits structured warnings (`UPG001`–`UPG004`) for fields +it can't fully translate. See the +[migration guide](../guides/migration-0-6-to-0-7.md) for the field +rename table and warning codes, and +[UNTP DPP versions](../concepts/untp-versions.md) for the full +version-handling story. + ## Next Steps - [CLI Usage Guide](../guides/cli-usage.md) — Full CLI reference -- [Validation Guide](../guides/validation.md) — Understanding the five validation layers + (validate, migrate, schema, …) +- [Validation Guide](../guides/validation.md) — Understanding the five + validation layers +- [UNTP DPP versions](../concepts/untp-versions.md) — Version detection, + defaults, adding a new version +- [Migration guide: 0.6 → 0.7](../guides/migration-0-6-to-0-7.md) — + The compat shim, field rename table, warning codes - [API Reference](../reference/api/validators.md) — Complete API documentation ## For AI Assistants diff --git a/docs/guides/cli-usage.md b/docs/guides/cli-usage.md index 1dea95d..83d31fe 100644 --- a/docs/guides/cli-usage.md +++ b/docs/guides/cli-usage.md @@ -24,16 +24,23 @@ 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) +- `--schema-version` — Schema version (default: `0.6.1`; 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) -**Examples:** +**v0.6.x examples:** ```bash -# Validate a single file +# Validate a single file (default schema-version is 0.6.1) dppvalidator validate passport.json +# Pin v0.6.1 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 dppvalidator validate passport1.json passport2.json passport3.json @@ -53,6 +60,20 @@ dppvalidator validate "*.json" --format json dppvalidator validate "*.json" --format table ``` +**v0.7.0 examples:** + +```bash +# Pin v0.7.0 explicitly. 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 +# inlined in the validation output. +dppvalidator validate passport-v06.json \ + --upgrade-from 0.6.1 \ + --schema-version 0.7.0 +``` + **Batch Output:** When validating multiple files, the output includes a summary: @@ -105,17 +126,70 @@ dppvalidator schema [options] **Examples:** -``` -# List available versions +```bash +# List every registered version (currently 0.6.0, 0.6.1, 0.7.0). dppvalidator schema list -# Show schema info +# Show schema info for v0.6.1. dppvalidator schema info -v 0.6.1 -# Download schema to local directory -dppvalidator schema download -v 0.6.1 -o ./schemas/ +# Show schema info for v0.7.0. +dppvalidator schema info -v 0.7.0 + +# Download schema to local directory. +dppvalidator schema download -v 0.7.0 -o ./schemas/ +``` + +### migrate + +Upgrade a v0.6.x DPP payload to v0.7.0 shape via the compat shim +(Phase 4). The full reference for the warning codes and field +renames is the [migration guide](migration-0-6-to-0-7.md). + +```text +dppvalidator migrate [options] +``` + +**Arguments:** + +- `input` — Path to v0.6.x JSON file, or `-` for stdin. + +**Options:** + +- `-o, --output` — Output file path (default: stdout) +- `--in-place` — Write the upgraded JSON back to the input path + (overwrites). Mutually exclusive with `-o`. +- `--accept-warnings` — Write the upgraded JSON even when the shim + emits warning- or error-severity events. Without this, the command + exits with code 1 on any non-info warning. +- `--from` — Source UNTP version family (default: `0.6.x`). Pass an + explicit `X.Y.Z` to pin. + +**Examples:** + +```bash +# Default — upgrade to stdout, refuse if warnings fire. +dppvalidator migrate passport.json + +# Write to an explicit output path. +dppvalidator migrate passport.json -o passport-v07.json + +# Overwrite the input. +dppvalidator migrate passport.json --in-place + +# Accept warnings; sidecar passport-v07.json.warnings.json is written +# alongside the upgraded payload. +dppvalidator migrate passport.json -o passport-v07.json --accept-warnings ``` +**Exit codes:** + +- `0` — Upgrade succeeded with no blocking warnings (or + `--accept-warnings` was given). +- `1` — Blocking warnings fired and `--accept-warnings` was not given; + the sidecar warnings file is still written. +- `2` — Error (file not found, invalid JSON, unknown source version). + ## Exit Codes | Code | Meaning | diff --git a/docs/guides/eudpp-export.md b/docs/guides/eudpp-export.md index 055ed3f..b4f8d0d 100644 --- a/docs/guides/eudpp-export.md +++ b/docs/guides/eudpp-export.md @@ -47,6 +47,7 @@ from dppvalidator.exporters import EUDPPJsonLDExporter exporter = EUDPPJsonLDExporter( map_terms=True, # Map UNTP terms to EU DPP (default: True) include_untp_context=False, # Include UNTP context in output (default: False) + schema_version=None, # None = auto-detect from passport class ) # Export methods @@ -55,6 +56,41 @@ jsonld_dict = exporter.export_dict(passport) # Returns dictionary exporter.export_to_file(passport, "output.jsonld") # Writes to file ``` +### Per-version dispatch (Phase 3c) + +The exporter is **version-aware**. UNTP v0.6 and v0.7 use different +source-side spellings (`serialNumber` vs `itemNumber`, +`producedByParty` vs `relatedParty`, …) but most map to the same EU +DPP target URI. The exporter resolves the right column of +`TermMapping` per call: + +- **`schema_version=None` (default)** — auto-detect from the + passport class's module path. A `dppvalidator.models.v0_7.*` + passport gets the v0.7 mapper, a v0.6 passport gets the v0.6 + mapper. One exporter instance can serve mixed inputs. +- **`schema_version="0.6.1"` or `"0.7.0"`** — pin explicitly. + Useful when the passport's source version is known up front + (e.g. CI pipelines), or when you want to *force* the v0.6 mapping + on a v0.7 passport for downstream-compat scenarios. + +```python +from dppvalidator.exporters import EUDPPJsonLDExporter +from dppvalidator.models.v0_7 import DigitalProductPassport + +passport = DigitalProductPassport.model_validate(payload_v07_dict) + +# Auto-detect (recommended). +EUDPPJsonLDExporter().export(passport) + +# Pin explicitly. +EUDPPJsonLDExporter(schema_version="0.7.0").export(passport) +``` + +Terms removed in v0.7 (e.g. `gtin`) are skipped from the v0.7 +mapper's index — they don't appear in the exported JSON-LD even if +the source class somehow carries them. Renamed terms route to the +correct EU DPP URI regardless of which source spelling was used. + ### Convenience Functions For simple use cases: @@ -63,26 +99,48 @@ For simple use cases: from dppvalidator.exporters import ( export_eudpp_jsonld, export_eudpp_jsonld_dict, + get_term_mapping_summary, ) -# String output +# String output (auto-detects version). jsonld = export_eudpp_jsonld(passport) -# Dictionary output -data = export_eudpp_jsonld_dict(passport, map_terms=True) +# Dictionary output, pinned to v0.7. +data = export_eudpp_jsonld_dict(passport, map_terms=True, schema_version="0.7.0") + +# Inspect the mapping table for a given version. +summary_v06 = get_term_mapping_summary("0.6.1") +summary_v07 = get_term_mapping_summary("0.7.0") ``` ## Term Mapping -The exporter maps UNTP terms to EU DPP Core Ontology terms: - -| UNTP Term | EU DPP Term | -| ------------------------ | ----------------- | -| `id` | `uniqueDPPID` | -| `DigitalProductPassport` | `eudpp:DPP` | -| `Product` | `eudpp:Product` | -| `validFrom` | `eudpp:validFrom` | -| `issuer` | `eudpp:hasIssuer` | +The exporter maps UNTP terms to EU DPP Core Ontology terms. The EU +DPP target URI is the same across UNTP versions; only the source-side +spelling shifts between v0.6 and v0.7. + + + +| UNTP v0.6 term | UNTP v0.7 term | EU DPP target URI | +| ------------------------ | ----------------------------------- | ----------------------------- | +| `id` | `id` | `eudpp:uniqueDPPID` | +| `DigitalProductPassport` | `DigitalProductPassport` | `eudpp:DPP` | +| `Product` | `Product` | `eudpp:Product` | +| `serialNumber` | `itemNumber` | `eudpp:uniqueProductID` | +| `producedByParty` | `relatedParty[role="manufacturer"]` | `eudpp:hasManufacturer` | +| `granularityLevel` | `idGranularity` | `eudpp:granularity` | +| `materialsProvenance` | `materialProvenance` | `eudpp:hasMaterialProvenance` | +| `conformityClaim` | `performanceClaim` | `eudpp:hasPerformanceClaim` | +| `gtin` | *removed* | `eudpp:GTIN` (v0.6 only) | +| `validFrom` | `validFrom` | `eudpp:validFrom` | +| `issuer` | `issuer` | `eudpp:hasIssuer` | + + + +The full table lives in +[`vocabularies/ontology.py:TERM_MAPPINGS`](https://github.com/artiso-ai/dppvalidator/blob/main/src/dppvalidator/vocabularies/ontology.py). +The `TERM_REMOVED` sentinel marks v0.6 fields with no v0.7 equivalent +(`gtin` today) — those rows drop out of the v0.7 mapper's index. ### Viewing Term Mappings diff --git a/docs/guides/jsonld.md b/docs/guides/jsonld.md index 4947745..756932f 100644 --- a/docs/guides/jsonld.md +++ b/docs/guides/jsonld.md @@ -22,13 +22,19 @@ print(jsonld_string) ## Output Format -The JSON-LD output includes the W3C VC context: +The JSON-LD output always includes the W3C VC v2 context plus the +UNTP context for the active version. The exporter auto-detects the +source version from the passport's class (Phase 3c — the +`EUDPPJsonLDExporter` reads the module path); `EUDPPJsonLDExporter(schema_version=…)` +pins it explicitly. + +### v0.6.x output ```json { "@context": [ "https://www.w3.org/ns/credentials/v2", - "https://vocabulary.uncefact.org/untp/dpp/0.6.1" + "https://test.uncefact.org/vocabulary/untp/dpp/0.6.1/" ], "type": ["DigitalProductPassport", "VerifiableCredential"], "id": "https://example.com/dpp/001", @@ -39,6 +45,25 @@ The JSON-LD output includes the W3C VC context: } ``` +### v0.7.0 output + +```json +{ + "@context": [ + "https://www.w3.org/ns/credentials/v2", + "https://vocabulary.uncefact.org/untp/0.7.0/context/" + ], + "type": ["DigitalProductPassport", "VerifiableCredential"], + "id": "https://example.com/dpp/001", + "name": "Acme DPP", + "issuer": { + "type": ["CredentialIssuer"], + "id": "did:web:example.com:issuer", + "name": "Acme Corp" + } +} +``` + ## Export Options ### Custom Context diff --git a/docs/guides/migration-0-6-to-0-7.md b/docs/guides/migration-0-6-to-0-7.md new file mode 100644 index 0000000..107d759 --- /dev/null +++ b/docs/guides/migration-0-6-to-0-7.md @@ -0,0 +1,196 @@ + + +# Migrating UNTP DPP payloads from 0.6.x to 0.7.0 + +UNTP DPP **0.7.0** restructures several core fields. dppvalidator +ships a **compat shim** (`dppvalidator.compat.upgrade_0_6_to_0_7`) +that rewrites a 0.6.x payload into 0.7.0 shape and emits structured +warnings for anything it can't fully translate. + +This guide covers: + +1. How to run the shim from the CLI and Python. +1. The complete field rename / shape-change table. +1. The four warning codes and what they mean. +1. The documented limitations (fields that need manual intervention). + +The conceptual overview of UNTP version handling lives in +[UNTP DPP versions](../concepts/untp-versions.md). + +## When you need this + +You need this guide if you: + +- have v0.6.x payloads and want to publish them as v0.7.0, +- have a pipeline that validates v0.6.x today and want to switch the + validation target without re-issuing every passport, or +- are authoring a new v0.7.0 payload and want a quick reference for + what changed. + +If you're authoring fresh v0.7.0 payloads, skip to the +[field rename table](#field-rename-and-shape-change-table) and the +[`untp-versions`](../concepts/untp-versions.md) page. + +## Quick start + +### CLI: rewrite a single file + +```bash +# Default — write to stdout, refuse if any warning fires. +dppvalidator migrate passport.json + +# Write to an explicit output path. +dppvalidator migrate passport.json -o passport-v07.json + +# Overwrite the input. Refuses on warnings unless --accept-warnings. +dppvalidator migrate passport.json --in-place + +# Accept the upgrade even if warnings fire. Produces a sidecar +# passport-v07.json.warnings.json with the full warning list. +dppvalidator migrate passport.json -o passport-v07.json --accept-warnings +``` + +### CLI: validate-then-upgrade in one shot + +```bash +# Run the shim, then validate the result against v0.7.0. +dppvalidator validate passport.json \ + --upgrade-from 0.6.1 \ + --schema-version 0.7.0 +``` + +### Python API + +```python +from dppvalidator.compat import upgrade + +with open("passport.json") as f: + src = json.load(f) + +upgraded, warnings = upgrade(src, country_lookup={"DE": "Germany"}) + +for w in warnings: + print(f"[{w.code}] ({w.severity.value}) {w.path}: {w.message}") +``` + +`upgrade()` is pure — it deep-copies its input, never mutates it. +The optional `country_lookup` populates `Country.countryName` for +ISO-3166-1 alpha-2 codes; codes outside the map fall back to +`{countryCode: …}` only. + +## Field rename and shape-change table + +The shim performs 17 transformation steps in a fixed order. The +most user-visible changes are summarised here; the full step-by-step +specification lives in +[`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). + +### Envelope-level changes + +| v0.6.x | v0.7.0 | Note | +| ---------------------------------------------------------------------- | ------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- | +| `@context: ".../test.uncefact.org/vocabulary/untp/dpp/0.6.x/"` | `@context: ".../vocabulary.uncefact.org/untp/0.7.0/context/"` | The W3C VC v2 context is preserved; only the UNTP entry is rewritten. | +| `name`: optional | `name`: **required** | Synthesised from `credentialSubject.product.name` when missing → `UPG002`. | +| `validFrom`: optional | `validFrom`: **required** | Cannot be synthesised — `UPG004` fires when missing. | +| `credentialSubject` is `ProductPassport` (envelope wrapping `Product`) | `credentialSubject` IS the `Product` directly | All `ProductPassport` siblings (claims, scorecards, materials, due-diligence) are folded onto the new `Product`. | + +### Product-level renames + +| v0.6.x | v0.7.0 | Note | +| -------------------------------------------------- | ----------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- | +| `Product.serialNumber` | `Product.itemNumber` | Pure rename. | +| `ProductPassport.granularityLevel` | `Product.idGranularity` | Carried up to the new `credentialSubject`. | +| `ProductPassport.materialsProvenance[]` | `Product.materialProvenance[]` | Singular noun in v0.7. | +| `ProductPassport.dueDiligenceDeclaration: Link` | `Product.relatedDocument[Link{name: "Due diligence declaration"}]` | Folded into the unified `relatedDocument` array. | +| `Product.furtherInformation: Link[]` | `Product.relatedDocument[]` | Same target; existing `relatedDocument` entries are preserved and the legacy ones are appended. | +| `Product.producedByParty: Party` | `Product.relatedParty[PartyRole{role: "manufacturer", party: }]` | Wrapped as a typed PartyRole. v0.7's `relatedParty` carries every supply-chain role, not just the manufacturer. | +| `Product.productCategory: Classification` (scalar) | `Product.productCategory: Classification[]` | Single-element array even when only one category is set. | +| `Product.registeredId` | _dropped_ — moves to `Party.registeredId` | The shim emits `UPG001`. Re-attach manually to the appropriate `relatedParty[*].party.registeredId` if needed. | + +### Per-claim changes + +| v0.6.x | v0.7.0 | Note | +| --------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | +| `ProductPassport.conformityClaim[]` | `Product.performanceClaim[]` | Plus the three v0.6 scorecards (emissions, circularity, traceability) are also folded here. | +| `Claim.assessmentDate` | `Claim.claimDate` | Field rename. | +| `Claim.assessmentCriteria: Criterion[]` | `Claim.referenceCriteria: list[dict]` | The Criterion type is gone; entries are passed through as free-form objects. | +| `Claim.declaredValue: Metric[]` | `Claim.claimedPerformance: Performance[]` | Each `Metric` is split: `metricName` → `metric.name`, `metricValue` → `measure`, `score` → `score.code`. | +| `Claim.conformityTopic: string` | `Claim.conformityTopic: ConformityTopic[]` | Wrapped as a single-element list with `id`/`name` set to the original string. | +| `Claim.conformityEvidence: SecureLink` | `Claim.evidence: Link[]` | The dedicated `SecureLink` type is gone in v0.7; v0.7 `Link` absorbs `digestMultibase` + `mediaType`. | +| `Claim.referenceStandard: Standard` | `Claim.referenceStandard: list[dict]` | Scalar wrapped to a single-element list. | +| `Claim.referenceRegulation: Regulation` | `Claim.referenceRegulation: list[dict]` | Same wrapping. | +| `Claim.conformance: bool` | _dropped_ | No v0.7 equivalent at the top level. | +| `EmissionsScorecard / CircularityScorecard / TraceabilityPerformance` | folded into `Claim` entries on `performanceClaim[]` with `conformityTopic` set to `Emissions`/`Circularity`/`Traceability` | Numeric scorecard fields become `claimedPerformance[].measure` entries; embedded Link fields become `evidence[]`. | + +### Country / Material / Classification + +| v0.6.x | v0.7.0 | Note | +| -------------------------------------------------------------------------------- | ------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `Product.countryOfProduction: "DE"` | `Product.countryOfProduction: {countryCode: "DE", countryName?: …}` | `countryName` is optional; populate via `country_lookup={"DE": "Germany"}` when invoking the shim. | +| `Material.originCountry: "DE"` | `Material.originCountry: Country` | Same wrapping as above. | +| `Material.symbol: ""` | `Material.symbol: Image{name, imageData, mediaType}` | Sentinel placeholders (`"undefined"`, etc.) are dropped with `UPG001`. Real base64 is upgraded with synthesised `name="Material symbol"` and `mediaType="image/png"` (`UPG002`). | +| `Material.materialType: Classification` (optional) | `Material.materialType: Classification` (**required**) | When missing, the shim emits `UPG004`; provide manually. | +| `Material.massFraction: float` (optional) | `Material.massFraction: float` (**required**) | Same: `UPG004` fires when missing. | +| `Classification.schemeID` | `Classification.schemeId` | camelCase fix; renamed everywhere recursively. | +| Embedded `type: [...]` arrays on `Dimension`, `Characteristics`, `Measure`, etc. | _stripped_ | v0.7 schema dropped these discriminators; the shim removes them. | + +## Warning codes + +Every transformation that can't translate cleanly emits a structured +`UpgradeWarning` with one of four codes: + +| Code | Severity | Meaning | +| -------- | -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `UPG001` | warning | **Lossy** — a v0.6 field has no v0.7 equivalent. Examples: `Product.registeredId` dropped; sentinel `Material.symbol` strings dropped. | +| `UPG002` | info / warning | **Synthesised** — the shim filled a missing field from another source. Examples: `name` synthesised from `Product.name`; `Material.symbol` upgraded to a v0.7 `Image` with synthesised `name` and `mediaType`. `INFO` severity for "country name not synthesised because no `country_lookup`"; `WARNING` for envelope-level synthesis. | +| `UPG003` | warning | **Unmapped country** — a country code is not in the bundled ISO-3166-1 list. The shim still wraps the value structurally, but it will fail v0.7 validation. | +| `UPG004` | error | **Required field missing** — a field that v0.7 requires is missing from the v0.6 source and the shim cannot synthesise it. The caller MUST provide the value before the upgraded payload validates. Examples: `validFrom`, `Material.materialType`, `Material.massFraction`. | + +The `migrate` CLI refuses to write the output file when any +warning- or error-severity event fires; pass `--accept-warnings` to +override. INFO-severity events never block. A sidecar +`.warnings.json` is always written alongside any blocking-warning +output (whether or not the main file is written) so the caller has a +machine-readable record. + +## Documented limitations + +These v0.6.x payloads cannot fully round-trip through the shim; each +case is intentional and surfaces as one of the warnings above: + +| Limitation | Code | What to do | +| ----------------------------------------------------------------- | ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `Product.registeredId` set | `UPG001` | Move the value to `Product.relatedParty[*].party.registeredId` on the appropriate party (typically the manufacturer). The shim drops it because the field's home moved from Product to Party. | +| `Material.materialType` missing | `UPG004` | v0.7 makes `materialType` required. Supply a `Classification` (e.g. `{schemeId: ".../cpc/", schemeName: "UN CPC", code: "12345", name: "..."}`). | +| `Material.massFraction` missing | `UPG004` | v0.7 makes `massFraction` required. Supply a numeric value in `[0, 1]`. | +| Top-level `name` missing AND `Product.name` missing | `UPG004` | Provide a top-level `name` manually. The shim only synthesises from the inner `Product.name`. | +| Top-level `validFrom` missing | `UPG004` | Add a real `validFrom` timestamp; the shim cannot fabricate a date. | +| `Material.symbol` is a non-base64 string | `UPG001` | Provide a v0.7 `Image` object with `name`, `imageData` (base64), and `mediaType`. The shim drops anything that doesn't decode as base64. | +| Country code not in bundled ISO-3166-1 list | `UPG003` | The shim wraps it structurally as `{countryCode: "XX"}`, but v0.7 model validation rejects non-ISO codes. Fix the source data. | +| Bare `ProductPassport` payload (no W3C VC envelope) | _shim no-ops_ | The shim is defined for full DPP envelopes; bare `ProductPassport` payloads were never valid v0.6 wire format. Wrap in a VC envelope first. | +| Domain-specific industry extensions (e.g. textiles plugin fields) | _passes through_ | Extension fields under `Characteristics` flow through (`extra="allow"`) but their internal shape isn't migrated. Verify against your plugin's v0.7 documentation. | + +## Round-trip verification + +The repository includes +[`tests/integration/test_compat_roundtrip.py`](https://github.com/artiso-ai/dppvalidator/blob/main/tests/integration/test_compat_roundtrip.py), +which upgrades every enveloped 0.6.x fixture under +`tests/fixtures/valid/` and asserts the result either validates +cleanly against the v0.7 model or surfaces an explanatory warning. +A silent failure (no warning, no validation pass) trips the assertion +immediately. + +If you build a CI check around the shim, mirror that contract: +**fail loudly when an upgrade produces an invalid payload AND no +warning explains why.** + +## See also + +- [UNTP DPP versions](../concepts/untp-versions.md) — overall version + handling, default version, detection rules. +- [Five-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. +- [Migration plan archive](https://github.com/artiso-ai/dppvalidator/blob/main/docs/plans/UNTP_0.7.0_MIGRATION.md) — Phase 4 + (compat shim) is the canonical engineering record. diff --git a/docs/guides/plugins.md b/docs/guides/plugins.md index 383a0a0..792856e 100644 --- a/docs/guides/plugins.md +++ b/docs/guides/plugins.md @@ -106,7 +106,92 @@ print("Validators:", registry.list_validators()) print("Exporters:", registry.list_exporters()) ``` +## Writing a version-aware rule + +UNTP DPP introduced a wire-shape change in **v0.7.0**: the +`ProductPassport` envelope is gone, so `credentialSubject` is now a +`Product` directly (no inner `.product` attribute). A rule written +against the v0.6 shape will silently no-op on v0.7 payloads — and +vice-versa. Plugins that target a specific version should declare +which shape they target. + +The contract: set +`applies_to_versions: tuple[str, ...] = ("0.7.0",)` on the rule +class, and **duck on attribute presence** so the rule no-ops +cleanly when the wrong-version passport flows through. + +```python +# brand_name_v07.py +from typing import Any, Literal + + +class BrandNameRuleV07: + """v0.7-shape brand-name check. + + Reads ``passport.credential_subject.name`` directly (v0.7 has + Product as credentialSubject) and accepts a brandOwner + relatedParty as an alternative attribution. + """ + + rule_id: str = "SEM_BRAND_V07" + description: str = "Products should attribute brand identity (v0.7)." + severity: Literal["error", "warning", "info"] = "warning" + + # Tells the engine's per-version rule dispatch which version this + # rule targets. If omitted, the rule runs for every version. + applies_to_versions: tuple[str, ...] = ("0.7.0",) + + def check(self, passport: Any) -> list[tuple[str, str]]: + cs = getattr(passport, "credential_subject", None) + if cs is None or hasattr(cs, "product"): + # v0.6 shape (or no cs) — skip cleanly. + return [] + + if getattr(cs, "name", None): + return [] + + # Fall back to a brandOwner relatedParty. + for entry in getattr(cs, "related_party", None) or []: + role = getattr(entry, "role", None) + if getattr(role, "value", role) == "brandOwner": + return [] + + return [ + ( + "$.credentialSubject", + "Product should attribute brand identity via Product.name " + "or a relatedParty with role 'brandOwner'.", + ) + ] +``` + +Register it as a separate entry point alongside the v0.6 sibling: + +```toml +[project.entry-points."dppvalidator.validators"] +brand_name = "my_package.brand_name:BrandNameRule" # v0.6 +brand_name_v07 = "my_package.brand_name_v07:BrandNameRuleV07" # v0.7 +``` + +Both rules co-exist in the registry; the engine picks the one +whose `applies_to_versions` matches the detected payload version. +A worked v0.6/v0.7 sibling pair lives in the +[`dppvalidator-example-plugin`](https://github.com/artiso-ai/dppvalidator/tree/main/examples/dppvalidator_example_plugin) +under `examples/`, with integration tests in +[`tests/integration/test_example_plugin.py`](https://github.com/artiso-ai/dppvalidator/blob/main/tests/integration/test_example_plugin.py). + +### Public-API stability for plugins + +The plugin's import path +(`from dppvalidator.models.passport import DigitalProductPassport`) +resolves to the **v0.6** model via the top-level shim, even after +the Phase 3 model relocation. v0.7 plugins should import from +`dppvalidator.models.v0_7.envelope` explicitly. The CI test +`tests/integration/test_example_plugin.py` pins this contract. + ## Next Steps - [API Reference](../reference/api/plugins.md) — Plugin Registry API +- [UNTP DPP versions](../concepts/untp-versions.md) — version detection + and the coexistence matrix. - [Validation Guide](validation.md) — Understanding validation layers diff --git a/docs/guides/validation.md b/docs/guides/validation.md index 27bf603..54922f8 100644 --- a/docs/guides/validation.md +++ b/docs/guides/validation.md @@ -26,19 +26,54 @@ else: The engine automatically detects the schema version from your document: ```python -# Auto-detection is the default -engine = ValidationEngine() # schema_version="auto" +# Auto-detection — engine reads $schema / @context / type from the payload. +engine = ValidationEngine() -# Or specify explicitly for deterministic behavior +# 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. +engine = ValidationEngine(schema_version="0.7.0") ``` Detection checks (in order): 1. `$schema` URL pattern -1. `@context` URLs +1. `@context` URLs (`https://test.uncefact.org/vocabulary/untp/dpp/0.6.x/` + for v0.6.x; `https://vocabulary.uncefact.org/untp/0.7.0/context/` for + v0.7.0) 1. `type` array presence -1. Fallback to default version +1. Fallback to `dppvalidator.schemas.registry.DEFAULT_SCHEMA_VERSION` + +The full version-handling story is in +[UNTP DPP versions](../concepts/untp-versions.md). + +### Validating v0.6.x payloads against v0.7.0 + +If you have v0.6.x payloads but want to validate them against the +v0.7.0 schema (because your downstream consumers have moved to +v0.7.0), use the **compat shim** via `--upgrade-from`: + +```bash +# Run the v0.6 → v0.7 shim, then validate against v0.7.0. +dppvalidator validate passport.json \ + --upgrade-from 0.6.1 \ + --schema-version 0.7.0 +``` + +```python +from dppvalidator.compat import upgrade +from dppvalidator import ValidationEngine + +upgraded, warnings = upgrade(payload_v06) +result = ValidationEngine(schema_version="0.7.0").validate(upgraded) +``` + +The shim emits structured warnings (`UPG001`–`UPG004`) for fields it +can't fully translate. See the +[migration guide](migration-0-6-to-0-7.md) for the warning codes, +field rename table, and known limitations. ## Validation Layers diff --git a/docs/index.md b/docs/index.md index 4ce8c32..ea3f080 100644 --- a/docs/index.md +++ b/docs/index.md @@ -18,9 +18,18 @@ ______________________________________________________________________ ## Features - :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** — Built-in support for UNTP DPP 0.6.1 +- :octicons-package-16: **UNTP DPP Schema Support** — Both **0.6.x** + (default) and **0.7.0** 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** — + `dppvalidator migrate` upgrades v0.6.x payloads to v0.7.0 shape + with structured warnings. See the + [migration guide](guides/migration-0-6-to-0-7.md). - :octicons-rocket-16: **High Performance** — 80,000+ validations per second -- :octicons-plug-16: **Plugin System** — Extensible with custom validators and exporters +- :octicons-plug-16: **Plugin System** — Extensible with custom + validators and exporters; version-aware rules with + `applies_to_versions` opt-in. - :octicons-file-code-16: **JSON-LD Export** — W3C Verifiable Credentials compliant output - :octicons-terminal-16: **CLI & API** — Use from command line or programmatically @@ -66,15 +75,24 @@ else: ### Command Line -``` -# Validate a DPP JSON file +```bash +# Validate a DPP JSON file (auto-detects version from the payload) dppvalidator validate passport.json +# Pin a specific UNTP version +dppvalidator validate passport.json --schema-version 0.7.0 + +# Upgrade a v0.6.x payload to v0.7.0 shape +dppvalidator migrate passport.json -o passport-v07.json + +# Validate-after-upgrade in one shot +dppvalidator validate passport.json --upgrade-from 0.6.1 --schema-version 0.7.0 + # Export to JSON-LD dppvalidator export passport.json --format jsonld -# Show schema information -dppvalidator schema --version 0.6.1 +# List every registered UNTP version +dppvalidator schema list ``` ## Documentation @@ -83,6 +101,10 @@ dppvalidator schema --version 0.6.1 - [Quick Start Tutorial](getting-started/quickstart.md) — Get started in 5 minutes - [CLI Usage](guides/cli-usage.md) — Command line reference - [Validation Guide](guides/validation.md) — Understanding validation layers +- [UNTP DPP versions](concepts/untp-versions.md) — Version handling, + detection, defaults, adding a new version +- [Migration guide: 0.6.x → 0.7.0](guides/migration-0-6-to-0-7.md) — + The compat shim, field rename table, warning codes - [API Reference](reference/api/validators.md) — Full API documentation ## For AI Assistants diff --git a/docs/reference/cli.md b/docs/reference/cli.md index 90ea77a..5f99768 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -37,13 +37,18 @@ Supports multiple files and glob patterns for batch validation. **Options:** -| Option | Default | Description | -| ------------------ | ------- | --------------------------------- | -| `-s, --strict` | false | Enable strict validation | -| `-f, --format` | text | Output format (text, json, table) | -| `--schema-version` | 0.6.1 | Schema version | -| `--fail-fast` | false | Stop on first error | -| `--max-errors` | 100 | Maximum errors to report | + + +| Option | Default | Description | +| ------------------ | ------- | ---------------------------------------------------------------------------- | +| `-s, --strict` | false | Enable strict validation | +| `-f, --format` | text | Output format (text, json, table) | +| `--schema-version` | 0.6.1 | Schema version (one of `0.6.0`, `0.6.1`, `0.7.0`) | +| `--upgrade-from` | none | Run the v0.6 → v0.7 compat shim before validating; accepts `0.6.0` / `0.6.1` | +| `--fail-fast` | false | Stop on first error | +| `--max-errors` | 100 | Maximum errors to report | + + ### export @@ -64,7 +69,7 @@ dppvalidator export INPUT [OPTIONS] Display schema information. -``` +```text dppvalidator schema [OPTIONS] ``` @@ -75,18 +80,58 @@ dppvalidator schema [OPTIONS] | `--version` | Schema version to display | | `--list` | List available versions | +### migrate + +Upgrade a v0.6.x DPP payload to v0.7.0 shape via the compat shim. + +```text +dppvalidator migrate INPUT [OPTIONS] +``` + +**Arguments:** + + + +| Argument | Description | +| -------- | ------------------------------------------ | +| INPUT | Path to v0.6.x JSON file, or `-` for stdin | + + + +**Options:** + + + +| Option | Default | Description | +| ------------------- | ------- | --------------------------------------------------------------------------------------------------------------------- | +| `-o, --output` | stdout | Output file path | +| `--in-place` | false | Write the upgraded JSON back to the input path. Mutually exclusive with `-o`. | +| `--accept-warnings` | false | Write the upgraded JSON even when the shim emits warning- or error-severity events. INFO-severity events never block. | +| `--from` | 0.6.x | Source UNTP version family. Pass `0.6.0` / `0.6.1` to pin. | + + + +A sidecar `.warnings.json` is always written alongside the +upgraded payload when blocking warnings fire (whether the main +output is written or not), so callers always have a +machine-readable record of every transformation. + ## Exit Codes -| Code | Meaning | -| ---- | ----------------- | -| 0 | Success / Valid | -| 1 | Validation failed | -| 2 | Error | + + +| Code | Meaning | +| ---- | ----------------------------------------------------------------------- | +| 0 | Success / Valid (validate); upgrade with no blocking warnings (migrate) | +| 1 | Validation failed (validate); blocking warnings (migrate) | +| 2 | Error (file not found, invalid JSON, unknown version) | + + ## Examples ```bash -# Validate a single file +# Validate a single file (auto-detects the UNTP version) dppvalidator validate passport.json # Validate multiple files @@ -98,12 +143,23 @@ dppvalidator validate "data/passports/*.json" # Batch validate with strict mode and JSON output dppvalidator validate "data/*.json" --strict --format json -# Validate with table output (summary view) -dppvalidator validate "*.json" --format table +# Pin to v0.7.0 (fail-fast on payloads that declare a different version) +dppvalidator validate passport.json --schema-version 0.7.0 + +# Run the v0.6 → v0.7 shim, then validate as v0.7.0 +dppvalidator validate passport-v06.json \ + --upgrade-from 0.6.1 \ + --schema-version 0.7.0 # Export to JSON-LD dppvalidator export passport.json -f jsonld -o output.jsonld -# List schema versions +# List schema versions (currently 0.6.0, 0.6.1, 0.7.0) dppvalidator schema --list + +# Upgrade a v0.6.x payload to v0.7.0 shape +dppvalidator migrate passport.json -o passport-v07.json + +# Upgrade in place, accepting warnings +dppvalidator migrate passport.json --in-place --accept-warnings ``` diff --git a/examples/dppvalidator_example_plugin/README.md b/examples/dppvalidator_example_plugin/README.md index 586bf6f..24af07c 100644 --- a/examples/dppvalidator_example_plugin/README.md +++ b/examples/dppvalidator_example_plugin/README.md @@ -28,10 +28,15 @@ pip install -e . ### Custom Validators -This plugin provides two example validators: +This plugin provides three example validators: -- **BrandNameRule** (`SEM_BRAND`) - Validates that products have a brand name -- **MinMaterialsRule** (`SEM_MINMAT`) - Warns if products have fewer than 2 materials declared +- **BrandNameRule** (`SEM_BRAND`) - Validates that v0.6 products have a + brand name. Reads `passport.credential_subject.product.name`. +- **BrandNameRuleV07** (`SEM_BRAND_V07`) - v0.7 variant; reads + `passport.credential_subject.name` directly and accepts a + `brandOwner` `relatedParty` as an alternative attribution. +- **MinMaterialsRule** (`SEM_MINMAT`) - Warns if v0.6 products have + fewer than 2 materials declared. ### Custom Exporter @@ -79,6 +84,49 @@ See the source code in `src/` for examples of how to: 1. Create custom exporters 1. Register plugins via entry points +## Writing a version-aware rule + +UNTP DPP introduced a wire-shape change in v0.7.0: the +`ProductPassport` envelope is gone, so `credentialSubject` is now a +`Product` directly (no inner `.product` attribute). A rule written +against the v0.6 shape will silently no-op on v0.7 payloads — and +vice-versa. Phase 4 of the migration (`compat/upgrade_0_6_to_0_7.py`) +helps callers upgrade payloads, but plugins still need to declare +which shape they target. + +This plugin demonstrates the version-aware-rule pattern with two +sibling rules: + +- `BrandNameRule` (in `validators.py`) — v0.6 shape; reads + `passport.credential_subject.product.name`. +- `BrandNameRuleV07` (in `brand_name_v07.py`) — v0.7 shape; reads + `passport.credential_subject.name` directly and also accepts a + `relatedParty` with `role="brandOwner"` as a brand attribution. + +The v0.7 rule advertises an `applies_to_versions = ("0.7.0",)` class +attribute. The engine's per-version rule dispatch consults that +attribute to decide whether to run the rule for a given payload's +detected version. As an extra safety net, `BrandNameRuleV07.check()` +**ducks on attribute presence**: if a v0.6 passport ever flows +through (e.g. a caller bypassed dispatch), the rule no-ops cleanly +because `credential_subject.product` exists — it never raises. + +### Recipe + +To author a version-aware rule for a UNTP version `X.Y.Z`: + +1. Create `your_rule_X_Y.py` in your plugin package. +1. Import the version-specific model: + `from dppvalidator.models.vX_Y.envelope import DigitalProductPassport`. +1. Set `applies_to_versions = ("X.Y.Z",)` on the rule class. +1. In `check()`, validate the shape with `hasattr` / `getattr` on the + passport before reading version-specific attributes; return `[]` + when the shape doesn't match. This makes the rule co-exist + gracefully with rules for other versions in the same registry. +1. Register the rule in `pyproject.toml` under + `[project.entry-points."dppvalidator.validators"]` with a unique + name (e.g. `brand_name_v07`). + ## License MIT diff --git a/examples/dppvalidator_example_plugin/pyproject.toml b/examples/dppvalidator_example_plugin/pyproject.toml index b82aead..a193d8e 100644 --- a/examples/dppvalidator_example_plugin/pyproject.toml +++ b/examples/dppvalidator_example_plugin/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "dppvalidator-example-plugin" -version = "0.1.0" +version = "0.2.0" description = "Example plugin for dppvalidator demonstrating custom validators and exporters" readme = "README.md" requires-python = ">=3.10" @@ -25,6 +25,7 @@ dependencies = ["dppvalidator>=0.3.0"] [project.entry-points."dppvalidator.validators"] brand_name = "dppvalidator_example_plugin.validators:BrandNameRule" +brand_name_v07 = "dppvalidator_example_plugin.brand_name_v07:BrandNameRuleV07" min_materials = "dppvalidator_example_plugin.validators:MinMaterialsRule" [project.entry-points."dppvalidator.exporters"] diff --git a/examples/dppvalidator_example_plugin/src/dppvalidator_example_plugin/__init__.py b/examples/dppvalidator_example_plugin/src/dppvalidator_example_plugin/__init__.py index 526dd9b..301433a 100644 --- a/examples/dppvalidator_example_plugin/src/dppvalidator_example_plugin/__init__.py +++ b/examples/dppvalidator_example_plugin/src/dppvalidator_example_plugin/__init__.py @@ -1,12 +1,21 @@ -"""Example plugin for dppvalidator demonstrating custom validators and exporters.""" +"""Example plugin for dppvalidator demonstrating custom validators and exporters. +Phase 6 of ``docs/plans/UNTP_0.7.0_MIGRATION.md`` adds +:class:`BrandNameRuleV07` — a v0.7-shape companion to the v0.6 +:class:`BrandNameRule` — so plugin authors see how to write a +version-aware rule that targets the new namespace without touching +the v0.6 surface. +""" + +from dppvalidator_example_plugin.brand_name_v07 import BrandNameRuleV07 from dppvalidator_example_plugin.exporters import CSVExporter from dppvalidator_example_plugin.validators import BrandNameRule, MinMaterialsRule -__version__ = "0.1.0" +__version__ = "0.2.0" __all__ = [ "BrandNameRule", + "BrandNameRuleV07", "MinMaterialsRule", "CSVExporter", ] diff --git a/examples/dppvalidator_example_plugin/src/dppvalidator_example_plugin/brand_name_v07.py b/examples/dppvalidator_example_plugin/src/dppvalidator_example_plugin/brand_name_v07.py new file mode 100644 index 0000000..e210273 --- /dev/null +++ b/examples/dppvalidator_example_plugin/src/dppvalidator_example_plugin/brand_name_v07.py @@ -0,0 +1,131 @@ +"""v0.7-shape variant of BrandNameRule for the example plugin. + +Phase 6 of ``docs/plans/UNTP_0.7.0_MIGRATION.md``: ships a sibling of +:class:`dppvalidator_example_plugin.validators.BrandNameRule` written +against the **v0.7 wire shape**, so plugin authors browsing the +example see how to author a version-aware rule that targets the new +namespace. + +Key differences from the v0.6 rule: + +- v0.7 ``credentialSubject`` *is* the Product directly — there is no + ``ProductPassport`` envelope, so the brand-name check reads + ``passport.credential_subject.name`` rather than + ``passport.credential_subject.product.name``. +- v0.7 introduces a ``relatedParty: list[PartyRole]`` field that + carries typed (role, party) pairs. A "brandOwner" entry on + ``relatedParty`` is the canonical way to attribute brand identity + in v0.7; this rule promotes that pattern by *upgrading* the + violation when neither a name nor a brandOwner party is present. + +The rule duck-types on the passport's module path so it can co-exist +with the v0.6 ``BrandNameRule`` in the same registry — the engine +runs both, and each silently no-ops when handed a passport from the +"wrong" version. +""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, Literal + +if TYPE_CHECKING: + from dppvalidator.models.v0_7.envelope import DigitalProductPassport + + +class BrandNameRuleV07: + """SEM_BRAND_V07: v0.7 variant of the brand-name semantic check. + + Targets the v0.7 ``credentialSubject`` (now a ``Product`` directly). + Emits a *warning* when ``Product.name`` is empty AND no + ``relatedParty`` carries the ``brandOwner`` role — both of those + are reasonable ways to attribute brand identity in v0.7. + + Example: + >>> rule = BrandNameRuleV07() + >>> violations = rule.check(passport) # passport: v0.7 DPP + >>> for path, message in violations: + ... print(f"{path}: {message}") + """ + + rule_id: str = "SEM_BRAND_V07" + description: str = ( + "Products should attribute brand identity via Product.name or a " + "relatedParty with role 'brandOwner' (UNTP v0.7 shape)" + ) + severity: Literal["error", "warning", "info"] = "warning" + + # The set of UNTP versions this rule targets. The engine's per-version + # rule dispatch (``ALL_RULES_BY_VERSION``) reads this attribute to + # decide whether to run the rule for a given payload version. + applies_to_versions: tuple[str, ...] = ("0.7.0",) + + def check( + self, + passport: DigitalProductPassport, + ) -> list[tuple[str, str]]: + """Check that brand identity is attributable. + + Args: + passport: A v0.7 ``DigitalProductPassport`` instance. + + Returns: + List of ``(path, message)`` tuples — empty when the passport + attributes brand identity through any supported channel. + + Notes: + Ducks on attribute presence rather than ``isinstance``: v0.6 + passports lack ``credential_subject.related_party`` and + ``credential_subject.name`` lives one level deeper, so the + rule no-ops cleanly when given a v0.6 passport. + """ + violations: list[tuple[str, str]] = [] + + product = self._extract_v07_product(passport) + if product is None: + # Wrong version shape — silently skip. + return violations + + has_name = bool(getattr(product, "name", None)) + has_brand_owner = self._has_brand_owner_party(product) + + if not has_name and not has_brand_owner: + violations.append( + ( + "$.credentialSubject", + "Product should attribute brand identity via Product.name " + "or a relatedParty with role 'brandOwner'.", + ) + ) + + return violations + + @staticmethod + def _extract_v07_product(passport: Any) -> Any | None: + """Return the v0.7 Product, or ``None`` if the shape doesn't match. + + v0.7's credentialSubject *is* a Product (no envelope). v0.6 + wraps it: ``credentialSubject.product`` — the presence of an + outer ``.product`` attribute is the cleanest discriminator. + """ + cs = getattr(passport, "credential_subject", None) + if cs is None: + return None + # v0.6 wraps the Product under credential_subject.product; + # v0.7 has the Product directly. + if hasattr(cs, "product"): + return None + return cs + + @staticmethod + def _has_brand_owner_party(product: Any) -> bool: + """True when a ``relatedParty`` carries the ``brandOwner`` role.""" + related = getattr(product, "related_party", None) or [] + for entry in related: + role = getattr(entry, "role", None) + # ``role`` is an Enum on the model but its value is a string; + # the engine's ``use_enum_values=True`` config means we may + # see either form depending on how the passport was loaded. + value = getattr(role, "value", role) + if value == "brandOwner": + return True + return False diff --git a/examples/dppvalidator_example_plugin/src/dppvalidator_example_plugin/validators.py b/examples/dppvalidator_example_plugin/src/dppvalidator_example_plugin/validators.py index fc5ca10..74ab7eb 100644 --- a/examples/dppvalidator_example_plugin/src/dppvalidator_example_plugin/validators.py +++ b/examples/dppvalidator_example_plugin/src/dppvalidator_example_plugin/validators.py @@ -2,27 +2,65 @@ These validators implement the SemanticRule protocol and are automatically discovered via Python entry points when the plugin is installed. + +Both rules below target the **v0.6.x** wire shape — `credentialSubject` +is a `ProductPassport` envelope wrapping `Product`, and material +provenance lives at `credentialSubject.materialsProvenance` (plural, +v0.6 spelling). They declare ``applies_to_versions`` so the engine's +per-version plugin dispatch (Phase 6 of +``docs/plans/UNTP_0.7.0_MIGRATION.md``) skips them when a v0.7 payload +flows through. As a defensive belt-and-braces, ``check()`` also ducks +on attribute presence so the rules no-op cleanly even if dispatch +ever misroutes a v0.7 passport into them. + +For the v0.7 sibling, see ``brand_name_v07.py``. """ from __future__ import annotations -from typing import TYPE_CHECKING, Literal +from typing import TYPE_CHECKING, Any, Literal if TYPE_CHECKING: from dppvalidator.models.passport import DigitalProductPassport +# Both rules target v0.6.x. The engine's per-version dispatch reads +# this tuple to skip non-matching payloads; the duck-typing in +# ``check()`` is a second line of defence in case dispatch is bypassed +# (e.g. by a caller invoking ``PluginRegistry.run_all_validators`` +# directly without ``schema_version=``). +_V06_VERSIONS: tuple[str, ...] = ("0.6.0", "0.6.1") + + +def _is_v06_shape(passport: Any) -> bool: + """True when ``passport`` is a v0.6-shaped envelope. + + v0.6 wraps the product under ``credential_subject.product``; v0.7 + has the Product directly as ``credential_subject``. Presence of + the inner ``.product`` attribute is the cleanest discriminator. + """ + cs = getattr(passport, "credential_subject", None) + if cs is None: + return False + return hasattr(cs, "product") + class BrandNameRule: - """SEM_BRAND: Products should have a brand name for traceability. + """SEM_BRAND: v0.6 products should have a brand name for traceability. - This example validator checks if products have a brand name specified, - which is important for consumer identification and traceability. + This example validator checks if products have a brand name + specified — important for consumer identification and traceability. + Targets the v0.6.x wire shape (`credentialSubject.product.name`). + For v0.7, see :class:`BrandNameRuleV07` in ``brand_name_v07.py``. """ rule_id: str = "SEM_BRAND" description: str = "Products should have a brand name" severity: Literal["error", "warning", "info"] = "warning" + # Engine's per-version dispatch consults this; v0.7 payloads are + # routed past us to ``BrandNameRuleV07`` instead. + applies_to_versions: tuple[str, ...] = _V06_VERSIONS + def check(self, passport: DigitalProductPassport) -> list[tuple[str, str]]: """Check if product has a brand name. @@ -34,7 +72,12 @@ def check(self, passport: DigitalProductPassport) -> list[tuple[str, str]]: """ violations: list[tuple[str, str]] = [] - if not passport.credential_subject: + if not _is_v06_shape(passport): + # Not a v0.6 passport — defensive no-op (the engine's + # per-version dispatch should already have skipped us). + return violations + + if passport.credential_subject is None: return violations product = passport.credential_subject.product @@ -50,16 +93,22 @@ def check(self, passport: DigitalProductPassport) -> list[tuple[str, str]]: class MinMaterialsRule: - """SEM_MINMAT: Products should declare at least 2 materials. + """SEM_MINMAT: v0.6 products should declare at least 2 materials. This example validator encourages comprehensive material disclosure - by checking if at least 2 materials are declared. + by checking if at least 2 materials are declared. Targets the v0.6.x + `credentialSubject.materialsProvenance` array (plural, v0.6 spelling). + A v0.7 sibling would read `credentialSubject.materialProvenance` + (singular) — see ``brand_name_v07.py`` for the version-aware-rule + pattern. """ rule_id: str = "SEM_MINMAT" description: str = "Products should declare at least 2 materials" severity: Literal["error", "warning", "info"] = "info" + applies_to_versions: tuple[str, ...] = _V06_VERSIONS + def check(self, passport: DigitalProductPassport) -> list[tuple[str, str]]: """Check if product has minimum materials declared. @@ -71,7 +120,10 @@ def check(self, passport: DigitalProductPassport) -> list[tuple[str, str]]: """ violations: list[tuple[str, str]] = [] - if not passport.credential_subject: + if not _is_v06_shape(passport): + return violations + + if passport.credential_subject is None: return violations materials = passport.credential_subject.materials_provenance diff --git a/mkdocs.yml b/mkdocs.yml index 1cce2e8..bccfd65 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -4,9 +4,17 @@ site_url: https://artiso-ai.github.io/dppvalidator repo_url: https://github.com/artiso-ai/dppvalidator repo_name: artiso-ai/dppvalidator edit_uri: edit/main/docs/ + +# Exclude engineering artefacts from the docs build. The migration plans +# under docs/plans/ are change-tracking documents (implementation logs, +# audit notes, tracking issues); they're not part of the user-facing +# docs site and contain cross-tree relative links to src/ and tests/ +# that mkdocs (rightly) flags as broken in strict mode. +exclude_docs: | + plans/ copyright: > - Copyright © 2026 artiso-ai - - Change cookie settings + Copyright © 2026 artiso-ai - Change cookie + settings theme: name: material @@ -46,7 +54,7 @@ plugins: - mkdocstrings: handlers: python: - paths: [src] + paths: [ src ] options: show_source: true show_root_heading: true @@ -56,17 +64,27 @@ plugins: markdown_extensions: - pymdownx.highlight: anchor_linenums: true + # Force a non-None ``title`` for every code block. Without + # this, ``pymdownx.highlight`` passes ``filename=None`` to + # pygments' ``HtmlFormatter``, which crashes with + # ``AttributeError: 'NoneType' object has no attribute 'replace'`` + # under pygments >=2.20.0 (the latter tightened None handling + # in ``html.escape(self._decodeifneeded(...))``). Enabling + # ``auto_title`` makes pymdownx fill ``title`` from the lexer + # name when the markdown didn't set one. Tracked upstream as a + # pymdown-extensions / pygments compatibility issue. + auto_title: true - pymdownx.superfences: custom_fences: - name: mermaid class: mermaid - format: !!python/name:pymdownx.superfences.fence_code_format + format: !!python/name:pymdownx.superfences.fence_code_format "" - pymdownx.tabbed: alternate_style: true - pymdownx.snippets - pymdownx.emoji: - emoji_index: !!python/name:material.extensions.emoji.twemoji - emoji_generator: !!python/name:material.extensions.emoji.to_svg + emoji_index: !!python/name:material.extensions.emoji.twemoji "" + emoji_generator: !!python/name:material.extensions.emoji.to_svg "" - admonition - pymdownx.details - attr_list @@ -102,8 +120,9 @@ extra: We use cookies to recognize your repeated visits and preferences, as well as to measure the effectiveness of our documentation and whether users find what they're searching for. With your consent, you're helping us to - make our documentation better. For more information, see our - Privacy Policy. + make our documentation better. For more information, see our Privacy + Policy. cookies: analytics: name: Google Analytics @@ -134,6 +153,7 @@ nav: - Use Cases: guides/use-cases.md - JSON-LD Export: guides/jsonld.md - EU DPP Export: guides/eudpp-export.md + - Migration 0.6 → 0.7: guides/migration-0-6-to-0-7.md - Plugin Development: guides/plugins.md - Reference: - CLI Reference: reference/cli.md @@ -143,6 +163,7 @@ nav: - Exporters: reference/api/exporters.md - Plugins: reference/api/plugins.md - Concepts: + - UNTP DPP versions: concepts/untp-versions.md - UNTP DPP Schema: concepts/untp-schema.md - Five-Layer Validation: concepts/validation-layers.md - EU DPP Ontology Alignment: concepts/eudpp-ontology-alignment.md @@ -215,6 +236,7 @@ nav: - MDL081 - Invalid Emission Value: errors/MDL081.md - MDL090 - Invalid Facility: errors/MDL090.md - MDL091 - Invalid Facility Location: errors/MDL091.md + - MDL098 - No Model Registered for Schema Version: errors/MDL098.md - MDL099 - Unknown Model Error: errors/MDL099.md - JSON-LD Errors: - JLD001 - Missing Context: errors/JLD001.md @@ -248,5 +270,12 @@ nav: - TXT003 - Missing Microplastic Data: errors/TXT003.md - TXT004 - Missing Durability Info: errors/TXT004.md - TXT005 - Missing Care Instructions: errors/TXT005.md + - Version Errors: + - VER001 - UNTP Version Mismatch: errors/VER001.md + - Upgrade-shim Errors: + - UPG001 - Lossy Upgrade Transformation: errors/UPG001.md + - UPG002 - Synthesised Value During Upgrade: errors/UPG002.md + - UPG003 - Unmapped Country Code: errors/UPG003.md + - UPG004 - Required v0.7 Field Missing: errors/UPG004.md - FAQ: faq.md - Changelog: changelog.md diff --git a/pyproject.toml b/pyproject.toml index a2f50c4..fcc5057 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "dppvalidator" -version = "0.3.2" +version = "0.4.0" 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" @@ -50,8 +50,12 @@ dependencies = [ "httpx>=0.28.0", "jsonschema>=4.23.0", "pyld>=2.0.4", - "cryptography>=43.0.0", - "PyJWT>=2.9.0", + # cryptography: floor lifted to >=46.0.7 to pull in fixes for + # CVE-2026-26007 (46.0.5), CVE-2026-34073 (46.0.6), + # CVE-2026-39892 (46.0.7). + "cryptography>=46.0.7", + # PyJWT: floor lifted to >=2.12.0 for CVE-2026-32597. + "PyJWT>=2.12.0", "base58>=2.1.0", ] @@ -89,11 +93,14 @@ include = [ "src/dppvalidator/**/*.xsd", "src/dppvalidator/**/*.yaml", ] +exclude = [ + "scripts/**", +] [dependency-groups] dev = [ - # Testing - "pytest>=8.0.0", + # Testing — pytest >=9.0.3 closes CVE-2025-71176. + "pytest>=9.0.3", "pytest-cov>=4.1.0", "pytest-asyncio>=0.24.0", "hypothesis>=6.100.0", @@ -105,6 +112,11 @@ dev = [ # Security & License Scanning "pip-audit>=2.7.0", "pip-licenses>=5.0.0", + # ``pip-audit`` pulls in ``pip-api`` which transitively pins + # ``pip`` itself. Without this floor, the resolver picks pip 25.3 + # (CVE-2026-1703 + CVE-2026-6357), which then surfaces during + # ``pip-audit`` runs against the lockfile-exported requirements. + "pip>=26.1", # SBOM Generation "cyclonedx-bom>=7.0.0", # Optional extras (include all for dev) diff --git a/scripts/check_error_docs.py b/scripts/check_error_docs.py index 9dc7f99..43ecc2c 100644 --- a/scripts/check_error_docs.py +++ b/scripts/check_error_docs.py @@ -24,8 +24,27 @@ # Error code pattern: 2-3 uppercase letters followed by 3 digits ERROR_CODE_PATTERN = re.compile(r"\b([A-Z]{2,3}\d{3})\b") -# Error code prefixes to check -KNOWN_PREFIXES = {"SCH", "PRS", "MDL", "SEM", "JLD", "VOC", "SIG", "CQ", "TXT"} +# Error code prefixes to check. +# +# - SCH/PRS/MDL/SEM/JLD/VOC: validator layers. +# - SIG: VC signature verification (Phase 5 of pre-0.4.0 work). +# - CQ: CIRPASS-2 conformance. +# - TXT: textile-pilot rules. +# - VER: UNTP version-mismatch errors (Phase 3.3 of UNTP 0.7.0 plan). +# - UPG: 0.6 → 0.7 compat-shim warnings (Phase 4 of UNTP 0.7.0 plan). +KNOWN_PREFIXES = { + "SCH", + "PRS", + "MDL", + "SEM", + "JLD", + "VOC", + "SIG", + "CQ", + "TXT", + "VER", + "UPG", +} def find_error_codes_in_source() -> set[str]: diff --git a/scripts/fetch_dpp_samples.py b/scripts/fetch_dpp_samples.py new file mode 100644 index 0000000..1ba7ac3 --- /dev/null +++ b/scripts/fetch_dpp_samples.py @@ -0,0 +1,412 @@ +#!/usr/bin/env python3 +""" +Script to fetch and evaluate Digital Product Passport samples for testing. + +Downloads DPP samples from various sources and evaluates their structure +to determine if they are valid candidates for testing the dppvalidator library. +""" + +from __future__ import annotations + +import hashlib +import json +import re +from dataclasses import dataclass, field +from pathlib import Path +from typing import Any +from urllib.parse import urlparse + +import httpx + +# Raw URLs provided by the user (with duplicates and fragments) +RAW_URLS = """ +https://untp-verifiable-credentials.s3.amazonaws.com/bc075c5f-2304-4b3f-bb24-46d9fa9a8e60.json +https://spherity.github.io/schemas/testing/breathable-t-shirt.json +https://raw.githubusercontent.com/eclipse-tractusx/sldt-semantic-models/main/io.catenax.battery.battery_pass/6.0.0/gen/BatteryPass.json +https://zenodo.org/records/15279026/preview/untp-dpp-instance-0.5.0-computer.json.txt +https://test.uncefact.org/vocabulary/untp/dia/DigitalIdentityAnchor-instance-0.6.1.json +https://opensource.unicc.org/phila/spec-untp/-/raw/main/website/samples/untp-digital-facility-record-v0.3.9.json +https://opensource.unicc.org/11dot2/spec-untp/-/raw/main/website/samples/untp-digital-product-passport-v0.3.10.json +https://test.uncefact.org/vocabulary/untp/dpp/untp-dpp-instance-0.6.0.json +https://batterypass.github.io/BatteryPassDataModel/BatteryPass/io.BatteryPass.GeneralProductInformation/1.2.0/gen/GeneralProductInformation-payload.json +https://batterypass.github.io/BatteryPassDataModel//BatteryPass/io.BatteryPass.CarbonFootprint/1.2.0/gen/CarbonFootprintForBatteries-payload.json +https://raw.githubusercontent.com/batterypass/BatteryPassDataModel/refs/heads/main/BatteryPass/io.BatteryPass.GeneralProductInformation/1.2.0/gen/GeneralProductInformation-ld.json +https://raw.githubusercontent.com/batterypass/BatteryPassDataModel/refs/heads/main/BatteryPass/io.BatteryPass.CarbonFootprint/1.2.0/gen/CarbonFootprintForBatteries-ld.json +https://raw.githubusercontent.com/batterypass/BatteryPassDataModel/refs/heads/main/BatteryPass/io.BatteryPass.Circularity/1.2.0/gen/Circularity-ld.json +https://raw.githubusercontent.com/batterypass/BatteryPassDataModel/refs/heads/main/BatteryPass/io.BatteryPass.MaterialComposition/1.2.0/gen/MaterialComposition-ld.json +https://nfc-forum.org/ndpp/long-dpp-example.json +https://batterypass.github.io/BatteryPassDataModel//BatteryPass/io.BatteryPass.MaterialComposition/1.2.0/gen/MaterialComposition-payload.json +""" + + +@dataclass +class Evaluation: + """Evaluation results for a DPP sample.""" + + is_json_ld: bool = False + has_context: bool = False + has_type: bool = False + detected_type: str | list[str] | None = None + is_verifiable_credential: bool = False + is_dpp_like: bool = False + is_battery_pass: bool = False + is_schema: bool = False + has_product_info: bool = False + top_level_keys: list[str] = field(default_factory=list) + recommendation: str = "unknown" + notes: list[str] = field(default_factory=list) + + +@dataclass +class DPPSample: + """Represents a downloaded DPP sample with metadata.""" + + url: str + filename: str + content: dict[str, Any] | None = None + error: str | None = None + content_hash: str | None = None + evaluation: Evaluation = field(default_factory=Evaluation) + + @property + def is_valid(self) -> bool: + return self.content is not None and self.error is None + + +def clean_and_dedupe_urls(raw_urls: str) -> list[str]: + """Parse, clean (remove fragments), and deduplicate URLs.""" + urls = [] + for line in raw_urls.strip().split("\n"): + line = line.strip() + if not line: + continue + # Handle malformed URLs (two URLs concatenated) + if "https://" in line[8:]: + parts = re.split(r"(?=https://)", line) + for part in parts: + if part: + urls.append(part) + else: + urls.append(line) + + # Remove fragments and deduplicate + cleaned = [] + seen = set() + for url in urls: + parsed = urlparse(url) + # Rebuild URL without fragment, normalize double slashes in path + clean_path = re.sub(r"//+", "/", parsed.path) + clean_url = f"{parsed.scheme}://{parsed.netloc}{clean_path}" + if parsed.query: + clean_url += f"?{parsed.query}" + + if clean_url not in seen: + seen.add(clean_url) + cleaned.append(clean_url) + + return cleaned + + +def derive_filename(url: str) -> str: + """Derive a meaningful filename from the URL.""" + parsed = urlparse(url) + path = parsed.path + + # Get the base filename + basename = Path(path).name + if not basename or basename == "": + basename = "unknown" + + # Remove .txt extension if present (some JSON files have .json.txt) + if basename.endswith(".json.txt"): + basename = basename[:-4] + elif not basename.endswith(".json"): + basename += ".json" + + # Add source prefix for clarity + domain = parsed.netloc.replace(".", "_") + if "github" in domain or "githubusercontent" in domain: + # Extract repo/org info + parts = path.split("/") + if len(parts) >= 2: + org_repo = "_".join(p for p in parts[1:3] if p) + return f"{org_repo}_{basename}" + + return f"{domain}_{basename}" + + +def evaluate_dpp_structure(data: dict[str, Any]) -> Evaluation: + """Evaluate if the JSON structure appears to be a valid DPP candidate.""" + evaluation = Evaluation() + + if not isinstance(data, dict): + evaluation.notes.append("Not a JSON object") + evaluation.recommendation = "reject" + return evaluation + + keys = list(data.keys()) + evaluation.top_level_keys = keys[:20] # Limit for readability + + # Check for JSON-LD markers + if "@context" in data: + evaluation.has_context = True + evaluation.is_json_ld = True + + if "@type" in data or "type" in data: + evaluation.has_type = True + type_val = data.get("@type") or data.get("type") + if isinstance(type_val, list): + evaluation.detected_type = type_val + else: + evaluation.detected_type = str(type_val) if type_val else None + + # Check for Verifiable Credential structure + vc_indicators = ["credentialSubject", "issuer", "issuanceDate", "proof"] + vc_count = sum(1 for k in vc_indicators if k in data) + if vc_count >= 2: + evaluation.is_verifiable_credential = True + + # Check for DPP-like structure + dpp_indicators = [ + "product", + "productIdentifier", + "productName", + "manufacturer", + "manufacturerInformation", + "circularity", + "sustainability", + "materials", + "materialComposition", + "carbonFootprint", + ] + dpp_count = sum(1 for k in dpp_indicators if k in data or k in str(data).lower()) + if dpp_count >= 2: + evaluation.is_dpp_like = True + + # Check for Battery Pass specific fields + battery_indicators = [ + "batteryId", + "batteryModel", + "batteryCategory", + "batteryWeight", + "ratedCapacity", + "batteryStatus", + "stateOfHealth", + "stateOfCharge", + ] + battery_count = sum(1 for k in battery_indicators if k in str(data)) + if battery_count >= 2: + evaluation.is_battery_pass = True + + # Check if it's a schema rather than an instance + schema_indicators = ["$schema", "$id", "properties", "definitions", "required"] + schema_count = sum(1 for k in schema_indicators if k in data) + if schema_count >= 3: + evaluation.is_schema = True + evaluation.notes.append("Appears to be a JSON Schema, not a DPP instance") + + # Check for product information + if "product" in data or "credentialSubject" in data: + evaluation.has_product_info = True + + # Determine recommendation + if evaluation.is_schema: + evaluation.recommendation = "schema_only" + elif evaluation.is_verifiable_credential and evaluation.is_dpp_like: + evaluation.recommendation = "excellent" + evaluation.notes.append("Verifiable Credential with DPP structure") + elif evaluation.is_verifiable_credential: + evaluation.recommendation = "good" + evaluation.notes.append("Verifiable Credential structure") + elif evaluation.is_battery_pass: + evaluation.recommendation = "good" + evaluation.notes.append("Battery Pass data") + elif evaluation.is_dpp_like: + evaluation.recommendation = "moderate" + evaluation.notes.append("DPP-like structure without VC wrapper") + elif evaluation.is_json_ld: + evaluation.recommendation = "maybe" + evaluation.notes.append("JSON-LD but structure unclear") + else: + evaluation.recommendation = "review" + evaluation.notes.append("Structure needs manual review") + + return evaluation + + +def fetch_sample(client: httpx.Client, url: str) -> DPPSample: + """Fetch a single DPP sample from URL.""" + filename = derive_filename(url) + sample = DPPSample(url=url, filename=filename) + + try: + response = client.get(url, follow_redirects=True, timeout=30.0) + response.raise_for_status() + + content_text = response.text + sample.content_hash = hashlib.sha256(content_text.encode()).hexdigest()[:16] + + # Try to parse as JSON + sample.content = json.loads(content_text) + sample.evaluation = evaluate_dpp_structure(sample.content) + + except httpx.HTTPStatusError as e: + sample.error = f"HTTP {e.response.status_code}: {e.response.reason_phrase}" + except httpx.RequestError as e: + sample.error = f"Request error: {e}" + except json.JSONDecodeError as e: + sample.error = f"Invalid JSON: {e}" + except Exception as e: + sample.error = f"Unexpected error: {e}" + + return sample + + +def fetch_all_samples(urls: list[str]) -> list[DPPSample]: + """Fetch all DPP samples from the provided URLs.""" + samples = [] + + with httpx.Client( + headers={ + "User-Agent": "dppvalidator-sample-fetcher/1.0", + "Accept": "application/json, application/ld+json, text/plain", + } + ) as client: + for i, url in enumerate(urls, 1): + print(f"[{i}/{len(urls)}] Fetching: {url[:80]}...") + sample = fetch_sample(client, url) + samples.append(sample) + + if sample.error: + print(f" ❌ Error: {sample.error}") + else: + print(f" ✓ {sample.filename} -> {sample.evaluation.recommendation}") + + return samples + + +def generate_report(samples: list[DPPSample]) -> str: + """Generate a markdown report of the evaluation results.""" + lines = ["# DPP Sample Evaluation Report\n"] + + # Summary + total = len(samples) + success = sum(1 for s in samples if s.is_valid) + failed = total - success + + lines.append("## Summary\n") + lines.append(f"- **Total URLs**: {total}") + lines.append(f"- **Successfully fetched**: {success}") + lines.append(f"- **Failed**: {failed}\n") + + # Group by recommendation + by_rec: dict[str, list[DPPSample]] = {} + for s in samples: + rec = s.evaluation.recommendation if s.is_valid else "failed" + by_rec.setdefault(rec, []).append(s) + + lines.append("## By Recommendation\n") + order = ["excellent", "good", "moderate", "maybe", "schema_only", "review", "failed"] + for rec in order: + if rec in by_rec: + lines.append(f"### {rec.upper()} ({len(by_rec[rec])})\n") + for s in by_rec[rec]: + if s.is_valid: + notes = ", ".join(s.evaluation.notes) + lines.append(f"- `{s.filename}`: {notes}") + else: + lines.append(f"- `{s.filename}`: {s.error}") + lines.append(f" - URL: {s.url}") + lines.append("") + + # Detailed evaluation + lines.append("## Detailed Evaluation\n") + for s in samples: + lines.append(f"### {s.filename}\n") + lines.append(f"- **URL**: {s.url}") + if s.error: + lines.append(f"- **Error**: {s.error}") + else: + lines.append(f"- **Hash**: {s.content_hash}") + lines.append(f"- **Recommendation**: {s.evaluation.recommendation}") + lines.append(f"- **Is JSON-LD**: {s.evaluation.is_json_ld}") + lines.append(f"- **Is VC**: {s.evaluation.is_verifiable_credential}") + lines.append(f"- **Is DPP-like**: {s.evaluation.is_dpp_like}") + lines.append(f"- **Is Battery Pass**: {s.evaluation.is_battery_pass}") + lines.append(f"- **Is Schema**: {s.evaluation.is_schema}") + lines.append(f"- **Type**: {s.evaluation.detected_type}") + lines.append(f"- **Top keys**: {', '.join(s.evaluation.top_level_keys[:10])}") + if s.evaluation.notes: + lines.append(f"- **Notes**: {'; '.join(s.evaluation.notes)}") + lines.append("") + + return "\n".join(lines) + + +def save_samples(samples: list[DPPSample], output_dir: Path) -> None: + """Save valid samples to the output directory.""" + output_dir.mkdir(parents=True, exist_ok=True) + + # Group by hash to avoid duplicates + by_hash: dict[str, DPPSample] = {} + for s in samples: + if s.is_valid and s.content_hash and s.content_hash not in by_hash: + by_hash[s.content_hash] = s + + for sample in by_hash.values(): + filepath = output_dir / sample.filename + # Avoid overwriting - add hash suffix if exists + if filepath.exists(): + stem = filepath.stem + filepath = output_dir / f"{stem}_{sample.content_hash}.json" + + with open(filepath, "w") as f: + json.dump(sample.content, f, indent=2) + print(f"Saved: {filepath.name}") + + +def main() -> None: + """Main entry point.""" + print("=" * 60) + print("DPP Sample Fetcher and Evaluator") + print("=" * 60) + + # Parse and dedupe URLs + urls = clean_and_dedupe_urls(RAW_URLS) + print(f"\nFound {len(urls)} unique URLs to process.\n") + + # Fetch all samples + samples = fetch_all_samples(urls) + + # Generate and save report + report = generate_report(samples) + report_path = Path(__file__).parent.parent / "tests" / "fixtures" / "samples_report.md" + report_path.parent.mkdir(parents=True, exist_ok=True) + with open(report_path, "w") as f: + f.write(report) + print(f"\nReport saved to: {report_path}") + + # Save samples to fixtures directory + samples_dir = Path(__file__).parent.parent / "tests" / "fixtures" / "samples" + print(f"\nSaving samples to: {samples_dir}") + save_samples(samples, samples_dir) + + # Print summary + print("\n" + "=" * 60) + print("SUMMARY") + print("=" * 60) + excellent = [s for s in samples if s.evaluation.recommendation == "excellent"] + good = [s for s in samples if s.evaluation.recommendation == "good"] + moderate = [s for s in samples if s.evaluation.recommendation == "moderate"] + schema_only = [s for s in samples if s.evaluation.recommendation == "schema_only"] + failed = [s for s in samples if not s.is_valid] + + print(f" Excellent candidates: {len(excellent)}") + print(f" Good candidates: {len(good)}") + print(f" Moderate candidates: {len(moderate)}") + print(f" Schema only: {len(schema_only)}") + print(f" Failed to fetch: {len(failed)}") + + +if __name__ == "__main__": + main() diff --git a/scripts/smoke_test.py b/scripts/smoke_test.py new file mode 100644 index 0000000..cd2390d --- /dev/null +++ b/scripts/smoke_test.py @@ -0,0 +1,895 @@ +#!/usr/bin/env python3 +"""Functional smoke test for ``dppvalidator``. + +Exercises every user-facing surface end-to-end against the bundled +test fixtures — CLI commands, Python APIs, plugin discovery, the +compat shim, and the EU DPP exporter. Standalone: no pytest, no +test framework, just subprocess + the project's installed Python. +Returns exit code ``0`` when every check passes, non-zero otherwise. + +Run from the repo root:: + + .venv/bin/python scripts/smoke_test.py + +Or, against the conda env:: + + DPP_BIN=$HOME/miniforge3/envs/dppvalidator/bin/dppvalidator \\ + PY_BIN=$HOME/miniforge3/envs/dppvalidator/bin/python \\ + python scripts/smoke_test.py + +Environment overrides: + +- ``DPP_BIN`` — path to the ``dppvalidator`` executable + (default: ``.venv/bin/dppvalidator``). +- ``PY_BIN`` — path to a Python that has ``dppvalidator`` installed + (default: ``.venv/bin/python``). + +Each section's checks are documented inline. The test is +intentionally **non-destructive**: it never writes outside +``tempfile.TemporaryDirectory()`` or pollutes the working tree. +""" + +from __future__ import annotations + +import json +import os +import re +import subprocess +import sys +import tempfile +import textwrap +import urllib.error +import urllib.request +from dataclasses import dataclass, field +from pathlib import Path + +REPO = Path(__file__).resolve().parent.parent +FIXTURES = REPO / "tests" / "fixtures" +DPP_BIN = os.environ.get("DPP_BIN", str(REPO / ".venv" / "bin" / "dppvalidator")) +PY_BIN = os.environ.get("PY_BIN", str(REPO / ".venv" / "bin" / "python")) + +# ANSI colours — disabled when output is piped (CI logs). +_ISATTY = sys.stdout.isatty() +GREEN = "\033[32m" if _ISATTY else "" +RED = "\033[31m" if _ISATTY else "" +YELLOW = "\033[33m" if _ISATTY else "" +BLUE = "\033[34m" if _ISATTY else "" +DIM = "\033[2m" if _ISATTY else "" +RESET = "\033[0m" if _ISATTY else "" + + +# --------------------------------------------------------------------------- +# Tiny assertion harness — no test framework, just structured output +# --------------------------------------------------------------------------- + + +@dataclass +class Smoke: + passed: int = 0 + failed: int = 0 + skipped: int = 0 + failures: list[tuple[str, str]] = field(default_factory=list) + + def section(self, label: str) -> None: + print(f"\n{BLUE}═══ {label} {'═' * (60 - len(label))}{RESET}") + + def ok(self, label: str) -> None: + self.passed += 1 + print(f" {GREEN}✓{RESET} {label}") + + def fail(self, label: str, detail: str = "") -> None: + self.failed += 1 + print(f" {RED}✗{RESET} {label}") + if detail: + for line in detail.rstrip().splitlines()[:6]: + print(f" {DIM}{line}{RESET}") + self.failures.append((label, detail)) + + def skip(self, label: str, reason: str) -> None: + self.skipped += 1 + print(f" {YELLOW}~{RESET} {label} {DIM}({reason}){RESET}") + + def assert_(self, label: str, condition: bool, detail: str = "") -> None: + if condition: + self.ok(label) + else: + self.fail(label, detail) + + def cli( + self, + args: list[str], + *, + stdin: str | None = None, + ) -> subprocess.CompletedProcess[str]: + """Invoke the CLI; never raises. Returns the completed process.""" + return subprocess.run( + [DPP_BIN, *args], + capture_output=True, + text=True, + input=stdin, + cwd=str(REPO), + check=False, + ) + + def py(self, code: str) -> subprocess.CompletedProcess[str]: + """Run a Python snippet under PY_BIN. Never raises.""" + return subprocess.run( + [PY_BIN, "-c", textwrap.dedent(code)], + capture_output=True, + text=True, + cwd=str(REPO), + check=False, + ) + + def report(self) -> int: + total = self.passed + self.failed + self.skipped + print() + print("─" * 64) + if self.failed == 0: + colour = GREEN + verdict = "PASSED" + else: + colour = RED + verdict = "FAILED" + print( + f"{colour}{verdict}{RESET}: " + f"{self.passed} passed, {self.failed} failed, {self.skipped} skipped " + f"({total} total)" + ) + if self.failures: + print() + print(f"{RED}Failures:{RESET}") + for label, detail in self.failures: + print(f" • {label}") + if detail: + for line in detail.rstrip().splitlines()[:3]: + print(f" {DIM}{line}{RESET}") + return 0 if self.failed == 0 else 1 + + +# --------------------------------------------------------------------------- +# Pre-flight +# --------------------------------------------------------------------------- + + +def _check_environment(s: Smoke) -> bool: + """Bail out early if the binaries we expect aren't there.""" + s.section("0. Pre-flight") + dpp_path = Path(DPP_BIN) + py_path = Path(PY_BIN) + + s.assert_( + f"DPP_BIN exists ({DPP_BIN})", + dpp_path.is_file() and os.access(dpp_path, os.X_OK), + f"set DPP_BIN to a working dppvalidator executable (got {DPP_BIN})", + ) + s.assert_( + f"PY_BIN exists ({PY_BIN})", + py_path.is_file() and os.access(py_path, os.X_OK), + f"set PY_BIN to a Python that has dppvalidator installed (got {PY_BIN})", + ) + s.assert_( + "fixtures directory present", + (FIXTURES / "valid").is_dir(), + f"missing: {FIXTURES / 'valid'}", + ) + return s.failed == 0 + + +# --------------------------------------------------------------------------- +# Sections — each tests one cohesive surface +# --------------------------------------------------------------------------- + + +def section_cli_sanity(s: Smoke) -> None: + s.section("1. CLI sanity") + r = s.cli(["--version"]) + s.assert_("--version exits 0", r.returncode == 0, r.stderr) + s.assert_( + "--version output mentions 'dppvalidator'", + "dppvalidator" in r.stdout.lower(), + r.stdout, + ) + + r = s.cli(["--help"]) + s.assert_("--help exits 0", r.returncode == 0, r.stderr) + for cmd in ("validate", "migrate", "export", "schema"): + s.assert_( + f"--help advertises subcommand '{cmd}'", + cmd in r.stdout, + r.stdout[:200], + ) + + +def section_schema_management(s: Smoke) -> None: + s.section("2. Schema management") + r = s.cli(["schema", "list"]) + s.assert_("schema list exits 0", r.returncode == 0, r.stderr) + for version in ("0.6.0", "0.6.1", "0.7.0"): + s.assert_( + f"schema list lists {version}", + version in r.stdout, + r.stdout, + ) + + r = s.cli(["schema", "info", "-v", "0.7.0"]) + s.assert_("schema info -v 0.7.0 exits 0", r.returncode == 0, r.stderr) + + +def section_validate_auto_detect(s: Smoke) -> None: + s.section("3. validate — auto-detect") + v06 = FIXTURES / "valid" / "untp-dpp-instance-0.6.1.json" + v07 = FIXTURES / "valid" / "untp-dpp-instance-0.7.0.json" + + r = s.cli(["validate", str(v06)]) + s.assert_("v0.6 fixture validates clean", r.returncode == 0, r.stdout) + s.assert_( + "v0.6 reports 'Schema version: 0.6.1'", + "Schema version: 0.6.1" in r.stdout, + r.stdout, + ) + + r = s.cli(["validate", str(v07)]) + s.assert_("v0.7 fixture validates clean", r.returncode == 0, r.stdout) + s.assert_( + "v0.7 reports 'Schema version: 0.7.0'", + "Schema version: 0.7.0" in r.stdout, + r.stdout, + ) + # Regressions from earlier rounds — must not resurface. + s.assert_( + "v0.7 emits no PLG001 (per-version plugin filter)", "PLG001" not in r.stdout, r.stdout[:300] + ) + s.assert_( + "v0.7 emits no VOC003 on UN CPC fixture (scheme-aware)", + "VOC003" not in r.stdout, + r.stdout[:300], + ) + s.assert_( + "v0.7 emits no VOC004 on UN CPC fixture (scheme-aware)", + "VOC004" not in r.stdout, + r.stdout[:300], + ) + + # Sibling v0.7 fixtures exercise different ``credentialSubject`` shapes + # (battery + cathode have richer attestation/material structures) — they + # should also auto-detect cleanly with no PLG001 or VOC003/VOC004 noise. + for sibling in ("untp-dpp-battery-instance-0.7.0.json", "untp-dpp-cathode-instance-0.7.0.json"): + f = FIXTURES / "valid" / sibling + if not f.is_file(): + s.skip(f"{sibling} round-trip", "fixture not vendored") + continue + r = s.cli(["validate", str(f)]) + s.assert_(f"{sibling} validates clean", r.returncode == 0, r.stdout) + s.assert_( + f"{sibling} auto-detects as 0.7.0", + "Schema version: 0.7.0" in r.stdout, + r.stdout[:200], + ) + s.assert_( + f"{sibling} emits no PLG001/VOC003/VOC004", + not any(c in r.stdout for c in ("PLG001", "VOC003", "VOC004")), + r.stdout[:300], + ) + + +def section_validate_explicit_pin(s: Smoke) -> None: + s.section("4. validate — explicit pin + VER001") + v06 = FIXTURES / "valid" / "untp-dpp-instance-0.6.1.json" + + r = s.cli(["validate", str(v06), "--schema-version", "0.6.1"]) + s.assert_("v0.6 + matching pin exits 0", r.returncode == 0, r.stdout) + + r = s.cli(["validate", str(v06), "--schema-version", "0.7.0"]) + s.assert_("VER001 fail-fast on mismatched pin", r.returncode != 0, r.stdout) + s.assert_("VER001 code surfaces in output", "VER001" in r.stdout, r.stdout) + + +def section_validate_invalid(s: Smoke) -> None: + s.section("5. validate — invalid fixtures (parametrized)") + invalid_root = FIXTURES / "invalid" + if not invalid_root.is_dir(): + s.skip("invalid-fixture sweep", f"missing {invalid_root}") + return + + # Anchor case — pin the SCH001/MDL001 contract on one well-known fixture. + anchor = invalid_root / "missing_issuer.json" + if anchor.is_file(): + r = s.cli(["validate", str(anchor)]) + s.assert_("missing_issuer.json exits non-zero", r.returncode != 0, r.stdout) + s.assert_( + "missing_issuer.json reports SCH001 or MDL001", + "SCH001" in r.stdout or "MDL001" in r.stdout, + r.stdout, + ) + + # Sweep every other v0.6 invalid fixture — each must (a) exit non-zero and + # (b) emit at least one structured error code. We don't assert which code, + # since that's the job of the unit suite — we're verifying the CLI's + # error-surfacing contract holds across every failure shape. + v06_invalids = sorted(p for p in invalid_root.glob("*.json") if p.name != "missing_issuer.json") + for f in v06_invalids: + r = s.cli(["validate", str(f)]) + s.assert_( + f"{f.name} exits non-zero", + r.returncode != 0, + f"exit={r.returncode} stdout={r.stdout[:200]}", + ) + # An error code is any AAA000-style token the CLI surfaces. + has_code = bool(re.search(r"\b[A-Z]{2,4}\d{3}\b", r.stdout)) + s.assert_(f"{f.name} surfaces a structured error code", has_code, r.stdout[:200]) + + # Sweep all v0.7 invalid fixtures (subdir). + v07_dir = invalid_root / "0.7.0" + if v07_dir.is_dir(): + v07_invalids = sorted(v07_dir.glob("*.json")) + for f in v07_invalids: + r = s.cli(["validate", str(f), "--schema-version", "0.7.0"]) + s.assert_( + f"v0.7/{f.name} exits non-zero", + r.returncode != 0, + f"exit={r.returncode} stdout={r.stdout[:200]}", + ) + + +def section_output_formats(s: Smoke) -> None: + s.section("6. validate — output formats") + v07 = FIXTURES / "valid" / "untp-dpp-instance-0.7.0.json" + + # JSON format must be parseable. + r = s.cli(["validate", str(v07), "--format", "json"]) + s.assert_("--format json exits 0", r.returncode == 0, r.stderr) + try: + parsed = json.loads(r.stdout) + s.assert_("--format json output is valid JSON", True) + s.assert_( + "--format json includes 'valid' key", + "valid" in parsed, + json.dumps(parsed, indent=2)[:200], + ) + except json.JSONDecodeError as exc: + s.fail("--format json output is valid JSON", f"{exc}: {r.stdout[:200]}") + + # Table format runs without crashing (Rich required for nicest output but + # the fallback table formatter ships in-tree). + r = s.cli(["validate", str(v07), "--format", "table"]) + s.assert_("--format table exits 0", r.returncode == 0, r.stderr) + + +def section_stdin(s: Smoke) -> None: + s.section("7. validate — stdin input") + payload = (FIXTURES / "valid" / "minimal_dpp.json").read_text(encoding="utf-8") + r = s.cli(["validate", "-"], stdin=payload) + # Either valid (0) or invalid (1) is fine — what matters is that the CLI + # accepted stdin without crashing (which would be exit code 2). + s.assert_( + "stdin input accepted (exit 0 or 1)", + r.returncode in (0, 1), + f"exit={r.returncode} stderr={r.stderr[:200]}", + ) + s.assert_( + "stdin output contains 'Schema version'", + "Schema version" in r.stdout, + r.stdout[:200], + ) + + +def section_migrate(s: Smoke) -> None: + s.section("8. migrate — v0.6 → v0.7") + v06 = FIXTURES / "valid" / "untp-dpp-instance-0.6.1.json" + + with tempfile.TemporaryDirectory() as tmpdir: + tmp = Path(tmpdir) + + # Happy path: --accept-warnings writes both upgraded.json + sidecar. + out = tmp / "upgraded.json" + r = s.cli(["migrate", str(v06), "-o", str(out), "--accept-warnings"]) + s.assert_("migrate --accept-warnings exits 0", r.returncode == 0, r.stderr) + s.assert_("migrate produced upgraded.json", out.is_file()) + sidecar = tmp / "upgraded.json.warnings.json" + s.assert_("migrate produced sidecar .warnings.json", sidecar.is_file()) + + if out.is_file(): + upgraded = json.loads(out.read_text(encoding="utf-8")) + ctxs = upgraded.get("@context") or [] + s.assert_( + "upgraded payload @context references v0.7", + any(isinstance(c, str) and "vocabulary.uncefact.org/untp/0.7" in c for c in ctxs), + json.dumps(ctxs)[:200], + ) + + if sidecar.is_file(): + warnings_doc = json.loads(sidecar.read_text(encoding="utf-8")) + s.assert_( + "sidecar records the v0.6 → v0.7 transition", + "0.6" in str(warnings_doc.get("schema_version_from", "")) + and "0.7" in str(warnings_doc.get("schema_version_to", "")), + json.dumps(warnings_doc)[:200], + ) + + # Refusal path: no --accept-warnings → exit 1, sidecar STILL written, + # main output NOT written. + out2 = tmp / "blocked.json" + r = s.cli(["migrate", str(v06), "-o", str(out2)]) + s.assert_( + "migrate without --accept-warnings refuses (exit 1)", + r.returncode == 1, + r.stderr or r.stdout, + ) + s.assert_( + "migrate did NOT write main file when blocked", + not out2.is_file(), + ) + s.assert_( + "migrate STILL wrote sidecar when blocked", + (tmp / "blocked.json.warnings.json").is_file(), + ) + + +def section_validate_upgrade_from(s: Smoke) -> None: + s.section("9. validate --upgrade-from (one-shot)") + v06 = FIXTURES / "valid" / "untp-dpp-instance-0.6.1.json" + r = s.cli( + [ + "validate", + str(v06), + "--upgrade-from", + "0.6.1", + "--schema-version", + "0.7.0", + ] + ) + # exit 0 (clean upgrade) or 1 (residual schema warnings) — both fine. + s.assert_( + "--upgrade-from runs without crashing", + r.returncode in (0, 1), + f"exit={r.returncode} stderr={r.stderr[:200]}", + ) + has_upgrade_warnings = any(c in r.stdout for c in ("UPG001", "UPG002", "UPG003", "UPG004")) + s.assert_( + "--upgrade-from surfaces UPG warnings", + has_upgrade_warnings, + r.stdout[:300], + ) + + +def section_export(s: Smoke) -> None: + s.section("10. export — JSON-LD") + v07 = FIXTURES / "valid" / "untp-dpp-instance-0.7.0.json" + with tempfile.TemporaryDirectory() as tmpdir: + out = Path(tmpdir) / "exported.jsonld" + r = s.cli(["export", str(v07), "--format", "jsonld", "-o", str(out)]) + s.assert_("export jsonld exits 0", r.returncode == 0, r.stderr) + s.assert_("export wrote output file", out.is_file()) + if out.is_file(): + data = json.loads(out.read_text(encoding="utf-8")) + s.assert_("exported output has @context", "@context" in data) + ctxs = data.get("@context") or [] + s.assert_( + "export @context references v0.7 (auto-detect threaded)", + any(isinstance(c, str) and "vocabulary.uncefact.org/untp/0.7" in c for c in ctxs), + json.dumps(ctxs)[:200], + ) + + # JSON format also works. + out_json = Path(tmpdir) / "exported.json" + r = s.cli(["export", str(v07), "--format", "json", "-o", str(out_json)]) + s.assert_("export json exits 0", r.returncode == 0, r.stderr) + s.assert_("export json wrote output file", out_json.is_file()) + + +def section_python_api(s: Smoke) -> None: + s.section("11. Python API — ValidationEngine") + r = s.py( + """ + from dppvalidator.validators import ValidationEngine + engine = ValidationEngine(schema_version='0.7.0', layers=['model']) + result = engine.validate({ + '@context': [ + 'https://www.w3.org/ns/credentials/v2', + 'https://vocabulary.uncefact.org/untp/0.7.0/context/', + ], + 'type': ['DigitalProductPassport', 'VerifiableCredential'], + 'id': 'urn:test', + 'name': 'Test', + 'issuer': {'type': ['CredentialIssuer'], 'id': 'did:test', 'name': 'Test'}, + 'validFrom': '2024-01-01T00:00:00Z', + 'credentialSubject': { + 'type': ['Product'], + 'id': 'urn:p', + 'name': 'P', + 'idScheme': {'type': ['IdentifierScheme'], 'id': 'urn:s', 'name': 's'}, + 'idGranularity': 'model', + 'productCategory': [{ + 'type': ['Classification'], + 'schemeId': 'urn:cpc', + 'schemeName': 's', + 'code': '1', + 'name': 'n', + }], + 'producedAtFacility': {'type': ['Facility'], 'id': 'urn:f', 'name': 'f'}, + 'countryOfProduction': {'countryCode': 'DE', 'countryName': 'Germany'}, + }, + }) + assert result.valid, f'unexpected errors: {result.errors}' + assert result.passport is not None + assert result.schema_version == '0.7.0' + print('OK') + """ + ) + s.assert_( + "ValidationEngine validates a v0.7 dict end-to-end", + r.returncode == 0 and "OK" in r.stdout, + r.stderr or r.stdout, + ) + + +def section_compat_api(s: Smoke) -> None: + s.section("12. Python API — compat shim") + v06 = FIXTURES / "valid" / "untp-dpp-instance-0.6.1.json" + r = s.py( + f""" + import json + from pathlib import Path + from dppvalidator.compat import ( + upgrade, + active_version, + is_version, + UpgradeWarning, + UpgradeSeverity, + ) + + v06 = json.loads(Path({json.dumps(str(v06))}).read_text(encoding='utf-8')) + upgraded, warnings = upgrade(v06, country_lookup={{'AU': 'Australia'}}) + + # Shape contract. + assert isinstance(upgraded, dict), type(upgraded) + assert isinstance(warnings, list), type(warnings) + assert all(isinstance(w, UpgradeWarning) for w in warnings) + assert any(w.severity == UpgradeSeverity.WARNING or + w.severity == UpgradeSeverity.INFO for w in warnings) + + # Context rewritten. + ctxs = upgraded.get('@context', []) + assert any('vocabulary.uncefact.org/untp/0.7' in c for c in ctxs + if isinstance(c, str)), ctxs + + # Helper APIs. + v = active_version() + assert isinstance(v, str) and v.count('.') == 2, v + assert is_version(v) + assert not is_version('9.9.9') + + print('OK') + """ + ) + s.assert_( + "compat.upgrade + active_version() + is_version()", + r.returncode == 0 and "OK" in r.stdout, + r.stderr or r.stdout, + ) + + +def section_eudpp_exporter(s: Smoke) -> None: + s.section("13. Python API — EU DPP exporter") + v07 = FIXTURES / "valid" / "untp-dpp-instance-0.7.0.json" + r = s.py( + f""" + import json + from pathlib import Path + from dppvalidator.models.v0_7.envelope import DigitalProductPassport + from dppvalidator.exporters import EUDPPJsonLDExporter + + v07 = json.loads(Path({json.dumps(str(v07))}).read_text(encoding='utf-8')) + passport = DigitalProductPassport.model_validate(v07) + + # Auto-detect from passport class. + out = EUDPPJsonLDExporter().export_dict(passport) + assert 'credentialSubject' in out + assert isinstance(out.get('@context'), list) + # eudpp:DPP type marker proves the term mapper applied. + types = out.get('type') or [] + assert 'eudpp:DPP' in types, types + + # Explicit pin. + out07 = EUDPPJsonLDExporter(schema_version='0.7.0').export_dict(passport) + assert 'credentialSubject' in out07 + + print('OK') + """ + ) + s.assert_( + "EUDPPJsonLDExporter auto-detect + explicit pin", + r.returncode == 0 and "OK" in r.stdout, + r.stderr or r.stdout, + ) + + +def section_plugin_discovery(s: Smoke) -> None: + s.section("14. Plugin discovery + version-aware dispatch") + r = s.py( + """ + from importlib.util import find_spec + if find_spec('dppvalidator_example_plugin') is None: + print('SKIP example plugin not installed') + else: + from dppvalidator.plugins.discovery import ( + discover_validators, + discover_exporters, + ) + from dppvalidator.plugins.registry import PluginRegistry + from dppvalidator.models.passport import DigitalProductPassport + from dppvalidator.models import CredentialIssuer + + v_names = sorted(n for n, _ in discover_validators()) + e_names = sorted(n for n, _ in discover_exporters()) + assert 'brand_name' in v_names, v_names + assert 'brand_name_v07' in v_names, v_names + assert 'min_materials' in v_names, v_names + assert 'csv' in e_names, e_names + + # Per-version filter contract: v0.6 plugin must NOT run on v0.7 + # passport (filtered before .check()), and vice-versa. + registry = PluginRegistry() + passport = DigitalProductPassport( + id='urn:test', + issuer=CredentialIssuer(id='did:test', name='Test'), + ) + v06_errs = registry.run_all_validators(passport, schema_version='0.6.1') + v07_errs = registry.run_all_validators(passport, schema_version='0.7.0') + for e in v06_errs: + assert e.code != 'PLG001', f'v0.6 plugin crashed: {e.message}' + for e in v07_errs: + assert e.code != 'PLG001', f'v0.7 plugin crashed: {e.message}' + print('OK') + """ + ) + if "SKIP" in r.stdout: + s.skip("plugin discovery", "example plugin not installed") + else: + s.assert_( + "plugin discovery + version-aware dispatch", + r.returncode == 0 and "OK" in r.stdout, + r.stderr or r.stdout, + ) + + +def section_manifest_integrity(s: Smoke) -> None: + s.section("15. Manifest integrity (every vendored artefact pinned)") + r = s.py( + """ + import hashlib, json + from pathlib import Path + repo = Path.cwd() + manifest_path = repo / 'src' / 'dppvalidator' / 'schemas' / 'data' / 'MANIFEST.json' + manifest = json.loads(manifest_path.read_text(encoding='utf-8')) + bad = [] + for entry in manifest['artefacts']: + f = repo / entry['path'] + if not f.is_file(): + bad.append(f"missing: {entry['path']}") + continue + data = f.read_bytes().replace(b'\\r\\n', b'\\n') + actual = hashlib.sha256(data).hexdigest() + if actual != entry['sha256']: + bad.append(f"hash mismatch: {entry['path']}") + if bad: + print('FAIL\\n' + '\\n'.join(bad)) + else: + print(f'OK {len(manifest["artefacts"])} artefacts') + """ + ) + s.assert_( + "every MANIFEST.json artefact's SHA-256 still matches", + r.returncode == 0 and r.stdout.startswith("OK"), + r.stderr or r.stdout, + ) + + +def section_doctor(s: Smoke) -> None: + s.section("16. doctor — environment health") + r = s.cli(["doctor"]) + # 0 (healthy) or 2 (issues but the command itself worked). + s.assert_( + "doctor runs without crashing", + r.returncode in (0, 2), + f"exit={r.returncode} stderr={r.stderr[:200]}", + ) + + +# --------------------------------------------------------------------------- +# Content negotiation against the upstream UN/CEFACT vocabulary endpoint +# --------------------------------------------------------------------------- + + +def _http_head_get(url: str, accept: str, timeout: float = 10.0) -> tuple[int, str, bytes]: + """Issue a GET (with Accept) and return (status, content-type, body). + + GET (not HEAD) because S3/CloudFront sometimes serves different + Content-Type headers on HEAD vs GET. We need the negotiated body + too, to verify it's parseable JSON-LD when that's what we asked + for. Capped at 256 KB so we don't pull the full ~150 KB twice in + a tight loop. + """ + req = urllib.request.Request( # noqa: S310 — fixed https URLs only + url, + headers={"Accept": accept, "User-Agent": "dppvalidator-smoke/1.0"}, + ) + with urllib.request.urlopen(req, timeout=timeout) as resp: # noqa: S310 + body = resp.read(262_144) + return resp.status, resp.headers.get("Content-Type", ""), body + + +def section_content_negotiation(s: Smoke) -> None: + """Verify upstream UN/CEFACT vocabulary honours JSON-LD content-negotiation. + + Two upstream endpoints with different contracts: + + 1. ``/untp/`` — vocabulary landing page. Full negotiation: + - ``Accept: application/ld+json`` → ``application/ld+json`` + - default / ``Accept: text/html`` → HTML landing page + - ``Accept: application/json`` → HTML (only ``ld+json`` is honoured) + + 2. ``/untp/0.7.0/context/`` — pure JSON-LD context document. + Unconditionally served as ``application/ld+json`` regardless of + ``Accept`` (there is no HTML alternative — a context IS the artefact). + + Both must always return parseable JSON-LD with a root ``@context`` when + dereferenced as part of normal validator flows. Network-dependent; + skips cleanly when the host is unreachable. + """ + s.section("17. Content negotiation — vocabulary.uncefact.org") + + # ── /untp/ — landing page with full negotiation ──────────────────────── + base = "https://vocabulary.uncefact.org/untp/" + base_label = "untp/" + try: + status, ctype, body = _http_head_get(base, "application/ld+json") + except (urllib.error.URLError, TimeoutError, OSError) as exc: + s.skip(f"{base_label} (network)", f"unavailable: {exc}") + else: + s.assert_(f"{base_label} ld+json returns 200", status == 200, f"got status={status}") + s.assert_( + f"{base_label} ld+json Content-Type is application/ld+json", + "application/ld+json" in ctype.lower(), + f"got Content-Type={ctype!r}", + ) + try: + doc = json.loads(body.decode("utf-8")) + s.assert_( + f"{base_label} ld+json body is parseable JSON-LD with @context", + isinstance(doc, dict) and "@context" in doc, + f"top keys: {sorted(doc) if isinstance(doc, dict) else type(doc).__name__}", + ) + except (json.JSONDecodeError, UnicodeDecodeError) as exc: + s.fail( + f"{base_label} ld+json body is parseable JSON-LD with @context", + f"{exc!r}: {body[:120]!r}", + ) + + # Default Accept must fall back to HTML — proves negotiation is real, + # not a hard-coded Content-Type. + try: + _, ctype_html, _ = _http_head_get(base, "text/html,*/*") + s.assert_( + f"{base_label} default Accept falls back to HTML (proves negotiation)", + "text/html" in ctype_html.lower(), + f"got Content-Type={ctype_html!r}", + ) + except (urllib.error.URLError, TimeoutError, OSError) as exc: + s.skip(f"{base_label} (HTML probe)", f"network blip: {exc}") + + # application/json currently falls back to HTML. Pin the observation + # so a future upstream change (good: starts honouring it; bad: 406s) + # is flagged loudly rather than silently changing validator semantics. + try: + _, ctype_json, _ = _http_head_get(base, "application/json") + s.assert_( + f"{base_label} application/json → HTML fallback (only ld+json is honoured)", + "text/html" in ctype_json.lower(), + f"got Content-Type={ctype_json!r} — upstream behaviour changed?", + ) + except (urllib.error.URLError, TimeoutError, OSError) as exc: + s.skip(f"{base_label} (json probe)", f"network blip: {exc}") + + # ── /untp/0.7.0/context/ — pure JSON-LD context, no negotiation ──────── + ctx_url = "https://vocabulary.uncefact.org/untp/0.7.0/context/" + ctx_label = "untp/0.7.0/context/" + for accept, accept_label in ( + ("application/ld+json", "ld+json"), + ("text/html,*/*", "default Accept"), + ("application/json", "application/json"), + ): + try: + status, ctype, body = _http_head_get(ctx_url, accept) + except (urllib.error.URLError, TimeoutError, OSError) as exc: + s.skip(f"{ctx_label} ({accept_label})", f"network blip: {exc}") + continue + s.assert_( + f"{ctx_label} {accept_label} returns 200", + status == 200, + f"got status={status}", + ) + # Context document is always served as ld+json — there's no + # HTML alternative because a JSON-LD context IS the artefact. + s.assert_( + f"{ctx_label} {accept_label} → application/ld+json (unconditional)", + "application/ld+json" in ctype.lower(), + f"got Content-Type={ctype!r}", + ) + + # Final cross-check: the context body really is the v0.7.0 UNTP context + # document — pin shape + a couple of well-known prefixes so silent upstream + # content drift is caught (e.g. if the file at this URL is replaced). + try: + _, _, body = _http_head_get(ctx_url, "application/ld+json") + doc = json.loads(body.decode("utf-8")) + ctx = doc.get("@context") + s.assert_( + "context body has @context as a dict (JSON-LD context document)", + isinstance(ctx, dict), + f"@context type: {type(ctx).__name__}", + ) + if isinstance(ctx, dict): + terms = list(ctx.keys()) + s.assert_( + "context body declares 'untp' prefix (right document at this URL)", + "untp" in terms, + f"terms[:8]: {terms[:8]}", + ) + # 105 KB context with ~30+ top-level terms is the expected shape; + # drop to >= 10 to leave headroom for upstream pruning while still + # catching catastrophic shrinkage (e.g. a stub or 404 page). + s.assert_( + "context body has substantial term mappings (not a stub)", + len(terms) >= 10, + f"only {len(terms)} terms — upstream stub?", + ) + except ( + urllib.error.URLError, + TimeoutError, + OSError, + json.JSONDecodeError, + UnicodeDecodeError, + ) as exc: + s.skip("context content cross-check", f"unavailable: {exc}") + + +# --------------------------------------------------------------------------- +# Orchestrator +# --------------------------------------------------------------------------- + + +def main() -> int: + s = Smoke() + print(f"{BLUE}dppvalidator functional smoke test{RESET}") + print(f"{DIM}DPP_BIN={DPP_BIN}{RESET}") + print(f"{DIM}PY_BIN={PY_BIN}{RESET}") + + if not _check_environment(s): + s.fail("pre-flight failed", "stopping early — fix the environment and re-run") + return s.report() + + section_cli_sanity(s) + section_schema_management(s) + section_validate_auto_detect(s) + section_validate_explicit_pin(s) + section_validate_invalid(s) + section_output_formats(s) + section_stdin(s) + section_migrate(s) + section_validate_upgrade_from(s) + section_export(s) + section_python_api(s) + section_compat_api(s) + section_eudpp_exporter(s) + section_plugin_discovery(s) + section_manifest_integrity(s) + section_doctor(s) + section_content_negotiation(s) + + return s.report() + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/src/dppvalidator/cli/commands/completions.py b/src/dppvalidator/cli/commands/completions.py index ec685f6..7bee768 100644 --- a/src/dppvalidator/cli/commands/completions.py +++ b/src/dppvalidator/cli/commands/completions.py @@ -70,7 +70,7 @@ return ;; --version) - COMPREPLY=($(compgen -W "0.6.0 0.6.1" -- "${cur}")) + COMPREPLY=($(compgen -W "__SCHEMA_VERSIONS__" -- "${cur}")) return ;; esac @@ -129,7 +129,7 @@ ) schema_opts=( - '--version[Schema version]:version:(0.6.0 0.6.1)' + '--version[Schema version]:version:(__SCHEMA_VERSIONS__)' '--help[Show help]' ) @@ -221,7 +221,7 @@ complete -c dppvalidator -n "__fish_seen_subcommand_from schema; and not __fish_seen_subcommand_from list info download" -a list -d "List available schemas" complete -c dppvalidator -n "__fish_seen_subcommand_from schema; and not __fish_seen_subcommand_from list info download" -a info -d "Show schema information" complete -c dppvalidator -n "__fish_seen_subcommand_from schema; and not __fish_seen_subcommand_from list info download" -a download -d "Download a schema" -complete -c dppvalidator -n "__fish_seen_subcommand_from schema" -l version -d "Schema version" -xa "0.6.0 0.6.1" +complete -c dppvalidator -n "__fish_seen_subcommand_from schema" -l version -d "Schema version" -xa "__SCHEMA_VERSIONS__" # completions options complete -c dppvalidator -n "__fish_seen_subcommand_from completions" -a "bash zsh fish" -d "Shell type" @@ -254,6 +254,23 @@ def add_parser(subparsers: _SubParsersAction[argparse.ArgumentParser]) -> None: ) +_SCHEMA_VERSIONS_SENTINEL = "__SCHEMA_VERSIONS__" + + +def _expand_schema_versions(template: str) -> str: + """Expand the ``__SCHEMA_VERSIONS__`` sentinel with live registry values. + + The completion templates carry a sentinel rather than a hardcoded version + list so that ``dppvalidator completions `` always reflects whatever + is registered in ``SCHEMA_REGISTRY`` at the moment the user generates the + completion. See docs/plans/UNTP_0.7.0_MIGRATION.md §Phase 1 / §7.1. + """ + from dppvalidator.schemas.registry import SCHEMA_REGISTRY + + versions = " ".join(sorted(SCHEMA_REGISTRY)) + return template.replace(_SCHEMA_VERSIONS_SENTINEL, versions) + + def run(args: argparse.Namespace) -> int: """Run the completions command. @@ -278,5 +295,5 @@ def run(args: argparse.Namespace) -> int: print(f"Unknown shell: {shell}", file=sys.stderr) return 2 - print(completions[shell]) + print(_expand_schema_versions(completions[shell])) return 0 diff --git a/src/dppvalidator/cli/commands/export.py b/src/dppvalidator/cli/commands/export.py index d92c0a8..04cbc54 100644 --- a/src/dppvalidator/cli/commands/export.py +++ b/src/dppvalidator/cli/commands/export.py @@ -8,6 +8,7 @@ from typing import TYPE_CHECKING, Any from dppvalidator.logging import get_logger +from dppvalidator.schemas.registry import DEFAULT_SCHEMA_VERSION if TYPE_CHECKING: from dppvalidator.cli.console import Console @@ -43,8 +44,12 @@ def add_parser(subparsers: Any) -> argparse.ArgumentParser: ) parser.add_argument( "--schema-version", - default="0.6.1", - help="Schema version (default: 0.6.1)", + default="auto", + help=( + "UNTP DPP schema version. Defaults to 'auto' — detected " + "from the payload's $schema or @context. Default-version " + f"fallback when detection finds nothing: {DEFAULT_SCHEMA_VERSION}." + ), ) parser.add_argument( "--compact", @@ -78,8 +83,14 @@ def run(args: argparse.Namespace, console: Console) -> int: indent = None if args.compact else 2 + # When the user passed --schema-version=auto (the default), the + # validator resolved a concrete version into ``result.schema_version``. + # Use that for the exporter so the output's @context URL matches the + # payload's actual version, not the literal 'auto' sentinel. + resolved_version = result.schema_version or args.schema_version + if args.format == "jsonld": - exporter = JSONLDExporter(version=args.schema_version) + exporter = JSONLDExporter(version=resolved_version) output = exporter.export(result.passport, indent=indent) else: exporter = JSONExporter() diff --git a/src/dppvalidator/cli/commands/init.py b/src/dppvalidator/cli/commands/init.py index 8fa2a26..1efef0c 100644 --- a/src/dppvalidator/cli/commands/init.py +++ b/src/dppvalidator/cli/commands/init.py @@ -11,6 +11,8 @@ from pathlib import Path from typing import TYPE_CHECKING, Any +from dppvalidator.schemas.registry import DEFAULT_SCHEMA_VERSION + if TYPE_CHECKING: from dppvalidator.cli.console import Console @@ -155,12 +157,16 @@ - [UNTP DPP Schema](https://untp.unece.org/specification/DigitalProductPassport) """ -DPPVALIDATOR_CONFIG = { +DPPVALIDATOR_CONFIG: dict[str, Any] = { "$schema": "https://artiso-ai.github.io/dppvalidator/schemas/config.json", "version": "1.0", "validation": { "strict": False, - "schema_version": "0.6.1", + # Resolved at template-emission time so a generated project always + # gets the validator's currently-shipping default UNTP version. + # Phase 3 will introduce a `--schema-version` flag on `init` so users + # can pin a specific version (e.g. `0.7.0`) at scaffolding time. + "schema_version": DEFAULT_SCHEMA_VERSION, "fail_on_warning": False, }, "paths": { diff --git a/src/dppvalidator/cli/commands/migrate.py b/src/dppvalidator/cli/commands/migrate.py new file mode 100644 index 0000000..36f0f55 --- /dev/null +++ b/src/dppvalidator/cli/commands/migrate.py @@ -0,0 +1,213 @@ +"""Migrate command: rewrite a v0.6.x DPP into v0.7.0 shape. + +Phase 4 of ``docs/plans/UNTP_0.7.0_MIGRATION.md`` introduces this +command. It runs the compat shim (see +:mod:`dppvalidator.compat.upgrade_0_6_to_0_7`) over a single input file +and writes the upgraded JSON to ``-o`` / ``--in-place``. + +By default, the command refuses to write the upgraded file when the +shim emits any ``warning``- or ``error``-severity warnings — the user +must opt in with ``--accept-warnings``. ``info``-severity events are +informational and never block. A sidecar ``.warnings.json`` +captures every warning whenever any non-info warning fires, regardless +of whether the write went through. +""" + +from __future__ import annotations + +import argparse +import json +import sys +from dataclasses import asdict +from pathlib import Path +from typing import TYPE_CHECKING, Any + +from dppvalidator.logging import get_logger + +if TYPE_CHECKING: + from dppvalidator.cli.console import Console + +logger = get_logger(__name__) + +EXIT_OK = 0 +EXIT_BLOCKED = 1 +EXIT_ERROR = 2 + + +def add_parser(subparsers: Any) -> argparse.ArgumentParser: + """Register the ``migrate`` subcommand.""" + parser = subparsers.add_parser( + "migrate", + help="Upgrade a v0.6.x DPP to v0.7.0 shape via the compat shim", + description=( + "Run the compat shim over a v0.6.x DPP and write the upgraded " + "JSON. Refuses to write when warnings fire unless " + "--accept-warnings is given. A sidecar warnings file is always " + "produced when warnings fire." + ), + ) + parser.add_argument( + "input", + help="Input file path, or '-' for stdin", + ) + parser.add_argument( + "-o", + "--output", + default=None, + help="Output file path (default: stdout)", + ) + parser.add_argument( + "--in-place", + action="store_true", + help="Write the upgraded JSON back to the input path (overwrites).", + ) + parser.add_argument( + "--accept-warnings", + action="store_true", + help=( + "Write the upgraded JSON even when the shim emits warnings or " + "errors. Without this, the command exits with code 1 on any " + "non-info warning." + ), + ) + parser.add_argument( + "--from", + dest="source_version", + default="0.6.x", + help=( + "Source UNTP version family (default: 0.6.x). Pass an explicit " + "X.Y.Z value to pin a specific source version." + ), + ) + return parser + + +def run(args: argparse.Namespace, console: Console) -> int: + """Execute the migrate command.""" + from dppvalidator.compat.upgrade_0_6_to_0_7 import ( + UpgradeSeverity, + upgrade, + ) + + data = _load_input(args.input, console) + if data is None: + return EXIT_ERROR + + if not args.source_version.startswith("0.6"): + console.print_error( + f"No upgrade shim registered for source version {args.source_version!r}.", + ) + return EXIT_ERROR + + try: + upgraded, warnings = upgrade(data) + except Exception as exc: + logger.exception("Upgrade shim crashed") + console.print_error(f"Upgrade failed: {exc}") + return EXIT_ERROR + + blocking = [w for w in warnings if w.severity != UpgradeSeverity.INFO] + + output_path = _resolve_output_path(args, console) + if output_path is None and args.in_place: + return EXIT_ERROR + + # Always write a sidecar warnings file when *any* blocking-grade + # warning fired, regardless of whether the main write goes through. + sidecar_path: Path | None = None + if blocking and output_path is not None: + sidecar_path = output_path.with_suffix(output_path.suffix + ".warnings.json") + _write_warnings_sidecar(sidecar_path, warnings) + console.print_warning( + f"{len(warnings)} warning(s) recorded in {sidecar_path}", + ) + + if blocking and not args.accept_warnings: + console.print_error( + f"Upgrade emitted {len(blocking)} blocking warning(s); refusing to " + "write. Re-run with --accept-warnings to override, or fix the " + "issues listed in the sidecar warnings file.", + ) + for w in warnings: + console.print(f" [{w.code}] ({w.severity.value}) {w.path}: {w.message}") + return EXIT_BLOCKED + + _write_output(upgraded, output_path, console) + + if warnings: + console.print(f"Upgraded with {len(warnings)} warning(s).") + for w in warnings: + console.print(f" [{w.code}] ({w.severity.value}) {w.path}: {w.message}") + else: + console.print_success("Upgraded with no warnings.") + + return EXIT_OK + + +def _resolve_output_path(args: argparse.Namespace, console: Console) -> Path | None: + """Return the resolved output path or ``None`` when stdout is the target.""" + if args.in_place and args.output: + console.print_error("--in-place and -o/--output are mutually exclusive.") + return None + if args.in_place: + if args.input == "-": + console.print_error("--in-place is incompatible with stdin input.") + return None + return Path(args.input) + if args.output: + return Path(args.output) + return None + + +def _load_input(input_path: str, console: Console) -> dict[str, Any] | None: + """Load JSON from a file path or stdin.""" + try: + if input_path == "-": + if hasattr(sys.stdin, "reconfigure"): + sys.stdin.reconfigure(encoding="utf-8") # type: ignore[union-attr] + content = sys.stdin.read() + else: + path = Path(input_path) + if not path.exists(): + console.print_error(f"File not found: {input_path}") + return None + content = path.read_text(encoding="utf-8") + return json.loads(content) + except json.JSONDecodeError as exc: + console.print_error(f"Invalid JSON: {exc}") + return None + except Exception as exc: + logger.exception("Unexpected error loading input") + console.print_error(str(exc)) + return None + + +def _write_output(payload: dict[str, Any], path: Path | None, console: Console) -> None: + """Write the upgraded JSON to ``path`` (or stdout if ``None``).""" + serialised = json.dumps(payload, indent=2, ensure_ascii=False, default=str) + if path is None: + # Stdout — bypass Rich so the output is pipe-friendly. + print(serialised) + return + path.write_text(serialised + "\n", encoding="utf-8") + console.print(f"Wrote {path}") + + +def _write_warnings_sidecar(path: Path, warnings: list[Any]) -> None: + """Persist the full warning list as JSON next to the upgraded payload.""" + from dppvalidator.schemas.registry import SCHEMA_REGISTRY + + target_candidates = [ + v for v in SCHEMA_REGISTRY if v.split(".")[0] == "0" and v.split(".")[1] == "7" + ] + target_version = ( + max(target_candidates, key=lambda v: tuple(int(x) for x in v.split("."))) + if target_candidates + else "0.7.x" + ) + payload = { + "schema_version_from": "0.6.x", + "schema_version_to": target_version, + "warnings": [{**asdict(w), "severity": w.severity.value} for w in warnings], + } + path.write_text(json.dumps(payload, indent=2) + "\n", encoding="utf-8") diff --git a/src/dppvalidator/cli/commands/schema.py b/src/dppvalidator/cli/commands/schema.py index ca848b5..a075332 100644 --- a/src/dppvalidator/cli/commands/schema.py +++ b/src/dppvalidator/cli/commands/schema.py @@ -6,6 +6,7 @@ from typing import TYPE_CHECKING, Any from dppvalidator.logging import get_logger +from dppvalidator.schemas.registry import DEFAULT_SCHEMA_VERSION if TYPE_CHECKING: from dppvalidator.cli.console import Console @@ -38,8 +39,8 @@ def add_parser(subparsers: Any) -> argparse.ArgumentParser: download_parser.add_argument( "-v", "--version", - default="0.6.1", - help="Schema version to download (default: 0.6.1)", + default=DEFAULT_SCHEMA_VERSION, + help=f"Schema version to download (default: {DEFAULT_SCHEMA_VERSION})", ) download_parser.add_argument( "-o", @@ -54,8 +55,8 @@ def add_parser(subparsers: Any) -> argparse.ArgumentParser: info_parser.add_argument( "-v", "--version", - default="0.6.1", - help="Schema version (default: 0.6.1)", + default=DEFAULT_SCHEMA_VERSION, + help=f"Schema version (default: {DEFAULT_SCHEMA_VERSION})", ) return parser @@ -75,27 +76,49 @@ def run(args: argparse.Namespace, console: Console) -> int: def _list_schemas(console: Console) -> int: - """List available schema versions.""" - from dppvalidator.exporters.contexts import CONTEXTS, DEFAULT_VERSION + """List available schema versions registered in ``SCHEMA_REGISTRY``. + + Source of truth is ``SCHEMA_REGISTRY``; ``CONTEXTS`` is consulted for the + JSON-LD context URLs of each version. Both are kept in lock-step — see + docs/plans/UNTP_0.7.0_MIGRATION.md §Phase 1. + """ + from dppvalidator.exporters.contexts import CONTEXTS + from dppvalidator.schemas.registry import SCHEMA_REGISTRY, SchemaRegistry + + default_version = SchemaRegistry().default_version table = console.create_table(title="Available UNTP DPP Schema Versions") table.add_column("Version") table.add_column("Default", justify="center") + table.add_column("Bundled", justify="center") table.add_column("Contexts") - for version, ctx in CONTEXTS.items(): - is_default = "✓" if version == DEFAULT_VERSION else "" - contexts = ", ".join(ctx.contexts) - table.add_row(version, is_default, contexts) + for version in sorted(SCHEMA_REGISTRY): + schema = SCHEMA_REGISTRY[version] + is_default = "✓" if version == default_version else "" + # ``sha256 is not None`` is the proxy for "we ship this file in-tree"; + # versions without a hash are registered but rely on a custom path + # being supplied at validate-time. + is_bundled = "✓" if schema.sha256 is not None else "" + ctx = CONTEXTS.get(version) + contexts = ", ".join(ctx.contexts) if ctx else "(no @context registered)" + table.add_row(version, is_default, is_bundled, contexts) console.print_table(table) return EXIT_VALID def _download_schema(version: str, output_dir: str | None, console: Console) -> int: - """Download schema for a version.""" + """Download a schema by version using the URL recorded in ``SCHEMA_REGISTRY``.""" from pathlib import Path + from dppvalidator.schemas.registry import SCHEMA_REGISTRY + + if version not in SCHEMA_REGISTRY: + console.print_error(f"Unknown version: {version}") + console.print(f"Available: {', '.join(sorted(SCHEMA_REGISTRY))}") + return EXIT_ERROR + try: import httpx except ImportError: @@ -103,7 +126,7 @@ def _download_schema(version: str, output_dir: str | None, console: Console) -> console.print("Install with: pip install 'dppvalidator[http]'") return EXIT_ERROR - schema_url = f"https://test.uncefact.org/vocabulary/untp/dpp/untp-dpp-schema-{version}.json" + schema_url = SCHEMA_REGISTRY[version].url try: logger.info("Downloading schema %s from %s", version, schema_url) @@ -126,26 +149,30 @@ def _download_schema(version: str, output_dir: str | None, console: Console) -> def _show_info(version: str, console: Console) -> int: - """Show schema information.""" + """Show schema information for ``version`` from the registries.""" from dppvalidator.exporters.contexts import CONTEXTS + from dppvalidator.schemas.registry import SCHEMA_REGISTRY - if version not in CONTEXTS: + if version not in SCHEMA_REGISTRY: console.print_error(f"Unknown version: {version}") - console.print(f"Available: {', '.join(CONTEXTS.keys())}") + console.print(f"Available: {', '.join(sorted(SCHEMA_REGISTRY))}") return EXIT_ERROR - ctx = CONTEXTS[version] - schema_url = f"https://test.uncefact.org/vocabulary/untp/dpp/untp-dpp-schema-{version}.json" + schema = SCHEMA_REGISTRY[version] + ctx = CONTEXTS.get(version) + type_arr = ctx.default_type if ctx else () + sha = schema.sha256 or "(not bundled — fetched on demand)" info = f"""[bold]UNTP DPP Schema v{version}[/bold] -Type: {", ".join(ctx.default_type)} +Type: {", ".join(type_arr) or "(no @context registered)"} Contexts: """ - for url in ctx.contexts: + for url in ctx.contexts if ctx else (): info += f" • {url}\n" - info += f"\nSchema URL:\n {schema_url}" + info += f"\nSchema URL:\n {schema.url}\n" + info += f"\nSHA-256:\n {sha}" console.print_panel(info, title=f"Schema v{version}") return EXIT_VALID diff --git a/src/dppvalidator/cli/commands/validate.py b/src/dppvalidator/cli/commands/validate.py index 1d23f14..51f3670 100644 --- a/src/dppvalidator/cli/commands/validate.py +++ b/src/dppvalidator/cli/commands/validate.py @@ -10,6 +10,7 @@ from typing import TYPE_CHECKING, Any from dppvalidator.logging import get_logger +from dppvalidator.schemas.registry import DEFAULT_SCHEMA_VERSION if TYPE_CHECKING: from dppvalidator.cli.console import Console @@ -48,8 +49,15 @@ def add_parser(subparsers: Any) -> argparse.ArgumentParser: ) parser.add_argument( "--schema-version", - default="0.6.1", - help="Schema version (default: 0.6.1)", + default="auto", + help=( + "UNTP DPP schema version to validate against. Defaults to " + "'auto' — the engine detects the version from the payload's " + "$schema or @context URLs. Pass an explicit version (e.g. " + "'0.6.1', '0.7.0') to fail fast with VER001 when the payload " + f"declares a different version. Default-version fallback when " + f"detection finds nothing: {DEFAULT_SCHEMA_VERSION}." + ), ) parser.add_argument( "--fail-fast", @@ -62,6 +70,15 @@ def add_parser(subparsers: Any) -> argparse.ArgumentParser: default=100, help="Maximum errors to report (default: 100)", ) + parser.add_argument( + "--upgrade-from", + default=None, + help=( + "Run the input through the compat shim from the named UNTP version " + "(e.g. 0.6.1) up to --schema-version before validating. Upgrade " + "warnings are reported alongside validation issues." + ), + ) return parser @@ -79,6 +96,10 @@ def run(args: argparse.Namespace, console: Console) -> int: strict_mode=args.strict, ) + upgrade_from = getattr(args, "upgrade_from", None) + if upgrade_from is not None: + _verify_upgrade_path(upgrade_from, args.schema_version, console) + all_valid = True has_load_error = False results: list[tuple[str, Any]] = [] @@ -89,6 +110,11 @@ def run(args: argparse.Namespace, console: Console) -> int: has_load_error = True continue + if upgrade_from is not None: + data, upgrade_warnings = _apply_upgrade(data, upgrade_from, file_path, console) + if upgrade_warnings: + _print_upgrade_warnings(upgrade_warnings, file_path, console) + result = engine.validate( data, fail_fast=args.fail_fast, @@ -147,6 +173,50 @@ def _resolve_inputs(inputs: list[str], console: Console) -> list[str]: return files +def _verify_upgrade_path(source: str, target: str, console: Console) -> None: + """Confirm we have a registered shim for ``source → target``. + + The current matrix is just ``0.6.x → 0.7.0`` (Phase 4). Anything else + is reported as a warning so the caller knows their flag had no effect; + we don't raise so the rest of the validation still runs. + """ + from dppvalidator.compat.upgrade_0_6_to_0_7 import upgrade as _u # noqa: F401 + + if not (source.startswith("0.6") and target.startswith("0.7")): + console.print_warning( + f"No upgrade shim registered for {source!r} → {target!r}; " + "input will be validated without transformation.", + ) + + +def _apply_upgrade( + data: dict[str, Any], source: str, path: str, console: Console +) -> tuple[dict[str, Any], list[Any]]: + """Run the v0.6 → v0.7 shim on ``data`` and return ``(upgraded, warnings)``.""" + if source.startswith("0.6"): + from dppvalidator.compat.upgrade_0_6_to_0_7 import upgrade + + try: + return upgrade(data) + except Exception as exc: # pragma: no cover — defensive + logger.exception("Upgrade shim crashed on %s", path) + console.print_error(f"Upgrade shim failed for {path}: {exc}") + return data, [] + return data, [] + + +def _print_upgrade_warnings(warnings: list[Any], input_path: str, console: Console) -> None: + """Print upgrade-shim warnings inline with validation output.""" + if not warnings: + return + console.print( + f"\n[bold yellow]Upgrade warnings ({len(warnings)})[/bold yellow] — {input_path}", + style="yellow", + ) + for w in warnings: + console.print(f" [{w.code}] ({w.severity.value}) {w.path}: {w.message}") + + def _load_input(input_path: str, console: Console) -> dict[str, Any] | None: """Load input data from file or stdin.""" try: diff --git a/src/dppvalidator/cli/commands/watch.py b/src/dppvalidator/cli/commands/watch.py index 70626a4..710a31a 100644 --- a/src/dppvalidator/cli/commands/watch.py +++ b/src/dppvalidator/cli/commands/watch.py @@ -9,6 +9,7 @@ from pathlib import Path from typing import TYPE_CHECKING, Any +from dppvalidator.schemas.registry import DEFAULT_SCHEMA_VERSION from dppvalidator.validators import ValidationEngine if TYPE_CHECKING: @@ -216,8 +217,13 @@ def add_parser(subparsers: Any) -> argparse.ArgumentParser: ) parser.add_argument( "--schema-version", - default="0.6.1", - help="Schema version (default: 0.6.1)", + default="auto", + help=( + "UNTP DPP schema version. Defaults to 'auto' — detected " + "from each watched payload's $schema or @context. Pass an " + "explicit version to fail-fast on payloads that declare a " + f"different one. Default-version fallback: {DEFAULT_SCHEMA_VERSION}." + ), ) return parser diff --git a/src/dppvalidator/cli/main.py b/src/dppvalidator/cli/main.py index f7f9da1..ceaf136 100644 --- a/src/dppvalidator/cli/main.py +++ b/src/dppvalidator/cli/main.py @@ -7,7 +7,16 @@ from collections.abc import Callable from typing import NoReturn -from dppvalidator.cli.commands import completions, doctor, export, init, schema, validate, watch +from dppvalidator.cli.commands import ( + completions, + doctor, + export, + init, + migrate, + schema, + validate, + watch, +) from dppvalidator.cli.console import Console from dppvalidator.logging import configure_logging @@ -26,6 +35,7 @@ "init": init.run, "doctor": doctor.run, "watch": watch.run, + "migrate": migrate.run, "completions": lambda args, _: completions.run(args), } @@ -72,6 +82,7 @@ def create_parser() -> argparse.ArgumentParser: init.add_parser(subparsers) doctor.add_parser(subparsers) watch.add_parser(subparsers) + migrate.add_parser(subparsers) completions.add_parser(subparsers) return parser diff --git a/src/dppvalidator/compat/__init__.py b/src/dppvalidator/compat/__init__.py new file mode 100644 index 0000000..1687992 --- /dev/null +++ b/src/dppvalidator/compat/__init__.py @@ -0,0 +1,69 @@ +"""Cross-version compatibility utilities for dppvalidator. + +This package houses two distinct concerns: + +1. **Active-version helpers** — :func:`active_version` and + :func:`is_version` give callers a single import-stable way to check + "what version of UNTP DPP is the engine defaulting to right now?" + without having to reach into :mod:`dppvalidator.schemas.registry`. + +2. **Compatibility shims** — modules named ``upgrade__to_`` + that take a payload in the older shape and rewrite it in place to + match the newer one. They emit structured :class:`UpgradeWarning` + entries when a transformation is lossy or has to synthesise a value; + the caller decides whether to accept the result or surface the + warnings to the end user. + +The :func:`active_version` and :func:`is_version` helpers are listed in +``.claude/rules/untp-versioning.md`` (cardinal rule 1) as the +canonical alternative to literal version strings outside the +:mod:`dppvalidator.schemas.registry` and +:mod:`dppvalidator.exporters.contexts` registries. + +See ``docs/plans/UNTP_0.7.0_MIGRATION.md`` §Phase 4 for the design. +""" + +from __future__ import annotations + +from dppvalidator.compat.upgrade_0_6_to_0_7 import ( + UPG_CODE_LOSSY, + UPG_CODE_REQUIRED_FIELD_MISSING, + UPG_CODE_SYNTHESISED, + UPG_CODE_UNMAPPED_COUNTRY, + UpgradeSeverity, + UpgradeWarning, + upgrade, +) + + +def active_version() -> str: + """Return the UNTP DPP version this build of dppvalidator targets. + + This is the value of :data:`DEFAULT_SCHEMA_VERSION` from the schema + registry, surfaced as a function so callers don't have to import the + registry directly. Use this whenever you need a "current default" + version literal in feature code — the no-version-literals guard + test (``tests/unit/test_no_version_literals.py``) refuses to let you + hardcode the string. + """ + from dppvalidator.schemas.registry import DEFAULT_SCHEMA_VERSION + + return DEFAULT_SCHEMA_VERSION + + +def is_version(version: str) -> bool: + """Return ``True`` if ``version`` matches the active default version.""" + return version == active_version() + + +__all__ = [ + "UPG_CODE_LOSSY", + "UPG_CODE_REQUIRED_FIELD_MISSING", + "UPG_CODE_SYNTHESISED", + "UPG_CODE_UNMAPPED_COUNTRY", + "UpgradeSeverity", + "UpgradeWarning", + "active_version", + "is_version", + "upgrade", +] diff --git a/src/dppvalidator/compat/upgrade_0_6_to_0_7.py b/src/dppvalidator/compat/upgrade_0_6_to_0_7.py new file mode 100644 index 0000000..b9c0840 --- /dev/null +++ b/src/dppvalidator/compat/upgrade_0_6_to_0_7.py @@ -0,0 +1,972 @@ +"""Compatibility shim: rewrite UNTP DPP 0.6.x payloads into 0.7.0 shape. + +This module is the operational core of Phase 4 of +``docs/plans/UNTP_0.7.0_MIGRATION.md``. It takes a JSON ``dict`` whose +shape matches the UNTP DPP 0.6.x wire format and returns a new ``dict`` +in 0.7.0 shape, plus a list of :class:`UpgradeWarning` entries +describing each transformation that was lossy, synthesised, or skipped. + +Design principles: + +- **Pure transformation, no validation.** The shim never raises on + malformed input; it makes a best-effort upgrade and lets the caller + validate the result against the v0.7 model. Anything that *can't* be + upgraded faithfully is surfaced as an :class:`UpgradeWarning`. +- **Structural over semantic.** The shim rewires field names and shapes + but never invents content. ``materialType`` cannot be guessed from + ``name``; ``massFraction`` cannot be inferred. Missing required-in-0.7 + fields therefore produce ``UPG004`` warnings, not synthesised values. +- **Side-effect-free.** The input ``dict`` is deep-copied; callers can + upgrade-then-keep-original without surprises. +- **Deterministic.** Two runs against the same input produce + byte-identical output and the same warning set in the same order. + +The 17 transformation steps and 4 warning codes are documented inline. +The transformation order matters: e.g. step 1 (context URL) must run +before step 3 (envelope flattening) so that detection helpers continue +to work on the partially-upgraded payload. +""" + +from __future__ import annotations + +import base64 +import re +from collections.abc import Mapping +from copy import deepcopy +from dataclasses import dataclass +from enum import Enum +from typing import Any + +from dppvalidator.logging import get_logger +from dppvalidator.vocabularies.loader import get_bundled_country_codes + +logger = get_logger(__name__) + + +# ============================================================================= +# Warning codes and types +# ============================================================================= + + +# Codes from docs/plans/UNTP_0.7.0_MIGRATION.md §Phase 4. The ``UPG`` prefix +# distinguishes them from validator codes (``CQ``, ``MDL``, ``JLD``, ``VER``). +UPG_CODE_LOSSY = "UPG001" +UPG_CODE_SYNTHESISED = "UPG002" +UPG_CODE_UNMAPPED_COUNTRY = "UPG003" +UPG_CODE_REQUIRED_FIELD_MISSING = "UPG004" + + +class UpgradeSeverity(str, Enum): + """Severity levels for :class:`UpgradeWarning`. + + ``info`` is for changes the caller can safely ignore (e.g. type + arrays stripped from embedded objects). ``warning`` is for + transformations that may need manual review (synthesised values, + unknown country codes). ``error`` is for required-in-0.7 fields + that the shim cannot synthesise — the result will not validate + cleanly until the caller fills these in. + """ + + INFO = "info" + WARNING = "warning" + ERROR = "error" + + +@dataclass(frozen=True, slots=True) +class UpgradeWarning: + """A single transformation event surfaced by the upgrade shim. + + Attributes: + code: One of the ``UPG`` codes above. Stable across releases — + consumers may pattern-match on the code to decide whether + to accept the upgrade. + path: JSONPath-like locator (e.g. + ``credentialSubject.materialProvenance[2].originCountry``) + pointing at where the transformation was applied. + message: Human-readable explanation. Include enough context to + be actionable when read in isolation. + severity: One of :class:`UpgradeSeverity`. Drives whether + ``dppvalidator migrate`` refuses to write the upgraded JSON + without ``--accept-warnings``. + """ + + code: str + path: str + message: str + severity: UpgradeSeverity = UpgradeSeverity.WARNING + + +# ============================================================================= +# Constants +# ============================================================================= + + +# v0.6.x → v0.7.0 context URL substitution. The v0.6.x family stored its +# UNTP context under ``test.uncefact.org/vocabulary/untp/dpp//``; +# v0.7.0 moved to ``vocabulary.uncefact.org/untp/0.7.0/context/``. We treat +# the patch level on the v0.6 side as flexible because the same shape +# applies to 0.6.0 and 0.6.1. +_V06_CONTEXT_RE = re.compile( + r"^https://test\.uncefact\.org/vocabulary/untp/dpp/0\.6(?:\.\d+)?/?$", +) +_V07_CONTEXT_URL = "https://vocabulary.uncefact.org/untp/0.7.0/context/" + +# v0.7.0 ``Image`` requires ``name``, ``imageData``, ``mediaType``. The shim +# uses these defaults when upgrading a v0.6 ``Material.symbol`` (which was +# a bare base64 string) — image bytes carry the data, the other two +# fields are synthesised and surfaced via UPG002. +_DEFAULT_IMAGE_MEDIA_TYPE = "image/png" +_DEFAULT_IMAGE_NAME = "Material symbol" + +# Embedded objects whose v0.6 wire shape carried a ``type: [...]`` +# discriminator that v0.7 dropped. Stripping these keeps strict-mode +# validation against the v0.7 schema clean. +_TYPES_TO_STRIP_ON: frozenset[str] = frozenset( + { + # Product-level container keys whose embedded objects no longer + # carry a ``type`` discriminator in v0.7 (Dimension, Characteristics). + "dimensions", + "characteristics", + # Measure-shaped sub-fields under Dimension and Performance.metricValue. + "weight", + "length", + "width", + "height", + "volume", + "mass", + "measure", + "thresholdValue", + "metricValue", + # Claim-shape descriptor (we keep the top-level Claim type but drop + # type arrays on lifted scorecard claims to match the v0.7 schema). + "claim", + } +) + +# Fields where Material.symbol may sit. v0.6 used a single string; v0.7 +# expects an Image object. Sentinel placeholders that are clearly not +# real base64 (e.g. "undefined" in our own legacy fixtures) are dropped +# rather than smuggled through as fake image bytes. +_KNOWN_SYMBOL_PLACEHOLDERS: frozenset[str] = frozenset({"undefined", "null", "none", ""}) + + +# ============================================================================= +# Public entry point +# ============================================================================= + + +def upgrade( + data: dict[str, Any], + *, + country_lookup: Mapping[str, str] | None = None, +) -> tuple[dict[str, Any], list[UpgradeWarning]]: + """Upgrade a UNTP DPP v0.6.x payload to v0.7.0 shape. + + Args: + data: The v0.6.x payload as a parsed JSON ``dict``. The input is + deep-copied; the caller's object is never mutated. + country_lookup: Optional ISO-3166-1 alpha-2 → country-name map. + When supplied, scalar country codes (e.g. ``"DE"``) are + wrapped as ``{countryCode: "DE", countryName: "Germany"}`` + using this mapping. When omitted, only ``countryCode`` is + populated and a UPG002 warning fires per wrapped scalar. + The bundled :data:`get_bundled_country_codes` set is always + used as the validity check (UPG003 fires for unknown codes). + + Returns: + Tuple of ``(upgraded_dict, warnings)``. The dict is ready to be + validated against the v0.7.0 model. Warnings are ordered by + the transformation step that emitted them, then by document + position. + + The 17 transformation steps execute in the order documented in the + migration plan. Each step is a private helper; the entry point is + this thin orchestrator. + """ + if not isinstance(data, dict): + raise TypeError( + f"upgrade() requires a dict, got {type(data).__name__!r}", + ) + + upgraded = deepcopy(data) + warnings: list[UpgradeWarning] = [] + valid_codes = get_bundled_country_codes() + + # Step 1 — context URL substitution. + _step1_replace_context(upgraded, warnings) + + # Step 3 — drop ProductPassport envelope (run before steps 2/4-13 so + # the rest of the pipeline operates on the flattened Product). Steps + # 4 (materialsProvenance), 5 (dueDiligenceDeclaration), 6 + # (conformityClaim), 7 (scorecards), and granularityLevel migration + # all read from the original ProductPassport envelope and write to + # the new credentialSubject (now a Product). + _step3_flatten_envelope(upgraded, warnings) + + # Step 2 — envelope ``name`` / ``validFrom`` (synthesise if absent). + # Runs after step 3 so we can synthesise ``name`` from + # ``credentialSubject.name`` (which by now is the product's name). + _step2_envelope_required_fields(upgraded, warnings) + + cs = upgraded.get("credentialSubject") + if not isinstance(cs, dict): + # No product to upgrade further — return what we have. + return upgraded, warnings + + # Steps 10–17 act on the credentialSubject (now a Product). + _step10_wrap_product_category(cs, warnings) + _step11_party_to_related_party(cs, warnings) + _step12_further_information_to_related_document(cs, warnings) + _step13_drop_registered_id(cs, warnings) + _step9_wrap_country_codes(cs, warnings, country_lookup, valid_codes) + _step14_material_symbol_to_image(cs, warnings) + _step17_material_required_fields(cs, warnings) + _step8_wrap_claim_references(cs, warnings) + _step16_rename_scheme_id(cs, warnings) + _step15_strip_embedded_types(cs, warnings) + + return upgraded, warnings + + +# ============================================================================= +# Step 1 — context URL substitution +# ============================================================================= + + +def _step1_replace_context( + data: dict[str, Any], + warnings: list[UpgradeWarning], # noqa: ARG001 — uniform signature; no warnings emitted +) -> None: + """Swap the v0.6 UNTP context URL for the v0.7.0 one. + + The W3C VC v2 context entry is left untouched. Anything that doesn't + look like a v0.6.x UNTP context URL is also left as-is — third-party + extensions plug into the context list, and we shouldn't smuggle + their references away. + """ + ctx = data.get("@context") + if not isinstance(ctx, list): + return + for i, entry in enumerate(ctx): + if isinstance(entry, str) and _V06_CONTEXT_RE.match(entry): + ctx[i] = _V07_CONTEXT_URL + + +# ============================================================================= +# Step 2 — envelope required fields +# ============================================================================= + + +def _step2_envelope_required_fields(data: dict[str, Any], warnings: list[UpgradeWarning]) -> None: + """Ensure the envelope has the v0.7-required ``name`` and ``validFrom``. + + ``name`` becomes a top-level required field in v0.7 (was implicit + via the product). ``validFrom`` was already common but the schema + elevates it to required — defensively add a placeholder if missing + so the result at least has a parsable shape. + """ + if not data.get("name"): + cs = data.get("credentialSubject") + synthesised: str | None = None + if isinstance(cs, dict): + cs_name = cs.get("name") + if isinstance(cs_name, str) and cs_name: + synthesised = cs_name + if synthesised: + data["name"] = synthesised + warnings.append( + UpgradeWarning( + code=UPG_CODE_SYNTHESISED, + path="name", + message=( + "Top-level ``name`` missing in v0.6 payload; " + "synthesised from credentialSubject.name." + ), + ), + ) + else: + warnings.append( + UpgradeWarning( + code=UPG_CODE_REQUIRED_FIELD_MISSING, + path="name", + message=( + "Top-level ``name`` is required in v0.7.0 and could " + "not be synthesised — provide manually." + ), + severity=UpgradeSeverity.ERROR, + ), + ) + + if not data.get("validFrom"): + warnings.append( + UpgradeWarning( + code=UPG_CODE_REQUIRED_FIELD_MISSING, + path="validFrom", + message=( + "Envelope ``validFrom`` is required in v0.7.0 and was " + "absent in the source payload." + ), + severity=UpgradeSeverity.ERROR, + ), + ) + + +# ============================================================================= +# Step 3 — drop ProductPassport envelope +# ============================================================================= + + +def _step3_flatten_envelope(data: dict[str, Any], warnings: list[UpgradeWarning]) -> None: + """Replace ``credentialSubject`` (the ProductPassport) with its product. + + The v0.6 ``ProductPassport`` carried siblings of the product + (``conformityClaim``, scorecards, ``materialsProvenance``, + ``dueDiligenceDeclaration``, ``granularityLevel``). v0.7 hangs all + of those off the Product itself. We move them in this step so later + steps can operate on a single flat object. + + ``granularityLevel`` becomes ``idGranularity`` here. + """ + cs = data.get("credentialSubject") + if not isinstance(cs, dict): + return + + pp_types = cs.get("type") + is_product_passport = ( + isinstance(pp_types, list) and "ProductPassport" in pp_types + ) or "product" in cs + + if not is_product_passport: + # Already flat — nothing to do. + return + + product = cs.get("product") + if not isinstance(product, dict): + # ProductPassport with no product — wrap an empty product so the + # rest of the pipeline has a target to write into. + product = {} + + # Carry siblings from ProductPassport onto the new credentialSubject. + granularity = cs.get("granularityLevel") + materials_provenance = cs.get("materialsProvenance") + conformity_claim = cs.get("conformityClaim") + emissions_scorecard = cs.get("emissionsScorecard") + circularity_scorecard = cs.get("circularityScorecard") + traceability_information = cs.get("traceabilityInformation") + due_diligence = cs.get("dueDiligenceDeclaration") + pp_id = cs.get("id") + + new_cs: dict[str, Any] = dict(product) + new_cs["type"] = ["Product"] + # Field rename: ``serialNumber`` (v0.6) → ``itemNumber`` (v0.7). + if "serialNumber" in new_cs and "itemNumber" not in new_cs: + new_cs["itemNumber"] = new_cs.pop("serialNumber") + if isinstance(granularity, str): + new_cs.setdefault("idGranularity", granularity) + if isinstance(materials_provenance, list): + new_cs["materialProvenance"] = materials_provenance + # Preserve the ProductPassport.id if the inner product had none — it + # was the credential-subject identifier in v0.6. + if pp_id and not new_cs.get("id"): + new_cs["id"] = pp_id + + performance_claim: list[dict[str, Any]] = [] + # Step 6 — conformityClaim → performanceClaim (kept whole here; field + # renames inside each Claim happen in step 8/16/15 below). + if isinstance(conformity_claim, list): + performance_claim.extend(_v06_claim_to_v07_claim(c) for c in conformity_claim) + + # Step 7 — scorecards → Claim entries. + if isinstance(emissions_scorecard, dict): + performance_claim.append( + _scorecard_to_claim( + topic="emissions", + scorecard=emissions_scorecard, + topic_name="Emissions", + ), + ) + if isinstance(circularity_scorecard, dict): + performance_claim.append( + _scorecard_to_claim( + topic="circularity", + scorecard=circularity_scorecard, + topic_name="Circularity", + ), + ) + if isinstance(traceability_information, list): + for entry in traceability_information: + if isinstance(entry, dict): + performance_claim.append( + _scorecard_to_claim( + topic="traceability", + scorecard=entry, + topic_name="Traceability", + ), + ) + + if performance_claim: + new_cs["performanceClaim"] = performance_claim + + # Step 5 — dueDiligenceDeclaration → relatedDocument[]. + if isinstance(due_diligence, dict): + related = list(new_cs.get("relatedDocument") or []) + link = dict(due_diligence) + link.setdefault("name", "Due diligence declaration") + related.append(link) + new_cs["relatedDocument"] = related + + data["credentialSubject"] = new_cs + + if conformity_claim or emissions_scorecard or circularity_scorecard or traceability_information: + warnings.append( + UpgradeWarning( + code=UPG_CODE_LOSSY, + path="credentialSubject.performanceClaim", + message=( + "v0.6 conformityClaim and scorecard arrays were folded into " + "v0.7 performanceClaim. Inner field shapes were rewritten " + "best-effort; review for fidelity." + ), + severity=UpgradeSeverity.WARNING, + ), + ) + + +def _v06_claim_to_v07_claim(claim: dict[str, Any]) -> dict[str, Any]: + """Rewire a single v0.6 ``Claim`` into v0.7 shape. + + Field renames (handled here, not in shared helpers, because they're + Claim-specific): + + - ``assessmentDate`` → ``claimDate`` + - ``assessmentCriteria`` (list[Criterion]) → ``referenceCriteria`` (list[dict]) + - ``declaredValue`` (list[Metric]) → ``claimedPerformance`` (list[Performance]) + - ``conformityTopic`` (str) → ``conformityTopic`` (list[ConformityTopic]) + - ``conformityEvidence`` (SecureLink) → ``evidence`` ([Link]) + - ``referenceStandard`` (Standard) → ``referenceStandard`` (list[dict]) + - ``referenceRegulation`` (Regulation) → ``referenceRegulation`` (list[dict]) + - ``conformance`` (bool) — dropped (no v0.7 equivalent at top level) + + The shape rewrite is best-effort: ``Performance`` requires a + ``metric`` plus at least one of ``measure``/``score``, so v0.6 + ``Metric`` rows get split (``metricName`` → ``metric.name``, + ``metricValue`` → ``measure``, ``score`` → ``score.code``). + """ + out: dict[str, Any] = {"type": ["Claim"]} + + # Pass-through fields. + for k in ("id", "description"): + if k in claim: + out[k] = claim[k] + # ``name`` is required in v0.7 Claim — fall back to description if absent. + name = claim.get("name") or claim.get("description") + if name: + out["name"] = name + + # Date rename. + if "assessmentDate" in claim: + out["claimDate"] = claim["assessmentDate"] + + # Criteria rename. + criteria = claim.get("assessmentCriteria") + if isinstance(criteria, list): + out["referenceCriteria"] = [_clean_v06_object(c) for c in criteria if isinstance(c, dict)] + + # Standard / Regulation: scalar → single-element list (step 8). + if isinstance(claim.get("referenceStandard"), dict): + out["referenceStandard"] = [claim["referenceStandard"]] + elif isinstance(claim.get("referenceStandard"), list): + out["referenceStandard"] = claim["referenceStandard"] + if isinstance(claim.get("referenceRegulation"), dict): + out["referenceRegulation"] = [claim["referenceRegulation"]] + elif isinstance(claim.get("referenceRegulation"), list): + out["referenceRegulation"] = claim["referenceRegulation"] + + # Performance readings. + declared = claim.get("declaredValue") + if isinstance(declared, list): + perf = [_v06_metric_to_v07_performance(m) for m in declared if isinstance(m, dict)] + if perf: + out["claimedPerformance"] = perf + + # Conformity topic. + topic = claim.get("conformityTopic") + if isinstance(topic, str) and topic: + out["conformityTopic"] = [{"type": ["ConformityTopic"], "name": topic, "id": topic}] + elif isinstance(topic, list): + out["conformityTopic"] = topic + + # Evidence link. + evidence = claim.get("conformityEvidence") + if isinstance(evidence, dict): + out["evidence"] = [evidence] + + return out + + +def _v06_metric_to_v07_performance(metric: dict[str, Any]) -> dict[str, Any]: + """Turn a v0.6 ``Metric`` into a v0.7 ``Performance``.""" + perf: dict[str, Any] = { + "metric": { + "type": ["PerformanceMetric"], + "name": metric.get("metricName") or "", + }, + } + metric_value = metric.get("metricValue") + if isinstance(metric_value, dict): + perf["measure"] = metric_value + score = metric.get("score") + if score: + perf["score"] = {"code": str(score)} + return perf + + +def _scorecard_to_claim( + *, topic: str, scorecard: dict[str, Any], topic_name: str +) -> dict[str, Any]: + """Materialise a v0.6 scorecard as a v0.7 ``Claim``. + + The conversion is structural: each numeric field on the scorecard + becomes a ``Performance`` entry with the field name as the metric + label. Free-form Link fields (e.g. ``recyclingInformation``) are + promoted to ``evidence`` entries. + """ + perfs: list[dict[str, Any]] = [] + evidence: list[dict[str, Any]] = [] + for k, v in scorecard.items(): + if k in {"type", "reportingStandard"}: + continue + if isinstance(v, (int, float)): + # Carbon footprints, recyclable-content fractions, MCI, etc. + perfs.append( + { + "metric": {"type": ["PerformanceMetric"], "name": k}, + "measure": {"value": float(v), "unit": "C62"}, # dimensionless + }, + ) + elif isinstance(v, dict) and ("linkURL" in v or "href" in v): + link = dict(v) + link.setdefault("name", k) + evidence.append(link) + + claim: dict[str, Any] = { + "type": ["Claim"], + "id": f"urn:dppvalidator:upgrade:{topic}-claim", + "name": f"{topic_name} (upgraded from v0.6 scorecard)", + "description": ( + f"Synthesised from the v0.6 {topic} scorecard. Field-by-field " + "fidelity is best-effort; review before publishing." + ), + "conformityTopic": [ + { + "type": ["ConformityTopic"], + "id": f"https://vocabulary.uncefact.org/conformity-topic/{topic}", + "name": topic_name, + }, + ], + } + if perfs: + claim["claimedPerformance"] = perfs + if evidence: + claim["evidence"] = evidence + reporting_standard = scorecard.get("reportingStandard") + if isinstance(reporting_standard, dict): + claim["referenceStandard"] = [reporting_standard] + return claim + + +def _clean_v06_object(obj: dict[str, Any]) -> dict[str, Any]: + """Pass through a v0.6 reference object largely untouched. + + Steps 15 (strip type) and 16 (rename schemeID) walk the whole tree + later, so we don't need to recurse here. The reason this helper + exists at all is to ensure we get a *new* dict reference (not a + shared mutable from the input), which keeps the deep-copy + invariant tight. + """ + return dict(obj) + + +# ============================================================================= +# Step 8 — wrap scalar Standard / Regulation into single-element lists +# ============================================================================= + + +def _step8_wrap_claim_references( + cs: dict[str, Any], + warnings: list[UpgradeWarning], # noqa: ARG001 — uniform signature; no warnings emitted +) -> None: + """Ensure ``referenceStandard`` / ``referenceRegulation`` are lists. + + Step 6 already wrapped scalars during the conformityClaim → performanceClaim + rewrite. This step is the safety net for performanceClaim entries + that *originated* in 0.7 shape (e.g. mixed payloads or already + upgraded data run through the shim a second time). + """ + claims = cs.get("performanceClaim") + if not isinstance(claims, list): + return + for claim in claims: + if not isinstance(claim, dict): + continue + for key in ("referenceStandard", "referenceRegulation"): + value = claim.get(key) + if isinstance(value, dict): + claim[key] = [value] + + +# ============================================================================= +# Step 9 — wrap scalar country codes +# ============================================================================= + + +def _step9_wrap_country_codes( + cs: dict[str, Any], + warnings: list[UpgradeWarning], + country_lookup: Mapping[str, str] | None, + valid_codes: frozenset[str], +) -> None: + """Wrap ISO-2 strings into ``{countryCode, countryName}`` objects. + + Walks ``credentialSubject.countryOfProduction`` (Product-level) and + every ``Material.originCountry`` under ``materialProvenance``. + + - If the code is not in :data:`get_bundled_country_codes`, emit + ``UPG003`` and still wrap (the v0.7 model regex will catch it + downstream, but the structural shape is at least correct). + - If the caller supplied a ``country_lookup`` map and the code + resolves, populate ``countryName``. Otherwise leave it unset and + emit ``UPG002`` so the caller can fill it in manually. + """ + _wrap_country_at( + cs, "countryOfProduction", "credentialSubject", warnings, country_lookup, valid_codes + ) + materials = cs.get("materialProvenance") + if isinstance(materials, list): + for i, m in enumerate(materials): + if isinstance(m, dict): + _wrap_country_at( + m, + "originCountry", + f"credentialSubject.materialProvenance[{i}]", + warnings, + country_lookup, + valid_codes, + ) + + +def _wrap_country_at( + obj: dict[str, Any], + field: str, + path: str, + warnings: list[UpgradeWarning], + country_lookup: Mapping[str, str] | None, + valid_codes: frozenset[str], +) -> None: + """Wrap a scalar at ``obj[field]`` into a Country object in place.""" + value = obj.get(field) + if not isinstance(value, str): + # Already an object, missing, or a list — nothing to do. + return + code = value.strip().upper() + out: dict[str, Any] = {"countryCode": code} + if code not in valid_codes: + warnings.append( + UpgradeWarning( + code=UPG_CODE_UNMAPPED_COUNTRY, + path=f"{path}.{field}", + message=( + f"Country code {code!r} is not in the bundled ISO-3166-1 " + "alpha-2 list — wrapped structurally but the value will " + "fail v0.7 validation." + ), + ), + ) + elif country_lookup is not None and code in country_lookup: + out["countryName"] = country_lookup[code] + else: + warnings.append( + UpgradeWarning( + code=UPG_CODE_SYNTHESISED, + path=f"{path}.{field}.countryName", + message=( + "Country wrapped without ``countryName`` — pass a " + "country_lookup mapping to populate it." + ), + severity=UpgradeSeverity.INFO, + ), + ) + obj[field] = out + + +# ============================================================================= +# Step 10 — wrap Product.productCategory: Classification into a list +# ============================================================================= + + +def _step10_wrap_product_category( + cs: dict[str, Any], + warnings: list[UpgradeWarning], # noqa: ARG001 — uniform signature; no warnings emitted +) -> None: + """``productCategory`` was scalar in 0.6, list in 0.7.""" + pc = cs.get("productCategory") + if isinstance(pc, dict): + cs["productCategory"] = [pc] + + +# ============================================================================= +# Step 11 — Product.producedByParty → relatedParty[] +# ============================================================================= + + +def _step11_party_to_related_party( + cs: dict[str, Any], + warnings: list[UpgradeWarning], # noqa: ARG001 — uniform signature; no warnings emitted +) -> None: + """Wrap ``producedByParty: Party`` into ``relatedParty: [PartyRole]``. + + The role is fixed to ``"manufacturer"`` because that's the implied + relationship of a producedByParty in v0.6. We append rather than + overwrite ``relatedParty`` to be safe with mixed-source payloads. + """ + party = cs.pop("producedByParty", None) + if not isinstance(party, dict): + return + related = list(cs.get("relatedParty") or []) + related.append({"role": "manufacturer", "party": party}) + cs["relatedParty"] = related + + +# ============================================================================= +# Step 12 — Product.furtherInformation → relatedDocument[] +# ============================================================================= + + +def _step12_further_information_to_related_document( + cs: dict[str, Any], + warnings: list[UpgradeWarning], # noqa: ARG001 — uniform signature; no warnings emitted +) -> None: + """Append ``furtherInformation`` links to ``relatedDocument``.""" + further = cs.pop("furtherInformation", None) + if not further: + return + related = list(cs.get("relatedDocument") or []) + if isinstance(further, list): + related.extend(further) + elif isinstance(further, dict): + related.append(further) + cs["relatedDocument"] = related + + +# ============================================================================= +# Step 13 — drop Product.registeredId +# ============================================================================= + + +def _step13_drop_registered_id(cs: dict[str, Any], warnings: list[UpgradeWarning]) -> None: + """``registeredId`` moves from Product to Party in v0.7.""" + if "registeredId" in cs: + cs.pop("registeredId") + warnings.append( + UpgradeWarning( + code=UPG_CODE_LOSSY, + path="credentialSubject.registeredId", + message=( + "v0.6 Product.registeredId has no v0.7 equivalent on " + "Product — the field has moved to Party. The value was " + "dropped; if required, re-attach it to " + "Product.relatedParty[*].party.registeredId manually." + ), + ), + ) + + +# ============================================================================= +# Step 14 — Material.symbol base64 string → Image object +# ============================================================================= + + +def _step14_material_symbol_to_image(cs: dict[str, Any], warnings: list[UpgradeWarning]) -> None: + """Convert the inline base64 ``symbol`` string to a v0.7 ``Image``. + + Sentinel placeholders (``"undefined"`` and friends) are dropped — + smuggling them through as fake image bytes would just trip the v0.7 + schema downstream. + """ + materials = cs.get("materialProvenance") + if not isinstance(materials, list): + return + for i, m in enumerate(materials): + if not isinstance(m, dict): + continue + symbol = m.get("symbol") + if not isinstance(symbol, str): + continue + if symbol.lower() in _KNOWN_SYMBOL_PLACEHOLDERS: + m.pop("symbol", None) + warnings.append( + UpgradeWarning( + code=UPG_CODE_LOSSY, + path=f"credentialSubject.materialProvenance[{i}].symbol", + message=( + f"Material.symbol placeholder {symbol!r} dropped during " + "upgrade — provide a real Image object if needed." + ), + severity=UpgradeSeverity.INFO, + ), + ) + continue + if not _looks_like_base64(symbol): + m.pop("symbol", None) + warnings.append( + UpgradeWarning( + code=UPG_CODE_LOSSY, + path=f"credentialSubject.materialProvenance[{i}].symbol", + message=( + "Material.symbol value did not look like base64 — " + "dropped during upgrade. Provide a v0.7 Image object." + ), + ), + ) + continue + m["symbol"] = { + "type": ["Image"], + "name": _DEFAULT_IMAGE_NAME, + "imageData": symbol, + "mediaType": _DEFAULT_IMAGE_MEDIA_TYPE, + } + warnings.append( + UpgradeWarning( + code=UPG_CODE_SYNTHESISED, + path=f"credentialSubject.materialProvenance[{i}].symbol", + message=( + "Material.symbol upgraded to v0.7 Image with synthesised " + f"name={_DEFAULT_IMAGE_NAME!r} and " + f"mediaType={_DEFAULT_IMAGE_MEDIA_TYPE!r}." + ), + ), + ) + + +def _looks_like_base64(value: str) -> bool: + """Heuristic: would this string decode as base64? + + We tolerate URL-safe variants and strip whitespace before checking. + Anything under 16 characters is treated as not-an-image to weed out + short sentinels. This is intentionally conservative — false negatives + just route the value into the lossy-drop path with a UPG001 warning. + """ + s = value.strip() + if len(s) < 16: + return False + try: + base64.b64decode(s, validate=True) + except (ValueError, base64.binascii.Error): # type: ignore[attr-defined] + return False + return True + + +# ============================================================================= +# Step 15 — strip type arrays on embedded objects +# ============================================================================= + + +def _step15_strip_embedded_types( + cs: dict[str, Any], + warnings: list[UpgradeWarning], # noqa: ARG001 — uniform signature; no warnings emitted +) -> None: + """Strip ``type`` from embedded objects whose v0.7 schema dropped it. + + Walks the credential subject tree and removes the ``type`` field on + any sub-object whose key matches :data:`_TYPES_TO_STRIP_ON`. The + Product-level ``type`` (``["Product"]``) is preserved. + """ + _strip_types_recursive(cs, parent_key=None) + + +def _strip_types_recursive(node: Any, parent_key: str | None) -> None: + if isinstance(node, dict): + if parent_key in _TYPES_TO_STRIP_ON and "type" in node: + node.pop("type", None) + for k, v in node.items(): + _strip_types_recursive(v, k) + elif isinstance(node, list): + for item in node: + _strip_types_recursive(item, parent_key) + + +# ============================================================================= +# Step 16 — rename Classification.schemeID → schemeId +# ============================================================================= + + +def _step16_rename_scheme_id( + cs: dict[str, Any], + warnings: list[UpgradeWarning], # noqa: ARG001 — uniform signature; no warnings emitted +) -> None: + """Rename ``schemeID`` (v0.6) to ``schemeId`` (v0.7) everywhere. + + Walks the credential subject tree without restriction — Classification + objects can hide under ``materialType``, ``productCategory[*]``, + ``performanceClaim[*].referenceCriteria[*].category[*]``, and so on. + A blind rename is correct because no other v0.6 schema uses the + uppercase spelling. + """ + _rename_key_recursive(cs, "schemeID", "schemeId") + + +def _rename_key_recursive(node: Any, old: str, new: str) -> None: + if isinstance(node, dict): + if old in node: + node[new] = node.pop(old) + for v in node.values(): + _rename_key_recursive(v, old, new) + elif isinstance(node, list): + for item in node: + _rename_key_recursive(item, old, new) + + +# ============================================================================= +# Step 17 — Material required-field detection +# ============================================================================= + + +def _step17_material_required_fields(cs: dict[str, Any], warnings: list[UpgradeWarning]) -> None: + """Emit UPG004 per Material missing ``materialType`` or ``massFraction``. + + These were optional in v0.6 and are required in v0.7. We don't + fabricate them — the caller has to supply real values before the + upgraded payload can validate. + """ + materials = cs.get("materialProvenance") + if not isinstance(materials, list): + return + for i, m in enumerate(materials): + if not isinstance(m, dict): + continue + if not m.get("materialType"): + warnings.append( + UpgradeWarning( + code=UPG_CODE_REQUIRED_FIELD_MISSING, + path=f"credentialSubject.materialProvenance[{i}].materialType", + message=( + "Material.materialType is required in v0.7.0. The " + "v0.6 source did not supply one; provide a " + "Classification object before validating." + ), + severity=UpgradeSeverity.ERROR, + ), + ) + if "massFraction" not in m or m.get("massFraction") is None: + warnings.append( + UpgradeWarning( + code=UPG_CODE_REQUIRED_FIELD_MISSING, + path=f"credentialSubject.materialProvenance[{i}].massFraction", + message=( + "Material.massFraction is required in v0.7.0. The " + "v0.6 source did not supply one; provide a numeric " + "value before validating." + ), + severity=UpgradeSeverity.ERROR, + ), + ) diff --git a/src/dppvalidator/exporters/contexts.py b/src/dppvalidator/exporters/contexts.py index 6b36c4f..80e697a 100644 --- a/src/dppvalidator/exporters/contexts.py +++ b/src/dppvalidator/exporters/contexts.py @@ -31,9 +31,21 @@ class ContextDefinition: ), default_type=("DigitalProductPassport", "VerifiableCredential"), ), + # UNTP 0.7.0 unifies DPP, DCC, DFR, DIA and DTE under one context served + # at vocabulary.uncefact.org/untp/0.7.0/context/. The DigitalProductPassport + # type itself is unchanged in name; the credentialSubject envelope is what + # restructures (see docs/plans/UNTP_0.7.0_MIGRATION.md §2.3). + "0.7.0": ContextDefinition( + version="0.7.0", + contexts=( + "https://www.w3.org/ns/credentials/v2", + "https://vocabulary.uncefact.org/untp/0.7.0/context/", + ), + default_type=("DigitalProductPassport", "VerifiableCredential"), + ), } -DEFAULT_VERSION = "0.6.1" +DEFAULT_VERSION = "0.6.1" # Phase 9 will flip this to "0.7.0" in dppvalidator 0.5.0. class ContextManager: diff --git a/src/dppvalidator/exporters/eudpp_jsonld.py b/src/dppvalidator/exporters/eudpp_jsonld.py index 33277f5..98d8dae 100644 --- a/src/dppvalidator/exporters/eudpp_jsonld.py +++ b/src/dppvalidator/exporters/eudpp_jsonld.py @@ -16,6 +16,7 @@ from typing import TYPE_CHECKING, Any from dppvalidator.logging import get_logger +from dppvalidator.schemas.registry import DEFAULT_SCHEMA_VERSION, SCHEMA_REGISTRY from dppvalidator.vocabularies.ontology import ( TERM_MAPPINGS, EUDPPNamespace, @@ -62,25 +63,52 @@ def get_eudpp_jsonld_context() -> list[Any]: class EUDPPTermMapper: """Map UNTP terms to EU DPP Core Ontology terms. - Provides bidirectional mapping between UNTP vocabulary and - EU DPP Core Ontology vocabulary for JSON-LD export. + Phase 3c of docs/plans/UNTP_0.7.0_MIGRATION.md added a ``schema_version`` + parameter so the mapper picks the right column out of + :data:`TERM_MAPPINGS`. The default value (:data:`DEFAULT_SCHEMA_VERSION`) + preserves the pre-Phase-3c behaviour for callers that don't pass an + explicit version. Note: the dispatch is purely a forward-mapping concern + — the EU DPP target URI is the same across UNTP versions; only the + source-side spelling (e.g. ``itemNumber`` vs ``serialNumber``) shifts. + + Terms removed in a given version (carrying the :data:`TERM_REMOVED` + sentinel for that column) are excluded from that version's mapper — + e.g. ``gtin`` does not appear in a v0.7 mapper's index because v0.7 + has no ``gtin`` field on the wire. """ - def __init__(self) -> None: - """Initialize term mapper.""" + def __init__(self, schema_version: str = DEFAULT_SCHEMA_VERSION) -> None: + """Initialize term mapper. + + Args: + schema_version: UNTP version whose source-side spellings to + index. Defaults to :data:`DEFAULT_SCHEMA_VERSION` for + backward compatibility with the Phase 3c-pre API. + """ + self.schema_version = schema_version self._mapper = OntologyMapper() self._untp_to_eudpp: dict[str, str] = {} self._eudpp_to_untp: dict[str, str] = {} for mapping in TERM_MAPPINGS: - # Extract local name from compact URI (e.g., "eudpp:Product" -> "Product") + term = mapping.term_for(schema_version) + if term is None: + # Skip rows whose ``untp_v0_X`` is TERM_REMOVED for this + # version — there's no source-side spelling to map from. + continue + # Extract local name from compact URI (e.g. ``eudpp:Product`` → + # ``Product``). eudpp_local = ( mapping.cirpass_uri.split(":")[-1] if ":" in mapping.cirpass_uri else mapping.cirpass_uri ) - self._untp_to_eudpp[mapping.untp_term] = eudpp_local - self._eudpp_to_untp[eudpp_local] = mapping.untp_term + self._untp_to_eudpp[term] = eudpp_local + # The reverse index is "last write wins" — multiple UNTP + # spellings can resolve to the same eudpp_local across the + # canonical and per-version columns; this matches the v0.6 + # default-mapper behaviour pre-Phase-3c. + self._eudpp_to_untp[eudpp_local] = term def map_key(self, untp_key: str) -> str: """Map a UNTP key to EU DPP equivalent. @@ -136,6 +164,13 @@ class EUDPPJsonLDExporter: vocabulary while preserving the original data structure. The UNTP models remain unchanged; only the export representation uses EU DPP terms. + Phase 3c of docs/plans/UNTP_0.7.0_MIGRATION.md added a + ``schema_version`` argument so the exporter dispatches to the right + column of :data:`TERM_MAPPINGS`. When omitted, it picks a sensible + default by inspecting the source passport class — v0.7 passports get + the v0.7 mapper, v0.6 passports get the v0.6 mapper. Callers that + pin an explicit version override that auto-detection. + Example: >>> exporter = EUDPPJsonLDExporter() >>> jsonld = exporter.export(passport) @@ -144,6 +179,9 @@ class EUDPPJsonLDExporter: Attributes: include_untp_context: Include UNTP context alongside EU DPP map_terms: Apply term mapping (UNTP → EU DPP) + schema_version: UNTP source version. When ``None``, auto-detected + from the passport's module path the first time + :meth:`export_dict` is called. """ def __init__( @@ -151,16 +189,87 @@ def __init__( *, include_untp_context: bool = False, map_terms: bool = True, + schema_version: str | None = None, ) -> None: """Initialize EU DPP exporter. Args: include_untp_context: Include UNTP context in output map_terms: Map UNTP terms to EU DPP equivalents + schema_version: UNTP source version, e.g. v0.7.0 written as a + SemVer string. When ``None``, the version is detected from + the passport class at export time. Pass an explicit version + when you need deterministic dispatch (e.g. when the exporter + is reused across calls with mixed-version inputs). """ self._include_untp = include_untp_context self._map_terms = map_terms - self._term_mapper = EUDPPTermMapper() + self._explicit_version = schema_version + # Cache of EUDPPTermMapper instances keyed on the resolved version. + # We don't build a default mapper eagerly because the version may + # only become known when ``export`` is called. + self._term_mapper_cache: dict[str, EUDPPTermMapper] = {} + # Eagerly populate the cache when the caller pinned a version. + if schema_version is not None: + self._term_mapper_cache[schema_version] = EUDPPTermMapper( + schema_version=schema_version, + ) + + @property + def schema_version(self) -> str | None: + """Return the explicitly-configured version, or ``None`` for auto-detect.""" + return self._explicit_version + + @property + def _term_mapper(self) -> EUDPPTermMapper: + """Backward-compat alias. + + Pre-Phase-3c the exporter exposed a single ``self._term_mapper`` and + external code (notably the existing test suite) reaches in to read + it. Resolving on access keeps the lookup deterministic when the + caller pinned ``schema_version`` and falls back to the global + default otherwise. + """ + version = self._explicit_version or DEFAULT_SCHEMA_VERSION + return self._mapper_for(version) + + def _mapper_for(self, version: str) -> EUDPPTermMapper: + """Return (and cache) the term mapper for ``version``.""" + cached = self._term_mapper_cache.get(version) + if cached is None: + cached = EUDPPTermMapper(schema_version=version) + self._term_mapper_cache[version] = cached + return cached + + @staticmethod + def _detect_version_from_passport( + passport: DigitalProductPassport, + ) -> str: + """Best-effort detection of the source UNTP version. + + Looks at the passport class's module path (``v0_6`` or ``v0_7``) + against :data:`SCHEMA_REGISTRY`. Each registered version's + ``major.minor`` becomes a ``vMAJOR_MINOR`` namespace key — the + resolver picks the registered version whose namespace appears in + the module path. When two registry entries share the same module + namespace (e.g. 0.6.0 and 0.6.1 both live under ``v0_6``) the + highest-patch version wins; that matches the package-canonical + spelling for the namespace. + + Falls back to :data:`DEFAULT_SCHEMA_VERSION` for unrecognised + layouts (e.g. third-party subclasses outside the in-tree + version-namespaced packages). + """ + module = type(passport).__module__ + candidates: list[str] = [] + for version in SCHEMA_REGISTRY: + major_minor = ".".join(version.split(".")[:2]) + ns_dotted = f".v{major_minor.replace('.', '_')}" + if f"{ns_dotted}." in module or module.endswith(ns_dotted): + candidates.append(version) + if not candidates: + return DEFAULT_SCHEMA_VERSION + return max(candidates, key=lambda v: tuple(int(p) for p in v.split("."))) def export( self, @@ -192,6 +301,13 @@ def export_dict( Returns: EU DPP JSON-LD formatted dictionary """ + # Resolve which version's mapper to use. Explicit ``schema_version`` + # passed to the constructor wins; otherwise we auto-detect from the + # passport class's module path. This is what lets a single exporter + # instance serve mixed v0.6/v0.7 input without configuration. + version = self._explicit_version or self._detect_version_from_passport(passport) + mapper = self._mapper_for(version) + # Get base UNTP JSON-LD representation base = passport.model_dump(mode="json", by_alias=True, exclude_none=True) @@ -200,12 +316,12 @@ def export_dict( # Map terms if enabled if self._map_terms: - data = self._map_document_terms(data) + data = self._map_document_terms(data, mapper) # Add EU DPP-specific metadata data = self._add_eudpp_metadata(data, passport) - logger.debug("Exported DPP to EU DPP JSON-LD format") + logger.debug("Exported DPP to EU DPP JSON-LD format (version=%s)", version) return data def export_to_file( @@ -250,11 +366,19 @@ def _apply_eudpp_context(self, data: dict[str, Any]) -> dict[str, Any]: result["@context"] = context return result - def _map_document_terms(self, data: dict[str, Any]) -> dict[str, Any]: + def _map_document_terms( + self, + data: dict[str, Any], + mapper: EUDPPTermMapper | None = None, + ) -> dict[str, Any]: """Recursively map UNTP terms to EU DPP equivalents. Args: data: JSON-LD dictionary + mapper: The version-specific term mapper to use. Defaults to + the exporter's resolved mapper (Phase 3c added the + ``mapper`` parameter so :meth:`export_dict` can thread the + version it picked into the recursive walk). Returns: Dictionary with mapped terms @@ -262,6 +386,9 @@ def _map_document_terms(self, data: dict[str, Any]) -> dict[str, Any]: if not isinstance(data, dict): return data + if mapper is None: + mapper = self._term_mapper + result: dict[str, Any] = {} for key, value in data.items(): @@ -271,14 +398,14 @@ def _map_document_terms(self, data: dict[str, Any]) -> dict[str, Any]: continue # Map the key - mapped_key = self._term_mapper.map_key(key) + mapped_key = mapper.map_key(key) # Recursively process values if isinstance(value, dict): - result[mapped_key] = self._map_document_terms(value) + result[mapped_key] = self._map_document_terms(value, mapper) elif isinstance(value, list): result[mapped_key] = [ - self._map_document_terms(item) if isinstance(item, dict) else item + self._map_document_terms(item, mapper) if isinstance(item, dict) else item for item in value ] else: @@ -286,23 +413,31 @@ def _map_document_terms(self, data: dict[str, Any]) -> dict[str, Any]: # Map type values if "type" in result: - result["type"] = self._map_type_value(result["type"]) + result["type"] = self._map_type_value(result["type"], mapper) return result - def _map_type_value(self, type_value: Any) -> Any: + def _map_type_value( + self, + type_value: Any, + mapper: EUDPPTermMapper | None = None, + ) -> Any: """Map type values to EU DPP equivalents. Args: type_value: Type string or list + mapper: Term mapper to use; defaults to the exporter's resolved + mapper for backward compatibility with pre-Phase-3c calls. Returns: Mapped type value(s) """ + if mapper is None: + mapper = self._term_mapper if isinstance(type_value, str): - return self._term_mapper.map_type(type_value) + return mapper.map_type(type_value) elif isinstance(type_value, list): - return [self._term_mapper.map_type(t) if isinstance(t, str) else t for t in type_value] + return [mapper.map_type(t) if isinstance(t, str) else t for t in type_value] return type_value def _add_eudpp_metadata( @@ -321,12 +456,19 @@ def _add_eudpp_metadata( if "schemaVersion" not in data: data["schemaVersion"] = "CIRPASS-2 v1.3.0" - # Add status if credential subject has it - if passport.credential_subject: - cs = passport.credential_subject - # Add granularity level with EU DPP term - if hasattr(cs, "granularity_level") and cs.granularity_level: - data["granularity"] = cs.granularity_level + # Surface granularity at the document root with the EU DPP key. + # In v0.6.x this lived at ``credentialSubject.granularity_level``; + # in v0.7 it's ``credentialSubject.id_granularity`` (Product is + # the credential subject directly). Both attributes are checked so + # the metadata line works for either envelope without a version + # branch — see the ``term_for`` mapping in ontology.py. + cs = getattr(passport, "credential_subject", None) + if cs is not None: + granularity = getattr(cs, "granularity_level", None) or getattr( + cs, "id_granularity", None + ) + if granularity: + data["granularity"] = granularity return data @@ -341,6 +483,7 @@ def export_eudpp_jsonld( *, indent: int = 2, map_terms: bool = True, + schema_version: str | None = None, ) -> str: """Export a DPP to EU DPP-aligned JSON-LD format. @@ -351,11 +494,13 @@ def export_eudpp_jsonld( passport: Validated DigitalProductPassport indent: JSON indentation map_terms: Map UNTP terms to EU DPP equivalents + schema_version: UNTP source version. When ``None``, auto-detected + from the passport's class (Phase 3c). Returns: EU DPP JSON-LD formatted string """ - exporter = EUDPPJsonLDExporter(map_terms=map_terms) + exporter = EUDPPJsonLDExporter(map_terms=map_terms, schema_version=schema_version) return exporter.export(passport, indent=indent) @@ -363,6 +508,7 @@ def export_eudpp_jsonld_dict( passport: DigitalProductPassport, *, map_terms: bool = True, + schema_version: str | None = None, ) -> dict[str, Any]: """Export a DPP to EU DPP-aligned JSON-LD dictionary. @@ -371,11 +517,13 @@ def export_eudpp_jsonld_dict( Args: passport: Validated DigitalProductPassport map_terms: Map UNTP terms to EU DPP equivalents + schema_version: UNTP source version. When ``None``, auto-detected + from the passport's class (Phase 3c). Returns: EU DPP JSON-LD dictionary """ - exporter = EUDPPJsonLDExporter(map_terms=map_terms) + exporter = EUDPPJsonLDExporter(map_terms=map_terms, schema_version=schema_version) return exporter.export_dict(passport) @@ -416,11 +564,18 @@ def validate_eudpp_export(data: dict[str, Any]) -> list[str]: return issues -def get_term_mapping_summary() -> dict[str, str]: +def get_term_mapping_summary( + schema_version: str = DEFAULT_SCHEMA_VERSION, +) -> dict[str, str]: """Get a summary of UNTP to EU DPP term mappings. + Args: + schema_version: UNTP version to summarise (Phase 3c). Defaults to + :data:`DEFAULT_SCHEMA_VERSION` to preserve the pre-Phase-3c + output for callers that don't specify. + Returns: - Dictionary mapping UNTP terms to EU DPP terms + Dictionary mapping UNTP terms to EU DPP terms for ``schema_version``. """ - mapper = EUDPPTermMapper() + mapper = EUDPPTermMapper(schema_version=schema_version) return {term: mapper.map_key(term) for term in mapper.mapped_keys} diff --git a/src/dppvalidator/models/claims.py b/src/dppvalidator/models/claims.py index 92b7bc0..4515d61 100644 --- a/src/dppvalidator/models/claims.py +++ b/src/dppvalidator/models/claims.py @@ -1,164 +1,30 @@ -"""Claim and conformity-related models for UNTP DPP.""" +"""Backward-compatibility re-export of v0.6.x ``claims``. -from __future__ import annotations - -from datetime import date -from typing import Annotated, ClassVar - -from pydantic import Field - -from dppvalidator.models.base import UNTPBaseModel, UNTPStrictModel -from dppvalidator.models.enums import ConformityTopic, CriterionStatus -from dppvalidator.models.identifiers import Party -from dppvalidator.models.primitives import Classification, FlexibleUri, Measure, SecureLink - - -class Metric(UNTPStrictModel): - """Performance metric with value and optional score.""" - - _jsonld_type: ClassVar[list[str]] = ["Metric"] - - metric_name: Annotated[ - str, - Field(..., alias="metricName", description="Human readable metric name"), - ] - metric_value: Annotated[ - Measure, - Field(..., alias="metricValue", description="Numeric value and unit"), - ] - score: str | None = Field(default=None, description="Score or rank for this metric") - accuracy: float | None = Field( - default=None, - ge=0, - le=1, - description="Accuracy as percentage (0-1)", - ) - - -class Criterion(UNTPBaseModel): - """Specific rule or criterion within a standard or regulation.""" - - _jsonld_type: ClassVar[list[str]] = ["Criterion"] +The actual class definitions live in :mod:`dppvalidator.models.v0_6.claims`. +This shim preserves the import path used by third-party plugins (e.g. +``from dppvalidator.models.claims import Claim``) — see Phase 3 of +docs/plans/UNTP_0.7.0_MIGRATION.md and the public-API stability contract in +§7.6 of that plan. - id: Annotated[ - FlexibleUri, - Field(..., description="Unique identifier for the criterion"), - ] - name: str = Field(..., description="Criterion name") - description: str = Field(..., description="Full text description of the criterion") - conformity_topic: Annotated[ - ConformityTopic, - Field(..., alias="conformityTopic", description="Conformity topic category"), - ] - status: CriterionStatus = Field(..., description="Lifecycle status") - sub_criterion: Annotated[ - list[Criterion] | None, - Field(default=None, alias="subCriterion", description="Subordinate criteria"), - ] - threshold_value: Annotated[ - Metric | None, - Field(default=None, alias="thresholdValue", description="Minimum compliance threshold"), - ] - performance_level: Annotated[ - str | None, - Field(default=None, alias="performanceLevel", description="Performance category code"), - ] - category: Annotated[ - list[Classification] | None, - Field(default=None, description="Product categories the criterion applies to"), - ] - tag: Annotated[ - list[str] | None, - Field(default=None, description="Tags for stakeholder/commodity types"), - ] +Through the 0.4.x line this re-exports v0.6.x classes (the current default +schema version). Phase 9 (validator 0.5.0) will switch the default to v0.7 +and update this shim accordingly. +""" +from __future__ import annotations -class Standard(UNTPStrictModel): - """Standard that specifies conformance criteria (e.g., ISO 14000).""" - - _jsonld_type: ClassVar[list[str]] = ["Standard"] - - id: Annotated[ - FlexibleUri | None, - Field(default=None, description="Unique identifier for the standard"), - ] - name: str | None = Field(default=None, description="Name of the standard") - issuing_party: Annotated[ - Party, - Field(..., alias="issuingParty", description="Party that issued the standard"), - ] - issue_date: Annotated[ - date | None, - Field(default=None, alias="issueDate", description="Date the standard was issued"), - ] - - -class Regulation(UNTPStrictModel): - """Regulation that defines assessment criteria.""" - - _jsonld_type: ClassVar[list[str]] = ["Regulation"] - - id: Annotated[ - FlexibleUri | None, - Field(default=None, description="Globally unique identifier of the regulation"), - ] - name: str | None = Field(default=None, description="Name of the regulation or act") - jurisdiction_country: Annotated[ - str | None, - Field( - default=None, - alias="jurisdictionCountry", - description="ISO 3166-1 jurisdiction country code", - ), - ] - administered_by: Annotated[ - Party, - Field(..., alias="administeredBy", description="Issuing body of the regulation"), - ] - effective_date: Annotated[ - date | None, - Field(default=None, alias="effectiveDate", description="Date regulation came into effect"), - ] - - -class Claim(UNTPBaseModel): - """Declaration of conformance with standard or regulation criteria.""" - - _jsonld_type: ClassVar[list[str]] = ["Claim", "Declaration"] - - id: Annotated[ - FlexibleUri, - Field(..., description="Unique identifier for the declaration"), - ] - description: str | None = Field(default=None, description="Textual description of the claim") - reference_standard: Annotated[ - Standard | None, - Field(default=None, alias="referenceStandard", description="Reference standard"), - ] - reference_regulation: Annotated[ - Regulation | None, - Field(default=None, alias="referenceRegulation", description="Reference regulation"), - ] - assessment_criteria: Annotated[ - list[Criterion] | None, - Field(default=None, alias="assessmentCriteria", description="Assessment specifications"), - ] - assessment_date: Annotated[ - date | None, - Field(default=None, alias="assessmentDate", description="Date of assessment"), - ] - declared_value: Annotated[ - list[Metric] | None, - Field(default=None, alias="declaredValue", description="Measured values"), - ] - conformance: bool = Field(..., description="Whether the claim conforms to criteria") - conformity_topic: Annotated[ - ConformityTopic, - Field(..., alias="conformityTopic", description="Conformity topic category"), - ] - conformity_evidence: Annotated[ - SecureLink | None, - Field( - default=None, alias="conformityEvidence", description="Evidence supporting the claim" - ), - ] +from dppvalidator.models.v0_6.claims import ( + Claim, + Criterion, + Metric, + Regulation, + Standard, +) + +__all__ = [ + "Claim", + "Criterion", + "Metric", + "Regulation", + "Standard", +] diff --git a/src/dppvalidator/models/credential.py b/src/dppvalidator/models/credential.py index 2215e99..b1f2ff7 100644 --- a/src/dppvalidator/models/credential.py +++ b/src/dppvalidator/models/credential.py @@ -1,154 +1,26 @@ -"""Credential and ProductPassport models for UNTP DPP.""" +"""Backward-compatibility re-export of v0.6.x ``credential``. -from __future__ import annotations +The actual class definitions live in :mod:`dppvalidator.models.v0_6.credential`. +This shim preserves the import path used by third-party plugins (e.g. +``from dppvalidator.models.credential import CredentialIssuer``) — see Phase 3 of +docs/plans/UNTP_0.7.0_MIGRATION.md and the public-API stability contract in +§7.6 of that plan. -from typing import Annotated, ClassVar +Through the 0.4.x line this re-exports v0.6.x classes (the current default +schema version). Phase 9 (validator 0.5.0) will switch the default to v0.7 +and update this shim accordingly. +""" -from pydantic import Field +from __future__ import annotations -from dppvalidator.models.base import UNTPBaseModel, UNTPStrictModel -from dppvalidator.models.claims import Claim -from dppvalidator.models.enums import GranularityLevel -from dppvalidator.models.identifiers import Party -from dppvalidator.models.materials import Material -from dppvalidator.models.performance import ( - CircularityPerformance, - EmissionsPerformance, - TraceabilityPerformance, +from dppvalidator.models.v0_6.credential import ( + CredentialIssuer, + CredentialStatus, + ProductPassport, ) -from dppvalidator.models.primitives import FlexibleUri, Link -from dppvalidator.models.product import Product - - -class CredentialStatus(UNTPBaseModel): - """Credential status for revocation checking per W3C VC v2. - - Used to check if a credential has been revoked or suspended. - Supports multiple status mechanisms (BitstringStatusList, StatusList2021, etc.). - """ - - _jsonld_type: ClassVar[list[str]] = ["CredentialStatus"] - - id: Annotated[ - FlexibleUri, - Field(..., description="URI identifying the status entry"), - ] - type: Annotated[ - str, - Field( - ..., - description="Status type (e.g., BitstringStatusListEntry, StatusList2021Entry)", - ), - ] - status_purpose: Annotated[ - str | None, - Field( - default=None, - alias="statusPurpose", - description="Purpose of status (revocation, suspension)", - ), - ] - status_list_index: Annotated[ - str | None, - Field( - default=None, - alias="statusListIndex", - description="Index in the status list", - ), - ] - status_list_credential: Annotated[ - FlexibleUri | None, - Field( - default=None, - alias="statusListCredential", - description="URI of the status list credential", - ), - ] - - -class CredentialIssuer(UNTPStrictModel): - """Issuer of a verifiable credential.""" - - _jsonld_type: ClassVar[list[str]] = ["CredentialIssuer"] - - id: Annotated[ - FlexibleUri, - Field(..., description="W3C DID of the issuer (did:web, did:webvh, or https URL)"), - ] - name: str = Field(..., description="Name of the issuer person or organisation") - issuer_also_known_as: Annotated[ - list[Party] | None, - Field( - default=None, - alias="issuerAlsoKnownAs", - description="Other registered identifiers for this issuer", - ), - ] - - -class ProductPassport(UNTPBaseModel): - """Product passport credential subject.""" - - _jsonld_type: ClassVar[list[str]] = ["ProductPassport"] - id: Annotated[ - FlexibleUri | None, - Field(default=None, description="Identifier for the credential subject (URI)"), - ] - product: Product | None = Field(default=None, description="Product information") - granularity_level: Annotated[ - GranularityLevel | None, - Field( - default=None, - alias="granularityLevel", - description="Item, batch, or model level passport", - ), - ] - conformity_claim: Annotated[ - list[Claim] | None, - Field( - default=None, - alias="conformityClaim", - description="Conformity claims about the product", - ), - ] - emissions_scorecard: Annotated[ - EmissionsPerformance | None, - Field( - default=None, - alias="emissionsScorecard", - description="Emissions performance scorecard", - ), - ] - traceability_information: Annotated[ - list[TraceabilityPerformance] | None, - Field( - default=None, - alias="traceabilityInformation", - description="Traceability events by value chain process", - ), - ] - circularity_scorecard: Annotated[ - CircularityPerformance | None, - Field( - default=None, - alias="circularityScorecard", - description="Circularity performance scorecard", - ), - ] - due_diligence_declaration: Annotated[ - Link | None, - Field( - default=None, - alias="dueDiligenceDeclaration", - description="Due diligence declaration link", - ), - ] - materials_provenance: Annotated[ - list[Material] | None, - Field( - default=None, - alias="materialsProvenance", - description="Material origin and mass fraction information", - ), - ] +__all__ = [ + "CredentialIssuer", + "CredentialStatus", + "ProductPassport", +] diff --git a/src/dppvalidator/models/enums.py b/src/dppvalidator/models/enums.py index 944bac1..2c3114c 100644 --- a/src/dppvalidator/models/enums.py +++ b/src/dppvalidator/models/enums.py @@ -1,70 +1,32 @@ -"""Enumeration types for UNTP DPP models.""" +"""Backward-compatibility re-export of v0.6.x ``enums``. -from __future__ import annotations - -from enum import Enum - - -class ConformityTopic(str, Enum): - """Conformity topic categories per UNTP specification.""" - - ENVIRONMENT_ENERGY = "environment.energy" - ENVIRONMENT_EMISSIONS = "environment.emissions" - ENVIRONMENT_WATER = "environment.water" - ENVIRONMENT_WASTE = "environment.waste" - ENVIRONMENT_DEFORESTATION = "environment.deforestation" - ENVIRONMENT_BIODIVERSITY = "environment.biodiversity" - CIRCULARITY_CONTENT = "circularity.content" - CIRCULARITY_DESIGN = "circularity.design" - SOCIAL_LABOUR = "social.labour" - SOCIAL_RIGHTS = "social.rights" - SOCIAL_COMMUNITY = "social.community" - SOCIAL_SAFETY = "social.safety" - GOVERNANCE_ETHICS = "governance.ethics" - GOVERNANCE_COMPLIANCE = "governance.compliance" - GOVERNANCE_TRANSPARENCY = "governance.transparency" - - -class GranularityLevel(str, Enum): - """Granularity level for product passports.""" - - ITEM = "item" - BATCH = "batch" - MODEL = "model" - - -class OperationalScope(str, Enum): - """Operational scope for emissions performance. +The actual class definitions live in :mod:`dppvalidator.models.v0_6.enums`. +This shim preserves the import path used by third-party plugins (e.g. +``from dppvalidator.models.enums import ConformityTopic``) — see Phase 3 of +docs/plans/UNTP_0.7.0_MIGRATION.md and the public-API stability contract in +§7.6 of that plan. - Supports both GHG Protocol scopes (Scope1/2/3) and lifecycle - assessment boundaries (CradleToGate/CradleToGrave). - """ +Through the 0.4.x line this re-exports v0.6.x classes (the current default +schema version). Phase 9 (validator 0.5.0) will switch the default to v0.7 +and update this shim accordingly. +""" - NONE = "None" - SCOPE1 = "Scope1" - SCOPE2 = "Scope2" - SCOPE3 = "Scope3" - CRADLE_TO_GATE = "CradleToGate" - CRADLE_TO_GRAVE = "CradleToGrave" - - -class HashMethod(str, Enum): - """Hash algorithm for secure links.""" - - SHA_256 = "SHA-256" - SHA_1 = "SHA-1" - - -class EncryptionMethod(str, Enum): - """Encryption method for secure links.""" - - NONE = "none" - AES = "AES" - - -class CriterionStatus(str, Enum): - """Lifecycle status of a criterion.""" +from __future__ import annotations - PROPOSED = "proposed" - ACTIVE = "active" - DEPRECATED = "deprecated" +from dppvalidator.models.v0_6.enums import ( + ConformityTopic, + CriterionStatus, + EncryptionMethod, + GranularityLevel, + HashMethod, + OperationalScope, +) + +__all__ = [ + "ConformityTopic", + "CriterionStatus", + "EncryptionMethod", + "GranularityLevel", + "HashMethod", + "OperationalScope", +] diff --git a/src/dppvalidator/models/identifiers.py b/src/dppvalidator/models/identifiers.py index 0b0ac19..6b06349 100644 --- a/src/dppvalidator/models/identifiers.py +++ b/src/dppvalidator/models/identifiers.py @@ -1,68 +1,26 @@ -"""Identifier-related models for UNTP DPP.""" +"""Backward-compatibility re-export of v0.6.x ``identifiers``. -from __future__ import annotations - -from typing import Annotated, ClassVar - -from pydantic import Field - -from dppvalidator.models.base import UNTPBaseModel, UNTPStrictModel -from dppvalidator.models.primitives import FlexibleUri - - -class IdentifierScheme(UNTPStrictModel): - """Identifier registration scheme for products, facilities, or organisations.""" - - _jsonld_type: ClassVar[list[str]] = ["IdentifierScheme"] +The actual class definitions live in :mod:`dppvalidator.models.v0_6.identifiers`. +This shim preserves the import path used by third-party plugins (e.g. +``from dppvalidator.models.identifiers import Facility``) — see Phase 3 of +docs/plans/UNTP_0.7.0_MIGRATION.md and the public-API stability contract in +§7.6 of that plan. - id: Annotated[ - FlexibleUri | None, - Field( - default=None, - description="Globally unique identifier of the registration scheme", - ), - ] - name: Annotated[ - str | None, - Field(default=None, description="Name of the identifier scheme"), - ] +Through the 0.4.x line this re-exports v0.6.x classes (the current default +schema version). Phase 9 (validator 0.5.0) will switch the default to v0.7 +and update this shim accordingly. +""" +from __future__ import annotations -class Party(UNTPBaseModel): - """A party (person or organisation) with identifier.""" - - _jsonld_type: ClassVar[list[str]] = ["Party"] - - id: Annotated[ - FlexibleUri, - Field(..., description="Globally unique ID of the party as a URI"), - ] - name: str = Field(..., description="Registered name of the party") - registered_id: Annotated[ - str | None, - Field( - default=None, - alias="registeredId", - description="Registration number within the register", - ), - ] - - -class Facility(UNTPBaseModel): - """A facility where products are manufactured.""" - - _jsonld_type: ClassVar[list[str]] = ["Facility"] - - id: Annotated[ - FlexibleUri, - Field(..., description="Globally unique ID of the facility as URI"), - ] - name: str = Field(..., description="Registered name of the facility") - registered_id: Annotated[ - str | None, - Field( - default=None, - alias="registeredId", - description="Registration number within the identifier scheme", - ), - ] +from dppvalidator.models.v0_6.identifiers import ( + Facility, + IdentifierScheme, + Party, +) + +__all__ = [ + "Facility", + "IdentifierScheme", + "Party", +] diff --git a/src/dppvalidator/models/materials.py b/src/dppvalidator/models/materials.py index 794ca5f..814f94d 100644 --- a/src/dppvalidator/models/materials.py +++ b/src/dppvalidator/models/materials.py @@ -1,78 +1,22 @@ -"""Material provenance models for UNTP DPP.""" +"""Backward-compatibility re-export of v0.6.x ``materials``. -from __future__ import annotations - -from typing import Annotated, ClassVar - -from pydantic import Field, model_validator +The actual class definitions live in :mod:`dppvalidator.models.v0_6.materials`. +This shim preserves the import path used by third-party plugins (e.g. +``from dppvalidator.models.materials import Material``) — see Phase 3 of +docs/plans/UNTP_0.7.0_MIGRATION.md and the public-API stability contract in +§7.6 of that plan. -from dppvalidator.models.base import UNTPBaseModel -from dppvalidator.models.primitives import Classification, Link, Measure +Through the 0.4.x line this re-exports v0.6.x classes (the current default +schema version). Phase 9 (validator 0.5.0) will switch the default to v0.7 +and update this shim accordingly. +""" +from __future__ import annotations -class Material(UNTPBaseModel): - """Material origin and mass fraction information.""" - - _jsonld_type: ClassVar[list[str]] = ["Material"] - - name: str = Field(..., description="Material name (e.g., 'Egyptian Cotton')") - origin_country: Annotated[ - str | None, - Field( - default=None, - alias="originCountry", - description="ISO 3166-1 country of origin", - ), - ] - material_type: Annotated[ - Classification | None, - Field( - default=None, - alias="materialType", - description="Material classification (e.g., UNFC)", - ), - ] - mass_fraction: Annotated[ - float | None, - Field( - default=None, - ge=0, - le=1, - alias="massFraction", - description="Mass fraction of product (0-1, sum should equal 1)", - ), - ] - mass: Measure | None = Field(default=None, description="Mass of the material component") - recycled_mass_fraction: Annotated[ - float | None, - Field( - default=None, - ge=0, - le=1, - alias="recycledMassFraction", - description="Fraction of material that is recycled (0-1)", - ), - ] - hazardous: bool | None = Field( - default=None, - description="Whether material is hazardous", - ) - symbol: str | None = Field( - default=None, - description="Base64 encoded visual symbol for the material", - ) - material_safety_information: Annotated[ - Link | None, - Field( - default=None, - alias="materialSafetyInformation", - description="Link to material safety data sheet", - ), - ] +from dppvalidator.models.v0_6.materials import ( + Material, +) - @model_validator(mode="after") - def validate_hazardous_requires_safety_info(self) -> Material: - """Ensure hazardous materials have safety information.""" - if self.hazardous and not self.material_safety_information: - raise ValueError("materialSafetyInformation is required when hazardous is true") - return self +__all__ = [ + "Material", +] diff --git a/src/dppvalidator/models/passport.py b/src/dppvalidator/models/passport.py index c6c5c0c..0a1ac92 100644 --- a/src/dppvalidator/models/passport.py +++ b/src/dppvalidator/models/passport.py @@ -1,94 +1,22 @@ -"""Digital Product Passport root model per UNTP/UNCEFACT v0.6.1.""" +"""Backward-compatibility re-export of v0.6.x ``passport``. -from __future__ import annotations +The actual class definitions live in :mod:`dppvalidator.models.v0_6.passport`. +This shim preserves the import path used by third-party plugins (e.g. +``from dppvalidator.models.passport import DigitalProductPassport``) — see Phase 3 of +docs/plans/UNTP_0.7.0_MIGRATION.md and the public-API stability contract in +§7.6 of that plan. -from datetime import datetime -from typing import Annotated, ClassVar +Through the 0.4.x line this re-exports v0.6.x classes (the current default +schema version). Phase 9 (validator 0.5.0) will switch the default to v0.7 +and update this shim accordingly. +""" -from pydantic import Field, model_validator +from __future__ import annotations -from dppvalidator.models.base import UNTPBaseModel -from dppvalidator.models.credential import ( - CredentialIssuer, - CredentialStatus, - ProductPassport, +from dppvalidator.models.v0_6.passport import ( + DigitalProductPassport, ) -from dppvalidator.models.primitives import FlexibleUri - - -class DigitalProductPassport(UNTPBaseModel): - """Digital Product Passport as a Verifiable Credential. - - Root model for UNTP DPP v0.6.1, combining DigitalProductPassport - and VerifiableCredential types per W3C VC v2 specification. - """ - - _jsonld_type: ClassVar[list[str]] = ["DigitalProductPassport", "VerifiableCredential"] - - context: Annotated[ - list[str], - Field( - alias="@context", - default=[ - "https://www.w3.org/ns/credentials/v2", - "https://test.uncefact.org/vocabulary/untp/dpp/0.6.1/", - ], - description="JSON-LD context URIs", - ), - ] - id: Annotated[ - FlexibleUri, - Field(..., description="Unique identifier (URI) for this passport"), - ] - issuer: CredentialIssuer = Field(..., description="Organisation issuing this credential") - valid_from: Annotated[ - datetime | None, - Field(default=None, alias="validFrom", description="Credential validity start"), - ] - valid_until: Annotated[ - datetime | None, - Field(default=None, alias="validUntil", description="Credential expiry date"), - ] - credential_subject: Annotated[ - ProductPassport | None, - Field( - default=None, - alias="credentialSubject", - description="The product passport content", - ), - ] - credential_status: Annotated[ - CredentialStatus | list[CredentialStatus] | None, - Field( - default=None, - alias="credentialStatus", - description="Credential revocation/suspension status per W3C VC v2", - ), - ] - - @model_validator(mode="after") - def validate_dates(self) -> DigitalProductPassport: - """Ensure validFrom is before validUntil if both are present.""" - if self.valid_from and self.valid_until and self.valid_from >= self.valid_until: - raise ValueError("validFrom must be before validUntil") - return self - - @model_validator(mode="after") - def validate_material_mass_fractions(self) -> DigitalProductPassport: - """Validate material mass fractions don't exceed 1.0. - Per UNTP spec, mass fractions can be partial declarations (sum < 1.0). - Only sum > 1.0 is physically impossible and should error. - Semantic validation checks for exact sum when appropriate. - """ - if not self.credential_subject: - return self - materials = self.credential_subject.materials_provenance - if not materials: - return self - fractions = [m.mass_fraction for m in materials if m.mass_fraction is not None] - if fractions: - total = sum(fractions) - if total > 1.01: # Allow small tolerance for floating point - raise ValueError(f"Material mass fractions cannot exceed 1.0, got {total:.3f}") - return self +__all__ = [ + "DigitalProductPassport", +] diff --git a/src/dppvalidator/models/performance.py b/src/dppvalidator/models/performance.py index c0bbcc9..c91669b 100644 --- a/src/dppvalidator/models/performance.py +++ b/src/dppvalidator/models/performance.py @@ -1,156 +1,26 @@ -"""Performance scorecard models for UNTP DPP.""" +"""Backward-compatibility re-export of v0.6.x ``performance``. -from __future__ import annotations - -from typing import Annotated, ClassVar - -from pydantic import Field - -from dppvalidator.models.base import UNTPBaseModel -from dppvalidator.models.claims import Standard -from dppvalidator.models.enums import OperationalScope -from dppvalidator.models.primitives import Link, SecureLink - - -class EmissionsPerformance(UNTPBaseModel): - """Emissions performance scorecard.""" - - _jsonld_type: ClassVar[list[str]] = ["EmissionsPerformance"] +The actual class definitions live in :mod:`dppvalidator.models.v0_6.performance`. +This shim preserves the import path used by third-party plugins (e.g. +``from dppvalidator.models.performance import CircularityPerformance``) — see Phase 3 of +docs/plans/UNTP_0.7.0_MIGRATION.md and the public-API stability contract in +§7.6 of that plan. - carbon_footprint: Annotated[ - float, - Field( - ..., - alias="carbonFootprint", - description="Carbon footprint in KgCO2e per declared unit", - ), - ] - declared_unit: Annotated[ - str, - Field( - ..., - alias="declaredUnit", - description="Unit of product (EA, KGM, LTR) for carbon footprint basis", - ), - ] - operational_scope: Annotated[ - OperationalScope | None, - Field( - default=None, - alias="operationalScope", - description="Emissions operational scope (GHG Protocol or lifecycle boundary)", - ), - ] - primary_sourced_ratio: Annotated[ - float, - Field( - ..., - ge=0, - le=1, - alias="primarySourcedRatio", - description="Ratio of primary source emissions data (0-1)", - ), - ] - reporting_standard: Annotated[ - Standard | None, - Field( - default=None, - alias="reportingStandard", - description="Reporting standard (GHG Protocol, IFRS S2, etc.)", - ), - ] +Through the 0.4.x line this re-exports v0.6.x classes (the current default +schema version). Phase 9 (validator 0.5.0) will switch the default to v0.7 +and update this shim accordingly. +""" +from __future__ import annotations -class TraceabilityPerformance(UNTPBaseModel): - """Traceability performance for a value chain process.""" - - _jsonld_type: ClassVar[list[str]] = ["TraceabilityPerformance"] - - value_chain_process: Annotated[ - str | None, - Field( - default=None, - alias="valueChainProcess", - description="Industry-specific value chain process name", - ), - ] - verified_ratio: Annotated[ - float | None, - Field( - default=None, - ge=0, - le=1, - alias="verifiedRatio", - description="Proportion of traced materials (0-1)", - ), - ] - traceability_event: Annotated[ - list[SecureLink] | None, - Field( - default=None, - alias="traceabilityEvent", - description="Links to traceability events", - ), - ] - - -class CircularityPerformance(UNTPBaseModel): - """Circularity performance scorecard.""" - - _jsonld_type: ClassVar[list[str]] = ["CircularityPerformance"] - - recycling_information: Annotated[ - Link | None, - Field( - default=None, - alias="recyclingInformation", - description="Link to recycling information", - ), - ] - repair_information: Annotated[ - Link | None, - Field( - default=None, - alias="repairInformation", - description="Link to repair instructions", - ), - ] - recyclable_content: Annotated[ - float | None, - Field( - default=None, - ge=0, - le=1, - alias="recyclableContent", - description="Fraction designed to be recyclable (0-1)", - ), - ] - recycled_content: Annotated[ - float | None, - Field( - default=None, - ge=0, - le=1, - alias="recycledContent", - description="Fraction of recycled content (0-1)", - ), - ] - utility_factor: Annotated[ - float | None, - Field( - default=None, - ge=0, - alias="utilityFactor", - description="Durability indicator (lifetime / industry avg)", - ), - ] - material_circularity_indicator: Annotated[ - float | None, - Field( - default=None, - ge=0, - le=1, - alias="materialCircularityIndicator", - description="Overall circularity indicator (0-1)", - ), - ] +from dppvalidator.models.v0_6.performance import ( + CircularityPerformance, + EmissionsPerformance, + TraceabilityPerformance, +) + +__all__ = [ + "CircularityPerformance", + "EmissionsPerformance", + "TraceabilityPerformance", +] diff --git a/src/dppvalidator/models/primitives.py b/src/dppvalidator/models/primitives.py index 11db322..1e5ae11 100644 --- a/src/dppvalidator/models/primitives.py +++ b/src/dppvalidator/models/primitives.py @@ -1,132 +1,30 @@ -"""Primitive types for UNTP DPP models.""" +"""Backward-compatibility re-export of v0.6.x ``primitives``. -from __future__ import annotations - -import re -from typing import Annotated, ClassVar +The actual class definitions live in :mod:`dppvalidator.models.v0_6.primitives`. +This shim preserves the import path used by third-party plugins (e.g. +``from dppvalidator.models.primitives import Classification``) — see Phase 3 of +docs/plans/UNTP_0.7.0_MIGRATION.md and the public-API stability contract in +§7.6 of that plan. -from pydantic import AfterValidator, Field, HttpUrl +Through the 0.4.x line this re-exports v0.6.x classes (the current default +schema version). Phase 9 (validator 0.5.0) will switch the default to v0.7 +and update this shim accordingly. +""" -from dppvalidator.models.base import UNTPStrictModel -from dppvalidator.models.enums import EncryptionMethod, HashMethod +from __future__ import annotations -# URI pattern supporting: -# - HTTP/HTTPS URLs (https://example.com) -# - DIDs (did:web:example.com, did:webvh:example.com) -# - URNs (urn:uuid:123, urn:isbn:123) -# - Custom schemes (example:product/1234, gs1:01/1234) -_URI_PATTERN = re.compile( - r"^[a-zA-Z][a-zA-Z0-9+.-]*:" # scheme (RFC 3986) - r".+$", # scheme-specific part (non-empty) - re.ASCII, +from dppvalidator.models.v0_6.primitives import ( + Classification, + FlexibleUri, + Link, + Measure, + SecureLink, ) - -def _validate_uri(value: str) -> str: - """Validate that a string is a valid URI per RFC 3986. - - Supports HTTP URLs, DIDs, URNs, and custom URI schemes. - """ - if not _URI_PATTERN.match(value): - raise ValueError( - f"Invalid URI: '{value}'. Must have format 'scheme:path' " - "(e.g., 'https://...', 'did:web:...', 'urn:uuid:...')" - ) - return value - - -# Flexible URI type for W3C VC / UNTP compatibility -# Accepts HTTP URLs, DIDs (did:web:, did:webvh:), URNs, and custom schemes -FlexibleUri = Annotated[str, AfterValidator(_validate_uri)] - - -class Measure(UNTPStrictModel): - """Numeric value with unit of measure (UNECE Rec20).""" - - _jsonld_type: ClassVar[list[str]] = ["Measure"] - - value: float = Field(..., description="The numeric value of the measure") - unit: str = Field( - ..., - description="Unit of measure from UNECE Rec20 (e.g., KGM, LTR, EA)", - ) - - -class Link(UNTPStrictModel): - """URL link with metadata.""" - - _jsonld_type: ClassVar[list[str]] = ["Link"] - - link_url: Annotated[ - HttpUrl | None, - Field(default=None, alias="linkURL", description="The URL of the target resource"), - ] - link_name: Annotated[ - str | None, - Field(default=None, alias="linkName", description="Display name for the target resource"), - ] - link_type: Annotated[ - str | None, - Field(default=None, alias="linkType", description="Type of the target resource"), - ] - - -class SecureLink(UNTPStrictModel): - """Link with hash and optional encryption for tamper evidence.""" - - _jsonld_type: ClassVar[list[str]] = ["SecureLink", "Link"] - - link_url: Annotated[ - HttpUrl | None, - Field(default=None, alias="linkURL", description="The URL of the target resource"), - ] - link_name: Annotated[ - str | None, - Field(default=None, alias="linkName", description="Display name for the target resource"), - ] - link_type: Annotated[ - str | None, - Field(default=None, alias="linkType", description="Type of the target resource"), - ] - hash_digest: Annotated[ - str | None, - Field(default=None, alias="hashDigest", description="Hash of the file"), - ] - hash_method: Annotated[ - HashMethod | None, - Field( - default=None, alias="hashMethod", description="Hashing algorithm (SHA-256 recommended)" - ), - ] - encryption_method: Annotated[ - EncryptionMethod | None, - Field( - default=None, - alias="encryptionMethod", - description="Encryption algorithm (AES recommended)", - ), - ] - - -class Classification(UNTPStrictModel): - """Classification scheme and code representing a category value.""" - - _jsonld_type: ClassVar[list[str]] = ["Classification"] - - id: Annotated[ - FlexibleUri, - Field(..., description="Globally unique URI representing the classifier value"), - ] - code: Annotated[ - str | None, - Field(default=None, description="Classification code within the scheme"), - ] - name: str = Field(..., description="Name of the classification") - scheme_id: Annotated[ - FlexibleUri | None, - Field(default=None, alias="schemeID", description="Classification scheme ID"), - ] - scheme_name: Annotated[ - str | None, - Field(default=None, alias="schemeName", description="Name of the classification scheme"), - ] +__all__ = [ + "Classification", + "FlexibleUri", + "Link", + "Measure", + "SecureLink", +] diff --git a/src/dppvalidator/models/product.py b/src/dppvalidator/models/product.py index 7be0780..b23023e 100644 --- a/src/dppvalidator/models/product.py +++ b/src/dppvalidator/models/product.py @@ -1,99 +1,26 @@ -"""Product-related models for UNTP DPP.""" +"""Backward-compatibility re-export of v0.6.x ``product``. -from __future__ import annotations - -from datetime import date -from typing import Annotated, ClassVar - -from pydantic import Field - -from dppvalidator.models.base import UNTPBaseModel -from dppvalidator.models.identifiers import Facility, IdentifierScheme, Party -from dppvalidator.models.primitives import Classification, FlexibleUri, Link, Measure - - -class Dimension(UNTPBaseModel): - """Physical dimensions (length, width, height) and weight/volume.""" - - _jsonld_type: ClassVar[list[str]] = ["Dimension"] +The actual class definitions live in :mod:`dppvalidator.models.v0_6.product`. +This shim preserves the import path used by third-party plugins (e.g. +``from dppvalidator.models.product import Characteristics``) — see Phase 3 of +docs/plans/UNTP_0.7.0_MIGRATION.md and the public-API stability contract in +§7.6 of that plan. - weight: Measure | None = Field(default=None, description="Weight of the product") - length: Measure | None = Field(default=None, description="Length of the product") - width: Measure | None = Field(default=None, description="Width of the product") - height: Measure | None = Field(default=None, description="Height of the product") - volume: Measure | None = Field(default=None, description="Displacement volume") +Through the 0.4.x line this re-exports v0.6.x classes (the current default +schema version). Phase 9 (validator 0.5.0) will switch the default to v0.7 +and update this shim accordingly. +""" +from __future__ import annotations -class Characteristics(UNTPBaseModel): - """Extension point for industry/product-specific characteristics.""" - - _jsonld_type: ClassVar[list[str]] = ["Characteristics"] - - -class Product(UNTPBaseModel): - """Product information including identification and manufacturer details.""" - - _jsonld_type: ClassVar[list[str]] = ["Product"] - - id: Annotated[ - FlexibleUri, - Field(..., description="Globally unique ID of the product as a URI"), - ] - name: str = Field(..., description="Registered name of the product") - registered_id: Annotated[ - str | None, - Field( - default=None, - alias="registeredId", - description="Registration number within the register", - ), - ] - id_scheme: Annotated[ - IdentifierScheme | None, - Field(default=None, alias="idScheme", description="Identifier scheme for this product"), - ] - batch_number: Annotated[ - str | None, - Field(default=None, alias="batchNumber", description="Production batch identifier"), - ] - product_image: Annotated[ - Link | None, - Field(default=None, alias="productImage", description="Reference to product image"), - ] - description: str | None = Field(default=None, description="Textual product description") - characteristics: Characteristics | None = Field( - default=None, description="Industry-specific characteristics" - ) - product_category: Annotated[ - list[Classification] | None, - Field(default=None, alias="productCategory", description="Product classification codes"), - ] - further_information: Annotated[ - list[Link] | None, - Field(default=None, alias="furtherInformation", description="Additional information links"), - ] - produced_by_party: Annotated[ - Party | None, - Field(default=None, alias="producedByParty", description="Manufacturing party"), - ] - produced_at_facility: Annotated[ - Facility | None, - Field(default=None, alias="producedAtFacility", description="Manufacturing facility"), - ] - production_date: Annotated[ - date | None, - Field(default=None, alias="productionDate", description="ISO 8601 production date"), - ] - country_of_production: Annotated[ - str | None, - Field( - default=None, - alias="countryOfProduction", - description="ISO 3166-1 country code of production", - ), - ] - serial_number: Annotated[ - str | None, - Field(default=None, alias="serialNumber", description="Serialised item identifier"), - ] - dimensions: Dimension | None = Field(default=None, description="Physical dimensions") +from dppvalidator.models.v0_6.product import ( + Characteristics, + Dimension, + Product, +) + +__all__ = [ + "Characteristics", + "Dimension", + "Product", +] diff --git a/src/dppvalidator/models/v0_6/__init__.py b/src/dppvalidator/models/v0_6/__init__.py new file mode 100644 index 0000000..8916c81 --- /dev/null +++ b/src/dppvalidator/models/v0_6/__init__.py @@ -0,0 +1,93 @@ +"""Pydantic models for UNTP DPP **v0.6.x**. + +Phase 3 of docs/plans/UNTP_0.7.0_MIGRATION.md splits the model package into +version-namespaced subpackages so 0.6.x and 0.7.x classes can coexist. This +subpackage holds the legacy 0.6.x shapes; the top-level +``dppvalidator.models`` and ``dppvalidator.models.passport`` re-export from +here for backward compatibility through the 0.4.x line. Phase 9 (validator +release 0.5.0) flips the default and re-exports v0.7 instead; Phase 10 +(release 0.6.0) removes this subpackage entirely. + +The ``examples/dppvalidator_example_plugin/`` and any third-party plugin +that imports ``from dppvalidator.models.passport import DigitalProductPassport`` +keeps working because of the re-export shim — see §4.1.8 / §7.6 of the plan. +""" + +from dppvalidator.models.v0_6.claims import ( + Claim, + Criterion, + Metric, + Regulation, + Standard, +) +from dppvalidator.models.v0_6.credential import ( + CredentialIssuer, + CredentialStatus, + ProductPassport, +) +from dppvalidator.models.v0_6.enums import ( + ConformityTopic, + CriterionStatus, + EncryptionMethod, + GranularityLevel, + HashMethod, + OperationalScope, +) +from dppvalidator.models.v0_6.identifiers import Facility, IdentifierScheme, Party +from dppvalidator.models.v0_6.materials import Material +from dppvalidator.models.v0_6.passport import DigitalProductPassport +from dppvalidator.models.v0_6.performance import ( + CircularityPerformance, + EmissionsPerformance, + TraceabilityPerformance, +) +from dppvalidator.models.v0_6.primitives import ( + Classification, + FlexibleUri, + Link, + Measure, + SecureLink, +) +from dppvalidator.models.v0_6.product import Characteristics, Dimension, Product + +__all__ = [ + # Claims + "Claim", + "Criterion", + "Metric", + "Regulation", + "Standard", + # Credential + "CredentialIssuer", + "CredentialStatus", + "ProductPassport", + # Enums + "ConformityTopic", + "CriterionStatus", + "EncryptionMethod", + "GranularityLevel", + "HashMethod", + "OperationalScope", + # Identifiers + "Facility", + "IdentifierScheme", + "Party", + # Materials + "Material", + # Passport (envelope) + "DigitalProductPassport", + # Performance scorecards (collapse into Claim.claimedPerformance in v0.7) + "CircularityPerformance", + "EmissionsPerformance", + "TraceabilityPerformance", + # Primitives + "Classification", + "FlexibleUri", + "Link", + "Measure", + "SecureLink", + # Product + "Characteristics", + "Dimension", + "Product", +] diff --git a/src/dppvalidator/models/v0_6/claims.py b/src/dppvalidator/models/v0_6/claims.py new file mode 100644 index 0000000..f4475ae --- /dev/null +++ b/src/dppvalidator/models/v0_6/claims.py @@ -0,0 +1,164 @@ +"""Claim and conformity-related models for UNTP DPP.""" + +from __future__ import annotations + +from datetime import date +from typing import Annotated, ClassVar + +from pydantic import Field + +from dppvalidator.models.base import UNTPBaseModel, UNTPStrictModel +from dppvalidator.models.v0_6.enums import ConformityTopic, CriterionStatus +from dppvalidator.models.v0_6.identifiers import Party +from dppvalidator.models.v0_6.primitives import Classification, FlexibleUri, Measure, SecureLink + + +class Metric(UNTPStrictModel): + """Performance metric with value and optional score.""" + + _jsonld_type: ClassVar[list[str]] = ["Metric"] + + metric_name: Annotated[ + str, + Field(..., alias="metricName", description="Human readable metric name"), + ] + metric_value: Annotated[ + Measure, + Field(..., alias="metricValue", description="Numeric value and unit"), + ] + score: str | None = Field(default=None, description="Score or rank for this metric") + accuracy: float | None = Field( + default=None, + ge=0, + le=1, + description="Accuracy as percentage (0-1)", + ) + + +class Criterion(UNTPBaseModel): + """Specific rule or criterion within a standard or regulation.""" + + _jsonld_type: ClassVar[list[str]] = ["Criterion"] + + id: Annotated[ + FlexibleUri, + Field(..., description="Unique identifier for the criterion"), + ] + name: str = Field(..., description="Criterion name") + description: str = Field(..., description="Full text description of the criterion") + conformity_topic: Annotated[ + ConformityTopic, + Field(..., alias="conformityTopic", description="Conformity topic category"), + ] + status: CriterionStatus = Field(..., description="Lifecycle status") + sub_criterion: Annotated[ + list[Criterion] | None, + Field(default=None, alias="subCriterion", description="Subordinate criteria"), + ] + threshold_value: Annotated[ + Metric | None, + Field(default=None, alias="thresholdValue", description="Minimum compliance threshold"), + ] + performance_level: Annotated[ + str | None, + Field(default=None, alias="performanceLevel", description="Performance category code"), + ] + category: Annotated[ + list[Classification] | None, + Field(default=None, description="Product categories the criterion applies to"), + ] + tag: Annotated[ + list[str] | None, + Field(default=None, description="Tags for stakeholder/commodity types"), + ] + + +class Standard(UNTPStrictModel): + """Standard that specifies conformance criteria (e.g., ISO 14000).""" + + _jsonld_type: ClassVar[list[str]] = ["Standard"] + + id: Annotated[ + FlexibleUri | None, + Field(default=None, description="Unique identifier for the standard"), + ] + name: str | None = Field(default=None, description="Name of the standard") + issuing_party: Annotated[ + Party, + Field(..., alias="issuingParty", description="Party that issued the standard"), + ] + issue_date: Annotated[ + date | None, + Field(default=None, alias="issueDate", description="Date the standard was issued"), + ] + + +class Regulation(UNTPStrictModel): + """Regulation that defines assessment criteria.""" + + _jsonld_type: ClassVar[list[str]] = ["Regulation"] + + id: Annotated[ + FlexibleUri | None, + Field(default=None, description="Globally unique identifier of the regulation"), + ] + name: str | None = Field(default=None, description="Name of the regulation or act") + jurisdiction_country: Annotated[ + str | None, + Field( + default=None, + alias="jurisdictionCountry", + description="ISO 3166-1 jurisdiction country code", + ), + ] + administered_by: Annotated[ + Party, + Field(..., alias="administeredBy", description="Issuing body of the regulation"), + ] + effective_date: Annotated[ + date | None, + Field(default=None, alias="effectiveDate", description="Date regulation came into effect"), + ] + + +class Claim(UNTPBaseModel): + """Declaration of conformance with standard or regulation criteria.""" + + _jsonld_type: ClassVar[list[str]] = ["Claim", "Declaration"] + + id: Annotated[ + FlexibleUri, + Field(..., description="Unique identifier for the declaration"), + ] + description: str | None = Field(default=None, description="Textual description of the claim") + reference_standard: Annotated[ + Standard | None, + Field(default=None, alias="referenceStandard", description="Reference standard"), + ] + reference_regulation: Annotated[ + Regulation | None, + Field(default=None, alias="referenceRegulation", description="Reference regulation"), + ] + assessment_criteria: Annotated[ + list[Criterion] | None, + Field(default=None, alias="assessmentCriteria", description="Assessment specifications"), + ] + assessment_date: Annotated[ + date | None, + Field(default=None, alias="assessmentDate", description="Date of assessment"), + ] + declared_value: Annotated[ + list[Metric] | None, + Field(default=None, alias="declaredValue", description="Measured values"), + ] + conformance: bool = Field(..., description="Whether the claim conforms to criteria") + conformity_topic: Annotated[ + ConformityTopic, + Field(..., alias="conformityTopic", description="Conformity topic category"), + ] + conformity_evidence: Annotated[ + SecureLink | None, + Field( + default=None, alias="conformityEvidence", description="Evidence supporting the claim" + ), + ] diff --git a/src/dppvalidator/models/v0_6/credential.py b/src/dppvalidator/models/v0_6/credential.py new file mode 100644 index 0000000..6fa80c0 --- /dev/null +++ b/src/dppvalidator/models/v0_6/credential.py @@ -0,0 +1,154 @@ +"""Credential and ProductPassport models for UNTP DPP.""" + +from __future__ import annotations + +from typing import Annotated, ClassVar + +from pydantic import Field + +from dppvalidator.models.base import UNTPBaseModel, UNTPStrictModel +from dppvalidator.models.v0_6.claims import Claim +from dppvalidator.models.v0_6.enums import GranularityLevel +from dppvalidator.models.v0_6.identifiers import Party +from dppvalidator.models.v0_6.materials import Material +from dppvalidator.models.v0_6.performance import ( + CircularityPerformance, + EmissionsPerformance, + TraceabilityPerformance, +) +from dppvalidator.models.v0_6.primitives import FlexibleUri, Link +from dppvalidator.models.v0_6.product import Product + + +class CredentialStatus(UNTPBaseModel): + """Credential status for revocation checking per W3C VC v2. + + Used to check if a credential has been revoked or suspended. + Supports multiple status mechanisms (BitstringStatusList, StatusList2021, etc.). + """ + + _jsonld_type: ClassVar[list[str]] = ["CredentialStatus"] + + id: Annotated[ + FlexibleUri, + Field(..., description="URI identifying the status entry"), + ] + type: Annotated[ + str, + Field( + ..., + description="Status type (e.g., BitstringStatusListEntry, StatusList2021Entry)", + ), + ] + status_purpose: Annotated[ + str | None, + Field( + default=None, + alias="statusPurpose", + description="Purpose of status (revocation, suspension)", + ), + ] + status_list_index: Annotated[ + str | None, + Field( + default=None, + alias="statusListIndex", + description="Index in the status list", + ), + ] + status_list_credential: Annotated[ + FlexibleUri | None, + Field( + default=None, + alias="statusListCredential", + description="URI of the status list credential", + ), + ] + + +class CredentialIssuer(UNTPStrictModel): + """Issuer of a verifiable credential.""" + + _jsonld_type: ClassVar[list[str]] = ["CredentialIssuer"] + + id: Annotated[ + FlexibleUri, + Field(..., description="W3C DID of the issuer (did:web, did:webvh, or https URL)"), + ] + name: str = Field(..., description="Name of the issuer person or organisation") + issuer_also_known_as: Annotated[ + list[Party] | None, + Field( + default=None, + alias="issuerAlsoKnownAs", + description="Other registered identifiers for this issuer", + ), + ] + + +class ProductPassport(UNTPBaseModel): + """Product passport credential subject.""" + + _jsonld_type: ClassVar[list[str]] = ["ProductPassport"] + + id: Annotated[ + FlexibleUri | None, + Field(default=None, description="Identifier for the credential subject (URI)"), + ] + product: Product | None = Field(default=None, description="Product information") + granularity_level: Annotated[ + GranularityLevel | None, + Field( + default=None, + alias="granularityLevel", + description="Item, batch, or model level passport", + ), + ] + conformity_claim: Annotated[ + list[Claim] | None, + Field( + default=None, + alias="conformityClaim", + description="Conformity claims about the product", + ), + ] + emissions_scorecard: Annotated[ + EmissionsPerformance | None, + Field( + default=None, + alias="emissionsScorecard", + description="Emissions performance scorecard", + ), + ] + traceability_information: Annotated[ + list[TraceabilityPerformance] | None, + Field( + default=None, + alias="traceabilityInformation", + description="Traceability events by value chain process", + ), + ] + circularity_scorecard: Annotated[ + CircularityPerformance | None, + Field( + default=None, + alias="circularityScorecard", + description="Circularity performance scorecard", + ), + ] + due_diligence_declaration: Annotated[ + Link | None, + Field( + default=None, + alias="dueDiligenceDeclaration", + description="Due diligence declaration link", + ), + ] + materials_provenance: Annotated[ + list[Material] | None, + Field( + default=None, + alias="materialsProvenance", + description="Material origin and mass fraction information", + ), + ] diff --git a/src/dppvalidator/models/v0_6/enums.py b/src/dppvalidator/models/v0_6/enums.py new file mode 100644 index 0000000..944bac1 --- /dev/null +++ b/src/dppvalidator/models/v0_6/enums.py @@ -0,0 +1,70 @@ +"""Enumeration types for UNTP DPP models.""" + +from __future__ import annotations + +from enum import Enum + + +class ConformityTopic(str, Enum): + """Conformity topic categories per UNTP specification.""" + + ENVIRONMENT_ENERGY = "environment.energy" + ENVIRONMENT_EMISSIONS = "environment.emissions" + ENVIRONMENT_WATER = "environment.water" + ENVIRONMENT_WASTE = "environment.waste" + ENVIRONMENT_DEFORESTATION = "environment.deforestation" + ENVIRONMENT_BIODIVERSITY = "environment.biodiversity" + CIRCULARITY_CONTENT = "circularity.content" + CIRCULARITY_DESIGN = "circularity.design" + SOCIAL_LABOUR = "social.labour" + SOCIAL_RIGHTS = "social.rights" + SOCIAL_COMMUNITY = "social.community" + SOCIAL_SAFETY = "social.safety" + GOVERNANCE_ETHICS = "governance.ethics" + GOVERNANCE_COMPLIANCE = "governance.compliance" + GOVERNANCE_TRANSPARENCY = "governance.transparency" + + +class GranularityLevel(str, Enum): + """Granularity level for product passports.""" + + ITEM = "item" + BATCH = "batch" + MODEL = "model" + + +class OperationalScope(str, Enum): + """Operational scope for emissions performance. + + Supports both GHG Protocol scopes (Scope1/2/3) and lifecycle + assessment boundaries (CradleToGate/CradleToGrave). + """ + + NONE = "None" + SCOPE1 = "Scope1" + SCOPE2 = "Scope2" + SCOPE3 = "Scope3" + CRADLE_TO_GATE = "CradleToGate" + CRADLE_TO_GRAVE = "CradleToGrave" + + +class HashMethod(str, Enum): + """Hash algorithm for secure links.""" + + SHA_256 = "SHA-256" + SHA_1 = "SHA-1" + + +class EncryptionMethod(str, Enum): + """Encryption method for secure links.""" + + NONE = "none" + AES = "AES" + + +class CriterionStatus(str, Enum): + """Lifecycle status of a criterion.""" + + PROPOSED = "proposed" + ACTIVE = "active" + DEPRECATED = "deprecated" diff --git a/src/dppvalidator/models/v0_6/identifiers.py b/src/dppvalidator/models/v0_6/identifiers.py new file mode 100644 index 0000000..394e8a6 --- /dev/null +++ b/src/dppvalidator/models/v0_6/identifiers.py @@ -0,0 +1,68 @@ +"""Identifier-related models for UNTP DPP.""" + +from __future__ import annotations + +from typing import Annotated, ClassVar + +from pydantic import Field + +from dppvalidator.models.base import UNTPBaseModel, UNTPStrictModel +from dppvalidator.models.v0_6.primitives import FlexibleUri + + +class IdentifierScheme(UNTPStrictModel): + """Identifier registration scheme for products, facilities, or organisations.""" + + _jsonld_type: ClassVar[list[str]] = ["IdentifierScheme"] + + id: Annotated[ + FlexibleUri | None, + Field( + default=None, + description="Globally unique identifier of the registration scheme", + ), + ] + name: Annotated[ + str | None, + Field(default=None, description="Name of the identifier scheme"), + ] + + +class Party(UNTPBaseModel): + """A party (person or organisation) with identifier.""" + + _jsonld_type: ClassVar[list[str]] = ["Party"] + + id: Annotated[ + FlexibleUri, + Field(..., description="Globally unique ID of the party as a URI"), + ] + name: str = Field(..., description="Registered name of the party") + registered_id: Annotated[ + str | None, + Field( + default=None, + alias="registeredId", + description="Registration number within the register", + ), + ] + + +class Facility(UNTPBaseModel): + """A facility where products are manufactured.""" + + _jsonld_type: ClassVar[list[str]] = ["Facility"] + + id: Annotated[ + FlexibleUri, + Field(..., description="Globally unique ID of the facility as URI"), + ] + name: str = Field(..., description="Registered name of the facility") + registered_id: Annotated[ + str | None, + Field( + default=None, + alias="registeredId", + description="Registration number within the identifier scheme", + ), + ] diff --git a/src/dppvalidator/models/v0_6/materials.py b/src/dppvalidator/models/v0_6/materials.py new file mode 100644 index 0000000..5af3095 --- /dev/null +++ b/src/dppvalidator/models/v0_6/materials.py @@ -0,0 +1,78 @@ +"""Material provenance models for UNTP DPP.""" + +from __future__ import annotations + +from typing import Annotated, ClassVar + +from pydantic import Field, model_validator + +from dppvalidator.models.base import UNTPBaseModel +from dppvalidator.models.v0_6.primitives import Classification, Link, Measure + + +class Material(UNTPBaseModel): + """Material origin and mass fraction information.""" + + _jsonld_type: ClassVar[list[str]] = ["Material"] + + name: str = Field(..., description="Material name (e.g., 'Egyptian Cotton')") + origin_country: Annotated[ + str | None, + Field( + default=None, + alias="originCountry", + description="ISO 3166-1 country of origin", + ), + ] + material_type: Annotated[ + Classification | None, + Field( + default=None, + alias="materialType", + description="Material classification (e.g., UNFC)", + ), + ] + mass_fraction: Annotated[ + float | None, + Field( + default=None, + ge=0, + le=1, + alias="massFraction", + description="Mass fraction of product (0-1, sum should equal 1)", + ), + ] + mass: Measure | None = Field(default=None, description="Mass of the material component") + recycled_mass_fraction: Annotated[ + float | None, + Field( + default=None, + ge=0, + le=1, + alias="recycledMassFraction", + description="Fraction of material that is recycled (0-1)", + ), + ] + hazardous: bool | None = Field( + default=None, + description="Whether material is hazardous", + ) + symbol: str | None = Field( + default=None, + description="Base64 encoded visual symbol for the material", + ) + material_safety_information: Annotated[ + Link | None, + Field( + default=None, + alias="materialSafetyInformation", + description="Link to material safety data sheet", + ), + ] + + @model_validator(mode="after") + def validate_hazardous_requires_safety_info(self) -> Material: + """Ensure hazardous materials have safety information.""" + if self.hazardous and not self.material_safety_information: + raise ValueError("materialSafetyInformation is required when hazardous is true") + return self diff --git a/src/dppvalidator/models/v0_6/passport.py b/src/dppvalidator/models/v0_6/passport.py new file mode 100644 index 0000000..93643c1 --- /dev/null +++ b/src/dppvalidator/models/v0_6/passport.py @@ -0,0 +1,94 @@ +"""Digital Product Passport root model per UNTP/UNCEFACT v0.6.1.""" + +from __future__ import annotations + +from datetime import datetime +from typing import Annotated, ClassVar + +from pydantic import Field, model_validator + +from dppvalidator.models.base import UNTPBaseModel +from dppvalidator.models.v0_6.credential import ( + CredentialIssuer, + CredentialStatus, + ProductPassport, +) +from dppvalidator.models.v0_6.primitives import FlexibleUri + + +class DigitalProductPassport(UNTPBaseModel): + """Digital Product Passport as a Verifiable Credential. + + Root model for UNTP DPP v0.6.1, combining DigitalProductPassport + and VerifiableCredential types per W3C VC v2 specification. + """ + + _jsonld_type: ClassVar[list[str]] = ["DigitalProductPassport", "VerifiableCredential"] + + context: Annotated[ + list[str], + Field( + alias="@context", + default=[ + "https://www.w3.org/ns/credentials/v2", + "https://test.uncefact.org/vocabulary/untp/dpp/0.6.1/", + ], + description="JSON-LD context URIs", + ), + ] + id: Annotated[ + FlexibleUri, + Field(..., description="Unique identifier (URI) for this passport"), + ] + issuer: CredentialIssuer = Field(..., description="Organisation issuing this credential") + valid_from: Annotated[ + datetime | None, + Field(default=None, alias="validFrom", description="Credential validity start"), + ] + valid_until: Annotated[ + datetime | None, + Field(default=None, alias="validUntil", description="Credential expiry date"), + ] + credential_subject: Annotated[ + ProductPassport | None, + Field( + default=None, + alias="credentialSubject", + description="The product passport content", + ), + ] + credential_status: Annotated[ + CredentialStatus | list[CredentialStatus] | None, + Field( + default=None, + alias="credentialStatus", + description="Credential revocation/suspension status per W3C VC v2", + ), + ] + + @model_validator(mode="after") + def validate_dates(self) -> DigitalProductPassport: + """Ensure validFrom is before validUntil if both are present.""" + if self.valid_from and self.valid_until and self.valid_from >= self.valid_until: + raise ValueError("validFrom must be before validUntil") + return self + + @model_validator(mode="after") + def validate_material_mass_fractions(self) -> DigitalProductPassport: + """Validate material mass fractions don't exceed 1.0. + + Per UNTP spec, mass fractions can be partial declarations (sum < 1.0). + Only sum > 1.0 is physically impossible and should error. + Semantic validation checks for exact sum when appropriate. + """ + if not self.credential_subject: + return self + materials = self.credential_subject.materials_provenance + if not materials: + return self + fractions = [m.mass_fraction for m in materials if m.mass_fraction is not None] + if fractions: + total = sum(fractions) + if total > 1.01: # Allow small tolerance for floating point + raise ValueError(f"Material mass fractions cannot exceed 1.0, got {total:.3f}") + return self diff --git a/src/dppvalidator/models/v0_6/performance.py b/src/dppvalidator/models/v0_6/performance.py new file mode 100644 index 0000000..f7f84da --- /dev/null +++ b/src/dppvalidator/models/v0_6/performance.py @@ -0,0 +1,156 @@ +"""Performance scorecard models for UNTP DPP.""" + +from __future__ import annotations + +from typing import Annotated, ClassVar + +from pydantic import Field + +from dppvalidator.models.base import UNTPBaseModel +from dppvalidator.models.v0_6.claims import Standard +from dppvalidator.models.v0_6.enums import OperationalScope +from dppvalidator.models.v0_6.primitives import Link, SecureLink + + +class EmissionsPerformance(UNTPBaseModel): + """Emissions performance scorecard.""" + + _jsonld_type: ClassVar[list[str]] = ["EmissionsPerformance"] + + carbon_footprint: Annotated[ + float, + Field( + ..., + alias="carbonFootprint", + description="Carbon footprint in KgCO2e per declared unit", + ), + ] + declared_unit: Annotated[ + str, + Field( + ..., + alias="declaredUnit", + description="Unit of product (EA, KGM, LTR) for carbon footprint basis", + ), + ] + operational_scope: Annotated[ + OperationalScope | None, + Field( + default=None, + alias="operationalScope", + description="Emissions operational scope (GHG Protocol or lifecycle boundary)", + ), + ] + primary_sourced_ratio: Annotated[ + float, + Field( + ..., + ge=0, + le=1, + alias="primarySourcedRatio", + description="Ratio of primary source emissions data (0-1)", + ), + ] + reporting_standard: Annotated[ + Standard | None, + Field( + default=None, + alias="reportingStandard", + description="Reporting standard (GHG Protocol, IFRS S2, etc.)", + ), + ] + + +class TraceabilityPerformance(UNTPBaseModel): + """Traceability performance for a value chain process.""" + + _jsonld_type: ClassVar[list[str]] = ["TraceabilityPerformance"] + + value_chain_process: Annotated[ + str | None, + Field( + default=None, + alias="valueChainProcess", + description="Industry-specific value chain process name", + ), + ] + verified_ratio: Annotated[ + float | None, + Field( + default=None, + ge=0, + le=1, + alias="verifiedRatio", + description="Proportion of traced materials (0-1)", + ), + ] + traceability_event: Annotated[ + list[SecureLink] | None, + Field( + default=None, + alias="traceabilityEvent", + description="Links to traceability events", + ), + ] + + +class CircularityPerformance(UNTPBaseModel): + """Circularity performance scorecard.""" + + _jsonld_type: ClassVar[list[str]] = ["CircularityPerformance"] + + recycling_information: Annotated[ + Link | None, + Field( + default=None, + alias="recyclingInformation", + description="Link to recycling information", + ), + ] + repair_information: Annotated[ + Link | None, + Field( + default=None, + alias="repairInformation", + description="Link to repair instructions", + ), + ] + recyclable_content: Annotated[ + float | None, + Field( + default=None, + ge=0, + le=1, + alias="recyclableContent", + description="Fraction designed to be recyclable (0-1)", + ), + ] + recycled_content: Annotated[ + float | None, + Field( + default=None, + ge=0, + le=1, + alias="recycledContent", + description="Fraction of recycled content (0-1)", + ), + ] + utility_factor: Annotated[ + float | None, + Field( + default=None, + ge=0, + alias="utilityFactor", + description="Durability indicator (lifetime / industry avg)", + ), + ] + material_circularity_indicator: Annotated[ + float | None, + Field( + default=None, + ge=0, + le=1, + alias="materialCircularityIndicator", + description="Overall circularity indicator (0-1)", + ), + ] diff --git a/src/dppvalidator/models/v0_6/primitives.py b/src/dppvalidator/models/v0_6/primitives.py new file mode 100644 index 0000000..240b706 --- /dev/null +++ b/src/dppvalidator/models/v0_6/primitives.py @@ -0,0 +1,132 @@ +"""Primitive types for UNTP DPP models.""" + +from __future__ import annotations + +import re +from typing import Annotated, ClassVar + +from pydantic import AfterValidator, Field, HttpUrl + +from dppvalidator.models.base import UNTPStrictModel +from dppvalidator.models.v0_6.enums import EncryptionMethod, HashMethod + +# URI pattern supporting: +# - HTTP/HTTPS URLs (https://example.com) +# - DIDs (did:web:example.com, did:webvh:example.com) +# - URNs (urn:uuid:123, urn:isbn:123) +# - Custom schemes (example:product/1234, gs1:01/1234) +_URI_PATTERN = re.compile( + r"^[a-zA-Z][a-zA-Z0-9+.-]*:" # scheme (RFC 3986) + r".+$", # scheme-specific part (non-empty) + re.ASCII, +) + + +def _validate_uri(value: str) -> str: + """Validate that a string is a valid URI per RFC 3986. + + Supports HTTP URLs, DIDs, URNs, and custom URI schemes. + """ + if not _URI_PATTERN.match(value): + raise ValueError( + f"Invalid URI: '{value}'. Must have format 'scheme:path' " + "(e.g., 'https://...', 'did:web:...', 'urn:uuid:...')" + ) + return value + + +# Flexible URI type for W3C VC / UNTP compatibility +# Accepts HTTP URLs, DIDs (did:web:, did:webvh:), URNs, and custom schemes +FlexibleUri = Annotated[str, AfterValidator(_validate_uri)] + + +class Measure(UNTPStrictModel): + """Numeric value with unit of measure (UNECE Rec20).""" + + _jsonld_type: ClassVar[list[str]] = ["Measure"] + + value: float = Field(..., description="The numeric value of the measure") + unit: str = Field( + ..., + description="Unit of measure from UNECE Rec20 (e.g., KGM, LTR, EA)", + ) + + +class Link(UNTPStrictModel): + """URL link with metadata.""" + + _jsonld_type: ClassVar[list[str]] = ["Link"] + + link_url: Annotated[ + HttpUrl | None, + Field(default=None, alias="linkURL", description="The URL of the target resource"), + ] + link_name: Annotated[ + str | None, + Field(default=None, alias="linkName", description="Display name for the target resource"), + ] + link_type: Annotated[ + str | None, + Field(default=None, alias="linkType", description="Type of the target resource"), + ] + + +class SecureLink(UNTPStrictModel): + """Link with hash and optional encryption for tamper evidence.""" + + _jsonld_type: ClassVar[list[str]] = ["SecureLink", "Link"] + + link_url: Annotated[ + HttpUrl | None, + Field(default=None, alias="linkURL", description="The URL of the target resource"), + ] + link_name: Annotated[ + str | None, + Field(default=None, alias="linkName", description="Display name for the target resource"), + ] + link_type: Annotated[ + str | None, + Field(default=None, alias="linkType", description="Type of the target resource"), + ] + hash_digest: Annotated[ + str | None, + Field(default=None, alias="hashDigest", description="Hash of the file"), + ] + hash_method: Annotated[ + HashMethod | None, + Field( + default=None, alias="hashMethod", description="Hashing algorithm (SHA-256 recommended)" + ), + ] + encryption_method: Annotated[ + EncryptionMethod | None, + Field( + default=None, + alias="encryptionMethod", + description="Encryption algorithm (AES recommended)", + ), + ] + + +class Classification(UNTPStrictModel): + """Classification scheme and code representing a category value.""" + + _jsonld_type: ClassVar[list[str]] = ["Classification"] + + id: Annotated[ + FlexibleUri, + Field(..., description="Globally unique URI representing the classifier value"), + ] + code: Annotated[ + str | None, + Field(default=None, description="Classification code within the scheme"), + ] + name: str = Field(..., description="Name of the classification") + scheme_id: Annotated[ + FlexibleUri | None, + Field(default=None, alias="schemeID", description="Classification scheme ID"), + ] + scheme_name: Annotated[ + str | None, + Field(default=None, alias="schemeName", description="Name of the classification scheme"), + ] diff --git a/src/dppvalidator/models/v0_6/product.py b/src/dppvalidator/models/v0_6/product.py new file mode 100644 index 0000000..ce664cf --- /dev/null +++ b/src/dppvalidator/models/v0_6/product.py @@ -0,0 +1,99 @@ +"""Product-related models for UNTP DPP.""" + +from __future__ import annotations + +from datetime import date +from typing import Annotated, ClassVar + +from pydantic import Field + +from dppvalidator.models.base import UNTPBaseModel +from dppvalidator.models.v0_6.identifiers import Facility, IdentifierScheme, Party +from dppvalidator.models.v0_6.primitives import Classification, FlexibleUri, Link, Measure + + +class Dimension(UNTPBaseModel): + """Physical dimensions (length, width, height) and weight/volume.""" + + _jsonld_type: ClassVar[list[str]] = ["Dimension"] + + weight: Measure | None = Field(default=None, description="Weight of the product") + length: Measure | None = Field(default=None, description="Length of the product") + width: Measure | None = Field(default=None, description="Width of the product") + height: Measure | None = Field(default=None, description="Height of the product") + volume: Measure | None = Field(default=None, description="Displacement volume") + + +class Characteristics(UNTPBaseModel): + """Extension point for industry/product-specific characteristics.""" + + _jsonld_type: ClassVar[list[str]] = ["Characteristics"] + + +class Product(UNTPBaseModel): + """Product information including identification and manufacturer details.""" + + _jsonld_type: ClassVar[list[str]] = ["Product"] + + id: Annotated[ + FlexibleUri, + Field(..., description="Globally unique ID of the product as a URI"), + ] + name: str = Field(..., description="Registered name of the product") + registered_id: Annotated[ + str | None, + Field( + default=None, + alias="registeredId", + description="Registration number within the register", + ), + ] + id_scheme: Annotated[ + IdentifierScheme | None, + Field(default=None, alias="idScheme", description="Identifier scheme for this product"), + ] + batch_number: Annotated[ + str | None, + Field(default=None, alias="batchNumber", description="Production batch identifier"), + ] + product_image: Annotated[ + Link | None, + Field(default=None, alias="productImage", description="Reference to product image"), + ] + description: str | None = Field(default=None, description="Textual product description") + characteristics: Characteristics | None = Field( + default=None, description="Industry-specific characteristics" + ) + product_category: Annotated[ + list[Classification] | None, + Field(default=None, alias="productCategory", description="Product classification codes"), + ] + further_information: Annotated[ + list[Link] | None, + Field(default=None, alias="furtherInformation", description="Additional information links"), + ] + produced_by_party: Annotated[ + Party | None, + Field(default=None, alias="producedByParty", description="Manufacturing party"), + ] + produced_at_facility: Annotated[ + Facility | None, + Field(default=None, alias="producedAtFacility", description="Manufacturing facility"), + ] + production_date: Annotated[ + date | None, + Field(default=None, alias="productionDate", description="ISO 8601 production date"), + ] + country_of_production: Annotated[ + str | None, + Field( + default=None, + alias="countryOfProduction", + description="ISO 3166-1 country code of production", + ), + ] + serial_number: Annotated[ + str | None, + Field(default=None, alias="serialNumber", description="Serialised item identifier"), + ] + dimensions: Dimension | None = Field(default=None, description="Physical dimensions") diff --git a/src/dppvalidator/models/v0_7/__init__.py b/src/dppvalidator/models/v0_7/__init__.py new file mode 100644 index 0000000..62a93a4 --- /dev/null +++ b/src/dppvalidator/models/v0_7/__init__.py @@ -0,0 +1,95 @@ +"""Pydantic models for UNTP DPP **v0.7.0**. + +Built in Phase 3 of docs/plans/UNTP_0.7.0_MIGRATION.md from the bundled +schema at ``src/dppvalidator/schemas/data/untp-dpp-schema-0.7.0.json`` +(SHA-pinned to upstream commit ``707cd526...`` of +``opensource.unicc.org/un/unece/uncefact/spec-untp``). + +The v0.7.0 envelope is structurally different from v0.6.x: the +``ProductPassport`` envelope class is gone, ``credentialSubject`` is a +:class:`Product` directly, and the three "scorecard" classes +(``EmissionsPerformance``, ``CircularityPerformance``, +``TraceabilityPerformance``) are folded into a single +:class:`Claim.claimedPerformance: list[Performance]` shape on the Product. +See §2 of the plan for the full delta tables. + +This subpackage is opt-in for the 0.4.x line: callers reach it explicitly +via ``from dppvalidator.models.v0_7 import DigitalProductPassport``. The +top-level ``dppvalidator.models`` namespace continues to re-export v0.6.x +shapes through 0.4.x; Phase 9 (validator 0.5.0) flips that default. +""" + +from dppvalidator.models.v0_7.claims import ( + Claim, + ConformityTopic, + Performance, + Period, + Score, +) +from dppvalidator.models.v0_7.envelope import ( + BitstringStatusListEntry, + CredentialIssuer, + CredentialStatus, + DigitalProductPassport, + IssuingSoftware, + RenderTemplate2024, + SoftwareVendor, +) +from dppvalidator.models.v0_7.identifiers import ( + Address, + Country, + Facility, + IdentifierScheme, + Party, + PartyRole, + PartyRoleEnum, +) +from dppvalidator.models.v0_7.materials import Material +from dppvalidator.models.v0_7.primitives import ( + Characteristics, + Classification, + Dimension, + FlexibleUri, + Image, + Link, + Measure, +) +from dppvalidator.models.v0_7.product import Package, Product + +__all__ = [ + # Envelope + "DigitalProductPassport", + "CredentialIssuer", + "CredentialStatus", + "BitstringStatusListEntry", + "IssuingSoftware", + "SoftwareVendor", + "RenderTemplate2024", + # Identifiers + "Address", + "Country", + "Facility", + "IdentifierScheme", + "Party", + "PartyRole", + "PartyRoleEnum", + # Primitives + "Characteristics", + "Classification", + "Dimension", + "FlexibleUri", + "Image", + "Link", + "Measure", + # Claims + "Claim", + "ConformityTopic", + "Performance", + "Period", + "Score", + # Materials + "Material", + # Product + "Package", + "Product", +] diff --git a/src/dppvalidator/models/v0_7/claims.py b/src/dppvalidator/models/v0_7/claims.py new file mode 100644 index 0000000..8cbf213 --- /dev/null +++ b/src/dppvalidator/models/v0_7/claims.py @@ -0,0 +1,217 @@ +"""Claim and conformity-related types for UNTP v0.7.0. + +This is where the biggest semantic shift lives compared to v0.6.x: + +- The three v0.6 "scorecard" classes (``EmissionsPerformance``, + ``CircularityPerformance``, ``TraceabilityPerformance``) are gone. They + fold into :class:`Claim.claimedPerformance: list[Performance]`, with the + topic of the claim carried by :class:`ConformityTopic` entries on + :attr:`Claim.conformityTopic`. +- The v0.6 ``Metric`` class is gone. Its content is split across + :class:`Performance.metric` (the metric being measured), + :class:`Performance.measure` (the numeric reading), and + :class:`Performance.score` (the qualitative score). +- The v0.6 ``Standard``, ``Regulation``, ``Criterion`` classes are now + inlined under :class:`Claim` as plain ``referenceStandard[]``, + ``referenceRegulation[]``, ``referenceCriteria[]`` arrays of free-form + reference objects. +- :class:`Period` is new — used by :attr:`Claim.applicablePeriod`. + +Cross-field invariants implemented as model validators: + +- :class:`Period`: ``startDate`` must be strictly before ``endDate`` when + both are present. +- :class:`Performance`: at least one of ``measure`` / ``score`` must be + present. A claim that conveys neither is meaningless. +- :class:`Claim`: when ``claimedPerformance`` is non-empty, an + ``applicablePeriod`` SHOULD be supplied (advisory; logged via the + semantic-rule layer in Phase 3b — not enforced here so that compact + claims still validate). +""" + +from __future__ import annotations + +from datetime import date +from typing import Annotated, Any, ClassVar + +from pydantic import Field, model_validator + +from dppvalidator.models.base import UNTPBaseModel +from dppvalidator.models.v0_7.primitives import ( + Classification, + FlexibleUri, + Link, + Measure, +) + + +class ConformityTopic(UNTPBaseModel): + """A topic that a claim conforms to (e.g. emissions, circularity, traceability). + + Drawn from the UNTP topics vocabulary at + ``https://vocabulary.uncefact.org/ConformityTopic#``. ``id`` and + ``name`` are required; ``definition`` carries the rich human-readable + definition. + """ + + _jsonld_type: ClassVar[list[str]] = ["ConformityTopic"] + + id: FlexibleUri = Field(..., description="URI identifying the conformity topic.") + name: str = Field(..., description="Short name (e.g. ``emissions``).") + definition: Annotated[str | None, Field(default=None)] + + +class Period(UNTPBaseModel): + """A date interval used by claims and reporting periods. + + Both bounds are optional individually so callers can express open-ended + periods, but if both are present the start must precede the end. + """ + + _jsonld_type: ClassVar[list[str]] = ["Period"] + + start_date: Annotated[date | None, Field(default=None, alias="startDate")] + end_date: Annotated[date | None, Field(default=None, alias="endDate")] + + @model_validator(mode="after") + def _validate_interval(self) -> Period: + if ( + self.start_date is not None + and self.end_date is not None + and self.start_date > self.end_date + ): + raise ValueError( + "Period.startDate must be on or before Period.endDate " + f"(got {self.start_date} > {self.end_date})", + ) + return self + + +class Score(UNTPBaseModel): + """A qualitative performance grade (e.g. ``AAA``, ``B``, ``A+``). + + The grade itself is a coded value (``code``); ``rank`` is an integer + where 1 is the highest rank within the scoring framework. The + framework that defines the codes is referenced via the parent + :class:`Performance` and the :class:`Claim` it belongs to (not by + Score itself). + """ + + _jsonld_type: ClassVar[list[str]] = ["Score"] + + code: str = Field(..., description="Coded score value (e.g. 'AAA', 'B').") + rank: Annotated[ + int | None, + Field( + default=None, + description="Integer rank within the framework (1 = highest).", + ), + ] + definition: Annotated[ + str | None, + Field(default=None, description="Description of the meaning of this score."), + ] + + +class Performance(UNTPBaseModel): + """A single performance reading: metric + measure and/or score. + + Replaces the v0.6 ``Metric`` class (and absorbs the per-topic scorecard + classes via :class:`Claim`). At least one of ``measure`` or ``score`` + must be present — a Performance that says *nothing* about the metric + is meaningless. + """ + + _jsonld_type: ClassVar[list[str]] = ["Performance"] + + metric: dict[str, Any] = Field( + ..., + description=( + "The metric being measured (free-form object; the upstream schema does " + "not enforce a sub-schema here, leaving room for industry-specific shapes)." + ), + ) + measure: Measure | None = Field(default=None, description="Quantitative reading.") + score: Score | None = Field(default=None, description="Qualitative grade.") + + @model_validator(mode="after") + def _at_least_one_outcome(self) -> Performance: + if self.measure is None and self.score is None: + raise ValueError( + "Performance must have at least one of ``measure`` or ``score`` — " + "a performance reading with neither value is meaningless.", + ) + return self + + +class Claim(UNTPBaseModel): + """A conformity claim attached to a :class:`Product` in v0.7.0. + + Where v0.6.x split conformity into ``conformityClaim`` (typed claims) + plus three separate scorecard classes, v0.7.0 unifies everything here: + set ``conformityTopic`` to mark the topic, ``claimedPerformance`` to + carry the readings, and ``referenceCriteria/Standard/Regulation`` to + point at the framework the claim conforms to. + + Reference fields are intentionally typed as ``list[dict[str, Any]]``: + the upstream schema leaves their internal shape open for now (Phase 3c + revisits this when the eudpp_jsonld exporter mapping is reworked). + """ + + _jsonld_type: ClassVar[list[str]] = ["Claim"] + + id: FlexibleUri = Field( + ..., description="Globally unique identifier of this claim (URI or UUID)." + ) + name: str = Field(..., description="Name of the claim — usually mirrors the criterion name.") + description: Annotated[str | None, Field(default=None)] + conformity_topic: Annotated[ + list[ConformityTopic], + Field(default_factory=list, alias="conformityTopic"), + ] + reference_criteria: Annotated[ + list[dict[str, Any]], + Field( + default_factory=list, + alias="referenceCriteria", + description="The criteria this claim is asserted against.", + ), + ] + reference_standard: Annotated[ + list[dict[str, Any]], + Field(default_factory=list, alias="referenceStandard"), + ] + reference_regulation: Annotated[ + list[dict[str, Any]], + Field(default_factory=list, alias="referenceRegulation"), + ] + claim_date: Annotated[ + date | None, + Field(default=None, alias="claimDate"), + ] + applicable_period: Annotated[ + Period | None, + Field(default=None, alias="applicablePeriod"), + ] + claimed_performance: Annotated[ + list[Performance], + Field( + default_factory=list, + alias="claimedPerformance", + description=( + "The performance levels claimed by this claim — replaces the v0.6.x " + "Emissions/Circularity/TraceabilityPerformance scorecards." + ), + ), + ] + evidence: Annotated[ + list[Link], + Field( + default_factory=list, + description="URIs of evidence supporting the claim (typically DCC credentials).", + ), + ] + classification: Annotated[ + list[Classification], + Field(default_factory=list), + ] diff --git a/src/dppvalidator/models/v0_7/envelope.py b/src/dppvalidator/models/v0_7/envelope.py new file mode 100644 index 0000000..9d3c7f3 --- /dev/null +++ b/src/dppvalidator/models/v0_7/envelope.py @@ -0,0 +1,245 @@ +"""W3C VC envelope and DPP credential class for UNTP v0.7.0. + +This module is what callers reach for when they want to validate a v0.7.0 +Digital Product Passport. The :class:`DigitalProductPassport` carries the +W3C VC v2 envelope (``@context``, ``id``, ``issuer``, ``validFrom``, +``validUntil``, …) and a :class:`Product` as its ``credentialSubject`` +— there is **no** ``ProductPassport`` envelope class in v0.7.0. + +New top-level fields compared to v0.6.x (now first-class): + +- :class:`IssuingSoftware` — software-vendor metadata for the credential. +- :class:`RenderTemplate2024` — render-method spec (stored, not executed). +- :class:`BitstringStatusListEntry` — first-class status-list shape. +- ``name`` is now a required envelope field. + +Cross-field invariants: + +- ``validFrom`` MUST precede ``validUntil`` when both are set (port from v0.6). +- ``name`` MUST be non-empty (now required by the schema). +""" + +from __future__ import annotations + +from datetime import datetime +from typing import Annotated, ClassVar + +from pydantic import Field, model_validator + +from dppvalidator.models.base import UNTPBaseModel, UNTPStrictModel +from dppvalidator.models.v0_7.identifiers import Party +from dppvalidator.models.v0_7.primitives import FlexibleUri +from dppvalidator.models.v0_7.product import Product + + +class CredentialIssuer(UNTPStrictModel): + """The party that issued a v0.7.0 DPP. + + The shape is the same as v0.6.x ``CredentialIssuer`` (id, name, + issuerAlsoKnownAs[]). The ``id`` MUST be a W3C DID per the spec; the + model accepts any URI to stay forgiving on intake. + """ + + _jsonld_type: ClassVar[list[str]] = ["CredentialIssuer"] + + id: FlexibleUri = Field( + ..., + description="W3C DID (did:web, did:webvh, …) or HTTPS identifier of the issuer.", + ) + name: str = Field(..., description="Human-readable issuer name.") + issuer_also_known_as: Annotated[ + list[Party] | None, + Field( + default=None, + alias="issuerAlsoKnownAs", + description="Other registered identities (parties) for this issuer.", + ), + ] + + +class SoftwareVendor(UNTPBaseModel): + """Vendor of the software that issued the credential. + + Used by :class:`IssuingSoftware`. The ``id`` is typically a + ``did:web:`` for the vendor's domain. + """ + + _jsonld_type: ClassVar[list[str]] = ["SoftwareVendor"] + + id: FlexibleUri = Field(..., description="DID or URI identifying the vendor.") + name: str = Field(..., description="Vendor company / organisation name.") + + +class IssuingSoftware(UNTPBaseModel): + """Metadata about the software that emitted this credential. + + New top-level field in v0.7.0 — captures the software supply chain so + consumers can trace which tool generated a passport. Optional but + recommended. + """ + + _jsonld_type: ClassVar[list[str]] = ["IssuingSoftware"] + + id: FlexibleUri = Field(..., description="URI identifying the issuing software product.") + name: str = Field(..., description="Product name (e.g. 'Sample Passport Builder').") + version: str = Field(..., description="Software version (e.g. '2026.04.1').") + vendor: SoftwareVendor = Field(..., description="The vendor that publishes this software.") + + +class RenderTemplate2024(UNTPBaseModel): + """A render-method specification (W3C VC 2.0 Render Method 2024). + + v0.7.0 stores rendering hints alongside the credential. We capture the + fields without executing them — actual rendering is a downstream + concern (out of scope per the migration plan §9). + """ + + _jsonld_type: ClassVar[list[str]] = ["RenderTemplate2024"] + + id: Annotated[FlexibleUri | None, Field(default=None)] + type: Annotated[ + str | list[str] | None, + Field( + default=None, description="Render-method type identifier (overrides the base default)." + ), + ] + name: Annotated[str | None, Field(default=None)] + template: Annotated[ + str | None, + Field( + default=None, + description="Inline template body (often a URL to a template file).", + ), + ] + digest_multibase: Annotated[ + str | None, + Field(default=None, alias="digestMultibase"), + ] + media_type: Annotated[ + str | None, + Field(default=None, alias="mediaType"), + ] + + +class BitstringStatusListEntry(UNTPBaseModel): + """W3C VC Bitstring Status List entry. + + Used by :attr:`DigitalProductPassport.credentialStatus`. v0.7.0 lifts + this to a first-class type (was a free-form ``CredentialStatus`` in + v0.6.x). + """ + + _jsonld_type: ClassVar[list[str]] = ["BitstringStatusListEntry"] + + id: FlexibleUri = Field(..., description="URI of this status entry.") + type: str = Field(default="BitstringStatusListEntry") + status_purpose: Annotated[ + str | None, + Field(default=None, alias="statusPurpose", description="e.g. 'revocation', 'suspension'."), + ] + status_list_index: Annotated[ + str | None, + Field(default=None, alias="statusListIndex"), + ] + status_list_credential: Annotated[ + FlexibleUri | None, + Field(default=None, alias="statusListCredential"), + ] + + +# Type alias kept compatible with v0.6 for the engine's ``credentialStatus`` +# field. v0.7.0 narrows the shape to BitstringStatusListEntry but a generic +# alias helps downstream code that branches on version. +CredentialStatus = BitstringStatusListEntry + + +class DigitalProductPassport(UNTPBaseModel): + """Root model for a UNTP v0.7.0 Digital Product Passport. + + Required envelope fields (per the upstream JSON Schema): + ``@context``, ``id``, ``issuer``, ``validFrom``, ``name``, ``credentialSubject``. + + Cross-field invariants: + + - ``validFrom`` < ``validUntil`` when both present. + - ``name`` is non-empty (delegated to Pydantic ``min_length=1``). + + The ``credentialSubject`` is a :class:`Product` directly — there is no + ``ProductPassport`` envelope class in v0.7.0. + """ + + _jsonld_type: ClassVar[list[str]] = ["DigitalProductPassport", "VerifiableCredential"] + + context: Annotated[ + list[str], + Field( + ..., + alias="@context", + description="JSON-LD context URIs. First entry is W3C VC v2; second is the UNTP 0.7.0 context.", + min_length=2, + ), + ] + id: FlexibleUri = Field(..., description="Globally unique DPP credential identifier (URI).") + name: str = Field( + ..., + min_length=1, + description="Human-readable credential title (now required by the v0.7.0 schema).", + ) + issuer: CredentialIssuer = Field(..., description="The party issuing this credential.") + valid_from: Annotated[ + datetime, + Field( + ..., + alias="validFrom", + description="Credential validity start (now required by the v0.7.0 schema).", + ), + ] + valid_until: Annotated[ + datetime | None, + Field(default=None, alias="validUntil", description="Credential expiry (optional)."), + ] + issuing_software: Annotated[ + IssuingSoftware | None, + Field( + default=None, + alias="issuingSoftware", + description="Metadata about the software that emitted this credential.", + ), + ] + render_method: Annotated[ + list[RenderTemplate2024] | None, + Field( + default=None, + alias="renderMethod", + description="Render-method specifications for human display.", + ), + ] + credential_status: Annotated[ + BitstringStatusListEntry | list[BitstringStatusListEntry] | None, + Field( + default=None, + alias="credentialStatus", + description="Revocation / suspension status entries.", + ), + ] + credential_subject: Annotated[ + Product, + Field( + ..., + alias="credentialSubject", + description="The product that this DPP describes (now a Product directly, no envelope).", + ), + ] + + @model_validator(mode="after") + def _validate_dates(self) -> DigitalProductPassport: + if ( + self.valid_until is not None + and self.valid_from is not None + and self.valid_from >= self.valid_until + ): + raise ValueError( + "DigitalProductPassport.validFrom must be strictly before validUntil " + f"(got {self.valid_from.isoformat()} >= {self.valid_until.isoformat()}).", + ) + return self diff --git a/src/dppvalidator/models/v0_7/identifiers.py b/src/dppvalidator/models/v0_7/identifiers.py new file mode 100644 index 0000000..e62d132 --- /dev/null +++ b/src/dppvalidator/models/v0_7/identifiers.py @@ -0,0 +1,210 @@ +"""Identifier and party-related types for UNTP v0.7.0. + +Compared to v0.6.x: + +- :class:`Country` is new. v0.6 stored ISO-3166 country codes as bare + strings (``Material.originCountry: "DE"``); v0.7 wraps them as + ``{"countryCode": "DE", "countryName": "Germany"}`` objects with + ``countryCode`` required and ``countryName`` recommended. +- :class:`Address` is new and reuses schema.org PostalAddress shape. +- :class:`Party` adds ``description``, ``registeredId``, and a nested + ``idScheme`` (replacing the v0.6 top-level ``IdentifierScheme`` class + inlined into Party). +- :class:`PartyRole` is new and wraps a Party with a ``role`` enum. + +Cross-field invariants (per the plan): + +- :class:`Country` ``countryCode`` matches ISO-3166-1 alpha-2 (two ASCII + uppercase letters). Validators import the existing alpha-2 enforcer from + ``dppvalidator.vocabularies.code_lists``. +""" + +from __future__ import annotations + +import re +from enum import Enum +from typing import Annotated, ClassVar + +from pydantic import Field, field_validator + +from dppvalidator.models.base import UNTPBaseModel +from dppvalidator.models.v0_7.primitives import FlexibleUri + +_ISO_3166_ALPHA2_RE = re.compile(r"^[A-Z]{2}$") + + +class IdentifierScheme(UNTPBaseModel): + """Reference to an identifier scheme that defines a code or URI space. + + v0.7.0 inlines this on :class:`Party.idScheme` and + :attr:`dppvalidator.models.v0_7.product.Product.idScheme`. v0.6 had it + as a top-level reusable class; the shape is the same (``id`` + ``name``). + """ + + _jsonld_type: ClassVar[list[str]] = ["IdentifierScheme"] + + id: FlexibleUri = Field(..., description="URI of the identifier scheme.") + name: str = Field(..., description="Human-readable name of the identifier scheme.") + + +class Country(UNTPBaseModel): + """ISO-3166 country code + name. + + Wire shape: ``{"countryCode": "DE", "countryName": "Germany"}``. + Only ``countryCode`` is strictly required; ``countryName`` is + recommended for human display but ``Country`` payloads from automated + sources may legitimately omit it. + """ + + _jsonld_type: ClassVar[list[str]] = ["Country"] + + country_code: Annotated[ + str, + Field( + ..., + alias="countryCode", + description="ISO-3166-1 alpha-2 country code (two uppercase ASCII letters).", + ), + ] + country_name: Annotated[ + str | None, + Field( + default=None, + alias="countryName", + description="Country name as published by ISO-3166-1.", + ), + ] + + @field_validator("country_code") + @classmethod + def _validate_alpha2(cls, value: str) -> str: + if not _ISO_3166_ALPHA2_RE.match(value): + raise ValueError( + f"Country.countryCode must be a two-letter ISO-3166-1 alpha-2 code " + f"(got {value!r}).", + ) + return value + + +class Address(UNTPBaseModel): + """Postal address — schema.org-compatible. + + v0.7.0 introduces this for facility / party addresses; v0.6.x had no + direct equivalent (addresses lived as free-form strings). + """ + + _jsonld_type: ClassVar[list[str]] = ["Address"] + + street_address: Annotated[ + str | None, + Field(default=None, alias="streetAddress"), + ] + postal_code: Annotated[str | None, Field(default=None, alias="postalCode")] + address_locality: Annotated[ + str | None, + Field(default=None, alias="addressLocality", description="City / town / village."), + ] + address_region: Annotated[ + str | None, + Field(default=None, alias="addressRegion", description="State / province / region."), + ] + address_country: Annotated[ + Country | None, + Field(default=None, alias="addressCountry"), + ] + + +class Party(UNTPBaseModel): + """An entity (legal or otherwise) referenced by a credential. + + v0.7.0 adds ``description``, ``registeredId``, and a nested + :class:`IdentifierScheme` on ``idScheme`` — the v0.6 ``Party`` had + only ``id`` and ``name`` plus optionally a top-level ``IdentifierScheme`` + reference. + """ + + _jsonld_type: ClassVar[list[str]] = ["Party"] + + id: FlexibleUri = Field(..., description="Globally unique identifier of the party (URI / DID).") + name: str = Field(..., description="Legal registered name of the party.") + description: Annotated[str | None, Field(default=None)] + registered_id: Annotated[ + str | None, + Field( + default=None, + alias="registeredId", + description="The registration number within the identifier scheme (alphanumeric).", + ), + ] + id_scheme: Annotated[ + IdentifierScheme | None, + Field( + default=None, + alias="idScheme", + description="The scheme that the ``id`` and ``registeredId`` are drawn from.", + ), + ] + + +class Facility(UNTPBaseModel): + """A facility (production site, warehouse, smelter, …). + + Used as ``Product.producedAtFacility`` and as the credential subject of + a :class:`DigitalFacilityRecord` (out of scope — Phase 3 only models + DPP). Shape is permissive in v0.7 because the upstream schema treats + facility metadata as extension-friendly. + """ + + _jsonld_type: ClassVar[list[str]] = ["Facility"] + + id: FlexibleUri = Field(..., description="Globally unique identifier of the facility.") + name: Annotated[str | None, Field(default=None)] + id_scheme: Annotated[ + IdentifierScheme | None, + Field(default=None, alias="idScheme"), + ] + address: Annotated[Address | None, Field(default=None)] + + +class PartyRoleEnum(str, Enum): + """Closed enumeration of party-relationship roles in UNTP v0.7.0. + + Mirrors the schema's enum at ``$defs.PartyRole.properties.role.enum``. + """ + + OWNER = "owner" + PRODUCER = "producer" + MANUFACTURER = "manufacturer" + PROCESSOR = "processor" + REMANUFACTURER = "remanufacturer" + RECYCLER = "recycler" + OPERATOR = "operator" + SERVICE_PROVIDER = "serviceProvider" + INSPECTOR = "inspector" + CERTIFIER = "certifier" + LOGISTICS_PROVIDER = "logisticsProvider" + CARRIER = "carrier" + CONSIGNOR = "consignor" + CONSIGNEE = "consignee" + IMPORTER = "importer" + EXPORTER = "exporter" + DISTRIBUTOR = "distributor" + RETAILER = "retailer" + BRAND_OWNER = "brandOwner" + REGULATOR = "regulator" + + +class PartyRole(UNTPBaseModel): + """A :class:`Party` plus the role it plays in a relationship. + + v0.7.0 introduces this so ``Product.relatedParty`` can be a list of + typed (role, party) pairs — replacing the v0.6 single + ``producedByParty: Party`` field with something more expressive. + """ + + _jsonld_type: ClassVar[list[str]] = ["PartyRole"] + + role: PartyRoleEnum = Field( + ..., description="The role played by the party in this relationship." + ) + party: Party = Field(..., description="The party that has the specified role.") diff --git a/src/dppvalidator/models/v0_7/materials.py b/src/dppvalidator/models/v0_7/materials.py new file mode 100644 index 0000000..782e80f --- /dev/null +++ b/src/dppvalidator/models/v0_7/materials.py @@ -0,0 +1,113 @@ +"""Material provenance for UNTP v0.7.0. + +Compared to v0.6.x: + +- ``materialType``, ``originCountry``, and ``massFraction`` are now + **required** (per the upstream schema). v0.6.x permitted all three to be + absent. The Phase 4 compatibility shim emits an ``UPG004`` warning when + upgrading legacy data that lacks any of these. +- ``originCountry`` shape moved from a bare ISO-3166 string (``"DE"``) to a + :class:`Country` object (``{"countryCode": "DE", "countryName": "Germany"}``). +- ``symbol`` moved from a bare base64 string to a structured + :class:`Image` (``{"contentType": …, "content": …}``). + +Cross-field invariants (per the plan): + +- ``hazardous=True`` requires ``materialSafetyInformation`` (port of the + v0.6.x SEM-class rule into a model-level invariant). +- Mass-fraction sum across an array of ``Material`` lives at the parent + (Product) level, not here — see :class:`dppvalidator.models.v0_7.product.Product`. +""" + +from __future__ import annotations + +from typing import Annotated, ClassVar + +from pydantic import Field, model_validator + +from dppvalidator.models.base import UNTPBaseModel +from dppvalidator.models.v0_7.identifiers import Country +from dppvalidator.models.v0_7.primitives import ( + Classification, + Image, + Link, + Measure, +) + + +class Material(UNTPBaseModel): + """Origin and composition info for one material in a product. + + Required: ``name``, ``originCountry``, ``materialType``, ``massFraction``. + Optional fields fill in mass / safety / recycled-content metadata. + """ + + _jsonld_type: ClassVar[list[str]] = ["Material"] + + name: str = Field(..., description="Material name (e.g. 'Egyptian Cotton').") + origin_country: Annotated[ + Country, + Field( + ..., + alias="originCountry", + description="ISO-3166 country of origin (now structured, was a bare string in v0.6.x).", + ), + ] + material_type: Annotated[ + Classification, + Field( + ..., + alias="materialType", + description="Classification of the material (e.g. drawn from UNFC).", + ), + ] + mass_fraction: Annotated[ + float, + Field( + ..., + ge=0, + le=1, + alias="massFraction", + description="Mass fraction of the product represented by this material (0..1).", + ), + ] + mass: Measure | None = Field( + default=None, description="Optional absolute mass of the material." + ) + recycled_mass_fraction: Annotated[ + float | None, + Field( + default=None, + ge=0, + le=1, + alias="recycledMassFraction", + description="Fraction of this material that is recycled content (0..1).", + ), + ] + hazardous: bool | None = Field( + default=None, + description="Whether the material is hazardous (drives the ``materialSafetyInformation`` requirement).", + ) + symbol: Annotated[ + Image | None, + Field( + default=None, + description="Visual symbol for the material (was a bare base64 string in v0.6.x).", + ), + ] + material_safety_information: Annotated[ + Link | None, + Field( + default=None, + alias="materialSafetyInformation", + description="Link to a material safety data sheet — required when ``hazardous`` is true.", + ), + ] + + @model_validator(mode="after") + def _hazardous_implies_safety_info(self) -> Material: + if self.hazardous and self.material_safety_information is None: + raise ValueError( + "Material.materialSafetyInformation is required when ``hazardous`` is true.", + ) + return self diff --git a/src/dppvalidator/models/v0_7/primitives.py b/src/dppvalidator/models/v0_7/primitives.py new file mode 100644 index 0000000..a41e0c0 --- /dev/null +++ b/src/dppvalidator/models/v0_7/primitives.py @@ -0,0 +1,240 @@ +"""Primitive types reused across UNTP v0.7.0 model classes. + +Compared to v0.6.x: + +- :class:`Classification` renames ``schemeID`` → ``schemeId`` (camelCase + fix) and adds an optional ``definition`` field. +- :class:`Link` absorbs the v0.6 ``SecureLink`` by adding optional + ``digestMultibase`` and ``mediaType`` fields. There is no separate + ``SecureLink`` class in v0.7.0. +- :class:`Measure` adds optional ``lowerTolerance`` and ``upperTolerance``. +- :class:`Image` is new (was an inline base64 string in v0.6.x ``Material.symbol``). +- :class:`Characteristics` and :class:`Dimension` keep the same shape but + drop their leading ``type`` discriminator from the schema. +""" + +from __future__ import annotations + +from typing import Annotated, Any, ClassVar + +from pydantic import Field, model_validator + +from dppvalidator.models.base import UNTPBaseModel, UNTPStrictModel + +# --------------------------------------------------------------------------- +# FlexibleUri — version-neutral URI handling. +# +# v0.7.0 keeps the same lenient stance as v0.6.x: schema fields typed as +# ``format: uri`` accept HTTPS URIs, DIDs, and other URI schemes. We model +# the wire shape as a plain ``str`` and let the JSON-LD layer carry the URI +# semantics. +# --------------------------------------------------------------------------- +FlexibleUri = str + + +class Classification(UNTPBaseModel): + """A code drawn from a controlled classification scheme. + + Used for product categories (e.g. UN CPC), material types (e.g. UNFC), + etc. The ``schemeId`` URI identifies the scheme; ``code`` is the code + value within the scheme; ``schemeName`` is the human-readable scheme + label. + + v0.6.x called this same shape ``schemeID`` (uppercase D); v0.7.0 fixes + the camelCase as ``schemeId`` (lowercase d) and requires ``schemeName``. + """ + + _jsonld_type: ClassVar[list[str]] = ["Classification"] + + scheme_id: Annotated[ + FlexibleUri, + Field( + ..., + alias="schemeId", + description="URI identifying the classification scheme.", + ), + ] + scheme_name: Annotated[ + str, + Field( + ..., + alias="schemeName", + description="Human-readable name of the classification scheme (e.g. 'UN CPC').", + ), + ] + code: str = Field(..., description="The classification code within the scheme.") + name: str = Field(..., description="Human-readable name for the classification value.") + definition: Annotated[ + str | None, + Field(default=None, description="Optional rich definition of the classification value."), + ] + + +class Link(UNTPBaseModel): + """A reference to a related document. + + v0.7.0 absorbs v0.6 ``SecureLink`` here: ``digestMultibase`` and + ``mediaType`` are now first-class on :class:`Link` itself. The + cross-field invariant — if you assert a hash, declare the media type — + fires as a model validator below. + """ + + _jsonld_type: ClassVar[list[str]] = ["Link"] + + href: FlexibleUri = Field( + ..., + alias="linkURL", + description="URL the link points at (alias 'linkURL' for compatibility with the schema).", + ) + name: Annotated[ + str | None, Field(default=None, description="Human-readable label for the link.") + ] + description: Annotated[str | None, Field(default=None)] + relationship: Annotated[ + str | None, + Field( + default=None, + description="Free-form classification of the link relationship (e.g. 'evidence', 'specification').", + ), + ] + media_type: Annotated[ + str | None, + Field( + default=None, + alias="mediaType", + description="IANA media type of the linked document (RFC 6838).", + ), + ] + digest_multibase: Annotated[ + str | None, + Field( + default=None, + alias="digestMultibase", + description=( + "Multibase-encoded multihash (https://www.w3.org/TR/vc-data-integrity/) " + "of the linked content. If set, ``mediaType`` SHOULD also be set so the " + "consumer knows how to interpret the bytes." + ), + ), + ] + + @model_validator(mode="after") + def _digest_implies_media_type(self) -> Link: + # Warning-grade invariant per docs/plans/UNTP_0.7.0_MIGRATION.md §3.2: + # we *recommend* mediaType when a digest is present, but we don't + # block validation — third-party documents that pin a hash without + # declaring a media type are still consumable. + # Semantic-rule layer (Phase 3b) is where this becomes a warning. + return self + + +class Measure(UNTPStrictModel): + """A numeric measurement with a unit-of-measure code. + + v0.7.0 adds optional ``lowerTolerance`` and ``upperTolerance`` to + express measurement precision (additive over v0.6.x). + """ + + _jsonld_type: ClassVar[list[str]] = ["Measure"] + + value: float = Field(..., description="The measured numeric value.") + unit: str = Field( + ..., + description="UN/CEFACT Recommendation 20 unit-of-measure code (e.g. KGM for kilogram).", + ) + lower_tolerance: Annotated[ + float | None, + Field( + default=None, + alias="lowerTolerance", + description="Optional lower tolerance bound (same unit as ``value``).", + ), + ] + upper_tolerance: Annotated[ + float | None, + Field( + default=None, + alias="upperTolerance", + description="Optional upper tolerance bound (same unit as ``value``).", + ), + ] + + @model_validator(mode="after") + def _validate_tolerances(self) -> Measure: + if ( + self.lower_tolerance is not None + and self.upper_tolerance is not None + and self.lower_tolerance > self.upper_tolerance + ): + raise ValueError( + "Measure.lowerTolerance must be ≤ upperTolerance " + f"(got {self.lower_tolerance} > {self.upper_tolerance})", + ) + return self + + +class Image(UNTPBaseModel): + """Base64-encoded image with display metadata. + + v0.7.0 introduces this as a structured replacement for the inline base64 + string previously used at v0.6 ``Material.symbol``. Used at + ``Material.symbol`` and ``Product.productLabel``. + + Required: ``name``, ``imageData``, ``mediaType``. ``description`` is + optional (used for things like Battery Reg. label captions). + """ + + _jsonld_type: ClassVar[list[str]] = ["Image"] + + name: str = Field(..., description="Display name for the image (e.g. 'CE Marking').") + description: Annotated[ + str | None, + Field(default=None, description="Detailed description / supporting information."), + ] + image_data: Annotated[ + str, + Field( + ..., + alias="imageData", + description="Base64-encoded image bytes.", + ), + ] + media_type: Annotated[ + str, + Field( + ..., + alias="mediaType", + description="IANA media type (e.g. image/png).", + ), + ] + + +class Dimension(UNTPBaseModel): + """Physical dimensions and mass of a product. + + Each axis is optional because not every product has every dimension + (bulk materials may have weight + volume but no length / width / height). + """ + + _jsonld_type: ClassVar[list[str]] = ["Dimension"] + + weight: Measure | None = Field(default=None) + length: Measure | None = Field(default=None) + width: Measure | None = Field(default=None) + height: Measure | None = Field(default=None) + volume: Measure | None = Field(default=None) + + +class Characteristics(UNTPBaseModel): + """Industry/product-specific characteristics extension point. + + The schema declares this as ``additionalProperties: true`` with no + fixed shape — extensions plug their own fields in here. + """ + + _jsonld_type: ClassVar[list[str]] = ["Characteristics"] + + # Pydantic ``extra="allow"`` is inherited from UNTPBaseModel, which is + # how arbitrary industry-specific keys flow through. + def __getitem__(self, key: str) -> Any: # convenience for callers + return getattr(self, key) diff --git a/src/dppvalidator/models/v0_7/product.py b/src/dppvalidator/models/v0_7/product.py new file mode 100644 index 0000000..3404dca --- /dev/null +++ b/src/dppvalidator/models/v0_7/product.py @@ -0,0 +1,254 @@ +"""Product model for UNTP v0.7.0. + +In v0.7.0 the :class:`Product` *is* the credentialSubject of a +:class:`DigitalProductPassport` — there is no longer a ``ProductPassport`` +envelope. Conformity claims and material provenance live directly here: + +- ``performanceClaim`` (was 0.6 ``conformityClaim`` + 3 scorecards) +- ``materialProvenance`` (singular noun; was 0.6 ``materialsProvenance``) +- ``relatedParty`` (typed list of role/party pairs; was 0.6 ``producedByParty``) +- ``relatedDocument`` (was scattered across 0.6 ``furtherInformation``, + ``dueDiligenceDeclaration``, etc.) + +Cross-field invariants per the plan: + +- ``idGranularity`` enum drives whether ``itemNumber`` (item-level passport) + or ``batchNumber`` (batch-level passport) is required. Replaces the + v0.6.x ``ProductPassport.granularityLevel`` rule. +- The mass-fraction sum across ``materialProvenance`` should be ≤ 1.0. + Enforced as an error here because the array context lives on the + product. Strict equality (sum == 1.0) is too strict — partial + declarations are valid; only sums *over* 1.0 are physically impossible. +""" + +from __future__ import annotations + +from datetime import date +from enum import Enum +from typing import Annotated, ClassVar + +from pydantic import Field, model_validator + +from dppvalidator.models.base import UNTPBaseModel +from dppvalidator.models.v0_7.claims import Claim +from dppvalidator.models.v0_7.identifiers import ( + Country, + Facility, + IdentifierScheme, + PartyRole, +) +from dppvalidator.models.v0_7.materials import Material +from dppvalidator.models.v0_7.primitives import ( + Characteristics, + Classification, + Dimension, + FlexibleUri, + Image, + Link, +) + + +class IdGranularity(str, Enum): + """How specifically does the credential identify the product? + + Mirrors the v0.6.x ``GranularityLevel`` enum. The string values are + case-sensitive and must match the upstream schema's enum exactly. + """ + + ITEM = "item" + BATCH = "batch" + MODEL = "model" + + +class Package(UNTPBaseModel): + """Product packaging information. + + v0.7.0 introduces this as a structured field on ``Product.packaging``; + v0.6.x had no equivalent. + """ + + _jsonld_type: ClassVar[list[str]] = ["Package"] + + package_type: Annotated[ + Classification | None, + Field(default=None, alias="packageType"), + ] + weight: Annotated[ + Measure | None, + Field(default=None, description="Weight of the packaging (separate from product weight)."), + ] + + +class Product(UNTPBaseModel): + """The credential subject of a v0.7.0 DPP. + + All required fields per the upstream schema's ``Product`` $def: + ``id``, ``name``, ``idScheme``, ``idGranularity``, ``productCategory``, + ``producedAtFacility``, ``countryOfProduction``. Everything else is + optional but commonly populated. + """ + + _jsonld_type: ClassVar[list[str]] = ["Product"] + + id: FlexibleUri = Field( + ..., + description="Globally unique identifier (URI / DID).", + ) + name: str = Field(..., description="The product name as known to the market.") + description: Annotated[str | None, Field(default=None)] + + id_scheme: Annotated[ + IdentifierScheme, + Field( + ..., + alias="idScheme", + description="The identifier scheme used by ``id`` (e.g. GS1 GTIN).", + ), + ] + model_number: Annotated[str | None, Field(default=None, alias="modelNumber")] + batch_number: Annotated[str | None, Field(default=None, alias="batchNumber")] + item_number: Annotated[ + str | None, + Field( + default=None, + alias="itemNumber", + description="Serialised item number (was ``serialNumber`` in v0.6.x).", + ), + ] + id_granularity: Annotated[ + IdGranularity, + Field( + ..., + alias="idGranularity", + description="Whether the credential covers a single item, a batch, or a model class.", + ), + ] + + product_image: Annotated[Link | None, Field(default=None, alias="productImage")] + characteristics: Characteristics | None = Field(default=None) + product_category: Annotated[ + list[Classification], + Field( + ..., + alias="productCategory", + description=( + "Product classification codes (e.g. UN CPC). Now an array in v0.7.0; " + "scalar Classification in v0.6.x." + ), + min_length=1, + ), + ] + related_document: Annotated[ + list[Link], + Field( + default_factory=list, + alias="relatedDocument", + description=( + "Links to related documents (specifications, brochures, ...). Notably absorbs the " + "v0.6.x ``furtherInformation`` and ``dueDiligenceDeclaration`` fields." + ), + ), + ] + related_party: Annotated[ + list[PartyRole], + Field( + default_factory=list, + alias="relatedParty", + description=( + "Parties with a defined role on this product (manufacturer, recycler, …). " + "Replaces the v0.6.x scalar ``producedByParty: Party``." + ), + ), + ] + produced_at_facility: Annotated[ + Facility, + Field( + ..., + alias="producedAtFacility", + description="The facility where this product/batch was produced.", + ), + ] + production_date: Annotated[ + date | None, + Field(default=None, alias="productionDate"), + ] + expiry_date: Annotated[ + date | None, + Field(default=None, alias="expiryDate"), + ] + country_of_production: Annotated[ + Country, + Field( + ..., + alias="countryOfProduction", + description="ISO-3166 country of production (was a bare string in v0.6.x).", + ), + ] + dimensions: Dimension | None = Field(default=None) + material_provenance: Annotated[ + list[Material], + Field( + default_factory=list, + alias="materialProvenance", + description=( + "Material origin / mass fraction information. Singular noun in v0.7.0 " + "(was ``materialsProvenance`` in v0.6.x)." + ), + ), + ] + packaging: Package | None = Field(default=None) + product_label: Annotated[ + list[Image], + Field(default_factory=list, alias="productLabel"), + ] + performance_claim: Annotated[ + list[Claim], + Field( + default_factory=list, + alias="performanceClaim", + description=( + "Performance / conformity claims about this product. Replaces the v0.6.x " + "``conformityClaim`` array AND the three Emissions/Circularity/Traceability scorecards." + ), + ), + ] + + @model_validator(mode="after") + def _granularity_implies_serial_or_batch(self) -> Product: + # ``UNTPBaseModel`` ships ``use_enum_values=True``, so by the time + # this runs ``self.id_granularity`` is the string value, not the + # IdGranularity instance — compare on value (``==``) rather than + # identity (``is``). + if self.id_granularity == IdGranularity.ITEM.value and not self.item_number: + raise ValueError( + "Product.itemNumber is required when idGranularity == 'item'.", + ) + if self.id_granularity == IdGranularity.BATCH.value and not self.batch_number: + raise ValueError( + "Product.batchNumber is required when idGranularity == 'batch'.", + ) + return self + + @model_validator(mode="after") + def _mass_fractions_sum_within_unity(self) -> Product: + if not self.material_provenance: + return self + # Partial declarations (sum < 1.0) are explicitly allowed by UNTP — that + # nuance lives in the semantic-rule layer (Phase 3b emits a SEM-class + # warning). Sums *above* 1.0 are physically impossible regardless and + # therefore caught here. + total = sum(m.mass_fraction for m in self.material_provenance) + if total > 1.0001: # tiny epsilon for float arithmetic + raise ValueError( + f"Sum of materialProvenance.massFraction across {len(self.material_provenance)} " + f"material(s) is {total:.4f} > 1.0 — physically impossible.", + ) + return self + + +# Resolve the forward reference inside Package.weight. We can't import +# Measure at the top of the file because product.py is imported by other +# modules and the cycle would resolve in the wrong direction. +from dppvalidator.models.v0_7.primitives import Measure # noqa: E402 + +Package.model_rebuild() diff --git a/src/dppvalidator/plugins/registry.py b/src/dppvalidator/plugins/registry.py index f575663..00d1518 100644 --- a/src/dppvalidator/plugins/registry.py +++ b/src/dppvalidator/plugins/registry.py @@ -119,6 +119,7 @@ def run_all_validators( passport: DigitalProductPassport, *, strict: bool = False, + schema_version: str | None = None, ) -> list[ValidationError]: """Run all registered validator plugins. @@ -126,6 +127,13 @@ def run_all_validators( passport: Parsed passport to validate strict: If True, raise PluginError on plugin failures instead of returning a warning. Useful for CI/CD pipelines. + schema_version: Resolved UNTP DPP version of the payload. When + supplied, plugins that declare an ``applies_to_versions`` + tuple are skipped for non-matching versions (the engine's + per-version dispatch contract — same pattern the built-in + semantic-rule registry uses via ``ALL_RULES_BY_VERSION``). + Plugins without that attribute keep running for every + payload — back-compat for pre-Phase-6 plugins. Returns: List of validation errors from all plugins @@ -139,6 +147,16 @@ def run_all_validators( try: instance = validator() if isinstance(validator, type) else validator + # Per-version filter. ``applies_to_versions`` is the + # declarative version-pin contract documented in + # ``docs/guides/plugins.md``. We honour it on both + # the class and the instance so authors can set it + # either way. Plugins that don't declare it run for + # every version (back-compat). + applies = getattr(instance, "applies_to_versions", None) + if applies and schema_version and schema_version not in applies: + continue + if hasattr(instance, "check"): violations = instance.check(passport) rule_id = getattr(instance, "rule_id", f"PLG_{name.upper()}") diff --git a/src/dppvalidator/schemas/data/MANIFEST.json b/src/dppvalidator/schemas/data/MANIFEST.json new file mode 100644 index 0000000..2f7842d --- /dev/null +++ b/src/dppvalidator/schemas/data/MANIFEST.json @@ -0,0 +1,89 @@ +{ + "$schema": "https://artiso-ai.github.io/dppvalidator/schemas/manifest-v1.json", + "manifest_version": 1, + "description": "Provenance + integrity manifest for every artefact bundled into the dppvalidator wheel under src/dppvalidator/{schemas,vocabularies}/data/. Each artefact carries the upstream URL it was vendored from and a SHA-256 of the bundled bytes; tests/unit/test_manifest_integrity.py (added in Phase 5) re-computes the hashes on every CI run. See docs/plans/UNTP_0.7.0_MIGRATION.md for the migration context.", + "artefacts": [ + { + "version": "0.6.1", + "kind": "untp-dpp-schema", + "path": "src/dppvalidator/schemas/data/untp-dpp-schema-0.6.1.json", + "source_url": "https://test.uncefact.org/vocabulary/untp/dpp/untp-dpp-schema-0.6.1.json", + "sha256": "c0fdd7da5d23b6aec5d1d0ce198ca8d1cd67ca27609395a1b4961b3d1a8549a8", + "bytes": 49381, + "pulled_at": "2025-01-30", + "notes": "Pulled before this manifest existed; date inferred from git history (commit dd0c84..). Hash verified against the bytes on disk on 2026-05-07." + }, + { + "version": "0.6.1", + "kind": "untp-jsonld-context", + "path": "src/dppvalidator/vocabularies/data/untp-context-0.6.1.jsonld", + "source_url": "https://test.uncefact.org/vocabulary/untp/dpp/0.6.1/", + "sha256": "e956ab00591cebabeb6080eb02cd2f22adbda88c4d7861e8e7f03ea64284370f", + "bytes": 34347, + "pulled_at": "2026-05-07", + "notes": "Vendored in Phase 2 to remove an implicit network dependency. Server: AWS S3, Last-Modified: 2025-06-16T21:40:30Z." + }, + { + "version": "0.7.0", + "kind": "untp-dpp-schema", + "path": "src/dppvalidator/schemas/data/untp-dpp-schema-0.7.0.json", + "source_url": "https://opensource.unicc.org/un/unece/uncefact/spec-untp/-/raw/707cd5267deddede24bb74e453a758561972a109/artefacts/schema/v0.7.0/dpp/DigitalProductPassport.json", + "production_url": "https://untp.unece.org/artefacts/schema/v0.7.0/dpp/DigitalProductPassport.json", + "sha256": "42c51943ab23547d5287899fd12b214b19b006c28d105a70ff390f8551b12653", + "bytes": 50362, + "pulled_at": "2026-05-07", + "upstream_tag": "v0.7.0", + "upstream_commit": "707cd5267deddede24bb74e453a758561972a109", + "notes": "Self-contained: every $ref is internal (#/$defs/...). Product.json from the upstream split layout is intentionally NOT vendored — its $defs are already embedded inside this file's $defs. The bytes at ``production_url`` were re-fetched and verified bit-identical to ``source_url`` on 2026-05-08 (same SHA-256). Known upstream quirk: the embedded ``$defs.Characteristics`` carries an empty ``properties: {}`` and a description that was copy-pasted from ``$defs.Claim`` (\"A declaration of conformance with one or more criteria…\"). The standalone ``Product.json`` upstream file has the canonical, richer Characteristics definition with a ``@context`` field for JSON-LD vocabulary scoping. dppvalidator models Characteristics as ``extra=\"allow\"`` so behaviour is unaffected; this is documented for future readers who hit the discrepancy." + }, + { + "version": "0.7.0", + "kind": "untp-jsonld-context", + "path": "src/dppvalidator/vocabularies/data/untp-context-0.7.0.jsonld", + "source_url": "https://opensource.unicc.org/un/unece/uncefact/spec-untp/-/raw/707cd5267deddede24bb74e453a758561972a109/artefacts/contexts/v0.7.0/untp-context.jsonld", + "production_url": "https://vocabulary.uncefact.org/untp/0.7.0/context/", + "sha256": "fbd4824e30d3cfc5cba949e1efe19b4c9ebaee056abe7aaf1c6b139a7bf91b0c", + "bytes": 105396, + "pulled_at": "2026-05-07", + "upstream_tag": "v0.7.0", + "upstream_commit": "707cd5267deddede24bb74e453a758561972a109", + "notes": "Unified context covering DigitalProductPassport, DigitalConformityCredential, DigitalFacilityRecord, DigitalIdentityAnchor, DigitalTraceabilityEvent." + }, + { + "version": "0.7.0", + "kind": "untp-vocabulary-ontology", + "path": "src/dppvalidator/vocabularies/data/untp-ontology.jsonld", + "source_url": "https://opensource.unicc.org/un/unece/uncefact/spec-untp/-/raw/707cd5267deddede24bb74e453a758561972a109/artefacts/vocabularies/untp-core/untp-ontology.jsonld", + "sha256": "752060cc15c6c77bfcea8b170f173239a705e9da389314c1cb2dacc8a69d93bc", + "bytes": 147724, + "pulled_at": "2026-05-07", + "upstream_tag": "v0.7.0", + "upstream_commit": "707cd5267deddede24bb74e453a758561972a109", + "notes": "Core UNTP RDFS/OWL ontology: 74 classes, 2 properties. Used by SHACL-based semantic validation (Phase 3b)." + }, + { + "version": "0.7.0", + "kind": "untp-vocabulary-metrics", + "path": "src/dppvalidator/vocabularies/data/untp-metrics.jsonld", + "source_url": "https://opensource.unicc.org/un/unece/uncefact/spec-untp/-/raw/707cd5267deddede24bb74e453a758561972a109/artefacts/vocabularies/untp-metrics/untp-metrics.jsonld", + "sha256": "77900ce1138be124976d138750bea24bacb6c8ba327672fe8598b85db99a0a36", + "bytes": 53765, + "pulled_at": "2026-05-07", + "upstream_tag": "v0.7.0", + "upstream_commit": "707cd5267deddede24bb74e453a758561972a109", + "notes": "Controlled metric vocabulary referenced by Performance.metric in the DPP schema." + }, + { + "version": "0.7.0", + "kind": "untp-vocabulary-topics", + "path": "src/dppvalidator/vocabularies/data/untp-topics.jsonld", + "source_url": "https://opensource.unicc.org/un/unece/uncefact/spec-untp/-/raw/707cd5267deddede24bb74e453a758561972a109/artefacts/vocabularies/untp-topics/untp-topics.jsonld", + "sha256": "49affcb265bdf2a7a92d1b171c49a27543bfb4915bcbd11dd6e571252a57bb12", + "bytes": 61045, + "pulled_at": "2026-05-07", + "upstream_tag": "v0.7.0", + "upstream_commit": "707cd5267deddede24bb74e453a758561972a109", + "notes": "Conformity-topic taxonomy referenced by Claim.conformityTopic in the DPP schema." + } + ] +} diff --git a/src/dppvalidator/schemas/data/README.md b/src/dppvalidator/schemas/data/README.md index 06479ac..90a50e8 100644 --- a/src/dppvalidator/schemas/data/README.md +++ b/src/dppvalidator/schemas/data/README.md @@ -1,20 +1,56 @@ # UNTP DPP Schema Files -This directory contains bundled JSON Schema files for the UN Transparency Protocol -(UNTP) Digital Product Passport (DPP) specification. +This directory contains bundled JSON Schema files for the UN +Transparency Protocol (UNTP) Digital Product Passport (DPP) +specification. ## Source -Schemas are sourced from the official UN/CEFACT vocabulary repository: +Schemas are sourced from the official UN/CEFACT vocabulary +repositories: -- **URL**: +- **v0.6.x**: +- **v0.7.0**: - **Specification**: -## Included Schemas +## Included schemas -| File | Version | Source URL | -| ---------------------------- | ------- | ------------------------------------------------------------------------------------ | -| `untp-dpp-schema-0.6.1.json` | 0.6.1 | [Download](https://test.uncefact.org/vocabulary/untp/dpp/untp-dpp-schema-0.6.1.json) | + + +| File | Version | Bytes | SHA-256 (LF-normalised) | +| ---------------------------- | ------- | -----: | ------------------------------------------------------------------ | +| `untp-dpp-schema-0.6.1.json` | 0.6.1 | 49 381 | `c0fdd7da5d23b6aec5d1d0ce198ca8d1cd67ca27609395a1b4961b3d1a8549a8` | +| `untp-dpp-schema-0.7.0.json` | 0.7.0 | 50 362 | `42c51943ab23547d5287899fd12b214b19b006c28d105a70ff390f8551b12653` | + + + +The full provenance + integrity record (source URL, production +mirror URL, upstream commit, pull date, notes) for each file lives +in [`MANIFEST.json`](MANIFEST.json) and is enforced by +[`tests/unit/test_manifest_integrity.py`](../../../../tests/unit/test_manifest_integrity.py). + +`v0.6.0` is registered in +[`schemas/registry.py`](../registry.py) but its bytes are **not** +bundled — it shares the wire shape of `v0.6.1` and the engine +defaults v0.6.x callers to the bundled `v0.6.1` schema. + +## Manifest + +Every artefact under this directory and under +`src/dppvalidator/vocabularies/data/` is required to appear in +[`MANIFEST.json`](MANIFEST.json) with version, source URL, +production URL (when set), SHA-256, and pull date. CI enforces this +contract via the manifest-integrity test; adding a vendored file +without a manifest entry trips the drift catch. + +The "two URLs per artefact" pattern records: + +- **`source_url`** — the SHA-pinned upstream URL the bundled bytes + came from. Immutable; used for re-pulling and integrity diffs. +- **`production_url`** — the canonical production hosting (e.g. + `untp.unece.org` for v0.7.0). Human-friendly; used for + documentation links. Verified bit-identical to `source_url` at + vendor time. ## License @@ -24,12 +60,20 @@ for licensing details. ## Updates -To update schemas, use the `SchemaLoader.download_schema()` method or fetch -directly from the source URLs above. +For routine refreshes (new patch level on an existing version), +re-fetch from the production URL and verify the SHA-256 matches the +manifest pin. If the upstream bytes changed, update the manifest +hash in the same change. + +For a new minor/major UNTP version, the recipe lives in +[`.claude/skills/untp-migrate/SKILL.md`](../../../../.claude/skills/untp-migrate/SKILL.md) +(invocable as `/untp-bump ` in Claude Code). The minimum +touch list and full versioning rules are in +[`.claude/rules/untp-versioning.md`](../../../../.claude/rules/untp-versioning.md). ```python from dppvalidator.schemas import SchemaLoader loader = SchemaLoader() -loader.download_schema("0.6.1", output_dir="./schemas/data") +loader.download_schema("0.7.0", output_dir="./schemas/data") ``` diff --git a/src/dppvalidator/schemas/data/untp-dpp-schema-0.7.0.json b/src/dppvalidator/schemas/data/untp-dpp-schema-0.7.0.json new file mode 100644 index 0000000..dcdf7db --- /dev/null +++ b/src/dppvalidator/schemas/data/untp-dpp-schema-0.7.0.json @@ -0,0 +1,1455 @@ +{ + "$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://vocabulary.uncefact.org/untp/0.7.0/context/" + ], + "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://vocabulary.uncefact.org/untp/0.7.0/context/", + "type": "string" + } + ], + "default": [ + "https://www.w3.org/ns/credentials/v2", + "https://vocabulary.uncefact.org/untp/0.7.0/context/" + ], + "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 this verifiable credential." + }, + "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": { + "example": "2024-03-15T12:00:00Z", + "type": "string", + "format": "date-time", + "description": "The date and time from which the credential is valid." + }, + "validUntil": { + "example": "2034-03-15T12:00:00Z", + "type": "string", + "format": "date-time", + "description": "The expiry date (if applicable) of this verifiable credential." + }, + "name": { + "example": "Some name", + "type": "string", + "description": "Name of this verifiable credential instance (eg the title of a digital product passport, facility record, lifecycle event, or conformity credential)" + }, + "credentialStatus": { + "$ref": "#/$defs/BitstringStatusListEntry", + "description": "A W3C VCDM2.0 compliant object containing credential status information." + }, + "renderMethod": { + "type": "array", + "items": { + "$ref": "#/$defs/RenderTemplate2024" + }, + "description": "Human rendering information for this credential. An array of render methods (eg RenderTemplate2024) that may be used to display the credential." + }, + "credentialSubject": { + "$ref": "#/$defs/Product", + "description": "The product that is the subject of this digital product passport." + }, + "issuingSoftware": { + "$ref": "#/$defs/IssuingSoftware", + "description": "Optional metadata identifying the software product (and its vendor) that issued this credential. Useful for vendor traceability and conformity testing. Issuers MAY omit this property." + } + }, + "description": "A digital Product Passport (DPP) credential.", + "required": [ + "@context", + "id", + "issuer", + "validFrom", + "name", + "credentialSubject" + ], + "$defs": { + "CredentialIssuer": { + "type": "object", + "additionalProperties": true, + "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": { + "type": { + "type": "array", + "readOnly": true, + "default": [ + "Party" + ], + "items": { + "type": "string" + }, + "allOf": [ + { + "contains": { + "const": "Party", + "minContains": 1 + } + } + ] + }, + "id": { + "example": "https://sample-business-register.gov/123456789", + "type": "string", + "format": "uri", + "description": "Globally unique identifier of this party. Typically represented as a URI identifierScheme/Identifier URI" + }, + "name": { + "example": "Sample Company Ltd", + "type": "string", + "description": "Legal registered name of this party." + }, + "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" + ] + }, + "BitstringStatusListEntry": { + "type": "object", + "additionalProperties": false, + "properties": { + "type": { + "type": "string", + "enum": [ + "BitstringStatusListEntry" + ], + "example": "BitstringStatusListEntry", + "description": "The type of status list - must be set to \"The type property MUST be BitstringStatusListEntry.\"" + }, + "id": { + "example": "https://example-cab.com/credentials/status/3#94567\"", + "type": "string", + "format": "uri", + "description": "optional identifier of this status list entry." + }, + "statusPurpose": { + "type": "string", + "enum": [ + "refresh", + "revocation", + "suspension", + "message" + ], + "example": "refresh", + "description": "Status purpose drawn from a standard list but extensible as per w3c bitstring status list specification." + }, + "statusListIndex": { + "minimum": 0, + "example": 94567, + "type": "integer", + "description": "\tThe statusListIndex property MUST be an arbitrary size integer greater than or equal to 0, expressed as a string in base 10. The value identifies the position of the status of the verifiable credential." + }, + "statusListCredential": { + "example": "https://example-cab.com/credentials/status/4", + "type": "string", + "format": "uri", + "description": "The statusListCredential property MUST be a URL to a verifiable credential. When the URL is dereferenced, the resulting verifiable credential MUST have type property that includes the BitstringStatusListCredential value." + } + }, + "description": "A privacy-preserving, space-efficient, and high-performance mechanism for publishing status information such as suspension or revocation of Verifiable Credentials through use of bitstrings. See https://www.w3.org/TR/vc-bitstring-status-list/ for full details.", + "required": [ + "type", + "statusPurpose", + "statusListIndex", + "statusListCredential" + ] + }, + "RenderTemplate2024": { + "type": "object", + "additionalProperties": false, + "properties": { + "type": { + "type": "array", + "readOnly": true, + "default": [ + "RenderTemplate2024" + ], + "items": { + "type": "string" + }, + "allOf": [ + { + "contains": { + "const": "RenderTemplate2024", + "minContains": 1 + } + } + ] + }, + "name": { + "type": "string", + "description": "Human facing display name for selection" + }, + "mediaQuery": { + "type": "string", + "description": "Media query as defined in https://www.w3.org/TR/mediaqueries-4/" + }, + "template": { + "type": "string", + "description": "An inline template field for use cases where remote retrieval of a render method is suboptimal" + }, + "url": { + "type": "string", + "format": "uri", + "description": "URL for remotely hosted template" + }, + "mediaType": { + "type": "string", + "description": "media type of the rendered output (eg text/html)" + }, + "digestMultibase": { + "type": "string", + "description": "Used for resource integrity and/or validation of the inline `template`" + } + }, + "description": "A single template format focused render method where the content/media type decision becomes secondary (and is expressed separately).See https://github.com/w3c-ccg/vc-render-method/issues/9" + }, + "IssuingSoftware": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "example": "https://yourdomain.com/.well-known/untp/software/yourproduct/2026.04.1", + "type": "string", + "format": "uri", + "description": "A resolvable identifier for the specific version of the software product that issued this credential." + }, + "name": { + "example": "Your Product Name", + "type": "string", + "description": "The name of the software product that issued this credential." + }, + "version": { + "example": "2026.04.1", + "type": "string", + "description": "The version of the software product that issued this credential." + }, + "vendor": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "example": "did:web:yourdomain.com", + "type": "string", + "format": "uri", + "description": "The decentralised identifier (DID) or other resolvable identifier of the software vendor." + }, + "name": { + "example": "Your Vendor Name", + "type": "string", + "description": "The name of the software vendor." + } + }, + "required": [ + "id", + "name" + ], + "description": "The vendor of the software product that issued this credential." + } + }, + "required": [ + "id", + "name", + "version", + "vendor" + ], + "description": "Optional metadata identifying the software product (and its vendor) that issued the parent credential. When present, all listed sub-properties MUST be populated; when absent, the credential is still valid (verifiers MUST treat the property as optional)." + }, + "Product": { + "type": "object", + "additionalProperties": true, + "properties": { + "type": { + "type": "array", + "readOnly": true, + "default": [ + "Product" + ], + "items": { + "type": "string" + }, + "allOf": [ + { + "contains": { + "const": "Product", + "minContains": 1 + } + } + ] + }, + "id": { + "example": "did:web:manufacturer.com:product:123456789", + "type": "string", + "format": "uri", + "description": "Globally unique identifier of this product. Typically represented as a URI identifierScheme/Identifier URI or, if self-issued, as a did." + }, + "name": { + "example": "600 Ah Lithium Battery", + "type": "string", + "description": "The product name as known to the market." + }, + "description": { + "type": "string", + "description": "Description of the product." + }, + "idScheme": { + "type": "object", + "properties": { + "type": { + "type": "array", + "readOnly": true, + "default": [ + "IdentifierScheme" + ], + "items": { + "type": "string" + }, + "allOf": [ + { + "contains": { + "const": "IdentifierScheme", + "minContains": 1 + } + } + ] + }, + "id": { + "type": "string", + "format": "uri", + "description": "The URI of this identifier scheme" + }, + "name": { + "example": "Global Identifier Scheme Name", + "type": "string", + "description": "The name of the identifier scheme. " + } + }, + "required": [ + "id", + "name" + ], + "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. " + }, + "modelNumber": { + "type": "string", + "description": "Where available, the model number (for manufactured products) or material identification (for bulk materials)" + }, + "batchNumber": { + "example": 6789, + "type": "string", + "description": "Identifier of the specific production batch of the product. Unique within the product class." + }, + "itemNumber": { + "example": 12345678, + "type": "string", + "description": "A number or code representing a specific serialised item of the product. Unique within product class." + }, + "idGranularity": { + "type": "string", + "enum": [ + "model", + "batch", + "item" + ], + "example": "model", + "description": "The identification granularity for this product (item, batch, model)" + }, + "productImage": { + "$ref": "#/$defs/Link", + "description": "Reference information (location, type, name) of an image of the product." + }, + "characteristics": { + "$ref": "#/$defs/Characteristics", + "description": "A set of industry specific product 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" + }, + "relatedDocument": { + "type": "array", + "items": { + "$ref": "#/$defs/Link" + }, + "description": "A list of links to documents providing additional product information. Documents that support a conformity claim (e.g. permits or certificates) SHOULD be referenced as claim evidence rather than here." + }, + "relatedParty": { + "type": "array", + "items": { + "$ref": "#/$defs/PartyRole" + }, + "description": "A list of parties with a defined relationship to this product" + }, + "producedAtFacility": { + "type": "object", + "properties": { + "type": { + "type": "array", + "readOnly": true, + "default": [ + "Facility" + ], + "items": { + "type": "string" + }, + "allOf": [ + { + "contains": { + "const": "Facility", + "minContains": 1 + } + } + ] + }, + "id": { + "example": "https://sample-location-register.com/987654321", + "type": "string", + "format": "uri", + "description": "Globally unique identifier of this facility. Typically represented as a URI identifierScheme/Identifier URI" + }, + "name": { + "example": "Sample Factory A", + "type": "string", + "description": "Name of this facility as defined the location register." + }, + "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." + }, + "expiryDate": { + "example": "2027-04-25", + "type": "string", + "format": "date", + "description": "The date at which this product is no longer fit for use. Typically used for a food product use-by date but may also represent the usable life of any product." + }, + "countryOfProduction": { + "$ref": "#/$defs/Country", + "description": "The country in which this item was produced / manufactured.using ISO-3166 code and name." + }, + "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, width, or height.\"weight\":{\"value\":10, \"unit\":\"KGM\"}" + }, + "materialProvenance": { + "type": "array", + "items": { + "$ref": "#/$defs/Material" + }, + "description": "A list of materials provenance objects providing details on the origin and mass fraction of materials of the product or batch." + }, + "packaging": { + "$ref": "#/$defs/Package", + "description": "The packaging for this product." + }, + "productLabel": { + "type": "array", + "items": { + "$ref": "#/$defs/Image" + }, + "description": "An array of labels that may appear on the product such as certification marks or regulatory labels." + }, + "performanceClaim": { + "type": "array", + "items": { + "$ref": "#/$defs/Claim" + }, + "description": "A list of performance claims (eg emissions intensity) for this product." + } + }, + "description": "The ProductInformation class encapsulates detailed information regarding a specific product, including its identification details, manufacturer, and other pertinent details.", + "required": [ + "id", + "name", + "idScheme", + "idGranularity", + "productCategory", + "producedAtFacility", + "countryOfProduction" + ] + }, + "Link": { + "type": "object", + "additionalProperties": false, + "properties": { + "linkURL": { + "example": "https://files.example-certifier.com/1234567.json", + "type": "string", + "format": "uri", + "description": "The URL of the target resource. " + }, + "linkName": { + "type": "string", + "description": "Display name for this link." + }, + "digestMultibase": { + "example": "abc123-example-digest-invalid", + "type": "string", + "description": "An optional multi-base encoded digest to ensure the content of the link has not changed. See https://www.w3.org/TR/vc-data-integrity/#resource-integrity for more information." + }, + "mediaType": { + "example": "application/ld+json", + "type": "string", + "description": "The media type of 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.", + "required": [ + "linkURL", + "linkName" + ] + }, + "Characteristics": { + "type": "object", + "additionalProperties": true, + "properties": {}, + "description": "A declaration of conformance with one or more criteria from a specific standard or regulation. " + }, + "Classification": { + "type": "object", + "additionalProperties": false, + "properties": { + "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" + }, + "definition": { + "type": "string", + "description": "A rich definition of this classification 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": [ + "code", + "name", + "schemeId", + "schemeName" + ] + }, + "PartyRole": { + "type": "object", + "additionalProperties": false, + "properties": { + "role": { + "type": "string", + "enum": [ + "owner", + "producer", + "manufacturer", + "processor", + "remanufacturer", + "recycler", + "operator", + "serviceProvider", + "inspector", + "certifier", + "logisticsProvider", + "carrier", + "consignor", + "consignee", + "importer", + "exporter", + "distributor", + "retailer", + "brandOwner", + "regulator" + ], + "example": "owner", + "description": "The role played by the party in this relationship" + }, + "party": { + "$ref": "#/$defs/Party", + "description": "The party that has the specified role." + } + }, + "description": "A party with a defined relationship to the referencing entity", + "required": [ + "role", + "party" + ] + }, + "Party": { + "type": "object", + "additionalProperties": true, + "properties": { + "type": { + "type": "array", + "readOnly": true, + "default": [ + "Party" + ], + "items": { + "type": "string" + }, + "allOf": [ + { + "contains": { + "const": "Party", + "minContains": 1 + } + } + ] + }, + "id": { + "example": "https://sample-business-register.gov/123456789", + "type": "string", + "format": "uri", + "description": "Globally unique identifier of this party. Typically represented as a URI identifierScheme/Identifier URI" + }, + "name": { + "example": "Sample Company Ltd", + "type": "string", + "description": "Legal registered name of this party." + }, + "description": { + "type": "string", + "description": "Description of the party including function and other names." + }, + "registeredId": { + "example": 90664869327, + "type": "string", + "description": "The registration number (alphanumeric) of the Party within the register. Unique within the register." + }, + "idScheme": { + "type": "object", + "properties": { + "type": { + "type": "array", + "readOnly": true, + "default": [ + "IdentifierScheme" + ], + "items": { + "type": "string" + }, + "allOf": [ + { + "contains": { + "const": "IdentifierScheme", + "minContains": 1 + } + } + ] + }, + "id": { + "type": "string", + "format": "uri", + "description": "The URI of this identifier scheme" + }, + "name": { + "example": "Global Identifier Scheme Name", + "type": "string", + "description": "The name of the identifier scheme. " + } + }, + "required": [ + "id", + "name" + ], + "description": "The identifier scheme of the party. Typically a national business register or a global scheme such as GLEIF. " + }, + "registrationCountry": { + "$ref": "#/$defs/Country", + "description": "the country in which this organisation is registered - using ISO-3166 code and name." + }, + "partyAddress": { + "$ref": "#/$defs/Address", + "description": "The address of the party" + }, + "organisationWebsite": { + "example": "https://example-company.com", + "type": "string", + "format": "uri", + "description": "Website for this organisation" + }, + "industryCategory": { + "type": "array", + "items": { + "$ref": "#/$defs/Classification" + }, + "description": "The industry categories for this organisation. Recommend use of UNCPC as the category scheme. for example - unstats.un.org/isic/1030" + }, + "partyAlsoKnownAs": { + "type": "array", + "items": { + "type": "object", + "properties": { + "type": { + "type": "array", + "readOnly": true, + "default": [ + "Party" + ], + "items": { + "type": "string" + }, + "allOf": [ + { + "contains": { + "const": "Party", + "minContains": 1 + } + } + ] + }, + "id": { + "example": "https://sample-business-register.gov/123456789", + "type": "string", + "format": "uri", + "description": "Globally unique identifier of this party. Typically represented as a URI identifierScheme/Identifier URI" + }, + "name": { + "example": "Sample Company Ltd", + "type": "string", + "description": "Legal registered name of this party." + }, + "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 organisation. For example DUNS, GLN, LEI, etc" + } + }, + "description": "An organisation. May be a supply chain actor, a certifier, a government agency.", + "required": [ + "id", + "name" + ] + }, + "Country": { + "type": "object", + "additionalProperties": false, + "properties": { + "countryCode": { + "type": "string", + "x-external-enumeration": "https://vocabulary.uncefact.org/CountryId#", + "description": "ISO 3166 country code\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 " + }, + "countryName": { + "type": "string", + "description": "Country Name as defined in ISO 3166" + } + }, + "description": "Country Code and Name from ISO 3166", + "required": [ + "countryCode" + ] + }, + "Address": { + "type": "object", + "additionalProperties": false, + "properties": { + "streetAddress": { + "example": "level 11, 15 London Circuit", + "type": "string", + "description": "the street address as an unstructured string." + }, + "postalCode": { + "example": 2601, + "type": "string", + "description": "The postal code or zip code for this address." + }, + "addressLocality": { + "example": "Acton", + "type": "string", + "description": "The city, suburb or township name." + }, + "addressRegion": { + "example": "ACT", + "type": "string", + "description": "The state or territory or province" + }, + "addressCountry": { + "$ref": "#/$defs/Country", + "description": "The address country as an ISO-3166 two letter country code and name." + } + }, + "description": "A postal address.", + "required": [ + "streetAddress", + "postalCode", + "addressLocality", + "addressRegion", + "addressCountry" + ] + }, + "Dimension": { + "type": "object", + "additionalProperties": true, + "properties": { + "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": { + "value": { + "example": 10, + "type": "number", + "description": "The numeric value of the measure" + }, + "upperTolerance": { + "type": "number", + "description": "The upper tolerance associated with this measure expressed in the same units as the measure. For example value=10, upperTolerance=0.1, unit=KGM would mean that this measure is 10kg + 0.1kg" + }, + "lowerTolerance": { + "type": "number", + "description": "The lower tolerance associated with this measure expressed in the same units as the measure. For example value=10, lowerTolerance=0.1, unit=KGM would mean that this measure is 10kg - 0.1kg" + }, + "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). There is an optional upper and lower tolerance which can be used to specify uncertainty in the measure. ", + "required": [ + "value", + "unit" + ] + }, + "Material": { + "type": "object", + "additionalProperties": true, + "properties": { + "name": { + "example": "Lithium Spodumene", + "type": "string", + "description": "Name of this material (eg \"Egyptian Cotton\")" + }, + "originCountry": { + "$ref": "#/$defs/Country", + "description": "A ISO 3166-1 code representing the country of origin of the component or ingredient." + }, + "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": { + "maximum": 1, + "minimum": 0, + "example": 0.2, + "type": "number", + "description": "The mass fraction as a decimal of the product (or facility reporting period) represented by this material. " + }, + "mass": { + "$ref": "#/$defs/Measure", + "description": "The mass of the material component." + }, + "recycledMassFraction": { + "maximum": 1, + "minimum": 0, + "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": { + "$ref": "#/$defs/Image", + "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", + "originCountry", + "materialType", + "massFraction" + ] + }, + "Image": { + "type": "object", + "additionalProperties": false, + "properties": { + "name": { + "example": "certification trust mark", + "type": "string", + "description": "the display name for this image" + }, + "description": { + "type": "string", + "description": "The detailed description / supporting information for this image." + }, + "imageData": { + "type": "string", + "format": "byte", + "description": "The image data encoded as a base64 string." + }, + "mediaType": { + "type": "string", + "x-external-enumeration": "https://mimetype.io/", + "description": "The media type of this image (eg image/png)\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://mimetype.io/\n " + } + }, + "description": "A binary image encoded as base64 text and embedded into the data. Use this for small images like certification trust marks or regulated labels. Large images should be external links.", + "required": [ + "name", + "imageData", + "mediaType" + ] + }, + "Package": { + "type": "object", + "additionalProperties": true, + "properties": { + "description": { + "type": "string", + "description": "Description of the packaging." + }, + "dimensions": { + "$ref": "#/$defs/Dimension", + "description": "dimensions of the packaging" + }, + "materialUsed": { + "type": "array", + "items": { + "$ref": "#/$defs/Material" + }, + "description": "materials used for the packaging." + }, + "packageLabel": { + "type": "array", + "items": { + "$ref": "#/$defs/Image" + }, + "description": "An array of package labels that may appear on the packaging together with their meaning. Use for small images that represent certification marks or regulatory requirements. Large images should be linked as evidence to claims." + }, + "performanceClaim": { + "type": "array", + "items": { + "$ref": "#/$defs/Claim" + }, + "description": "conformity claims made about the packaging." + } + }, + "description": "Details of product packaging", + "required": [ + "description", + "dimensions", + "materialUsed" + ] + }, + "Claim": { + "type": "object", + "additionalProperties": true, + "properties": { + "type": { + "type": "array", + "readOnly": true, + "default": [ + "Claim" + ], + "items": { + "type": "string" + }, + "allOf": [ + { + "contains": { + "const": "Claim", + "minContains": 1 + } + } + ] + }, + "id": { + "example": "https://sample-company.com/claim/e78dab5d-b6f6-4bc4-a458-7feb039f6cb3", + "type": "string", + "format": "uri", + "description": "Globally unique identifier of this claim. Typically represented as a URI companyURL/claimID URI or a UUID" + }, + "name": { + "example": "Sample company Forced Labour claim", + "type": "string", + "description": "Name of this claim - typically similar or the same as the referenced criterion name." + }, + "description": { + "type": "string", + "description": "Description of this conformity claim" + }, + "referenceCriteria": { + "type": "array", + "items": { + "type": "object", + "properties": { + "type": { + "type": "array", + "readOnly": true, + "default": [ + "Criterion" + ], + "items": { + "type": "string" + }, + "allOf": [ + { + "contains": { + "const": "Criterion", + "minContains": 1 + } + } + ] + }, + "id": { + "example": "https://vocabulary.sample-scheme.org/criterion/lb/v1.0", + "type": "string", + "format": "uri", + "description": "Globally unique identifier of this conformity criterion. Typically represented as a URI SchemeOwner/CriterionID URI" + }, + "name": { + "example": "Forced labour assessment criterion", + "type": "string", + "description": "Name of this criterion as defined by the scheme owner." + } + }, + "required": [ + "id", + "name" + ] + }, + "description": "The criterion against which the claim is made." + }, + "referenceRegulation": { + "type": "array", + "items": { + "type": "object", + "properties": { + "type": { + "type": "array", + "readOnly": true, + "default": [ + "Regulation" + ], + "items": { + "type": "string" + }, + "allOf": [ + { + "contains": { + "const": "Regulation", + "minContains": 1 + } + } + ] + }, + "id": { + "example": "https://regulations.country.gov/ABC-12345", + "type": "string", + "format": "uri", + "description": "Globally unique identifier of this standard. Typically represented as a URI government/regulation URI" + }, + "name": { + "example": "Due Diligence Directove", + "type": "string", + "description": "Name of this regulation as defined by the regulator." + } + }, + "required": [ + "id", + "name" + ] + }, + "description": "List of references to regulation to which conformity is claimed claimed for this product" + }, + "referenceStandard": { + "type": "array", + "items": { + "type": "object", + "properties": { + "type": { + "type": "array", + "readOnly": true, + "default": [ + "Standard" + ], + "items": { + "type": "string" + }, + "allOf": [ + { + "contains": { + "const": "Standard", + "minContains": 1 + } + } + ] + }, + "id": { + "example": "https://sample-standards.org/A1234", + "type": "string", + "format": "uri", + "description": "Globally unique identifier of this standard. Typically represented as a URI issuer/standard URI" + }, + "name": { + "example": "Labour rights standard", + "type": "string", + "description": "Name for this standard" + } + }, + "required": [ + "id", + "name" + ] + }, + "description": "List of references to standards to which conformity is claimed claimed for this product" + }, + "claimDate": { + "type": "string", + "format": "date", + "description": "That date on which the claimed performance is applicable." + }, + "applicablePeriod": { + "$ref": "#/$defs/Period", + "description": "The applicable reporting period for this facility record." + }, + "claimedPerformance": { + "type": "array", + "items": { + "$ref": "#/$defs/Performance" + }, + "description": "The claimed performance level " + }, + "evidence": { + "type": "array", + "items": { + "$ref": "#/$defs/Link" + }, + "description": "A URI pointing to the evidence supporting the claim. SHOULD be a URL to a UNTP Digital Conformity Credential (DCC)" + }, + "conformityTopic": { + "type": "array", + "items": { + "$ref": "#/$defs/ConformityTopic" + }, + "description": "The conformity topic category for this assessment" + } + }, + "description": "A performance claim about a product, facility, or organisation that is made against a well defined criterion.", + "required": [ + "id", + "name", + "referenceCriteria", + "claimDate", + "claimedPerformance", + "conformityTopic" + ] + }, + "Period": { + "type": "object", + "additionalProperties": false, + "properties": { + "startDate": { + "type": "string", + "format": "date", + "description": "The period start date" + }, + "endDate": { + "type": "string", + "format": "date", + "description": "The period end date" + }, + "periodInformation": { + "type": "string", + "description": "Additional information relevant to this reporting period" + } + }, + "description": "A period of time, typically a month, quarter or a year, which defines the context boundary for reported facts.", + "required": [ + "startDate", + "endDate" + ] + }, + "Performance": { + "type": "object", + "additionalProperties": false, + "properties": { + "metric": { + "type": "object", + "properties": { + "type": { + "type": "array", + "readOnly": true, + "default": [ + "PerformanceMetric" + ], + "items": { + "type": "string" + }, + "allOf": [ + { + "contains": { + "const": "PerformanceMetric", + "minContains": 1 + } + } + ] + }, + "id": { + "example": "https://authority.gov/schemeABC/123456789", + "type": "string", + "format": "uri", + "description": "Globally unique identifier of this reporting metric. " + }, + "name": { + "example": "emissions intensity", + "type": "string", + "description": "A human readable name for this metric (for example \"water usage per Kg of material\")" + } + }, + "required": [ + "id", + "name" + ], + "description": "The metric (eg material emissions intensity CO2e/Kg or percentage of young workers) that is measured." + }, + "measure": { + "$ref": "#/$defs/Measure", + "description": "The measured performance value" + }, + "score": { + "$ref": "#/$defs/Score", + "description": "A performance score (eg \"AA\") drawn from a scoring framework defined by the scheme or criterion." + } + }, + "description": "A claimed, assessed, or required performance level defined either by a scoring system or a numeric measure. When a numeric measure is provided, the metric classifying the measurement is required. When only a score is provided, the scoring framework is discoverable via the conformity scheme or criterion.", + "dependentRequired": { + "measure": [ + "metric" + ] + } + }, + "Score": { + "type": "object", + "additionalProperties": false, + "properties": { + "code": { + "type": "string", + "description": "The coded value for this score (eg \"AAA\")" + }, + "rank": { + "type": "integer", + "description": "The ranking of this score within the scoring framework - using an integer where \"1\" is the highest rank." + }, + "definition": { + "type": "string", + "description": "A description of the meaning of this score." + } + }, + "description": "A single score within a scoring framework. ", + "required": [ + "code" + ] + }, + "ConformityTopic": { + "type": "object", + "additionalProperties": false, + "properties": { + "type": { + "type": "array", + "readOnly": true, + "default": [ + "ConformityTopic" + ], + "items": { + "type": "string" + }, + "allOf": [ + { + "contains": { + "const": "ConformityTopic", + "minContains": 1 + } + } + ] + }, + "id": { + "type": "string", + "format": "uri", + "description": "The unique identifier for this conformity topic" + }, + "name": { + "example": "forced-labour", + "type": "string", + "description": "The human readable name for this conformity topic." + }, + "definition": { + "type": "string", + "description": "The rich definition of this conformity topic." + } + }, + "description": "The UNTP standard classification scheme for conformity topic. see http://vocabulary.uncefact.org/ConformityTopic", + "required": [ + "id", + "name" + ] + } + } +} diff --git a/src/dppvalidator/schemas/registry.py b/src/dppvalidator/schemas/registry.py index f845fd4..ed1272f 100644 --- a/src/dppvalidator/schemas/registry.py +++ b/src/dppvalidator/schemas/registry.py @@ -9,12 +9,31 @@ @dataclass(frozen=True, slots=True) class SchemaVersion: - """Schema version definition with integrity metadata.""" + """Schema version definition with integrity metadata. + + Attributes: + version: SemVer version string (e.g. ``0.6.1``, ``0.7.0``). + url: SHA-pinned upstream URL the bundled bytes were vendored from. + Used for re-pulling and integrity diffs; not for runtime fetch. + sha256: SHA-256 of the LF-normalised bundled bytes; ``None`` when + the schema isn't bundled (legacy 0.6.0 entry). + context_urls: JSON-LD context URIs that pair with this schema + version (W3C VC + UNTP DPP context per version). + production_url: Optional canonical production URL — the + human-friendly "how-the-spec-publishes-it" URL (e.g. + ``https://untp.unece.org/...``). When set, the bytes at + this URL are byte-for-byte identical to those at :attr:`url` + (verified at vendor time); the SHA pin is enforced against + the bundled copy regardless. The two-URL split lets the + registry record provenance ("where does this schema *live*?") + separately from integrity ("what bytes did we ship?"). + """ version: str url: str sha256: str | None context_urls: tuple[str, ...] + production_url: str | None = None def verify_integrity(self, content: bytes) -> bool: """Verify content matches expected SHA-256 hash. @@ -52,9 +71,34 @@ def verify_integrity(self, content: bytes) -> bool: "https://test.uncefact.org/vocabulary/untp/dpp/0.6.1/", ), ), + # UNTP 0.7.0. Two URLs are tracked: ``url`` is the SHA-pinned upstream + # raw URL we vendored from; ``production_url`` is the canonical + # production hosting at ``untp.unece.org`` (verified bit-identical to + # the SHA-pinned source on 2026-05-08 — same SHA-256). The + # production CloudFront mirror for the JSON-LD context is captured + # under ``context_urls`` below. The ``sha256`` pins the bundled file + # at src/dppvalidator/schemas/data/untp-dpp-schema-0.7.0.json + # (vendored in Phase 2, see docs/plans/UNTP_0.7.0_MIGRATION.md). The + # hash is cross-verified by tests/unit/test_manifest_integrity.py. + "0.7.0": SchemaVersion( + version="0.7.0", + url=( + "https://opensource.unicc.org/un/unece/uncefact/spec-untp/-/raw/" + "707cd5267deddede24bb74e453a758561972a109/artefacts/schema/v0.7.0/dpp/" + "DigitalProductPassport.json" + ), + sha256="42c51943ab23547d5287899fd12b214b19b006c28d105a70ff390f8551b12653", + context_urls=( + "https://www.w3.org/ns/credentials/v2", + "https://vocabulary.uncefact.org/untp/0.7.0/context/", + ), + production_url=( + "https://untp.unece.org/artefacts/schema/v0.7.0/dpp/DigitalProductPassport.json" + ), + ), } -DEFAULT_SCHEMA_VERSION = "0.6.1" +DEFAULT_SCHEMA_VERSION = "0.6.1" # Phase 9 will flip this to "0.7.0" in dppvalidator 0.5.0. class SchemaRegistry: @@ -103,6 +147,20 @@ def get_context_urls(self, version: str | None = None) -> tuple[str, ...]: """ return self.get_schema(version).context_urls + def get_production_url(self, version: str | None = None) -> str | None: + """Return the canonical production URL for the schema, if known. + + The production URL is the human-friendly hosting (e.g. + ``https://untp.unece.org/...``) — distinct from the SHA-pinned + :meth:`get_schema_url`, which points at the immutable source the + bytes were vendored from. Both URLs serve the same bytes; the + split lets callers reach for whichever URL is appropriate + (documentation links → production_url; integrity diff → + ``url``). Returns ``None`` for versions that have no published + production URL recorded. + """ + return self.get_schema(version).production_url + @property def available_versions(self) -> list[str]: """List of available schema versions.""" diff --git a/src/dppvalidator/validators/deep.py b/src/dppvalidator/validators/deep.py index 47fc917..1b5860d 100644 --- a/src/dppvalidator/validators/deep.py +++ b/src/dppvalidator/validators/deep.py @@ -16,18 +16,52 @@ import httpx from dppvalidator.logging import get_logger +from dppvalidator.schemas.registry import DEFAULT_SCHEMA_VERSION from dppvalidator.validators.results import ValidationError, ValidationResult logger = get_logger(__name__) -# Default link paths to follow in DPP documents -DEFAULT_LINK_PATHS = [ - "credentialSubject.traceabilityEvents", - "credentialSubject.conformityClaim", - "credentialSubject.product.traceabilityInfo", - "credentialSubject.materialsProvenance", -] +# Per-version JSON paths the deep crawler follows when looking for linked +# documents. Each entry is a ``credentialSubject``-rooted dotted path; the +# crawler walks the parsed payload and dereferences any URL it finds at +# those paths. Lists in the path use the ``[*]`` syntax to mean "every +# entry" (handled in :class:`DeepValidator._extract_links`). +# +# Adding a new UNTP version means adding one entry here — see Phase 3b of +# docs/plans/UNTP_0.7.0_MIGRATION.md and the cardinal rule in +# .claude/rules/untp-versioning.md (rule 2: "one source of truth per +# surface"). +LINK_PATHS_BY_VERSION: dict[str, list[str]] = { + "0.6.0": [ + "credentialSubject.traceabilityEvents", + "credentialSubject.conformityClaim", + "credentialSubject.product.traceabilityInfo", + "credentialSubject.materialsProvenance", + ], + "0.6.1": [ + "credentialSubject.traceabilityEvents", + "credentialSubject.conformityClaim", + "credentialSubject.product.traceabilityInfo", + "credentialSubject.materialsProvenance", + ], + "0.7.0": [ + # v0.7.0 envelope: credentialSubject IS the Product, conformity + # claims live on it directly, and material provenance is the + # singular noun. ``[*]`` means "iterate every list element". + "credentialSubject.evidence", + "credentialSubject.relatedDocument", + "credentialSubject.performanceClaim[*].evidence", + "credentialSubject.materialProvenance[*].materialSafetyInformation", + "credentialSubject.relatedParty[*].party.id", + ], +} + +# Backward-compat alias. Pre-Phase-3b callers that imported +# ``DEFAULT_LINK_PATHS`` see the v0.6.x list — same value the constant +# carried before the version-keyed dispatch landed. New code should use +# :data:`LINK_PATHS_BY_VERSION` keyed on the active schema version. +DEFAULT_LINK_PATHS = LINK_PATHS_BY_VERSION["0.6.1"] @dataclass @@ -147,22 +181,38 @@ def __init__( retry_config: RetryConfig | None = None, timeout: float = 30.0, auth_header: dict[str, str] | None = None, + schema_version: str = DEFAULT_SCHEMA_VERSION, ) -> None: """Initialize the deep validator. Args: validator_factory: Factory to create ValidationEngine instances max_depth: Maximum depth to traverse (0 = root only) - follow_links: JSON paths to follow for links + follow_links: JSON paths to follow for links. When ``None``, + the path list is selected from :data:`LINK_PATHS_BY_VERSION` + keyed on ``schema_version``. The v0.7.0 paths + (``performanceClaim[*].evidence``, ``relatedDocument``, + ``materialProvenance[*].materialSafetyInformation``, + ``relatedParty[*].party.id``, ``evidence``) differ + substantially from the v0.6.x set — see Phase 3b of + docs/plans/UNTP_0.7.0_MIGRATION.md. rate_limiter: Rate limiter for HTTP requests retry_config: Retry configuration for failed requests timeout: HTTP request timeout in seconds auth_header: Authorization headers for requests + schema_version: UNTP DPP version. Drives the default + ``follow_links`` selection from + :data:`LINK_PATHS_BY_VERSION`. Ignored when + ``follow_links`` is supplied explicitly. """ self._validator_factory = validator_factory self.max_depth = max_depth - self.follow_links = follow_links or DEFAULT_LINK_PATHS + self.schema_version = schema_version + if follow_links is not None: + self.follow_links = follow_links + else: + self.follow_links = LINK_PATHS_BY_VERSION.get(schema_version, DEFAULT_LINK_PATHS) self.rate_limiter = rate_limiter or RateLimiter() self.retry_config = retry_config or RetryConfig() self.timeout = timeout @@ -339,10 +389,25 @@ def _extract_links( return links def _get_urls_at_path(self, data: dict[str, Any], path: str) -> list[str]: - """Get URLs from a JSON path in the data.""" + """Get URLs from a JSON path in the data. + + Supports two syntactic forms for list traversal: + + - **Implicit:** ``credentialSubject.materialsProvenance.name`` — + when the resolver hits a list, it collects ``name`` from every + item. This is the v0.6.x convention. + - **Explicit:** ``credentialSubject.performanceClaim[*].evidence`` + — the ``[*]`` token is normalised away (v0.7.0 paths use this + form for clarity). + + Both forms produce the same result; ``[*]`` is purely a readability + marker for the v0.7 path table in :data:`LINK_PATHS_BY_VERSION`. + """ urls = [] - parts = path.split(".") - current = data + # Normalise ``segment[*]`` → ``segment`` so the resolver below + # doesn't have to know about the explicit list-iteration token. + parts = [segment.replace("[*]", "") for segment in path.split(".")] + current: Any = data for part in parts: if current is None: @@ -401,13 +466,17 @@ async def validate_deep( follow_links: list[str] | None = None, timeout: float = 30.0, auth_header: dict[str, str] | None = None, + schema_version: str = DEFAULT_SCHEMA_VERSION, ) -> DeepValidationResult: """Perform deep validation with default settings. Args: data: Root DPP document data max_depth: Maximum depth to traverse - follow_links: JSON paths to follow for links + follow_links: JSON paths to follow for links. When ``None``, + picked from :data:`LINK_PATHS_BY_VERSION` based on + ``schema_version`` (Phase 3b). + schema_version: UNTP DPP version. Selects the default link paths. timeout: HTTP request timeout auth_header: Authorization headers @@ -420,5 +489,6 @@ async def validate_deep( follow_links=follow_links, timeout=timeout, auth_header=auth_header, + schema_version=schema_version, ) return await validator.validate(data) diff --git a/src/dppvalidator/validators/detection.py b/src/dppvalidator/validators/detection.py index 3669e1a..47621f5 100644 --- a/src/dppvalidator/validators/detection.py +++ b/src/dppvalidator/validators/detection.py @@ -7,9 +7,28 @@ from dppvalidator.schemas.registry import DEFAULT_SCHEMA_VERSION, SCHEMA_REGISTRY -# Patterns for extracting version from URLs -_SCHEMA_URL_PATTERN = re.compile(r"untp-dpp-schema-(\d+\.\d+\.\d+)\.json") -_CONTEXT_URL_PATTERN = re.compile(r"/untp/dpp/(\d+\.\d+\.\d+)/?") +# Patterns for extracting version from URLs. +# +# Two URL conventions are recognised, in priority order: +# 1. Legacy (UNTP 0.6.x): schema basename `untp-dpp-schema-X.Y.Z.json` and +# context `/untp/dpp/X.Y.Z/` under `test.uncefact.org`. +# 2. Modern (UNTP 0.7.0+): schema lives at any path containing the version +# as a `/X.Y.Z/` or `/vX.Y.Z/` segment followed by a credential-type +# basename (`DigitalProductPassport.json`), and the context lives at +# `/untp/X.Y.Z/context/?` under `vocabulary.uncefact.org`. +# +# False positives are guarded by the `version in SCHEMA_REGISTRY` membership +# check at the call sites, so unrelated `/X.Y.Z/` path segments cannot smuggle +# in unsupported versions. Adding a new URL convention means appending a +# pattern here — see docs/plans/UNTP_0.7.0_MIGRATION.md §Phase 1 and §7. +_SCHEMA_URL_PATTERNS: tuple[re.Pattern[str], ...] = ( + re.compile(r"untp-dpp-schema-(\d+\.\d+\.\d+)\.json"), + re.compile(r"/v?(\d+\.\d+\.\d+)/[^?#]*DigitalProductPassport[^?#]*\.json"), +) +_CONTEXT_URL_PATTERNS: tuple[re.Pattern[str], ...] = ( + re.compile(r"/untp/dpp/(\d+\.\d+\.\d+)/?"), + re.compile(r"/untp/(\d+\.\d+\.\d+)/(?:context/?)?"), +) # Expected types for UNTP DPP _DPP_TYPES = frozenset({"DigitalProductPassport", "VerifiableCredential"}) @@ -28,7 +47,8 @@ def detect_schema_version(data: dict[str, Any]) -> str: data: Raw DPP JSON data Returns: - Detected schema version string (e.g., "0.6.1") + Detected schema version string (one of `SCHEMA_REGISTRY.keys()`). + Falls back to ``DEFAULT_SCHEMA_VERSION`` when no marker is present. """ # Priority 1: Check $schema URL version = _detect_from_schema_url(data) @@ -48,6 +68,28 @@ def detect_schema_version(data: dict[str, Any]) -> str: return DEFAULT_SCHEMA_VERSION +def detect_declared_version(data: dict[str, Any]) -> str | None: + """Return the version a payload **explicitly declares**, or ``None``. + + Distinct from :func:`detect_schema_version`: this helper returns + ``None`` when no UNTP-versioned ``$schema`` URL or UNTP-versioned + ``@context`` URL is present, instead of falling back to a default. The + engine uses this for the VER001 mismatch check (see Phase 3.3 of + docs/plans/UNTP_0.7.0_MIGRATION.md): if a payload declares a version + that conflicts with the engine's explicitly-configured version, fail + fast — but if the payload declares no version at all, trust the + user's configuration without complaint. + + Args: + data: Raw DPP JSON data + + Returns: + The declared version (one of ``SCHEMA_REGISTRY.keys()``), or + ``None`` when the payload carries no version markers. + """ + return _detect_from_schema_url(data) or _detect_from_context(data) + + def _detect_from_schema_url(data: dict[str, Any]) -> str | None: """Extract version from $schema URL. @@ -61,11 +103,10 @@ def _detect_from_schema_url(data: dict[str, Any]) -> str | None: if not isinstance(schema_url, str): return None - match = _SCHEMA_URL_PATTERN.search(schema_url) - if match: - version = match.group(1) - if version in SCHEMA_REGISTRY: - return version + for pattern in _SCHEMA_URL_PATTERNS: + match = pattern.search(schema_url) + if match and match.group(1) in SCHEMA_REGISTRY: + return match.group(1) return None @@ -93,11 +134,10 @@ def _detect_from_context(data: dict[str, Any]) -> str | None: # Search for version pattern in any context URL for url in urls: - match = _CONTEXT_URL_PATTERN.search(url) - if match: - version = match.group(1) - if version in SCHEMA_REGISTRY: - return version + for pattern in _CONTEXT_URL_PATTERNS: + match = pattern.search(url) + if match and match.group(1) in SCHEMA_REGISTRY: + return match.group(1) return None diff --git a/src/dppvalidator/validators/engine.py b/src/dppvalidator/validators/engine.py index 1b66eed..775e5a2 100644 --- a/src/dppvalidator/validators/engine.py +++ b/src/dppvalidator/validators/engine.py @@ -19,7 +19,10 @@ from typing import TYPE_CHECKING, Any, Literal from dppvalidator.logging import get_logger -from dppvalidator.validators.detection import detect_schema_version +from dppvalidator.validators.detection import ( + detect_declared_version, + detect_schema_version, +) from dppvalidator.validators.layers import ( JsonLdLayer, ModelLayer, @@ -290,6 +293,36 @@ def validate( ) context.result.parse_time_ms = parse_time + # VER001: when the user explicitly configured a version (NOT + # auto-detect) and the payload itself declares a different version + # via ``$schema`` or ``@context`` URLs, fail fast. UNTPBaseModel + # has ``extra="allow"``, so without this check a mis-versioned + # payload would silently lose fields. See plan §4.1.7. + if not self._auto_detect: + declared = detect_declared_version(parsed_data) + if declared is not None and declared != effective_version: + context.result.errors.append( + ValidationError( + path="$", + message=( + f"Payload declares UNTP version {declared!r} but the engine is " + f"configured for {effective_version!r}. Re-run with " + f"`schema_version={declared!r}` (or `schema_version='auto'`) " + "to validate against the version the payload actually claims." + ), + code="VER001", + layer="engine", + severity="error", + context={ + "declared_version": declared, + "configured_version": effective_version, + }, + ), + ) + context.result.valid = False + context.result.schema_version = effective_version + return context.result + # Build and execute validation layers validation_layers = self._build_layers(effective_version) for layer in validation_layers: diff --git a/src/dppvalidator/validators/errors.py b/src/dppvalidator/validators/errors.py index 9af74a3..7737b97 100644 --- a/src/dppvalidator/validators/errors.py +++ b/src/dppvalidator/validators/errors.py @@ -128,14 +128,47 @@ class ErrorSuggestionDict(TypedDict, total=False): }, } -# Known valid values for "Did you mean?" suggestions +# Known valid values for "Did you mean?" suggestions. +# +# Field names are the on-the-wire (camelCase) spellings. Both UNTP +# version's spellings appear here for fields that were renamed across +# v0.6 → v0.7 — e.g. ``granularityLevel`` (v0.6) and ``idGranularity`` +# (v0.7) share the same enum values, so users on either version get +# typo suggestions. +_GRANULARITY_VALUES: list[str] = ["item", "batch", "model"] +_PARTY_ROLE_VALUES: list[str] = [ + # Mirrors dppvalidator.models.v0_7.identifiers.PartyRoleEnum. + "owner", + "producer", + "manufacturer", + "processor", + "remanufacturer", + "recycler", + "operator", + "serviceProvider", + "inspector", + "certifier", + "logisticsProvider", + "carrier", + "consignor", + "consignee", + "importer", + "exporter", + "distributor", + "retailer", + "brandOwner", + "regulator", +] + KNOWN_VALUES: dict[str, list[str]] = { "type": [ "DigitalProductPassport", "VerifiableCredential", "EnvelopedVerifiableCredential", ], - "granularityLevel": ["item", "batch", "model"], + "granularityLevel": _GRANULARITY_VALUES, # v0.6 spelling + "idGranularity": _GRANULARITY_VALUES, # v0.7 spelling + "role": _PARTY_ROLE_VALUES, # v0.7 PartyRole.role "operationalScope": ["None", "Scope1", "Scope2", "Scope3", "CradleToGate", "CradleToGrave"], "claimType": [ "Certification", diff --git a/src/dppvalidator/validators/jsonld_semantic.py b/src/dppvalidator/validators/jsonld_semantic.py index 15d3318..d573d5f 100644 --- a/src/dppvalidator/validators/jsonld_semantic.py +++ b/src/dppvalidator/validators/jsonld_semantic.py @@ -2,14 +2,17 @@ from __future__ import annotations +import json import time from functools import lru_cache +from importlib import resources from typing import Any from pyld import jsonld from pyld.jsonld import JsonLdError from dppvalidator.logging import get_logger +from dppvalidator.schemas.registry import DEFAULT_SCHEMA_VERSION, SCHEMA_REGISTRY from dppvalidator.validators.results import ValidationError, ValidationResult logger = get_logger(__name__) @@ -56,9 +59,53 @@ } } -# URLs that map to bundled contexts -BUNDLED_CONTEXT_URLS = { + +def _load_bundled_untp_contexts() -> dict[str, dict[str, Any]]: + """Map every registered UNTP context URL to its bundled document. + + Walks ``SCHEMA_REGISTRY`` and reads + ``src/dppvalidator/vocabularies/data/untp-context-.jsonld`` for + each entry. Each ``SchemaVersion.context_urls`` tuple is ``(VC v2 URL, + UNTP URL)``; this function maps that second URL → bundled JSON-LD doc. + + Versions whose context file is not vendored are silently skipped, so a + partial install still works (and the network fallback in + :class:`CachingDocumentLoader` handles the rest). + + Returns: + Mapping ``{untp_context_url: parsed_jsonld_document}`` registered by + Phase 2 of docs/plans/UNTP_0.7.0_MIGRATION.md. + """ + out: dict[str, dict[str, Any]] = {} + for version, schema in SCHEMA_REGISTRY.items(): + if len(schema.context_urls) < 2: + continue + untp_url = schema.context_urls[1] + try: + ctx_file = resources.files("dppvalidator.vocabularies.data").joinpath( + f"untp-context-{version}.jsonld" + ) + content = ctx_file.read_text(encoding="utf-8") + except (FileNotFoundError, ModuleNotFoundError): + continue + try: + out[untp_url] = json.loads(content) + except json.JSONDecodeError: + logger.warning( + "Bundled UNTP context for %s is not valid JSON; skipping bundling", + version, + ) + return out + + +# URLs that map to bundled contexts. The W3C VC v2 entry is hand-crafted (the +# real document is huge but we only need a minimal subset for DPP expansion); +# the UNTP entries come from the bundled files registered in Phase 2 so the +# JSON-LD layer is fully offline-capable for both the legacy 0.6.x and the +# modern 0.7.0 namespaces. +BUNDLED_CONTEXT_URLS: dict[str, dict[str, Any]] = { "https://www.w3.org/ns/credentials/v2": _BUNDLED_VC_V2_CONTEXT, + **_load_bundled_untp_contexts(), } @@ -131,7 +178,7 @@ class JSONLDValidator: def __init__( self, - schema_version: str = "0.6.1", + schema_version: str = DEFAULT_SCHEMA_VERSION, strict: bool = False, cache_contexts: bool = True, ) -> None: diff --git a/src/dppvalidator/validators/layers.py b/src/dppvalidator/validators/layers.py index e26127d..879dcd9 100644 --- a/src/dppvalidator/validators/layers.py +++ b/src/dppvalidator/validators/layers.py @@ -244,7 +244,14 @@ def execute(self, context: ValidationContext) -> ValidationResult: if self._registry is None: return ValidationResult(valid=True, schema_version=self._schema_version) - plugin_errors = self._registry.run_all_validators(context.passport) + # Pass the engine's resolved version so plugins that declare + # ``applies_to_versions`` get filtered out for non-matching + # payloads. Plugins without that attribute keep running for + # every payload (back-compat for pre-Phase-6 plugins). + plugin_errors = self._registry.run_all_validators( + context.passport, + schema_version=self._schema_version, + ) errors = [e for e in plugin_errors if e.severity == "error"] warnings = [e for e in plugin_errors if e.severity == "warning"] diff --git a/src/dppvalidator/validators/model.py b/src/dppvalidator/validators/model.py index 58165a8..208cb01 100644 --- a/src/dppvalidator/validators/model.py +++ b/src/dppvalidator/validators/model.py @@ -3,13 +3,32 @@ from __future__ import annotations import time -from typing import Any +from typing import TYPE_CHECKING, Any +from pydantic import BaseModel from pydantic import ValidationError as PydanticValidationError -from dppvalidator.models.passport import DigitalProductPassport +from dppvalidator.models import v0_6, v0_7 +from dppvalidator.schemas.registry import DEFAULT_SCHEMA_VERSION from dppvalidator.validators.results import ValidationError, ValidationResult +if TYPE_CHECKING: + pass + +# Single dispatch table for "which Pydantic root class validates which UNTP +# version". Adding a new version here is a one-line change — see Phase 3.3 +# of docs/plans/UNTP_0.7.0_MIGRATION.md and the cardinal rules in +# .claude/rules/untp-versioning.md (rule 3). +# +# Keep keys aligned with ``SCHEMA_REGISTRY`` keys; the +# ``test_model_dispatch_covers_registry`` test in +# tests/unit/test_v07_models.py guarantees this. +_MODEL_BY_VERSION: dict[str, type[BaseModel]] = { + "0.6.0": v0_6.DigitalProductPassport, + "0.6.1": v0_6.DigitalProductPassport, + "0.7.0": v0_7.DigitalProductPassport, +} + # Stable error code mapping based on Pydantic error types # See: https://docs.pydantic.dev/latest/errors/validation_errors/ PYDANTIC_ERROR_CODES: dict[str, str] = { @@ -70,7 +89,7 @@ class ModelValidator: name: str = "model" layer: str = "model" - def __init__(self, schema_version: str = "0.6.1") -> None: + def __init__(self, schema_version: str = DEFAULT_SCHEMA_VERSION) -> None: """Initialize model validator. Args: @@ -81,6 +100,11 @@ def __init__(self, schema_version: str = "0.6.1") -> None: def validate(self, data: dict[str, Any]) -> ValidationResult: """Validate data using Pydantic models. + The Pydantic root class is selected from :data:`_MODEL_BY_VERSION` + keyed on ``self.schema_version``. Adding a new UNTP version means + adding one entry there (and shipping the model package); no + changes are needed in this method. + Args: data: Raw JSON data to validate @@ -89,27 +113,51 @@ def validate(self, data: dict[str, Any]) -> ValidationResult: """ start_time = time.perf_counter() errors: list[ValidationError] = [] - passport: DigitalProductPassport | None = None - - try: - passport = DigitalProductPassport.model_validate(data) - except PydanticValidationError as e: - for error in e.errors(): - json_path = self._loc_to_path(error.get("loc", ())) - error_type = error.get("type", "unknown") - errors.append( - ValidationError( - path=json_path, - message=error.get("msg", "Validation error"), - code=self._get_error_code(error_type), - layer="model", - severity="error", - context={ - "type": error_type, - "input": self._safe_input(error.get("input")), - }, + # Annotated as ``BaseModel | None`` rather than the v0.6 + # ``DigitalProductPassport`` so ``_MODEL_BY_VERSION`` can return + # either a v0.6 or a v0.7 root class. Callers downcast or use + # ``isinstance`` when they need a specific shape — see + # docs/plans/UNTP_0.7.0_MIGRATION.md §3.3. + passport: BaseModel | None = None + + model_cls = _MODEL_BY_VERSION.get(self.schema_version) + if model_cls is None: + # Unsupported version — fail fast with a structured error rather + # than silently coercing to whatever the default model accepts. + available = ", ".join(sorted(_MODEL_BY_VERSION)) + errors.append( + ValidationError( + path="$", + message=( + f"No Pydantic model registered for schema version " + f"{self.schema_version!r}. Registered: {available}." + ), + code="MDL098", + layer="model", + severity="error", + context={"requested_version": self.schema_version}, + ), + ) + else: + try: + passport = model_cls.model_validate(data) + except PydanticValidationError as e: + for error in e.errors(): + json_path = self._loc_to_path(error.get("loc", ())) + error_type = error.get("type", "unknown") + errors.append( + ValidationError( + path=json_path, + message=error.get("msg", "Validation error"), + code=self._get_error_code(error_type), + layer="model", + severity="error", + context={ + "type": error_type, + "input": self._safe_input(error.get("input")), + }, + ) ) - ) validation_time = (time.perf_counter() - start_time) * 1000 @@ -117,7 +165,10 @@ def validate(self, data: dict[str, Any]) -> ValidationResult: valid=len(errors) == 0, errors=errors, schema_version=self.schema_version, - passport=passport, + # `passport` is a v0.6 or v0.7 DigitalProductPassport at runtime; + # ``ValidationResult.passport`` is annotated with the v0.6 type + # under TYPE_CHECKING for backward compat (see plan §3.3). + passport=passport, # type: ignore[arg-type] validation_time_ms=validation_time, ) diff --git a/src/dppvalidator/validators/results.py b/src/dppvalidator/validators/results.py index b31aa15..e95c9d0 100644 --- a/src/dppvalidator/validators/results.py +++ b/src/dppvalidator/validators/results.py @@ -7,6 +7,8 @@ from datetime import datetime from typing import TYPE_CHECKING, Any, Literal +from dppvalidator.schemas.registry import DEFAULT_SCHEMA_VERSION + if TYPE_CHECKING: from dppvalidator.models.passport import DigitalProductPassport @@ -30,7 +32,7 @@ class ValidationError: path: str message: str code: str - layer: Literal["schema", "model", "semantic", "jsonld", "plugin", "vocabulary"] + layer: Literal["schema", "model", "semantic", "jsonld", "plugin", "vocabulary", "engine"] severity: Literal["error", "warning", "info"] = "error" suggestion: str | None = None docs_url: str | None = None @@ -79,7 +81,7 @@ class ValidationResult: errors: list[ValidationError] = field(default_factory=list) warnings: list[ValidationError] = field(default_factory=list) info: list[ValidationError] = field(default_factory=list) - schema_version: str = "0.6.1" + schema_version: str = DEFAULT_SCHEMA_VERSION validated_at: datetime = field(default_factory=datetime.now) passport: DigitalProductPassport | None = None parse_time_ms: float = 0.0 diff --git a/src/dppvalidator/validators/rules/__init__.py b/src/dppvalidator/validators/rules/__init__.py index 0ba27f1..41a1ca2 100644 --- a/src/dppvalidator/validators/rules/__init__.py +++ b/src/dppvalidator/validators/rules/__init__.py @@ -1,7 +1,42 @@ -"""Pluggable semantic validation rules.""" +"""Pluggable semantic validation rules. -from dppvalidator.validators.rules.base import ( +Phase 3b of docs/plans/UNTP_0.7.0_MIGRATION.md split this package into +:mod:`dppvalidator.validators.rules.v0_6` and +:mod:`dppvalidator.validators.rules.v0_7` so 0.6.x and 0.7.0 rule sets can +coexist. This module exposes: + +* :data:`ALL_RULES_BY_VERSION` — the dispatch table consumed by + :class:`dppvalidator.validators.semantic.SemanticValidator` when no + custom ``rules`` list is passed. +* :data:`ALL_RULES` — the default rule set (kept as the v0.6.x list for + the 0.4.x line so existing callers continue to see the same behaviour). +* All v0.6 rule classes — re-exported for backward compatibility, so + ``from dppvalidator.validators.rules import MassFractionSumRule`` keeps + working. + +Adding a new UNTP version: + +1. Build the ported rules under ``rules/v0_X/`` (one module per topic). +2. Import its ``ALL_RULES_V0_X`` list here and add it to + :data:`ALL_RULES_BY_VERSION`. +3. The :class:`SemanticValidator` picks it up automatically — no further + wiring required. See ``.claude/rules/untp-versioning.md`` (rule 2). +""" + +from __future__ import annotations + +# v0.6 (default in the 0.4.x line) — re-export for backward compat. +from dppvalidator.validators.rules.v0_6 import ( + ALL_RULES_V0_6, + CIRPASS_RULES, + TEXTILE_RULES, CircularityContentRule, + CIRPASSGranularityConsistencyRule, + CIRPASSMandatoryAttributesRule, + CIRPASSOperatorIdentifierRule, + CIRPASSSubstancesOfConcernRule, + CIRPASSValidityPeriodRule, + CIRPASSWeightVolumeRule, ConformityClaimRule, GranularitySerialNumberRule, GTINChecksumRule, @@ -10,74 +45,62 @@ MassFractionSumRule, MaterialCodeRule, OperationalScopeRule, - ValidityDateRule, -) -from dppvalidator.validators.rules.cirpass import ( - CIRPASS_RULES, - CIRPASSGranularityConsistencyRule, - CIRPASSMandatoryAttributesRule, - CIRPASSOperatorIdentifierRule, - CIRPASSSubstancesOfConcernRule, - CIRPASSValidityPeriodRule, - CIRPASSWeightVolumeRule, -) -from dppvalidator.validators.rules.textile import ( - TEXTILE_RULES, TextileCareInstructionsRule, TextileDurabilityRule, TextileEnvironmentalCategory, TextileHSCodeRule, TextileMaterialCompositionRule, TextileMicroplasticRule, + ValidityDateRule, get_textile_environmental_categories, is_textile_product, ) +from dppvalidator.validators.rules.v0_7 import ALL_RULES_V0_7 -ALL_RULES = [ - # Base UNTP rules - MassFractionSumRule(), - ValidityDateRule(), - HazardousMaterialRule(), - CircularityContentRule(), - ConformityClaimRule(), - GranularitySerialNumberRule(), - OperationalScopeRule(), - MaterialCodeRule(), - HSCodeRule(), - GTINChecksumRule(), - # CIRPASS-2 rules (CQ-based) - *CIRPASS_RULES, -] +# Backward-compat default. The 0.4.x line ships with v0.6 as the default +# schema version, so ``ALL_RULES`` points at the v0.6 list. Phase 9 flips +# this to the v0.7 list when ``DEFAULT_SCHEMA_VERSION`` flips. +ALL_RULES = list(ALL_RULES_V0_6) + +# Version-keyed dispatch table consumed by ``SemanticValidator``. Both +# 0.6.0 and 0.6.1 share the same rule set because the model shape is the +# same; 0.7.0 has its own. +ALL_RULES_BY_VERSION: dict[str, list] = { + "0.6.0": ALL_RULES_V0_6, + "0.6.1": ALL_RULES_V0_6, + "0.7.0": ALL_RULES_V0_7, +} __all__ = [ "ALL_RULES", - # Base rules - "MassFractionSumRule", - "ValidityDateRule", - "HazardousMaterialRule", - "CircularityContentRule", - "ConformityClaimRule", - "GranularitySerialNumberRule", - "OperationalScopeRule", - "MaterialCodeRule", - "HSCodeRule", - "GTINChecksumRule", - # CIRPASS-2 rules + "ALL_RULES_BY_VERSION", + "ALL_RULES_V0_6", + "ALL_RULES_V0_7", + # v0.6 re-exports (backward compat) "CIRPASS_RULES", + "CIRPASSGranularityConsistencyRule", "CIRPASSMandatoryAttributesRule", - "CIRPASSSubstancesOfConcernRule", "CIRPASSOperatorIdentifierRule", + "CIRPASSSubstancesOfConcernRule", "CIRPASSValidityPeriodRule", "CIRPASSWeightVolumeRule", - "CIRPASSGranularityConsistencyRule", - # Textile sector rules + "CircularityContentRule", + "ConformityClaimRule", + "GTINChecksumRule", + "GranularitySerialNumberRule", + "HSCodeRule", + "HazardousMaterialRule", + "MassFractionSumRule", + "MaterialCodeRule", + "OperationalScopeRule", "TEXTILE_RULES", + "TextileCareInstructionsRule", + "TextileDurabilityRule", + "TextileEnvironmentalCategory", "TextileHSCodeRule", "TextileMaterialCompositionRule", "TextileMicroplasticRule", - "TextileDurabilityRule", - "TextileCareInstructionsRule", - "TextileEnvironmentalCategory", - "is_textile_product", + "ValidityDateRule", "get_textile_environmental_categories", + "is_textile_product", ] diff --git a/src/dppvalidator/validators/rules/base.py b/src/dppvalidator/validators/rules/base.py index 0b39d53..81c172b 100644 --- a/src/dppvalidator/validators/rules/base.py +++ b/src/dppvalidator/validators/rules/base.py @@ -1,398 +1,39 @@ -"""Semantic validation rule implementations.""" +"""Backward-compatibility re-export of v0.6.x base UNTP semantic rules. -from __future__ import annotations - -import re -from collections.abc import Callable -from typing import TYPE_CHECKING, Literal - -from dppvalidator.vocabularies.code_lists import ( - is_valid_hs_code as _is_valid_hs_code, -) -from dppvalidator.vocabularies.code_lists import ( - is_valid_material_code as _is_valid_material_code, -) -from dppvalidator.vocabularies.code_lists import ( - validate_gtin as _validate_gtin, -) - -if TYPE_CHECKING: - from dppvalidator.models.passport import DigitalProductPassport - - -class MassFractionSumRule: - """SEM001: Material mass fractions should sum to 1.0. - - Per UNTP spec, partial declarations (sum < 1.0) are valid but should - be flagged as a warning. Only sum > 1.0 is physically impossible. - """ - - rule_id: str = "SEM001" - description: str = "Material mass fractions should sum to 1.0" - severity: Literal["error", "warning", "info"] = "warning" - suggestion: str = "Adjust material mass fractions to sum to 1.0 (100%)" - docs_url: str = "https://artiso-ai.github.io/dppvalidator/errors/SEM001" - - def check(self, passport: DigitalProductPassport) -> list[tuple[str, str]]: - """Check mass fraction sum.""" - violations: list[tuple[str, str]] = [] - - if not passport.credential_subject: - return violations - - materials = passport.credential_subject.materials_provenance - if not materials: - return violations - - fractions = [m.mass_fraction for m in materials if m.mass_fraction is not None] - if fractions: - total = sum(fractions) - # Only flag if significantly different from 1.0 - # Partial declarations (< 1.0) are valid per UNTP spec - if abs(total - 1.0) > 0.01: - violations.append( - ( - "$.credentialSubject.materialsProvenance", - f"Mass fractions sum to {total:.3f}, expected 1.0 (partial declaration)", - ) - ) - - return violations - - -class ValidityDateRule: - """SEM002: validFrom must be before validUntil.""" - - rule_id: str = "SEM002" - description: str = "validFrom must be before validUntil" - severity: Literal["error", "warning", "info"] = "error" - suggestion: str = "Ensure validFrom is before validUntil" - docs_url: str = "https://artiso-ai.github.io/dppvalidator/errors/SEM002" - - def check(self, passport: DigitalProductPassport) -> list[tuple[str, str]]: - """Check validity date ordering.""" - violations: list[tuple[str, str]] = [] - - if ( - passport.valid_from - and passport.valid_until - and passport.valid_from >= passport.valid_until - ): - violations.append( - ( - "$.validFrom", - f"validFrom ({passport.valid_from}) must be before validUntil ({passport.valid_until})", - ) - ) - - return violations - - -class HazardousMaterialRule: - """SEM003: hazardous=true requires materialSafetyInformation.""" - - rule_id: str = "SEM003" - description: str = "Hazardous materials require safety information" - severity: Literal["error", "warning", "info"] = "error" - suggestion: str = "Add materialSafetyInformation for hazardous materials" - docs_url: str = "https://artiso-ai.github.io/dppvalidator/errors/SEM003" - - def check(self, passport: DigitalProductPassport) -> list[tuple[str, str]]: - """Check hazardous material safety info.""" - violations: list[tuple[str, str]] = [] - - if not passport.credential_subject: - return violations - - materials = passport.credential_subject.materials_provenance - if not materials: - return violations - - for i, material in enumerate(materials): - if material.hazardous and not material.material_safety_information: - violations.append( - ( - f"$.credentialSubject.materialsProvenance[{i}]", - f"Material '{material.name}' is hazardous but missing materialSafetyInformation", - ) - ) - - return violations - - -class CircularityContentRule: - """SEM004: recycledContent should not exceed recyclableContent.""" - - rule_id: str = "SEM004" - description: str = "recycledContent should not exceed recyclableContent" - severity: Literal["error", "warning", "info"] = "warning" - suggestion: str = "recycledContent cannot exceed recyclableContent" - docs_url: str = "https://artiso-ai.github.io/dppvalidator/errors/SEM004" - - def check(self, passport: DigitalProductPassport) -> list[tuple[str, str]]: - """Check circularity content consistency.""" - violations: list[tuple[str, str]] = [] - - if not passport.credential_subject: - return violations - - scorecard = passport.credential_subject.circularity_scorecard - if not scorecard: - return violations - - recycled = scorecard.recycled_content - recyclable = scorecard.recyclable_content - - if recycled is not None and recyclable is not None and recycled > recyclable: - violations.append( - ( - "$.credentialSubject.circularityScorecard", - f"recycledContent ({recycled}) exceeds recyclableContent ({recyclable})", - ) - ) - - return violations - - -class ConformityClaimRule: - """SEM005: At least one conformityClaim is recommended.""" - - rule_id: str = "SEM005" - description: str = "At least one conformityClaim is recommended" - severity: Literal["error", "warning", "info"] = "info" - suggestion: str = "Add conformity claims for sustainability or compliance" - docs_url: str = "https://artiso-ai.github.io/dppvalidator/errors/SEM005" - - def check(self, passport: DigitalProductPassport) -> list[tuple[str, str]]: - """Check for conformity claims.""" - violations: list[tuple[str, str]] = [] - - if not passport.credential_subject: - return violations - - claims = passport.credential_subject.conformity_claim - if not claims: - violations.append( - ( - "$.credentialSubject.conformityClaim", - "No conformity claims present. Consider adding sustainability or compliance claims.", - ) - ) +Phase 3b of docs/plans/UNTP_0.7.0_MIGRATION.md split semantic rules into +``rules/v0_6/`` and ``rules/v0_7/`` subpackages. This shim preserves the +import path used by existing tests and any third-party plugin +(``from dppvalidator.validators.rules.base import CircularityContentRule``) — see +the public-API stability contract in §7.6 of the plan. - return violations +Through the 0.4.x line this re-exports v0.6.x rules. Phase 9 will switch +the default to v0.7 and update this shim accordingly. +""" +from __future__ import annotations -class GranularitySerialNumberRule: - """SEM006: granularityLevel=item requires serialNumber.""" - - rule_id: str = "SEM006" - description: str = "Item-level passports require serial numbers" - severity: Literal["error", "warning", "info"] = "warning" - suggestion: str = "Add serialNumber for item-level granularity passports" - docs_url: str = "https://artiso-ai.github.io/dppvalidator/errors/SEM006" - - def check(self, passport: DigitalProductPassport) -> list[tuple[str, str]]: - """Check serial number for item-level passports.""" - violations: list[tuple[str, str]] = [] - - if not passport.credential_subject: - return violations - - granularity = passport.credential_subject.granularity_level - product = passport.credential_subject.product - - if ( - granularity - and str(granularity) == "item" - and (not product or not product.serial_number) - ): - violations.append( - ( - "$.credentialSubject.product.serialNumber", - "granularityLevel is 'item' but serialNumber is missing", - ) - ) - - return violations - - -class OperationalScopeRule: - """SEM007: carbonFootprint should have operationalScope defined.""" - - rule_id: str = "SEM007" - description: str = "Emissions data should specify operational scope" - severity: Literal["error", "warning", "info"] = "warning" - suggestion: str = "Specify operationalScope with carbonFootprint data" - docs_url: str = "https://artiso-ai.github.io/dppvalidator/errors/SEM007" - - def check(self, passport: DigitalProductPassport) -> list[tuple[str, str]]: - """Check operational scope for emissions data.""" - violations: list[tuple[str, str]] = [] - - if not passport.credential_subject: - return violations - - scorecard = passport.credential_subject.emissions_scorecard - if not scorecard: - return violations - - if scorecard.carbon_footprint and not scorecard.operational_scope: - violations.append( - ( - "$.credentialSubject.emissionsScorecard.operationalScope", - "carbonFootprint is specified but operationalScope is missing", - ) - ) - - return violations - - -class MaterialCodeRule: - """VOC003: Material code must be valid per UNECE Rec 46. - - Validates material codes in materialsProvenance against the - UNECE Recommendation 46 material classification codes. - """ - - rule_id: str = "VOC003" - description: str = "Material code must be in UNECE Rec 46" - severity: Literal["error", "warning", "info"] = "warning" - suggestion: str = "Use a valid UNECE Rec 46 material code" - docs_url: str = "https://artiso-ai.github.io/dppvalidator/errors/VOC003" - - def __init__( - self, - material_validator: Callable[[str], bool] | None = None, - ) -> None: - """Initialize with optional custom validator.""" - self._is_valid_material_code = material_validator or _is_valid_material_code - - def check(self, passport: DigitalProductPassport) -> list[tuple[str, str]]: - """Check material codes against UNECE Rec 46.""" - violations: list[tuple[str, str]] = [] - - if not passport.credential_subject: - return violations - - materials = passport.credential_subject.materials_provenance - if not materials: - return violations - - for i, material in enumerate(materials): - # Check material_type.code if present - if material.material_type and material.material_type.code: - code = material.material_type.code - if not self._is_valid_material_code(code): - violations.append( - ( - f"$.credentialSubject.materialsProvenance[{i}].materialType.code", - f"Invalid material code '{code}' - not found in UNECE Rec 46", - ) - ) - - return violations - - -class HSCodeRule: - """VOC004: HS code must be valid for product category. - - Validates HS codes in product information against the - Harmonized System textile chapters (50-63). - """ - - rule_id: str = "VOC004" - description: str = "HS code must be valid for product category" - severity: Literal["error", "warning", "info"] = "warning" - suggestion: str = "Use a valid HS code for textiles (chapters 50-63)" - docs_url: str = "https://artiso-ai.github.io/dppvalidator/errors/VOC004" - - def __init__( - self, - hs_validator: Callable[[str], bool] | None = None, - ) -> None: - """Initialize with optional custom validator.""" - self._is_valid_hs_code = hs_validator or _is_valid_hs_code - - def check(self, passport: DigitalProductPassport) -> list[tuple[str, str]]: - """Check HS codes for validity.""" - violations: list[tuple[str, str]] = [] - - if not passport.credential_subject: - return violations - - product = passport.credential_subject.product - if not product: - return violations - - # Check product category classifications for HS codes - if product.product_category: - for i, classification in enumerate(product.product_category): - code = classification.code if classification.code else "" - # Only validate if it looks like an HS code (4+ digits) - if code.isdigit() and len(code) >= 4 and not self._is_valid_hs_code(code): - violations.append( - ( - f"$.credentialSubject.product.productCategory[{i}].code", - f"Invalid HS code '{code}' - not found in textile chapters (50-63)", - ) - ) - - return violations - - -class GTINChecksumRule: - """VOC005: GTIN must have valid check digit. - - Validates GTIN (Global Trade Item Number) checksums in product - identifiers using the GS1 check digit algorithm. - """ - - rule_id: str = "VOC005" - description: str = "GTIN must have valid check digit" - severity: Literal["error", "warning", "info"] = "error" - suggestion: str = "Verify the GTIN check digit using GS1 algorithm" - docs_url: str = "https://artiso-ai.github.io/dppvalidator/errors/VOC005" - - def __init__( - self, - gtin_validator: Callable[[str], bool] | None = None, - ) -> None: - """Initialize with optional custom validator.""" - self._validate_gtin = gtin_validator or _validate_gtin - - def check(self, passport: DigitalProductPassport) -> list[tuple[str, str]]: - """Check GTIN checksums.""" - violations: list[tuple[str, str]] = [] - - if not passport.credential_subject: - return violations - - product = passport.credential_subject.product - if not product: - return violations - - # Check product ID if it looks like a GTIN - if product.id: - product_id = product.id - # Extract GTIN from GS1 Digital Link or plain number - if "/01/" in product_id: - match = re.search(r"/01/(\d{8,14})", product_id) - if match: - gtin = match.group(1) - if not self._validate_gtin(gtin): - violations.append( - ( - "$.credentialSubject.product.id", - f"Invalid GTIN checksum in '{gtin}'", - ) - ) - elif product_id.isdigit() and len(product_id) in (8, 12, 13, 14): - if not self._validate_gtin(product_id): - violations.append( - ( - "$.credentialSubject.product.id", - f"Invalid GTIN checksum in '{product_id}'", - ) - ) +from dppvalidator.validators.rules.v0_6.base import ( + CircularityContentRule, + ConformityClaimRule, + GranularitySerialNumberRule, + GTINChecksumRule, + HazardousMaterialRule, + HSCodeRule, + MassFractionSumRule, + MaterialCodeRule, + OperationalScopeRule, + ValidityDateRule, +) - return violations +__all__ = [ + "CircularityContentRule", + "ConformityClaimRule", + "GTINChecksumRule", + "GranularitySerialNumberRule", + "HSCodeRule", + "HazardousMaterialRule", + "MassFractionSumRule", + "MaterialCodeRule", + "OperationalScopeRule", + "ValidityDateRule", +] diff --git a/src/dppvalidator/validators/rules/cirpass.py b/src/dppvalidator/validators/rules/cirpass.py index 441e43f..7fbdf10 100644 --- a/src/dppvalidator/validators/rules/cirpass.py +++ b/src/dppvalidator/validators/rules/cirpass.py @@ -1,306 +1,33 @@ -"""CIRPASS-2 semantic validation rules based on Competency Questions. +"""Backward-compatibility re-export of v0.6.x CIRPASS-2 CQ-based rules. -Source: Ontology Requirements Specification for an EU DPP Core Ontology Proposal -DOI: 10.5281/zenodo.14892665 +Phase 3b of docs/plans/UNTP_0.7.0_MIGRATION.md split semantic rules into +``rules/v0_6/`` and ``rules/v0_7/`` subpackages. This shim preserves the +import path used by existing tests and any third-party plugin +(``from dppvalidator.validators.rules.cirpass import CIRPASS_RULES``) — see +the public-API stability contract in §7.6 of the plan. -These rules implement validation checks derived from CIRPASS-2 Competency Questions -(CQs) which define functional requirements for the EU DPP Core Ontology. +Through the 0.4.x line this re-exports v0.6.x rules. Phase 9 will switch +the default to v0.7 and update this shim accordingly. """ from __future__ import annotations -import re -from typing import TYPE_CHECKING, Literal - -if TYPE_CHECKING: - from dppvalidator.models.passport import DigitalProductPassport - - -class CIRPASSMandatoryAttributesRule: - """CQ001: DPP must have mandatory attributes per ESPR. - - Based on CQ1: "What are the values of all or selected mandatory attributes - of a DPP required by ESPR and delegated acts?" - - Checks that essential DPP attributes are present. - """ - - rule_id: str = "CQ001" - description: str = "DPP must have mandatory ESPR attributes" - severity: Literal["error", "warning", "info"] = "error" - suggestion: str = "Add required attributes: issuer, validFrom, credentialSubject" - docs_url: str = "https://artiso-ai.github.io/dppvalidator/errors/CQ001" - - def check(self, passport: DigitalProductPassport) -> list[tuple[str, str]]: - """Check mandatory DPP attributes are present.""" - violations: list[tuple[str, str]] = [] - - # Check issuer (ESPR Annex III (g)) - if not passport.issuer: - violations.append(("$.issuer", "Missing issuer - required per ESPR Annex III (g)")) - - # Check validFrom (ESPR Art 9(2i)) - if not passport.valid_from: - violations.append(("$.validFrom", "Missing validFrom - required per ESPR Art 9(2i)")) - - # Check credentialSubject - if not passport.credential_subject: - violations.append( - ( - "$.credentialSubject", - "Missing credentialSubject - DPP has no product data", - ) - ) - return violations - - # Check product (core content) - if not passport.credential_subject.product: - violations.append( - ( - "$.credentialSubject.product", - "Missing product information - required per ESPR", - ) - ) - - return violations - - -class CIRPASSSubstancesOfConcernRule: - """CQ004: Substances of concern must have proper identification. - - Based on CQ4: "What are the names and numeric codes of substances of - concern present in the product?" - - Checks that substances of concern have CAS numbers or other identifiers. - """ - - rule_id: str = "CQ004" - description: str = "Substances of concern must have CAS numbers" - severity: Literal["error", "warning", "info"] = "error" - suggestion: str = "Add CAS number or EINECS number for each substance of concern" - docs_url: str = "https://artiso-ai.github.io/dppvalidator/errors/CQ004" - - # CAS number pattern: NNNNNN-NN-N (digits with hyphens) - CAS_PATTERN = re.compile(r"^\d{2,7}-\d{2}-\d$") - - def check(self, passport: DigitalProductPassport) -> list[tuple[str, str]]: - """Check substances of concern have proper identification.""" - violations: list[tuple[str, str]] = [] - - if not passport.credential_subject: - return violations - - materials = passport.credential_subject.materials_provenance - if not materials: - return violations - - for i, material in enumerate(materials): - # Check if material is flagged as hazardous but lacks identifier - if material.hazardous: - has_cas = False - # Check if name contains CAS-like pattern or has ID - if material.name and self.CAS_PATTERN.search(material.name): - has_cas = True - # Check material_type for code - if material.material_type and material.material_type.code: - code = material.material_type.code - if self.CAS_PATTERN.match(code) or code.startswith("EINECS"): - has_cas = True - - if not has_cas: - violations.append( - ( - f"$.credentialSubject.materialsProvenance[{i}]", - f"Hazardous material '{material.name}' missing CAS/EINECS " - "number per ESPR Art 7(5a)", - ) - ) - - return violations - - -class CIRPASSOperatorIdentifierRule: - """CQ011: Manufacturer must have unique operator identifier. - - Based on CQ11: "What is the manufacturer's unique operator identifier - of a product?" - - Checks that the issuer (manufacturer) has a proper identifier. - """ - - rule_id: str = "CQ011" - description: str = "Manufacturer must have unique operator identifier" - severity: Literal["error", "warning", "info"] = "error" - suggestion: str = "Add issuer.id with GLN, DUNS, or LEI identifier" - docs_url: str = "https://artiso-ai.github.io/dppvalidator/errors/CQ011" - - def check(self, passport: DigitalProductPassport) -> list[tuple[str, str]]: - """Check manufacturer has unique operator identifier.""" - violations: list[tuple[str, str]] = [] - - if not passport.issuer: - violations.append( - ( - "$.issuer", - "Missing issuer - cannot verify operator identifier", - ) - ) - return violations - - # Check issuer has an ID - if not passport.issuer.id: - violations.append( - ( - "$.issuer.id", - "Missing issuer.id - manufacturer requires unique operator " - "identifier per ESPR Annex III (g)", - ) - ) - - return violations - - -class CIRPASSValidityPeriodRule: - """CQ016: DPP must have validity period information. - - Based on CQ16: "What is the date the product was placed on the EU market - and what is the duration of validity period of the DPP?" - - Checks that DPP has market placement date and validity period. - """ - - rule_id: str = "CQ016" - description: str = "DPP must have validity period" - severity: Literal["error", "warning", "info"] = "warning" - suggestion: str = "Add validFrom and validUntil dates per ESPR Art 9(2i)" - docs_url: str = "https://artiso-ai.github.io/dppvalidator/errors/CQ016" - - def check(self, passport: DigitalProductPassport) -> list[tuple[str, str]]: - """Check DPP has validity period.""" - violations: list[tuple[str, str]] = [] - - if not passport.valid_from: - violations.append( - ( - "$.validFrom", - "Missing validFrom - market placement date required per ESPR Art 9(2i)", - ) - ) - - if not passport.valid_until: - violations.append( - ( - "$.validUntil", - "Missing validUntil - DPP validity duration recommended per ESPR Art 9(2i)", - ) - ) - - return violations - - -class CIRPASSWeightVolumeRule: - """CQ020: Product must declare weight and volume. - - Based on CQ20: "What are the weight and volume of the product and its - packaging, and the product-to-packaging ratio?" - - Checks that product has weight/volume declarations per ESPR Annex I(j). - """ - - rule_id: str = "CQ020" - description: str = "Product should declare weight and volume" - severity: Literal["error", "warning", "info"] = "warning" - suggestion: str = "Add product dimension (weight/volume) per ESPR Annex I(j)" - docs_url: str = "https://artiso-ai.github.io/dppvalidator/errors/CQ020" - - def check(self, passport: DigitalProductPassport) -> list[tuple[str, str]]: - """Check product has weight/volume declarations.""" - violations: list[tuple[str, str]] = [] - - if not passport.credential_subject: - return violations - - product = passport.credential_subject.product - if not product: - return violations - - # Check for dimensions data - has_dimension = False - if product.dimensions: - dim = product.dimensions - if dim.weight or dim.length or dim.width or dim.height or dim.volume: - has_dimension = True - - if not has_dimension: - violations.append( - ( - "$.credentialSubject.product.dimension", - "Missing weight/volume - product dimensions recommended per ESPR Annex I(j)", - ) - ) - - return violations - - -class CIRPASSGranularityConsistencyRule: - """CQ017: Granularity level must be consistent with identifiers. - - Based on CQ17 and Standardisation Request 5423: granularityLevel must - align with available identifiers (model/batch/item). - - Checks granularity level consistency with identifier presence. - """ - - rule_id: str = "CQ017" - description: str = "Granularity level must match identifier granularity" - severity: Literal["error", "warning", "info"] = "warning" - suggestion: str = "Ensure granularityLevel matches available identifiers" - docs_url: str = "https://artiso-ai.github.io/dppvalidator/errors/CQ017" - - def check(self, passport: DigitalProductPassport) -> list[tuple[str, str]]: - """Check granularity level consistency.""" - violations: list[tuple[str, str]] = [] - - if not passport.credential_subject: - return violations - - granularity = passport.credential_subject.granularity_level - product = passport.credential_subject.product - - if not granularity or not product: - return violations - - granularity_str = str(granularity).lower() - - # Item level requires serial number - if granularity_str == "item" and not product.serial_number: - violations.append( - ( - "$.credentialSubject.product.serialNumber", - "granularityLevel is 'item' but serialNumber is missing - " - "per SR5423 Annex II Part B 1.1(4)", - ) - ) - - # Batch level requires batch number - if granularity_str == "batch" and not product.batch_number: - violations.append( - ( - "$.credentialSubject.product.batchNumber", - "granularityLevel is 'batch' but batchNumber is missing - " - "per SR5423 Annex II Part B 1.1(3)", - ) - ) - - return violations - - -# List of all CIRPASS rules for easy registration -CIRPASS_RULES = [ - CIRPASSMandatoryAttributesRule(), - CIRPASSSubstancesOfConcernRule(), - CIRPASSOperatorIdentifierRule(), - CIRPASSValidityPeriodRule(), - CIRPASSWeightVolumeRule(), - CIRPASSGranularityConsistencyRule(), +from dppvalidator.validators.rules.v0_6.cirpass import ( + CIRPASS_RULES, + CIRPASSGranularityConsistencyRule, + CIRPASSMandatoryAttributesRule, + CIRPASSOperatorIdentifierRule, + CIRPASSSubstancesOfConcernRule, + CIRPASSValidityPeriodRule, + CIRPASSWeightVolumeRule, +) + +__all__ = [ + "CIRPASS_RULES", + "CIRPASSGranularityConsistencyRule", + "CIRPASSMandatoryAttributesRule", + "CIRPASSOperatorIdentifierRule", + "CIRPASSSubstancesOfConcernRule", + "CIRPASSValidityPeriodRule", + "CIRPASSWeightVolumeRule", ] diff --git a/src/dppvalidator/validators/rules/textile.py b/src/dppvalidator/validators/rules/textile.py index a227008..1ea5044 100644 --- a/src/dppvalidator/validators/rules/textile.py +++ b/src/dppvalidator/validators/rules/textile.py @@ -1,359 +1,37 @@ -"""CIRPASS-2 textile sector validation rules. +"""Backward-compatibility re-export of v0.6.x textile-sector rules. -Source: DPP Granularity Options for Textiles/Apparel -DOI: 10.5281/zenodo.17582219 +Phase 3b of docs/plans/UNTP_0.7.0_MIGRATION.md split semantic rules into +``rules/v0_6/`` and ``rules/v0_7/`` subpackages. This shim preserves the +import path used by existing tests and any third-party plugin +(``from dppvalidator.validators.rules.textile import TEXTILE_RULES``) — see +the public-API stability contract in §7.6 of the plan. -These rules implement textile-specific validation based on CIRPASS-2 -granularity analysis and JRC preparatory study environmental categories. +Through the 0.4.x line this re-exports v0.6.x rules. Phase 9 will switch +the default to v0.7 and update this shim accordingly. """ from __future__ import annotations -from enum import Enum -from typing import TYPE_CHECKING, Literal - -if TYPE_CHECKING: - from dppvalidator.models.passport import DigitalProductPassport - - -class TextileEnvironmentalCategory(str, Enum): - """Environmental impact categories for textiles per JRC preparatory study.""" - - WATER_CONSUMPTION = "water_consumption" - ENERGY_CONSUMPTION = "energy_consumption" - CHEMICAL_SUBSTANCES = "chemical_substances" - TEXTILE_WASTE = "textile_waste" - GHG_EMISSIONS = "ghg_emissions" - MICROPLASTIC_RELEASE = "microplastic_release" - POLLUTION = "pollution" # COD/NOx/SOx - - -# HS code chapters for textiles (50-63) -TEXTILE_HS_CHAPTERS = frozenset(str(i) for i in range(50, 64)) - -# Textile fiber material codes (UNECE Rec 46 subset) -TEXTILE_MATERIAL_CODES = frozenset( - [ - "COTTON", - "CO", - "WOOL", - "WO", - "SILK", - "SE", - "LINEN", - "LI", - "POLYESTER", - "PL", - "NYLON", - "PA", - "ACRYLIC", - "PC", - "VISCOSE", - "VI", - "ELASTANE", - "EL", - "MODAL", - "MD", - "LYOCELL", - "CLY", - "HEMP", - "HA", - "JUTE", - "JU", - "RAMIE", - "RA", - "CASHMERE", - "WS", - "ALPACA", - "WP", - "MOHAIR", - "WM", - "ANGORA", - "WA", - ] +from dppvalidator.validators.rules.v0_6.textile import ( + TEXTILE_RULES, + TextileCareInstructionsRule, + TextileDurabilityRule, + TextileEnvironmentalCategory, + TextileHSCodeRule, + TextileMaterialCompositionRule, + TextileMicroplasticRule, + get_textile_environmental_categories, + is_textile_product, ) - -class TextileHSCodeRule: - """TXT001: Product must have valid textile HS code. - - Validates that textile products have HS codes in chapters 50-63. - """ - - rule_id: str = "TXT001" - description: str = "Textile product must have valid HS code (chapters 50-63)" - severity: Literal["error", "warning", "info"] = "warning" - suggestion: str = "Add product category with HS code in chapters 50-63" - docs_url: str = "https://artiso-ai.github.io/dppvalidator/errors/TXT001" - - def check(self, passport: DigitalProductPassport) -> list[tuple[str, str]]: - """Check textile product has valid HS code.""" - violations: list[tuple[str, str]] = [] - - if not passport.credential_subject: - return violations - - product = passport.credential_subject.product - if not product: - return violations - - # Check product category for HS codes - if not product.product_category: - violations.append( - ( - "$.credentialSubject.product.productCategory", - "Textile product missing product category with HS code", - ) - ) - return violations - - has_textile_hs = False - for classification in product.product_category: - if classification.code: - code = classification.code.replace(".", "").replace(" ", "") - if len(code) >= 2 and code[:2] in TEXTILE_HS_CHAPTERS: - has_textile_hs = True - break - - if not has_textile_hs: - violations.append( - ( - "$.credentialSubject.product.productCategory", - "No textile HS code found (chapters 50-63 required)", - ) - ) - - return violations - - -class TextileMaterialCompositionRule: - """TXT002: Textile must declare material composition. - - Per ESPR requirements, textile products must declare fiber composition. - """ - - rule_id: str = "TXT002" - description: str = "Textile must declare material composition" - severity: Literal["error", "warning", "info"] = "error" - suggestion: str = "Add materialsProvenance with fiber types and mass fractions" - docs_url: str = "https://artiso-ai.github.io/dppvalidator/errors/TXT002" - - def check(self, passport: DigitalProductPassport) -> list[tuple[str, str]]: - """Check textile has material composition.""" - violations: list[tuple[str, str]] = [] - - if not passport.credential_subject: - return violations - - materials = passport.credential_subject.materials_provenance - if not materials: - violations.append( - ( - "$.credentialSubject.materialsProvenance", - "Textile product missing material composition declaration", - ) - ) - return violations - - # Check at least one material has mass fraction - has_fraction = False - for material in materials: - if material.mass_fraction is not None: - has_fraction = True - break - - if not has_fraction: - violations.append( - ( - "$.credentialSubject.materialsProvenance", - "Textile materials missing mass fraction (fiber %) declaration", - ) - ) - - return violations - - -class TextileMicroplasticRule: - """TXT003: Synthetic textiles should declare microplastic release. - - Per JRC preparatory study, synthetic fiber products should declare - microplastic release potential. - """ - - rule_id: str = "TXT003" - description: str = "Synthetic textiles should declare microplastic release" - severity: Literal["error", "warning", "info"] = "info" - suggestion: str = "Add microplastic release data for synthetic fiber products" - docs_url: str = "https://artiso-ai.github.io/dppvalidator/errors/TXT003" - - # Synthetic fiber codes that may release microplastics - SYNTHETIC_FIBERS = frozenset( - [ - "POLYESTER", - "PL", - "NYLON", - "PA", - "ACRYLIC", - "PC", - "ELASTANE", - "EL", - "POLYPROPYLENE", - "PP", - ] - ) - - def check(self, passport: DigitalProductPassport) -> list[tuple[str, str]]: - """Check synthetic textile declares microplastic release.""" - violations: list[tuple[str, str]] = [] - - if not passport.credential_subject: - return violations - - materials = passport.credential_subject.materials_provenance - if not materials: - return violations - - # Check if product contains synthetic fibers - has_synthetic = False - for material in materials: - if material.name: - name_upper = material.name.upper() - if any(fiber in name_upper for fiber in self.SYNTHETIC_FIBERS): - has_synthetic = True - break - if material.material_type and material.material_type.code: - code_upper = material.material_type.code.upper() - if code_upper in self.SYNTHETIC_FIBERS: - has_synthetic = True - break - - if not has_synthetic: - return violations - - # Check for environmental footprint data - scorecard = passport.credential_subject.circularity_scorecard - emissions = passport.credential_subject.emissions_scorecard - - # If synthetic but no environmental data, suggest microplastic info - if not scorecard and not emissions: - violations.append( - ( - "$.credentialSubject", - "Synthetic textile product - consider adding microplastic " - "release data per JRC preparatory study", - ) - ) - - return violations - - -class TextileDurabilityRule: - """TXT004: Textile products should have durability information. - - Per ESPR Annex I, durability is a key product parameter for textiles. - """ - - rule_id: str = "TXT004" - description: str = "Textile should declare durability information" - severity: Literal["error", "warning", "info"] = "info" - suggestion: str = "Add product characteristics with durability data" - docs_url: str = "https://artiso-ai.github.io/dppvalidator/errors/TXT004" - - def check(self, passport: DigitalProductPassport) -> list[tuple[str, str]]: - """Check textile has durability information.""" - violations: list[tuple[str, str]] = [] - - if not passport.credential_subject: - return violations - - product = passport.credential_subject.product - if not product: - return violations - - # Check for characteristics (where durability info would be) - if not product.characteristics: - violations.append( - ( - "$.credentialSubject.product.characteristics", - "Textile product - consider adding durability characteristics " - "per ESPR Annex I requirements", - ) - ) - - return violations - - -class TextileCareInstructionsRule: - """TXT005: Textile products should have care instructions. - - Care instructions are required for consumer textiles per EU labeling - requirements and extend product lifetime. - """ - - rule_id: str = "TXT005" - description: str = "Textile should have care instructions" - severity: Literal["error", "warning", "info"] = "info" - suggestion: str = "Add furtherInformation link to care instructions" - docs_url: str = "https://artiso-ai.github.io/dppvalidator/errors/TXT005" - - def check(self, passport: DigitalProductPassport) -> list[tuple[str, str]]: - """Check textile has care instructions.""" - violations: list[tuple[str, str]] = [] - - if not passport.credential_subject: - return violations - - product = passport.credential_subject.product - if not product: - return violations - - # Check for further information links - if not product.further_information: - violations.append( - ( - "$.credentialSubject.product.furtherInformation", - "Textile product - consider adding care instructions link", - ) - ) - - return violations - - -# List of all textile rules for easy registration -TEXTILE_RULES = [ - TextileHSCodeRule(), - TextileMaterialCompositionRule(), - TextileMicroplasticRule(), - TextileDurabilityRule(), - TextileCareInstructionsRule(), +__all__ = [ + "TEXTILE_RULES", + "TextileCareInstructionsRule", + "TextileDurabilityRule", + "TextileEnvironmentalCategory", + "TextileHSCodeRule", + "TextileMaterialCompositionRule", + "TextileMicroplasticRule", + "get_textile_environmental_categories", + "is_textile_product", ] - - -def is_textile_product(passport: DigitalProductPassport) -> bool: - """Check if a passport represents a textile product. - - Args: - passport: Digital Product Passport to check - - Returns: - True if product has textile HS code (chapters 50-63) - """ - if not passport.credential_subject: - return False - - product = passport.credential_subject.product - if not product or not product.product_category: - return False - - for classification in product.product_category: - if classification.code: - code = classification.code.replace(".", "").replace(" ", "") - if len(code) >= 2 and code[:2] in TEXTILE_HS_CHAPTERS: - return True - - return False - - -def get_textile_environmental_categories() -> list[str]: - """Get list of textile environmental impact categories.""" - return [cat.value for cat in TextileEnvironmentalCategory] diff --git a/src/dppvalidator/validators/rules/v0_6/__init__.py b/src/dppvalidator/validators/rules/v0_6/__init__.py new file mode 100644 index 0000000..7e84f01 --- /dev/null +++ b/src/dppvalidator/validators/rules/v0_6/__init__.py @@ -0,0 +1,102 @@ +"""Semantic validation rules for UNTP v0.6.x. + +Phase 3b of docs/plans/UNTP_0.7.0_MIGRATION.md split the rule modules into +version-namespaced subpackages so the v0.6.x rules and v0.7.0 rules can +coexist. This subpackage holds the legacy v0.6.x rules, preserved verbatim. + +Each rule walks the v0.6 model shape (``passport.credential_subject.product.*``, +``passport.credential_subject.materials_provenance``, scorecard classes, etc.). +Rules that no longer apply to v0.7.0 — like +:class:`OperationalScopeRule`, which inspects +``EmissionsPerformance.operational_scope`` (a class that doesn't exist in +v0.7.0) — are kept here but excluded from the v0.7.0 rule set in +``rules/v0_7/__init__.py``. +""" + +from __future__ import annotations + +from dppvalidator.validators.rules.v0_6.base import ( + CircularityContentRule, + ConformityClaimRule, + GranularitySerialNumberRule, + GTINChecksumRule, + HazardousMaterialRule, + HSCodeRule, + MassFractionSumRule, + MaterialCodeRule, + OperationalScopeRule, + ValidityDateRule, +) +from dppvalidator.validators.rules.v0_6.cirpass import ( + CIRPASS_RULES, + CIRPASSGranularityConsistencyRule, + CIRPASSMandatoryAttributesRule, + CIRPASSOperatorIdentifierRule, + CIRPASSSubstancesOfConcernRule, + CIRPASSValidityPeriodRule, + CIRPASSWeightVolumeRule, +) +from dppvalidator.validators.rules.v0_6.textile import ( + TEXTILE_RULES, + TextileCareInstructionsRule, + TextileDurabilityRule, + TextileEnvironmentalCategory, + TextileHSCodeRule, + TextileMaterialCompositionRule, + TextileMicroplasticRule, + get_textile_environmental_categories, + is_textile_product, +) + +# The default v0.6.x rule list — includes everything in the base file plus +# CIRPASS-2 CQ-based rules. Textile rules are *not* part of the default set +# (they're sector-specific and opt-in). This list is what +# ``ALL_RULES_BY_VERSION["0.6.x"]`` resolves to. +ALL_RULES_V0_6 = [ + # Base UNTP rules + MassFractionSumRule(), + ValidityDateRule(), + HazardousMaterialRule(), + CircularityContentRule(), + ConformityClaimRule(), + GranularitySerialNumberRule(), + OperationalScopeRule(), + MaterialCodeRule(), + HSCodeRule(), + GTINChecksumRule(), + # CIRPASS-2 rules (CQ-based) + *CIRPASS_RULES, +] + +__all__ = [ + "ALL_RULES_V0_6", + # Base rules + "CircularityContentRule", + "ConformityClaimRule", + "GTINChecksumRule", + "GranularitySerialNumberRule", + "HSCodeRule", + "HazardousMaterialRule", + "MassFractionSumRule", + "MaterialCodeRule", + "OperationalScopeRule", + "ValidityDateRule", + # CIRPASS rules + "CIRPASS_RULES", + "CIRPASSGranularityConsistencyRule", + "CIRPASSMandatoryAttributesRule", + "CIRPASSOperatorIdentifierRule", + "CIRPASSSubstancesOfConcernRule", + "CIRPASSValidityPeriodRule", + "CIRPASSWeightVolumeRule", + # Textile sector rules + "TEXTILE_RULES", + "TextileCareInstructionsRule", + "TextileDurabilityRule", + "TextileEnvironmentalCategory", + "TextileHSCodeRule", + "TextileMaterialCompositionRule", + "TextileMicroplasticRule", + "get_textile_environmental_categories", + "is_textile_product", +] diff --git a/src/dppvalidator/validators/rules/v0_6/base.py b/src/dppvalidator/validators/rules/v0_6/base.py new file mode 100644 index 0000000..b142e6c --- /dev/null +++ b/src/dppvalidator/validators/rules/v0_6/base.py @@ -0,0 +1,434 @@ +"""Semantic validation rule implementations.""" + +from __future__ import annotations + +import re +from collections.abc import Callable +from typing import TYPE_CHECKING, Literal + +from dppvalidator.vocabularies.code_lists import ( + is_hs_scheme as _is_hs_scheme, +) +from dppvalidator.vocabularies.code_lists import ( + is_unece_rec46_scheme as _is_unece_rec46_scheme, +) +from dppvalidator.vocabularies.code_lists import ( + is_valid_hs_code as _is_valid_hs_code, +) +from dppvalidator.vocabularies.code_lists import ( + is_valid_material_code as _is_valid_material_code, +) +from dppvalidator.vocabularies.code_lists import ( + validate_gtin as _validate_gtin, +) + +if TYPE_CHECKING: + from dppvalidator.models.passport import DigitalProductPassport + + +class MassFractionSumRule: + """SEM001: Material mass fractions should sum to 1.0. + + Per UNTP spec, partial declarations (sum < 1.0) are valid but should + be flagged as a warning. Only sum > 1.0 is physically impossible. + """ + + rule_id: str = "SEM001" + description: str = "Material mass fractions should sum to 1.0" + severity: Literal["error", "warning", "info"] = "warning" + suggestion: str = "Adjust material mass fractions to sum to 1.0 (100%)" + docs_url: str = "https://artiso-ai.github.io/dppvalidator/errors/SEM001" + + def check(self, passport: DigitalProductPassport) -> list[tuple[str, str]]: + """Check mass fraction sum.""" + violations: list[tuple[str, str]] = [] + + if not passport.credential_subject: + return violations + + materials = passport.credential_subject.materials_provenance + if not materials: + return violations + + fractions = [m.mass_fraction for m in materials if m.mass_fraction is not None] + if fractions: + total = sum(fractions) + # Only flag if significantly different from 1.0 + # Partial declarations (< 1.0) are valid per UNTP spec + if abs(total - 1.0) > 0.01: + violations.append( + ( + "$.credentialSubject.materialsProvenance", + f"Mass fractions sum to {total:.3f}, expected 1.0 (partial declaration)", + ) + ) + + return violations + + +class ValidityDateRule: + """SEM002: validFrom must be before validUntil.""" + + rule_id: str = "SEM002" + description: str = "validFrom must be before validUntil" + severity: Literal["error", "warning", "info"] = "error" + suggestion: str = "Ensure validFrom is before validUntil" + docs_url: str = "https://artiso-ai.github.io/dppvalidator/errors/SEM002" + + def check(self, passport: DigitalProductPassport) -> list[tuple[str, str]]: + """Check validity date ordering.""" + violations: list[tuple[str, str]] = [] + + if ( + passport.valid_from + and passport.valid_until + and passport.valid_from >= passport.valid_until + ): + violations.append( + ( + "$.validFrom", + f"validFrom ({passport.valid_from}) must be before validUntil ({passport.valid_until})", + ) + ) + + return violations + + +class HazardousMaterialRule: + """SEM003: hazardous=true requires materialSafetyInformation.""" + + rule_id: str = "SEM003" + description: str = "Hazardous materials require safety information" + severity: Literal["error", "warning", "info"] = "error" + suggestion: str = "Add materialSafetyInformation for hazardous materials" + docs_url: str = "https://artiso-ai.github.io/dppvalidator/errors/SEM003" + + def check(self, passport: DigitalProductPassport) -> list[tuple[str, str]]: + """Check hazardous material safety info.""" + violations: list[tuple[str, str]] = [] + + if not passport.credential_subject: + return violations + + materials = passport.credential_subject.materials_provenance + if not materials: + return violations + + for i, material in enumerate(materials): + if material.hazardous and not material.material_safety_information: + violations.append( + ( + f"$.credentialSubject.materialsProvenance[{i}]", + f"Material '{material.name}' is hazardous but missing materialSafetyInformation", + ) + ) + + return violations + + +class CircularityContentRule: + """SEM004: recycledContent should not exceed recyclableContent.""" + + rule_id: str = "SEM004" + description: str = "recycledContent should not exceed recyclableContent" + severity: Literal["error", "warning", "info"] = "warning" + suggestion: str = "recycledContent cannot exceed recyclableContent" + docs_url: str = "https://artiso-ai.github.io/dppvalidator/errors/SEM004" + + def check(self, passport: DigitalProductPassport) -> list[tuple[str, str]]: + """Check circularity content consistency.""" + violations: list[tuple[str, str]] = [] + + if not passport.credential_subject: + return violations + + scorecard = passport.credential_subject.circularity_scorecard + if not scorecard: + return violations + + recycled = scorecard.recycled_content + recyclable = scorecard.recyclable_content + + if recycled is not None and recyclable is not None and recycled > recyclable: + violations.append( + ( + "$.credentialSubject.circularityScorecard", + f"recycledContent ({recycled}) exceeds recyclableContent ({recyclable})", + ) + ) + + return violations + + +class ConformityClaimRule: + """SEM005: At least one conformityClaim is recommended.""" + + rule_id: str = "SEM005" + description: str = "At least one conformityClaim is recommended" + severity: Literal["error", "warning", "info"] = "info" + suggestion: str = "Add conformity claims for sustainability or compliance" + docs_url: str = "https://artiso-ai.github.io/dppvalidator/errors/SEM005" + + def check(self, passport: DigitalProductPassport) -> list[tuple[str, str]]: + """Check for conformity claims.""" + violations: list[tuple[str, str]] = [] + + if not passport.credential_subject: + return violations + + claims = passport.credential_subject.conformity_claim + if not claims: + violations.append( + ( + "$.credentialSubject.conformityClaim", + "No conformity claims present. Consider adding sustainability or compliance claims.", + ) + ) + + return violations + + +class GranularitySerialNumberRule: + """SEM006: granularityLevel=item requires serialNumber.""" + + rule_id: str = "SEM006" + description: str = "Item-level passports require serial numbers" + severity: Literal["error", "warning", "info"] = "warning" + suggestion: str = "Add serialNumber for item-level granularity passports" + docs_url: str = "https://artiso-ai.github.io/dppvalidator/errors/SEM006" + + def check(self, passport: DigitalProductPassport) -> list[tuple[str, str]]: + """Check serial number for item-level passports.""" + violations: list[tuple[str, str]] = [] + + if not passport.credential_subject: + return violations + + granularity = passport.credential_subject.granularity_level + product = passport.credential_subject.product + + if ( + granularity + and str(granularity) == "item" + and (not product or not product.serial_number) + ): + violations.append( + ( + "$.credentialSubject.product.serialNumber", + "granularityLevel is 'item' but serialNumber is missing", + ) + ) + + return violations + + +class OperationalScopeRule: + """SEM007: carbonFootprint should have operationalScope defined.""" + + rule_id: str = "SEM007" + description: str = "Emissions data should specify operational scope" + severity: Literal["error", "warning", "info"] = "warning" + suggestion: str = "Specify operationalScope with carbonFootprint data" + docs_url: str = "https://artiso-ai.github.io/dppvalidator/errors/SEM007" + + def check(self, passport: DigitalProductPassport) -> list[tuple[str, str]]: + """Check operational scope for emissions data.""" + violations: list[tuple[str, str]] = [] + + if not passport.credential_subject: + return violations + + scorecard = passport.credential_subject.emissions_scorecard + if not scorecard: + return violations + + if scorecard.carbon_footprint and not scorecard.operational_scope: + violations.append( + ( + "$.credentialSubject.emissionsScorecard.operationalScope", + "carbonFootprint is specified but operationalScope is missing", + ) + ) + + return violations + + +class MaterialCodeRule: + """VOC003: Material code must be valid per UNECE Rec 46. + + Validates material codes in materialsProvenance against the + UNECE Recommendation 46 material classification codes. + + The rule is **scheme-aware**: when ``materialType.schemeID`` + declares a non-Rec46 classification (e.g. UN CPC), the rule + skips silently rather than producing false positives. When + ``schemeID`` is missing, it falls back to the pre-fix + best-effort behaviour for back-compat with legacy v0.6 fixtures. + See :func:`dppvalidator.vocabularies.code_lists.is_unece_rec46_scheme`. + """ + + rule_id: str = "VOC003" + description: str = "Material code must be in UNECE Rec 46" + severity: Literal["error", "warning", "info"] = "warning" + suggestion: str = "Use a valid UNECE Rec 46 material code" + docs_url: str = "https://artiso-ai.github.io/dppvalidator/errors/VOC003" + + def __init__( + self, + material_validator: Callable[[str], bool] | None = None, + ) -> None: + """Initialize with optional custom validator.""" + self._is_valid_material_code = material_validator or _is_valid_material_code + + def check(self, passport: DigitalProductPassport) -> list[tuple[str, str]]: + """Check material codes against UNECE Rec 46.""" + violations: list[tuple[str, str]] = [] + + if not passport.credential_subject: + return violations + + materials = passport.credential_subject.materials_provenance + if not materials: + return violations + + for i, material in enumerate(materials): + mt = material.material_type + if not mt or not mt.code: + continue + code = mt.code + # v0.6 ``Classification`` exposes the scheme via the + # ``scheme_id`` attribute (alias of ``schemeID`` on the + # wire — see ``models/v0_6/primitives.py``). + scheme_id = getattr(mt, "scheme_id", None) + if scheme_id and not _is_unece_rec46_scheme(scheme_id): + # Code claims a different scheme — we have no opinion. + continue + if not self._is_valid_material_code(code): + violations.append( + ( + f"$.credentialSubject.materialsProvenance[{i}].materialType.code", + f"Invalid material code '{code}' - not found in UNECE Rec 46", + ) + ) + + return violations + + +class HSCodeRule: + """VOC004: HS code must be valid for product category. + + Validates HS codes in product information against the + Harmonized System textile chapters (50-63). + + The rule is **scheme-aware**: when ``classification.schemeID`` + declares a non-HS classification, the rule skips silently. When + ``schemeID`` is missing, it falls back to the + length-and-digit heuristic that matches the pre-fix behaviour. + See :func:`dppvalidator.vocabularies.code_lists.is_hs_scheme`. + """ + + rule_id: str = "VOC004" + description: str = "HS code must be valid for product category" + severity: Literal["error", "warning", "info"] = "warning" + suggestion: str = "Use a valid HS code for textiles (chapters 50-63)" + docs_url: str = "https://artiso-ai.github.io/dppvalidator/errors/VOC004" + + def __init__( + self, + hs_validator: Callable[[str], bool] | None = None, + ) -> None: + """Initialize with optional custom validator.""" + self._is_valid_hs_code = hs_validator or _is_valid_hs_code + + def check(self, passport: DigitalProductPassport) -> list[tuple[str, str]]: + """Check HS codes for validity.""" + violations: list[tuple[str, str]] = [] + + if not passport.credential_subject: + return violations + + product = passport.credential_subject.product + if not product: + return violations + + # Check product category classifications for HS codes + if product.product_category: + for i, classification in enumerate(product.product_category): + code = classification.code if classification.code else "" + scheme_id = getattr(classification, "scheme_id", None) + # Scheme-aware gate: when schemeID is set, only fire + # for HS schemes. When it isn't, fall back to the + # length / digit heuristic (4+ digits looks HS-shaped). + if scheme_id is not None: + if not _is_hs_scheme(scheme_id): + continue + should_check = bool(code) + else: + should_check = code.isdigit() and len(code) >= 4 + if should_check and not self._is_valid_hs_code(code): + violations.append( + ( + f"$.credentialSubject.product.productCategory[{i}].code", + f"Invalid HS code '{code}' - not found in textile chapters (50-63)", + ) + ) + + return violations + + +class GTINChecksumRule: + """VOC005: GTIN must have valid check digit. + + Validates GTIN (Global Trade Item Number) checksums in product + identifiers using the GS1 check digit algorithm. + """ + + rule_id: str = "VOC005" + description: str = "GTIN must have valid check digit" + severity: Literal["error", "warning", "info"] = "error" + suggestion: str = "Verify the GTIN check digit using GS1 algorithm" + docs_url: str = "https://artiso-ai.github.io/dppvalidator/errors/VOC005" + + def __init__( + self, + gtin_validator: Callable[[str], bool] | None = None, + ) -> None: + """Initialize with optional custom validator.""" + self._validate_gtin = gtin_validator or _validate_gtin + + def check(self, passport: DigitalProductPassport) -> list[tuple[str, str]]: + """Check GTIN checksums.""" + violations: list[tuple[str, str]] = [] + + if not passport.credential_subject: + return violations + + product = passport.credential_subject.product + if not product: + return violations + + # Check product ID if it looks like a GTIN + if product.id: + product_id = product.id + # Extract GTIN from GS1 Digital Link or plain number + if "/01/" in product_id: + match = re.search(r"/01/(\d{8,14})", product_id) + if match: + gtin = match.group(1) + if not self._validate_gtin(gtin): + violations.append( + ( + "$.credentialSubject.product.id", + f"Invalid GTIN checksum in '{gtin}'", + ) + ) + elif product_id.isdigit() and len(product_id) in (8, 12, 13, 14): + if not self._validate_gtin(product_id): + violations.append( + ( + "$.credentialSubject.product.id", + f"Invalid GTIN checksum in '{product_id}'", + ) + ) + + return violations diff --git a/src/dppvalidator/validators/rules/v0_6/cirpass.py b/src/dppvalidator/validators/rules/v0_6/cirpass.py new file mode 100644 index 0000000..441e43f --- /dev/null +++ b/src/dppvalidator/validators/rules/v0_6/cirpass.py @@ -0,0 +1,306 @@ +"""CIRPASS-2 semantic validation rules based on Competency Questions. + +Source: Ontology Requirements Specification for an EU DPP Core Ontology Proposal +DOI: 10.5281/zenodo.14892665 + +These rules implement validation checks derived from CIRPASS-2 Competency Questions +(CQs) which define functional requirements for the EU DPP Core Ontology. +""" + +from __future__ import annotations + +import re +from typing import TYPE_CHECKING, Literal + +if TYPE_CHECKING: + from dppvalidator.models.passport import DigitalProductPassport + + +class CIRPASSMandatoryAttributesRule: + """CQ001: DPP must have mandatory attributes per ESPR. + + Based on CQ1: "What are the values of all or selected mandatory attributes + of a DPP required by ESPR and delegated acts?" + + Checks that essential DPP attributes are present. + """ + + rule_id: str = "CQ001" + description: str = "DPP must have mandatory ESPR attributes" + severity: Literal["error", "warning", "info"] = "error" + suggestion: str = "Add required attributes: issuer, validFrom, credentialSubject" + docs_url: str = "https://artiso-ai.github.io/dppvalidator/errors/CQ001" + + def check(self, passport: DigitalProductPassport) -> list[tuple[str, str]]: + """Check mandatory DPP attributes are present.""" + violations: list[tuple[str, str]] = [] + + # Check issuer (ESPR Annex III (g)) + if not passport.issuer: + violations.append(("$.issuer", "Missing issuer - required per ESPR Annex III (g)")) + + # Check validFrom (ESPR Art 9(2i)) + if not passport.valid_from: + violations.append(("$.validFrom", "Missing validFrom - required per ESPR Art 9(2i)")) + + # Check credentialSubject + if not passport.credential_subject: + violations.append( + ( + "$.credentialSubject", + "Missing credentialSubject - DPP has no product data", + ) + ) + return violations + + # Check product (core content) + if not passport.credential_subject.product: + violations.append( + ( + "$.credentialSubject.product", + "Missing product information - required per ESPR", + ) + ) + + return violations + + +class CIRPASSSubstancesOfConcernRule: + """CQ004: Substances of concern must have proper identification. + + Based on CQ4: "What are the names and numeric codes of substances of + concern present in the product?" + + Checks that substances of concern have CAS numbers or other identifiers. + """ + + rule_id: str = "CQ004" + description: str = "Substances of concern must have CAS numbers" + severity: Literal["error", "warning", "info"] = "error" + suggestion: str = "Add CAS number or EINECS number for each substance of concern" + docs_url: str = "https://artiso-ai.github.io/dppvalidator/errors/CQ004" + + # CAS number pattern: NNNNNN-NN-N (digits with hyphens) + CAS_PATTERN = re.compile(r"^\d{2,7}-\d{2}-\d$") + + def check(self, passport: DigitalProductPassport) -> list[tuple[str, str]]: + """Check substances of concern have proper identification.""" + violations: list[tuple[str, str]] = [] + + if not passport.credential_subject: + return violations + + materials = passport.credential_subject.materials_provenance + if not materials: + return violations + + for i, material in enumerate(materials): + # Check if material is flagged as hazardous but lacks identifier + if material.hazardous: + has_cas = False + # Check if name contains CAS-like pattern or has ID + if material.name and self.CAS_PATTERN.search(material.name): + has_cas = True + # Check material_type for code + if material.material_type and material.material_type.code: + code = material.material_type.code + if self.CAS_PATTERN.match(code) or code.startswith("EINECS"): + has_cas = True + + if not has_cas: + violations.append( + ( + f"$.credentialSubject.materialsProvenance[{i}]", + f"Hazardous material '{material.name}' missing CAS/EINECS " + "number per ESPR Art 7(5a)", + ) + ) + + return violations + + +class CIRPASSOperatorIdentifierRule: + """CQ011: Manufacturer must have unique operator identifier. + + Based on CQ11: "What is the manufacturer's unique operator identifier + of a product?" + + Checks that the issuer (manufacturer) has a proper identifier. + """ + + rule_id: str = "CQ011" + description: str = "Manufacturer must have unique operator identifier" + severity: Literal["error", "warning", "info"] = "error" + suggestion: str = "Add issuer.id with GLN, DUNS, or LEI identifier" + docs_url: str = "https://artiso-ai.github.io/dppvalidator/errors/CQ011" + + def check(self, passport: DigitalProductPassport) -> list[tuple[str, str]]: + """Check manufacturer has unique operator identifier.""" + violations: list[tuple[str, str]] = [] + + if not passport.issuer: + violations.append( + ( + "$.issuer", + "Missing issuer - cannot verify operator identifier", + ) + ) + return violations + + # Check issuer has an ID + if not passport.issuer.id: + violations.append( + ( + "$.issuer.id", + "Missing issuer.id - manufacturer requires unique operator " + "identifier per ESPR Annex III (g)", + ) + ) + + return violations + + +class CIRPASSValidityPeriodRule: + """CQ016: DPP must have validity period information. + + Based on CQ16: "What is the date the product was placed on the EU market + and what is the duration of validity period of the DPP?" + + Checks that DPP has market placement date and validity period. + """ + + rule_id: str = "CQ016" + description: str = "DPP must have validity period" + severity: Literal["error", "warning", "info"] = "warning" + suggestion: str = "Add validFrom and validUntil dates per ESPR Art 9(2i)" + docs_url: str = "https://artiso-ai.github.io/dppvalidator/errors/CQ016" + + def check(self, passport: DigitalProductPassport) -> list[tuple[str, str]]: + """Check DPP has validity period.""" + violations: list[tuple[str, str]] = [] + + if not passport.valid_from: + violations.append( + ( + "$.validFrom", + "Missing validFrom - market placement date required per ESPR Art 9(2i)", + ) + ) + + if not passport.valid_until: + violations.append( + ( + "$.validUntil", + "Missing validUntil - DPP validity duration recommended per ESPR Art 9(2i)", + ) + ) + + return violations + + +class CIRPASSWeightVolumeRule: + """CQ020: Product must declare weight and volume. + + Based on CQ20: "What are the weight and volume of the product and its + packaging, and the product-to-packaging ratio?" + + Checks that product has weight/volume declarations per ESPR Annex I(j). + """ + + rule_id: str = "CQ020" + description: str = "Product should declare weight and volume" + severity: Literal["error", "warning", "info"] = "warning" + suggestion: str = "Add product dimension (weight/volume) per ESPR Annex I(j)" + docs_url: str = "https://artiso-ai.github.io/dppvalidator/errors/CQ020" + + def check(self, passport: DigitalProductPassport) -> list[tuple[str, str]]: + """Check product has weight/volume declarations.""" + violations: list[tuple[str, str]] = [] + + if not passport.credential_subject: + return violations + + product = passport.credential_subject.product + if not product: + return violations + + # Check for dimensions data + has_dimension = False + if product.dimensions: + dim = product.dimensions + if dim.weight or dim.length or dim.width or dim.height or dim.volume: + has_dimension = True + + if not has_dimension: + violations.append( + ( + "$.credentialSubject.product.dimension", + "Missing weight/volume - product dimensions recommended per ESPR Annex I(j)", + ) + ) + + return violations + + +class CIRPASSGranularityConsistencyRule: + """CQ017: Granularity level must be consistent with identifiers. + + Based on CQ17 and Standardisation Request 5423: granularityLevel must + align with available identifiers (model/batch/item). + + Checks granularity level consistency with identifier presence. + """ + + rule_id: str = "CQ017" + description: str = "Granularity level must match identifier granularity" + severity: Literal["error", "warning", "info"] = "warning" + suggestion: str = "Ensure granularityLevel matches available identifiers" + docs_url: str = "https://artiso-ai.github.io/dppvalidator/errors/CQ017" + + def check(self, passport: DigitalProductPassport) -> list[tuple[str, str]]: + """Check granularity level consistency.""" + violations: list[tuple[str, str]] = [] + + if not passport.credential_subject: + return violations + + granularity = passport.credential_subject.granularity_level + product = passport.credential_subject.product + + if not granularity or not product: + return violations + + granularity_str = str(granularity).lower() + + # Item level requires serial number + if granularity_str == "item" and not product.serial_number: + violations.append( + ( + "$.credentialSubject.product.serialNumber", + "granularityLevel is 'item' but serialNumber is missing - " + "per SR5423 Annex II Part B 1.1(4)", + ) + ) + + # Batch level requires batch number + if granularity_str == "batch" and not product.batch_number: + violations.append( + ( + "$.credentialSubject.product.batchNumber", + "granularityLevel is 'batch' but batchNumber is missing - " + "per SR5423 Annex II Part B 1.1(3)", + ) + ) + + return violations + + +# List of all CIRPASS rules for easy registration +CIRPASS_RULES = [ + CIRPASSMandatoryAttributesRule(), + CIRPASSSubstancesOfConcernRule(), + CIRPASSOperatorIdentifierRule(), + CIRPASSValidityPeriodRule(), + CIRPASSWeightVolumeRule(), + CIRPASSGranularityConsistencyRule(), +] diff --git a/src/dppvalidator/validators/rules/v0_6/textile.py b/src/dppvalidator/validators/rules/v0_6/textile.py new file mode 100644 index 0000000..a227008 --- /dev/null +++ b/src/dppvalidator/validators/rules/v0_6/textile.py @@ -0,0 +1,359 @@ +"""CIRPASS-2 textile sector validation rules. + +Source: DPP Granularity Options for Textiles/Apparel +DOI: 10.5281/zenodo.17582219 + +These rules implement textile-specific validation based on CIRPASS-2 +granularity analysis and JRC preparatory study environmental categories. +""" + +from __future__ import annotations + +from enum import Enum +from typing import TYPE_CHECKING, Literal + +if TYPE_CHECKING: + from dppvalidator.models.passport import DigitalProductPassport + + +class TextileEnvironmentalCategory(str, Enum): + """Environmental impact categories for textiles per JRC preparatory study.""" + + WATER_CONSUMPTION = "water_consumption" + ENERGY_CONSUMPTION = "energy_consumption" + CHEMICAL_SUBSTANCES = "chemical_substances" + TEXTILE_WASTE = "textile_waste" + GHG_EMISSIONS = "ghg_emissions" + MICROPLASTIC_RELEASE = "microplastic_release" + POLLUTION = "pollution" # COD/NOx/SOx + + +# HS code chapters for textiles (50-63) +TEXTILE_HS_CHAPTERS = frozenset(str(i) for i in range(50, 64)) + +# Textile fiber material codes (UNECE Rec 46 subset) +TEXTILE_MATERIAL_CODES = frozenset( + [ + "COTTON", + "CO", + "WOOL", + "WO", + "SILK", + "SE", + "LINEN", + "LI", + "POLYESTER", + "PL", + "NYLON", + "PA", + "ACRYLIC", + "PC", + "VISCOSE", + "VI", + "ELASTANE", + "EL", + "MODAL", + "MD", + "LYOCELL", + "CLY", + "HEMP", + "HA", + "JUTE", + "JU", + "RAMIE", + "RA", + "CASHMERE", + "WS", + "ALPACA", + "WP", + "MOHAIR", + "WM", + "ANGORA", + "WA", + ] +) + + +class TextileHSCodeRule: + """TXT001: Product must have valid textile HS code. + + Validates that textile products have HS codes in chapters 50-63. + """ + + rule_id: str = "TXT001" + description: str = "Textile product must have valid HS code (chapters 50-63)" + severity: Literal["error", "warning", "info"] = "warning" + suggestion: str = "Add product category with HS code in chapters 50-63" + docs_url: str = "https://artiso-ai.github.io/dppvalidator/errors/TXT001" + + def check(self, passport: DigitalProductPassport) -> list[tuple[str, str]]: + """Check textile product has valid HS code.""" + violations: list[tuple[str, str]] = [] + + if not passport.credential_subject: + return violations + + product = passport.credential_subject.product + if not product: + return violations + + # Check product category for HS codes + if not product.product_category: + violations.append( + ( + "$.credentialSubject.product.productCategory", + "Textile product missing product category with HS code", + ) + ) + return violations + + has_textile_hs = False + for classification in product.product_category: + if classification.code: + code = classification.code.replace(".", "").replace(" ", "") + if len(code) >= 2 and code[:2] in TEXTILE_HS_CHAPTERS: + has_textile_hs = True + break + + if not has_textile_hs: + violations.append( + ( + "$.credentialSubject.product.productCategory", + "No textile HS code found (chapters 50-63 required)", + ) + ) + + return violations + + +class TextileMaterialCompositionRule: + """TXT002: Textile must declare material composition. + + Per ESPR requirements, textile products must declare fiber composition. + """ + + rule_id: str = "TXT002" + description: str = "Textile must declare material composition" + severity: Literal["error", "warning", "info"] = "error" + suggestion: str = "Add materialsProvenance with fiber types and mass fractions" + docs_url: str = "https://artiso-ai.github.io/dppvalidator/errors/TXT002" + + def check(self, passport: DigitalProductPassport) -> list[tuple[str, str]]: + """Check textile has material composition.""" + violations: list[tuple[str, str]] = [] + + if not passport.credential_subject: + return violations + + materials = passport.credential_subject.materials_provenance + if not materials: + violations.append( + ( + "$.credentialSubject.materialsProvenance", + "Textile product missing material composition declaration", + ) + ) + return violations + + # Check at least one material has mass fraction + has_fraction = False + for material in materials: + if material.mass_fraction is not None: + has_fraction = True + break + + if not has_fraction: + violations.append( + ( + "$.credentialSubject.materialsProvenance", + "Textile materials missing mass fraction (fiber %) declaration", + ) + ) + + return violations + + +class TextileMicroplasticRule: + """TXT003: Synthetic textiles should declare microplastic release. + + Per JRC preparatory study, synthetic fiber products should declare + microplastic release potential. + """ + + rule_id: str = "TXT003" + description: str = "Synthetic textiles should declare microplastic release" + severity: Literal["error", "warning", "info"] = "info" + suggestion: str = "Add microplastic release data for synthetic fiber products" + docs_url: str = "https://artiso-ai.github.io/dppvalidator/errors/TXT003" + + # Synthetic fiber codes that may release microplastics + SYNTHETIC_FIBERS = frozenset( + [ + "POLYESTER", + "PL", + "NYLON", + "PA", + "ACRYLIC", + "PC", + "ELASTANE", + "EL", + "POLYPROPYLENE", + "PP", + ] + ) + + def check(self, passport: DigitalProductPassport) -> list[tuple[str, str]]: + """Check synthetic textile declares microplastic release.""" + violations: list[tuple[str, str]] = [] + + if not passport.credential_subject: + return violations + + materials = passport.credential_subject.materials_provenance + if not materials: + return violations + + # Check if product contains synthetic fibers + has_synthetic = False + for material in materials: + if material.name: + name_upper = material.name.upper() + if any(fiber in name_upper for fiber in self.SYNTHETIC_FIBERS): + has_synthetic = True + break + if material.material_type and material.material_type.code: + code_upper = material.material_type.code.upper() + if code_upper in self.SYNTHETIC_FIBERS: + has_synthetic = True + break + + if not has_synthetic: + return violations + + # Check for environmental footprint data + scorecard = passport.credential_subject.circularity_scorecard + emissions = passport.credential_subject.emissions_scorecard + + # If synthetic but no environmental data, suggest microplastic info + if not scorecard and not emissions: + violations.append( + ( + "$.credentialSubject", + "Synthetic textile product - consider adding microplastic " + "release data per JRC preparatory study", + ) + ) + + return violations + + +class TextileDurabilityRule: + """TXT004: Textile products should have durability information. + + Per ESPR Annex I, durability is a key product parameter for textiles. + """ + + rule_id: str = "TXT004" + description: str = "Textile should declare durability information" + severity: Literal["error", "warning", "info"] = "info" + suggestion: str = "Add product characteristics with durability data" + docs_url: str = "https://artiso-ai.github.io/dppvalidator/errors/TXT004" + + def check(self, passport: DigitalProductPassport) -> list[tuple[str, str]]: + """Check textile has durability information.""" + violations: list[tuple[str, str]] = [] + + if not passport.credential_subject: + return violations + + product = passport.credential_subject.product + if not product: + return violations + + # Check for characteristics (where durability info would be) + if not product.characteristics: + violations.append( + ( + "$.credentialSubject.product.characteristics", + "Textile product - consider adding durability characteristics " + "per ESPR Annex I requirements", + ) + ) + + return violations + + +class TextileCareInstructionsRule: + """TXT005: Textile products should have care instructions. + + Care instructions are required for consumer textiles per EU labeling + requirements and extend product lifetime. + """ + + rule_id: str = "TXT005" + description: str = "Textile should have care instructions" + severity: Literal["error", "warning", "info"] = "info" + suggestion: str = "Add furtherInformation link to care instructions" + docs_url: str = "https://artiso-ai.github.io/dppvalidator/errors/TXT005" + + def check(self, passport: DigitalProductPassport) -> list[tuple[str, str]]: + """Check textile has care instructions.""" + violations: list[tuple[str, str]] = [] + + if not passport.credential_subject: + return violations + + product = passport.credential_subject.product + if not product: + return violations + + # Check for further information links + if not product.further_information: + violations.append( + ( + "$.credentialSubject.product.furtherInformation", + "Textile product - consider adding care instructions link", + ) + ) + + return violations + + +# List of all textile rules for easy registration +TEXTILE_RULES = [ + TextileHSCodeRule(), + TextileMaterialCompositionRule(), + TextileMicroplasticRule(), + TextileDurabilityRule(), + TextileCareInstructionsRule(), +] + + +def is_textile_product(passport: DigitalProductPassport) -> bool: + """Check if a passport represents a textile product. + + Args: + passport: Digital Product Passport to check + + Returns: + True if product has textile HS code (chapters 50-63) + """ + if not passport.credential_subject: + return False + + product = passport.credential_subject.product + if not product or not product.product_category: + return False + + for classification in product.product_category: + if classification.code: + code = classification.code.replace(".", "").replace(" ", "") + if len(code) >= 2 and code[:2] in TEXTILE_HS_CHAPTERS: + return True + + return False + + +def get_textile_environmental_categories() -> list[str]: + """Get list of textile environmental impact categories.""" + return [cat.value for cat in TextileEnvironmentalCategory] diff --git a/src/dppvalidator/validators/rules/v0_7/__init__.py b/src/dppvalidator/validators/rules/v0_7/__init__.py new file mode 100644 index 0000000..54e2f9f --- /dev/null +++ b/src/dppvalidator/validators/rules/v0_7/__init__.py @@ -0,0 +1,96 @@ +"""Semantic validation rules for UNTP v0.7.0. + +Phase 3b of docs/plans/UNTP_0.7.0_MIGRATION.md added this subpackage to +hold semantic rules adapted to the v0.7.0 model shape. Each rule walks +the new envelope (``credentialSubject`` is :class:`Product` directly, +``materialProvenance`` is the singular noun, scorecards collapse into +``performanceClaim``). Rules whose underlying field/class no longer +exists in v0.7 are dropped — see +:data:`dppvalidator.validators.rules.v0_7.base.DROPPED_RULES_V0_6_TO_V0_7`. + +The v0.7 :data:`ALL_RULES_V0_7` list mirrors the v0.6 default-set with +those drops applied (e.g. ``OperationalScopeRule``). +""" + +from __future__ import annotations + +from dppvalidator.validators.rules.v0_7.base import ( + DROPPED_RULES_V0_6_TO_V0_7, + CircularityContentRule, + ConformityClaimRule, + GranularitySerialNumberRule, + GTINChecksumRule, + HazardousMaterialRule, + HSCodeRule, + MassFractionSumRule, + MaterialCodeRule, + ValidityDateRule, +) +from dppvalidator.validators.rules.v0_7.cirpass import ( + CIRPASS_RULES_V0_7, + CIRPASSGranularityConsistencyRule, + CIRPASSMandatoryAttributesRule, + CIRPASSOperatorIdentifierRule, + CIRPASSSubstancesOfConcernRule, + CIRPASSValidityPeriodRule, + CIRPASSWeightVolumeRule, +) +from dppvalidator.validators.rules.v0_7.textile import ( + TEXTILE_RULES_V0_7, + TextileCareInstructionsRule, + TextileDurabilityRule, + TextileEnvironmentalCategory, + TextileHSCodeRule, + TextileMaterialCompositionRule, + TextileMicroplasticRule, + is_textile_product, +) + +# v0.7 default rule list. Note the absence of OperationalScopeRule +# (SEM007 — folded into Claim/Performance in v0.7; see DROPPED_RULES). +ALL_RULES_V0_7 = [ + # Base UNTP rules (v0.7 shape) + MassFractionSumRule(), + ValidityDateRule(), + HazardousMaterialRule(), + CircularityContentRule(), + ConformityClaimRule(), + GranularitySerialNumberRule(), + MaterialCodeRule(), + HSCodeRule(), + GTINChecksumRule(), + # CIRPASS-2 CQ-coded rules (v0.7 shape) + *CIRPASS_RULES_V0_7, +] + +__all__ = [ + "ALL_RULES_V0_7", + "DROPPED_RULES_V0_6_TO_V0_7", + # Base + "CircularityContentRule", + "ConformityClaimRule", + "GTINChecksumRule", + "GranularitySerialNumberRule", + "HSCodeRule", + "HazardousMaterialRule", + "MassFractionSumRule", + "MaterialCodeRule", + "ValidityDateRule", + # CIRPASS + "CIRPASS_RULES_V0_7", + "CIRPASSGranularityConsistencyRule", + "CIRPASSMandatoryAttributesRule", + "CIRPASSOperatorIdentifierRule", + "CIRPASSSubstancesOfConcernRule", + "CIRPASSValidityPeriodRule", + "CIRPASSWeightVolumeRule", + # Textile + "TEXTILE_RULES_V0_7", + "TextileCareInstructionsRule", + "TextileDurabilityRule", + "TextileEnvironmentalCategory", + "TextileHSCodeRule", + "TextileMaterialCompositionRule", + "TextileMicroplasticRule", + "is_textile_product", +] diff --git a/src/dppvalidator/validators/rules/v0_7/base.py b/src/dppvalidator/validators/rules/v0_7/base.py new file mode 100644 index 0000000..82f97bd --- /dev/null +++ b/src/dppvalidator/validators/rules/v0_7/base.py @@ -0,0 +1,473 @@ +"""Base UNTP semantic rules adapted for the v0.7.0 model shape. + +The rules carry the same SEM/VOC error codes as their v0.6.x counterparts in +:mod:`dppvalidator.validators.rules.v0_6.base`, but they walk the new +v0.7.0 envelope: ``credentialSubject`` is :class:`Product` directly (no +``ProductPassport`` wrapper), ``materialProvenance`` is the singular noun, +and the three v0.6 scorecard classes are folded into +``performanceClaim: list[Claim]`` keyed by ``conformityTopic``. + +Rules dropped (not in :data:`ALL_RULES_V0_7`): + +- ``OperationalScopeRule`` (SEM007). v0.7 doesn't expose + ``EmissionsPerformance.operationalScope`` because the + ``EmissionsPerformance`` class itself is gone. The plan §Phase 3b says + to drop with a deprecation note rather than retro-fitting onto a + metadata claim — see :data:`DROPPED_RULES_V0_6_TO_V0_7`. + +Rules ported: + +- ``MassFractionSumRule`` (SEM001) — walks ``credential_subject.material_provenance``. +- ``ValidityDateRule`` (SEM002) — same envelope shape; defensive duplicate + of the model-level ``_validate_validity_window`` invariant. +- ``HazardousMaterialRule`` (SEM003) — walks ``material_provenance``. +- ``CircularityContentRule`` (SEM004) — walks ``performance_claim`` filtered + by ``conformityTopic.name == "circularity"`` and reads from + ``claimedPerformance[*].measure`` / ``score``. +- ``ConformityClaimRule`` (SEM005) — walks ``performance_claim``. +- ``GranularitySerialNumberRule`` (SEM006) — defensive duplicate of the + model-level ``_granularity_implies_serial_or_batch`` invariant; covers + ``id_granularity == 'item'`` ↔ ``item_number`` and the new + ``id_granularity == 'batch'`` ↔ ``batch_number`` rule from v0.7. +- ``MaterialCodeRule`` (VOC003) — walks ``material_provenance[*].material_type.code``. +- ``HSCodeRule`` (VOC004) — walks ``credential_subject.product_category[*].code``. +- ``GTINChecksumRule`` (VOC005) — walks ``credential_subject.id``. +""" + +from __future__ import annotations + +import re +from collections.abc import Callable +from typing import TYPE_CHECKING, Literal + +from dppvalidator.vocabularies.code_lists import ( + is_hs_scheme as _is_hs_scheme, +) +from dppvalidator.vocabularies.code_lists import ( + is_unece_rec46_scheme as _is_unece_rec46_scheme, +) +from dppvalidator.vocabularies.code_lists import ( + is_valid_hs_code as _is_valid_hs_code, +) +from dppvalidator.vocabularies.code_lists import ( + is_valid_material_code as _is_valid_material_code, +) +from dppvalidator.vocabularies.code_lists import ( + validate_gtin as _validate_gtin, +) + +if TYPE_CHECKING: + from dppvalidator.models.v0_7.claims import Claim + from dppvalidator.models.v0_7.envelope import DigitalProductPassport + +# A note for callers building rule sets manually: these rules don't apply to +# v0.7 because the underlying field/class no longer exists. Phase 3b §Plan +# §3.2 captures the rationale. +DROPPED_RULES_V0_6_TO_V0_7: dict[str, str] = { + "SEM007": ( + "OperationalScopeRule was tied to v0.6 EmissionsPerformance.operational_scope. " + "v0.7.0 folds emissions data into Claim.claimedPerformance keyed by " + "ConformityTopic, so there's no longer a discrete operationalScope field " + "to validate. Drop the rule rather than retro-fitting." + ), +} + + +def _circularity_topic(claim: Claim) -> bool: + """True when the claim's conformityTopic includes ``"circularity"``. + + Loose match: the schema's ConformityTopic.name field is free-form, + so we check both the topic name and the topic ID for the substring + ``"circularity"`` (case-insensitive). Falls back to ``False`` if the + claim has no conformityTopic entries. + """ + topics = getattr(claim, "conformity_topic", None) or [] + for t in topics: + name = (getattr(t, "name", "") or "").lower() + ident = (getattr(t, "id", "") or "").lower() + if "circularity" in name or "circularity" in ident: + return True + return False + + +class MassFractionSumRule: + """SEM001 (v0.7): material mass-fractions should sum to 1.0. + + The model-level ``Product._mass_fractions_sum_within_unity`` invariant + catches sums *above* 1.0 (which are physically impossible). This + semantic rule warns on partial declarations (sum < 1.0) so the user is + aware their material list is incomplete. + """ + + rule_id: str = "SEM001" + description: str = "Material mass fractions should sum to 1.0" + severity: Literal["error", "warning", "info"] = "warning" + suggestion: str = "Adjust material mass fractions to sum to 1.0 (100%)" + docs_url: str = "https://artiso-ai.github.io/dppvalidator/errors/SEM001" + + def check(self, passport: DigitalProductPassport) -> list[tuple[str, str]]: + violations: list[tuple[str, str]] = [] + product = passport.credential_subject + if product is None: + return violations + + materials = getattr(product, "material_provenance", []) or [] + if not materials: + return violations + + fractions = [m.mass_fraction for m in materials if m.mass_fraction is not None] + if not fractions: + return violations + + total = sum(fractions) + if abs(total - 1.0) > 0.01: + violations.append( + ( + "$.credentialSubject.materialProvenance", + f"Mass fractions sum to {total:.3f}, expected 1.0 (partial declaration)", + ) + ) + return violations + + +class ValidityDateRule: + """SEM002 (v0.7): validFrom must be before validUntil. + + Defensive duplicate — the v0.7 envelope already enforces this as a + Pydantic model-level invariant (``DigitalProductPassport._validate_validity_window``). + The rule is kept so semantic-layer error reporting is consistent with + v0.6.x, where the same check is *only* at the semantic layer. + """ + + rule_id: str = "SEM002" + description: str = "validFrom must be before validUntil" + severity: Literal["error", "warning", "info"] = "error" + suggestion: str = "Ensure validFrom is before validUntil" + docs_url: str = "https://artiso-ai.github.io/dppvalidator/errors/SEM002" + + def check(self, passport: DigitalProductPassport) -> list[tuple[str, str]]: + violations: list[tuple[str, str]] = [] + if ( + passport.valid_from + and passport.valid_until + and passport.valid_from >= passport.valid_until + ): + violations.append( + ( + "$.validFrom", + f"validFrom ({passport.valid_from}) must be before validUntil ({passport.valid_until})", + ) + ) + return violations + + +class HazardousMaterialRule: + """SEM003 (v0.7): hazardous=True requires materialSafetyInformation.""" + + rule_id: str = "SEM003" + description: str = "Hazardous materials require safety information" + severity: Literal["error", "warning", "info"] = "error" + suggestion: str = "Add materialSafetyInformation for hazardous materials" + docs_url: str = "https://artiso-ai.github.io/dppvalidator/errors/SEM003" + + def check(self, passport: DigitalProductPassport) -> list[tuple[str, str]]: + violations: list[tuple[str, str]] = [] + product = passport.credential_subject + if product is None: + return violations + + materials = getattr(product, "material_provenance", []) or [] + for i, material in enumerate(materials): + if material.hazardous and not material.material_safety_information: + violations.append( + ( + f"$.credentialSubject.materialProvenance[{i}]", + f"Material '{material.name}' is hazardous but missing materialSafetyInformation", + ) + ) + return violations + + +class CircularityContentRule: + """SEM004 (v0.7): recycledContent should not exceed recyclableContent. + + v0.7 has no ``CircularityScorecard`` class; circularity is expressed as + a :class:`Claim` with ``conformityTopic`` containing "circularity", + whose ``claimedPerformance`` array carries the readings. This rule + inspects each circularity claim and pairs values whose ``metric.id`` + looks like a content-fraction metric. + + To keep the rule shape-tolerant — the upstream schema doesn't + constrain ``Performance.metric`` to a specific shape — we look up the + metric by name keywords (``recycled``, ``recyclable``) and compare + pairs. Tests in ``test_semantic_rules_v07.py`` exercise this with + canonical metric IDs. + """ + + rule_id: str = "SEM004" + description: str = "recycledContent should not exceed recyclableContent" + severity: Literal["error", "warning", "info"] = "warning" + suggestion: str = "recycledContent cannot exceed recyclableContent" + docs_url: str = "https://artiso-ai.github.io/dppvalidator/errors/SEM004" + + def check(self, passport: DigitalProductPassport) -> list[tuple[str, str]]: + violations: list[tuple[str, str]] = [] + product = passport.credential_subject + if product is None: + return violations + + for claim_idx, claim in enumerate(getattr(product, "performance_claim", []) or []): + if not _circularity_topic(claim): + continue + recycled = recyclable = None + for perf in getattr(claim, "claimed_performance", []) or []: + metric = perf.metric or {} + key = " ".join(str(metric.get(k, "")) for k in ("id", "name", "label")).lower() + value = perf.measure.value if perf.measure else None + if value is None: + continue + if "recycledcontent" in key.replace(" ", "") or "recycled content" in key: + recycled = value + elif "recyclablecontent" in key.replace(" ", "") or "recyclable content" in key: + recyclable = value + if recycled is not None and recyclable is not None and recycled > recyclable: + violations.append( + ( + f"$.credentialSubject.performanceClaim[{claim_idx}]", + f"recycledContent ({recycled}) exceeds recyclableContent ({recyclable})", + ) + ) + return violations + + +class ConformityClaimRule: + """SEM005 (v0.7): at least one performanceClaim is recommended. + + v0.6's ``conformityClaim`` array is now ``performanceClaim`` on Product; + the warning text is updated accordingly so users searching for "no + conformity claims" are still pointed at the right field. + """ + + rule_id: str = "SEM005" + description: str = "At least one performanceClaim is recommended" + severity: Literal["error", "warning", "info"] = "info" + suggestion: str = "Add performance / conformity claims for sustainability or compliance" + docs_url: str = "https://artiso-ai.github.io/dppvalidator/errors/SEM005" + + def check(self, passport: DigitalProductPassport) -> list[tuple[str, str]]: + violations: list[tuple[str, str]] = [] + product = passport.credential_subject + if product is None: + return violations + + claims = getattr(product, "performance_claim", []) or [] + if not claims: + violations.append( + ( + "$.credentialSubject.performanceClaim", + "No performance claims present. Consider adding sustainability or compliance claims.", + ) + ) + return violations + + +class GranularitySerialNumberRule: + """SEM006 (v0.7): item-/batch-level granularity require itemNumber/batchNumber. + + Defensive duplicate of the v0.7 model-level + ``Product._granularity_implies_serial_or_batch`` invariant. Issued at + semantic layer for parity with v0.6.x reporting (where it's the + only place the check happens). + """ + + rule_id: str = "SEM006" + description: str = "Item-/batch-level passports require an item or batch number" + severity: Literal["error", "warning", "info"] = "warning" + suggestion: str = "Add itemNumber for 'item' granularity or batchNumber for 'batch' granularity" + docs_url: str = "https://artiso-ai.github.io/dppvalidator/errors/SEM006" + + def check(self, passport: DigitalProductPassport) -> list[tuple[str, str]]: + violations: list[tuple[str, str]] = [] + product = passport.credential_subject + if product is None: + return violations + + granularity = str(getattr(product, "id_granularity", "") or "") + if granularity == "item" and not getattr(product, "item_number", None): + violations.append( + ( + "$.credentialSubject.itemNumber", + "idGranularity is 'item' but itemNumber is missing", + ) + ) + elif granularity == "batch" and not getattr(product, "batch_number", None): + violations.append( + ( + "$.credentialSubject.batchNumber", + "idGranularity is 'batch' but batchNumber is missing", + ) + ) + return violations + + +class MaterialCodeRule: + """VOC003 (v0.7): material code must be valid per UNECE Rec 46. + + The rule is **scheme-aware**: it only fires when the + ``materialType.schemeId`` declares the code as UNECE Rec 46 + (detected by :func:`_is_unece_rec46_scheme`). Codes drawn from + other classifications (UN CPC, GS1 GPC, NACE, …) are skipped — + firing the textile-pilot validator on them produced false + positives. When ``schemeId`` is missing entirely, the rule falls + back to the pre-fix best-effort behaviour (a textile DPP without + a declared scheme is the most common producer of legacy v0.6 + fixtures). + """ + + rule_id: str = "VOC003" + description: str = "Material code must be in UNECE Rec 46" + severity: Literal["error", "warning", "info"] = "warning" + suggestion: str = "Use a valid UNECE Rec 46 material code" + docs_url: str = "https://artiso-ai.github.io/dppvalidator/errors/VOC003" + + def __init__( + self, + material_validator: Callable[[str], bool] | None = None, + ) -> None: + self._is_valid_material_code = material_validator or _is_valid_material_code + + def check(self, passport: DigitalProductPassport) -> list[tuple[str, str]]: + violations: list[tuple[str, str]] = [] + product = passport.credential_subject + if product is None: + return violations + + materials = getattr(product, "material_provenance", []) or [] + for i, material in enumerate(materials): + mt = getattr(material, "material_type", None) + if mt is None: + continue + code = getattr(mt, "code", None) + if not code: + continue + scheme_id = getattr(mt, "scheme_id", None) + # When schemeId is set but isn't UNECE Rec 46, the rule + # has no opinion — skip silently rather than false-positive. + if scheme_id and not _is_unece_rec46_scheme(scheme_id): + continue + if not self._is_valid_material_code(code): + violations.append( + ( + f"$.credentialSubject.materialProvenance[{i}].materialType.code", + f"Invalid material code '{code}' - not found in UNECE Rec 46", + ) + ) + return violations + + +class HSCodeRule: + """VOC004 (v0.7): HS code must be valid for product category. + + v0.7 ``Product.product_category`` is a list of :class:`Classification` + (was a single Classification in v0.6); this rule iterates over them. + + The rule is **scheme-aware**: it only fires when the + ``classification.schemeId`` declares the code as a Harmonized + System code (detected by :func:`_is_hs_scheme`). Codes drawn + from other classifications are skipped. When ``schemeId`` is + missing, the rule falls back to a length+digit heuristic + (4+ digits) that matches the pre-fix best-effort behaviour for + legacy v0.6 fixtures that don't declare a scheme. + """ + + rule_id: str = "VOC004" + description: str = "HS code must be valid for product category" + severity: Literal["error", "warning", "info"] = "warning" + suggestion: str = "Use a valid HS code for textiles (chapters 50-63)" + docs_url: str = "https://artiso-ai.github.io/dppvalidator/errors/VOC004" + + def __init__( + self, + hs_validator: Callable[[str], bool] | None = None, + ) -> None: + self._is_valid_hs_code = hs_validator or _is_valid_hs_code + + def check(self, passport: DigitalProductPassport) -> list[tuple[str, str]]: + violations: list[tuple[str, str]] = [] + product = passport.credential_subject + if product is None: + return violations + + categories = getattr(product, "product_category", []) or [] + for i, classification in enumerate(categories): + code = getattr(classification, "code", None) or "" + scheme_id = getattr(classification, "scheme_id", None) + # Scheme-aware gate: when schemeId is set, only fire for + # HS schemes. When it isn't, fall back to the length / + # digit heuristic (4+ digits looks HS-shaped). + if scheme_id is not None: + if not _is_hs_scheme(scheme_id): + continue + should_check = bool(code) + else: + should_check = code.isdigit() and len(code) >= 4 + if should_check and not self._is_valid_hs_code(code): + violations.append( + ( + f"$.credentialSubject.productCategory[{i}].code", + f"Invalid HS code '{code}' - not found in textile chapters (50-63)", + ) + ) + return violations + + +class GTINChecksumRule: + """VOC005 (v0.7): GTIN must have valid check digit. + + Walks ``credential_subject.id`` (was ``credentialSubject.product.id`` + in v0.6) and validates GS1 check-digits when the URI looks like a + GS1 Digital Link or a bare GTIN. + """ + + rule_id: str = "VOC005" + description: str = "GTIN must have valid check digit" + severity: Literal["error", "warning", "info"] = "error" + suggestion: str = "Verify the GTIN check digit using GS1 algorithm" + docs_url: str = "https://artiso-ai.github.io/dppvalidator/errors/VOC005" + + def __init__( + self, + gtin_validator: Callable[[str], bool] | None = None, + ) -> None: + self._validate_gtin = gtin_validator or _validate_gtin + + def check(self, passport: DigitalProductPassport) -> list[tuple[str, str]]: + violations: list[tuple[str, str]] = [] + product = passport.credential_subject + if product is None: + return violations + + product_id = getattr(product, "id", None) + if not product_id: + return violations + + if "/01/" in product_id: + match = re.search(r"/01/(\d{8,14})", product_id) + if match: + gtin = match.group(1) + if not self._validate_gtin(gtin): + violations.append( + ( + "$.credentialSubject.id", + f"Invalid GTIN checksum in '{gtin}'", + ) + ) + elif product_id.isdigit() and len(product_id) in (8, 12, 13, 14): + if not self._validate_gtin(product_id): + violations.append( + ( + "$.credentialSubject.id", + f"Invalid GTIN checksum in '{product_id}'", + ) + ) + return violations diff --git a/src/dppvalidator/validators/rules/v0_7/cirpass.py b/src/dppvalidator/validators/rules/v0_7/cirpass.py new file mode 100644 index 0000000..a1c2aa4 --- /dev/null +++ b/src/dppvalidator/validators/rules/v0_7/cirpass.py @@ -0,0 +1,240 @@ +"""CIRPASS-2 CQ-based rules adapted for the UNTP v0.7.0 envelope. + +Same CQ-coded rules as :mod:`dppvalidator.validators.rules.v0_6.cirpass`, +walking the new v0.7.0 shape: + +- ``credentialSubject`` is :class:`Product` directly (no + ``ProductPassport`` wrapper). The ``CIRPASSMandatoryAttributesRule`` + check that v0.6.x ran for ``credentialSubject.product`` is dropped + because the *presence* of the product subject IS the + ``credentialSubject``. +- ``materialsProvenance`` is renamed ``materialProvenance``. +- ``granularityLevel`` is renamed ``idGranularity`` and lives on + ``credentialSubject`` directly (not nested inside a ProductPassport). +- ``product.serial_number`` is now ``credentialSubject.item_number``. +- All CQ codes (CQ001, CQ004, CQ011, CQ016, CQ017, CQ020) are preserved. +""" + +from __future__ import annotations + +import re +from typing import TYPE_CHECKING, Literal + +if TYPE_CHECKING: + from dppvalidator.models.v0_7.envelope import DigitalProductPassport + + +class CIRPASSMandatoryAttributesRule: + """CQ001 (v0.7): DPP must have mandatory attributes per ESPR. + + For v0.7.0 the check simplifies: there is no ``credentialSubject.product`` + nesting, so we verify ``credentialSubject`` itself is present (which is + a Product) plus the mandatory envelope fields (``issuer``, ``validFrom``). + """ + + rule_id: str = "CQ001" + description: str = "DPP must have mandatory ESPR attributes" + severity: Literal["error", "warning", "info"] = "error" + suggestion: str = "Add required attributes: issuer, validFrom, credentialSubject" + docs_url: str = "https://artiso-ai.github.io/dppvalidator/errors/CQ001" + + def check(self, passport: DigitalProductPassport) -> list[tuple[str, str]]: + violations: list[tuple[str, str]] = [] + + if not passport.issuer: + violations.append(("$.issuer", "Missing issuer - required per ESPR Annex III (g)")) + + if not passport.valid_from: + violations.append(("$.validFrom", "Missing validFrom - required per ESPR Art 9(2i)")) + + if not passport.credential_subject: + violations.append( + ( + "$.credentialSubject", + "Missing credentialSubject - DPP has no product data", + ) + ) + + return violations + + +class CIRPASSSubstancesOfConcernRule: + """CQ004 (v0.7): substances of concern must have proper identification.""" + + rule_id: str = "CQ004" + description: str = "Substances of concern must have CAS numbers" + severity: Literal["error", "warning", "info"] = "error" + suggestion: str = "Add CAS number or EINECS number for each substance of concern" + docs_url: str = "https://artiso-ai.github.io/dppvalidator/errors/CQ004" + + CAS_PATTERN = re.compile(r"^\d{2,7}-\d{2}-\d$") + + def check(self, passport: DigitalProductPassport) -> list[tuple[str, str]]: + violations: list[tuple[str, str]] = [] + product = passport.credential_subject + if product is None: + return violations + + materials = getattr(product, "material_provenance", []) or [] + for i, material in enumerate(materials): + if material.hazardous: + has_cas = False + if material.name and self.CAS_PATTERN.search(material.name): + has_cas = True + mt = getattr(material, "material_type", None) + code = getattr(mt, "code", None) if mt else None + if code and (self.CAS_PATTERN.match(code) or code.startswith("EINECS")): + has_cas = True + + if not has_cas: + violations.append( + ( + f"$.credentialSubject.materialProvenance[{i}]", + f"Hazardous material '{material.name}' missing CAS/EINECS " + "number per ESPR Art 7(5a)", + ) + ) + return violations + + +class CIRPASSOperatorIdentifierRule: + """CQ011 (v0.7): manufacturer must have unique operator identifier. + + Same shape as v0.6 (issuer.id is unchanged across versions); the rule + is duplicated only so the v0.7 ALL_RULES set carries it independently. + """ + + rule_id: str = "CQ011" + description: str = "Manufacturer must have unique operator identifier" + severity: Literal["error", "warning", "info"] = "error" + suggestion: str = "Add issuer.id with GLN, DUNS, or LEI identifier" + docs_url: str = "https://artiso-ai.github.io/dppvalidator/errors/CQ011" + + def check(self, passport: DigitalProductPassport) -> list[tuple[str, str]]: + violations: list[tuple[str, str]] = [] + + if not passport.issuer: + violations.append(("$.issuer", "Missing issuer - cannot verify operator identifier")) + return violations + + if not passport.issuer.id: + violations.append( + ( + "$.issuer.id", + "Missing issuer.id - manufacturer requires unique operator " + "identifier per ESPR Annex III (g)", + ) + ) + return violations + + +class CIRPASSValidityPeriodRule: + """CQ016 (v0.7): DPP must have validity period information.""" + + rule_id: str = "CQ016" + description: str = "DPP must have validity period" + severity: Literal["error", "warning", "info"] = "warning" + suggestion: str = "Add validFrom and validUntil dates per ESPR Art 9(2i)" + docs_url: str = "https://artiso-ai.github.io/dppvalidator/errors/CQ016" + + def check(self, passport: DigitalProductPassport) -> list[tuple[str, str]]: + violations: list[tuple[str, str]] = [] + + if not passport.valid_from: + violations.append( + ( + "$.validFrom", + "Missing validFrom - market placement date required per ESPR Art 9(2i)", + ) + ) + + if not passport.valid_until: + violations.append( + ( + "$.validUntil", + "Missing validUntil - DPP validity duration recommended per ESPR Art 9(2i)", + ) + ) + return violations + + +class CIRPASSWeightVolumeRule: + """CQ020 (v0.7): product should declare weight and volume.""" + + rule_id: str = "CQ020" + description: str = "Product should declare weight and volume" + severity: Literal["error", "warning", "info"] = "warning" + suggestion: str = "Add product dimensions (weight/volume) per ESPR Annex I(j)" + docs_url: str = "https://artiso-ai.github.io/dppvalidator/errors/CQ020" + + def check(self, passport: DigitalProductPassport) -> list[tuple[str, str]]: + violations: list[tuple[str, str]] = [] + product = passport.credential_subject + if product is None: + return violations + + dim = getattr(product, "dimensions", None) + has_dimension = bool(dim) and any( + getattr(dim, attr, None) is not None + for attr in ("weight", "length", "width", "height", "volume") + ) + + if not has_dimension: + violations.append( + ( + "$.credentialSubject.dimensions", + "Missing weight/volume - product dimensions recommended per ESPR Annex I(j)", + ) + ) + return violations + + +class CIRPASSGranularityConsistencyRule: + """CQ017 (v0.7): granularity level must be consistent with identifiers. + + v0.7 renames ``granularityLevel`` → ``idGranularity`` and + ``serialNumber`` → ``itemNumber``. Codes and ESPR references unchanged. + """ + + rule_id: str = "CQ017" + description: str = "Granularity level must match identifier granularity" + severity: Literal["error", "warning", "info"] = "warning" + suggestion: str = "Ensure idGranularity matches available identifiers" + docs_url: str = "https://artiso-ai.github.io/dppvalidator/errors/CQ017" + + def check(self, passport: DigitalProductPassport) -> list[tuple[str, str]]: + violations: list[tuple[str, str]] = [] + product = passport.credential_subject + if product is None: + return violations + + granularity = str(getattr(product, "id_granularity", "") or "").lower() + + if granularity == "item" and not getattr(product, "item_number", None): + violations.append( + ( + "$.credentialSubject.itemNumber", + "idGranularity is 'item' but itemNumber is missing - " + "per SR5423 Annex II Part B 1.1(4)", + ) + ) + + if granularity == "batch" and not getattr(product, "batch_number", None): + violations.append( + ( + "$.credentialSubject.batchNumber", + "idGranularity is 'batch' but batchNumber is missing - " + "per SR5423 Annex II Part B 1.1(3)", + ) + ) + return violations + + +CIRPASS_RULES_V0_7 = [ + CIRPASSMandatoryAttributesRule(), + CIRPASSSubstancesOfConcernRule(), + CIRPASSOperatorIdentifierRule(), + CIRPASSValidityPeriodRule(), + CIRPASSWeightVolumeRule(), + CIRPASSGranularityConsistencyRule(), +] diff --git a/src/dppvalidator/validators/rules/v0_7/textile.py b/src/dppvalidator/validators/rules/v0_7/textile.py new file mode 100644 index 0000000..c12206d --- /dev/null +++ b/src/dppvalidator/validators/rules/v0_7/textile.py @@ -0,0 +1,277 @@ +"""Textile sector rules adapted for the UNTP v0.7.0 envelope. + +Same TXT-coded rules as :mod:`dppvalidator.validators.rules.v0_6.textile`, +walking the v0.7 shape: + +- ``credentialSubject`` is the :class:`Product` directly (no + ``credentialSubject.product`` traversal). +- ``materialsProvenance`` → ``materialProvenance`` (singular). +- ``furtherInformation`` (v0.6 ``Product.furtherInformation: Link``) is + replaced by ``relatedDocument: list[Link]`` (v0.7 absorbs both + ``furtherInformation`` and ``dueDiligenceDeclaration`` into this array). +- The two scorecard classes (``circularityScorecard``, ``emissionsScorecard``) + are gone; the microplastic-data presence check now looks at + ``performanceClaim`` instead. + +The shared helpers — :class:`TextileEnvironmentalCategory`, +``TEXTILE_HS_CHAPTERS``, ``TEXTILE_MATERIAL_CODES`` — are imported from the +v0.6 module so they live in one place. They're version-neutral data tables. +""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, Literal + +from dppvalidator.validators.rules.v0_6.textile import ( + TEXTILE_HS_CHAPTERS, + TextileEnvironmentalCategory, # re-export — version-neutral enum +) + +if TYPE_CHECKING: + from dppvalidator.models.v0_7.envelope import DigitalProductPassport + + +__all__ = [ + "TEXTILE_HS_CHAPTERS", + "TEXTILE_RULES_V0_7", + "TextileCareInstructionsRule", + "TextileDurabilityRule", + "TextileEnvironmentalCategory", + "TextileHSCodeRule", + "TextileMaterialCompositionRule", + "TextileMicroplasticRule", + "is_textile_product", +] + + +class TextileHSCodeRule: + """TXT001 (v0.7): textile product must have valid HS code.""" + + rule_id: str = "TXT001" + description: str = "Textile product must have valid HS code (chapters 50-63)" + severity: Literal["error", "warning", "info"] = "warning" + suggestion: str = "Add product category with HS code in chapters 50-63" + docs_url: str = "https://artiso-ai.github.io/dppvalidator/errors/TXT001" + + def check(self, passport: DigitalProductPassport) -> list[tuple[str, str]]: + violations: list[tuple[str, str]] = [] + product = passport.credential_subject + if product is None: + return violations + + categories = getattr(product, "product_category", []) or [] + if not categories: + violations.append( + ( + "$.credentialSubject.productCategory", + "Textile product missing product category with HS code", + ) + ) + return violations + + has_textile_hs = False + for classification in categories: + code = getattr(classification, "code", None) or "" + stripped = code.replace(".", "").replace(" ", "") + if len(stripped) >= 2 and stripped[:2] in TEXTILE_HS_CHAPTERS: + has_textile_hs = True + break + + if not has_textile_hs: + violations.append( + ( + "$.credentialSubject.productCategory", + "No textile HS code found (chapters 50-63 required)", + ) + ) + return violations + + +class TextileMaterialCompositionRule: + """TXT002 (v0.7): textile must declare material composition.""" + + rule_id: str = "TXT002" + description: str = "Textile must declare material composition" + severity: Literal["error", "warning", "info"] = "error" + suggestion: str = "Add materialProvenance with fiber types and mass fractions" + docs_url: str = "https://artiso-ai.github.io/dppvalidator/errors/TXT002" + + def check(self, passport: DigitalProductPassport) -> list[tuple[str, str]]: + violations: list[tuple[str, str]] = [] + product = passport.credential_subject + if product is None: + return violations + + materials = getattr(product, "material_provenance", []) or [] + if not materials: + violations.append( + ( + "$.credentialSubject.materialProvenance", + "Textile product missing material composition declaration", + ) + ) + return violations + + if not any(m.mass_fraction is not None for m in materials): + violations.append( + ( + "$.credentialSubject.materialProvenance", + "Textile materials missing mass fraction (fiber %) declaration", + ) + ) + return violations + + +class TextileMicroplasticRule: + """TXT003 (v0.7): synthetic textiles should declare microplastic release. + + v0.7 has no scorecard classes; the heuristic for "did the producer + declare environmental data?" is now "is there at least one + performanceClaim?". This is a deliberate softening — Phase 5/Phase 7 + can refine this when the topic taxonomy is settled. + """ + + rule_id: str = "TXT003" + description: str = "Synthetic textiles should declare microplastic release" + severity: Literal["error", "warning", "info"] = "info" + suggestion: str = "Add microplastic release data for synthetic fiber products" + docs_url: str = "https://artiso-ai.github.io/dppvalidator/errors/TXT003" + + SYNTHETIC_FIBERS = frozenset( + [ + "POLYESTER", + "PL", + "NYLON", + "PA", + "ACRYLIC", + "PC", + "ELASTANE", + "EL", + "POLYPROPYLENE", + "PP", + ] + ) + + def check(self, passport: DigitalProductPassport) -> list[tuple[str, str]]: + violations: list[tuple[str, str]] = [] + product = passport.credential_subject + if product is None: + return violations + + materials = getattr(product, "material_provenance", []) or [] + if not materials: + return violations + + has_synthetic = False + for material in materials: + name = (material.name or "").upper() + if any(fiber in name for fiber in self.SYNTHETIC_FIBERS): + has_synthetic = True + break + mt = getattr(material, "material_type", None) + code = (getattr(mt, "code", None) or "").upper() if mt else "" + if code in self.SYNTHETIC_FIBERS: + has_synthetic = True + break + + if not has_synthetic: + return violations + + # In v0.7 the "is there environmental data?" heuristic is the + # presence of any performanceClaim entry. If there's nothing, hint + # at adding microplastic data. + claims = getattr(product, "performance_claim", []) or [] + if not claims: + violations.append( + ( + "$.credentialSubject", + "Synthetic textile product - consider adding microplastic " + "release data per JRC preparatory study", + ) + ) + return violations + + +class TextileDurabilityRule: + """TXT004 (v0.7): textile products should have durability information.""" + + rule_id: str = "TXT004" + description: str = "Textile should declare durability information" + severity: Literal["error", "warning", "info"] = "info" + suggestion: str = "Add product characteristics with durability data" + docs_url: str = "https://artiso-ai.github.io/dppvalidator/errors/TXT004" + + def check(self, passport: DigitalProductPassport) -> list[tuple[str, str]]: + violations: list[tuple[str, str]] = [] + product = passport.credential_subject + if product is None: + return violations + + if not getattr(product, "characteristics", None): + violations.append( + ( + "$.credentialSubject.characteristics", + "Textile product - consider adding durability characteristics " + "per ESPR Annex I requirements", + ) + ) + return violations + + +class TextileCareInstructionsRule: + """TXT005 (v0.7): textile products should have care instructions. + + v0.7 absorbs the v0.6 ``furtherInformation`` field into + ``relatedDocument: list[Link]``. The check now succeeds when at least + one link is present in ``relatedDocument``. + """ + + rule_id: str = "TXT005" + description: str = "Textile should have care instructions" + severity: Literal["error", "warning", "info"] = "info" + suggestion: str = "Add a relatedDocument link with care instructions" + docs_url: str = "https://artiso-ai.github.io/dppvalidator/errors/TXT005" + + def check(self, passport: DigitalProductPassport) -> list[tuple[str, str]]: + violations: list[tuple[str, str]] = [] + product = passport.credential_subject + if product is None: + return violations + + documents = getattr(product, "related_document", []) or [] + if not documents: + violations.append( + ( + "$.credentialSubject.relatedDocument", + "Textile product - consider adding care instructions link", + ) + ) + return violations + + +TEXTILE_RULES_V0_7 = [ + TextileHSCodeRule(), + TextileMaterialCompositionRule(), + TextileMicroplasticRule(), + TextileDurabilityRule(), + TextileCareInstructionsRule(), +] + + +def is_textile_product(passport: DigitalProductPassport) -> bool: + """Return True when the v0.7 DPP describes a textile product. + + Mirrors the v0.6 helper, but walks the new envelope shape + (``passport.credential_subject.product_category`` instead of + ``passport.credential_subject.product.product_category``). + """ + product = passport.credential_subject + if product is None: + return False + + for classification in getattr(product, "product_category", []) or []: + code = (getattr(classification, "code", None) or "").replace(".", "").replace(" ", "") + if len(code) >= 2 and code[:2] in TEXTILE_HS_CHAPTERS: + return True + + return False diff --git a/src/dppvalidator/validators/schema.py b/src/dppvalidator/validators/schema.py index fcc2cbf..50b49dc 100644 --- a/src/dppvalidator/validators/schema.py +++ b/src/dppvalidator/validators/schema.py @@ -11,6 +11,7 @@ from jsonschema import Draft202012Validator +from dppvalidator.schemas.registry import DEFAULT_SCHEMA_VERSION from dppvalidator.validators.results import ValidationError, ValidationResult # Schema type for dual-mode validation (Phase 6) @@ -54,7 +55,7 @@ class SchemaValidator: def __init__( self, - schema_version: str = "0.6.1", + schema_version: str = DEFAULT_SCHEMA_VERSION, schema_type: SchemaType = "untp", schema_path: Path | None = None, strict: bool = False, @@ -62,7 +63,10 @@ def __init__( """Initialize schema validator. Args: - schema_version: Schema version ("0.6.1" for UNTP, "1.3.0" for CIRPASS) + schema_version: Schema version. For ``schema_type="untp"`` use a + version registered in ``dppvalidator.schemas.SCHEMA_REGISTRY``; + for ``schema_type="cirpass"`` use a CIRPASS DPP version. Defaults + to ``DEFAULT_SCHEMA_VERSION``. schema_type: Schema type - "untp" (default) or "cirpass" for EU DPP schema_path: Optional custom schema path. If None, uses bundled schema. strict: If True, disallows additional properties not in schema diff --git a/src/dppvalidator/validators/semantic.py b/src/dppvalidator/validators/semantic.py index 422ad11..d95d2b0 100644 --- a/src/dppvalidator/validators/semantic.py +++ b/src/dppvalidator/validators/semantic.py @@ -5,8 +5,9 @@ import time from typing import TYPE_CHECKING, Any, Literal +from dppvalidator.schemas.registry import DEFAULT_SCHEMA_VERSION from dppvalidator.validators.results import ValidationError, ValidationResult -from dppvalidator.validators.rules import ALL_RULES +from dppvalidator.validators.rules import ALL_RULES, ALL_RULES_BY_VERSION if TYPE_CHECKING: from dppvalidator.models.passport import DigitalProductPassport @@ -16,7 +17,12 @@ class SemanticValidator: """Semantic validation layer for business rules. Applies domain-specific validation rules that go beyond - schema and type validation. + schema and type validation. Rule-set selection is **version-aware**: + when ``rules`` is left at the default ``None``, the validator looks + up the right rule set in :data:`ALL_RULES_BY_VERSION` keyed on + ``schema_version``. This is what stops the v0.6 ``CQ001`` rule from + firing as a false positive on a v0.7 payload — see Phase 3b of + docs/plans/UNTP_0.7.0_MIGRATION.md. """ name: str = "semantic" @@ -24,17 +30,27 @@ class SemanticValidator: def __init__( self, - schema_version: str = "0.6.1", + schema_version: str = DEFAULT_SCHEMA_VERSION, rules: list[Any] | None = None, ) -> None: """Initialize semantic validator. Args: - schema_version: UNTP DPP schema version - rules: Custom rules list. If None, uses ALL_RULES. + schema_version: UNTP DPP schema version. Used to pick the + appropriate rule set from :data:`ALL_RULES_BY_VERSION` + when ``rules`` is ``None``. + rules: Custom rules list. If supplied, it overrides the + version-keyed dispatch — callers can still inject a + hand-curated subset for tests or plugin scenarios. + If ``None``, the version-keyed lookup runs; if the + version is unknown to the registry the dispatch falls + back to :data:`ALL_RULES` (the default v0.6.x set). """ self.schema_version = schema_version - self.rules = rules if rules is not None else ALL_RULES + if rules is not None: + self.rules = rules + else: + self.rules = ALL_RULES_BY_VERSION.get(schema_version, ALL_RULES) def validate( self, diff --git a/src/dppvalidator/verifier/verifier.py b/src/dppvalidator/verifier/verifier.py index 3830933..82a26b3 100644 --- a/src/dppvalidator/verifier/verifier.py +++ b/src/dppvalidator/verifier/verifier.py @@ -194,11 +194,24 @@ def _verify_ed25519_proof( if not proof_value: return None - # Decode multibase-encoded signature (z prefix = base58btc) + # Decode the proof value. A leading "z" SHOULD indicate + # multibase base58btc per the Data Integrity spec, but + # standard base64 also contains "z" in its alphabet — so a + # legacy base64-encoded signature may collide with the + # multibase prefix. Try base58btc first when the prefix + # matches; fall back to base64 if that decode fails or + # yields empty bytes. + signature = b"" if proof_value.startswith("z"): - signature = self._decode_base58btc(proof_value[1:]) - else: - signature = base64.b64decode(proof_value) + try: + signature = self._decode_base58btc(proof_value[1:]) + except Exception: + signature = b"" + if not signature: + try: + signature = base64.b64decode(proof_value) + except Exception: + return None if not signature: return None diff --git a/src/dppvalidator/vocabularies/code_lists.py b/src/dppvalidator/vocabularies/code_lists.py index 20093e6..c9560c8 100644 --- a/src/dppvalidator/vocabularies/code_lists.py +++ b/src/dppvalidator/vocabularies/code_lists.py @@ -110,6 +110,65 @@ def is_textile_hs_code(code: str) -> bool: return False +# --------------------------------------------------------------------------- +# Scheme-id detectors +# +# Used by ``MaterialCodeRule`` (VOC003) and ``HSCodeRule`` (VOC004) to gate +# their checks. A ``Classification.schemeId`` is the contract a payload +# uses to declare *which* code list a code belongs to. The textile-pilot +# validators in this module check codes against UNECE Rec 46 and the HS +# textile chapters specifically — firing them on UN CPC, NACE, GS1 GPC, +# or any other classification produces false positives. The rules only +# call ``is_valid_material_code`` / ``is_valid_hs_code`` when the scheme +# id matches one of the patterns below; otherwise they skip silently. +# +# The patterns are deliberately lenient (substring match, case-folded) +# because UN/CEFACT, WCO, and CIRPASS each publish slightly different +# canonical URLs over time. Adding a new positive pattern is a one-line +# change here when fixtures surface a real-world schemeId we want to +# pick up. +# --------------------------------------------------------------------------- + + +_UNECE_REC46_SCHEME_TOKENS: tuple[str, ...] = ( + "unece-rec-46", + "unecerec46", + "rec-46", + "rec46", + "uncefact:codelist:standard:unece:material", + "vocabulary.uncefact.org/unecerec46", +) + +_HS_SCHEME_TOKENS: tuple[str, ...] = ( + "wcoomd", + "harmonized-system", + "harmonized_system", + "uncefact:codelist:standard:wco:hs", + # ``/hs/`` and ``/hs-`` cover the most common URL paths that publish + # Harmonized-System nomenclature variants; the leading slash anchors + # the match to a path segment rather than e.g. an arbitrary brand + # name that happens to contain ``hs``. + "/hs/", + "/hs-", +) + + +def is_unece_rec46_scheme(scheme_id: str | None) -> bool: + """True when ``scheme_id`` plausibly identifies UNECE Rec 46.""" + if not scheme_id: + return False + s = scheme_id.lower() + return any(tok in s for tok in _UNECE_REC46_SCHEME_TOKENS) + + +def is_hs_scheme(scheme_id: str | None) -> bool: + """True when ``scheme_id`` plausibly identifies a Harmonized System code list.""" + if not scheme_id: + return False + s = scheme_id.lower() + return any(tok in s for tok in _HS_SCHEME_TOKENS) + + def validate_gtin(gtin: str) -> bool: """Validate a GTIN (Global Trade Item Number) checksum. diff --git a/src/dppvalidator/vocabularies/data/untp-context-0.6.1.jsonld b/src/dppvalidator/vocabularies/data/untp-context-0.6.1.jsonld new file mode 100644 index 0000000..c311c34 --- /dev/null +++ b/src/dppvalidator/vocabularies/data/untp-context-0.6.1.jsonld @@ -0,0 +1,1164 @@ +{ + "@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" + } + } + } + } +} \ No newline at end of file diff --git a/src/dppvalidator/vocabularies/data/untp-context-0.7.0.jsonld b/src/dppvalidator/vocabularies/data/untp-context-0.7.0.jsonld new file mode 100644 index 0000000..bb353a3 --- /dev/null +++ b/src/dppvalidator/vocabularies/data/untp-context-0.7.0.jsonld @@ -0,0 +1,3493 @@ +{ + "@context": { + "untp": "https://vocabulary.uncefact.org/untp/", + "schema": "https://schema.org/", + "renderMethodPrefix": "https://w3id.org/vc/render-method#", + "xsd": "http://www.w3.org/2001/XMLSchema#", + "@protected": true, + "@version": 1.1, + "type": "@type", + "id": "@id", + "issuingSoftware": { + "@id": "untp:issuingSoftware", + "@type": "@id", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/untp/IssuingSoftware#", + "name": { + "@id": "schema:name" + }, + "version": { + "@id": "untp:version", + "@type": "xsd:string" + }, + "vendor": { + "@id": "untp:vendor", + "@type": "@id", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/untp/SoftwareVendor#", + "name": { + "@id": "schema:name" + } + } + } + } + }, + "DigitalProductPassport": { + "@protected": true, + "@id": "untp:DigitalProductPassport", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/untp/DigitalProductPassport#" + } + }, + "DigitalConformityCredential": { + "@protected": true, + "@id": "untp:DigitalConformityCredential", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/untp/DigitalConformityCredential#" + } + }, + "DigitalFacilityRecord": { + "@protected": true, + "@id": "untp:DigitalFacilityRecord", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/untp/DigitalFacilityRecord#" + } + }, + "DigitalIdentityAnchor": { + "@protected": true, + "@id": "untp:DigitalIdentityAnchor", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/untp/DigitalIdentityAnchor#" + } + }, + "DigitalTraceabilityEvent": { + "@protected": true, + "@id": "untp:DigitalTraceabilityEvent", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/untp/DigitalTraceabilityEvent#" + } + }, + "RenderTemplate2024": { + "@protected": true, + "@id": "untp:RenderTemplate2024", + "@context": { + "@protected": true, + "mediaQuery": { + "@id": "untp:mediaQuery", + "@type": "xsd:string" + }, + "template": { + "@id": "untp:template", + "@type": "xsd:string" + }, + "url": { + "@id": "untp:url", + "@type": "xsd:anyURI" + }, + "mediaType": { + "@id": "https://schema.org/encodingFormat" + }, + "digestMultibase": { + "@id": "https://w3id.org/security#digestMultibase", + "@type": "https://w3id.org/security#multibase" + } + } + }, + "IdentifierScheme": { + "@protected": true, + "@id": "untp:IdentifierScheme", + "@context": { + "@protected": true, + "name": { + "@id": "schema:name" + } + } + }, + "Party": { + "@protected": true, + "@id": "untp:Party", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/untp/Party#", + "name": { + "@id": "schema:name" + }, + "description": { + "@id": "schema:description" + }, + "registeredId": { + "@id": "untp:registeredId", + "@type": "xsd:string" + }, + "idScheme": { + "@id": "untp:idScheme", + "@type": "@id", + "@context": { + "@protected": true, + "id": { + "@id": "untp:id", + "@type": "xsd:anyURI" + }, + "name": { + "@id": "schema:name" + } + } + }, + "registrationCountry": { + "@protected": true, + "@id": "untp:registrationCountry", + "@context": { + "@protected": true, + "countryCode": { + "@id": "untp:countryCode", + "@type": "@vocab", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/CountryId#" + } + }, + "countryName": { + "@id": "untp:countryName", + "@type": "xsd:string" + } + } + }, + "partyAddress": { + "@protected": true, + "@id": "untp:partyAddress", + "@context": { + "@protected": true, + "streetAddress": { + "@id": "schema:streetAddress", + "@type": "xsd:string" + }, + "postalCode": { + "@id": "schema:postalCode", + "@type": "xsd:string" + }, + "addressLocality": { + "@id": "schema:addressLocality", + "@type": "xsd:string" + }, + "addressRegion": { + "@id": "schema:addressRegion", + "@type": "xsd:string" + }, + "addressCountry": { + "@protected": true, + "@id": "untp:addressCountry", + "@context": { + "@protected": true, + "countryCode": { + "@id": "untp:countryCode", + "@type": "@vocab", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/CountryId#" + } + }, + "countryName": { + "@id": "untp:countryName", + "@type": "xsd:string" + } + } + } + } + }, + "organisationWebsite": { + "@id": "untp:organisationWebsite", + "@type": "xsd:anyURI" + }, + "industryCategory": { + "@protected": true, + "@id": "untp:industryCategory", + "@context": { + "@protected": true, + "code": { + "@id": "untp:code", + "@type": "xsd:string" + }, + "name": { + "@id": "schema:name" + }, + "definition": { + "@id": "untp:definition", + "@type": "xsd:string" + }, + "schemeId": { + "@id": "untp:schemeId", + "@type": "xsd:anyURI" + }, + "schemeName": { + "@id": "untp:schemeName", + "@type": "xsd:string" + } + } + }, + "partyAlsoKnownAs": { + "@id": "untp:partyAlsoKnownAs", + "@type": "@id" + } + } + }, + "CredentialIssuer": { + "@protected": true, + "@id": "untp:CredentialIssuer", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/untp/CredentialIssuer#", + "name": { + "@id": "schema:name" + }, + "issuerAlsoKnownAs": { + "@id": "untp:issuerAlsoKnownAs", + "@type": "@id" + } + } + }, + "Entity": { + "@protected": false, + "@id": "untp:Entity", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/untp/Entity#", + "name": { + "@id": "schema:name" + }, + "description": { + "@id": "schema:description" + } + } + }, + "ConformityTopic": { + "@protected": true, + "@id": "untp:ConformityTopic", + "@context": { + "@protected": true, + "name": { + "@id": "schema:name" + }, + "definition": { + "@id": "untp:definition", + "@type": "xsd:string" + } + } + }, + "PerformanceMetric": { + "@protected": true, + "@id": "untp:PerformanceMetric", + "@context": { + "@protected": true, + "name": { + "@id": "schema:name" + }, + "description": { + "@id": "schema:description" + }, + "improvementDirection": { + "@id": "untp:improvementDirection", + "@type": "@vocab", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/untp/ImprovementIndicator#" + } + }, + "aggregationMethod": { + "@id": "untp:aggregationMethod", + "@type": "@vocab", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/untp/AggregationType#" + } + }, + "allowedUnit": { + "@id": "untp:allowedUnit", + "@type": "@vocab", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/UnitMeasureCode#" + } + } + } + }, + "Criterion": { + "@protected": true, + "@id": "untp:Criterion", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/untp/Criterion#", + "name": { + "@id": "schema:name" + }, + "description": { + "@id": "schema:description" + }, + "version": { + "@id": "untp:version", + "@type": "xsd:string" + }, + "status": { + "@id": "untp:status", + "@type": "@vocab", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/untp/CriterionStatus#" + } + }, + "documentation": { + "@id": "untp:documentation", + "@type": "xsd:anyURI" + }, + "conformityTopic": { + "@id": "untp:conformityTopic", + "@type": "@id", + "@container": "@set" + }, + "tag": { + "@id": "untp:tag", + "@type": "xsd:string" + }, + "requiredPerformance": { + "@protected": true, + "@id": "untp:requiredPerformance", + "@context": { + "@protected": true, + "metric": { + "@id": "untp:metric", + "@type": "@id" + }, + "measure": { + "@protected": true, + "@id": "untp:measure", + "@context": { + "@protected": true, + "value": { + "@id": "untp:value", + "@type": "xsd:double" + }, + "upperTolerance": { + "@id": "untp:upperTolerance", + "@type": "xsd:double" + }, + "lowerTolerance": { + "@id": "untp:lowerTolerance", + "@type": "xsd:double" + }, + "unit": { + "@id": "untp:unit", + "@type": "@vocab", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/UnitMeasureCode#" + } + } + } + }, + "score": { + "@protected": true, + "@id": "untp:score", + "@context": { + "@protected": true, + "code": { + "@id": "untp:code", + "@type": "xsd:string" + }, + "rank": { + "@id": "untp:rank", + "@type": "xsd:integer" + }, + "definition": { + "@id": "untp:definition", + "@type": "xsd:string" + } + } + } + } + } + } + }, + "Regulation": { + "@protected": true, + "@id": "untp:Regulation", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/untp/Regulation#", + "name": { + "@id": "schema:name" + }, + "description": { + "@id": "schema:description" + }, + "jurisdictionCountry": { + "@protected": true, + "@id": "untp:jurisdictionCountry", + "@context": { + "@protected": true, + "countryCode": { + "@id": "untp:countryCode", + "@type": "@vocab", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/CountryId#" + } + }, + "countryName": { + "@id": "untp:countryName", + "@type": "xsd:string" + } + } + }, + "administeredBy": { + "@id": "untp:administeredBy", + "@type": "@id" + }, + "effectiveDate": { + "@id": "untp:effectiveDate", + "@type": "xsd:date" + } + } + }, + "Standard": { + "@protected": true, + "@id": "untp:Standard", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/untp/Standard#", + "name": { + "@id": "schema:name" + }, + "description": { + "@id": "schema:description" + }, + "issuingParty": { + "@id": "untp:issuingParty", + "@type": "@id" + }, + "issueDate": { + "@id": "untp:issueDate", + "@type": "xsd:date" + } + } + }, + "Claim": { + "@protected": true, + "@id": "untp:Claim", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/untp/Claim#", + "name": { + "@id": "schema:name" + }, + "description": { + "@id": "schema:description" + }, + "referenceCriteria": { + "@id": "untp:referenceCriteria", + "@type": "@id" + }, + "referenceRegulation": { + "@id": "untp:referenceRegulation", + "@type": "@id" + }, + "referenceStandard": { + "@id": "untp:referenceStandard", + "@type": "@id" + }, + "claimDate": { + "@id": "untp:claimDate", + "@type": "xsd:date" + }, + "applicablePeriod": { + "@protected": true, + "@id": "untp:applicablePeriod", + "@context": { + "@protected": true, + "startDate": { + "@id": "untp:startDate", + "@type": "xsd:date" + }, + "endDate": { + "@id": "untp:endDate", + "@type": "xsd:date" + }, + "periodInformation": { + "@id": "untp:periodInformation", + "@type": "xsd:string" + } + } + }, + "claimedPerformance": { + "@protected": true, + "@id": "untp:claimedPerformance", + "@context": { + "@protected": true, + "metric": { + "@id": "untp:metric", + "@type": "@id" + }, + "measure": { + "@protected": true, + "@id": "untp:measure", + "@context": { + "@protected": true, + "value": { + "@id": "untp:value", + "@type": "xsd:double" + }, + "upperTolerance": { + "@id": "untp:upperTolerance", + "@type": "xsd:double" + }, + "lowerTolerance": { + "@id": "untp:lowerTolerance", + "@type": "xsd:double" + }, + "unit": { + "@id": "untp:unit", + "@type": "@vocab", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/UnitMeasureCode#" + } + } + } + }, + "score": { + "@protected": true, + "@id": "untp:score", + "@context": { + "@protected": true, + "code": { + "@id": "untp:code", + "@type": "xsd:string" + }, + "rank": { + "@id": "untp:rank", + "@type": "xsd:integer" + }, + "definition": { + "@id": "untp:definition", + "@type": "xsd:string" + } + } + } + } + }, + "evidence": { + "@protected": true, + "@id": "untp:evidence", + "@context": { + "@protected": true, + "linkURL": { + "@id": "untp:linkURL", + "@type": "xsd:anyURI" + }, + "linkName": { + "@id": "untp:linkName", + "@type": "xsd:string" + }, + "digestMultibase": { + "@id": "https://w3id.org/security#digestMultibase", + "@type": "https://w3id.org/security#multibase" + }, + "mediaType": { + "@id": "https://schema.org/encodingFormat" + }, + "linkType": { + "@id": "untp:linkType", + "@type": "xsd:string" + } + } + }, + "conformityTopic": { + "@id": "untp:conformityTopic", + "@type": "@id", + "@container": "@set" + } + } + }, + "Facility": { + "@protected": true, + "@id": "untp:Facility", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/untp/Facility#", + "name": { + "@id": "schema:name" + }, + "description": { + "@id": "schema:description" + }, + "registeredId": { + "@id": "untp:registeredId", + "@type": "xsd:string" + }, + "idScheme": { + "@id": "untp:idScheme", + "@type": "@id", + "@context": { + "@protected": true, + "id": { + "@id": "untp:id", + "@type": "xsd:anyURI" + }, + "name": { + "@id": "schema:name" + } + } + }, + "countryOfOperation": { + "@protected": true, + "@id": "untp:countryOfOperation", + "@context": { + "@protected": true, + "countryCode": { + "@id": "untp:countryCode", + "@type": "@vocab", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/CountryId#" + } + }, + "countryName": { + "@id": "untp:countryName", + "@type": "xsd:string" + } + } + }, + "processCategory": { + "@protected": true, + "@id": "untp:processCategory", + "@context": { + "@protected": true, + "code": { + "@id": "untp:code", + "@type": "xsd:string" + }, + "name": { + "@id": "schema:name" + }, + "definition": { + "@id": "untp:definition", + "@type": "xsd:string" + }, + "schemeId": { + "@id": "untp:schemeId", + "@type": "xsd:anyURI" + }, + "schemeName": { + "@id": "untp:schemeName", + "@type": "xsd:string" + } + } + }, + "relatedParty": { + "@protected": true, + "@id": "untp:relatedParty", + "@context": { + "@protected": true, + "role": { + "@id": "untp:role", + "@type": "@vocab", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/untp/PartyRole#" + } + }, + "party": { + "@id": "untp:party", + "@type": "@id" + } + } + }, + "relatedDocument": { + "@protected": true, + "@id": "untp:relatedDocument", + "@context": { + "@protected": true, + "linkURL": { + "@id": "untp:linkURL", + "@type": "xsd:anyURI" + }, + "linkName": { + "@id": "untp:linkName", + "@type": "xsd:string" + }, + "digestMultibase": { + "@id": "https://w3id.org/security#digestMultibase", + "@type": "https://w3id.org/security#multibase" + }, + "mediaType": { + "@id": "https://schema.org/encodingFormat" + }, + "linkType": { + "@id": "untp:linkType", + "@type": "xsd:string" + } + } + }, + "facilityAlsoKnownAs": { + "@id": "untp:facilityAlsoKnownAs", + "@type": "@id" + }, + "locationInformation": { + "@protected": true, + "@id": "untp:locationInformation", + "@context": { + "@protected": true, + "plusCode": { + "@id": "untp:plusCode", + "@type": "xsd:anyURI" + }, + "geoLocation": { + "@protected": true, + "@id": "untp:geoLocation", + "@context": { + "@protected": true, + "latitude": { + "@id": "untp:latitude", + "@type": "xsd:double" + }, + "longitude": { + "@id": "untp:longitude", + "@type": "xsd:double" + } + } + }, + "geoBoundary": { + "@protected": true, + "@id": "untp:geoBoundary", + "@context": { + "@protected": true, + "latitude": { + "@id": "untp:latitude", + "@type": "xsd:double" + }, + "longitude": { + "@id": "untp:longitude", + "@type": "xsd:double" + } + } + } + } + }, + "address": { + "@protected": true, + "@id": "untp:address", + "@context": { + "@protected": true, + "streetAddress": { + "@id": "schema:streetAddress", + "@type": "xsd:string" + }, + "postalCode": { + "@id": "schema:postalCode", + "@type": "xsd:string" + }, + "addressLocality": { + "@id": "schema:addressLocality", + "@type": "xsd:string" + }, + "addressRegion": { + "@id": "schema:addressRegion", + "@type": "xsd:string" + }, + "addressCountry": { + "@protected": true, + "@id": "untp:addressCountry", + "@context": { + "@protected": true, + "countryCode": { + "@id": "untp:countryCode", + "@type": "@vocab", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/CountryId#" + } + }, + "countryName": { + "@id": "untp:countryName", + "@type": "xsd:string" + } + } + } + } + }, + "materialUsage": { + "@protected": true, + "@id": "untp:materialUsage", + "@context": { + "@protected": true, + "applicablePeriod": { + "@protected": true, + "@id": "untp:applicablePeriod", + "@context": { + "@protected": true, + "startDate": { + "@id": "untp:startDate", + "@type": "xsd:date" + }, + "endDate": { + "@id": "untp:endDate", + "@type": "xsd:date" + }, + "periodInformation": { + "@id": "untp:periodInformation", + "@type": "xsd:string" + } + } + }, + "materialConsumed": { + "@protected": true, + "@id": "untp:materialConsumed", + "@context": { + "@protected": true, + "name": { + "@id": "schema:name" + }, + "originCountry": { + "@protected": true, + "@id": "untp:originCountry", + "@context": { + "@protected": true, + "countryCode": { + "@id": "untp:countryCode", + "@type": "@vocab", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/CountryId#" + } + }, + "countryName": { + "@id": "untp:countryName", + "@type": "xsd:string" + } + } + }, + "materialType": { + "@protected": true, + "@id": "untp:materialType", + "@context": { + "@protected": true, + "code": { + "@id": "untp:code", + "@type": "xsd:string" + }, + "name": { + "@id": "schema:name" + }, + "definition": { + "@id": "untp:definition", + "@type": "xsd:string" + }, + "schemeId": { + "@id": "untp:schemeId", + "@type": "xsd:anyURI" + }, + "schemeName": { + "@id": "untp:schemeName", + "@type": "xsd:string" + } + } + }, + "massFraction": { + "@id": "untp:massFraction", + "@type": "xsd:double" + }, + "mass": { + "@protected": true, + "@id": "untp:mass", + "@context": { + "@protected": true, + "value": { + "@id": "untp:value", + "@type": "xsd:double" + }, + "upperTolerance": { + "@id": "untp:upperTolerance", + "@type": "xsd:double" + }, + "lowerTolerance": { + "@id": "untp:lowerTolerance", + "@type": "xsd:double" + }, + "unit": { + "@id": "untp:unit", + "@type": "@vocab", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/UnitMeasureCode#" + } + } + } + }, + "recycledMassFraction": { + "@id": "untp:recycledMassFraction", + "@type": "xsd:double" + }, + "hazardous": { + "@id": "untp:hazardous", + "@type": "xsd:boolean" + }, + "symbol": { + "@protected": true, + "@id": "untp:symbol", + "@context": { + "@protected": true, + "name": { + "@id": "schema:name" + }, + "description": { + "@id": "schema:description" + }, + "imageData": { + "@id": "untp:imageData", + "@type": "xsd:string" + }, + "mediaType": { + "@id": "https://schema.org/encodingFormat" + } + } + }, + "materialSafetyInformation": { + "@protected": true, + "@id": "untp:materialSafetyInformation", + "@context": { + "@protected": true, + "linkURL": { + "@id": "untp:linkURL", + "@type": "xsd:anyURI" + }, + "linkName": { + "@id": "untp:linkName", + "@type": "xsd:string" + }, + "digestMultibase": { + "@id": "https://w3id.org/security#digestMultibase", + "@type": "https://w3id.org/security#multibase" + }, + "mediaType": { + "@id": "https://schema.org/encodingFormat" + }, + "linkType": { + "@id": "untp:linkType", + "@type": "xsd:string" + } + } + } + } + } + } + }, + "performanceClaim": { + "@id": "untp:performanceClaim", + "@type": "@id" + } + } + }, + "ConformityScheme": { + "@protected": true, + "@id": "untp:ConformityScheme", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/untp/ConformityScheme#", + "name": { + "@id": "schema:name" + }, + "description": { + "@id": "schema:description" + }, + "owner": { + "@id": "untp:owner", + "@type": "@id" + }, + "endorsementLevel": { + "@id": "untp:endorsementLevel", + "@type": "@vocab", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/untp/SchemeEndorsementLevel#" + } + }, + "endorsement": { + "@protected": true, + "@id": "untp:endorsement", + "@context": { + "@protected": true, + "name": { + "@id": "schema:name" + }, + "trustmark": { + "@protected": true, + "@id": "untp:trustmark", + "@context": { + "@protected": true, + "name": { + "@id": "schema:name" + }, + "description": { + "@id": "schema:description" + }, + "imageData": { + "@id": "untp:imageData", + "@type": "xsd:string" + }, + "mediaType": { + "@id": "https://schema.org/encodingFormat" + } + } + }, + "issuingAuthority": { + "@id": "untp:issuingAuthority", + "@type": "@id" + }, + "endorsementEvidence": { + "@protected": true, + "@id": "untp:endorsementEvidence", + "@context": { + "@protected": true, + "linkURL": { + "@id": "untp:linkURL", + "@type": "xsd:anyURI" + }, + "linkName": { + "@id": "untp:linkName", + "@type": "xsd:string" + }, + "digestMultibase": { + "@id": "https://w3id.org/security#digestMultibase", + "@type": "https://w3id.org/security#multibase" + }, + "mediaType": { + "@id": "https://schema.org/encodingFormat" + }, + "linkType": { + "@id": "untp:linkType", + "@type": "xsd:string" + } + } + } + } + }, + "schemeScoringFramework": { + "@protected": true, + "@id": "untp:schemeScoringFramework", + "@context": { + "@protected": true, + "name": { + "@id": "schema:name" + }, + "description": { + "@id": "schema:description" + }, + "score": { + "@protected": true, + "@id": "untp:score", + "@context": { + "@protected": true, + "code": { + "@id": "untp:code", + "@type": "xsd:string" + }, + "rank": { + "@id": "untp:rank", + "@type": "xsd:integer" + }, + "definition": { + "@id": "untp:definition", + "@type": "xsd:string" + } + } + } + } + }, + "documentation": { + "@id": "untp:documentation", + "@type": "xsd:anyURI" + }, + "licenseType": { + "@id": "untp:licenseType", + "@type": "@vocab", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/untp/LicenseType#" + } + }, + "establishedDate": { + "@id": "untp:establishedDate", + "@type": "xsd:date" + }, + "geographicScope": { + "@protected": true, + "@id": "untp:geographicScope", + "@context": { + "@protected": true, + "code": { + "@id": "untp:code", + "@type": "xsd:string" + }, + "name": { + "@id": "schema:name" + }, + "definition": { + "@id": "untp:definition", + "@type": "xsd:string" + }, + "schemeId": { + "@id": "untp:schemeId", + "@type": "xsd:anyURI" + }, + "schemeName": { + "@id": "untp:schemeName", + "@type": "xsd:string" + } + } + }, + "industryScope": { + "@protected": true, + "@id": "untp:industryScope", + "@context": { + "@protected": true, + "code": { + "@id": "untp:code", + "@type": "xsd:string" + }, + "name": { + "@id": "schema:name" + }, + "definition": { + "@id": "untp:definition", + "@type": "xsd:string" + }, + "schemeId": { + "@id": "untp:schemeId", + "@type": "xsd:anyURI" + }, + "schemeName": { + "@id": "untp:schemeName", + "@type": "xsd:string" + } + } + }, + "trustmark": { + "@protected": true, + "@id": "untp:trustmark", + "@context": { + "@protected": true, + "name": { + "@id": "schema:name" + }, + "description": { + "@id": "schema:description" + }, + "imageData": { + "@id": "untp:imageData", + "@type": "xsd:string" + }, + "mediaType": { + "@id": "https://schema.org/encodingFormat" + } + } + }, + "conformsTo": { + "@protected": true, + "@id": "untp:conformsTo", + "@context": { + "@protected": true, + "linkURL": { + "@id": "untp:linkURL", + "@type": "xsd:anyURI" + }, + "linkName": { + "@id": "untp:linkName", + "@type": "xsd:string" + }, + "digestMultibase": { + "@id": "https://w3id.org/security#digestMultibase", + "@type": "https://w3id.org/security#multibase" + }, + "mediaType": { + "@id": "https://schema.org/encodingFormat" + }, + "linkType": { + "@id": "untp:linkType", + "@type": "xsd:string" + } + } + }, + "includedProfile": { + "@id": "untp:includedProfile", + "@type": "@id" + } + } + }, + "ConformityProfile": { + "@protected": true, + "@id": "untp:ConformityProfile", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/untp/ConformityProfile#", + "name": { + "@id": "schema:name" + }, + "description": { + "@id": "schema:description" + }, + "version": { + "@id": "untp:version", + "@type": "xsd:string" + }, + "validFrom": { + "@id": "untp:validFrom", + "@type": "xsd:date" + }, + "status": { + "@id": "untp:status", + "@type": "@vocab", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/untp/CriterionStatus#" + } + }, + "subjectType": { + "@id": "untp:subjectType", + "@type": "@vocab", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/untp/AssessmentSubjectType#" + } + }, + "standardAlignment": { + "@protected": true, + "@id": "untp:standardAlignment", + "@context": { + "@protected": true, + "standard": { + "@id": "untp:standard", + "@type": "@id" + }, + "alignmentLevel": { + "@id": "untp:alignmentLevel", + "@type": "@vocab", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/untp/SchemeAlignmentLevel#" + } + } + } + }, + "regulatoryAlignment": { + "@protected": true, + "@id": "untp:regulatoryAlignment", + "@context": { + "@protected": true, + "regulation": { + "@id": "untp:regulation", + "@type": "@id" + }, + "alignmentLevel": { + "@id": "untp:alignmentLevel", + "@type": "@vocab", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/untp/SchemeAlignmentLevel#" + } + } + } + }, + "criterionScoringFramework": { + "@protected": true, + "@id": "untp:criterionScoringFramework", + "@context": { + "@protected": true, + "name": { + "@id": "schema:name" + }, + "description": { + "@id": "schema:description" + }, + "score": { + "@protected": true, + "@id": "untp:score", + "@context": { + "@protected": true, + "code": { + "@id": "untp:code", + "@type": "xsd:string" + }, + "rank": { + "@id": "untp:rank", + "@type": "xsd:integer" + }, + "definition": { + "@id": "untp:definition", + "@type": "xsd:string" + } + } + } + } + }, + "documentation": { + "@id": "untp:documentation", + "@type": "xsd:anyURI" + }, + "criterion": { + "@id": "untp:criterion", + "@type": "@id" + }, + "scope": { + "@protected": true, + "@id": "untp:scope", + "@context": { + "@protected": true, + "code": { + "@id": "untp:code", + "@type": "xsd:string" + }, + "name": { + "@id": "schema:name" + }, + "definition": { + "@id": "untp:definition", + "@type": "xsd:string" + }, + "schemeId": { + "@id": "untp:schemeId", + "@type": "xsd:anyURI" + }, + "schemeName": { + "@id": "untp:schemeName", + "@type": "xsd:string" + } + } + }, + "scheme": { + "@id": "untp:scheme", + "@type": "@id" + } + } + }, + "Product": { + "@protected": true, + "@id": "untp:Product", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/untp/Product#", + "name": { + "@id": "schema:name" + }, + "description": { + "@id": "schema:description" + }, + "idScheme": { + "@id": "untp:idScheme", + "@type": "@id", + "@context": { + "@protected": true, + "id": { + "@id": "untp:id", + "@type": "xsd:anyURI" + }, + "name": { + "@id": "schema:name" + } + } + }, + "modelNumber": { + "@id": "untp:modelNumber", + "@type": "xsd:string" + }, + "batchNumber": { + "@id": "untp:batchNumber", + "@type": "xsd:string" + }, + "itemNumber": { + "@id": "untp:itemNumber", + "@type": "xsd:string" + }, + "idGranularity": { + "@id": "untp:idGranularity", + "@type": "@vocab", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/untp/ProductIDGranularity#" + } + }, + "characteristics": { + "@id": "untp:characteristics", + "@context": { + "@vocab": "https://vocabulary.uncefact.org/untp/Characteristics#" + } + }, + "productImage": { + "@protected": true, + "@id": "untp:productImage", + "@context": { + "@protected": true, + "linkURL": { + "@id": "untp:linkURL", + "@type": "xsd:anyURI" + }, + "linkName": { + "@id": "untp:linkName", + "@type": "xsd:string" + }, + "digestMultibase": { + "@id": "https://w3id.org/security#digestMultibase", + "@type": "https://w3id.org/security#multibase" + }, + "mediaType": { + "@id": "https://schema.org/encodingFormat" + }, + "linkType": { + "@id": "untp:linkType", + "@type": "xsd:string" + } + } + }, + "productCategory": { + "@protected": true, + "@id": "untp:productCategory", + "@context": { + "@protected": true, + "code": { + "@id": "untp:code", + "@type": "xsd:string" + }, + "name": { + "@id": "schema:name" + }, + "definition": { + "@id": "untp:definition", + "@type": "xsd:string" + }, + "schemeId": { + "@id": "untp:schemeId", + "@type": "xsd:anyURI" + }, + "schemeName": { + "@id": "untp:schemeName", + "@type": "xsd:string" + } + } + }, + "relatedDocument": { + "@protected": true, + "@id": "untp:relatedDocument", + "@context": { + "@protected": true, + "linkURL": { + "@id": "untp:linkURL", + "@type": "xsd:anyURI" + }, + "linkName": { + "@id": "untp:linkName", + "@type": "xsd:string" + }, + "digestMultibase": { + "@id": "https://w3id.org/security#digestMultibase", + "@type": "https://w3id.org/security#multibase" + }, + "mediaType": { + "@id": "https://schema.org/encodingFormat" + }, + "linkType": { + "@id": "untp:linkType", + "@type": "xsd:string" + } + } + }, + "relatedParty": { + "@protected": true, + "@id": "untp:relatedParty", + "@context": { + "@protected": true, + "role": { + "@id": "untp:role", + "@type": "@vocab", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/untp/PartyRole#" + } + }, + "party": { + "@id": "untp:party", + "@type": "@id" + } + } + }, + "producedAtFacility": { + "@id": "untp:producedAtFacility", + "@type": "@id" + }, + "productionDate": { + "@id": "untp:productionDate", + "@type": "xsd:date" + }, + "expiryDate": { + "@id": "untp:expiryDate", + "@type": "xsd:date" + }, + "countryOfProduction": { + "@protected": true, + "@id": "untp:countryOfProduction", + "@context": { + "@protected": true, + "countryCode": { + "@id": "untp:countryCode", + "@type": "@vocab", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/CountryId#" + } + }, + "countryName": { + "@id": "untp:countryName", + "@type": "xsd:string" + } + } + }, + "dimensions": { + "@protected": true, + "@id": "untp:dimensions", + "@context": { + "@protected": true, + "weight": { + "@protected": true, + "@id": "untp:weight", + "@context": { + "@protected": true, + "value": { + "@id": "untp:value", + "@type": "xsd:double" + }, + "upperTolerance": { + "@id": "untp:upperTolerance", + "@type": "xsd:double" + }, + "lowerTolerance": { + "@id": "untp:lowerTolerance", + "@type": "xsd:double" + }, + "unit": { + "@id": "untp:unit", + "@type": "@vocab", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/UnitMeasureCode#" + } + } + } + }, + "length": { + "@protected": true, + "@id": "untp:length", + "@context": { + "@protected": true, + "value": { + "@id": "untp:value", + "@type": "xsd:double" + }, + "upperTolerance": { + "@id": "untp:upperTolerance", + "@type": "xsd:double" + }, + "lowerTolerance": { + "@id": "untp:lowerTolerance", + "@type": "xsd:double" + }, + "unit": { + "@id": "untp:unit", + "@type": "@vocab", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/UnitMeasureCode#" + } + } + } + }, + "width": { + "@protected": true, + "@id": "untp:width", + "@context": { + "@protected": true, + "value": { + "@id": "untp:value", + "@type": "xsd:double" + }, + "upperTolerance": { + "@id": "untp:upperTolerance", + "@type": "xsd:double" + }, + "lowerTolerance": { + "@id": "untp:lowerTolerance", + "@type": "xsd:double" + }, + "unit": { + "@id": "untp:unit", + "@type": "@vocab", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/UnitMeasureCode#" + } + } + } + }, + "height": { + "@protected": true, + "@id": "untp:height", + "@context": { + "@protected": true, + "value": { + "@id": "untp:value", + "@type": "xsd:double" + }, + "upperTolerance": { + "@id": "untp:upperTolerance", + "@type": "xsd:double" + }, + "lowerTolerance": { + "@id": "untp:lowerTolerance", + "@type": "xsd:double" + }, + "unit": { + "@id": "untp:unit", + "@type": "@vocab", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/UnitMeasureCode#" + } + } + } + }, + "volume": { + "@protected": true, + "@id": "untp:volume", + "@context": { + "@protected": true, + "value": { + "@id": "untp:value", + "@type": "xsd:double" + }, + "upperTolerance": { + "@id": "untp:upperTolerance", + "@type": "xsd:double" + }, + "lowerTolerance": { + "@id": "untp:lowerTolerance", + "@type": "xsd:double" + }, + "unit": { + "@id": "untp:unit", + "@type": "@vocab", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/UnitMeasureCode#" + } + } + } + } + } + }, + "materialProvenance": { + "@protected": true, + "@id": "untp:materialProvenance", + "@context": { + "@protected": true, + "name": { + "@id": "schema:name" + }, + "originCountry": { + "@protected": true, + "@id": "untp:originCountry", + "@context": { + "@protected": true, + "countryCode": { + "@id": "untp:countryCode", + "@type": "@vocab", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/CountryId#" + } + }, + "countryName": { + "@id": "untp:countryName", + "@type": "xsd:string" + } + } + }, + "materialType": { + "@protected": true, + "@id": "untp:materialType", + "@context": { + "@protected": true, + "code": { + "@id": "untp:code", + "@type": "xsd:string" + }, + "name": { + "@id": "schema:name" + }, + "definition": { + "@id": "untp:definition", + "@type": "xsd:string" + }, + "schemeId": { + "@id": "untp:schemeId", + "@type": "xsd:anyURI" + }, + "schemeName": { + "@id": "untp:schemeName", + "@type": "xsd:string" + } + } + }, + "massFraction": { + "@id": "untp:massFraction", + "@type": "xsd:double" + }, + "mass": { + "@protected": true, + "@id": "untp:mass", + "@context": { + "@protected": true, + "value": { + "@id": "untp:value", + "@type": "xsd:double" + }, + "upperTolerance": { + "@id": "untp:upperTolerance", + "@type": "xsd:double" + }, + "lowerTolerance": { + "@id": "untp:lowerTolerance", + "@type": "xsd:double" + }, + "unit": { + "@id": "untp:unit", + "@type": "@vocab", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/UnitMeasureCode#" + } + } + } + }, + "recycledMassFraction": { + "@id": "untp:recycledMassFraction", + "@type": "xsd:double" + }, + "hazardous": { + "@id": "untp:hazardous", + "@type": "xsd:boolean" + }, + "symbol": { + "@protected": true, + "@id": "untp:symbol", + "@context": { + "@protected": true, + "name": { + "@id": "schema:name" + }, + "description": { + "@id": "schema:description" + }, + "imageData": { + "@id": "untp:imageData", + "@type": "xsd:string" + }, + "mediaType": { + "@id": "https://schema.org/encodingFormat" + } + } + }, + "materialSafetyInformation": { + "@protected": true, + "@id": "untp:materialSafetyInformation", + "@context": { + "@protected": true, + "linkURL": { + "@id": "untp:linkURL", + "@type": "xsd:anyURI" + }, + "linkName": { + "@id": "untp:linkName", + "@type": "xsd:string" + }, + "digestMultibase": { + "@id": "https://w3id.org/security#digestMultibase", + "@type": "https://w3id.org/security#multibase" + }, + "mediaType": { + "@id": "https://schema.org/encodingFormat" + }, + "linkType": { + "@id": "untp:linkType", + "@type": "xsd:string" + } + } + } + } + }, + "packaging": { + "@protected": true, + "@id": "untp:packaging", + "@context": { + "@protected": true, + "description": { + "@id": "schema:description" + }, + "dimensions": { + "@protected": true, + "@id": "untp:dimensions", + "@context": { + "@protected": true, + "weight": { + "@protected": true, + "@id": "untp:weight", + "@context": { + "@protected": true, + "value": { + "@id": "untp:value", + "@type": "xsd:double" + }, + "upperTolerance": { + "@id": "untp:upperTolerance", + "@type": "xsd:double" + }, + "lowerTolerance": { + "@id": "untp:lowerTolerance", + "@type": "xsd:double" + }, + "unit": { + "@id": "untp:unit", + "@type": "@vocab", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/UnitMeasureCode#" + } + } + } + }, + "length": { + "@protected": true, + "@id": "untp:length", + "@context": { + "@protected": true, + "value": { + "@id": "untp:value", + "@type": "xsd:double" + }, + "upperTolerance": { + "@id": "untp:upperTolerance", + "@type": "xsd:double" + }, + "lowerTolerance": { + "@id": "untp:lowerTolerance", + "@type": "xsd:double" + }, + "unit": { + "@id": "untp:unit", + "@type": "@vocab", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/UnitMeasureCode#" + } + } + } + }, + "width": { + "@protected": true, + "@id": "untp:width", + "@context": { + "@protected": true, + "value": { + "@id": "untp:value", + "@type": "xsd:double" + }, + "upperTolerance": { + "@id": "untp:upperTolerance", + "@type": "xsd:double" + }, + "lowerTolerance": { + "@id": "untp:lowerTolerance", + "@type": "xsd:double" + }, + "unit": { + "@id": "untp:unit", + "@type": "@vocab", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/UnitMeasureCode#" + } + } + } + }, + "height": { + "@protected": true, + "@id": "untp:height", + "@context": { + "@protected": true, + "value": { + "@id": "untp:value", + "@type": "xsd:double" + }, + "upperTolerance": { + "@id": "untp:upperTolerance", + "@type": "xsd:double" + }, + "lowerTolerance": { + "@id": "untp:lowerTolerance", + "@type": "xsd:double" + }, + "unit": { + "@id": "untp:unit", + "@type": "@vocab", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/UnitMeasureCode#" + } + } + } + }, + "volume": { + "@protected": true, + "@id": "untp:volume", + "@context": { + "@protected": true, + "value": { + "@id": "untp:value", + "@type": "xsd:double" + }, + "upperTolerance": { + "@id": "untp:upperTolerance", + "@type": "xsd:double" + }, + "lowerTolerance": { + "@id": "untp:lowerTolerance", + "@type": "xsd:double" + }, + "unit": { + "@id": "untp:unit", + "@type": "@vocab", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/UnitMeasureCode#" + } + } + } + } + } + }, + "materialUsed": { + "@protected": true, + "@id": "untp:materialUsed", + "@context": { + "@protected": true, + "name": { + "@id": "schema:name" + }, + "originCountry": { + "@protected": true, + "@id": "untp:originCountry", + "@context": { + "@protected": true, + "countryCode": { + "@id": "untp:countryCode", + "@type": "@vocab", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/CountryId#" + } + }, + "countryName": { + "@id": "untp:countryName", + "@type": "xsd:string" + } + } + }, + "materialType": { + "@protected": true, + "@id": "untp:materialType", + "@context": { + "@protected": true, + "code": { + "@id": "untp:code", + "@type": "xsd:string" + }, + "name": { + "@id": "schema:name" + }, + "definition": { + "@id": "untp:definition", + "@type": "xsd:string" + }, + "schemeId": { + "@id": "untp:schemeId", + "@type": "xsd:anyURI" + }, + "schemeName": { + "@id": "untp:schemeName", + "@type": "xsd:string" + } + } + }, + "massFraction": { + "@id": "untp:massFraction", + "@type": "xsd:double" + }, + "mass": { + "@protected": true, + "@id": "untp:mass", + "@context": { + "@protected": true, + "value": { + "@id": "untp:value", + "@type": "xsd:double" + }, + "upperTolerance": { + "@id": "untp:upperTolerance", + "@type": "xsd:double" + }, + "lowerTolerance": { + "@id": "untp:lowerTolerance", + "@type": "xsd:double" + }, + "unit": { + "@id": "untp:unit", + "@type": "@vocab", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/UnitMeasureCode#" + } + } + } + }, + "recycledMassFraction": { + "@id": "untp:recycledMassFraction", + "@type": "xsd:double" + }, + "hazardous": { + "@id": "untp:hazardous", + "@type": "xsd:boolean" + }, + "symbol": { + "@protected": true, + "@id": "untp:symbol", + "@context": { + "@protected": true, + "name": { + "@id": "schema:name" + }, + "description": { + "@id": "schema:description" + }, + "imageData": { + "@id": "untp:imageData", + "@type": "xsd:string" + }, + "mediaType": { + "@id": "https://schema.org/encodingFormat" + } + } + }, + "materialSafetyInformation": { + "@protected": true, + "@id": "untp:materialSafetyInformation", + "@context": { + "@protected": true, + "linkURL": { + "@id": "untp:linkURL", + "@type": "xsd:anyURI" + }, + "linkName": { + "@id": "untp:linkName", + "@type": "xsd:string" + }, + "digestMultibase": { + "@id": "https://w3id.org/security#digestMultibase", + "@type": "https://w3id.org/security#multibase" + }, + "mediaType": { + "@id": "https://schema.org/encodingFormat" + }, + "linkType": { + "@id": "untp:linkType", + "@type": "xsd:string" + } + } + } + } + }, + "packageLabel": { + "@protected": true, + "@id": "untp:packageLabel", + "@context": { + "@protected": true, + "name": { + "@id": "schema:name" + }, + "description": { + "@id": "schema:description" + }, + "imageData": { + "@id": "untp:imageData", + "@type": "xsd:string" + }, + "mediaType": { + "@id": "https://schema.org/encodingFormat" + } + } + }, + "performanceClaim": { + "@id": "untp:performanceClaim", + "@type": "@id" + } + } + }, + "productLabel": { + "@protected": true, + "@id": "untp:productLabel", + "@context": { + "@protected": true, + "name": { + "@id": "schema:name" + }, + "description": { + "@id": "schema:description" + }, + "imageData": { + "@id": "untp:imageData", + "@type": "xsd:string" + }, + "mediaType": { + "@id": "https://schema.org/encodingFormat" + } + } + }, + "performanceClaim": { + "@id": "untp:performanceClaim", + "@type": "@id" + } + } + }, + "ConformityAssessment": { + "@protected": true, + "@id": "untp:ConformityAssessment", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/untp/ConformityAssessment#", + "name": { + "@id": "schema:name" + }, + "description": { + "@id": "schema:description" + }, + "assessmentCriteria": { + "@id": "untp:assessmentCriteria", + "@type": "@id" + }, + "assessmentDate": { + "@id": "untp:assessmentDate", + "@type": "xsd:date" + }, + "assessedPerformance": { + "@protected": true, + "@id": "untp:assessedPerformance", + "@context": { + "@protected": true, + "metric": { + "@id": "untp:metric", + "@type": "@id" + }, + "measure": { + "@protected": true, + "@id": "untp:measure", + "@context": { + "@protected": true, + "value": { + "@id": "untp:value", + "@type": "xsd:double" + }, + "upperTolerance": { + "@id": "untp:upperTolerance", + "@type": "xsd:double" + }, + "lowerTolerance": { + "@id": "untp:lowerTolerance", + "@type": "xsd:double" + }, + "unit": { + "@id": "untp:unit", + "@type": "@vocab", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/UnitMeasureCode#" + } + } + } + }, + "score": { + "@protected": true, + "@id": "untp:score", + "@context": { + "@protected": true, + "code": { + "@id": "untp:code", + "@type": "xsd:string" + }, + "rank": { + "@id": "untp:rank", + "@type": "xsd:integer" + }, + "definition": { + "@id": "untp:definition", + "@type": "xsd:string" + } + } + } + } + }, + "assessedProduct": { + "@protected": true, + "@id": "untp:assessedProduct", + "@context": { + "@protected": true, + "product": { + "@id": "untp:product", + "@type": "@id" + }, + "idVerifiedByCAB": { + "@id": "untp:idVerifiedByCAB", + "@type": "xsd:boolean" + } + } + }, + "assessedFacility": { + "@protected": true, + "@id": "untp:assessedFacility", + "@context": { + "@protected": true, + "facility": { + "@id": "untp:facility", + "@type": "@id" + }, + "idVerifiedByCAB": { + "@id": "untp:idVerifiedByCAB", + "@type": "xsd:boolean" + } + } + }, + "assessedOrganisation": { + "@id": "untp:assessedOrganisation", + "@type": "@id" + }, + "referenceStandard": { + "@id": "untp:referenceStandard", + "@type": "@id" + }, + "referenceRegulation": { + "@id": "untp:referenceRegulation", + "@type": "@id" + }, + "specifiedCondition": { + "@id": "untp:specifiedCondition", + "@type": "xsd:string" + }, + "evidence": { + "@protected": true, + "@id": "untp:evidence", + "@context": { + "@protected": true, + "linkURL": { + "@id": "untp:linkURL", + "@type": "xsd:anyURI" + }, + "linkName": { + "@id": "untp:linkName", + "@type": "xsd:string" + }, + "digestMultibase": { + "@id": "https://w3id.org/security#digestMultibase", + "@type": "https://w3id.org/security#multibase" + }, + "mediaType": { + "@id": "https://schema.org/encodingFormat" + }, + "linkType": { + "@id": "untp:linkType", + "@type": "xsd:string" + } + } + }, + "conformityTopic": { + "@id": "untp:conformityTopic", + "@type": "@id", + "@container": "@set" + }, + "conformance": { + "@id": "untp:conformance", + "@type": "xsd:boolean" + } + } + }, + "ConformityAttestation": { + "@protected": true, + "@id": "untp:ConformityAttestation", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/untp/ConformityAttestation#", + "name": { + "@id": "schema:name" + }, + "description": { + "@id": "schema:description" + }, + "assessorLevel": { + "@id": "untp:assessorLevel", + "@type": "@vocab", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/untp/AssessorLevel#" + } + }, + "assessmentLevel": { + "@id": "untp:assessmentLevel", + "@type": "@vocab", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/untp/AssessmentLevel#" + } + }, + "attestationType": { + "@id": "untp:attestationType", + "@type": "@vocab", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/untp/AttestationType#" + } + }, + "issuedToParty": { + "@id": "untp:issuedToParty", + "@type": "@id" + }, + "authorisation": { + "@protected": true, + "@id": "untp:authorisation", + "@context": { + "@protected": true, + "name": { + "@id": "schema:name" + }, + "trustmark": { + "@protected": true, + "@id": "untp:trustmark", + "@context": { + "@protected": true, + "name": { + "@id": "schema:name" + }, + "description": { + "@id": "schema:description" + }, + "imageData": { + "@id": "untp:imageData", + "@type": "xsd:string" + }, + "mediaType": { + "@id": "https://schema.org/encodingFormat" + } + } + }, + "issuingAuthority": { + "@id": "untp:issuingAuthority", + "@type": "@id" + }, + "endorsementEvidence": { + "@protected": true, + "@id": "untp:endorsementEvidence", + "@context": { + "@protected": true, + "linkURL": { + "@id": "untp:linkURL", + "@type": "xsd:anyURI" + }, + "linkName": { + "@id": "untp:linkName", + "@type": "xsd:string" + }, + "digestMultibase": { + "@id": "https://w3id.org/security#digestMultibase", + "@type": "https://w3id.org/security#multibase" + }, + "mediaType": { + "@id": "https://schema.org/encodingFormat" + }, + "linkType": { + "@id": "untp:linkType", + "@type": "xsd:string" + } + } + } + } + }, + "referenceScheme": { + "@id": "untp:referenceScheme", + "@type": "@id" + }, + "referenceProfile": { + "@id": "untp:referenceProfile", + "@type": "@id" + }, + "profileScore": { + "@protected": true, + "@id": "untp:profileScore", + "@context": { + "@protected": true, + "code": { + "@id": "untp:code", + "@type": "xsd:string" + }, + "rank": { + "@id": "untp:rank", + "@type": "xsd:integer" + }, + "definition": { + "@id": "untp:definition", + "@type": "xsd:string" + } + } + }, + "conformityCertificate": { + "@protected": true, + "@id": "untp:conformityCertificate", + "@context": { + "@protected": true, + "linkURL": { + "@id": "untp:linkURL", + "@type": "xsd:anyURI" + }, + "linkName": { + "@id": "untp:linkName", + "@type": "xsd:string" + }, + "digestMultibase": { + "@id": "https://w3id.org/security#digestMultibase", + "@type": "https://w3id.org/security#multibase" + }, + "mediaType": { + "@id": "https://schema.org/encodingFormat" + }, + "linkType": { + "@id": "untp:linkType", + "@type": "xsd:string" + } + } + }, + "auditableEvidence": { + "@protected": true, + "@id": "untp:auditableEvidence", + "@context": { + "@protected": true, + "linkURL": { + "@id": "untp:linkURL", + "@type": "xsd:anyURI" + }, + "linkName": { + "@id": "untp:linkName", + "@type": "xsd:string" + }, + "digestMultibase": { + "@id": "https://w3id.org/security#digestMultibase", + "@type": "https://w3id.org/security#multibase" + }, + "mediaType": { + "@id": "https://schema.org/encodingFormat" + }, + "linkType": { + "@id": "untp:linkType", + "@type": "xsd:string" + } + } + }, + "trustmark": { + "@protected": true, + "@id": "untp:trustmark", + "@context": { + "@protected": true, + "name": { + "@id": "schema:name" + }, + "description": { + "@id": "schema:description" + }, + "imageData": { + "@id": "untp:imageData", + "@type": "xsd:string" + }, + "mediaType": { + "@id": "https://schema.org/encodingFormat" + } + } + }, + "conformityAssessment": { + "@id": "untp:conformityAssessment", + "@type": "@id" + } + } + }, + "LifecycleEvent": { + "@protected": true, + "@id": "untp:LifecycleEvent", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/untp/LifecycleEvent#", + "name": { + "@id": "schema:name" + }, + "description": { + "@id": "schema:description" + }, + "eventDate": { + "@id": "untp:eventDate", + "@type": "xsd:datetime" + }, + "sensorData": { + "@protected": true, + "@id": "untp:sensorData", + "@context": { + "@protected": true, + "metric": { + "@id": "untp:metric", + "@type": "@id" + }, + "measure": { + "@protected": true, + "@id": "untp:measure", + "@context": { + "@protected": true, + "value": { + "@id": "untp:value", + "@type": "xsd:double" + }, + "upperTolerance": { + "@id": "untp:upperTolerance", + "@type": "xsd:double" + }, + "lowerTolerance": { + "@id": "untp:lowerTolerance", + "@type": "xsd:double" + }, + "unit": { + "@id": "untp:unit", + "@type": "@vocab", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/UnitMeasureCode#" + } + } + } + }, + "rawData": { + "@protected": true, + "@id": "untp:rawData", + "@context": { + "@protected": true, + "linkURL": { + "@id": "untp:linkURL", + "@type": "xsd:anyURI" + }, + "linkName": { + "@id": "untp:linkName", + "@type": "xsd:string" + }, + "digestMultibase": { + "@id": "https://w3id.org/security#digestMultibase", + "@type": "https://w3id.org/security#multibase" + }, + "mediaType": { + "@id": "https://schema.org/encodingFormat" + }, + "linkType": { + "@id": "untp:linkType", + "@type": "xsd:string" + } + } + }, + "sensor": { + "@id": "untp:sensor", + "@type": "@id" + }, + "geoLocation": { + "@protected": true, + "@id": "untp:geoLocation", + "@context": { + "@protected": true, + "latitude": { + "@id": "untp:latitude", + "@type": "xsd:double" + }, + "longitude": { + "@id": "untp:longitude", + "@type": "xsd:double" + } + } + } + } + }, + "relatedDocument": { + "@protected": true, + "@id": "untp:relatedDocument", + "@context": { + "@protected": true, + "linkURL": { + "@id": "untp:linkURL", + "@type": "xsd:anyURI" + }, + "linkName": { + "@id": "untp:linkName", + "@type": "xsd:string" + }, + "digestMultibase": { + "@id": "https://w3id.org/security#digestMultibase", + "@type": "https://w3id.org/security#multibase" + }, + "mediaType": { + "@id": "https://schema.org/encodingFormat" + }, + "linkType": { + "@id": "untp:linkType", + "@type": "xsd:string" + } + } + }, + "activityType": { + "@protected": true, + "@id": "untp:activityType", + "@context": { + "@protected": true, + "code": { + "@id": "untp:code", + "@type": "xsd:string" + }, + "name": { + "@id": "schema:name" + }, + "definition": { + "@id": "untp:definition", + "@type": "xsd:string" + }, + "schemeId": { + "@id": "untp:schemeId", + "@type": "xsd:anyURI" + }, + "schemeName": { + "@id": "untp:schemeName", + "@type": "xsd:string" + } + } + }, + "relatedParty": { + "@protected": true, + "@id": "untp:relatedParty", + "@context": { + "@protected": true, + "role": { + "@id": "untp:role", + "@type": "@vocab", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/untp/PartyRole#" + } + }, + "party": { + "@id": "untp:party", + "@type": "@id" + } + } + } + } + }, + "MakeEvent": { + "@protected": true, + "@id": "untp:MakeEvent", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/untp/MakeEvent#", + "name": { + "@id": "schema:name" + }, + "description": { + "@id": "schema:description" + }, + "eventDate": { + "@id": "untp:eventDate", + "@type": "xsd:datetime" + }, + "sensorData": { + "@protected": true, + "@id": "untp:sensorData", + "@context": { + "@protected": true, + "metric": { + "@id": "untp:metric", + "@type": "@id" + }, + "measure": { + "@protected": true, + "@id": "untp:measure", + "@context": { + "@protected": true, + "value": { + "@id": "untp:value", + "@type": "xsd:double" + }, + "upperTolerance": { + "@id": "untp:upperTolerance", + "@type": "xsd:double" + }, + "lowerTolerance": { + "@id": "untp:lowerTolerance", + "@type": "xsd:double" + }, + "unit": { + "@id": "untp:unit", + "@type": "@vocab", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/UnitMeasureCode#" + } + } + } + }, + "rawData": { + "@protected": true, + "@id": "untp:rawData", + "@context": { + "@protected": true, + "linkURL": { + "@id": "untp:linkURL", + "@type": "xsd:anyURI" + }, + "linkName": { + "@id": "untp:linkName", + "@type": "xsd:string" + }, + "digestMultibase": { + "@id": "https://w3id.org/security#digestMultibase", + "@type": "https://w3id.org/security#multibase" + }, + "mediaType": { + "@id": "https://schema.org/encodingFormat" + }, + "linkType": { + "@id": "untp:linkType", + "@type": "xsd:string" + } + } + }, + "sensor": { + "@id": "untp:sensor", + "@type": "@id" + }, + "geoLocation": { + "@protected": true, + "@id": "untp:geoLocation", + "@context": { + "@protected": true, + "latitude": { + "@id": "untp:latitude", + "@type": "xsd:double" + }, + "longitude": { + "@id": "untp:longitude", + "@type": "xsd:double" + } + } + } + } + }, + "relatedDocument": { + "@protected": true, + "@id": "untp:relatedDocument", + "@context": { + "@protected": true, + "linkURL": { + "@id": "untp:linkURL", + "@type": "xsd:anyURI" + }, + "linkName": { + "@id": "untp:linkName", + "@type": "xsd:string" + }, + "digestMultibase": { + "@id": "https://w3id.org/security#digestMultibase", + "@type": "https://w3id.org/security#multibase" + }, + "mediaType": { + "@id": "https://schema.org/encodingFormat" + }, + "linkType": { + "@id": "untp:linkType", + "@type": "xsd:string" + } + } + }, + "activityType": { + "@protected": true, + "@id": "untp:activityType", + "@context": { + "@protected": true, + "code": { + "@id": "untp:code", + "@type": "xsd:string" + }, + "name": { + "@id": "schema:name" + }, + "definition": { + "@id": "untp:definition", + "@type": "xsd:string" + }, + "schemeId": { + "@id": "untp:schemeId", + "@type": "xsd:anyURI" + }, + "schemeName": { + "@id": "untp:schemeName", + "@type": "xsd:string" + } + } + }, + "relatedParty": { + "@protected": true, + "@id": "untp:relatedParty", + "@context": { + "@protected": true, + "role": { + "@id": "untp:role", + "@type": "@vocab", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/untp/PartyRole#" + } + }, + "party": { + "@id": "untp:party", + "@type": "@id" + } + } + }, + "inputProduct": { + "@protected": true, + "@id": "untp:inputProduct", + "@context": { + "@protected": true, + "product": { + "@id": "untp:product", + "@type": "@id" + }, + "quantity": { + "@protected": true, + "@id": "untp:quantity", + "@context": { + "@protected": true, + "value": { + "@id": "untp:value", + "@type": "xsd:double" + }, + "upperTolerance": { + "@id": "untp:upperTolerance", + "@type": "xsd:double" + }, + "lowerTolerance": { + "@id": "untp:lowerTolerance", + "@type": "xsd:double" + }, + "unit": { + "@id": "untp:unit", + "@type": "@vocab", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/UnitMeasureCode#" + } + } + } + }, + "disposition": { + "@id": "untp:disposition", + "@type": "@vocab", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/untp/ProductStatus#" + } + } + } + }, + "outputProduct": { + "@protected": true, + "@id": "untp:outputProduct", + "@context": { + "@protected": true, + "product": { + "@id": "untp:product", + "@type": "@id" + }, + "quantity": { + "@protected": true, + "@id": "untp:quantity", + "@context": { + "@protected": true, + "value": { + "@id": "untp:value", + "@type": "xsd:double" + }, + "upperTolerance": { + "@id": "untp:upperTolerance", + "@type": "xsd:double" + }, + "lowerTolerance": { + "@id": "untp:lowerTolerance", + "@type": "xsd:double" + }, + "unit": { + "@id": "untp:unit", + "@type": "@vocab", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/UnitMeasureCode#" + } + } + } + }, + "disposition": { + "@id": "untp:disposition", + "@type": "@vocab", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/untp/ProductStatus#" + } + } + } + }, + "madeAtFacility": { + "@id": "untp:madeAtFacility", + "@type": "@id" + } + } + }, + "MoveEvent": { + "@protected": true, + "@id": "untp:MoveEvent", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/untp/MoveEvent#", + "name": { + "@id": "schema:name" + }, + "description": { + "@id": "schema:description" + }, + "eventDate": { + "@id": "untp:eventDate", + "@type": "xsd:datetime" + }, + "sensorData": { + "@protected": true, + "@id": "untp:sensorData", + "@context": { + "@protected": true, + "metric": { + "@id": "untp:metric", + "@type": "@id" + }, + "measure": { + "@protected": true, + "@id": "untp:measure", + "@context": { + "@protected": true, + "value": { + "@id": "untp:value", + "@type": "xsd:double" + }, + "upperTolerance": { + "@id": "untp:upperTolerance", + "@type": "xsd:double" + }, + "lowerTolerance": { + "@id": "untp:lowerTolerance", + "@type": "xsd:double" + }, + "unit": { + "@id": "untp:unit", + "@type": "@vocab", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/UnitMeasureCode#" + } + } + } + }, + "rawData": { + "@protected": true, + "@id": "untp:rawData", + "@context": { + "@protected": true, + "linkURL": { + "@id": "untp:linkURL", + "@type": "xsd:anyURI" + }, + "linkName": { + "@id": "untp:linkName", + "@type": "xsd:string" + }, + "digestMultibase": { + "@id": "https://w3id.org/security#digestMultibase", + "@type": "https://w3id.org/security#multibase" + }, + "mediaType": { + "@id": "https://schema.org/encodingFormat" + }, + "linkType": { + "@id": "untp:linkType", + "@type": "xsd:string" + } + } + }, + "sensor": { + "@id": "untp:sensor", + "@type": "@id" + }, + "geoLocation": { + "@protected": true, + "@id": "untp:geoLocation", + "@context": { + "@protected": true, + "latitude": { + "@id": "untp:latitude", + "@type": "xsd:double" + }, + "longitude": { + "@id": "untp:longitude", + "@type": "xsd:double" + } + } + } + } + }, + "relatedDocument": { + "@protected": true, + "@id": "untp:relatedDocument", + "@context": { + "@protected": true, + "linkURL": { + "@id": "untp:linkURL", + "@type": "xsd:anyURI" + }, + "linkName": { + "@id": "untp:linkName", + "@type": "xsd:string" + }, + "digestMultibase": { + "@id": "https://w3id.org/security#digestMultibase", + "@type": "https://w3id.org/security#multibase" + }, + "mediaType": { + "@id": "https://schema.org/encodingFormat" + }, + "linkType": { + "@id": "untp:linkType", + "@type": "xsd:string" + } + } + }, + "activityType": { + "@protected": true, + "@id": "untp:activityType", + "@context": { + "@protected": true, + "code": { + "@id": "untp:code", + "@type": "xsd:string" + }, + "name": { + "@id": "schema:name" + }, + "definition": { + "@id": "untp:definition", + "@type": "xsd:string" + }, + "schemeId": { + "@id": "untp:schemeId", + "@type": "xsd:anyURI" + }, + "schemeName": { + "@id": "untp:schemeName", + "@type": "xsd:string" + } + } + }, + "relatedParty": { + "@protected": true, + "@id": "untp:relatedParty", + "@context": { + "@protected": true, + "role": { + "@id": "untp:role", + "@type": "@vocab", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/untp/PartyRole#" + } + }, + "party": { + "@id": "untp:party", + "@type": "@id" + } + } + }, + "movedProduct": { + "@protected": true, + "@id": "untp:movedProduct", + "@context": { + "@protected": true, + "product": { + "@id": "untp:product", + "@type": "@id" + }, + "quantity": { + "@protected": true, + "@id": "untp:quantity", + "@context": { + "@protected": true, + "value": { + "@id": "untp:value", + "@type": "xsd:double" + }, + "upperTolerance": { + "@id": "untp:upperTolerance", + "@type": "xsd:double" + }, + "lowerTolerance": { + "@id": "untp:lowerTolerance", + "@type": "xsd:double" + }, + "unit": { + "@id": "untp:unit", + "@type": "@vocab", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/UnitMeasureCode#" + } + } + } + }, + "disposition": { + "@id": "untp:disposition", + "@type": "@vocab", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/untp/ProductStatus#" + } + } + } + }, + "fromFacility": { + "@id": "untp:fromFacility", + "@type": "@id" + }, + "toFacility": { + "@id": "untp:toFacility", + "@type": "@id" + }, + "consignmentId": { + "@id": "untp:consignmentId", + "@type": "xsd:anyURI" + } + } + }, + "ModifyEvent": { + "@protected": true, + "@id": "untp:ModifyEvent", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/untp/ModifyEvent#", + "name": { + "@id": "schema:name" + }, + "description": { + "@id": "schema:description" + }, + "eventDate": { + "@id": "untp:eventDate", + "@type": "xsd:datetime" + }, + "sensorData": { + "@protected": true, + "@id": "untp:sensorData", + "@context": { + "@protected": true, + "metric": { + "@id": "untp:metric", + "@type": "@id" + }, + "measure": { + "@protected": true, + "@id": "untp:measure", + "@context": { + "@protected": true, + "value": { + "@id": "untp:value", + "@type": "xsd:double" + }, + "upperTolerance": { + "@id": "untp:upperTolerance", + "@type": "xsd:double" + }, + "lowerTolerance": { + "@id": "untp:lowerTolerance", + "@type": "xsd:double" + }, + "unit": { + "@id": "untp:unit", + "@type": "@vocab", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/UnitMeasureCode#" + } + } + } + }, + "rawData": { + "@protected": true, + "@id": "untp:rawData", + "@context": { + "@protected": true, + "linkURL": { + "@id": "untp:linkURL", + "@type": "xsd:anyURI" + }, + "linkName": { + "@id": "untp:linkName", + "@type": "xsd:string" + }, + "digestMultibase": { + "@id": "https://w3id.org/security#digestMultibase", + "@type": "https://w3id.org/security#multibase" + }, + "mediaType": { + "@id": "https://schema.org/encodingFormat" + }, + "linkType": { + "@id": "untp:linkType", + "@type": "xsd:string" + } + } + }, + "sensor": { + "@id": "untp:sensor", + "@type": "@id" + }, + "geoLocation": { + "@protected": true, + "@id": "untp:geoLocation", + "@context": { + "@protected": true, + "latitude": { + "@id": "untp:latitude", + "@type": "xsd:double" + }, + "longitude": { + "@id": "untp:longitude", + "@type": "xsd:double" + } + } + } + } + }, + "relatedDocument": { + "@protected": true, + "@id": "untp:relatedDocument", + "@context": { + "@protected": true, + "linkURL": { + "@id": "untp:linkURL", + "@type": "xsd:anyURI" + }, + "linkName": { + "@id": "untp:linkName", + "@type": "xsd:string" + }, + "digestMultibase": { + "@id": "https://w3id.org/security#digestMultibase", + "@type": "https://w3id.org/security#multibase" + }, + "mediaType": { + "@id": "https://schema.org/encodingFormat" + }, + "linkType": { + "@id": "untp:linkType", + "@type": "xsd:string" + } + } + }, + "activityType": { + "@protected": true, + "@id": "untp:activityType", + "@context": { + "@protected": true, + "code": { + "@id": "untp:code", + "@type": "xsd:string" + }, + "name": { + "@id": "schema:name" + }, + "definition": { + "@id": "untp:definition", + "@type": "xsd:string" + }, + "schemeId": { + "@id": "untp:schemeId", + "@type": "xsd:anyURI" + }, + "schemeName": { + "@id": "untp:schemeName", + "@type": "xsd:string" + } + } + }, + "relatedParty": { + "@protected": true, + "@id": "untp:relatedParty", + "@context": { + "@protected": true, + "role": { + "@id": "untp:role", + "@type": "@vocab", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/untp/PartyRole#" + } + }, + "party": { + "@id": "untp:party", + "@type": "@id" + } + } + }, + "modifiedProduct": { + "@protected": true, + "@id": "untp:modifiedProduct", + "@context": { + "@protected": true, + "product": { + "@id": "untp:product", + "@type": "@id" + }, + "quantity": { + "@protected": true, + "@id": "untp:quantity", + "@context": { + "@protected": true, + "value": { + "@id": "untp:value", + "@type": "xsd:double" + }, + "upperTolerance": { + "@id": "untp:upperTolerance", + "@type": "xsd:double" + }, + "lowerTolerance": { + "@id": "untp:lowerTolerance", + "@type": "xsd:double" + }, + "unit": { + "@id": "untp:unit", + "@type": "@vocab", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/UnitMeasureCode#" + } + } + } + }, + "disposition": { + "@id": "untp:disposition", + "@type": "@vocab", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/untp/ProductStatus#" + } + } + } + }, + "modifiedAtFacility": { + "@id": "untp:modifiedAtFacility", + "@type": "@id" + } + } + }, + "RegisteredIdentity": { + "@protected": true, + "@id": "untp:RegisteredIdentity", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/untp/RegisteredIdentity#", + "registeredName": { + "@id": "untp:registeredName", + "@type": "xsd:string" + }, + "registeredId": { + "@id": "untp:registeredId", + "@type": "xsd:string" + }, + "registeredDate": { + "@id": "untp:registeredDate", + "@type": "xsd:date" + }, + "publicInformation": { + "@id": "untp:publicInformation", + "@type": "xsd:anyURI" + }, + "idScheme": { + "@id": "untp:idScheme", + "@type": "@id", + "@context": { + "@protected": true, + "id": { + "@id": "untp:id", + "@type": "xsd:anyURI" + }, + "name": { + "@id": "schema:name" + } + } + }, + "registrar": { + "@id": "untp:registrar", + "@type": "@id" + }, + "registerType": { + "@id": "untp:registerType", + "@type": "@vocab", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/untp/RegistryType#" + } + }, + "registrationScope": { + "@id": "untp:registrationScope", + "@type": "xsd:anyURI" + } + } + } + } +} diff --git a/src/dppvalidator/vocabularies/data/untp-metrics.jsonld b/src/dppvalidator/vocabularies/data/untp-metrics.jsonld new file mode 100644 index 0000000..59dbd80 --- /dev/null +++ b/src/dppvalidator/vocabularies/data/untp-metrics.jsonld @@ -0,0 +1,1146 @@ +{ + "@context": { + "skos": "http://www.w3.org/2004/02/skos/core#", + "dcterms": "http://purl.org/dc/terms/", + "xsd": "http://www.w3.org/2001/XMLSchema#", + "owl": "http://www.w3.org/2002/07/owl#", + "rdfs": "http://www.w3.org/2000/01/rdf-schema#", + "untp": "https://vocabulary.uncefact.org/untp/", + "metrics": "https://vocabulary.uncefact.org/performance-metrics/", + "rec20": "https://vocabulary.uncefact.org/rec20/", + "prefLabel": { "@id": "skos:prefLabel", "@language": "en" }, + "definition": { "@id": "skos:definition", "@language": "en" }, + "notation": "skos:notation", + "scopeNote": { "@id": "skos:scopeNote", "@language": "en" }, + "broader": { "@id": "skos:broader", "@type": "@id" }, + "narrower": { "@id": "skos:narrower", "@type": "@id", "@container": "@set" }, + "topConceptOf": { "@id": "skos:topConceptOf", "@type": "@id" }, + "hasTopConcept": { "@id": "skos:hasTopConcept", "@type": "@id", "@container": "@set" }, + "inScheme": { "@id": "skos:inScheme", "@type": "@id" }, + "closeMatch": { "@id": "skos:closeMatch", "@type": "@id", "@container": "@set" }, + "allowedUnit": "untp:allowedUnit", + "aggregationMethod": "untp:aggregationMethod", + "improvementDirection": "untp:improvementDirection" + }, + "@graph": [ + { + "@id": "https://vocabulary.uncefact.org/performance-metrics/", + "@type": "skos:ConceptScheme", + "dcterms:title": { "@value": "UNTP Performance Metrics Vocabulary", "@language": "en" }, + "dcterms:description": { "@value": "A hierarchical vocabulary of standardised performance metrics for tagging fine-grained product and facility-level sustainability claims. Enables automatic roll-up to enterprise-level disclosures aligned with IFRS S1/S2, GRI, ESRS, and EU Battery Regulation. Counterpart to the UNTP Conformity Topic Classification — topics classify what is being assessed, metrics define what is measured.", "@language": "en" }, + "dcterms:creator": "United Nations Economic Commission for Europe (UNECE)", + "dcterms:license": "https://creativecommons.org/licenses/by/4.0/", + "owl:versionInfo": "0.1.0-working", + "dcterms:issued": "2026-03-13", + "dcterms:modified": "2026-03-13", + "hasTopConcept": [ + "metrics:greenhouse-gas-emissions", + "metrics:energy", + "metrics:water", + "metrics:waste-and-circularity", + "metrics:biodiversity-and-land-use", + "metrics:pollution", + "metrics:workforce", + "metrics:governance", + "metrics:product-safety-and-quality", + "metrics:food-safety-and-quality" + ] + }, + + { + "@id": "metrics:greenhouse-gas-emissions", + "@type": "skos:Concept", + "prefLabel": "Greenhouse Gas Emissions", + "definition": "Metrics for measuring, reporting, and reducing greenhouse gas emissions across all scopes, including absolute values, intensities, and reduction progress.", + "notation": "01", + "topConceptOf": "https://vocabulary.uncefact.org/performance-metrics/", + "inScheme": "https://vocabulary.uncefact.org/performance-metrics/", + "scopeNote": "IFRS S2 paras 29–36; ESRS E1; GRI 305; GHG Protocol Corporate Standard.", + "narrower": [ + "metrics:scope-1-ghg-emissions", + "metrics:scope-2-ghg-emissions", + "metrics:scope-3-upstream-emissions", + "metrics:scope-3-downstream-emissions", + "metrics:total-ghg-emissions", + "metrics:ghg-emissions-intensity", + "metrics:product-carbon-footprint", + "metrics:biogenic-emissions", + "metrics:ghg-reduction-progress" + ] + }, + { + "@id": "metrics:scope-1-ghg-emissions", + "@type": "skos:Concept", + "prefLabel": "Scope 1 GHG Emissions", + "definition": "Absolute GHG emissions from sources owned or controlled by the reporting entity, in tonnes CO2 equivalent.", + "notation": "01.01", + "broader": "metrics:greenhouse-gas-emissions", + "inScheme": "https://vocabulary.uncefact.org/performance-metrics/", + "scopeNote": "IFRS S2 para 29(a); ESRS E1-6; GRI 305-1; GHG Protocol Scope 1.", + "allowedUnit": "TNE", + "aggregationMethod": "sum", + "improvementDirection": "lower" + }, + { + "@id": "metrics:scope-2-ghg-emissions", + "@type": "skos:Concept", + "prefLabel": "Scope 2 GHG Emissions", + "definition": "Indirect GHG emissions from purchased electricity, steam, heating, and cooling consumed by the reporting entity, in tonnes CO2 equivalent.", + "notation": "01.02", + "broader": "metrics:greenhouse-gas-emissions", + "inScheme": "https://vocabulary.uncefact.org/performance-metrics/", + "scopeNote": "IFRS S2 para 29(a); ESRS E1-6; GRI 305-2; GHG Protocol Scope 2.", + "allowedUnit": "TNE", + "aggregationMethod": "sum", + "improvementDirection": "lower" + }, + { + "@id": "metrics:scope-3-upstream-emissions", + "@type": "skos:Concept", + "prefLabel": "Scope 3 Upstream Emissions", + "definition": "Indirect GHG emissions occurring in the upstream value chain including purchased goods, transportation, and business travel, in tonnes CO2 equivalent.", + "notation": "01.03", + "broader": "metrics:greenhouse-gas-emissions", + "inScheme": "https://vocabulary.uncefact.org/performance-metrics/", + "scopeNote": "IFRS S2 para 29(a); ESRS E1-6; GRI 305-3; GHG Protocol Scope 3 categories 1–8.", + "allowedUnit": "TNE", + "aggregationMethod": "sum", + "improvementDirection": "lower" + }, + { + "@id": "metrics:scope-3-downstream-emissions", + "@type": "skos:Concept", + "prefLabel": "Scope 3 Downstream Emissions", + "definition": "Indirect GHG emissions occurring in the downstream value chain including product use, end-of-life treatment, and distribution, in tonnes CO2 equivalent.", + "notation": "01.04", + "broader": "metrics:greenhouse-gas-emissions", + "inScheme": "https://vocabulary.uncefact.org/performance-metrics/", + "scopeNote": "IFRS S2 para 29(a); ESRS E1-6; GRI 305-3; GHG Protocol Scope 3 categories 9–15.", + "allowedUnit": "TNE", + "aggregationMethod": "sum", + "improvementDirection": "lower" + }, + { + "@id": "metrics:total-ghg-emissions", + "@type": "skos:Concept", + "prefLabel": "Total GHG Emissions", + "definition": "Sum of Scope 1, Scope 2, and Scope 3 greenhouse gas emissions, in tonnes CO2 equivalent.", + "notation": "01.05", + "broader": "metrics:greenhouse-gas-emissions", + "inScheme": "https://vocabulary.uncefact.org/performance-metrics/", + "scopeNote": "IFRS S2 para 29; ESRS E1-6; GRI 305.", + "allowedUnit": "TNE", + "aggregationMethod": "sum", + "improvementDirection": "lower" + }, + { + "@id": "metrics:ghg-emissions-intensity", + "@type": "skos:Concept", + "prefLabel": "GHG Emissions Intensity", + "definition": "Greenhouse gas emissions per unit of economic output or physical activity, expressed as kg CO2e per unit of measure.", + "notation": "01.06", + "broader": "metrics:greenhouse-gas-emissions", + "inScheme": "https://vocabulary.uncefact.org/performance-metrics/", + "scopeNote": "IFRS S2 para 29(b); ESRS E1-6; GRI 305-4.", + "allowedUnit": "KGM", + "aggregationMethod": "weighted-average", + "improvementDirection": "lower" + }, + { + "@id": "metrics:product-carbon-footprint", + "@type": "skos:Concept", + "prefLabel": "Product Carbon Footprint", + "definition": "Total lifecycle greenhouse gas emissions attributable to a single product unit, from raw material extraction through end-of-life, in kg CO2 equivalent.", + "notation": "01.07", + "broader": "metrics:greenhouse-gas-emissions", + "inScheme": "https://vocabulary.uncefact.org/performance-metrics/", + "scopeNote": "ISO 14067; EU PEF method; EU Battery Regulation Art. 7.", + "allowedUnit": "KGM", + "aggregationMethod": "weighted-average", + "improvementDirection": "lower" + }, + { + "@id": "metrics:biogenic-emissions", + "@type": "skos:Concept", + "prefLabel": "Biogenic Emissions", + "definition": "CO2 emissions from the combustion or biodegradation of biomass, reported separately from fossil-fuel emissions, in tonnes CO2.", + "notation": "01.08", + "broader": "metrics:greenhouse-gas-emissions", + "inScheme": "https://vocabulary.uncefact.org/performance-metrics/", + "scopeNote": "GHG Protocol Land Sector and Removals Guidance; GRI 305-1.", + "allowedUnit": "TNE", + "aggregationMethod": "sum", + "improvementDirection": "lower" + }, + { + "@id": "metrics:ghg-reduction-progress", + "@type": "skos:Concept", + "prefLabel": "GHG Reduction Target Progress", + "definition": "Percentage of committed GHG reduction target achieved, measured against a declared baseline year.", + "notation": "01.09", + "broader": "metrics:greenhouse-gas-emissions", + "inScheme": "https://vocabulary.uncefact.org/performance-metrics/", + "scopeNote": "IFRS S2 para 33; ESRS E1-4; SBTi Target Validation Protocol.", + "allowedUnit": "P1", + "aggregationMethod": "latest", + "improvementDirection": "higher" + }, + + { + "@id": "metrics:energy", + "@type": "skos:Concept", + "prefLabel": "Energy", + "definition": "Metrics for measuring energy consumption, renewable energy share, and energy efficiency across operations and supply chains.", + "notation": "02", + "topConceptOf": "https://vocabulary.uncefact.org/performance-metrics/", + "inScheme": "https://vocabulary.uncefact.org/performance-metrics/", + "scopeNote": "IFRS S2 para 29; ESRS E1; GRI 302; EU Energy Efficiency Directive.", + "narrower": [ + "metrics:total-energy-consumption", + "metrics:renewable-energy-percentage", + "metrics:energy-intensity", + "metrics:onsite-renewable-generation", + "metrics:non-renewable-energy-consumption" + ] + }, + { + "@id": "metrics:total-energy-consumption", + "@type": "skos:Concept", + "prefLabel": "Total Energy Consumption", + "definition": "Total energy consumed from all sources including fuel, electricity, heating, cooling, and steam, in megawatt hours.", + "notation": "02.01", + "broader": "metrics:energy", + "inScheme": "https://vocabulary.uncefact.org/performance-metrics/", + "scopeNote": "ESRS E1-5; GRI 302-1; ISO 50001.", + "allowedUnit": "MWH", + "aggregationMethod": "sum", + "improvementDirection": "lower" + }, + { + "@id": "metrics:renewable-energy-percentage", + "@type": "skos:Concept", + "prefLabel": "Renewable Energy Percentage", + "definition": "Share of total energy consumption sourced from renewable sources such as solar, wind, hydro, and geothermal.", + "notation": "02.02", + "broader": "metrics:energy", + "inScheme": "https://vocabulary.uncefact.org/performance-metrics/", + "scopeNote": "ESRS E1-5; GRI 302-1; RE100 reporting.", + "allowedUnit": "P1", + "aggregationMethod": "weighted-average", + "improvementDirection": "higher" + }, + { + "@id": "metrics:energy-intensity", + "@type": "skos:Concept", + "prefLabel": "Energy Intensity", + "definition": "Energy consumed per unit of economic output or physical activity, expressed as MWh per unit of measure.", + "notation": "02.03", + "broader": "metrics:energy", + "inScheme": "https://vocabulary.uncefact.org/performance-metrics/", + "scopeNote": "ESRS E1-5; GRI 302-3.", + "allowedUnit": "MWH", + "aggregationMethod": "weighted-average", + "improvementDirection": "lower" + }, + { + "@id": "metrics:onsite-renewable-generation", + "@type": "skos:Concept", + "prefLabel": "On-site Renewable Generation", + "definition": "Total renewable energy generated on-site from owned or controlled installations, in megawatt hours.", + "notation": "02.04", + "broader": "metrics:energy", + "inScheme": "https://vocabulary.uncefact.org/performance-metrics/", + "scopeNote": "GRI 302-1; RE100 reporting methodology.", + "allowedUnit": "MWH", + "aggregationMethod": "sum", + "improvementDirection": "higher" + }, + { + "@id": "metrics:non-renewable-energy-consumption", + "@type": "skos:Concept", + "prefLabel": "Non-Renewable Energy Consumption", + "definition": "Energy consumed from non-renewable sources including fossil fuels and nuclear, in megawatt hours.", + "notation": "02.05", + "broader": "metrics:energy", + "inScheme": "https://vocabulary.uncefact.org/performance-metrics/", + "scopeNote": "ESRS E1-5; GRI 302-1.", + "allowedUnit": "MWH", + "aggregationMethod": "sum", + "improvementDirection": "lower" + }, + + { + "@id": "metrics:water", + "@type": "skos:Concept", + "prefLabel": "Water", + "definition": "Metrics for measuring water withdrawal, consumption, discharge, recycling, and usage intensity across operations.", + "notation": "03", + "topConceptOf": "https://vocabulary.uncefact.org/performance-metrics/", + "inScheme": "https://vocabulary.uncefact.org/performance-metrics/", + "scopeNote": "ESRS E3; GRI 303; CEO Water Mandate; Alliance for Water Stewardship.", + "narrower": [ + "metrics:total-water-withdrawal", + "metrics:water-consumption", + "metrics:water-recycling-rate", + "metrics:water-discharge", + "metrics:water-intensity", + "metrics:water-stress-area-withdrawal" + ] + }, + { + "@id": "metrics:total-water-withdrawal", + "@type": "skos:Concept", + "prefLabel": "Total Water Withdrawal", + "definition": "Total volume of water drawn from surface, ground, sea, produced, or third-party sources, in cubic metres.", + "notation": "03.01", + "broader": "metrics:water", + "inScheme": "https://vocabulary.uncefact.org/performance-metrics/", + "scopeNote": "ESRS E3-4; GRI 303-3.", + "allowedUnit": "MTQ", + "aggregationMethod": "sum", + "improvementDirection": "lower" + }, + { + "@id": "metrics:water-consumption", + "@type": "skos:Concept", + "prefLabel": "Water Consumption", + "definition": "Volume of water withdrawn that is not returned to the original source, representing net water removed from the environment, in cubic metres.", + "notation": "03.02", + "broader": "metrics:water", + "inScheme": "https://vocabulary.uncefact.org/performance-metrics/", + "scopeNote": "ESRS E3-4; GRI 303-5.", + "allowedUnit": "MTQ", + "aggregationMethod": "sum", + "improvementDirection": "lower" + }, + { + "@id": "metrics:water-recycling-rate", + "@type": "skos:Concept", + "prefLabel": "Water Recycling Rate", + "definition": "Percentage of total water use that is recycled or reused within operations.", + "notation": "03.03", + "broader": "metrics:water", + "inScheme": "https://vocabulary.uncefact.org/performance-metrics/", + "scopeNote": "GRI 303-3; CEO Water Mandate.", + "allowedUnit": "P1", + "aggregationMethod": "weighted-average", + "improvementDirection": "higher" + }, + { + "@id": "metrics:water-discharge", + "@type": "skos:Concept", + "prefLabel": "Water Discharge", + "definition": "Total volume of effluent water discharged to surface water, groundwater, or third-party treatment, in cubic metres.", + "notation": "03.04", + "broader": "metrics:water", + "inScheme": "https://vocabulary.uncefact.org/performance-metrics/", + "scopeNote": "ESRS E3-4; GRI 303-4.", + "allowedUnit": "MTQ", + "aggregationMethod": "sum", + "improvementDirection": "lower" + }, + { + "@id": "metrics:water-intensity", + "@type": "skos:Concept", + "prefLabel": "Water Intensity", + "definition": "Water consumed per unit of economic output or physical activity, expressed as litres per unit of measure.", + "notation": "03.05", + "broader": "metrics:water", + "inScheme": "https://vocabulary.uncefact.org/performance-metrics/", + "scopeNote": "ESRS E3-4; GRI 303-5.", + "allowedUnit": "LTR", + "aggregationMethod": "weighted-average", + "improvementDirection": "lower" + }, + { + "@id": "metrics:water-stress-area-withdrawal", + "@type": "skos:Concept", + "prefLabel": "Water Stress Area Withdrawal", + "definition": "Volume of water withdrawn from areas classified as high or extremely-high baseline water stress, in cubic metres.", + "notation": "03.06", + "broader": "metrics:water", + "inScheme": "https://vocabulary.uncefact.org/performance-metrics/", + "scopeNote": "ESRS E3-4; GRI 303-3; WRI Aqueduct water stress classifications.", + "allowedUnit": "MTQ", + "aggregationMethod": "sum", + "improvementDirection": "lower" + }, + + { + "@id": "metrics:waste-and-circularity", + "@type": "skos:Concept", + "prefLabel": "Waste and Circularity", + "definition": "Metrics for measuring waste generation, diversion, recycled content, recyclability, and circular economy performance.", + "notation": "04", + "topConceptOf": "https://vocabulary.uncefact.org/performance-metrics/", + "inScheme": "https://vocabulary.uncefact.org/performance-metrics/", + "scopeNote": "ESRS E5; GRI 306; EU ESPR Art. 5–8; EU Waste Framework Directive.", + "narrower": [ + "metrics:total-waste-generated", + "metrics:hazardous-waste-generated", + "metrics:waste-diversion-rate", + "metrics:recycled-content-percentage", + "metrics:recyclability-rate", + "metrics:material-recovery-rate", + "metrics:waste-to-landfill", + "metrics:product-durability-index", + "metrics:reuse-remanufacturing-rate" + ] + }, + { + "@id": "metrics:total-waste-generated", + "@type": "skos:Concept", + "prefLabel": "Total Waste Generated", + "definition": "Total weight of hazardous and non-hazardous waste generated by operations, in tonnes.", + "notation": "04.01", + "broader": "metrics:waste-and-circularity", + "inScheme": "https://vocabulary.uncefact.org/performance-metrics/", + "scopeNote": "ESRS E5-5; GRI 306-3.", + "allowedUnit": "TNE", + "aggregationMethod": "sum", + "improvementDirection": "lower" + }, + { + "@id": "metrics:hazardous-waste-generated", + "@type": "skos:Concept", + "prefLabel": "Hazardous Waste Generated", + "definition": "Total weight of waste classified as hazardous under applicable regulations, in tonnes.", + "notation": "04.02", + "broader": "metrics:waste-and-circularity", + "inScheme": "https://vocabulary.uncefact.org/performance-metrics/", + "scopeNote": "ESRS E5-5; GRI 306-3; Basel Convention.", + "allowedUnit": "TNE", + "aggregationMethod": "sum", + "improvementDirection": "lower" + }, + { + "@id": "metrics:waste-diversion-rate", + "@type": "skos:Concept", + "prefLabel": "Waste Diversion Rate", + "definition": "Percentage of total waste diverted from landfill and incineration through recycling, composting, or other recovery methods.", + "notation": "04.03", + "broader": "metrics:waste-and-circularity", + "inScheme": "https://vocabulary.uncefact.org/performance-metrics/", + "scopeNote": "ESRS E5-5; GRI 306-4.", + "allowedUnit": "P1", + "aggregationMethod": "weighted-average", + "improvementDirection": "higher" + }, + { + "@id": "metrics:recycled-content-percentage", + "@type": "skos:Concept", + "prefLabel": "Recycled Content Percentage", + "definition": "Share of pre-consumer and post-consumer recycled material in the total weight of a product or material input.", + "notation": "04.04", + "broader": "metrics:waste-and-circularity", + "inScheme": "https://vocabulary.uncefact.org/performance-metrics/", + "scopeNote": "EU ESPR Art. 8; EU Battery Regulation Art. 8; ISO 14021; GRI 301-2.", + "allowedUnit": "P1", + "aggregationMethod": "weighted-average", + "improvementDirection": "higher" + }, + { + "@id": "metrics:recyclability-rate", + "@type": "skos:Concept", + "prefLabel": "Recyclability Rate", + "definition": "Percentage of product weight that is technically recyclable at end of life under available infrastructure.", + "notation": "04.05", + "broader": "metrics:waste-and-circularity", + "inScheme": "https://vocabulary.uncefact.org/performance-metrics/", + "scopeNote": "EU ESPR Art. 6; ISO 14021.", + "allowedUnit": "P1", + "aggregationMethod": "weighted-average", + "improvementDirection": "higher" + }, + { + "@id": "metrics:material-recovery-rate", + "@type": "skos:Concept", + "prefLabel": "Material Recovery Rate", + "definition": "Percentage of end-of-life product mass actually recovered through recycling, remanufacturing, or refurbishment processes.", + "notation": "04.06", + "broader": "metrics:waste-and-circularity", + "inScheme": "https://vocabulary.uncefact.org/performance-metrics/", + "scopeNote": "EU ESPR Art. 6; EU Waste Framework Directive Art. 11; GRI 306-4.", + "allowedUnit": "P1", + "aggregationMethod": "weighted-average", + "improvementDirection": "higher" + }, + { + "@id": "metrics:waste-to-landfill", + "@type": "skos:Concept", + "prefLabel": "Waste to Landfill", + "definition": "Total weight of waste disposed via landfill, in tonnes.", + "notation": "04.07", + "broader": "metrics:waste-and-circularity", + "inScheme": "https://vocabulary.uncefact.org/performance-metrics/", + "scopeNote": "ESRS E5-5; GRI 306-5.", + "allowedUnit": "TNE", + "aggregationMethod": "sum", + "improvementDirection": "lower" + }, + { + "@id": "metrics:product-durability-index", + "@type": "skos:Concept", + "prefLabel": "Product Durability Index", + "definition": "Expected useful life of a product under normal conditions of use, expressed in years or cycles as applicable.", + "notation": "04.08", + "broader": "metrics:waste-and-circularity", + "inScheme": "https://vocabulary.uncefact.org/performance-metrics/", + "scopeNote": "EU ESPR Art. 5 – Durability requirements; ESRS E5.", + "allowedUnit": "ANN", + "aggregationMethod": "average", + "improvementDirection": "higher" + }, + { + "@id": "metrics:reuse-remanufacturing-rate", + "@type": "skos:Concept", + "prefLabel": "Reuse and Remanufacturing Rate", + "definition": "Percentage of product units or components returned to service through reuse, refurbishment, or remanufacturing.", + "notation": "04.09", + "broader": "metrics:waste-and-circularity", + "inScheme": "https://vocabulary.uncefact.org/performance-metrics/", + "scopeNote": "EU ESPR Art. 6; EU Waste Framework Directive.", + "allowedUnit": "P1", + "aggregationMethod": "weighted-average", + "improvementDirection": "higher" + }, + + { + "@id": "metrics:biodiversity-and-land-use", + "@type": "skos:Concept", + "prefLabel": "Biodiversity and Land Use", + "definition": "Metrics for measuring deforestation-free sourcing, land-use change, biodiversity impact, and protection of sensitive areas.", + "notation": "05", + "topConceptOf": "https://vocabulary.uncefact.org/performance-metrics/", + "inScheme": "https://vocabulary.uncefact.org/performance-metrics/", + "scopeNote": "ESRS E4; GRI 304; TNFD; EU Deforestation Regulation; Kunming-Montreal Global Biodiversity Framework.", + "narrower": [ + "metrics:deforestation-free-sourcing", + "metrics:land-use-change", + "metrics:biodiversity-impact-score", + "metrics:protected-area-impact" + ] + }, + { + "@id": "metrics:deforestation-free-sourcing", + "@type": "skos:Concept", + "prefLabel": "Deforestation-Free Sourcing", + "definition": "Percentage of raw material inputs verified as sourced without associated deforestation or forest degradation after a declared cut-off date.", + "notation": "05.01", + "broader": "metrics:biodiversity-and-land-use", + "inScheme": "https://vocabulary.uncefact.org/performance-metrics/", + "scopeNote": "EU Deforestation Regulation (EUDR); ESRS E4; GRI 304.", + "allowedUnit": "P1", + "aggregationMethod": "weighted-average", + "improvementDirection": "higher" + }, + { + "@id": "metrics:land-use-change", + "@type": "skos:Concept", + "prefLabel": "Land Use Change", + "definition": "Area of natural ecosystems converted to managed land for production or extraction activities, in hectares.", + "notation": "05.02", + "broader": "metrics:biodiversity-and-land-use", + "inScheme": "https://vocabulary.uncefact.org/performance-metrics/", + "scopeNote": "ESRS E4; GRI 304-1; GHG Protocol Land Sector Guidance.", + "allowedUnit": "HAR", + "aggregationMethod": "sum", + "improvementDirection": "lower" + }, + { + "@id": "metrics:biodiversity-impact-score", + "@type": "skos:Concept", + "prefLabel": "Biodiversity Impact Score", + "definition": "Composite index quantifying the impact of operations on species diversity and ecosystem integrity, using a recognised assessment framework (e.g., STAR, BII).", + "notation": "05.03", + "broader": "metrics:biodiversity-and-land-use", + "inScheme": "https://vocabulary.uncefact.org/performance-metrics/", + "scopeNote": "TNFD LEAP approach; ESRS E4; SBTN biodiversity targets.", + "allowedUnit": "C62", + "aggregationMethod": "average", + "improvementDirection": "lower" + }, + { + "@id": "metrics:protected-area-impact", + "@type": "skos:Concept", + "prefLabel": "Protected Area Impact", + "definition": "Area of operations, sourcing, or infrastructure footprint located within or adjacent to legally protected or high-biodiversity-value areas, in hectares.", + "notation": "05.04", + "broader": "metrics:biodiversity-and-land-use", + "inScheme": "https://vocabulary.uncefact.org/performance-metrics/", + "scopeNote": "ESRS E4; GRI 304-1; IUCN Protected Area categories.", + "allowedUnit": "HAR", + "aggregationMethod": "sum", + "improvementDirection": "lower" + }, + + { + "@id": "metrics:pollution", + "@type": "skos:Concept", + "prefLabel": "Pollution", + "definition": "Metrics for measuring air pollutant emissions, hazardous substance releases, and chemical safety performance beyond GHG emissions.", + "notation": "06", + "topConceptOf": "https://vocabulary.uncefact.org/performance-metrics/", + "inScheme": "https://vocabulary.uncefact.org/performance-metrics/", + "scopeNote": "ESRS E2; GRI 305 (non-GHG); EU Industrial Emissions Directive; Stockholm Convention; Montreal Protocol.", + "narrower": [ + "metrics:sox-emissions", + "metrics:nox-emissions", + "metrics:voc-emissions", + "metrics:particulate-matter-emissions", + "metrics:substances-of-concern", + "metrics:ozone-depleting-emissions" + ] + }, + { + "@id": "metrics:sox-emissions", + "@type": "skos:Concept", + "prefLabel": "SOx Emissions", + "definition": "Total mass of sulphur oxides released to air from stationary and mobile sources, in tonnes.", + "notation": "06.01", + "broader": "metrics:pollution", + "inScheme": "https://vocabulary.uncefact.org/performance-metrics/", + "scopeNote": "ESRS E2-4; GRI 305-7; EU Industrial Emissions Directive.", + "allowedUnit": "TNE", + "aggregationMethod": "sum", + "improvementDirection": "lower" + }, + { + "@id": "metrics:nox-emissions", + "@type": "skos:Concept", + "prefLabel": "NOx Emissions", + "definition": "Total mass of nitrogen oxides released to air from combustion and industrial processes, in tonnes.", + "notation": "06.02", + "broader": "metrics:pollution", + "inScheme": "https://vocabulary.uncefact.org/performance-metrics/", + "scopeNote": "ESRS E2-4; GRI 305-7; EU Industrial Emissions Directive.", + "allowedUnit": "TNE", + "aggregationMethod": "sum", + "improvementDirection": "lower" + }, + { + "@id": "metrics:voc-emissions", + "@type": "skos:Concept", + "prefLabel": "VOC Emissions", + "definition": "Total mass of volatile organic compounds released to air from solvents, coatings, and industrial processes, in tonnes.", + "notation": "06.03", + "broader": "metrics:pollution", + "inScheme": "https://vocabulary.uncefact.org/performance-metrics/", + "scopeNote": "ESRS E2-4; GRI 305-7; EU Solvents Directive.", + "allowedUnit": "TNE", + "aggregationMethod": "sum", + "improvementDirection": "lower" + }, + { + "@id": "metrics:particulate-matter-emissions", + "@type": "skos:Concept", + "prefLabel": "Particulate Matter Emissions", + "definition": "Total mass of fine particulate matter (PM2.5 and PM10) released to air from operations, in tonnes.", + "notation": "06.04", + "broader": "metrics:pollution", + "inScheme": "https://vocabulary.uncefact.org/performance-metrics/", + "scopeNote": "ESRS E2-4; GRI 305-7; WHO Air Quality Guidelines.", + "allowedUnit": "TNE", + "aggregationMethod": "sum", + "improvementDirection": "lower" + }, + { + "@id": "metrics:substances-of-concern", + "@type": "skos:Concept", + "prefLabel": "Substances of Concern", + "definition": "Total mass of substances of concern or substances of very high concern (SVHC) present in products or released during production, in kilograms.", + "notation": "06.05", + "broader": "metrics:pollution", + "inScheme": "https://vocabulary.uncefact.org/performance-metrics/", + "scopeNote": "EU ESPR Annex I; REACH SVHC candidate list; ESRS E2.", + "allowedUnit": "KGM", + "aggregationMethod": "sum", + "improvementDirection": "lower" + }, + { + "@id": "metrics:ozone-depleting-emissions", + "@type": "skos:Concept", + "prefLabel": "Ozone-Depleting Substance Emissions", + "definition": "Total mass of ozone-depleting substances released, measured in kg CFC-11 equivalent.", + "notation": "06.06", + "broader": "metrics:pollution", + "inScheme": "https://vocabulary.uncefact.org/performance-metrics/", + "scopeNote": "GRI 305-6; Montreal Protocol; ESRS E2.", + "allowedUnit": "KGM", + "aggregationMethod": "sum", + "improvementDirection": "lower" + }, + + { + "@id": "metrics:workforce", + "@type": "skos:Concept", + "prefLabel": "Workforce", + "definition": "Metrics for measuring labour practices, workplace safety, diversity, equity, and human rights performance across operations and supply chains.", + "notation": "07", + "topConceptOf": "https://vocabulary.uncefact.org/performance-metrics/", + "inScheme": "https://vocabulary.uncefact.org/performance-metrics/", + "scopeNote": "ESRS S1, S2; GRI 401–409; IFRS S1; ILO Core Conventions; UN Guiding Principles on Business and Human Rights.", + "narrower": [ + "metrics:living-wage-coverage", + "metrics:lost-time-injury-rate", + "metrics:gender-pay-gap", + "metrics:women-in-management", + "metrics:training-hours-per-employee", + "metrics:collective-bargaining-coverage", + "metrics:employee-turnover-rate", + "metrics:child-labor-incidents", + "metrics:forced-labor-incidents", + "metrics:workforce-diversity-ratio" + ] + }, + { + "@id": "metrics:living-wage-coverage", + "@type": "skos:Concept", + "prefLabel": "Living Wage Coverage", + "definition": "Percentage of workers (including contractor and supply-chain workers in scope) receiving at least a verified living wage.", + "notation": "07.01", + "broader": "metrics:workforce", + "inScheme": "https://vocabulary.uncefact.org/performance-metrics/", + "scopeNote": "ESRS S1-10; GRI 202-1; Global Living Wage Coalition methodology.", + "allowedUnit": "P1", + "aggregationMethod": "weighted-average", + "improvementDirection": "higher" + }, + { + "@id": "metrics:lost-time-injury-rate", + "@type": "skos:Concept", + "prefLabel": "Lost Time Injury Frequency Rate", + "definition": "Number of lost-time injuries per one million hours worked, measuring workplace safety performance.", + "notation": "07.02", + "broader": "metrics:workforce", + "inScheme": "https://vocabulary.uncefact.org/performance-metrics/", + "scopeNote": "ESRS S1-14; GRI 403-9; ISO 45001.", + "allowedUnit": "C62", + "aggregationMethod": "average", + "improvementDirection": "lower" + }, + { + "@id": "metrics:gender-pay-gap", + "@type": "skos:Concept", + "prefLabel": "Gender Pay Gap", + "definition": "Difference in average compensation between male and female employees as a percentage of male average compensation.", + "notation": "07.03", + "broader": "metrics:workforce", + "inScheme": "https://vocabulary.uncefact.org/performance-metrics/", + "scopeNote": "ESRS S1-16; GRI 405-2; EU Pay Transparency Directive.", + "allowedUnit": "P1", + "aggregationMethod": "weighted-average", + "improvementDirection": "lower" + }, + { + "@id": "metrics:women-in-management", + "@type": "skos:Concept", + "prefLabel": "Women in Management", + "definition": "Percentage of management and leadership positions held by women.", + "notation": "07.04", + "broader": "metrics:workforce", + "inScheme": "https://vocabulary.uncefact.org/performance-metrics/", + "scopeNote": "ESRS S1-9; GRI 405-1.", + "allowedUnit": "P1", + "aggregationMethod": "weighted-average", + "improvementDirection": "higher" + }, + { + "@id": "metrics:training-hours-per-employee", + "@type": "skos:Concept", + "prefLabel": "Training Hours per Employee", + "definition": "Average number of hours of training and professional development provided per employee per year.", + "notation": "07.05", + "broader": "metrics:workforce", + "inScheme": "https://vocabulary.uncefact.org/performance-metrics/", + "scopeNote": "ESRS S1-13; GRI 404-1.", + "allowedUnit": "HUR", + "aggregationMethod": "average", + "improvementDirection": "higher" + }, + { + "@id": "metrics:collective-bargaining-coverage", + "@type": "skos:Concept", + "prefLabel": "Collective Bargaining Coverage", + "definition": "Percentage of employees covered by collective bargaining agreements.", + "notation": "07.06", + "broader": "metrics:workforce", + "inScheme": "https://vocabulary.uncefact.org/performance-metrics/", + "scopeNote": "ESRS S1-8; GRI 407-1; ILO Convention 98.", + "allowedUnit": "P1", + "aggregationMethod": "weighted-average", + "improvementDirection": "higher" + }, + { + "@id": "metrics:employee-turnover-rate", + "@type": "skos:Concept", + "prefLabel": "Employee Turnover Rate", + "definition": "Percentage of employees who leave the organisation voluntarily or involuntarily during the reporting period.", + "notation": "07.07", + "broader": "metrics:workforce", + "inScheme": "https://vocabulary.uncefact.org/performance-metrics/", + "scopeNote": "ESRS S1-6; GRI 401-1.", + "allowedUnit": "P1", + "aggregationMethod": "average", + "improvementDirection": "lower" + }, + { + "@id": "metrics:child-labor-incidents", + "@type": "skos:Concept", + "prefLabel": "Child Labor Incidents", + "definition": "Number of confirmed incidents of child labor identified in own operations and supply chain during the reporting period.", + "notation": "07.08", + "broader": "metrics:workforce", + "inScheme": "https://vocabulary.uncefact.org/performance-metrics/", + "scopeNote": "ESRS S1, S2; GRI 408-1; ILO Conventions 138, 182.", + "allowedUnit": "C62", + "aggregationMethod": "count", + "improvementDirection": "lower" + }, + { + "@id": "metrics:forced-labor-incidents", + "@type": "skos:Concept", + "prefLabel": "Forced Labor Incidents", + "definition": "Number of confirmed incidents of forced, bonded, or compulsory labor identified in own operations and supply chain during the reporting period.", + "notation": "07.09", + "broader": "metrics:workforce", + "inScheme": "https://vocabulary.uncefact.org/performance-metrics/", + "scopeNote": "ESRS S1, S2; GRI 409-1; ILO Conventions 29, 105.", + "allowedUnit": "C62", + "aggregationMethod": "count", + "improvementDirection": "lower" + }, + { + "@id": "metrics:workforce-diversity-ratio", + "@type": "skos:Concept", + "prefLabel": "Workforce Diversity Ratio", + "definition": "Representation of under-represented groups in the workforce as a percentage of total headcount, covering gender, ethnicity, disability, and other protected characteristics.", + "notation": "07.10", + "broader": "metrics:workforce", + "inScheme": "https://vocabulary.uncefact.org/performance-metrics/", + "scopeNote": "ESRS S1-9; GRI 405-1.", + "allowedUnit": "P1", + "aggregationMethod": "weighted-average", + "improvementDirection": "higher" + }, + + { + "@id": "metrics:governance", + "@type": "skos:Concept", + "prefLabel": "Governance", + "definition": "Metrics for measuring anti-corruption practices, supply chain due diligence, ESG disclosure quality, and grievance mechanism effectiveness.", + "notation": "08", + "topConceptOf": "https://vocabulary.uncefact.org/performance-metrics/", + "inScheme": "https://vocabulary.uncefact.org/performance-metrics/", + "scopeNote": "ESRS G1; GRI 205, 308, 414; IFRS S1; OECD Guidelines Chapter VII.", + "narrower": [ + "metrics:anti-corruption-training-coverage", + "metrics:supplier-due-diligence-coverage", + "metrics:esg-disclosure-score", + "metrics:grievance-response-rate" + ] + }, + { + "@id": "metrics:anti-corruption-training-coverage", + "@type": "skos:Concept", + "prefLabel": "Anti-Corruption Training Coverage", + "definition": "Percentage of employees and governance body members who have received anti-corruption training during the reporting period.", + "notation": "08.01", + "broader": "metrics:governance", + "inScheme": "https://vocabulary.uncefact.org/performance-metrics/", + "scopeNote": "ESRS G1-4; GRI 205-2; OECD Anti-Bribery Convention.", + "allowedUnit": "P1", + "aggregationMethod": "weighted-average", + "improvementDirection": "higher" + }, + { + "@id": "metrics:supplier-due-diligence-coverage", + "@type": "skos:Concept", + "prefLabel": "Supplier Due Diligence Coverage", + "definition": "Percentage of significant suppliers assessed against environmental and social due diligence criteria during the reporting period.", + "notation": "08.02", + "broader": "metrics:governance", + "inScheme": "https://vocabulary.uncefact.org/performance-metrics/", + "scopeNote": "ESRS G1-5; GRI 308-1, 414-1; EU CSDDD.", + "allowedUnit": "P1", + "aggregationMethod": "weighted-average", + "improvementDirection": "higher" + }, + { + "@id": "metrics:esg-disclosure-score", + "@type": "skos:Concept", + "prefLabel": "ESG Disclosure Score", + "definition": "Composite score measuring the completeness, accuracy, and timeliness of environmental, social, and governance public disclosures.", + "notation": "08.03", + "broader": "metrics:governance", + "inScheme": "https://vocabulary.uncefact.org/performance-metrics/", + "scopeNote": "IFRS S1; ESRS 1; CDP Disclosure Scoring Methodology.", + "allowedUnit": "P1", + "aggregationMethod": "latest", + "improvementDirection": "higher" + }, + { + "@id": "metrics:grievance-response-rate", + "@type": "skos:Concept", + "prefLabel": "Grievance Response Rate", + "definition": "Percentage of grievances received through formal mechanisms that were acknowledged and addressed within the defined response timeframe.", + "notation": "08.04", + "broader": "metrics:governance", + "inScheme": "https://vocabulary.uncefact.org/performance-metrics/", + "scopeNote": "ESRS S1-17, S2-11; GRI 2-25, 2-26; UN Guiding Principles Principle 31.", + "allowedUnit": "P1", + "aggregationMethod": "average", + "improvementDirection": "higher" + }, + + { + "@id": "metrics:product-safety-and-quality", + "@type": "skos:Concept", + "prefLabel": "Product Safety and Quality", + "definition": "Metrics for measuring physical, mechanical, thermal, electrical, chemical, and fire safety properties of products and materials against applicable safety standards and performance requirements.", + "notation": "09", + "topConceptOf": "https://vocabulary.uncefact.org/performance-metrics/", + "inScheme": "https://vocabulary.uncefact.org/performance-metrics/", + "scopeNote": "EU General Product Safety Regulation (EU) 2023/988; ICC International Building Code; ISO/IEC product safety standards; EU Construction Products Regulation.", + "narrower": [ + "metrics:mechanical-strength", + "metrics:impact-resistance", + "metrics:thermal-performance", + "metrics:fire-resistance-rating", + "metrics:electrical-safety-rating", + "metrics:flammability-rating", + "metrics:chemical-substance-concentration", + "metrics:noise-emission-level" + ] + }, + { + "@id": "metrics:mechanical-strength", + "@type": "skos:Concept", + "prefLabel": "Mechanical Strength", + "definition": "Tensile, compressive, or flexural strength of a material or product under specified test conditions, in megapascals.", + "notation": "09.01", + "broader": "metrics:product-safety-and-quality", + "inScheme": "https://vocabulary.uncefact.org/performance-metrics/", + "scopeNote": "ISO 527 (tensile, plastics); ISO 6892 (tensile, metals); ASTM C39 (compressive, concrete); ICC IBC structural requirements.", + "allowedUnit": "MPA", + "aggregationMethod": "minimum", + "improvementDirection": "higher" + }, + { + "@id": "metrics:impact-resistance", + "@type": "skos:Concept", + "prefLabel": "Impact Resistance", + "definition": "Energy absorbed by a material or product before fracture under impact loading, in joules.", + "notation": "09.02", + "broader": "metrics:product-safety-and-quality", + "inScheme": "https://vocabulary.uncefact.org/performance-metrics/", + "scopeNote": "ISO 179 (Charpy impact, plastics); ISO 148 (Charpy impact, metals); IEC 62262 (IK rating, equipment enclosures).", + "allowedUnit": "JOU", + "aggregationMethod": "minimum", + "improvementDirection": "higher" + }, + { + "@id": "metrics:thermal-performance", + "@type": "skos:Concept", + "prefLabel": "Thermal Performance", + "definition": "Thermal resistance (R-value) or thermal conductivity of a material or assembly, indicating its ability to insulate against heat transfer.", + "notation": "09.03", + "broader": "metrics:product-safety-and-quality", + "inScheme": "https://vocabulary.uncefact.org/performance-metrics/", + "scopeNote": "ICC International Energy Conservation Code (IECC); ISO 22007; ASTM C518; EU Energy Performance of Buildings Directive.", + "allowedUnit": "C62", + "aggregationMethod": "weighted-average", + "improvementDirection": "higher" + }, + { + "@id": "metrics:fire-resistance-rating", + "@type": "skos:Concept", + "prefLabel": "Fire Resistance Rating", + "definition": "Duration a material or assembly maintains structural integrity, insulation, and limits heat transfer under standard fire exposure conditions, in minutes.", + "notation": "09.04", + "broader": "metrics:product-safety-and-quality", + "inScheme": "https://vocabulary.uncefact.org/performance-metrics/", + "scopeNote": "ICC IBC Chapter 7; ASTM E119; ISO 834; EU Construction Products Regulation (EN 13501).", + "allowedUnit": "MIN", + "aggregationMethod": "minimum", + "improvementDirection": "higher" + }, + { + "@id": "metrics:electrical-safety-rating", + "@type": "skos:Concept", + "prefLabel": "Electrical Safety Rating", + "definition": "Composite test result or classification for electrical insulation, shock protection, and fault tolerance under applicable safety standards.", + "notation": "09.05", + "broader": "metrics:product-safety-and-quality", + "inScheme": "https://vocabulary.uncefact.org/performance-metrics/", + "scopeNote": "IEC 60335 (household appliances); IEC 60601 (medical devices); IEC 62368 (AV/IT equipment); UL product safety standards.", + "allowedUnit": "C62", + "aggregationMethod": "minimum", + "improvementDirection": "higher" + }, + { + "@id": "metrics:flammability-rating", + "@type": "skos:Concept", + "prefLabel": "Flammability Rating", + "definition": "Classification of a material's reaction to fire, covering ignitability, flame spread, heat release, and smoke generation under standard test conditions.", + "notation": "09.06", + "broader": "metrics:product-safety-and-quality", + "inScheme": "https://vocabulary.uncefact.org/performance-metrics/", + "scopeNote": "EN 13501 (EU Euroclasses); UL 94 (plastics); ASTM E84 (surface burning); EU GPSR flammability requirements.", + "allowedUnit": "C62", + "aggregationMethod": "minimum", + "improvementDirection": "higher" + }, + { + "@id": "metrics:chemical-substance-concentration", + "@type": "skos:Concept", + "prefLabel": "Chemical Substance Concentration", + "definition": "Concentration of a specified regulated or restricted substance present in a product, in milligrams per kilogram.", + "notation": "09.07", + "broader": "metrics:product-safety-and-quality", + "inScheme": "https://vocabulary.uncefact.org/performance-metrics/", + "scopeNote": "EU REACH (SVHCs); EU RoHS Directive; EU ESPR Annex I; EU GPSR chemical safety requirements.", + "allowedUnit": "MK", + "aggregationMethod": "maximum", + "improvementDirection": "lower" + }, + { + "@id": "metrics:noise-emission-level", + "@type": "skos:Concept", + "prefLabel": "Noise Emission Level", + "definition": "Sound power or sound pressure level emitted by a product during normal operation, in decibels.", + "notation": "09.08", + "broader": "metrics:product-safety-and-quality", + "inScheme": "https://vocabulary.uncefact.org/performance-metrics/", + "scopeNote": "EU Outdoor Noise Directive 2000/14/EC; ISO 3744; IEC 60704 (household appliances); EU Energy Labelling Regulation.", + "allowedUnit": "C62", + "aggregationMethod": "maximum", + "improvementDirection": "lower" + }, + + { + "@id": "metrics:food-safety-and-quality", + "@type": "skos:Concept", + "prefLabel": "Food Safety and Quality", + "definition": "Metrics for measuring microbiological safety, chemical contaminant levels, pesticide and veterinary drug residues, food additive levels, nutritional content, and allergen presence in food products.", + "notation": "10", + "topConceptOf": "https://vocabulary.uncefact.org/performance-metrics/", + "inScheme": "https://vocabulary.uncefact.org/performance-metrics/", + "scopeNote": "Codex Alimentarius (FAO/WHO); EU General Food Law Regulation (EC) 178/2002; EU food safety regulations; ISO 22000.", + "narrower": [ + "metrics:microbiological-count", + "metrics:chemical-contaminant-level", + "metrics:pesticide-residue-level", + "metrics:veterinary-drug-residue-level", + "metrics:food-additive-level", + "metrics:nutritional-content", + "metrics:allergen-presence", + "metrics:shelf-life-duration" + ] + }, + { + "@id": "metrics:microbiological-count", + "@type": "skos:Concept", + "prefLabel": "Microbiological Count", + "definition": "Colony-forming units of a specified microorganism per unit of food, measuring microbiological safety and hygiene performance.", + "notation": "10.01", + "broader": "metrics:food-safety-and-quality", + "inScheme": "https://vocabulary.uncefact.org/performance-metrics/", + "scopeNote": "Codex CAC/GL 21; EU Regulation (EC) 2073/2005 on microbiological criteria for foodstuffs; ISO 4833.", + "allowedUnit": "C62", + "aggregationMethod": "maximum", + "improvementDirection": "lower" + }, + { + "@id": "metrics:chemical-contaminant-level", + "@type": "skos:Concept", + "prefLabel": "Chemical Contaminant Level", + "definition": "Concentration of a specified chemical contaminant (heavy metals, mycotoxins, dioxins, etc.) in food, in milligrams per kilogram.", + "notation": "10.02", + "broader": "metrics:food-safety-and-quality", + "inScheme": "https://vocabulary.uncefact.org/performance-metrics/", + "scopeNote": "Codex CXS 193 (General Standard for Contaminants and Toxins); EU Regulation (EC) 1881/2006.", + "allowedUnit": "MK", + "aggregationMethod": "maximum", + "improvementDirection": "lower" + }, + { + "@id": "metrics:pesticide-residue-level", + "@type": "skos:Concept", + "prefLabel": "Pesticide Residue Level", + "definition": "Concentration of a specified pesticide residue in food, measured against the applicable maximum residue limit, in milligrams per kilogram.", + "notation": "10.03", + "broader": "metrics:food-safety-and-quality", + "inScheme": "https://vocabulary.uncefact.org/performance-metrics/", + "scopeNote": "Codex Maximum Residue Limits for Pesticides (CX/MRL); EU Regulation (EC) 396/2005.", + "allowedUnit": "MK", + "aggregationMethod": "maximum", + "improvementDirection": "lower" + }, + { + "@id": "metrics:veterinary-drug-residue-level", + "@type": "skos:Concept", + "prefLabel": "Veterinary Drug Residue Level", + "definition": "Concentration of a specified veterinary drug residue in animal-derived food, measured against the applicable maximum residue limit, in micrograms per kilogram.", + "notation": "10.04", + "broader": "metrics:food-safety-and-quality", + "inScheme": "https://vocabulary.uncefact.org/performance-metrics/", + "scopeNote": "Codex Maximum Residue Limits for Veterinary Drugs (CX/MRL); EU Regulation (EU) 37/2010.", + "allowedUnit": "MK", + "aggregationMethod": "maximum", + "improvementDirection": "lower" + }, + { + "@id": "metrics:food-additive-level", + "@type": "skos:Concept", + "prefLabel": "Food Additive Level", + "definition": "Concentration of a specified food additive in the final product, measured against the applicable maximum permitted level, in milligrams per kilogram.", + "notation": "10.05", + "broader": "metrics:food-safety-and-quality", + "inScheme": "https://vocabulary.uncefact.org/performance-metrics/", + "scopeNote": "Codex General Standard for Food Additives (CXS 192); EU Regulation (EC) 1333/2008.", + "allowedUnit": "MK", + "aggregationMethod": "maximum", + "improvementDirection": "lower" + }, + { + "@id": "metrics:nutritional-content", + "@type": "skos:Concept", + "prefLabel": "Nutritional Content", + "definition": "Amount of a specified nutrient (energy, protein, fat, carbohydrate, sugar, sodium, fibre, vitamins, minerals) per standard serving or per 100 grams of food.", + "notation": "10.06", + "broader": "metrics:food-safety-and-quality", + "inScheme": "https://vocabulary.uncefact.org/performance-metrics/", + "scopeNote": "Codex CXS 1-1985 (General Standard for Labelling); Codex CXG 2-1985 (Nutrition Labelling Guidelines); EU Regulation (EU) 1169/2011.", + "allowedUnit": "GRM", + "aggregationMethod": "average", + "improvementDirection": "context-dependent" + }, + { + "@id": "metrics:allergen-presence", + "@type": "skos:Concept", + "prefLabel": "Allergen Presence", + "definition": "Declared presence or measured concentration of a specified allergen in a food product, supporting consumer safety and regulatory labelling requirements.", + "notation": "10.07", + "broader": "metrics:food-safety-and-quality", + "inScheme": "https://vocabulary.uncefact.org/performance-metrics/", + "scopeNote": "Codex CXS 1-1985 (allergen labelling); EU Regulation (EU) 1169/2011 Annex II; Codex CXA 4-1989 (allergen classification).", + "allowedUnit": "MK", + "aggregationMethod": "maximum", + "improvementDirection": "lower" + }, + { + "@id": "metrics:shelf-life-duration", + "@type": "skos:Concept", + "prefLabel": "Shelf Life Duration", + "definition": "Expected period during which a food product maintains safety and quality under stated storage conditions, in days.", + "notation": "10.08", + "broader": "metrics:food-safety-and-quality", + "inScheme": "https://vocabulary.uncefact.org/performance-metrics/", + "scopeNote": "Codex General Principles of Food Hygiene (CXC 1-1969); EU Regulation (EU) 1169/2011 (date marking); ISO 22000.", + "allowedUnit": "DAY", + "aggregationMethod": "minimum", + "improvementDirection": "higher" + } + ] +} diff --git a/src/dppvalidator/vocabularies/data/untp-ontology.jsonld b/src/dppvalidator/vocabularies/data/untp-ontology.jsonld new file mode 100644 index 0000000..d0054f6 --- /dev/null +++ b/src/dppvalidator/vocabularies/data/untp-ontology.jsonld @@ -0,0 +1,5046 @@ +{ + "@context": { + "untp": "https://vocabulary.uncefact.org/untp/", + "xsd": "http://www.w3.org/2001/XMLSchema#", + "rdf": "http://www.w3.org/1999/02/22-rdf-syntax-ns#", + "owl": "http://www.w3.org/2002/07/owl#", + "rdfs": "http://www.w3.org/2000/01/rdf-schema#", + "schema": "https://schema.org/", + "dcterms": "http://purl.org/dc/terms/", + "vann": "http://purl.org/vocab/vann/", + "foaf": "http://xmlns.com/foaf/0.1/" + }, + "@graph": [ + { + "@id": "https://vocabulary.uncefact.org/untp/", + "@type": "owl:Ontology", + "vann:preferredNamespacePrefix": "untp", + "vann:preferredNamespaceUri": "https://vocabulary.uncefact.org/untp/", + "dcterms:title": "UNTP Core Vocabulary", + "dcterms:description": "Core classes and properties for the UNTP data model (JSON-LD/RDF).", + "owl:versionInfo": "working" + }, + { + "@id": "untp:credentialSubjectType", + "@type": "rdf:Property", + "rdfs:comment": "The expected type of the credentialSubject for this credential class. Used to connect UNTP credential types to the UNTP domain classes that populate the W3C VCDM credentialSubject property, without redefining the W3C property itself.", + "rdfs:label": "credentialSubjectType", + "schema:domainIncludes": [ + { + "@id": "untp:DigitalProductPassport" + }, + { + "@id": "untp:DigitalFacilityRecord" + }, + { + "@id": "untp:DigitalConformityCredential" + }, + { + "@id": "untp:DigitalTraceabilityEvent" + }, + { + "@id": "untp:DigitalIdentityAnchor" + } + ], + "schema:rangeIncludes": { + "@id": "rdfs:Class" + } + }, + { + "@id": "untp:extendsModel", + "@type": "rdf:Property", + "rdfs:comment": "Indicates that this UNTP class reuses and extends a class defined in an external vocabulary (e.g. W3C VCDM, schema.org). The external class defines the envelope or base properties; UNTP defines only the extensions. This annotation enables human-readable renderings to display or link to the inherited properties without redefining them.", + "rdfs:label": "extendsModel", + "schema:domainIncludes": [ + { + "@id": "untp:VerifiableCredential" + }, + { + "@id": "untp:Address" + } + ], + "schema:rangeIncludes": { + "@id": "rdfs:Class" + } + }, + { + "@id": "untp:DigitalProductPassport", + "@type": "rdfs:Class", + "rdfs:comment": "A digital Product Passport (DPP) credential.", + "rdfs:label": "DigitalProductPassport", + "rdfs:subClassOf": "untp:VerifiableCredential", + "untp:credentialSubjectType": { + "@id": "untp:Product" + } + }, + { + "@id": "untp:VerifiableCredential", + "@type": "rdfs:Class", + "rdfs:comment": "A verifiable credential is a digital and verifiable version of everyday credentials such as certificates and licenses. It conforms to the W3C Verifiable Credentials Data Model v2.0 (VCDM).", + "rdfs:label": "VerifiableCredential", + "untp:extendsModel": { + "@id": "https://www.w3.org/2018/credentials#VerifiableCredential" + } + }, + { + "@id": "untp:DigitalFacilityRecord", + "@type": "rdfs:Class", + "rdfs:comment": "A digital Facility Record (DFR) credential.", + "rdfs:label": "DigitalFacilityRecord", + "rdfs:subClassOf": "untp:VerifiableCredential", + "untp:credentialSubjectType": { + "@id": "untp:Facility" + } + }, + { + "@id": "untp:CredentialIssuer", + "@type": "rdfs:Class", + "rdfs:comment": "The issuer party (person or organisation) of a verifiable credential.", + "rdfs:label": "CredentialIssuer" + }, + { + "@id": "untp:IssuingSoftware", + "@type": "rdfs:Class", + "rdfs:comment": "Optional metadata identifying the software product (and its vendor) that issued the parent credential. Used for vendor traceability and conformity testing.", + "rdfs:label": "IssuingSoftware" + }, + { + "@id": "untp:SoftwareVendor", + "@type": "rdfs:Class", + "rdfs:comment": "The vendor of a software product that issued a UNTP credential.", + "rdfs:label": "SoftwareVendor" + }, + { + "@id": "untp:Party", + "@type": "rdfs:Class", + "rdfs:comment": "An organisation. May be a supply chain actor, a certifier, a government agency.", + "rdfs:label": "Party" + }, + { + "@id": "untp:Entity", + "@type": "rdfs:Class", + "rdfs:comment": "A uniquely identified entity", + "rdfs:label": "Entity" + }, + { + "@id": "untp:IdentifierScheme", + "@type": "rdfs:Class", + "rdfs:comment": "An identifier registration scheme for products, facilities, or organisations. Typically operated by a state, national or global authority.", + "rdfs:label": "IdentifierScheme" + }, + { + "@id": "untp:Country", + "@type": "rdfs:Class", + "rdfs:comment": "Country Code and Name from ISO 3166", + "rdfs:label": "Country" + }, + { + "@id": "untp:Address", + "@type": "rdfs:Class", + "rdfs:comment": "A postal address. Reuses streetAddress, postalCode, addressLocality, and addressRegion from schema.org PostalAddress. Extends with addressCountry (an ISO-3166 country code/name structure).", + "rdfs:label": "Address", + "untp:extendsModel": { + "@id": "schema:PostalAddress" + } + }, + { + "@id": "untp:Classification", + "@type": "rdfs:Class", + "rdfs:comment": "A classification scheme and code / name representing a category value for a product, entity, or facility.", + "rdfs:label": "Classification" + }, + { + "@id": "untp:BitstringStatusListEntry", + "@type": "rdfs:Class", + "rdfs:comment": "A privacy-preserving, space-efficient, and high-performance mechanism for publishing status information such as suspension or revocation of Verifiable Credentials through use of bitstrings. See https://www.w3.org/TR/vc-bitstring-status-list/ for full details.", + "rdfs:label": "BitstringStatusListEntry" + }, + { + "@id": "untp:RenderTemplate2024", + "@type": "rdfs:Class", + "rdfs:comment": "A single template format focused render method where the content/media type decision becomes secondary (and is expressed separately).See https://github.com/w3c-ccg/vc-render-method/issues/9", + "rdfs:label": "RenderTemplate2024" + }, + { + "@id": "untp:Facility", + "@type": "rdfs:Class", + "rdfs:comment": "The physical site (eg farm or factory) where the product or materials was produced.", + "rdfs:label": "Facility" + }, + { + "@id": "untp:PartyRole", + "@type": "rdfs:Class", + "rdfs:comment": "A party with a defined relationship to the referencing entity", + "rdfs:label": "PartyRole" + }, + { + "@id": "untp:Link", + "@type": "rdfs:Class", + "rdfs:comment": "A structure to provide a URL link plus metadata associated with the link.", + "rdfs:label": "Link" + }, + { + "@id": "untp:Location", + "@type": "rdfs:Class", + "rdfs:comment": "Location information including address and geo-location of points, areas, and boundaries. At least one of plusCode, geoLocation, or geoBoundary are required.", + "rdfs:label": "Location" + }, + { + "@id": "untp:Coordinate", + "@type": "rdfs:Class", + "rdfs:comment": "A geographic point defined by latitude and longitude using the WGS84 geodetic coordinate reference system (EPSG:4326). Latitude and longitude are expressed in decimal degrees as floating-point numbers. Coordinates follow the conventional order (latitude, longitude) and represent a point on the Earth’s surface.", + "rdfs:label": "Coordinate" + }, + { + "@id": "untp:MaterialUsage", + "@type": "rdfs:Class", + "rdfs:comment": "A material usage record defining the consumption of materials for a given period, typically at an operating facility. Used to specify volumetric consumption and country of origin without specifying specific suppliers.", + "rdfs:label": "MaterialUsage" + }, + { + "@id": "untp:Period", + "@type": "rdfs:Class", + "rdfs:comment": "A period of time, typically a month, quarter or a year, which defines the context boundary for reported facts.", + "rdfs:label": "Period" + }, + { + "@id": "untp:Material", + "@type": "rdfs:Class", + "rdfs:comment": "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.", + "rdfs:label": "Material" + }, + { + "@id": "untp:Measure", + "@type": "rdfs:Class", + "rdfs:comment": "The measure class defines a numeric measured value (eg 10) and a coded unit of measure (eg KG). There is an optional upper and lower tolerance which can be used to specify uncertainty in the measure. ", + "rdfs:label": "Measure" + }, + { + "@id": "untp:Image", + "@type": "rdfs:Class", + "rdfs:comment": "A binary image encoded as base64 text and embedded into the data. Use this for small images like certification trust marks or regulated labels. Large images should be external links.", + "rdfs:label": "Image" + }, + { + "@id": "untp:Claim", + "@type": "rdfs:Class", + "rdfs:comment": "A performance claim about a product, facility, or organisation that is made against a well defined criterion.", + "rdfs:label": "Claim" + }, + { + "@id": "untp:Criterion", + "@type": "rdfs:Class", + "rdfs:comment": "A specific rule or criterion within a standard or regulation. eg a carbon intensity calculation rule within an emissions standard.", + "rdfs:label": "Criterion" + }, + { + "@id": "untp:ConformityTopic", + "@type": "rdfs:Class", + "rdfs:comment": "The UNTP standard classification scheme for conformity topic. see http://vocabulary.uncefact.org/ConformityTopic", + "rdfs:label": "ConformityTopic" + }, + { + "@id": "untp:Performance", + "@type": "rdfs:Class", + "rdfs:comment": "A claimed, assessed, or required performance level defined either by a scoring system or a numeric measure.", + "rdfs:label": "Performance" + }, + { + "@id": "untp:PerformanceMetric", + "@type": "rdfs:Class", + "rdfs:comment": "A standardised data point for performance reporting (eg product carbon footprint)", + "rdfs:label": "PerformanceMetric" + }, + { + "@id": "untp:Score", + "@type": "rdfs:Class", + "rdfs:comment": "A single score within a scoring framework. ", + "rdfs:label": "Score" + }, + { + "@id": "untp:Regulation", + "@type": "rdfs:Class", + "rdfs:comment": "A regulation (eg EU deforestation regulation) that defines the criteria for assessment.", + "rdfs:label": "Regulation" + }, + { + "@id": "untp:Standard", + "@type": "rdfs:Class", + "rdfs:comment": "A standard (eg ISO 14000) that specifies the criteria for conformance.", + "rdfs:label": "Standard" + }, + { + "@id": "untp:DigitalConformityCredential", + "@type": "rdfs:Class", + "rdfs:comment": "A Digital Conformity Credential (DCC) credential.", + "rdfs:label": "DigitalConformityCredential", + "rdfs:subClassOf": "untp:VerifiableCredential", + "untp:credentialSubjectType": { + "@id": "untp:ConformityAttestation" + } + }, + { + "@id": "untp:ConformityAttestation", + "@type": "rdfs:Class", + "rdfs:comment": "A conformity attestation issued by a competent body that defines one or more assessments (eg carbon intensity) about a product (eg battery) against a specification (eg LCA method) defined in a standard or regulation.", + "rdfs:label": "ConformityAttestation" + }, + { + "@id": "untp:Endorsement", + "@type": "rdfs:Class", + "rdfs:comment": "The authority under which a conformity claim is issued. For example a national accreditation authority may authorise a test lab to issue test certificates about a product against a standard. ", + "rdfs:label": "Endorsement" + }, + { + "@id": "untp:ConformityScheme", + "@type": "rdfs:Class", + "rdfs:comment": "A formal governance scheme under which an attestation is issued (eg ACRS structural steel certification) ", + "rdfs:label": "ConformityScheme" + }, + { + "@id": "untp:ScoringFramework", + "@type": "rdfs:Class", + "rdfs:comment": "A scoring framework used for performance level assessments against a criteria or scheme. For example forced labour performance might score A to D depending on the percentage of workforce subject to recruitment fees.", + "rdfs:label": "ScoringFramework" + }, + { + "@id": "untp:ConformityProfile", + "@type": "rdfs:Class", + "rdfs:comment": "A versioned conformity profile, managed under a scheme, which includes a specific list of versioned criteria. A conformity profile represents the precise scope of a conformity attestation. ", + "rdfs:label": "ConformityProfile" + }, + { + "@id": "untp:StandardAlignment", + "@type": "rdfs:Class", + "rdfs:comment": "A voluntary standard and an alignment level (exceeds, meets, partial).", + "rdfs:label": "StandardAlignment" + }, + { + "@id": "untp:RegulatoryAlignment", + "@type": "rdfs:Class", + "rdfs:comment": "A national regulation or international treaty and an alignment level (exceeds, meets, partial).", + "rdfs:label": "RegulatoryAlignment" + }, + { + "@id": "untp:ConformityAssessment", + "@type": "rdfs:Class", + "rdfs:comment": "A specific assessment about the product or facility against a specific specification. Eg the carbon intensity of a given product or batch.", + "rdfs:label": "ConformityAssessment" + }, + { + "@id": "untp:ProductVerification", + "@type": "rdfs:Class", + "rdfs:comment": "The product which is the subject of this conformity assessment", + "rdfs:label": "ProductVerification" + }, + { + "@id": "untp:Product", + "@type": "rdfs:Class", + "rdfs:comment": "The ProductInformation class encapsulates detailed information regarding a specific product, including its identification details, manufacturer, and other pertinent details.", + "rdfs:label": "Product" + }, + { + "@id": "untp:Characteristics", + "@type": "rdfs:Class", + "rdfs:comment": "A declaration of conformance with one or more criteria from a specific standard or regulation. ", + "rdfs:label": "Characteristics" + }, + { + "@id": "untp:Dimension", + "@type": "rdfs:Class", + "rdfs:comment": "Overall (length, width, height) dimensions and weight/volume of an item.", + "rdfs:label": "Dimension" + }, + { + "@id": "untp:Package", + "@type": "rdfs:Class", + "rdfs:comment": "Details of product packaging", + "rdfs:label": "Package" + }, + { + "@id": "untp:FacilityVerification", + "@type": "rdfs:Class", + "rdfs:comment": "The facility which is the subject of this conformity assessment", + "rdfs:label": "FacilityVerification" + }, + { + "@id": "untp:DigitalTraceabilityEvent", + "@type": "rdfs:Class", + "rdfs:comment": "A Digital Traceability Event (DTE) credential.", + "rdfs:label": "DigitalTraceabilityEvent", + "rdfs:subClassOf": "untp:VerifiableCredential", + "untp:credentialSubjectType": { + "@id": "untp:LifecycleEvent" + } + }, + { + "@id": "untp:LifecycleEvent", + "@type": "rdfs:Class", + "rdfs:comment": "This abstract event structure provides a common language to describe product lifecycle events such as shipments, inspections, manufacturing processes, etc.", + "rdfs:label": "LifecycleEvent" + }, + { + "@id": "untp:MakeEvent", + "@type": "rdfs:Class", + "rdfs:comment": "Transformation (manufacture/ production) of input products to output products at a given facility.", + "rdfs:label": "MakeEvent", + "rdfs:subClassOf": "untp:LifecycleEvent" + }, + { + "@id": "untp:SensorData", + "@type": "rdfs:Class", + "rdfs:comment": "A sensor data recording associated with this event", + "rdfs:label": "SensorData" + }, + { + "@id": "untp:EventProduct", + "@type": "rdfs:Class", + "rdfs:comment": "A quantity of products or materials involved in a lifecycle event.", + "rdfs:label": "EventProduct" + }, + { + "@id": "untp:MoveEvent", + "@type": "rdfs:Class", + "rdfs:comment": "Transfer (shipment) of products from one facility to another.", + "rdfs:label": "MoveEvent", + "rdfs:subClassOf": "untp:LifecycleEvent" + }, + { + "@id": "untp:ModifyEvent", + "@type": "rdfs:Class", + "rdfs:comment": "Intervention (eg repair) on a product without changing it's identity at a given facility.", + "rdfs:label": "ModifyEvent", + "rdfs:subClassOf": "untp:LifecycleEvent" + }, + { + "@id": "untp:DigitalIdentityAnchor", + "@type": "rdfs:Class", + "rdfs:comment": "The Digital Identity Anchor (DIA) is a very simple credential that is issued by a trusted authority and asserts an equivalence between a member identity as known to the authority (eg a VAT number) and one or more decentralised identifiers (DIDs) held by the member.", + "rdfs:label": "DigitalIdentityAnchor", + "rdfs:subClassOf": "untp:VerifiableCredential", + "untp:credentialSubjectType": { + "@id": "untp:RegisteredIdentity" + } + }, + { + "@id": "untp:RegisteredIdentity", + "@type": "rdfs:Class", + "rdfs:comment": "The identity anchor is a mapping between a registry member identity and one or more decentralised identifiers owned by the member. It may also list a set of membership scopes.", + "rdfs:label": "RegisteredIdentity" + }, + { + "@id": "untp:id", + "schema:rangeIncludes": { + "@id": "xsd:anyURI" + }, + "@type": [ + "rdf:Property", + "owl:DatatypeProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:CredentialIssuer" + }, + { + "@id": "untp:Party" + }, + { + "@id": "untp:Entity" + }, + { + "@id": "untp:IdentifierScheme" + }, + { + "@id": "untp:BitstringStatusListEntry" + }, + { + "@id": "untp:Facility" + }, + { + "@id": "untp:Claim" + }, + { + "@id": "untp:Criterion" + }, + { + "@id": "untp:ConformityTopic" + }, + { + "@id": "untp:PerformanceMetric" + }, + { + "@id": "untp:Regulation" + }, + { + "@id": "untp:Standard" + }, + { + "@id": "untp:ConformityAttestation" + }, + { + "@id": "untp:ConformityScheme" + }, + { + "@id": "untp:ConformityProfile" + }, + { + "@id": "untp:ConformityAssessment" + }, + { + "@id": "untp:Product" + }, + { + "@id": "untp:LifecycleEvent" + }, + { + "@id": "untp:MakeEvent" + }, + { + "@id": "untp:MoveEvent" + }, + { + "@id": "untp:ModifyEvent" + }, + { + "@id": "untp:RegisteredIdentity" + } + ], + "rdfs:comment": [ + "The W3C DID of the issuer - should be a did:web or did:webvh", + "Globally unique identifier of this party. Typically represented as a URI identifierScheme/Identifier URI", + "The globally unique identifier of this entity. ", + "The URI of this identifier scheme", + "optional identifier of this status list entry.", + "Globally unique identifier of this facility. Typically represented as a URI identifierScheme/Identifier URI", + "Globally unique identifier of this claim. Typically represented as a URI companyURL/claimID URI or a UUID", + "Globally unique identifier of this conformity criterion. Typically represented as a URI SchemeOwner/CriterionID URI", + "The unique identifier for this conformity topic", + "Globally unique identifier of this reporting metric. ", + "Globally unique identifier of this standard. Typically represented as a URI government/regulation URI", + "Globally unique identifier of this standard. Typically represented as a URI issuer/standard URI", + "Globally unique identifier of this attestation. Typically represented as a URI AssessmentBody/CertificateID URI or a UUID", + "Globally unique identifier of this conformity scheme. Typically represented as a URI SchemeOwner/SchemeName URI", + "Globally unique identifier of this context specific conformity profile. Typically represented as a URI SchemeOwner/profileID URI", + "Globally unique identifier of this assessment. Typically represented as a URI AssessmentBody/Assessment URI or a UUID", + "Globally unique identifier of this product. Typically represented as a URI identifierScheme/Identifier URI or, if self-issued, as a did.", + "Globally unique ID for this lifecycle event. Should be a URI. Can be a UUID.", + "Globally unique ID for this lifecycle event. Should be a URI. Can be a UUID.", + "Globally unique ID for this lifecycle event. Should be a URI. Can be a UUID.", + "Globally unique ID for this lifecycle event. Should be a URI. Can be a UUID.", + "The DID that is controlled by the registered member and is linked to the registeredID through this Identity Anchor credential" + ], + "rdfs:label": "id" + }, + { + "@id": "untp:name", + "schema:rangeIncludes": { + "@id": "xsd:string" + }, + "@type": [ + "rdf:Property", + "owl:DatatypeProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:CredentialIssuer" + }, + { + "@id": "untp:Party" + }, + { + "@id": "untp:Entity" + }, + { + "@id": "untp:IdentifierScheme" + }, + { + "@id": "untp:Classification" + }, + { + "@id": "untp:RenderTemplate2024" + }, + { + "@id": "untp:Facility" + }, + { + "@id": "untp:Material" + }, + { + "@id": "untp:Image" + }, + { + "@id": "untp:Claim" + }, + { + "@id": "untp:Criterion" + }, + { + "@id": "untp:ConformityTopic" + }, + { + "@id": "untp:PerformanceMetric" + }, + { + "@id": "untp:Regulation" + }, + { + "@id": "untp:Standard" + }, + { + "@id": "untp:ConformityAttestation" + }, + { + "@id": "untp:Endorsement" + }, + { + "@id": "untp:ConformityScheme" + }, + { + "@id": "untp:ScoringFramework" + }, + { + "@id": "untp:ConformityProfile" + }, + { + "@id": "untp:ConformityAssessment" + }, + { + "@id": "untp:Product" + }, + { + "@id": "untp:LifecycleEvent" + }, + { + "@id": "untp:MakeEvent" + }, + { + "@id": "untp:MoveEvent" + }, + { + "@id": "untp:ModifyEvent" + } + ], + "rdfs:comment": [ + "The name of the issuer person or organisation", + "Legal registered name of this party.", + "The name of this entity.", + "The name of the identifier scheme. ", + "Name of the classification represented by the code", + "Human facing display name for selection", + "Name of this facility as defined the location register.", + "Name of this material (eg \"Egyptian Cotton\")", + "the display name for this image", + "Name of this claim - typically similar or the same as the referenced criterion name.", + "Name of this criterion as defined by the scheme owner.", + "The human readable name for this conformity topic.", + "A human readable name for this metric (for example \"water usage per Kg of material\")", + "Name of this regulation as defined by the regulator.", + "Name for this standard", + "Name of this attestation - typically the title of the certificate.", + "The name of the accreditation.", + "Name of this scheme as defined by the scheme owner.", + "A name for this scoring framework. Must be unique within a scheme.", + "Name of this conformity profile as defined by the scheme owner.", + "Name of this assessment - typically similar or the same as the referenced criterion name.", + "The product name as known to the market.", + "The name for this lifecycle event ", + "The name for this lifecycle event ", + "The name for this lifecycle event ", + "The name for this lifecycle event " + ], + "rdfs:label": "name" + }, + { + "@id": "untp:issuerAlsoKnownAs", + "schema:rangeIncludes": { + "@id": "untp:Party" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:CredentialIssuer" + } + ], + "rdfs:comment": [ + "An optional list of other registered identifiers for this credential issuer " + ], + "rdfs:label": "issuerAlsoKnownAs" + }, + { + "@id": "untp:issuingSoftware", + "schema:rangeIncludes": { + "@id": "untp:IssuingSoftware" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:DigitalProductPassport" + }, + { + "@id": "untp:DigitalConformityCredential" + }, + { + "@id": "untp:DigitalFacilityRecord" + }, + { + "@id": "untp:DigitalIdentityAnchor" + }, + { + "@id": "untp:DigitalTraceabilityEvent" + } + ], + "rdfs:comment": [ + "Optional metadata identifying the software product (and its vendor) that issued this credential." + ], + "rdfs:label": "issuingSoftware" + }, + { + "@id": "untp:vendor", + "schema:rangeIncludes": { + "@id": "untp:SoftwareVendor" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:IssuingSoftware" + } + ], + "rdfs:comment": [ + "The vendor of the software product that issued the parent credential." + ], + "rdfs:label": "vendor" + }, + { + "@id": "untp:description", + "schema:rangeIncludes": { + "@id": "xsd:string" + }, + "@type": [ + "rdf:Property", + "owl:DatatypeProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Party" + }, + { + "@id": "untp:Entity" + }, + { + "@id": "untp:Facility" + }, + { + "@id": "untp:Image" + }, + { + "@id": "untp:Claim" + }, + { + "@id": "untp:Criterion" + }, + { + "@id": "untp:Regulation" + }, + { + "@id": "untp:Standard" + }, + { + "@id": "untp:ConformityAttestation" + }, + { + "@id": "untp:ConformityScheme" + }, + { + "@id": "untp:ScoringFramework" + }, + { + "@id": "untp:ConformityProfile" + }, + { + "@id": "untp:ConformityAssessment" + }, + { + "@id": "untp:Product" + }, + { + "@id": "untp:Package" + }, + { + "@id": "untp:LifecycleEvent" + }, + { + "@id": "untp:MakeEvent" + }, + { + "@id": "untp:MoveEvent" + }, + { + "@id": "untp:ModifyEvent" + }, + { + "@id": "untp:PerformanceMetric" + } + ], + "rdfs:comment": [ + "Description of the party including function and other names.", + "A rich descrition of this identified entity. ", + "Description of the facility including function and other names.", + "The detailed description / supporting information for this image.", + "Description of this conformity claim", + "Description of this criterion", + "Description of this regulation.", + "Description of this standard.", + "Description of this attestation.", + "Description of this conformity scheme", + "A full text description of the criterion that clearly specifies how compliance is achieved and measured. ", + "The description of this versioned and context specific conformity profile.", + "Description of this conformity assessment ", + "Description of the product.", + "Description of the packaging.", + "The description of this lifecycle event.", + "The description of this lifecycle event.", + "The description of this lifecycle event.", + "The description of this lifecycle event.", + "A rich description of this reporting metric." + ], + "rdfs:label": "description" + }, + { + "@id": "untp:registeredId", + "schema:rangeIncludes": { + "@id": "xsd:string" + }, + "@type": [ + "rdf:Property", + "owl:DatatypeProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Party" + }, + { + "@id": "untp:Facility" + }, + { + "@id": "untp:RegisteredIdentity" + } + ], + "rdfs:comment": [ + "The registration number (alphanumeric) of the Party within the register. Unique within the register.", + "The registration number (alphanumeric) of the facility within the identifier scheme. Unique within the register.", + "The registration number (alphanumeric) of the entity within the register. Unique within the register." + ], + "rdfs:label": "registeredId" + }, + { + "@id": "untp:idScheme", + "schema:rangeIncludes": { + "@id": "untp:IdentifierScheme" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Party" + }, + { + "@id": "untp:Facility" + }, + { + "@id": "untp:Product" + }, + { + "@id": "untp:RegisteredIdentity" + } + ], + "rdfs:comment": [ + "The identifier scheme of the party. Typically a national business register or a global scheme such as GLEIF. ", + "The ID scheme of the facility. eg a GS1 GLN or a National land registry scheme. If self issued then use the party ID of the facility owner. ", + "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. ", + "The identifier scheme for this registered entity ID." + ], + "rdfs:label": "idScheme" + }, + { + "@id": "untp:registrationCountry", + "schema:rangeIncludes": { + "@id": "untp:Country" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Party" + } + ], + "rdfs:comment": [ + "the country in which this organisation is registered - using ISO-3166 code and name." + ], + "rdfs:label": "registrationCountry" + }, + { + "@id": "untp:partyAddress", + "schema:rangeIncludes": { + "@id": "untp:Address" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Party" + } + ], + "rdfs:comment": [ + "The address of the party" + ], + "rdfs:label": "partyAddress" + }, + { + "@id": "untp:organisationWebsite", + "schema:rangeIncludes": { + "@id": "xsd:anyURI" + }, + "@type": [ + "rdf:Property", + "owl:DatatypeProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Party" + } + ], + "rdfs:comment": [ + "Website for this organisation" + ], + "rdfs:label": "organisationWebsite" + }, + { + "@id": "untp:industryCategory", + "schema:rangeIncludes": { + "@id": "untp:Classification" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Party" + } + ], + "rdfs:comment": [ + "The industry categories for this organisation. Recommend use of UNCPC as the category scheme. for example - unstats.un.org/isic/1030" + ], + "rdfs:label": "industryCategory" + }, + { + "@id": "untp:partyAlsoKnownAs", + "schema:rangeIncludes": { + "@id": "untp:Party" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Party" + } + ], + "rdfs:comment": [ + "An optional list of other registered identifiers for this organisation. For example DUNS, GLN, LEI, etc" + ], + "rdfs:label": "partyAlsoKnownAs" + }, + { + "@id": "untp:countryCode", + "schema:rangeIncludes": { + "@id": "untp:CountryCode" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Country" + } + ], + "rdfs:comment": [ + "ISO 3166 country code" + ], + "rdfs:label": "countryCode" + }, + { + "@id": "untp:countryName", + "schema:rangeIncludes": { + "@id": "xsd:string" + }, + "@type": [ + "rdf:Property", + "owl:DatatypeProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Country" + } + ], + "rdfs:comment": [ + "Country Name as defined in ISO 3166" + ], + "rdfs:label": "countryName" + }, + { + "@id": "untp:addressCountry", + "schema:rangeIncludes": { + "@id": "untp:Country" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Address" + } + ], + "rdfs:comment": [ + "The address country as an ISO-3166 two letter country code and name." + ], + "rdfs:label": "addressCountry" + }, + { + "@id": "untp:code", + "schema:rangeIncludes": { + "@id": "xsd:string" + }, + "@type": [ + "rdf:Property", + "owl:DatatypeProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Classification" + }, + { + "@id": "untp:Score" + } + ], + "rdfs:comment": [ + "classification code within the scheme", + "The coded value for this score (eg \"AAA\")" + ], + "rdfs:label": "code" + }, + { + "@id": "untp:definition", + "schema:rangeIncludes": { + "@id": "xsd:string" + }, + "@type": [ + "rdf:Property", + "owl:DatatypeProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Classification" + }, + { + "@id": "untp:ConformityTopic" + }, + { + "@id": "untp:Score" + } + ], + "rdfs:comment": [ + "A rich definition of this classification code.", + "The rich definition of this conformity topic.", + "A description of the meaning of this score." + ], + "rdfs:label": "definition" + }, + { + "@id": "untp:schemeId", + "schema:rangeIncludes": { + "@id": "xsd:anyURI" + }, + "@type": [ + "rdf:Property", + "owl:DatatypeProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Classification" + } + ], + "rdfs:comment": [ + "Classification scheme ID" + ], + "rdfs:label": "schemeId" + }, + { + "@id": "untp:schemeName", + "schema:rangeIncludes": { + "@id": "xsd:string" + }, + "@type": [ + "rdf:Property", + "owl:DatatypeProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Classification" + } + ], + "rdfs:comment": [ + "The name of the classification scheme" + ], + "rdfs:label": "schemeName" + }, + { + "@id": "untp:type", + "schema:rangeIncludes": { + "@id": "xsd:string" + }, + "@type": [ + "rdf:Property", + "owl:DatatypeProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:BitstringStatusListEntry" + } + ], + "rdfs:comment": [ + "The type of status list - must be set to \"The type property MUST be BitstringStatusListEntry.\"" + ], + "rdfs:label": "type" + }, + { + "@id": "untp:statusPurpose", + "schema:rangeIncludes": { + "@id": "untp:CredentialStatus" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:BitstringStatusListEntry" + } + ], + "rdfs:comment": [ + "Status purpose drawn from a standard list but extensible as per w3c bitstring status list specification." + ], + "rdfs:label": "statusPurpose" + }, + { + "@id": "untp:statusListIndex", + "schema:rangeIncludes": { + "@id": "xsd:integer" + }, + "@type": [ + "rdf:Property", + "owl:DatatypeProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:BitstringStatusListEntry" + } + ], + "rdfs:comment": [ + "\tThe statusListIndex property MUST be an arbitrary size integer greater than or equal to 0, expressed as a string in base 10. The value identifies the position of the status of the verifiable credential." + ], + "rdfs:label": "statusListIndex" + }, + { + "@id": "untp:statusListCredential", + "schema:rangeIncludes": { + "@id": "xsd:anyURI" + }, + "@type": [ + "rdf:Property", + "owl:DatatypeProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:BitstringStatusListEntry" + } + ], + "rdfs:comment": [ + "The statusListCredential property MUST be a URL to a verifiable credential. When the URL is dereferenced, the resulting verifiable credential MUST have type property that includes the BitstringStatusListCredential value." + ], + "rdfs:label": "statusListCredential" + }, + { + "@id": "untp:mediaQuery", + "schema:rangeIncludes": { + "@id": "xsd:string" + }, + "@type": [ + "rdf:Property", + "owl:DatatypeProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:RenderTemplate2024" + } + ], + "rdfs:comment": [ + "Media query as defined in https://www.w3.org/TR/mediaqueries-4/" + ], + "rdfs:label": "mediaQuery" + }, + { + "@id": "untp:template", + "schema:rangeIncludes": { + "@id": "xsd:string" + }, + "@type": [ + "rdf:Property", + "owl:DatatypeProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:RenderTemplate2024" + } + ], + "rdfs:comment": [ + "An inline template field for use cases where remote retrieval of a render method is suboptimal" + ], + "rdfs:label": "template" + }, + { + "@id": "untp:url", + "schema:rangeIncludes": { + "@id": "xsd:anyURI" + }, + "@type": [ + "rdf:Property", + "owl:DatatypeProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:RenderTemplate2024" + } + ], + "rdfs:comment": [ + "URL for remotely hosted template" + ], + "rdfs:label": "url" + }, + { + "@id": "untp:mediaType", + "schema:rangeIncludes": { + "@id": "xsd:string" + }, + "@type": [ + "rdf:Property", + "owl:DatatypeProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:RenderTemplate2024" + }, + { + "@id": "untp:Link" + }, + { + "@id": "untp:Image" + } + ], + "rdfs:comment": [ + "media type of the rendered output (eg text/html)", + "The media type of the target resource.", + "The media type of this image (eg image/png)" + ], + "rdfs:label": "mediaType" + }, + { + "@id": "untp:digestMultibase", + "schema:rangeIncludes": { + "@id": "xsd:string" + }, + "@type": [ + "rdf:Property", + "owl:DatatypeProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:RenderTemplate2024" + }, + { + "@id": "untp:Link" + } + ], + "rdfs:comment": [ + "Used for resource integrity and/or validation of the inline `template`", + "An optional multi-base encoded digest to ensure the content of the link has not changed. See https://www.w3.org/TR/vc-data-integrity/#resource-integrity for more information." + ], + "rdfs:label": "digestMultibase" + }, + { + "@id": "untp:countryOfOperation", + "schema:rangeIncludes": { + "@id": "untp:Country" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Facility" + } + ], + "rdfs:comment": [ + "The country in which this facility is operating.using ISO-3166 code and name." + ], + "rdfs:label": "countryOfOperation" + }, + { + "@id": "untp:processCategory", + "schema:rangeIncludes": { + "@id": "untp:Classification" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Facility" + } + ], + "rdfs:comment": [ + "The industrial or production processes performed by this facility. Example unstats.un.org/isic/1030." + ], + "rdfs:label": "processCategory" + }, + { + "@id": "untp:relatedParty", + "schema:rangeIncludes": { + "@id": "untp:PartyRole" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Facility" + }, + { + "@id": "untp:Product" + }, + { + "@id": "untp:LifecycleEvent" + }, + { + "@id": "untp:MakeEvent" + }, + { + "@id": "untp:MoveEvent" + }, + { + "@id": "untp:ModifyEvent" + } + ], + "rdfs:comment": [ + "A list of parties with a specified role relationship to this facility ", + "A list of parties with a defined relationship to this product", + "Any related parties and their roles involved in this event (eg the carrier for a shipment event)", + "Any related parties and their roles involved in this event (eg the carrier for a shipment event)", + "Any related parties and their roles involved in this event (eg the carrier for a shipment event)", + "Any related parties and their roles involved in this event (eg the carrier for a shipment event)" + ], + "rdfs:label": "relatedParty" + }, + { + "@id": "untp:relatedDocument", + "schema:rangeIncludes": { + "@id": "untp:Link" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Facility" + }, + { + "@id": "untp:Product" + }, + { + "@id": "untp:LifecycleEvent" + }, + { + "@id": "untp:MakeEvent" + }, + { + "@id": "untp:MoveEvent" + }, + { + "@id": "untp:ModifyEvent" + } + ], + "rdfs:comment": [ + "A list of links to documents providing additional facility information. Documents that support a conformity claim (e.g. permits or certificates) SHOULD be referenced as claim evidence rather than here.", + "A list of links to documents providing additional product information. Documents that support a conformity claim (e.g. permits or certificates) SHOULD be referenced as claim evidence rather than here.", + "A list of links to documentary evidence that supports this event. ", + "A list of links to documentary evidence that supports this event. ", + "A list of links to documentary evidence that supports this event. ", + "A list of links to documentary evidence that supports this event. " + ], + "rdfs:label": "relatedDocument" + }, + { + "@id": "untp:facilityAlsoKnownAs", + "schema:rangeIncludes": { + "@id": "untp:Facility" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Facility" + } + ], + "rdfs:comment": [ + "An optional list of other registered identifiers for this facility - eg GLNs or other schemes." + ], + "rdfs:label": "facilityAlsoKnownAs" + }, + { + "@id": "untp:locationInformation", + "schema:rangeIncludes": { + "@id": "untp:Location" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Facility" + } + ], + "rdfs:comment": [ + "Geo-location information for this facility as a resolvable geographic area (a Plus Code), and/or a geo-located point (latitude / longitude), and/or a defined boundary (GeoJSON Polygon)." + ], + "rdfs:label": "locationInformation" + }, + { + "@id": "untp:address", + "schema:rangeIncludes": { + "@id": "untp:Address" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Facility" + } + ], + "rdfs:comment": [ + "The Postal address of the location." + ], + "rdfs:label": "address" + }, + { + "@id": "untp:materialUsage", + "schema:rangeIncludes": { + "@id": "untp:MaterialUsage" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Facility" + } + ], + "rdfs:comment": [ + "The type and provenance of materials consumed by the facility during the reporting period. " + ], + "rdfs:label": "materialUsage" + }, + { + "@id": "untp:performanceClaim", + "schema:rangeIncludes": { + "@id": "untp:Claim" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Facility" + }, + { + "@id": "untp:Product" + }, + { + "@id": "untp:Package" + } + ], + "rdfs:comment": [ + "A list of performance claims (eg deforestation status) for this facility.", + "A list of performance claims (eg emissions intensity) for this product.", + "conformity claims made about the packaging." + ], + "rdfs:label": "performanceClaim" + }, + { + "@id": "untp:role", + "schema:rangeIncludes": { + "@id": "untp:PartyRole" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:PartyRole" + } + ], + "rdfs:comment": [ + "The role played by the party in this relationship" + ], + "rdfs:label": "role" + }, + { + "@id": "untp:party", + "schema:rangeIncludes": { + "@id": "untp:Party" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:PartyRole" + } + ], + "rdfs:comment": [ + "The party that has the specified role." + ], + "rdfs:label": "party" + }, + { + "@id": "untp:linkURL", + "schema:rangeIncludes": { + "@id": "xsd:anyURI" + }, + "@type": [ + "rdf:Property", + "owl:DatatypeProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Link" + } + ], + "rdfs:comment": [ + "The URL of the target resource. " + ], + "rdfs:label": "linkURL" + }, + { + "@id": "untp:linkName", + "schema:rangeIncludes": { + "@id": "xsd:string" + }, + "@type": [ + "rdf:Property", + "owl:DatatypeProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Link" + } + ], + "rdfs:comment": [ + "Display name for this link." + ], + "rdfs:label": "linkName" + }, + { + "@id": "untp:linkType", + "schema:rangeIncludes": { + "@id": "xsd:string" + }, + "@type": [ + "rdf:Property", + "owl:DatatypeProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Link" + } + ], + "rdfs:comment": [ + "The type of the target resource - drawn from a controlled vocabulary " + ], + "rdfs:label": "linkType" + }, + { + "@id": "untp:plusCode", + "schema:rangeIncludes": { + "@id": "xsd:anyURI" + }, + "@type": [ + "rdf:Property", + "owl:DatatypeProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Location" + } + ], + "rdfs:comment": [ + "An open location code (https://maps.google.com/pluscodes/) representing this geographic location or region. Open location codes can represent any sized area from a point to a large region and are easily resolved to a visual map location. " + ], + "rdfs:label": "plusCode" + }, + { + "@id": "untp:geoLocation", + "schema:rangeIncludes": { + "@id": "untp:Coordinate" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Location" + }, + { + "@id": "untp:SensorData" + } + ], + "rdfs:comment": [ + "The latitude and longitude coordinates that best represent the specified location. ", + "The geolocation of this sensor data recording event." + ], + "rdfs:label": "geoLocation" + }, + { + "@id": "untp:geoBoundary", + "schema:rangeIncludes": { + "@id": "untp:Coordinate" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Location" + } + ], + "rdfs:comment": [ + "The list of ordered coordinates that define a closed area polygon as a location boundary. The first and last coordinates in the array must match - thereby defining a closed boundary." + ], + "rdfs:label": "geoBoundary" + }, + { + "@id": "untp:latitude", + "schema:rangeIncludes": { + "@id": "xsd:double" + }, + "@type": [ + "rdf:Property", + "owl:DatatypeProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Coordinate" + } + ], + "rdfs:comment": [ + "latitude: Angular distance north or south of the equator, expressed in decimal degrees.Valid range: −90.0 to +90.0." + ], + "rdfs:label": "latitude" + }, + { + "@id": "untp:longitude", + "schema:rangeIncludes": { + "@id": "xsd:double" + }, + "@type": [ + "rdf:Property", + "owl:DatatypeProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Coordinate" + } + ], + "rdfs:comment": [ + "longitude: Angular distance east or west of the Prime Meridian, expressed in decimal degrees.Valid range: −180.0 to +180.0." + ], + "rdfs:label": "longitude" + }, + { + "@id": "untp:applicablePeriod", + "schema:rangeIncludes": { + "@id": "untp:Period" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:MaterialUsage" + }, + { + "@id": "untp:Claim" + } + ], + "rdfs:comment": [ + "The period over which this material consumption is reported", + "The applicable reporting period for this facility record." + ], + "rdfs:label": "applicablePeriod" + }, + { + "@id": "untp:materialConsumed", + "schema:rangeIncludes": { + "@id": "untp:Material" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:MaterialUsage" + } + ], + "rdfs:comment": [ + "An list of materials consumed during the usage period. " + ], + "rdfs:label": "materialConsumed" + }, + { + "@id": "untp:startDate", + "schema:rangeIncludes": { + "@id": "xsd:date" + }, + "@type": [ + "rdf:Property", + "owl:DatatypeProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Period" + } + ], + "rdfs:comment": [ + "The period start date" + ], + "rdfs:label": "startDate" + }, + { + "@id": "untp:endDate", + "schema:rangeIncludes": { + "@id": "xsd:date" + }, + "@type": [ + "rdf:Property", + "owl:DatatypeProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Period" + } + ], + "rdfs:comment": [ + "The period end date" + ], + "rdfs:label": "endDate" + }, + { + "@id": "untp:periodInformation", + "schema:rangeIncludes": { + "@id": "xsd:string" + }, + "@type": [ + "rdf:Property", + "owl:DatatypeProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Period" + } + ], + "rdfs:comment": [ + "Additional information relevant to this reporting period" + ], + "rdfs:label": "periodInformation" + }, + { + "@id": "untp:originCountry", + "schema:rangeIncludes": { + "@id": "untp:Country" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Material" + } + ], + "rdfs:comment": [ + "A ISO 3166-1 code representing the country of origin of the component or ingredient." + ], + "rdfs:label": "originCountry" + }, + { + "@id": "untp:materialType", + "schema:rangeIncludes": { + "@id": "untp:Classification" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Material" + } + ], + "rdfs:comment": [ + "The type of this material - as a value drawn from a controlled vocabulary eg from UN Framework Classification for Resources (UNFC)." + ], + "rdfs:label": "materialType" + }, + { + "@id": "untp:massFraction", + "schema:rangeIncludes": { + "@id": "xsd:double" + }, + "@type": [ + "rdf:Property", + "owl:DatatypeProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Material" + } + ], + "rdfs:comment": [ + "The mass fraction as a decimal of the product (or facility reporting period) represented by this material. " + ], + "rdfs:label": "massFraction" + }, + { + "@id": "untp:mass", + "schema:rangeIncludes": { + "@id": "untp:Measure" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Material" + } + ], + "rdfs:comment": [ + "The mass of the material component." + ], + "rdfs:label": "mass" + }, + { + "@id": "untp:recycledMassFraction", + "schema:rangeIncludes": { + "@id": "xsd:double" + }, + "@type": [ + "rdf:Property", + "owl:DatatypeProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Material" + } + ], + "rdfs:comment": [ + "Mass fraction of this material that is recycled (eg 50% recycled Lithium)" + ], + "rdfs:label": "recycledMassFraction" + }, + { + "@id": "untp:hazardous", + "schema:rangeIncludes": { + "@id": "xsd:boolean" + }, + "@type": [ + "rdf:Property", + "owl:DatatypeProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Material" + } + ], + "rdfs:comment": [ + "Indicates whether this material is hazardous. If true then the materialSafetyInformation property must be present" + ], + "rdfs:label": "hazardous" + }, + { + "@id": "untp:symbol", + "schema:rangeIncludes": { + "@id": "untp:Image" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Material" + } + ], + "rdfs:comment": [ + "Based 64 encoded binary used to represent a visual symbol for a given material. " + ], + "rdfs:label": "symbol" + }, + { + "@id": "untp:materialSafetyInformation", + "schema:rangeIncludes": { + "@id": "untp:Link" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Material" + } + ], + "rdfs:comment": [ + "Reference to further information about safe handling of this hazardous material (for example a link to a material safety data sheet)" + ], + "rdfs:label": "materialSafetyInformation" + }, + { + "@id": "untp:value", + "schema:rangeIncludes": { + "@id": "xsd:double" + }, + "@type": [ + "rdf:Property", + "owl:DatatypeProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Measure" + } + ], + "rdfs:comment": [ + "The numeric value of the measure" + ], + "rdfs:label": "value" + }, + { + "@id": "untp:upperTolerance", + "schema:rangeIncludes": { + "@id": "xsd:double" + }, + "@type": [ + "rdf:Property", + "owl:DatatypeProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Measure" + } + ], + "rdfs:comment": [ + "The upper tolerance associated with this measure expressed in the same units as the measure. For example value=10, upperTolerance=0.1, unit=KGM would mean that this measure is 10kg + 0.1kg" + ], + "rdfs:label": "upperTolerance" + }, + { + "@id": "untp:lowerTolerance", + "schema:rangeIncludes": { + "@id": "xsd:double" + }, + "@type": [ + "rdf:Property", + "owl:DatatypeProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Measure" + } + ], + "rdfs:comment": [ + "The lower tolerance associated with this measure expressed in the same units as the measure. For example value=10, lowerTolerance=0.1, unit=KGM would mean that this measure is 10kg - 0.1kg" + ], + "rdfs:label": "lowerTolerance" + }, + { + "@id": "untp:unit", + "schema:rangeIncludes": { + "@id": "untp:UnitOfMeasure" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Measure" + } + ], + "rdfs:comment": [ + "Unit of measure drawn from the UNECE Rec20 measure code list." + ], + "rdfs:label": "unit" + }, + { + "@id": "untp:imageData", + "schema:rangeIncludes": { + "@id": "xsd:string" + }, + "@type": [ + "rdf:Property", + "owl:DatatypeProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Image" + } + ], + "rdfs:comment": [ + "The image data encoded as a base64 string." + ], + "rdfs:label": "imageData" + }, + { + "@id": "untp:referenceCriteria", + "schema:rangeIncludes": { + "@id": "untp:Criterion" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Claim" + } + ], + "rdfs:comment": [ + "The criterion against which the claim is made." + ], + "rdfs:label": "referenceCriteria" + }, + { + "@id": "untp:referenceRegulation", + "schema:rangeIncludes": { + "@id": "untp:Regulation" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Claim" + }, + { + "@id": "untp:ConformityAssessment" + } + ], + "rdfs:comment": [ + "List of references to regulation to which conformity is claimed claimed for this product", + "The reference to the regulation that defines the assessment criteria" + ], + "rdfs:label": "referenceRegulation" + }, + { + "@id": "untp:referenceStandard", + "schema:rangeIncludes": { + "@id": "untp:Standard" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Claim" + }, + { + "@id": "untp:ConformityAssessment" + } + ], + "rdfs:comment": [ + "List of references to standards to which conformity is claimed claimed for this product", + "The reference to the standard that defines the specification / criteria" + ], + "rdfs:label": "referenceStandard" + }, + { + "@id": "untp:claimDate", + "schema:rangeIncludes": { + "@id": "xsd:date" + }, + "@type": [ + "rdf:Property", + "owl:DatatypeProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Claim" + } + ], + "rdfs:comment": [ + "That date on which the claimed performance is applicable." + ], + "rdfs:label": "claimDate" + }, + { + "@id": "untp:claimedPerformance", + "schema:rangeIncludes": { + "@id": "untp:Performance" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Claim" + } + ], + "rdfs:comment": [ + "The claimed performance level " + ], + "rdfs:label": "claimedPerformance" + }, + { + "@id": "untp:evidence", + "schema:rangeIncludes": { + "@id": "untp:Link" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Claim" + }, + { + "@id": "untp:ConformityAssessment" + } + ], + "rdfs:comment": [ + "A URI pointing to the evidence supporting the claim. SHOULD be a URL to a UNTP Digital Conformity Credential (DCC)", + "Evidence to support this specific assessment." + ], + "rdfs:label": "evidence" + }, + { + "@id": "untp:conformityTopic", + "schema:rangeIncludes": { + "@id": "untp:ConformityTopic" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Claim" + }, + { + "@id": "untp:Criterion" + }, + { + "@id": "untp:ConformityAssessment" + } + ], + "rdfs:comment": [ + "The conformity topic category for this assessment", + "A global UN/CEFACT standard conformity topic code. ", + "The UNTP conformity topic used to categorise this assessment. Should match the topic defined by the scheme criterion." + ], + "rdfs:label": "conformityTopic" + }, + { + "@id": "untp:version", + "schema:rangeIncludes": { + "@id": "xsd:string" + }, + "@type": [ + "rdf:Property", + "owl:DatatypeProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Criterion" + }, + { + "@id": "untp:ConformityProfile" + }, + { + "@id": "untp:IssuingSoftware" + } + ], + "rdfs:comment": [ + "The major.minor version of the criterion. Minor versions represent changes that would not invalidate an assessment made under a previous version.", + "Version of this scheme following SemVer best practice (major.minor.patch). " + ], + "rdfs:label": "version" + }, + { + "@id": "untp:status", + "schema:rangeIncludes": { + "@id": "untp:CriterionStatus" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Criterion" + }, + { + "@id": "untp:ConformityProfile" + } + ], + "rdfs:comment": [ + "The lifecycle status of this criterion. ", + "The status of this conformity profile (draft, active, deprecated)" + ], + "rdfs:label": "status" + }, + { + "@id": "untp:documentation", + "schema:rangeIncludes": { + "@id": "xsd:anyURI" + }, + "@type": [ + "rdf:Property", + "owl:DatatypeProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Criterion" + }, + { + "@id": "untp:ConformityScheme" + }, + { + "@id": "untp:ConformityProfile" + } + ], + "rdfs:comment": [ + "A web page carrying detailed information about this criterion.", + "A web page providing full documentation of this scheme.", + "A web page that describes this entity in detail." + ], + "rdfs:label": "documentation" + }, + { + "@id": "untp:requiredPerformance", + "schema:rangeIncludes": { + "@id": "untp:Performance" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Criterion" + } + ], + "rdfs:comment": [ + "The required performance level as one or more score and/or a metric that represents compliance defined by the criteria" + ], + "rdfs:label": "requiredPerformance" + }, + { + "@id": "untp:tag", + "schema:rangeIncludes": { + "@id": "xsd:string" + }, + "@type": [ + "rdf:Property", + "owl:DatatypeProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Criterion" + } + ], + "rdfs:comment": [ + "A set of tags that can be used by the scheme owner to be able to filter or group criterion in a large vocabulary for specific use cases." + ], + "rdfs:label": "tag" + }, + { + "@id": "untp:metric", + "schema:rangeIncludes": { + "@id": "untp:PerformanceMetric" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Performance" + }, + { + "@id": "untp:SensorData" + } + ], + "rdfs:comment": [ + "The metric (eg material emissions intensity CO2e/Kg or percentage of young workers) that is measured.", + "The type of measurement recorded in this sensor data event." + ], + "rdfs:label": "metric" + }, + { + "@id": "untp:improvementDirection", + "schema:rangeIncludes": { + "@id": "untp:ImprovementIndicator" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:PerformanceMetric" + } + ], + "rdfs:comment": [ + "Indicator of whether conforming performance is greater than or less than the defined threshold." + ], + "rdfs:label": "improvementDirection" + }, + { + "@id": "untp:aggregationMethod", + "schema:rangeIncludes": { + "@id": "untp:AggregationType" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:PerformanceMetric" + } + ], + "rdfs:comment": [ + "Indicates how to aggregate multiple values to report a single performance metric." + ], + "rdfs:label": "aggregationMethod" + }, + { + "@id": "untp:measure", + "schema:rangeIncludes": { + "@id": "untp:Measure" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Performance" + }, + { + "@id": "untp:SensorData" + } + ], + "rdfs:comment": [ + "The measured performance value", + "The value measured by this sensor measurement event." + ], + "rdfs:label": "measure" + }, + { + "@id": "untp:score", + "schema:rangeIncludes": { + "@id": "untp:Score" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Performance" + }, + { + "@id": "untp:ScoringFramework" + } + ], + "rdfs:comment": [ + "A performance score (eg \"AA\") drawn from a scoring framework defined by the scheme or criterion.", + "A list of scores and ranks associated with this scoring framework." + ], + "rdfs:label": "score" + }, + { + "@id": "untp:allowedUnit", + "schema:rangeIncludes": { + "@id": "untp:UnitOfMeasure" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:PerformanceMetric" + } + ], + "rdfs:comment": [ + "The allowed units for value reporting against this metric (eg cubic meters)" + ], + "rdfs:label": "allowedUnit" + }, + { + "@id": "untp:rank", + "schema:rangeIncludes": { + "@id": "xsd:integer" + }, + "@type": [ + "rdf:Property", + "owl:DatatypeProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Score" + } + ], + "rdfs:comment": [ + "The ranking of this score within the scoring framework - using an integer where \"1\" is the highest rank." + ], + "rdfs:label": "rank" + }, + { + "@id": "untp:jurisdictionCountry", + "schema:rangeIncludes": { + "@id": "untp:Country" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Regulation" + } + ], + "rdfs:comment": [ + "The legal jurisdiction (country) under which the regulation is issued." + ], + "rdfs:label": "jurisdictionCountry" + }, + { + "@id": "untp:administeredBy", + "schema:rangeIncludes": { + "@id": "untp:Party" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Regulation" + } + ], + "rdfs:comment": [ + "the issuing body of the regulation. For example Australian Government Department of Climate Change, Energy, the Environment and Water" + ], + "rdfs:label": "administeredBy" + }, + { + "@id": "untp:effectiveDate", + "schema:rangeIncludes": { + "@id": "xsd:date" + }, + "@type": [ + "rdf:Property", + "owl:DatatypeProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Regulation" + } + ], + "rdfs:comment": [ + "the date at which the regulation came into effect." + ], + "rdfs:label": "effectiveDate" + }, + { + "@id": "untp:issuingParty", + "schema:rangeIncludes": { + "@id": "untp:Party" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Standard" + } + ], + "rdfs:comment": [ + "The party that issued the standard " + ], + "rdfs:label": "issuingParty" + }, + { + "@id": "untp:issueDate", + "schema:rangeIncludes": { + "@id": "xsd:date" + }, + "@type": [ + "rdf:Property", + "owl:DatatypeProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Standard" + } + ], + "rdfs:comment": [ + "The date when the standard was issued." + ], + "rdfs:label": "issueDate" + }, + { + "@id": "untp:assessorLevel", + "schema:rangeIncludes": { + "@id": "untp:AssessorLevel" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:ConformityAttestation" + } + ], + "rdfs:comment": [ + "Assurance code pertaining to assessor (relation to the object under assessment)" + ], + "rdfs:label": "assessorLevel" + }, + { + "@id": "untp:assessmentLevel", + "schema:rangeIncludes": { + "@id": "untp:AssessmentLevel" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:ConformityAttestation" + } + ], + "rdfs:comment": [ + "Assurance pertaining to assessment (any authority or support for the assessment process)" + ], + "rdfs:label": "assessmentLevel" + }, + { + "@id": "untp:attestationType", + "schema:rangeIncludes": { + "@id": "untp:AttestationType" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:ConformityAttestation" + } + ], + "rdfs:comment": [ + "The type of criterion (optional or mandatory)." + ], + "rdfs:label": "attestationType" + }, + { + "@id": "untp:issuedToParty", + "schema:rangeIncludes": { + "@id": "untp:Party" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:ConformityAttestation" + } + ], + "rdfs:comment": [ + "The party to whom the conformity attestation was issued." + ], + "rdfs:label": "issuedToParty" + }, + { + "@id": "untp:authorisation", + "schema:rangeIncludes": { + "@id": "untp:Endorsement" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:ConformityAttestation" + } + ], + "rdfs:comment": [ + "The authority under which a conformity claim is issued. For example a national accreditation authority may authorise a test lab to issue test certificates about a product against a standard. " + ], + "rdfs:label": "authorisation" + }, + { + "@id": "untp:referenceScheme", + "schema:rangeIncludes": { + "@id": "untp:ConformityScheme" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:ConformityAttestation" + } + ], + "rdfs:comment": [ + "The conformity scheme under which this attestation is made." + ], + "rdfs:label": "referenceScheme" + }, + { + "@id": "untp:referenceProfile", + "schema:rangeIncludes": { + "@id": "untp:ConformityProfile" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:ConformityAttestation" + } + ], + "rdfs:comment": [ + "The specific versioned conformity profile (comprising a set of versioned criteria) against which this conformity attestation is made." + ], + "rdfs:label": "referenceProfile" + }, + { + "@id": "untp:profileScore", + "schema:rangeIncludes": { + "@id": "untp:Score" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:ConformityAttestation" + } + ], + "rdfs:comment": [ + "The overall performance against a scheme level performance measurement framework for the referenced profile or scheme." + ], + "rdfs:label": "profileScore" + }, + { + "@id": "untp:conformityCertificate", + "schema:rangeIncludes": { + "@id": "untp:Link" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:ConformityAttestation" + } + ], + "rdfs:comment": [ + "A reference to the human / printable version of this conformity attestation - typically represented as a PDF document. The document may have more details than are represented in the digital attestation." + ], + "rdfs:label": "conformityCertificate" + }, + { + "@id": "untp:auditableEvidence", + "schema:rangeIncludes": { + "@id": "untp:Link" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:ConformityAttestation" + } + ], + "rdfs:comment": [ + "Auditable evidence supporting this assessment such as raw measurements, supporting documents. This is usually private data and would normally be encrypted." + ], + "rdfs:label": "auditableEvidence" + }, + { + "@id": "untp:trustmark", + "schema:rangeIncludes": { + "@id": "untp:Image" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:ConformityAttestation" + }, + { + "@id": "untp:Endorsement" + }, + { + "@id": "untp:ConformityScheme" + } + ], + "rdfs:comment": [ + "A trust mark as a small binary image encoded as base64 with a description. Maye be displayed on the conformity credential rendering.", + "The trust mark image awarded by the AB to the CAB to indicate accreditation.", + "The trust mark or seal used by this conformity scheme." + ], + "rdfs:label": "trustmark" + }, + { + "@id": "untp:conformityAssessment", + "schema:rangeIncludes": { + "@id": "untp:ConformityAssessment" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:ConformityAttestation" + } + ], + "rdfs:comment": [ + "A list of individual assessment made under this attestation. " + ], + "rdfs:label": "conformityAssessment" + }, + { + "@id": "untp:issuingAuthority", + "schema:rangeIncludes": { + "@id": "untp:Party" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Endorsement" + } + ], + "rdfs:comment": [ + "The competent authority that issued the accreditation." + ], + "rdfs:label": "issuingAuthority" + }, + { + "@id": "untp:endorsementEvidence", + "schema:rangeIncludes": { + "@id": "untp:Link" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Endorsement" + } + ], + "rdfs:comment": [ + "The evidence that supports the authority under which the attestation is issued - for an example an accreditation certificate." + ], + "rdfs:label": "endorsementEvidence" + }, + { + "@id": "untp:owner", + "schema:rangeIncludes": { + "@id": "untp:Party" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:ConformityScheme" + } + ], + "rdfs:comment": [ + "The party that is the owner / maintainer of this conformity scheme." + ], + "rdfs:label": "owner" + }, + { + "@id": "untp:endorsementLevel", + "schema:rangeIncludes": { + "@id": "untp:SchemeEndorsementLevel" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:ConformityScheme" + } + ], + "rdfs:comment": [ + "The scheme assurance type." + ], + "rdfs:label": "endorsementLevel" + }, + { + "@id": "untp:endorsement", + "schema:rangeIncludes": { + "@id": "untp:Endorsement" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:ConformityScheme" + } + ], + "rdfs:comment": [ + "The endorsement provided to the scheme by an external authority such as a regulator, an accreditaiton authority, or a benchmarking scheme." + ], + "rdfs:label": "endorsement" + }, + { + "@id": "untp:schemeScoringFramework", + "schema:rangeIncludes": { + "@id": "untp:ScoringFramework" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:ConformityScheme" + } + ], + "rdfs:comment": [ + "The scheme level overall scoring framework that represents the achievement levels (AA, A, B etc) that maybe be awarded to the subject of an independent assessment under the scheme." + ], + "rdfs:label": "schemeScoringFramework" + }, + { + "@id": "untp:licenseType", + "schema:rangeIncludes": { + "@id": "untp:LicenseType" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:ConformityScheme" + } + ], + "rdfs:comment": [ + "Descriptive name and URL link to the license conditions associated with this scheme." + ], + "rdfs:label": "licenseType" + }, + { + "@id": "untp:establishedDate", + "schema:rangeIncludes": { + "@id": "xsd:date" + }, + "@type": [ + "rdf:Property", + "owl:DatatypeProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:ConformityScheme" + } + ], + "rdfs:comment": [ + "The date when this scheme was first established. " + ], + "rdfs:label": "establishedDate" + }, + { + "@id": "untp:geographicScope", + "schema:rangeIncludes": { + "@id": "untp:Classification" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:ConformityScheme" + } + ], + "rdfs:comment": [ + "The geographic scope of this scheme as a list of ISO-3166 countries, regions, or code=001, name=Worldwide to indicate global coverage." + ], + "rdfs:label": "geographicScope" + }, + { + "@id": "untp:industryScope", + "schema:rangeIncludes": { + "@id": "untp:Classification" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:ConformityScheme" + } + ], + "rdfs:comment": [ + "A list of UN ISIC code & name indicating the industry scope for this scheme. " + ], + "rdfs:label": "industryScope" + }, + { + "@id": "untp:conformsTo", + "schema:rangeIncludes": { + "@id": "untp:Link" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:ConformityScheme" + } + ], + "rdfs:comment": [ + "The name and URI of the vocabulary standard (eg UNTP CVC) that the machine readable version of this sceme conforms to." + ], + "rdfs:label": "conformsTo" + }, + { + "@id": "untp:includedProfile", + "schema:rangeIncludes": { + "@id": "untp:ConformityProfile" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:ConformityScheme" + } + ], + "rdfs:comment": [ + "The list of versioned conformity profiles included in this scheme" + ], + "rdfs:label": "includedProfile" + }, + { + "@id": "untp:validFrom", + "schema:rangeIncludes": { + "@id": "xsd:date" + }, + "@type": [ + "rdf:Property", + "owl:DatatypeProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:ConformityProfile" + } + ], + "rdfs:comment": [ + "The data from which this scheme version is valid." + ], + "rdfs:label": "validFrom" + }, + { + "@id": "untp:subjectType", + "schema:rangeIncludes": { + "@id": "untp:AssessmentSubjectType" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:ConformityProfile" + } + ], + "rdfs:comment": [ + "The type of the subject of assessments made under this conformity profile (eg product, facility, organisation)" + ], + "rdfs:label": "subjectType" + }, + { + "@id": "untp:standardAlignment", + "schema:rangeIncludes": { + "@id": "untp:StandardAlignment" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:ConformityProfile" + } + ], + "rdfs:comment": [ + "A list of voluntary standards referenced by this conformity profile and against which some level of compliance can be inferred for subjects that pass an assessment. " + ], + "rdfs:label": "standardAlignment" + }, + { + "@id": "untp:regulatoryAlignment", + "schema:rangeIncludes": { + "@id": "untp:RegulatoryAlignment" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:ConformityProfile" + } + ], + "rdfs:comment": [ + "A list of regulations or legally binding conventions referenced by this conformity profile and against which some level of compliance can be inferred for subjects that pass an assessment. " + ], + "rdfs:label": "regulatoryAlignment" + }, + { + "@id": "untp:criterionScoringFramework", + "schema:rangeIncludes": { + "@id": "untp:ScoringFramework" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:ConformityProfile" + } + ], + "rdfs:comment": [ + "A list of named scoring frameworks that are applied by criterion within this profile. " + ], + "rdfs:label": "criterionScoringFramework" + }, + { + "@id": "untp:criterion", + "schema:rangeIncludes": { + "@id": "untp:Criterion" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:ConformityProfile" + } + ], + "rdfs:comment": [ + "A list of criterion that are included in this conformity profile." + ], + "rdfs:label": "criterion" + }, + { + "@id": "untp:scope", + "schema:rangeIncludes": { + "@id": "untp:Classification" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:ConformityProfile" + } + ], + "rdfs:comment": [ + "A set of classification codes that may be used to categorize the applicability of this criteria - for example industry sector, jurisdiction or commodity type - based on a formal vocabulary." + ], + "rdfs:label": "scope" + }, + { + "@id": "untp:scheme", + "schema:rangeIncludes": { + "@id": "untp:ConformityScheme" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:ConformityProfile" + } + ], + "rdfs:comment": [ + "The conformity scheme under which this versioned profile is maintained." + ], + "rdfs:label": "scheme" + }, + { + "@id": "untp:standard", + "schema:rangeIncludes": { + "@id": "untp:Standard" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:StandardAlignment" + } + ], + "rdfs:comment": [ + "The standard against which this alignment assessment is made." + ], + "rdfs:label": "standard" + }, + { + "@id": "untp:alignmentLevel", + "schema:rangeIncludes": { + "@id": "untp:SchemeAlignmentLevel" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:StandardAlignment" + }, + { + "@id": "untp:RegulatoryAlignment" + } + ], + "rdfs:comment": [ + "A level of alignment with the referenced standard (exceeds, meets, partial,..)", + "A level of alignment with the referenced standard (exceeds, meets, partial,..)" + ], + "rdfs:label": "alignmentLevel" + }, + { + "@id": "untp:regulation", + "schema:rangeIncludes": { + "@id": "untp:Regulation" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:RegulatoryAlignment" + } + ], + "rdfs:comment": [ + "The regulation against which this alignment assessment is made." + ], + "rdfs:label": "regulation" + }, + { + "@id": "untp:assessmentCriteria", + "schema:rangeIncludes": { + "@id": "untp:Criterion" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:ConformityAssessment" + } + ], + "rdfs:comment": [ + "The specification against which the assessment is made." + ], + "rdfs:label": "assessmentCriteria" + }, + { + "@id": "untp:assessmentDate", + "schema:rangeIncludes": { + "@id": "xsd:date" + }, + "@type": [ + "rdf:Property", + "owl:DatatypeProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:ConformityAssessment" + } + ], + "rdfs:comment": [ + "The date on which this assessment was made. " + ], + "rdfs:label": "assessmentDate" + }, + { + "@id": "untp:assessedPerformance", + "schema:rangeIncludes": { + "@id": "untp:Performance" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:ConformityAssessment" + } + ], + "rdfs:comment": [ + "The assessed performance against criteria." + ], + "rdfs:label": "assessedPerformance" + }, + { + "@id": "untp:assessedProduct", + "schema:rangeIncludes": { + "@id": "untp:ProductVerification" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:ConformityAssessment" + } + ], + "rdfs:comment": [ + "The product which is the subject of this assessment." + ], + "rdfs:label": "assessedProduct" + }, + { + "@id": "untp:assessedFacility", + "schema:rangeIncludes": { + "@id": "untp:FacilityVerification" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:ConformityAssessment" + } + ], + "rdfs:comment": [ + "The facility which is the subject of this assessment." + ], + "rdfs:label": "assessedFacility" + }, + { + "@id": "untp:assessedOrganisation", + "schema:rangeIncludes": { + "@id": "untp:Party" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:ConformityAssessment" + } + ], + "rdfs:comment": [ + "An organisation that is the subject of this assessment." + ], + "rdfs:label": "assessedOrganisation" + }, + { + "@id": "untp:specifiedCondition", + "schema:rangeIncludes": { + "@id": "xsd:string" + }, + "@type": [ + "rdf:Property", + "owl:DatatypeProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:ConformityAssessment" + } + ], + "rdfs:comment": [ + "A list of specific conditions that constrain this conformity assessment. For example a specific jurisdiction, material type, or test method." + ], + "rdfs:label": "specifiedCondition" + }, + { + "@id": "untp:conformance", + "schema:rangeIncludes": { + "@id": "xsd:boolean" + }, + "@type": [ + "rdf:Property", + "owl:DatatypeProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:ConformityAssessment" + } + ], + "rdfs:comment": [ + "An indicator (true / false) whether the outcome of this assessment is conformant to the requirements defined by the standard or criterion." + ], + "rdfs:label": "conformance" + }, + { + "@id": "untp:product", + "schema:rangeIncludes": { + "@id": "untp:Product" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:ProductVerification" + }, + { + "@id": "untp:EventProduct" + } + ], + "rdfs:comment": [ + "The product, serial or batch that is the subject of this assessment", + "The product item / model / batch subject to this lifecycle event." + ], + "rdfs:label": "product" + }, + { + "@id": "untp:idVerifiedByCAB", + "schema:rangeIncludes": { + "@id": "xsd:boolean" + }, + "@type": [ + "rdf:Property", + "owl:DatatypeProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:ProductVerification" + }, + { + "@id": "untp:FacilityVerification" + } + ], + "rdfs:comment": [ + "Indicates whether the conformity assessment body has verified the identity product that is the subject of the assessment.", + "Indicates whether the conformity assessment body has verified the identity of the facility which is the subject of the assessment." + ], + "rdfs:label": "idVerifiedByCAB" + }, + { + "@id": "untp:modelNumber", + "schema:rangeIncludes": { + "@id": "xsd:string" + }, + "@type": [ + "rdf:Property", + "owl:DatatypeProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Product" + } + ], + "rdfs:comment": [ + "Where available, the model number (for manufactured products) or material identification (for bulk materials)" + ], + "rdfs:label": "modelNumber" + }, + { + "@id": "untp:batchNumber", + "schema:rangeIncludes": { + "@id": "xsd:string" + }, + "@type": [ + "rdf:Property", + "owl:DatatypeProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Product" + } + ], + "rdfs:comment": [ + "Identifier of the specific production batch of the product. Unique within the product class." + ], + "rdfs:label": "batchNumber" + }, + { + "@id": "untp:itemNumber", + "schema:rangeIncludes": { + "@id": "xsd:string" + }, + "@type": [ + "rdf:Property", + "owl:DatatypeProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Product" + } + ], + "rdfs:comment": [ + "A number or code representing a specific serialised item of the product. Unique within product class." + ], + "rdfs:label": "itemNumber" + }, + { + "@id": "untp:idGranularity", + "schema:rangeIncludes": { + "@id": "untp:ProductIDGranularity" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Product" + } + ], + "rdfs:comment": [ + "The identification granularity for this product (item, batch, model)" + ], + "rdfs:label": "idGranularity" + }, + { + "@id": "untp:productImage", + "schema:rangeIncludes": { + "@id": "untp:Link" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Product" + } + ], + "rdfs:comment": [ + "Reference information (location, type, name) of an image of the product." + ], + "rdfs:label": "productImage" + }, + { + "@id": "untp:characteristics", + "schema:rangeIncludes": { + "@id": "untp:Characteristics" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Product" + } + ], + "rdfs:comment": [ + "A set of industry specific product information. " + ], + "rdfs:label": "characteristics" + }, + { + "@id": "untp:productCategory", + "schema:rangeIncludes": { + "@id": "untp:Classification" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Product" + } + ], + "rdfs:comment": [ + "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" + ], + "rdfs:label": "productCategory" + }, + { + "@id": "untp:producedAtFacility", + "schema:rangeIncludes": { + "@id": "untp:Facility" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Product" + } + ], + "rdfs:comment": [ + "The Facility where the product batch was produced / manufactured." + ], + "rdfs:label": "producedAtFacility" + }, + { + "@id": "untp:productionDate", + "schema:rangeIncludes": { + "@id": "xsd:date" + }, + "@type": [ + "rdf:Property", + "owl:DatatypeProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Product" + } + ], + "rdfs:comment": [ + "The ISO 8601 date on which the product batch or individual serialised item was manufactured." + ], + "rdfs:label": "productionDate" + }, + { + "@id": "untp:expiryDate", + "schema:rangeIncludes": { + "@id": "xsd:date" + }, + "@type": [ + "rdf:Property", + "owl:DatatypeProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Product" + } + ], + "rdfs:comment": [ + "The date at which this product is no longer fit for use. Typically used for a food product use-by date but may also represent the usable life of any product." + ], + "rdfs:label": "expiryDate" + }, + { + "@id": "untp:countryOfProduction", + "schema:rangeIncludes": { + "@id": "untp:Country" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Product" + } + ], + "rdfs:comment": [ + "The country in which this item was produced / manufactured.using ISO-3166 code and name." + ], + "rdfs:label": "countryOfProduction" + }, + { + "@id": "untp:dimensions", + "schema:rangeIncludes": { + "@id": "untp:Dimension" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Product" + }, + { + "@id": "untp:Package" + } + ], + "rdfs:comment": [ + "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, width, or height.\"weight\":{\"value\":10, \"unit\":\"KGM\"}", + "dimensions of the packaging" + ], + "rdfs:label": "dimensions" + }, + { + "@id": "untp:materialProvenance", + "schema:rangeIncludes": { + "@id": "untp:Material" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Product" + } + ], + "rdfs:comment": [ + "A list of materials provenance objects providing details on the origin and mass fraction of materials of the product or batch." + ], + "rdfs:label": "materialProvenance" + }, + { + "@id": "untp:packaging", + "schema:rangeIncludes": { + "@id": "untp:Package" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Product" + } + ], + "rdfs:comment": [ + "The packaging for this product." + ], + "rdfs:label": "packaging" + }, + { + "@id": "untp:productLabel", + "schema:rangeIncludes": { + "@id": "untp:Image" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Product" + } + ], + "rdfs:comment": [ + "An array of labels that may appear on the product such as certification marks or regulatory labels." + ], + "rdfs:label": "productLabel" + }, + { + "@id": "untp:weight", + "schema:rangeIncludes": { + "@id": "untp:Measure" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Dimension" + } + ], + "rdfs:comment": [ + "the weight of the product. EG {\"value\":10, \"unit\":\"KGM\"}" + ], + "rdfs:label": "weight" + }, + { + "@id": "untp:length", + "schema:rangeIncludes": { + "@id": "untp:Measure" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Dimension" + } + ], + "rdfs:comment": [ + "The length of the product or packaging eg {\"value\":840, \"unit\":\"MMT\"}" + ], + "rdfs:label": "length" + }, + { + "@id": "untp:width", + "schema:rangeIncludes": { + "@id": "untp:Measure" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Dimension" + } + ], + "rdfs:comment": [ + "The width of the product or packaging. eg {\"value\":150, \"unit\":\"MMT\"}" + ], + "rdfs:label": "width" + }, + { + "@id": "untp:height", + "schema:rangeIncludes": { + "@id": "untp:Measure" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Dimension" + } + ], + "rdfs:comment": [ + "The height of the product or packaging. eg {\"value\":220, \"unit\":\"MMT\"}" + ], + "rdfs:label": "height" + }, + { + "@id": "untp:volume", + "schema:rangeIncludes": { + "@id": "untp:Measure" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Dimension" + } + ], + "rdfs:comment": [ + "The displacement volume of the product. eg {\"value\":7.5, \"unit\":\"LTR\"}" + ], + "rdfs:label": "volume" + }, + { + "@id": "untp:materialUsed", + "schema:rangeIncludes": { + "@id": "untp:Material" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Package" + } + ], + "rdfs:comment": [ + "materials used for the packaging." + ], + "rdfs:label": "materialUsed" + }, + { + "@id": "untp:packageLabel", + "schema:rangeIncludes": { + "@id": "untp:Image" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Package" + } + ], + "rdfs:comment": [ + "An array of package labels that may appear on the packaging together with their meaning. Use for small images that represent certification marks or regulatory requirements. Large images should be linked as evidence to claims." + ], + "rdfs:label": "packageLabel" + }, + { + "@id": "untp:facility", + "schema:rangeIncludes": { + "@id": "untp:Facility" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:FacilityVerification" + } + ], + "rdfs:comment": [ + "The facility which is the subject of this assessment" + ], + "rdfs:label": "facility" + }, + { + "@id": "untp:eventDate", + "schema:rangeIncludes": { + "@id": "xsd:datetime" + }, + "@type": [ + "rdf:Property", + "owl:DatatypeProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:LifecycleEvent" + }, + { + "@id": "untp:MakeEvent" + }, + { + "@id": "untp:MoveEvent" + }, + { + "@id": "untp:ModifyEvent" + } + ], + "rdfs:comment": [ + "The date and time at which this lifecycle event occurs. use 00:00 for time if only a date is required.", + "The date and time at which this lifecycle event occurs. use 00:00 for time if only a date is required.", + "The date and time at which this lifecycle event occurs. use 00:00 for time if only a date is required.", + "The date and time at which this lifecycle event occurs. use 00:00 for time if only a date is required." + ], + "rdfs:label": "eventDate" + }, + { + "@id": "untp:sensorData", + "schema:rangeIncludes": { + "@id": "untp:SensorData" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:LifecycleEvent" + }, + { + "@id": "untp:MakeEvent" + }, + { + "@id": "untp:MoveEvent" + }, + { + "@id": "untp:ModifyEvent" + } + ], + "rdfs:comment": [ + "A sensor data set associated with this lifecycle event.", + "A sensor data set associated with this lifecycle event.", + "A sensor data set associated with this lifecycle event.", + "A sensor data set associated with this lifecycle event." + ], + "rdfs:label": "sensorData" + }, + { + "@id": "untp:activityType", + "schema:rangeIncludes": { + "@id": "untp:Classification" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:LifecycleEvent" + }, + { + "@id": "untp:MakeEvent" + }, + { + "@id": "untp:MoveEvent" + }, + { + "@id": "untp:ModifyEvent" + } + ], + "rdfs:comment": [ + "The business activity that this event represents (eg shipping, repair, etc) using a standard classification scheme - eg https://ref.gs1.org/cbv/BizStep. This may be replaced with industry specific vocabularies (ginning, spinning, weaving, dyeing, etc in textiles)", + "The business activity that this event represents (eg shipping, repair, etc) using a standard classification scheme - eg https://ref.gs1.org/cbv/BizStep. This may be replaced with industry specific vocabularies (ginning, spinning, weaving, dyeing, etc in textiles)", + "The business activity that this event represents (eg shipping, repair, etc) using a standard classification scheme - eg https://ref.gs1.org/cbv/BizStep. This may be replaced with industry specific vocabularies (ginning, spinning, weaving, dyeing, etc in textiles)", + "The business activity that this event represents (eg shipping, repair, etc) using a standard classification scheme - eg https://ref.gs1.org/cbv/BizStep. This may be replaced with industry specific vocabularies (ginning, spinning, weaving, dyeing, etc in textiles)" + ], + "rdfs:label": "activityType" + }, + { + "@id": "untp:inputProduct", + "schema:rangeIncludes": { + "@id": "untp:EventProduct" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:MakeEvent" + } + ], + "rdfs:comment": [ + "An array of input products and quantities for this production or manufacturing process" + ], + "rdfs:label": "inputProduct" + }, + { + "@id": "untp:outputProduct", + "schema:rangeIncludes": { + "@id": "untp:EventProduct" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:MakeEvent" + } + ], + "rdfs:comment": [ + "An array of output products and quantities for this produciton or manufacturing process" + ], + "rdfs:label": "outputProduct" + }, + { + "@id": "untp:madeAtFacility", + "schema:rangeIncludes": { + "@id": "untp:Facility" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:MakeEvent" + } + ], + "rdfs:comment": [ + "The facility at which this production / manufacturing event happens." + ], + "rdfs:label": "madeAtFacility" + }, + { + "@id": "untp:rawData", + "schema:rangeIncludes": { + "@id": "untp:Link" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:SensorData" + } + ], + "rdfs:comment": [ + "Link to raw data file associated with this sensor reading (eg an image)." + ], + "rdfs:label": "rawData" + }, + { + "@id": "untp:sensor", + "schema:rangeIncludes": { + "@id": "untp:Product" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:SensorData" + } + ], + "rdfs:comment": [ + "The sensor device used for this sensor measurement" + ], + "rdfs:label": "sensor" + }, + { + "@id": "untp:quantity", + "schema:rangeIncludes": { + "@id": "untp:Measure" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:EventProduct" + } + ], + "rdfs:comment": [ + "The quantity of product subject to this lifecycle event. Not needed for serialised items." + ], + "rdfs:label": "quantity" + }, + { + "@id": "untp:disposition", + "schema:rangeIncludes": { + "@id": "untp:ProductStatus" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:EventProduct" + } + ], + "rdfs:comment": [ + "The status of the product after the event has happened." + ], + "rdfs:label": "disposition" + }, + { + "@id": "untp:movedProduct", + "schema:rangeIncludes": { + "@id": "untp:EventProduct" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:MoveEvent" + } + ], + "rdfs:comment": [ + "An array of products and quantities for this movement / shipment process" + ], + "rdfs:label": "movedProduct" + }, + { + "@id": "untp:fromFacility", + "schema:rangeIncludes": { + "@id": "untp:Facility" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:MoveEvent" + } + ], + "rdfs:comment": [ + "The source facility for this movement / shipment of products" + ], + "rdfs:label": "fromFacility" + }, + { + "@id": "untp:toFacility", + "schema:rangeIncludes": { + "@id": "untp:Facility" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:MoveEvent" + } + ], + "rdfs:comment": [ + "The destination facility for this movement / shipment of products" + ], + "rdfs:label": "toFacility" + }, + { + "@id": "untp:consignmentId", + "schema:rangeIncludes": { + "@id": "xsd:anyURI" + }, + "@type": [ + "rdf:Property", + "owl:DatatypeProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:MoveEvent" + } + ], + "rdfs:comment": [ + "The consignment ID related to this movement of products. Ideally this is a resolvable URL but if not available then use a URN notation such as urn:carrier:waybillNumber." + ], + "rdfs:label": "consignmentId" + }, + { + "@id": "untp:modifiedProduct", + "schema:rangeIncludes": { + "@id": "untp:EventProduct" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:ModifyEvent" + } + ], + "rdfs:comment": [ + "An array of products and quantities for this intervention (repair, inspection, etc)" + ], + "rdfs:label": "modifiedProduct" + }, + { + "@id": "untp:modifiedAtFacility", + "schema:rangeIncludes": { + "@id": "untp:Facility" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:ModifyEvent" + } + ], + "rdfs:comment": [ + "The facility at which this intervention event happens." + ], + "rdfs:label": "modifiedAtFacility" + }, + { + "@id": "untp:registeredName", + "schema:rangeIncludes": { + "@id": "xsd:string" + }, + "@type": [ + "rdf:Property", + "owl:DatatypeProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:RegisteredIdentity" + } + ], + "rdfs:comment": [ + "The registered name of the entity within the identifier scheme. Examples: product - EV battery 300Ah, Party - Sample Company Pty Ltd, Facility - Green Acres battery factory " + ], + "rdfs:label": "registeredName" + }, + { + "@id": "untp:registeredDate", + "schema:rangeIncludes": { + "@id": "xsd:date" + }, + "@type": [ + "rdf:Property", + "owl:DatatypeProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:RegisteredIdentity" + } + ], + "rdfs:comment": [ + "The date on which this identity was first registered with the registrar." + ], + "rdfs:label": "registeredDate" + }, + { + "@id": "untp:publicInformation", + "schema:rangeIncludes": { + "@id": "xsd:anyURI" + }, + "@type": [ + "rdf:Property", + "owl:DatatypeProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:RegisteredIdentity" + } + ], + "rdfs:comment": [ + "A link to further information about the registered entity on the authoritative registrar site." + ], + "rdfs:label": "publicInformation" + }, + { + "@id": "untp:registrar", + "schema:rangeIncludes": { + "@id": "untp:Party" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:RegisteredIdentity" + } + ], + "rdfs:comment": [ + "The registrar party that operates the register." + ], + "rdfs:label": "registrar" + }, + { + "@id": "untp:registerType", + "schema:rangeIncludes": { + "@id": "untp:RegistryType" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:RegisteredIdentity" + } + ], + "rdfs:comment": [ + "The thematic purpose of the register - organisations, facilities, products, trademarks, etc" + ], + "rdfs:label": "registerType" + }, + { + "@id": "untp:registrationScope", + "schema:rangeIncludes": { + "@id": "xsd:anyURI" + }, + "@type": [ + "rdf:Property", + "owl:DatatypeProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:RegisteredIdentity" + } + ], + "rdfs:comment": [ + "List of URIs that represent the roles or scopes of membership. For example [\"https://abr.business.gov.au/Help/EntityTypeDescription?Id=19\"]" + ], + "rdfs:label": "registrationScope" + }, + { + "@id": "untp:AssessmentLevel", + "@type": "rdfs:Class", + "rdfs:label": "AssessmentLevel", + "rdfs:comment": "Type of authority endorsement of the assessment process" + }, + { + "@id": "untp:AssessmentLevel#authority-benchmark", + "@type": "untp:AssessmentLevel", + "rdf:value": "authority-benchmark", + "rdfs:label": "Authority-derived assurance: Recognition by approved benchmarking organisation", + "rdfs:comment": "Benchmarking of scheme by an organization approved to UNIDO benchmarking\nprinciples and process. UNIDO Global Best Practice Framework for Organisations Performing Benchmarking Activities for Certification-related Conformity Assessment Schemes 2026" + }, + { + "@id": "untp:AssessmentLevel#authority-mandate", + "@type": "untp:AssessmentLevel", + "rdf:value": "authority-mandate", + "rdfs:label": "Authority-derived assurance: Recognition by government mandate", + "rdfs:comment": "Government mandate for conformity assessment activity. Ownership or mandate provided by national government or intergovernmental entity." + }, + { + "@id": "untp:AssessmentLevel#authority-globalmra", + "@type": "untp:AssessmentLevel", + "rdf:value": "authority-globalmra", + "rdfs:label": "Authority-derived assurance:Global accreditation mutual recognition arrangement", + "rdfs:comment": "Accreditation of CAB under global mutual recognition arrangement by a body peer-evaluated\nto ISO/IEC 17011. Scheme evaluation is a prerequisite for accreditation of CABs by bodies that are signatories to the Global Accreditation Cooperation Incorporated Mutual Recognition Arrangement." + }, + { + "@id": "untp:AssessmentLevel#authority-peer", + "@type": "untp:AssessmentLevel", + "rdf:value": "authority-peer", + "rdfs:label": "Authority-derived assurance: Recognition by a governmental peer assessment authority", + "rdfs:comment": "Peer assessment process managed by government. Ownership or mandate provided by national government or intergovernmental entity." + }, + { + "@id": "untp:AssessmentLevel#authority-extended-mra", + "@type": "untp:AssessmentLevel", + "rdf:value": "authority-extended-mra", + "rdfs:label": "Authority- derived assurance: Peer assessment body recognition for accredited CAB", + "rdfs:comment": "Independent peer assessment for accredited CAB. This pathway applies to CABs accredited under the Mutual Recognition Arrangement of the Global Accreditation Cooperation Incorporated. Schemes used by CABs may be owned by the peer assessment body but the CAB itself shall not be owned by or otherwise related to the peer assessment body." + }, + { + "@id": "untp:AssessmentLevel#scheme-self", + "@type": "untp:AssessmentLevel", + "rdf:value": "scheme-self", + "rdfs:label": "Scheme-derived assurance: Self-declaration by registered scheme", + "rdfs:comment": "Scheme owner directly conducting conformity assessment activities. The linked scheme self-declaration can be used to assist in judging credibility of the scheme." + }, + { + "@id": "untp:AssessmentLevel#scheme-cab", + "@type": "untp:AssessmentLevel", + "rdf:value": "scheme-cab", + "rdfs:label": "Scheme-derived assurance: Recognition of CAB by registered scheme", + "rdfs:comment": "Scheme owner recognition of other parties assessing against the scheme standards. The linked scheme self-declaration can be used to assist in judging credibility of the scheme. Users of conformity credentials issued by a CAB recognised under a scheme may refer to the linked scheme self-declaration for details of the CAB-approval process used by the scheme owner" + }, + { + "@id": "untp:AssessmentLevel#no-endorsement", + "@type": "untp:AssessmentLevel", + "rdf:value": "no-endorsement", + "rdfs:label": "No endorsement.", + "rdfs:comment": "conformity assessment claiming no external authority or else unspecified" + }, + { + "@id": "untp:AssessmentSubjectType", + "@type": "rdfs:Class", + "rdfs:label": "AssessmentSubjectType", + "rdfs:comment": "The type of entity being assessed." + }, + { + "@id": "untp:AssessmentSubjectType#product", + "@type": "untp:AssessmentSubjectType", + "rdf:value": "product", + "rdfs:label": "Product", + "rdfs:comment": "The conformity profile targets products — assessing characteristics, composition, performance, or safety of manufactured goods." + }, + { + "@id": "untp:AssessmentSubjectType#facility", + "@type": "untp:AssessmentSubjectType", + "rdf:value": "facility", + "rdfs:label": "Facility", + "rdfs:comment": "The conformity profile targets facilities — assessing the operational practices, environmental performance, or working conditions at a specific site." + }, + { + "@id": "untp:AssessmentSubjectType#organisation", + "@type": "untp:AssessmentSubjectType", + "rdf:value": "organisation", + "rdfs:label": "Organisation", + "rdfs:comment": "The conformity profile targets organisations — assessing entity-level governance, policies, management systems, or corporate sustainability performance." + }, + { + "@id": "untp:AssessorLevel", + "@type": "rdfs:Class", + "rdfs:label": "AssessorLevel", + "rdfs:comment": "Code that describes the level of independent assurance of the specific assessment" + }, + { + "@id": "untp:AssessorLevel#self", + "@type": "untp:AssessorLevel", + "rdf:value": "self", + "rdfs:label": "Self assessed", + "rdfs:comment": " self-assessment" + }, + { + "@id": "untp:AssessorLevel#commercial", + "@type": "untp:AssessorLevel", + "rdf:value": "commercial", + "rdfs:label": "Commercial assessment", + "rdfs:comment": " conformity assessment by related body or under commercial contract" + }, + { + "@id": "untp:AssessorLevel#buyer", + "@type": "untp:AssessorLevel", + "rdf:value": "buyer", + "rdfs:label": "Buyer assessment", + "rdfs:comment": " conformity assessment by potential purchaser" + }, + { + "@id": "untp:AssessorLevel#membership", + "@type": "untp:AssessorLevel", + "rdf:value": "membership", + "rdfs:label": "Industry body assessment", + "rdfs:comment": " conformity assessment by industry representative body or membership body" + }, + { + "@id": "untp:AssessorLevel#unspecified", + "@type": "untp:AssessorLevel", + "rdf:value": "unspecified", + "rdfs:label": "No independent assessment", + "rdfs:comment": " conformity assessment by party with unspecified relationship " + }, + { + "@id": "untp:AssessorLevel#3rdParty", + "@type": "untp:AssessorLevel", + "rdf:value": "3rdParty", + "rdfs:label": "Independent third party assessment", + "rdfs:comment": " 3rd party (independent) conformity assessment" + }, + { + "@id": "untp:AssessorLevel#hybrid", + "@type": "untp:AssessorLevel", + "rdf:value": "hybrid", + "rdfs:label": "Input from self-declaring parties", + "rdfs:comment": "2nd or 3rd party conformity assessment that is dependent on the accuracy of information provided by self-declaring parties" + }, + { + "@id": "untp:AttestationType", + "@type": "rdfs:Class", + "rdfs:label": "AttestationType", + "rdfs:comment": "A code for the type of the attestation credential" + }, + { + "@id": "untp:AttestationType#certification", + "@type": "untp:AttestationType", + "rdf:value": "certification", + "rdfs:label": "certification", + "rdfs:comment": "A formal third party certification of conformity" + }, + { + "@id": "untp:AttestationType#declaration", + "@type": "untp:AttestationType", + "rdf:value": "declaration", + "rdfs:label": "declaration", + "rdfs:comment": "A self assessed declaration of conformity" + }, + { + "@id": "untp:AttestationType#inspection", + "@type": "untp:AttestationType", + "rdf:value": "inspection", + "rdfs:label": "inspection", + "rdfs:comment": "An Inspection report " + }, + { + "@id": "untp:AttestationType#testing", + "@type": "untp:AttestationType", + "rdf:value": "testing", + "rdfs:label": "testing", + "rdfs:comment": "A test report" + }, + { + "@id": "untp:AttestationType#verification", + "@type": "untp:AttestationType", + "rdf:value": "verification", + "rdfs:label": "verification", + "rdfs:comment": "A verification report" + }, + { + "@id": "untp:AttestationType#validation", + "@type": "untp:AttestationType", + "rdf:value": "validation", + "rdfs:label": "validation", + "rdfs:comment": "A validation report" + }, + { + "@id": "untp:AttestationType#calibration", + "@type": "untp:AttestationType", + "rdf:value": "calibration", + "rdfs:label": "calibration", + "rdfs:comment": "An equipment calibration report" + }, + { + "@id": "untp:CountryCode", + "@type": "rdfs:Class", + "rdfs:label": "CountryCode", + "rdfs:comment": "ISO 2 letter country code" + }, + { + "@id": "untp:CredentialStatus", + "@type": "rdfs:Class", + "rdfs:label": "CredentialStatus", + "rdfs:comment": "The status purpose of a credential status entry within a W3C Verifiable Credential, indicating the type of status check that can be performed (e.g. revocation, suspension, refresh, or message)." + }, + { + "@id": "untp:CredentialStatus#refresh", + "@type": "untp:CredentialStatus", + "rdf:value": "refresh", + "rdfs:label": "refresh", + "rdfs:comment": "Used to signal that an updated verifiable credential is available via the credential's refresh service feature. This status does not invalidate the verifiable credential and is not reversible." + }, + { + "@id": "untp:CredentialStatus#revocation", + "@type": "untp:CredentialStatus", + "rdf:value": "revocation", + "rdfs:label": "revocation", + "rdfs:comment": "Used to cancel the validity of a verifiable credential. This status is not reversible." + }, + { + "@id": "untp:CredentialStatus#suspension", + "@type": "untp:CredentialStatus", + "rdf:value": "suspension", + "rdfs:label": "suspension", + "rdfs:comment": "Used to temporarily prevent the acceptance of a verifiable credential. This status is reversible." + }, + { + "@id": "untp:CredentialStatus#message", + "@type": "untp:CredentialStatus", + "rdf:value": "message", + "rdfs:label": "message", + "rdfs:comment": "Used to indicate a ussuer specified flexible status message associated with a verifiable credential. The status message descriptions MUST be defined in credentialSubject.statusMessages. credentialSubject.statusSize MUST be specified when this statusPurpose value is used." + }, + { + "@id": "untp:CriterionStatus", + "@type": "rdfs:Class", + "rdfs:label": "CriterionStatus", + "rdfs:comment": "The status of the conformity profile or criterion" + }, + { + "@id": "untp:CriterionStatus#proposed", + "@type": "untp:CriterionStatus", + "rdf:value": "proposed", + "rdfs:label": "Proposed", + "rdfs:comment": "The criterion is proposed" + }, + { + "@id": "untp:CriterionStatus#active", + "@type": "untp:CriterionStatus", + "rdf:value": "active", + "rdfs:label": "Active", + "rdfs:comment": "The criterion is in active use." + }, + { + "@id": "untp:CriterionStatus#deprecated", + "@type": "untp:CriterionStatus", + "rdf:value": "deprecated", + "rdfs:label": "Deprecated", + "rdfs:comment": "The criterion is deprecated." + }, + { + "@id": "untp:LicenseType", + "@type": "rdfs:Class", + "rdfs:label": "LicenseType", + "rdfs:comment": "The license type of the published vocabulary" + }, + { + "@id": "untp:LicenseType#proprietary-Code", + "@type": "untp:LicenseType", + "rdf:value": "proprietary-Code", + "rdfs:label": "Proprietary", + "rdfs:comment": "Commercial software, internal docs. Restrictiveness - Very high" + }, + { + "@id": "untp:LicenseType#proprietary-Document", + "@type": "untp:LicenseType", + "rdf:value": "proprietary-Document", + "rdfs:label": "Documentation licenses", + "rdfs:comment": "Manuals, standards. Restrictiveness - Medium" + }, + { + "@id": "untp:LicenseType#permissive-OpenSource", + "@type": "untp:LicenseType", + "rdf:value": "permissive-OpenSource", + "rdfs:label": "Permissive open source", + "rdfs:comment": "Libraries, frameworks. Restrictiveness - Low" + }, + { + "@id": "untp:LicenseType#copyleft", + "@type": "untp:LicenseType", + "rdf:value": "copyleft", + "rdfs:label": "Copyleft", + "rdfs:comment": "Platforms, infrastructure. Restrictiveness - Medium–high" + }, + { + "@id": "untp:LicenseType#creative-Commons", + "@type": "untp:LicenseType", + "rdf:value": "creative-Commons", + "rdfs:label": "Creative Commons", + "rdfs:comment": "Media, publications. Restrictiveness - Variable" + }, + { + "@id": "untp:LicenseType#source-Available", + "@type": "untp:LicenseType", + "rdf:value": "source-Available", + "rdfs:label": "Source-available", + "rdfs:comment": "Commercial SaaS vendors. Restrictiveness - Medium–high" + }, + { + "@id": "untp:LicenseType#public", + "@type": "untp:LicenseType", + "rdf:value": "public", + "rdfs:label": "Public domain", + "rdfs:comment": "Data, examples. Restrictiveness - None" + }, + { + "@id": "untp:MimeType", + "@type": "rdfs:Class", + "rdfs:label": "MimeType", + "rdfs:comment": "IANA multipart media encoding type " + }, + { + "@id": "untp:PartyRole", + "@type": "rdfs:Class", + "rdfs:label": "PartyRole", + "rdfs:comment": "The role for this facility - party or product - party relationship" + }, + { + "@id": "untp:PartyRole#owner", + "@type": "untp:PartyRole", + "rdf:value": "owner", + "rdfs:label": "Party that owns the product or asset" + }, + { + "@id": "untp:PartyRole#producer", + "@type": "untp:PartyRole", + "rdf:value": "producer", + "rdfs:label": "Party that extracts, grows, or produces raw materials" + }, + { + "@id": "untp:PartyRole#manufacturer", + "@type": "untp:PartyRole", + "rdf:value": "manufacturer", + "rdfs:label": "Party that manufactures or assembles the product" + }, + { + "@id": "untp:PartyRole#processor", + "@type": "untp:PartyRole", + "rdf:value": "processor", + "rdfs:label": "Party that processes or transforms materials" + }, + { + "@id": "untp:PartyRole#remanufacturer", + "@type": "untp:PartyRole", + "rdf:value": "remanufacturer", + "rdfs:label": "Party that remanufactures or refurbishes products" + }, + { + "@id": "untp:PartyRole#recycler", + "@type": "untp:PartyRole", + "rdf:value": "recycler", + "rdfs:label": "Party that recovers materials from products" + }, + { + "@id": "untp:PartyRole#operator", + "@type": "untp:PartyRole", + "rdf:value": "operator", + "rdfs:label": "Party operating a facility or process" + }, + { + "@id": "untp:PartyRole#serviceProvider", + "@type": "untp:PartyRole", + "rdf:value": "serviceProvider", + "rdfs:label": "Party providing maintenance or servicing" + }, + { + "@id": "untp:PartyRole#inspector", + "@type": "untp:PartyRole", + "rdf:value": "inspector", + "rdfs:label": "Party performing inspection or testing" + }, + { + "@id": "untp:PartyRole#certifier", + "@type": "untp:PartyRole", + "rdf:value": "certifier", + "rdfs:label": "Party issuing certification or conformity assessment" + }, + { + "@id": "untp:PartyRole#logisticsProvider", + "@type": "untp:PartyRole", + "rdf:value": "logisticsProvider", + "rdfs:label": "Party responsible for logistics operations" + }, + { + "@id": "untp:PartyRole#carrier", + "@type": "untp:PartyRole", + "rdf:value": "carrier", + "rdfs:label": "Party physically transporting the goods" + }, + { + "@id": "untp:PartyRole#consignor", + "@type": "untp:PartyRole", + "rdf:value": "consignor", + "rdfs:label": "Party sending the goods" + }, + { + "@id": "untp:PartyRole#consignee", + "@type": "untp:PartyRole", + "rdf:value": "consignee", + "rdfs:label": "Party receiving the goods" + }, + { + "@id": "untp:PartyRole#importer", + "@type": "untp:PartyRole", + "rdf:value": "importer", + "rdfs:label": "Party importing the goods into a jurisdiction" + }, + { + "@id": "untp:PartyRole#exporter", + "@type": "untp:PartyRole", + "rdf:value": "exporter", + "rdfs:label": "Party exporting the goods from a jurisdiction" + }, + { + "@id": "untp:PartyRole#distributor", + "@type": "untp:PartyRole", + "rdf:value": "distributor", + "rdfs:label": "Party distributing goods in the supply chain" + }, + { + "@id": "untp:PartyRole#retailer", + "@type": "untp:PartyRole", + "rdf:value": "retailer", + "rdfs:label": "Party selling goods to end users" + }, + { + "@id": "untp:PartyRole#brandOwner", + "@type": "untp:PartyRole", + "rdf:value": "brandOwner", + "rdfs:label": "Party responsible for the brand or product specification" + }, + { + "@id": "untp:PartyRole#regulator", + "@type": "untp:PartyRole", + "rdf:value": "regulator", + "rdfs:label": "Authority responsible for regulatory oversight" + }, + { + "@id": "untp:ImprovementIndicator", + "@type": "rdfs:Class", + "rdfs:label": "ImprovementIndicator", + "rdfs:comment": "Indicator of whether conforming performance is greater than or less than the defined threshold." + }, + { + "@id": "untp:ImprovementIndicator#higher", + "@type": "untp:ImprovementIndicator", + "rdf:value": "higher", + "rdfs:label": "higher", + "rdfs:comment": "Performance improves with a higher measured value" + }, + { + "@id": "untp:ImprovementIndicator#lower", + "@type": "untp:ImprovementIndicator", + "rdf:value": "lower", + "rdfs:label": "lower", + "rdfs:comment": "Performance improves with a lower measured value" + }, + { + "@id": "untp:AggregationType", + "@type": "rdfs:Class", + "rdfs:label": "AggregationType", + "rdfs:comment": "Indicates how to aggregate multiple values to report a single performance metric." + }, + { + "@id": "untp:AggregationType#sum", + "@type": "untp:AggregationType", + "rdf:value": "sum", + "rdfs:label": "sum", + "rdfs:comment": "Values add up (e.g. total GHG emissions across all facilities = sum of each facility's emissions)" + }, + { + "@id": "untp:AggregationType#weighted-average", + "@type": "untp:AggregationType", + "rdf:value": "weighted-average", + "rdfs:label": "weighted-average", + "rdfs:comment": "Values must be averaged weighted by volume/output (e.g. emissions intensity per kg across suppliers)" + }, + { + "@id": "untp:AggregationType#latest", + "@type": "untp:AggregationType", + "rdf:value": "latest", + "rdfs:label": "latest", + "rdfs:comment": "Only the most recent value is meaningful (e.g. a biodiversity assessment score where only the current state matters)" + }, + { + "@id": "untp:ProductIDGranularity", + "@type": "rdfs:Class", + "rdfs:label": "ProductIDGranularity", + "rdfs:comment": "Product identification granularity" + }, + { + "@id": "untp:ProductIDGranularity#model", + "@type": "untp:ProductIDGranularity", + "rdf:value": "model", + "rdfs:label": "product model level ID", + "rdfs:comment": "" + }, + { + "@id": "untp:ProductIDGranularity#batch", + "@type": "untp:ProductIDGranularity", + "rdf:value": "batch", + "rdfs:label": "product manufactured batch level ID", + "rdfs:comment": "" + }, + { + "@id": "untp:ProductIDGranularity#item", + "@type": "untp:ProductIDGranularity", + "rdf:value": "item", + "rdfs:label": "serialised item level ID", + "rdfs:comment": "" + }, + { + "@id": "untp:ProductStatus", + "@type": "rdfs:Class", + "rdfs:label": "ProductStatus", + "rdfs:comment": "The lifecycle status of a product, describing its current state from initial production through to eventual disposal or recycling. Used as the value of the disposition property on EventProduct in traceability events." + }, + { + "@id": "untp:ProductStatus#new", + "@type": "untp:ProductStatus", + "rdf:value": "new", + "rdfs:label": "New", + "rdfs:comment": "Product has been newly manufactured or produced and has not yet entered service. Equivalent to GS1 CBV Disp-active." + }, + { + "@id": "untp:ProductStatus#inTransit", + "@type": "untp:ProductStatus", + "rdf:value": "inTransit", + "rdfs:label": "In Transit", + "rdfs:comment": "Product has been shipped and is in transit between facilities. Equivalent to GS1 CBV Disp-in_transit." + }, + { + "@id": "untp:ProductStatus#active", + "@type": "untp:ProductStatus", + "rdf:value": "active", + "rdfs:label": "Active", + "rdfs:comment": "Product is in active service or use by the end customer or a downstream manufacturer. Equivalent to GS1 CBV Disp-retail_sold." + }, + { + "@id": "untp:ProductStatus#repaired", + "@type": "untp:ProductStatus", + "rdf:value": "repaired", + "rdfs:label": "Repaired", + "rdfs:comment": "Product has been repaired or refurbished to restore functionality and returned to service. Equivalent to GS1 CBV Disp-available (after a repairing step)." + }, + { + "@id": "untp:ProductStatus#recalled", + "@type": "untp:ProductStatus", + "rdf:value": "recalled", + "rdfs:label": "Recalled", + "rdfs:comment": "Product has been withdrawn from the market or service due to a safety, quality, or compliance issue. Equivalent to GS1 CBV Disp-recalled." + }, + { + "@id": "untp:ProductStatus#expired", + "@type": "untp:ProductStatus", + "rdf:value": "expired", + "rdfs:label": "Expired", + "rdfs:comment": "Product has passed its use-by, certification, or regulatory expiration date. Equivalent to GS1 CBV Disp-expired." + }, + { + "@id": "untp:ProductStatus#consumed", + "@type": "untp:ProductStatus", + "rdf:value": "consumed", + "rdfs:label": "Consumed", + "rdfs:comment": "Product has been consumed as an input to a manufacturing process and no longer exists as a separate item. No direct GS1 CBV equivalent." + }, + { + "@id": "untp:ProductStatus#recycled", + "@type": "untp:ProductStatus", + "rdf:value": "recycled", + "rdfs:label": "Recycled", + "rdfs:comment": "Product has been processed to recover constituent materials for reuse in new products. No direct GS1 CBV equivalent." + }, + { + "@id": "untp:ProductStatus#disposed", + "@type": "untp:ProductStatus", + "rdf:value": "disposed", + "rdfs:label": "Disposed", + "rdfs:comment": "Product has reached end of life and has been disposed of or destroyed without material recovery. Equivalent to GS1 CBV Disp-disposed and Disp-destroyed." + }, + { + "@id": "untp:RegistryType", + "@type": "rdfs:Class", + "rdfs:label": "RegistryType", + "rdfs:comment": "A registry category code." + }, + { + "@id": "untp:RegistryType#product", + "@type": "untp:RegistryType", + "rdf:value": "product", + "rdfs:label": "Product", + "rdfs:comment": "A register of products or product classes, such as a national product catalogue or a GS1 GTIN registry." + }, + { + "@id": "untp:RegistryType#facility", + "@type": "untp:RegistryType", + "rdf:value": "facility", + "rdfs:label": "Facility", + "rdfs:comment": "A register of facilities or sites, such as a mining cadastre, environmental permit register, or industrial facility directory." + }, + { + "@id": "untp:RegistryType#business", + "@type": "untp:RegistryType", + "rdf:value": "business", + "rdfs:label": "Business", + "rdfs:comment": "A register of business entities or legal persons, such as a national company register, VAT register, or LEI registry." + }, + { + "@id": "untp:RegistryType#trademark", + "@type": "untp:RegistryType", + "rdf:value": "trademark", + "rdfs:label": "Trademark", + "rdfs:comment": "A register of trademarks, certification marks, or other intellectual property identifiers maintained by a national or international IP office." + }, + { + "@id": "untp:RegistryType#land", + "@type": "untp:RegistryType", + "rdf:value": "land", + "rdfs:label": "Land", + "rdfs:comment": "A register of land titles, parcels, or cadastral boundaries, such as a national land registry or territorial cadastre." + }, + { + "@id": "untp:RegistryType#accreditation", + "@type": "untp:RegistryType", + "rdf:value": "accreditation", + "rdfs:label": "Accreditation", + "rdfs:comment": "A register of accredited conformity assessment bodies, maintained by a national or regional accreditation authority." + }, + { + "@id": "untp:SchemeAlignmentLevel", + "@type": "rdfs:Class", + "rdfs:label": "SchemeAlignmentLevel", + "rdfs:comment": "Alignment level of a scheme profile or criterion against a reference standard or regulation" + }, + { + "@id": "untp:SchemeAlignmentLevel#meets", + "@type": "untp:SchemeAlignmentLevel", + "rdf:value": "meets", + "rdfs:label": "Meets", + "rdfs:comment": "The scheme profile or criterion fully satisfies the requirements of the referenced standard or regulation." + }, + { + "@id": "untp:SchemeAlignmentLevel#exceeds", + "@type": "untp:SchemeAlignmentLevel", + "rdf:value": "exceeds", + "rdfs:label": "Exceeds", + "rdfs:comment": "The scheme profile or criterion goes beyond the requirements of the referenced standard or regulation, imposing stricter thresholds or broader scope." + }, + { + "@id": "untp:SchemeAlignmentLevel#partial", + "@type": "untp:SchemeAlignmentLevel", + "rdf:value": "partial", + "rdfs:label": "Partially meets", + "rdfs:comment": "The scheme profile or criterion addresses some but not all requirements of the referenced standard or regulation." + }, + { + "@id": "untp:SchemeEndorsementLevel", + "@type": "rdfs:Class", + "rdfs:label": "SchemeEndorsementLevel", + "rdfs:comment": "The level of endorsement or recognition that a conformity scheme has received from authoritative bodies, indicating the degree of independent assurance over the scheme's credibility and rigour." + }, + { + "@id": "untp:SchemeEndorsementLevel#endorsed_self", + "@type": "untp:SchemeEndorsementLevel", + "rdf:value": "endorsed_self", + "rdfs:label": "Self-declaration by scheme owner", + "rdfs:comment": "Scheme owner self-declaration using the UNTP scheme declaration template" + }, + { + "@id": "untp:SchemeEndorsementLevel#endorsed_mandate", + "@type": "untp:SchemeEndorsementLevel", + "rdf:value": "endorsed_mandate", + "rdfs:label": "Government owned or mandated scheme", + "rdfs:comment": "Ownership of scheme or mandate for adoption of scheme by national government or intergovernmental entity." + }, + { + "@id": "untp:SchemeEndorsementLevel#endorsed_accreditation", + "@type": "untp:SchemeEndorsementLevel", + "rdf:value": "endorsed_accreditation", + "rdfs:label": "Accreditation authority endorsement of scheme suitability", + "rdfs:comment": "Scheme evaluated for suitability by the Global Accreditation Cooperation Incorporated, or by an accreditation body member of the Global Mutual Recognition Arrangement for such scope, or by a Regional Accreditation Cooperation member." + }, + { + "@id": "untp:SchemeEndorsementLevel#endorsed_benchmarked", + "@type": "untp:SchemeEndorsementLevel", + "rdf:value": "endorsed_benchmarked", + "rdfs:label": "Scheme recognition by a benchmarking organisation approved to UNIDO principles and process", + "rdfs:comment": "Benchmarking of scheme by an organization approved to UNIDO benchmarking principles and process. UNIDO Global Best Practice Framework for Organisations Performing Benchmarking Activities for Certification-related Conformity Assessment Schemes 2026" + }, + { + "@id": "untp:UnitOfMeasure", + "@type": "rdfs:Class", + "rdfs:label": "UnitOfMeasure", + "rdfs:comment": "UNECE Recommendation 20 Unit of Measure codelist" + } + ] +} diff --git a/src/dppvalidator/vocabularies/data/untp-topics.jsonld b/src/dppvalidator/vocabularies/data/untp-topics.jsonld new file mode 100644 index 0000000..4d3326b --- /dev/null +++ b/src/dppvalidator/vocabularies/data/untp-topics.jsonld @@ -0,0 +1,1281 @@ +{ + "@context": { + "skos": "http://www.w3.org/2004/02/skos/core#", + "dcterms": "http://purl.org/dc/terms/", + "xsd": "http://www.w3.org/2001/XMLSchema#", + "owl": "http://www.w3.org/2002/07/owl#", + "rdfs": "http://www.w3.org/2000/01/rdf-schema#", + "sdg": "http://metadata.un.org/sdg/", + "topics": "https://vocabulary.uncefact.org/conformity-topics/", + "prefLabel": { "@id": "skos:prefLabel", "@language": "en" }, + "definition": { "@id": "skos:definition", "@language": "en" }, + "notation": "skos:notation", + "scopeNote": { "@id": "skos:scopeNote", "@language": "en" }, + "broader": { "@id": "skos:broader", "@type": "@id" }, + "narrower": { "@id": "skos:narrower", "@type": "@id", "@container": "@set" }, + "topConceptOf": { "@id": "skos:topConceptOf", "@type": "@id" }, + "hasTopConcept": { "@id": "skos:hasTopConcept", "@type": "@id", "@container": "@set" }, + "inScheme": { "@id": "skos:inScheme", "@type": "@id" }, + "relatedMatch": { "@id": "skos:relatedMatch", "@type": "@id", "@container": "@set" } + }, + "@graph": [ + { + "@id": "https://vocabulary.uncefact.org/conformity-topics/", + "@type": "skos:ConceptScheme", + "dcterms:title": { "@value": "UNTP Conformity Topic Classification", "@language": "en" }, + "dcterms:description": { "@value": "A hierarchical classification scheme for conformity topics used to categorise conformity criteria published by scheme owners. Encompasses sustainability (environmental, social, governance), product integrity, trade compliance, technical conformity, and information security domains. Designed as a common reference taxonomy for interoperable conformity assessments across regulatory frameworks and voluntary standards.", "@language": "en" }, + "dcterms:creator": "United Nations Economic Commission for Europe (UNECE)", + "dcterms:license": "https://creativecommons.org/licenses/by/4.0/", + "owl:versionInfo": "0.2.0-working", + "dcterms:issued": "2025-01-01", + "dcterms:modified": "2026-03-13", + "hasTopConcept": [ + "topics:ecological-resilience", + "topics:human-equity-and-welfare", + "topics:ethical-governance", + "topics:product-integrity", + "topics:circular-value-chains", + "topics:economic-sustainability", + "topics:health-and-safety", + "topics:systemic-sustainability", + "topics:trade-and-market-access", + "topics:technical-conformity", + "topics:information-security" + ] + }, + + { + "@id": "topics:ecological-resilience", + "@type": "skos:Concept", + "prefLabel": "Ecological Resilience", + "definition": "Environmental protection, resource conservation, and climate resilience. Covers emissions reduction, energy transition, water stewardship, waste prevention, biodiversity, and circular design.", + "notation": "01", + "topConceptOf": "https://vocabulary.uncefact.org/conformity-topics/", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "scopeNote": "UN SDGs 6, 7, 12, 13, 14, 15; OECD Guidelines Chapter VI: Environment; EU ESPR Art. 5-8 and Annex I.", + "relatedMatch": ["sdg:6", "sdg:7", "sdg:12", "sdg:13", "sdg:14", "sdg:15"], + "narrower": [ + "topics:greenhouse-gas-emissions", + "topics:renewable-energy-use", + "topics:water-conservation", + "topics:waste-minimization", + "topics:ecosystem-preservation", + "topics:forest-conservation", + "topics:recycled-material-integration", + "topics:sustainable-product-design", + "topics:chemical-safety", + "topics:air-quality-management" + ] + }, + { + "@id": "topics:greenhouse-gas-emissions", + "@type": "skos:Concept", + "prefLabel": "Greenhouse Gas Emissions", + "definition": "Measuring, reporting, and reducing greenhouse gas emissions (CO2, methane, N2O, F-gases) across production, transport, and supply chain activities.", + "notation": "01.01", + "broader": "topics:ecological-resilience", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:13"], + "scopeNote": "EU ESPR Art. 5 - Environmental Sustainability; UNTP environment.emissions." + }, + { + "@id": "topics:renewable-energy-use", + "@type": "skos:Concept", + "prefLabel": "Renewable Energy Use", + "definition": "Transition to sustainable energy sources including solar, wind, hydro, and other renewables in production and operations.", + "notation": "01.02", + "broader": "topics:ecological-resilience", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:7"], + "scopeNote": "EU ESPR Art. 7 - Energy Efficiency; UNTP environment.energy." + }, + { + "@id": "topics:water-conservation", + "@type": "skos:Concept", + "prefLabel": "Water Conservation", + "definition": "Sustainable water management including efficient use, pollution prevention, and watershed protection throughout operations and supply chains.", + "notation": "01.03", + "broader": "topics:ecological-resilience", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:6"], + "scopeNote": "EU ESPR Annex I - Water Use; UNTP environment.water." + }, + { + "@id": "topics:waste-minimization", + "@type": "skos:Concept", + "prefLabel": "Waste Minimization", + "definition": "Reducing waste generation through prevention, reuse, and improved production processes across the product lifecycle.", + "notation": "01.04", + "broader": "topics:ecological-resilience", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:12"], + "scopeNote": "EU ESPR Art. 6 - Waste Prevention; UNTP environment.waste." + }, + { + "@id": "topics:ecosystem-preservation", + "@type": "skos:Concept", + "prefLabel": "Ecosystem Preservation", + "definition": "Protecting biodiversity, natural habitats, and ecosystem services from degradation caused by production and extraction activities.", + "notation": "01.05", + "broader": "topics:ecological-resilience", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:15"], + "scopeNote": "EU ESPR Annex I - Biodiversity Impact; UNTP environment.biodiversity." + }, + { + "@id": "topics:forest-conservation", + "@type": "skos:Concept", + "prefLabel": "Forest Conservation", + "definition": "Preventing deforestation and promoting sustainable forestry practices in raw material sourcing and land use.", + "notation": "01.06", + "broader": "topics:ecological-resilience", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:15"], + "scopeNote": "EU ESPR Art. 5 - Resource Use; UNTP environment.deforestation." + }, + { + "@id": "topics:recycled-material-integration", + "@type": "skos:Concept", + "prefLabel": "Recycled Material Integration", + "definition": "Incorporation of secondary and recycled materials into production processes, reducing dependence on virgin resources.", + "notation": "01.07", + "broader": "topics:ecological-resilience", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:12"], + "scopeNote": "EU ESPR Art. 8 - Recycled Content; UNTP circularity.content." + }, + { + "@id": "topics:sustainable-product-design", + "@type": "skos:Concept", + "prefLabel": "Sustainable Product Design", + "definition": "Designing products for durability, repairability, recyclability, and minimal environmental impact throughout their lifecycle.", + "notation": "01.08", + "broader": "topics:ecological-resilience", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:12"], + "scopeNote": "EU ESPR Art. 5 - Durability and Recyclability; UNTP circularity.design." + }, + { + "@id": "topics:chemical-safety", + "@type": "skos:Concept", + "prefLabel": "Chemical Safety", + "definition": "Restriction and responsible management of hazardous substances in materials, products, and production processes.", + "notation": "01.09", + "broader": "topics:ecological-resilience", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:12"], + "scopeNote": "EU ESPR Annex I - Substance Restrictions." + }, + { + "@id": "topics:air-quality-management", + "@type": "skos:Concept", + "prefLabel": "Air Quality Management", + "definition": "Controlling and reducing non-GHG air pollutant emissions including SOx, NOx, VOCs, particulates, and ozone-depleting substances from operations and production processes.", + "notation": "01.10", + "broader": "topics:ecological-resilience", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:3", "sdg:13"], + "scopeNote": "EU ESPR Annex I - Air Emissions; WHO Air Quality Guidelines; Montreal Protocol (ozone-depleting substances)." + }, + + { + "@id": "topics:human-equity-and-welfare", + "@type": "skos:Concept", + "prefLabel": "Human Equity and Welfare", + "definition": "Protection of human rights, promotion of fair labor practices, and support for community wellbeing across operations and supply chains.", + "notation": "02", + "topConceptOf": "https://vocabulary.uncefact.org/conformity-topics/", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "scopeNote": "UN SDGs 1, 3, 4, 5, 8, 10; OECD Guidelines Chapter IV: Human Rights and Chapter V: Employment and Industrial Relations; EU ESPR Art. 10.", + "relatedMatch": ["sdg:1", "sdg:3", "sdg:4", "sdg:5", "sdg:8", "sdg:10"], + "narrower": [ + "topics:rights-and-equality", + "topics:decent-work-conditions", + "topics:workplace-safety", + "topics:community-empowerment", + "topics:worker-representation", + "topics:forced-labor-elimination", + "topics:youth-protection", + "topics:gender-equity" + ] + }, + { + "@id": "topics:rights-and-equality", + "@type": "skos:Concept", + "prefLabel": "Rights and Equality", + "definition": "Ensuring non-discrimination and equal treatment regardless of race, gender, religion, disability, or other protected characteristics.", + "notation": "02.01", + "broader": "topics:human-equity-and-welfare", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:10"], + "scopeNote": "EU ESPR Art. 10 - Social Sustainability; UNTP social.rights." + }, + { + "@id": "topics:decent-work-conditions", + "@type": "skos:Concept", + "prefLabel": "Decent Work Conditions", + "definition": "Provision of fair wages, reasonable working hours, and dignified employment conditions throughout the supply chain.", + "notation": "02.02", + "broader": "topics:human-equity-and-welfare", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:8"], + "scopeNote": "EU ESPR Annex I - Supply Chain Due Diligence; UNTP social.labour." + }, + { + "@id": "topics:workplace-safety", + "@type": "skos:Concept", + "prefLabel": "Workplace Safety", + "definition": "Protecting worker health and safety through hazard prevention, protective equipment, and safe working environments.", + "notation": "02.03", + "broader": "topics:human-equity-and-welfare", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:3"], + "scopeNote": "EU ESPR Art. 10 - Social Impact; UNTP social.safety." + }, + { + "@id": "topics:community-empowerment", + "@type": "skos:Concept", + "prefLabel": "Community Empowerment", + "definition": "Supporting local community development, livelihoods, and participation in decisions that affect their wellbeing.", + "notation": "02.04", + "broader": "topics:human-equity-and-welfare", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:1"], + "scopeNote": "EU ESPR Art. 10 - Stakeholder Engagement; UNTP social.community." + }, + { + "@id": "topics:worker-representation", + "@type": "skos:Concept", + "prefLabel": "Worker Representation", + "definition": "Respecting freedom of association, collective bargaining rights, and worker participation in workplace governance.", + "notation": "02.05", + "broader": "topics:human-equity-and-welfare", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:8"], + "scopeNote": "EU ESPR Annex I - Labor Rights." + }, + { + "@id": "topics:forced-labor-elimination", + "@type": "skos:Concept", + "prefLabel": "Forced Labor Elimination", + "definition": "Preventing all forms of forced, bonded, or compulsory labor including debt bondage and human trafficking in supply chains.", + "notation": "02.06", + "broader": "topics:human-equity-and-welfare", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:8"], + "scopeNote": "EU ESPR Art. 10 - Human Rights Due Diligence." + }, + { + "@id": "topics:youth-protection", + "@type": "skos:Concept", + "prefLabel": "Youth Protection", + "definition": "Safeguarding young workers from hazardous conditions and eliminating child labor in all forms across supply chains.", + "notation": "02.07", + "broader": "topics:human-equity-and-welfare", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:8"], + "scopeNote": "EU ESPR Annex I - Child Labor Ban." + }, + { + "@id": "topics:gender-equity", + "@type": "skos:Concept", + "prefLabel": "Gender Equity", + "definition": "Promoting gender diversity, equal opportunity, and elimination of gender-based discrimination in employment and business practices.", + "notation": "02.08", + "broader": "topics:human-equity-and-welfare", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:5"], + "scopeNote": "EU ESPR Art. 10 - Social Sustainability." + }, + + { + "@id": "topics:ethical-governance", + "@type": "skos:Concept", + "prefLabel": "Ethical Governance", + "definition": "Promoting organizational integrity, accountability, and transparent practices in business operations and decision-making.", + "notation": "03", + "topConceptOf": "https://vocabulary.uncefact.org/conformity-topics/", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "scopeNote": "UN SDG 16; OECD Guidelines Chapter II: General Policies and Chapter VII: Combating Bribery; EU ESPR Art. 11-12.", + "relatedMatch": ["sdg:16"], + "narrower": [ + "topics:anti-corruption-measures", + "topics:open-reporting", + "topics:legal-compliance", + "topics:responsible-procurement", + "topics:stakeholder-inclusion", + "topics:data-privacy", + "topics:ip-protection", + "topics:competitive-fairness" + ] + }, + { + "@id": "topics:anti-corruption-measures", + "@type": "skos:Concept", + "prefLabel": "Anti-Corruption Measures", + "definition": "Preventing bribery, extortion, and corrupt practices through policies, controls, and organizational culture.", + "notation": "03.01", + "broader": "topics:ethical-governance", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:16"], + "scopeNote": "EU ESPR Art. 11 - Governance Requirements; UNTP governance.ethics." + }, + { + "@id": "topics:open-reporting", + "@type": "skos:Concept", + "prefLabel": "Open Reporting", + "definition": "Transparent disclosure of environmental, social, and governance performance to stakeholders and the public.", + "notation": "03.02", + "broader": "topics:ethical-governance", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:16"], + "scopeNote": "EU ESPR Art. 12 - Information Requirements; UNTP governance.transparency." + }, + { + "@id": "topics:legal-compliance", + "@type": "skos:Concept", + "prefLabel": "Legal Compliance", + "definition": "Adherence to applicable laws, regulations, and legal obligations in all jurisdictions of operation.", + "notation": "03.03", + "broader": "topics:ethical-governance", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:16"], + "scopeNote": "EU ESPR Art. 4 - Compliance Obligations; UNTP governance.compliance." + }, + { + "@id": "topics:responsible-procurement", + "@type": "skos:Concept", + "prefLabel": "Responsible Procurement", + "definition": "Ethical sourcing and purchasing practices that consider environmental, social, and governance factors in supplier selection.", + "notation": "03.04", + "broader": "topics:ethical-governance", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:16"], + "scopeNote": "EU ESPR Art. 10 - Supply Chain Responsibility." + }, + { + "@id": "topics:stakeholder-inclusion", + "@type": "skos:Concept", + "prefLabel": "Stakeholder Inclusion", + "definition": "Meaningful engagement with affected parties including workers, communities, and civil society in governance processes.", + "notation": "03.05", + "broader": "topics:ethical-governance", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:16"], + "scopeNote": "EU ESPR Art. 10 - Stakeholder Dialogue." + }, + { + "@id": "topics:data-privacy", + "@type": "skos:Concept", + "prefLabel": "Data Privacy", + "definition": "Protection of personal information and responsible data handling in compliance with privacy regulations and ethical standards.", + "notation": "03.06", + "broader": "topics:ethical-governance", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:16"], + "scopeNote": "EU ESPR Art. 12 - Digital Product Passport." + }, + { + "@id": "topics:ip-protection", + "@type": "skos:Concept", + "prefLabel": "Intellectual Property Protection", + "definition": "Respecting intellectual property rights including patents, trademarks, copyrights, and trade secrets.", + "notation": "03.07", + "broader": "topics:ethical-governance", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:16"], + "scopeNote": "EU ESPR Art. 11 - Governance Standards." + }, + { + "@id": "topics:competitive-fairness", + "@type": "skos:Concept", + "prefLabel": "Competitive Fairness", + "definition": "Ensuring fair market practices, preventing anti-competitive behavior, and maintaining a level playing field.", + "notation": "03.08", + "broader": "topics:ethical-governance", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:16"], + "scopeNote": "EU ESPR Art. 11 - Governance." + }, + + { + "@id": "topics:product-integrity", + "@type": "skos:Concept", + "prefLabel": "Product Integrity", + "definition": "Ensuring products are safe, reliable, and meet quality and sustainability standards throughout their lifecycle.", + "notation": "04", + "topConceptOf": "https://vocabulary.uncefact.org/conformity-topics/", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "scopeNote": "UN SDGs 9, 12; OECD Guidelines Chapter VIII: Consumer Interests; EU ESPR Art. 4-7 and Annex I.", + "relatedMatch": ["sdg:9", "sdg:12"], + "narrower": [ + "topics:product-safety-standards", + "topics:quality-performance", + "topics:substance-control", + "topics:product-longevity", + "topics:standards-adherence", + "topics:supply-chain-traceability", + "topics:consumer-information", + "topics:end-of-life-management" + ] + }, + { + "@id": "topics:product-safety-standards", + "@type": "skos:Concept", + "prefLabel": "Product Safety Standards", + "definition": "Ensuring consumer safety through compliance with product safety requirements, testing, and hazard prevention.", + "notation": "04.01", + "broader": "topics:product-integrity", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:12"], + "scopeNote": "EU ESPR Art. 5 - Safety Requirements; UNTP social.safety." + }, + { + "@id": "topics:quality-performance", + "@type": "skos:Concept", + "prefLabel": "Quality Performance", + "definition": "Meeting defined performance specifications, functional requirements, and quality benchmarks for products and services.", + "notation": "04.02", + "broader": "topics:product-integrity", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:9"], + "scopeNote": "EU ESPR Art. 5 - Performance Standards." + }, + { + "@id": "topics:substance-control", + "@type": "skos:Concept", + "prefLabel": "Substance Control", + "definition": "Banning or restricting harmful materials and substances of concern in product composition and manufacturing.", + "notation": "04.03", + "broader": "topics:product-integrity", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:12"], + "scopeNote": "EU ESPR Annex I - Substance Restrictions." + }, + { + "@id": "topics:product-longevity", + "@type": "skos:Concept", + "prefLabel": "Product Longevity", + "definition": "Enhancing product durability, repairability, and lifespan to reduce premature obsolescence and waste.", + "notation": "04.04", + "broader": "topics:product-integrity", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:12"], + "scopeNote": "EU ESPR Art. 5 - Durability; UNTP circularity.design." + }, + { + "@id": "topics:standards-adherence", + "@type": "skos:Concept", + "prefLabel": "Standards Adherence", + "definition": "Compliance with applicable product certifications, industry standards, and regulatory requirements.", + "notation": "04.05", + "broader": "topics:product-integrity", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:9"], + "scopeNote": "EU ESPR Art. 4 - Ecodesign Requirements." + }, + { + "@id": "topics:supply-chain-traceability", + "@type": "skos:Concept", + "prefLabel": "Supply Chain Traceability", + "definition": "Tracking product origins, components, and transformations throughout the supply chain to enable transparency and accountability.", + "notation": "04.06", + "broader": "topics:product-integrity", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:12"], + "scopeNote": "EU ESPR Art. 12 - Digital Product Passport; UNTP governance.transparency." + }, + { + "@id": "topics:consumer-information", + "@type": "skos:Concept", + "prefLabel": "Consumer Information", + "definition": "Providing clear, accurate, and accessible product labeling and information to enable informed consumer choices.", + "notation": "04.07", + "broader": "topics:product-integrity", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:12"], + "scopeNote": "EU ESPR Art. 7 - Information Obligations." + }, + { + "@id": "topics:end-of-life-management", + "@type": "skos:Concept", + "prefLabel": "End-of-Life Management", + "definition": "Effective collection, recycling, and disposal processes for products at end of useful life, minimizing environmental impact.", + "notation": "04.08", + "broader": "topics:product-integrity", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:12"], + "scopeNote": "EU ESPR Art. 6 - End-of-Life Requirements." + }, + + { + "@id": "topics:circular-value-chains", + "@type": "skos:Concept", + "prefLabel": "Circular Value Chains", + "definition": "Advancing sustainability, circularity, and responsible practices throughout supply and production networks.", + "notation": "05", + "topConceptOf": "https://vocabulary.uncefact.org/conformity-topics/", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "scopeNote": "UN SDGs 8, 12, 17; OECD Guidelines Chapter II: General Policies and Chapter VI: Environment; EU ESPR Art. 8, 10.", + "relatedMatch": ["sdg:8", "sdg:12", "sdg:17"], + "narrower": [ + "topics:ethical-material-sourcing", + "topics:supplier-sustainability", + "topics:resource-circularity", + "topics:energy-optimization", + "topics:supply-chain-labor-rights", + "topics:origin-tracking", + "topics:supplier-development", + "topics:supply-chain-risk-reduction" + ] + }, + { + "@id": "topics:ethical-material-sourcing", + "@type": "skos:Concept", + "prefLabel": "Ethical Material Sourcing", + "definition": "Procuring raw materials through sustainable and responsible practices, avoiding conflict minerals and environmentally destructive extraction.", + "notation": "05.01", + "broader": "topics:circular-value-chains", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:12"], + "scopeNote": "EU ESPR Art. 10 - Supply Chain Due Diligence; UNTP governance.transparency." + }, + { + "@id": "topics:supplier-sustainability", + "@type": "skos:Concept", + "prefLabel": "Supplier Sustainability", + "definition": "Ensuring suppliers meet environmental, social, and governance requirements through assessment, monitoring, and collaboration.", + "notation": "05.02", + "broader": "topics:circular-value-chains", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:17"], + "scopeNote": "EU ESPR Art. 10 - Supply Chain Responsibility." + }, + { + "@id": "topics:resource-circularity", + "@type": "skos:Concept", + "prefLabel": "Resource Circularity", + "definition": "Promoting reuse, remanufacturing, and recycling of materials to create closed-loop resource flows.", + "notation": "05.03", + "broader": "topics:circular-value-chains", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:12"], + "scopeNote": "EU ESPR Art. 8 - Recycled Content; UNTP circularity.content." + }, + { + "@id": "topics:energy-optimization", + "@type": "skos:Concept", + "prefLabel": "Energy Optimization", + "definition": "Improving energy efficiency across supply chain operations including manufacturing, logistics, and warehousing.", + "notation": "05.04", + "broader": "topics:circular-value-chains", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:7"], + "scopeNote": "EU ESPR Art. 7 - Energy Efficiency." + }, + { + "@id": "topics:supply-chain-labor-rights", + "@type": "skos:Concept", + "prefLabel": "Supply Chain Labor Rights", + "definition": "Ensuring fair treatment of workers throughout the supply chain including subcontractors and informal workers.", + "notation": "05.05", + "broader": "topics:circular-value-chains", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:8"], + "scopeNote": "EU ESPR Annex I - Labor Standards." + }, + { + "@id": "topics:origin-tracking", + "@type": "skos:Concept", + "prefLabel": "Origin Tracking", + "definition": "Transparent documentation and verification of material and product origins throughout the supply chain.", + "notation": "05.06", + "broader": "topics:circular-value-chains", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:12"], + "scopeNote": "EU ESPR Art. 12 - Digital Product Passport." + }, + { + "@id": "topics:supplier-development", + "@type": "skos:Concept", + "prefLabel": "Supplier Development", + "definition": "Building supplier capacity and capability to meet sustainability requirements through training, support, and partnership.", + "notation": "05.07", + "broader": "topics:circular-value-chains", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:17"], + "scopeNote": "EU ESPR Art. 10 - Supply Chain Support." + }, + { + "@id": "topics:supply-chain-risk-reduction", + "@type": "skos:Concept", + "prefLabel": "Supply Chain Risk Reduction", + "definition": "Identifying, assessing, and mitigating environmental, social, and operational vulnerabilities in supply networks.", + "notation": "05.08", + "broader": "topics:circular-value-chains", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:12"], + "scopeNote": "EU ESPR Art. 10 - Risk Management." + }, + + { + "@id": "topics:economic-sustainability", + "@type": "skos:Concept", + "prefLabel": "Economic Sustainability", + "definition": "Balancing profitability with sustainable economic practices that create shared value for businesses and communities.", + "notation": "06", + "topConceptOf": "https://vocabulary.uncefact.org/conformity-topics/", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "scopeNote": "UN SDGs 8, 9; OECD Guidelines Chapter II: General Policies; EU ESPR Art. 5, 7, 10, 11.", + "relatedMatch": ["sdg:8", "sdg:9"], + "narrower": [ + "topics:business-resilience", + "topics:sustainable-investment", + "topics:green-innovation", + "topics:employment-opportunities", + "topics:regional-economic-growth", + "topics:resource-efficiency", + "topics:economic-risk-management", + "topics:supply-network-strength" + ] + }, + { + "@id": "topics:business-resilience", + "@type": "skos:Concept", + "prefLabel": "Business Resilience", + "definition": "Building long-term profitability and organizational resilience through sustainable business models and practices.", + "notation": "06.01", + "broader": "topics:economic-sustainability", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:8"], + "scopeNote": "EU ESPR Art. 11 - Governance for Sustainability." + }, + { + "@id": "topics:sustainable-investment", + "@type": "skos:Concept", + "prefLabel": "Sustainable Investment", + "definition": "Directing capital toward green initiatives, sustainable technologies, and projects with positive environmental and social outcomes.", + "notation": "06.02", + "broader": "topics:economic-sustainability", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:9"], + "scopeNote": "EU ESPR Art. 5 - Resource Efficiency." + }, + { + "@id": "topics:green-innovation", + "@type": "skos:Concept", + "prefLabel": "Green Innovation", + "definition": "Developing sustainable technologies, processes, and business models that reduce environmental impact while creating economic value.", + "notation": "06.03", + "broader": "topics:economic-sustainability", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:9"], + "scopeNote": "EU ESPR Art. 5 - Innovation Requirements." + }, + { + "@id": "topics:employment-opportunities", + "@type": "skos:Concept", + "prefLabel": "Employment Opportunities", + "definition": "Creating decent jobs and fostering inclusive economic participation through sustainable business growth.", + "notation": "06.04", + "broader": "topics:economic-sustainability", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:8"], + "scopeNote": "EU ESPR Art. 10 - Social Impact." + }, + { + "@id": "topics:regional-economic-growth", + "@type": "skos:Concept", + "prefLabel": "Regional Economic Growth", + "definition": "Supporting local economic development and equitable distribution of economic benefits in communities of operation.", + "notation": "06.05", + "broader": "topics:economic-sustainability", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:8"], + "scopeNote": "EU ESPR Art. 10 - Community Benefits; UNTP social.community." + }, + { + "@id": "topics:resource-efficiency", + "@type": "skos:Concept", + "prefLabel": "Resource Efficiency", + "definition": "Optimizing resource utilization to reduce costs and environmental impact while maintaining productivity.", + "notation": "06.06", + "broader": "topics:economic-sustainability", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:8"], + "scopeNote": "EU ESPR Art. 7 - Efficiency Standards." + }, + { + "@id": "topics:economic-risk-management", + "@type": "skos:Concept", + "prefLabel": "Economic Risk Management", + "definition": "Assessing and managing financial risks arising from environmental, social, and governance factors.", + "notation": "06.07", + "broader": "topics:economic-sustainability", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:8"], + "scopeNote": "EU ESPR Art. 11 - Governance." + }, + { + "@id": "topics:supply-network-strength", + "@type": "skos:Concept", + "prefLabel": "Supply Network Strength", + "definition": "Enhancing the stability, diversity, and resilience of value chain networks against disruption.", + "notation": "06.08", + "broader": "topics:economic-sustainability", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:9"], + "scopeNote": "EU ESPR Art. 10 - Supply Chain Resilience." + }, + + { + "@id": "topics:health-and-safety", + "@type": "skos:Concept", + "prefLabel": "Health and Safety Assurance", + "definition": "Prioritizing the health and safety of workers and communities through hazard prevention, preparedness, and wellbeing support.", + "notation": "07", + "topConceptOf": "https://vocabulary.uncefact.org/conformity-topics/", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "scopeNote": "UN SDG 3; OECD Guidelines Chapter V: Employment and Industrial Relations; EU ESPR Art. 5, 10 and Annex I.", + "relatedMatch": ["sdg:3"], + "narrower": [ + "topics:workplace-hazard-control", + "topics:emergency-readiness", + "topics:exposure-management", + "topics:living-conditions", + "topics:healthcare-access", + "topics:wellbeing-support", + "topics:nutrition-standards", + "topics:ergonomic-design" + ] + }, + { + "@id": "topics:workplace-hazard-control", + "@type": "skos:Concept", + "prefLabel": "Workplace Hazard Control", + "definition": "Systematic identification, assessment, and mitigation of workplace hazards to reduce risk of injury and illness, including incident reporting, investigation, and corrective action.", + "notation": "07.01", + "broader": "topics:health-and-safety", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:3"], + "scopeNote": "EU ESPR Art. 10 - Social Sustainability; UNTP social.safety." + }, + { + "@id": "topics:emergency-readiness", + "@type": "skos:Concept", + "prefLabel": "Emergency Readiness", + "definition": "Preparedness planning, training, and response capabilities for workplace emergencies including fire, chemical spills, and natural disasters.", + "notation": "07.02", + "broader": "topics:health-and-safety", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:3"], + "scopeNote": "EU ESPR Annex I - Safety Measures." + }, + { + "@id": "topics:exposure-management", + "@type": "skos:Concept", + "prefLabel": "Exposure Management", + "definition": "Controlling worker exposure to harmful chemical, biological, and physical agents through monitoring and protective measures.", + "notation": "07.03", + "broader": "topics:health-and-safety", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:3"], + "scopeNote": "EU ESPR Annex I - Substance Safety." + }, + { + "@id": "topics:living-conditions", + "@type": "skos:Concept", + "prefLabel": "Living Conditions", + "definition": "Ensuring safe, sanitary, and dignified accommodation for workers where employer-provided housing is applicable.", + "notation": "07.04", + "broader": "topics:health-and-safety", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:3"], + "scopeNote": "EU ESPR Art. 10 - Worker Welfare." + }, + { + "@id": "topics:healthcare-access", + "@type": "skos:Concept", + "prefLabel": "Healthcare Access", + "definition": "Providing access to medical support, occupational health services, and health insurance for workers.", + "notation": "07.05", + "broader": "topics:health-and-safety", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:3"], + "scopeNote": "EU ESPR Art. 10 - Health Provisions." + }, + { + "@id": "topics:wellbeing-support", + "@type": "skos:Concept", + "prefLabel": "Wellbeing Support", + "definition": "Addressing worker mental health, stress management, and overall wellbeing through support programs and workplace culture.", + "notation": "07.06", + "broader": "topics:health-and-safety", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:3"], + "scopeNote": "EU ESPR Art. 10 - Social Impact." + }, + { + "@id": "topics:nutrition-standards", + "@type": "skos:Concept", + "prefLabel": "Nutrition Standards", + "definition": "Ensuring safe, adequate, and nutritious food provisions for workers where employer-provided meals are applicable.", + "notation": "07.07", + "broader": "topics:health-and-safety", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:3"], + "scopeNote": "EU ESPR Art. 10 - Worker Welfare." + }, + { + "@id": "topics:ergonomic-design", + "@type": "skos:Concept", + "prefLabel": "Ergonomic Design", + "definition": "Designing safe physical work environments that minimize musculoskeletal strain and support worker comfort and productivity.", + "notation": "07.08", + "broader": "topics:health-and-safety", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:3"], + "scopeNote": "EU ESPR Art. 5 - Product Safety." + }, + + { + "@id": "topics:systemic-sustainability", + "@type": "skos:Concept", + "prefLabel": "Systemic Sustainability", + "definition": "Establishing management frameworks, policies, and processes for systematic improvement of environmental, social, and governance outcomes.", + "notation": "08", + "topConceptOf": "https://vocabulary.uncefact.org/conformity-topics/", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "scopeNote": "UN SDGs 12, 16; OECD Guidelines Chapter II: General Policies; EU ESPR Art. 4, 5, 10-12.", + "relatedMatch": ["sdg:12", "sdg:16"], + "narrower": [ + "topics:sustainability-policies", + "topics:risk-identification", + "topics:outcome-tracking", + "topics:capacity-building", + "topics:process-enhancement", + "topics:feedback-channels", + "topics:compliance-verification", + "topics:transparent-communication" + ] + }, + { + "@id": "topics:sustainability-policies", + "@type": "skos:Concept", + "prefLabel": "Sustainability Policies", + "definition": "Formal organizational commitments, policies, and targets for environmental, social, and governance performance.", + "notation": "08.01", + "broader": "topics:systemic-sustainability", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:16"], + "scopeNote": "EU ESPR Art. 11 - Governance Framework." + }, + { + "@id": "topics:risk-identification", + "@type": "skos:Concept", + "prefLabel": "Risk Identification", + "definition": "Systematic assessment and prioritization of environmental, social, and governance risks across operations and supply chains.", + "notation": "08.02", + "broader": "topics:systemic-sustainability", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:12"], + "scopeNote": "EU ESPR Art. 10 - Due Diligence." + }, + { + "@id": "topics:outcome-tracking", + "@type": "skos:Concept", + "prefLabel": "Outcome Tracking", + "definition": "Monitoring, measuring, and reporting on sustainability performance against defined targets and indicators.", + "notation": "08.03", + "broader": "topics:systemic-sustainability", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:12"], + "scopeNote": "EU ESPR Art. 12 - Reporting Requirements." + }, + { + "@id": "topics:capacity-building", + "@type": "skos:Concept", + "prefLabel": "Capacity Building", + "definition": "Training and developing stakeholder knowledge and skills to implement and maintain sustainability practices.", + "notation": "08.04", + "broader": "topics:systemic-sustainability", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:12"], + "scopeNote": "EU ESPR Art. 10 - Stakeholder Support." + }, + { + "@id": "topics:process-enhancement", + "@type": "skos:Concept", + "prefLabel": "Process Enhancement", + "definition": "Continuous improvement of operational processes to achieve better sustainability outcomes over time.", + "notation": "08.05", + "broader": "topics:systemic-sustainability", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:12"], + "scopeNote": "EU ESPR Art. 5 - Performance Improvement." + }, + { + "@id": "topics:feedback-channels", + "@type": "skos:Concept", + "prefLabel": "Feedback Channels", + "definition": "Accessible grievance mechanisms, whistleblower protections, and feedback systems for workers, communities, and stakeholders to raise concerns without fear of retaliation.", + "notation": "08.06", + "broader": "topics:systemic-sustainability", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:16"], + "scopeNote": "EU ESPR Art. 10 - Stakeholder Engagement." + }, + { + "@id": "topics:compliance-verification", + "@type": "skos:Concept", + "prefLabel": "Compliance Verification", + "definition": "Independent audits, inspections, and verification processes to confirm adherence to sustainability standards and regulations.", + "notation": "08.07", + "broader": "topics:systemic-sustainability", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:16"], + "scopeNote": "EU ESPR Art. 4 - Compliance Monitoring." + }, + { + "@id": "topics:transparent-communication", + "@type": "skos:Concept", + "prefLabel": "Transparent Communication", + "definition": "Public disclosure and reporting of sustainability policies, performance, and progress to stakeholders.", + "notation": "08.08", + "broader": "topics:systemic-sustainability", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:16"], + "scopeNote": "EU ESPR Art. 12 - Information Disclosure; UNTP governance.transparency." + }, + + { + "@id": "topics:trade-and-market-access", + "@type": "skos:Concept", + "prefLabel": "Trade and Market Access", + "definition": "Adherence to trade regulations, customs requirements, market access rules, and cross-border compliance frameworks.", + "notation": "09", + "topConceptOf": "https://vocabulary.uncefact.org/conformity-topics/", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "scopeNote": "WTO TBT and SPS Agreements; UNECE Trade Facilitation Recommendations; WCO Harmonized System.", + "relatedMatch": ["sdg:17"], + "narrower": [ + "topics:import-export-controls", + "topics:customs-classification", + "topics:rules-of-origin", + "topics:sanctions-compliance", + "topics:market-authorization", + "topics:trade-documentation", + "topics:tariff-and-duty-compliance", + "topics:mutual-recognition" + ] + }, + { + "@id": "topics:import-export-controls", + "@type": "skos:Concept", + "prefLabel": "Import and Export Controls", + "definition": "Compliance with cross-border trade restrictions, licensing requirements, and controlled goods regulations.", + "notation": "09.01", + "broader": "topics:trade-and-market-access", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:17"], + "scopeNote": "WTO Trade Facilitation Agreement; national export control regimes." + }, + { + "@id": "topics:customs-classification", + "@type": "skos:Concept", + "prefLabel": "Customs Classification", + "definition": "Accurate tariff classification and customs valuation of goods in accordance with the Harmonized System and national schedules.", + "notation": "09.02", + "broader": "topics:trade-and-market-access", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:17"], + "scopeNote": "WCO Harmonized System Convention." + }, + { + "@id": "topics:rules-of-origin", + "@type": "skos:Concept", + "prefLabel": "Rules of Origin", + "definition": "Verification of product origin to determine eligibility for preferential tariff treatment under trade agreements.", + "notation": "09.03", + "broader": "topics:trade-and-market-access", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:17"], + "scopeNote": "WTO Agreement on Rules of Origin; WCO Revised Kyoto Convention." + }, + { + "@id": "topics:sanctions-compliance", + "@type": "skos:Concept", + "prefLabel": "Sanctions Compliance", + "definition": "Adherence to international trade sanctions, embargoes, and restricted party screening requirements.", + "notation": "09.04", + "broader": "topics:trade-and-market-access", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:16"], + "scopeNote": "UN Security Council sanctions; national sanctions regimes." + }, + { + "@id": "topics:market-authorization", + "@type": "skos:Concept", + "prefLabel": "Market Authorization", + "definition": "Meeting regulatory requirements for market entry including product registration, type approval, and pre-market conformity assessment.", + "notation": "09.05", + "broader": "topics:trade-and-market-access", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:17"], + "scopeNote": "WTO TBT Agreement Art. 5 - Conformity Assessment Procedures." + }, + { + "@id": "topics:trade-documentation", + "@type": "skos:Concept", + "prefLabel": "Trade Documentation", + "definition": "Accuracy, completeness, and digital exchange of trade and customs documentation including certificates, invoices, and declarations.", + "notation": "09.06", + "broader": "topics:trade-and-market-access", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:17"], + "scopeNote": "UNECE Trade Facilitation Recommendations; UN/CEFACT standards." + }, + { + "@id": "topics:tariff-and-duty-compliance", + "@type": "skos:Concept", + "prefLabel": "Tariff and Duty Compliance", + "definition": "Correct assessment, declaration, and payment of applicable customs duties, taxes, and fees.", + "notation": "09.07", + "broader": "topics:trade-and-market-access", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:17"], + "scopeNote": "WCO Revised Kyoto Convention; national customs legislation." + }, + { + "@id": "topics:mutual-recognition", + "@type": "skos:Concept", + "prefLabel": "Mutual Recognition", + "definition": "Acceptance of conformity assessment results, certifications, and test reports across jurisdictions through mutual recognition agreements.", + "notation": "09.08", + "broader": "topics:trade-and-market-access", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:17"], + "scopeNote": "WTO TBT Agreement Art. 6; ILAC and IAF mutual recognition arrangements." + }, + + { + "@id": "topics:technical-conformity", + "@type": "skos:Concept", + "prefLabel": "Technical Conformity", + "definition": "Adherence to technical regulations, voluntary standards, and conformity assessment procedures that ensure product and process fitness for purpose.", + "notation": "10", + "topConceptOf": "https://vocabulary.uncefact.org/conformity-topics/", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "scopeNote": "WTO TBT Agreement; ISO/IEC 17000 series conformity assessment standards; Codex Alimentarius.", + "relatedMatch": ["sdg:9"], + "narrower": [ + "topics:technical-regulations", + "topics:voluntary-standards", + "topics:metrology-and-measurement", + "topics:testing-and-certification", + "topics:sanitary-and-phytosanitary", + "topics:interoperability-standards", + "topics:accessibility-requirements", + "topics:performance-specifications" + ] + }, + { + "@id": "topics:technical-regulations", + "@type": "skos:Concept", + "prefLabel": "Technical Regulations", + "definition": "Compliance with mandatory government-imposed technical requirements for products, processes, and production methods.", + "notation": "10.01", + "broader": "topics:technical-conformity", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:9"], + "scopeNote": "WTO TBT Agreement Art. 2 - Technical Regulations." + }, + { + "@id": "topics:voluntary-standards", + "@type": "skos:Concept", + "prefLabel": "Voluntary Standards", + "definition": "Adherence to consensus-based standards developed by recognized standards bodies for products, services, and management systems.", + "notation": "10.02", + "broader": "topics:technical-conformity", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:9"], + "scopeNote": "WTO TBT Agreement Art. 4 - Standards; ISO, IEC, ITU standards." + }, + { + "@id": "topics:metrology-and-measurement", + "@type": "skos:Concept", + "prefLabel": "Metrology and Measurement", + "definition": "Accuracy and traceability of measurements and calibrations to national and international measurement standards.", + "notation": "10.03", + "broader": "topics:technical-conformity", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:9"], + "scopeNote": "BIPM International System of Units; OIML Recommendations." + }, + { + "@id": "topics:testing-and-certification", + "@type": "skos:Concept", + "prefLabel": "Testing and Certification", + "definition": "Third-party conformity assessment including laboratory testing, product certification, and inspection by accredited bodies.", + "notation": "10.04", + "broader": "topics:technical-conformity", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:9"], + "scopeNote": "ISO/IEC 17065 Product Certification; ISO/IEC 17025 Testing Laboratories." + }, + { + "@id": "topics:sanitary-and-phytosanitary", + "@type": "skos:Concept", + "prefLabel": "Sanitary and Phytosanitary Measures", + "definition": "Compliance with food safety, animal health, and plant health standards designed to protect human, animal, and plant life.", + "notation": "10.05", + "broader": "topics:technical-conformity", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:3"], + "scopeNote": "WTO SPS Agreement; Codex Alimentarius; OIE; IPPC." + }, + { + "@id": "topics:interoperability-standards", + "@type": "skos:Concept", + "prefLabel": "Interoperability Standards", + "definition": "Conformity with standards ensuring compatibility, data exchange, and seamless interaction between systems, components, and services.", + "notation": "10.06", + "broader": "topics:technical-conformity", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:9"], + "scopeNote": "ISO/IEC JTC 1 information technology standards; W3C web standards." + }, + { + "@id": "topics:accessibility-requirements", + "@type": "skos:Concept", + "prefLabel": "Accessibility Requirements", + "definition": "Compliance with inclusive design and accessibility standards ensuring products and services are usable by people with diverse abilities.", + "notation": "10.07", + "broader": "topics:technical-conformity", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:10"], + "scopeNote": "ISO 21542 Accessibility; WCAG 2.1; EN 301 549." + }, + { + "@id": "topics:performance-specifications", + "@type": "skos:Concept", + "prefLabel": "Performance Specifications", + "definition": "Meeting defined functional, reliability, and performance benchmarks established by regulations, standards, or contractual requirements.", + "notation": "10.08", + "broader": "topics:technical-conformity", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:9"], + "scopeNote": "Industry-specific performance standards and testing protocols." + }, + + { + "@id": "topics:information-security", + "@type": "skos:Concept", + "prefLabel": "Information Security and Digital Trust", + "definition": "Protection of data, digital systems, and information assets, and the establishment of trust frameworks for digital interactions.", + "notation": "11", + "topConceptOf": "https://vocabulary.uncefact.org/conformity-topics/", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "scopeNote": "ISO/IEC 27001 Information Security; GDPR; eIDAS; NIST Cybersecurity Framework.", + "relatedMatch": ["sdg:9", "sdg:16"], + "narrower": [ + "topics:data-protection-and-privacy", + "topics:cybersecurity-controls", + "topics:digital-identity-and-trust", + "topics:access-management", + "topics:incident-response", + "topics:system-integrity", + "topics:encryption-and-data-security", + "topics:audit-and-accountability" + ] + }, + { + "@id": "topics:data-protection-and-privacy", + "@type": "skos:Concept", + "prefLabel": "Data Protection and Privacy", + "definition": "Safeguarding personal and sensitive data in compliance with privacy regulations, consent requirements, and ethical data handling principles.", + "notation": "11.01", + "broader": "topics:information-security", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:16"], + "scopeNote": "GDPR; ISO/IEC 27701 Privacy Information Management." + }, + { + "@id": "topics:cybersecurity-controls", + "@type": "skos:Concept", + "prefLabel": "Cybersecurity Controls", + "definition": "Implementation of technical and organizational security measures to protect digital infrastructure from threats and vulnerabilities.", + "notation": "11.02", + "broader": "topics:information-security", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:9"], + "scopeNote": "ISO/IEC 27001; NIST Cybersecurity Framework; IEC 62443." + }, + { + "@id": "topics:digital-identity-and-trust", + "@type": "skos:Concept", + "prefLabel": "Digital Identity and Trust", + "definition": "Verification and assurance of digital identities, credentials, and trust relationships in electronic transactions and communications.", + "notation": "11.03", + "broader": "topics:information-security", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:16"], + "scopeNote": "eIDAS Regulation; W3C Verifiable Credentials; UNTP Digital Identity Anchor." + }, + { + "@id": "topics:access-management", + "@type": "skos:Concept", + "prefLabel": "Access Management", + "definition": "Controls for authentication, authorization, and system access ensuring only authorized parties can access resources and data.", + "notation": "11.04", + "broader": "topics:information-security", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:16"], + "scopeNote": "ISO/IEC 27001 Annex A - Access Control." + }, + { + "@id": "topics:incident-response", + "@type": "skos:Concept", + "prefLabel": "Incident Response and Recovery", + "definition": "Preparedness planning, detection, response procedures, and recovery capabilities for security breaches and system failures.", + "notation": "11.05", + "broader": "topics:information-security", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:9"], + "scopeNote": "ISO/IEC 27035 Incident Management; NIST SP 800-61." + }, + { + "@id": "topics:system-integrity", + "@type": "skos:Concept", + "prefLabel": "System Integrity and Availability", + "definition": "Ensuring reliability, uptime, and integrity of digital systems through resilient architecture and continuity planning.", + "notation": "11.06", + "broader": "topics:information-security", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:9"], + "scopeNote": "ISO 22301 Business Continuity; ISO/IEC 27001 Availability Controls." + }, + { + "@id": "topics:encryption-and-data-security", + "@type": "skos:Concept", + "prefLabel": "Encryption and Data Security", + "definition": "Protection of data confidentiality and integrity in transit and at rest through cryptographic controls and key management.", + "notation": "11.07", + "broader": "topics:information-security", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:9"], + "scopeNote": "ISO/IEC 19790 Cryptographic Modules; NIST FIPS 140." + }, + { + "@id": "topics:audit-and-accountability", + "@type": "skos:Concept", + "prefLabel": "Audit Trail and Accountability", + "definition": "Logging, monitoring, and accountability mechanisms for digital activities to support compliance verification and forensic analysis.", + "notation": "11.08", + "broader": "topics:information-security", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:16"], + "scopeNote": "ISO/IEC 27001 Annex A - Logging and Monitoring." + } + ] +} diff --git a/src/dppvalidator/vocabularies/ontology.py b/src/dppvalidator/vocabularies/ontology.py index 4b06405..c1ac4a1 100644 --- a/src/dppvalidator/vocabularies/ontology.py +++ b/src/dppvalidator/vocabularies/ontology.py @@ -81,20 +81,80 @@ class DPPGranularity(str, Enum): PRODUCT = "product" # Single unit (official term, not 'item') +# Sentinel that signals "this term has no equivalent in the given UNTP version" +# (e.g. v0.6's ``gtin`` field is gone in v0.7 — encoded as ``Product.id`` plus +# ``idScheme`` on a GS1 scheme). Using a sentinel rather than ``None`` keeps +# the dataclass slots type-clean (``str``) and makes "intentionally absent" +# explicit when reading the table. +TERM_REMOVED: str = "" + + @dataclass(frozen=True, slots=True) class TermMapping: - """Mapping between UNTP term and CIRPASS ontology URI.""" + """Mapping between UNTP term(s) and a CIRPASS / EU DPP ontology URI. + + Phase 3c of docs/plans/UNTP_0.7.0_MIGRATION.md added per-version columns + so a single mapping row can carry both UNTP v0.6.x and v0.7.x term + names without duplicating the ESPR reference and description. + + Attributes: + untp_term: The "canonical" UNTP term — historically the v0.6 field + name, kept as the row's primary key so backward-compat callers + and existing tests continue to work without modification. + cirpass_uri: EU DPP Core Ontology URI in compact form + (e.g. ``eudpp:Product``). + description: Human-readable summary of the mapping. + espr_reference: ESPR / SR5423 / ISO citation for traceability. + untp_v0_6: The term spelling used by UNTP 0.6.x. Defaults to + :attr:`untp_term` so unchanged rows don't need to repeat themselves. + untp_v0_7: The term spelling used by UNTP 0.7.0. Defaults to + :attr:`untp_term` (i.e. unchanged across versions). Set + explicitly for renames, or to :data:`TERM_REMOVED` for fields + that no longer exist in v0.7 (e.g. ``gtin``). + """ untp_term: str cirpass_uri: str description: str espr_reference: str | None = None + untp_v0_6: str | None = None + untp_v0_7: str | None = None + + def term_for(self, version: str) -> str | None: + """Return the UNTP term for a given version, or ``None`` if removed. + + Resolution rules (in order): + + 1. If a per-version column is set explicitly, use it. + 2. Otherwise fall back to :attr:`untp_term` (the canonical v0.6 spelling). + 3. If the per-version column is :data:`TERM_REMOVED`, return ``None`` + — the field has no equivalent in this UNTP version. + + Unknown version strings (anything that's not a 0.6.x / 0.7.x prefix) + fall back to :attr:`untp_term` so the table is forward-compatible. + """ + explicit: str | None + if version.startswith("0.6"): + explicit = self.untp_v0_6 + elif version.startswith("0.7"): + explicit = self.untp_v0_7 + else: + explicit = None + + chosen = explicit if explicit is not None else self.untp_term + return None if chosen == TERM_REMOVED else chosen # Term mappings from UNTP to EU DPP Core Ontology # Based on official CIRPASS-2 ontology v1.7.1 +# +# Mapping rows are written so the row's primary ``untp_term`` is the v0.6 +# spelling — this keeps the OntologyMapper's existing semantics. Rows that +# rename in v0.7 carry an explicit ``untp_v0_7`` column. Rows that remove +# in v0.7 use :data:`TERM_REMOVED`. See Phase 3c of +# docs/plans/UNTP_0.7.0_MIGRATION.md. TERM_MAPPINGS: tuple[TermMapping, ...] = ( - # Core DPP and Product classes + # Core DPP and Product classes (unchanged across versions). TermMapping( untp_term="DigitalProductPassport", cirpass_uri="eudpp:DPP", @@ -114,11 +174,13 @@ class TermMapping: description="Unique DPP identifier (URI)", espr_reference="ESPR Art 9(1)", ), + # ``serialNumber`` (v0.6) → ``itemNumber`` (v0.7); same EU DPP target. TermMapping( untp_term="serialNumber", cirpass_uri="eudpp:uniqueProductID", - description="Unique product identifier", + description="Unique product identifier (item-level)", espr_reference="ESPR Art 2(30)", + untp_v0_7="itemNumber", ), TermMapping( untp_term="name", @@ -138,11 +200,15 @@ class TermMapping: description="Product image URI", espr_reference="ESPR Annex III", ), + # ``gtin`` is removed in v0.7. v0.7 encodes GS1 GTINs by combining + # ``Product.id`` with an ``idScheme`` whose URI points at the GS1 + # register — so there's no single field to re-map onto eudpp:GTIN. TermMapping( untp_term="gtin", cirpass_uri="eudpp:GTIN", description="Global Trade Identification Number", espr_reference="ISO/IEC 15459-6", + untp_v0_7=TERM_REMOVED, ), TermMapping( untp_term="productCategory", @@ -157,11 +223,17 @@ class TermMapping: description="DPP issuer (economic operator)", espr_reference="ESPR Annex III (g)", ), + # ``producedByParty: Party`` (v0.6) → ``relatedParty: list[PartyRole]`` + # (v0.7). The v0.7 field is structurally different (typed list of + # role/party pairs) but the EU DPP target ``hasManufacturer`` is the + # same when the role is "manufacturer" — the exporter handles the + # role filtering separately. TermMapping( untp_term="producedByParty", cirpass_uri="eudpp:hasManufacturer", description="Product manufacturer", espr_reference="ESPR Annex III (g)", + untp_v0_7="relatedParty", ), TermMapping( untp_term="producedAtFacility", @@ -176,7 +248,7 @@ class TermMapping: description="Product contains substance of concern", espr_reference="ESPR Art 7(5)", ), - # Validity and lifecycle + # Validity and lifecycle (envelope-level fields — same in both versions). TermMapping( untp_term="validFrom", cirpass_uri="eudpp:validFrom", @@ -207,12 +279,13 @@ class TermMapping: description="Link to previous DPP", espr_reference="ESPR Art 11(d)", ), - # Granularity + # Granularity: ``granularityLevel`` (v0.6) → ``idGranularity`` (v0.7). TermMapping( untp_term="granularityLevel", cirpass_uri="eudpp:granularity", description="DPP granularity (model/batch/product)", espr_reference="SR5423 Annex II Part B 1.1", + untp_v0_7="idGranularity", ), # Product properties TermMapping( @@ -240,22 +313,76 @@ class TermMapping: description="Product is spare part of another", espr_reference="ESPR Art 2", ), + # ---- v0.7-only mappings ---------------------------------------------- + # ``materialsProvenance`` (v0.6) → ``materialProvenance`` (v0.7, + # singular noun). Both spellings need to map to the same EU DPP + # predicate — we add a row whose canonical ``untp_term`` is the v0.6 + # name and whose v0.7 column carries the new spelling. + TermMapping( + untp_term="materialsProvenance", + cirpass_uri="eudpp:hasMaterialProvenance", + description="Material origin and mass-fraction information", + espr_reference="ESPR Art 7(5)", + untp_v0_7="materialProvenance", + ), + # ``conformityClaim`` (v0.6) collapses with the three scorecard + # classes into ``performanceClaim`` (v0.7). For ontology-mapping + # purposes both target the EU DPP performance/claim predicate. + TermMapping( + untp_term="conformityClaim", + cirpass_uri="eudpp:hasPerformanceClaim", + description="Performance / conformity claim attached to a product", + espr_reference="ESPR Annex III", + untp_v0_7="performanceClaim", + ), ) class OntologyMapper: - """Maps UNTP terms to CIRPASS ontology URIs.""" + """Maps UNTP terms to CIRPASS / EU DPP ontology URIs. + + Phase 3c added per-version awareness: callers that pass a UNTP + ``schema_version`` get the right column out of :data:`TERM_MAPPINGS`. + Callers that don't (the pre-Phase-3c API) keep the v0.6 behaviour — + the ``untp_term`` column remains the canonical key, so existing + forward and reverse lookups work unchanged. + """ def __init__(self) -> None: """Initialize mapper with term mappings.""" + # Forward lookup is keyed on the row's canonical ``untp_term`` + # (v0.6 spelling). v0.7-specific spellings are reachable via + # ``find_mapping_for_term(term, version)``. self._untp_to_cirpass: dict[str, TermMapping] = {m.untp_term: m for m in TERM_MAPPINGS} self._cirpass_to_untp: dict[str, TermMapping] = {m.cirpass_uri: m for m in TERM_MAPPINGS} + # Per-version forward index — populated lazily when needed via + # :meth:`_index_for_version`. Keys are e.g. ``"itemNumber"`` for + # v0.7 lookups. + self._index_cache: dict[str, dict[str, TermMapping]] = {} + + # Secondary index of every non-canonical, non-removed term spelling + # across all per-version columns (e.g. ``itemNumber`` for v0.7). + # This lets ``get_mapping`` and the no-version + # ``find_mapping_for_term`` resolve renamed-only terms without + # branching on a specific UNTP version literal. + secondary: dict[str, TermMapping] = {} + for mapping in TERM_MAPPINGS: + for alt in (mapping.untp_v0_6, mapping.untp_v0_7): + if alt is None or alt == TERM_REMOVED: + continue + if alt == mapping.untp_term: + continue + # Last write wins on collision, matching the per-version + # index behaviour below. + secondary[alt] = mapping + self._secondary_index: dict[str, TermMapping] = secondary + def to_cirpass(self, untp_term: str) -> str | None: """Get CIRPASS URI for a UNTP term. Args: - untp_term: UNTP vocabulary term + untp_term: UNTP vocabulary term (canonical / v0.6 spelling). Returns: CIRPASS ontology URI or None if not mapped @@ -263,54 +390,90 @@ def to_cirpass(self, untp_term: str) -> str | None: mapping = self._untp_to_cirpass.get(untp_term) return mapping.cirpass_uri if mapping else None - def to_untp(self, cirpass_uri: str) -> str | None: - """Get UNTP term for a CIRPASS URI. + def to_untp(self, cirpass_uri: str, version: str | None = None) -> str | None: + """Get UNTP term for a CIRPASS URI, optionally version-aware. Args: cirpass_uri: CIRPASS ontology URI + version: UNTP version SemVer string (e.g. for v0.7.0). If + supplied, the returned term reflects the spelling that + version uses (e.g. ``itemNumber`` for v0.7 instead of + ``serialNumber``). If the term is removed in that version, + returns ``None``. When ``version`` is ``None`` the canonical + (v0.6) spelling is returned — pre-Phase-3c behaviour. Returns: - UNTP term or None if not mapped + UNTP term or None if not mapped (or removed in this version). """ mapping = self._cirpass_to_untp.get(cirpass_uri) - return mapping.untp_term if mapping else None + if mapping is None: + return None + if version is None: + return mapping.untp_term + return mapping.term_for(version) def get_mapping(self, term: str) -> TermMapping | None: """Get full mapping for a term (UNTP or CIRPASS). - Args: - term: UNTP term or CIRPASS URI - - Returns: - TermMapping or None if not found + Recognises the canonical (v0.6) spelling, every per-version + spelling registered in :data:`TERM_MAPPINGS`, and the EU DPP URI + as keys. Returns ``None`` if no row matches. """ - return self._untp_to_cirpass.get(term) or self._cirpass_to_untp.get(term) + return ( + self._untp_to_cirpass.get(term) + or self._cirpass_to_untp.get(term) + or self._secondary_index.get(term) + ) def get_espr_reference(self, untp_term: str) -> str | None: - """Get ESPR reference for a UNTP term. - - Args: - untp_term: UNTP vocabulary term - - Returns: - ESPR reference string or None - """ + """Get ESPR reference for a UNTP term.""" mapping = self._untp_to_cirpass.get(untp_term) return mapping.espr_reference if mapping else None def iter_mappings(self) -> Iterator[TermMapping]: - """Iterate over all term mappings. + """Iterate over all term mappings.""" + yield from TERM_MAPPINGS - Yields: - TermMapping instances + def find_mapping_for_term(self, term: str, version: str | None = None) -> TermMapping | None: + """Look up a mapping by the version-specific spelling of a term. + + Phase 3c helper: callers that observe a v0.7 field name on the wire + (e.g. ``itemNumber`` or ``materialProvenance``) can resolve it to + the same :class:`TermMapping` row as the v0.6 spelling. When + ``version`` is ``None`` the canonical-key index is consulted first + and the secondary index of all per-version spellings is used as + a fallback. """ - yield from TERM_MAPPINGS + if version is None: + return self._untp_to_cirpass.get(term) or self._secondary_index.get(term) + return self._index_for_version(version).get(term) + + def _index_for_version(self, version: str) -> dict[str, TermMapping]: + """Build (and cache) the version-keyed forward index.""" + cached = self._index_cache.get(version) + if cached is not None: + return cached + index: dict[str, TermMapping] = {} + for mapping in TERM_MAPPINGS: + term = mapping.term_for(version) + if term is None: + continue + # Last write wins on collision — rows ordered later in + # TERM_MAPPINGS take precedence, which matches Python dict + # initialisation semantics elsewhere in this module. + index[term] = mapping + self._index_cache[version] = index + return index @property def mapped_terms(self) -> list[str]: - """List of all mapped UNTP terms.""" + """List of all mapped UNTP terms (v0.6 canonical spellings).""" return list(self._untp_to_cirpass.keys()) + def mapped_terms_for(self, version: str) -> list[str]: + """List of UNTP terms for a specific version (Phase 3c).""" + return list(self._index_for_version(version).keys()) + @property def mapping_count(self) -> int: """Number of term mappings.""" diff --git a/tests/conftest.py b/tests/conftest.py index 027f0c9..5fa751b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,18 +1,73 @@ -"""Shared pytest fixtures for dppvalidator tests.""" +"""Shared pytest fixtures for dppvalidator tests. + +Phase 5 of ``docs/plans/UNTP_0.7.0_MIGRATION.md`` parametrises the +:func:`valid_dpp_data` fixture over both supported UNTP DPP versions +(0.6.1 and 0.7.0). Tests that need to pin a specific version use the +``@pytest.mark.dpp_version("X.Y.Z")`` marker; tests without a marker +run against both shapes. + +The version list is read from the schema registry — adding a new +UNTP version automatically expands the matrix. +""" from __future__ import annotations +import json +from typing import TYPE_CHECKING, Any + import pytest +from dppvalidator.schemas.registry import SCHEMA_REGISTRY -@pytest.fixture -def valid_dpp_data() -> dict: - """Return a minimal valid DPP that passes CIRPASS validation. +if TYPE_CHECKING: + from collections.abc import Iterable + +# The matrix used by parametrised fixtures. We surface "0.6.1" + "0.7.0" +# as canonical entries; "0.6.0" stays in the registry but isn't matrix-tested +# because it shares the v0.6 wire shape with 0.6.1 — adding it would +# duplicate every test slot for no extra coverage. +_MATRIX_DPP_VERSIONS: tuple[str, ...] = ("0.6.1", "0.7.0") + + +def pytest_configure(config: pytest.Config) -> None: + """Register dppvalidator-specific markers.""" + config.addinivalue_line( + "markers", + "dpp_version(version): pin a parametrised dpp fixture to a single " + "UNTP DPP version. Use to keep a test class scoped to one wire shape " + "when its assertions are version-specific (Phase 5).", + ) + + +def _matrix_versions() -> tuple[str, ...]: + """Return the parametrisation matrix, intersected with the registry. + + Defensive: if a matrix version somehow drops out of the registry + we'd silently parametrise over a missing schema. This guards against + that by filtering to the current registry's version set. + """ + return tuple(v for v in _MATRIX_DPP_VERSIONS if v in SCHEMA_REGISTRY) - This fixture provides a DPP structure that satisfies: - - CQ001: Mandatory ESPR attributes (issuer, validFrom, credentialSubject.product) - - CQ016: Validity period (validFrom, validUntil) + +@pytest.fixture(params=_matrix_versions(), ids=lambda v: f"v{v}") +def dpp_version(request: pytest.FixtureRequest) -> str: + """The UNTP DPP version for the current parametrisation slot. + + Tests can pin to a single version with + ``@pytest.mark.dpp_version("0.6.1")``; non-matching params are + skipped so the surviving run is single-version. Tests that don't + apply the marker run twice (once per matrix version). """ + marker = request.node.get_closest_marker("dpp_version") + if marker is not None: + wanted = marker.args[0] if marker.args else None + if wanted is not None and wanted != request.param: + pytest.skip(f"dpp_version({wanted!r}) marker filters out {request.param!r}") + return request.param + + +def _v06_payload() -> dict[str, Any]: + """Minimal valid 0.6.x DPP payload.""" return { "@context": [ "https://www.w3.org/ns/credentials/v2", @@ -37,19 +92,96 @@ def valid_dpp_data() -> dict: } +def _v07_payload() -> dict[str, Any]: + """Minimal valid 0.7.0 DPP payload. + + Includes every v0.7-required field on Product so the engine's + model-layer validation passes without modification. + """ + return { + "@context": [ + "https://www.w3.org/ns/credentials/v2", + "https://vocabulary.uncefact.org/untp/0.7.0/context/", + ], + "type": ["DigitalProductPassport", "VerifiableCredential"], + "id": "https://example.com/credentials/dpp-001", + "name": "Minimal v0.7 DPP", + "issuer": { + "type": ["CredentialIssuer"], + "id": "did:example:issuer-001", + "name": "Example Company Ltd", + }, + "validFrom": "2024-01-01T00:00:00Z", + "validUntil": "2034-01-01T00:00:00Z", + "credentialSubject": { + "type": ["Product"], + "id": "https://example.com/products/001", + "name": "Example Product", + "idScheme": { + "type": ["IdentifierScheme"], + "id": "https://example.com/schemes/internal", + "name": "Internal product scheme", + }, + "idGranularity": "model", + "productCategory": [ + { + "type": ["Classification"], + "schemeId": "https://unstats.un.org/unsd/classifications/Econ/cpc/", + "schemeName": "UN Central Product Classification", + "code": "12345", + "name": "Example category", + } + ], + "producedAtFacility": { + "type": ["Facility"], + "id": "https://example.com/facilities/001", + "name": "Example facility", + }, + "countryOfProduction": {"countryCode": "DE", "countryName": "Germany"}, + }, + } + + +_PAYLOAD_BY_VERSION: dict[str, Any] = { + "0.6.1": _v06_payload, + "0.7.0": _v07_payload, +} + + @pytest.fixture -def valid_dpp_json(valid_dpp_data: dict) -> str: - """Return valid DPP as JSON string.""" - import json +def valid_dpp_data(dpp_version: str) -> dict[str, Any]: + """Return a minimal valid DPP for the active parametrisation slot. + + Phase 5: this fixture is now parametrised over both supported UNTP + versions via :func:`dpp_version`. Tests that hardcode a specific + UNTP version on the engine should pin themselves with + ``@pytest.mark.dpp_version("X.Y.Z")``. + The returned payload satisfies: + - CQ001: Mandatory ESPR attributes (issuer, validFrom, credentialSubject.product[/Product]). + - CQ016: Validity period (validFrom, validUntil). + - All v0.7-required Product fields when the version slot is 0.7.0. + """ + factory = _PAYLOAD_BY_VERSION.get(dpp_version) + if factory is None: # pragma: no cover — defensive + raise RuntimeError(f"No fixture payload registered for {dpp_version!r}") + return factory() + + +@pytest.fixture +def valid_dpp_json(valid_dpp_data: dict[str, Any]) -> str: + """Return :func:`valid_dpp_data` serialised as a JSON string.""" return json.dumps(valid_dpp_data) @pytest.fixture -def minimal_dpp_data() -> dict: - """Return minimal DPP data (may not pass all CIRPASS rules). +def minimal_dpp_data() -> dict[str, Any]: + """Return minimal v0.6.x DPP data (may not pass all CIRPASS rules). Use this for tests that specifically test partial/incomplete DPPs. + Pinned to v0.6.x because tests using it typically construct a + :class:`dppvalidator.models.passport.DigitalProductPassport` + directly — the top-level alias resolves to the v0.6 model. """ return { "@context": [ @@ -62,3 +194,8 @@ def minimal_dpp_data() -> dict: "name": "Example Company Ltd", }, } + + +def all_matrix_versions() -> Iterable[str]: + """Public helper for tests that need to know the matrix versions.""" + return _matrix_versions() diff --git a/tests/fixtures/invalid/0.7.0/claim_missing_performance.json b/tests/fixtures/invalid/0.7.0/claim_missing_performance.json new file mode 100644 index 0000000..495da17 --- /dev/null +++ b/tests/fixtures/invalid/0.7.0/claim_missing_performance.json @@ -0,0 +1,53 @@ +{ + "type": ["DigitalProductPassport", "VerifiableCredential"], + "@context": [ + "https://www.w3.org/ns/credentials/v2", + "https://vocabulary.uncefact.org/untp/0.7.0/context/" + ], + "id": "https://example.com/credentials/dpp-claim-broken", + "name": "Claim with no claimedPerformance and no score — Performance invariant fails", + "issuer": { + "type": ["CredentialIssuer"], + "id": "did:example:issuer-001", + "name": "Example Company Ltd" + }, + "validFrom": "2024-01-01T00:00:00Z", + "credentialSubject": { + "type": ["Product"], + "id": "https://example.com/products/001", + "name": "Example Product", + "idScheme": { + "type": ["IdentifierScheme"], + "id": "https://example.com/schemes/internal", + "name": "Internal product scheme" + }, + "idGranularity": "model", + "productCategory": [ + { + "type": ["Classification"], + "schemeId": "https://unstats.un.org/unsd/classifications/Econ/cpc/", + "schemeName": "UN Central Product Classification", + "code": "12345", + "name": "Example category" + } + ], + "producedAtFacility": { + "type": ["Facility"], + "id": "https://example.com/facilities/001", + "name": "Example facility" + }, + "countryOfProduction": {"countryCode": "DE", "countryName": "Germany"}, + "performanceClaim": [ + { + "type": ["Claim"], + "id": "https://example.com/claims/broken", + "name": "Broken claim", + "claimedPerformance": [ + { + "metric": {"type": ["PerformanceMetric"], "name": "Carbon footprint"} + } + ] + } + ] + } +} diff --git a/tests/fixtures/invalid/0.7.0/country_string_regression.json b/tests/fixtures/invalid/0.7.0/country_string_regression.json new file mode 100644 index 0000000..b701532 --- /dev/null +++ b/tests/fixtures/invalid/0.7.0/country_string_regression.json @@ -0,0 +1,41 @@ +{ + "type": ["DigitalProductPassport", "VerifiableCredential"], + "@context": [ + "https://www.w3.org/ns/credentials/v2", + "https://vocabulary.uncefact.org/untp/0.7.0/context/" + ], + "id": "https://example.com/credentials/dpp-country-regression", + "name": "Country code as bare string — should fail (v0.7 expects {countryCode, countryName})", + "issuer": { + "type": ["CredentialIssuer"], + "id": "did:example:issuer-001", + "name": "Example Company Ltd" + }, + "validFrom": "2024-01-01T00:00:00Z", + "credentialSubject": { + "type": ["Product"], + "id": "https://example.com/products/001", + "name": "Example Product", + "idScheme": { + "type": ["IdentifierScheme"], + "id": "https://example.com/schemes/internal", + "name": "Internal product scheme" + }, + "idGranularity": "model", + "productCategory": [ + { + "type": ["Classification"], + "schemeId": "https://unstats.un.org/unsd/classifications/Econ/cpc/", + "schemeName": "UN Central Product Classification", + "code": "12345", + "name": "Example category" + } + ], + "producedAtFacility": { + "type": ["Facility"], + "id": "https://example.com/facilities/001", + "name": "Example facility" + }, + "countryOfProduction": "DE" + } +} diff --git a/tests/fixtures/invalid/0.7.0/material_missing_materialType.json b/tests/fixtures/invalid/0.7.0/material_missing_materialType.json new file mode 100644 index 0000000..446c1bd --- /dev/null +++ b/tests/fixtures/invalid/0.7.0/material_missing_materialType.json @@ -0,0 +1,48 @@ +{ + "type": ["DigitalProductPassport", "VerifiableCredential"], + "@context": [ + "https://www.w3.org/ns/credentials/v2", + "https://vocabulary.uncefact.org/untp/0.7.0/context/" + ], + "id": "https://example.com/credentials/dpp-material-missing-type", + "name": "Material missing required materialType — should fail", + "issuer": { + "type": ["CredentialIssuer"], + "id": "did:example:issuer-001", + "name": "Example Company Ltd" + }, + "validFrom": "2024-01-01T00:00:00Z", + "credentialSubject": { + "type": ["Product"], + "id": "https://example.com/products/001", + "name": "Example Product", + "idScheme": { + "type": ["IdentifierScheme"], + "id": "https://example.com/schemes/internal", + "name": "Internal product scheme" + }, + "idGranularity": "model", + "productCategory": [ + { + "type": ["Classification"], + "schemeId": "https://unstats.un.org/unsd/classifications/Econ/cpc/", + "schemeName": "UN Central Product Classification", + "code": "12345", + "name": "Example category" + } + ], + "producedAtFacility": { + "type": ["Facility"], + "id": "https://example.com/facilities/001", + "name": "Example facility" + }, + "countryOfProduction": {"countryCode": "DE", "countryName": "Germany"}, + "materialProvenance": [ + { + "name": "Steel", + "originCountry": {"countryCode": "DE", "countryName": "Germany"}, + "massFraction": 0.5 + } + ] + } +} diff --git a/tests/fixtures/invalid/0.7.0/missing_credentialSubject.json b/tests/fixtures/invalid/0.7.0/missing_credentialSubject.json new file mode 100644 index 0000000..a68a123 --- /dev/null +++ b/tests/fixtures/invalid/0.7.0/missing_credentialSubject.json @@ -0,0 +1,15 @@ +{ + "type": ["DigitalProductPassport", "VerifiableCredential"], + "@context": [ + "https://www.w3.org/ns/credentials/v2", + "https://vocabulary.uncefact.org/untp/0.7.0/context/" + ], + "id": "https://example.com/credentials/dpp-missing-cs", + "name": "Missing credentialSubject — should fail", + "issuer": { + "type": ["CredentialIssuer"], + "id": "did:example:issuer-001", + "name": "Example Company Ltd" + }, + "validFrom": "2024-01-01T00:00:00Z" +} diff --git a/tests/fixtures/invalid/0.7.0/missing_name.json b/tests/fixtures/invalid/0.7.0/missing_name.json new file mode 100644 index 0000000..c8de3c4 --- /dev/null +++ b/tests/fixtures/invalid/0.7.0/missing_name.json @@ -0,0 +1,40 @@ +{ + "type": ["DigitalProductPassport", "VerifiableCredential"], + "@context": [ + "https://www.w3.org/ns/credentials/v2", + "https://vocabulary.uncefact.org/untp/0.7.0/context/" + ], + "id": "https://example.com/credentials/dpp-missing-name", + "issuer": { + "type": ["CredentialIssuer"], + "id": "did:example:issuer-001", + "name": "Example Company Ltd" + }, + "validFrom": "2024-01-01T00:00:00Z", + "credentialSubject": { + "type": ["Product"], + "id": "https://example.com/products/001", + "name": "Example Product", + "idScheme": { + "type": ["IdentifierScheme"], + "id": "https://example.com/schemes/internal", + "name": "Internal product scheme" + }, + "idGranularity": "model", + "productCategory": [ + { + "type": ["Classification"], + "schemeId": "https://unstats.un.org/unsd/classifications/Econ/cpc/", + "schemeName": "UN Central Product Classification", + "code": "12345", + "name": "Example category" + } + ], + "producedAtFacility": { + "type": ["Facility"], + "id": "https://example.com/facilities/001", + "name": "Example facility" + }, + "countryOfProduction": {"countryCode": "DE", "countryName": "Germany"} + } +} diff --git a/tests/fixtures/invalid/0.7.0/missing_validFrom.json b/tests/fixtures/invalid/0.7.0/missing_validFrom.json new file mode 100644 index 0000000..a113af6 --- /dev/null +++ b/tests/fixtures/invalid/0.7.0/missing_validFrom.json @@ -0,0 +1,40 @@ +{ + "type": ["DigitalProductPassport", "VerifiableCredential"], + "@context": [ + "https://www.w3.org/ns/credentials/v2", + "https://vocabulary.uncefact.org/untp/0.7.0/context/" + ], + "id": "https://example.com/credentials/dpp-missing-validFrom", + "name": "Missing validFrom — should fail", + "issuer": { + "type": ["CredentialIssuer"], + "id": "did:example:issuer-001", + "name": "Example Company Ltd" + }, + "credentialSubject": { + "type": ["Product"], + "id": "https://example.com/products/001", + "name": "Example Product", + "idScheme": { + "type": ["IdentifierScheme"], + "id": "https://example.com/schemes/internal", + "name": "Internal product scheme" + }, + "idGranularity": "model", + "productCategory": [ + { + "type": ["Classification"], + "schemeId": "https://unstats.un.org/unsd/classifications/Econ/cpc/", + "schemeName": "UN Central Product Classification", + "code": "12345", + "name": "Example category" + } + ], + "producedAtFacility": { + "type": ["Facility"], + "id": "https://example.com/facilities/001", + "name": "Example facility" + }, + "countryOfProduction": {"countryCode": "DE", "countryName": "Germany"} + } +} diff --git a/tests/fixtures/samples/BatteryPassDataModel_BatteryPass_GeneralProductInformation-payload.json b/tests/fixtures/samples/BatteryPassDataModel_BatteryPass_GeneralProductInformation-payload.json new file mode 100644 index 0000000..d6230da --- /dev/null +++ b/tests/fixtures/samples/BatteryPassDataModel_BatteryPass_GeneralProductInformation-payload.json @@ -0,0 +1,37 @@ +{ + "batteryCategory": "lmt", + "operatorInformation": { + "identifier": "VLhpfQGTMDYpsBZxvfBoeygjb", + "emailAddress": "-w-......www-www-w-...--w--w.-w-www--.w...-w.www.w..-w..--.ww@.ww..-www-ww--ww.ww---w.w-w.w.ww...-w%mKozizkOjnxNSVyMliITPITDtxTLqUoUkEZlpZTvFMYBIjfUgqHyJIztdiBfETLcpZPXYcTSCSpSTTFAfi", + "postalAddress": { + "addressCountry": "Germany", + "streetAddress": "Hindenburgstr. 10", + "postalCode": "10719" + }, + "contactName": "RYtGKbgicZaHCBRQDSx", + "webAddress": "ftp://ftp.is.co.za/rfc/rfc1808.txt" + }, + "productIdentifier": "eOMtThyhVNLWUZNRcBaQKxI", + "batteryStatus": "Original", + "puttingIntoService": "2025-06-17T11:14:27.698+02:00", + "batteryMass": 699, + "manufacturingDate": "2025-06-17T11:14:27.698+02:00", + "batteryPassportIdentifier": "urn:bmwk:123456687678", + "warrentyPeriod": "--06", + "manufacturerInformation": { + "identifier": "JxkyvRnL", + "emailAddress": ".ww-...-ww...@ww.-..w.-www-.-.-w--w.ww--..-.w.ww.-...w.-.w.jGphZZQBHAFyIqSqHUNGwnomwanuIDiDpbLftOdDNZMwzMeqjAQpLVSSkKExPOypZXmRDeCGjVwKMtHHEhhmd", + "postalAddress": { + "addressCountry": "Germany", + "streetAddress": "Hindenburgstr. 10", + "postalCode": "10719" + }, + "contactName": "yedUsFwdkelQbxeTeQOvaScfqIOOmaa", + "webAddress": "telnet://192.0.2.16:80/" + }, + "manufacturingPlace": { + "addressCountry": "Germany", + "streetAddress": "Hindenburgstr. 10", + "postalCode": "10719" + } +} diff --git a/tests/fixtures/samples/batterypass_BatteryPassDataModel_CarbonFootprintForBatteries-ld.json b/tests/fixtures/samples/batterypass_BatteryPassDataModel_CarbonFootprintForBatteries-ld.json new file mode 100644 index 0000000..d77a246 --- /dev/null +++ b/tests/fixtures/samples/batterypass_BatteryPassDataModel_CarbonFootprintForBatteries-ld.json @@ -0,0 +1,244 @@ +{ + "@graph": [ + { + "@id": "urn:samm:io.BatteryPass.CarbonFootprint:1.2.0#CarbonFootprintPerLifecycleStageEntity", + "samm:properties": { + "@list": [ + { + "@id": "urn:samm:io.BatteryPass.CarbonFootprint:1.2.0#lifecycleStage" + }, + { + "@id": "urn:samm:io.BatteryPass.CarbonFootprint:1.2.0#carbonFootprint" + } + ] + }, + "samm:description": { + "@language": "en", + "@value": "The carbon footprint of the battery as share of total Battery Carbon Footprint, differentiated per life cycle stage raw material extraction, main production, distribution and end of \u00b4\u2510\u00a2ife and recycling." + }, + "@type": "samm:Entity" + }, + { + "@id": "urn:samm:io.BatteryPass.CarbonFootprint:1.2.0#batteryCarbonFootprint", + "samm:characteristic": { + "@id": "urn:samm:io.BatteryPass.CarbonFootprint:1.2.0#BatteryCarbonFootprint" + }, + "samm:description": { + "@language": "en", + "@value": "The carbon footprint of the battery, calculated as kg of carbon dioxide equivalent per one kWh of the total energy provided by the battery over its expected service life, as declared in the Carbon Footprint Declaration.\nDIN DKE Spec 99100 chapter reference: 6.3.2" + }, + "@type": "samm:Property" + }, + { + "@id": "urn:samm:io.BatteryPass.CarbonFootprint:1.2.0#BatteryCarbonFootprint", + "samm-c:unit": { + "@id": "urn:samm:io.BatteryPass.CarbonFootprint:1.2.0#kilogramperkilowatthour" + }, + "samm:dataType": { + "@id": "xsd:double" + }, + "samm:description": { + "@language": "en", + "@value": "The battery carbon footprint is an aggregation of the carbon footprint of the individual lifecycle stages" + }, + "@type": "samm-c:Measurement" + }, + { + "@id": "urn:samm:io.BatteryPass.CarbonFootprint:1.2.0#kilogramperkilowatthour", + "samm:symbol": "CO2e/kWh", + "samm:commonCode": "kg CO2e/kWh", + "@type": "samm:Unit" + }, + { + "@id": "urn:samm:io.BatteryPass.CarbonFootprint:1.2.0#CarbonFootprintValue", + "samm-c:unit": { + "@id": "urn:samm:io.BatteryPass.CarbonFootprint:1.2.0#kilogramperkilowatthour" + }, + "samm:dataType": { + "@id": "xsd:double" + }, + "@type": "samm-c:Measurement" + }, + { + "@id": "urn:samm:io.BatteryPass.CarbonFootprint:1.2.0#lifecycleStage", + "samm:characteristic": { + "@id": "urn:samm:io.BatteryPass.CarbonFootprint:1.2.0#LifecycleStage" + }, + "samm:description": { + "@language": "en", + "@value": "The description of the life cycle stage " + }, + "@type": "samm:Property" + }, + { + "@id": "urn:samm:io.BatteryPass.CarbonFootprint:1.2.0#LifecycleStage", + "samm-c:values": { + "@list": [ + "RawMaterialExtraction", + "MainProduction", + "Distribution", + "Recycling" + ] + }, + "samm:dataType": { + "@id": "xsd:string" + }, + "@type": "samm-c:Enumeration" + }, + { + "@id": "urn:samm:io.BatteryPass.CarbonFootprint:1.2.0#CarbonFootprintForBatteries", + "samm:events": { + "@list": [] + }, + "samm:operations": { + "@list": [] + }, + "samm:properties": { + "@list": [ + { + "@id": "urn:samm:io.BatteryPass.CarbonFootprint:1.2.0#batteryCarbonFootprint" + }, + { + "@id": "urn:samm:io.BatteryPass.CarbonFootprint:1.2.0#carbonFootprintPerLifecycleStage" + }, + { + "@id": "urn:samm:io.BatteryPass.CarbonFootprint:1.2.0#carbonFootprintPerformanceClass" + }, + { + "@id": "urn:samm:io.BatteryPass.CarbonFootprint:1.2.0#carbonFootprintStudy" + }, + { + "@id": "_:b10" + } + ] + }, + "samm:see": { + "@id": "https://eur-lex.europa.eu/legal-content/EN/TXT/PDF/?uri=CELEX:32023R1542" + }, + "samm:description": { + "@language": "en", + "@value": "The battery passport must contain carbon footprint per functional unit of the battery as declared in the battery carbon footprint declaration in accordance with the entry into force of the implementing acts on the format of declaration. Reference: REGULATION (EU) 2023/1542 aka EU Battery Regulation" + }, + "@type": "samm:Aspect" + }, + { + "@id": "urn:samm:io.BatteryPass.CarbonFootprint:1.2.0#carbonFootprint", + "samm:characteristic": { + "@id": "urn:samm:io.BatteryPass.CarbonFootprint:1.2.0#CarbonFootprintValue" + }, + "samm:description": { + "@language": "en", + "@value": "Carbon footprint of the individual lifecycle stage" + }, + "@type": "samm:Property" + }, + { + "@id": "urn:samm:io.BatteryPass.CarbonFootprint:1.2.0#CarbonFootprintPerformanceClass", + "samm:dataType": { + "@id": "xsd:string" + }, + "samm:description": { + "@language": "en", + "@value": "EV, industrial and LMT batteries shall bear a conspicuous, clearly legible and indelible label indicating the carbon footprint of the battery and the carbon footprint performance class that the relevant battery model per manufacturing plant corresponds to. The carbon footprint performance class shall be accessible via the battery passport. A meaningful number of classes of performance will be developed (?) with category A being the best class with the lowest carbon footprint life cycle impact." + }, + "@type": "samm-c:Code" + }, + { + "@id": "urn:samm:io.BatteryPass.CarbonFootprint:1.2.0#carbonFootprintPerformanceClass", + "samm:characteristic": { + "@id": "urn:samm:io.BatteryPass.CarbonFootprint:1.2.0#CarbonFootprintPerformanceClass" + }, + "samm:description": { + "@language": "en", + "@value": "The carbon footprint performance class that the relevant battery model per manufacturing plant corresponds to.\n\nDIN DKE Spec 99100 chapter reference: 6.3.7" + }, + "@type": "samm:Property" + }, + { + "@id": "urn:samm:io.BatteryPass.CarbonFootprint:1.2.0#absoluteCarbonFootprint", + "samm:characteristic": { + "@id": "urn:samm:io.BatteryPass.CarbonFootprint:1.2.0#AbsoluteCarbonFootprint" + }, + "samm:description": { + "@language": "en", + "@value": "As a non-mandatory data attribute, the battery passport should include the battery carbon footprint in absolute terms.\n\nThe absolute battery carbon footprint should be calculated as kilograms of carbon dioxide equivalent, without reference to the functional unit as prescribed by the battery regulation.\n\nDIN DKE Spec 99100 chapter reference: 6.3.10" + }, + "@type": "samm:Property" + }, + { + "@id": "urn:samm:io.BatteryPass.CarbonFootprint:1.2.0#AbsoluteCarbonFootprint", + "samm-c:unit": { + "@id": "urn:samm:io.BatteryPass.CarbonFootprint:1.2.0#kilogramm" + }, + "samm:dataType": { + "@id": "xsd:double" + }, + "@type": "samm-c:Measurement" + }, + { + "@id": "urn:samm:io.BatteryPass.CarbonFootprint:1.2.0#CarbonFootprints", + "samm:dataType": { + "@id": "urn:samm:io.BatteryPass.CarbonFootprint:1.2.0#CarbonFootprintPerLifecycleStageEntity" + }, + "samm:description": { + "@language": "en", + "@value": "CarbainFootprints per lifecycle stage" + }, + "@type": "samm-c:List" + }, + { + "@id": "urn:samm:io.BatteryPass.CarbonFootprint:1.2.0#kilogramm", + "samm:symbol": "kg", + "samm:referenceUnit": { + "@id": "unit:kilogram" + }, + "samm:preferredName": { + "@language": "en", + "@value": "Kilogramm Co2 Equivalent" + }, + "@type": "samm:Unit" + }, + { + "@id": "urn:samm:io.BatteryPass.CarbonFootprint:1.2.0#carbonFootprintStudy", + "samm:characteristic": { + "@id": "samm-c:ResourcePath" + }, + "samm:description": { + "@language": "en", + "@value": "A web link to get access to a public version of the study supporting the carbon footprint values.\n\nDIN DKE Spec 99100 chapter reference: 6.3.8" + }, + "@type": "samm:Property" + }, + { + "@id": "_:b10", + "samm:optional": { + "@value": "true", + "@type": "xsd:boolean" + }, + "samm:property": { + "@id": "urn:samm:io.BatteryPass.CarbonFootprint:1.2.0#absoluteCarbonFootprint" + } + }, + { + "@id": "urn:samm:io.BatteryPass.CarbonFootprint:1.2.0#carbonFootprintPerLifecycleStage", + "samm:characteristic": { + "@id": "urn:samm:io.BatteryPass.CarbonFootprint:1.2.0#CarbonFootprints" + }, + "samm:description": { + "@language": "en", + "@value": "The carbon footprint of the battery as share of total Battery Carbon Footprint, differentiated per life cycle stages raw material extraction, battery production, distribution and recycling.\n\nDIN DKE Spec 99100 chapter reference: \n6.3.3: Raw material extraction\n6.3.4: Main production\n6.3.5: Distrinution\n6.3.6: EoL/Recycling" + }, + "@type": "samm:Property" + } + ], + "@context": { + "samm-e": "urn:samm:org.eclipse.esmf.samm:entity:2.1.0#", + "unit": "urn:samm:org.eclipse.esmf.samm:unit:2.1.0#", + "samm-c": "urn:samm:org.eclipse.esmf.samm:characteristic:2.1.0#", + "rdf": "http://www.w3.org/1999/02/22-rdf-syntax-ns#", + "xsd": "http://www.w3.org/2001/XMLSchema#", + "rdfs": "http://www.w3.org/2000/01/rdf-schema#", + "samm": "urn:samm:org.eclipse.esmf.samm:meta-model:2.1.0#", + "@vocab": "urn:samm:io.BatteryPass.CarbonFootprint:1.2.0#" + } +} diff --git a/tests/fixtures/samples/batterypass_BatteryPassDataModel_Circularity-ld.json b/tests/fixtures/samples/batterypass_BatteryPassDataModel_Circularity-ld.json new file mode 100644 index 0000000..606e06d --- /dev/null +++ b/tests/fixtures/samples/batterypass_BatteryPassDataModel_Circularity-ld.json @@ -0,0 +1,714 @@ +{ + "@graph": [ + { + "@id": "urn:samm:io.BatteryPass.Circularity:1.2.0#EndOfLifeInformation", + "samm:dataType": { + "@id": "urn:samm:io.BatteryPass.Circularity:1.2.0#EndOfLifeInformationEntity" + }, + "@type": "samm-c:SingleEntity" + }, + { + "@id": "urn:samm:io.BatteryPass.Circularity:1.2.0#EndOfLifeInformationEntity", + "samm:properties": { + "@list": [ + { + "@id": "urn:samm:io.BatteryPass.Circularity:1.2.0#wastePrevention" + }, + { + "@id": "urn:samm:io.BatteryPass.Circularity:1.2.0#separateCollection" + }, + { + "@id": "urn:samm:io.BatteryPass.Circularity:1.2.0#informationOnCollection" + } + ] + }, + "@type": "samm:Entity" + }, + { + "@id": "urn:samm:io.BatteryPass.Circularity:1.2.0#SparePartSourcesList", + "samm:dataType": { + "@id": "urn:samm:io.BatteryPass.Circularity:1.2.0#SparePartSupplierEntity" + }, + "@type": "samm-c:List" + }, + { + "@id": "urn:samm:io.BatteryPass.Circularity:1.2.0#SparePartSupplierEntity", + "samm:properties": { + "@list": [ + { + "@id": "urn:samm:io.BatteryPass.Circularity:1.2.0#nameOfSupplier" + }, + { + "@id": "urn:samm:io.BatteryPass.Circularity:1.2.0#addressOfSupplier" + }, + { + "@id": "urn:samm:io.BatteryPass.Circularity:1.2.0#emailAddressOfSupplier" + }, + { + "@id": "urn:samm:io.BatteryPass.Circularity:1.2.0#supplierWebAddress" + }, + { + "@id": "urn:samm:io.BatteryPass.Circularity:1.2.0#components" + } + ] + }, + "samm:description": { + "@language": "en", + "@value": "The part numbers for components should be provided together with the postal address, e-mail address and web address of the sources for spare parts." + }, + "@type": "samm:Entity" + }, + { + "@id": "urn:samm:io.BatteryPass.Circularity:1.2.0#ValidEmailAddress", + "samm:value": "^[\\w.-]+@[\\w.-]+\\.[A-Za-z]{2,}$", + "@type": "samm-c:RegularExpressionConstraint" + }, + { + "@id": "urn:samm:io.BatteryPass.Circularity:1.2.0#PreConsumerShare", + "samm-c:unit": { + "@id": "unit:percent" + }, + "samm:dataType": { + "@id": "xsd:float" + }, + "@type": "samm-c:Measurement" + }, + { + "@id": "urn:samm:io.BatteryPass.Circularity:1.2.0#informationOnCollection", + "samm:characteristic": { + "@id": "urn:samm:io.BatteryPass.Circularity:1.2.0#ResourcePath" + }, + "samm:description": { + "@language": "en", + "@value": "Prevention and management of waste batteries: Point (c) of Article 60(1): information on the separate collection, the take back, the collection points and preparing for re-use, preparing for repurposing, and recycling operations available for waste batteries" + }, + "@type": "samm:Property" + }, + { + "@id": "urn:samm:io.BatteryPass.Circularity:1.2.0#documentURL", + "samm:characteristic": { + "@id": "samm-c:ResourcePath" + }, + "samm:description": { + "@language": "en", + "@value": "Link to document" + }, + "@type": "samm:Property" + }, + { + "@id": "urn:samm:io.BatteryPass.Circularity:1.2.0#documentType", + "samm:characteristic": { + "@id": "urn:samm:io.BatteryPass.Circularity:1.2.0#Documenttype" + }, + "samm:description": { + "@language": "en", + "@value": "Describes type for document e.g. Dismantling manual" + }, + "@type": "samm:Property" + }, + { + "@id": "urn:samm:io.BatteryPass.Circularity:1.2.0#Documenttype", + "samm-c:values": { + "@list": [ + "BillOfMaterial", + "Model3D", + "DismantlingManual", + "RemovalManual", + "OtherManual", + "Drawing" + ] + }, + "samm:dataType": { + "@id": "xsd:string" + }, + "@type": "samm-c:Enumeration" + }, + { + "@id": "urn:samm:io.BatteryPass.Circularity:1.2.0#ConsumerShareRange", + "samm-c:upperBoundDefinition": { + "@id": "samm-c:AT_MOST" + }, + "samm-c:lowerBoundDefinition": { + "@id": "samm-c:AT_LEAST" + }, + "samm-c:maxValue": { + "@value": "100", + "@type": "xsd:float" + }, + "samm-c:minValue": { + "@value": "0", + "@type": "xsd:float" + }, + "@type": "samm-c:RangeConstraint" + }, + { + "@id": "urn:samm:io.BatteryPass.Circularity:1.2.0#supplierWebAddress", + "samm:characteristic": { + "@id": "samm-c:ResourcePath" + }, + "samm:description": { + "@language": "en", + "@value": "Web address of supplier for spare parts." + }, + "@type": "samm:Property" + }, + { + "@id": "urn:samm:io.BatteryPass.Circularity:1.2.0#partName", + "samm:exampleValue": "Cell", + "samm:characteristic": { + "@id": "samm-c:Text" + }, + "samm:preferredName": { + "@language": "en", + "@value": "PartName" + }, + "@type": "samm:Property" + }, + { + "@id": "urn:samm:io.BatteryPass.Circularity:1.2.0#mimeType", + "samm:characteristic": { + "@id": "samm-c:MimeType" + }, + "samm:description": { + "@language": "en", + "@value": "Defines internet media typ to determin how to interpret the documentURL" + }, + "@type": "samm:Property" + }, + { + "@id": "urn:samm:io.BatteryPass.Circularity:1.2.0#safetyMeasures", + "samm:characteristic": { + "@id": "urn:samm:io.BatteryPass.Circularity:1.2.0#SafetyMeasures" + }, + "samm:description": { + "@language": "en", + "@value": "Safety measures and instructions should also take past negative and extreme events as well as the separate data attributes ?battery status? and ?battery composition/chemistry? into account.\n\nDIN DKE Spec 99100 chapter reference: 6.6.1.5" + }, + "samm:preferredName": { + "@language": "en", + "@value": "SafetyMeasures" + }, + "@type": "samm:Property" + }, + { + "@id": "urn:samm:io.BatteryPass.Circularity:1.2.0#SafetyMeasures", + "samm:dataType": { + "@id": "urn:samm:io.BatteryPass.Circularity:1.2.0#SafetyMeasuresEntity" + }, + "@type": "samm-c:SingleEntity" + }, + { + "@id": "urn:samm:io.BatteryPass.Circularity:1.2.0#ComponentEntity", + "samm:properties": { + "@list": [ + { + "@id": "urn:samm:io.BatteryPass.Circularity:1.2.0#partName" + }, + { + "@id": "urn:samm:io.BatteryPass.Circularity:1.2.0#partNumber" + } + ] + }, + "@type": "samm:Entity" + }, + { + "@id": "urn:samm:io.BatteryPass.Circularity:1.2.0#AddressOfSupplier", + "samm:dataType": { + "@id": "urn:samm:io.BatteryPass.Circularity:1.2.0#PostalAddress" + }, + "samm:see": { + "@id": "https://schema.org/address" + }, + "@type": "samm-c:SingleEntity" + }, + { + "@id": "urn:samm:io.BatteryPass.Circularity:1.2.0#PostalAddress", + "samm:properties": { + "@list": [ + { + "@id": "urn:samm:io.BatteryPass.Circularity:1.2.0#addressCountry" + }, + { + "@id": "urn:samm:io.BatteryPass.Circularity:1.2.0#postalCode" + }, + { + "@id": "urn:samm:io.BatteryPass.Circularity:1.2.0#streetAddress" + } + ] + }, + "samm:see": { + "@id": "https://schema.org/PostalAddress" + }, + "@type": "samm:Entity" + }, + { + "@id": "urn:samm:io.BatteryPass.Circularity:1.2.0#postConsumerShare", + "samm:characteristic": { + "@id": "urn:samm:io.BatteryPass.Circularity:1.2.0#PostConsumerShareTrait" + }, + "samm:description": { + "@language": "en", + "@value": "Recycled material share from post-consumer waste (end-of-life scrap) of the active material." + }, + "@type": "samm:Property" + }, + { + "@id": "urn:samm:io.BatteryPass.Circularity:1.2.0#PostConsumerShareTrait", + "samm-c:constraint": { + "@id": "urn:samm:io.BatteryPass.Circularity:1.2.0#ConsumerShareRange" + }, + "samm-c:baseCharacteristic": { + "@id": "urn:samm:io.BatteryPass.Circularity:1.2.0#PostConsumerShare" + }, + "@type": "samm-c:Trait" + }, + { + "@id": "urn:samm:io.BatteryPass.Circularity:1.2.0#DismantlingandRemovalDocumentation", + "samm:properties": { + "@list": [ + { + "@id": "urn:samm:io.BatteryPass.Circularity:1.2.0#documentType" + }, + { + "@id": "urn:samm:io.BatteryPass.Circularity:1.2.0#mimeType" + }, + { + "@id": "urn:samm:io.BatteryPass.Circularity:1.2.0#documentURL" + } + ] + }, + "samm:description": { + "@language": "en", + "@value": "Dismantling and Removal information, including at least:- Exploded diagrams of the battery system/pack showing the location of battery cells- Disassembly sequences- Type and number of fastening techniques to be unlocked- Tools required for disassembly- Warnings if risk of damaging parts exists- Amount of cells used and layoutEUBR: Annex XIII (2c)" + }, + "@type": "samm:Entity" + }, + { + "@id": "urn:samm:io.BatteryPass.Circularity:1.2.0#endOfLifeInformation", + "samm:characteristic": { + "@id": "urn:samm:io.BatteryPass.Circularity:1.2.0#EndOfLifeInformation" + }, + "samm:description": { + "@language": "en", + "@value": "Producer or producer responsibility organisations shall make information available to distributors and end-users on: the role of end-users in contributing to waste prevention, including by information on good practices and recommendations concerning the use of batteries aiming at extending their use phase and the possibilities of re-use, preparing for re-use, preparing for repurpose, repurposing and remanufacturing.\n\nDIN DKE Spec 99100 chapter reference: 6.6.3.2 - 6.6.3.4" + }, + "samm:preferredName": { + "@language": "en", + "@value": "EndOfLifeInformation" + }, + "@type": "samm:Property" + }, + { + "@id": "urn:samm:io.BatteryPass.Circularity:1.2.0#wastePrevention", + "samm:characteristic": { + "@id": "urn:samm:io.BatteryPass.Circularity:1.2.0#ResourcePath" + }, + "samm:description": { + "@language": "en", + "@value": "Prevention and management of waste batteries: Point (a) of Article 60(1): Information on the role of end-users in contributing to waste prevention, including by information on good practices and recommendations concerning the use of batteries aiming at extending their use phase and the possibilities of re-use, preparing for re-use, preparing for repurpose, repurposing and remanufacturing" + }, + "@type": "samm:Property" + }, + { + "@id": "urn:samm:io.BatteryPass.Circularity:1.2.0#dismantlingAndRemovalInformation", + "samm:characteristic": { + "@id": "urn:samm:io.BatteryPass.Circularity:1.2.0#DocumentationList" + }, + "samm:description": { + "@language": "en", + "@value": "Dismantling and Removal information, including at least:- Exploded diagrams of the battery system/pack showing the location of battery cells- Disassembly sequences- Type and number of fastening techniques to be unlocked- Tools required for disassembly- Warnings if risk of damaging parts exists- Amount of cells used and layout. BR Annex XIII (2c)\n\nDIN DKE Spec 99100 chapter reference: 6.6.1.2" + }, + "@type": "samm:Property" + }, + { + "@id": "urn:samm:io.BatteryPass.Circularity:1.2.0#preConsumerShare", + "samm:characteristic": { + "@id": "urn:samm:io.BatteryPass.Circularity:1.2.0#PreConsumerShareTrait" + }, + "samm:description": { + "@language": "en", + "@value": "Recycled material share from pre-consumer waste (manufacturing waste, excluding run-around scrap) of the active material." + }, + "@type": "samm:Property" + }, + { + "@id": "urn:samm:io.BatteryPass.Circularity:1.2.0#ComponentList", + "samm:dataType": { + "@id": "urn:samm:io.BatteryPass.Circularity:1.2.0#ComponentEntity" + }, + "samm:description": { + "@language": "en", + "@value": "List of components available at supplier" + }, + "@type": "samm-c:List" + }, + { + "@id": "urn:samm:io.BatteryPass.Circularity:1.2.0#PreConsumerShareTrait", + "samm-c:constraint": { + "@id": "urn:samm:io.BatteryPass.Circularity:1.2.0#ConsumerShareRange" + }, + "samm-c:baseCharacteristic": { + "@id": "urn:samm:io.BatteryPass.Circularity:1.2.0#PreConsumerShare" + }, + "@type": "samm-c:Trait" + }, + { + "@id": "urn:samm:io.BatteryPass.Circularity:1.2.0#SafetyMeasuresEntity", + "samm:properties": { + "@list": [ + { + "@id": "urn:samm:io.BatteryPass.Circularity:1.2.0#safetyInstructions" + }, + { + "@id": "urn:samm:io.BatteryPass.Circularity:1.2.0#extinguishingAgent" + } + ] + }, + "samm:description": { + "@language": "en", + "@value": "The safety measures should be provided via the instruction manual as URL linking to PDF." + }, + "@type": "samm:Entity" + }, + { + "@id": "urn:samm:io.BatteryPass.Circularity:1.2.0#extinguishingAgent", + "samm:characteristic": { + "@id": "urn:samm:io.BatteryPass.Circularity:1.2.0#ExtinguishingAgentsList" + }, + "samm:description": { + "@language": "en", + "@value": "Usable extinguishing agents refering to classes of extinguishers (A, B, C, D, K).EUBR: Annex XIII (1a) ? Annex VI Part A (9)" + }, + "@type": "samm:Property" + }, + { + "@id": "urn:samm:io.BatteryPass.Circularity:1.2.0#PartNumber", + "samm:dataType": { + "@id": "xsd:string" + }, + "@type": "samm-c:Code" + }, + { + "@id": "urn:samm:io.BatteryPass.Circularity:1.2.0#RecycledMaterial", + "samm-c:values": { + "@list": [ + "Cobalt", + "Nickel", + "Lithium", + "Lead", + "Cobalt", + "Nickel", + "Lithium", + "Lead" + ] + }, + "samm:dataType": { + "@id": "xsd:string" + }, + "@type": "samm-c:Enumeration" + }, + { + "@id": "urn:samm:io.BatteryPass.Circularity:1.2.0#streetAddress", + "samm:exampleValue": "Street 1", + "samm:characteristic": { + "@id": "samm-c:Text" + }, + "samm:see": { + "@id": "https://schema.org/streetAddress" + }, + "@type": "samm:Property" + }, + { + "@id": "urn:samm:io.BatteryPass.Circularity:1.2.0#ResourcePath", + "samm:dataType": { + "@id": "xsd:anyURI" + }, + "samm:description": { + "@language": "en", + "@value": "The path of a resource." + }, + "samm:preferredName": { + "@language": "en", + "@value": "Resource Path" + }, + "@type": "samm:Characteristic" + }, + { + "@id": "urn:samm:io.BatteryPass.Circularity:1.2.0#nameOfSupplier", + "samm:characteristic": { + "@id": "samm-c:Text" + }, + "samm:description": { + "@language": "en", + "@value": "Name of Supplier" + }, + "@type": "samm:Property" + }, + { + "@id": "urn:samm:io.BatteryPass.Circularity:1.2.0#safetyInstructions", + "samm:characteristic": { + "@id": "urn:samm:io.BatteryPass.Circularity:1.2.0#ResourcePath" + }, + "samm:description": { + "@language": "en", + "@value": "- Safety measures. - Necessary safety instructions to handle waste batteries, including in relation to the risks associated with, and the handling of, batteries containing lithium." + }, + "@type": "samm:Property" + }, + { + "@id": "urn:samm:io.BatteryPass.Circularity:1.2.0#renewableContent", + "samm:characteristic": { + "@id": "urn:samm:io.BatteryPass.Circularity:1.2.0#RenewableContent" + }, + "samm:description": { + "@language": "en", + "@value": "Share of renewable material content. A renewable material is a material made of natural resources that can be replenished. \n\nDIN DKE Spec 99100 chapter reference: 6.6.2.11" + }, + "samm:preferredName": { + "@language": "en", + "@value": "RenewableContent" + }, + "@type": "samm:Property" + }, + { + "@id": "urn:samm:io.BatteryPass.Circularity:1.2.0#recycledContent", + "samm:characteristic": { + "@id": "urn:samm:io.BatteryPass.Circularity:1.2.0#RecycledContentList" + }, + "samm:description": { + "@language": "en", + "@value": "Share of material recovered from waste present in active materials for each battery model per year and per manufacturing plant.\n\nDIN DKE Spec 99100 chapter reference: 6.6.2.3 - 6.6.2.10" + }, + "@type": "samm:Property" + }, + { + "@id": "urn:samm:io.BatteryPass.Circularity:1.2.0#RecycledContentList", + "samm:dataType": { + "@id": "urn:samm:io.BatteryPass.Circularity:1.2.0#RecycledContentEntity" + }, + "@type": "samm-c:List" + }, + { + "@id": "urn:samm:io.BatteryPass.Circularity:1.2.0#addressOfSupplier", + "samm:characteristic": { + "@id": "urn:samm:io.BatteryPass.Circularity:1.2.0#AddressOfSupplier" + }, + "samm:description": { + "@language": "en", + "@value": "Postal address of supplier for spare parts." + }, + "@type": "samm:Property" + }, + { + "@id": "urn:samm:io.BatteryPass.Circularity:1.2.0#RenewableContent", + "samm-c:unit": { + "@id": "unit:percent" + }, + "samm:dataType": { + "@id": "xsd:float" + }, + "@type": "samm-c:Measurement" + }, + { + "@id": "urn:samm:io.BatteryPass.Circularity:1.2.0#components", + "samm:characteristic": { + "@id": "urn:samm:io.BatteryPass.Circularity:1.2.0#ComponentList" + }, + "samm:description": { + "@language": "en", + "@value": "Components available at supplier" + }, + "@type": "samm:Property" + }, + { + "@id": "urn:samm:io.BatteryPass.Circularity:1.2.0#recycledMaterial", + "samm:characteristic": { + "@id": "urn:samm:io.BatteryPass.Circularity:1.2.0#RecycledMaterial" + }, + "samm:description": { + "@language": "en", + "@value": "Name of recycled material" + }, + "samm:preferredName": { + "@language": "en", + "@value": "RecycledMaterial" + }, + "@type": "samm:Property" + }, + { + "@id": "urn:samm:io.BatteryPass.Circularity:1.2.0#RecycledContentEntity", + "samm:properties": { + "@list": [ + { + "@id": "urn:samm:io.BatteryPass.Circularity:1.2.0#preConsumerShare" + }, + { + "@id": "urn:samm:io.BatteryPass.Circularity:1.2.0#recycledMaterial" + }, + { + "@id": "urn:samm:io.BatteryPass.Circularity:1.2.0#postConsumerShare" + } + ] + }, + "samm:description": { + "@language": "en", + "@value": "A battery passport must include recycled content information.\n\nThe content information must include the percentage share of nickel that is present in active materials and that has been recovered from battery manufacturing waste, for each battery model per year and per manufacturing plant." + }, + "@type": "samm:Entity" + }, + { + "@id": "urn:samm:io.BatteryPass.Circularity:1.2.0#PostConsumerShare", + "samm-c:unit": { + "@id": "unit:percent" + }, + "samm:dataType": { + "@id": "xsd:float" + }, + "@type": "samm-c:Measurement" + }, + { + "@id": "urn:samm:io.BatteryPass.Circularity:1.2.0#emailAddressOfSupplier", + "samm:characteristic": { + "@id": "urn:samm:io.BatteryPass.Circularity:1.2.0#EmailAddressOfSupplierTrait" + }, + "samm:description": { + "@language": "en", + "@value": "E-mail address of supplier for spare parts." + }, + "@type": "samm:Property" + }, + { + "@id": "urn:samm:io.BatteryPass.Circularity:1.2.0#EmailAddressOfSupplierTrait", + "samm-c:constraint": { + "@id": "urn:samm:io.BatteryPass.Circularity:1.2.0#ValidEmailAddress" + }, + "samm-c:baseCharacteristic": { + "@id": "samm-c:Text" + }, + "@type": "samm-c:Trait" + }, + { + "@id": "urn:samm:io.BatteryPass.Circularity:1.2.0#separateCollection", + "samm:characteristic": { + "@id": "urn:samm:io.BatteryPass.Circularity:1.2.0#ResourcePath" + }, + "samm:description": { + "@language": "en", + "@value": "Prevention and management of waste batteries: Point (b) of Article 60(1): Information on the role of end-users in contributing to the separate collection of waste batteries in accordance with their obligations under Article 51 so as to allow their treatment" + }, + "@type": "samm:Property" + }, + { + "@id": "urn:samm:io.BatteryPass.Circularity:1.2.0#postalCode", + "samm:exampleValue": "DE-10719", + "samm:characteristic": { + "@id": "samm-c:Text" + }, + "samm:see": { + "@id": "https://schema.org/postalCode" + }, + "samm:preferredName": { + "@language": "en", + "@value": "PostalCode" + }, + "@type": "samm:Property" + }, + { + "@id": "urn:samm:io.BatteryPass.Circularity:1.2.0#addressCountry", + "samm:exampleValue": "Germany", + "samm:characteristic": { + "@id": "samm-c:Text" + }, + "samm:see": { + "@id": "https://schema.org/addressCountry" + }, + "@type": "samm:Property" + }, + { + "@id": "urn:samm:io.BatteryPass.Circularity:1.2.0#DocumentationList", + "samm:dataType": { + "@id": "urn:samm:io.BatteryPass.Circularity:1.2.0#DismantlingandRemovalDocumentation" + }, + "samm:description": { + "@language": "en", + "@value": "A collection of required documentation to support EoL actions" + }, + "@type": "samm-c:List" + }, + { + "@id": "urn:samm:io.BatteryPass.Circularity:1.2.0#ExtinguishingAgentsList", + "samm:dataType": { + "@id": "xsd:string" + }, + "@type": "samm-c:List" + }, + { + "@id": "urn:samm:io.BatteryPass.Circularity:1.2.0#Circularity", + "samm:events": { + "@list": [] + }, + "samm:operations": { + "@list": [] + }, + "samm:properties": { + "@list": [ + { + "@id": "urn:samm:io.BatteryPass.Circularity:1.2.0#dismantlingAndRemovalInformation" + }, + { + "@id": "urn:samm:io.BatteryPass.Circularity:1.2.0#sparePartSources" + }, + { + "@id": "urn:samm:io.BatteryPass.Circularity:1.2.0#recycledContent" + }, + { + "@id": "urn:samm:io.BatteryPass.Circularity:1.2.0#safetyMeasures" + }, + { + "@id": "urn:samm:io.BatteryPass.Circularity:1.2.0#endOfLifeInformation" + }, + { + "@id": "urn:samm:io.BatteryPass.Circularity:1.2.0#renewableContent" + } + ] + }, + "samm:description": { + "@language": "en", + "@value": "Dismantling information (including at least: exploded diagrams of the battery system/pack showing the location of battery cells; disassembly sequences; type and number of fastening techniques to be unlocked; tools required for disassembly; warnings if risk of damaging parts exists; amount of cells used and layout); part numbers for components and contact details of sources for replacement spares; safety measures (Annex XIII (2b-d)); usable extinguishing agent (Annex VI, Part A(9)). 2024 Circulor (for and on behalf of the Battery Pass Consortium). This work is licensed under a Creative Commons License Attribution-NonCommercial 4.0 International (CC BY-NC 4.0). Readers may reproduce material for their own publications, as long as it is not sold commercially and is given appropriate attribution." + }, + "@type": "samm:Aspect" + }, + { + "@id": "urn:samm:io.BatteryPass.Circularity:1.2.0#sparePartSources", + "samm:characteristic": { + "@id": "urn:samm:io.BatteryPass.Circularity:1.2.0#SparePartSourcesList" + }, + "samm:description": { + "@language": "en", + "@value": "Contact details of sources for replacement spares. Postal address, including name and brand names, postal code and place, street and number, country, telephone, if any. BR Annex XIII (2b)\n\nDIN DKE Spec 99100 chapter reference: 6.6.1.3" + }, + "@type": "samm:Property" + }, + { + "@id": "urn:samm:io.BatteryPass.Circularity:1.2.0#partNumber", + "samm:characteristic": { + "@id": "urn:samm:io.BatteryPass.Circularity:1.2.0#PartNumber" + }, + "samm:description": { + "@language": "en", + "@value": "Part Number of Component" + }, + "@type": "samm:Property" + } + ], + "@context": { + "samm-e": "urn:samm:org.eclipse.esmf.samm:entity:2.1.0#", + "unit": "urn:samm:org.eclipse.esmf.samm:unit:2.1.0#", + "samm-c": "urn:samm:org.eclipse.esmf.samm:characteristic:2.1.0#", + "rdf": "http://www.w3.org/1999/02/22-rdf-syntax-ns#", + "xsd": "http://www.w3.org/2001/XMLSchema#", + "rdfs": "http://www.w3.org/2000/01/rdf-schema#", + "samm": "urn:samm:org.eclipse.esmf.samm:meta-model:2.1.0#", + "@vocab": "urn:samm:io.BatteryPass.Circularity:1.2.0#" + } +} diff --git a/tests/fixtures/samples/batterypass_BatteryPassDataModel_GeneralProductInformation-ld.json b/tests/fixtures/samples/batterypass_BatteryPassDataModel_GeneralProductInformation-ld.json new file mode 100644 index 0000000..fb7dc6e --- /dev/null +++ b/tests/fixtures/samples/batterypass_BatteryPassDataModel_GeneralProductInformation-ld.json @@ -0,0 +1,497 @@ +{ + "@graph": [ + { + "@id": "urn:samm:io.BatteryPass.GeneralProductInformation:1.0.0#BatteryMassMeasurable", + "samm-c:unit": { + "@id": "unit:kilogram" + }, + "samm:dataType": { + "@id": "xsd:float" + }, + "samm:description": { + "@language": "en", + "@value": "Weight of the battery\nEUBR: Annex XIII (1a) ? Annex VI Part A (5)" + }, + "@type": "samm-c:Measurement" + }, + { + "@id": "urn:samm:io.BatteryPass.GeneralProductInformation:1.0.0#warrentyPeriod", + "samm:characteristic": { + "@id": "urn:samm:io.BatteryPass.GeneralProductInformation:1.0.0#WarrentyPeriod" + }, + "samm:description": { + "@language": "en", + "@value": "The battery passport must include information about the period for which the commercial warranty applies.\n\nDIN DKE Spec chapter reference: 6.1.3.4" + }, + "@type": "samm:Property" + }, + { + "@id": "urn:samm:io.BatteryPass.GeneralProductInformation:1.0.0#ManufacturerInformation", + "samm:dataType": { + "@id": "urn:samm:io.BatteryPass.GeneralProductInformation:1.0.0#ContactInformationEntity" + }, + "@type": "samm-c:SingleEntity" + }, + { + "@id": "urn:samm:io.BatteryPass.GeneralProductInformation:1.0.0#ContactInformationEntity", + "samm:properties": { + "@list": [ + { + "@id": "urn:samm:io.BatteryPass.GeneralProductInformation:1.0.0#contactName" + }, + { + "@id": "urn:samm:io.BatteryPass.GeneralProductInformation:1.0.0#postalAddress" + }, + { + "@id": "urn:samm:io.BatteryPass.GeneralProductInformation:1.0.0#identifier" + } + ] + }, + "@type": "samm:Entity" + }, + { + "@id": "urn:samm:io.BatteryPass.GeneralProductInformation:1.0.0#ProductPassportIdentifierTrait", + "samm-c:constraint": { + "@id": "urn:samm:io.BatteryPass.GeneralProductInformation:1.0.0#CodeConstraint" + }, + "samm-c:baseCharacteristic": { + "@id": "urn:samm:io.BatteryPass.GeneralProductInformation:1.0.0#ProductPassportIdentifierCode" + }, + "@type": "samm-c:Trait" + }, + { + "@id": "urn:samm:io.BatteryPass.GeneralProductInformation:1.0.0#CodeConstraint", + "samm:value": "^urn:[a-z0-9]+:[a-z0-9]+$", + "samm:description": { + "@language": "en", + "@value": "Code constraint for URN" + }, + "@type": "samm-c:RegularExpressionConstraint" + }, + { + "@id": "urn:samm:io.BatteryPass.GeneralProductInformation:1.0.0#ProductPassportIdentifierCode", + "samm:dataType": { + "@id": "xsd:string" + }, + "samm:description": { + "@language": "en", + "@value": "A unique identifier is defined as \"a unique string of characters for the identification of batteries that also enables a web link to the battery passport\" (Art. 3(66)), to be attributed by the economic operator placing the battery on the market (Art. 77(3)). The unique identifier shall comply with the standard (?ISO/IEC?) 15459:2015 or equivalent (Art. 77(3)). A QR code shall provide access to the battery passport and be linked to the unique identifier (Art. 77(3)). Batteries shall ?bear a model identification and batch or serial number, or product number or another element allowing their identification? (Art. 38(6)). \n\nBattery Regulation Reference: Art. 77(3); Art. 3(66); Art. 38(6)" + }, + "samm:preferredName": { + "@language": "en", + "@value": "ProductPassportIdentifierCode" + }, + "@type": "samm-c:Code" + }, + { + "@id": "urn:samm:io.BatteryPass.GeneralProductInformation:1.0.0#BatteryCategoryEnum", + "samm-c:values": { + "@list": [ + "lmt", + "ev", + "industrial", + "stationary" + ] + }, + "samm:dataType": { + "@id": "xsd:string" + }, + "@type": "samm-c:Enumeration" + }, + { + "@id": "urn:samm:io.BatteryPass.GeneralProductInformation:1.0.0#ProductIdentifierCode", + "samm:dataType": { + "@id": "xsd:string" + }, + "samm:description": { + "@language": "en", + "@value": "A unique identifier is defined as \"a unique string of characters for the identification of batteries that also enables a web link to the battery passport\" (Art. 3(66)), to be attributed by the economic operator placing the battery on the market (Art. 77(3)). The unique identifier shall comply with the standard (?ISO/IEC?) 15459:2015 or equivalent (Art. 77(3)). A QR code shall provide access to the battery passport and be linked to the unique identifier (Art. 77(3)). Batteries shall ?bear a model identification and batch or serial number, or product number or another element allowing their identification? (Art. 38(6)). \n\nBattery Regulation Reference: Art. 77(3); Art. 3(66); Art. 38(6)" + }, + "samm:preferredName": { + "@language": "en", + "@value": "ProductIdentifierCode" + }, + "@type": "samm-c:Code" + }, + { + "@id": "urn:samm:io.BatteryPass.GeneralProductInformation:1.0.0#addressCountry", + "samm:exampleValue": "Germany", + "samm:characteristic": { + "@id": "samm-c:Text" + }, + "samm:see": { + "@id": "https://schema.org/addressCountry" + }, + "@type": "samm:Property" + }, + { + "@id": "urn:samm:io.BatteryPass.GeneralProductInformation:1.0.0#manufacturerInformation", + "samm:characteristic": { + "@id": "urn:samm:io.BatteryPass.GeneralProductInformation:1.0.0#ManufacturerInformation" + }, + "samm:description": { + "@language": "en", + "@value": "Unambiguous identification of the manufacturer of the battery, suggested via a unique operator identifier (as \"unique string of characters for the identification of actors involved in the value chain of products\", ESPR Art. 2(32)). \n\nDIN DKE Spec chapter reference: 6.1.2.4" + }, + "samm:preferredName": { + "@language": "en", + "@value": "ManufacturerIdentification" + }, + "@type": "samm:Property" + }, + { + "@id": "urn:samm:io.BatteryPass.GeneralProductInformation:1.0.0#batteryPassportIdentifier", + "samm:exampleValue": "urn:bmwk:123456687678", + "samm:characteristic": { + "@id": "urn:samm:io.BatteryPass.GeneralProductInformation:1.0.0#ProductPassportIdentifierTrait" + }, + "samm:description": { + "@language": "en", + "@value": "Unique identifier allowing for the unambiguous identification of each individual battery and hence each corresponding battery passport (exploration of a potential additional battery passport identifier (not requried per Battery Regulation) ongoing)." + }, + "samm:preferredName": { + "@language": "en", + "@value": "BatteryPassportIdentifier" + }, + "@type": "samm:Property" + }, + { + "@id": "urn:samm:io.BatteryPass.GeneralProductInformation:1.0.0#manufacturingPlace", + "samm:characteristic": { + "@id": "urn:samm:io.BatteryPass.GeneralProductInformation:1.0.0#ManufacturingPlace" + }, + "samm:see": { + "@id": "https://schema.org/PostalAddress" + }, + "samm:description": { + "@language": "en", + "@value": "Unambiguous identification of the manufacturing facility (e.g. country, city, street, building (if needed)), suggested via a unique facility identifier (as \"unique string of characters for the identification of locations or buildings involved in the value chain of a product or used by actors involved in the value chain of a product\", ESPR Art. 2(33)).\n\nDIN DKE Spec chapter reference: 6.1.3.1" + }, + "samm:preferredName": { + "@language": "en", + "@value": "ManufacturingPlace" + }, + "@type": "samm:Property" + }, + { + "@id": "urn:samm:io.BatteryPass.GeneralProductInformation:1.0.0#ManufacturingPlace", + "samm:dataType": { + "@id": "urn:samm:io.BatteryPass.GeneralProductInformation:1.0.0#PostalAddressEntity" + }, + "samm:see": { + "@id": "https://schema.org/PostalAddress" + }, + "@type": "samm-c:SingleEntity" + }, + { + "@id": "urn:samm:io.BatteryPass.GeneralProductInformation:1.0.0#PostalAddressEntity", + "samm:properties": { + "@list": [ + { + "@id": "urn:samm:io.BatteryPass.GeneralProductInformation:1.0.0#addressCountry" + }, + { + "@id": "urn:samm:io.BatteryPass.GeneralProductInformation:1.0.0#postalCode" + }, + { + "@id": "urn:samm:io.BatteryPass.GeneralProductInformation:1.0.0#streetAddress" + } + ] + }, + "samm:see": { + "@id": "https://schema.org/PostalAddress" + }, + "@type": "samm:Entity" + }, + { + "@id": "urn:samm:io.BatteryPass.GeneralProductInformation:1.0.0#productIdentifier", + "samm:characteristic": { + "@id": "urn:samm:io.BatteryPass.GeneralProductInformation:1.0.0#ProductIdentifierCode" + }, + "samm:description": { + "@language": "en", + "@value": "Unique identifier allowing for the unambiguous identification of each individual battery and hence each corresponding battery passport (exploration of a potential additional battery passport identifier (not requried per Battery Regulation) ongoing).\nDIN DKE Spec chapter reference: 6.1.2.2" + }, + "samm:preferredName": { + "@language": "en", + "@value": "ProductIdentifier" + }, + "@type": "samm:Property" + }, + { + "@id": "urn:samm:io.BatteryPass.GeneralProductInformation:1.0.0#puttingIntoService", + "samm:characteristic": { + "@id": "urn:samm:io.BatteryPass.GeneralProductInformation:1.0.0#PuttingIntoServiceDateTime" + }, + "samm:description": { + "@language": "en", + "@value": "Where appropriate, the battery passport must include information on the date of putting the battery into service. BR Annex VI Part A (1); Art. 3(33); Art. 38(7); ESPR Art. 2(32)\n\nDIN DKE Spec chapter reference: 6.1.3.3" + }, + "@type": "samm:Property" + }, + { + "@id": "urn:samm:io.BatteryPass.GeneralProductInformation:1.0.0#PuttingIntoServiceDateTime", + "samm:dataType": { + "@id": "xsd:dateTime" + }, + "@type": "samm:Characteristic" + }, + { + "@id": "urn:samm:io.BatteryPass.GeneralProductInformation:1.0.0#operatorInformation", + "samm:characteristic": { + "@id": "urn:samm:io.BatteryPass.GeneralProductInformation:1.0.0#OperatorInformation" + }, + "samm:description": { + "@language": "en", + "@value": "State the name, trade name or mark, postal address, web ad-dress, e-mail address. Suggested reporting via a unique operator identifier (see requirements of unique battery identifier).\n\nDIN DKE Spec chapter reference: 6.1.2.3" + }, + "samm:preferredName": { + "@language": "en", + "@value": "OperatorInformation" + }, + "@type": "samm:Property" + }, + { + "@id": "urn:samm:io.BatteryPass.GeneralProductInformation:1.0.0#batteryCategory", + "samm:characteristic": { + "@id": "urn:samm:io.BatteryPass.GeneralProductInformation:1.0.0#BatteryCategoryEnum" + }, + "samm:description": { + "@language": "en", + "@value": "Categories relevant for the battery passport: LMT battery, ?electric vehicle battery, stationary or other industrial battery >2kWh.\n\nDIN DKE Spec chapter reference: 6.1.3.5" + }, + "samm:preferredName": { + "@language": "en", + "@value": "BatteryCategory" + }, + "@type": "samm:Property" + }, + { + "@id": "urn:samm:io.BatteryPass.GeneralProductInformation:1.0.0#identifier", + "samm:characteristic": { + "@id": "urn:samm:io.BatteryPass.GeneralProductInformation:1.0.0#Identifier" + }, + "@type": "samm:Property" + }, + { + "@id": "urn:samm:io.BatteryPass.GeneralProductInformation:1.0.0#postalCode", + "samm:exampleValue": "10719", + "samm:characteristic": { + "@id": "samm-c:Text" + }, + "samm:see": { + "@id": "https://schema.org/postalCode" + }, + "@type": "samm:Property" + }, + { + "@id": "urn:samm:io.BatteryPass.GeneralProductInformation:1.0.0#WarrentyPeriod", + "samm-c:unit": { + "@id": "unit:month" + }, + "samm:dataType": { + "@id": "xsd:gMonth" + }, + "@type": "samm-c:Measurement" + }, + { + "@id": "urn:samm:io.BatteryPass.GeneralProductInformation:1.0.0#batteryMass", + "samm:exampleValue": { + "@value": "699", + "@type": "xsd:float" + }, + "samm:characteristic": { + "@id": "urn:samm:io.BatteryPass.GeneralProductInformation:1.0.0#BatteryMassMeasurable" + }, + "samm:description": { + "@language": "en", + "@value": "Mass of the entire battery in kilograms. Voluntary: if the battery is defined on pack or module level: also weight of the modules and/or cells.\n\nDIN DKE Spec chapter reference: 6.1.3.6" + }, + "@type": "samm:Property" + }, + { + "@id": "urn:samm:io.BatteryPass.GeneralProductInformation:1.0.0#Identifier", + "samm:dataType": { + "@id": "xsd:string" + }, + "samm:description": { + "@language": "en", + "@value": "Not demanded by the EU Battery Regulation" + }, + "samm:preferredName": { + "@language": "en", + "@value": "EconomicOperatorCode" + }, + "@type": "samm-c:Code" + }, + { + "@id": "urn:samm:io.BatteryPass.GeneralProductInformation:1.0.0#BatteryStatusEnumeration", + "samm-c:values": { + "@list": [ + "Original", + "Repurposed", + "Reused", + "Remanufactured", + "Waste" + ] + }, + "samm:dataType": { + "@id": "xsd:string" + }, + "samm:description": { + "@language": "en", + "@value": "Lifecycle status of the battery. Status defined from a list, with the options suggested as follows: 'original', 'repurposed', 'reused', 'remanufactured', 'waste'\n\nEUBR: Annex XIII (4c)" + }, + "@type": "samm-c:Enumeration" + }, + { + "@id": "urn:samm:io.BatteryPass.GeneralProductInformation:1.0.0#postalAddress", + "samm:characteristic": { + "@id": "urn:samm:io.BatteryPass.GeneralProductInformation:1.0.0#PostalAddress" + }, + "@type": "samm:Property" + }, + { + "@id": "urn:samm:io.BatteryPass.GeneralProductInformation:1.0.0#PostalAddress", + "samm:dataType": { + "@id": "urn:samm:io.BatteryPass.GeneralProductInformation:1.0.0#PostalAddressEntity" + }, + "@type": "samm-c:SingleEntity" + }, + { + "@id": "urn:samm:io.BatteryPass.GeneralProductInformation:1.0.0#GeneralProductInformation", + "samm:events": { + "@list": [] + }, + "samm:operations": { + "@list": [] + }, + "samm:properties": { + "@list": [ + { + "@id": "urn:samm:io.BatteryPass.GeneralProductInformation:1.0.0#productIdentifier" + }, + { + "@id": "urn:samm:io.BatteryPass.GeneralProductInformation:1.0.0#batteryPassportIdentifier" + }, + { + "@id": "urn:samm:io.BatteryPass.GeneralProductInformation:1.0.0#batteryCategory" + }, + { + "@id": "urn:samm:io.BatteryPass.GeneralProductInformation:1.0.0#manufacturerInformation" + }, + { + "@id": "urn:samm:io.BatteryPass.GeneralProductInformation:1.0.0#manufacturingDate" + }, + { + "@id": "urn:samm:io.BatteryPass.GeneralProductInformation:1.0.0#batteryStatus" + }, + { + "@id": "urn:samm:io.BatteryPass.GeneralProductInformation:1.0.0#batteryMass" + }, + { + "@id": "urn:samm:io.BatteryPass.GeneralProductInformation:1.0.0#manufacturingPlace" + }, + { + "@id": "urn:samm:io.BatteryPass.GeneralProductInformation:1.0.0#operatorInformation" + }, + { + "@id": "urn:samm:io.BatteryPass.GeneralProductInformation:1.0.0#puttingIntoService" + }, + { + "@id": "urn:samm:io.BatteryPass.GeneralProductInformation:1.0.0#warrentyPeriod" + } + ] + }, + "samm:description": { + "@language": "en", + "@value": "Mandatory data: Product identification; manufacturer?s identification; manufacturing place; manufacturing date; battery category; battery weight; battery status (Annex VI, Part A and Annex XIII)\nCopyright ? 2023 Circulor (for and on behalf of the Battery Pass Consortium). This work is li-censed under a Creative Commons License Attribution-NonCommercial 4.0 International (CC BY-NC 4.0). Readers may reproduce material for their own publications, as long as it is not sold com-mercially and is given appropriate attribution." + }, + "@type": "samm:Aspect" + }, + { + "@id": "urn:samm:io.BatteryPass.GeneralProductInformation:1.0.0#contactName", + "samm:characteristic": { + "@id": "samm-c:Text" + }, + "@type": "samm:Property" + }, + { + "@id": "urn:samm:io.BatteryPass.GeneralProductInformation:1.0.0#ManufacturingDateTimeStamp", + "samm:dataType": { + "@id": "xsd:dateTimeStamp" + }, + "samm:description": { + "@language": "en", + "@value": "Manufacturing date (month and year)\nRegulation Reference: Annex XIII (1a) ? Annex VI Part A (4); Annex VII Part B (1)" + }, + "samm:preferredName": { + "@language": "en", + "@value": "ManufacturingDate" + }, + "@type": "samm:Characteristic" + }, + { + "@id": "urn:samm:io.BatteryPass.GeneralProductInformation:1.0.0#streetAddress", + "samm:exampleValue": "Hindenburgstr. 10", + "samm:characteristic": { + "@id": "samm-c:Text" + }, + "samm:see": { + "@id": "https://schema.org/streetAddress" + }, + "@type": "samm:Property" + }, + { + "@id": "urn:samm:io.BatteryPass.GeneralProductInformation:1.0.0#batteryStatus", + "samm:exampleValue": "Original", + "samm:characteristic": { + "@id": "urn:samm:io.BatteryPass.GeneralProductInformation:1.0.0#BatteryStatusEnumeration" + }, + "samm:description": { + "@language": "en", + "@value": "Lifecycle status of the battery. Status defined from a list, with the options suggested as follows: 'original', 'repurposed', 'reused', 'remanufactured', 'waste'.\n\nDIN DKE Spec chapter reference: 6.1.3.7" + }, + "samm:preferredName": { + "@language": "en", + "@value": "BatteryStatus" + }, + "@type": "samm:Property" + }, + { + "@id": "urn:samm:io.BatteryPass.GeneralProductInformation:1.0.0#OperatorInformation", + "samm:dataType": { + "@id": "urn:samm:io.BatteryPass.GeneralProductInformation:1.0.0#ContactInformationEntity" + }, + "@type": "samm-c:SingleEntity" + }, + { + "@id": "urn:samm:io.BatteryPass.GeneralProductInformation:1.0.0#manufacturingDate", + "samm:characteristic": { + "@id": "urn:samm:io.BatteryPass.GeneralProductInformation:1.0.0#ManufacturingDateTimeStamp" + }, + "samm:description": { + "@language": "en", + "@value": "The manufacturing date should not only relate to the battery model, but to the battery item.\n\nThe date code should comply with DIN ISO 8601 1:2020 12 and ISO 8601 2:2019.\n\nDIN DKE Spec chapter reference: 6.1.3.2" + }, + "@type": "samm:Property" + }, + { + "@id": "urn:samm:io.BatteryPass.GeneralProductInformation:1.0.0#Characteristic4", + "samm:dataType": { + "@id": "urn:samm:io.BatteryPass.GeneralProductInformation:1.0.0#ContactInformationEntity" + }, + "@type": "samm:Characteristic" + } + ], + "@context": { + "samm-e": "urn:samm:org.eclipse.esmf.samm:entity:2.1.0#", + "unit": "urn:samm:org.eclipse.esmf.samm:unit:2.1.0#", + "samm-c": "urn:samm:org.eclipse.esmf.samm:characteristic:2.1.0#", + "rdf": "http://www.w3.org/1999/02/22-rdf-syntax-ns#", + "xsd": "http://www.w3.org/2001/XMLSchema#", + "rdfs": "http://www.w3.org/2000/01/rdf-schema#", + "samm": "urn:samm:org.eclipse.esmf.samm:meta-model:2.1.0#", + "@vocab": "urn:samm:io.BatteryPass.GeneralProductInformation:1.0.0#" + } +} diff --git a/tests/fixtures/samples/batterypass_BatteryPassDataModel_MaterialComposition-ld.json b/tests/fixtures/samples/batterypass_BatteryPassDataModel_MaterialComposition-ld.json new file mode 100644 index 0000000..5209dad --- /dev/null +++ b/tests/fixtures/samples/batterypass_BatteryPassDataModel_MaterialComposition-ld.json @@ -0,0 +1,536 @@ +{ + "@graph": [ + { + "@id": "urn:samm:io.BatteryPass.MaterialComposition:1.2.0#isCriticalRawMaterial", + "samm:exampleValue": { + "@value": "true", + "@type": "xsd:boolean" + }, + "samm:characteristic": { + "@id": "samm-c:Boolean" + }, + "samm:description": { + "@language": "en", + "@value": "The battery passport must contain information on the critical raw materials present in the battery.\n\nThe information on the critical raw materials must also be provided on the battery label.\nPer Annex VI, Part A(10), critical raw materials must be reported if present in the battery in a concentration of more than 0,1 % weight by weight. " + }, + "@type": "samm:Property" + }, + { + "@id": "urn:samm:io.BatteryPass.MaterialComposition:1.2.0#clearName", + "samm:exampleValue": "Lithium nickel manganese cobalt oxides", + "samm:characteristic": { + "@id": "samm-c:Text" + }, + "@type": "samm:Property" + }, + { + "@id": "urn:samm:io.BatteryPass.MaterialComposition:1.2.0#hazardousSubstanceLocation", + "samm:characteristic": { + "@id": "urn:samm:io.BatteryPass.MaterialComposition:1.2.0#HubstanceSubstanceLocationEntity" + }, + "samm:description": { + "@language": "en", + "@value": "Hazardous substances (No. 19-23): Location on a (sub-)component-level of all hazardous substances (as ?any substance that poses a threat to human health and the environment?). Suggested via a unique identifier or nomenclature." + }, + "samm:preferredName": { + "@language": "en", + "@value": "Location" + }, + "@type": "samm:Property" + }, + { + "@id": "urn:samm:io.BatteryPass.MaterialComposition:1.2.0#HubstanceSubstanceLocationEntity", + "samm:dataType": { + "@id": "urn:samm:io.BatteryPass.MaterialComposition:1.2.0#BatteryLocationEntity" + }, + "samm:description": { + "@language": "en", + "@value": "\"The impact of substances, in particular, hazardous substances, contained in batteries on the environment and on human health or safety of persons, including impact due to inappropriate discarding of waste batteries such as littering or discarding as unsorted municipal waste?." + }, + "samm:preferredName": { + "@language": "en", + "@value": "Location" + }, + "@type": "samm-c:SingleEntity" + }, + { + "@id": "urn:samm:io.BatteryPass.MaterialComposition:1.2.0#HazardousSubstanceClassChrateristicEnum", + "samm-c:values": { + "@list": [ + "AcuteToxicity", + "SkinCorrosionOrIrritation", + "EyeDamageOrIrritation" + ] + }, + "samm:dataType": { + "@id": "xsd:string" + }, + "@type": "samm-c:Enumeration" + }, + { + "@id": "urn:samm:io.BatteryPass.MaterialComposition:1.2.0#MaterialIdentifierTrait", + "samm-c:constraint": { + "@id": "urn:samm:io.BatteryPass.MaterialComposition:1.2.0#CASNumberConstraint" + }, + "samm-c:baseCharacteristic": { + "@id": "samm-c:Text" + }, + "@type": "samm-c:Trait" + }, + { + "@id": "urn:samm:io.BatteryPass.MaterialComposition:1.2.0#CASNumberConstraint", + "samm:value": "^\\d{2,7}-\\d{2}-\\d{1}$", + "@type": "samm-c:RegularExpressionConstraint" + }, + { + "@id": "urn:samm:io.BatteryPass.MaterialComposition:1.2.0#batteryMaterials", + "samm:characteristic": { + "@id": "urn:samm:io.BatteryPass.MaterialComposition:1.2.0#BatteryMaterialList" + }, + "samm:description": { + "@language": "en", + "@value": "\"Component materials used\" (No. 17.a-c): Naming the materials (as a composition of substances) in cathode, anode, electrolyte according to public standards, including specification of the corresponding component (i.e., cathode, anode, or electrolyte). We suggest a reporting threshold of 0.1 % weight by weight.\n\nDIN DKE Spec 99100 chapter reference: 6.5.3-6.5.4" + }, + "samm:preferredName": { + "@language": "en", + "@value": "BatteryMaterials" + }, + "@type": "samm:Property" + }, + { + "@id": "urn:samm:io.BatteryPass.MaterialComposition:1.2.0#ImpactList", + "samm-c:elementCharacteristic": { + "@id": "samm-c:Text" + }, + "samm:dataType": { + "@id": "xsd:string" + }, + "@type": "samm-c:List" + }, + { + "@id": "urn:samm:io.BatteryPass.MaterialComposition:1.2.0#componentName", + "samm:exampleValue": "Anode", + "samm:characteristic": { + "@id": "samm-c:Text" + }, + "samm:preferredName": { + "@language": "en", + "@value": "Name" + }, + "@type": "samm:Property" + }, + { + "@id": "urn:samm:io.BatteryPass.MaterialComposition:1.2.0#hazardousSubstanceIdentifier", + "samm:characteristic": { + "@id": "urn:samm:io.BatteryPass.MaterialComposition:1.2.0#MaterialIdentifierTrait" + }, + "samm:see": { + "@id": "https://www.cas.org/cas-data/cas-registry" + }, + "samm:description": { + "@language": "en", + "@value": "CAS identifier of hazardous substance" + }, + "samm:preferredName": { + "@language": "en", + "@value": "Identifier" + }, + "@type": "samm:Property" + }, + { + "@id": "urn:samm:io.BatteryPass.MaterialComposition:1.2.0#batteryMaterialIdentifier", + "samm:exampleValue": "7439-93-2 ", + "samm:characteristic": { + "@id": "urn:samm:io.BatteryPass.MaterialComposition:1.2.0#MaterialIdentifierTrait" + }, + "samm:description": { + "@language": "en", + "@value": "CAS Number " + }, + "samm:preferredName": { + "@language": "en", + "@value": "Identifier" + }, + "@type": "samm:Property" + }, + { + "@id": "urn:samm:io.BatteryPass.MaterialComposition:1.2.0#BatteryMaterialList", + "samm:dataType": { + "@id": "urn:samm:io.BatteryPass.MaterialComposition:1.2.0#BatteryMaterialEntity" + }, + "samm:description": { + "@language": "en", + "@value": "Detailed composition, including materials used in the cathode, anode, and electrolyte\n\nEUBR: Annex XIII (2a)" + }, + "samm:preferredName": { + "@language": "en", + "@value": "Battery Material List" + }, + "@type": "samm-c:List" + }, + { + "@id": "urn:samm:io.BatteryPass.MaterialComposition:1.2.0#hazardousSubstanceConcentration", + "samm:characteristic": { + "@id": "urn:samm:io.BatteryPass.MaterialComposition:1.2.0#HazardousSubstanceConcentrationCharacteristic" + }, + "samm:description": { + "@language": "en", + "@value": "Concentration of hazardous substance" + }, + "samm:preferredName": { + "@language": "en", + "@value": "Concentration" + }, + "@type": "samm:Property" + }, + { + "@id": "urn:samm:io.BatteryPass.MaterialComposition:1.2.0#HazardousSubstanceConcentrationCharacteristic", + "samm-c:unit": { + "@id": "unit:percent" + }, + "samm:dataType": { + "@id": "xsd:double" + }, + "@type": "samm-c:Measurement" + }, + { + "@id": "urn:samm:io.BatteryPass.MaterialComposition:1.2.0#hazardousSubstanceImpact", + "samm:characteristic": { + "@id": "urn:samm:io.BatteryPass.MaterialComposition:1.2.0#ImpactList" + }, + "samm:description": { + "@language": "en", + "@value": "Impact statements based on, e.g., REACH or GHS for all hazard classes applicable to substances in the battery." + }, + "samm:preferredName": { + "@language": "en", + "@value": "Impact" + }, + "@type": "samm:Property" + }, + { + "@id": "urn:samm:io.BatteryPass.MaterialComposition:1.2.0#hazardousSubstanceName", + "samm:characteristic": { + "@id": "samm-c:Text" + }, + "samm:description": { + "@language": "en", + "@value": "Clear name of hazardous substance" + }, + "samm:preferredName": { + "@language": "en", + "@value": "Name" + }, + "@type": "samm:Property" + }, + { + "@id": "urn:samm:io.BatteryPass.MaterialComposition:1.2.0#BatteryLocationEntity", + "samm:properties": { + "@list": [ + { + "@id": "urn:samm:io.BatteryPass.MaterialComposition:1.2.0#componentName" + }, + { + "@id": "_:b11" + } + ] + }, + "samm:description": { + "@language": "en", + "@value": "Battery component that includes the material" + }, + "samm:preferredName": { + "@language": "en", + "@value": "BatteryLocation " + }, + "@type": "samm:Entity" + }, + { + "@id": "urn:samm:io.BatteryPass.MaterialComposition:1.2.0#shortName", + "samm:exampleValue": "NMC", + "samm:characteristic": { + "@id": "samm-c:Text" + }, + "@type": "samm:Property" + }, + { + "@id": "_:b11", + "samm:optional": { + "@value": "true", + "@type": "xsd:boolean" + }, + "samm:property": { + "@id": "urn:samm:io.BatteryPass.MaterialComposition:1.2.0#componentId" + } + }, + { + "@id": "urn:samm:io.BatteryPass.MaterialComposition:1.2.0#Weight", + "samm-c:unit": { + "@id": "unit:gram" + }, + "samm:dataType": { + "@id": "xsd:float" + }, + "@type": "samm-c:Measurement" + }, + { + "@id": "urn:samm:io.BatteryPass.MaterialComposition:1.2.0#BatteryMaterialEntity", + "samm:properties": { + "@list": [ + { + "@id": "urn:samm:io.BatteryPass.MaterialComposition:1.2.0#batteryMaterialLocation" + }, + { + "@id": "urn:samm:io.BatteryPass.MaterialComposition:1.2.0#batteryMaterialIdentifier" + }, + { + "@id": "urn:samm:io.BatteryPass.MaterialComposition:1.2.0#batteryMaterialName" + }, + { + "@id": "urn:samm:io.BatteryPass.MaterialComposition:1.2.0#batteryMaterialMass" + }, + { + "@id": "urn:samm:io.BatteryPass.MaterialComposition:1.2.0#isCriticalRawMaterial" + } + ] + }, + "samm:preferredName": { + "@language": "en", + "@value": "Material" + }, + "@type": "samm:Entity" + }, + { + "@id": "urn:samm:io.BatteryPass.MaterialComposition:1.2.0#batteryMaterialLocation", + "samm:characteristic": { + "@id": "urn:samm:io.BatteryPass.MaterialComposition:1.2.0#BatteryMaterialLocation" + }, + "samm:description": { + "@language": "en", + "@value": "Battery component that relates to the material" + }, + "samm:preferredName": { + "@language": "en", + "@value": "Location" + }, + "@type": "samm:Property" + }, + { + "@id": "urn:samm:io.BatteryPass.MaterialComposition:1.2.0#hazardousSubstanceClass", + "samm:characteristic": { + "@id": "urn:samm:io.BatteryPass.MaterialComposition:1.2.0#HazardousSubstanceClassChrateristicEnum" + }, + "samm:description": { + "@language": "en", + "@value": "Battery Regulation narrows reporting to substances falling under defined hazard classes and categories of the CLP regulation." + }, + "samm:preferredName": { + "@language": "en", + "@value": "Class" + }, + "@type": "samm:Property" + }, + { + "@id": "urn:samm:io.BatteryPass.MaterialComposition:1.2.0#batteryMaterialMass", + "samm:characteristic": { + "@id": "urn:samm:io.BatteryPass.MaterialComposition:1.2.0#Weight" + }, + "samm:description": { + "@language": "en", + "@value": "Weight of component material" + }, + "samm:preferredName": { + "@language": "en", + "@value": "Weight" + }, + "@type": "samm:Property" + }, + { + "@id": "urn:samm:io.BatteryPass.MaterialComposition:1.2.0#batteryChemistry", + "samm:characteristic": { + "@id": "urn:samm:io.BatteryPass.MaterialComposition:1.2.0#BatteryChemistryEntityList" + }, + "samm:description": { + "@language": "en", + "@value": "Composition of a product in general terms by specifying the cathode and anode active material as well as electrolyte.\n\nDIN DKE Spec 99100 chapter reference: 6.5.2" + }, + "samm:preferredName": { + "@language": "en", + "@value": "ProductChemistry" + }, + "@type": "samm:Property" + }, + { + "@id": "urn:samm:io.BatteryPass.MaterialComposition:1.2.0#BatteryChemistryEntityList", + "samm:dataType": { + "@id": "urn:samm:io.BatteryPass.MaterialComposition:1.2.0#BatteryChemistryEntity" + }, + "samm:description": { + "@language": "en", + "@value": "Detailed composition, including materials used in the cathode, anode, and electrolyte.\nAll common cells have two electrodes and an electrolyte. The specific combination of materials used to make these components is called \"chemistry.\" A cell's chemistry largely determines its properties, while most variations within it are caused by additives, purification, and design elements.\n\nEUBR: Annex XIII (1b) ? Annex VI Part A (7)" + }, + "samm:preferredName": { + "@language": "en", + "@value": "BatteryChemistryEntity" + }, + "@type": "samm-c:SingleEntity" + }, + { + "@id": "urn:samm:io.BatteryPass.MaterialComposition:1.2.0#BatteryMaterialLocation", + "samm:dataType": { + "@id": "urn:samm:io.BatteryPass.MaterialComposition:1.2.0#BatteryLocationEntity" + }, + "samm:preferredName": { + "@language": "en", + "@value": "Location" + }, + "@type": "samm-c:SingleEntity" + }, + { + "@id": "urn:samm:io.BatteryPass.MaterialComposition:1.2.0#componentId", + "samm:characteristic": { + "@id": "samm-c:Text" + }, + "samm:preferredName": { + "@language": "en", + "@value": "SubstanceId" + }, + "@type": "samm:Property" + }, + { + "@id": "urn:samm:io.BatteryPass.MaterialComposition:1.2.0#hazardousSubstances", + "samm:characteristic": { + "@id": "urn:samm:io.BatteryPass.MaterialComposition:1.2.0#HazardousSubstancesList" + }, + "samm:description": { + "@language": "en", + "@value": "\"Hazardous substances\" (No 20.a-e): Name (agreed substance nomenclature, e.g. IUPAC or chemical name) all hazardous substance (as ?any substance that poses a threat to human health and the environment?). Suggested above 0.1 % weight by weight within each (sub-)component.\n\nDIN DKE Spec 99100 chapter reference: 6.5.4 - 6.5.6" + }, + "samm:preferredName": { + "@language": "en", + "@value": "HazardousSubstances" + }, + "@type": "samm:Property" + }, + { + "@id": "urn:samm:io.BatteryPass.MaterialComposition:1.2.0#HazardousSubstancesList", + "samm:dataType": { + "@id": "urn:samm:io.BatteryPass.MaterialComposition:1.2.0#HazardousSubstanceEntity" + }, + "samm:description": { + "@language": "en", + "@value": "Hazardous substances contained in the battery other than mercury, cadmium or lead. Substance as a chemical element and its compounds in the natural state or the result of a manufacturing process (ECHA). Battery Regulation narrows reporting to substances falling under defined hazard classes and categories of the CLP regulation.\n\nEUBR: Annex XIII (1b) ? Annex VI Part A (8)" + }, + "samm:preferredName": { + "@language": "en", + "@value": "HazardousSubstances" + }, + "@type": "samm-c:List" + }, + { + "@id": "urn:samm:io.BatteryPass.MaterialComposition:1.2.0#BatteryChemistryEntity", + "samm:properties": { + "@list": [ + { + "@id": "urn:samm:io.BatteryPass.MaterialComposition:1.2.0#shortName" + }, + { + "@id": "urn:samm:io.BatteryPass.MaterialComposition:1.2.0#clearName" + } + ] + }, + "samm:preferredName": { + "@language": "en", + "@value": "Chemistry" + }, + "@type": "samm:Entity" + }, + { + "@id": "urn:samm:io.BatteryPass.MaterialComposition:1.2.0#batteryMaterialName", + "samm:exampleValue": "Lithium", + "samm:characteristic": { + "@id": "samm-c:Text" + }, + "samm:description": { + "@language": "en", + "@value": "Clear name of Material" + }, + "samm:preferredName": { + "@language": "en", + "@value": "Name" + }, + "@type": "samm:Property" + }, + { + "@id": "urn:samm:io.BatteryPass.MaterialComposition:1.2.0#MaterialComposition", + "samm:events": { + "@list": [] + }, + "samm:operations": { + "@list": [] + }, + "samm:properties": { + "@list": [ + { + "@id": "urn:samm:io.BatteryPass.MaterialComposition:1.2.0#batteryChemistry" + }, + { + "@id": "urn:samm:io.BatteryPass.MaterialComposition:1.2.0#batteryMaterials" + }, + { + "@id": "urn:samm:io.BatteryPass.MaterialComposition:1.2.0#hazardousSubstances" + } + ] + }, + "samm:description": { + "@language": "en", + "@value": "Mandatory data: Battery chemistry; critical raw materials; materials used in the cathode, anode, and \nelectrolyte; hazardous substances; impact of substances on the environment and on human health or \nsafety\n\nCopyright ? 2024 Circulor (for and on behalf of the Battery Pass Consortium). This work is li-censed under a Creative Commons License Attribution-NonCommercial 4.0 International (CC BY-NC 4.0). Readers may reproduce material for their own publications, as long as it is not sold com-mercially and is given appropriate attribution." + }, + "samm:preferredName": { + "@language": "en", + "@value": "MaterialComposition" + }, + "@type": "samm:Aspect" + }, + { + "@id": "urn:samm:io.BatteryPass.MaterialComposition:1.2.0#HazardousSubstanceEntity", + "samm:properties": { + "@list": [ + { + "@id": "urn:samm:io.BatteryPass.MaterialComposition:1.2.0#hazardousSubstanceClass" + }, + { + "@id": "urn:samm:io.BatteryPass.MaterialComposition:1.2.0#hazardousSubstanceName" + }, + { + "@id": "urn:samm:io.BatteryPass.MaterialComposition:1.2.0#hazardousSubstanceConcentration" + }, + { + "@id": "urn:samm:io.BatteryPass.MaterialComposition:1.2.0#hazardousSubstanceImpact" + }, + { + "@id": "urn:samm:io.BatteryPass.MaterialComposition:1.2.0#hazardousSubstanceLocation" + }, + { + "@id": "urn:samm:io.BatteryPass.MaterialComposition:1.2.0#hazardousSubstanceIdentifier" + } + ] + }, + "samm:description": { + "@language": "en", + "@value": "Hazardous substances (No. 19-23): Name (agreed substance nomenclature, e.g. IUPAC or chemical name) all hazardous substance (as ?any substance that poses a threat to human health and the environment?). Suggested above 0.1 % weight by weight within each (sub-)component." + }, + "@type": "samm:Entity" + } + ], + "@context": { + "samm-e": "urn:samm:org.eclipse.esmf.samm:entity:2.1.0#", + "unit": "urn:samm:org.eclipse.esmf.samm:unit:2.1.0#", + "samm-c": "urn:samm:org.eclipse.esmf.samm:characteristic:2.1.0#", + "rdf": "http://www.w3.org/1999/02/22-rdf-syntax-ns#", + "xsd": "http://www.w3.org/2001/XMLSchema#", + "rdfs": "http://www.w3.org/2000/01/rdf-schema#", + "samm": "urn:samm:org.eclipse.esmf.samm:meta-model:2.1.0#", + "@vocab": "urn:samm:io.BatteryPass.MaterialComposition:1.2.0#" + } +} diff --git a/tests/fixtures/samples/eclipse-tractusx_sldt-semantic-models_BatteryPass.json b/tests/fixtures/samples/eclipse-tractusx_sldt-semantic-models_BatteryPass.json new file mode 100644 index 0000000..10a875b --- /dev/null +++ b/tests/fixtures/samples/eclipse-tractusx_sldt-semantic-models_BatteryPass.json @@ -0,0 +1,643 @@ +{ + "characteristics": { + "physicalDimension": { + "length": { + "value": 20.0, + "unit": "unit:millimetre" + }, + "width": { + "value": 20.0, + "unit": "unit:millimetre" + }, + "weight": { + "value": 20.0, + "unit": "unit:gram" + }, + "height": { + "value": 20.0, + "unit": "unit:millimetre" + } + }, + "warranty": { + "lifeValue": 36, + "lifeUnit": "unit:day" + } + }, + "metadata": { + "backupReference": "https://dummy.link", + "registrationIdentifier": "https://dummy.link/ID8283746239078", + "economicOperatorId": "BPNL0123456789ZZ", + "lastModification": "2000-01-01", + "predecessor": "urn:uuid:00000000-0000-0000-0000-000000000000", + "issueDate": "2000-01-01", + "version": "1.0.0", + "passportIdentifier": "urn:uuid:550e8400-e29b-41d4-a716-446655440000", + "status": "draft", + "expirationDate": "2030-01-01" + }, + "commercial": { + "placedOnMarket": "2000-01-01", + "purpose": [ + "automotive" + ] + }, + "identification": { + "chemistry": "Nickel Cobalt Manganese (NCM)", + "idDmc": "34567890", + "identification": { + "batch": [ + { + "value": "BID12345678", + "key": "batchId" + } + ], + "codes": [ + { + "value": "8703 24 10 00", + "key": "TARIC" + } + ], + "type": { + "manufacturerPartId": "123-0.740-3434-A", + "nameAtManufacturer": "Mirror left" + }, + "classification": [ + { + "classificationStandard": "GIN 20510-21513", + "classificationID": "1004712", + "classificationDescription": "Generic standard for classification of parts in the automotive industry." + } + ], + "serial": [ + { + "value": "SN12345678", + "key": "partInstanceId" + } + ], + "dataCarrier": { + "carrierType": "QR", + "carrierLayout": "upper-left side" + } + }, + "category": "SLI" + }, + "performance": { + "rated": { + "roundTripEfficiency": { + "depthOfDischarge": 90.5, + "temperature": 20.0, + "50PercentLife": 89.0, + "initial": 96.0 + }, + "selfDischargingRate": 0.25, + "performanceDocument": [ + { + "contentType": "URL", + "header": "Example Document XYZ", + "content": "https://dummy.link" + } + ], + "testReport": [ + { + "contentType": "URL", + "header": "Example Document XYZ", + "content": "https://dummy.link" + } + ], + "temperature": { + "lower": -18.0, + "upper": 60.0 + }, + "lifetime": { + "report": [ + { + "contentType": "URL", + "header": "Example Document XYZ", + "content": "https://dummy.link" + } + ], + "cycleLifeTesting": { + "temperature": 20.0, + "depthOfDischarge": 90.5, + "appliedDischargeRate": 4.0, + "cycles": 1500, + "appliedChargeRate": 3.0 + }, + "expectedYears": 8 + }, + "power": { + "at20SoC": 35000.0, + "temperature": 20.0, + "value": 40000.0, + "at80SoC": 39000.0 + }, + "resistance": { + "temperature": 20.0, + "cell": 0.025, + "pack": 0.55, + "module": 0.2 + }, + "voltage": { + "temperature": 20.0, + "min": 2.5, + "nominal": 3.7, + "max": 4.2 + }, + "energy": { + "temperature": 20.0, + "value": 0.5 + }, + "capacity": { + "temperature": 20.0, + "value": 4.0, + "thresholdExhaustion": 80.0 + } + }, + "dynamic": { + "selfDischargingRate": 0.25, + "roundTripEfficiency": { + "remaining": { + "value": 50.0, + "time": "2023-12-07T10:39:13.576+01:00" + }, + "fade": { + "value": 50.0, + "time": "2023-12-07T10:39:13.576+01:00" + } + }, + "operatingEnvironment": [ + { + "contentType": "URL", + "header": "Example Document XYZ", + "content": "https://dummy.link" + } + ], + "stateOfCharge": { + "value": 50.0, + "time": "2023-12-07T10:39:13.576+01:00" + }, + "performanceDocument": [ + { + "contentType": "URL", + "header": "Example Document XYZ", + "content": "https://dummy.link" + } + ], + "fullCycles": { + "value": 1500, + "time": "2023-12-07T10:39:13.576+01:00" + }, + "power": { + "remaining": { + "value": 40000.0, + "time": "2023-12-07T10:39:13.576+01:00" + }, + "fade": { + "value": 50.0, + "time": "2023-12-07T10:39:13.576+01:00" + } + }, + "negativeEvents": [ + { + "contentType": "URL", + "header": "Example Document XYZ", + "content": "https://dummy.link" + } + ], + "resistance": { + "increase": { + "cell": { + "value": 50.0, + "time": "2023-12-07T10:39:13.576+01:00" + }, + "pack": { + "value": 50.0, + "time": "2023-12-07T10:39:13.576+01:00" + }, + "module": { + "value": 50.0, + "time": "2023-12-07T10:39:13.576+01:00" + } + }, + "remaining": { + "cell": { + "value": 0.3, + "time": "2023-12-07T10:39:13.576+01:00" + }, + "pack": { + "value": 0.3, + "time": "2023-12-07T10:39:13.576+01:00" + }, + "module": { + "value": 0.3, + "time": "2023-12-07T10:39:13.576+01:00" + } + } + }, + "capacity": { + "fade": { + "value": 50.0, + "time": "2023-12-07T10:39:13.576+01:00" + }, + "throughput": { + "value": 4.0, + "time": "2023-12-07T10:39:13.576+01:00" + }, + "capacity": { + "value": 4.0, + "time": "2023-12-07T10:39:13.576+01:00" + } + }, + "energy": { + "remaining": { + "value": 0.5, + "time": "2023-12-07T10:39:13.576+01:00" + }, + "soce": { + "value": 50.0, + "time": "2023-12-07T10:39:13.576+01:00" + }, + "throughput": { + "value": 0.5, + "time": "2023-12-07T10:39:13.576+01:00" + } + } + } + }, + "sources": [ + { + "header": "Example Document XYZ", + "category": "Product Specifications", + "type": "URL", + "content": "https://dummy.link" + } + ], + "materials": { + "hazardous": { + "cadmium": { + "concentration": 5.3, + "location": "Housing", + "critical": true, + "impactOfSubstances": [ + { + "contentType": "URL", + "header": "Example Document XYZ", + "content": "https://dummy.link" + } + ], + "materialUnit": "unit:partPerMillion", + "documentation": [ + { + "contentType": "URL", + "header": "Example Document XYZ", + "content": "https://dummy.link" + } + ] + }, + "other": [ + { + "critical": true, + "impactOfSubstances": [ + { + "contentType": "URL", + "header": "Example Document XYZ", + "content": "https://dummy.link" + } + ], + "documentation": [ + { + "contentType": "URL", + "header": "Example Document XYZ", + "content": "https://dummy.link" + } + ], + "concentration": 5.3, + "materialIdentification": [ + { + "type": "CAS", + "name": "phenolphthalein", + "id": "201-004-7" + } + ], + "location": "Housing", + "materialUnit": "unit:partPerMillion" + } + ], + "mercury": { + "concentration": 5.3, + "location": "Housing", + "critical": true, + "impactOfSubstances": [ + { + "contentType": "URL", + "header": "Example Document XYZ", + "content": "https://dummy.link" + } + ], + "materialUnit": "unit:partPerMillion", + "documentation": [ + { + "contentType": "URL", + "header": "Example Document XYZ", + "content": "https://dummy.link" + } + ] + }, + "lead": { + "recycled": 12.5, + "critical": true, + "impactOfSubstances": [ + { + "contentType": "URL", + "header": "Example Document XYZ", + "content": "https://dummy.link" + } + ], + "documentation": [ + { + "contentType": "URL", + "header": "Example Document XYZ", + "content": "https://dummy.link" + } + ], + "location": "Housing", + "concentration": 5.3, + "materialUnit": "unit:partPerMillion" + } + }, + "active": { + "nickel": { + "location": "Housing", + "recycled": 12.5, + "critical": true, + "documentation": [ + { + "contentType": "URL", + "header": "Example Document XYZ", + "content": "https://dummy.link" + } + ] + }, + "lithium": { + "location": "Housing", + "recycled": 12.5, + "critical": true, + "documentation": [ + { + "contentType": "URL", + "header": "Example Document XYZ", + "content": "https://dummy.link" + } + ] + }, + "cobalt": { + "location": "Housing", + "recycled": 12.5, + "critical": true, + "documentation": [ + { + "contentType": "URL", + "header": "Example Document XYZ", + "content": "https://dummy.link" + } + ] + }, + "other": [ + { + "location": "Housing", + "materialIdentification": [ + { + "type": "CAS", + "name": "phenolphthalein", + "id": "201-004-7" + } + ], + "recycled": 12.5, + "critical": true, + "documentation": [ + { + "contentType": "URL", + "header": "Example Document XYZ", + "content": "https://dummy.link" + } + ] + } + ], + "lead": { + "recycled": 12.5, + "critical": true, + "impactOfSubstances": [ + { + "contentType": "URL", + "header": "Example Document XYZ", + "content": "https://dummy.link" + } + ], + "documentation": [ + { + "contentType": "URL", + "header": "Example Document XYZ", + "content": "https://dummy.link" + } + ], + "location": "Housing", + "concentration": 5.3, + "materialUnit": "unit:partPerMillion" + } + }, + "composition": [ + { + "unit": "unit:partPerMillion", + "recycled": 12.5, + "critical": true, + "renewable": 23.5, + "documentation": [ + { + "contentType": "URL", + "header": "Example Document XYZ", + "content": "https://dummy.link" + } + ], + "concentration": 5.3, + "location": "Housing", + "id": [ + { + "type": "CAS", + "name": "phenolphthalein", + "id": "201-004-7" + } + ] + } + ] + }, + "safety": { + "usableExtinguishAgent": [ + { + "fireClass": "A, B", + "document": [ + { + "contentType": "URL", + "header": "Example Document XYZ", + "content": "https://dummy.link" + } + ], + "media": "Dry Powder" + } + ], + "safeDischarging": [ + { + "contentType": "URL", + "header": "Example Document XYZ", + "content": "https://dummy.link" + } + ], + "meaningOfLabels": [ + { + "contentType": "URL", + "header": "Example Document XYZ", + "content": "https://dummy.link" + } + ], + "dismantling": [ + { + "contentType": "URL", + "header": "Example Document XYZ", + "content": "https://dummy.link" + } + ], + "removalFromAppliance": [ + { + "contentType": "URL", + "header": "Example Document XYZ", + "content": "https://dummy.link" + } + ], + "safetyMeasures": [ + { + "contentType": "URL", + "header": "Example Document XYZ", + "content": "https://dummy.link" + } + ] + }, + "handling": { + "applicable": true, + "content": { + "producer": [ + { + "id": "BPNL0123456789ZZ" + } + ], + "sparePart": [ + { + "manufacturerPartId": "123-0.740-3434-A", + "nameAtManufacturer": "Mirror left" + } + ] + } + }, + "conformity": { + "declarationOfConformityId": "0978234-34567890-01", + "thirdPartyAssurance": [ + { + "contentType": "URL", + "header": "Example Document XYZ", + "content": "https://dummy.link" + } + ], + "resultOfTestReport": [ + { + "contentType": "URL", + "header": "Example Document XYZ", + "content": "https://dummy.link" + } + ], + "declarationOfConformity": [ + { + "contentType": "URL", + "header": "Example Document XYZ", + "content": "https://dummy.link" + } + ], + "dueDiligencePolicy": [ + { + "contentType": "URL", + "header": "Example Document XYZ", + "content": "https://dummy.link" + } + ] + }, + "operation": { + "intoServiceDate": "9494-37-15", + "manufacturer": { + "facility": [ + { + "facility": "BPNA1234567890AA" + } + ], + "manufacturingDate": "2000-01-31", + "manufacturer": "BPNLG7OCVQYDXMzJ" + } + }, + "sustainability": { + "documents": { + "separateCollection": [ + { + "contentType": "URL", + "header": "Example Document XYZ", + "content": "https://dummy.link" + } + ], + "sustainabilityReport": [ + { + "contentType": "URL", + "header": "Example Document XYZ", + "content": "https://dummy.link" + } + ], + "euTaxonomyDisclosureStatement": [ + { + "contentType": "URL", + "header": "Example Document XYZ", + "content": "https://dummy.link" + } + ], + "wastePrevention": [ + { + "contentType": "URL", + "header": "Example Document XYZ", + "content": "https://dummy.link" + } + ] + }, + "carbonFootprint": [ + { + "lifecycle": "main product production", + "rulebook": [ + { + "contentType": "URL", + "header": "Example Document XYZ", + "content": "https://dummy.link" + } + ], + "unit": "kg CO2 / kWh", + "performanceClass": "A", + "manufacturingPlant": [ + { + "facility": "BPNA1234567890AA" + } + ], + "type": "Climate Change Total", + "value": 12.678, + "declaration": [ + { + "contentType": "URL", + "header": "Example Document XYZ", + "content": "https://dummy.link" + } + ] + } + ], + "status": "original" + } +} diff --git a/tests/fixtures/samples/nfc-forum_org_long-dpp-example.json b/tests/fixtures/samples/nfc-forum_org_long-dpp-example.json new file mode 100644 index 0000000..3a5e447 --- /dev/null +++ b/tests/fixtures/samples/nfc-forum_org_long-dpp-example.json @@ -0,0 +1,43 @@ +{ + "productID": "12345-67890", + "productName": "Eco-Friendly Water Bottle", + "manufacturer": { + "name": "Green Products Ltd.", + "address": "123 Green Way, Sustainability City, EC1 2AB, Country" + }, + "productionDate": "2023-05-20", + "expiryDate": "2026-05-20", + "materials": [ + { + "materialID": "M-001", + "materialName": "Recycled PET", + "percentage": 80 + }, + { + "materialID": "M-002", + "materialName": "Stainless Steel", + "percentage": 20 + } + ], + "environmentalImpact": { + "carbonFootprint": "2.5 kg CO2", + "waterUsage": "10 liters", + "recyclability": "95%" + }, + "compliance": [ + { + "standard": "ISO 14001", + "certificationDate": "2023-06-15" + }, + { + "standard": "ISO 45001", + "certificationDate": "2023-07-01" + } + ], + "endOfLifeInstructions": { + "recycling": "Place in plastic recycling bin", + "disposal": "Dispose of at designated recycling center", + "reuse": "Refill and reuse as water bottle" + }, + "digitalPassportLink": "https/nfc-forum.org/ndpp/12345-67890.json" +} diff --git a/tests/fixtures/samples/opensource_unicc_org_untp-digital-facility-record-v0.3.9.json b/tests/fixtures/samples/opensource_unicc_org_untp-digital-facility-record-v0.3.9.json new file mode 100644 index 0000000..2b47756 --- /dev/null +++ b/tests/fixtures/samples/opensource_unicc_org_untp-digital-facility-record-v0.3.9.json @@ -0,0 +1,462 @@ +{ + "type": [ + "DigitalFacilityRecord", + "VerifiableCredential" + ], + "@context": [ + "https://www.w3.org/ns/credentials/v2", + "https://vocabulary.uncefact.org/untp/dfr/0.3.9" + ], + "id": "https://example-company.com/credentials/2a423366-a0d6-4855-ba65-2e0c926d09b0", + "issuer": { + "type": [ + "CredentialIssuer" + ], + "id": "did:web:identifiers.example-company.com:12345", + "name": "Example Company Pty Ltd", + "otherIdentifiers": [ + { + "type": [ + "Entity" + ], + "id": "https://id.gs1.org/01/09520123456788/21/12345", + "name": "EV battery 300Ah.", + "registeredId": "90664869327", + "idScheme": { + "type": [ + "IdentifierScheme" + ], + "id": "https://id.gs1.org/01/", + "name": "Global Trade Identification Number (GTIN)" + } + }, + { + "type": [ + "Entity" + ], + "id": "https://id.gs1.org/01/09520123456788/21/12345", + "name": "EV battery 300Ah.", + "registeredId": "90664869327", + "idScheme": { + "type": [ + "IdentifierScheme" + ], + "id": "https://id.gs1.org/01/", + "name": "Global Trade Identification Number (GTIN)" + } + } + ] + }, + "validFrom": 2024, + "validUntil": 2034, + "credentialSubject": { + "type": [ + "Facility", + "Entity" + ], + "id": "https://id.gs1.org/gln/0614141123452", + "registeredId": "614141123452", + "description": "LiFePO4 Battery plant number 7", + "name": "Example facility 7", + "idScheme": { + "type": [ + "IdentifierScheme" + ], + "id": "https://id.gs1.org/01/", + "name": "Global Trade Identification Number (GTIN)" + }, + "countryOfOperation": "AU", + "processCategories": [ + { + "type": [ + "Classification" + ], + "id": "https://unstats.un.org/unsd/classifications/Econ/cpc/46410", + "code": "46410", + "name": "Primary cells and primary batteries", + "schemeID": "https://unstats.un.org/unsd/classifications/Econ/cpc/", + "schemeName": "UN Central Product Classification (CPC)" + }, + { + "type": [ + "Classification" + ], + "id": "https://unstats.un.org/unsd/classifications/Econ/cpc/46410", + "code": "46410", + "name": "Primary cells and primary batteries", + "schemeID": "https://unstats.un.org/unsd/classifications/Econ/cpc/", + "schemeName": "UN Central Product Classification (CPC)" + } + ], + "operatedByParty": { + "type": [ + "Entity" + ], + "id": "https://id.gs1.org/01/09520123456788/21/12345", + "name": "EV battery 300Ah.", + "registeredId": "90664869327", + "idScheme": { + "type": [ + "IdentifierScheme" + ], + "id": "https://id.gs1.org/01/", + "name": "Global Trade Identification Number (GTIN)" + } + }, + "otherIdentifiers": [ + { + "type": [ + "Entity" + ], + "id": "https://id.gs1.org/01/09520123456788/21/12345", + "name": "EV battery 300Ah.", + "registeredId": "90664869327", + "idScheme": { + "type": [ + "IdentifierScheme" + ], + "id": "https://id.gs1.org/01/", + "name": "Global Trade Identification Number (GTIN)" + } + }, + { + "type": [ + "Entity" + ], + "id": "https://id.gs1.org/01/09520123456788/21/12345", + "name": "EV battery 300Ah.", + "registeredId": "90664869327", + "idScheme": { + "type": [ + "IdentifierScheme" + ], + "id": "https://id.gs1.org/01/", + "name": "Global Trade Identification Number (GTIN)" + } + } + ], + "address": { + "streetAddress": "level 11, 15 London Circuit", + "postalCode": "2601", + "addressLocality": "Acton", + "addressRegion": "ACT", + "addressCountry": "AU" + }, + "locationInformation": { + "geoLocation": { + "type": "Point", + "coordinates": { + "data": [ + 3.141579, + 3.141579 + ] + } + }, + "geoBoundary": { + "type": "Polygon", + "coordinates": [ + { + "data": [ + { + "data": [ + 3.141579, + 3.141579 + ] + }, + { + "data": [ + 3.141579, + 3.141579 + ] + } + ] + }, + { + "data": [ + { + "data": [ + 3.141579, + 3.141579 + ] + }, + { + "data": [ + 3.141579, + 3.141579 + ] + } + ] + } + ] + } + }, + "conformityDeclarations": [ + { + "type": [ + "Declaration" + ], + "id": "https://jargon.sh", + "referenceStandard": { + "type": [ + "Standard" + ], + "id": "https://www.globalbattery.org/media/publications/gba-rulebook-v2.0-master.pdf", + "name": "GBA Battery Passport Greenhouse Gas Rulebook - V.2.0", + "issuingParty": { + "type": [ + "Entity" + ], + "id": "https://id.gs1.org/01/09520123456788/21/12345", + "name": "EV battery 300Ah.", + "registeredId": "90664869327", + "idScheme": { + "type": [ + "IdentifierScheme" + ], + "id": "https://id.gs1.org/01/", + "name": "Global Trade Identification Number (GTIN)" + } + }, + "issueDate": 2023 + }, + "referenceRegulation": { + "type": [ + "Regulation" + ], + "id": "https://www.legislation.gov.au/F2008L02309/latest/versions", + "name": "NNational Greenhouse and Energy Reporting (Measurement) Determination", + "jurisdictionCountry": "Enumeration Value", + "administeredBy": { + "type": [ + "Entity" + ], + "id": "https://id.gs1.org/01/09520123456788/21/12345", + "name": "EV battery 300Ah.", + "registeredId": "90664869327", + "idScheme": { + "type": [ + "IdentifierScheme" + ], + "id": "https://id.gs1.org/01/", + "name": "Global Trade Identification Number (GTIN)" + } + }, + "effectiveDate": 2024 + }, + "assessmentCriteria": [ + { + "type": [ + "Criterion" + ], + "id": "https://www.globalbattery.org/media/publications/gba-rulebook-v2.0-master.pdf#BatteryAssembly", + "name": "GBA Battery rule book v2.0 battery assembly guidelines.", + "thresholdValues": [ + { + "metricName": "GHG emissions intensity", + "metricValue": { + "value": 10, + "unit": "KGM" + }, + "accuracy": 0.05 + }, + { + "metricName": "GHG emissions intensity", + "metricValue": { + "value": 10, + "unit": "KGM" + }, + "accuracy": 0.05 + } + ] + }, + { + "type": [ + "Criterion" + ], + "id": "https://www.globalbattery.org/media/publications/gba-rulebook-v2.0-master.pdf#BatteryAssembly", + "name": "GBA Battery rule book v2.0 battery assembly guidelines.", + "thresholdValues": [ + { + "metricName": "GHG emissions intensity", + "metricValue": { + "value": 10, + "unit": "KGM" + }, + "accuracy": 0.05 + }, + { + "metricName": "GHG emissions intensity", + "metricValue": { + "value": 10, + "unit": "KGM" + }, + "accuracy": 0.05 + } + ] + } + ], + "declaredValues": [ + { + "metricName": "GHG emissions intensity", + "metricValue": { + "value": 10, + "unit": "KGM" + }, + "accuracy": 0.05 + }, + { + "metricName": "GHG emissions intensity", + "metricValue": { + "value": 10, + "unit": "KGM" + }, + "accuracy": 0.05 + } + ], + "compliance": true, + "conformityTopic": "environment.energy", + "conformityEvidence": { + "linkURL": "https://files.example-certifier.com/1234567.json", + "linkName": "GBA rule book conformity certificate", + "linkType": "https://test.uncefact.org/vocabulary/linkTypes/dcc", + "hashDigest": "6239119", + "hashMethod": "SHA-256", + "encryptionMethod": "AES" + } + }, + { + "type": [ + "Declaration" + ], + "id": "https://jargon.sh", + "referenceStandard": { + "type": [ + "Standard" + ], + "id": "https://www.globalbattery.org/media/publications/gba-rulebook-v2.0-master.pdf", + "name": "GBA Battery Passport Greenhouse Gas Rulebook - V.2.0", + "issuingParty": { + "type": [ + "Entity" + ], + "id": "https://id.gs1.org/01/09520123456788/21/12345", + "name": "EV battery 300Ah.", + "registeredId": "90664869327", + "idScheme": { + "type": [ + "IdentifierScheme" + ], + "id": "https://id.gs1.org/01/", + "name": "Global Trade Identification Number (GTIN)" + } + }, + "issueDate": 2023 + }, + "referenceRegulation": { + "type": [ + "Regulation" + ], + "id": "https://www.legislation.gov.au/F2008L02309/latest/versions", + "name": "NNational Greenhouse and Energy Reporting (Measurement) Determination", + "jurisdictionCountry": "Enumeration Value", + "administeredBy": { + "type": [ + "Entity" + ], + "id": "https://id.gs1.org/01/09520123456788/21/12345", + "name": "EV battery 300Ah.", + "registeredId": "90664869327", + "idScheme": { + "type": [ + "IdentifierScheme" + ], + "id": "https://id.gs1.org/01/", + "name": "Global Trade Identification Number (GTIN)" + } + }, + "effectiveDate": 2024 + }, + "assessmentCriteria": [ + { + "type": [ + "Criterion" + ], + "id": "https://www.globalbattery.org/media/publications/gba-rulebook-v2.0-master.pdf#BatteryAssembly", + "name": "GBA Battery rule book v2.0 battery assembly guidelines.", + "thresholdValues": [ + { + "metricName": "GHG emissions intensity", + "metricValue": { + "value": 10, + "unit": "KGM" + }, + "accuracy": 0.05 + }, + { + "metricName": "GHG emissions intensity", + "metricValue": { + "value": 10, + "unit": "KGM" + }, + "accuracy": 0.05 + } + ] + }, + { + "type": [ + "Criterion" + ], + "id": "https://www.globalbattery.org/media/publications/gba-rulebook-v2.0-master.pdf#BatteryAssembly", + "name": "GBA Battery rule book v2.0 battery assembly guidelines.", + "thresholdValues": [ + { + "metricName": "GHG emissions intensity", + "metricValue": { + "value": 10, + "unit": "KGM" + }, + "accuracy": 0.05 + }, + { + "metricName": "GHG emissions intensity", + "metricValue": { + "value": 10, + "unit": "KGM" + }, + "accuracy": 0.05 + } + ] + } + ], + "declaredValues": [ + { + "metricName": "GHG emissions intensity", + "metricValue": { + "value": 10, + "unit": "KGM" + }, + "accuracy": 0.05 + }, + { + "metricName": "GHG emissions intensity", + "metricValue": { + "value": 10, + "unit": "KGM" + }, + "accuracy": 0.05 + } + ], + "compliance": true, + "conformityTopic": "environment.energy", + "conformityEvidence": { + "linkURL": "https://files.example-certifier.com/1234567.json", + "linkName": "GBA rule book conformity certificate", + "linkType": "https://test.uncefact.org/vocabulary/linkTypes/dcc", + "hashDigest": "6239119", + "hashMethod": "SHA-256", + "encryptionMethod": "AES" + } + } + ] + } +} diff --git a/tests/fixtures/samples/opensource_unicc_org_untp-digital-product-passport-v0.3.10.json b/tests/fixtures/samples/opensource_unicc_org_untp-digital-product-passport-v0.3.10.json new file mode 100644 index 0000000..405df56 --- /dev/null +++ b/tests/fixtures/samples/opensource_unicc_org_untp-digital-product-passport-v0.3.10.json @@ -0,0 +1,389 @@ +{ + "type": [ + "DigitalProductPassport", + "VerifiableCredential" + ], + "@context": [ + "https://www.w3.org/ns/credentials/v2", + "https://test.uncefact.org/vocabulary/untp/dpp/0.3.10/" + ], + "id": "https://example-company.com/credentials/2a423366-a0d6-4855-ba65-2e0c926d09b0", + "issuer": { + "type": [ + "CredentialIssuer" + ], + "id": "did:web:identifiers.example-company.com:12345", + "name": "Example Company Pty Ltd", + "otherIdentifiers": [ + { + "type": [ + "Entity" + ], + "id": "https://business.gov.au/ABN/View?abn=1234567890", + "name": "Sample Company Pty Ltd", + "registeredId": "1234567890", + "idScheme": { + "type": [ + "IdentifierScheme" + ], + "id": "https://business.gov.au/ABN/", + "name": "Australian Business Number" + } + } + ] + }, + "validFrom": "2024-03-15T12:00:00", + "validUntil": "2034-03-15T12:00:00", + "credentialSubject": { + "type": [ + "Product", + "Entity" + ], + "id": "https://id.gs1.org/01/09520123456788/21/12345", + "name": "EV battery 300Ah.", + "registeredId": "90664869327", + "idScheme": { + "type": [ + "IdentifierScheme" + ], + "id": "https://id.gs1.org/01/", + "name": "Global Trade Identification Number (GTIN)" + }, + "serialNumber": "12345678", + "batchNumber": "6789", + "productImage": { + "linkURL": "https://files.example-company.com/123456789.jpg", + "linkName": "EV battery 300Ah", + "linkType": "https://www.gs1.org/voc/relatedImage" + }, + "description": "400Ah 24v LiFePO4 battery", + "productCategory": [ + { + "type": [ + "Classification" + ], + "id": "https://unstats.un.org/unsd/classifications/Econ/cpc/46410", + "code": "46410", + "name": "Primary cells and primary batteries", + "schemeID": "https://unstats.un.org/unsd/classifications/Econ/cpc/", + "schemeName": "UN Central Product Classification (CPC)" + } + ], + "furtherInformation": [ + { + "linkURL": "https://files.example-company.com/products/90664869327/index.html", + "linkName": "Product Information page", + "linkType": "https://www.gs1.org/voc/sustainabilityInfo" + } + ], + "producedByParty": { + "type": [ + "Entity" + ], + "id": "https://business.gov.au/ABN/View?abn=1234567890", + "name": "Sample Company Pty Ltd", + "registeredId": "1234567890", + "idScheme": { + "type": [ + "IdentifierScheme" + ], + "id": "https://business.gov.au/ABN/", + "name": "Australian Business Number" + } + }, + "producedAtFacility": { + "type": [ + "Entity" + ], + "id": "https://maps.app.goo.gl/QBF7Xy4S9QjHJrzb7", + "name": "Sample EV battery manufacturing site", + "registeredId": "QBF7Xy4S9QjHJrzb7", + "idScheme": { + "type": [ + "IdentifierScheme" + ], + "id": "hhttps://maps.app.goo.gl", + "name": "Google Pin" + } + }, + "dimensions": { + "weight": { + "value": 20, + "unit": "KGM" + }, + "length": { + "value": 1, + "unit": "MTR" + }, + "width": { + "value": 0.5, + "unit": "MTR" + }, + "height": { + "value": 0.3, + "unit": "MTR" + }, + "volume": { + "value": 0.15, + "unit": "MTQ" + } + }, + "productionDate": "2024-04-25", + "countryOfProduction": "AU", + "materialsProvenance": [ + { + "name": "Lithium Spodumene", + "originCountry": "AU", + "materialType": { + "type": [ + "Classification" + ], + "id": "https://unstats.un.org/unsd/classifications/Econ/cpc/14290", + "code": "46410", + "name": "Other non-ferrous metal ores and concentrates (other than uranium or thorium ores and concentrates)", + "schemeID": "https://unstats.un.org/unsd/classifications/Econ/cpc/", + "schemeName": "UN Central Product Classification (CPC)" + }, + "massFraction": 0.2, + "recycledAmount": 0.5, + "hazardous": true, + "materialSafetyInformation": { + "linkURL": "https://sampleLithiumCompany.com/msds/1234567.json", + "linkName": "Lithium safe handling instructions", + "linkType": "https://www.gs1.org/voc/safetyInfo" + } + }, + { + "name": "Copper Concentrate", + "originCountry": "CA", + "materialType": { + "type": [ + "Classification" + ], + "id": "https://unstats.un.org/unsd/classifications/Econ/cpc/14210", + "code": "14210", + "name": "Copper, ores and concentrates", + "schemeID": "https://unstats.un.org/unsd/classifications/Econ/cpc/", + "schemeName": "UN Central Product Classification (CPC)" + }, + "massFraction": 0.2, + "recycledAmount": 0.5, + "hazardous": false + } + ], + "conformityDeclarations": [ + { + "type": [ + "Declaration" + ], + "id": "https://files.example-company.com/declarations/90664869327/", + "referenceStandard": { + "type": [ + "Standard" + ], + "id": "https://www.globalbattery.org/media/publications/gba-rulebook-v2.0-master.pdf", + "name": "GBA Battery Passport Greenhouse Gas Rulebook - V.2.0", + "issuingParty": { + "type": [ + "Entity" + ], + "id": "https://kbopub.economie.fgov.be/kbopub/toonondernemingps.html?ondernemingsnummer=786222414", + "name": "Global Battery Alliance", + "registeredId": "786222414", + "idScheme": { + "type": [ + "IdentifierScheme" + ], + "id": "https://kbopub.economie.fgov.be/", + "name": "Belgian business register" + } + }, + "issueDate": "2023-12-05" + }, + "referenceRegulation": { + "type": [ + "Regulation" + ], + "id": "https://www.legislation.gov.au/F2008L02309/latest/versions", + "name": "National Greenhouse and Energy Reporting (Measurement) Determination", + "jurisdictionCountry": "Enumeration Value", + "administeredBy": { + "type": [ + "Entity" + ], + "id": "https://abr.business.gov.au/ABN/View?abn=72321984210", + "name": "Clean Energy Regulator", + "registeredId": "72321984210", + "idScheme": { + "type": [ + "IdentifierScheme" + ], + "id": "https://abr.business.gov.au/ABN/", + "name": "Australian Business Number" + } + }, + "effectiveDate": "2024-03-20" + }, + "assessmentCriteria": [ + { + "type": [ + "Criterion" + ], + "id": "https://www.globalbattery.org/media/publications/gba-rulebook-v2.0-master.pdf#BatteryAssembly", + "name": "GBA Battery rule book v2.0 battery assembly guidelines.", + "thresholdValues": [ + { + "metricName": "Industry Average emissions intensity", + "metricValue": { + "value": 1.8, + "unit": "NIL" + } + } + ] + } + ], + "declaredValues": [ + { + "metricName": "GHG emissions intensity", + "metricValue": { + "value": 1.5, + "unit": "NIL" + }, + "accuracy": 0.05 + }, + { + "metricName": "GHG emissions footprint", + "metricValue": { + "value": 15, + "unit": "KGM" + }, + "accuracy": 0.05 + } + ], + "compliance": true, + "conformityTopic": "environment.emissions", + "conformityEvidence": { + "linkURL": "https://files.example-certifier.com/1234567.json", + "linkName": "GBA rule book conformity certificate", + "linkType": "https://test.uncefact.org/vocabulary/linkTypes/dcc", + "hashDigest": "6239119", + "hashMethod": "SHA-256", + "encryptionMethod": "AES" + } + }, + { + "type": [ + "Declaration" + ], + "id": "https://files.example-company.com/declarations/906648677543/", + "referenceRegulation": { + "type": [ + "Regulation" + ], + "id": "https://www.legislation.gov.au/C2009A00028/2021-09-11/text", + "name": "Fair work act 2009", + "jurisdictionCountry": "AU", + "administeredBy": { + "type": [ + "Entity" + ], + "id": "https://abr.business.gov.au/ABN/View?abn=96584957427", + "name": "Department of Employment and Workplace Relations", + "registeredId": "96584957427", + "idScheme": { + "type": [ + "IdentifierScheme" + ], + "id": "https://business.gov.au/ABN/", + "name": "Australian Business Number" + } + }, + "effectiveDate": "2024-03-20" + }, + "assessmentCriteria": [ + { + "type": [ + "Criterion" + ], + "id": "https://www.legislation.gov.au/C2009A00028/2021-09-11/text", + "name": "National minimum wage orders", + "thresholdValues": [ + { + "metricName": "Minimum wage", + "metricValue": { + "value": 25, + "unit": "AUD" + } + } + ] + } + ], + "compliance": true, + "conformityTopic": "social.labour" + } + ], + "circularityScorecard": { + "recyclingInformation": { + "linkURL": "https://files.example-company.com/products/123456789/recycling.pdf", + "linkName": "Recycling instructions", + "linkType": "https://www.gs1.org/voc/recyclingAndRepairInfo" + }, + "repairInformation": { + "linkURL": "https://files.example-company.com/products/123456789/repair.pdf", + "linkName": "Repair instructions", + "linkType": "https://www.gs1.org/voc/recyclingAndRepairInfo" + }, + "recyclableContent": 0.5, + "recyecledContent": 0.3, + "utilityFactor": 1.2, + "materialCircularityIndicator": 0.67 + }, + "emissionsScorecard": { + "carbonFootprint": 1.8, + "declaredUnit": "KGM", + "operationalScope": "CradleToGate", + "primarySourcedRatio": 0.3, + "reportingStandard": { + "type": [ + "Standard" + ], + "id": "https://www.globalbattery.org/media/publications/gba-rulebook-v2.0-master.pdf", + "name": "GBA Battery Passport Greenhouse Gas Rulebook - V.2.0", + "issuingParty": { + "type": [ + "Entity" + ], + "id": "https://kbopub.economie.fgov.be/kbopub/toonondernemingps.html?ondernemingsnummer=786222414", + "name": "Global Battery Alliance", + "registeredId": "786222414", + "idScheme": { + "type": [ + "IdentifierScheme" + ], + "id": "https://kbopub.economie.fgov.be/", + "name": "Belgian business register" + } + }, + "issueDate": "2023-12-05" + } + }, + "traceabilityInformation": [ + { + "linkURL": "https://files.sampleCompany.com/events/123456789.json", + "linkName": "Battery Assembly Event", + "linkType": "https://test.uncefact.org/vocabulary/linkTypes/dte", + "hashDigest": "50af99a26f4af48c9f4ad8cf9d2f5018780ab4bb1167f0e94884ec228f1ba832", + "hashMethod": "SHA-256", + "encryptionMethod": "AES" + }, + { + "linkURL": "https://files.sampleCompany.com/events/123454321.json", + "linkName": "Battery Packaging Event", + "linkType": "https://test.uncefact.org/vocabulary/linkTypes/dte", + "hashDigest": "50af99a26f4af48c9f4ad8cf9d2f5018780ab4bb1167f0e94884ec228f1ba832", + "hashMethod": "SHA-256", + "encryptionMethod": "AES" + } + ] + } +} diff --git a/tests/fixtures/samples/schemas_testing_breathable-t-shirt.json b/tests/fixtures/samples/schemas_testing_breathable-t-shirt.json new file mode 100644 index 0000000..7272e54 --- /dev/null +++ b/tests/fixtures/samples/schemas_testing_breathable-t-shirt.json @@ -0,0 +1,26 @@ +{ + "@context": { + "@version": 1.1, + "id": "@id", + "type": "@type", + "shirt": "https://spherity.github.io/schemas/testing/breathable-t-shirt.json#", + "schema": "https://schema.org/", + "BreathableTShirt": "shirt:BreathableTShirt", + "name": { + "@id": "shirt:name", + "@type": "schema:text" + }, + "material": { + "@id": "shirt:material", + "@type": "schema:text" + }, + "availablePrintTypes": { + "@id": "shirt:availablePrintTypes", + "@type": "schema:text" + }, + "designedBy": { + "@id": "shirt:designedBy", + "@type": "schema:text" + } + } +} diff --git a/tests/fixtures/samples/test_uncefact_org_DigitalIdentityAnchor-instance-0.6.1.json b/tests/fixtures/samples/test_uncefact_org_DigitalIdentityAnchor-instance-0.6.1.json new file mode 100644 index 0000000..b60afcd --- /dev/null +++ b/tests/fixtures/samples/test_uncefact_org_DigitalIdentityAnchor-instance-0.6.1.json @@ -0,0 +1,66 @@ +{ + "type": [ + "DigitalIdentityAnchor", + "VerifiableCredential" + ], + "@context": [ + "https://www.w3.org/ns/credentials/v2", + "https://test.uncefact.org/vocabulary/untp/dia/0.6.0/" + ], + "id": "https://example-company.com/credentials/2a423366-a0d6-4855-ba65-2e0c926d09b0", + "issuer": { + "type": [ + "CredentialIssuer" + ], + "id": "did:web:identifiers.example-company.com:12345", + "name": "Example Company Pty Ltd", + "issuerAlsoKnownAs": [ + { + "id": "https://abr.business.gov.au/ABN/View?abn=90664869327", + "name": "Sample Company Pty Ltd.", + "registeredId": "90664869327", + "idScheme": { + "type": [ + "IdentifierScheme" + ], + "id": "https://id.gs1.org/01/", + "name": "Global Trade Identification Number (GTIN)" + } + }, + { + "id": "https://abr.business.gov.au/ABN/View?abn=90664869327", + "name": "Sample Company Pty Ltd.", + "registeredId": "90664869327", + "idScheme": { + "type": [ + "IdentifierScheme" + ], + "id": "https://id.gs1.org/01/", + "name": "Global Trade Identification Number (GTIN)" + } + } + ] + }, + "validFrom": "2024-03-15T12:00:00Z", + "validUntil": "2034-03-15T12:00:00Z", + "credentialSubject": { + "type": [ + "RegisteredIdentity" + ], + "id": "did:web:samplecompany.com/123456789", + "name": "Sample business Ltd", + "registeredId": "123456789", + "idScheme": { + "type": [ + "IdentifierScheme" + ], + "id": "https://id.gs1.org/01/", + "name": "Global Trade Identification Number (GTIN)" + }, + "registerType": "Business", + "registrationScopeList": [ + "https://jargon.sh", + "https://jargon.sh" + ] + } +} diff --git a/tests/fixtures/samples/test_uncefact_org_untp-dpp-instance-0.6.0.json b/tests/fixtures/samples/test_uncefact_org_untp-dpp-instance-0.6.0.json new file mode 100644 index 0000000..b4cfcab --- /dev/null +++ b/tests/fixtures/samples/test_uncefact_org_untp-dpp-instance-0.6.0.json @@ -0,0 +1,561 @@ +{ + "type": [ + "DigitalProductPassport", + "VerifiableCredential" + ], + "@context": [ + "https://www.w3.org/ns/credentials/v2", + "https://test.uncefact.org/vocabulary/untp/dpp/0.6.0/" + ], + "id": "https://example-company.com/credentials/2a423366-a0d6-4855-ba65-2e0c926d09b0", + "issuer": { + "type": [ + "CredentialIssuer" + ], + "id": "did:web:identifiers.example-company.com:12345", + "name": "Example Company Pty Ltd", + "issuerAlsoKnownAs": [ + { + "id": "https://abr.business.gov.au/ABN/View?abn=90664869327", + "name": "Sample Company Pty Ltd.", + "registeredId": "90664869327", + "idScheme": { + "type": [ + "IdentifierScheme" + ], + "id": "https://id.gs1.org/01/", + "name": "Global Trade Identification Number (GTIN)" + } + }, + { + "id": "https://abr.business.gov.au/ABN/View?abn=90664869327", + "name": "Sample Company Pty Ltd.", + "registeredId": "90664869327", + "idScheme": { + "type": [ + "IdentifierScheme" + ], + "id": "https://id.gs1.org/01/", + "name": "Global Trade Identification Number (GTIN)" + } + } + ] + }, + "validFrom": "2024-03-15T12:00:00Z", + "validUntil": "2034-03-15T12:00:00Z", + "credentialSubject": { + "type": [ + "ProductPassport" + ], + "id": "example:product/1234", + "product": { + "type": [ + "Product" + ], + "id": "https://id.gs1.org/01/09520123456788/21/12345", + "name": "EV battery 300Ah.", + "registeredId": "09520123456788.21.12345", + "idScheme": { + "type": [ + "IdentifierScheme" + ], + "id": "https://id.gs1.org/01/", + "name": "Global Trade Identification Number (GTIN)" + }, + "batchNumber": "6789", + "productImage": { + "linkURL": "https://files.example-certifier.com/1234567.json", + "linkName": "GBA rule book conformity certificate", + "linkType": "https://test.uncefact.org/vocabulary/linkTypes/dcc" + }, + "description": "400Ah 24v LiFePO4 battery", + "productCategory": [ + { + "type": [ + "Classification" + ], + "id": "https://unstats.un.org/unsd/classifications/Econ/cpc/46410", + "code": "46410", + "name": "Primary cells and primary batteries", + "schemeID": "https://unstats.un.org/unsd/classifications/Econ/cpc/", + "schemeName": "UN Central Product Classification (CPC)" + }, + { + "type": [ + "Classification" + ], + "id": "https://unstats.un.org/unsd/classifications/Econ/cpc/46410", + "code": "46410", + "name": "Primary cells and primary batteries", + "schemeID": "https://unstats.un.org/unsd/classifications/Econ/cpc/", + "schemeName": "UN Central Product Classification (CPC)" + } + ], + "furtherInformation": [ + { + "linkURL": "https://files.example-certifier.com/1234567.json", + "linkName": "GBA rule book conformity certificate", + "linkType": "https://test.uncefact.org/vocabulary/linkTypes/dcc" + }, + { + "linkURL": "https://files.example-certifier.com/1234567.json", + "linkName": "GBA rule book conformity certificate", + "linkType": "https://test.uncefact.org/vocabulary/linkTypes/dcc" + } + ], + "producedByParty": { + "id": "https://abr.business.gov.au/ABN/View?abn=90664869327", + "name": "Sample Company Pty Ltd.", + "registeredId": "90664869327", + "idScheme": { + "type": [ + "IdentifierScheme" + ], + "id": "https://id.gs1.org/01/", + "name": "Global Trade Identification Number (GTIN)" + } + }, + "producedAtFacility": { + "id": "https://sample-facility-register.com/1234567", + "name": "Greenacres battery factory", + "registeredId": "1234567", + "idScheme": { + "type": [ + "IdentifierScheme" + ], + "id": "https://id.gs1.org/01/", + "name": "Global Trade Identification Number (GTIN)" + } + }, + "productionDate": "2024-04-25", + "countryOfProduction": "AU", + "serialNumber": "12345678", + "dimensions": { + "weight": { + "value": 10, + "unit": "KGM" + }, + "length": { + "value": 10, + "unit": "KGM" + }, + "width": { + "value": 10, + "unit": "KGM" + }, + "height": { + "value": 10, + "unit": "KGM" + }, + "volume": { + "value": 10, + "unit": "KGM" + } + } + }, + "granularityLevel": "batch", + "conformityClaim": [ + { + "type": [ + "Claim", + "Declaration" + ], + "id": "https://products.example-company.com/09520123456788/declarations/12345", + "description": "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.", + "referenceStandard": { + "type": [ + "Standard" + ], + "id": "https://www.globalbattery.org/media/publications/gba-rulebook-v2.0-master.pdf", + "name": "GBA Battery Passport Greenhouse Gas Rulebook - V.2.0", + "issuingParty": { + "id": "https://abr.business.gov.au/ABN/View?abn=90664869327", + "name": "Sample Company Pty Ltd.", + "registeredId": "90664869327", + "idScheme": { + "type": [ + "IdentifierScheme" + ], + "id": "https://id.gs1.org/01/", + "name": "Global Trade Identification Number (GTIN)" + } + }, + "issueDate": "2023-12-05" + }, + "referenceRegulation": { + "type": [ + "Regulation" + ], + "id": "https://www.legislation.gov.au/F2008L02309/latest/versions", + "name": "NNational Greenhouse and Energy Reporting (Measurement) Determination", + "jurisdictionCountry": "AU", + "administeredBy": { + "id": "https://abr.business.gov.au/ABN/View?abn=90664869327", + "name": "Sample Company Pty Ltd.", + "registeredId": "90664869327", + "idScheme": { + "type": [ + "IdentifierScheme" + ], + "id": "https://id.gs1.org/01/", + "name": "Global Trade Identification Number (GTIN)" + } + }, + "effectiveDate": "2024-03-20" + }, + "assessmentCriteria": [ + { + "type": [ + "Criterion" + ], + "id": "https://www.globalbattery.org/media/publications/gba-rulebook-v2.0-master.pdf#BatteryAssembly", + "name": "GBA Battery rule book v2.0 battery assembly guidelines.", + "description": "Battery is designed for easy disassembly and recycling at end-of-life.", + "conformityTopic": "environment.energy", + "status": "proposed", + "subCriterion": [], + "thresholdValue": { + "metricName": "GHG emissions intensity", + "metricValue": { + "value": 10, + "unit": "KGM" + }, + "score": "BB", + "accuracy": 0.05 + }, + "performanceLevel": "\"Category 3 recyclable with 73% recyclability\"", + "tags": "The quick brown fox jumps over the lazy dog." + }, + { + "type": [ + "Criterion" + ], + "id": "https://www.globalbattery.org/media/publications/gba-rulebook-v2.0-master.pdf#BatteryAssembly", + "name": "GBA Battery rule book v2.0 battery assembly guidelines.", + "description": "Battery is designed for easy disassembly and recycling at end-of-life.", + "conformityTopic": "environment.waste", + "status": "proposed", + "subCriterion": [], + "thresholdValue": { + "metricName": "GHG emissions intensity", + "metricValue": { + "value": 10, + "unit": "KGM" + }, + "score": "BB", + "accuracy": 0.05 + }, + "performanceLevel": "\"Category 3 recyclable with 73% recyclability\"", + "tags": "The quick brown fox jumps over the lazy dog." + } + ], + "assessmentDate": "2024-03-15", + "declaredValue": [ + { + "metricName": "GHG emissions intensity", + "metricValue": { + "value": 10, + "unit": "KGM" + }, + "score": "BB", + "accuracy": 0.05 + }, + { + "metricName": "GHG emissions intensity", + "metricValue": { + "value": 10, + "unit": "KGM" + }, + "score": "BB", + "accuracy": 0.05 + } + ], + "conformance": true, + "conformityTopic": "environment.emissions", + "conformityEvidence": { + "linkURL": "https://files.example-certifier.com/1234567.json", + "linkName": "GBA rule book conformity certificate", + "linkType": "https://test.uncefact.org/vocabulary/linkTypes/dcc", + "hashDigest": "6239119dda5bd4c8a6ffb832fe16feaa5c27b7dba154d24c53d4470a2c69adc2", + "hashMethod": "SHA-256", + "encryptionMethod": "AES" + } + }, + { + "type": [ + "Claim", + "Declaration" + ], + "id": "https://products.example-company.com/09520123456788/declarations/12345", + "description": "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.", + "referenceStandard": { + "type": [ + "Standard" + ], + "id": "https://www.globalbattery.org/media/publications/gba-rulebook-v2.0-master.pdf", + "name": "GBA Battery Passport Greenhouse Gas Rulebook - V.2.0", + "issuingParty": { + "id": "https://abr.business.gov.au/ABN/View?abn=90664869327", + "name": "Sample Company Pty Ltd.", + "registeredId": "90664869327", + "idScheme": { + "type": [ + "IdentifierScheme" + ], + "id": "https://id.gs1.org/01/", + "name": "Global Trade Identification Number (GTIN)" + } + }, + "issueDate": "2023-12-05" + }, + "referenceRegulation": { + "type": [ + "Regulation" + ], + "id": "https://www.legislation.gov.au/F2008L02309/latest/versions", + "name": "NNational Greenhouse and Energy Reporting (Measurement) Determination", + "jurisdictionCountry": "AU", + "administeredBy": { + "id": "https://abr.business.gov.au/ABN/View?abn=90664869327", + "name": "Sample Company Pty Ltd.", + "registeredId": "90664869327", + "idScheme": { + "type": [ + "IdentifierScheme" + ], + "id": "https://id.gs1.org/01/", + "name": "Global Trade Identification Number (GTIN)" + } + }, + "effectiveDate": "2024-03-20" + }, + "assessmentCriteria": [ + { + "type": [ + "Criterion" + ], + "id": "https://www.globalbattery.org/media/publications/gba-rulebook-v2.0-master.pdf#BatteryAssembly", + "name": "GBA Battery rule book v2.0 battery assembly guidelines.", + "description": "Battery is designed for easy disassembly and recycling at end-of-life.", + "conformityTopic": "circularity.content", + "status": "proposed", + "subCriterion": [], + "thresholdValue": { + "metricName": "GHG emissions intensity", + "metricValue": { + "value": 10, + "unit": "KGM" + }, + "score": "BB", + "accuracy": 0.05 + }, + "performanceLevel": "\"Category 3 recyclable with 73% recyclability\"", + "tags": "The quick brown fox jumps over the lazy dog." + }, + { + "type": [ + "Criterion" + ], + "id": "https://www.globalbattery.org/media/publications/gba-rulebook-v2.0-master.pdf#BatteryAssembly", + "name": "GBA Battery rule book v2.0 battery assembly guidelines.", + "description": "Battery is designed for easy disassembly and recycling at end-of-life.", + "conformityTopic": "social.rights", + "status": "proposed", + "subCriterion": [], + "thresholdValue": { + "metricName": "GHG emissions intensity", + "metricValue": { + "value": 10, + "unit": "KGM" + }, + "score": "BB", + "accuracy": 0.05 + }, + "performanceLevel": "\"Category 3 recyclable with 73% recyclability\"", + "tags": "The quick brown fox jumps over the lazy dog." + } + ], + "assessmentDate": "2024-03-15", + "declaredValue": [ + { + "metricName": "GHG emissions intensity", + "metricValue": { + "value": 10, + "unit": "KGM" + }, + "score": "BB", + "accuracy": 0.05 + }, + { + "metricName": "GHG emissions intensity", + "metricValue": { + "value": 10, + "unit": "KGM" + }, + "score": "BB", + "accuracy": 0.05 + } + ], + "conformance": true, + "conformityTopic": "environment.emissions", + "conformityEvidence": { + "linkURL": "https://files.example-certifier.com/1234567.json", + "linkName": "GBA rule book conformity certificate", + "linkType": "https://test.uncefact.org/vocabulary/linkTypes/dcc", + "hashDigest": "6239119dda5bd4c8a6ffb832fe16feaa5c27b7dba154d24c53d4470a2c69adc2", + "hashMethod": "SHA-256", + "encryptionMethod": "AES" + } + } + ], + "emissionsScorecard": { + "carbonFootprint": 1.8, + "declaredUnit": "KGM", + "operationalScope": "CradleToGate", + "primarySourcedRatio": 0.3, + "reportingStandard": { + "type": [ + "Standard" + ], + "id": "https://www.globalbattery.org/media/publications/gba-rulebook-v2.0-master.pdf", + "name": "GBA Battery Passport Greenhouse Gas Rulebook - V.2.0", + "issuingParty": { + "id": "https://abr.business.gov.au/ABN/View?abn=90664869327", + "name": "Sample Company Pty Ltd.", + "registeredId": "90664869327", + "idScheme": { + "type": [ + "IdentifierScheme" + ], + "id": "https://id.gs1.org/01/", + "name": "Global Trade Identification Number (GTIN)" + } + }, + "issueDate": "2023-12-05" + } + }, + "traceabilityInformation": [ + { + "valueChainProcess": "Spinning", + "verifiedRatio": 0.5, + "traceabilityEvent": [ + { + "linkURL": "https://files.example-certifier.com/1234567.json", + "linkName": "GBA rule book conformity certificate", + "linkType": "https://test.uncefact.org/vocabulary/linkTypes/dcc", + "hashDigest": "6239119dda5bd4c8a6ffb832fe16feaa5c27b7dba154d24c53d4470a2c69adc2", + "hashMethod": "SHA-256", + "encryptionMethod": "AES" + }, + { + "linkURL": "https://files.example-certifier.com/1234567.json", + "linkName": "GBA rule book conformity certificate", + "linkType": "https://test.uncefact.org/vocabulary/linkTypes/dcc", + "hashDigest": "6239119dda5bd4c8a6ffb832fe16feaa5c27b7dba154d24c53d4470a2c69adc2", + "hashMethod": "SHA-256", + "encryptionMethod": "AES" + } + ] + }, + { + "valueChainProcess": "Spinning", + "verifiedRatio": 0.5, + "traceabilityEvent": [ + { + "linkURL": "https://files.example-certifier.com/1234567.json", + "linkName": "GBA rule book conformity certificate", + "linkType": "https://test.uncefact.org/vocabulary/linkTypes/dcc", + "hashDigest": "6239119dda5bd4c8a6ffb832fe16feaa5c27b7dba154d24c53d4470a2c69adc2", + "hashMethod": "SHA-256", + "encryptionMethod": "AES" + }, + { + "linkURL": "https://files.example-certifier.com/1234567.json", + "linkName": "GBA rule book conformity certificate", + "linkType": "https://test.uncefact.org/vocabulary/linkTypes/dcc", + "hashDigest": "6239119dda5bd4c8a6ffb832fe16feaa5c27b7dba154d24c53d4470a2c69adc2", + "hashMethod": "SHA-256", + "encryptionMethod": "AES" + } + ] + } + ], + "circularityScorecard": { + "recyclingInformation": { + "linkURL": "https://files.example-certifier.com/1234567.json", + "linkName": "GBA rule book conformity certificate", + "linkType": "https://test.uncefact.org/vocabulary/linkTypes/dcc" + }, + "repairInformation": { + "linkURL": "https://files.example-certifier.com/1234567.json", + "linkName": "GBA rule book conformity certificate", + "linkType": "https://test.uncefact.org/vocabulary/linkTypes/dcc" + }, + "recyclableContent": 0.5, + "recycledContent": 0.3, + "utilityFactor": 1.2, + "materialCircularityIndicator": 0.67 + }, + "dueDiligenceDeclaration": { + "linkURL": "https://files.example-certifier.com/1234567.json", + "linkName": "GBA rule book conformity certificate", + "linkType": "https://test.uncefact.org/vocabulary/linkTypes/dcc" + }, + "materialsProvenance": [ + { + "name": "Lithium Spodumene", + "originCountry": "AU", + "materialType": { + "type": [ + "Classification" + ], + "id": "https://unstats.un.org/unsd/classifications/Econ/cpc/46410", + "code": "46410", + "name": "Primary cells and primary batteries", + "schemeID": "https://unstats.un.org/unsd/classifications/Econ/cpc/", + "schemeName": "UN Central Product Classification (CPC)" + }, + "massFraction": 0.2, + "mass": { + "value": 10, + "unit": "KGM" + }, + "recycledMassFraction": 0.5, + "hazardous": false, + "symbol": "undefined", + "materialSafetyInformation": { + "linkURL": "https://files.example-certifier.com/1234567.json", + "linkName": "GBA rule book conformity certificate", + "linkType": "https://test.uncefact.org/vocabulary/linkTypes/dcc" + } + }, + { + "name": "Lithium Spodumene", + "originCountry": "AU", + "materialType": { + "type": [ + "Classification" + ], + "id": "https://unstats.un.org/unsd/classifications/Econ/cpc/46410", + "code": "46410", + "name": "Primary cells and primary batteries", + "schemeID": "https://unstats.un.org/unsd/classifications/Econ/cpc/", + "schemeName": "UN Central Product Classification (CPC)" + }, + "massFraction": 0.2, + "mass": { + "value": 10, + "unit": "KGM" + }, + "recycledMassFraction": 0.5, + "hazardous": false, + "symbol": "undefined", + "materialSafetyInformation": { + "linkURL": "https://files.example-certifier.com/1234567.json", + "linkName": "GBA rule book conformity certificate", + "linkType": "https://test.uncefact.org/vocabulary/linkTypes/dcc" + } + } + ] + } +} diff --git a/tests/fixtures/samples/untp-verifiable-credentials_s3_amazonaws_com_bc075c5f-2304-4b3f-bb24-46d9fa9a8e60.json b/tests/fixtures/samples/untp-verifiable-credentials_s3_amazonaws_com_bc075c5f-2304-4b3f-bb24-46d9fa9a8e60.json new file mode 100644 index 0000000..25caf25 --- /dev/null +++ b/tests/fixtures/samples/untp-verifiable-credentials_s3_amazonaws_com_bc075c5f-2304-4b3f-bb24-46d9fa9a8e60.json @@ -0,0 +1,8 @@ +{ + "@context": [ + "https://www.w3.org/ns/credentials/v2", + "https://test.uncefact.org/vocabulary/untp/dpp/0.6.0/" + ], + "type": "EnvelopedVerifiableCredential", + "id": "data:application/vc+jwt,eyJhbGciOiJFZERTQSIsImtpZCI6ImRpZDp3ZWI6dW5jZWZhY3QuZ2l0aHViLmlvOnByb2plY3QtdmNraXQ6dGVzdC1hbmQtZGV2ZWxvcG1lbnQjN2FmMTM2YThlZmExMWE0ZGYyZTkwMTBiOTcyYmRiOTJhMDAxMzcyNGI1MGU1ZWZhNDU0MDdhMmRkZWExODRlNi1Kc29uV2ViS2V5LWtleS0wIiwiY3R5IjoidmMiLCJ0eXAiOiJ2Yytqd3QifQ.eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvbnMvY3JlZGVudGlhbHMvdjIiLCJodHRwczovL3Rlc3QudW5jZWZhY3Qub3JnL3ZvY2FidWxhcnkvdW50cC9kcHAvMC42LjAvIl0sInR5cGUiOlsiRGlnaXRhbFByb2R1Y3RQYXNzcG9ydCIsIlZlcmlmaWFibGVDcmVkZW50aWFsIl0sImlzc3VlciI6eyJ0eXBlIjpbIkNyZWRlbnRpYWxJc3N1ZXIiXSwiaWQiOiJkaWQ6d2ViOnVuY2VmYWN0LmdpdGh1Yi5pbzpwcm9qZWN0LXZja2l0OnRlc3QtYW5kLWRldmVsb3BtZW50IiwibmFtZSI6IkVjb0NoYXJnZSBCYXR0ZXJ5IFN5c3RlbXMgUHR5IEx0ZCJ9LCJjcmVkZW50aWFsU3ViamVjdCI6eyJ0eXBlIjpbIlByb2R1Y3RQYXNzcG9ydCJdLCJwcm9kdWN0Ijp7InR5cGUiOlsiUHJvZHVjdCJdLCJpZCI6Imh0dHBzOi8vaWQuZ3MxLm9yZy8wMS8wOTUyMDEyMzQ1Njc4OC8yMS8wMDAxIiwibmFtZSI6IkVjb0NoYXJnZSBFViBCYXR0ZXJ5IFBhY2siLCJyZWdpc3RlcmVkSWQiOiIwOTUyMDEyMzQ1Njc4OCIsInNlcmlhbE51bWJlciI6IjAwMDEiLCJiYXRjaE51bWJlciI6IiIsImlkU2NoZW1lIjp7InR5cGUiOlsiSWRlbnRpZmllclNjaGVtZSJdLCJpZCI6Imh0dHBzOi8vaWQuZ3MxLm9yZy8wMSIsIm5hbWUiOiJHbG9iYWwgVHJhZGUgSXRlbSBOdW1iZXIgKEdUSU4pIn0sInByb2R1Y3RJbWFnZSI6eyJsaW5rVVJMIjoiaHR0cHM6Ly9jLmFuaW1hYXBwLmNvbS9iM3ZmMk0yMC9pbWcvcHAtaGVhZGVyQDJ4LnBuZyIsImxpbmtOYW1lIjoiRVYgQmF0dGVyeSAzMDBBaCBJbWFnZSJ9LCJkZXNjcmlwdGlvbiI6IkhpZ2gtcGVyZm9ybWFuY2UgYXV0b21vdGl2ZS1ncmFkZSBsaXRoaXVtLWlvbiBiYXR0ZXJ5IHBhY2sgZGVzaWduZWQgZm9yIGVsZWN0cmljIHZlaGljbGVzLiBNYW51ZmFjdHVyZWQgd2l0aCByZXNwb25zaWJseSBzb3VyY2VkIG1hdGVyaWFscyBhbmQgYSA5NSUgcmVjeWNsYWJpbGl0eSByYXRlLCBpdCByZWR1Y2VzIGxpZmVjeWNsZSBlbWlzc2lvbnMgdGhyb3VnaCBlY28tZnJpZW5kbHkgcHJvZHVjdGlvbiBhbmQgdmVyaWZpZWQgcmVjeWNsaW5nIHByb2dyYW1zLiIsInByb2R1Y3RDYXRlZ29yeSI6W3sidHlwZSI6WyJDbGFzc2lmaWNhdGlvbiJdLCJpZCI6Imh0dHBzOi8vdW5zdGF0cy51bi5vcmcvdW5zZC9jbGFzc2lmaWNhdGlvbnMvRWNvbi9jcGMvNDY0MTAiLCJjb2RlIjoiNDY0MTAiLCJuYW1lIjoiUHJpbWFyeSBjZWxscyBhbmQgcHJpbWFyeSBiYXR0ZXJpZXMiLCJzY2hlbWVJRCI6Imh0dHBzOi8vdW5zdGF0cy51bi5vcmcvdW5zZC9jbGFzc2lmaWNhdGlvbnMvRWNvbi9jcGMiLCJzY2hlbWVOYW1lIjoiVU4gQ2VudHJhbCBQcm9kdWN0IENsYXNzaWZpY2F0aW9uIChDUEMpIn1dLCJwcm9kdWNlZEJ5UGFydHkiOnsiaWQiOiJodHRwczovL2lkci51bnRwLnNob3d0aGV0aGluZy5jb20vYXBpLzEuMC4wL2F0by9hYm4vOTA2NjQ4NjkzMjc_bGlua1R5cGU9YXRvOnJlZ2lzdHJ5RW50cnkiLCJuYW1lIjoiRWNvQ2hhcmdlIEJhdHRlcnkgU3lzdGVtcyBQdHkgTHRkIiwicmVnaXN0ZXJlZElkIjoiOTA2NjQ4NjkzMjcifSwicHJvZHVjZWRBdEZhY2lsaXR5Ijp7ImlkIjoiaHR0cHM6Ly9pZHIudW50cC5zaG93dGhldGhpbmcuY29tL2FwaS8xLjAuMC9nczEvZ2xuLzEzMjEyMDIyOTA2NDg_bGlua1R5cGU9Z3MxOmxvY2F0aW9uSW5mbyIsIm5hbWUiOiJFY29DaGFyZ2UgU3lkbmV5IE1hbnVmYWN0dXJpbmcgUGxhbnQiLCJyZWdpc3RlcmVkSWQiOiIxMzIxMjAyMjkwNjQ4In0sImRpbWVuc2lvbnMiOnsid2VpZ2h0Ijp7InZhbHVlIjo0ODAsInVuaXQiOiJLR00ifSwibGVuZ3RoIjp7InZhbHVlIjoyODAwLCJ1bml0IjoiTU1UIn0sIndpZHRoIjp7InZhbHVlIjoxNjAwLCJ1bml0IjoiTU1UIn0sImhlaWdodCI6eyJ2YWx1ZSI6MjAwLCJ1bml0IjoiTU1UIn0sInZvbHVtZSI6eyJ2YWx1ZSI6MjUwLCJ1bml0IjoiRE1RIn19LCJwcm9kdWN0aW9uRGF0ZSI6IjIwMjQtMDMtMTUiLCJjb3VudHJ5T2ZQcm9kdWN0aW9uIjoiQVUifSwiZ3JhbnVsYXJpdHlMZXZlbCI6Iml0ZW0iLCJkdWVEaWxpZ2VuY2VEZWNsYXJhdGlvbiI6eyJsaW5rVVJMIjoiaHR0cHM6Ly9leGFtcGxlLmNvbS9kdWUtZGlsaWdlbmNlLzEyMzQ1NjciLCJsaW5rTmFtZSI6IkR1ZSBEaWxpZ2VuY2UgRGVjbGFyYXRpb24ifSwibWF0ZXJpYWxzUHJvdmVuYW5jZSI6W3sibmFtZSI6IkxpdGhpdW0iLCJvcmlnaW5Db3VudHJ5IjoiQVUiLCJtYXNzRnJhY3Rpb24iOjAuMywibWFzcyI6eyJ2YWx1ZSI6NzUsInVuaXQiOiJLR00ifSwiaGF6YXJkb3VzIjp0cnVlfSx7Im5hbWUiOiJOaWNrZWwiLCJvcmlnaW5Db3VudHJ5IjoiQVUiLCJtYXNzRnJhY3Rpb24iOjAuMywibWFzcyI6eyJ2YWx1ZSI6NzUsInVuaXQiOiJLR00ifSwiaGF6YXJkb3VzIjp0cnVlfSx7Im5hbWUiOiJDb2JhbHQiLCJvcmlnaW5Db3VudHJ5IjoiQVUiLCJtYXNzRnJhY3Rpb24iOjAuMiwibWFzcyI6eyJ2YWx1ZSI6NTAsInVuaXQiOiJLR00ifSwiaGF6YXJkb3VzIjp0cnVlfSx7Im5hbWUiOiJPdGhlciBNYXRlcmlhbHMiLCJvcmlnaW5Db3VudHJ5IjoiQVUiLCJtYXNzRnJhY3Rpb24iOjAuMiwibWFzcyI6eyJ2YWx1ZSI6NTAsInVuaXQiOiJLR00ifSwiaGF6YXJkb3VzIjpmYWxzZX1dLCJjb25mb3JtaXR5Q2xhaW0iOlt7InR5cGUiOlsiQ2xhaW0iLCJEZWNsYXJhdGlvbiJdLCJpZCI6Imh0dHBzOi8vZXhhbXBsZS5jb20vYXR0ZXN0YXRpb25zLzIwMjQtMDAxIiwiYXNzZXNzbWVudERhdGUiOiIyMDI0LTAzLTE1IiwiY29uZm9ybWFuY2UiOnRydWUsImNvbmZvcm1pdHlUb3BpYyI6InNvY2lhbC5zYWZldHkiLCJjb25mb3JtaXR5RXZpZGVuY2UiOnsibGlua1VSTCI6Imh0dHBzOi8vaWRyLnVudHAuc2hvd3RoZXRoaW5nLmNvbS9hcGkvMS4wLjAvZ3MxL2d0aW4vMDk1MjAxMjM0NTY3ODgvMjEvMDAwMT9saW5rVHlwZT1nczE6Y2VydGlmaWNhdGlvbkluZm8iLCJsaW5rTmFtZSI6IkNvbmZvcm1pdHkgRXZpZGVuY2UifSwicmVmZXJlbmNlU3RhbmRhcmQiOnsidHlwZSI6WyJTdGFuZGFyZCJdLCJpZCI6Imh0dHBzOi8vd3d3Lmlzby5vcmcvc3RhbmRhcmQvNzE0MDcuaHRtbCIsIm5hbWUiOiJJU08gMTI0MDUtNDoyMDE4IC0gVGVzdCBzcGVjaWZpY2F0aW9uIGZvciBsaXRoaXVtLWlvbiB0cmFjdGlvbiBiYXR0ZXJ5IHBhY2tzIGFuZCBzeXN0ZW1zIiwiaXNzdWluZ1BhcnR5Ijp7ImlkIjoiaHR0cHM6Ly93d3cuaXNvLm9yZyIsIm5hbWUiOiJJbnRlcm5hdGlvbmFsIE9yZ2FuaXphdGlvbiBmb3IgU3RhbmRhcmRpemF0aW9uIn0sImlzc3VlRGF0ZSI6IjIwMTgtMDItMTUifSwiZGVjbGFyZWRWYWx1ZSI6W3sibWV0cmljTmFtZSI6IlNob3J0IENpcmN1aXQgVGVzdCBUZW1wZXJhdHVyZSBSaXNlIiwibWV0cmljVmFsdWUiOnsidmFsdWUiOjAsInVuaXQiOiJDRUwifSwiYWNjdXJhY3kiOjAuMDEsInNjb3JlIjoiQSJ9LHsibWV0cmljTmFtZSI6Ik1heGltdW0gT3BlcmF0aW5nIFRlbXBlcmF0dXJlIiwibWV0cmljVmFsdWUiOnsidmFsdWUiOjU1LCJ1bml0IjoiQ0VMIn0sImFjY3VyYWN5IjowLjAxLCJzY29yZSI6IkEifV19XSwiY2lyY3VsYXJpdHlTY29yZWNhcmQiOnsicmVjeWNsYWJsZUNvbnRlbnQiOjAuOTUsInJlY3ljbGVkQ29udGVudCI6MC4zLCJ1dGlsaXR5RmFjdG9yIjoxLjIsIm1hdGVyaWFsQ2lyY3VsYXJpdHlJbmRpY2F0b3IiOjAuODUsInJlY3ljbGluZ0luZm9ybWF0aW9uIjp7ImxpbmtVUkwiOiJodHRwczovL2V4YW1wbGUuY29tL3JlY3ljbGluZy9ldjMwMCIsImxpbmtOYW1lIjoiQmF0dGVyeSBSZWN5Y2xpbmcgR3VpZGVsaW5lcyJ9LCJyZXBhaXJJbmZvcm1hdGlvbiI6eyJsaW5rVVJMIjoiaHR0cHM6Ly9leGFtcGxlLmNvbS9yZXBhaXIvZXYzMDAiLCJsaW5rTmFtZSI6IlJlcGFpciBJbnN0cnVjdGlvbnMifX0sImVtaXNzaW9uc1Njb3JlY2FyZCI6eyJjYXJib25Gb290cHJpbnQiOjI1LCJkZWNsYXJlZFVuaXQiOiJLR00iLCJvcGVyYXRpb25hbFNjb3BlIjoiQ3JhZGxlVG9HYXRlIiwicHJpbWFyeVNvdXJjZWRSYXRpbyI6MC45NSwicmVwb3J0aW5nU3RhbmRhcmQiOnsidHlwZSI6WyJTdGFuZGFyZCJdLCJpZCI6Imh0dHBzOi8vZ2hncHJvdG9jb2wub3JnL3Byb2R1Y3Qtc3RhbmRhcmQiLCJpc3N1ZURhdGUiOiIyMDExLTExLTE1IiwibmFtZSI6IkdIRyBQcm90b2NvbCBQcm9kdWN0IExpZmUgQ3ljbGUgQWNjb3VudGluZyBhbmQgUmVwb3J0aW5nIFN0YW5kYXJkIiwiaXNzdWluZ1BhcnR5Ijp7ImlkIjoiaHR0cHM6Ly9naGdwcm90b2NvbC5vcmciLCJuYW1lIjoiR3JlZW5ob3VzZSBHYXMgUHJvdG9jb2wifX19LCJ0cmFjZWFiaWxpdHlJbmZvcm1hdGlvbiI6W3sidmFsdWVDaGFpblByb2Nlc3MiOiJNYW51ZmFjdHVyaW5nIiwidmVyaWZpZWRSYXRpbyI6MC41LCJ0cmFjZWFiaWxpdHlFdmVudCI6W3sibGlua1VSTCI6Imh0dHBzOi8vaWRyLnVudHAuc2hvd3RoZXRoaW5nLmNvbS9hcGkvMS4wLjAvZ3MxLzAxLzA5NTIwMTIzNDU2Nzg4LzIxLzAwMDE_bGlua1R5cGU9Z3MxOnRyYWNlYWJpbGl0eSIsImxpbmtOYW1lIjoiVHJhbnNmb3JtYXRpb24gRXZlbnQiLCJsaW5rVHlwZSI6Imh0dHBzOi8vdGVzdC51bmNlZmFjdC5vcmcvdm9jYWJ1bGFyeS9saW5rVHlwZXMvZHRlIn1dfV19LCJ2YWxpZEZyb20iOiIyMDI1LTA2LTE3VDIyOjIxOjI1Ljg5OFoiLCJjcmVkZW50aWFsU3RhdHVzIjp7ImlkIjoiaHR0cHM6Ly92Y2tpdC51bnRwLnNob3d0aGV0aGluZy5jb20vY3JlZGVudGlhbHMvc3RhdHVzL2JpdHN0cmluZy1zdGF0dXMtbGlzdC8zIzEzNSIsInR5cGUiOiJCaXRzdHJpbmdTdGF0dXNMaXN0RW50cnkiLCJzdGF0dXNQdXJwb3NlIjoicmV2b2NhdGlvbiIsInN0YXR1c0xpc3RJbmRleCI6MTM1LCJzdGF0dXNMaXN0Q3JlZGVudGlhbCI6Imh0dHBzOi8vdmNraXQudW50cC5zaG93dGhldGhpbmcuY29tL2NyZWRlbnRpYWxzL3N0YXR1cy9iaXRzdHJpbmctc3RhdHVzLWxpc3QvMyJ9LCJpZCI6InVybjp1dWlkOmI3NTMwNDBjLTE2MDItNDViOC1iZjU5LTE1YWIzMzFhMmM3YSIsInJlbmRlck1ldGhvZCI6W3sidHlwZSI6IldlYlJlbmRlcmluZ1RlbXBsYXRlMjAyMiIsInRlbXBsYXRlIjoiPCFET0NUWVBFIGh0bWw-PGh0bWwgbGFuZz1cImVuXCI-IDxoZWFkPiA8bWV0YSBjaGFyc2V0PVwiVVRGLThcIiAvPiA8bWV0YSBuYW1lPVwidmlld3BvcnRcIiBjb250ZW50PVwid2lkdGg9ZGV2aWNlLXdpZHRoLCBpbml0aWFsLXNjYWxlPTEuMFwiIC8-IDxsaW5rIGhyZWY9XCJodHRwczovL2ZvbnRzLmdvb2dsZWFwaXMuY29tL2NzczI_ZmFtaWx5PUxhdG86aXRhbCx3Z2h0QDAsMTAwOzAsMzAwOzAsNDAwOzAsNzAwOzAsOTAwOzEsMTAwOzEsMzAwOzEsNDAwOzEsNzAwOzEsOTAwJmRpc3BsYXk9c3dhcFwiIHJlbD1cInN0eWxlc2hlZXRcIiAvPiA8dGl0bGU-RGlnaXRhbCBQcm9kdWN0IFBhc3Nwb3J0PC90aXRsZT4gPHN0eWxlPiA6cm9vdCB7IC8qIEJyYW5kIENvbG9ycyAqLyAtLWNvbG9yLXByaW1hcnk6IHJnYmEoMzEsIDkwLCAxNDksIDEpOyAvKiBDb2xvciBmb3IgcGFzc3BvcnQgYm94IGl0ZW0gdGV4dCwgZW1pc3Npb24gc2NvcmVjYXJkIHVuaXQsIGNvbmZvcm1pdHkgZGV0YWlscywgYW5kIGhpc3RvcnkgdmFsdWUgY2hhaW4gcHJvY2VzcyB0ZXh0OyBEZWZhdWx0OiByZ2JhKDMxLCA5MCwgMTQ5LCAxKSAqLyAvKiBOZXV0cmFscyAqLyAtLWNvbG9yLXdoaXRlOiByZ2JhKDI1NSwgMjU1LCAyNTUsIDEpOyAvKiBCYWNrZ3JvdW5kIGNvbG9yIGZvciBtYWluIGNvbnRhaW5lciwgY29uZm9ybWl0eSBjYXJkcywgYW5kIGlzc3VpbmcgZGV0YWlscyBzZWN0aW9uOyBEZWZhdWx0OiByZ2JhKDI1NSwgMjU1LCAyNTUsIDEpICovIC0tY29sb3ItYmxhY2s6IHJnYmEoMCwgMCwgMCwgMSk7IC8qIFRleHQgY29sb3IgZm9yIHNlY3Rpb24gZGVzY3JpcHRpb25zLCBpbmZvcm1hdGlvbiB0ZXh0LCBjb21wb3NpdGlvbiB0aXRsZSwgY29tcG9zaXRpb24gcGVyY2VudCwgY29tcG9zaXRpb24gdGFnIGl0ZW0sIGhpc3RvcnkgaXRlbSBzcGFuLCBhbmQgdHJhY2VhYmlsaXR5IGNhcmQgdGV4dDsgRGVmYXVsdDogcmdiYSgwLCAwLCAwLCAxKSAqLyAtLWNvbG9yLWdyYXktNzAwOiByZ2JhKDM1LCA0NiwgNjEsIDEpOyAvKiBUZXh0IGNvbG9yIGZvciBsaW5rcywgc2VjdGlvbiB0aXRsZXMsIHRhYmxlIGl0ZW0gdmFsdWVzLCBkZWNsYXJlZCB2YWx1ZSB0ZXh0LCBhbmQgaGVhZGVyIGJhdGNoIGl0ZW0gbGlua3M7IERlZmF1bHQ6IHJnYmEoMzUsIDQ2LCA2MSwgMSkgKi8gLS1jb2xvci1ncmF5LTYwMDogcmdiYSg4NSwgOTYsIDExMCwgMSk7IC8qIFRleHQgY29sb3IgZm9yIHRhYmxlIGl0ZW0gc3BhbnMsIGNvbmZvcm1pdHkgbGFiZWxzLCBzY29yZSBuYW1lLCBwYXNzcG9ydCBhbm5vdGF0aW9uLCBjb25mb3JtaXR5IGluZm8sIGRlY2xhcmVkIHZhbHVlIHNwYW4sIGNvdW50cnkgY29kZSwgZm9vdGVyIHRleHQsIGFuZCBoZWFkZXIgaW1hZ2UgYmFja2dyb3VuZDsgRGVmYXVsdDogcmdiYSg4NSwgOTYsIDExMCwgMSkgKi8gLS1jb2xvci1ncmF5LTQwMDogcmdiYSgyMTIsIDIxNCwgMjE2LCAxKTsgLyogQm9yZGVyIGNvbG9yIGZvciB0YWJsZSBpdGVtcywgY29uZm9ybWl0eSBjYXJkcywgY29tcG9zaXRpb24gYm94IGl0ZW1zLCBoaXN0b3J5IGl0ZW1zLCBhbmQgcGFzc3BvcnQgYm94OyBEZWZhdWx0OiByZ2JhKDIxMiwgMjE0LCAyMTYsIDEpICovIC0tY29sb3ItZ3JheS0xMDA6IHJnYmEoMjQ3LCAyNTAsIDI1MywgMSk7IC8qIEJhY2tncm91bmQgY29sb3IgZm9yIGZvb3RlciwgdmVyaWZpZWQgcmF0aW8sIGNvbXBvc2l0aW9uIHRhZyBpdGVtLCBoZWFkZXIgYmF0Y2gsIGFuZCBvbmUgcGFzc3BvcnQgYm94IGl0ZW07IERlZmF1bHQ6IHJnYmEoMjQ3LCAyNTAsIDI1MywgMSkgKi8gLyogU2VtYW50aWMgKEZ1bmN0aW9uYWwpIENvbG9ycyAqLyAtLWNvbG9yLWxpbmstdW5kZXJsaW5lLWRhcms6IHJnYmEoNzksIDE0OSwgMjIxLCAxKTsgLyogVW5kZXJsaW5lIGNvbG9yIGZvciBibHVlLWJvdHRvbS1saW5lLXRoaWNrIGxpbmtzOyBEZWZhdWx0OiByZ2JhKDc5LCAxNDksIDIyMSwgMSkgKi8gLS1jb2xvci1hY2NlbnQtc3VjY2VzczogcmdiYSgxODQsIDIzNiwgMTgyLCAxKTsgLyogQmFja2dyb3VuZCBjb2xvciBmb3IgZ3JlZW4gY29uZm9ybWFuY2UgYmFkZ2U7IERlZmF1bHQ6IHJnYmEoMTg0LCAyMzYsIDE4MiwgMSkgKi8gLS1jb2xvci1hY2NlbnQtZXJyb3I6IHJnYmEoMjU1LCAxODgsIDE4MywgMSk7IC8qIEJhY2tncm91bmQgY29sb3IgZm9yIHJlZCBjb25mb3JtYW5jZSBiYWRnZTsgRGVmYXVsdDogcmdiYSgyNTUsIDE4OCwgMTgzLCAxKSAqLyAtLWNvbG9yLWljb246IHJnYmEoMzEsIDkwLCAxNDksIDEpOyAvKiBGaWxsIGFuZCBzdHJva2UgY29sb3IgZm9yIGFsbCBTVkcgaWNvbnM7IERlZmF1bHQ6IHJnYmEoMzEsIDkwLCAxNDksIDEpICovIC8qIEZvbnQgVmFyaWFibGVzICovIC0tZm9udC1mYW1pbHk6IFwiTGF0b1wiLCBzYW5zLXNlcmlmOyAvKiBGb250IGZhbWlseSBmb3IgYWxsIHRleHQ7IERlZmF1bHQ6IExhdG8sIHNhbnMtc2VyaWYgKi8gLyogRm9udCBXZWlnaHQgVmFyaWFibGVzICovIC0tZm9udC13ZWlnaHQtbGlnaHQ6IDMwMDsgLyogRm9udCB3ZWlnaHQgZm9yIGluZm9ybWF0aW9uIHRleHQ7IERlZmF1bHQ6IDMwMCAqLyAtLWZvbnQtd2VpZ2h0LXJlZ3VsYXI6IDQwMDsgLyogRm9udCB3ZWlnaHQgZm9yIHNlY3Rpb24gZGVzY3JpcHRpb25zLCBjb25mb3JtaXR5IGxhYmVscywgdGFibGUgaXRlbSBzcGFucywgZGVjbGFyZWQgdmFsdWUgdGV4dCwgcGFzc3BvcnQgYW5ub3RhdGlvbiwgY29uZm9ybWl0eSBpbmZvLCBzY29yZSBuYW1lLCBjb21wb3NpdGlvbiB0YWcgaXRlbSwgY291bnRyeSBjb2RlLCBmb290ZXIgdGV4dCwgaGlzdG9yeSBpdGVtIHNwYW4sIHRyYWNlYWJpbGl0eSBjYXJkIHRleHQsIGFuZCBoZWFkZXIgaW1hZ2UgdG9wLWxlZnQgdGV4dDsgRGVmYXVsdDogNDAwICovIC0tZm9udC13ZWlnaHQtbWVkaXVtOiA1MDA7IC8qIEZvbnQgd2VpZ2h0IGZvciBsaW5rcywgdGFibGUgaXRlbSBwYXJhZ3JhcGhzLCBjb21wb3NpdGlvbiBwZXJjZW50LCBhbmQgaGVhZGVyIGltYWdlIHRvcC1sZWZ0IHRleHQ7IERlZmF1bHQ6IDUwMCAqLyAtLWZvbnQtd2VpZ2h0LWJvbGQ6IDYwMDsgLyogRm9udCB3ZWlnaHQgZm9yIHNlY3Rpb24gdGl0bGVzLCBjb21wb3NpdGlvbiB0aXRsZSwgYW5kIGNvbmZvcm1hbmNlIGJhZGdlIHRleHQ7IERlZmF1bHQ6IDYwMCAqLyAtLWZvbnQtd2VpZ2h0LWJsYWNrOiA5MDA7IC8qIEZvbnQgd2VpZ2h0IGZvciBoZWFkZXIgaW1hZ2UgYm90dG9tIGxlZnQgaDEsIHBhc3Nwb3J0IGJveCBpdGVtIGgzLCBhbmQgZW1pc3Npb24gc2NvcmUgdW5pdDsgRGVmYXVsdDogOTAwICovIC8qIE90aGVyIFZhcmlhYmxlcyAqLyAtLWltYWdlLXNyYzogdXJsKFwie3tjcmVkZW50aWFsU3ViamVjdC5wcm9kdWN0LnByb2R1Y3RJbWFnZS5saW5rVVJMfX1cIik7IC8qIEJhY2tncm91bmQgaW1hZ2UgZm9yIGhlYWRlciBpbWFnZTsgRGVmYXVsdDogdXJsKFwie3tjcmVkZW50aWFsU3ViamVjdC5wcm9kdWN0LnByb2R1Y3RJbWFnZS5saW5rVVJMfX1cIikgKi8gfSAqIHsgbWFyZ2luOiAwOyBwYWRkaW5nOiAwOyBib3gtc2l6aW5nOiBib3JkZXItYm94OyB9IGJvZHkgeyBmb250LWZhbWlseTogdmFyKC0tZm9udC1mYW1pbHkpOyBjb2xvcjogdmFyKC0tY29sb3ItZ3JheS02MDApOyBmb250LXdlaWdodDogdmFyKC0tZm9udC13ZWlnaHQtcmVndWxhcik7IH0gLmNvbnRhaW5lciB7IG1pbi13aWR0aDogMTUwcHg7IHdpZHRoOiAxMDAlOyBtYXJnaW46IDAgYXV0bzsgZGlzcGxheTogZmxleDsgZmxleC1kaXJlY3Rpb246IGNvbHVtbjsgZ2FwOiAzMnB4OyB3b3JkLWJyZWFrOiBicmVhay13b3JkOyBiYWNrZ3JvdW5kLWNvbG9yOiB2YXIoLS1jb2xvci13aGl0ZSk7IH0gc2VjdGlvbiwgaGVhZGVyLCBmb290ZXIgeyBwYWRkaW5nOiAwIDE2cHg7IH0gLyogTmV1dHJhbGlzZSBkZWZhdWx0IG1hcmdpbnMgb24gaGVhZGVyIGVsZW1lbnRzIHdpdGhpbiBzZWN0aW9ucyAqLyBzZWN0aW9uIGhlYWRlciB7IG1hcmdpbjogMDsgfSAuaGVhZGVyLWltYWdlIHsgYmFja2dyb3VuZC1jb2xvcjogdmFyKC0tY29sb3ItZ3JheS02MDApOyBiYWNrZ3JvdW5kLWltYWdlOiB2YXIoLS1pbWFnZS1zcmMsIG5vbmUpLCBsaW5lYXItZ3JhZGllbnQoIDI0OC4zNmRlZywgcmdiYSgwLCAwLCAwLCAwLjE4KSA3LjYlLCByZ2JhKDAsIDAsIDAsIDAuNikgNzAuNTIlICk7IGJhY2tncm91bmQtc2l6ZTogY292ZXI7IGJhY2tncm91bmQtcG9zaXRpb246IGNlbnRlcjsgYmFja2dyb3VuZC1yZXBlYXQ6IG5vLXJlcGVhdDsgaGVpZ2h0OiAyMzJweDsgcG9zaXRpb246IHJlbGF0aXZlOyB9IC5oZWFkZXItaW1hZ2UtdG9wLWxlZnQgeyBwb3NpdGlvbjogYWJzb2x1dGU7IHRvcDogMjVweDsgbGVmdDogMTVweDsgZm9udC13ZWlnaHQ6IHZhcigtLWZvbnQtd2VpZ2h0LW1lZGl1bSk7IGZvbnQtc2l6ZTogMTZweDsgbGluZS1oZWlnaHQ6IDIycHg7IGNvbG9yOiB2YXIoLS1jb2xvci13aGl0ZSk7IH0gLmhlYWRlci1pbWFnZS1ib3R0b20tbGVmdCB7IHBvc2l0aW9uOiBhYnNvbHV0ZTsgYm90dG9tOiAxOHB4OyBsZWZ0OiAxNXB4OyBjb2xvcjogdmFyKC0tY29sb3Itd2hpdGUpOyB9IC5oZWFkZXItaW1hZ2UtYm90dG9tLWxlZnQgaDEgeyBmb250LXNpemU6IDMwcHg7IGZvbnQtd2VpZ2h0OiB2YXIoLS1mb250LXdlaWdodC1ibGFjayk7IGxpbmUtaGVpZ2h0OiAzMi41cHg7IH0gLmhlYWRlci1iYXRjaCB7IHBhZGRpbmc6IDEycHggMTZweDsgYmFja2dyb3VuZC1jb2xvcjogdmFyKC0tY29sb3ItZ3JheS0xMDApOyBkaXNwbGF5OiBmbGV4OyBmbGV4LXdyYXA6IHdyYXA7IGp1c3RpZnktY29udGVudDogc3BhY2UtYmV0d2VlbjsgZ2FwOiAxMnB4OyB9IC5oZWFkZXItYmF0Y2gtaXRlbSB7IGRpc3BsYXk6IGZsZXg7IGFsaWduLWl0ZW1zOiBjZW50ZXI7IGdhcDogNnB4OyBmbGV4LWdyb3c6IDE7IG1pbi13aWR0aDogMDsgfSAuaGVhZGVyLWJhdGNoLWl0ZW0gYSB7IGNvbG9yOiB2YXIoLS1jb2xvci1ncmF5LTcwMCk7IGZvbnQtc2l6ZTogMTRweDsgZm9udC13ZWlnaHQ6IHZhcigtLWZvbnQtd2VpZ2h0LW1lZGl1bSk7IH0gLmhlYWRlci1iYXRjaC1pdGVtIHN2ZyB7IGZpbGw6IHZhcigtLWNvbG9yLWljb24pOyBzdHJva2U6IHZhcigtLWNvbG9yLWljb24pOyB9IC8qIEdlbmVyYWwgU2VjdGlvbiBTdHlsZXMgKi8gLnNlY3Rpb24tdGl0bGUgeyBmb250LXNpemU6IDE4cHg7IGZvbnQtd2VpZ2h0OiB2YXIoLS1mb250LXdlaWdodC1ib2xkKTsgbGluZS1oZWlnaHQ6IDE5LjYycHg7IGNvbG9yOiB2YXIoLS1jb2xvci1ncmF5LTcwMCk7IH0gLnNlY3Rpb24tZGVzY3JpcHRpb24geyBtYXJnaW4tdG9wOiAxMnB4OyBmb250LXNpemU6IDE2cHg7IGxpbmUtaGVpZ2h0OiAxOC44OHB4OyBjb2xvcjogdmFyKC0tY29sb3ItYmxhY2spOyBmb250LXdlaWdodDogdmFyKC0tZm9udC13ZWlnaHQtcmVndWxhcik7IH0gLyogVGFibGUgU3R5bGVzICovIC50YWJsZSB7IGRpc3BsYXk6IGZsZXg7IGZsZXgtZGlyZWN0aW9uOiBjb2x1bW47IGdhcDogMTBweDsgfSAudGFibGUtaXRlbSB7IGRpc3BsYXk6IGdyaWQ7IGdyaWQtdGVtcGxhdGUtY29sdW1uczogMWZyIDJmcjsgY29sdW1uLWdhcDogMTZweDsgYWxpZ24taXRlbXM6IGNlbnRlcjsgcGFkZGluZy1ib3R0b206IDEwcHg7IGJvcmRlci1ib3R0b206IDFweCBzb2xpZCB2YXIoLS1jb2xvci1ncmF5LTQwMCk7IH0gLnRhYmxlLWl0ZW0gc3BhbiB7IGZvbnQtc2l6ZTogMTZweDsgZm9udC13ZWlnaHQ6IHZhcigtLWZvbnQtd2VpZ2h0LXJlZ3VsYXIpOyBjb2xvcjogdmFyKC0tY29sb3ItZ3JheS02MDApOyB9IC50YWJsZS1pdGVtIHAsIC50YWJsZS1pdGVtIGEgeyBmb250LXNpemU6IDE2cHg7IGZvbnQtd2VpZ2h0OiB2YXIoLS1mb250LXdlaWdodC1tZWRpdW0pOyBjb2xvcjogdmFyKC0tY29sb3ItZ3JheS03MDApOyB9IC5pdGVtLXZhbHVlIHsgZGlzcGxheTogZmxleDsgZmxleC1kaXJlY3Rpb246IGNvbHVtbjsgZm9udC1zaXplOiAxNnB4OyBmb250LXdlaWdodDogdmFyKC0tZm9udC13ZWlnaHQtbWVkaXVtKTsgY29sb3I6IHZhcigtLWNvbG9yLWdyYXktNjAwKTsgfSAuaXRlbS12YWx1ZSBzcGFuIHsgY29sb3I6IHZhcigtLWNvbG9yLWdyYXktNzAwKTsgfSAuaW5mb3JtYXRpb24tdGV4dCB7IGZvbnQtc2l6ZTogMTlweDsgcGFkZGluZy1ib3R0b206IDhweDsgZm9udC13ZWlnaHQ6IHZhcigtLWZvbnQtd2VpZ2h0LWxpZ2h0KTsgY29sb3I6IHZhcigtLWNvbG9yLWJsYWNrKTsgbGluZS1oZWlnaHQ6IDIyLjQycHg7IH0gLmluZm9ybWF0aW9uLXNob3ctbW9yZSB7IGRpc3BsYXk6IGZsZXg7IGZsZXgtZGlyZWN0aW9uOiBjb2x1bW47IGdhcDogMTBweDsgZm9udC1zaXplOiAxNHB4OyBmb250LXdlaWdodDogdmFyKC0tZm9udC13ZWlnaHQtbWVkaXVtKTsgfSAvKiBQcm9kdWN0aW9uIFNlY3Rpb24gKi8gLnByb2R1Y3Rpb24geyBkaXNwbGF5OiBmbGV4OyBmbGV4LWRpcmVjdGlvbjogY29sdW1uOyBnYXA6IDEycHg7IH0gLyogUGFzc3BvcnQgU2VjdGlvbiAqLyAucGFzc3BvcnQgeyBkaXNwbGF5OiBmbGV4OyBmbGV4LWRpcmVjdGlvbjogY29sdW1uOyBnYXA6IDI0cHg7IH0gLnBhc3Nwb3J0LWJveCB7IGRpc3BsYXk6IGdyaWQ7IGdyaWQtdGVtcGxhdGUtY29sdW1uczogMWZyIDFmcjsgYm9yZGVyOiAxcHggc29saWQgdmFyKC0tY29sb3ItZ3JheS00MDApOyBib3JkZXItcmFkaXVzOiA1cHg7IH0gLnBhc3Nwb3J0LWJveC1pdGVtIHsgZGlzcGxheTogZmxleDsgZmxleC1kaXJlY3Rpb246IGNvbHVtbjsganVzdGlmeS1jb250ZW50OiBjZW50ZXI7IGFsaWduLWl0ZW1zOiBjZW50ZXI7IHBhZGRpbmc6IDEycHggMTZweDsgYm9yZGVyOiAxcHggc29saWQgdmFyKC0tY29sb3ItZ3JheS00MDApOyBtaW4taGVpZ2h0OiA5NHB4OyBjb2xvcjogdmFyKC0tY29sb3ItcHJpbWFyeSk7IH0gLnBhc3Nwb3J0LWJveC1pdGVtIGgzIHsgZm9udC1zaXplOiA0MHB4OyBmb250LXdlaWdodDogdmFyKC0tZm9udC13ZWlnaHQtYmxhY2spOyBsaW5lLWhlaWdodDogNDMuMzNweDsgbGV0dGVyLXNwYWNpbmc6IDJweDsgfSAucGFzc3BvcnQtYm94LWl0ZW0gcCB7IG1hcmdpbi10b3A6IDhweDsgZm9udC1zaXplOiAxNXB4OyBmb250LXdlaWdodDogdmFyKC0tZm9udC13ZWlnaHQtYm9sZCk7IH0gLnBhc3Nwb3J0LWJveC1pdGVtOm50aC1jaGlsZCg0KSB7IGJhY2tncm91bmQtY29sb3I6IHZhcigtLWNvbG9yLWdyYXktMTAwKTsgfSAucGFzc3BvcnQtYW5ub3RhdGlvbiB7IGZvbnQtc2l6ZTogMTRweDsgZm9udC13ZWlnaHQ6IHZhcigtLWZvbnQtd2VpZ2h0LXJlZ3VsYXIpOyBsaW5lLWhlaWdodDogMTUuMjZweDsgY29sb3I6IHZhcigtLWNvbG9yLWdyYXktNjAwKTsgfSAudHJhY2VhYmlsaXR5LWNhcmRzIHsgZGlzcGxheTogZmxleDsgZmxleC1kaXJlY3Rpb246IGNvbHVtbjsgZ2FwOiAxMnB4OyB9IC50cmFjZWFiaWxpdHktY2FyZCB7IGRpc3BsYXk6IGdyaWQ7IGdyaWQtdGVtcGxhdGUtY29sdW1uczogM2ZyIDFmcjsgYWxpZ24taXRlbXM6IGNlbnRlcjsgdGV4dC1kZWNvcmF0aW9uOiBub25lOyB9IC50cmFjZWFiaWxpdHktY2FyZC10ZXh0IHsgZGlzcGxheTogZmxleDsgYWxpZ24taXRlbXM6IGNlbnRlcjsgZ2FwOiA4cHg7IGZvbnQtc2l6ZTogMTZweDsgZm9udC13ZWlnaHQ6IHZhcigtLWZvbnQtd2VpZ2h0LXJlZ3VsYXIpOyBjb2xvcjogdmFyKC0tY29sb3ItYmxhY2spOyB9IC50cmFjZWFiaWxpdHktY2FyZC10ZXh0IHN2ZyB7IGZpbGw6IHZhcigtLWNvbG9yLWljb24pOyBzdHJva2U6IHZhcigtLWNvbG9yLWljb24pOyB9IC50cmFjZWFiaWxpdHktY2FyZC12aWV3LWRldGFpbHMgeyBkaXNwbGF5OiBmbGV4OyBqdXN0aWZ5LWNvbnRlbnQ6IGZsZXgtZW5kOyBnYXA6IDhweDsgfSAvKiBFbWlzc2lvbiBTY29yZWNhcmQgKi8gLmVtaXNzaW9uLXNjb3JlLWNhcmQgeyBkaXNwbGF5OiBmbGV4OyBmbGV4LWRpcmVjdGlvbjogY29sdW1uOyBnYXA6IDEycHg7IH0gLnNjb3JlIHsgZGlzcGxheTogZmxleDsgZmxleC1kaXJlY3Rpb246IGNvbHVtbjsgZ2FwOiA2cHg7IH0gLnNjb3JlLXVuaXQgeyBmb250LXNpemU6IDQwcHg7IGZvbnQtd2VpZ2h0OiB2YXIoLS1mb250LXdlaWdodC1ibGFjayk7IGxpbmUtaGVpZ2h0OiA0My4zM3B4OyBjb2xvcjogdmFyKC0tY29sb3ItcHJpbWFyeSk7IGxldHRlci1zcGFjaW5nOiAycHg7IH0gLnNjb3JlLW5hbWUgeyBmb250LXNpemU6IDE2cHg7IGZvbnQtd2VpZ2h0OiB2YXIoLS1mb250LXdlaWdodC1yZWd1bGFyKTsgY29sb3I6IHZhcigtLWNvbG9yLWdyYXktNjAwKTsgfSAvKiBEZWNsYXJhdGlvbnMgKi8gLmRlY2xhcmF0aW9ucyB7IGRpc3BsYXk6IGZsZXg7IGZsZXgtZGlyZWN0aW9uOiBjb2x1bW47IGdhcDogMTJweDsgfSAuY2FyZHMtY29uZm9ybWl0aWVzIHsgZGlzcGxheTogZmxleDsgZmxleC1kaXJlY3Rpb246IGNvbHVtbjsgZ2FwOiA4cHg7IH0gLmNhcmRzLWNvbmZvcm1pdHkgeyBkaXNwbGF5OiBmbGV4OyBmbGV4LWRpcmVjdGlvbjogY29sdW1uOyBnYXA6IDhweDsgcGFkZGluZzogMTZweDsgYmFja2dyb3VuZC1jb2xvcjogdmFyKC0tY29sb3Itd2hpdGUpOyBib3JkZXI6IDFweCBzb2xpZCB2YXIoLS1jb2xvci1ncmF5LTQwMCk7IGJvcmRlci1yYWRpdXM6IDRweDsgfSAuY2FyZHMtY29uZm9ybWl0eSBoZWFkZXIgeyBtYXJnaW46IDA7IH0gLmNvbmZvcm1hbmNlLWhlYWRlciB7IGRpc3BsYXk6IGZsZXg7IGp1c3RpZnktY29udGVudDogc3BhY2UtYmV0d2VlbjsgYWxpZ24taXRlbXM6IGNlbnRlcjsgZmxleC13cmFwOiB3cmFwOyBnYXA6IDVweDsgfSAuY29uZm9ybWFuY2Utc3RhdHVzIHsgZGlzcGxheTogZmxleDsgYWxpZ24taXRlbXM6IGNlbnRlcjsgZ2FwOiA0cHg7IH0gLmNvbmZvcm1hbmNlLWxhYmVsIHsgZm9udC1zaXplOiAxNHB4OyBmb250LXdlaWdodDogdmFyKC0tZm9udC13ZWlnaHQtcmVndWxhcik7IGNvbG9yOiB2YXIoLS1jb2xvci1ncmF5LTYwMCk7IH0gLnRhZ3MtVkMtYmFkZ2UtcmVkLCAudGFncy1WQy1iYWRnZS1ncmVlbiB7IHBhZGRpbmc6IDRweCA4cHg7IGJvcmRlci1yYWRpdXM6IDhweDsgZm9udC1zaXplOiAxNHB4OyBmb250LXdlaWdodDogdmFyKC0tZm9udC13ZWlnaHQtYm9sZCk7IH0gLnRhZ3MtVkMtYmFkZ2UtcmVkIHsgYmFja2dyb3VuZC1jb2xvcjogdmFyKC0tY29sb3ItYWNjZW50LWVycm9yKTsgY29sb3I6IHZhcigtLWNvbG9yLWdyYXktNjAwKTsgfSAudGFncy1WQy1iYWRnZS1ncmVlbiB7IGJhY2tncm91bmQtY29sb3I6IHZhcigtLWNvbG9yLWFjY2VudC1zdWNjZXNzKTsgY29sb3I6IHZhcigtLWNvbG9yLWdyYXktNjAwKTsgfSAuY29uZm9ybWl0eS1kZXRhaWxzIHsgZm9udC1zaXplOiAxOHB4OyBmb250LXdlaWdodDogdmFyKC0tZm9udC13ZWlnaHQtcmVndWxhcik7IGNvbG9yOiB2YXIoLS1jb2xvci1wcmltYXJ5KTsgfSAuY29uZm9ybWl0eS1pbmZvIHsgZGlzcGxheTogZmxleDsgZmxleC1kaXJlY3Rpb246IGNvbHVtbjsgZ2FwOiA4cHg7IH0gLmNvbmZvcm1pdHktaW5mbyBwIHsgZm9udC1zaXplOiAxNHB4OyBmb250LXdlaWdodDogdmFyKC0tZm9udC13ZWlnaHQtcmVndWxhcik7IGNvbG9yOiB2YXIoLS1jb2xvci1ncmF5LTYwMCk7IH0gLmRlY2xhcmVkLXZhbHVlcyB7IGRpc3BsYXk6IGZsZXg7IGZsZXgtZGlyZWN0aW9uOiBjb2x1bW47IGdhcDogNHB4OyB9IC5kZWNsYXJlZC12YWx1ZSBwIHsgZm9udC1zaXplOiAxNnB4OyBmb250LXdlaWdodDogdmFyKC0tZm9udC13ZWlnaHQtcmVndWxhcik7IGNvbG9yOiB2YXIoLS1jb2xvci1ncmF5LTcwMCk7IH0gLmRlY2xhcmVkLXZhbHVlIHNwYW4geyBmb250LXNpemU6IDE0cHg7IGNvbG9yOiB2YXIoLS1jb2xvci1ncmF5LTYwMCk7IH0gLyogQ29tcG9zaXRpb24gKi8gLmNvbXBvc2l0aW9uLWJveCB7IGRpc3BsYXk6IGZsZXg7IGZsZXgtZGlyZWN0aW9uOiBjb2x1bW47IGdhcDogOHB4OyBtYXJnaW4tdG9wOiAxMnB4OyB9IC5jb21wb3NpdGlvbi1ib3gtaXRlbSB7IGRpc3BsYXk6IGdyaWQ7IGdyaWQtdGVtcGxhdGUtY29sdW1uczogMWZyIGF1dG87IGJvcmRlcjogMXB4IHNvbGlkIHZhcigtLWNvbG9yLWdyYXktNDAwKTsgYm9yZGVyLXJhZGl1czogNHB4OyBwYWRkaW5nOiAxNnB4OyB9IC5jb21wb3NpdGlvbi1maXJzdC1jb2x1bW4geyBkaXNwbGF5OiBncmlkOyBncmlkLXRlbXBsYXRlLWNvbHVtbnM6IDQwcHggMWZyOyBnYXA6IDEycHg7IH0gLmNvbXBvc2l0aW9uLXBlcmNlbnQgeyBmb250LXNpemU6IDE2cHg7IGZvbnQtd2VpZ2h0OiB2YXIoLS1mb250LXdlaWdodC1tZWRpdW0pOyBjb2xvcjogdmFyKC0tY29sb3ItYmxhY2spOyB9IC5jb21wb3NpdGlvbi10aXRsZSB7IGZvbnQtc2l6ZTogMTZweDsgZm9udC13ZWlnaHQ6IHZhcigtLWZvbnQtd2VpZ2h0LWJvbGQpOyBjb2xvcjogdmFyKC0tY29sb3ItYmxhY2spOyB9IC5jb21wb3NpdGlvbi10YWcgeyBkaXNwbGF5OiBmbGV4OyBnYXA6IDRweDsgfSAuY29tcG9zaXRpb24tdGFnLWl0ZW0geyBmb250LXNpemU6IDE0cHg7IGZvbnQtd2VpZ2h0OiB2YXIoLS1mb250LXdlaWdodC1yZWd1bGFyKTsgY29sb3I6IHZhcigtLWNvbG9yLWJsYWNrKTsgYmFja2dyb3VuZC1jb2xvcjogdmFyKC0tY29sb3ItZ3JheS0xMDApOyBwYWRkaW5nOiAycHggNHB4OyB9IC5jb3VudHJ5LWNvZGUgeyBmb250LXNpemU6IDE0cHg7IGZvbnQtd2VpZ2h0OiB2YXIoLS1mb250LXdlaWdodC1yZWd1bGFyKTsgY29sb3I6IHZhcigtLWNvbG9yLWdyYXktNjAwKTsgfSAvKiBIaXN0b3J5ICovIC5oaXN0b3J5IHsgZGlzcGxheTogZmxleDsgZmxleC1kaXJlY3Rpb246IGNvbHVtbjsgZ2FwOiAxMnB4OyB9IC5oaXN0b3J5LXZhbHVlLWNoYWluIHsgZGlzcGxheTogZmxleDsgZmxleC1kaXJlY3Rpb246IGNvbHVtbjsgZ2FwOiA0cHg7IH0gLmhpc3RvcnktdmFsdWUtY2hhaW4gcCB7IGZvbnQtc2l6ZTogMThweDsgZm9udC13ZWlnaHQ6IHZhcigtLWZvbnQtd2VpZ2h0LXJlZ3VsYXIpOyBjb2xvcjogdmFyKC0tY29sb3ItcHJpbWFyeSk7IH0gLnZlcmlmaWVkLXJhdGlvIHsgYmFja2dyb3VuZC1jb2xvcjogdmFyKC0tY29sb3ItZ3JheS0xMDApOyBwYWRkaW5nOiAycHggNHB4OyB3aWR0aDogZml0LWNvbnRlbnQ7IH0gLmhpc3RvcnktaXRlbSB7IGRpc3BsYXk6IGdyaWQ7IGdyaWQtdGVtcGxhdGUtY29sdW1uczogMWZyIGF1dG87IHBhZGRpbmc6IDEwcHggMDsgYm9yZGVyLWJvdHRvbTogMXB4IHNvbGlkIHZhcigtLWNvbG9yLWdyYXktNDAwKTsgfSAuaGlzdG9yeS1pdGVtIHNwYW4geyBmb250LXNpemU6IDE2cHg7IGZvbnQtd2VpZ2h0OiB2YXIoLS1mb250LXdlaWdodC1yZWd1bGFyKTsgY29sb3I6IHZhcigtLWNvbG9yLWJsYWNrKTsgfSAuaGlzdG9yeS1pdGVtIGEgeyBmb250LXNpemU6IDE2cHg7IGZvbnQtd2VpZ2h0OiB2YXIoLS1mb250LXdlaWdodC1tZWRpdW0pOyBjb2xvcjogdmFyKC0tY29sb3ItZ3JheS03MDApOyB9IC8qIElzc3VlZCBCeSAqLyAuaXNzdWVkLWJ5IHsgZGlzcGxheTogZmxleDsgZmxleC1kaXJlY3Rpb246IGNvbHVtbjsgZ2FwOiAxMnB4OyB9IC8qIEZvb3RlciAqLyBmb290ZXIgeyBwYWRkaW5nOiAxNnB4IDE2cHggMzJweDsgYmFja2dyb3VuZC1jb2xvcjogdmFyKC0tY29sb3ItZ3JheS0xMDApOyB9IGZvb3RlciBwIHsgZm9udC1zaXplOiAxNHB4OyBmb250LXdlaWdodDogdmFyKC0tZm9udC13ZWlnaHQtcmVndWxhcik7IGNvbG9yOiB2YXIoLS1jb2xvci1ncmF5LTYwMCk7IH0gLyogTGlua3MgKi8gLmJsdWUtYm90dG9tLWxpbmUtdGhpY2sgeyB0ZXh0LWRlY29yYXRpb246IHVuZGVybGluZTsgdGV4dC1kZWNvcmF0aW9uLXRoaWNrbmVzczogMnB4OyB0ZXh0LWRlY29yYXRpb24tY29sb3I6IHZhcigtLWNvbG9yLWxpbmstdW5kZXJsaW5lLWRhcmspOyB0ZXh0LXVuZGVybGluZS1vZmZzZXQ6IDNweDsgY29sb3I6IHZhcigtLWNvbG9yLWdyYXktNzAwKTsgfSAuYmx1ZS1ib3R0b20tbGluZS10aGljay5kaXNhYmxlZCB7IHBvaW50ZXItZXZlbnRzOiBub25lOyBjdXJzb3I6IG5vdC1hbGxvd2VkOyB0ZXh0LWRlY29yYXRpb246IG5vbmU7IH0gLmJsdWUtYm90dG9tLWxpbmUtdGhpY2suZGlzYWJsZWQ6Zm9jdXMgeyBvdXRsaW5lOiBub25lOyB9IC5ncmF5LWJvdHRvbS1saW5lIHsgYm9yZGVyLWJvdHRvbTogMXB4IHNvbGlkIHZhcigtLWNvbG9yLWdyYXktNjAwKTsgdGV4dC1kZWNvcmF0aW9uOiBub25lOyBjb2xvcjogdmFyKC0tY29sb3ItZ3JheS02MDApOyB9IC8qIERlc2t0b3AgQWRqdXN0bWVudHMgKi8gQG1lZGlhIChtaW4td2lkdGg6IDEyMDBweCkgeyAuY29udGFpbmVyIHsgbWF4LXdpZHRoOiAxMjAwcHg7IH0gfSA8L3N0eWxlPiA8L2hlYWQ-IDxib2R5PiA8ZGl2IGNsYXNzPVwiY29udGFpbmVyXCI-IDxoZWFkZXIgY2xhc3M9XCJoZWFkZXJcIj4gPGRpdiBjbGFzcz1cImhlYWRlci1pbWFnZVwiPiA8cCBjbGFzcz1cImhlYWRlci1pbWFnZS10b3AtbGVmdFwiPlBST0RVQ1QgUEFTU1BPUlQ8L3A-IDxkaXYgY2xhc3M9XCJoZWFkZXItaW1hZ2UtYm90dG9tLWxlZnRcIj4gPGgxPnt7Y3JlZGVudGlhbFN1YmplY3QucHJvZHVjdC5uYW1lfX08L2gxPiA8L2Rpdj4gPC9kaXY-IDxkaXYgY2xhc3M9XCJoZWFkZXItYmF0Y2hcIj4ge3sjaWYgY3JlZGVudGlhbFN1YmplY3QucHJvZHVjdC5yZWdpc3RlcmVkSWR9fSA8ZGl2IGNsYXNzPVwiaGVhZGVyLWJhdGNoLWl0ZW1cIj4gPHN2ZyB3aWR0aD1cIjE0XCIgaGVpZ2h0PVwiMTRcIiB2aWV3Qm94PVwiMCAwIDE0IDE0XCIgZmlsbD1cIm5vbmVcIiB4bWxucz1cImh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnXCIgPiA8cGF0aCBkPVwiTTIuNDUgMy41QzIuMTcxNTIgMy41IDEuOTA0NDUgMy4zODkzOCAxLjcwNzU0IDMuMTkyNDZDMS41MTA2MiAyLjk5NTU1IDEuNCAyLjcyODQ4IDEuNCAyLjQ1QzEuNCAyLjE3MTUyIDEuNTEwNjIgMS45MDQ0NSAxLjcwNzU0IDEuNzA3NTRDMS45MDQ0NSAxLjUxMDYyIDIuMTcxNTIgMS40IDIuNDUgMS40QzIuNzI4NDggMS40IDIuOTk1NTUgMS41MTA2MiAzLjE5MjQ2IDEuNzA3NTRDMy4zODkzOCAxLjkwNDQ1IDMuNSAyLjE3MTUyIDMuNSAyLjQ1QzMuNSAyLjcyODQ4IDMuMzg5MzggMi45OTU1NSAzLjE5MjQ2IDMuMTkyNDZDMi45OTU1NSAzLjM4OTM4IDIuNzI4NDggMy41IDIuNDUgMy41Wk0xMy41ODcgNi43MDZMNy4yODcgMC40MDZDNy4wMzUgMC4xNTQgNi42ODUgMCA2LjMgMEgxLjRDMC42MjMgMCAwIDAuNjIzIDAgMS40VjYuM0MwIDYuNjg1IDAuMTU0IDcuMDM1IDAuNDEzIDcuMjg3TDYuNzA2IDEzLjU4N0M2Ljk2NSAxMy44MzkgNy4zMTUgMTQgNy43IDE0QzguMDg1IDE0IDguNDM1IDEzLjgzOSA4LjY4NyAxMy41ODdMMTMuNTg3IDguNjg3QzEzLjg0NiA4LjQzNSAxNCA4LjA4NSAxNCA3LjdDMTQgNy4zMDggMTMuODM5IDYuOTU4IDEzLjU4NyA2LjcwNlpcIiBmaWxsPVwidmFyKC0tY29sb3ItaWNvbilcIiBzdHJva2U9XCJ2YXIoLS1jb2xvci1pY29uKVwiIC8-IDwvc3ZnPiA8YSBocmVmPVwie3tjcmVkZW50aWFsU3ViamVjdC5wcm9kdWN0LmlkfX1cIiBjbGFzcz1cImJsdWUtYm90dG9tLWxpbmUtdGhpY2tcIiB0YXJnZXQ9XCJfYmxhbmtcIiA-SUQ6IHt7Y3JlZGVudGlhbFN1YmplY3QucHJvZHVjdC5yZWdpc3RlcmVkSWR9fTwvYSA-IDwvZGl2PiB7ey9pZn19IHt7I2lmIGNyZWRlbnRpYWxTdWJqZWN0LnByb2R1Y3QuYmF0Y2hOdW1iZXJ9fSA8ZGl2IGNsYXNzPVwiaGVhZGVyLWJhdGNoLWl0ZW1cIj4gPHN2ZyB3aWR0aD1cIjE0XCIgaGVpZ2h0PVwiMTRcIiB2aWV3Qm94PVwiMCAwIDE0IDE0XCIgZmlsbD1cIm5vbmVcIiB4bWxucz1cImh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnXCIgPiA8cGF0aCBkPVwiTTIuNDUgMy41QzIuMTcxNTIgMy41IDEuOTA0NDUgMy4zODkzOCAxLjcwNzU0IDMuMTkyNDZDMS41MTA2MiAyLjk5NTU1IDEuNCAyLjcyODQ4IDEuNCAyLjQ1QzEuNCAyLjE3MTUyIDEuNTEwNjIgMS45MDQ0NSAxLjcwNzU0IDEuNzA3NTRDMS45MDQ0NSAxLjUxMDYyIDIuMTcxNTIgMS40IDIuNDUgMS40QzIuNzI4NDggMS40IDIuOTk1NTUgMS41MTA2MiAzLjE5MjQ2IDEuNzA3NTRDMy4zODkzOCAxLjkwNDQ1IDMuNSAyLjE3MTUyIDMuNSAyLjQ1QzMuNSAyLjcyODQ4IDMuMzg5MzggMi45OTU1NSAzLjE5MjQ2IDMuMTkyNDZDMi45OTU1NSAzLjM4OTM4IDIuNzI4NDggMy41IDIuNDUgMy41Wk0xMy41ODcgNi43MDZMNy4yODcgMC40MDZDNy4wMzUgMC4xNTQgNi42ODUgMCA2LjMgMEgxLjRDMC42MjMgMCAwIDAuNjIzIDAgMS40VjYuM0MwIDYuNjg1IDAuMTU0IDcuMDM1IDAuNDEzIDcuMjg3TDYuNzA2IDEzLjU4N0M2Ljk2NSAxMy44MzkgNy4zMTUgMTQgNy43IDE0QzguMDg1IDE0IDguNDM1IDEzLjgzOSA4LjY4NyAxMy41ODdMMTMuNTg3IDguNjg3QzEzLjg0NiA4LjQzNSAxNCA4LjA4NSAxNCA3LjdDMTQgNy4zMDggMTMuODM5IDYuOTU4IDEzLjU4NyA2LjcwNlpcIiBmaWxsPVwidmFyKC0tY29sb3ItaWNvbilcIiBzdHJva2U9XCJ2YXIoLS1jb2xvci1pY29uKVwiIC8-IDwvc3ZnPiA8YT5CYXRjaDoge3tjcmVkZW50aWFsU3ViamVjdC5wcm9kdWN0LmJhdGNoTnVtYmVyfX08L2E-IDwvZGl2PiB7ey9pZn19IHt7I2lmIGNyZWRlbnRpYWxTdWJqZWN0LnByb2R1Y3Quc2VyaWFsTnVtYmVyfX0gPGRpdiBjbGFzcz1cImhlYWRlci1iYXRjaC1pdGVtXCI-IDxzdmcgd2lkdGg9XCIxNFwiIGhlaWdodD1cIjE0XCIgdmlld0JveD1cIjAgMCAxNCAxNFwiIGZpbGw9XCJub25lXCIgeG1sbnM9XCJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2Z1wiID4gPHBhdGggZD1cIk0yLjQ1IDMuNUMyLjE3MTUyIDMuNSAxLjkwNDQ1IDMuMzg5MzggMS43MDc1NCAzLjE5MjQ2QzEuNTEwNjIgMi45OTU1NSAxLjQgMi43Mjg0OCAxLjQgMi40NUMxLjQgMi4xNzE1MiAxLjUxMDYyIDEuOTA0NDUgMS43MDc1NCAxLjcwNzU0QzEuOTA0NDUgMS41MTA2MiAyLjE3MTUyIDEuNCAyLjQ1IDEuNEMyLjcyODQ4IDEuNCAyLjk5NTU1IDEuNTEwNjIgMy4xOTI0NiAxLjcwNzU0QzMuMzg5MzggMS45MDQ0NSAzLjUgMi4xNzE1MiAzLjUgMi40NUMzLjUgMi43Mjg0OCAzLjM4OTM4IDIuOTk1NTUgMy4xOTI0NiAzLjE5MjQ2QzIuOTk1NTUgMy4zODkzOCAyLjcyODQ4IDMuNSAyLjQ1IDMuNVpNMTMuNTg3IDYuNzA2TDcuMjg3IDAuNDA2QzcuMDM1IDAuMTU0IDYuNjg1IDAgNi4zIDBIMS40QzAuNjIzIDAgMCAwLjYyMyAwIDEuNFY2LjNDMCA2LjY4NSAwLjE1NCA3LjAzNSAwLjQxMyA3LjI4N0w2LjcwNiAxMy41ODdDNi45NjUgMTMuODM5IDcuMzE1IDE0IDcuNyAxNEM4LjA4NSAxNCA4LjQzNSAxMy44MzkgOC42ODcgMTMuNTg3TDEzLjU4NyA4LjY4N0MxMy44NDYgOC40MzUgMTQgOC4wODUgMTQgNy43QzE0IDcuMzA4IDEzLjgzOSA2Ljk1OCAxMy41ODcgNi43MDZaXCIgZmlsbD1cInZhcigtLWNvbG9yLWljb24pXCIgc3Ryb2tlPVwidmFyKC0tY29sb3ItaWNvbilcIiAvPiA8L3N2Zz4gPGE-U2VyaWFsOiB7e2NyZWRlbnRpYWxTdWJqZWN0LnByb2R1Y3Quc2VyaWFsTnVtYmVyfX08L2E-IDwvZGl2PiB7ey9pZn19IDwvZGl2PiA8L2hlYWRlcj4gPHNlY3Rpb24-IHt7I2lmIGNyZWRlbnRpYWxTdWJqZWN0LnByb2R1Y3QuZGVzY3JpcHRpb259fSA8ZGl2IGNsYXNzPVwiaW5mb3JtYXRpb24tdGV4dFwiPiB7e2NyZWRlbnRpYWxTdWJqZWN0LnByb2R1Y3QuZGVzY3JpcHRpb259fSA8L2Rpdj4ge3svaWZ9fSB7eyNpZiBjcmVkZW50aWFsU3ViamVjdC5wcm9kdWN0LmZ1cnRoZXJJbmZvcm1hdGlvbn19IDxkaXYgY2xhc3M9XCJpbmZvcm1hdGlvbi1zaG93LW1vcmVcIj4ge3sjZWFjaCBjcmVkZW50aWFsU3ViamVjdC5wcm9kdWN0LmZ1cnRoZXJJbmZvcm1hdGlvbn19IHt7I2lmIGxpbmtVUkx9fSB7eyNpZiBsaW5rTmFtZX19IDxhIGhyZWY9XCJ7e2xpbmtVUkx9fVwiIGNsYXNzPVwiYmx1ZS1ib3R0b20tbGluZS10aGlja1wiIHRhcmdldD1cIl9ibGFua1wiPiB7e2xpbmtOYW1lfX0gPC9hPiB7ey9pZn19IHt7L2lmfX0ge3svZWFjaH19IDwvZGl2PiB7ey9pZn19IDwvc2VjdGlvbj4ge3sjaWYgY3JlZGVudGlhbFN1YmplY3QucHJvZHVjdC5jaGFyYWN0ZXJpc3RpY3N9fSA8c2VjdGlvbiBjbGFzcz1cInByb2R1Y3Rpb25cIj4gPGRpdiBjbGFzcz1cInNlY3Rpb24tdGl0bGVcIj5DaGFyYWN0ZXJpc3RpY3M8L2Rpdj4gPGRpdiBjbGFzcz1cInRhYmxlXCI-IHt7I2VhY2ggY3JlZGVudGlhbFN1YmplY3QucHJvZHVjdC5jaGFyYWN0ZXJpc3RpY3N9fSA8ZGl2IGNsYXNzPVwidGFibGUtaXRlbVwiPiA8c3Bhbj57e0BrZXl9fTwvc3Bhbj4gPHAgY2xhc3M9XCJpdGVtLXZhbHVlXCI-e3t0aGlzfX08L3A-IDwvZGl2PiB7ey9lYWNofX0gPC9kaXY-IDwvc2VjdGlvbj4ge3svaWZ9fSA8c2VjdGlvbiBjbGFzcz1cInByb2R1Y3Rpb25cIj4gPGRpdiBjbGFzcz1cInNlY3Rpb24tdGl0bGVcIj5Qcm9kdWN0aW9uPC9kaXY-IDxkaXYgY2xhc3M9XCJ0YWJsZVwiPiB7eyNpZiBjcmVkZW50aWFsU3ViamVjdC5wcm9kdWN0LnByb2R1Y3RDYXRlZ29yeX19IDxkaXYgY2xhc3M9XCJ0YWJsZS1pdGVtXCI-IDxzcGFuPlByb2R1Y3QgY2F0ZWdvcnk8L3NwYW4-IDxwIGNsYXNzPVwiaXRlbS12YWx1ZVwiPiB7eyNlYWNoIGNyZWRlbnRpYWxTdWJqZWN0LnByb2R1Y3QucHJvZHVjdENhdGVnb3J5fX17e25hbWV9fXt7I3VubGVzcyBAbGFzdH19LCB7ey91bmxlc3N9fXt7L2VhY2h9fSA8L3A-IDwvZGl2PiB7ey9pZn19IHt7I2lmIGNyZWRlbnRpYWxTdWJqZWN0LnByb2R1Y3QucHJvZHVjZWRCeVBhcnR5fX0gPGRpdiBjbGFzcz1cInRhYmxlLWl0ZW1cIj4gPHNwYW4-UHJvZHVjZWQgYnk8L3NwYW4-IDxhIGhyZWY9XCJ7e2NyZWRlbnRpYWxTdWJqZWN0LnByb2R1Y3QucHJvZHVjZWRCeVBhcnR5LmlkfX1cIiBjbGFzcz1cImJsdWUtYm90dG9tLWxpbmUtdGhpY2tcIiB0YXJnZXQ9XCJfYmxhbmtcIiA-e3tjcmVkZW50aWFsU3ViamVjdC5wcm9kdWN0LnByb2R1Y2VkQnlQYXJ0eS5uYW1lfX08L2EgPiA8L2Rpdj4ge3svaWZ9fSB7eyNpZiBjcmVkZW50aWFsU3ViamVjdC5wcm9kdWN0LnByb2R1Y2VkQXRGYWNpbGl0eX19IHt7I3dpdGggY3JlZGVudGlhbFN1YmplY3QucHJvZHVjdC5wcm9kdWNlZEF0RmFjaWxpdHl9fSA8ZGl2IGNsYXNzPVwidGFibGUtaXRlbVwiPiA8c3Bhbj5Qcm9kdWNlZCBhdDwvc3Bhbj4gPHAgY2xhc3M9XCJpdGVtLXZhbHVlXCI-IDxhIGhyZWY9XCJ7e2lkfX1cIiBjbGFzcz1cImJsdWUtYm90dG9tLWxpbmUtdGhpY2tcIiB0YXJnZXQ9XCJfYmxhbmtcIj57e25hbWV9fTwvYT4gPC9wPiA8L2Rpdj4gPCEtLSBUT0RPOiBBZGQgbG9jYXRpb25JbmZvcm1hdGlvbiBhbmQgYWRkcmVzcyBiYWNrIHRvIHRoZSBEUFAgZGF0YSBtb2RlbCAtLT4ge3sjaWYgYWRkcmVzc319IDxkaXYgY2xhc3M9XCJ0YWJsZS1pdGVtXCI-IDxzcGFuPkxvY2F0aW9uPC9zcGFuPiA8cCBjbGFzcz1cIml0ZW0tdmFsdWVcIj4gPGEgaHJlZj1cInt7bG9jYXRpb25JbmZvcm1hdGlvbi5wbHVzQ29kZX19XCIgY2xhc3M9XCJibHVlLWJvdHRvbS1saW5lLXRoaWNrIHt7I3VubGVzcyBsb2NhdGlvbkluZm9ybWF0aW9uLnBsdXNDb2RlfX1kaXNhYmxlZHt7L3VubGVzc319XCIge3sjdW5sZXNzIGxvY2F0aW9uSW5mb3JtYXRpb24ucGx1c0NvZGV9fWFyaWEtZGlzYWJsZWQ9XCJ0cnVlXCIgdGFiaW5kZXg9XCItMVwie3svdW5sZXNzfX0gYXJpYS1sYWJlbD1cIlZpZXcgbG9jYXRpb24gb24gbWFwXCIgdGFyZ2V0PVwiX2JsYW5rXCIgPiB7eyNpZiBhZGRyZXNzLnN0cmVldEFkZHJlc3N9fXt7YWRkcmVzcy5zdHJlZXRBZGRyZXNzfX17ey9pZn19IHt7I2lmIGFkZHJlc3MuYWRkcmVzc0xvY2FsaXR5fX17e2FkZHJlc3MuYWRkcmVzc0xvY2FsaXR5fX17ey9pZn19IHt7I2lmIGFkZHJlc3MuYWRkcmVzc1JlZ2lvbn19e3thZGRyZXNzLmFkZHJlc3NSZWdpb259fXt7L2lmfX0ge3sjaWYgYWRkcmVzcy5wb3N0YWxDb2RlfX17e2FkZHJlc3MucG9zdGFsQ29kZX19e3svaWZ9fSA8L2E-IDwvcD4gPC9kaXY-IHt7L2lmfX0ge3svd2l0aH19IHt7L2lmfX0ge3sjaWYgY3JlZGVudGlhbFN1YmplY3QucHJvZHVjdC5wcm9kdWN0aW9uRGF0ZX19IDxkaXYgY2xhc3M9XCJ0YWJsZS1pdGVtXCI-IDxzcGFuPkRhdGUgcHJvZHVjZWQ8L3NwYW4-IDxwIGNsYXNzPVwiaXRlbS12YWx1ZVwiPnt7Y3JlZGVudGlhbFN1YmplY3QucHJvZHVjdC5wcm9kdWN0aW9uRGF0ZX19PC9wPiA8L2Rpdj4ge3svaWZ9fSB7eyNpZiBjcmVkZW50aWFsU3ViamVjdC5wcm9kdWN0LmNvdW50cnlPZlByb2R1Y3Rpb259fSA8ZGl2IGNsYXNzPVwidGFibGUtaXRlbVwiPiA8c3Bhbj5Db3VudHJ5PC9zcGFuPiA8cCBjbGFzcz1cIml0ZW0tdmFsdWVcIj57e2NyZWRlbnRpYWxTdWJqZWN0LnByb2R1Y3QuY291bnRyeU9mUHJvZHVjdGlvbn19PC9wPiA8L2Rpdj4ge3svaWZ9fSB7eyNpZiBjcmVkZW50aWFsU3ViamVjdC5wcm9kdWN0LmRpbWVuc2lvbnN9fSA8ZGl2IGNsYXNzPVwidGFibGUtaXRlbVwiPiA8c3Bhbj5EaW1lbnNpb25zPC9zcGFuPiA8cCBjbGFzcz1cIml0ZW0tdmFsdWVcIj4ge3sjaWYgY3JlZGVudGlhbFN1YmplY3QucHJvZHVjdC5kaW1lbnNpb25zLndlaWdodH19IDxzcGFuPldlaWdodDoge3tjcmVkZW50aWFsU3ViamVjdC5wcm9kdWN0LmRpbWVuc2lvbnMud2VpZ2h0LnZhbHVlfX17e2NyZWRlbnRpYWxTdWJqZWN0LnByb2R1Y3QuZGltZW5zaW9ucy53ZWlnaHQudW5pdH19PC9zcGFuPiB7ey9pZn19IHt7I2lmIGNyZWRlbnRpYWxTdWJqZWN0LnByb2R1Y3QuZGltZW5zaW9ucy5sZW5ndGh9fSA8c3Bhbj5MZW5ndGg6IHt7Y3JlZGVudGlhbFN1YmplY3QucHJvZHVjdC5kaW1lbnNpb25zLmxlbmd0aC52YWx1ZX19e3tjcmVkZW50aWFsU3ViamVjdC5wcm9kdWN0LmRpbWVuc2lvbnMubGVuZ3RoLnVuaXR9fTwvc3Bhbj4ge3svaWZ9fSB7eyNpZiBjcmVkZW50aWFsU3ViamVjdC5wcm9kdWN0LmRpbWVuc2lvbnMud2lkdGh9fSA8c3Bhbj5XaWR0aDoge3tjcmVkZW50aWFsU3ViamVjdC5wcm9kdWN0LmRpbWVuc2lvbnMud2lkdGgudmFsdWV9fXt7Y3JlZGVudGlhbFN1YmplY3QucHJvZHVjdC5kaW1lbnNpb25zLndpZHRoLnVuaXR9fTwvc3Bhbj4ge3svaWZ9fSB7eyNpZiBjcmVkZW50aWFsU3ViamVjdC5wcm9kdWN0LmRpbWVuc2lvbnMuaGVpZ2h0fX0gPHNwYW4-SGVpZ2h0OiB7e2NyZWRlbnRpYWxTdWJqZWN0LnByb2R1Y3QuZGltZW5zaW9ucy5oZWlnaHQudmFsdWV9fXt7Y3JlZGVudGlhbFN1YmplY3QucHJvZHVjdC5kaW1lbnNpb25zLmhlaWdodC51bml0fX08L3NwYW4-IHt7L2lmfX0ge3sjaWYgY3JlZGVudGlhbFN1YmplY3QucHJvZHVjdC5kaW1lbnNpb25zLnZvbHVtZX19IDxzcGFuPlZvbHVtZToge3tjcmVkZW50aWFsU3ViamVjdC5wcm9kdWN0LmRpbWVuc2lvbnMudm9sdW1lLnZhbHVlfX17e2NyZWRlbnRpYWxTdWJqZWN0LnByb2R1Y3QuZGltZW5zaW9ucy52b2x1bWUudW5pdH19PC9zcGFuPiB7ey9pZn19IDwvcD4gPC9kaXY-IHt7L2lmfX0gPC9kaXY-IDwvc2VjdGlvbj4ge3sjaWYgY3JlZGVudGlhbFN1YmplY3QuY2lyY3VsYXJpdHlTY29yZWNhcmR9fSA8c2VjdGlvbiBjbGFzcz1cInBhc3Nwb3J0XCI-IDxkaXY-IDxoMyBjbGFzcz1cInNlY3Rpb24tdGl0bGVcIj5DaXJjdWxhcml0eSBTY29yZWNhcmQ8L2gzPiA8cCBjbGFzcz1cInNlY3Rpb24tZGVzY3JpcHRpb25cIj4gVGhlIGNpcmN1bGFyaXR5IFNjb3JlY2FyZCBwcm92aWRlcyBhIHNpbXBsZSBoaWdoIGxldmVsIHN1bW1hcnkgb2YgY2lyY3VsYXJpdHkgcGVyZm9ybWFuY2Ugb2YgdGhlIHByb2R1Y3QuIDwvcD4gPC9kaXY-IDxkaXYgY2xhc3M9XCJwYXNzcG9ydC1ib3hcIj4ge3sjaWYgY3JlZGVudGlhbFN1YmplY3QuY2lyY3VsYXJpdHlTY29yZWNhcmQucmVjeWNsYWJsZUNvbnRlbnR9fSA8ZGl2IGNsYXNzPVwicGFzc3BvcnQtYm94LWl0ZW1cIj4gPGgzPnt7Y3JlZGVudGlhbFN1YmplY3QuY2lyY3VsYXJpdHlTY29yZWNhcmQucmVjeWNsYWJsZUNvbnRlbnR9fSU8L2gzPiA8cD5SZWN5Y2xhYmxlIGNvbnRlbnQ8L3A-IDwvZGl2PiB7ey9pZn19IHt7I2lmIGNyZWRlbnRpYWxTdWJqZWN0LmNpcmN1bGFyaXR5U2NvcmVjYXJkLnJlY3ljbGVkQ29udGVudH19IDxkaXYgY2xhc3M9XCJwYXNzcG9ydC1ib3gtaXRlbVwiPiA8aDM-e3tjcmVkZW50aWFsU3ViamVjdC5jaXJjdWxhcml0eVNjb3JlY2FyZC5yZWN5Y2xlZENvbnRlbnR9fSU8L2gzPiA8cD5SZWN5Y2xlZCBjb250ZW50PC9wPiA8L2Rpdj4ge3svaWZ9fSB7eyNpZiBjcmVkZW50aWFsU3ViamVjdC5jaXJjdWxhcml0eVNjb3JlY2FyZC51dGlsaXR5RmFjdG9yfX0gPGRpdiBjbGFzcz1cInBhc3Nwb3J0LWJveC1pdGVtXCI-IDxoMz57e2NyZWRlbnRpYWxTdWJqZWN0LmNpcmN1bGFyaXR5U2NvcmVjYXJkLnV0aWxpdHlGYWN0b3J9fTwvaDM-IDxwPlV0aWxpdHkgZmFjdG9yPC9wPiA8L2Rpdj4ge3svaWZ9fSB7eyNpZiBjcmVkZW50aWFsU3ViamVjdC5jaXJjdWxhcml0eVNjb3JlY2FyZC5tYXRlcmlhbENpcmN1bGFyaXR5SW5kaWNhdG9yfX0gPGRpdiBjbGFzcz1cInBhc3Nwb3J0LWJveC1pdGVtXCI-IDxoMz57e2NyZWRlbnRpYWxTdWJqZWN0LmNpcmN1bGFyaXR5U2NvcmVjYXJkLm1hdGVyaWFsQ2lyY3VsYXJpdHlJbmRpY2F0b3J9fTwvaDM-IDxwPk1hdGVyaWFsIGNpcmN1bGFyaXR5KjwvcD4gPC9kaXY-IHt7L2lmfX0gPC9kaXY-IHt7I2lmIGNyZWRlbnRpYWxTdWJqZWN0LmNpcmN1bGFyaXR5U2NvcmVjYXJkLm1hdGVyaWFsQ2lyY3VsYXJpdHlJbmRpY2F0b3J9fSA8ZGl2IGNsYXNzPVwicGFzc3BvcnQtYW5ub3RhdGlvblwiPiA8cD4qVGhlIE1hdGVyaWFsIENpcmN1bGFyaXR5IEluZGljYXRvciBwcm92aWRlcyBhbiBvdmVyYWxsIGNpcmN1bGFyaXR5IHNjb3JlIHdoaWNoIGlzIGEgZnVuY3Rpb24gb2YgYWxsIHRocmVlIG9mIHRoZSBlYXJsaWVyIG1lYXN1cmVzLjwvcD4gPC9kaXY-IHt7L2lmfX0gPGRpdiBjbGFzcz1cInRyYWNlYWJpbGl0eS1jYXJkc1wiPiB7eyNpZiBjcmVkZW50aWFsU3ViamVjdC5jaXJjdWxhcml0eVNjb3JlY2FyZC5yZWN5Y2xpbmdJbmZvcm1hdGlvbi5saW5rVVJMfX0gPGEgaHJlZj1cInt7Y3JlZGVudGlhbFN1YmplY3QuY2lyY3VsYXJpdHlTY29yZWNhcmQucmVjeWNsaW5nSW5mb3JtYXRpb24ubGlua1VSTH19XCIgY2xhc3M9XCJ0cmFjZWFiaWxpdHktY2FyZFwiIHRhcmdldD1cIl9ibGFua1wiPiA8ZGl2IGNsYXNzPVwidHJhY2VhYmlsaXR5LWNhcmQtdGV4dFwiPiA8c3ZnIHdpZHRoPVwiMjRcIiBoZWlnaHQ9XCIyNFwiIHZpZXdCb3g9XCIwIDAgMjQgMjRcIiBmaWxsPVwibm9uZVwiIHhtbG5zPVwiaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmdcIiA-IDxwYXRoIGQ9XCJNMjEuODIgMTUuNDJMMTkuMzIgMTkuNzVDMTguODMgMjAuNjEgMTcuOTIgMjEuMDYgMTcgMjFIMTVWMjNMMTIuNSAxOC41TDE1IDE0VjE2SDE3LjgyTDE1LjYgMTIuMTVMMTkuOTMgOS42NUwyMS43MyAxMi43N0MyMi4yNSAxMy41NCAyMi4zMiAxNC41NyAyMS44MiAxNS40MlpNOS4yMTAwMyAzLjA2SDE0LjIxQzE1LjE5IDMuMDYgMTYuMDQgMy42MyAxNi40NSA0LjQ1TDE3LjQ1IDYuMTlMMTkuMTggNS4xOUwxNi41NCA5LjZMMTEuMzkgOS42OUwxMy4xMiA4LjY5TDExLjcxIDYuMjRMOS41MDAwMyAxMC4wOUw1LjE2MDAzIDcuNTlMNi45NjAwMyA0LjQ3QzcuMzcwMDMgMy42NCA4LjIyMDAzIDMuMDYgOS4yMTAwMyAzLjA2Wk01LjA1MDAzIDE5Ljc2TDIuNTUwMDMgMTUuNDNDMi4wNjAwMyAxNC41OCAyLjEzMDAzIDEzLjU2IDIuNjQwMDMgMTIuNzlMMy42NDAwMyAxMS4wNkwxLjkxMDAzIDEwLjA2TDcuMDUwMDMgMTAuMTRMOS43MDAwMyAxNC41Nkw3Ljk3MDAzIDEzLjU2TDYuNTYwMDMgMTZIMTFWMjFINy40MDAwM0M2LjkzMTU0IDIxLjAzMzkgNi40NjI5MyAyMC45MzU3IDYuMDQ3NSAyMC43MTY1QzUuNjMyMDYgMjAuNDk3MyA1LjI4NjQ4IDIwLjE2NTkgNS4wNTAwMyAxOS43NlpcIiBmaWxsPVwidmFyKC0tY29sb3ItaWNvbilcIiBzdHJva2U9XCJ2YXIoLS1jb2xvci1pY29uKVwiIC8-IDwvc3ZnPiA8cD5SZWN5Y2xpbmcgaW5zdHJ1Y3Rpb25zPC9wPiA8L2Rpdj4gPGRpdiBjbGFzcz1cInRyYWNlYWJpbGl0eS1jYXJkLXZpZXctZGV0YWlsc1wiPiA8c3ZnIHdpZHRoPVwiOVwiIGhlaWdodD1cIjE2XCIgdmlld0JveD1cIjAgMCA5IDE2XCIgZmlsbD1cIm5vbmVcIiB4bWxucz1cImh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnXCIgPiA8cGF0aCBkPVwiTTEgMUw4IDhMMSAxNVwiIHN0cm9rZT1cInZhcigtLWNvbG9yLWljb24pXCIgc3Ryb2tlLXdpZHRoPVwiMlwiIHN0cm9rZS1saW5lY2FwPVwicm91bmRcIiBzdHJva2UtbGluZWpvaW49XCJyb3VuZFwiIC8-IDwvc3ZnPiA8L2Rpdj4gPC9hPiB7ey9pZn19IHt7I2lmIGNyZWRlbnRpYWxTdWJqZWN0LmNpcmN1bGFyaXR5U2NvcmVjYXJkLnJlcGFpckluZm9ybWF0aW9uLmxpbmtVUkx9fSA8YSBocmVmPVwie3tjcmVkZW50aWFsU3ViamVjdC5jaXJjdWxhcml0eVNjb3JlY2FyZC5yZXBhaXJJbmZvcm1hdGlvbi5saW5rVVJMfX1cIiBjbGFzcz1cInRyYWNlYWJpbGl0eS1jYXJkXCIgdGFyZ2V0PVwiX2JsYW5rXCI-IDxkaXYgY2xhc3M9XCJ0cmFjZWFiaWxpdHktY2FyZC10ZXh0XCI-IDxzdmcgd2lkdGg9XCIyNFwiIGhlaWdodD1cIjI0XCIgdmlld0JveD1cIjAgMCAyNCAyNFwiIGZpbGw9XCJub25lXCIgeG1sbnM9XCJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2Z1wiID4gPHBhdGggZD1cIk0xOC44NSAyMS45NzVDMTguNzE2NyAyMS45NzUgMTguNTkxNyAyMS45NTQzIDE4LjQ3NSAyMS45MTNDMTguMzU4MyAyMS44NzE3IDE4LjI1IDIxLjgwMDcgMTguMTUgMjEuN0wxMy4wNSAxNi42QzEyLjk1IDE2LjUgMTIuODc5IDE2LjM5MTcgMTIuODM3IDE2LjI3NUMxMi43OTUgMTYuMTU4MyAxMi43NzQzIDE2LjAzMzMgMTIuNzc1IDE1LjlDMTIuNzc1NyAxNS43NjY3IDEyLjc5NjcgMTUuNjQxNyAxMi44MzggMTUuNTI1QzEyLjg3OTMgMTUuNDA4MyAxMi45NSAxNS4zIDEzLjA1IDE1LjJMMTUuMTc1IDEzLjA3NUMxNS4yNzUgMTIuOTc1IDE1LjM4MzMgMTIuOTA0MyAxNS41IDEyLjg2M0MxNS42MTY3IDEyLjgyMTcgMTUuNzQxNyAxMi44MDA3IDE1Ljg3NSAxMi44QzE2LjAwODMgMTIuNzk5MyAxNi4xMzMzIDEyLjgyMDMgMTYuMjUgMTIuODYzQzE2LjM2NjcgMTIuOTA1NyAxNi40NzUgMTIuOTc2MyAxNi41NzUgMTMuMDc1TDIxLjY3NSAxOC4xNzVDMjEuNzc1IDE4LjI3NSAyMS44NDYgMTguMzgzMyAyMS44ODggMTguNUMyMS45MyAxOC42MTY3IDIxLjk1MDcgMTguNzQxNyAyMS45NSAxOC44NzVDMjEuOTQ5MyAxOS4wMDgzIDIxLjkyODcgMTkuMTMzMyAyMS44ODggMTkuMjVDMjEuODQ3MyAxOS4zNjY3IDIxLjc3NjMgMTkuNDc1IDIxLjY3NSAxOS41NzVMMTkuNTUgMjEuN0MxOS40NSAyMS44IDE5LjM0MTcgMjEuODcxIDE5LjIyNSAyMS45MTNDMTkuMTA4MyAyMS45NTUgMTguOTgzMyAyMS45NzU3IDE4Ljg1IDIxLjk3NVpNMTguODUgMTkuNkwxOS41NzUgMTguODc1TDE1LjkgMTUuMkwxNS4xNzUgMTUuOTI1TDE4Ljg1IDE5LjZaTTUuMTI1IDIyQzQuOTkxNjcgMjIgNC44NjI2NyAyMS45NzUgNC43MzggMjEuOTI1QzQuNjEzMzMgMjEuODc1IDQuNTAwNjcgMjEuOCA0LjQgMjEuN0wyLjMgMTkuNkMyLjIgMTkuNSAyLjEyNSAxOS4zODczIDIuMDc1IDE5LjI2MkMyLjAyNSAxOS4xMzY3IDIgMTkuMDA4IDIgMTguODc2QzIgMTguNzQ0IDIuMDI1IDE4LjYxOSAyLjA3NSAxOC41MDFDMi4xMjUgMTguMzgzIDIuMiAxOC4yNzQ3IDIuMyAxOC4xNzZMNy42IDEyLjg3Nkg5LjcyNUwxMC41NzUgMTIuMDI2TDYuNDUgNy45SDUuMDI1TDIgNC44NzVMNC44MjUgMi4wNUw3Ljg1IDUuMDc1VjYuNUwxMS45NzUgMTAuNjI1TDE0Ljg3NSA3LjcyNUwxMy44IDYuNjVMMTUuMiA1LjI1SDEyLjM3NUwxMS42NzUgNC41NUwxNS4yMjUgMUwxNS45MjUgMS43VjQuNTI1TDE3LjMyNSAzLjEyNUwyMC44NzUgNi42NzVDMjEuMTU4MyA2Ljk1ODMzIDIxLjM3NSA3LjI3OTMzIDIxLjUyNSA3LjYzOEMyMS42NzUgNy45OTY2NyAyMS43NSA4LjM3NTY3IDIxLjc1IDguNzc1QzIxLjc1IDkuMTc0MzMgMjEuNjc1IDkuNTU3NjcgMjEuNTI1IDkuOTI1QzIxLjM3NSAxMC4yOTIzIDIxLjE1ODMgMTAuNjE3MyAyMC44NzUgMTAuOUwxOC43NSA4Ljc3NUwxNy4zNSAxMC4xNzVMMTYuMyA5LjEyNUwxMS4xMjUgMTQuM1YxNi40TDUuODI1IDIxLjdDNS43MjUgMjEuOCA1LjYxNjY3IDIxLjg3NSA1LjUgMjEuOTI1QzUuMzgzMzMgMjEuOTc1IDUuMjU4MzMgMjIgNS4xMjUgMjJaTTUuMTI1IDE5LjZMOS4zNzUgMTUuMzVWMTQuNjI1SDguNjVMNC40IDE4Ljg3NUw1LjEyNSAxOS42Wk01LjEyNSAxOS42TDQuNCAxOC44NzVMNC43NzUgMTkuMjI1TDUuMTI1IDE5LjZaXCIgZmlsbD1cInZhcigtLWNvbG9yLWljb24pXCIgc3Ryb2tlPVwidmFyKC0tY29sb3ItaWNvbilcIiAvPiA8L3N2Zz4gPHA-UmVwYWlyIGluc3RydWN0aW9uczwvcD4gPC9kaXY-IDxkaXYgY2xhc3M9XCJ0cmFjZWFiaWxpdHktY2FyZC12aWV3LWRldGFpbHNcIj4gPHN2ZyB3aWR0aD1cIjlcIiBoZWlnaHQ9XCIxNlwiIHZpZXdCb3g9XCIwIDAgOSAxNlwiIGZpbGw9XCJub25lXCIgeG1sbnM9XCJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2Z1wiID4gPHBhdGggZD1cIk0xIDFMOCA4TDEgMTVcIiBzdHJva2U9XCJ2YXIoLS1jb2xvci1pY29uKVwiIHN0cm9rZS13aWR0aD1cIjJcIiBzdHJva2UtbGluZWNhcD1cInJvdW5kXCIgc3Ryb2tlLWxpbmVqb2luPVwicm91bmRcIiAvPiA8L3N2Zz4gPC9kaXY-IDwvYT4ge3svaWZ9fSA8L2Rpdj4gPC9zZWN0aW9uPiB7ey9pZn19IHt7I2lmIGNyZWRlbnRpYWxTdWJqZWN0LmVtaXNzaW9uc1Njb3JlY2FyZH19IDxzZWN0aW9uIGNsYXNzPVwiZW1pc3Npb24tc2NvcmUtY2FyZFwiPiA8ZGl2PiA8aDMgY2xhc3M9XCJzZWN0aW9uLXRpdGxlXCI-RW1pc3Npb25zIFNjb3JlY2FyZDwvaDM-IDxwIGNsYXNzPVwic2VjdGlvbi1kZXNjcmlwdGlvblwiPiBUaGUgRW1pc3Npb25zIFNjb3JlY2FyZCBnaXZlcyBhIGNsZWFyIHNuYXBzaG90IG9mIHRoZSBwcm9kdWN0J3MgZ3JlZW5ob3VzZSBnYXMgKEdIRykgZW1pc3Npb25zIHBlcmZvcm1hbmNlLCBwcm92aWRpbmcgYSBzaW5nbGUgaW5kaWNhdG9yIHRvIGFzc2VzcyBpdHMgb3ZlcmFsbCBlbnZpcm9ubWVudGFsIGltcGFjdC4gPC9wPiA8L2Rpdj4gPGRpdiBjbGFzcz1cInNjb3JlXCI-IDxwIGNsYXNzPVwic2NvcmUtdW5pdFwiPiB7e2NyZWRlbnRpYWxTdWJqZWN0LmVtaXNzaW9uc1Njb3JlY2FyZC5jYXJib25Gb290cHJpbnR9fXt7Y3JlZGVudGlhbFN1YmplY3QuZW1pc3Npb25zU2NvcmVjYXJkLmRlY2xhcmVkVW5pdH19IDwvcD4gPHAgY2xhc3M9XCJzY29yZS1uYW1lXCI-Q28yRXE8L3A-IDwvZGl2PiA8ZGl2IGNsYXNzPVwidGFibGVcIj4gPGRpdiBjbGFzcz1cInRhYmxlLWl0ZW1cIj4gPHNwYW4-U2NvcGUgaW5jbHVkZXM8L3NwYW4-IDxwIGNsYXNzPVwiaXRlbS12YWx1ZVwiPnt7Y3JlZGVudGlhbFN1YmplY3QuZW1pc3Npb25zU2NvcmVjYXJkLm9wZXJhdGlvbmFsU2NvcGV9fTwvcD4gPC9kaXY-IDxkaXYgY2xhc3M9XCJ0YWJsZS1pdGVtXCI-IDxzcGFuPlByaW1hcnkgc291cmNlZCByYXRpbyo8L3NwYW4-IDxwIGNsYXNzPVwiaXRlbS12YWx1ZVwiPnt7Y3JlZGVudGlhbFN1YmplY3QuZW1pc3Npb25zU2NvcmVjYXJkLnByaW1hcnlTb3VyY2VkUmF0aW99fSUgcHJpbWFyeSBzb3VyY2VzPC9wPiA8L2Rpdj4ge3sjaWYgY3JlZGVudGlhbFN1YmplY3QuZW1pc3Npb25zU2NvcmVjYXJkLnJlcG9ydGluZ1N0YW5kYXJkfX0ge3sjaWYgY3JlZGVudGlhbFN1YmplY3QuZW1pc3Npb25zU2NvcmVjYXJkLnJlcG9ydGluZ1N0YW5kYXJkLm5hbWV9fSA8ZGl2IGNsYXNzPVwidGFibGUtaXRlbVwiPiA8c3Bhbj5SZXBvcnRpbmcgc3RhbmRhcmQ8L3NwYW4-IHt7I2lmIGNyZWRlbnRpYWxTdWJqZWN0LmVtaXNzaW9uc1Njb3JlY2FyZC5yZXBvcnRpbmdTdGFuZGFyZC5pZH19IDxhIGhyZWY9XCJ7e2NyZWRlbnRpYWxTdWJqZWN0LmVtaXNzaW9uc1Njb3JlY2FyZC5yZXBvcnRpbmdTdGFuZGFyZC5pZH19XCIgY2xhc3M9XCJibHVlLWJvdHRvbS1saW5lLXRoaWNrXCIgdGFyZ2V0PVwiX2JsYW5rXCI-IHt7Y3JlZGVudGlhbFN1YmplY3QuZW1pc3Npb25zU2NvcmVjYXJkLnJlcG9ydGluZ1N0YW5kYXJkLm5hbWV9fSA8L2E-IHt7ZWxzZX19IDxwIGNsYXNzPVwiaXRlbS12YWx1ZVwiPiB7e2NyZWRlbnRpYWxTdWJqZWN0LmVtaXNzaW9uc1Njb3JlY2FyZC5yZXBvcnRpbmdTdGFuZGFyZC5uYW1lfX0gPC9wPiB7ey9pZn19IDwvZGl2PiB7ey9pZn19IHt7I2lmIGNyZWRlbnRpYWxTdWJqZWN0LmVtaXNzaW9uc1Njb3JlY2FyZC5yZXBvcnRpbmdTdGFuZGFyZC5pc3N1ZURhdGV9fSA8ZGl2IGNsYXNzPVwidGFibGUtaXRlbVwiPiA8c3Bhbj5Jc3N1ZSBkYXRlPC9zcGFuPiA8cCBjbGFzcz1cIml0ZW0tdmFsdWVcIj57e2NyZWRlbnRpYWxTdWJqZWN0LmVtaXNzaW9uc1Njb3JlY2FyZC5yZXBvcnRpbmdTdGFuZGFyZC5pc3N1ZURhdGV9fTwvcD4gPC9kaXY-IHt7L2lmfX0ge3svaWZ9fSA8L2Rpdj4gPGRpdiBjbGFzcz1cInBhc3Nwb3J0LWFubm90YXRpb25cIj4gPHA-KlRoZSBQcmltYXJ5IFNvdXJjZWQgUmF0aW8gc2hvd3MgdGhlIHBlcmNlbnRhZ2Ugb2Ygc2NvcGUgMyBlbWlzc2lvbnMgZGF0YSB0aGF0IGlzIGRpcmVjdGx5IGNvbGxlY3RlZCBmcm9tIGFjdHVhbCBzb3VyY2VzLCByYXRoZXIgdGhhbiBiZWluZyBiYXNlZCBvbiBlc3RpbWF0ZXMuPC9wPiA8L2Rpdj4gPC9zZWN0aW9uPiB7ey9pZn19IHt7I2lmIGNyZWRlbnRpYWxTdWJqZWN0LmNvbmZvcm1pdHlDbGFpbX19IDxzZWN0aW9uIGNsYXNzPVwiZGVjbGFyYXRpb25zXCI-IDxkaXY-IDxoMyBjbGFzcz1cInNlY3Rpb24tdGl0bGVcIj5EZWNsYXJhdGlvbnM8L2gzPiA8L2Rpdj4gPGRpdiBjbGFzcz1cImNhcmRzLWNvbmZvcm1pdGllc1wiPiB7eyNlYWNoIGNyZWRlbnRpYWxTdWJqZWN0LmNvbmZvcm1pdHlDbGFpbX19IDxhcnRpY2xlIGNsYXNzPVwiY2FyZHMtY29uZm9ybWl0eVwiPiA8ZGl2IGNsYXNzPVwiY29uZm9ybWFuY2UtaGVhZGVyXCI-IDxkaXYgY2xhc3M9XCJjb25mb3JtYW5jZS1zdGF0dXNcIj4gPHNwYW4gY2xhc3M9XCJjb25mb3JtYW5jZS1sYWJlbFwiPkNvbmZvcm1hbmNlOjwvc3Bhbj4gPGRpdiBjbGFzcz1cInt7I2lmIGNvbmZvcm1hbmNlfX10YWdzLVZDLWJhZGdlLWdyZWVue3tlbHNlfX10YWdzLVZDLWJhZGdlLXJlZHt7L2lmfX1cIj4ge3sjaWYgY29uZm9ybWFuY2V9fVllc3t7ZWxzZX19Tm97ey9pZn19IDwvZGl2PiA8L2Rpdj4ge3sjaWYgYXNzZXNzbWVudERhdGV9fSA8c3BhbiBjbGFzcz1cImNvbmZvcm1hbmNlLWxhYmVsXCI-QXNzZXNzZWQ6IHt7YXNzZXNzbWVudERhdGV9fTwvc3Bhbj4ge3svaWZ9fSA8L2Rpdj4ge3sjaWYgY29uZm9ybWl0eUV2aWRlbmNlLmxpbmtOYW1lfX0gPGRpdiBjbGFzcz1cImNvbmZvcm1pdHktZGV0YWlsc1wiPnt7Y29uZm9ybWl0eUV2aWRlbmNlLmxpbmtOYW1lfX08L2Rpdj4ge3svaWZ9fSA8ZGl2IGNsYXNzPVwiY29uZm9ybWl0eS1pbmZvXCI-IHt7I2lmIHJlZmVyZW5jZVJlZ3VsYXRpb259fSA8cD4ge3sjaWYgcmVmZXJlbmNlUmVndWxhdGlvbi5uYW1lfX0ge3tyZWZlcmVuY2VSZWd1bGF0aW9uLm5hbWV9fSB7eyNpZiByZWZlcmVuY2VSZWd1bGF0aW9uLmp1cmlzZGljdGlvbkNvdW50cnl9fSBhZG1pbmlzdGVyZWQgaW4ge3tyZWZlcmVuY2VSZWd1bGF0aW9uLmp1cmlzZGljdGlvbkNvdW50cnl9fSB7ey9pZn19IHt7I2lmIHJlZmVyZW5jZVJlZ3VsYXRpb24uYWRtaW5pc3RlcmVkQnl9fSBhZG1pbmlzdGVyZWQgYnkgPGEgaHJlZj1cInt7cmVmZXJlbmNlUmVndWxhdGlvbi5hZG1pbmlzdGVyZWRCeS5pZH19XCIgY2xhc3M9XCJncmF5LWJvdHRvbS1saW5lXCIgdGFyZ2V0PVwiX2JsYW5rXCIgPiB7e3JlZmVyZW5jZVJlZ3VsYXRpb24uYWRtaW5pc3RlcmVkQnkubmFtZX19IDwvYT4ge3svaWZ9fSB7e2Vsc2UgaWYgcmVmZXJlbmNlUmVndWxhdGlvbi5qdXJpc2RpY3Rpb25Db3VudHJ5fX0gQWRtaW5pc3RlcmVkIGluIHt7cmVmZXJlbmNlUmVndWxhdGlvbi5qdXJpc2RpY3Rpb25Db3VudHJ5fX0ge3sjaWYgcmVmZXJlbmNlUmVndWxhdGlvbi5hZG1pbmlzdGVyZWRCeX19IGJ5IDxhIGhyZWY9XCJ7e3JlZmVyZW5jZVJlZ3VsYXRpb24uYWRtaW5pc3RlcmVkQnkuaWR9fVwiIGNsYXNzPVwiZ3JheS1ib3R0b20tbGluZVwiIHRhcmdldD1cIl9ibGFua1wiID4ge3tyZWZlcmVuY2VSZWd1bGF0aW9uLmFkbWluaXN0ZXJlZEJ5Lm5hbWV9fSA8L2E-IHt7L2lmfX0ge3tlbHNlIGlmIHJlZmVyZW5jZVJlZ3VsYXRpb24uYWRtaW5pc3RlcmVkQnl9fSBBZG1pbmlzdGVyZWQgYnkgPGEgaHJlZj1cInt7cmVmZXJlbmNlUmVndWxhdGlvbi5hZG1pbmlzdGVyZWRCeS5pZH19XCIgY2xhc3M9XCJncmF5LWJvdHRvbS1saW5lXCIgdGFyZ2V0PVwiX2JsYW5rXCIgPiB7e3JlZmVyZW5jZVJlZ3VsYXRpb24uYWRtaW5pc3RlcmVkQnkubmFtZX19IDwvYT4ge3svaWZ9fSA8L3A-IHt7L2lmfX0ge3sjaWYgcmVmZXJlbmNlU3RhbmRhcmR9fSB7eyNpZiByZWZlcmVuY2VTdGFuZGFyZC5uYW1lfX0gPHA-IHt7cmVmZXJlbmNlU3RhbmRhcmQubmFtZX19IHt7I2lmIHJlZmVyZW5jZVN0YW5kYXJkLmlzc3VpbmdQYXJ0eX19IGlzc3VlZCBieSA8YSBocmVmPVwie3tyZWZlcmVuY2VTdGFuZGFyZC5pc3N1aW5nUGFydHkuaWR9fVwiIGNsYXNzPVwiZ3JheS1ib3R0b20tbGluZVwiIHRhcmdldD1cIl9ibGFua1wiID4ge3tyZWZlcmVuY2VTdGFuZGFyZC5pc3N1aW5nUGFydHkubmFtZX19IDwvYT4ge3svaWZ9fSA8L3A-IHt7L2lmfX0ge3svaWZ9fSA8L2Rpdj4ge3sjaWYgZGVjbGFyZWRWYWx1ZX19IDxkaXYgY2xhc3M9XCJkZWNsYXJlZC12YWx1ZXNcIj4ge3sjZWFjaCBkZWNsYXJlZFZhbHVlfX0gPGRpdiBjbGFzcz1cImRlY2xhcmVkLXZhbHVlXCI-IDxwPnt7bWV0cmljTmFtZX19IGlzIHt7bWV0cmljVmFsdWUudmFsdWV9fXt7bWV0cmljVmFsdWUudW5pdH19PC9wPiB7eyNpZiBzY29yZX19IDxzcGFuPiBTY29yZToge3tzY29yZX19e3sjaWYgYWNjdXJhY3l9fSB8IEFjY3VyYWN5OiB7e2FjY3VyYWN5fX17ey9pZn19IDwvc3Bhbj4ge3tlbHNlIGlmIGFjY3VyYWN5fX0gPHNwYW4-IEFjY3VyYWN5OiB7e2FjY3VyYWN5fX0gPC9zcGFuPiB7ey9pZn19IDwvZGl2PiB7ey9lYWNofX0gPC9kaXY-IHt7L2lmfX0ge3sjaWYgY29uZm9ybWl0eUV2aWRlbmNlLmxpbmtVUkx9fSA8YSBocmVmPVwie3tjb25mb3JtaXR5RXZpZGVuY2UubGlua1VSTH19XCIgY2xhc3M9XCJ0cmFjZWFiaWxpdHktY2FyZFwiIHRhcmdldD1cIl9ibGFua1wiPiA8ZGl2IGNsYXNzPVwidHJhY2VhYmlsaXR5LWNhcmQtdGV4dFwiPiA8c3ZnIHdpZHRoPVwiMjRcIiBoZWlnaHQ9XCIyNFwiIHZpZXdCb3g9XCIwIDAgMjQgMjRcIiBmaWxsPVwibm9uZVwiIHhtbG5zPVwiaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmdcIiA-IDxwYXRoIGQ9XCJNNSAyMUM0LjQ1IDIxIDMuOTc5MzMgMjAuODA0MyAzLjU4OCAyMC40MTNDMy4xOTY2NyAyMC4wMjE3IDMuMDAwNjcgMTkuNTUwNyAzIDE5VjVDMyA0LjQ1IDMuMTk2IDMuOTc5MzMgMy41ODggMy41ODhDMy45OCAzLjE5NjY3IDQuNDUwNjcgMy4wMDA2NyA1IDNIMTlDMTkuNTUgMyAyMC4wMjEgMy4xOTYgMjAuNDEzIDMuNTg4QzIwLjgwNSAzLjk4IDIxLjAwMDcgNC40NTA2NyAyMSA1VjE5QzIxIDE5LjU1IDIwLjgwNDMgMjAuMDIxIDIwLjQxMyAyMC40MTNDMjAuMDIxNyAyMC44MDUgMTkuNTUwNyAyMS4wMDA3IDE5IDIxSDVaTTUgNVYxOUgxOVY1SDE3VjEyTDE0LjUgMTAuNUwxMiAxMlY1SDVaXCIgZmlsbD1cInZhcigtLWNvbG9yLWljb24pXCIgPjwvcGF0aD4gPC9zdmc-IDxwPkV2aWRlbmNlPC9wPiA8L2Rpdj4gPGRpdiBjbGFzcz1cInRyYWNlYWJpbGl0eS1jYXJkLXZpZXctZGV0YWlsc1wiPiA8c3ZnIHdpZHRoPVwiOVwiIGhlaWdodD1cIjE2XCIgdmlld0JveD1cIjAgMCA5IDE2XCIgZmlsbD1cIm5vbmVcIiB4bWxucz1cImh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnXCIgPiA8cGF0aCBkPVwiTTEgMUw4IDhMMSAxNVwiIHN0cm9rZT1cInZhcigtLWNvbG9yLWljb24pXCIgc3Ryb2tlLXdpZHRoPVwiMlwiIHN0cm9rZS1saW5lY2FwPVwicm91bmRcIiBzdHJva2UtbGluZWpvaW49XCJyb3VuZFwiIC8-IDwvc3ZnPiA8L2Rpdj4gPC9hPiB7ey9pZn19IDwvYXJ0aWNsZT4ge3svZWFjaH19IDwvZGl2PiA8L3NlY3Rpb24-IHt7L2lmfX0ge3sjaWYgY3JlZGVudGlhbFN1YmplY3QubWF0ZXJpYWxzUHJvdmVuYW5jZX19IHt7I2lmIGNyZWRlbnRpYWxTdWJqZWN0Lm1hdGVyaWFsc1Byb3ZlbmFuY2UuMC5tYXNzRnJhY3Rpb259fSB7eyNpZiBjcmVkZW50aWFsU3ViamVjdC5tYXRlcmlhbHNQcm92ZW5hbmNlLjAubmFtZX19IDxzZWN0aW9uIGNsYXNzPVwiY29tcG9zaXRpb25cIj4gPGRpdj4gPGgzIGNsYXNzPVwic2VjdGlvbi10aXRsZVwiPlByb2R1Y3QgQ29tcG9zaXRpb248L2gzPiA8cCBjbGFzcz1cInNlY3Rpb24tZGVzY3JpcHRpb25cIj4gQSBjb21wbGV0ZSBsaXN0IG9mIG1hdGVyaWFscyB0aGF0IG1ha2UgdXAgdGhlIGNvbXBvc2l0aW9uIG9mIHRoaXMgcHJvZHVjdC4gPC9wPiA8L2Rpdj4gPGRpdiBjbGFzcz1cImNvbXBvc2l0aW9uLWJveFwiPiB7eyNlYWNoIGNyZWRlbnRpYWxTdWJqZWN0Lm1hdGVyaWFsc1Byb3ZlbmFuY2V9fSA8YXJ0aWNsZSBjbGFzcz1cImNvbXBvc2l0aW9uLWJveC1pdGVtXCI-IDxkaXYgY2xhc3M9XCJjb21wb3NpdGlvbi1maXJzdC1jb2x1bW5cIj4gPHAgY2xhc3M9XCJjb21wb3NpdGlvbi1wZXJjZW50XCI-e3ttYXNzRnJhY3Rpb259fSU8L3A-IDxkaXY-IDxwIGNsYXNzPVwiY29tcG9zaXRpb24tdGl0bGVcIj4ge3sjaWYgbWFzc319e3ttYXNzLnZhbHVlfX17e21hc3MudW5pdH19IHt7L2lmfX17e25hbWV9fSA8L3A-IDxkaXYgY2xhc3M9XCJjb21wb3NpdGlvbi10YWdcIj4ge3sjaWYgcmVjeWNsZWRNYXNzRnJhY3Rpb259fSA8cCBjbGFzcz1cImNvbXBvc2l0aW9uLXRhZy1pdGVtXCI-UmVjeWNsZWQge3tyZWN5Y2xlZE1hc3NGcmFjdGlvbn19JTwvcD4ge3svaWZ9fSA8IS0tIFRPRE86IElmIGhhemFyZG91cyBpcyBub3QgcHJlc2VudCBpdCB3aWxsIGRpc3BsYXkgdGhlIHRhZyBcIkhhemFyZCBOb1wiIHdoaWNoIG1heSBub3QgYmUgdHJ1ZS4gLS0-IDxwIGNsYXNzPVwiY29tcG9zaXRpb24tdGFnLWl0ZW1cIj5IYXphcmQge3sjaWYgaGF6YXJkb3VzfX1ZZXN7e2Vsc2V9fU5ve3svaWZ9fTwvcD4gPC9kaXY-IHt7I2lmIG1hdGVyaWFsU2FmZXR5SW5mb3JtYXRpb24ubGlua1VSTH19IDxhIGhyZWY9XCJ7e21hdGVyaWFsU2FmZXR5SW5mb3JtYXRpb24ubGlua1VSTH19XCIgY2xhc3M9XCJibHVlLWJvdHRvbS1saW5lLXRoaWNrXCIgdGFyZ2V0PVwiX2JsYW5rXCI-e3ttYXRlcmlhbFNhZmV0eUluZm9ybWF0aW9uLmxpbmtOYW1lfX08L2E-IHt7L2lmfX0gPC9kaXY-IDwvZGl2PiB7eyNpZiBvcmlnaW5Db3VudHJ5fX0gPGRpdiBjbGFzcz1cImNvdW50cnktY29kZVwiPnt7b3JpZ2luQ291bnRyeX19PC9kaXY-IHt7L2lmfX0gPC9hcnRpY2xlPiB7ey9lYWNofX0gPC9kaXY-IDwvc2VjdGlvbj4ge3svaWZ9fSB7ey9pZn19IHt7L2lmfX0ge3sjaWYgY3JlZGVudGlhbFN1YmplY3QudHJhY2VhYmlsaXR5SW5mb3JtYXRpb259fSA8c2VjdGlvbiBjbGFzcz1cImhpc3RvcnlcIj4gPGRpdj4gPGgzIGNsYXNzPVwic2VjdGlvbi10aXRsZVwiPkhpc3Rvcnk8L2gzPiA8L2Rpdj4ge3sjaWYgY3JlZGVudGlhbFN1YmplY3QuZHVlRGlsaWdlbmNlRGVjbGFyYXRpb24ubGlua1VSTH19IDxhIGhyZWY9XCJ7e2NyZWRlbnRpYWxTdWJqZWN0LmR1ZURpbGlnZW5jZURlY2xhcmF0aW9uLmxpbmtVUkx9fVwiIGNsYXNzPVwidHJhY2VhYmlsaXR5LWNhcmRcIiB0YXJnZXQ9XCJfYmxhbmtcIj4gPGRpdiBjbGFzcz1cInRyYWNlYWJpbGl0eS1jYXJkLXRleHRcIj4gPHN2ZyB3aWR0aD1cIjI0XCIgaGVpZ2h0PVwiMjRcIiB2aWV3Qm94PVwiMCAwIDI0IDI0XCIgZmlsbD1cIm5vbmVcIiB4bWxucz1cImh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnXCIgPiA8cGF0aCBkPVwiTTUgMjFDNC40NSAyMSAzLjk3OTMzIDIwLjgwNDMgMy41ODggMjAuNDEzQzMuMTk2NjcgMjAuMDIxNyAzLjAwMDY3IDE5LjU1MDcgMyAxOVY1QzMgNC40NSAzLjE5NiAzLjk3OTMzIDMuNTg4IDMuNTg4QzMuOTggMy4xOTY2NyA0LjQ1MDY3IDMuMDAwNjcgNSAzSDE5QzE5LjU1IDMgMjAuMDIxIDMuMTk2IDIwLjQxMyAzLjU4OEMyMC44MDUgMy45OCAyMS4wMDA3IDQuNDUwNjcgMjEgNVYxOUMyMSAxOS41NSAyMC44MDQzIDIwLjAyMSAyMC40MTMgMjAuNDEzQzIwLjAyMTcgMjAuODA1IDE5LjU1MDcgMjEuMDAwNyAxOSAyMUg1Wk01IDVWMTlIMTlWNUgxN1YxMkwxNC41IDEwLjVMMTIgMTJWNUg1WlwiIGZpbGw9XCJ2YXIoLS1jb2xvci1pY29uKVwiID48L3BhdGg-IDwvc3ZnPiA8cD5TdXBwbHkgY2hhaW4gZHVlIGRpbGlnZW5jZSByZXBvcnQ8L3A-IDwvZGl2PiA8ZGl2IGNsYXNzPVwidHJhY2VhYmlsaXR5LWNhcmQtdmlldy1kZXRhaWxzXCI-IDxzdmcgd2lkdGg9XCI5XCIgaGVpZ2h0PVwiMTZcIiB2aWV3Qm94PVwiMCAwIDkgMTZcIiBmaWxsPVwibm9uZVwiIHhtbG5zPVwiaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmdcIiA-IDxwYXRoIGQ9XCJNMSAxTDggOEwxIDE1XCIgc3Ryb2tlPVwidmFyKC0tY29sb3ItaWNvbilcIiBzdHJva2Utd2lkdGg9XCIyXCIgc3Ryb2tlLWxpbmVjYXA9XCJyb3VuZFwiIHN0cm9rZS1saW5lam9pbj1cInJvdW5kXCIgLz4gPC9zdmc-IDwvZGl2PiA8L2E-IHt7L2lmfX0ge3sjZWFjaCBjcmVkZW50aWFsU3ViamVjdC50cmFjZWFiaWxpdHlJbmZvcm1hdGlvbn19IDxkaXYgY2xhc3M9XCJoaXN0b3J5LXZhbHVlLWNoYWluXCI-IHt7I2lmIHZhbHVlQ2hhaW5Qcm9jZXNzfX0gPHA-e3t2YWx1ZUNoYWluUHJvY2Vzc319PC9wPiB7eyNpZiB2ZXJpZmllZFJhdGlvfX0gPGRpdiBjbGFzcz1cInZlcmlmaWVkLXJhdGlvXCI-IDxwPlZlcmlmaWVkIHJhdGlvIHt7dmVyaWZpZWRSYXRpb319PC9wPiA8L2Rpdj4ge3svaWZ9fSB7ey9pZn19IDwvZGl2PiB7eyNpZiB0cmFjZWFiaWxpdHlFdmVudH19IDxkaXY-IHt7I2VhY2ggdHJhY2VhYmlsaXR5RXZlbnR9fSB7eyNpZiBsaW5rTmFtZX19IHt7I2lmIGxpbmtVUkx9fSA8ZGl2IGNsYXNzPVwiaGlzdG9yeS1pdGVtXCI-IDxzcGFuPnt7bGlua05hbWV9fTwvc3Bhbj4gPGEgaHJlZj1cInt7bGlua1VSTH19XCIgY2xhc3M9XCJibHVlLWJvdHRvbS1saW5lLXRoaWNrXCIgdGFyZ2V0PVwiX2JsYW5rXCI-VmlldzwvYT4gPC9kaXY-IHt7L2lmfX0ge3svaWZ9fSB7ey9lYWNofX0gPC9kaXY-IHt7L2lmfX0ge3svZWFjaH19IDwvc2VjdGlvbj4ge3svaWZ9fSA8c2VjdGlvbiBjbGFzcz1cImlzc3VlZC1ieVwiPiA8ZGl2PiA8aDMgY2xhc3M9XCJzZWN0aW9uLXRpdGxlXCI-UGFzc3BvcnQgSXNzdWVkIEJ5PC9oMz4gPC9kaXY-IDxkaXYgY2xhc3M9XCJ0YWJsZVwiPiA8ZGl2IGNsYXNzPVwidGFibGUtaXRlbVwiPiA8c3Bhbj5PcmdhbmlzYXRpb248L3NwYW4-IDxwIGNsYXNzPVwiaXRlbS12YWx1ZVwiPnt7aXNzdWVyLm5hbWV9fTwvcD4gPC9kaXY-IDxkaXYgY2xhc3M9XCJ0YWJsZS1pdGVtXCI-IDxzcGFuPlJlZ2lzdGVyZWQgSUQ8L3NwYW4-IDxhIGhyZWY9XCJ7e2lzc3Vlci5pZH19XCIgY2xhc3M9XCJibHVlLWJvdHRvbS1saW5lLXRoaWNrXCIgdGFyZ2V0PVwiX2JsYW5rXCI-e3tpc3N1ZXIuaWR9fTwvYT4gPC9kaXY-IHt7I2lmIHZhbGlkRnJvbX19IDxkaXYgY2xhc3M9XCJ0YWJsZS1pdGVtXCI-IDxzcGFuPlZhbGlkIGZyb208L3NwYW4-IDxwIGNsYXNzPVwiaXRlbS12YWx1ZVwiPnt7dmFsaWRGcm9tfX08L3A-IDwvZGl2PiB7ey9pZn19IHt7I2lmIHZhbGlkVW50aWx9fSA8ZGl2IGNsYXNzPVwidGFibGUtaXRlbVwiPiA8c3Bhbj5WYWxpZCB0bzwvc3Bhbj4gPHAgY2xhc3M9XCJpdGVtLXZhbHVlXCI-e3t2YWxpZFVudGlsfX08L3A-IDwvZGl2PiB7ey9pZn19IDwvZGl2PiA8L3NlY3Rpb24-IDxmb290ZXI-IDxwPiBUaGlzIERpZ2l0YWwgUHJvZHVjdCBQYXNzcG9ydCAoRFBQKSBpcyBhIGRpZ2l0YWwgcmVjb3JkIG9mIHRoZSBwcm9kdWN0J3Mgc3VzdGFpbmFiaWxpdHkgYW5kIGVudmlyb25tZW50YWwgcGVyZm9ybWFuY2UsIGVuc3VyaW5nIHRyYW5zcGFyZW5jeSBhbmQgYWNjb3VudGFiaWxpdHkgaW4gbGluZSB3aXRoIFVOVFAgc3RhbmRhcmRzLiBGb3IgbW9yZSBpbmZvcm1hdGlvbiB2aXNpdCA8YSBocmVmPVwiaHR0cHM6Ly91bmNlZmFjdC5naXRodWIuaW8vc3BlYy11bnRwL1wiIGNsYXNzPVwiZ3JheS1ib3R0b20tbGluZVwiIHRhcmdldD1cIl9ibGFua1wiPnVuY2VmYWN0LmdpdGh1Yi5pby9zcGVjLXVudHAvPC9hPi4gPC9wPiA8L2Zvb3Rlcj4gPC9kaXY-IDwvYm9keT48L2h0bWw-In1dfQ.RQduJiUVKinv0ytXB9LtumEphVb5lOiWS0lOgHgWBpXr7D8NDSHFOZ78pllrbVRC8M1oI6sx6tvRetcME4MYCA" +} diff --git a/tests/fixtures/samples_report.baseline.md b/tests/fixtures/samples_report.baseline.md new file mode 100644 index 0000000..3c4a8c1 --- /dev/null +++ b/tests/fixtures/samples_report.baseline.md @@ -0,0 +1,255 @@ +# DPP Sample Evaluation Report + +## Summary + +- **Total URLs**: 16 +- **Successfully fetched**: 13 +- **Failed**: 3 + +## By Recommendation + +### EXCELLENT (2) + +- `opensource_unicc_org_untp-digital-product-passport-v0.3.10.json`: Verifiable Credential with DPP structure + - URL: https://opensource.unicc.org/11dot2/spec-untp/-/raw/main/website/samples/untp-digital-product-passport-v0.3.10.json +- `test_uncefact_org_untp-dpp-instance-0.6.0.json`: Verifiable Credential with DPP structure + - URL: https://test.uncefact.org/vocabulary/untp/dpp/untp-dpp-instance-0.6.0.json + +### GOOD (4) + +- `test_uncefact_org_DigitalIdentityAnchor-instance-0.6.1.json`: Verifiable Credential structure + - URL: https://test.uncefact.org/vocabulary/untp/dia/DigitalIdentityAnchor-instance-0.6.1.json +- `opensource_unicc_org_untp-digital-facility-record-v0.3.9.json`: Verifiable Credential structure + - URL: https://opensource.unicc.org/phila/spec-untp/-/raw/main/website/samples/untp-digital-facility-record-v0.3.9.json +- `BatteryPassDataModel_BatteryPass_GeneralProductInformation-payload.json`: Battery Pass data + - URL: https://batterypass.github.io/BatteryPassDataModel/BatteryPass/io.BatteryPass.GeneralProductInformation/1.2.0/gen/GeneralProductInformation-payload.json +- `batterypass_BatteryPassDataModel_GeneralProductInformation-ld.json`: Battery Pass data + - URL: https://raw.githubusercontent.com/batterypass/BatteryPassDataModel/refs/heads/main/BatteryPass/io.BatteryPass.GeneralProductInformation/1.2.0/gen/GeneralProductInformation-ld.json + +### MODERATE (4) + +- `eclipse-tractusx_sldt-semantic-models_BatteryPass.json`: DPP-like structure without VC wrapper + - URL: https://raw.githubusercontent.com/eclipse-tractusx/sldt-semantic-models/main/io.catenax.battery.battery_pass/6.0.0/gen/BatteryPass.json +- `batterypass_BatteryPassDataModel_Circularity-ld.json`: DPP-like structure without VC wrapper + - URL: https://raw.githubusercontent.com/batterypass/BatteryPassDataModel/refs/heads/main/BatteryPass/io.BatteryPass.Circularity/1.2.0/gen/Circularity-ld.json +- `batterypass_BatteryPassDataModel_MaterialComposition-ld.json`: DPP-like structure without VC wrapper + - URL: https://raw.githubusercontent.com/batterypass/BatteryPassDataModel/refs/heads/main/BatteryPass/io.BatteryPass.MaterialComposition/1.2.0/gen/MaterialComposition-ld.json +- `nfc-forum_org_long-dpp-example.json`: DPP-like structure without VC wrapper + - URL: https://nfc-forum.org/ndpp/long-dpp-example.json + +### MAYBE (3) + +- `untp-verifiable-credentials_s3_amazonaws_com_bc075c5f-2304-4b3f-bb24-46d9fa9a8e60.json`: JSON-LD but structure unclear + - URL: https://untp-verifiable-credentials.s3.amazonaws.com/bc075c5f-2304-4b3f-bb24-46d9fa9a8e60.json +- `schemas_testing_breathable-t-shirt.json`: JSON-LD but structure unclear + - URL: https://spherity.github.io/schemas/testing/breathable-t-shirt.json +- `batterypass_BatteryPassDataModel_CarbonFootprintForBatteries-ld.json`: JSON-LD but structure unclear + - URL: https://raw.githubusercontent.com/batterypass/BatteryPassDataModel/refs/heads/main/BatteryPass/io.BatteryPass.CarbonFootprint/1.2.0/gen/CarbonFootprintForBatteries-ld.json + +### FAILED (3) + +- `zenodo_org_untp-dpp-instance-0.5.0-computer.json`: Invalid JSON: Expecting value: line 2 column 1 (char 1) + - URL: https://zenodo.org/records/15279026/preview/untp-dpp-instance-0.5.0-computer.json.txt +- `BatteryPassDataModel_BatteryPass_CarbonFootprintForBatteries-payload.json`: Invalid JSON: Expecting value: line 1 column 1 (char 0) + - URL: https://batterypass.github.io/BatteryPassDataModel/BatteryPass/io.BatteryPass.CarbonFootprint/1.2.0/gen/CarbonFootprintForBatteries-payload.json +- `BatteryPassDataModel_BatteryPass_MaterialComposition-payload.json`: Invalid JSON: Expecting value: line 1 column 1 (char 0) + - URL: https://batterypass.github.io/BatteryPassDataModel/BatteryPass/io.BatteryPass.MaterialComposition/1.2.0/gen/MaterialComposition-payload.json + +## Detailed Evaluation + +### untp-verifiable-credentials_s3_amazonaws_com_bc075c5f-2304-4b3f-bb24-46d9fa9a8e60.json + +- **URL**: https://untp-verifiable-credentials.s3.amazonaws.com/bc075c5f-2304-4b3f-bb24-46d9fa9a8e60.json +- **Hash**: 7fdae740e64218ab +- **Recommendation**: maybe +- **Is JSON-LD**: True +- **Is VC**: False +- **Is DPP-like**: False +- **Is Battery Pass**: False +- **Is Schema**: False +- **Type**: EnvelopedVerifiableCredential +- **Top keys**: @context, type, id +- **Notes**: JSON-LD but structure unclear + +### schemas_testing_breathable-t-shirt.json + +- **URL**: https://spherity.github.io/schemas/testing/breathable-t-shirt.json +- **Hash**: f5132472ac04920b +- **Recommendation**: maybe +- **Is JSON-LD**: True +- **Is VC**: False +- **Is DPP-like**: False +- **Is Battery Pass**: False +- **Is Schema**: False +- **Type**: None +- **Top keys**: @context +- **Notes**: JSON-LD but structure unclear + +### eclipse-tractusx_sldt-semantic-models_BatteryPass.json + +- **URL**: https://raw.githubusercontent.com/eclipse-tractusx/sldt-semantic-models/main/io.catenax.battery.battery_pass/6.0.0/gen/BatteryPass.json +- **Hash**: 50afbb8d50f3be29 +- **Recommendation**: moderate +- **Is JSON-LD**: False +- **Is VC**: False +- **Is DPP-like**: True +- **Is Battery Pass**: False +- **Is Schema**: False +- **Type**: None +- **Top keys**: characteristics, metadata, commercial, identification, performance, sources, materials, safety, handling, conformity +- **Notes**: DPP-like structure without VC wrapper + +### zenodo_org_untp-dpp-instance-0.5.0-computer.json + +- **URL**: https://zenodo.org/records/15279026/preview/untp-dpp-instance-0.5.0-computer.json.txt +- **Error**: Invalid JSON: Expecting value: line 2 column 1 (char 1) + +### test_uncefact_org_DigitalIdentityAnchor-instance-0.6.1.json + +- **URL**: https://test.uncefact.org/vocabulary/untp/dia/DigitalIdentityAnchor-instance-0.6.1.json +- **Hash**: 6784faa60f59cb76 +- **Recommendation**: good +- **Is JSON-LD**: True +- **Is VC**: True +- **Is DPP-like**: False +- **Is Battery Pass**: False +- **Is Schema**: False +- **Type**: ['DigitalIdentityAnchor', 'VerifiableCredential'] +- **Top keys**: type, @context, id, issuer, validFrom, validUntil, credentialSubject +- **Notes**: Verifiable Credential structure + +### opensource_unicc_org_untp-digital-facility-record-v0.3.9.json + +- **URL**: https://opensource.unicc.org/phila/spec-untp/-/raw/main/website/samples/untp-digital-facility-record-v0.3.9.json +- **Hash**: 5a6025ab1335864f +- **Recommendation**: good +- **Is JSON-LD**: True +- **Is VC**: True +- **Is DPP-like**: False +- **Is Battery Pass**: False +- **Is Schema**: False +- **Type**: ['DigitalFacilityRecord', 'VerifiableCredential'] +- **Top keys**: type, @context, id, issuer, validFrom, validUntil, credentialSubject +- **Notes**: Verifiable Credential structure + +### opensource_unicc_org_untp-digital-product-passport-v0.3.10.json + +- **URL**: https://opensource.unicc.org/11dot2/spec-untp/-/raw/main/website/samples/untp-digital-product-passport-v0.3.10.json +- **Hash**: 5b112fea72fc74b6 +- **Recommendation**: excellent +- **Is JSON-LD**: True +- **Is VC**: True +- **Is DPP-like**: True +- **Is Battery Pass**: False +- **Is Schema**: False +- **Type**: ['DigitalProductPassport', 'VerifiableCredential'] +- **Top keys**: type, @context, id, issuer, validFrom, validUntil, credentialSubject +- **Notes**: Verifiable Credential with DPP structure + +### test_uncefact_org_untp-dpp-instance-0.6.0.json + +- **URL**: https://test.uncefact.org/vocabulary/untp/dpp/untp-dpp-instance-0.6.0.json +- **Hash**: dceb94862b90bce6 +- **Recommendation**: excellent +- **Is JSON-LD**: True +- **Is VC**: True +- **Is DPP-like**: True +- **Is Battery Pass**: False +- **Is Schema**: False +- **Type**: ['DigitalProductPassport', 'VerifiableCredential'] +- **Top keys**: type, @context, id, issuer, validFrom, validUntil, credentialSubject +- **Notes**: Verifiable Credential with DPP structure + +### BatteryPassDataModel_BatteryPass_GeneralProductInformation-payload.json + +- **URL**: https://batterypass.github.io/BatteryPassDataModel/BatteryPass/io.BatteryPass.GeneralProductInformation/1.2.0/gen/GeneralProductInformation-payload.json +- **Hash**: d9d8393364648ed9 +- **Recommendation**: good +- **Is JSON-LD**: False +- **Is VC**: False +- **Is DPP-like**: True +- **Is Battery Pass**: True +- **Is Schema**: False +- **Type**: None +- **Top keys**: batteryCategory, operatorInformation, productIdentifier, batteryStatus, puttingIntoService, batteryMass, manufacturingDate, batteryPassportIdentifier, warrentyPeriod, manufacturerInformation +- **Notes**: Battery Pass data + +### BatteryPassDataModel_BatteryPass_CarbonFootprintForBatteries-payload.json + +- **URL**: https://batterypass.github.io/BatteryPassDataModel/BatteryPass/io.BatteryPass.CarbonFootprint/1.2.0/gen/CarbonFootprintForBatteries-payload.json +- **Error**: Invalid JSON: Expecting value: line 1 column 1 (char 0) + +### batterypass_BatteryPassDataModel_GeneralProductInformation-ld.json + +- **URL**: https://raw.githubusercontent.com/batterypass/BatteryPassDataModel/refs/heads/main/BatteryPass/io.BatteryPass.GeneralProductInformation/1.2.0/gen/GeneralProductInformation-ld.json +- **Hash**: df821e9ad855ca75 +- **Recommendation**: good +- **Is JSON-LD**: True +- **Is VC**: False +- **Is DPP-like**: True +- **Is Battery Pass**: True +- **Is Schema**: False +- **Type**: None +- **Top keys**: @graph, @context +- **Notes**: Battery Pass data + +### batterypass_BatteryPassDataModel_CarbonFootprintForBatteries-ld.json + +- **URL**: https://raw.githubusercontent.com/batterypass/BatteryPassDataModel/refs/heads/main/BatteryPass/io.BatteryPass.CarbonFootprint/1.2.0/gen/CarbonFootprintForBatteries-ld.json +- **Hash**: ebcb4870f6fd59e2 +- **Recommendation**: maybe +- **Is JSON-LD**: True +- **Is VC**: False +- **Is DPP-like**: False +- **Is Battery Pass**: False +- **Is Schema**: False +- **Type**: None +- **Top keys**: @graph, @context +- **Notes**: JSON-LD but structure unclear + +### batterypass_BatteryPassDataModel_Circularity-ld.json + +- **URL**: https://raw.githubusercontent.com/batterypass/BatteryPassDataModel/refs/heads/main/BatteryPass/io.BatteryPass.Circularity/1.2.0/gen/Circularity-ld.json +- **Hash**: bcb5d1e4c3e1822b +- **Recommendation**: moderate +- **Is JSON-LD**: True +- **Is VC**: False +- **Is DPP-like**: True +- **Is Battery Pass**: False +- **Is Schema**: False +- **Type**: None +- **Top keys**: @graph, @context +- **Notes**: DPP-like structure without VC wrapper + +### batterypass_BatteryPassDataModel_MaterialComposition-ld.json + +- **URL**: https://raw.githubusercontent.com/batterypass/BatteryPassDataModel/refs/heads/main/BatteryPass/io.BatteryPass.MaterialComposition/1.2.0/gen/MaterialComposition-ld.json +- **Hash**: e0692ea9b1f7a837 +- **Recommendation**: moderate +- **Is JSON-LD**: True +- **Is VC**: False +- **Is DPP-like**: True +- **Is Battery Pass**: False +- **Is Schema**: False +- **Type**: None +- **Top keys**: @graph, @context +- **Notes**: DPP-like structure without VC wrapper + +### nfc-forum_org_long-dpp-example.json + +- **URL**: https://nfc-forum.org/ndpp/long-dpp-example.json +- **Hash**: 57c0c2ed05527cd0 +- **Recommendation**: moderate +- **Is JSON-LD**: False +- **Is VC**: False +- **Is DPP-like**: True +- **Is Battery Pass**: False +- **Is Schema**: False +- **Type**: None +- **Top keys**: productID, productName, manufacturer, productionDate, expiryDate, materials, environmentalImpact, compliance, endOfLifeInstructions, digitalPassportLink +- **Notes**: DPP-like structure without VC wrapper + +### BatteryPassDataModel_BatteryPass_MaterialComposition-payload.json + +- **URL**: https://batterypass.github.io/BatteryPassDataModel/BatteryPass/io.BatteryPass.MaterialComposition/1.2.0/gen/MaterialComposition-payload.json +- **Error**: Invalid JSON: Expecting value: line 1 column 1 (char 0) diff --git a/tests/fixtures/samples_report.md b/tests/fixtures/samples_report.md new file mode 100644 index 0000000..3c4a8c1 --- /dev/null +++ b/tests/fixtures/samples_report.md @@ -0,0 +1,255 @@ +# DPP Sample Evaluation Report + +## Summary + +- **Total URLs**: 16 +- **Successfully fetched**: 13 +- **Failed**: 3 + +## By Recommendation + +### EXCELLENT (2) + +- `opensource_unicc_org_untp-digital-product-passport-v0.3.10.json`: Verifiable Credential with DPP structure + - URL: https://opensource.unicc.org/11dot2/spec-untp/-/raw/main/website/samples/untp-digital-product-passport-v0.3.10.json +- `test_uncefact_org_untp-dpp-instance-0.6.0.json`: Verifiable Credential with DPP structure + - URL: https://test.uncefact.org/vocabulary/untp/dpp/untp-dpp-instance-0.6.0.json + +### GOOD (4) + +- `test_uncefact_org_DigitalIdentityAnchor-instance-0.6.1.json`: Verifiable Credential structure + - URL: https://test.uncefact.org/vocabulary/untp/dia/DigitalIdentityAnchor-instance-0.6.1.json +- `opensource_unicc_org_untp-digital-facility-record-v0.3.9.json`: Verifiable Credential structure + - URL: https://opensource.unicc.org/phila/spec-untp/-/raw/main/website/samples/untp-digital-facility-record-v0.3.9.json +- `BatteryPassDataModel_BatteryPass_GeneralProductInformation-payload.json`: Battery Pass data + - URL: https://batterypass.github.io/BatteryPassDataModel/BatteryPass/io.BatteryPass.GeneralProductInformation/1.2.0/gen/GeneralProductInformation-payload.json +- `batterypass_BatteryPassDataModel_GeneralProductInformation-ld.json`: Battery Pass data + - URL: https://raw.githubusercontent.com/batterypass/BatteryPassDataModel/refs/heads/main/BatteryPass/io.BatteryPass.GeneralProductInformation/1.2.0/gen/GeneralProductInformation-ld.json + +### MODERATE (4) + +- `eclipse-tractusx_sldt-semantic-models_BatteryPass.json`: DPP-like structure without VC wrapper + - URL: https://raw.githubusercontent.com/eclipse-tractusx/sldt-semantic-models/main/io.catenax.battery.battery_pass/6.0.0/gen/BatteryPass.json +- `batterypass_BatteryPassDataModel_Circularity-ld.json`: DPP-like structure without VC wrapper + - URL: https://raw.githubusercontent.com/batterypass/BatteryPassDataModel/refs/heads/main/BatteryPass/io.BatteryPass.Circularity/1.2.0/gen/Circularity-ld.json +- `batterypass_BatteryPassDataModel_MaterialComposition-ld.json`: DPP-like structure without VC wrapper + - URL: https://raw.githubusercontent.com/batterypass/BatteryPassDataModel/refs/heads/main/BatteryPass/io.BatteryPass.MaterialComposition/1.2.0/gen/MaterialComposition-ld.json +- `nfc-forum_org_long-dpp-example.json`: DPP-like structure without VC wrapper + - URL: https://nfc-forum.org/ndpp/long-dpp-example.json + +### MAYBE (3) + +- `untp-verifiable-credentials_s3_amazonaws_com_bc075c5f-2304-4b3f-bb24-46d9fa9a8e60.json`: JSON-LD but structure unclear + - URL: https://untp-verifiable-credentials.s3.amazonaws.com/bc075c5f-2304-4b3f-bb24-46d9fa9a8e60.json +- `schemas_testing_breathable-t-shirt.json`: JSON-LD but structure unclear + - URL: https://spherity.github.io/schemas/testing/breathable-t-shirt.json +- `batterypass_BatteryPassDataModel_CarbonFootprintForBatteries-ld.json`: JSON-LD but structure unclear + - URL: https://raw.githubusercontent.com/batterypass/BatteryPassDataModel/refs/heads/main/BatteryPass/io.BatteryPass.CarbonFootprint/1.2.0/gen/CarbonFootprintForBatteries-ld.json + +### FAILED (3) + +- `zenodo_org_untp-dpp-instance-0.5.0-computer.json`: Invalid JSON: Expecting value: line 2 column 1 (char 1) + - URL: https://zenodo.org/records/15279026/preview/untp-dpp-instance-0.5.0-computer.json.txt +- `BatteryPassDataModel_BatteryPass_CarbonFootprintForBatteries-payload.json`: Invalid JSON: Expecting value: line 1 column 1 (char 0) + - URL: https://batterypass.github.io/BatteryPassDataModel/BatteryPass/io.BatteryPass.CarbonFootprint/1.2.0/gen/CarbonFootprintForBatteries-payload.json +- `BatteryPassDataModel_BatteryPass_MaterialComposition-payload.json`: Invalid JSON: Expecting value: line 1 column 1 (char 0) + - URL: https://batterypass.github.io/BatteryPassDataModel/BatteryPass/io.BatteryPass.MaterialComposition/1.2.0/gen/MaterialComposition-payload.json + +## Detailed Evaluation + +### untp-verifiable-credentials_s3_amazonaws_com_bc075c5f-2304-4b3f-bb24-46d9fa9a8e60.json + +- **URL**: https://untp-verifiable-credentials.s3.amazonaws.com/bc075c5f-2304-4b3f-bb24-46d9fa9a8e60.json +- **Hash**: 7fdae740e64218ab +- **Recommendation**: maybe +- **Is JSON-LD**: True +- **Is VC**: False +- **Is DPP-like**: False +- **Is Battery Pass**: False +- **Is Schema**: False +- **Type**: EnvelopedVerifiableCredential +- **Top keys**: @context, type, id +- **Notes**: JSON-LD but structure unclear + +### schemas_testing_breathable-t-shirt.json + +- **URL**: https://spherity.github.io/schemas/testing/breathable-t-shirt.json +- **Hash**: f5132472ac04920b +- **Recommendation**: maybe +- **Is JSON-LD**: True +- **Is VC**: False +- **Is DPP-like**: False +- **Is Battery Pass**: False +- **Is Schema**: False +- **Type**: None +- **Top keys**: @context +- **Notes**: JSON-LD but structure unclear + +### eclipse-tractusx_sldt-semantic-models_BatteryPass.json + +- **URL**: https://raw.githubusercontent.com/eclipse-tractusx/sldt-semantic-models/main/io.catenax.battery.battery_pass/6.0.0/gen/BatteryPass.json +- **Hash**: 50afbb8d50f3be29 +- **Recommendation**: moderate +- **Is JSON-LD**: False +- **Is VC**: False +- **Is DPP-like**: True +- **Is Battery Pass**: False +- **Is Schema**: False +- **Type**: None +- **Top keys**: characteristics, metadata, commercial, identification, performance, sources, materials, safety, handling, conformity +- **Notes**: DPP-like structure without VC wrapper + +### zenodo_org_untp-dpp-instance-0.5.0-computer.json + +- **URL**: https://zenodo.org/records/15279026/preview/untp-dpp-instance-0.5.0-computer.json.txt +- **Error**: Invalid JSON: Expecting value: line 2 column 1 (char 1) + +### test_uncefact_org_DigitalIdentityAnchor-instance-0.6.1.json + +- **URL**: https://test.uncefact.org/vocabulary/untp/dia/DigitalIdentityAnchor-instance-0.6.1.json +- **Hash**: 6784faa60f59cb76 +- **Recommendation**: good +- **Is JSON-LD**: True +- **Is VC**: True +- **Is DPP-like**: False +- **Is Battery Pass**: False +- **Is Schema**: False +- **Type**: ['DigitalIdentityAnchor', 'VerifiableCredential'] +- **Top keys**: type, @context, id, issuer, validFrom, validUntil, credentialSubject +- **Notes**: Verifiable Credential structure + +### opensource_unicc_org_untp-digital-facility-record-v0.3.9.json + +- **URL**: https://opensource.unicc.org/phila/spec-untp/-/raw/main/website/samples/untp-digital-facility-record-v0.3.9.json +- **Hash**: 5a6025ab1335864f +- **Recommendation**: good +- **Is JSON-LD**: True +- **Is VC**: True +- **Is DPP-like**: False +- **Is Battery Pass**: False +- **Is Schema**: False +- **Type**: ['DigitalFacilityRecord', 'VerifiableCredential'] +- **Top keys**: type, @context, id, issuer, validFrom, validUntil, credentialSubject +- **Notes**: Verifiable Credential structure + +### opensource_unicc_org_untp-digital-product-passport-v0.3.10.json + +- **URL**: https://opensource.unicc.org/11dot2/spec-untp/-/raw/main/website/samples/untp-digital-product-passport-v0.3.10.json +- **Hash**: 5b112fea72fc74b6 +- **Recommendation**: excellent +- **Is JSON-LD**: True +- **Is VC**: True +- **Is DPP-like**: True +- **Is Battery Pass**: False +- **Is Schema**: False +- **Type**: ['DigitalProductPassport', 'VerifiableCredential'] +- **Top keys**: type, @context, id, issuer, validFrom, validUntil, credentialSubject +- **Notes**: Verifiable Credential with DPP structure + +### test_uncefact_org_untp-dpp-instance-0.6.0.json + +- **URL**: https://test.uncefact.org/vocabulary/untp/dpp/untp-dpp-instance-0.6.0.json +- **Hash**: dceb94862b90bce6 +- **Recommendation**: excellent +- **Is JSON-LD**: True +- **Is VC**: True +- **Is DPP-like**: True +- **Is Battery Pass**: False +- **Is Schema**: False +- **Type**: ['DigitalProductPassport', 'VerifiableCredential'] +- **Top keys**: type, @context, id, issuer, validFrom, validUntil, credentialSubject +- **Notes**: Verifiable Credential with DPP structure + +### BatteryPassDataModel_BatteryPass_GeneralProductInformation-payload.json + +- **URL**: https://batterypass.github.io/BatteryPassDataModel/BatteryPass/io.BatteryPass.GeneralProductInformation/1.2.0/gen/GeneralProductInformation-payload.json +- **Hash**: d9d8393364648ed9 +- **Recommendation**: good +- **Is JSON-LD**: False +- **Is VC**: False +- **Is DPP-like**: True +- **Is Battery Pass**: True +- **Is Schema**: False +- **Type**: None +- **Top keys**: batteryCategory, operatorInformation, productIdentifier, batteryStatus, puttingIntoService, batteryMass, manufacturingDate, batteryPassportIdentifier, warrentyPeriod, manufacturerInformation +- **Notes**: Battery Pass data + +### BatteryPassDataModel_BatteryPass_CarbonFootprintForBatteries-payload.json + +- **URL**: https://batterypass.github.io/BatteryPassDataModel/BatteryPass/io.BatteryPass.CarbonFootprint/1.2.0/gen/CarbonFootprintForBatteries-payload.json +- **Error**: Invalid JSON: Expecting value: line 1 column 1 (char 0) + +### batterypass_BatteryPassDataModel_GeneralProductInformation-ld.json + +- **URL**: https://raw.githubusercontent.com/batterypass/BatteryPassDataModel/refs/heads/main/BatteryPass/io.BatteryPass.GeneralProductInformation/1.2.0/gen/GeneralProductInformation-ld.json +- **Hash**: df821e9ad855ca75 +- **Recommendation**: good +- **Is JSON-LD**: True +- **Is VC**: False +- **Is DPP-like**: True +- **Is Battery Pass**: True +- **Is Schema**: False +- **Type**: None +- **Top keys**: @graph, @context +- **Notes**: Battery Pass data + +### batterypass_BatteryPassDataModel_CarbonFootprintForBatteries-ld.json + +- **URL**: https://raw.githubusercontent.com/batterypass/BatteryPassDataModel/refs/heads/main/BatteryPass/io.BatteryPass.CarbonFootprint/1.2.0/gen/CarbonFootprintForBatteries-ld.json +- **Hash**: ebcb4870f6fd59e2 +- **Recommendation**: maybe +- **Is JSON-LD**: True +- **Is VC**: False +- **Is DPP-like**: False +- **Is Battery Pass**: False +- **Is Schema**: False +- **Type**: None +- **Top keys**: @graph, @context +- **Notes**: JSON-LD but structure unclear + +### batterypass_BatteryPassDataModel_Circularity-ld.json + +- **URL**: https://raw.githubusercontent.com/batterypass/BatteryPassDataModel/refs/heads/main/BatteryPass/io.BatteryPass.Circularity/1.2.0/gen/Circularity-ld.json +- **Hash**: bcb5d1e4c3e1822b +- **Recommendation**: moderate +- **Is JSON-LD**: True +- **Is VC**: False +- **Is DPP-like**: True +- **Is Battery Pass**: False +- **Is Schema**: False +- **Type**: None +- **Top keys**: @graph, @context +- **Notes**: DPP-like structure without VC wrapper + +### batterypass_BatteryPassDataModel_MaterialComposition-ld.json + +- **URL**: https://raw.githubusercontent.com/batterypass/BatteryPassDataModel/refs/heads/main/BatteryPass/io.BatteryPass.MaterialComposition/1.2.0/gen/MaterialComposition-ld.json +- **Hash**: e0692ea9b1f7a837 +- **Recommendation**: moderate +- **Is JSON-LD**: True +- **Is VC**: False +- **Is DPP-like**: True +- **Is Battery Pass**: False +- **Is Schema**: False +- **Type**: None +- **Top keys**: @graph, @context +- **Notes**: DPP-like structure without VC wrapper + +### nfc-forum_org_long-dpp-example.json + +- **URL**: https://nfc-forum.org/ndpp/long-dpp-example.json +- **Hash**: 57c0c2ed05527cd0 +- **Recommendation**: moderate +- **Is JSON-LD**: False +- **Is VC**: False +- **Is DPP-like**: True +- **Is Battery Pass**: False +- **Is Schema**: False +- **Type**: None +- **Top keys**: productID, productName, manufacturer, productionDate, expiryDate, materials, environmentalImpact, compliance, endOfLifeInstructions, digitalPassportLink +- **Notes**: DPP-like structure without VC wrapper + +### BatteryPassDataModel_BatteryPass_MaterialComposition-payload.json + +- **URL**: https://batterypass.github.io/BatteryPassDataModel/BatteryPass/io.BatteryPass.MaterialComposition/1.2.0/gen/MaterialComposition-payload.json +- **Error**: Invalid JSON: Expecting value: line 1 column 1 (char 0) diff --git a/tests/fixtures/upstream/SOURCES.md b/tests/fixtures/upstream/SOURCES.md new file mode 100644 index 0000000..6dfe22b --- /dev/null +++ b/tests/fixtures/upstream/SOURCES.md @@ -0,0 +1,96 @@ + + +# Upstream UNTP artefacts (vendored) + +This directory holds **read-only, byte-for-byte copies** of UN/CEFACT UNTP artefacts pulled from the upstream GitLab repository. They drive the `tests/fixtures/upstream/` validation matrix and are the source of truth that the bundled `src/dppvalidator/{schemas,vocabularies}/data/` files derive from. Do not edit them — re-vendor when the upstream tag changes. + +Each version directory pins the exact upstream commit SHA so that re-pulling against a moved tag is detectable as a hash mismatch. + +| Directory | Vendored from | Purpose | +| -------------------- | ----------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | +| [`v0.7.0/`](v0.7.0/) | tag `v0.7.0` | First release supported under the `0.4.0` migration plan ([docs/plans/UNTP_0.7.0_MIGRATION.md](../../../docs/plans/UNTP_0.7.0_MIGRATION.md)) | + +______________________________________________________________________ + +## v0.7.0 — UNTP DPP `0.7.0` + +**Upstream:** `https://opensource.unicc.org/un/unece/uncefact/spec-untp.git` +**Tag:** `v0.7.0` +**Pinned commit SHA:** `707cd5267deddede24bb74e453a758561972a109` +**Tag created:** `2026-05-04T10:34:14+00:00` +**Pulled:** `2026-05-07` +**Raw URL prefix:** `https://opensource.unicc.org/un/unece/uncefact/spec-untp/-/raw/707cd5267deddede24bb74e453a758561972a109/artefacts` +**Production mirror prefix:** `https://untp.unece.org/artefacts` + +The "Raw URL prefix" is the SHA-pinned source we vendored from (immutable; safe to diff against). The "Production mirror prefix" is the human-friendly hosting at `untp.unece.org` — verified bit-identical to the SHA-pinned source on 2026-05-08 (every artefact's SHA-256 matched). Use the production mirror for documentation links; use the SHA-pinned URL for integrity checks. + +### Files + +| Local path | Upstream path | Bytes | SHA-256 | +| ------------------------------------------------------------- | --------------------------------------------------------------------------- | ------: | ------------------------------------------------------------------ | +| `v0.7.0/schema/DigitalProductPassport.json` | `artefacts/schema/v0.7.0/dpp/DigitalProductPassport.json` | 50 362 | `42c51943ab23547d5287899fd12b214b19b006c28d105a70ff390f8551b12653` | +| `v0.7.0/schema/Product.json` | `artefacts/schema/v0.7.0/dpp/Product.json` | 38 990 | `fde2e1f11b0bbebd8fc209675c0575f1ff8359a9b52e5557f01d41c11f9ef23f` | +| `v0.7.0/contexts/untp-context.jsonld` | `artefacts/contexts/v0.7.0/untp-context.jsonld` | 105 396 | `fbd4824e30d3cfc5cba949e1efe19b4c9ebaee056abe7aaf1c6b139a7bf91b0c` | +| `v0.7.0/samples/DigitalProductPassport_instance.json` | `artefacts/samples/v0.7.0/dpp/DigitalProductPassport_instance.json` | 8 749 | `4c8df24357651169a90242b3f779842573104ed5f755d8fbe817f3129e8f0f91` | +| `v0.7.0/samples/DigitalProductPassport_battery_instance.json` | `artefacts/samples/v0.7.0/dpp/DigitalProductPassport_battery_instance.json` | 23 268 | `462264fcc6a4dc5ebcdc69cfbe238f76d2efa75534b71d5e1195d33139c7e599` | +| `v0.7.0/samples/DigitalProductPassport_cathode_instance.json` | `artefacts/samples/v0.7.0/dpp/DigitalProductPassport_cathode_instance.json` | 9 628 | `65841b5f60aa0b11e8b5c19656525023c2d62be8e40a3757c08e36426c8c79f4` | +| `v0.7.0/vocabularies/untp-ontology.jsonld` | `artefacts/vocabularies/untp-core/untp-ontology.jsonld` | 147 724 | `752060cc15c6c77bfcea8b170f173239a705e9da389314c1cb2dacc8a69d93bc` | +| `v0.7.0/vocabularies/untp-metrics.jsonld` | `artefacts/vocabularies/untp-metrics/untp-metrics.jsonld` | 53 765 | `77900ce1138be124976d138750bea24bacb6c8ba327672fe8598b85db99a0a36` | +| `v0.7.0/vocabularies/untp-topics.jsonld` | `artefacts/vocabularies/untp-topics/untp-topics.jsonld` | 61 045 | `49affcb265bdf2a7a92d1b171c49a27543bfb4915bcbd11dd6e571252a57bb12` | + +### Quick-look facts + +- **DPP schema** — required: `[@context, id, issuer, validFrom, name, credentialSubject]`; 22 `$defs`; `credentialSubject` is `Product` (no `ProductPassport` envelope). +- **Context** — single unified `@context` covering DPP/DCC/DFR/DIA/DTE; 36 top-level term keys; `untp` prefix is `https://vocabulary.uncefact.org/untp/`; JSON-LD `@version: 1.1`. +- **Samples** — all three samples validate cleanly against the bundled DPP schema (verified at vendor time). + +### Verifying integrity + +The snippet below extracts the `(local-path, sha256)` pairs from the table above and re-checks them with `shasum`. It is robust to Markdown table reformatting because it pattern-matches on the literal backticks around the path and the 64-char hex SHA, not on byte positions: + +```bash +python3 - <<'PY' | shasum -a 256 -c +import re, pathlib +src = pathlib.Path("tests/fixtures/upstream/SOURCES.md").read_text() +# Match a path-in-backticks (`v0.X.Y/...`) followed within the same row by +# a 64-char hex SHA-256 in backticks. `.*?` is non-greedy so adjacent rows +# don't bleed into each other. +pat = r"`(v0\.\d+\.\d+/[^`]+)`.*?`([0-9a-f]{64})`" +for path, sha in re.findall(pat, src): + print(f"{sha} tests/fixtures/upstream/{path}") +PY +``` + +Or, equivalently, re-pull and diff: + +```bash +sha=707cd5267deddede24bb74e453a758561972a109 +base=https://opensource.unicc.org/un/unece/uncefact/spec-untp/-/raw/$sha/artefacts +diff <(curl -sL $base/schema/v0.7.0/dpp/DigitalProductPassport.json) tests/fixtures/upstream/v0.7.0/schema/DigitalProductPassport.json +``` + +### Production mirror cross-check + +The production mirror at `untp.unece.org` is verified bit-identical to the SHA-pinned `opensource.unicc.org` source on each re-vendor pull. To re-run the cross-check against the current bundled bytes: + +```bash +prod=https://untp.unece.org/artefacts +diff <(curl -sL $prod/schema/v0.7.0/dpp/DigitalProductPassport.json) tests/fixtures/upstream/v0.7.0/schema/DigitalProductPassport.json +diff <(curl -sL $prod/schema/v0.7.0/dpp/Product.json) tests/fixtures/upstream/v0.7.0/schema/Product.json +diff <(curl -sL $prod/samples/v0.7.0/dpp/DigitalProductPassport_instance.json) tests/fixtures/upstream/v0.7.0/samples/DigitalProductPassport_instance.json +diff <(curl -sL $prod/samples/v0.7.0/dpp/DigitalProductPassport_battery_instance.json) tests/fixtures/upstream/v0.7.0/samples/DigitalProductPassport_battery_instance.json +diff <(curl -sL $prod/samples/v0.7.0/dpp/DigitalProductPassport_cathode_instance.json) tests/fixtures/upstream/v0.7.0/samples/DigitalProductPassport_cathode_instance.json +``` + +Each `diff` should produce no output. A non-empty diff means upstream republished the artefact at the production mirror without re-tagging — open an issue with UN/CEFACT and re-pin the registry against the new bytes. + +### Re-vendoring (when a new upstream tag lands) + +1. Resolve the new tag's commit SHA (`curl -sL "https://opensource.unicc.org/api/v4/projects/62/repository/tags/" | jq -r .commit.id`). +1. Run the fetch helper (or use the `/untp-bump ` Claude Code slash command, which scripts steps 2–4 of [docs/plans/UNTP_0.7.0_MIGRATION.md](../../../docs/plans/UNTP_0.7.0_MIGRATION.md) §7.2). +1. Re-compute SHA-256s and append a new section to this file. +1. Open a tracking PR linking back to the upstream tag. + +### License + +The UNTP specification artefacts are published by UN/CEFACT under [GPL-3.0-or-later](https://www.gnu.org/licenses/gpl-3.0.html) per the upstream repository. They are vendored here for **test fixture use only** under that licence — they are not redistributed inside the `dppvalidator` Python wheel. The vendored copies are read-only; modifications to the upstream content must happen upstream. diff --git a/tests/fixtures/upstream/v0.7.0/contexts/untp-context.jsonld b/tests/fixtures/upstream/v0.7.0/contexts/untp-context.jsonld new file mode 100644 index 0000000..bb353a3 --- /dev/null +++ b/tests/fixtures/upstream/v0.7.0/contexts/untp-context.jsonld @@ -0,0 +1,3493 @@ +{ + "@context": { + "untp": "https://vocabulary.uncefact.org/untp/", + "schema": "https://schema.org/", + "renderMethodPrefix": "https://w3id.org/vc/render-method#", + "xsd": "http://www.w3.org/2001/XMLSchema#", + "@protected": true, + "@version": 1.1, + "type": "@type", + "id": "@id", + "issuingSoftware": { + "@id": "untp:issuingSoftware", + "@type": "@id", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/untp/IssuingSoftware#", + "name": { + "@id": "schema:name" + }, + "version": { + "@id": "untp:version", + "@type": "xsd:string" + }, + "vendor": { + "@id": "untp:vendor", + "@type": "@id", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/untp/SoftwareVendor#", + "name": { + "@id": "schema:name" + } + } + } + } + }, + "DigitalProductPassport": { + "@protected": true, + "@id": "untp:DigitalProductPassport", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/untp/DigitalProductPassport#" + } + }, + "DigitalConformityCredential": { + "@protected": true, + "@id": "untp:DigitalConformityCredential", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/untp/DigitalConformityCredential#" + } + }, + "DigitalFacilityRecord": { + "@protected": true, + "@id": "untp:DigitalFacilityRecord", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/untp/DigitalFacilityRecord#" + } + }, + "DigitalIdentityAnchor": { + "@protected": true, + "@id": "untp:DigitalIdentityAnchor", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/untp/DigitalIdentityAnchor#" + } + }, + "DigitalTraceabilityEvent": { + "@protected": true, + "@id": "untp:DigitalTraceabilityEvent", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/untp/DigitalTraceabilityEvent#" + } + }, + "RenderTemplate2024": { + "@protected": true, + "@id": "untp:RenderTemplate2024", + "@context": { + "@protected": true, + "mediaQuery": { + "@id": "untp:mediaQuery", + "@type": "xsd:string" + }, + "template": { + "@id": "untp:template", + "@type": "xsd:string" + }, + "url": { + "@id": "untp:url", + "@type": "xsd:anyURI" + }, + "mediaType": { + "@id": "https://schema.org/encodingFormat" + }, + "digestMultibase": { + "@id": "https://w3id.org/security#digestMultibase", + "@type": "https://w3id.org/security#multibase" + } + } + }, + "IdentifierScheme": { + "@protected": true, + "@id": "untp:IdentifierScheme", + "@context": { + "@protected": true, + "name": { + "@id": "schema:name" + } + } + }, + "Party": { + "@protected": true, + "@id": "untp:Party", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/untp/Party#", + "name": { + "@id": "schema:name" + }, + "description": { + "@id": "schema:description" + }, + "registeredId": { + "@id": "untp:registeredId", + "@type": "xsd:string" + }, + "idScheme": { + "@id": "untp:idScheme", + "@type": "@id", + "@context": { + "@protected": true, + "id": { + "@id": "untp:id", + "@type": "xsd:anyURI" + }, + "name": { + "@id": "schema:name" + } + } + }, + "registrationCountry": { + "@protected": true, + "@id": "untp:registrationCountry", + "@context": { + "@protected": true, + "countryCode": { + "@id": "untp:countryCode", + "@type": "@vocab", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/CountryId#" + } + }, + "countryName": { + "@id": "untp:countryName", + "@type": "xsd:string" + } + } + }, + "partyAddress": { + "@protected": true, + "@id": "untp:partyAddress", + "@context": { + "@protected": true, + "streetAddress": { + "@id": "schema:streetAddress", + "@type": "xsd:string" + }, + "postalCode": { + "@id": "schema:postalCode", + "@type": "xsd:string" + }, + "addressLocality": { + "@id": "schema:addressLocality", + "@type": "xsd:string" + }, + "addressRegion": { + "@id": "schema:addressRegion", + "@type": "xsd:string" + }, + "addressCountry": { + "@protected": true, + "@id": "untp:addressCountry", + "@context": { + "@protected": true, + "countryCode": { + "@id": "untp:countryCode", + "@type": "@vocab", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/CountryId#" + } + }, + "countryName": { + "@id": "untp:countryName", + "@type": "xsd:string" + } + } + } + } + }, + "organisationWebsite": { + "@id": "untp:organisationWebsite", + "@type": "xsd:anyURI" + }, + "industryCategory": { + "@protected": true, + "@id": "untp:industryCategory", + "@context": { + "@protected": true, + "code": { + "@id": "untp:code", + "@type": "xsd:string" + }, + "name": { + "@id": "schema:name" + }, + "definition": { + "@id": "untp:definition", + "@type": "xsd:string" + }, + "schemeId": { + "@id": "untp:schemeId", + "@type": "xsd:anyURI" + }, + "schemeName": { + "@id": "untp:schemeName", + "@type": "xsd:string" + } + } + }, + "partyAlsoKnownAs": { + "@id": "untp:partyAlsoKnownAs", + "@type": "@id" + } + } + }, + "CredentialIssuer": { + "@protected": true, + "@id": "untp:CredentialIssuer", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/untp/CredentialIssuer#", + "name": { + "@id": "schema:name" + }, + "issuerAlsoKnownAs": { + "@id": "untp:issuerAlsoKnownAs", + "@type": "@id" + } + } + }, + "Entity": { + "@protected": false, + "@id": "untp:Entity", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/untp/Entity#", + "name": { + "@id": "schema:name" + }, + "description": { + "@id": "schema:description" + } + } + }, + "ConformityTopic": { + "@protected": true, + "@id": "untp:ConformityTopic", + "@context": { + "@protected": true, + "name": { + "@id": "schema:name" + }, + "definition": { + "@id": "untp:definition", + "@type": "xsd:string" + } + } + }, + "PerformanceMetric": { + "@protected": true, + "@id": "untp:PerformanceMetric", + "@context": { + "@protected": true, + "name": { + "@id": "schema:name" + }, + "description": { + "@id": "schema:description" + }, + "improvementDirection": { + "@id": "untp:improvementDirection", + "@type": "@vocab", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/untp/ImprovementIndicator#" + } + }, + "aggregationMethod": { + "@id": "untp:aggregationMethod", + "@type": "@vocab", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/untp/AggregationType#" + } + }, + "allowedUnit": { + "@id": "untp:allowedUnit", + "@type": "@vocab", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/UnitMeasureCode#" + } + } + } + }, + "Criterion": { + "@protected": true, + "@id": "untp:Criterion", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/untp/Criterion#", + "name": { + "@id": "schema:name" + }, + "description": { + "@id": "schema:description" + }, + "version": { + "@id": "untp:version", + "@type": "xsd:string" + }, + "status": { + "@id": "untp:status", + "@type": "@vocab", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/untp/CriterionStatus#" + } + }, + "documentation": { + "@id": "untp:documentation", + "@type": "xsd:anyURI" + }, + "conformityTopic": { + "@id": "untp:conformityTopic", + "@type": "@id", + "@container": "@set" + }, + "tag": { + "@id": "untp:tag", + "@type": "xsd:string" + }, + "requiredPerformance": { + "@protected": true, + "@id": "untp:requiredPerformance", + "@context": { + "@protected": true, + "metric": { + "@id": "untp:metric", + "@type": "@id" + }, + "measure": { + "@protected": true, + "@id": "untp:measure", + "@context": { + "@protected": true, + "value": { + "@id": "untp:value", + "@type": "xsd:double" + }, + "upperTolerance": { + "@id": "untp:upperTolerance", + "@type": "xsd:double" + }, + "lowerTolerance": { + "@id": "untp:lowerTolerance", + "@type": "xsd:double" + }, + "unit": { + "@id": "untp:unit", + "@type": "@vocab", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/UnitMeasureCode#" + } + } + } + }, + "score": { + "@protected": true, + "@id": "untp:score", + "@context": { + "@protected": true, + "code": { + "@id": "untp:code", + "@type": "xsd:string" + }, + "rank": { + "@id": "untp:rank", + "@type": "xsd:integer" + }, + "definition": { + "@id": "untp:definition", + "@type": "xsd:string" + } + } + } + } + } + } + }, + "Regulation": { + "@protected": true, + "@id": "untp:Regulation", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/untp/Regulation#", + "name": { + "@id": "schema:name" + }, + "description": { + "@id": "schema:description" + }, + "jurisdictionCountry": { + "@protected": true, + "@id": "untp:jurisdictionCountry", + "@context": { + "@protected": true, + "countryCode": { + "@id": "untp:countryCode", + "@type": "@vocab", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/CountryId#" + } + }, + "countryName": { + "@id": "untp:countryName", + "@type": "xsd:string" + } + } + }, + "administeredBy": { + "@id": "untp:administeredBy", + "@type": "@id" + }, + "effectiveDate": { + "@id": "untp:effectiveDate", + "@type": "xsd:date" + } + } + }, + "Standard": { + "@protected": true, + "@id": "untp:Standard", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/untp/Standard#", + "name": { + "@id": "schema:name" + }, + "description": { + "@id": "schema:description" + }, + "issuingParty": { + "@id": "untp:issuingParty", + "@type": "@id" + }, + "issueDate": { + "@id": "untp:issueDate", + "@type": "xsd:date" + } + } + }, + "Claim": { + "@protected": true, + "@id": "untp:Claim", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/untp/Claim#", + "name": { + "@id": "schema:name" + }, + "description": { + "@id": "schema:description" + }, + "referenceCriteria": { + "@id": "untp:referenceCriteria", + "@type": "@id" + }, + "referenceRegulation": { + "@id": "untp:referenceRegulation", + "@type": "@id" + }, + "referenceStandard": { + "@id": "untp:referenceStandard", + "@type": "@id" + }, + "claimDate": { + "@id": "untp:claimDate", + "@type": "xsd:date" + }, + "applicablePeriod": { + "@protected": true, + "@id": "untp:applicablePeriod", + "@context": { + "@protected": true, + "startDate": { + "@id": "untp:startDate", + "@type": "xsd:date" + }, + "endDate": { + "@id": "untp:endDate", + "@type": "xsd:date" + }, + "periodInformation": { + "@id": "untp:periodInformation", + "@type": "xsd:string" + } + } + }, + "claimedPerformance": { + "@protected": true, + "@id": "untp:claimedPerformance", + "@context": { + "@protected": true, + "metric": { + "@id": "untp:metric", + "@type": "@id" + }, + "measure": { + "@protected": true, + "@id": "untp:measure", + "@context": { + "@protected": true, + "value": { + "@id": "untp:value", + "@type": "xsd:double" + }, + "upperTolerance": { + "@id": "untp:upperTolerance", + "@type": "xsd:double" + }, + "lowerTolerance": { + "@id": "untp:lowerTolerance", + "@type": "xsd:double" + }, + "unit": { + "@id": "untp:unit", + "@type": "@vocab", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/UnitMeasureCode#" + } + } + } + }, + "score": { + "@protected": true, + "@id": "untp:score", + "@context": { + "@protected": true, + "code": { + "@id": "untp:code", + "@type": "xsd:string" + }, + "rank": { + "@id": "untp:rank", + "@type": "xsd:integer" + }, + "definition": { + "@id": "untp:definition", + "@type": "xsd:string" + } + } + } + } + }, + "evidence": { + "@protected": true, + "@id": "untp:evidence", + "@context": { + "@protected": true, + "linkURL": { + "@id": "untp:linkURL", + "@type": "xsd:anyURI" + }, + "linkName": { + "@id": "untp:linkName", + "@type": "xsd:string" + }, + "digestMultibase": { + "@id": "https://w3id.org/security#digestMultibase", + "@type": "https://w3id.org/security#multibase" + }, + "mediaType": { + "@id": "https://schema.org/encodingFormat" + }, + "linkType": { + "@id": "untp:linkType", + "@type": "xsd:string" + } + } + }, + "conformityTopic": { + "@id": "untp:conformityTopic", + "@type": "@id", + "@container": "@set" + } + } + }, + "Facility": { + "@protected": true, + "@id": "untp:Facility", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/untp/Facility#", + "name": { + "@id": "schema:name" + }, + "description": { + "@id": "schema:description" + }, + "registeredId": { + "@id": "untp:registeredId", + "@type": "xsd:string" + }, + "idScheme": { + "@id": "untp:idScheme", + "@type": "@id", + "@context": { + "@protected": true, + "id": { + "@id": "untp:id", + "@type": "xsd:anyURI" + }, + "name": { + "@id": "schema:name" + } + } + }, + "countryOfOperation": { + "@protected": true, + "@id": "untp:countryOfOperation", + "@context": { + "@protected": true, + "countryCode": { + "@id": "untp:countryCode", + "@type": "@vocab", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/CountryId#" + } + }, + "countryName": { + "@id": "untp:countryName", + "@type": "xsd:string" + } + } + }, + "processCategory": { + "@protected": true, + "@id": "untp:processCategory", + "@context": { + "@protected": true, + "code": { + "@id": "untp:code", + "@type": "xsd:string" + }, + "name": { + "@id": "schema:name" + }, + "definition": { + "@id": "untp:definition", + "@type": "xsd:string" + }, + "schemeId": { + "@id": "untp:schemeId", + "@type": "xsd:anyURI" + }, + "schemeName": { + "@id": "untp:schemeName", + "@type": "xsd:string" + } + } + }, + "relatedParty": { + "@protected": true, + "@id": "untp:relatedParty", + "@context": { + "@protected": true, + "role": { + "@id": "untp:role", + "@type": "@vocab", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/untp/PartyRole#" + } + }, + "party": { + "@id": "untp:party", + "@type": "@id" + } + } + }, + "relatedDocument": { + "@protected": true, + "@id": "untp:relatedDocument", + "@context": { + "@protected": true, + "linkURL": { + "@id": "untp:linkURL", + "@type": "xsd:anyURI" + }, + "linkName": { + "@id": "untp:linkName", + "@type": "xsd:string" + }, + "digestMultibase": { + "@id": "https://w3id.org/security#digestMultibase", + "@type": "https://w3id.org/security#multibase" + }, + "mediaType": { + "@id": "https://schema.org/encodingFormat" + }, + "linkType": { + "@id": "untp:linkType", + "@type": "xsd:string" + } + } + }, + "facilityAlsoKnownAs": { + "@id": "untp:facilityAlsoKnownAs", + "@type": "@id" + }, + "locationInformation": { + "@protected": true, + "@id": "untp:locationInformation", + "@context": { + "@protected": true, + "plusCode": { + "@id": "untp:plusCode", + "@type": "xsd:anyURI" + }, + "geoLocation": { + "@protected": true, + "@id": "untp:geoLocation", + "@context": { + "@protected": true, + "latitude": { + "@id": "untp:latitude", + "@type": "xsd:double" + }, + "longitude": { + "@id": "untp:longitude", + "@type": "xsd:double" + } + } + }, + "geoBoundary": { + "@protected": true, + "@id": "untp:geoBoundary", + "@context": { + "@protected": true, + "latitude": { + "@id": "untp:latitude", + "@type": "xsd:double" + }, + "longitude": { + "@id": "untp:longitude", + "@type": "xsd:double" + } + } + } + } + }, + "address": { + "@protected": true, + "@id": "untp:address", + "@context": { + "@protected": true, + "streetAddress": { + "@id": "schema:streetAddress", + "@type": "xsd:string" + }, + "postalCode": { + "@id": "schema:postalCode", + "@type": "xsd:string" + }, + "addressLocality": { + "@id": "schema:addressLocality", + "@type": "xsd:string" + }, + "addressRegion": { + "@id": "schema:addressRegion", + "@type": "xsd:string" + }, + "addressCountry": { + "@protected": true, + "@id": "untp:addressCountry", + "@context": { + "@protected": true, + "countryCode": { + "@id": "untp:countryCode", + "@type": "@vocab", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/CountryId#" + } + }, + "countryName": { + "@id": "untp:countryName", + "@type": "xsd:string" + } + } + } + } + }, + "materialUsage": { + "@protected": true, + "@id": "untp:materialUsage", + "@context": { + "@protected": true, + "applicablePeriod": { + "@protected": true, + "@id": "untp:applicablePeriod", + "@context": { + "@protected": true, + "startDate": { + "@id": "untp:startDate", + "@type": "xsd:date" + }, + "endDate": { + "@id": "untp:endDate", + "@type": "xsd:date" + }, + "periodInformation": { + "@id": "untp:periodInformation", + "@type": "xsd:string" + } + } + }, + "materialConsumed": { + "@protected": true, + "@id": "untp:materialConsumed", + "@context": { + "@protected": true, + "name": { + "@id": "schema:name" + }, + "originCountry": { + "@protected": true, + "@id": "untp:originCountry", + "@context": { + "@protected": true, + "countryCode": { + "@id": "untp:countryCode", + "@type": "@vocab", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/CountryId#" + } + }, + "countryName": { + "@id": "untp:countryName", + "@type": "xsd:string" + } + } + }, + "materialType": { + "@protected": true, + "@id": "untp:materialType", + "@context": { + "@protected": true, + "code": { + "@id": "untp:code", + "@type": "xsd:string" + }, + "name": { + "@id": "schema:name" + }, + "definition": { + "@id": "untp:definition", + "@type": "xsd:string" + }, + "schemeId": { + "@id": "untp:schemeId", + "@type": "xsd:anyURI" + }, + "schemeName": { + "@id": "untp:schemeName", + "@type": "xsd:string" + } + } + }, + "massFraction": { + "@id": "untp:massFraction", + "@type": "xsd:double" + }, + "mass": { + "@protected": true, + "@id": "untp:mass", + "@context": { + "@protected": true, + "value": { + "@id": "untp:value", + "@type": "xsd:double" + }, + "upperTolerance": { + "@id": "untp:upperTolerance", + "@type": "xsd:double" + }, + "lowerTolerance": { + "@id": "untp:lowerTolerance", + "@type": "xsd:double" + }, + "unit": { + "@id": "untp:unit", + "@type": "@vocab", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/UnitMeasureCode#" + } + } + } + }, + "recycledMassFraction": { + "@id": "untp:recycledMassFraction", + "@type": "xsd:double" + }, + "hazardous": { + "@id": "untp:hazardous", + "@type": "xsd:boolean" + }, + "symbol": { + "@protected": true, + "@id": "untp:symbol", + "@context": { + "@protected": true, + "name": { + "@id": "schema:name" + }, + "description": { + "@id": "schema:description" + }, + "imageData": { + "@id": "untp:imageData", + "@type": "xsd:string" + }, + "mediaType": { + "@id": "https://schema.org/encodingFormat" + } + } + }, + "materialSafetyInformation": { + "@protected": true, + "@id": "untp:materialSafetyInformation", + "@context": { + "@protected": true, + "linkURL": { + "@id": "untp:linkURL", + "@type": "xsd:anyURI" + }, + "linkName": { + "@id": "untp:linkName", + "@type": "xsd:string" + }, + "digestMultibase": { + "@id": "https://w3id.org/security#digestMultibase", + "@type": "https://w3id.org/security#multibase" + }, + "mediaType": { + "@id": "https://schema.org/encodingFormat" + }, + "linkType": { + "@id": "untp:linkType", + "@type": "xsd:string" + } + } + } + } + } + } + }, + "performanceClaim": { + "@id": "untp:performanceClaim", + "@type": "@id" + } + } + }, + "ConformityScheme": { + "@protected": true, + "@id": "untp:ConformityScheme", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/untp/ConformityScheme#", + "name": { + "@id": "schema:name" + }, + "description": { + "@id": "schema:description" + }, + "owner": { + "@id": "untp:owner", + "@type": "@id" + }, + "endorsementLevel": { + "@id": "untp:endorsementLevel", + "@type": "@vocab", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/untp/SchemeEndorsementLevel#" + } + }, + "endorsement": { + "@protected": true, + "@id": "untp:endorsement", + "@context": { + "@protected": true, + "name": { + "@id": "schema:name" + }, + "trustmark": { + "@protected": true, + "@id": "untp:trustmark", + "@context": { + "@protected": true, + "name": { + "@id": "schema:name" + }, + "description": { + "@id": "schema:description" + }, + "imageData": { + "@id": "untp:imageData", + "@type": "xsd:string" + }, + "mediaType": { + "@id": "https://schema.org/encodingFormat" + } + } + }, + "issuingAuthority": { + "@id": "untp:issuingAuthority", + "@type": "@id" + }, + "endorsementEvidence": { + "@protected": true, + "@id": "untp:endorsementEvidence", + "@context": { + "@protected": true, + "linkURL": { + "@id": "untp:linkURL", + "@type": "xsd:anyURI" + }, + "linkName": { + "@id": "untp:linkName", + "@type": "xsd:string" + }, + "digestMultibase": { + "@id": "https://w3id.org/security#digestMultibase", + "@type": "https://w3id.org/security#multibase" + }, + "mediaType": { + "@id": "https://schema.org/encodingFormat" + }, + "linkType": { + "@id": "untp:linkType", + "@type": "xsd:string" + } + } + } + } + }, + "schemeScoringFramework": { + "@protected": true, + "@id": "untp:schemeScoringFramework", + "@context": { + "@protected": true, + "name": { + "@id": "schema:name" + }, + "description": { + "@id": "schema:description" + }, + "score": { + "@protected": true, + "@id": "untp:score", + "@context": { + "@protected": true, + "code": { + "@id": "untp:code", + "@type": "xsd:string" + }, + "rank": { + "@id": "untp:rank", + "@type": "xsd:integer" + }, + "definition": { + "@id": "untp:definition", + "@type": "xsd:string" + } + } + } + } + }, + "documentation": { + "@id": "untp:documentation", + "@type": "xsd:anyURI" + }, + "licenseType": { + "@id": "untp:licenseType", + "@type": "@vocab", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/untp/LicenseType#" + } + }, + "establishedDate": { + "@id": "untp:establishedDate", + "@type": "xsd:date" + }, + "geographicScope": { + "@protected": true, + "@id": "untp:geographicScope", + "@context": { + "@protected": true, + "code": { + "@id": "untp:code", + "@type": "xsd:string" + }, + "name": { + "@id": "schema:name" + }, + "definition": { + "@id": "untp:definition", + "@type": "xsd:string" + }, + "schemeId": { + "@id": "untp:schemeId", + "@type": "xsd:anyURI" + }, + "schemeName": { + "@id": "untp:schemeName", + "@type": "xsd:string" + } + } + }, + "industryScope": { + "@protected": true, + "@id": "untp:industryScope", + "@context": { + "@protected": true, + "code": { + "@id": "untp:code", + "@type": "xsd:string" + }, + "name": { + "@id": "schema:name" + }, + "definition": { + "@id": "untp:definition", + "@type": "xsd:string" + }, + "schemeId": { + "@id": "untp:schemeId", + "@type": "xsd:anyURI" + }, + "schemeName": { + "@id": "untp:schemeName", + "@type": "xsd:string" + } + } + }, + "trustmark": { + "@protected": true, + "@id": "untp:trustmark", + "@context": { + "@protected": true, + "name": { + "@id": "schema:name" + }, + "description": { + "@id": "schema:description" + }, + "imageData": { + "@id": "untp:imageData", + "@type": "xsd:string" + }, + "mediaType": { + "@id": "https://schema.org/encodingFormat" + } + } + }, + "conformsTo": { + "@protected": true, + "@id": "untp:conformsTo", + "@context": { + "@protected": true, + "linkURL": { + "@id": "untp:linkURL", + "@type": "xsd:anyURI" + }, + "linkName": { + "@id": "untp:linkName", + "@type": "xsd:string" + }, + "digestMultibase": { + "@id": "https://w3id.org/security#digestMultibase", + "@type": "https://w3id.org/security#multibase" + }, + "mediaType": { + "@id": "https://schema.org/encodingFormat" + }, + "linkType": { + "@id": "untp:linkType", + "@type": "xsd:string" + } + } + }, + "includedProfile": { + "@id": "untp:includedProfile", + "@type": "@id" + } + } + }, + "ConformityProfile": { + "@protected": true, + "@id": "untp:ConformityProfile", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/untp/ConformityProfile#", + "name": { + "@id": "schema:name" + }, + "description": { + "@id": "schema:description" + }, + "version": { + "@id": "untp:version", + "@type": "xsd:string" + }, + "validFrom": { + "@id": "untp:validFrom", + "@type": "xsd:date" + }, + "status": { + "@id": "untp:status", + "@type": "@vocab", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/untp/CriterionStatus#" + } + }, + "subjectType": { + "@id": "untp:subjectType", + "@type": "@vocab", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/untp/AssessmentSubjectType#" + } + }, + "standardAlignment": { + "@protected": true, + "@id": "untp:standardAlignment", + "@context": { + "@protected": true, + "standard": { + "@id": "untp:standard", + "@type": "@id" + }, + "alignmentLevel": { + "@id": "untp:alignmentLevel", + "@type": "@vocab", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/untp/SchemeAlignmentLevel#" + } + } + } + }, + "regulatoryAlignment": { + "@protected": true, + "@id": "untp:regulatoryAlignment", + "@context": { + "@protected": true, + "regulation": { + "@id": "untp:regulation", + "@type": "@id" + }, + "alignmentLevel": { + "@id": "untp:alignmentLevel", + "@type": "@vocab", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/untp/SchemeAlignmentLevel#" + } + } + } + }, + "criterionScoringFramework": { + "@protected": true, + "@id": "untp:criterionScoringFramework", + "@context": { + "@protected": true, + "name": { + "@id": "schema:name" + }, + "description": { + "@id": "schema:description" + }, + "score": { + "@protected": true, + "@id": "untp:score", + "@context": { + "@protected": true, + "code": { + "@id": "untp:code", + "@type": "xsd:string" + }, + "rank": { + "@id": "untp:rank", + "@type": "xsd:integer" + }, + "definition": { + "@id": "untp:definition", + "@type": "xsd:string" + } + } + } + } + }, + "documentation": { + "@id": "untp:documentation", + "@type": "xsd:anyURI" + }, + "criterion": { + "@id": "untp:criterion", + "@type": "@id" + }, + "scope": { + "@protected": true, + "@id": "untp:scope", + "@context": { + "@protected": true, + "code": { + "@id": "untp:code", + "@type": "xsd:string" + }, + "name": { + "@id": "schema:name" + }, + "definition": { + "@id": "untp:definition", + "@type": "xsd:string" + }, + "schemeId": { + "@id": "untp:schemeId", + "@type": "xsd:anyURI" + }, + "schemeName": { + "@id": "untp:schemeName", + "@type": "xsd:string" + } + } + }, + "scheme": { + "@id": "untp:scheme", + "@type": "@id" + } + } + }, + "Product": { + "@protected": true, + "@id": "untp:Product", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/untp/Product#", + "name": { + "@id": "schema:name" + }, + "description": { + "@id": "schema:description" + }, + "idScheme": { + "@id": "untp:idScheme", + "@type": "@id", + "@context": { + "@protected": true, + "id": { + "@id": "untp:id", + "@type": "xsd:anyURI" + }, + "name": { + "@id": "schema:name" + } + } + }, + "modelNumber": { + "@id": "untp:modelNumber", + "@type": "xsd:string" + }, + "batchNumber": { + "@id": "untp:batchNumber", + "@type": "xsd:string" + }, + "itemNumber": { + "@id": "untp:itemNumber", + "@type": "xsd:string" + }, + "idGranularity": { + "@id": "untp:idGranularity", + "@type": "@vocab", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/untp/ProductIDGranularity#" + } + }, + "characteristics": { + "@id": "untp:characteristics", + "@context": { + "@vocab": "https://vocabulary.uncefact.org/untp/Characteristics#" + } + }, + "productImage": { + "@protected": true, + "@id": "untp:productImage", + "@context": { + "@protected": true, + "linkURL": { + "@id": "untp:linkURL", + "@type": "xsd:anyURI" + }, + "linkName": { + "@id": "untp:linkName", + "@type": "xsd:string" + }, + "digestMultibase": { + "@id": "https://w3id.org/security#digestMultibase", + "@type": "https://w3id.org/security#multibase" + }, + "mediaType": { + "@id": "https://schema.org/encodingFormat" + }, + "linkType": { + "@id": "untp:linkType", + "@type": "xsd:string" + } + } + }, + "productCategory": { + "@protected": true, + "@id": "untp:productCategory", + "@context": { + "@protected": true, + "code": { + "@id": "untp:code", + "@type": "xsd:string" + }, + "name": { + "@id": "schema:name" + }, + "definition": { + "@id": "untp:definition", + "@type": "xsd:string" + }, + "schemeId": { + "@id": "untp:schemeId", + "@type": "xsd:anyURI" + }, + "schemeName": { + "@id": "untp:schemeName", + "@type": "xsd:string" + } + } + }, + "relatedDocument": { + "@protected": true, + "@id": "untp:relatedDocument", + "@context": { + "@protected": true, + "linkURL": { + "@id": "untp:linkURL", + "@type": "xsd:anyURI" + }, + "linkName": { + "@id": "untp:linkName", + "@type": "xsd:string" + }, + "digestMultibase": { + "@id": "https://w3id.org/security#digestMultibase", + "@type": "https://w3id.org/security#multibase" + }, + "mediaType": { + "@id": "https://schema.org/encodingFormat" + }, + "linkType": { + "@id": "untp:linkType", + "@type": "xsd:string" + } + } + }, + "relatedParty": { + "@protected": true, + "@id": "untp:relatedParty", + "@context": { + "@protected": true, + "role": { + "@id": "untp:role", + "@type": "@vocab", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/untp/PartyRole#" + } + }, + "party": { + "@id": "untp:party", + "@type": "@id" + } + } + }, + "producedAtFacility": { + "@id": "untp:producedAtFacility", + "@type": "@id" + }, + "productionDate": { + "@id": "untp:productionDate", + "@type": "xsd:date" + }, + "expiryDate": { + "@id": "untp:expiryDate", + "@type": "xsd:date" + }, + "countryOfProduction": { + "@protected": true, + "@id": "untp:countryOfProduction", + "@context": { + "@protected": true, + "countryCode": { + "@id": "untp:countryCode", + "@type": "@vocab", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/CountryId#" + } + }, + "countryName": { + "@id": "untp:countryName", + "@type": "xsd:string" + } + } + }, + "dimensions": { + "@protected": true, + "@id": "untp:dimensions", + "@context": { + "@protected": true, + "weight": { + "@protected": true, + "@id": "untp:weight", + "@context": { + "@protected": true, + "value": { + "@id": "untp:value", + "@type": "xsd:double" + }, + "upperTolerance": { + "@id": "untp:upperTolerance", + "@type": "xsd:double" + }, + "lowerTolerance": { + "@id": "untp:lowerTolerance", + "@type": "xsd:double" + }, + "unit": { + "@id": "untp:unit", + "@type": "@vocab", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/UnitMeasureCode#" + } + } + } + }, + "length": { + "@protected": true, + "@id": "untp:length", + "@context": { + "@protected": true, + "value": { + "@id": "untp:value", + "@type": "xsd:double" + }, + "upperTolerance": { + "@id": "untp:upperTolerance", + "@type": "xsd:double" + }, + "lowerTolerance": { + "@id": "untp:lowerTolerance", + "@type": "xsd:double" + }, + "unit": { + "@id": "untp:unit", + "@type": "@vocab", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/UnitMeasureCode#" + } + } + } + }, + "width": { + "@protected": true, + "@id": "untp:width", + "@context": { + "@protected": true, + "value": { + "@id": "untp:value", + "@type": "xsd:double" + }, + "upperTolerance": { + "@id": "untp:upperTolerance", + "@type": "xsd:double" + }, + "lowerTolerance": { + "@id": "untp:lowerTolerance", + "@type": "xsd:double" + }, + "unit": { + "@id": "untp:unit", + "@type": "@vocab", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/UnitMeasureCode#" + } + } + } + }, + "height": { + "@protected": true, + "@id": "untp:height", + "@context": { + "@protected": true, + "value": { + "@id": "untp:value", + "@type": "xsd:double" + }, + "upperTolerance": { + "@id": "untp:upperTolerance", + "@type": "xsd:double" + }, + "lowerTolerance": { + "@id": "untp:lowerTolerance", + "@type": "xsd:double" + }, + "unit": { + "@id": "untp:unit", + "@type": "@vocab", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/UnitMeasureCode#" + } + } + } + }, + "volume": { + "@protected": true, + "@id": "untp:volume", + "@context": { + "@protected": true, + "value": { + "@id": "untp:value", + "@type": "xsd:double" + }, + "upperTolerance": { + "@id": "untp:upperTolerance", + "@type": "xsd:double" + }, + "lowerTolerance": { + "@id": "untp:lowerTolerance", + "@type": "xsd:double" + }, + "unit": { + "@id": "untp:unit", + "@type": "@vocab", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/UnitMeasureCode#" + } + } + } + } + } + }, + "materialProvenance": { + "@protected": true, + "@id": "untp:materialProvenance", + "@context": { + "@protected": true, + "name": { + "@id": "schema:name" + }, + "originCountry": { + "@protected": true, + "@id": "untp:originCountry", + "@context": { + "@protected": true, + "countryCode": { + "@id": "untp:countryCode", + "@type": "@vocab", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/CountryId#" + } + }, + "countryName": { + "@id": "untp:countryName", + "@type": "xsd:string" + } + } + }, + "materialType": { + "@protected": true, + "@id": "untp:materialType", + "@context": { + "@protected": true, + "code": { + "@id": "untp:code", + "@type": "xsd:string" + }, + "name": { + "@id": "schema:name" + }, + "definition": { + "@id": "untp:definition", + "@type": "xsd:string" + }, + "schemeId": { + "@id": "untp:schemeId", + "@type": "xsd:anyURI" + }, + "schemeName": { + "@id": "untp:schemeName", + "@type": "xsd:string" + } + } + }, + "massFraction": { + "@id": "untp:massFraction", + "@type": "xsd:double" + }, + "mass": { + "@protected": true, + "@id": "untp:mass", + "@context": { + "@protected": true, + "value": { + "@id": "untp:value", + "@type": "xsd:double" + }, + "upperTolerance": { + "@id": "untp:upperTolerance", + "@type": "xsd:double" + }, + "lowerTolerance": { + "@id": "untp:lowerTolerance", + "@type": "xsd:double" + }, + "unit": { + "@id": "untp:unit", + "@type": "@vocab", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/UnitMeasureCode#" + } + } + } + }, + "recycledMassFraction": { + "@id": "untp:recycledMassFraction", + "@type": "xsd:double" + }, + "hazardous": { + "@id": "untp:hazardous", + "@type": "xsd:boolean" + }, + "symbol": { + "@protected": true, + "@id": "untp:symbol", + "@context": { + "@protected": true, + "name": { + "@id": "schema:name" + }, + "description": { + "@id": "schema:description" + }, + "imageData": { + "@id": "untp:imageData", + "@type": "xsd:string" + }, + "mediaType": { + "@id": "https://schema.org/encodingFormat" + } + } + }, + "materialSafetyInformation": { + "@protected": true, + "@id": "untp:materialSafetyInformation", + "@context": { + "@protected": true, + "linkURL": { + "@id": "untp:linkURL", + "@type": "xsd:anyURI" + }, + "linkName": { + "@id": "untp:linkName", + "@type": "xsd:string" + }, + "digestMultibase": { + "@id": "https://w3id.org/security#digestMultibase", + "@type": "https://w3id.org/security#multibase" + }, + "mediaType": { + "@id": "https://schema.org/encodingFormat" + }, + "linkType": { + "@id": "untp:linkType", + "@type": "xsd:string" + } + } + } + } + }, + "packaging": { + "@protected": true, + "@id": "untp:packaging", + "@context": { + "@protected": true, + "description": { + "@id": "schema:description" + }, + "dimensions": { + "@protected": true, + "@id": "untp:dimensions", + "@context": { + "@protected": true, + "weight": { + "@protected": true, + "@id": "untp:weight", + "@context": { + "@protected": true, + "value": { + "@id": "untp:value", + "@type": "xsd:double" + }, + "upperTolerance": { + "@id": "untp:upperTolerance", + "@type": "xsd:double" + }, + "lowerTolerance": { + "@id": "untp:lowerTolerance", + "@type": "xsd:double" + }, + "unit": { + "@id": "untp:unit", + "@type": "@vocab", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/UnitMeasureCode#" + } + } + } + }, + "length": { + "@protected": true, + "@id": "untp:length", + "@context": { + "@protected": true, + "value": { + "@id": "untp:value", + "@type": "xsd:double" + }, + "upperTolerance": { + "@id": "untp:upperTolerance", + "@type": "xsd:double" + }, + "lowerTolerance": { + "@id": "untp:lowerTolerance", + "@type": "xsd:double" + }, + "unit": { + "@id": "untp:unit", + "@type": "@vocab", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/UnitMeasureCode#" + } + } + } + }, + "width": { + "@protected": true, + "@id": "untp:width", + "@context": { + "@protected": true, + "value": { + "@id": "untp:value", + "@type": "xsd:double" + }, + "upperTolerance": { + "@id": "untp:upperTolerance", + "@type": "xsd:double" + }, + "lowerTolerance": { + "@id": "untp:lowerTolerance", + "@type": "xsd:double" + }, + "unit": { + "@id": "untp:unit", + "@type": "@vocab", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/UnitMeasureCode#" + } + } + } + }, + "height": { + "@protected": true, + "@id": "untp:height", + "@context": { + "@protected": true, + "value": { + "@id": "untp:value", + "@type": "xsd:double" + }, + "upperTolerance": { + "@id": "untp:upperTolerance", + "@type": "xsd:double" + }, + "lowerTolerance": { + "@id": "untp:lowerTolerance", + "@type": "xsd:double" + }, + "unit": { + "@id": "untp:unit", + "@type": "@vocab", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/UnitMeasureCode#" + } + } + } + }, + "volume": { + "@protected": true, + "@id": "untp:volume", + "@context": { + "@protected": true, + "value": { + "@id": "untp:value", + "@type": "xsd:double" + }, + "upperTolerance": { + "@id": "untp:upperTolerance", + "@type": "xsd:double" + }, + "lowerTolerance": { + "@id": "untp:lowerTolerance", + "@type": "xsd:double" + }, + "unit": { + "@id": "untp:unit", + "@type": "@vocab", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/UnitMeasureCode#" + } + } + } + } + } + }, + "materialUsed": { + "@protected": true, + "@id": "untp:materialUsed", + "@context": { + "@protected": true, + "name": { + "@id": "schema:name" + }, + "originCountry": { + "@protected": true, + "@id": "untp:originCountry", + "@context": { + "@protected": true, + "countryCode": { + "@id": "untp:countryCode", + "@type": "@vocab", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/CountryId#" + } + }, + "countryName": { + "@id": "untp:countryName", + "@type": "xsd:string" + } + } + }, + "materialType": { + "@protected": true, + "@id": "untp:materialType", + "@context": { + "@protected": true, + "code": { + "@id": "untp:code", + "@type": "xsd:string" + }, + "name": { + "@id": "schema:name" + }, + "definition": { + "@id": "untp:definition", + "@type": "xsd:string" + }, + "schemeId": { + "@id": "untp:schemeId", + "@type": "xsd:anyURI" + }, + "schemeName": { + "@id": "untp:schemeName", + "@type": "xsd:string" + } + } + }, + "massFraction": { + "@id": "untp:massFraction", + "@type": "xsd:double" + }, + "mass": { + "@protected": true, + "@id": "untp:mass", + "@context": { + "@protected": true, + "value": { + "@id": "untp:value", + "@type": "xsd:double" + }, + "upperTolerance": { + "@id": "untp:upperTolerance", + "@type": "xsd:double" + }, + "lowerTolerance": { + "@id": "untp:lowerTolerance", + "@type": "xsd:double" + }, + "unit": { + "@id": "untp:unit", + "@type": "@vocab", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/UnitMeasureCode#" + } + } + } + }, + "recycledMassFraction": { + "@id": "untp:recycledMassFraction", + "@type": "xsd:double" + }, + "hazardous": { + "@id": "untp:hazardous", + "@type": "xsd:boolean" + }, + "symbol": { + "@protected": true, + "@id": "untp:symbol", + "@context": { + "@protected": true, + "name": { + "@id": "schema:name" + }, + "description": { + "@id": "schema:description" + }, + "imageData": { + "@id": "untp:imageData", + "@type": "xsd:string" + }, + "mediaType": { + "@id": "https://schema.org/encodingFormat" + } + } + }, + "materialSafetyInformation": { + "@protected": true, + "@id": "untp:materialSafetyInformation", + "@context": { + "@protected": true, + "linkURL": { + "@id": "untp:linkURL", + "@type": "xsd:anyURI" + }, + "linkName": { + "@id": "untp:linkName", + "@type": "xsd:string" + }, + "digestMultibase": { + "@id": "https://w3id.org/security#digestMultibase", + "@type": "https://w3id.org/security#multibase" + }, + "mediaType": { + "@id": "https://schema.org/encodingFormat" + }, + "linkType": { + "@id": "untp:linkType", + "@type": "xsd:string" + } + } + } + } + }, + "packageLabel": { + "@protected": true, + "@id": "untp:packageLabel", + "@context": { + "@protected": true, + "name": { + "@id": "schema:name" + }, + "description": { + "@id": "schema:description" + }, + "imageData": { + "@id": "untp:imageData", + "@type": "xsd:string" + }, + "mediaType": { + "@id": "https://schema.org/encodingFormat" + } + } + }, + "performanceClaim": { + "@id": "untp:performanceClaim", + "@type": "@id" + } + } + }, + "productLabel": { + "@protected": true, + "@id": "untp:productLabel", + "@context": { + "@protected": true, + "name": { + "@id": "schema:name" + }, + "description": { + "@id": "schema:description" + }, + "imageData": { + "@id": "untp:imageData", + "@type": "xsd:string" + }, + "mediaType": { + "@id": "https://schema.org/encodingFormat" + } + } + }, + "performanceClaim": { + "@id": "untp:performanceClaim", + "@type": "@id" + } + } + }, + "ConformityAssessment": { + "@protected": true, + "@id": "untp:ConformityAssessment", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/untp/ConformityAssessment#", + "name": { + "@id": "schema:name" + }, + "description": { + "@id": "schema:description" + }, + "assessmentCriteria": { + "@id": "untp:assessmentCriteria", + "@type": "@id" + }, + "assessmentDate": { + "@id": "untp:assessmentDate", + "@type": "xsd:date" + }, + "assessedPerformance": { + "@protected": true, + "@id": "untp:assessedPerformance", + "@context": { + "@protected": true, + "metric": { + "@id": "untp:metric", + "@type": "@id" + }, + "measure": { + "@protected": true, + "@id": "untp:measure", + "@context": { + "@protected": true, + "value": { + "@id": "untp:value", + "@type": "xsd:double" + }, + "upperTolerance": { + "@id": "untp:upperTolerance", + "@type": "xsd:double" + }, + "lowerTolerance": { + "@id": "untp:lowerTolerance", + "@type": "xsd:double" + }, + "unit": { + "@id": "untp:unit", + "@type": "@vocab", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/UnitMeasureCode#" + } + } + } + }, + "score": { + "@protected": true, + "@id": "untp:score", + "@context": { + "@protected": true, + "code": { + "@id": "untp:code", + "@type": "xsd:string" + }, + "rank": { + "@id": "untp:rank", + "@type": "xsd:integer" + }, + "definition": { + "@id": "untp:definition", + "@type": "xsd:string" + } + } + } + } + }, + "assessedProduct": { + "@protected": true, + "@id": "untp:assessedProduct", + "@context": { + "@protected": true, + "product": { + "@id": "untp:product", + "@type": "@id" + }, + "idVerifiedByCAB": { + "@id": "untp:idVerifiedByCAB", + "@type": "xsd:boolean" + } + } + }, + "assessedFacility": { + "@protected": true, + "@id": "untp:assessedFacility", + "@context": { + "@protected": true, + "facility": { + "@id": "untp:facility", + "@type": "@id" + }, + "idVerifiedByCAB": { + "@id": "untp:idVerifiedByCAB", + "@type": "xsd:boolean" + } + } + }, + "assessedOrganisation": { + "@id": "untp:assessedOrganisation", + "@type": "@id" + }, + "referenceStandard": { + "@id": "untp:referenceStandard", + "@type": "@id" + }, + "referenceRegulation": { + "@id": "untp:referenceRegulation", + "@type": "@id" + }, + "specifiedCondition": { + "@id": "untp:specifiedCondition", + "@type": "xsd:string" + }, + "evidence": { + "@protected": true, + "@id": "untp:evidence", + "@context": { + "@protected": true, + "linkURL": { + "@id": "untp:linkURL", + "@type": "xsd:anyURI" + }, + "linkName": { + "@id": "untp:linkName", + "@type": "xsd:string" + }, + "digestMultibase": { + "@id": "https://w3id.org/security#digestMultibase", + "@type": "https://w3id.org/security#multibase" + }, + "mediaType": { + "@id": "https://schema.org/encodingFormat" + }, + "linkType": { + "@id": "untp:linkType", + "@type": "xsd:string" + } + } + }, + "conformityTopic": { + "@id": "untp:conformityTopic", + "@type": "@id", + "@container": "@set" + }, + "conformance": { + "@id": "untp:conformance", + "@type": "xsd:boolean" + } + } + }, + "ConformityAttestation": { + "@protected": true, + "@id": "untp:ConformityAttestation", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/untp/ConformityAttestation#", + "name": { + "@id": "schema:name" + }, + "description": { + "@id": "schema:description" + }, + "assessorLevel": { + "@id": "untp:assessorLevel", + "@type": "@vocab", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/untp/AssessorLevel#" + } + }, + "assessmentLevel": { + "@id": "untp:assessmentLevel", + "@type": "@vocab", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/untp/AssessmentLevel#" + } + }, + "attestationType": { + "@id": "untp:attestationType", + "@type": "@vocab", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/untp/AttestationType#" + } + }, + "issuedToParty": { + "@id": "untp:issuedToParty", + "@type": "@id" + }, + "authorisation": { + "@protected": true, + "@id": "untp:authorisation", + "@context": { + "@protected": true, + "name": { + "@id": "schema:name" + }, + "trustmark": { + "@protected": true, + "@id": "untp:trustmark", + "@context": { + "@protected": true, + "name": { + "@id": "schema:name" + }, + "description": { + "@id": "schema:description" + }, + "imageData": { + "@id": "untp:imageData", + "@type": "xsd:string" + }, + "mediaType": { + "@id": "https://schema.org/encodingFormat" + } + } + }, + "issuingAuthority": { + "@id": "untp:issuingAuthority", + "@type": "@id" + }, + "endorsementEvidence": { + "@protected": true, + "@id": "untp:endorsementEvidence", + "@context": { + "@protected": true, + "linkURL": { + "@id": "untp:linkURL", + "@type": "xsd:anyURI" + }, + "linkName": { + "@id": "untp:linkName", + "@type": "xsd:string" + }, + "digestMultibase": { + "@id": "https://w3id.org/security#digestMultibase", + "@type": "https://w3id.org/security#multibase" + }, + "mediaType": { + "@id": "https://schema.org/encodingFormat" + }, + "linkType": { + "@id": "untp:linkType", + "@type": "xsd:string" + } + } + } + } + }, + "referenceScheme": { + "@id": "untp:referenceScheme", + "@type": "@id" + }, + "referenceProfile": { + "@id": "untp:referenceProfile", + "@type": "@id" + }, + "profileScore": { + "@protected": true, + "@id": "untp:profileScore", + "@context": { + "@protected": true, + "code": { + "@id": "untp:code", + "@type": "xsd:string" + }, + "rank": { + "@id": "untp:rank", + "@type": "xsd:integer" + }, + "definition": { + "@id": "untp:definition", + "@type": "xsd:string" + } + } + }, + "conformityCertificate": { + "@protected": true, + "@id": "untp:conformityCertificate", + "@context": { + "@protected": true, + "linkURL": { + "@id": "untp:linkURL", + "@type": "xsd:anyURI" + }, + "linkName": { + "@id": "untp:linkName", + "@type": "xsd:string" + }, + "digestMultibase": { + "@id": "https://w3id.org/security#digestMultibase", + "@type": "https://w3id.org/security#multibase" + }, + "mediaType": { + "@id": "https://schema.org/encodingFormat" + }, + "linkType": { + "@id": "untp:linkType", + "@type": "xsd:string" + } + } + }, + "auditableEvidence": { + "@protected": true, + "@id": "untp:auditableEvidence", + "@context": { + "@protected": true, + "linkURL": { + "@id": "untp:linkURL", + "@type": "xsd:anyURI" + }, + "linkName": { + "@id": "untp:linkName", + "@type": "xsd:string" + }, + "digestMultibase": { + "@id": "https://w3id.org/security#digestMultibase", + "@type": "https://w3id.org/security#multibase" + }, + "mediaType": { + "@id": "https://schema.org/encodingFormat" + }, + "linkType": { + "@id": "untp:linkType", + "@type": "xsd:string" + } + } + }, + "trustmark": { + "@protected": true, + "@id": "untp:trustmark", + "@context": { + "@protected": true, + "name": { + "@id": "schema:name" + }, + "description": { + "@id": "schema:description" + }, + "imageData": { + "@id": "untp:imageData", + "@type": "xsd:string" + }, + "mediaType": { + "@id": "https://schema.org/encodingFormat" + } + } + }, + "conformityAssessment": { + "@id": "untp:conformityAssessment", + "@type": "@id" + } + } + }, + "LifecycleEvent": { + "@protected": true, + "@id": "untp:LifecycleEvent", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/untp/LifecycleEvent#", + "name": { + "@id": "schema:name" + }, + "description": { + "@id": "schema:description" + }, + "eventDate": { + "@id": "untp:eventDate", + "@type": "xsd:datetime" + }, + "sensorData": { + "@protected": true, + "@id": "untp:sensorData", + "@context": { + "@protected": true, + "metric": { + "@id": "untp:metric", + "@type": "@id" + }, + "measure": { + "@protected": true, + "@id": "untp:measure", + "@context": { + "@protected": true, + "value": { + "@id": "untp:value", + "@type": "xsd:double" + }, + "upperTolerance": { + "@id": "untp:upperTolerance", + "@type": "xsd:double" + }, + "lowerTolerance": { + "@id": "untp:lowerTolerance", + "@type": "xsd:double" + }, + "unit": { + "@id": "untp:unit", + "@type": "@vocab", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/UnitMeasureCode#" + } + } + } + }, + "rawData": { + "@protected": true, + "@id": "untp:rawData", + "@context": { + "@protected": true, + "linkURL": { + "@id": "untp:linkURL", + "@type": "xsd:anyURI" + }, + "linkName": { + "@id": "untp:linkName", + "@type": "xsd:string" + }, + "digestMultibase": { + "@id": "https://w3id.org/security#digestMultibase", + "@type": "https://w3id.org/security#multibase" + }, + "mediaType": { + "@id": "https://schema.org/encodingFormat" + }, + "linkType": { + "@id": "untp:linkType", + "@type": "xsd:string" + } + } + }, + "sensor": { + "@id": "untp:sensor", + "@type": "@id" + }, + "geoLocation": { + "@protected": true, + "@id": "untp:geoLocation", + "@context": { + "@protected": true, + "latitude": { + "@id": "untp:latitude", + "@type": "xsd:double" + }, + "longitude": { + "@id": "untp:longitude", + "@type": "xsd:double" + } + } + } + } + }, + "relatedDocument": { + "@protected": true, + "@id": "untp:relatedDocument", + "@context": { + "@protected": true, + "linkURL": { + "@id": "untp:linkURL", + "@type": "xsd:anyURI" + }, + "linkName": { + "@id": "untp:linkName", + "@type": "xsd:string" + }, + "digestMultibase": { + "@id": "https://w3id.org/security#digestMultibase", + "@type": "https://w3id.org/security#multibase" + }, + "mediaType": { + "@id": "https://schema.org/encodingFormat" + }, + "linkType": { + "@id": "untp:linkType", + "@type": "xsd:string" + } + } + }, + "activityType": { + "@protected": true, + "@id": "untp:activityType", + "@context": { + "@protected": true, + "code": { + "@id": "untp:code", + "@type": "xsd:string" + }, + "name": { + "@id": "schema:name" + }, + "definition": { + "@id": "untp:definition", + "@type": "xsd:string" + }, + "schemeId": { + "@id": "untp:schemeId", + "@type": "xsd:anyURI" + }, + "schemeName": { + "@id": "untp:schemeName", + "@type": "xsd:string" + } + } + }, + "relatedParty": { + "@protected": true, + "@id": "untp:relatedParty", + "@context": { + "@protected": true, + "role": { + "@id": "untp:role", + "@type": "@vocab", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/untp/PartyRole#" + } + }, + "party": { + "@id": "untp:party", + "@type": "@id" + } + } + } + } + }, + "MakeEvent": { + "@protected": true, + "@id": "untp:MakeEvent", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/untp/MakeEvent#", + "name": { + "@id": "schema:name" + }, + "description": { + "@id": "schema:description" + }, + "eventDate": { + "@id": "untp:eventDate", + "@type": "xsd:datetime" + }, + "sensorData": { + "@protected": true, + "@id": "untp:sensorData", + "@context": { + "@protected": true, + "metric": { + "@id": "untp:metric", + "@type": "@id" + }, + "measure": { + "@protected": true, + "@id": "untp:measure", + "@context": { + "@protected": true, + "value": { + "@id": "untp:value", + "@type": "xsd:double" + }, + "upperTolerance": { + "@id": "untp:upperTolerance", + "@type": "xsd:double" + }, + "lowerTolerance": { + "@id": "untp:lowerTolerance", + "@type": "xsd:double" + }, + "unit": { + "@id": "untp:unit", + "@type": "@vocab", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/UnitMeasureCode#" + } + } + } + }, + "rawData": { + "@protected": true, + "@id": "untp:rawData", + "@context": { + "@protected": true, + "linkURL": { + "@id": "untp:linkURL", + "@type": "xsd:anyURI" + }, + "linkName": { + "@id": "untp:linkName", + "@type": "xsd:string" + }, + "digestMultibase": { + "@id": "https://w3id.org/security#digestMultibase", + "@type": "https://w3id.org/security#multibase" + }, + "mediaType": { + "@id": "https://schema.org/encodingFormat" + }, + "linkType": { + "@id": "untp:linkType", + "@type": "xsd:string" + } + } + }, + "sensor": { + "@id": "untp:sensor", + "@type": "@id" + }, + "geoLocation": { + "@protected": true, + "@id": "untp:geoLocation", + "@context": { + "@protected": true, + "latitude": { + "@id": "untp:latitude", + "@type": "xsd:double" + }, + "longitude": { + "@id": "untp:longitude", + "@type": "xsd:double" + } + } + } + } + }, + "relatedDocument": { + "@protected": true, + "@id": "untp:relatedDocument", + "@context": { + "@protected": true, + "linkURL": { + "@id": "untp:linkURL", + "@type": "xsd:anyURI" + }, + "linkName": { + "@id": "untp:linkName", + "@type": "xsd:string" + }, + "digestMultibase": { + "@id": "https://w3id.org/security#digestMultibase", + "@type": "https://w3id.org/security#multibase" + }, + "mediaType": { + "@id": "https://schema.org/encodingFormat" + }, + "linkType": { + "@id": "untp:linkType", + "@type": "xsd:string" + } + } + }, + "activityType": { + "@protected": true, + "@id": "untp:activityType", + "@context": { + "@protected": true, + "code": { + "@id": "untp:code", + "@type": "xsd:string" + }, + "name": { + "@id": "schema:name" + }, + "definition": { + "@id": "untp:definition", + "@type": "xsd:string" + }, + "schemeId": { + "@id": "untp:schemeId", + "@type": "xsd:anyURI" + }, + "schemeName": { + "@id": "untp:schemeName", + "@type": "xsd:string" + } + } + }, + "relatedParty": { + "@protected": true, + "@id": "untp:relatedParty", + "@context": { + "@protected": true, + "role": { + "@id": "untp:role", + "@type": "@vocab", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/untp/PartyRole#" + } + }, + "party": { + "@id": "untp:party", + "@type": "@id" + } + } + }, + "inputProduct": { + "@protected": true, + "@id": "untp:inputProduct", + "@context": { + "@protected": true, + "product": { + "@id": "untp:product", + "@type": "@id" + }, + "quantity": { + "@protected": true, + "@id": "untp:quantity", + "@context": { + "@protected": true, + "value": { + "@id": "untp:value", + "@type": "xsd:double" + }, + "upperTolerance": { + "@id": "untp:upperTolerance", + "@type": "xsd:double" + }, + "lowerTolerance": { + "@id": "untp:lowerTolerance", + "@type": "xsd:double" + }, + "unit": { + "@id": "untp:unit", + "@type": "@vocab", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/UnitMeasureCode#" + } + } + } + }, + "disposition": { + "@id": "untp:disposition", + "@type": "@vocab", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/untp/ProductStatus#" + } + } + } + }, + "outputProduct": { + "@protected": true, + "@id": "untp:outputProduct", + "@context": { + "@protected": true, + "product": { + "@id": "untp:product", + "@type": "@id" + }, + "quantity": { + "@protected": true, + "@id": "untp:quantity", + "@context": { + "@protected": true, + "value": { + "@id": "untp:value", + "@type": "xsd:double" + }, + "upperTolerance": { + "@id": "untp:upperTolerance", + "@type": "xsd:double" + }, + "lowerTolerance": { + "@id": "untp:lowerTolerance", + "@type": "xsd:double" + }, + "unit": { + "@id": "untp:unit", + "@type": "@vocab", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/UnitMeasureCode#" + } + } + } + }, + "disposition": { + "@id": "untp:disposition", + "@type": "@vocab", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/untp/ProductStatus#" + } + } + } + }, + "madeAtFacility": { + "@id": "untp:madeAtFacility", + "@type": "@id" + } + } + }, + "MoveEvent": { + "@protected": true, + "@id": "untp:MoveEvent", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/untp/MoveEvent#", + "name": { + "@id": "schema:name" + }, + "description": { + "@id": "schema:description" + }, + "eventDate": { + "@id": "untp:eventDate", + "@type": "xsd:datetime" + }, + "sensorData": { + "@protected": true, + "@id": "untp:sensorData", + "@context": { + "@protected": true, + "metric": { + "@id": "untp:metric", + "@type": "@id" + }, + "measure": { + "@protected": true, + "@id": "untp:measure", + "@context": { + "@protected": true, + "value": { + "@id": "untp:value", + "@type": "xsd:double" + }, + "upperTolerance": { + "@id": "untp:upperTolerance", + "@type": "xsd:double" + }, + "lowerTolerance": { + "@id": "untp:lowerTolerance", + "@type": "xsd:double" + }, + "unit": { + "@id": "untp:unit", + "@type": "@vocab", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/UnitMeasureCode#" + } + } + } + }, + "rawData": { + "@protected": true, + "@id": "untp:rawData", + "@context": { + "@protected": true, + "linkURL": { + "@id": "untp:linkURL", + "@type": "xsd:anyURI" + }, + "linkName": { + "@id": "untp:linkName", + "@type": "xsd:string" + }, + "digestMultibase": { + "@id": "https://w3id.org/security#digestMultibase", + "@type": "https://w3id.org/security#multibase" + }, + "mediaType": { + "@id": "https://schema.org/encodingFormat" + }, + "linkType": { + "@id": "untp:linkType", + "@type": "xsd:string" + } + } + }, + "sensor": { + "@id": "untp:sensor", + "@type": "@id" + }, + "geoLocation": { + "@protected": true, + "@id": "untp:geoLocation", + "@context": { + "@protected": true, + "latitude": { + "@id": "untp:latitude", + "@type": "xsd:double" + }, + "longitude": { + "@id": "untp:longitude", + "@type": "xsd:double" + } + } + } + } + }, + "relatedDocument": { + "@protected": true, + "@id": "untp:relatedDocument", + "@context": { + "@protected": true, + "linkURL": { + "@id": "untp:linkURL", + "@type": "xsd:anyURI" + }, + "linkName": { + "@id": "untp:linkName", + "@type": "xsd:string" + }, + "digestMultibase": { + "@id": "https://w3id.org/security#digestMultibase", + "@type": "https://w3id.org/security#multibase" + }, + "mediaType": { + "@id": "https://schema.org/encodingFormat" + }, + "linkType": { + "@id": "untp:linkType", + "@type": "xsd:string" + } + } + }, + "activityType": { + "@protected": true, + "@id": "untp:activityType", + "@context": { + "@protected": true, + "code": { + "@id": "untp:code", + "@type": "xsd:string" + }, + "name": { + "@id": "schema:name" + }, + "definition": { + "@id": "untp:definition", + "@type": "xsd:string" + }, + "schemeId": { + "@id": "untp:schemeId", + "@type": "xsd:anyURI" + }, + "schemeName": { + "@id": "untp:schemeName", + "@type": "xsd:string" + } + } + }, + "relatedParty": { + "@protected": true, + "@id": "untp:relatedParty", + "@context": { + "@protected": true, + "role": { + "@id": "untp:role", + "@type": "@vocab", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/untp/PartyRole#" + } + }, + "party": { + "@id": "untp:party", + "@type": "@id" + } + } + }, + "movedProduct": { + "@protected": true, + "@id": "untp:movedProduct", + "@context": { + "@protected": true, + "product": { + "@id": "untp:product", + "@type": "@id" + }, + "quantity": { + "@protected": true, + "@id": "untp:quantity", + "@context": { + "@protected": true, + "value": { + "@id": "untp:value", + "@type": "xsd:double" + }, + "upperTolerance": { + "@id": "untp:upperTolerance", + "@type": "xsd:double" + }, + "lowerTolerance": { + "@id": "untp:lowerTolerance", + "@type": "xsd:double" + }, + "unit": { + "@id": "untp:unit", + "@type": "@vocab", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/UnitMeasureCode#" + } + } + } + }, + "disposition": { + "@id": "untp:disposition", + "@type": "@vocab", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/untp/ProductStatus#" + } + } + } + }, + "fromFacility": { + "@id": "untp:fromFacility", + "@type": "@id" + }, + "toFacility": { + "@id": "untp:toFacility", + "@type": "@id" + }, + "consignmentId": { + "@id": "untp:consignmentId", + "@type": "xsd:anyURI" + } + } + }, + "ModifyEvent": { + "@protected": true, + "@id": "untp:ModifyEvent", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/untp/ModifyEvent#", + "name": { + "@id": "schema:name" + }, + "description": { + "@id": "schema:description" + }, + "eventDate": { + "@id": "untp:eventDate", + "@type": "xsd:datetime" + }, + "sensorData": { + "@protected": true, + "@id": "untp:sensorData", + "@context": { + "@protected": true, + "metric": { + "@id": "untp:metric", + "@type": "@id" + }, + "measure": { + "@protected": true, + "@id": "untp:measure", + "@context": { + "@protected": true, + "value": { + "@id": "untp:value", + "@type": "xsd:double" + }, + "upperTolerance": { + "@id": "untp:upperTolerance", + "@type": "xsd:double" + }, + "lowerTolerance": { + "@id": "untp:lowerTolerance", + "@type": "xsd:double" + }, + "unit": { + "@id": "untp:unit", + "@type": "@vocab", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/UnitMeasureCode#" + } + } + } + }, + "rawData": { + "@protected": true, + "@id": "untp:rawData", + "@context": { + "@protected": true, + "linkURL": { + "@id": "untp:linkURL", + "@type": "xsd:anyURI" + }, + "linkName": { + "@id": "untp:linkName", + "@type": "xsd:string" + }, + "digestMultibase": { + "@id": "https://w3id.org/security#digestMultibase", + "@type": "https://w3id.org/security#multibase" + }, + "mediaType": { + "@id": "https://schema.org/encodingFormat" + }, + "linkType": { + "@id": "untp:linkType", + "@type": "xsd:string" + } + } + }, + "sensor": { + "@id": "untp:sensor", + "@type": "@id" + }, + "geoLocation": { + "@protected": true, + "@id": "untp:geoLocation", + "@context": { + "@protected": true, + "latitude": { + "@id": "untp:latitude", + "@type": "xsd:double" + }, + "longitude": { + "@id": "untp:longitude", + "@type": "xsd:double" + } + } + } + } + }, + "relatedDocument": { + "@protected": true, + "@id": "untp:relatedDocument", + "@context": { + "@protected": true, + "linkURL": { + "@id": "untp:linkURL", + "@type": "xsd:anyURI" + }, + "linkName": { + "@id": "untp:linkName", + "@type": "xsd:string" + }, + "digestMultibase": { + "@id": "https://w3id.org/security#digestMultibase", + "@type": "https://w3id.org/security#multibase" + }, + "mediaType": { + "@id": "https://schema.org/encodingFormat" + }, + "linkType": { + "@id": "untp:linkType", + "@type": "xsd:string" + } + } + }, + "activityType": { + "@protected": true, + "@id": "untp:activityType", + "@context": { + "@protected": true, + "code": { + "@id": "untp:code", + "@type": "xsd:string" + }, + "name": { + "@id": "schema:name" + }, + "definition": { + "@id": "untp:definition", + "@type": "xsd:string" + }, + "schemeId": { + "@id": "untp:schemeId", + "@type": "xsd:anyURI" + }, + "schemeName": { + "@id": "untp:schemeName", + "@type": "xsd:string" + } + } + }, + "relatedParty": { + "@protected": true, + "@id": "untp:relatedParty", + "@context": { + "@protected": true, + "role": { + "@id": "untp:role", + "@type": "@vocab", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/untp/PartyRole#" + } + }, + "party": { + "@id": "untp:party", + "@type": "@id" + } + } + }, + "modifiedProduct": { + "@protected": true, + "@id": "untp:modifiedProduct", + "@context": { + "@protected": true, + "product": { + "@id": "untp:product", + "@type": "@id" + }, + "quantity": { + "@protected": true, + "@id": "untp:quantity", + "@context": { + "@protected": true, + "value": { + "@id": "untp:value", + "@type": "xsd:double" + }, + "upperTolerance": { + "@id": "untp:upperTolerance", + "@type": "xsd:double" + }, + "lowerTolerance": { + "@id": "untp:lowerTolerance", + "@type": "xsd:double" + }, + "unit": { + "@id": "untp:unit", + "@type": "@vocab", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/UnitMeasureCode#" + } + } + } + }, + "disposition": { + "@id": "untp:disposition", + "@type": "@vocab", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/untp/ProductStatus#" + } + } + } + }, + "modifiedAtFacility": { + "@id": "untp:modifiedAtFacility", + "@type": "@id" + } + } + }, + "RegisteredIdentity": { + "@protected": true, + "@id": "untp:RegisteredIdentity", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/untp/RegisteredIdentity#", + "registeredName": { + "@id": "untp:registeredName", + "@type": "xsd:string" + }, + "registeredId": { + "@id": "untp:registeredId", + "@type": "xsd:string" + }, + "registeredDate": { + "@id": "untp:registeredDate", + "@type": "xsd:date" + }, + "publicInformation": { + "@id": "untp:publicInformation", + "@type": "xsd:anyURI" + }, + "idScheme": { + "@id": "untp:idScheme", + "@type": "@id", + "@context": { + "@protected": true, + "id": { + "@id": "untp:id", + "@type": "xsd:anyURI" + }, + "name": { + "@id": "schema:name" + } + } + }, + "registrar": { + "@id": "untp:registrar", + "@type": "@id" + }, + "registerType": { + "@id": "untp:registerType", + "@type": "@vocab", + "@context": { + "@protected": true, + "@vocab": "https://vocabulary.uncefact.org/untp/RegistryType#" + } + }, + "registrationScope": { + "@id": "untp:registrationScope", + "@type": "xsd:anyURI" + } + } + } + } +} diff --git a/tests/fixtures/upstream/v0.7.0/samples/DigitalProductPassport_battery_instance.json b/tests/fixtures/upstream/v0.7.0/samples/DigitalProductPassport_battery_instance.json new file mode 100644 index 0000000..5a39115 --- /dev/null +++ b/tests/fixtures/upstream/v0.7.0/samples/DigitalProductPassport_battery_instance.json @@ -0,0 +1,720 @@ +{ + "type": [ + "DigitalProductPassport", + "VerifiableCredential" + ], + "@context": [ + "https://www.w3.org/ns/credentials/v2", + "https://vocabulary.uncefact.org/untp/0.7.0/context/" + ], + "id": "https://credentials.sample-battery.example.com/dpp/bat-75kwh-2025", + "issuer": { + "type": [ + "CredentialIssuer" + ], + "id": "did:web:sample-battery.example.com", + "name": "Sample Battery Mfg GmbH", + "issuerAlsoKnownAs": [ + { + "type": [ + "Party" + ], + "id": "https://sample-register.example.com/companies/BAT-001", + "name": "Sample Battery Mfg GmbH", + "registeredId": "BAT-001", + "idScheme": { + "type": [ + "IdentifierScheme" + ], + "id": "https://sample-register.example.com", + "name": "Sample Commercial Register" + } + } + ] + }, + "validFrom": "2025-03-01T00:00:00Z", + "validUntil": "2035-03-01T00:00:00Z", + "name": "Digital Product Passport — 75 kWh Li-ion Battery Pack", + "issuingSoftware": { + "id": "https://sample-software-vendor.example.com/.well-known/untp/software/passport-builder/2026.04.1", + "name": "Sample Passport Builder", + "version": "2026.04.1", + "vendor": { + "id": "did:web:sample-software-vendor.example.com", + "name": "Sample Software Vendor Inc" + } + }, + "credentialSubject": { + "type": [ + "Product" + ], + "id": "https://id.sample-battery.example.com/product/bat-75kwh-2025", + "name": "75 kWh Li-ion Battery Pack", + "description": "75 kWh NMC 811 lithium-ion battery pack for electric vehicle applications. Assembled at the Sample Battery Factory in Salzgitter, Germany. Energy density 166 Wh/kg, designed for 1500+ charge cycles.", + "idScheme": { + "type": [ + "IdentifierScheme" + ], + "id": "https://id.sample-battery.example.com", + "name": "Sample Product Identifier Scheme" + }, + "modelNumber": "BAT-NMC811-75", + "batchNumber": "2025-SZG-0342", + "itemNumber": "BAT-75-2025-00471", + "idGranularity": "item", + "productCategory": [ + { + "code": "46410", + "name": "Primary cells and primary batteries", + "definition": "Primary cells and primary batteries and parts thereof.", + "schemeId": "https://unstats.un.org/unsd/classifications/Econ/cpc/", + "schemeName": "UN Central Product Classification (CPC)" + } + ], + "characteristics": { + "batteryChemistry": "NMC 811 (LiNi0.8Mn0.1Co0.1O2)", + "batteryCategory": "EV", + "ratedCapacity": { + "value": 150, + "unit": "Ah" + }, + "certifiedUsableEnergy": { + "value": 75, + "unit": "kWh" + }, + "nominalVoltage": { + "value": 400, + "unit": "V" + }, + "minimumVoltage": { + "value": 280, + "unit": "V" + }, + "maximumVoltage": { + "value": 450, + "unit": "V" + }, + "originalPowerCapability": { + "value": 250000, + "unit": "W" + }, + "maximumPermittedPower": { + "value": 270000, + "unit": "W" + }, + "initialInternalResistance": { + "cell": { + "value": 0.8, + "unit": "mOhm" + }, + "pack": { + "value": 45, + "unit": "mOhm" + } + }, + "expectedLifetimeYears": 15, + "expectedLifetimeCycles": 1500, + "capacityThresholdForExhaustion": 80, + "temperatureRangeIdleState": { + "lower": -20, + "upper": 50, + "unit": "CEL" + }, + "initialSelfDischargeRate": { + "value": 2, + "unit": "%/month" + }, + "initialRoundTripEnergyEfficiency": 95, + "extinguishingAgent": "Class D dry powder or CO2", + "warrantyPeriodMonths": 96 + }, + "relatedDocument": [ + { + "linkURL": "https://credentials.sample-vap-cab.example.com/dcc/battery-003", + "linkName": "RBA VAP Certification — Sample Battery Factory", + "mediaType": "application/ld+json", + "linkType": "https://test.uncefact.org/vocabulary/linkTypes/dcc" + }, + { + "linkURL": "https://docs.sample-battery.example.com/dismantling/BAT-NMC811-75-manual.pdf", + "linkName": "Dismantling and Disassembly Manual", + "mediaType": "application/pdf", + "linkType": "https://test.uncefact.org/vocabulary/linkTypes/dismantlingInfo" + }, + { + "linkURL": "https://docs.sample-battery.example.com/due-diligence/2025-report.pdf", + "linkName": "Supply Chain Due Diligence Report 2025", + "mediaType": "application/pdf", + "linkType": "https://test.uncefact.org/vocabulary/linkTypes/dueDiligenceReport" + }, + { + "linkURL": "https://docs.sample-battery.example.com/carbon-footprint/BAT-NMC811-75-study.pdf", + "linkName": "Carbon Footprint Study — 75 kWh Battery Pack", + "mediaType": "application/pdf", + "linkType": "https://test.uncefact.org/vocabulary/linkTypes/carbonFootprintStudy" + }, + { + "linkURL": "https://docs.sample-battery.example.com/spare-parts/BAT-NMC811-75.html", + "linkName": "Spare Parts and Service Information", + "mediaType": "text/html", + "linkType": "https://test.uncefact.org/vocabulary/linkTypes/sparePartsInfo" + }, + { + "linkURL": "https://docs.sample-battery.example.com/safety/BAT-NMC811-75-measures.pdf", + "linkName": "Safety Measures", + "mediaType": "application/pdf", + "linkType": "https://test.uncefact.org/vocabulary/linkTypes/safetyMeasures" + }, + { + "linkURL": "https://docs.sample-battery.example.com/end-of-life/battery-collection-guidance.pdf", + "linkName": "Battery Collection and End-of-Life Treatment Guidance", + "mediaType": "application/pdf", + "linkType": "https://test.uncefact.org/vocabulary/linkTypes/endOfLifeInfo" + }, + { + "linkURL": "https://docs.sample-battery.example.com/conformity/eu-doc-BAT-NMC811-75.pdf", + "linkName": "EU Declaration of Conformity", + "mediaType": "application/pdf", + "linkType": "https://test.uncefact.org/vocabulary/linkTypes/euDeclarationOfConformity" + } + ], + "relatedParty": [ + { + "role": "manufacturer", + "party": { + "type": [ + "Party" + ], + "id": "did:web:sample-battery.example.com", + "name": "Sample Battery Mfg GmbH", + "registeredId": "BAT-001", + "idScheme": { + "type": [ + "IdentifierScheme" + ], + "id": "https://sample-register.example.com", + "name": "Sample Commercial Register" + }, + "registrationCountry": { + "countryCode": "DE", + "countryName": "Germany" + } + } + } + ], + "producedAtFacility": { + "type": [ + "Facility" + ], + "id": "https://facility-register.example.com/fac-003", + "name": "Sample Battery Factory", + "registeredId": "fac-003", + "idScheme": { + "type": [ + "IdentifierScheme" + ], + "id": "https://facility-register.example.com", + "name": "UNTP Sample Facility Register" + } + }, + "productionDate": "2025-03-01", + "countryOfProduction": { + "countryCode": "DE", + "countryName": "Germany" + }, + "dimensions": { + "weight": { + "value": 450, + "unit": "KGM" + }, + "length": { + "value": 2100, + "unit": "MMT" + }, + "width": { + "value": 1500, + "unit": "MMT" + }, + "height": { + "value": 150, + "unit": "MMT" + } + }, + "materialProvenance": [ + { + "name": "Copper cathode", + "originCountry": { + "countryCode": "JP", + "countryName": "Japan" + }, + "materialType": { + "code": "41521", + "name": "Unwrought copper", + "definition": "Copper, unrefined; copper anodes for electrolytic refining; refined copper and copper alloys, unwrought.", + "schemeId": "https://unstats.un.org/unsd/classifications/Econ/cpc/", + "schemeName": "UN Central Product Classification (CPC)" + }, + "massFraction": 0.08, + "mass": { + "value": 36, + "unit": "KGM" + }, + "recycledMassFraction": 0.12, + "hazardous": false + }, + { + "name": "Cobalt sulphate", + "originCountry": { + "countryCode": "CD", + "countryName": "Congo (Democratic Republic of the)" + }, + "materialType": { + "code": "14210", + "name": "Cobalt ores and concentrates", + "definition": "Cobalt ores and concentrates.", + "schemeId": "https://unstats.un.org/unsd/classifications/Econ/cpc/", + "schemeName": "UN Central Product Classification (CPC)" + }, + "massFraction": 0.05, + "mass": { + "value": 22.5, + "unit": "KGM" + }, + "recycledMassFraction": 0.16, + "hazardous": false + }, + { + "name": "Lithium carbonate", + "originCountry": { + "countryCode": "CL", + "countryName": "Chile" + }, + "materialType": { + "code": "14290", + "name": "Other non-ferrous metal ores and concentrates", + "definition": "Lithium, beryllium, and other non-ferrous metal ores and concentrates.", + "schemeId": "https://unstats.un.org/unsd/classifications/Econ/cpc/", + "schemeName": "UN Central Product Classification (CPC)" + }, + "massFraction": 0.12, + "mass": { + "value": 54, + "unit": "KGM" + }, + "recycledMassFraction": 0.06, + "hazardous": false + }, + { + "name": "Nickel sulphate", + "originCountry": { + "countryCode": "ID", + "countryName": "Indonesia" + }, + "materialType": { + "code": "14230", + "name": "Nickel ores and concentrates", + "definition": "Nickel ores and concentrates.", + "schemeId": "https://unstats.un.org/unsd/classifications/Econ/cpc/", + "schemeName": "UN Central Product Classification (CPC)" + }, + "massFraction": 0.35, + "mass": { + "value": 157.5, + "unit": "KGM" + }, + "recycledMassFraction": 0.08, + "hazardous": false + }, + { + "name": "Graphite (anode material)", + "originCountry": { + "countryCode": "MZ", + "countryName": "Mozambique" + }, + "materialType": { + "code": "15310", + "name": "Natural graphite", + "definition": "Natural graphite.", + "schemeId": "https://unstats.un.org/unsd/classifications/Econ/cpc/", + "schemeName": "UN Central Product Classification (CPC)" + }, + "massFraction": 0.18, + "mass": { + "value": 81, + "unit": "KGM" + }, + "recycledMassFraction": 0, + "hazardous": false + }, + { + "name": "Other components (electrolyte, separator, casing, BMS)", + "originCountry": { + "countryCode": "DE", + "countryName": "Germany" + }, + "materialType": { + "code": "46410", + "name": "Primary cells and primary batteries", + "definition": "Primary cells and primary batteries and parts thereof.", + "schemeId": "https://unstats.un.org/unsd/classifications/Econ/cpc/", + "schemeName": "UN Central Product Classification (CPC)" + }, + "massFraction": 0.22, + "mass": { + "value": 99, + "unit": "KGM" + }, + "recycledMassFraction": 0.25, + "hazardous": false + } + ], + "packaging": { + "description": "Reinforced steel transit crate with foam inserts", + "dimensions": { + "weight": { + "value": 35, + "unit": "KGM" + }, + "length": { + "value": 2300, + "unit": "MMT" + }, + "width": { + "value": 1700, + "unit": "MMT" + }, + "height": { + "value": 350, + "unit": "MMT" + } + }, + "materialUsed": [ + { + "name": "Steel crate", + "originCountry": { + "countryCode": "DE", + "countryName": "Germany" + }, + "materialType": { + "code": "41211", + "name": "Flat-rolled products of iron or non-alloy steel", + "schemeId": "https://unstats.un.org/unsd/classifications/Econ/cpc/", + "schemeName": "UN Central Product Classification (CPC)" + }, + "massFraction": 0.85, + "recycledMassFraction": 0.7, + "hazardous": false + } + ] + }, + "productLabel": [ + { + "name": "CE Marking", + "description": "EU conformity marking for the battery pack", + "imageData": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==", + "mediaType": "image/png" + }, + { + "name": "Separate Collection Symbol", + "description": "Crossed-out wheeled bin indicating separate collection requirement per EU Battery Regulation Article 13", + "imageData": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==", + "mediaType": "image/png" + }, + { + "name": "Carbon Footprint Performance Class", + "description": "Battery carbon footprint class label (Class B) per EU Battery Regulation Article 7", + "imageData": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==", + "mediaType": "image/png" + } + ], + "performanceClaim": [ + { + "type": [ + "Claim" + ], + "id": "https://sample-battery.example.com/claims/battery-carbon-2025", + "name": "Battery Carbon Footprint", + "description": "Cradle-to-gate carbon footprint of the 75 kWh battery pack per kWh of capacity, covering all lifecycle stages as required by EU Battery Regulation Article 7.", + "referenceRegulation": [ + { + "type": [ + "Regulation" + ], + "id": "https://eur-lex.europa.eu/eli/reg/2023/1542/oj", + "name": "EU Battery Regulation (EU) 2023/1542" + } + ], + "referenceCriteria": [ + { + "type": [ + "Criterion" + ], + "id": "https://sample-scheme.responsiblebusiness.org/criteria/ghg-reporting/v8", + "name": "GHG Emissions Reporting (RBA Code of Conduct Section C.1)" + } + ], + "claimDate": "2025-03-01", + "claimedPerformance": [ + { + "metric": { + "type": [ + "PerformanceMetric" + ], + "id": "https://vocabulary.uncefact.org/performance-metric/product-carbon-footprint", + "name": "Product Carbon Footprint" + }, + "measure": { + "value": 61, + "unit": "KGM" + }, + "score": { + "code": "B", + "rank": 2, + "definition": "Carbon footprint performance class B per EU Battery Regulation" + } + } + ], + "evidence": [ + { + "linkURL": "https://credentials.sample-cab.example.com/dcc/carbon-verification-bat-75kwh", + "linkName": "Carbon Footprint Verification — Sample CAB", + "mediaType": "application/ld+json", + "linkType": "https://test.uncefact.org/vocabulary/linkTypes/dcc" + } + ], + "conformityTopic": [ + { + "type": [ + "ConformityTopic" + ], + "id": "https://vocabulary.uncefact.org/conformity-topic/greenhouse-gas-emissions", + "name": "Greenhouse Gas Emissions" + } + ] + }, + { + "type": [ + "Claim" + ], + "id": "https://sample-battery.example.com/claims/battery-recycled-2025", + "name": "Recycled Content — Battery Pack", + "description": "Recycled content percentages for critical raw materials (cobalt, lithium, nickel, lead) as required by EU Battery Regulation Article 8.", + "referenceRegulation": [ + { + "type": [ + "Regulation" + ], + "id": "https://eur-lex.europa.eu/eli/reg/2023/1542/oj", + "name": "EU Battery Regulation (EU) 2023/1542" + } + ], + "referenceCriteria": [ + { + "type": [ + "Criterion" + ], + "id": "https://sample-scheme.responsiblebusiness.org/criteria/recycled-content/v8", + "name": "Recycled Content Requirements (RBA Code of Conduct Section C.5)" + } + ], + "claimDate": "2025-03-01", + "claimedPerformance": [ + { + "metric": { + "type": [ + "PerformanceMetric" + ], + "id": "https://vocabulary.uncefact.org/performance-metric/recycled-content-percentage", + "name": "Recycled Content Percentage" + }, + "measure": { + "value": 16, + "unit": "P1" + } + }, + { + "metric": { + "type": [ + "PerformanceMetric" + ], + "id": "https://vocabulary.uncefact.org/performance-metric/recycled-content-percentage", + "name": "Cobalt Recycled Content" + }, + "measure": { + "value": 16, + "unit": "P1" + } + }, + { + "metric": { + "type": [ + "PerformanceMetric" + ], + "id": "https://vocabulary.uncefact.org/performance-metric/recycled-content-percentage", + "name": "Lithium Recycled Content" + }, + "measure": { + "value": 6, + "unit": "P1" + } + }, + { + "metric": { + "type": [ + "PerformanceMetric" + ], + "id": "https://vocabulary.uncefact.org/performance-metric/recycled-content-percentage", + "name": "Nickel Recycled Content" + }, + "measure": { + "value": 8, + "unit": "P1" + } + } + ], + "conformityTopic": [ + { + "type": [ + "ConformityTopic" + ], + "id": "https://vocabulary.uncefact.org/conformity-topic/recycled-material-integration", + "name": "Recycled Material Integration" + } + ] + }, + { + "type": [ + "Claim" + ], + "id": "https://sample-battery.example.com/claims/battery-efficiency-2025", + "name": "Round Trip Energy Efficiency", + "description": "Initial round trip energy efficiency and projected efficiency at 50% of cycle-life.", + "referenceRegulation": [ + { + "type": [ + "Regulation" + ], + "id": "https://eur-lex.europa.eu/eli/reg/2023/1542/oj", + "name": "EU Battery Regulation (EU) 2023/1542" + } + ], + "referenceCriteria": [ + { + "type": [ + "Criterion" + ], + "id": "https://sample-scheme.responsiblebusiness.org/criteria/energy-efficiency/v8", + "name": "Energy Efficiency (RBA Code of Conduct Section C.3)" + } + ], + "claimDate": "2025-03-01", + "claimedPerformance": [ + { + "metric": { + "type": [ + "PerformanceMetric" + ], + "id": "https://vocabulary.uncefact.org/performance-metric/energy-intensity", + "name": "Initial Round Trip Energy Efficiency" + }, + "measure": { + "value": 95, + "unit": "P1" + } + }, + { + "metric": { + "type": [ + "PerformanceMetric" + ], + "id": "https://vocabulary.uncefact.org/performance-metric/energy-intensity", + "name": "Round Trip Energy Efficiency at 50% Cycle-life" + }, + "measure": { + "value": 90, + "unit": "P1" + } + } + ], + "conformityTopic": [ + { + "type": [ + "ConformityTopic" + ], + "id": "https://vocabulary.uncefact.org/conformity-topic/energy-optimization", + "name": "Energy Optimization" + } + ] + }, + { + "type": [ + "Claim" + ], + "id": "https://sample-battery.example.com/claims/battery-due-diligence-2025", + "name": "Ethical Material Sourcing", + "description": "Compliance with supply chain due diligence obligations under EU Battery Regulation Articles 48-52 and OECD Due Diligence Guidance for Responsible Supply Chains of Minerals.", + "referenceRegulation": [ + { + "type": [ + "Regulation" + ], + "id": "https://eur-lex.europa.eu/eli/reg/2023/1542/oj", + "name": "EU Battery Regulation (EU) 2023/1542" + } + ], + "referenceStandard": [ + { + "type": [ + "Standard" + ], + "id": "https://www.oecd.org/corporate/mne/mining.htm", + "name": "OECD Due Diligence Guidance for Responsible Supply Chains of Minerals" + } + ], + "referenceCriteria": [ + { + "type": [ + "Criterion" + ], + "id": "https://sample-scheme.responsiblebusiness.org/criteria/responsible-minerals/v8", + "name": "Responsible Minerals Sourcing (RBA Code of Conduct Section C.7)" + } + ], + "claimDate": "2025-03-01", + "claimedPerformance": [ + { + "metric": { + "type": [ + "PerformanceMetric" + ], + "id": "https://vocabulary.uncefact.org/performance-metric/supplier-due-diligence-coverage", + "name": "Supplier Due Diligence Coverage" + }, + "score": { + "code": "compliant", + "rank": 1, + "definition": "Fully compliant with due diligence obligations" + } + } + ], + "evidence": [ + { + "linkURL": "https://credentials.sample-cab.example.com/dcc/due-diligence-bat-2025", + "linkName": "Third-party Due Diligence Assurance — Sample CAB", + "mediaType": "application/ld+json", + "linkType": "https://test.uncefact.org/vocabulary/linkTypes/dcc" + } + ], + "conformityTopic": [ + { + "type": [ + "ConformityTopic" + ], + "id": "https://vocabulary.uncefact.org/conformity-topic/ethical-material-sourcing", + "name": "Ethical Material Sourcing" + } + ] + } + ] + } +} diff --git a/tests/fixtures/upstream/v0.7.0/samples/DigitalProductPassport_cathode_instance.json b/tests/fixtures/upstream/v0.7.0/samples/DigitalProductPassport_cathode_instance.json new file mode 100644 index 0000000..2e87963 --- /dev/null +++ b/tests/fixtures/upstream/v0.7.0/samples/DigitalProductPassport_cathode_instance.json @@ -0,0 +1,284 @@ +{ + "type": [ + "DigitalProductPassport", + "VerifiableCredential" + ], + "@context": [ + "https://www.w3.org/ns/credentials/v2", + "https://vocabulary.uncefact.org/untp/0.7.0/context/" + ], + "id": "https://credentials.sample-refinery.example.com/dpp/cu-cathode-2025", + "issuer": { + "type": [ + "CredentialIssuer" + ], + "id": "did:web:sample-refinery.example.com", + "name": "Sample Copper Refinery Co. Ltd", + "issuerAlsoKnownAs": [ + { + "type": [ + "Party" + ], + "id": "https://www.sample-register.example.com/henkorireki-johoto.html?selHouzinNo=REF-001", + "name": "Sample Copper Refinery Co. Ltd", + "registeredId": "REF-001", + "idScheme": { + "type": [ + "IdentifierScheme" + ], + "id": "https://www.sample-register.example.com", + "name": "Japan Corporate Number (Houjin Bangou)" + } + } + ] + }, + "validFrom": "2025-03-01T00:00:00Z", + "validUntil": "2026-03-01T00:00:00Z", + "name": "Digital Product Passport — LME Grade A Copper Cathode", + "issuingSoftware": { + "id": "https://sample-software-vendor.example.com/.well-known/untp/software/passport-builder/2026.04.1", + "name": "Sample Passport Builder", + "version": "2026.04.1", + "vendor": { + "id": "did:web:sample-software-vendor.example.com", + "name": "Sample Software Vendor Inc" + } + }, + "credentialSubject": { + "type": [ + "Product" + ], + "id": "https://id.sample-refinery.example.com/product/cu-cathode-2025", + "name": "LME Grade A Copper Cathode", + "description": "LME Grade A copper cathode (Cu 99.99%) produced by electrolytic refining at Sample Copper Refinery. Each cathode weighs approximately 125 kg and meets London Metal Exchange delivery specifications.", + "idScheme": { + "type": [ + "IdentifierScheme" + ], + "id": "https://id.sample-refinery.example.com", + "name": "Sample Product Identifier Scheme" + }, + "modelNumber": "SR-CU-CATH-9999", + "batchNumber": "2025-Q1-0812", + "idGranularity": "model", + "productCategory": [ + { + "code": "41521", + "name": "Unwrought copper", + "definition": "Copper, unrefined; copper anodes for electrolytic refining; refined copper and copper alloys, unwrought.", + "schemeId": "https://unstats.un.org/unsd/classifications/Econ/cpc/", + "schemeName": "UN Central Product Classification (CPC)" + } + ], + "relatedDocument": [ + { + "linkURL": "https://credentials.sample-cab.example.com/dcc/smelter-002", + "linkName": "Coppermark Certification — Sample Copper Refinery", + "mediaType": "application/ld+json", + "linkType": "https://test.uncefact.org/vocabulary/linkTypes/dcc" + } + ], + "relatedParty": [ + { + "role": "manufacturer", + "party": { + "type": [ + "Party" + ], + "id": "did:web:sample-refinery.example.com", + "name": "Sample Copper Refinery Co. Ltd", + "registeredId": "REF-001", + "idScheme": { + "type": [ + "IdentifierScheme" + ], + "id": "https://www.sample-register.example.com", + "name": "Japan Corporate Number (Houjin Bangou)" + }, + "registrationCountry": { + "countryCode": "JP", + "countryName": "Japan" + } + } + } + ], + "producedAtFacility": { + "type": [ + "Facility" + ], + "id": "https://facility-register.example.com/fac-002", + "name": "Sample Copper Refinery", + "registeredId": "fac-002", + "idScheme": { + "type": [ + "IdentifierScheme" + ], + "id": "https://facility-register.example.com", + "name": "UNTP Sample Facility Register" + } + }, + "productionDate": "2025-03-01", + "countryOfProduction": { + "countryCode": "JP", + "countryName": "Japan" + }, + "dimensions": { + "weight": { + "value": 125, + "unit": "KGM" + } + }, + "materialProvenance": [ + { + "name": "Copper concentrate", + "originCountry": { + "countryCode": "ZM", + "countryName": "Zambia" + }, + "materialType": { + "code": "14110", + "name": "Copper ores and concentrates", + "definition": "Copper ores and concentrates obtained from mining operations.", + "schemeId": "https://unstats.un.org/unsd/classifications/Econ/cpc/", + "schemeName": "UN Central Product Classification (CPC)" + }, + "massFraction": 0.88, + "mass": { + "value": 110, + "unit": "KGM" + }, + "recycledMassFraction": 0, + "hazardous": false + }, + { + "name": "Recycled copper scrap", + "originCountry": { + "countryCode": "JP", + "countryName": "Japan" + }, + "materialType": { + "code": "41521", + "name": "Unwrought copper", + "definition": "Copper, unrefined; copper anodes for electrolytic refining; refined copper and copper alloys, unwrought.", + "schemeId": "https://unstats.un.org/unsd/classifications/Econ/cpc/", + "schemeName": "UN Central Product Classification (CPC)" + }, + "massFraction": 0.12, + "mass": { + "value": 15, + "unit": "KGM" + }, + "recycledMassFraction": 1, + "hazardous": false + } + ], + "performanceClaim": [ + { + "type": [ + "Claim" + ], + "id": "https://sample-refinery.example.com/claims/product-carbon-2025", + "name": "Product Carbon Footprint — Copper Cathode", + "description": "Cradle-to-gate carbon footprint of copper cathode per tonne produced at Sample smelter.", + "referenceCriteria": [ + { + "type": [ + "Criterion" + ], + "id": "https://sample-scheme.coppermark.org/criteria/ghg-management/v3", + "name": "GHG Emissions Management (Coppermark RRA Criterion 26)", + "conformityTopic": [ + { + "type": [ + "ConformityTopic" + ], + "id": "https://vocabulary.uncefact.org/conformity-topic/greenhouse-gas-emissions", + "name": "Greenhouse Gas Emissions", + "definition": "Assessment of direct and indirect greenhouse gas emissions across scopes 1, 2, and 3, including measurement, reporting, and reduction targets aligned with climate science." + } + ] + } + ], + "claimDate": "2025-03-01", + "claimedPerformance": [ + { + "metric": { + "type": [ + "PerformanceMetric" + ], + "id": "https://vocabulary.uncefact.org/performance-metric/product-carbon-footprint", + "name": "Product Carbon Footprint" + }, + "measure": { + "value": 3.8, + "unit": "KGM" + } + } + ], + "conformityTopic": [ + { + "type": [ + "ConformityTopic" + ], + "id": "https://vocabulary.uncefact.org/conformity-topic/greenhouse-gas-emissions", + "name": "Greenhouse Gas Emissions", + "definition": "Assessment of direct and indirect greenhouse gas emissions across scopes 1, 2, and 3, including measurement, reporting, and reduction targets aligned with climate science." + } + ] + }, + { + "type": [ + "Claim" + ], + "id": "https://sample-refinery.example.com/claims/product-recycled-2025", + "name": "Recycled Content — Copper Cathode", + "description": "Percentage of recycled copper content in cathode output at Sample smelter.", + "referenceCriteria": [ + { + "type": [ + "Criterion" + ], + "id": "https://sample-scheme.coppermark.org/criteria/recycled-feedstock/v3", + "name": "Recycled Feedstock Management (Coppermark RRA Criterion 31)", + "conformityTopic": [ + { + "type": [ + "ConformityTopic" + ], + "id": "https://vocabulary.uncefact.org/conformity-topic/recycled-material-integration", + "name": "Recycled Material Integration", + "definition": "Incorporation of recycled and reclaimed materials into products and processes, promoting circular material flows and reducing demand for virgin resources." + } + ] + } + ], + "claimDate": "2025-03-01", + "claimedPerformance": [ + { + "metric": { + "type": [ + "PerformanceMetric" + ], + "id": "https://vocabulary.uncefact.org/performance-metric/recycled-content-percentage", + "name": "Recycled Content Percentage" + }, + "measure": { + "value": 12, + "unit": "P1" + } + } + ], + "conformityTopic": [ + { + "type": [ + "ConformityTopic" + ], + "id": "https://vocabulary.uncefact.org/conformity-topic/recycled-material-integration", + "name": "Recycled Material Integration", + "definition": "Incorporation of recycled and reclaimed materials into products and processes, promoting circular material flows and reducing demand for virgin resources." + } + ] + } + ] + } +} diff --git a/tests/fixtures/upstream/v0.7.0/samples/DigitalProductPassport_instance.json b/tests/fixtures/upstream/v0.7.0/samples/DigitalProductPassport_instance.json new file mode 100644 index 0000000..b8f686c --- /dev/null +++ b/tests/fixtures/upstream/v0.7.0/samples/DigitalProductPassport_instance.json @@ -0,0 +1,263 @@ +{ + "type": [ + "DigitalProductPassport", + "VerifiableCredential" + ], + "@context": [ + "https://www.w3.org/ns/credentials/v2", + "https://vocabulary.uncefact.org/untp/0.7.0/context/" + ], + "id": "https://credentials.sample-mine.example.com/dpp/cu-conc-2025", + "issuer": { + "type": [ + "CredentialIssuer" + ], + "id": "did:web:sample-mine.example.com", + "name": "Sample Copper Mine Pty Ltd", + "issuerAlsoKnownAs": [ + { + "type": [ + "Party" + ], + "id": "https://sample-register.example.com/companies/MINE-001", + "name": "Sample Copper Mine Pty Ltd", + "registeredId": "MINE-001", + "idScheme": { + "type": [ + "IdentifierScheme" + ], + "id": "https://sample-register.example.com", + "name": "Patents and Companies Registration Agency (Zambia)" + } + } + ] + }, + "validFrom": "2025-03-01T00:00:00Z", + "validUntil": "2026-03-01T00:00:00Z", + "name": "Digital Product Passport — Copper Concentrate (Cu 30%)", + "issuingSoftware": { + "id": "https://sample-software-vendor.example.com/.well-known/untp/software/passport-builder/2026.04.1", + "name": "Sample Passport Builder", + "version": "2026.04.1", + "vendor": { + "id": "did:web:sample-software-vendor.example.com", + "name": "Sample Software Vendor Inc" + } + }, + "credentialSubject": { + "type": [ + "Product" + ], + "id": "https://id.sample-mine.example.com/product/cu-conc-2025", + "name": "Copper Concentrate (Cu 30%)", + "description": "Copper sulphide flotation concentrate with approximately 30% copper content, produced at Sample Copper Mine. Suitable for smelting to produce refined copper cathode.", + "idScheme": { + "type": [ + "IdentifierScheme" + ], + "id": "https://id.sample-mine.example.com", + "name": "Sample Product Identifier Scheme" + }, + "modelNumber": "SM-CU-CONC-30", + "batchNumber": "2025-Q1-4501", + "idGranularity": "model", + "productCategory": [ + { + "code": "14110", + "name": "Copper ores and concentrates", + "definition": "Copper ores and concentrates obtained from mining operations.", + "schemeId": "https://unstats.un.org/unsd/classifications/Econ/cpc/", + "schemeName": "UN Central Product Classification (CPC)" + } + ], + "relatedDocument": [ + { + "linkURL": "https://credentials.sample-cab.example.com/dcc/mine-001", + "linkName": "Coppermark Certification — Sample Copper Mine", + "mediaType": "application/ld+json", + "linkType": "https://test.uncefact.org/vocabulary/linkTypes/dcc" + } + ], + "relatedParty": [ + { + "role": "manufacturer", + "party": { + "type": [ + "Party" + ], + "id": "did:web:sample-mine.example.com", + "name": "Sample Copper Mine Pty Ltd", + "registeredId": "MINE-001", + "idScheme": { + "type": [ + "IdentifierScheme" + ], + "id": "https://sample-register.example.com", + "name": "Patents and Companies Registration Agency (Zambia)" + }, + "registrationCountry": { + "countryCode": "ZM", + "countryName": "Zambia" + } + } + } + ], + "producedAtFacility": { + "type": [ + "Facility" + ], + "id": "https://facility-register.example.com/fac-001", + "name": "Sample Copper Mine", + "registeredId": "fac-001", + "idScheme": { + "type": [ + "IdentifierScheme" + ], + "id": "https://facility-register.example.com", + "name": "UNTP Sample Facility Register" + } + }, + "productionDate": "2025-03-01", + "countryOfProduction": { + "countryCode": "ZM", + "countryName": "Zambia" + }, + "dimensions": { + "weight": { + "value": 1000, + "unit": "KGM" + } + }, + "materialProvenance": [ + { + "name": "Copper ore", + "originCountry": { + "countryCode": "ZM", + "countryName": "Zambia" + }, + "materialType": { + "code": "14110", + "name": "Copper ores and concentrates", + "definition": "Copper ores and concentrates obtained from mining operations.", + "schemeId": "https://unstats.un.org/unsd/classifications/Econ/cpc/", + "schemeName": "UN Central Product Classification (CPC)" + }, + "massFraction": 0.3, + "mass": { + "value": 300, + "unit": "KGM" + }, + "recycledMassFraction": 0, + "hazardous": false + } + ], + "performanceClaim": [ + { + "type": [ + "Claim" + ], + "id": "https://sample-mine.example.com/claims/product-carbon-2025", + "name": "Product Carbon Footprint — Copper Concentrate", + "description": "Cradle-to-gate carbon footprint of copper concentrate per tonne produced at Sample mine.", + "referenceCriteria": [ + { + "type": [ + "Criterion" + ], + "id": "https://sample-scheme.coppermark.org/criteria/ghg-management/v3", + "name": "GHG Emissions Management (Coppermark RRA Criterion 26)", + "conformityTopic": [ + { + "type": [ + "ConformityTopic" + ], + "id": "https://vocabulary.uncefact.org/conformity-topic/greenhouse-gas-emissions", + "name": "Greenhouse Gas Emissions", + "definition": "Assessment of direct and indirect greenhouse gas emissions across scopes 1, 2, and 3, including measurement, reporting, and reduction targets aligned with climate science." + } + ] + } + ], + "claimDate": "2025-03-01", + "claimedPerformance": [ + { + "metric": { + "type": [ + "PerformanceMetric" + ], + "id": "https://vocabulary.uncefact.org/performance-metric/product-carbon-footprint", + "name": "Product Carbon Footprint" + }, + "measure": { + "value": 2.1, + "unit": "KGM" + } + } + ], + "conformityTopic": [ + { + "type": [ + "ConformityTopic" + ], + "id": "https://vocabulary.uncefact.org/conformity-topic/greenhouse-gas-emissions", + "name": "Greenhouse Gas Emissions", + "definition": "Assessment of direct and indirect greenhouse gas emissions across scopes 1, 2, and 3, including measurement, reporting, and reduction targets aligned with climate science." + } + ] + }, + { + "type": [ + "Claim" + ], + "id": "https://sample-mine.example.com/claims/product-water-2025", + "name": "Water Intensity — Copper Concentrate", + "description": "Water consumption per tonne of copper concentrate produced at Sample mine.", + "referenceCriteria": [ + { + "type": [ + "Criterion" + ], + "id": "https://sample-scheme.coppermark.org/criteria/water-stewardship/v3", + "name": "Water Stewardship (Coppermark RRA Criterion 27)", + "conformityTopic": [ + { + "type": [ + "ConformityTopic" + ], + "id": "https://vocabulary.uncefact.org/conformity-topic/water-conservation", + "name": "Water Conservation", + "definition": "Efficient and responsible management of water resources, including reduction of water consumption, recycling, and protection of water quality in operations and supply chains." + } + ] + } + ], + "claimDate": "2025-03-01", + "claimedPerformance": [ + { + "metric": { + "type": [ + "PerformanceMetric" + ], + "id": "https://vocabulary.uncefact.org/performance-metric/water-intensity", + "name": "Water Intensity" + }, + "measure": { + "value": 15, + "unit": "MTQ" + } + } + ], + "conformityTopic": [ + { + "type": [ + "ConformityTopic" + ], + "id": "https://vocabulary.uncefact.org/conformity-topic/water-conservation", + "name": "Water Conservation", + "definition": "Efficient and responsible management of water resources, including reduction of water consumption, recycling, and protection of water quality in operations and supply chains." + } + ] + } + ] + } +} diff --git a/tests/fixtures/upstream/v0.7.0/schema/DigitalProductPassport.json b/tests/fixtures/upstream/v0.7.0/schema/DigitalProductPassport.json new file mode 100644 index 0000000..dcdf7db --- /dev/null +++ b/tests/fixtures/upstream/v0.7.0/schema/DigitalProductPassport.json @@ -0,0 +1,1455 @@ +{ + "$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://vocabulary.uncefact.org/untp/0.7.0/context/" + ], + "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://vocabulary.uncefact.org/untp/0.7.0/context/", + "type": "string" + } + ], + "default": [ + "https://www.w3.org/ns/credentials/v2", + "https://vocabulary.uncefact.org/untp/0.7.0/context/" + ], + "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 this verifiable credential." + }, + "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": { + "example": "2024-03-15T12:00:00Z", + "type": "string", + "format": "date-time", + "description": "The date and time from which the credential is valid." + }, + "validUntil": { + "example": "2034-03-15T12:00:00Z", + "type": "string", + "format": "date-time", + "description": "The expiry date (if applicable) of this verifiable credential." + }, + "name": { + "example": "Some name", + "type": "string", + "description": "Name of this verifiable credential instance (eg the title of a digital product passport, facility record, lifecycle event, or conformity credential)" + }, + "credentialStatus": { + "$ref": "#/$defs/BitstringStatusListEntry", + "description": "A W3C VCDM2.0 compliant object containing credential status information." + }, + "renderMethod": { + "type": "array", + "items": { + "$ref": "#/$defs/RenderTemplate2024" + }, + "description": "Human rendering information for this credential. An array of render methods (eg RenderTemplate2024) that may be used to display the credential." + }, + "credentialSubject": { + "$ref": "#/$defs/Product", + "description": "The product that is the subject of this digital product passport." + }, + "issuingSoftware": { + "$ref": "#/$defs/IssuingSoftware", + "description": "Optional metadata identifying the software product (and its vendor) that issued this credential. Useful for vendor traceability and conformity testing. Issuers MAY omit this property." + } + }, + "description": "A digital Product Passport (DPP) credential.", + "required": [ + "@context", + "id", + "issuer", + "validFrom", + "name", + "credentialSubject" + ], + "$defs": { + "CredentialIssuer": { + "type": "object", + "additionalProperties": true, + "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": { + "type": { + "type": "array", + "readOnly": true, + "default": [ + "Party" + ], + "items": { + "type": "string" + }, + "allOf": [ + { + "contains": { + "const": "Party", + "minContains": 1 + } + } + ] + }, + "id": { + "example": "https://sample-business-register.gov/123456789", + "type": "string", + "format": "uri", + "description": "Globally unique identifier of this party. Typically represented as a URI identifierScheme/Identifier URI" + }, + "name": { + "example": "Sample Company Ltd", + "type": "string", + "description": "Legal registered name of this party." + }, + "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" + ] + }, + "BitstringStatusListEntry": { + "type": "object", + "additionalProperties": false, + "properties": { + "type": { + "type": "string", + "enum": [ + "BitstringStatusListEntry" + ], + "example": "BitstringStatusListEntry", + "description": "The type of status list - must be set to \"The type property MUST be BitstringStatusListEntry.\"" + }, + "id": { + "example": "https://example-cab.com/credentials/status/3#94567\"", + "type": "string", + "format": "uri", + "description": "optional identifier of this status list entry." + }, + "statusPurpose": { + "type": "string", + "enum": [ + "refresh", + "revocation", + "suspension", + "message" + ], + "example": "refresh", + "description": "Status purpose drawn from a standard list but extensible as per w3c bitstring status list specification." + }, + "statusListIndex": { + "minimum": 0, + "example": 94567, + "type": "integer", + "description": "\tThe statusListIndex property MUST be an arbitrary size integer greater than or equal to 0, expressed as a string in base 10. The value identifies the position of the status of the verifiable credential." + }, + "statusListCredential": { + "example": "https://example-cab.com/credentials/status/4", + "type": "string", + "format": "uri", + "description": "The statusListCredential property MUST be a URL to a verifiable credential. When the URL is dereferenced, the resulting verifiable credential MUST have type property that includes the BitstringStatusListCredential value." + } + }, + "description": "A privacy-preserving, space-efficient, and high-performance mechanism for publishing status information such as suspension or revocation of Verifiable Credentials through use of bitstrings. See https://www.w3.org/TR/vc-bitstring-status-list/ for full details.", + "required": [ + "type", + "statusPurpose", + "statusListIndex", + "statusListCredential" + ] + }, + "RenderTemplate2024": { + "type": "object", + "additionalProperties": false, + "properties": { + "type": { + "type": "array", + "readOnly": true, + "default": [ + "RenderTemplate2024" + ], + "items": { + "type": "string" + }, + "allOf": [ + { + "contains": { + "const": "RenderTemplate2024", + "minContains": 1 + } + } + ] + }, + "name": { + "type": "string", + "description": "Human facing display name for selection" + }, + "mediaQuery": { + "type": "string", + "description": "Media query as defined in https://www.w3.org/TR/mediaqueries-4/" + }, + "template": { + "type": "string", + "description": "An inline template field for use cases where remote retrieval of a render method is suboptimal" + }, + "url": { + "type": "string", + "format": "uri", + "description": "URL for remotely hosted template" + }, + "mediaType": { + "type": "string", + "description": "media type of the rendered output (eg text/html)" + }, + "digestMultibase": { + "type": "string", + "description": "Used for resource integrity and/or validation of the inline `template`" + } + }, + "description": "A single template format focused render method where the content/media type decision becomes secondary (and is expressed separately).See https://github.com/w3c-ccg/vc-render-method/issues/9" + }, + "IssuingSoftware": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "example": "https://yourdomain.com/.well-known/untp/software/yourproduct/2026.04.1", + "type": "string", + "format": "uri", + "description": "A resolvable identifier for the specific version of the software product that issued this credential." + }, + "name": { + "example": "Your Product Name", + "type": "string", + "description": "The name of the software product that issued this credential." + }, + "version": { + "example": "2026.04.1", + "type": "string", + "description": "The version of the software product that issued this credential." + }, + "vendor": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "example": "did:web:yourdomain.com", + "type": "string", + "format": "uri", + "description": "The decentralised identifier (DID) or other resolvable identifier of the software vendor." + }, + "name": { + "example": "Your Vendor Name", + "type": "string", + "description": "The name of the software vendor." + } + }, + "required": [ + "id", + "name" + ], + "description": "The vendor of the software product that issued this credential." + } + }, + "required": [ + "id", + "name", + "version", + "vendor" + ], + "description": "Optional metadata identifying the software product (and its vendor) that issued the parent credential. When present, all listed sub-properties MUST be populated; when absent, the credential is still valid (verifiers MUST treat the property as optional)." + }, + "Product": { + "type": "object", + "additionalProperties": true, + "properties": { + "type": { + "type": "array", + "readOnly": true, + "default": [ + "Product" + ], + "items": { + "type": "string" + }, + "allOf": [ + { + "contains": { + "const": "Product", + "minContains": 1 + } + } + ] + }, + "id": { + "example": "did:web:manufacturer.com:product:123456789", + "type": "string", + "format": "uri", + "description": "Globally unique identifier of this product. Typically represented as a URI identifierScheme/Identifier URI or, if self-issued, as a did." + }, + "name": { + "example": "600 Ah Lithium Battery", + "type": "string", + "description": "The product name as known to the market." + }, + "description": { + "type": "string", + "description": "Description of the product." + }, + "idScheme": { + "type": "object", + "properties": { + "type": { + "type": "array", + "readOnly": true, + "default": [ + "IdentifierScheme" + ], + "items": { + "type": "string" + }, + "allOf": [ + { + "contains": { + "const": "IdentifierScheme", + "minContains": 1 + } + } + ] + }, + "id": { + "type": "string", + "format": "uri", + "description": "The URI of this identifier scheme" + }, + "name": { + "example": "Global Identifier Scheme Name", + "type": "string", + "description": "The name of the identifier scheme. " + } + }, + "required": [ + "id", + "name" + ], + "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. " + }, + "modelNumber": { + "type": "string", + "description": "Where available, the model number (for manufactured products) or material identification (for bulk materials)" + }, + "batchNumber": { + "example": 6789, + "type": "string", + "description": "Identifier of the specific production batch of the product. Unique within the product class." + }, + "itemNumber": { + "example": 12345678, + "type": "string", + "description": "A number or code representing a specific serialised item of the product. Unique within product class." + }, + "idGranularity": { + "type": "string", + "enum": [ + "model", + "batch", + "item" + ], + "example": "model", + "description": "The identification granularity for this product (item, batch, model)" + }, + "productImage": { + "$ref": "#/$defs/Link", + "description": "Reference information (location, type, name) of an image of the product." + }, + "characteristics": { + "$ref": "#/$defs/Characteristics", + "description": "A set of industry specific product 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" + }, + "relatedDocument": { + "type": "array", + "items": { + "$ref": "#/$defs/Link" + }, + "description": "A list of links to documents providing additional product information. Documents that support a conformity claim (e.g. permits or certificates) SHOULD be referenced as claim evidence rather than here." + }, + "relatedParty": { + "type": "array", + "items": { + "$ref": "#/$defs/PartyRole" + }, + "description": "A list of parties with a defined relationship to this product" + }, + "producedAtFacility": { + "type": "object", + "properties": { + "type": { + "type": "array", + "readOnly": true, + "default": [ + "Facility" + ], + "items": { + "type": "string" + }, + "allOf": [ + { + "contains": { + "const": "Facility", + "minContains": 1 + } + } + ] + }, + "id": { + "example": "https://sample-location-register.com/987654321", + "type": "string", + "format": "uri", + "description": "Globally unique identifier of this facility. Typically represented as a URI identifierScheme/Identifier URI" + }, + "name": { + "example": "Sample Factory A", + "type": "string", + "description": "Name of this facility as defined the location register." + }, + "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." + }, + "expiryDate": { + "example": "2027-04-25", + "type": "string", + "format": "date", + "description": "The date at which this product is no longer fit for use. Typically used for a food product use-by date but may also represent the usable life of any product." + }, + "countryOfProduction": { + "$ref": "#/$defs/Country", + "description": "The country in which this item was produced / manufactured.using ISO-3166 code and name." + }, + "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, width, or height.\"weight\":{\"value\":10, \"unit\":\"KGM\"}" + }, + "materialProvenance": { + "type": "array", + "items": { + "$ref": "#/$defs/Material" + }, + "description": "A list of materials provenance objects providing details on the origin and mass fraction of materials of the product or batch." + }, + "packaging": { + "$ref": "#/$defs/Package", + "description": "The packaging for this product." + }, + "productLabel": { + "type": "array", + "items": { + "$ref": "#/$defs/Image" + }, + "description": "An array of labels that may appear on the product such as certification marks or regulatory labels." + }, + "performanceClaim": { + "type": "array", + "items": { + "$ref": "#/$defs/Claim" + }, + "description": "A list of performance claims (eg emissions intensity) for this product." + } + }, + "description": "The ProductInformation class encapsulates detailed information regarding a specific product, including its identification details, manufacturer, and other pertinent details.", + "required": [ + "id", + "name", + "idScheme", + "idGranularity", + "productCategory", + "producedAtFacility", + "countryOfProduction" + ] + }, + "Link": { + "type": "object", + "additionalProperties": false, + "properties": { + "linkURL": { + "example": "https://files.example-certifier.com/1234567.json", + "type": "string", + "format": "uri", + "description": "The URL of the target resource. " + }, + "linkName": { + "type": "string", + "description": "Display name for this link." + }, + "digestMultibase": { + "example": "abc123-example-digest-invalid", + "type": "string", + "description": "An optional multi-base encoded digest to ensure the content of the link has not changed. See https://www.w3.org/TR/vc-data-integrity/#resource-integrity for more information." + }, + "mediaType": { + "example": "application/ld+json", + "type": "string", + "description": "The media type of 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.", + "required": [ + "linkURL", + "linkName" + ] + }, + "Characteristics": { + "type": "object", + "additionalProperties": true, + "properties": {}, + "description": "A declaration of conformance with one or more criteria from a specific standard or regulation. " + }, + "Classification": { + "type": "object", + "additionalProperties": false, + "properties": { + "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" + }, + "definition": { + "type": "string", + "description": "A rich definition of this classification 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": [ + "code", + "name", + "schemeId", + "schemeName" + ] + }, + "PartyRole": { + "type": "object", + "additionalProperties": false, + "properties": { + "role": { + "type": "string", + "enum": [ + "owner", + "producer", + "manufacturer", + "processor", + "remanufacturer", + "recycler", + "operator", + "serviceProvider", + "inspector", + "certifier", + "logisticsProvider", + "carrier", + "consignor", + "consignee", + "importer", + "exporter", + "distributor", + "retailer", + "brandOwner", + "regulator" + ], + "example": "owner", + "description": "The role played by the party in this relationship" + }, + "party": { + "$ref": "#/$defs/Party", + "description": "The party that has the specified role." + } + }, + "description": "A party with a defined relationship to the referencing entity", + "required": [ + "role", + "party" + ] + }, + "Party": { + "type": "object", + "additionalProperties": true, + "properties": { + "type": { + "type": "array", + "readOnly": true, + "default": [ + "Party" + ], + "items": { + "type": "string" + }, + "allOf": [ + { + "contains": { + "const": "Party", + "minContains": 1 + } + } + ] + }, + "id": { + "example": "https://sample-business-register.gov/123456789", + "type": "string", + "format": "uri", + "description": "Globally unique identifier of this party. Typically represented as a URI identifierScheme/Identifier URI" + }, + "name": { + "example": "Sample Company Ltd", + "type": "string", + "description": "Legal registered name of this party." + }, + "description": { + "type": "string", + "description": "Description of the party including function and other names." + }, + "registeredId": { + "example": 90664869327, + "type": "string", + "description": "The registration number (alphanumeric) of the Party within the register. Unique within the register." + }, + "idScheme": { + "type": "object", + "properties": { + "type": { + "type": "array", + "readOnly": true, + "default": [ + "IdentifierScheme" + ], + "items": { + "type": "string" + }, + "allOf": [ + { + "contains": { + "const": "IdentifierScheme", + "minContains": 1 + } + } + ] + }, + "id": { + "type": "string", + "format": "uri", + "description": "The URI of this identifier scheme" + }, + "name": { + "example": "Global Identifier Scheme Name", + "type": "string", + "description": "The name of the identifier scheme. " + } + }, + "required": [ + "id", + "name" + ], + "description": "The identifier scheme of the party. Typically a national business register or a global scheme such as GLEIF. " + }, + "registrationCountry": { + "$ref": "#/$defs/Country", + "description": "the country in which this organisation is registered - using ISO-3166 code and name." + }, + "partyAddress": { + "$ref": "#/$defs/Address", + "description": "The address of the party" + }, + "organisationWebsite": { + "example": "https://example-company.com", + "type": "string", + "format": "uri", + "description": "Website for this organisation" + }, + "industryCategory": { + "type": "array", + "items": { + "$ref": "#/$defs/Classification" + }, + "description": "The industry categories for this organisation. Recommend use of UNCPC as the category scheme. for example - unstats.un.org/isic/1030" + }, + "partyAlsoKnownAs": { + "type": "array", + "items": { + "type": "object", + "properties": { + "type": { + "type": "array", + "readOnly": true, + "default": [ + "Party" + ], + "items": { + "type": "string" + }, + "allOf": [ + { + "contains": { + "const": "Party", + "minContains": 1 + } + } + ] + }, + "id": { + "example": "https://sample-business-register.gov/123456789", + "type": "string", + "format": "uri", + "description": "Globally unique identifier of this party. Typically represented as a URI identifierScheme/Identifier URI" + }, + "name": { + "example": "Sample Company Ltd", + "type": "string", + "description": "Legal registered name of this party." + }, + "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 organisation. For example DUNS, GLN, LEI, etc" + } + }, + "description": "An organisation. May be a supply chain actor, a certifier, a government agency.", + "required": [ + "id", + "name" + ] + }, + "Country": { + "type": "object", + "additionalProperties": false, + "properties": { + "countryCode": { + "type": "string", + "x-external-enumeration": "https://vocabulary.uncefact.org/CountryId#", + "description": "ISO 3166 country code\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 " + }, + "countryName": { + "type": "string", + "description": "Country Name as defined in ISO 3166" + } + }, + "description": "Country Code and Name from ISO 3166", + "required": [ + "countryCode" + ] + }, + "Address": { + "type": "object", + "additionalProperties": false, + "properties": { + "streetAddress": { + "example": "level 11, 15 London Circuit", + "type": "string", + "description": "the street address as an unstructured string." + }, + "postalCode": { + "example": 2601, + "type": "string", + "description": "The postal code or zip code for this address." + }, + "addressLocality": { + "example": "Acton", + "type": "string", + "description": "The city, suburb or township name." + }, + "addressRegion": { + "example": "ACT", + "type": "string", + "description": "The state or territory or province" + }, + "addressCountry": { + "$ref": "#/$defs/Country", + "description": "The address country as an ISO-3166 two letter country code and name." + } + }, + "description": "A postal address.", + "required": [ + "streetAddress", + "postalCode", + "addressLocality", + "addressRegion", + "addressCountry" + ] + }, + "Dimension": { + "type": "object", + "additionalProperties": true, + "properties": { + "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": { + "value": { + "example": 10, + "type": "number", + "description": "The numeric value of the measure" + }, + "upperTolerance": { + "type": "number", + "description": "The upper tolerance associated with this measure expressed in the same units as the measure. For example value=10, upperTolerance=0.1, unit=KGM would mean that this measure is 10kg + 0.1kg" + }, + "lowerTolerance": { + "type": "number", + "description": "The lower tolerance associated with this measure expressed in the same units as the measure. For example value=10, lowerTolerance=0.1, unit=KGM would mean that this measure is 10kg - 0.1kg" + }, + "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). There is an optional upper and lower tolerance which can be used to specify uncertainty in the measure. ", + "required": [ + "value", + "unit" + ] + }, + "Material": { + "type": "object", + "additionalProperties": true, + "properties": { + "name": { + "example": "Lithium Spodumene", + "type": "string", + "description": "Name of this material (eg \"Egyptian Cotton\")" + }, + "originCountry": { + "$ref": "#/$defs/Country", + "description": "A ISO 3166-1 code representing the country of origin of the component or ingredient." + }, + "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": { + "maximum": 1, + "minimum": 0, + "example": 0.2, + "type": "number", + "description": "The mass fraction as a decimal of the product (or facility reporting period) represented by this material. " + }, + "mass": { + "$ref": "#/$defs/Measure", + "description": "The mass of the material component." + }, + "recycledMassFraction": { + "maximum": 1, + "minimum": 0, + "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": { + "$ref": "#/$defs/Image", + "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", + "originCountry", + "materialType", + "massFraction" + ] + }, + "Image": { + "type": "object", + "additionalProperties": false, + "properties": { + "name": { + "example": "certification trust mark", + "type": "string", + "description": "the display name for this image" + }, + "description": { + "type": "string", + "description": "The detailed description / supporting information for this image." + }, + "imageData": { + "type": "string", + "format": "byte", + "description": "The image data encoded as a base64 string." + }, + "mediaType": { + "type": "string", + "x-external-enumeration": "https://mimetype.io/", + "description": "The media type of this image (eg image/png)\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://mimetype.io/\n " + } + }, + "description": "A binary image encoded as base64 text and embedded into the data. Use this for small images like certification trust marks or regulated labels. Large images should be external links.", + "required": [ + "name", + "imageData", + "mediaType" + ] + }, + "Package": { + "type": "object", + "additionalProperties": true, + "properties": { + "description": { + "type": "string", + "description": "Description of the packaging." + }, + "dimensions": { + "$ref": "#/$defs/Dimension", + "description": "dimensions of the packaging" + }, + "materialUsed": { + "type": "array", + "items": { + "$ref": "#/$defs/Material" + }, + "description": "materials used for the packaging." + }, + "packageLabel": { + "type": "array", + "items": { + "$ref": "#/$defs/Image" + }, + "description": "An array of package labels that may appear on the packaging together with their meaning. Use for small images that represent certification marks or regulatory requirements. Large images should be linked as evidence to claims." + }, + "performanceClaim": { + "type": "array", + "items": { + "$ref": "#/$defs/Claim" + }, + "description": "conformity claims made about the packaging." + } + }, + "description": "Details of product packaging", + "required": [ + "description", + "dimensions", + "materialUsed" + ] + }, + "Claim": { + "type": "object", + "additionalProperties": true, + "properties": { + "type": { + "type": "array", + "readOnly": true, + "default": [ + "Claim" + ], + "items": { + "type": "string" + }, + "allOf": [ + { + "contains": { + "const": "Claim", + "minContains": 1 + } + } + ] + }, + "id": { + "example": "https://sample-company.com/claim/e78dab5d-b6f6-4bc4-a458-7feb039f6cb3", + "type": "string", + "format": "uri", + "description": "Globally unique identifier of this claim. Typically represented as a URI companyURL/claimID URI or a UUID" + }, + "name": { + "example": "Sample company Forced Labour claim", + "type": "string", + "description": "Name of this claim - typically similar or the same as the referenced criterion name." + }, + "description": { + "type": "string", + "description": "Description of this conformity claim" + }, + "referenceCriteria": { + "type": "array", + "items": { + "type": "object", + "properties": { + "type": { + "type": "array", + "readOnly": true, + "default": [ + "Criterion" + ], + "items": { + "type": "string" + }, + "allOf": [ + { + "contains": { + "const": "Criterion", + "minContains": 1 + } + } + ] + }, + "id": { + "example": "https://vocabulary.sample-scheme.org/criterion/lb/v1.0", + "type": "string", + "format": "uri", + "description": "Globally unique identifier of this conformity criterion. Typically represented as a URI SchemeOwner/CriterionID URI" + }, + "name": { + "example": "Forced labour assessment criterion", + "type": "string", + "description": "Name of this criterion as defined by the scheme owner." + } + }, + "required": [ + "id", + "name" + ] + }, + "description": "The criterion against which the claim is made." + }, + "referenceRegulation": { + "type": "array", + "items": { + "type": "object", + "properties": { + "type": { + "type": "array", + "readOnly": true, + "default": [ + "Regulation" + ], + "items": { + "type": "string" + }, + "allOf": [ + { + "contains": { + "const": "Regulation", + "minContains": 1 + } + } + ] + }, + "id": { + "example": "https://regulations.country.gov/ABC-12345", + "type": "string", + "format": "uri", + "description": "Globally unique identifier of this standard. Typically represented as a URI government/regulation URI" + }, + "name": { + "example": "Due Diligence Directove", + "type": "string", + "description": "Name of this regulation as defined by the regulator." + } + }, + "required": [ + "id", + "name" + ] + }, + "description": "List of references to regulation to which conformity is claimed claimed for this product" + }, + "referenceStandard": { + "type": "array", + "items": { + "type": "object", + "properties": { + "type": { + "type": "array", + "readOnly": true, + "default": [ + "Standard" + ], + "items": { + "type": "string" + }, + "allOf": [ + { + "contains": { + "const": "Standard", + "minContains": 1 + } + } + ] + }, + "id": { + "example": "https://sample-standards.org/A1234", + "type": "string", + "format": "uri", + "description": "Globally unique identifier of this standard. Typically represented as a URI issuer/standard URI" + }, + "name": { + "example": "Labour rights standard", + "type": "string", + "description": "Name for this standard" + } + }, + "required": [ + "id", + "name" + ] + }, + "description": "List of references to standards to which conformity is claimed claimed for this product" + }, + "claimDate": { + "type": "string", + "format": "date", + "description": "That date on which the claimed performance is applicable." + }, + "applicablePeriod": { + "$ref": "#/$defs/Period", + "description": "The applicable reporting period for this facility record." + }, + "claimedPerformance": { + "type": "array", + "items": { + "$ref": "#/$defs/Performance" + }, + "description": "The claimed performance level " + }, + "evidence": { + "type": "array", + "items": { + "$ref": "#/$defs/Link" + }, + "description": "A URI pointing to the evidence supporting the claim. SHOULD be a URL to a UNTP Digital Conformity Credential (DCC)" + }, + "conformityTopic": { + "type": "array", + "items": { + "$ref": "#/$defs/ConformityTopic" + }, + "description": "The conformity topic category for this assessment" + } + }, + "description": "A performance claim about a product, facility, or organisation that is made against a well defined criterion.", + "required": [ + "id", + "name", + "referenceCriteria", + "claimDate", + "claimedPerformance", + "conformityTopic" + ] + }, + "Period": { + "type": "object", + "additionalProperties": false, + "properties": { + "startDate": { + "type": "string", + "format": "date", + "description": "The period start date" + }, + "endDate": { + "type": "string", + "format": "date", + "description": "The period end date" + }, + "periodInformation": { + "type": "string", + "description": "Additional information relevant to this reporting period" + } + }, + "description": "A period of time, typically a month, quarter or a year, which defines the context boundary for reported facts.", + "required": [ + "startDate", + "endDate" + ] + }, + "Performance": { + "type": "object", + "additionalProperties": false, + "properties": { + "metric": { + "type": "object", + "properties": { + "type": { + "type": "array", + "readOnly": true, + "default": [ + "PerformanceMetric" + ], + "items": { + "type": "string" + }, + "allOf": [ + { + "contains": { + "const": "PerformanceMetric", + "minContains": 1 + } + } + ] + }, + "id": { + "example": "https://authority.gov/schemeABC/123456789", + "type": "string", + "format": "uri", + "description": "Globally unique identifier of this reporting metric. " + }, + "name": { + "example": "emissions intensity", + "type": "string", + "description": "A human readable name for this metric (for example \"water usage per Kg of material\")" + } + }, + "required": [ + "id", + "name" + ], + "description": "The metric (eg material emissions intensity CO2e/Kg or percentage of young workers) that is measured." + }, + "measure": { + "$ref": "#/$defs/Measure", + "description": "The measured performance value" + }, + "score": { + "$ref": "#/$defs/Score", + "description": "A performance score (eg \"AA\") drawn from a scoring framework defined by the scheme or criterion." + } + }, + "description": "A claimed, assessed, or required performance level defined either by a scoring system or a numeric measure. When a numeric measure is provided, the metric classifying the measurement is required. When only a score is provided, the scoring framework is discoverable via the conformity scheme or criterion.", + "dependentRequired": { + "measure": [ + "metric" + ] + } + }, + "Score": { + "type": "object", + "additionalProperties": false, + "properties": { + "code": { + "type": "string", + "description": "The coded value for this score (eg \"AAA\")" + }, + "rank": { + "type": "integer", + "description": "The ranking of this score within the scoring framework - using an integer where \"1\" is the highest rank." + }, + "definition": { + "type": "string", + "description": "A description of the meaning of this score." + } + }, + "description": "A single score within a scoring framework. ", + "required": [ + "code" + ] + }, + "ConformityTopic": { + "type": "object", + "additionalProperties": false, + "properties": { + "type": { + "type": "array", + "readOnly": true, + "default": [ + "ConformityTopic" + ], + "items": { + "type": "string" + }, + "allOf": [ + { + "contains": { + "const": "ConformityTopic", + "minContains": 1 + } + } + ] + }, + "id": { + "type": "string", + "format": "uri", + "description": "The unique identifier for this conformity topic" + }, + "name": { + "example": "forced-labour", + "type": "string", + "description": "The human readable name for this conformity topic." + }, + "definition": { + "type": "string", + "description": "The rich definition of this conformity topic." + } + }, + "description": "The UNTP standard classification scheme for conformity topic. see http://vocabulary.uncefact.org/ConformityTopic", + "required": [ + "id", + "name" + ] + } + } +} diff --git a/tests/fixtures/upstream/v0.7.0/schema/Product.json b/tests/fixtures/upstream/v0.7.0/schema/Product.json new file mode 100644 index 0000000..ab0cb99 --- /dev/null +++ b/tests/fixtures/upstream/v0.7.0/schema/Product.json @@ -0,0 +1,1121 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "additionalProperties": true, + "properties": { + "type": { + "type": "array", + "readOnly": true, + "default": [ + "Product" + ], + "items": { + "type": "string" + }, + "allOf": [ + { + "contains": { + "const": "Product", + "minContains": 1 + } + } + ] + }, + "id": { + "example": "did:web:manufacturer.com:product:123456789", + "type": "string", + "format": "uri", + "description": "Globally unique identifier of this product. Typically represented as a URI identifierScheme/Identifier URI or, if self-issued, as a did." + }, + "name": { + "example": "600 Ah Lithium Battery", + "type": "string", + "description": "The product name as known to the market." + }, + "description": { + "type": "string", + "description": "Description of the product." + }, + "idScheme": { + "type": "object", + "properties": { + "type": { + "type": "array", + "readOnly": true, + "default": [ + "IdentifierScheme" + ], + "items": { + "type": "string" + }, + "allOf": [ + { + "contains": { + "const": "IdentifierScheme", + "minContains": 1 + } + } + ] + }, + "id": { + "type": "string", + "format": "uri", + "description": "The URI of this identifier scheme" + }, + "name": { + "example": "Global Identifier Scheme Name", + "type": "string", + "description": "The name of the identifier scheme. " + } + }, + "required": [ + "id", + "name" + ], + "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. " + }, + "modelNumber": { + "type": "string", + "description": "Where available, the model number (for manufactured products) or material identification (for bulk materials)" + }, + "batchNumber": { + "example": 6789, + "type": "string", + "description": "Identifier of the specific production batch of the product. Unique within the product class." + }, + "itemNumber": { + "example": 12345678, + "type": "string", + "description": "A number or code representing a specific serialised item of the product. Unique within product class." + }, + "idGranularity": { + "type": "string", + "enum": [ + "model", + "batch", + "item" + ], + "example": "model", + "description": "The identification granularity for this product (item, batch, model)" + }, + "productImage": { + "$ref": "#/$defs/Link", + "description": "Reference information (location, type, name) of an image of the product." + }, + "characteristics": { + "$ref": "#/$defs/Characteristics", + "description": "A set of industry specific product 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" + }, + "relatedDocument": { + "type": "array", + "items": { + "$ref": "#/$defs/Link" + }, + "description": "A list of links to documents providing additional product information. Documents that support a conformity claim (e.g. permits or certificates) SHOULD be referenced as claim evidence rather than here." + }, + "relatedParty": { + "type": "array", + "items": { + "$ref": "#/$defs/PartyRole" + }, + "description": "A list of parties with a defined relationship to this product" + }, + "producedAtFacility": { + "type": "object", + "properties": { + "type": { + "type": "array", + "readOnly": true, + "default": [ + "Facility" + ], + "items": { + "type": "string" + }, + "allOf": [ + { + "contains": { + "const": "Facility", + "minContains": 1 + } + } + ] + }, + "id": { + "example": "https://sample-location-register.com/987654321", + "type": "string", + "format": "uri", + "description": "Globally unique identifier of this facility. Typically represented as a URI identifierScheme/Identifier URI" + }, + "name": { + "example": "Sample Factory A", + "type": "string", + "description": "Name of this facility as defined the location register." + }, + "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." + }, + "expiryDate": { + "example": "2027-04-25", + "type": "string", + "format": "date", + "description": "The date at which this product is no longer fit for use. Typically used for a food product use-by date but may also represent the usable life of any product." + }, + "countryOfProduction": { + "$ref": "#/$defs/Country", + "description": "The country in which this item was produced / manufactured.using ISO-3166 code and name." + }, + "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, width, or height.\"weight\":{\"value\":10, \"unit\":\"KGM\"}" + }, + "materialProvenance": { + "type": "array", + "items": { + "$ref": "#/$defs/Material" + }, + "description": "A list of materials provenance objects providing details on the origin and mass fraction of materials of the product or batch." + }, + "packaging": { + "$ref": "#/$defs/Package", + "description": "The packaging for this product." + }, + "productLabel": { + "type": "array", + "items": { + "$ref": "#/$defs/Image" + }, + "description": "An array of labels that may appear on the product such as certification marks or regulatory labels." + }, + "performanceClaim": { + "type": "array", + "items": { + "$ref": "#/$defs/Claim" + }, + "description": "A list of performance claims (eg emissions intensity) for this product." + } + }, + "description": "The ProductInformation class encapsulates detailed information regarding a specific product, including its identification details, manufacturer, and other pertinent details.", + "required": [ + "id", + "name", + "idScheme", + "idGranularity", + "productCategory", + "producedAtFacility", + "countryOfProduction" + ], + "$defs": { + "Link": { + "type": "object", + "additionalProperties": false, + "properties": { + "linkURL": { + "example": "https://files.example-certifier.com/1234567.json", + "type": "string", + "format": "uri", + "description": "The URL of the target resource. " + }, + "linkName": { + "type": "string", + "description": "Display name for this link." + }, + "digestMultibase": { + "example": "abc123-example-digest-invalid", + "type": "string", + "description": "An optional multi-base encoded digest to ensure the content of the link has not changed. See https://www.w3.org/TR/vc-data-integrity/#resource-integrity for more information." + }, + "mediaType": { + "example": "application/ld+json", + "type": "string", + "description": "The media type of 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.", + "required": [ + "linkURL", + "linkName" + ] + }, + "Characteristics": { + "type": "object", + "additionalProperties": true, + "properties": { + "@context": { + "description": "Optional JSON-LD scoped context used to declare vocabulary prefixes for extension properties. Most commonly a single-entry object that binds a prefix to an industry or vendor namespace (for example { \"battery\": \"https://example-industry.org/battery/v1/\" }), allowing extension keys in this characteristics object to be written as \"battery:batteryChemistry\" and expanded to IRIs in that namespace. When omitted, extension keys default to the UNTP characteristics namespace defined in the UNTP JSON-LD context.", + "oneOf": [ + { + "type": "object" + }, + { + "type": "array", + "items": { + "type": [ + "string", + "object" + ] + } + } + ] + } + }, + "description": "A set of quantitative or qualitative characteristics describing a product. This is a deliberate extension point: implementers MAY add arbitrary properties beyond those defined here (for example industry-specific attributes such as batteryChemistry, recycledContentPercentage, or gradeClass). Extension keys are preserved in the expanded JSON-LD graph — unknown keys default to the UNTP characteristics namespace (https://vocabulary.uncefact.org/untp/Characteristics/) so they remain addressable for SPARQL / SHACL consumers. Implementers who maintain their own published vocabulary SHOULD declare a scoped @context inside the characteristics object to redirect extension keys into their namespace. Example:\n\n \"characteristics\": {\n \"@context\": { \"battery\": \"https://example-industry.org/battery/v1/\" },\n \"type\": [\"Characteristics\"],\n \"battery:batteryChemistry\": \"NMC 811\",\n \"battery:batteryCategory\": \"EV\"\n }" + }, + "Classification": { + "type": "object", + "additionalProperties": false, + "properties": { + "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" + }, + "definition": { + "type": "string", + "description": "A rich definition of this classification 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": [ + "code", + "name", + "schemeId", + "schemeName" + ] + }, + "PartyRole": { + "type": "object", + "additionalProperties": false, + "properties": { + "role": { + "type": "string", + "enum": [ + "owner", + "producer", + "manufacturer", + "processor", + "remanufacturer", + "recycler", + "operator", + "serviceProvider", + "inspector", + "certifier", + "logisticsProvider", + "carrier", + "consignor", + "consignee", + "importer", + "exporter", + "distributor", + "retailer", + "brandOwner", + "regulator" + ], + "example": "owner", + "description": "The role played by the party in this relationship" + }, + "party": { + "$ref": "#/$defs/Party", + "description": "The party that has the specified role." + } + }, + "description": "A party with a defined relationship to the referencing entity", + "required": [ + "role", + "party" + ] + }, + "Party": { + "type": "object", + "additionalProperties": true, + "properties": { + "type": { + "type": "array", + "readOnly": true, + "default": [ + "Party" + ], + "items": { + "type": "string" + }, + "allOf": [ + { + "contains": { + "const": "Party", + "minContains": 1 + } + } + ] + }, + "id": { + "example": "https://sample-business-register.gov/123456789", + "type": "string", + "format": "uri", + "description": "Globally unique identifier of this party. Typically represented as a URI identifierScheme/Identifier URI" + }, + "name": { + "example": "Sample Company Ltd", + "type": "string", + "description": "Legal registered name of this party." + }, + "description": { + "type": "string", + "description": "Description of the party including function and other names." + }, + "registeredId": { + "example": 90664869327, + "type": "string", + "description": "The registration number (alphanumeric) of the Party within the register. Unique within the register." + }, + "idScheme": { + "type": "object", + "properties": { + "type": { + "type": "array", + "readOnly": true, + "default": [ + "IdentifierScheme" + ], + "items": { + "type": "string" + }, + "allOf": [ + { + "contains": { + "const": "IdentifierScheme", + "minContains": 1 + } + } + ] + }, + "id": { + "type": "string", + "format": "uri", + "description": "The URI of this identifier scheme" + }, + "name": { + "example": "Global Identifier Scheme Name", + "type": "string", + "description": "The name of the identifier scheme. " + } + }, + "required": [ + "id", + "name" + ], + "description": "The identifier scheme of the party. Typically a national business register or a global scheme such as GLEIF. " + }, + "registrationCountry": { + "$ref": "#/$defs/Country", + "description": "the country in which this organisation is registered - using ISO-3166 code and name." + }, + "partyAddress": { + "$ref": "#/$defs/Address", + "description": "The address of the party" + }, + "organisationWebsite": { + "example": "https://example-company.com", + "type": "string", + "format": "uri", + "description": "Website for this organisation" + }, + "industryCategory": { + "type": "array", + "items": { + "$ref": "#/$defs/Classification" + }, + "description": "The industry categories for this organisation. Recommend use of UNCPC as the category scheme. for example - unstats.un.org/isic/1030" + }, + "partyAlsoKnownAs": { + "type": "array", + "items": { + "type": "object", + "properties": { + "type": { + "type": "array", + "readOnly": true, + "default": [ + "Party" + ], + "items": { + "type": "string" + }, + "allOf": [ + { + "contains": { + "const": "Party", + "minContains": 1 + } + } + ] + }, + "id": { + "example": "https://sample-business-register.gov/123456789", + "type": "string", + "format": "uri", + "description": "Globally unique identifier of this party. Typically represented as a URI identifierScheme/Identifier URI" + }, + "name": { + "example": "Sample Company Ltd", + "type": "string", + "description": "Legal registered name of this party." + }, + "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 organisation. For example DUNS, GLN, LEI, etc" + } + }, + "description": "An organisation. May be a supply chain actor, a certifier, a government agency.", + "required": [ + "id", + "name" + ] + }, + "Country": { + "type": "object", + "additionalProperties": false, + "properties": { + "countryCode": { + "type": "string", + "x-external-enumeration": "https://vocabulary.uncefact.org/CountryId#", + "description": "ISO 3166 country code\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 " + }, + "countryName": { + "type": "string", + "description": "Country Name as defined in ISO 3166" + } + }, + "description": "Country Code and Name from ISO 3166", + "required": [ + "countryCode" + ] + }, + "Address": { + "type": "object", + "additionalProperties": false, + "properties": { + "streetAddress": { + "example": "level 11, 15 London Circuit", + "type": "string", + "description": "the street address as an unstructured string." + }, + "postalCode": { + "example": 2601, + "type": "string", + "description": "The postal code or zip code for this address." + }, + "addressLocality": { + "example": "Acton", + "type": "string", + "description": "The city, suburb or township name." + }, + "addressRegion": { + "example": "ACT", + "type": "string", + "description": "The state or territory or province" + }, + "addressCountry": { + "$ref": "#/$defs/Country", + "description": "The address country as an ISO-3166 two letter country code and name." + } + }, + "description": "A postal address.", + "required": [ + "streetAddress", + "postalCode", + "addressLocality", + "addressRegion", + "addressCountry" + ] + }, + "Dimension": { + "type": "object", + "additionalProperties": true, + "properties": { + "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": { + "value": { + "example": 10, + "type": "number", + "description": "The numeric value of the measure" + }, + "upperTolerance": { + "type": "number", + "description": "The upper tolerance associated with this measure expressed in the same units as the measure. For example value=10, upperTolerance=0.1, unit=KGM would mean that this measure is 10kg + 0.1kg" + }, + "lowerTolerance": { + "type": "number", + "description": "The lower tolerance associated with this measure expressed in the same units as the measure. For example value=10, lowerTolerance=0.1, unit=KGM would mean that this measure is 10kg - 0.1kg" + }, + "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). There is an optional upper and lower tolerance which can be used to specify uncertainty in the measure. ", + "required": [ + "value", + "unit" + ] + }, + "Material": { + "type": "object", + "additionalProperties": true, + "properties": { + "name": { + "example": "Lithium Spodumene", + "type": "string", + "description": "Name of this material (eg \"Egyptian Cotton\")" + }, + "originCountry": { + "$ref": "#/$defs/Country", + "description": "A ISO 3166-1 code representing the country of origin of the component or ingredient." + }, + "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": { + "maximum": 1, + "minimum": 0, + "example": 0.2, + "type": "number", + "description": "The mass fraction as a decimal of the product (or facility reporting period) represented by this material. " + }, + "mass": { + "$ref": "#/$defs/Measure", + "description": "The mass of the material component." + }, + "recycledMassFraction": { + "maximum": 1, + "minimum": 0, + "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": { + "$ref": "#/$defs/Image", + "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", + "originCountry", + "materialType", + "massFraction" + ] + }, + "Image": { + "type": "object", + "additionalProperties": false, + "properties": { + "name": { + "example": "certification trust mark", + "type": "string", + "description": "the display name for this image" + }, + "description": { + "type": "string", + "description": "The detailed description / supporting information for this image." + }, + "imageData": { + "type": "string", + "format": "byte", + "description": "The image data encoded as a base64 string." + }, + "mediaType": { + "type": "string", + "x-external-enumeration": "https://mimetype.io/", + "description": "The media type of this image (eg image/png)\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://mimetype.io/\n " + } + }, + "description": "A binary image encoded as base64 text and embedded into the data. Use this for small images like certification trust marks or regulated labels. Large images should be external links.", + "required": [ + "name", + "imageData", + "mediaType" + ] + }, + "Package": { + "type": "object", + "additionalProperties": true, + "properties": { + "description": { + "type": "string", + "description": "Description of the packaging." + }, + "dimensions": { + "$ref": "#/$defs/Dimension", + "description": "dimensions of the packaging" + }, + "materialUsed": { + "type": "array", + "items": { + "$ref": "#/$defs/Material" + }, + "description": "materials used for the packaging." + }, + "packageLabel": { + "type": "array", + "items": { + "$ref": "#/$defs/Image" + }, + "description": "An array of package labels that may appear on the packaging together with their meaning. Use for small images that represent certification marks or regulatory requirements. Large images should be linked as evidence to claims." + }, + "performanceClaim": { + "type": "array", + "items": { + "$ref": "#/$defs/Claim" + }, + "description": "conformity claims made about the packaging." + } + }, + "description": "Details of product packaging", + "required": [ + "description", + "dimensions", + "materialUsed" + ] + }, + "Claim": { + "type": "object", + "additionalProperties": true, + "properties": { + "type": { + "type": "array", + "readOnly": true, + "default": [ + "Claim" + ], + "items": { + "type": "string" + }, + "allOf": [ + { + "contains": { + "const": "Claim", + "minContains": 1 + } + } + ] + }, + "id": { + "example": "https://sample-company.com/claim/e78dab5d-b6f6-4bc4-a458-7feb039f6cb3", + "type": "string", + "format": "uri", + "description": "Globally unique identifier of this claim. Typically represented as a URI companyURL/claimID URI or a UUID" + }, + "name": { + "example": "Sample company Forced Labour claim", + "type": "string", + "description": "Name of this claim - typically similar or the same as the referenced criterion name." + }, + "description": { + "type": "string", + "description": "Description of this conformity claim" + }, + "referenceCriteria": { + "type": "array", + "items": { + "type": "object", + "properties": { + "type": { + "type": "array", + "readOnly": true, + "default": [ + "Criterion" + ], + "items": { + "type": "string" + }, + "allOf": [ + { + "contains": { + "const": "Criterion", + "minContains": 1 + } + } + ] + }, + "id": { + "example": "https://vocabulary.sample-scheme.org/criterion/lb/v1.0", + "type": "string", + "format": "uri", + "description": "Globally unique identifier of this conformity criterion. Typically represented as a URI SchemeOwner/CriterionID URI" + }, + "name": { + "example": "Forced labour assessment criterion", + "type": "string", + "description": "Name of this criterion as defined by the scheme owner." + } + }, + "required": [ + "id", + "name" + ] + }, + "description": "The criterion against which the claim is made." + }, + "referenceRegulation": { + "type": "array", + "items": { + "type": "object", + "properties": { + "type": { + "type": "array", + "readOnly": true, + "default": [ + "Regulation" + ], + "items": { + "type": "string" + }, + "allOf": [ + { + "contains": { + "const": "Regulation", + "minContains": 1 + } + } + ] + }, + "id": { + "example": "https://regulations.country.gov/ABC-12345", + "type": "string", + "format": "uri", + "description": "Globally unique identifier of this standard. Typically represented as a URI government/regulation URI" + }, + "name": { + "example": "Due Diligence Directove", + "type": "string", + "description": "Name of this regulation as defined by the regulator." + } + }, + "required": [ + "id", + "name" + ] + }, + "description": "List of references to regulation to which conformity is claimed claimed for this product" + }, + "referenceStandard": { + "type": "array", + "items": { + "type": "object", + "properties": { + "type": { + "type": "array", + "readOnly": true, + "default": [ + "Standard" + ], + "items": { + "type": "string" + }, + "allOf": [ + { + "contains": { + "const": "Standard", + "minContains": 1 + } + } + ] + }, + "id": { + "example": "https://sample-standards.org/A1234", + "type": "string", + "format": "uri", + "description": "Globally unique identifier of this standard. Typically represented as a URI issuer/standard URI" + }, + "name": { + "example": "Labour rights standard", + "type": "string", + "description": "Name for this standard" + } + }, + "required": [ + "id", + "name" + ] + }, + "description": "List of references to standards to which conformity is claimed claimed for this product" + }, + "claimDate": { + "type": "string", + "format": "date", + "description": "That date on which the claimed performance is applicable." + }, + "applicablePeriod": { + "$ref": "#/$defs/Period", + "description": "The applicable reporting period for this facility record." + }, + "claimedPerformance": { + "type": "array", + "items": { + "$ref": "#/$defs/Performance" + }, + "description": "The claimed performance level " + }, + "evidence": { + "type": "array", + "items": { + "$ref": "#/$defs/Link" + }, + "description": "A URI pointing to the evidence supporting the claim. SHOULD be a URL to a UNTP Digital Conformity Credential (DCC)" + }, + "conformityTopic": { + "type": "array", + "items": { + "$ref": "#/$defs/ConformityTopic" + }, + "description": "The conformity topic category for this assessment" + } + }, + "description": "A performance claim about a product, facility, or organisation that is made against a well defined criterion.", + "required": [ + "id", + "name", + "referenceCriteria", + "claimDate", + "claimedPerformance", + "conformityTopic" + ] + }, + "Period": { + "type": "object", + "additionalProperties": false, + "properties": { + "startDate": { + "type": "string", + "format": "date", + "description": "The period start date" + }, + "endDate": { + "type": "string", + "format": "date", + "description": "The period end date" + }, + "periodInformation": { + "type": "string", + "description": "Additional information relevant to this reporting period" + } + }, + "description": "A period of time, typically a month, quarter or a year, which defines the context boundary for reported facts.", + "required": [ + "startDate", + "endDate" + ] + }, + "Performance": { + "type": "object", + "additionalProperties": false, + "properties": { + "metric": { + "type": "object", + "properties": { + "type": { + "type": "array", + "readOnly": true, + "default": [ + "PerformanceMetric" + ], + "items": { + "type": "string" + }, + "allOf": [ + { + "contains": { + "const": "PerformanceMetric", + "minContains": 1 + } + } + ] + }, + "id": { + "example": "https://authority.gov/schemeABC/123456789", + "type": "string", + "format": "uri", + "description": "Globally unique identifier of this reporting metric. " + }, + "name": { + "example": "emissions intensity", + "type": "string", + "description": "A human readable name for this metric (for example \"water usage per Kg of material\")" + } + }, + "required": [ + "id", + "name" + ], + "description": "The metric (eg material emissions intensity CO2e/Kg or percentage of young workers) that is measured." + }, + "measure": { + "$ref": "#/$defs/Measure", + "description": "The measured performance value" + }, + "score": { + "$ref": "#/$defs/Score", + "description": "A performance score (eg \"AA\") drawn from a scoring framework defined by the scheme or criterion." + } + }, + "description": "A claimed, assessed, or required performance level defined either by a scoring system or a numeric measure. When a numeric measure is provided, the metric classifying the measurement is required. When only a score is provided, the scoring framework is discoverable via the conformity scheme or criterion.", + "dependentRequired": { + "measure": [ + "metric" + ] + } + }, + "Score": { + "type": "object", + "additionalProperties": false, + "properties": { + "code": { + "type": "string", + "description": "The coded value for this score (eg \"AAA\")" + }, + "rank": { + "type": "integer", + "description": "The ranking of this score within the scoring framework - using an integer where \"1\" is the highest rank." + }, + "definition": { + "type": "string", + "description": "A description of the meaning of this score." + } + }, + "description": "A single score within a scoring framework. ", + "required": [ + "code" + ] + }, + "ConformityTopic": { + "type": "object", + "additionalProperties": false, + "properties": { + "type": { + "type": "array", + "readOnly": true, + "default": [ + "ConformityTopic" + ], + "items": { + "type": "string" + }, + "allOf": [ + { + "contains": { + "const": "ConformityTopic", + "minContains": 1 + } + } + ] + }, + "id": { + "type": "string", + "format": "uri", + "description": "The unique identifier for this conformity topic" + }, + "name": { + "example": "forced-labour", + "type": "string", + "description": "The human readable name for this conformity topic." + }, + "definition": { + "type": "string", + "description": "The rich definition of this conformity topic." + } + }, + "description": "The UNTP standard classification scheme for conformity topic. see http://vocabulary.uncefact.org/ConformityTopic", + "required": [ + "id", + "name" + ] + } + } +} diff --git a/tests/fixtures/upstream/v0.7.0/vocabularies/untp-metrics.jsonld b/tests/fixtures/upstream/v0.7.0/vocabularies/untp-metrics.jsonld new file mode 100644 index 0000000..59dbd80 --- /dev/null +++ b/tests/fixtures/upstream/v0.7.0/vocabularies/untp-metrics.jsonld @@ -0,0 +1,1146 @@ +{ + "@context": { + "skos": "http://www.w3.org/2004/02/skos/core#", + "dcterms": "http://purl.org/dc/terms/", + "xsd": "http://www.w3.org/2001/XMLSchema#", + "owl": "http://www.w3.org/2002/07/owl#", + "rdfs": "http://www.w3.org/2000/01/rdf-schema#", + "untp": "https://vocabulary.uncefact.org/untp/", + "metrics": "https://vocabulary.uncefact.org/performance-metrics/", + "rec20": "https://vocabulary.uncefact.org/rec20/", + "prefLabel": { "@id": "skos:prefLabel", "@language": "en" }, + "definition": { "@id": "skos:definition", "@language": "en" }, + "notation": "skos:notation", + "scopeNote": { "@id": "skos:scopeNote", "@language": "en" }, + "broader": { "@id": "skos:broader", "@type": "@id" }, + "narrower": { "@id": "skos:narrower", "@type": "@id", "@container": "@set" }, + "topConceptOf": { "@id": "skos:topConceptOf", "@type": "@id" }, + "hasTopConcept": { "@id": "skos:hasTopConcept", "@type": "@id", "@container": "@set" }, + "inScheme": { "@id": "skos:inScheme", "@type": "@id" }, + "closeMatch": { "@id": "skos:closeMatch", "@type": "@id", "@container": "@set" }, + "allowedUnit": "untp:allowedUnit", + "aggregationMethod": "untp:aggregationMethod", + "improvementDirection": "untp:improvementDirection" + }, + "@graph": [ + { + "@id": "https://vocabulary.uncefact.org/performance-metrics/", + "@type": "skos:ConceptScheme", + "dcterms:title": { "@value": "UNTP Performance Metrics Vocabulary", "@language": "en" }, + "dcterms:description": { "@value": "A hierarchical vocabulary of standardised performance metrics for tagging fine-grained product and facility-level sustainability claims. Enables automatic roll-up to enterprise-level disclosures aligned with IFRS S1/S2, GRI, ESRS, and EU Battery Regulation. Counterpart to the UNTP Conformity Topic Classification — topics classify what is being assessed, metrics define what is measured.", "@language": "en" }, + "dcterms:creator": "United Nations Economic Commission for Europe (UNECE)", + "dcterms:license": "https://creativecommons.org/licenses/by/4.0/", + "owl:versionInfo": "0.1.0-working", + "dcterms:issued": "2026-03-13", + "dcterms:modified": "2026-03-13", + "hasTopConcept": [ + "metrics:greenhouse-gas-emissions", + "metrics:energy", + "metrics:water", + "metrics:waste-and-circularity", + "metrics:biodiversity-and-land-use", + "metrics:pollution", + "metrics:workforce", + "metrics:governance", + "metrics:product-safety-and-quality", + "metrics:food-safety-and-quality" + ] + }, + + { + "@id": "metrics:greenhouse-gas-emissions", + "@type": "skos:Concept", + "prefLabel": "Greenhouse Gas Emissions", + "definition": "Metrics for measuring, reporting, and reducing greenhouse gas emissions across all scopes, including absolute values, intensities, and reduction progress.", + "notation": "01", + "topConceptOf": "https://vocabulary.uncefact.org/performance-metrics/", + "inScheme": "https://vocabulary.uncefact.org/performance-metrics/", + "scopeNote": "IFRS S2 paras 29–36; ESRS E1; GRI 305; GHG Protocol Corporate Standard.", + "narrower": [ + "metrics:scope-1-ghg-emissions", + "metrics:scope-2-ghg-emissions", + "metrics:scope-3-upstream-emissions", + "metrics:scope-3-downstream-emissions", + "metrics:total-ghg-emissions", + "metrics:ghg-emissions-intensity", + "metrics:product-carbon-footprint", + "metrics:biogenic-emissions", + "metrics:ghg-reduction-progress" + ] + }, + { + "@id": "metrics:scope-1-ghg-emissions", + "@type": "skos:Concept", + "prefLabel": "Scope 1 GHG Emissions", + "definition": "Absolute GHG emissions from sources owned or controlled by the reporting entity, in tonnes CO2 equivalent.", + "notation": "01.01", + "broader": "metrics:greenhouse-gas-emissions", + "inScheme": "https://vocabulary.uncefact.org/performance-metrics/", + "scopeNote": "IFRS S2 para 29(a); ESRS E1-6; GRI 305-1; GHG Protocol Scope 1.", + "allowedUnit": "TNE", + "aggregationMethod": "sum", + "improvementDirection": "lower" + }, + { + "@id": "metrics:scope-2-ghg-emissions", + "@type": "skos:Concept", + "prefLabel": "Scope 2 GHG Emissions", + "definition": "Indirect GHG emissions from purchased electricity, steam, heating, and cooling consumed by the reporting entity, in tonnes CO2 equivalent.", + "notation": "01.02", + "broader": "metrics:greenhouse-gas-emissions", + "inScheme": "https://vocabulary.uncefact.org/performance-metrics/", + "scopeNote": "IFRS S2 para 29(a); ESRS E1-6; GRI 305-2; GHG Protocol Scope 2.", + "allowedUnit": "TNE", + "aggregationMethod": "sum", + "improvementDirection": "lower" + }, + { + "@id": "metrics:scope-3-upstream-emissions", + "@type": "skos:Concept", + "prefLabel": "Scope 3 Upstream Emissions", + "definition": "Indirect GHG emissions occurring in the upstream value chain including purchased goods, transportation, and business travel, in tonnes CO2 equivalent.", + "notation": "01.03", + "broader": "metrics:greenhouse-gas-emissions", + "inScheme": "https://vocabulary.uncefact.org/performance-metrics/", + "scopeNote": "IFRS S2 para 29(a); ESRS E1-6; GRI 305-3; GHG Protocol Scope 3 categories 1–8.", + "allowedUnit": "TNE", + "aggregationMethod": "sum", + "improvementDirection": "lower" + }, + { + "@id": "metrics:scope-3-downstream-emissions", + "@type": "skos:Concept", + "prefLabel": "Scope 3 Downstream Emissions", + "definition": "Indirect GHG emissions occurring in the downstream value chain including product use, end-of-life treatment, and distribution, in tonnes CO2 equivalent.", + "notation": "01.04", + "broader": "metrics:greenhouse-gas-emissions", + "inScheme": "https://vocabulary.uncefact.org/performance-metrics/", + "scopeNote": "IFRS S2 para 29(a); ESRS E1-6; GRI 305-3; GHG Protocol Scope 3 categories 9–15.", + "allowedUnit": "TNE", + "aggregationMethod": "sum", + "improvementDirection": "lower" + }, + { + "@id": "metrics:total-ghg-emissions", + "@type": "skos:Concept", + "prefLabel": "Total GHG Emissions", + "definition": "Sum of Scope 1, Scope 2, and Scope 3 greenhouse gas emissions, in tonnes CO2 equivalent.", + "notation": "01.05", + "broader": "metrics:greenhouse-gas-emissions", + "inScheme": "https://vocabulary.uncefact.org/performance-metrics/", + "scopeNote": "IFRS S2 para 29; ESRS E1-6; GRI 305.", + "allowedUnit": "TNE", + "aggregationMethod": "sum", + "improvementDirection": "lower" + }, + { + "@id": "metrics:ghg-emissions-intensity", + "@type": "skos:Concept", + "prefLabel": "GHG Emissions Intensity", + "definition": "Greenhouse gas emissions per unit of economic output or physical activity, expressed as kg CO2e per unit of measure.", + "notation": "01.06", + "broader": "metrics:greenhouse-gas-emissions", + "inScheme": "https://vocabulary.uncefact.org/performance-metrics/", + "scopeNote": "IFRS S2 para 29(b); ESRS E1-6; GRI 305-4.", + "allowedUnit": "KGM", + "aggregationMethod": "weighted-average", + "improvementDirection": "lower" + }, + { + "@id": "metrics:product-carbon-footprint", + "@type": "skos:Concept", + "prefLabel": "Product Carbon Footprint", + "definition": "Total lifecycle greenhouse gas emissions attributable to a single product unit, from raw material extraction through end-of-life, in kg CO2 equivalent.", + "notation": "01.07", + "broader": "metrics:greenhouse-gas-emissions", + "inScheme": "https://vocabulary.uncefact.org/performance-metrics/", + "scopeNote": "ISO 14067; EU PEF method; EU Battery Regulation Art. 7.", + "allowedUnit": "KGM", + "aggregationMethod": "weighted-average", + "improvementDirection": "lower" + }, + { + "@id": "metrics:biogenic-emissions", + "@type": "skos:Concept", + "prefLabel": "Biogenic Emissions", + "definition": "CO2 emissions from the combustion or biodegradation of biomass, reported separately from fossil-fuel emissions, in tonnes CO2.", + "notation": "01.08", + "broader": "metrics:greenhouse-gas-emissions", + "inScheme": "https://vocabulary.uncefact.org/performance-metrics/", + "scopeNote": "GHG Protocol Land Sector and Removals Guidance; GRI 305-1.", + "allowedUnit": "TNE", + "aggregationMethod": "sum", + "improvementDirection": "lower" + }, + { + "@id": "metrics:ghg-reduction-progress", + "@type": "skos:Concept", + "prefLabel": "GHG Reduction Target Progress", + "definition": "Percentage of committed GHG reduction target achieved, measured against a declared baseline year.", + "notation": "01.09", + "broader": "metrics:greenhouse-gas-emissions", + "inScheme": "https://vocabulary.uncefact.org/performance-metrics/", + "scopeNote": "IFRS S2 para 33; ESRS E1-4; SBTi Target Validation Protocol.", + "allowedUnit": "P1", + "aggregationMethod": "latest", + "improvementDirection": "higher" + }, + + { + "@id": "metrics:energy", + "@type": "skos:Concept", + "prefLabel": "Energy", + "definition": "Metrics for measuring energy consumption, renewable energy share, and energy efficiency across operations and supply chains.", + "notation": "02", + "topConceptOf": "https://vocabulary.uncefact.org/performance-metrics/", + "inScheme": "https://vocabulary.uncefact.org/performance-metrics/", + "scopeNote": "IFRS S2 para 29; ESRS E1; GRI 302; EU Energy Efficiency Directive.", + "narrower": [ + "metrics:total-energy-consumption", + "metrics:renewable-energy-percentage", + "metrics:energy-intensity", + "metrics:onsite-renewable-generation", + "metrics:non-renewable-energy-consumption" + ] + }, + { + "@id": "metrics:total-energy-consumption", + "@type": "skos:Concept", + "prefLabel": "Total Energy Consumption", + "definition": "Total energy consumed from all sources including fuel, electricity, heating, cooling, and steam, in megawatt hours.", + "notation": "02.01", + "broader": "metrics:energy", + "inScheme": "https://vocabulary.uncefact.org/performance-metrics/", + "scopeNote": "ESRS E1-5; GRI 302-1; ISO 50001.", + "allowedUnit": "MWH", + "aggregationMethod": "sum", + "improvementDirection": "lower" + }, + { + "@id": "metrics:renewable-energy-percentage", + "@type": "skos:Concept", + "prefLabel": "Renewable Energy Percentage", + "definition": "Share of total energy consumption sourced from renewable sources such as solar, wind, hydro, and geothermal.", + "notation": "02.02", + "broader": "metrics:energy", + "inScheme": "https://vocabulary.uncefact.org/performance-metrics/", + "scopeNote": "ESRS E1-5; GRI 302-1; RE100 reporting.", + "allowedUnit": "P1", + "aggregationMethod": "weighted-average", + "improvementDirection": "higher" + }, + { + "@id": "metrics:energy-intensity", + "@type": "skos:Concept", + "prefLabel": "Energy Intensity", + "definition": "Energy consumed per unit of economic output or physical activity, expressed as MWh per unit of measure.", + "notation": "02.03", + "broader": "metrics:energy", + "inScheme": "https://vocabulary.uncefact.org/performance-metrics/", + "scopeNote": "ESRS E1-5; GRI 302-3.", + "allowedUnit": "MWH", + "aggregationMethod": "weighted-average", + "improvementDirection": "lower" + }, + { + "@id": "metrics:onsite-renewable-generation", + "@type": "skos:Concept", + "prefLabel": "On-site Renewable Generation", + "definition": "Total renewable energy generated on-site from owned or controlled installations, in megawatt hours.", + "notation": "02.04", + "broader": "metrics:energy", + "inScheme": "https://vocabulary.uncefact.org/performance-metrics/", + "scopeNote": "GRI 302-1; RE100 reporting methodology.", + "allowedUnit": "MWH", + "aggregationMethod": "sum", + "improvementDirection": "higher" + }, + { + "@id": "metrics:non-renewable-energy-consumption", + "@type": "skos:Concept", + "prefLabel": "Non-Renewable Energy Consumption", + "definition": "Energy consumed from non-renewable sources including fossil fuels and nuclear, in megawatt hours.", + "notation": "02.05", + "broader": "metrics:energy", + "inScheme": "https://vocabulary.uncefact.org/performance-metrics/", + "scopeNote": "ESRS E1-5; GRI 302-1.", + "allowedUnit": "MWH", + "aggregationMethod": "sum", + "improvementDirection": "lower" + }, + + { + "@id": "metrics:water", + "@type": "skos:Concept", + "prefLabel": "Water", + "definition": "Metrics for measuring water withdrawal, consumption, discharge, recycling, and usage intensity across operations.", + "notation": "03", + "topConceptOf": "https://vocabulary.uncefact.org/performance-metrics/", + "inScheme": "https://vocabulary.uncefact.org/performance-metrics/", + "scopeNote": "ESRS E3; GRI 303; CEO Water Mandate; Alliance for Water Stewardship.", + "narrower": [ + "metrics:total-water-withdrawal", + "metrics:water-consumption", + "metrics:water-recycling-rate", + "metrics:water-discharge", + "metrics:water-intensity", + "metrics:water-stress-area-withdrawal" + ] + }, + { + "@id": "metrics:total-water-withdrawal", + "@type": "skos:Concept", + "prefLabel": "Total Water Withdrawal", + "definition": "Total volume of water drawn from surface, ground, sea, produced, or third-party sources, in cubic metres.", + "notation": "03.01", + "broader": "metrics:water", + "inScheme": "https://vocabulary.uncefact.org/performance-metrics/", + "scopeNote": "ESRS E3-4; GRI 303-3.", + "allowedUnit": "MTQ", + "aggregationMethod": "sum", + "improvementDirection": "lower" + }, + { + "@id": "metrics:water-consumption", + "@type": "skos:Concept", + "prefLabel": "Water Consumption", + "definition": "Volume of water withdrawn that is not returned to the original source, representing net water removed from the environment, in cubic metres.", + "notation": "03.02", + "broader": "metrics:water", + "inScheme": "https://vocabulary.uncefact.org/performance-metrics/", + "scopeNote": "ESRS E3-4; GRI 303-5.", + "allowedUnit": "MTQ", + "aggregationMethod": "sum", + "improvementDirection": "lower" + }, + { + "@id": "metrics:water-recycling-rate", + "@type": "skos:Concept", + "prefLabel": "Water Recycling Rate", + "definition": "Percentage of total water use that is recycled or reused within operations.", + "notation": "03.03", + "broader": "metrics:water", + "inScheme": "https://vocabulary.uncefact.org/performance-metrics/", + "scopeNote": "GRI 303-3; CEO Water Mandate.", + "allowedUnit": "P1", + "aggregationMethod": "weighted-average", + "improvementDirection": "higher" + }, + { + "@id": "metrics:water-discharge", + "@type": "skos:Concept", + "prefLabel": "Water Discharge", + "definition": "Total volume of effluent water discharged to surface water, groundwater, or third-party treatment, in cubic metres.", + "notation": "03.04", + "broader": "metrics:water", + "inScheme": "https://vocabulary.uncefact.org/performance-metrics/", + "scopeNote": "ESRS E3-4; GRI 303-4.", + "allowedUnit": "MTQ", + "aggregationMethod": "sum", + "improvementDirection": "lower" + }, + { + "@id": "metrics:water-intensity", + "@type": "skos:Concept", + "prefLabel": "Water Intensity", + "definition": "Water consumed per unit of economic output or physical activity, expressed as litres per unit of measure.", + "notation": "03.05", + "broader": "metrics:water", + "inScheme": "https://vocabulary.uncefact.org/performance-metrics/", + "scopeNote": "ESRS E3-4; GRI 303-5.", + "allowedUnit": "LTR", + "aggregationMethod": "weighted-average", + "improvementDirection": "lower" + }, + { + "@id": "metrics:water-stress-area-withdrawal", + "@type": "skos:Concept", + "prefLabel": "Water Stress Area Withdrawal", + "definition": "Volume of water withdrawn from areas classified as high or extremely-high baseline water stress, in cubic metres.", + "notation": "03.06", + "broader": "metrics:water", + "inScheme": "https://vocabulary.uncefact.org/performance-metrics/", + "scopeNote": "ESRS E3-4; GRI 303-3; WRI Aqueduct water stress classifications.", + "allowedUnit": "MTQ", + "aggregationMethod": "sum", + "improvementDirection": "lower" + }, + + { + "@id": "metrics:waste-and-circularity", + "@type": "skos:Concept", + "prefLabel": "Waste and Circularity", + "definition": "Metrics for measuring waste generation, diversion, recycled content, recyclability, and circular economy performance.", + "notation": "04", + "topConceptOf": "https://vocabulary.uncefact.org/performance-metrics/", + "inScheme": "https://vocabulary.uncefact.org/performance-metrics/", + "scopeNote": "ESRS E5; GRI 306; EU ESPR Art. 5–8; EU Waste Framework Directive.", + "narrower": [ + "metrics:total-waste-generated", + "metrics:hazardous-waste-generated", + "metrics:waste-diversion-rate", + "metrics:recycled-content-percentage", + "metrics:recyclability-rate", + "metrics:material-recovery-rate", + "metrics:waste-to-landfill", + "metrics:product-durability-index", + "metrics:reuse-remanufacturing-rate" + ] + }, + { + "@id": "metrics:total-waste-generated", + "@type": "skos:Concept", + "prefLabel": "Total Waste Generated", + "definition": "Total weight of hazardous and non-hazardous waste generated by operations, in tonnes.", + "notation": "04.01", + "broader": "metrics:waste-and-circularity", + "inScheme": "https://vocabulary.uncefact.org/performance-metrics/", + "scopeNote": "ESRS E5-5; GRI 306-3.", + "allowedUnit": "TNE", + "aggregationMethod": "sum", + "improvementDirection": "lower" + }, + { + "@id": "metrics:hazardous-waste-generated", + "@type": "skos:Concept", + "prefLabel": "Hazardous Waste Generated", + "definition": "Total weight of waste classified as hazardous under applicable regulations, in tonnes.", + "notation": "04.02", + "broader": "metrics:waste-and-circularity", + "inScheme": "https://vocabulary.uncefact.org/performance-metrics/", + "scopeNote": "ESRS E5-5; GRI 306-3; Basel Convention.", + "allowedUnit": "TNE", + "aggregationMethod": "sum", + "improvementDirection": "lower" + }, + { + "@id": "metrics:waste-diversion-rate", + "@type": "skos:Concept", + "prefLabel": "Waste Diversion Rate", + "definition": "Percentage of total waste diverted from landfill and incineration through recycling, composting, or other recovery methods.", + "notation": "04.03", + "broader": "metrics:waste-and-circularity", + "inScheme": "https://vocabulary.uncefact.org/performance-metrics/", + "scopeNote": "ESRS E5-5; GRI 306-4.", + "allowedUnit": "P1", + "aggregationMethod": "weighted-average", + "improvementDirection": "higher" + }, + { + "@id": "metrics:recycled-content-percentage", + "@type": "skos:Concept", + "prefLabel": "Recycled Content Percentage", + "definition": "Share of pre-consumer and post-consumer recycled material in the total weight of a product or material input.", + "notation": "04.04", + "broader": "metrics:waste-and-circularity", + "inScheme": "https://vocabulary.uncefact.org/performance-metrics/", + "scopeNote": "EU ESPR Art. 8; EU Battery Regulation Art. 8; ISO 14021; GRI 301-2.", + "allowedUnit": "P1", + "aggregationMethod": "weighted-average", + "improvementDirection": "higher" + }, + { + "@id": "metrics:recyclability-rate", + "@type": "skos:Concept", + "prefLabel": "Recyclability Rate", + "definition": "Percentage of product weight that is technically recyclable at end of life under available infrastructure.", + "notation": "04.05", + "broader": "metrics:waste-and-circularity", + "inScheme": "https://vocabulary.uncefact.org/performance-metrics/", + "scopeNote": "EU ESPR Art. 6; ISO 14021.", + "allowedUnit": "P1", + "aggregationMethod": "weighted-average", + "improvementDirection": "higher" + }, + { + "@id": "metrics:material-recovery-rate", + "@type": "skos:Concept", + "prefLabel": "Material Recovery Rate", + "definition": "Percentage of end-of-life product mass actually recovered through recycling, remanufacturing, or refurbishment processes.", + "notation": "04.06", + "broader": "metrics:waste-and-circularity", + "inScheme": "https://vocabulary.uncefact.org/performance-metrics/", + "scopeNote": "EU ESPR Art. 6; EU Waste Framework Directive Art. 11; GRI 306-4.", + "allowedUnit": "P1", + "aggregationMethod": "weighted-average", + "improvementDirection": "higher" + }, + { + "@id": "metrics:waste-to-landfill", + "@type": "skos:Concept", + "prefLabel": "Waste to Landfill", + "definition": "Total weight of waste disposed via landfill, in tonnes.", + "notation": "04.07", + "broader": "metrics:waste-and-circularity", + "inScheme": "https://vocabulary.uncefact.org/performance-metrics/", + "scopeNote": "ESRS E5-5; GRI 306-5.", + "allowedUnit": "TNE", + "aggregationMethod": "sum", + "improvementDirection": "lower" + }, + { + "@id": "metrics:product-durability-index", + "@type": "skos:Concept", + "prefLabel": "Product Durability Index", + "definition": "Expected useful life of a product under normal conditions of use, expressed in years or cycles as applicable.", + "notation": "04.08", + "broader": "metrics:waste-and-circularity", + "inScheme": "https://vocabulary.uncefact.org/performance-metrics/", + "scopeNote": "EU ESPR Art. 5 – Durability requirements; ESRS E5.", + "allowedUnit": "ANN", + "aggregationMethod": "average", + "improvementDirection": "higher" + }, + { + "@id": "metrics:reuse-remanufacturing-rate", + "@type": "skos:Concept", + "prefLabel": "Reuse and Remanufacturing Rate", + "definition": "Percentage of product units or components returned to service through reuse, refurbishment, or remanufacturing.", + "notation": "04.09", + "broader": "metrics:waste-and-circularity", + "inScheme": "https://vocabulary.uncefact.org/performance-metrics/", + "scopeNote": "EU ESPR Art. 6; EU Waste Framework Directive.", + "allowedUnit": "P1", + "aggregationMethod": "weighted-average", + "improvementDirection": "higher" + }, + + { + "@id": "metrics:biodiversity-and-land-use", + "@type": "skos:Concept", + "prefLabel": "Biodiversity and Land Use", + "definition": "Metrics for measuring deforestation-free sourcing, land-use change, biodiversity impact, and protection of sensitive areas.", + "notation": "05", + "topConceptOf": "https://vocabulary.uncefact.org/performance-metrics/", + "inScheme": "https://vocabulary.uncefact.org/performance-metrics/", + "scopeNote": "ESRS E4; GRI 304; TNFD; EU Deforestation Regulation; Kunming-Montreal Global Biodiversity Framework.", + "narrower": [ + "metrics:deforestation-free-sourcing", + "metrics:land-use-change", + "metrics:biodiversity-impact-score", + "metrics:protected-area-impact" + ] + }, + { + "@id": "metrics:deforestation-free-sourcing", + "@type": "skos:Concept", + "prefLabel": "Deforestation-Free Sourcing", + "definition": "Percentage of raw material inputs verified as sourced without associated deforestation or forest degradation after a declared cut-off date.", + "notation": "05.01", + "broader": "metrics:biodiversity-and-land-use", + "inScheme": "https://vocabulary.uncefact.org/performance-metrics/", + "scopeNote": "EU Deforestation Regulation (EUDR); ESRS E4; GRI 304.", + "allowedUnit": "P1", + "aggregationMethod": "weighted-average", + "improvementDirection": "higher" + }, + { + "@id": "metrics:land-use-change", + "@type": "skos:Concept", + "prefLabel": "Land Use Change", + "definition": "Area of natural ecosystems converted to managed land for production or extraction activities, in hectares.", + "notation": "05.02", + "broader": "metrics:biodiversity-and-land-use", + "inScheme": "https://vocabulary.uncefact.org/performance-metrics/", + "scopeNote": "ESRS E4; GRI 304-1; GHG Protocol Land Sector Guidance.", + "allowedUnit": "HAR", + "aggregationMethod": "sum", + "improvementDirection": "lower" + }, + { + "@id": "metrics:biodiversity-impact-score", + "@type": "skos:Concept", + "prefLabel": "Biodiversity Impact Score", + "definition": "Composite index quantifying the impact of operations on species diversity and ecosystem integrity, using a recognised assessment framework (e.g., STAR, BII).", + "notation": "05.03", + "broader": "metrics:biodiversity-and-land-use", + "inScheme": "https://vocabulary.uncefact.org/performance-metrics/", + "scopeNote": "TNFD LEAP approach; ESRS E4; SBTN biodiversity targets.", + "allowedUnit": "C62", + "aggregationMethod": "average", + "improvementDirection": "lower" + }, + { + "@id": "metrics:protected-area-impact", + "@type": "skos:Concept", + "prefLabel": "Protected Area Impact", + "definition": "Area of operations, sourcing, or infrastructure footprint located within or adjacent to legally protected or high-biodiversity-value areas, in hectares.", + "notation": "05.04", + "broader": "metrics:biodiversity-and-land-use", + "inScheme": "https://vocabulary.uncefact.org/performance-metrics/", + "scopeNote": "ESRS E4; GRI 304-1; IUCN Protected Area categories.", + "allowedUnit": "HAR", + "aggregationMethod": "sum", + "improvementDirection": "lower" + }, + + { + "@id": "metrics:pollution", + "@type": "skos:Concept", + "prefLabel": "Pollution", + "definition": "Metrics for measuring air pollutant emissions, hazardous substance releases, and chemical safety performance beyond GHG emissions.", + "notation": "06", + "topConceptOf": "https://vocabulary.uncefact.org/performance-metrics/", + "inScheme": "https://vocabulary.uncefact.org/performance-metrics/", + "scopeNote": "ESRS E2; GRI 305 (non-GHG); EU Industrial Emissions Directive; Stockholm Convention; Montreal Protocol.", + "narrower": [ + "metrics:sox-emissions", + "metrics:nox-emissions", + "metrics:voc-emissions", + "metrics:particulate-matter-emissions", + "metrics:substances-of-concern", + "metrics:ozone-depleting-emissions" + ] + }, + { + "@id": "metrics:sox-emissions", + "@type": "skos:Concept", + "prefLabel": "SOx Emissions", + "definition": "Total mass of sulphur oxides released to air from stationary and mobile sources, in tonnes.", + "notation": "06.01", + "broader": "metrics:pollution", + "inScheme": "https://vocabulary.uncefact.org/performance-metrics/", + "scopeNote": "ESRS E2-4; GRI 305-7; EU Industrial Emissions Directive.", + "allowedUnit": "TNE", + "aggregationMethod": "sum", + "improvementDirection": "lower" + }, + { + "@id": "metrics:nox-emissions", + "@type": "skos:Concept", + "prefLabel": "NOx Emissions", + "definition": "Total mass of nitrogen oxides released to air from combustion and industrial processes, in tonnes.", + "notation": "06.02", + "broader": "metrics:pollution", + "inScheme": "https://vocabulary.uncefact.org/performance-metrics/", + "scopeNote": "ESRS E2-4; GRI 305-7; EU Industrial Emissions Directive.", + "allowedUnit": "TNE", + "aggregationMethod": "sum", + "improvementDirection": "lower" + }, + { + "@id": "metrics:voc-emissions", + "@type": "skos:Concept", + "prefLabel": "VOC Emissions", + "definition": "Total mass of volatile organic compounds released to air from solvents, coatings, and industrial processes, in tonnes.", + "notation": "06.03", + "broader": "metrics:pollution", + "inScheme": "https://vocabulary.uncefact.org/performance-metrics/", + "scopeNote": "ESRS E2-4; GRI 305-7; EU Solvents Directive.", + "allowedUnit": "TNE", + "aggregationMethod": "sum", + "improvementDirection": "lower" + }, + { + "@id": "metrics:particulate-matter-emissions", + "@type": "skos:Concept", + "prefLabel": "Particulate Matter Emissions", + "definition": "Total mass of fine particulate matter (PM2.5 and PM10) released to air from operations, in tonnes.", + "notation": "06.04", + "broader": "metrics:pollution", + "inScheme": "https://vocabulary.uncefact.org/performance-metrics/", + "scopeNote": "ESRS E2-4; GRI 305-7; WHO Air Quality Guidelines.", + "allowedUnit": "TNE", + "aggregationMethod": "sum", + "improvementDirection": "lower" + }, + { + "@id": "metrics:substances-of-concern", + "@type": "skos:Concept", + "prefLabel": "Substances of Concern", + "definition": "Total mass of substances of concern or substances of very high concern (SVHC) present in products or released during production, in kilograms.", + "notation": "06.05", + "broader": "metrics:pollution", + "inScheme": "https://vocabulary.uncefact.org/performance-metrics/", + "scopeNote": "EU ESPR Annex I; REACH SVHC candidate list; ESRS E2.", + "allowedUnit": "KGM", + "aggregationMethod": "sum", + "improvementDirection": "lower" + }, + { + "@id": "metrics:ozone-depleting-emissions", + "@type": "skos:Concept", + "prefLabel": "Ozone-Depleting Substance Emissions", + "definition": "Total mass of ozone-depleting substances released, measured in kg CFC-11 equivalent.", + "notation": "06.06", + "broader": "metrics:pollution", + "inScheme": "https://vocabulary.uncefact.org/performance-metrics/", + "scopeNote": "GRI 305-6; Montreal Protocol; ESRS E2.", + "allowedUnit": "KGM", + "aggregationMethod": "sum", + "improvementDirection": "lower" + }, + + { + "@id": "metrics:workforce", + "@type": "skos:Concept", + "prefLabel": "Workforce", + "definition": "Metrics for measuring labour practices, workplace safety, diversity, equity, and human rights performance across operations and supply chains.", + "notation": "07", + "topConceptOf": "https://vocabulary.uncefact.org/performance-metrics/", + "inScheme": "https://vocabulary.uncefact.org/performance-metrics/", + "scopeNote": "ESRS S1, S2; GRI 401–409; IFRS S1; ILO Core Conventions; UN Guiding Principles on Business and Human Rights.", + "narrower": [ + "metrics:living-wage-coverage", + "metrics:lost-time-injury-rate", + "metrics:gender-pay-gap", + "metrics:women-in-management", + "metrics:training-hours-per-employee", + "metrics:collective-bargaining-coverage", + "metrics:employee-turnover-rate", + "metrics:child-labor-incidents", + "metrics:forced-labor-incidents", + "metrics:workforce-diversity-ratio" + ] + }, + { + "@id": "metrics:living-wage-coverage", + "@type": "skos:Concept", + "prefLabel": "Living Wage Coverage", + "definition": "Percentage of workers (including contractor and supply-chain workers in scope) receiving at least a verified living wage.", + "notation": "07.01", + "broader": "metrics:workforce", + "inScheme": "https://vocabulary.uncefact.org/performance-metrics/", + "scopeNote": "ESRS S1-10; GRI 202-1; Global Living Wage Coalition methodology.", + "allowedUnit": "P1", + "aggregationMethod": "weighted-average", + "improvementDirection": "higher" + }, + { + "@id": "metrics:lost-time-injury-rate", + "@type": "skos:Concept", + "prefLabel": "Lost Time Injury Frequency Rate", + "definition": "Number of lost-time injuries per one million hours worked, measuring workplace safety performance.", + "notation": "07.02", + "broader": "metrics:workforce", + "inScheme": "https://vocabulary.uncefact.org/performance-metrics/", + "scopeNote": "ESRS S1-14; GRI 403-9; ISO 45001.", + "allowedUnit": "C62", + "aggregationMethod": "average", + "improvementDirection": "lower" + }, + { + "@id": "metrics:gender-pay-gap", + "@type": "skos:Concept", + "prefLabel": "Gender Pay Gap", + "definition": "Difference in average compensation between male and female employees as a percentage of male average compensation.", + "notation": "07.03", + "broader": "metrics:workforce", + "inScheme": "https://vocabulary.uncefact.org/performance-metrics/", + "scopeNote": "ESRS S1-16; GRI 405-2; EU Pay Transparency Directive.", + "allowedUnit": "P1", + "aggregationMethod": "weighted-average", + "improvementDirection": "lower" + }, + { + "@id": "metrics:women-in-management", + "@type": "skos:Concept", + "prefLabel": "Women in Management", + "definition": "Percentage of management and leadership positions held by women.", + "notation": "07.04", + "broader": "metrics:workforce", + "inScheme": "https://vocabulary.uncefact.org/performance-metrics/", + "scopeNote": "ESRS S1-9; GRI 405-1.", + "allowedUnit": "P1", + "aggregationMethod": "weighted-average", + "improvementDirection": "higher" + }, + { + "@id": "metrics:training-hours-per-employee", + "@type": "skos:Concept", + "prefLabel": "Training Hours per Employee", + "definition": "Average number of hours of training and professional development provided per employee per year.", + "notation": "07.05", + "broader": "metrics:workforce", + "inScheme": "https://vocabulary.uncefact.org/performance-metrics/", + "scopeNote": "ESRS S1-13; GRI 404-1.", + "allowedUnit": "HUR", + "aggregationMethod": "average", + "improvementDirection": "higher" + }, + { + "@id": "metrics:collective-bargaining-coverage", + "@type": "skos:Concept", + "prefLabel": "Collective Bargaining Coverage", + "definition": "Percentage of employees covered by collective bargaining agreements.", + "notation": "07.06", + "broader": "metrics:workforce", + "inScheme": "https://vocabulary.uncefact.org/performance-metrics/", + "scopeNote": "ESRS S1-8; GRI 407-1; ILO Convention 98.", + "allowedUnit": "P1", + "aggregationMethod": "weighted-average", + "improvementDirection": "higher" + }, + { + "@id": "metrics:employee-turnover-rate", + "@type": "skos:Concept", + "prefLabel": "Employee Turnover Rate", + "definition": "Percentage of employees who leave the organisation voluntarily or involuntarily during the reporting period.", + "notation": "07.07", + "broader": "metrics:workforce", + "inScheme": "https://vocabulary.uncefact.org/performance-metrics/", + "scopeNote": "ESRS S1-6; GRI 401-1.", + "allowedUnit": "P1", + "aggregationMethod": "average", + "improvementDirection": "lower" + }, + { + "@id": "metrics:child-labor-incidents", + "@type": "skos:Concept", + "prefLabel": "Child Labor Incidents", + "definition": "Number of confirmed incidents of child labor identified in own operations and supply chain during the reporting period.", + "notation": "07.08", + "broader": "metrics:workforce", + "inScheme": "https://vocabulary.uncefact.org/performance-metrics/", + "scopeNote": "ESRS S1, S2; GRI 408-1; ILO Conventions 138, 182.", + "allowedUnit": "C62", + "aggregationMethod": "count", + "improvementDirection": "lower" + }, + { + "@id": "metrics:forced-labor-incidents", + "@type": "skos:Concept", + "prefLabel": "Forced Labor Incidents", + "definition": "Number of confirmed incidents of forced, bonded, or compulsory labor identified in own operations and supply chain during the reporting period.", + "notation": "07.09", + "broader": "metrics:workforce", + "inScheme": "https://vocabulary.uncefact.org/performance-metrics/", + "scopeNote": "ESRS S1, S2; GRI 409-1; ILO Conventions 29, 105.", + "allowedUnit": "C62", + "aggregationMethod": "count", + "improvementDirection": "lower" + }, + { + "@id": "metrics:workforce-diversity-ratio", + "@type": "skos:Concept", + "prefLabel": "Workforce Diversity Ratio", + "definition": "Representation of under-represented groups in the workforce as a percentage of total headcount, covering gender, ethnicity, disability, and other protected characteristics.", + "notation": "07.10", + "broader": "metrics:workforce", + "inScheme": "https://vocabulary.uncefact.org/performance-metrics/", + "scopeNote": "ESRS S1-9; GRI 405-1.", + "allowedUnit": "P1", + "aggregationMethod": "weighted-average", + "improvementDirection": "higher" + }, + + { + "@id": "metrics:governance", + "@type": "skos:Concept", + "prefLabel": "Governance", + "definition": "Metrics for measuring anti-corruption practices, supply chain due diligence, ESG disclosure quality, and grievance mechanism effectiveness.", + "notation": "08", + "topConceptOf": "https://vocabulary.uncefact.org/performance-metrics/", + "inScheme": "https://vocabulary.uncefact.org/performance-metrics/", + "scopeNote": "ESRS G1; GRI 205, 308, 414; IFRS S1; OECD Guidelines Chapter VII.", + "narrower": [ + "metrics:anti-corruption-training-coverage", + "metrics:supplier-due-diligence-coverage", + "metrics:esg-disclosure-score", + "metrics:grievance-response-rate" + ] + }, + { + "@id": "metrics:anti-corruption-training-coverage", + "@type": "skos:Concept", + "prefLabel": "Anti-Corruption Training Coverage", + "definition": "Percentage of employees and governance body members who have received anti-corruption training during the reporting period.", + "notation": "08.01", + "broader": "metrics:governance", + "inScheme": "https://vocabulary.uncefact.org/performance-metrics/", + "scopeNote": "ESRS G1-4; GRI 205-2; OECD Anti-Bribery Convention.", + "allowedUnit": "P1", + "aggregationMethod": "weighted-average", + "improvementDirection": "higher" + }, + { + "@id": "metrics:supplier-due-diligence-coverage", + "@type": "skos:Concept", + "prefLabel": "Supplier Due Diligence Coverage", + "definition": "Percentage of significant suppliers assessed against environmental and social due diligence criteria during the reporting period.", + "notation": "08.02", + "broader": "metrics:governance", + "inScheme": "https://vocabulary.uncefact.org/performance-metrics/", + "scopeNote": "ESRS G1-5; GRI 308-1, 414-1; EU CSDDD.", + "allowedUnit": "P1", + "aggregationMethod": "weighted-average", + "improvementDirection": "higher" + }, + { + "@id": "metrics:esg-disclosure-score", + "@type": "skos:Concept", + "prefLabel": "ESG Disclosure Score", + "definition": "Composite score measuring the completeness, accuracy, and timeliness of environmental, social, and governance public disclosures.", + "notation": "08.03", + "broader": "metrics:governance", + "inScheme": "https://vocabulary.uncefact.org/performance-metrics/", + "scopeNote": "IFRS S1; ESRS 1; CDP Disclosure Scoring Methodology.", + "allowedUnit": "P1", + "aggregationMethod": "latest", + "improvementDirection": "higher" + }, + { + "@id": "metrics:grievance-response-rate", + "@type": "skos:Concept", + "prefLabel": "Grievance Response Rate", + "definition": "Percentage of grievances received through formal mechanisms that were acknowledged and addressed within the defined response timeframe.", + "notation": "08.04", + "broader": "metrics:governance", + "inScheme": "https://vocabulary.uncefact.org/performance-metrics/", + "scopeNote": "ESRS S1-17, S2-11; GRI 2-25, 2-26; UN Guiding Principles Principle 31.", + "allowedUnit": "P1", + "aggregationMethod": "average", + "improvementDirection": "higher" + }, + + { + "@id": "metrics:product-safety-and-quality", + "@type": "skos:Concept", + "prefLabel": "Product Safety and Quality", + "definition": "Metrics for measuring physical, mechanical, thermal, electrical, chemical, and fire safety properties of products and materials against applicable safety standards and performance requirements.", + "notation": "09", + "topConceptOf": "https://vocabulary.uncefact.org/performance-metrics/", + "inScheme": "https://vocabulary.uncefact.org/performance-metrics/", + "scopeNote": "EU General Product Safety Regulation (EU) 2023/988; ICC International Building Code; ISO/IEC product safety standards; EU Construction Products Regulation.", + "narrower": [ + "metrics:mechanical-strength", + "metrics:impact-resistance", + "metrics:thermal-performance", + "metrics:fire-resistance-rating", + "metrics:electrical-safety-rating", + "metrics:flammability-rating", + "metrics:chemical-substance-concentration", + "metrics:noise-emission-level" + ] + }, + { + "@id": "metrics:mechanical-strength", + "@type": "skos:Concept", + "prefLabel": "Mechanical Strength", + "definition": "Tensile, compressive, or flexural strength of a material or product under specified test conditions, in megapascals.", + "notation": "09.01", + "broader": "metrics:product-safety-and-quality", + "inScheme": "https://vocabulary.uncefact.org/performance-metrics/", + "scopeNote": "ISO 527 (tensile, plastics); ISO 6892 (tensile, metals); ASTM C39 (compressive, concrete); ICC IBC structural requirements.", + "allowedUnit": "MPA", + "aggregationMethod": "minimum", + "improvementDirection": "higher" + }, + { + "@id": "metrics:impact-resistance", + "@type": "skos:Concept", + "prefLabel": "Impact Resistance", + "definition": "Energy absorbed by a material or product before fracture under impact loading, in joules.", + "notation": "09.02", + "broader": "metrics:product-safety-and-quality", + "inScheme": "https://vocabulary.uncefact.org/performance-metrics/", + "scopeNote": "ISO 179 (Charpy impact, plastics); ISO 148 (Charpy impact, metals); IEC 62262 (IK rating, equipment enclosures).", + "allowedUnit": "JOU", + "aggregationMethod": "minimum", + "improvementDirection": "higher" + }, + { + "@id": "metrics:thermal-performance", + "@type": "skos:Concept", + "prefLabel": "Thermal Performance", + "definition": "Thermal resistance (R-value) or thermal conductivity of a material or assembly, indicating its ability to insulate against heat transfer.", + "notation": "09.03", + "broader": "metrics:product-safety-and-quality", + "inScheme": "https://vocabulary.uncefact.org/performance-metrics/", + "scopeNote": "ICC International Energy Conservation Code (IECC); ISO 22007; ASTM C518; EU Energy Performance of Buildings Directive.", + "allowedUnit": "C62", + "aggregationMethod": "weighted-average", + "improvementDirection": "higher" + }, + { + "@id": "metrics:fire-resistance-rating", + "@type": "skos:Concept", + "prefLabel": "Fire Resistance Rating", + "definition": "Duration a material or assembly maintains structural integrity, insulation, and limits heat transfer under standard fire exposure conditions, in minutes.", + "notation": "09.04", + "broader": "metrics:product-safety-and-quality", + "inScheme": "https://vocabulary.uncefact.org/performance-metrics/", + "scopeNote": "ICC IBC Chapter 7; ASTM E119; ISO 834; EU Construction Products Regulation (EN 13501).", + "allowedUnit": "MIN", + "aggregationMethod": "minimum", + "improvementDirection": "higher" + }, + { + "@id": "metrics:electrical-safety-rating", + "@type": "skos:Concept", + "prefLabel": "Electrical Safety Rating", + "definition": "Composite test result or classification for electrical insulation, shock protection, and fault tolerance under applicable safety standards.", + "notation": "09.05", + "broader": "metrics:product-safety-and-quality", + "inScheme": "https://vocabulary.uncefact.org/performance-metrics/", + "scopeNote": "IEC 60335 (household appliances); IEC 60601 (medical devices); IEC 62368 (AV/IT equipment); UL product safety standards.", + "allowedUnit": "C62", + "aggregationMethod": "minimum", + "improvementDirection": "higher" + }, + { + "@id": "metrics:flammability-rating", + "@type": "skos:Concept", + "prefLabel": "Flammability Rating", + "definition": "Classification of a material's reaction to fire, covering ignitability, flame spread, heat release, and smoke generation under standard test conditions.", + "notation": "09.06", + "broader": "metrics:product-safety-and-quality", + "inScheme": "https://vocabulary.uncefact.org/performance-metrics/", + "scopeNote": "EN 13501 (EU Euroclasses); UL 94 (plastics); ASTM E84 (surface burning); EU GPSR flammability requirements.", + "allowedUnit": "C62", + "aggregationMethod": "minimum", + "improvementDirection": "higher" + }, + { + "@id": "metrics:chemical-substance-concentration", + "@type": "skos:Concept", + "prefLabel": "Chemical Substance Concentration", + "definition": "Concentration of a specified regulated or restricted substance present in a product, in milligrams per kilogram.", + "notation": "09.07", + "broader": "metrics:product-safety-and-quality", + "inScheme": "https://vocabulary.uncefact.org/performance-metrics/", + "scopeNote": "EU REACH (SVHCs); EU RoHS Directive; EU ESPR Annex I; EU GPSR chemical safety requirements.", + "allowedUnit": "MK", + "aggregationMethod": "maximum", + "improvementDirection": "lower" + }, + { + "@id": "metrics:noise-emission-level", + "@type": "skos:Concept", + "prefLabel": "Noise Emission Level", + "definition": "Sound power or sound pressure level emitted by a product during normal operation, in decibels.", + "notation": "09.08", + "broader": "metrics:product-safety-and-quality", + "inScheme": "https://vocabulary.uncefact.org/performance-metrics/", + "scopeNote": "EU Outdoor Noise Directive 2000/14/EC; ISO 3744; IEC 60704 (household appliances); EU Energy Labelling Regulation.", + "allowedUnit": "C62", + "aggregationMethod": "maximum", + "improvementDirection": "lower" + }, + + { + "@id": "metrics:food-safety-and-quality", + "@type": "skos:Concept", + "prefLabel": "Food Safety and Quality", + "definition": "Metrics for measuring microbiological safety, chemical contaminant levels, pesticide and veterinary drug residues, food additive levels, nutritional content, and allergen presence in food products.", + "notation": "10", + "topConceptOf": "https://vocabulary.uncefact.org/performance-metrics/", + "inScheme": "https://vocabulary.uncefact.org/performance-metrics/", + "scopeNote": "Codex Alimentarius (FAO/WHO); EU General Food Law Regulation (EC) 178/2002; EU food safety regulations; ISO 22000.", + "narrower": [ + "metrics:microbiological-count", + "metrics:chemical-contaminant-level", + "metrics:pesticide-residue-level", + "metrics:veterinary-drug-residue-level", + "metrics:food-additive-level", + "metrics:nutritional-content", + "metrics:allergen-presence", + "metrics:shelf-life-duration" + ] + }, + { + "@id": "metrics:microbiological-count", + "@type": "skos:Concept", + "prefLabel": "Microbiological Count", + "definition": "Colony-forming units of a specified microorganism per unit of food, measuring microbiological safety and hygiene performance.", + "notation": "10.01", + "broader": "metrics:food-safety-and-quality", + "inScheme": "https://vocabulary.uncefact.org/performance-metrics/", + "scopeNote": "Codex CAC/GL 21; EU Regulation (EC) 2073/2005 on microbiological criteria for foodstuffs; ISO 4833.", + "allowedUnit": "C62", + "aggregationMethod": "maximum", + "improvementDirection": "lower" + }, + { + "@id": "metrics:chemical-contaminant-level", + "@type": "skos:Concept", + "prefLabel": "Chemical Contaminant Level", + "definition": "Concentration of a specified chemical contaminant (heavy metals, mycotoxins, dioxins, etc.) in food, in milligrams per kilogram.", + "notation": "10.02", + "broader": "metrics:food-safety-and-quality", + "inScheme": "https://vocabulary.uncefact.org/performance-metrics/", + "scopeNote": "Codex CXS 193 (General Standard for Contaminants and Toxins); EU Regulation (EC) 1881/2006.", + "allowedUnit": "MK", + "aggregationMethod": "maximum", + "improvementDirection": "lower" + }, + { + "@id": "metrics:pesticide-residue-level", + "@type": "skos:Concept", + "prefLabel": "Pesticide Residue Level", + "definition": "Concentration of a specified pesticide residue in food, measured against the applicable maximum residue limit, in milligrams per kilogram.", + "notation": "10.03", + "broader": "metrics:food-safety-and-quality", + "inScheme": "https://vocabulary.uncefact.org/performance-metrics/", + "scopeNote": "Codex Maximum Residue Limits for Pesticides (CX/MRL); EU Regulation (EC) 396/2005.", + "allowedUnit": "MK", + "aggregationMethod": "maximum", + "improvementDirection": "lower" + }, + { + "@id": "metrics:veterinary-drug-residue-level", + "@type": "skos:Concept", + "prefLabel": "Veterinary Drug Residue Level", + "definition": "Concentration of a specified veterinary drug residue in animal-derived food, measured against the applicable maximum residue limit, in micrograms per kilogram.", + "notation": "10.04", + "broader": "metrics:food-safety-and-quality", + "inScheme": "https://vocabulary.uncefact.org/performance-metrics/", + "scopeNote": "Codex Maximum Residue Limits for Veterinary Drugs (CX/MRL); EU Regulation (EU) 37/2010.", + "allowedUnit": "MK", + "aggregationMethod": "maximum", + "improvementDirection": "lower" + }, + { + "@id": "metrics:food-additive-level", + "@type": "skos:Concept", + "prefLabel": "Food Additive Level", + "definition": "Concentration of a specified food additive in the final product, measured against the applicable maximum permitted level, in milligrams per kilogram.", + "notation": "10.05", + "broader": "metrics:food-safety-and-quality", + "inScheme": "https://vocabulary.uncefact.org/performance-metrics/", + "scopeNote": "Codex General Standard for Food Additives (CXS 192); EU Regulation (EC) 1333/2008.", + "allowedUnit": "MK", + "aggregationMethod": "maximum", + "improvementDirection": "lower" + }, + { + "@id": "metrics:nutritional-content", + "@type": "skos:Concept", + "prefLabel": "Nutritional Content", + "definition": "Amount of a specified nutrient (energy, protein, fat, carbohydrate, sugar, sodium, fibre, vitamins, minerals) per standard serving or per 100 grams of food.", + "notation": "10.06", + "broader": "metrics:food-safety-and-quality", + "inScheme": "https://vocabulary.uncefact.org/performance-metrics/", + "scopeNote": "Codex CXS 1-1985 (General Standard for Labelling); Codex CXG 2-1985 (Nutrition Labelling Guidelines); EU Regulation (EU) 1169/2011.", + "allowedUnit": "GRM", + "aggregationMethod": "average", + "improvementDirection": "context-dependent" + }, + { + "@id": "metrics:allergen-presence", + "@type": "skos:Concept", + "prefLabel": "Allergen Presence", + "definition": "Declared presence or measured concentration of a specified allergen in a food product, supporting consumer safety and regulatory labelling requirements.", + "notation": "10.07", + "broader": "metrics:food-safety-and-quality", + "inScheme": "https://vocabulary.uncefact.org/performance-metrics/", + "scopeNote": "Codex CXS 1-1985 (allergen labelling); EU Regulation (EU) 1169/2011 Annex II; Codex CXA 4-1989 (allergen classification).", + "allowedUnit": "MK", + "aggregationMethod": "maximum", + "improvementDirection": "lower" + }, + { + "@id": "metrics:shelf-life-duration", + "@type": "skos:Concept", + "prefLabel": "Shelf Life Duration", + "definition": "Expected period during which a food product maintains safety and quality under stated storage conditions, in days.", + "notation": "10.08", + "broader": "metrics:food-safety-and-quality", + "inScheme": "https://vocabulary.uncefact.org/performance-metrics/", + "scopeNote": "Codex General Principles of Food Hygiene (CXC 1-1969); EU Regulation (EU) 1169/2011 (date marking); ISO 22000.", + "allowedUnit": "DAY", + "aggregationMethod": "minimum", + "improvementDirection": "higher" + } + ] +} diff --git a/tests/fixtures/upstream/v0.7.0/vocabularies/untp-ontology.jsonld b/tests/fixtures/upstream/v0.7.0/vocabularies/untp-ontology.jsonld new file mode 100644 index 0000000..d0054f6 --- /dev/null +++ b/tests/fixtures/upstream/v0.7.0/vocabularies/untp-ontology.jsonld @@ -0,0 +1,5046 @@ +{ + "@context": { + "untp": "https://vocabulary.uncefact.org/untp/", + "xsd": "http://www.w3.org/2001/XMLSchema#", + "rdf": "http://www.w3.org/1999/02/22-rdf-syntax-ns#", + "owl": "http://www.w3.org/2002/07/owl#", + "rdfs": "http://www.w3.org/2000/01/rdf-schema#", + "schema": "https://schema.org/", + "dcterms": "http://purl.org/dc/terms/", + "vann": "http://purl.org/vocab/vann/", + "foaf": "http://xmlns.com/foaf/0.1/" + }, + "@graph": [ + { + "@id": "https://vocabulary.uncefact.org/untp/", + "@type": "owl:Ontology", + "vann:preferredNamespacePrefix": "untp", + "vann:preferredNamespaceUri": "https://vocabulary.uncefact.org/untp/", + "dcterms:title": "UNTP Core Vocabulary", + "dcterms:description": "Core classes and properties for the UNTP data model (JSON-LD/RDF).", + "owl:versionInfo": "working" + }, + { + "@id": "untp:credentialSubjectType", + "@type": "rdf:Property", + "rdfs:comment": "The expected type of the credentialSubject for this credential class. Used to connect UNTP credential types to the UNTP domain classes that populate the W3C VCDM credentialSubject property, without redefining the W3C property itself.", + "rdfs:label": "credentialSubjectType", + "schema:domainIncludes": [ + { + "@id": "untp:DigitalProductPassport" + }, + { + "@id": "untp:DigitalFacilityRecord" + }, + { + "@id": "untp:DigitalConformityCredential" + }, + { + "@id": "untp:DigitalTraceabilityEvent" + }, + { + "@id": "untp:DigitalIdentityAnchor" + } + ], + "schema:rangeIncludes": { + "@id": "rdfs:Class" + } + }, + { + "@id": "untp:extendsModel", + "@type": "rdf:Property", + "rdfs:comment": "Indicates that this UNTP class reuses and extends a class defined in an external vocabulary (e.g. W3C VCDM, schema.org). The external class defines the envelope or base properties; UNTP defines only the extensions. This annotation enables human-readable renderings to display or link to the inherited properties without redefining them.", + "rdfs:label": "extendsModel", + "schema:domainIncludes": [ + { + "@id": "untp:VerifiableCredential" + }, + { + "@id": "untp:Address" + } + ], + "schema:rangeIncludes": { + "@id": "rdfs:Class" + } + }, + { + "@id": "untp:DigitalProductPassport", + "@type": "rdfs:Class", + "rdfs:comment": "A digital Product Passport (DPP) credential.", + "rdfs:label": "DigitalProductPassport", + "rdfs:subClassOf": "untp:VerifiableCredential", + "untp:credentialSubjectType": { + "@id": "untp:Product" + } + }, + { + "@id": "untp:VerifiableCredential", + "@type": "rdfs:Class", + "rdfs:comment": "A verifiable credential is a digital and verifiable version of everyday credentials such as certificates and licenses. It conforms to the W3C Verifiable Credentials Data Model v2.0 (VCDM).", + "rdfs:label": "VerifiableCredential", + "untp:extendsModel": { + "@id": "https://www.w3.org/2018/credentials#VerifiableCredential" + } + }, + { + "@id": "untp:DigitalFacilityRecord", + "@type": "rdfs:Class", + "rdfs:comment": "A digital Facility Record (DFR) credential.", + "rdfs:label": "DigitalFacilityRecord", + "rdfs:subClassOf": "untp:VerifiableCredential", + "untp:credentialSubjectType": { + "@id": "untp:Facility" + } + }, + { + "@id": "untp:CredentialIssuer", + "@type": "rdfs:Class", + "rdfs:comment": "The issuer party (person or organisation) of a verifiable credential.", + "rdfs:label": "CredentialIssuer" + }, + { + "@id": "untp:IssuingSoftware", + "@type": "rdfs:Class", + "rdfs:comment": "Optional metadata identifying the software product (and its vendor) that issued the parent credential. Used for vendor traceability and conformity testing.", + "rdfs:label": "IssuingSoftware" + }, + { + "@id": "untp:SoftwareVendor", + "@type": "rdfs:Class", + "rdfs:comment": "The vendor of a software product that issued a UNTP credential.", + "rdfs:label": "SoftwareVendor" + }, + { + "@id": "untp:Party", + "@type": "rdfs:Class", + "rdfs:comment": "An organisation. May be a supply chain actor, a certifier, a government agency.", + "rdfs:label": "Party" + }, + { + "@id": "untp:Entity", + "@type": "rdfs:Class", + "rdfs:comment": "A uniquely identified entity", + "rdfs:label": "Entity" + }, + { + "@id": "untp:IdentifierScheme", + "@type": "rdfs:Class", + "rdfs:comment": "An identifier registration scheme for products, facilities, or organisations. Typically operated by a state, national or global authority.", + "rdfs:label": "IdentifierScheme" + }, + { + "@id": "untp:Country", + "@type": "rdfs:Class", + "rdfs:comment": "Country Code and Name from ISO 3166", + "rdfs:label": "Country" + }, + { + "@id": "untp:Address", + "@type": "rdfs:Class", + "rdfs:comment": "A postal address. Reuses streetAddress, postalCode, addressLocality, and addressRegion from schema.org PostalAddress. Extends with addressCountry (an ISO-3166 country code/name structure).", + "rdfs:label": "Address", + "untp:extendsModel": { + "@id": "schema:PostalAddress" + } + }, + { + "@id": "untp:Classification", + "@type": "rdfs:Class", + "rdfs:comment": "A classification scheme and code / name representing a category value for a product, entity, or facility.", + "rdfs:label": "Classification" + }, + { + "@id": "untp:BitstringStatusListEntry", + "@type": "rdfs:Class", + "rdfs:comment": "A privacy-preserving, space-efficient, and high-performance mechanism for publishing status information such as suspension or revocation of Verifiable Credentials through use of bitstrings. See https://www.w3.org/TR/vc-bitstring-status-list/ for full details.", + "rdfs:label": "BitstringStatusListEntry" + }, + { + "@id": "untp:RenderTemplate2024", + "@type": "rdfs:Class", + "rdfs:comment": "A single template format focused render method where the content/media type decision becomes secondary (and is expressed separately).See https://github.com/w3c-ccg/vc-render-method/issues/9", + "rdfs:label": "RenderTemplate2024" + }, + { + "@id": "untp:Facility", + "@type": "rdfs:Class", + "rdfs:comment": "The physical site (eg farm or factory) where the product or materials was produced.", + "rdfs:label": "Facility" + }, + { + "@id": "untp:PartyRole", + "@type": "rdfs:Class", + "rdfs:comment": "A party with a defined relationship to the referencing entity", + "rdfs:label": "PartyRole" + }, + { + "@id": "untp:Link", + "@type": "rdfs:Class", + "rdfs:comment": "A structure to provide a URL link plus metadata associated with the link.", + "rdfs:label": "Link" + }, + { + "@id": "untp:Location", + "@type": "rdfs:Class", + "rdfs:comment": "Location information including address and geo-location of points, areas, and boundaries. At least one of plusCode, geoLocation, or geoBoundary are required.", + "rdfs:label": "Location" + }, + { + "@id": "untp:Coordinate", + "@type": "rdfs:Class", + "rdfs:comment": "A geographic point defined by latitude and longitude using the WGS84 geodetic coordinate reference system (EPSG:4326). Latitude and longitude are expressed in decimal degrees as floating-point numbers. Coordinates follow the conventional order (latitude, longitude) and represent a point on the Earth’s surface.", + "rdfs:label": "Coordinate" + }, + { + "@id": "untp:MaterialUsage", + "@type": "rdfs:Class", + "rdfs:comment": "A material usage record defining the consumption of materials for a given period, typically at an operating facility. Used to specify volumetric consumption and country of origin without specifying specific suppliers.", + "rdfs:label": "MaterialUsage" + }, + { + "@id": "untp:Period", + "@type": "rdfs:Class", + "rdfs:comment": "A period of time, typically a month, quarter or a year, which defines the context boundary for reported facts.", + "rdfs:label": "Period" + }, + { + "@id": "untp:Material", + "@type": "rdfs:Class", + "rdfs:comment": "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.", + "rdfs:label": "Material" + }, + { + "@id": "untp:Measure", + "@type": "rdfs:Class", + "rdfs:comment": "The measure class defines a numeric measured value (eg 10) and a coded unit of measure (eg KG). There is an optional upper and lower tolerance which can be used to specify uncertainty in the measure. ", + "rdfs:label": "Measure" + }, + { + "@id": "untp:Image", + "@type": "rdfs:Class", + "rdfs:comment": "A binary image encoded as base64 text and embedded into the data. Use this for small images like certification trust marks or regulated labels. Large images should be external links.", + "rdfs:label": "Image" + }, + { + "@id": "untp:Claim", + "@type": "rdfs:Class", + "rdfs:comment": "A performance claim about a product, facility, or organisation that is made against a well defined criterion.", + "rdfs:label": "Claim" + }, + { + "@id": "untp:Criterion", + "@type": "rdfs:Class", + "rdfs:comment": "A specific rule or criterion within a standard or regulation. eg a carbon intensity calculation rule within an emissions standard.", + "rdfs:label": "Criterion" + }, + { + "@id": "untp:ConformityTopic", + "@type": "rdfs:Class", + "rdfs:comment": "The UNTP standard classification scheme for conformity topic. see http://vocabulary.uncefact.org/ConformityTopic", + "rdfs:label": "ConformityTopic" + }, + { + "@id": "untp:Performance", + "@type": "rdfs:Class", + "rdfs:comment": "A claimed, assessed, or required performance level defined either by a scoring system or a numeric measure.", + "rdfs:label": "Performance" + }, + { + "@id": "untp:PerformanceMetric", + "@type": "rdfs:Class", + "rdfs:comment": "A standardised data point for performance reporting (eg product carbon footprint)", + "rdfs:label": "PerformanceMetric" + }, + { + "@id": "untp:Score", + "@type": "rdfs:Class", + "rdfs:comment": "A single score within a scoring framework. ", + "rdfs:label": "Score" + }, + { + "@id": "untp:Regulation", + "@type": "rdfs:Class", + "rdfs:comment": "A regulation (eg EU deforestation regulation) that defines the criteria for assessment.", + "rdfs:label": "Regulation" + }, + { + "@id": "untp:Standard", + "@type": "rdfs:Class", + "rdfs:comment": "A standard (eg ISO 14000) that specifies the criteria for conformance.", + "rdfs:label": "Standard" + }, + { + "@id": "untp:DigitalConformityCredential", + "@type": "rdfs:Class", + "rdfs:comment": "A Digital Conformity Credential (DCC) credential.", + "rdfs:label": "DigitalConformityCredential", + "rdfs:subClassOf": "untp:VerifiableCredential", + "untp:credentialSubjectType": { + "@id": "untp:ConformityAttestation" + } + }, + { + "@id": "untp:ConformityAttestation", + "@type": "rdfs:Class", + "rdfs:comment": "A conformity attestation issued by a competent body that defines one or more assessments (eg carbon intensity) about a product (eg battery) against a specification (eg LCA method) defined in a standard or regulation.", + "rdfs:label": "ConformityAttestation" + }, + { + "@id": "untp:Endorsement", + "@type": "rdfs:Class", + "rdfs:comment": "The authority under which a conformity claim is issued. For example a national accreditation authority may authorise a test lab to issue test certificates about a product against a standard. ", + "rdfs:label": "Endorsement" + }, + { + "@id": "untp:ConformityScheme", + "@type": "rdfs:Class", + "rdfs:comment": "A formal governance scheme under which an attestation is issued (eg ACRS structural steel certification) ", + "rdfs:label": "ConformityScheme" + }, + { + "@id": "untp:ScoringFramework", + "@type": "rdfs:Class", + "rdfs:comment": "A scoring framework used for performance level assessments against a criteria or scheme. For example forced labour performance might score A to D depending on the percentage of workforce subject to recruitment fees.", + "rdfs:label": "ScoringFramework" + }, + { + "@id": "untp:ConformityProfile", + "@type": "rdfs:Class", + "rdfs:comment": "A versioned conformity profile, managed under a scheme, which includes a specific list of versioned criteria. A conformity profile represents the precise scope of a conformity attestation. ", + "rdfs:label": "ConformityProfile" + }, + { + "@id": "untp:StandardAlignment", + "@type": "rdfs:Class", + "rdfs:comment": "A voluntary standard and an alignment level (exceeds, meets, partial).", + "rdfs:label": "StandardAlignment" + }, + { + "@id": "untp:RegulatoryAlignment", + "@type": "rdfs:Class", + "rdfs:comment": "A national regulation or international treaty and an alignment level (exceeds, meets, partial).", + "rdfs:label": "RegulatoryAlignment" + }, + { + "@id": "untp:ConformityAssessment", + "@type": "rdfs:Class", + "rdfs:comment": "A specific assessment about the product or facility against a specific specification. Eg the carbon intensity of a given product or batch.", + "rdfs:label": "ConformityAssessment" + }, + { + "@id": "untp:ProductVerification", + "@type": "rdfs:Class", + "rdfs:comment": "The product which is the subject of this conformity assessment", + "rdfs:label": "ProductVerification" + }, + { + "@id": "untp:Product", + "@type": "rdfs:Class", + "rdfs:comment": "The ProductInformation class encapsulates detailed information regarding a specific product, including its identification details, manufacturer, and other pertinent details.", + "rdfs:label": "Product" + }, + { + "@id": "untp:Characteristics", + "@type": "rdfs:Class", + "rdfs:comment": "A declaration of conformance with one or more criteria from a specific standard or regulation. ", + "rdfs:label": "Characteristics" + }, + { + "@id": "untp:Dimension", + "@type": "rdfs:Class", + "rdfs:comment": "Overall (length, width, height) dimensions and weight/volume of an item.", + "rdfs:label": "Dimension" + }, + { + "@id": "untp:Package", + "@type": "rdfs:Class", + "rdfs:comment": "Details of product packaging", + "rdfs:label": "Package" + }, + { + "@id": "untp:FacilityVerification", + "@type": "rdfs:Class", + "rdfs:comment": "The facility which is the subject of this conformity assessment", + "rdfs:label": "FacilityVerification" + }, + { + "@id": "untp:DigitalTraceabilityEvent", + "@type": "rdfs:Class", + "rdfs:comment": "A Digital Traceability Event (DTE) credential.", + "rdfs:label": "DigitalTraceabilityEvent", + "rdfs:subClassOf": "untp:VerifiableCredential", + "untp:credentialSubjectType": { + "@id": "untp:LifecycleEvent" + } + }, + { + "@id": "untp:LifecycleEvent", + "@type": "rdfs:Class", + "rdfs:comment": "This abstract event structure provides a common language to describe product lifecycle events such as shipments, inspections, manufacturing processes, etc.", + "rdfs:label": "LifecycleEvent" + }, + { + "@id": "untp:MakeEvent", + "@type": "rdfs:Class", + "rdfs:comment": "Transformation (manufacture/ production) of input products to output products at a given facility.", + "rdfs:label": "MakeEvent", + "rdfs:subClassOf": "untp:LifecycleEvent" + }, + { + "@id": "untp:SensorData", + "@type": "rdfs:Class", + "rdfs:comment": "A sensor data recording associated with this event", + "rdfs:label": "SensorData" + }, + { + "@id": "untp:EventProduct", + "@type": "rdfs:Class", + "rdfs:comment": "A quantity of products or materials involved in a lifecycle event.", + "rdfs:label": "EventProduct" + }, + { + "@id": "untp:MoveEvent", + "@type": "rdfs:Class", + "rdfs:comment": "Transfer (shipment) of products from one facility to another.", + "rdfs:label": "MoveEvent", + "rdfs:subClassOf": "untp:LifecycleEvent" + }, + { + "@id": "untp:ModifyEvent", + "@type": "rdfs:Class", + "rdfs:comment": "Intervention (eg repair) on a product without changing it's identity at a given facility.", + "rdfs:label": "ModifyEvent", + "rdfs:subClassOf": "untp:LifecycleEvent" + }, + { + "@id": "untp:DigitalIdentityAnchor", + "@type": "rdfs:Class", + "rdfs:comment": "The Digital Identity Anchor (DIA) is a very simple credential that is issued by a trusted authority and asserts an equivalence between a member identity as known to the authority (eg a VAT number) and one or more decentralised identifiers (DIDs) held by the member.", + "rdfs:label": "DigitalIdentityAnchor", + "rdfs:subClassOf": "untp:VerifiableCredential", + "untp:credentialSubjectType": { + "@id": "untp:RegisteredIdentity" + } + }, + { + "@id": "untp:RegisteredIdentity", + "@type": "rdfs:Class", + "rdfs:comment": "The identity anchor is a mapping between a registry member identity and one or more decentralised identifiers owned by the member. It may also list a set of membership scopes.", + "rdfs:label": "RegisteredIdentity" + }, + { + "@id": "untp:id", + "schema:rangeIncludes": { + "@id": "xsd:anyURI" + }, + "@type": [ + "rdf:Property", + "owl:DatatypeProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:CredentialIssuer" + }, + { + "@id": "untp:Party" + }, + { + "@id": "untp:Entity" + }, + { + "@id": "untp:IdentifierScheme" + }, + { + "@id": "untp:BitstringStatusListEntry" + }, + { + "@id": "untp:Facility" + }, + { + "@id": "untp:Claim" + }, + { + "@id": "untp:Criterion" + }, + { + "@id": "untp:ConformityTopic" + }, + { + "@id": "untp:PerformanceMetric" + }, + { + "@id": "untp:Regulation" + }, + { + "@id": "untp:Standard" + }, + { + "@id": "untp:ConformityAttestation" + }, + { + "@id": "untp:ConformityScheme" + }, + { + "@id": "untp:ConformityProfile" + }, + { + "@id": "untp:ConformityAssessment" + }, + { + "@id": "untp:Product" + }, + { + "@id": "untp:LifecycleEvent" + }, + { + "@id": "untp:MakeEvent" + }, + { + "@id": "untp:MoveEvent" + }, + { + "@id": "untp:ModifyEvent" + }, + { + "@id": "untp:RegisteredIdentity" + } + ], + "rdfs:comment": [ + "The W3C DID of the issuer - should be a did:web or did:webvh", + "Globally unique identifier of this party. Typically represented as a URI identifierScheme/Identifier URI", + "The globally unique identifier of this entity. ", + "The URI of this identifier scheme", + "optional identifier of this status list entry.", + "Globally unique identifier of this facility. Typically represented as a URI identifierScheme/Identifier URI", + "Globally unique identifier of this claim. Typically represented as a URI companyURL/claimID URI or a UUID", + "Globally unique identifier of this conformity criterion. Typically represented as a URI SchemeOwner/CriterionID URI", + "The unique identifier for this conformity topic", + "Globally unique identifier of this reporting metric. ", + "Globally unique identifier of this standard. Typically represented as a URI government/regulation URI", + "Globally unique identifier of this standard. Typically represented as a URI issuer/standard URI", + "Globally unique identifier of this attestation. Typically represented as a URI AssessmentBody/CertificateID URI or a UUID", + "Globally unique identifier of this conformity scheme. Typically represented as a URI SchemeOwner/SchemeName URI", + "Globally unique identifier of this context specific conformity profile. Typically represented as a URI SchemeOwner/profileID URI", + "Globally unique identifier of this assessment. Typically represented as a URI AssessmentBody/Assessment URI or a UUID", + "Globally unique identifier of this product. Typically represented as a URI identifierScheme/Identifier URI or, if self-issued, as a did.", + "Globally unique ID for this lifecycle event. Should be a URI. Can be a UUID.", + "Globally unique ID for this lifecycle event. Should be a URI. Can be a UUID.", + "Globally unique ID for this lifecycle event. Should be a URI. Can be a UUID.", + "Globally unique ID for this lifecycle event. Should be a URI. Can be a UUID.", + "The DID that is controlled by the registered member and is linked to the registeredID through this Identity Anchor credential" + ], + "rdfs:label": "id" + }, + { + "@id": "untp:name", + "schema:rangeIncludes": { + "@id": "xsd:string" + }, + "@type": [ + "rdf:Property", + "owl:DatatypeProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:CredentialIssuer" + }, + { + "@id": "untp:Party" + }, + { + "@id": "untp:Entity" + }, + { + "@id": "untp:IdentifierScheme" + }, + { + "@id": "untp:Classification" + }, + { + "@id": "untp:RenderTemplate2024" + }, + { + "@id": "untp:Facility" + }, + { + "@id": "untp:Material" + }, + { + "@id": "untp:Image" + }, + { + "@id": "untp:Claim" + }, + { + "@id": "untp:Criterion" + }, + { + "@id": "untp:ConformityTopic" + }, + { + "@id": "untp:PerformanceMetric" + }, + { + "@id": "untp:Regulation" + }, + { + "@id": "untp:Standard" + }, + { + "@id": "untp:ConformityAttestation" + }, + { + "@id": "untp:Endorsement" + }, + { + "@id": "untp:ConformityScheme" + }, + { + "@id": "untp:ScoringFramework" + }, + { + "@id": "untp:ConformityProfile" + }, + { + "@id": "untp:ConformityAssessment" + }, + { + "@id": "untp:Product" + }, + { + "@id": "untp:LifecycleEvent" + }, + { + "@id": "untp:MakeEvent" + }, + { + "@id": "untp:MoveEvent" + }, + { + "@id": "untp:ModifyEvent" + } + ], + "rdfs:comment": [ + "The name of the issuer person or organisation", + "Legal registered name of this party.", + "The name of this entity.", + "The name of the identifier scheme. ", + "Name of the classification represented by the code", + "Human facing display name for selection", + "Name of this facility as defined the location register.", + "Name of this material (eg \"Egyptian Cotton\")", + "the display name for this image", + "Name of this claim - typically similar or the same as the referenced criterion name.", + "Name of this criterion as defined by the scheme owner.", + "The human readable name for this conformity topic.", + "A human readable name for this metric (for example \"water usage per Kg of material\")", + "Name of this regulation as defined by the regulator.", + "Name for this standard", + "Name of this attestation - typically the title of the certificate.", + "The name of the accreditation.", + "Name of this scheme as defined by the scheme owner.", + "A name for this scoring framework. Must be unique within a scheme.", + "Name of this conformity profile as defined by the scheme owner.", + "Name of this assessment - typically similar or the same as the referenced criterion name.", + "The product name as known to the market.", + "The name for this lifecycle event ", + "The name for this lifecycle event ", + "The name for this lifecycle event ", + "The name for this lifecycle event " + ], + "rdfs:label": "name" + }, + { + "@id": "untp:issuerAlsoKnownAs", + "schema:rangeIncludes": { + "@id": "untp:Party" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:CredentialIssuer" + } + ], + "rdfs:comment": [ + "An optional list of other registered identifiers for this credential issuer " + ], + "rdfs:label": "issuerAlsoKnownAs" + }, + { + "@id": "untp:issuingSoftware", + "schema:rangeIncludes": { + "@id": "untp:IssuingSoftware" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:DigitalProductPassport" + }, + { + "@id": "untp:DigitalConformityCredential" + }, + { + "@id": "untp:DigitalFacilityRecord" + }, + { + "@id": "untp:DigitalIdentityAnchor" + }, + { + "@id": "untp:DigitalTraceabilityEvent" + } + ], + "rdfs:comment": [ + "Optional metadata identifying the software product (and its vendor) that issued this credential." + ], + "rdfs:label": "issuingSoftware" + }, + { + "@id": "untp:vendor", + "schema:rangeIncludes": { + "@id": "untp:SoftwareVendor" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:IssuingSoftware" + } + ], + "rdfs:comment": [ + "The vendor of the software product that issued the parent credential." + ], + "rdfs:label": "vendor" + }, + { + "@id": "untp:description", + "schema:rangeIncludes": { + "@id": "xsd:string" + }, + "@type": [ + "rdf:Property", + "owl:DatatypeProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Party" + }, + { + "@id": "untp:Entity" + }, + { + "@id": "untp:Facility" + }, + { + "@id": "untp:Image" + }, + { + "@id": "untp:Claim" + }, + { + "@id": "untp:Criterion" + }, + { + "@id": "untp:Regulation" + }, + { + "@id": "untp:Standard" + }, + { + "@id": "untp:ConformityAttestation" + }, + { + "@id": "untp:ConformityScheme" + }, + { + "@id": "untp:ScoringFramework" + }, + { + "@id": "untp:ConformityProfile" + }, + { + "@id": "untp:ConformityAssessment" + }, + { + "@id": "untp:Product" + }, + { + "@id": "untp:Package" + }, + { + "@id": "untp:LifecycleEvent" + }, + { + "@id": "untp:MakeEvent" + }, + { + "@id": "untp:MoveEvent" + }, + { + "@id": "untp:ModifyEvent" + }, + { + "@id": "untp:PerformanceMetric" + } + ], + "rdfs:comment": [ + "Description of the party including function and other names.", + "A rich descrition of this identified entity. ", + "Description of the facility including function and other names.", + "The detailed description / supporting information for this image.", + "Description of this conformity claim", + "Description of this criterion", + "Description of this regulation.", + "Description of this standard.", + "Description of this attestation.", + "Description of this conformity scheme", + "A full text description of the criterion that clearly specifies how compliance is achieved and measured. ", + "The description of this versioned and context specific conformity profile.", + "Description of this conformity assessment ", + "Description of the product.", + "Description of the packaging.", + "The description of this lifecycle event.", + "The description of this lifecycle event.", + "The description of this lifecycle event.", + "The description of this lifecycle event.", + "A rich description of this reporting metric." + ], + "rdfs:label": "description" + }, + { + "@id": "untp:registeredId", + "schema:rangeIncludes": { + "@id": "xsd:string" + }, + "@type": [ + "rdf:Property", + "owl:DatatypeProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Party" + }, + { + "@id": "untp:Facility" + }, + { + "@id": "untp:RegisteredIdentity" + } + ], + "rdfs:comment": [ + "The registration number (alphanumeric) of the Party within the register. Unique within the register.", + "The registration number (alphanumeric) of the facility within the identifier scheme. Unique within the register.", + "The registration number (alphanumeric) of the entity within the register. Unique within the register." + ], + "rdfs:label": "registeredId" + }, + { + "@id": "untp:idScheme", + "schema:rangeIncludes": { + "@id": "untp:IdentifierScheme" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Party" + }, + { + "@id": "untp:Facility" + }, + { + "@id": "untp:Product" + }, + { + "@id": "untp:RegisteredIdentity" + } + ], + "rdfs:comment": [ + "The identifier scheme of the party. Typically a national business register or a global scheme such as GLEIF. ", + "The ID scheme of the facility. eg a GS1 GLN or a National land registry scheme. If self issued then use the party ID of the facility owner. ", + "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. ", + "The identifier scheme for this registered entity ID." + ], + "rdfs:label": "idScheme" + }, + { + "@id": "untp:registrationCountry", + "schema:rangeIncludes": { + "@id": "untp:Country" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Party" + } + ], + "rdfs:comment": [ + "the country in which this organisation is registered - using ISO-3166 code and name." + ], + "rdfs:label": "registrationCountry" + }, + { + "@id": "untp:partyAddress", + "schema:rangeIncludes": { + "@id": "untp:Address" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Party" + } + ], + "rdfs:comment": [ + "The address of the party" + ], + "rdfs:label": "partyAddress" + }, + { + "@id": "untp:organisationWebsite", + "schema:rangeIncludes": { + "@id": "xsd:anyURI" + }, + "@type": [ + "rdf:Property", + "owl:DatatypeProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Party" + } + ], + "rdfs:comment": [ + "Website for this organisation" + ], + "rdfs:label": "organisationWebsite" + }, + { + "@id": "untp:industryCategory", + "schema:rangeIncludes": { + "@id": "untp:Classification" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Party" + } + ], + "rdfs:comment": [ + "The industry categories for this organisation. Recommend use of UNCPC as the category scheme. for example - unstats.un.org/isic/1030" + ], + "rdfs:label": "industryCategory" + }, + { + "@id": "untp:partyAlsoKnownAs", + "schema:rangeIncludes": { + "@id": "untp:Party" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Party" + } + ], + "rdfs:comment": [ + "An optional list of other registered identifiers for this organisation. For example DUNS, GLN, LEI, etc" + ], + "rdfs:label": "partyAlsoKnownAs" + }, + { + "@id": "untp:countryCode", + "schema:rangeIncludes": { + "@id": "untp:CountryCode" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Country" + } + ], + "rdfs:comment": [ + "ISO 3166 country code" + ], + "rdfs:label": "countryCode" + }, + { + "@id": "untp:countryName", + "schema:rangeIncludes": { + "@id": "xsd:string" + }, + "@type": [ + "rdf:Property", + "owl:DatatypeProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Country" + } + ], + "rdfs:comment": [ + "Country Name as defined in ISO 3166" + ], + "rdfs:label": "countryName" + }, + { + "@id": "untp:addressCountry", + "schema:rangeIncludes": { + "@id": "untp:Country" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Address" + } + ], + "rdfs:comment": [ + "The address country as an ISO-3166 two letter country code and name." + ], + "rdfs:label": "addressCountry" + }, + { + "@id": "untp:code", + "schema:rangeIncludes": { + "@id": "xsd:string" + }, + "@type": [ + "rdf:Property", + "owl:DatatypeProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Classification" + }, + { + "@id": "untp:Score" + } + ], + "rdfs:comment": [ + "classification code within the scheme", + "The coded value for this score (eg \"AAA\")" + ], + "rdfs:label": "code" + }, + { + "@id": "untp:definition", + "schema:rangeIncludes": { + "@id": "xsd:string" + }, + "@type": [ + "rdf:Property", + "owl:DatatypeProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Classification" + }, + { + "@id": "untp:ConformityTopic" + }, + { + "@id": "untp:Score" + } + ], + "rdfs:comment": [ + "A rich definition of this classification code.", + "The rich definition of this conformity topic.", + "A description of the meaning of this score." + ], + "rdfs:label": "definition" + }, + { + "@id": "untp:schemeId", + "schema:rangeIncludes": { + "@id": "xsd:anyURI" + }, + "@type": [ + "rdf:Property", + "owl:DatatypeProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Classification" + } + ], + "rdfs:comment": [ + "Classification scheme ID" + ], + "rdfs:label": "schemeId" + }, + { + "@id": "untp:schemeName", + "schema:rangeIncludes": { + "@id": "xsd:string" + }, + "@type": [ + "rdf:Property", + "owl:DatatypeProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Classification" + } + ], + "rdfs:comment": [ + "The name of the classification scheme" + ], + "rdfs:label": "schemeName" + }, + { + "@id": "untp:type", + "schema:rangeIncludes": { + "@id": "xsd:string" + }, + "@type": [ + "rdf:Property", + "owl:DatatypeProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:BitstringStatusListEntry" + } + ], + "rdfs:comment": [ + "The type of status list - must be set to \"The type property MUST be BitstringStatusListEntry.\"" + ], + "rdfs:label": "type" + }, + { + "@id": "untp:statusPurpose", + "schema:rangeIncludes": { + "@id": "untp:CredentialStatus" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:BitstringStatusListEntry" + } + ], + "rdfs:comment": [ + "Status purpose drawn from a standard list but extensible as per w3c bitstring status list specification." + ], + "rdfs:label": "statusPurpose" + }, + { + "@id": "untp:statusListIndex", + "schema:rangeIncludes": { + "@id": "xsd:integer" + }, + "@type": [ + "rdf:Property", + "owl:DatatypeProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:BitstringStatusListEntry" + } + ], + "rdfs:comment": [ + "\tThe statusListIndex property MUST be an arbitrary size integer greater than or equal to 0, expressed as a string in base 10. The value identifies the position of the status of the verifiable credential." + ], + "rdfs:label": "statusListIndex" + }, + { + "@id": "untp:statusListCredential", + "schema:rangeIncludes": { + "@id": "xsd:anyURI" + }, + "@type": [ + "rdf:Property", + "owl:DatatypeProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:BitstringStatusListEntry" + } + ], + "rdfs:comment": [ + "The statusListCredential property MUST be a URL to a verifiable credential. When the URL is dereferenced, the resulting verifiable credential MUST have type property that includes the BitstringStatusListCredential value." + ], + "rdfs:label": "statusListCredential" + }, + { + "@id": "untp:mediaQuery", + "schema:rangeIncludes": { + "@id": "xsd:string" + }, + "@type": [ + "rdf:Property", + "owl:DatatypeProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:RenderTemplate2024" + } + ], + "rdfs:comment": [ + "Media query as defined in https://www.w3.org/TR/mediaqueries-4/" + ], + "rdfs:label": "mediaQuery" + }, + { + "@id": "untp:template", + "schema:rangeIncludes": { + "@id": "xsd:string" + }, + "@type": [ + "rdf:Property", + "owl:DatatypeProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:RenderTemplate2024" + } + ], + "rdfs:comment": [ + "An inline template field for use cases where remote retrieval of a render method is suboptimal" + ], + "rdfs:label": "template" + }, + { + "@id": "untp:url", + "schema:rangeIncludes": { + "@id": "xsd:anyURI" + }, + "@type": [ + "rdf:Property", + "owl:DatatypeProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:RenderTemplate2024" + } + ], + "rdfs:comment": [ + "URL for remotely hosted template" + ], + "rdfs:label": "url" + }, + { + "@id": "untp:mediaType", + "schema:rangeIncludes": { + "@id": "xsd:string" + }, + "@type": [ + "rdf:Property", + "owl:DatatypeProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:RenderTemplate2024" + }, + { + "@id": "untp:Link" + }, + { + "@id": "untp:Image" + } + ], + "rdfs:comment": [ + "media type of the rendered output (eg text/html)", + "The media type of the target resource.", + "The media type of this image (eg image/png)" + ], + "rdfs:label": "mediaType" + }, + { + "@id": "untp:digestMultibase", + "schema:rangeIncludes": { + "@id": "xsd:string" + }, + "@type": [ + "rdf:Property", + "owl:DatatypeProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:RenderTemplate2024" + }, + { + "@id": "untp:Link" + } + ], + "rdfs:comment": [ + "Used for resource integrity and/or validation of the inline `template`", + "An optional multi-base encoded digest to ensure the content of the link has not changed. See https://www.w3.org/TR/vc-data-integrity/#resource-integrity for more information." + ], + "rdfs:label": "digestMultibase" + }, + { + "@id": "untp:countryOfOperation", + "schema:rangeIncludes": { + "@id": "untp:Country" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Facility" + } + ], + "rdfs:comment": [ + "The country in which this facility is operating.using ISO-3166 code and name." + ], + "rdfs:label": "countryOfOperation" + }, + { + "@id": "untp:processCategory", + "schema:rangeIncludes": { + "@id": "untp:Classification" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Facility" + } + ], + "rdfs:comment": [ + "The industrial or production processes performed by this facility. Example unstats.un.org/isic/1030." + ], + "rdfs:label": "processCategory" + }, + { + "@id": "untp:relatedParty", + "schema:rangeIncludes": { + "@id": "untp:PartyRole" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Facility" + }, + { + "@id": "untp:Product" + }, + { + "@id": "untp:LifecycleEvent" + }, + { + "@id": "untp:MakeEvent" + }, + { + "@id": "untp:MoveEvent" + }, + { + "@id": "untp:ModifyEvent" + } + ], + "rdfs:comment": [ + "A list of parties with a specified role relationship to this facility ", + "A list of parties with a defined relationship to this product", + "Any related parties and their roles involved in this event (eg the carrier for a shipment event)", + "Any related parties and their roles involved in this event (eg the carrier for a shipment event)", + "Any related parties and their roles involved in this event (eg the carrier for a shipment event)", + "Any related parties and their roles involved in this event (eg the carrier for a shipment event)" + ], + "rdfs:label": "relatedParty" + }, + { + "@id": "untp:relatedDocument", + "schema:rangeIncludes": { + "@id": "untp:Link" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Facility" + }, + { + "@id": "untp:Product" + }, + { + "@id": "untp:LifecycleEvent" + }, + { + "@id": "untp:MakeEvent" + }, + { + "@id": "untp:MoveEvent" + }, + { + "@id": "untp:ModifyEvent" + } + ], + "rdfs:comment": [ + "A list of links to documents providing additional facility information. Documents that support a conformity claim (e.g. permits or certificates) SHOULD be referenced as claim evidence rather than here.", + "A list of links to documents providing additional product information. Documents that support a conformity claim (e.g. permits or certificates) SHOULD be referenced as claim evidence rather than here.", + "A list of links to documentary evidence that supports this event. ", + "A list of links to documentary evidence that supports this event. ", + "A list of links to documentary evidence that supports this event. ", + "A list of links to documentary evidence that supports this event. " + ], + "rdfs:label": "relatedDocument" + }, + { + "@id": "untp:facilityAlsoKnownAs", + "schema:rangeIncludes": { + "@id": "untp:Facility" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Facility" + } + ], + "rdfs:comment": [ + "An optional list of other registered identifiers for this facility - eg GLNs or other schemes." + ], + "rdfs:label": "facilityAlsoKnownAs" + }, + { + "@id": "untp:locationInformation", + "schema:rangeIncludes": { + "@id": "untp:Location" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Facility" + } + ], + "rdfs:comment": [ + "Geo-location information for this facility as a resolvable geographic area (a Plus Code), and/or a geo-located point (latitude / longitude), and/or a defined boundary (GeoJSON Polygon)." + ], + "rdfs:label": "locationInformation" + }, + { + "@id": "untp:address", + "schema:rangeIncludes": { + "@id": "untp:Address" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Facility" + } + ], + "rdfs:comment": [ + "The Postal address of the location." + ], + "rdfs:label": "address" + }, + { + "@id": "untp:materialUsage", + "schema:rangeIncludes": { + "@id": "untp:MaterialUsage" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Facility" + } + ], + "rdfs:comment": [ + "The type and provenance of materials consumed by the facility during the reporting period. " + ], + "rdfs:label": "materialUsage" + }, + { + "@id": "untp:performanceClaim", + "schema:rangeIncludes": { + "@id": "untp:Claim" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Facility" + }, + { + "@id": "untp:Product" + }, + { + "@id": "untp:Package" + } + ], + "rdfs:comment": [ + "A list of performance claims (eg deforestation status) for this facility.", + "A list of performance claims (eg emissions intensity) for this product.", + "conformity claims made about the packaging." + ], + "rdfs:label": "performanceClaim" + }, + { + "@id": "untp:role", + "schema:rangeIncludes": { + "@id": "untp:PartyRole" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:PartyRole" + } + ], + "rdfs:comment": [ + "The role played by the party in this relationship" + ], + "rdfs:label": "role" + }, + { + "@id": "untp:party", + "schema:rangeIncludes": { + "@id": "untp:Party" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:PartyRole" + } + ], + "rdfs:comment": [ + "The party that has the specified role." + ], + "rdfs:label": "party" + }, + { + "@id": "untp:linkURL", + "schema:rangeIncludes": { + "@id": "xsd:anyURI" + }, + "@type": [ + "rdf:Property", + "owl:DatatypeProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Link" + } + ], + "rdfs:comment": [ + "The URL of the target resource. " + ], + "rdfs:label": "linkURL" + }, + { + "@id": "untp:linkName", + "schema:rangeIncludes": { + "@id": "xsd:string" + }, + "@type": [ + "rdf:Property", + "owl:DatatypeProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Link" + } + ], + "rdfs:comment": [ + "Display name for this link." + ], + "rdfs:label": "linkName" + }, + { + "@id": "untp:linkType", + "schema:rangeIncludes": { + "@id": "xsd:string" + }, + "@type": [ + "rdf:Property", + "owl:DatatypeProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Link" + } + ], + "rdfs:comment": [ + "The type of the target resource - drawn from a controlled vocabulary " + ], + "rdfs:label": "linkType" + }, + { + "@id": "untp:plusCode", + "schema:rangeIncludes": { + "@id": "xsd:anyURI" + }, + "@type": [ + "rdf:Property", + "owl:DatatypeProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Location" + } + ], + "rdfs:comment": [ + "An open location code (https://maps.google.com/pluscodes/) representing this geographic location or region. Open location codes can represent any sized area from a point to a large region and are easily resolved to a visual map location. " + ], + "rdfs:label": "plusCode" + }, + { + "@id": "untp:geoLocation", + "schema:rangeIncludes": { + "@id": "untp:Coordinate" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Location" + }, + { + "@id": "untp:SensorData" + } + ], + "rdfs:comment": [ + "The latitude and longitude coordinates that best represent the specified location. ", + "The geolocation of this sensor data recording event." + ], + "rdfs:label": "geoLocation" + }, + { + "@id": "untp:geoBoundary", + "schema:rangeIncludes": { + "@id": "untp:Coordinate" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Location" + } + ], + "rdfs:comment": [ + "The list of ordered coordinates that define a closed area polygon as a location boundary. The first and last coordinates in the array must match - thereby defining a closed boundary." + ], + "rdfs:label": "geoBoundary" + }, + { + "@id": "untp:latitude", + "schema:rangeIncludes": { + "@id": "xsd:double" + }, + "@type": [ + "rdf:Property", + "owl:DatatypeProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Coordinate" + } + ], + "rdfs:comment": [ + "latitude: Angular distance north or south of the equator, expressed in decimal degrees.Valid range: −90.0 to +90.0." + ], + "rdfs:label": "latitude" + }, + { + "@id": "untp:longitude", + "schema:rangeIncludes": { + "@id": "xsd:double" + }, + "@type": [ + "rdf:Property", + "owl:DatatypeProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Coordinate" + } + ], + "rdfs:comment": [ + "longitude: Angular distance east or west of the Prime Meridian, expressed in decimal degrees.Valid range: −180.0 to +180.0." + ], + "rdfs:label": "longitude" + }, + { + "@id": "untp:applicablePeriod", + "schema:rangeIncludes": { + "@id": "untp:Period" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:MaterialUsage" + }, + { + "@id": "untp:Claim" + } + ], + "rdfs:comment": [ + "The period over which this material consumption is reported", + "The applicable reporting period for this facility record." + ], + "rdfs:label": "applicablePeriod" + }, + { + "@id": "untp:materialConsumed", + "schema:rangeIncludes": { + "@id": "untp:Material" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:MaterialUsage" + } + ], + "rdfs:comment": [ + "An list of materials consumed during the usage period. " + ], + "rdfs:label": "materialConsumed" + }, + { + "@id": "untp:startDate", + "schema:rangeIncludes": { + "@id": "xsd:date" + }, + "@type": [ + "rdf:Property", + "owl:DatatypeProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Period" + } + ], + "rdfs:comment": [ + "The period start date" + ], + "rdfs:label": "startDate" + }, + { + "@id": "untp:endDate", + "schema:rangeIncludes": { + "@id": "xsd:date" + }, + "@type": [ + "rdf:Property", + "owl:DatatypeProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Period" + } + ], + "rdfs:comment": [ + "The period end date" + ], + "rdfs:label": "endDate" + }, + { + "@id": "untp:periodInformation", + "schema:rangeIncludes": { + "@id": "xsd:string" + }, + "@type": [ + "rdf:Property", + "owl:DatatypeProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Period" + } + ], + "rdfs:comment": [ + "Additional information relevant to this reporting period" + ], + "rdfs:label": "periodInformation" + }, + { + "@id": "untp:originCountry", + "schema:rangeIncludes": { + "@id": "untp:Country" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Material" + } + ], + "rdfs:comment": [ + "A ISO 3166-1 code representing the country of origin of the component or ingredient." + ], + "rdfs:label": "originCountry" + }, + { + "@id": "untp:materialType", + "schema:rangeIncludes": { + "@id": "untp:Classification" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Material" + } + ], + "rdfs:comment": [ + "The type of this material - as a value drawn from a controlled vocabulary eg from UN Framework Classification for Resources (UNFC)." + ], + "rdfs:label": "materialType" + }, + { + "@id": "untp:massFraction", + "schema:rangeIncludes": { + "@id": "xsd:double" + }, + "@type": [ + "rdf:Property", + "owl:DatatypeProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Material" + } + ], + "rdfs:comment": [ + "The mass fraction as a decimal of the product (or facility reporting period) represented by this material. " + ], + "rdfs:label": "massFraction" + }, + { + "@id": "untp:mass", + "schema:rangeIncludes": { + "@id": "untp:Measure" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Material" + } + ], + "rdfs:comment": [ + "The mass of the material component." + ], + "rdfs:label": "mass" + }, + { + "@id": "untp:recycledMassFraction", + "schema:rangeIncludes": { + "@id": "xsd:double" + }, + "@type": [ + "rdf:Property", + "owl:DatatypeProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Material" + } + ], + "rdfs:comment": [ + "Mass fraction of this material that is recycled (eg 50% recycled Lithium)" + ], + "rdfs:label": "recycledMassFraction" + }, + { + "@id": "untp:hazardous", + "schema:rangeIncludes": { + "@id": "xsd:boolean" + }, + "@type": [ + "rdf:Property", + "owl:DatatypeProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Material" + } + ], + "rdfs:comment": [ + "Indicates whether this material is hazardous. If true then the materialSafetyInformation property must be present" + ], + "rdfs:label": "hazardous" + }, + { + "@id": "untp:symbol", + "schema:rangeIncludes": { + "@id": "untp:Image" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Material" + } + ], + "rdfs:comment": [ + "Based 64 encoded binary used to represent a visual symbol for a given material. " + ], + "rdfs:label": "symbol" + }, + { + "@id": "untp:materialSafetyInformation", + "schema:rangeIncludes": { + "@id": "untp:Link" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Material" + } + ], + "rdfs:comment": [ + "Reference to further information about safe handling of this hazardous material (for example a link to a material safety data sheet)" + ], + "rdfs:label": "materialSafetyInformation" + }, + { + "@id": "untp:value", + "schema:rangeIncludes": { + "@id": "xsd:double" + }, + "@type": [ + "rdf:Property", + "owl:DatatypeProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Measure" + } + ], + "rdfs:comment": [ + "The numeric value of the measure" + ], + "rdfs:label": "value" + }, + { + "@id": "untp:upperTolerance", + "schema:rangeIncludes": { + "@id": "xsd:double" + }, + "@type": [ + "rdf:Property", + "owl:DatatypeProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Measure" + } + ], + "rdfs:comment": [ + "The upper tolerance associated with this measure expressed in the same units as the measure. For example value=10, upperTolerance=0.1, unit=KGM would mean that this measure is 10kg + 0.1kg" + ], + "rdfs:label": "upperTolerance" + }, + { + "@id": "untp:lowerTolerance", + "schema:rangeIncludes": { + "@id": "xsd:double" + }, + "@type": [ + "rdf:Property", + "owl:DatatypeProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Measure" + } + ], + "rdfs:comment": [ + "The lower tolerance associated with this measure expressed in the same units as the measure. For example value=10, lowerTolerance=0.1, unit=KGM would mean that this measure is 10kg - 0.1kg" + ], + "rdfs:label": "lowerTolerance" + }, + { + "@id": "untp:unit", + "schema:rangeIncludes": { + "@id": "untp:UnitOfMeasure" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Measure" + } + ], + "rdfs:comment": [ + "Unit of measure drawn from the UNECE Rec20 measure code list." + ], + "rdfs:label": "unit" + }, + { + "@id": "untp:imageData", + "schema:rangeIncludes": { + "@id": "xsd:string" + }, + "@type": [ + "rdf:Property", + "owl:DatatypeProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Image" + } + ], + "rdfs:comment": [ + "The image data encoded as a base64 string." + ], + "rdfs:label": "imageData" + }, + { + "@id": "untp:referenceCriteria", + "schema:rangeIncludes": { + "@id": "untp:Criterion" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Claim" + } + ], + "rdfs:comment": [ + "The criterion against which the claim is made." + ], + "rdfs:label": "referenceCriteria" + }, + { + "@id": "untp:referenceRegulation", + "schema:rangeIncludes": { + "@id": "untp:Regulation" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Claim" + }, + { + "@id": "untp:ConformityAssessment" + } + ], + "rdfs:comment": [ + "List of references to regulation to which conformity is claimed claimed for this product", + "The reference to the regulation that defines the assessment criteria" + ], + "rdfs:label": "referenceRegulation" + }, + { + "@id": "untp:referenceStandard", + "schema:rangeIncludes": { + "@id": "untp:Standard" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Claim" + }, + { + "@id": "untp:ConformityAssessment" + } + ], + "rdfs:comment": [ + "List of references to standards to which conformity is claimed claimed for this product", + "The reference to the standard that defines the specification / criteria" + ], + "rdfs:label": "referenceStandard" + }, + { + "@id": "untp:claimDate", + "schema:rangeIncludes": { + "@id": "xsd:date" + }, + "@type": [ + "rdf:Property", + "owl:DatatypeProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Claim" + } + ], + "rdfs:comment": [ + "That date on which the claimed performance is applicable." + ], + "rdfs:label": "claimDate" + }, + { + "@id": "untp:claimedPerformance", + "schema:rangeIncludes": { + "@id": "untp:Performance" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Claim" + } + ], + "rdfs:comment": [ + "The claimed performance level " + ], + "rdfs:label": "claimedPerformance" + }, + { + "@id": "untp:evidence", + "schema:rangeIncludes": { + "@id": "untp:Link" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Claim" + }, + { + "@id": "untp:ConformityAssessment" + } + ], + "rdfs:comment": [ + "A URI pointing to the evidence supporting the claim. SHOULD be a URL to a UNTP Digital Conformity Credential (DCC)", + "Evidence to support this specific assessment." + ], + "rdfs:label": "evidence" + }, + { + "@id": "untp:conformityTopic", + "schema:rangeIncludes": { + "@id": "untp:ConformityTopic" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Claim" + }, + { + "@id": "untp:Criterion" + }, + { + "@id": "untp:ConformityAssessment" + } + ], + "rdfs:comment": [ + "The conformity topic category for this assessment", + "A global UN/CEFACT standard conformity topic code. ", + "The UNTP conformity topic used to categorise this assessment. Should match the topic defined by the scheme criterion." + ], + "rdfs:label": "conformityTopic" + }, + { + "@id": "untp:version", + "schema:rangeIncludes": { + "@id": "xsd:string" + }, + "@type": [ + "rdf:Property", + "owl:DatatypeProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Criterion" + }, + { + "@id": "untp:ConformityProfile" + }, + { + "@id": "untp:IssuingSoftware" + } + ], + "rdfs:comment": [ + "The major.minor version of the criterion. Minor versions represent changes that would not invalidate an assessment made under a previous version.", + "Version of this scheme following SemVer best practice (major.minor.patch). " + ], + "rdfs:label": "version" + }, + { + "@id": "untp:status", + "schema:rangeIncludes": { + "@id": "untp:CriterionStatus" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Criterion" + }, + { + "@id": "untp:ConformityProfile" + } + ], + "rdfs:comment": [ + "The lifecycle status of this criterion. ", + "The status of this conformity profile (draft, active, deprecated)" + ], + "rdfs:label": "status" + }, + { + "@id": "untp:documentation", + "schema:rangeIncludes": { + "@id": "xsd:anyURI" + }, + "@type": [ + "rdf:Property", + "owl:DatatypeProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Criterion" + }, + { + "@id": "untp:ConformityScheme" + }, + { + "@id": "untp:ConformityProfile" + } + ], + "rdfs:comment": [ + "A web page carrying detailed information about this criterion.", + "A web page providing full documentation of this scheme.", + "A web page that describes this entity in detail." + ], + "rdfs:label": "documentation" + }, + { + "@id": "untp:requiredPerformance", + "schema:rangeIncludes": { + "@id": "untp:Performance" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Criterion" + } + ], + "rdfs:comment": [ + "The required performance level as one or more score and/or a metric that represents compliance defined by the criteria" + ], + "rdfs:label": "requiredPerformance" + }, + { + "@id": "untp:tag", + "schema:rangeIncludes": { + "@id": "xsd:string" + }, + "@type": [ + "rdf:Property", + "owl:DatatypeProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Criterion" + } + ], + "rdfs:comment": [ + "A set of tags that can be used by the scheme owner to be able to filter or group criterion in a large vocabulary for specific use cases." + ], + "rdfs:label": "tag" + }, + { + "@id": "untp:metric", + "schema:rangeIncludes": { + "@id": "untp:PerformanceMetric" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Performance" + }, + { + "@id": "untp:SensorData" + } + ], + "rdfs:comment": [ + "The metric (eg material emissions intensity CO2e/Kg or percentage of young workers) that is measured.", + "The type of measurement recorded in this sensor data event." + ], + "rdfs:label": "metric" + }, + { + "@id": "untp:improvementDirection", + "schema:rangeIncludes": { + "@id": "untp:ImprovementIndicator" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:PerformanceMetric" + } + ], + "rdfs:comment": [ + "Indicator of whether conforming performance is greater than or less than the defined threshold." + ], + "rdfs:label": "improvementDirection" + }, + { + "@id": "untp:aggregationMethod", + "schema:rangeIncludes": { + "@id": "untp:AggregationType" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:PerformanceMetric" + } + ], + "rdfs:comment": [ + "Indicates how to aggregate multiple values to report a single performance metric." + ], + "rdfs:label": "aggregationMethod" + }, + { + "@id": "untp:measure", + "schema:rangeIncludes": { + "@id": "untp:Measure" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Performance" + }, + { + "@id": "untp:SensorData" + } + ], + "rdfs:comment": [ + "The measured performance value", + "The value measured by this sensor measurement event." + ], + "rdfs:label": "measure" + }, + { + "@id": "untp:score", + "schema:rangeIncludes": { + "@id": "untp:Score" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Performance" + }, + { + "@id": "untp:ScoringFramework" + } + ], + "rdfs:comment": [ + "A performance score (eg \"AA\") drawn from a scoring framework defined by the scheme or criterion.", + "A list of scores and ranks associated with this scoring framework." + ], + "rdfs:label": "score" + }, + { + "@id": "untp:allowedUnit", + "schema:rangeIncludes": { + "@id": "untp:UnitOfMeasure" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:PerformanceMetric" + } + ], + "rdfs:comment": [ + "The allowed units for value reporting against this metric (eg cubic meters)" + ], + "rdfs:label": "allowedUnit" + }, + { + "@id": "untp:rank", + "schema:rangeIncludes": { + "@id": "xsd:integer" + }, + "@type": [ + "rdf:Property", + "owl:DatatypeProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Score" + } + ], + "rdfs:comment": [ + "The ranking of this score within the scoring framework - using an integer where \"1\" is the highest rank." + ], + "rdfs:label": "rank" + }, + { + "@id": "untp:jurisdictionCountry", + "schema:rangeIncludes": { + "@id": "untp:Country" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Regulation" + } + ], + "rdfs:comment": [ + "The legal jurisdiction (country) under which the regulation is issued." + ], + "rdfs:label": "jurisdictionCountry" + }, + { + "@id": "untp:administeredBy", + "schema:rangeIncludes": { + "@id": "untp:Party" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Regulation" + } + ], + "rdfs:comment": [ + "the issuing body of the regulation. For example Australian Government Department of Climate Change, Energy, the Environment and Water" + ], + "rdfs:label": "administeredBy" + }, + { + "@id": "untp:effectiveDate", + "schema:rangeIncludes": { + "@id": "xsd:date" + }, + "@type": [ + "rdf:Property", + "owl:DatatypeProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Regulation" + } + ], + "rdfs:comment": [ + "the date at which the regulation came into effect." + ], + "rdfs:label": "effectiveDate" + }, + { + "@id": "untp:issuingParty", + "schema:rangeIncludes": { + "@id": "untp:Party" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Standard" + } + ], + "rdfs:comment": [ + "The party that issued the standard " + ], + "rdfs:label": "issuingParty" + }, + { + "@id": "untp:issueDate", + "schema:rangeIncludes": { + "@id": "xsd:date" + }, + "@type": [ + "rdf:Property", + "owl:DatatypeProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Standard" + } + ], + "rdfs:comment": [ + "The date when the standard was issued." + ], + "rdfs:label": "issueDate" + }, + { + "@id": "untp:assessorLevel", + "schema:rangeIncludes": { + "@id": "untp:AssessorLevel" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:ConformityAttestation" + } + ], + "rdfs:comment": [ + "Assurance code pertaining to assessor (relation to the object under assessment)" + ], + "rdfs:label": "assessorLevel" + }, + { + "@id": "untp:assessmentLevel", + "schema:rangeIncludes": { + "@id": "untp:AssessmentLevel" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:ConformityAttestation" + } + ], + "rdfs:comment": [ + "Assurance pertaining to assessment (any authority or support for the assessment process)" + ], + "rdfs:label": "assessmentLevel" + }, + { + "@id": "untp:attestationType", + "schema:rangeIncludes": { + "@id": "untp:AttestationType" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:ConformityAttestation" + } + ], + "rdfs:comment": [ + "The type of criterion (optional or mandatory)." + ], + "rdfs:label": "attestationType" + }, + { + "@id": "untp:issuedToParty", + "schema:rangeIncludes": { + "@id": "untp:Party" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:ConformityAttestation" + } + ], + "rdfs:comment": [ + "The party to whom the conformity attestation was issued." + ], + "rdfs:label": "issuedToParty" + }, + { + "@id": "untp:authorisation", + "schema:rangeIncludes": { + "@id": "untp:Endorsement" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:ConformityAttestation" + } + ], + "rdfs:comment": [ + "The authority under which a conformity claim is issued. For example a national accreditation authority may authorise a test lab to issue test certificates about a product against a standard. " + ], + "rdfs:label": "authorisation" + }, + { + "@id": "untp:referenceScheme", + "schema:rangeIncludes": { + "@id": "untp:ConformityScheme" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:ConformityAttestation" + } + ], + "rdfs:comment": [ + "The conformity scheme under which this attestation is made." + ], + "rdfs:label": "referenceScheme" + }, + { + "@id": "untp:referenceProfile", + "schema:rangeIncludes": { + "@id": "untp:ConformityProfile" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:ConformityAttestation" + } + ], + "rdfs:comment": [ + "The specific versioned conformity profile (comprising a set of versioned criteria) against which this conformity attestation is made." + ], + "rdfs:label": "referenceProfile" + }, + { + "@id": "untp:profileScore", + "schema:rangeIncludes": { + "@id": "untp:Score" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:ConformityAttestation" + } + ], + "rdfs:comment": [ + "The overall performance against a scheme level performance measurement framework for the referenced profile or scheme." + ], + "rdfs:label": "profileScore" + }, + { + "@id": "untp:conformityCertificate", + "schema:rangeIncludes": { + "@id": "untp:Link" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:ConformityAttestation" + } + ], + "rdfs:comment": [ + "A reference to the human / printable version of this conformity attestation - typically represented as a PDF document. The document may have more details than are represented in the digital attestation." + ], + "rdfs:label": "conformityCertificate" + }, + { + "@id": "untp:auditableEvidence", + "schema:rangeIncludes": { + "@id": "untp:Link" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:ConformityAttestation" + } + ], + "rdfs:comment": [ + "Auditable evidence supporting this assessment such as raw measurements, supporting documents. This is usually private data and would normally be encrypted." + ], + "rdfs:label": "auditableEvidence" + }, + { + "@id": "untp:trustmark", + "schema:rangeIncludes": { + "@id": "untp:Image" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:ConformityAttestation" + }, + { + "@id": "untp:Endorsement" + }, + { + "@id": "untp:ConformityScheme" + } + ], + "rdfs:comment": [ + "A trust mark as a small binary image encoded as base64 with a description. Maye be displayed on the conformity credential rendering.", + "The trust mark image awarded by the AB to the CAB to indicate accreditation.", + "The trust mark or seal used by this conformity scheme." + ], + "rdfs:label": "trustmark" + }, + { + "@id": "untp:conformityAssessment", + "schema:rangeIncludes": { + "@id": "untp:ConformityAssessment" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:ConformityAttestation" + } + ], + "rdfs:comment": [ + "A list of individual assessment made under this attestation. " + ], + "rdfs:label": "conformityAssessment" + }, + { + "@id": "untp:issuingAuthority", + "schema:rangeIncludes": { + "@id": "untp:Party" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Endorsement" + } + ], + "rdfs:comment": [ + "The competent authority that issued the accreditation." + ], + "rdfs:label": "issuingAuthority" + }, + { + "@id": "untp:endorsementEvidence", + "schema:rangeIncludes": { + "@id": "untp:Link" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Endorsement" + } + ], + "rdfs:comment": [ + "The evidence that supports the authority under which the attestation is issued - for an example an accreditation certificate." + ], + "rdfs:label": "endorsementEvidence" + }, + { + "@id": "untp:owner", + "schema:rangeIncludes": { + "@id": "untp:Party" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:ConformityScheme" + } + ], + "rdfs:comment": [ + "The party that is the owner / maintainer of this conformity scheme." + ], + "rdfs:label": "owner" + }, + { + "@id": "untp:endorsementLevel", + "schema:rangeIncludes": { + "@id": "untp:SchemeEndorsementLevel" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:ConformityScheme" + } + ], + "rdfs:comment": [ + "The scheme assurance type." + ], + "rdfs:label": "endorsementLevel" + }, + { + "@id": "untp:endorsement", + "schema:rangeIncludes": { + "@id": "untp:Endorsement" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:ConformityScheme" + } + ], + "rdfs:comment": [ + "The endorsement provided to the scheme by an external authority such as a regulator, an accreditaiton authority, or a benchmarking scheme." + ], + "rdfs:label": "endorsement" + }, + { + "@id": "untp:schemeScoringFramework", + "schema:rangeIncludes": { + "@id": "untp:ScoringFramework" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:ConformityScheme" + } + ], + "rdfs:comment": [ + "The scheme level overall scoring framework that represents the achievement levels (AA, A, B etc) that maybe be awarded to the subject of an independent assessment under the scheme." + ], + "rdfs:label": "schemeScoringFramework" + }, + { + "@id": "untp:licenseType", + "schema:rangeIncludes": { + "@id": "untp:LicenseType" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:ConformityScheme" + } + ], + "rdfs:comment": [ + "Descriptive name and URL link to the license conditions associated with this scheme." + ], + "rdfs:label": "licenseType" + }, + { + "@id": "untp:establishedDate", + "schema:rangeIncludes": { + "@id": "xsd:date" + }, + "@type": [ + "rdf:Property", + "owl:DatatypeProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:ConformityScheme" + } + ], + "rdfs:comment": [ + "The date when this scheme was first established. " + ], + "rdfs:label": "establishedDate" + }, + { + "@id": "untp:geographicScope", + "schema:rangeIncludes": { + "@id": "untp:Classification" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:ConformityScheme" + } + ], + "rdfs:comment": [ + "The geographic scope of this scheme as a list of ISO-3166 countries, regions, or code=001, name=Worldwide to indicate global coverage." + ], + "rdfs:label": "geographicScope" + }, + { + "@id": "untp:industryScope", + "schema:rangeIncludes": { + "@id": "untp:Classification" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:ConformityScheme" + } + ], + "rdfs:comment": [ + "A list of UN ISIC code & name indicating the industry scope for this scheme. " + ], + "rdfs:label": "industryScope" + }, + { + "@id": "untp:conformsTo", + "schema:rangeIncludes": { + "@id": "untp:Link" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:ConformityScheme" + } + ], + "rdfs:comment": [ + "The name and URI of the vocabulary standard (eg UNTP CVC) that the machine readable version of this sceme conforms to." + ], + "rdfs:label": "conformsTo" + }, + { + "@id": "untp:includedProfile", + "schema:rangeIncludes": { + "@id": "untp:ConformityProfile" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:ConformityScheme" + } + ], + "rdfs:comment": [ + "The list of versioned conformity profiles included in this scheme" + ], + "rdfs:label": "includedProfile" + }, + { + "@id": "untp:validFrom", + "schema:rangeIncludes": { + "@id": "xsd:date" + }, + "@type": [ + "rdf:Property", + "owl:DatatypeProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:ConformityProfile" + } + ], + "rdfs:comment": [ + "The data from which this scheme version is valid." + ], + "rdfs:label": "validFrom" + }, + { + "@id": "untp:subjectType", + "schema:rangeIncludes": { + "@id": "untp:AssessmentSubjectType" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:ConformityProfile" + } + ], + "rdfs:comment": [ + "The type of the subject of assessments made under this conformity profile (eg product, facility, organisation)" + ], + "rdfs:label": "subjectType" + }, + { + "@id": "untp:standardAlignment", + "schema:rangeIncludes": { + "@id": "untp:StandardAlignment" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:ConformityProfile" + } + ], + "rdfs:comment": [ + "A list of voluntary standards referenced by this conformity profile and against which some level of compliance can be inferred for subjects that pass an assessment. " + ], + "rdfs:label": "standardAlignment" + }, + { + "@id": "untp:regulatoryAlignment", + "schema:rangeIncludes": { + "@id": "untp:RegulatoryAlignment" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:ConformityProfile" + } + ], + "rdfs:comment": [ + "A list of regulations or legally binding conventions referenced by this conformity profile and against which some level of compliance can be inferred for subjects that pass an assessment. " + ], + "rdfs:label": "regulatoryAlignment" + }, + { + "@id": "untp:criterionScoringFramework", + "schema:rangeIncludes": { + "@id": "untp:ScoringFramework" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:ConformityProfile" + } + ], + "rdfs:comment": [ + "A list of named scoring frameworks that are applied by criterion within this profile. " + ], + "rdfs:label": "criterionScoringFramework" + }, + { + "@id": "untp:criterion", + "schema:rangeIncludes": { + "@id": "untp:Criterion" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:ConformityProfile" + } + ], + "rdfs:comment": [ + "A list of criterion that are included in this conformity profile." + ], + "rdfs:label": "criterion" + }, + { + "@id": "untp:scope", + "schema:rangeIncludes": { + "@id": "untp:Classification" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:ConformityProfile" + } + ], + "rdfs:comment": [ + "A set of classification codes that may be used to categorize the applicability of this criteria - for example industry sector, jurisdiction or commodity type - based on a formal vocabulary." + ], + "rdfs:label": "scope" + }, + { + "@id": "untp:scheme", + "schema:rangeIncludes": { + "@id": "untp:ConformityScheme" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:ConformityProfile" + } + ], + "rdfs:comment": [ + "The conformity scheme under which this versioned profile is maintained." + ], + "rdfs:label": "scheme" + }, + { + "@id": "untp:standard", + "schema:rangeIncludes": { + "@id": "untp:Standard" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:StandardAlignment" + } + ], + "rdfs:comment": [ + "The standard against which this alignment assessment is made." + ], + "rdfs:label": "standard" + }, + { + "@id": "untp:alignmentLevel", + "schema:rangeIncludes": { + "@id": "untp:SchemeAlignmentLevel" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:StandardAlignment" + }, + { + "@id": "untp:RegulatoryAlignment" + } + ], + "rdfs:comment": [ + "A level of alignment with the referenced standard (exceeds, meets, partial,..)", + "A level of alignment with the referenced standard (exceeds, meets, partial,..)" + ], + "rdfs:label": "alignmentLevel" + }, + { + "@id": "untp:regulation", + "schema:rangeIncludes": { + "@id": "untp:Regulation" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:RegulatoryAlignment" + } + ], + "rdfs:comment": [ + "The regulation against which this alignment assessment is made." + ], + "rdfs:label": "regulation" + }, + { + "@id": "untp:assessmentCriteria", + "schema:rangeIncludes": { + "@id": "untp:Criterion" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:ConformityAssessment" + } + ], + "rdfs:comment": [ + "The specification against which the assessment is made." + ], + "rdfs:label": "assessmentCriteria" + }, + { + "@id": "untp:assessmentDate", + "schema:rangeIncludes": { + "@id": "xsd:date" + }, + "@type": [ + "rdf:Property", + "owl:DatatypeProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:ConformityAssessment" + } + ], + "rdfs:comment": [ + "The date on which this assessment was made. " + ], + "rdfs:label": "assessmentDate" + }, + { + "@id": "untp:assessedPerformance", + "schema:rangeIncludes": { + "@id": "untp:Performance" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:ConformityAssessment" + } + ], + "rdfs:comment": [ + "The assessed performance against criteria." + ], + "rdfs:label": "assessedPerformance" + }, + { + "@id": "untp:assessedProduct", + "schema:rangeIncludes": { + "@id": "untp:ProductVerification" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:ConformityAssessment" + } + ], + "rdfs:comment": [ + "The product which is the subject of this assessment." + ], + "rdfs:label": "assessedProduct" + }, + { + "@id": "untp:assessedFacility", + "schema:rangeIncludes": { + "@id": "untp:FacilityVerification" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:ConformityAssessment" + } + ], + "rdfs:comment": [ + "The facility which is the subject of this assessment." + ], + "rdfs:label": "assessedFacility" + }, + { + "@id": "untp:assessedOrganisation", + "schema:rangeIncludes": { + "@id": "untp:Party" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:ConformityAssessment" + } + ], + "rdfs:comment": [ + "An organisation that is the subject of this assessment." + ], + "rdfs:label": "assessedOrganisation" + }, + { + "@id": "untp:specifiedCondition", + "schema:rangeIncludes": { + "@id": "xsd:string" + }, + "@type": [ + "rdf:Property", + "owl:DatatypeProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:ConformityAssessment" + } + ], + "rdfs:comment": [ + "A list of specific conditions that constrain this conformity assessment. For example a specific jurisdiction, material type, or test method." + ], + "rdfs:label": "specifiedCondition" + }, + { + "@id": "untp:conformance", + "schema:rangeIncludes": { + "@id": "xsd:boolean" + }, + "@type": [ + "rdf:Property", + "owl:DatatypeProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:ConformityAssessment" + } + ], + "rdfs:comment": [ + "An indicator (true / false) whether the outcome of this assessment is conformant to the requirements defined by the standard or criterion." + ], + "rdfs:label": "conformance" + }, + { + "@id": "untp:product", + "schema:rangeIncludes": { + "@id": "untp:Product" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:ProductVerification" + }, + { + "@id": "untp:EventProduct" + } + ], + "rdfs:comment": [ + "The product, serial or batch that is the subject of this assessment", + "The product item / model / batch subject to this lifecycle event." + ], + "rdfs:label": "product" + }, + { + "@id": "untp:idVerifiedByCAB", + "schema:rangeIncludes": { + "@id": "xsd:boolean" + }, + "@type": [ + "rdf:Property", + "owl:DatatypeProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:ProductVerification" + }, + { + "@id": "untp:FacilityVerification" + } + ], + "rdfs:comment": [ + "Indicates whether the conformity assessment body has verified the identity product that is the subject of the assessment.", + "Indicates whether the conformity assessment body has verified the identity of the facility which is the subject of the assessment." + ], + "rdfs:label": "idVerifiedByCAB" + }, + { + "@id": "untp:modelNumber", + "schema:rangeIncludes": { + "@id": "xsd:string" + }, + "@type": [ + "rdf:Property", + "owl:DatatypeProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Product" + } + ], + "rdfs:comment": [ + "Where available, the model number (for manufactured products) or material identification (for bulk materials)" + ], + "rdfs:label": "modelNumber" + }, + { + "@id": "untp:batchNumber", + "schema:rangeIncludes": { + "@id": "xsd:string" + }, + "@type": [ + "rdf:Property", + "owl:DatatypeProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Product" + } + ], + "rdfs:comment": [ + "Identifier of the specific production batch of the product. Unique within the product class." + ], + "rdfs:label": "batchNumber" + }, + { + "@id": "untp:itemNumber", + "schema:rangeIncludes": { + "@id": "xsd:string" + }, + "@type": [ + "rdf:Property", + "owl:DatatypeProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Product" + } + ], + "rdfs:comment": [ + "A number or code representing a specific serialised item of the product. Unique within product class." + ], + "rdfs:label": "itemNumber" + }, + { + "@id": "untp:idGranularity", + "schema:rangeIncludes": { + "@id": "untp:ProductIDGranularity" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Product" + } + ], + "rdfs:comment": [ + "The identification granularity for this product (item, batch, model)" + ], + "rdfs:label": "idGranularity" + }, + { + "@id": "untp:productImage", + "schema:rangeIncludes": { + "@id": "untp:Link" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Product" + } + ], + "rdfs:comment": [ + "Reference information (location, type, name) of an image of the product." + ], + "rdfs:label": "productImage" + }, + { + "@id": "untp:characteristics", + "schema:rangeIncludes": { + "@id": "untp:Characteristics" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Product" + } + ], + "rdfs:comment": [ + "A set of industry specific product information. " + ], + "rdfs:label": "characteristics" + }, + { + "@id": "untp:productCategory", + "schema:rangeIncludes": { + "@id": "untp:Classification" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Product" + } + ], + "rdfs:comment": [ + "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" + ], + "rdfs:label": "productCategory" + }, + { + "@id": "untp:producedAtFacility", + "schema:rangeIncludes": { + "@id": "untp:Facility" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Product" + } + ], + "rdfs:comment": [ + "The Facility where the product batch was produced / manufactured." + ], + "rdfs:label": "producedAtFacility" + }, + { + "@id": "untp:productionDate", + "schema:rangeIncludes": { + "@id": "xsd:date" + }, + "@type": [ + "rdf:Property", + "owl:DatatypeProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Product" + } + ], + "rdfs:comment": [ + "The ISO 8601 date on which the product batch or individual serialised item was manufactured." + ], + "rdfs:label": "productionDate" + }, + { + "@id": "untp:expiryDate", + "schema:rangeIncludes": { + "@id": "xsd:date" + }, + "@type": [ + "rdf:Property", + "owl:DatatypeProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Product" + } + ], + "rdfs:comment": [ + "The date at which this product is no longer fit for use. Typically used for a food product use-by date but may also represent the usable life of any product." + ], + "rdfs:label": "expiryDate" + }, + { + "@id": "untp:countryOfProduction", + "schema:rangeIncludes": { + "@id": "untp:Country" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Product" + } + ], + "rdfs:comment": [ + "The country in which this item was produced / manufactured.using ISO-3166 code and name." + ], + "rdfs:label": "countryOfProduction" + }, + { + "@id": "untp:dimensions", + "schema:rangeIncludes": { + "@id": "untp:Dimension" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Product" + }, + { + "@id": "untp:Package" + } + ], + "rdfs:comment": [ + "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, width, or height.\"weight\":{\"value\":10, \"unit\":\"KGM\"}", + "dimensions of the packaging" + ], + "rdfs:label": "dimensions" + }, + { + "@id": "untp:materialProvenance", + "schema:rangeIncludes": { + "@id": "untp:Material" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Product" + } + ], + "rdfs:comment": [ + "A list of materials provenance objects providing details on the origin and mass fraction of materials of the product or batch." + ], + "rdfs:label": "materialProvenance" + }, + { + "@id": "untp:packaging", + "schema:rangeIncludes": { + "@id": "untp:Package" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Product" + } + ], + "rdfs:comment": [ + "The packaging for this product." + ], + "rdfs:label": "packaging" + }, + { + "@id": "untp:productLabel", + "schema:rangeIncludes": { + "@id": "untp:Image" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Product" + } + ], + "rdfs:comment": [ + "An array of labels that may appear on the product such as certification marks or regulatory labels." + ], + "rdfs:label": "productLabel" + }, + { + "@id": "untp:weight", + "schema:rangeIncludes": { + "@id": "untp:Measure" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Dimension" + } + ], + "rdfs:comment": [ + "the weight of the product. EG {\"value\":10, \"unit\":\"KGM\"}" + ], + "rdfs:label": "weight" + }, + { + "@id": "untp:length", + "schema:rangeIncludes": { + "@id": "untp:Measure" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Dimension" + } + ], + "rdfs:comment": [ + "The length of the product or packaging eg {\"value\":840, \"unit\":\"MMT\"}" + ], + "rdfs:label": "length" + }, + { + "@id": "untp:width", + "schema:rangeIncludes": { + "@id": "untp:Measure" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Dimension" + } + ], + "rdfs:comment": [ + "The width of the product or packaging. eg {\"value\":150, \"unit\":\"MMT\"}" + ], + "rdfs:label": "width" + }, + { + "@id": "untp:height", + "schema:rangeIncludes": { + "@id": "untp:Measure" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Dimension" + } + ], + "rdfs:comment": [ + "The height of the product or packaging. eg {\"value\":220, \"unit\":\"MMT\"}" + ], + "rdfs:label": "height" + }, + { + "@id": "untp:volume", + "schema:rangeIncludes": { + "@id": "untp:Measure" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Dimension" + } + ], + "rdfs:comment": [ + "The displacement volume of the product. eg {\"value\":7.5, \"unit\":\"LTR\"}" + ], + "rdfs:label": "volume" + }, + { + "@id": "untp:materialUsed", + "schema:rangeIncludes": { + "@id": "untp:Material" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Package" + } + ], + "rdfs:comment": [ + "materials used for the packaging." + ], + "rdfs:label": "materialUsed" + }, + { + "@id": "untp:packageLabel", + "schema:rangeIncludes": { + "@id": "untp:Image" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:Package" + } + ], + "rdfs:comment": [ + "An array of package labels that may appear on the packaging together with their meaning. Use for small images that represent certification marks or regulatory requirements. Large images should be linked as evidence to claims." + ], + "rdfs:label": "packageLabel" + }, + { + "@id": "untp:facility", + "schema:rangeIncludes": { + "@id": "untp:Facility" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:FacilityVerification" + } + ], + "rdfs:comment": [ + "The facility which is the subject of this assessment" + ], + "rdfs:label": "facility" + }, + { + "@id": "untp:eventDate", + "schema:rangeIncludes": { + "@id": "xsd:datetime" + }, + "@type": [ + "rdf:Property", + "owl:DatatypeProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:LifecycleEvent" + }, + { + "@id": "untp:MakeEvent" + }, + { + "@id": "untp:MoveEvent" + }, + { + "@id": "untp:ModifyEvent" + } + ], + "rdfs:comment": [ + "The date and time at which this lifecycle event occurs. use 00:00 for time if only a date is required.", + "The date and time at which this lifecycle event occurs. use 00:00 for time if only a date is required.", + "The date and time at which this lifecycle event occurs. use 00:00 for time if only a date is required.", + "The date and time at which this lifecycle event occurs. use 00:00 for time if only a date is required." + ], + "rdfs:label": "eventDate" + }, + { + "@id": "untp:sensorData", + "schema:rangeIncludes": { + "@id": "untp:SensorData" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:LifecycleEvent" + }, + { + "@id": "untp:MakeEvent" + }, + { + "@id": "untp:MoveEvent" + }, + { + "@id": "untp:ModifyEvent" + } + ], + "rdfs:comment": [ + "A sensor data set associated with this lifecycle event.", + "A sensor data set associated with this lifecycle event.", + "A sensor data set associated with this lifecycle event.", + "A sensor data set associated with this lifecycle event." + ], + "rdfs:label": "sensorData" + }, + { + "@id": "untp:activityType", + "schema:rangeIncludes": { + "@id": "untp:Classification" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:LifecycleEvent" + }, + { + "@id": "untp:MakeEvent" + }, + { + "@id": "untp:MoveEvent" + }, + { + "@id": "untp:ModifyEvent" + } + ], + "rdfs:comment": [ + "The business activity that this event represents (eg shipping, repair, etc) using a standard classification scheme - eg https://ref.gs1.org/cbv/BizStep. This may be replaced with industry specific vocabularies (ginning, spinning, weaving, dyeing, etc in textiles)", + "The business activity that this event represents (eg shipping, repair, etc) using a standard classification scheme - eg https://ref.gs1.org/cbv/BizStep. This may be replaced with industry specific vocabularies (ginning, spinning, weaving, dyeing, etc in textiles)", + "The business activity that this event represents (eg shipping, repair, etc) using a standard classification scheme - eg https://ref.gs1.org/cbv/BizStep. This may be replaced with industry specific vocabularies (ginning, spinning, weaving, dyeing, etc in textiles)", + "The business activity that this event represents (eg shipping, repair, etc) using a standard classification scheme - eg https://ref.gs1.org/cbv/BizStep. This may be replaced with industry specific vocabularies (ginning, spinning, weaving, dyeing, etc in textiles)" + ], + "rdfs:label": "activityType" + }, + { + "@id": "untp:inputProduct", + "schema:rangeIncludes": { + "@id": "untp:EventProduct" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:MakeEvent" + } + ], + "rdfs:comment": [ + "An array of input products and quantities for this production or manufacturing process" + ], + "rdfs:label": "inputProduct" + }, + { + "@id": "untp:outputProduct", + "schema:rangeIncludes": { + "@id": "untp:EventProduct" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:MakeEvent" + } + ], + "rdfs:comment": [ + "An array of output products and quantities for this produciton or manufacturing process" + ], + "rdfs:label": "outputProduct" + }, + { + "@id": "untp:madeAtFacility", + "schema:rangeIncludes": { + "@id": "untp:Facility" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:MakeEvent" + } + ], + "rdfs:comment": [ + "The facility at which this production / manufacturing event happens." + ], + "rdfs:label": "madeAtFacility" + }, + { + "@id": "untp:rawData", + "schema:rangeIncludes": { + "@id": "untp:Link" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:SensorData" + } + ], + "rdfs:comment": [ + "Link to raw data file associated with this sensor reading (eg an image)." + ], + "rdfs:label": "rawData" + }, + { + "@id": "untp:sensor", + "schema:rangeIncludes": { + "@id": "untp:Product" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:SensorData" + } + ], + "rdfs:comment": [ + "The sensor device used for this sensor measurement" + ], + "rdfs:label": "sensor" + }, + { + "@id": "untp:quantity", + "schema:rangeIncludes": { + "@id": "untp:Measure" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:EventProduct" + } + ], + "rdfs:comment": [ + "The quantity of product subject to this lifecycle event. Not needed for serialised items." + ], + "rdfs:label": "quantity" + }, + { + "@id": "untp:disposition", + "schema:rangeIncludes": { + "@id": "untp:ProductStatus" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:EventProduct" + } + ], + "rdfs:comment": [ + "The status of the product after the event has happened." + ], + "rdfs:label": "disposition" + }, + { + "@id": "untp:movedProduct", + "schema:rangeIncludes": { + "@id": "untp:EventProduct" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:MoveEvent" + } + ], + "rdfs:comment": [ + "An array of products and quantities for this movement / shipment process" + ], + "rdfs:label": "movedProduct" + }, + { + "@id": "untp:fromFacility", + "schema:rangeIncludes": { + "@id": "untp:Facility" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:MoveEvent" + } + ], + "rdfs:comment": [ + "The source facility for this movement / shipment of products" + ], + "rdfs:label": "fromFacility" + }, + { + "@id": "untp:toFacility", + "schema:rangeIncludes": { + "@id": "untp:Facility" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:MoveEvent" + } + ], + "rdfs:comment": [ + "The destination facility for this movement / shipment of products" + ], + "rdfs:label": "toFacility" + }, + { + "@id": "untp:consignmentId", + "schema:rangeIncludes": { + "@id": "xsd:anyURI" + }, + "@type": [ + "rdf:Property", + "owl:DatatypeProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:MoveEvent" + } + ], + "rdfs:comment": [ + "The consignment ID related to this movement of products. Ideally this is a resolvable URL but if not available then use a URN notation such as urn:carrier:waybillNumber." + ], + "rdfs:label": "consignmentId" + }, + { + "@id": "untp:modifiedProduct", + "schema:rangeIncludes": { + "@id": "untp:EventProduct" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:ModifyEvent" + } + ], + "rdfs:comment": [ + "An array of products and quantities for this intervention (repair, inspection, etc)" + ], + "rdfs:label": "modifiedProduct" + }, + { + "@id": "untp:modifiedAtFacility", + "schema:rangeIncludes": { + "@id": "untp:Facility" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:ModifyEvent" + } + ], + "rdfs:comment": [ + "The facility at which this intervention event happens." + ], + "rdfs:label": "modifiedAtFacility" + }, + { + "@id": "untp:registeredName", + "schema:rangeIncludes": { + "@id": "xsd:string" + }, + "@type": [ + "rdf:Property", + "owl:DatatypeProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:RegisteredIdentity" + } + ], + "rdfs:comment": [ + "The registered name of the entity within the identifier scheme. Examples: product - EV battery 300Ah, Party - Sample Company Pty Ltd, Facility - Green Acres battery factory " + ], + "rdfs:label": "registeredName" + }, + { + "@id": "untp:registeredDate", + "schema:rangeIncludes": { + "@id": "xsd:date" + }, + "@type": [ + "rdf:Property", + "owl:DatatypeProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:RegisteredIdentity" + } + ], + "rdfs:comment": [ + "The date on which this identity was first registered with the registrar." + ], + "rdfs:label": "registeredDate" + }, + { + "@id": "untp:publicInformation", + "schema:rangeIncludes": { + "@id": "xsd:anyURI" + }, + "@type": [ + "rdf:Property", + "owl:DatatypeProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:RegisteredIdentity" + } + ], + "rdfs:comment": [ + "A link to further information about the registered entity on the authoritative registrar site." + ], + "rdfs:label": "publicInformation" + }, + { + "@id": "untp:registrar", + "schema:rangeIncludes": { + "@id": "untp:Party" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:RegisteredIdentity" + } + ], + "rdfs:comment": [ + "The registrar party that operates the register." + ], + "rdfs:label": "registrar" + }, + { + "@id": "untp:registerType", + "schema:rangeIncludes": { + "@id": "untp:RegistryType" + }, + "@type": [ + "rdf:Property", + "owl:ObjectProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:RegisteredIdentity" + } + ], + "rdfs:comment": [ + "The thematic purpose of the register - organisations, facilities, products, trademarks, etc" + ], + "rdfs:label": "registerType" + }, + { + "@id": "untp:registrationScope", + "schema:rangeIncludes": { + "@id": "xsd:anyURI" + }, + "@type": [ + "rdf:Property", + "owl:DatatypeProperty" + ], + "schema:domainIncludes": [ + { + "@id": "untp:RegisteredIdentity" + } + ], + "rdfs:comment": [ + "List of URIs that represent the roles or scopes of membership. For example [\"https://abr.business.gov.au/Help/EntityTypeDescription?Id=19\"]" + ], + "rdfs:label": "registrationScope" + }, + { + "@id": "untp:AssessmentLevel", + "@type": "rdfs:Class", + "rdfs:label": "AssessmentLevel", + "rdfs:comment": "Type of authority endorsement of the assessment process" + }, + { + "@id": "untp:AssessmentLevel#authority-benchmark", + "@type": "untp:AssessmentLevel", + "rdf:value": "authority-benchmark", + "rdfs:label": "Authority-derived assurance: Recognition by approved benchmarking organisation", + "rdfs:comment": "Benchmarking of scheme by an organization approved to UNIDO benchmarking\nprinciples and process. UNIDO Global Best Practice Framework for Organisations Performing Benchmarking Activities for Certification-related Conformity Assessment Schemes 2026" + }, + { + "@id": "untp:AssessmentLevel#authority-mandate", + "@type": "untp:AssessmentLevel", + "rdf:value": "authority-mandate", + "rdfs:label": "Authority-derived assurance: Recognition by government mandate", + "rdfs:comment": "Government mandate for conformity assessment activity. Ownership or mandate provided by national government or intergovernmental entity." + }, + { + "@id": "untp:AssessmentLevel#authority-globalmra", + "@type": "untp:AssessmentLevel", + "rdf:value": "authority-globalmra", + "rdfs:label": "Authority-derived assurance:Global accreditation mutual recognition arrangement", + "rdfs:comment": "Accreditation of CAB under global mutual recognition arrangement by a body peer-evaluated\nto ISO/IEC 17011. Scheme evaluation is a prerequisite for accreditation of CABs by bodies that are signatories to the Global Accreditation Cooperation Incorporated Mutual Recognition Arrangement." + }, + { + "@id": "untp:AssessmentLevel#authority-peer", + "@type": "untp:AssessmentLevel", + "rdf:value": "authority-peer", + "rdfs:label": "Authority-derived assurance: Recognition by a governmental peer assessment authority", + "rdfs:comment": "Peer assessment process managed by government. Ownership or mandate provided by national government or intergovernmental entity." + }, + { + "@id": "untp:AssessmentLevel#authority-extended-mra", + "@type": "untp:AssessmentLevel", + "rdf:value": "authority-extended-mra", + "rdfs:label": "Authority- derived assurance: Peer assessment body recognition for accredited CAB", + "rdfs:comment": "Independent peer assessment for accredited CAB. This pathway applies to CABs accredited under the Mutual Recognition Arrangement of the Global Accreditation Cooperation Incorporated. Schemes used by CABs may be owned by the peer assessment body but the CAB itself shall not be owned by or otherwise related to the peer assessment body." + }, + { + "@id": "untp:AssessmentLevel#scheme-self", + "@type": "untp:AssessmentLevel", + "rdf:value": "scheme-self", + "rdfs:label": "Scheme-derived assurance: Self-declaration by registered scheme", + "rdfs:comment": "Scheme owner directly conducting conformity assessment activities. The linked scheme self-declaration can be used to assist in judging credibility of the scheme." + }, + { + "@id": "untp:AssessmentLevel#scheme-cab", + "@type": "untp:AssessmentLevel", + "rdf:value": "scheme-cab", + "rdfs:label": "Scheme-derived assurance: Recognition of CAB by registered scheme", + "rdfs:comment": "Scheme owner recognition of other parties assessing against the scheme standards. The linked scheme self-declaration can be used to assist in judging credibility of the scheme. Users of conformity credentials issued by a CAB recognised under a scheme may refer to the linked scheme self-declaration for details of the CAB-approval process used by the scheme owner" + }, + { + "@id": "untp:AssessmentLevel#no-endorsement", + "@type": "untp:AssessmentLevel", + "rdf:value": "no-endorsement", + "rdfs:label": "No endorsement.", + "rdfs:comment": "conformity assessment claiming no external authority or else unspecified" + }, + { + "@id": "untp:AssessmentSubjectType", + "@type": "rdfs:Class", + "rdfs:label": "AssessmentSubjectType", + "rdfs:comment": "The type of entity being assessed." + }, + { + "@id": "untp:AssessmentSubjectType#product", + "@type": "untp:AssessmentSubjectType", + "rdf:value": "product", + "rdfs:label": "Product", + "rdfs:comment": "The conformity profile targets products — assessing characteristics, composition, performance, or safety of manufactured goods." + }, + { + "@id": "untp:AssessmentSubjectType#facility", + "@type": "untp:AssessmentSubjectType", + "rdf:value": "facility", + "rdfs:label": "Facility", + "rdfs:comment": "The conformity profile targets facilities — assessing the operational practices, environmental performance, or working conditions at a specific site." + }, + { + "@id": "untp:AssessmentSubjectType#organisation", + "@type": "untp:AssessmentSubjectType", + "rdf:value": "organisation", + "rdfs:label": "Organisation", + "rdfs:comment": "The conformity profile targets organisations — assessing entity-level governance, policies, management systems, or corporate sustainability performance." + }, + { + "@id": "untp:AssessorLevel", + "@type": "rdfs:Class", + "rdfs:label": "AssessorLevel", + "rdfs:comment": "Code that describes the level of independent assurance of the specific assessment" + }, + { + "@id": "untp:AssessorLevel#self", + "@type": "untp:AssessorLevel", + "rdf:value": "self", + "rdfs:label": "Self assessed", + "rdfs:comment": " self-assessment" + }, + { + "@id": "untp:AssessorLevel#commercial", + "@type": "untp:AssessorLevel", + "rdf:value": "commercial", + "rdfs:label": "Commercial assessment", + "rdfs:comment": " conformity assessment by related body or under commercial contract" + }, + { + "@id": "untp:AssessorLevel#buyer", + "@type": "untp:AssessorLevel", + "rdf:value": "buyer", + "rdfs:label": "Buyer assessment", + "rdfs:comment": " conformity assessment by potential purchaser" + }, + { + "@id": "untp:AssessorLevel#membership", + "@type": "untp:AssessorLevel", + "rdf:value": "membership", + "rdfs:label": "Industry body assessment", + "rdfs:comment": " conformity assessment by industry representative body or membership body" + }, + { + "@id": "untp:AssessorLevel#unspecified", + "@type": "untp:AssessorLevel", + "rdf:value": "unspecified", + "rdfs:label": "No independent assessment", + "rdfs:comment": " conformity assessment by party with unspecified relationship " + }, + { + "@id": "untp:AssessorLevel#3rdParty", + "@type": "untp:AssessorLevel", + "rdf:value": "3rdParty", + "rdfs:label": "Independent third party assessment", + "rdfs:comment": " 3rd party (independent) conformity assessment" + }, + { + "@id": "untp:AssessorLevel#hybrid", + "@type": "untp:AssessorLevel", + "rdf:value": "hybrid", + "rdfs:label": "Input from self-declaring parties", + "rdfs:comment": "2nd or 3rd party conformity assessment that is dependent on the accuracy of information provided by self-declaring parties" + }, + { + "@id": "untp:AttestationType", + "@type": "rdfs:Class", + "rdfs:label": "AttestationType", + "rdfs:comment": "A code for the type of the attestation credential" + }, + { + "@id": "untp:AttestationType#certification", + "@type": "untp:AttestationType", + "rdf:value": "certification", + "rdfs:label": "certification", + "rdfs:comment": "A formal third party certification of conformity" + }, + { + "@id": "untp:AttestationType#declaration", + "@type": "untp:AttestationType", + "rdf:value": "declaration", + "rdfs:label": "declaration", + "rdfs:comment": "A self assessed declaration of conformity" + }, + { + "@id": "untp:AttestationType#inspection", + "@type": "untp:AttestationType", + "rdf:value": "inspection", + "rdfs:label": "inspection", + "rdfs:comment": "An Inspection report " + }, + { + "@id": "untp:AttestationType#testing", + "@type": "untp:AttestationType", + "rdf:value": "testing", + "rdfs:label": "testing", + "rdfs:comment": "A test report" + }, + { + "@id": "untp:AttestationType#verification", + "@type": "untp:AttestationType", + "rdf:value": "verification", + "rdfs:label": "verification", + "rdfs:comment": "A verification report" + }, + { + "@id": "untp:AttestationType#validation", + "@type": "untp:AttestationType", + "rdf:value": "validation", + "rdfs:label": "validation", + "rdfs:comment": "A validation report" + }, + { + "@id": "untp:AttestationType#calibration", + "@type": "untp:AttestationType", + "rdf:value": "calibration", + "rdfs:label": "calibration", + "rdfs:comment": "An equipment calibration report" + }, + { + "@id": "untp:CountryCode", + "@type": "rdfs:Class", + "rdfs:label": "CountryCode", + "rdfs:comment": "ISO 2 letter country code" + }, + { + "@id": "untp:CredentialStatus", + "@type": "rdfs:Class", + "rdfs:label": "CredentialStatus", + "rdfs:comment": "The status purpose of a credential status entry within a W3C Verifiable Credential, indicating the type of status check that can be performed (e.g. revocation, suspension, refresh, or message)." + }, + { + "@id": "untp:CredentialStatus#refresh", + "@type": "untp:CredentialStatus", + "rdf:value": "refresh", + "rdfs:label": "refresh", + "rdfs:comment": "Used to signal that an updated verifiable credential is available via the credential's refresh service feature. This status does not invalidate the verifiable credential and is not reversible." + }, + { + "@id": "untp:CredentialStatus#revocation", + "@type": "untp:CredentialStatus", + "rdf:value": "revocation", + "rdfs:label": "revocation", + "rdfs:comment": "Used to cancel the validity of a verifiable credential. This status is not reversible." + }, + { + "@id": "untp:CredentialStatus#suspension", + "@type": "untp:CredentialStatus", + "rdf:value": "suspension", + "rdfs:label": "suspension", + "rdfs:comment": "Used to temporarily prevent the acceptance of a verifiable credential. This status is reversible." + }, + { + "@id": "untp:CredentialStatus#message", + "@type": "untp:CredentialStatus", + "rdf:value": "message", + "rdfs:label": "message", + "rdfs:comment": "Used to indicate a ussuer specified flexible status message associated with a verifiable credential. The status message descriptions MUST be defined in credentialSubject.statusMessages. credentialSubject.statusSize MUST be specified when this statusPurpose value is used." + }, + { + "@id": "untp:CriterionStatus", + "@type": "rdfs:Class", + "rdfs:label": "CriterionStatus", + "rdfs:comment": "The status of the conformity profile or criterion" + }, + { + "@id": "untp:CriterionStatus#proposed", + "@type": "untp:CriterionStatus", + "rdf:value": "proposed", + "rdfs:label": "Proposed", + "rdfs:comment": "The criterion is proposed" + }, + { + "@id": "untp:CriterionStatus#active", + "@type": "untp:CriterionStatus", + "rdf:value": "active", + "rdfs:label": "Active", + "rdfs:comment": "The criterion is in active use." + }, + { + "@id": "untp:CriterionStatus#deprecated", + "@type": "untp:CriterionStatus", + "rdf:value": "deprecated", + "rdfs:label": "Deprecated", + "rdfs:comment": "The criterion is deprecated." + }, + { + "@id": "untp:LicenseType", + "@type": "rdfs:Class", + "rdfs:label": "LicenseType", + "rdfs:comment": "The license type of the published vocabulary" + }, + { + "@id": "untp:LicenseType#proprietary-Code", + "@type": "untp:LicenseType", + "rdf:value": "proprietary-Code", + "rdfs:label": "Proprietary", + "rdfs:comment": "Commercial software, internal docs. Restrictiveness - Very high" + }, + { + "@id": "untp:LicenseType#proprietary-Document", + "@type": "untp:LicenseType", + "rdf:value": "proprietary-Document", + "rdfs:label": "Documentation licenses", + "rdfs:comment": "Manuals, standards. Restrictiveness - Medium" + }, + { + "@id": "untp:LicenseType#permissive-OpenSource", + "@type": "untp:LicenseType", + "rdf:value": "permissive-OpenSource", + "rdfs:label": "Permissive open source", + "rdfs:comment": "Libraries, frameworks. Restrictiveness - Low" + }, + { + "@id": "untp:LicenseType#copyleft", + "@type": "untp:LicenseType", + "rdf:value": "copyleft", + "rdfs:label": "Copyleft", + "rdfs:comment": "Platforms, infrastructure. Restrictiveness - Medium–high" + }, + { + "@id": "untp:LicenseType#creative-Commons", + "@type": "untp:LicenseType", + "rdf:value": "creative-Commons", + "rdfs:label": "Creative Commons", + "rdfs:comment": "Media, publications. Restrictiveness - Variable" + }, + { + "@id": "untp:LicenseType#source-Available", + "@type": "untp:LicenseType", + "rdf:value": "source-Available", + "rdfs:label": "Source-available", + "rdfs:comment": "Commercial SaaS vendors. Restrictiveness - Medium–high" + }, + { + "@id": "untp:LicenseType#public", + "@type": "untp:LicenseType", + "rdf:value": "public", + "rdfs:label": "Public domain", + "rdfs:comment": "Data, examples. Restrictiveness - None" + }, + { + "@id": "untp:MimeType", + "@type": "rdfs:Class", + "rdfs:label": "MimeType", + "rdfs:comment": "IANA multipart media encoding type " + }, + { + "@id": "untp:PartyRole", + "@type": "rdfs:Class", + "rdfs:label": "PartyRole", + "rdfs:comment": "The role for this facility - party or product - party relationship" + }, + { + "@id": "untp:PartyRole#owner", + "@type": "untp:PartyRole", + "rdf:value": "owner", + "rdfs:label": "Party that owns the product or asset" + }, + { + "@id": "untp:PartyRole#producer", + "@type": "untp:PartyRole", + "rdf:value": "producer", + "rdfs:label": "Party that extracts, grows, or produces raw materials" + }, + { + "@id": "untp:PartyRole#manufacturer", + "@type": "untp:PartyRole", + "rdf:value": "manufacturer", + "rdfs:label": "Party that manufactures or assembles the product" + }, + { + "@id": "untp:PartyRole#processor", + "@type": "untp:PartyRole", + "rdf:value": "processor", + "rdfs:label": "Party that processes or transforms materials" + }, + { + "@id": "untp:PartyRole#remanufacturer", + "@type": "untp:PartyRole", + "rdf:value": "remanufacturer", + "rdfs:label": "Party that remanufactures or refurbishes products" + }, + { + "@id": "untp:PartyRole#recycler", + "@type": "untp:PartyRole", + "rdf:value": "recycler", + "rdfs:label": "Party that recovers materials from products" + }, + { + "@id": "untp:PartyRole#operator", + "@type": "untp:PartyRole", + "rdf:value": "operator", + "rdfs:label": "Party operating a facility or process" + }, + { + "@id": "untp:PartyRole#serviceProvider", + "@type": "untp:PartyRole", + "rdf:value": "serviceProvider", + "rdfs:label": "Party providing maintenance or servicing" + }, + { + "@id": "untp:PartyRole#inspector", + "@type": "untp:PartyRole", + "rdf:value": "inspector", + "rdfs:label": "Party performing inspection or testing" + }, + { + "@id": "untp:PartyRole#certifier", + "@type": "untp:PartyRole", + "rdf:value": "certifier", + "rdfs:label": "Party issuing certification or conformity assessment" + }, + { + "@id": "untp:PartyRole#logisticsProvider", + "@type": "untp:PartyRole", + "rdf:value": "logisticsProvider", + "rdfs:label": "Party responsible for logistics operations" + }, + { + "@id": "untp:PartyRole#carrier", + "@type": "untp:PartyRole", + "rdf:value": "carrier", + "rdfs:label": "Party physically transporting the goods" + }, + { + "@id": "untp:PartyRole#consignor", + "@type": "untp:PartyRole", + "rdf:value": "consignor", + "rdfs:label": "Party sending the goods" + }, + { + "@id": "untp:PartyRole#consignee", + "@type": "untp:PartyRole", + "rdf:value": "consignee", + "rdfs:label": "Party receiving the goods" + }, + { + "@id": "untp:PartyRole#importer", + "@type": "untp:PartyRole", + "rdf:value": "importer", + "rdfs:label": "Party importing the goods into a jurisdiction" + }, + { + "@id": "untp:PartyRole#exporter", + "@type": "untp:PartyRole", + "rdf:value": "exporter", + "rdfs:label": "Party exporting the goods from a jurisdiction" + }, + { + "@id": "untp:PartyRole#distributor", + "@type": "untp:PartyRole", + "rdf:value": "distributor", + "rdfs:label": "Party distributing goods in the supply chain" + }, + { + "@id": "untp:PartyRole#retailer", + "@type": "untp:PartyRole", + "rdf:value": "retailer", + "rdfs:label": "Party selling goods to end users" + }, + { + "@id": "untp:PartyRole#brandOwner", + "@type": "untp:PartyRole", + "rdf:value": "brandOwner", + "rdfs:label": "Party responsible for the brand or product specification" + }, + { + "@id": "untp:PartyRole#regulator", + "@type": "untp:PartyRole", + "rdf:value": "regulator", + "rdfs:label": "Authority responsible for regulatory oversight" + }, + { + "@id": "untp:ImprovementIndicator", + "@type": "rdfs:Class", + "rdfs:label": "ImprovementIndicator", + "rdfs:comment": "Indicator of whether conforming performance is greater than or less than the defined threshold." + }, + { + "@id": "untp:ImprovementIndicator#higher", + "@type": "untp:ImprovementIndicator", + "rdf:value": "higher", + "rdfs:label": "higher", + "rdfs:comment": "Performance improves with a higher measured value" + }, + { + "@id": "untp:ImprovementIndicator#lower", + "@type": "untp:ImprovementIndicator", + "rdf:value": "lower", + "rdfs:label": "lower", + "rdfs:comment": "Performance improves with a lower measured value" + }, + { + "@id": "untp:AggregationType", + "@type": "rdfs:Class", + "rdfs:label": "AggregationType", + "rdfs:comment": "Indicates how to aggregate multiple values to report a single performance metric." + }, + { + "@id": "untp:AggregationType#sum", + "@type": "untp:AggregationType", + "rdf:value": "sum", + "rdfs:label": "sum", + "rdfs:comment": "Values add up (e.g. total GHG emissions across all facilities = sum of each facility's emissions)" + }, + { + "@id": "untp:AggregationType#weighted-average", + "@type": "untp:AggregationType", + "rdf:value": "weighted-average", + "rdfs:label": "weighted-average", + "rdfs:comment": "Values must be averaged weighted by volume/output (e.g. emissions intensity per kg across suppliers)" + }, + { + "@id": "untp:AggregationType#latest", + "@type": "untp:AggregationType", + "rdf:value": "latest", + "rdfs:label": "latest", + "rdfs:comment": "Only the most recent value is meaningful (e.g. a biodiversity assessment score where only the current state matters)" + }, + { + "@id": "untp:ProductIDGranularity", + "@type": "rdfs:Class", + "rdfs:label": "ProductIDGranularity", + "rdfs:comment": "Product identification granularity" + }, + { + "@id": "untp:ProductIDGranularity#model", + "@type": "untp:ProductIDGranularity", + "rdf:value": "model", + "rdfs:label": "product model level ID", + "rdfs:comment": "" + }, + { + "@id": "untp:ProductIDGranularity#batch", + "@type": "untp:ProductIDGranularity", + "rdf:value": "batch", + "rdfs:label": "product manufactured batch level ID", + "rdfs:comment": "" + }, + { + "@id": "untp:ProductIDGranularity#item", + "@type": "untp:ProductIDGranularity", + "rdf:value": "item", + "rdfs:label": "serialised item level ID", + "rdfs:comment": "" + }, + { + "@id": "untp:ProductStatus", + "@type": "rdfs:Class", + "rdfs:label": "ProductStatus", + "rdfs:comment": "The lifecycle status of a product, describing its current state from initial production through to eventual disposal or recycling. Used as the value of the disposition property on EventProduct in traceability events." + }, + { + "@id": "untp:ProductStatus#new", + "@type": "untp:ProductStatus", + "rdf:value": "new", + "rdfs:label": "New", + "rdfs:comment": "Product has been newly manufactured or produced and has not yet entered service. Equivalent to GS1 CBV Disp-active." + }, + { + "@id": "untp:ProductStatus#inTransit", + "@type": "untp:ProductStatus", + "rdf:value": "inTransit", + "rdfs:label": "In Transit", + "rdfs:comment": "Product has been shipped and is in transit between facilities. Equivalent to GS1 CBV Disp-in_transit." + }, + { + "@id": "untp:ProductStatus#active", + "@type": "untp:ProductStatus", + "rdf:value": "active", + "rdfs:label": "Active", + "rdfs:comment": "Product is in active service or use by the end customer or a downstream manufacturer. Equivalent to GS1 CBV Disp-retail_sold." + }, + { + "@id": "untp:ProductStatus#repaired", + "@type": "untp:ProductStatus", + "rdf:value": "repaired", + "rdfs:label": "Repaired", + "rdfs:comment": "Product has been repaired or refurbished to restore functionality and returned to service. Equivalent to GS1 CBV Disp-available (after a repairing step)." + }, + { + "@id": "untp:ProductStatus#recalled", + "@type": "untp:ProductStatus", + "rdf:value": "recalled", + "rdfs:label": "Recalled", + "rdfs:comment": "Product has been withdrawn from the market or service due to a safety, quality, or compliance issue. Equivalent to GS1 CBV Disp-recalled." + }, + { + "@id": "untp:ProductStatus#expired", + "@type": "untp:ProductStatus", + "rdf:value": "expired", + "rdfs:label": "Expired", + "rdfs:comment": "Product has passed its use-by, certification, or regulatory expiration date. Equivalent to GS1 CBV Disp-expired." + }, + { + "@id": "untp:ProductStatus#consumed", + "@type": "untp:ProductStatus", + "rdf:value": "consumed", + "rdfs:label": "Consumed", + "rdfs:comment": "Product has been consumed as an input to a manufacturing process and no longer exists as a separate item. No direct GS1 CBV equivalent." + }, + { + "@id": "untp:ProductStatus#recycled", + "@type": "untp:ProductStatus", + "rdf:value": "recycled", + "rdfs:label": "Recycled", + "rdfs:comment": "Product has been processed to recover constituent materials for reuse in new products. No direct GS1 CBV equivalent." + }, + { + "@id": "untp:ProductStatus#disposed", + "@type": "untp:ProductStatus", + "rdf:value": "disposed", + "rdfs:label": "Disposed", + "rdfs:comment": "Product has reached end of life and has been disposed of or destroyed without material recovery. Equivalent to GS1 CBV Disp-disposed and Disp-destroyed." + }, + { + "@id": "untp:RegistryType", + "@type": "rdfs:Class", + "rdfs:label": "RegistryType", + "rdfs:comment": "A registry category code." + }, + { + "@id": "untp:RegistryType#product", + "@type": "untp:RegistryType", + "rdf:value": "product", + "rdfs:label": "Product", + "rdfs:comment": "A register of products or product classes, such as a national product catalogue or a GS1 GTIN registry." + }, + { + "@id": "untp:RegistryType#facility", + "@type": "untp:RegistryType", + "rdf:value": "facility", + "rdfs:label": "Facility", + "rdfs:comment": "A register of facilities or sites, such as a mining cadastre, environmental permit register, or industrial facility directory." + }, + { + "@id": "untp:RegistryType#business", + "@type": "untp:RegistryType", + "rdf:value": "business", + "rdfs:label": "Business", + "rdfs:comment": "A register of business entities or legal persons, such as a national company register, VAT register, or LEI registry." + }, + { + "@id": "untp:RegistryType#trademark", + "@type": "untp:RegistryType", + "rdf:value": "trademark", + "rdfs:label": "Trademark", + "rdfs:comment": "A register of trademarks, certification marks, or other intellectual property identifiers maintained by a national or international IP office." + }, + { + "@id": "untp:RegistryType#land", + "@type": "untp:RegistryType", + "rdf:value": "land", + "rdfs:label": "Land", + "rdfs:comment": "A register of land titles, parcels, or cadastral boundaries, such as a national land registry or territorial cadastre." + }, + { + "@id": "untp:RegistryType#accreditation", + "@type": "untp:RegistryType", + "rdf:value": "accreditation", + "rdfs:label": "Accreditation", + "rdfs:comment": "A register of accredited conformity assessment bodies, maintained by a national or regional accreditation authority." + }, + { + "@id": "untp:SchemeAlignmentLevel", + "@type": "rdfs:Class", + "rdfs:label": "SchemeAlignmentLevel", + "rdfs:comment": "Alignment level of a scheme profile or criterion against a reference standard or regulation" + }, + { + "@id": "untp:SchemeAlignmentLevel#meets", + "@type": "untp:SchemeAlignmentLevel", + "rdf:value": "meets", + "rdfs:label": "Meets", + "rdfs:comment": "The scheme profile or criterion fully satisfies the requirements of the referenced standard or regulation." + }, + { + "@id": "untp:SchemeAlignmentLevel#exceeds", + "@type": "untp:SchemeAlignmentLevel", + "rdf:value": "exceeds", + "rdfs:label": "Exceeds", + "rdfs:comment": "The scheme profile or criterion goes beyond the requirements of the referenced standard or regulation, imposing stricter thresholds or broader scope." + }, + { + "@id": "untp:SchemeAlignmentLevel#partial", + "@type": "untp:SchemeAlignmentLevel", + "rdf:value": "partial", + "rdfs:label": "Partially meets", + "rdfs:comment": "The scheme profile or criterion addresses some but not all requirements of the referenced standard or regulation." + }, + { + "@id": "untp:SchemeEndorsementLevel", + "@type": "rdfs:Class", + "rdfs:label": "SchemeEndorsementLevel", + "rdfs:comment": "The level of endorsement or recognition that a conformity scheme has received from authoritative bodies, indicating the degree of independent assurance over the scheme's credibility and rigour." + }, + { + "@id": "untp:SchemeEndorsementLevel#endorsed_self", + "@type": "untp:SchemeEndorsementLevel", + "rdf:value": "endorsed_self", + "rdfs:label": "Self-declaration by scheme owner", + "rdfs:comment": "Scheme owner self-declaration using the UNTP scheme declaration template" + }, + { + "@id": "untp:SchemeEndorsementLevel#endorsed_mandate", + "@type": "untp:SchemeEndorsementLevel", + "rdf:value": "endorsed_mandate", + "rdfs:label": "Government owned or mandated scheme", + "rdfs:comment": "Ownership of scheme or mandate for adoption of scheme by national government or intergovernmental entity." + }, + { + "@id": "untp:SchemeEndorsementLevel#endorsed_accreditation", + "@type": "untp:SchemeEndorsementLevel", + "rdf:value": "endorsed_accreditation", + "rdfs:label": "Accreditation authority endorsement of scheme suitability", + "rdfs:comment": "Scheme evaluated for suitability by the Global Accreditation Cooperation Incorporated, or by an accreditation body member of the Global Mutual Recognition Arrangement for such scope, or by a Regional Accreditation Cooperation member." + }, + { + "@id": "untp:SchemeEndorsementLevel#endorsed_benchmarked", + "@type": "untp:SchemeEndorsementLevel", + "rdf:value": "endorsed_benchmarked", + "rdfs:label": "Scheme recognition by a benchmarking organisation approved to UNIDO principles and process", + "rdfs:comment": "Benchmarking of scheme by an organization approved to UNIDO benchmarking principles and process. UNIDO Global Best Practice Framework for Organisations Performing Benchmarking Activities for Certification-related Conformity Assessment Schemes 2026" + }, + { + "@id": "untp:UnitOfMeasure", + "@type": "rdfs:Class", + "rdfs:label": "UnitOfMeasure", + "rdfs:comment": "UNECE Recommendation 20 Unit of Measure codelist" + } + ] +} diff --git a/tests/fixtures/upstream/v0.7.0/vocabularies/untp-topics.jsonld b/tests/fixtures/upstream/v0.7.0/vocabularies/untp-topics.jsonld new file mode 100644 index 0000000..4d3326b --- /dev/null +++ b/tests/fixtures/upstream/v0.7.0/vocabularies/untp-topics.jsonld @@ -0,0 +1,1281 @@ +{ + "@context": { + "skos": "http://www.w3.org/2004/02/skos/core#", + "dcterms": "http://purl.org/dc/terms/", + "xsd": "http://www.w3.org/2001/XMLSchema#", + "owl": "http://www.w3.org/2002/07/owl#", + "rdfs": "http://www.w3.org/2000/01/rdf-schema#", + "sdg": "http://metadata.un.org/sdg/", + "topics": "https://vocabulary.uncefact.org/conformity-topics/", + "prefLabel": { "@id": "skos:prefLabel", "@language": "en" }, + "definition": { "@id": "skos:definition", "@language": "en" }, + "notation": "skos:notation", + "scopeNote": { "@id": "skos:scopeNote", "@language": "en" }, + "broader": { "@id": "skos:broader", "@type": "@id" }, + "narrower": { "@id": "skos:narrower", "@type": "@id", "@container": "@set" }, + "topConceptOf": { "@id": "skos:topConceptOf", "@type": "@id" }, + "hasTopConcept": { "@id": "skos:hasTopConcept", "@type": "@id", "@container": "@set" }, + "inScheme": { "@id": "skos:inScheme", "@type": "@id" }, + "relatedMatch": { "@id": "skos:relatedMatch", "@type": "@id", "@container": "@set" } + }, + "@graph": [ + { + "@id": "https://vocabulary.uncefact.org/conformity-topics/", + "@type": "skos:ConceptScheme", + "dcterms:title": { "@value": "UNTP Conformity Topic Classification", "@language": "en" }, + "dcterms:description": { "@value": "A hierarchical classification scheme for conformity topics used to categorise conformity criteria published by scheme owners. Encompasses sustainability (environmental, social, governance), product integrity, trade compliance, technical conformity, and information security domains. Designed as a common reference taxonomy for interoperable conformity assessments across regulatory frameworks and voluntary standards.", "@language": "en" }, + "dcterms:creator": "United Nations Economic Commission for Europe (UNECE)", + "dcterms:license": "https://creativecommons.org/licenses/by/4.0/", + "owl:versionInfo": "0.2.0-working", + "dcterms:issued": "2025-01-01", + "dcterms:modified": "2026-03-13", + "hasTopConcept": [ + "topics:ecological-resilience", + "topics:human-equity-and-welfare", + "topics:ethical-governance", + "topics:product-integrity", + "topics:circular-value-chains", + "topics:economic-sustainability", + "topics:health-and-safety", + "topics:systemic-sustainability", + "topics:trade-and-market-access", + "topics:technical-conformity", + "topics:information-security" + ] + }, + + { + "@id": "topics:ecological-resilience", + "@type": "skos:Concept", + "prefLabel": "Ecological Resilience", + "definition": "Environmental protection, resource conservation, and climate resilience. Covers emissions reduction, energy transition, water stewardship, waste prevention, biodiversity, and circular design.", + "notation": "01", + "topConceptOf": "https://vocabulary.uncefact.org/conformity-topics/", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "scopeNote": "UN SDGs 6, 7, 12, 13, 14, 15; OECD Guidelines Chapter VI: Environment; EU ESPR Art. 5-8 and Annex I.", + "relatedMatch": ["sdg:6", "sdg:7", "sdg:12", "sdg:13", "sdg:14", "sdg:15"], + "narrower": [ + "topics:greenhouse-gas-emissions", + "topics:renewable-energy-use", + "topics:water-conservation", + "topics:waste-minimization", + "topics:ecosystem-preservation", + "topics:forest-conservation", + "topics:recycled-material-integration", + "topics:sustainable-product-design", + "topics:chemical-safety", + "topics:air-quality-management" + ] + }, + { + "@id": "topics:greenhouse-gas-emissions", + "@type": "skos:Concept", + "prefLabel": "Greenhouse Gas Emissions", + "definition": "Measuring, reporting, and reducing greenhouse gas emissions (CO2, methane, N2O, F-gases) across production, transport, and supply chain activities.", + "notation": "01.01", + "broader": "topics:ecological-resilience", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:13"], + "scopeNote": "EU ESPR Art. 5 - Environmental Sustainability; UNTP environment.emissions." + }, + { + "@id": "topics:renewable-energy-use", + "@type": "skos:Concept", + "prefLabel": "Renewable Energy Use", + "definition": "Transition to sustainable energy sources including solar, wind, hydro, and other renewables in production and operations.", + "notation": "01.02", + "broader": "topics:ecological-resilience", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:7"], + "scopeNote": "EU ESPR Art. 7 - Energy Efficiency; UNTP environment.energy." + }, + { + "@id": "topics:water-conservation", + "@type": "skos:Concept", + "prefLabel": "Water Conservation", + "definition": "Sustainable water management including efficient use, pollution prevention, and watershed protection throughout operations and supply chains.", + "notation": "01.03", + "broader": "topics:ecological-resilience", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:6"], + "scopeNote": "EU ESPR Annex I - Water Use; UNTP environment.water." + }, + { + "@id": "topics:waste-minimization", + "@type": "skos:Concept", + "prefLabel": "Waste Minimization", + "definition": "Reducing waste generation through prevention, reuse, and improved production processes across the product lifecycle.", + "notation": "01.04", + "broader": "topics:ecological-resilience", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:12"], + "scopeNote": "EU ESPR Art. 6 - Waste Prevention; UNTP environment.waste." + }, + { + "@id": "topics:ecosystem-preservation", + "@type": "skos:Concept", + "prefLabel": "Ecosystem Preservation", + "definition": "Protecting biodiversity, natural habitats, and ecosystem services from degradation caused by production and extraction activities.", + "notation": "01.05", + "broader": "topics:ecological-resilience", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:15"], + "scopeNote": "EU ESPR Annex I - Biodiversity Impact; UNTP environment.biodiversity." + }, + { + "@id": "topics:forest-conservation", + "@type": "skos:Concept", + "prefLabel": "Forest Conservation", + "definition": "Preventing deforestation and promoting sustainable forestry practices in raw material sourcing and land use.", + "notation": "01.06", + "broader": "topics:ecological-resilience", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:15"], + "scopeNote": "EU ESPR Art. 5 - Resource Use; UNTP environment.deforestation." + }, + { + "@id": "topics:recycled-material-integration", + "@type": "skos:Concept", + "prefLabel": "Recycled Material Integration", + "definition": "Incorporation of secondary and recycled materials into production processes, reducing dependence on virgin resources.", + "notation": "01.07", + "broader": "topics:ecological-resilience", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:12"], + "scopeNote": "EU ESPR Art. 8 - Recycled Content; UNTP circularity.content." + }, + { + "@id": "topics:sustainable-product-design", + "@type": "skos:Concept", + "prefLabel": "Sustainable Product Design", + "definition": "Designing products for durability, repairability, recyclability, and minimal environmental impact throughout their lifecycle.", + "notation": "01.08", + "broader": "topics:ecological-resilience", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:12"], + "scopeNote": "EU ESPR Art. 5 - Durability and Recyclability; UNTP circularity.design." + }, + { + "@id": "topics:chemical-safety", + "@type": "skos:Concept", + "prefLabel": "Chemical Safety", + "definition": "Restriction and responsible management of hazardous substances in materials, products, and production processes.", + "notation": "01.09", + "broader": "topics:ecological-resilience", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:12"], + "scopeNote": "EU ESPR Annex I - Substance Restrictions." + }, + { + "@id": "topics:air-quality-management", + "@type": "skos:Concept", + "prefLabel": "Air Quality Management", + "definition": "Controlling and reducing non-GHG air pollutant emissions including SOx, NOx, VOCs, particulates, and ozone-depleting substances from operations and production processes.", + "notation": "01.10", + "broader": "topics:ecological-resilience", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:3", "sdg:13"], + "scopeNote": "EU ESPR Annex I - Air Emissions; WHO Air Quality Guidelines; Montreal Protocol (ozone-depleting substances)." + }, + + { + "@id": "topics:human-equity-and-welfare", + "@type": "skos:Concept", + "prefLabel": "Human Equity and Welfare", + "definition": "Protection of human rights, promotion of fair labor practices, and support for community wellbeing across operations and supply chains.", + "notation": "02", + "topConceptOf": "https://vocabulary.uncefact.org/conformity-topics/", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "scopeNote": "UN SDGs 1, 3, 4, 5, 8, 10; OECD Guidelines Chapter IV: Human Rights and Chapter V: Employment and Industrial Relations; EU ESPR Art. 10.", + "relatedMatch": ["sdg:1", "sdg:3", "sdg:4", "sdg:5", "sdg:8", "sdg:10"], + "narrower": [ + "topics:rights-and-equality", + "topics:decent-work-conditions", + "topics:workplace-safety", + "topics:community-empowerment", + "topics:worker-representation", + "topics:forced-labor-elimination", + "topics:youth-protection", + "topics:gender-equity" + ] + }, + { + "@id": "topics:rights-and-equality", + "@type": "skos:Concept", + "prefLabel": "Rights and Equality", + "definition": "Ensuring non-discrimination and equal treatment regardless of race, gender, religion, disability, or other protected characteristics.", + "notation": "02.01", + "broader": "topics:human-equity-and-welfare", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:10"], + "scopeNote": "EU ESPR Art. 10 - Social Sustainability; UNTP social.rights." + }, + { + "@id": "topics:decent-work-conditions", + "@type": "skos:Concept", + "prefLabel": "Decent Work Conditions", + "definition": "Provision of fair wages, reasonable working hours, and dignified employment conditions throughout the supply chain.", + "notation": "02.02", + "broader": "topics:human-equity-and-welfare", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:8"], + "scopeNote": "EU ESPR Annex I - Supply Chain Due Diligence; UNTP social.labour." + }, + { + "@id": "topics:workplace-safety", + "@type": "skos:Concept", + "prefLabel": "Workplace Safety", + "definition": "Protecting worker health and safety through hazard prevention, protective equipment, and safe working environments.", + "notation": "02.03", + "broader": "topics:human-equity-and-welfare", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:3"], + "scopeNote": "EU ESPR Art. 10 - Social Impact; UNTP social.safety." + }, + { + "@id": "topics:community-empowerment", + "@type": "skos:Concept", + "prefLabel": "Community Empowerment", + "definition": "Supporting local community development, livelihoods, and participation in decisions that affect their wellbeing.", + "notation": "02.04", + "broader": "topics:human-equity-and-welfare", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:1"], + "scopeNote": "EU ESPR Art. 10 - Stakeholder Engagement; UNTP social.community." + }, + { + "@id": "topics:worker-representation", + "@type": "skos:Concept", + "prefLabel": "Worker Representation", + "definition": "Respecting freedom of association, collective bargaining rights, and worker participation in workplace governance.", + "notation": "02.05", + "broader": "topics:human-equity-and-welfare", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:8"], + "scopeNote": "EU ESPR Annex I - Labor Rights." + }, + { + "@id": "topics:forced-labor-elimination", + "@type": "skos:Concept", + "prefLabel": "Forced Labor Elimination", + "definition": "Preventing all forms of forced, bonded, or compulsory labor including debt bondage and human trafficking in supply chains.", + "notation": "02.06", + "broader": "topics:human-equity-and-welfare", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:8"], + "scopeNote": "EU ESPR Art. 10 - Human Rights Due Diligence." + }, + { + "@id": "topics:youth-protection", + "@type": "skos:Concept", + "prefLabel": "Youth Protection", + "definition": "Safeguarding young workers from hazardous conditions and eliminating child labor in all forms across supply chains.", + "notation": "02.07", + "broader": "topics:human-equity-and-welfare", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:8"], + "scopeNote": "EU ESPR Annex I - Child Labor Ban." + }, + { + "@id": "topics:gender-equity", + "@type": "skos:Concept", + "prefLabel": "Gender Equity", + "definition": "Promoting gender diversity, equal opportunity, and elimination of gender-based discrimination in employment and business practices.", + "notation": "02.08", + "broader": "topics:human-equity-and-welfare", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:5"], + "scopeNote": "EU ESPR Art. 10 - Social Sustainability." + }, + + { + "@id": "topics:ethical-governance", + "@type": "skos:Concept", + "prefLabel": "Ethical Governance", + "definition": "Promoting organizational integrity, accountability, and transparent practices in business operations and decision-making.", + "notation": "03", + "topConceptOf": "https://vocabulary.uncefact.org/conformity-topics/", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "scopeNote": "UN SDG 16; OECD Guidelines Chapter II: General Policies and Chapter VII: Combating Bribery; EU ESPR Art. 11-12.", + "relatedMatch": ["sdg:16"], + "narrower": [ + "topics:anti-corruption-measures", + "topics:open-reporting", + "topics:legal-compliance", + "topics:responsible-procurement", + "topics:stakeholder-inclusion", + "topics:data-privacy", + "topics:ip-protection", + "topics:competitive-fairness" + ] + }, + { + "@id": "topics:anti-corruption-measures", + "@type": "skos:Concept", + "prefLabel": "Anti-Corruption Measures", + "definition": "Preventing bribery, extortion, and corrupt practices through policies, controls, and organizational culture.", + "notation": "03.01", + "broader": "topics:ethical-governance", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:16"], + "scopeNote": "EU ESPR Art. 11 - Governance Requirements; UNTP governance.ethics." + }, + { + "@id": "topics:open-reporting", + "@type": "skos:Concept", + "prefLabel": "Open Reporting", + "definition": "Transparent disclosure of environmental, social, and governance performance to stakeholders and the public.", + "notation": "03.02", + "broader": "topics:ethical-governance", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:16"], + "scopeNote": "EU ESPR Art. 12 - Information Requirements; UNTP governance.transparency." + }, + { + "@id": "topics:legal-compliance", + "@type": "skos:Concept", + "prefLabel": "Legal Compliance", + "definition": "Adherence to applicable laws, regulations, and legal obligations in all jurisdictions of operation.", + "notation": "03.03", + "broader": "topics:ethical-governance", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:16"], + "scopeNote": "EU ESPR Art. 4 - Compliance Obligations; UNTP governance.compliance." + }, + { + "@id": "topics:responsible-procurement", + "@type": "skos:Concept", + "prefLabel": "Responsible Procurement", + "definition": "Ethical sourcing and purchasing practices that consider environmental, social, and governance factors in supplier selection.", + "notation": "03.04", + "broader": "topics:ethical-governance", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:16"], + "scopeNote": "EU ESPR Art. 10 - Supply Chain Responsibility." + }, + { + "@id": "topics:stakeholder-inclusion", + "@type": "skos:Concept", + "prefLabel": "Stakeholder Inclusion", + "definition": "Meaningful engagement with affected parties including workers, communities, and civil society in governance processes.", + "notation": "03.05", + "broader": "topics:ethical-governance", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:16"], + "scopeNote": "EU ESPR Art. 10 - Stakeholder Dialogue." + }, + { + "@id": "topics:data-privacy", + "@type": "skos:Concept", + "prefLabel": "Data Privacy", + "definition": "Protection of personal information and responsible data handling in compliance with privacy regulations and ethical standards.", + "notation": "03.06", + "broader": "topics:ethical-governance", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:16"], + "scopeNote": "EU ESPR Art. 12 - Digital Product Passport." + }, + { + "@id": "topics:ip-protection", + "@type": "skos:Concept", + "prefLabel": "Intellectual Property Protection", + "definition": "Respecting intellectual property rights including patents, trademarks, copyrights, and trade secrets.", + "notation": "03.07", + "broader": "topics:ethical-governance", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:16"], + "scopeNote": "EU ESPR Art. 11 - Governance Standards." + }, + { + "@id": "topics:competitive-fairness", + "@type": "skos:Concept", + "prefLabel": "Competitive Fairness", + "definition": "Ensuring fair market practices, preventing anti-competitive behavior, and maintaining a level playing field.", + "notation": "03.08", + "broader": "topics:ethical-governance", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:16"], + "scopeNote": "EU ESPR Art. 11 - Governance." + }, + + { + "@id": "topics:product-integrity", + "@type": "skos:Concept", + "prefLabel": "Product Integrity", + "definition": "Ensuring products are safe, reliable, and meet quality and sustainability standards throughout their lifecycle.", + "notation": "04", + "topConceptOf": "https://vocabulary.uncefact.org/conformity-topics/", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "scopeNote": "UN SDGs 9, 12; OECD Guidelines Chapter VIII: Consumer Interests; EU ESPR Art. 4-7 and Annex I.", + "relatedMatch": ["sdg:9", "sdg:12"], + "narrower": [ + "topics:product-safety-standards", + "topics:quality-performance", + "topics:substance-control", + "topics:product-longevity", + "topics:standards-adherence", + "topics:supply-chain-traceability", + "topics:consumer-information", + "topics:end-of-life-management" + ] + }, + { + "@id": "topics:product-safety-standards", + "@type": "skos:Concept", + "prefLabel": "Product Safety Standards", + "definition": "Ensuring consumer safety through compliance with product safety requirements, testing, and hazard prevention.", + "notation": "04.01", + "broader": "topics:product-integrity", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:12"], + "scopeNote": "EU ESPR Art. 5 - Safety Requirements; UNTP social.safety." + }, + { + "@id": "topics:quality-performance", + "@type": "skos:Concept", + "prefLabel": "Quality Performance", + "definition": "Meeting defined performance specifications, functional requirements, and quality benchmarks for products and services.", + "notation": "04.02", + "broader": "topics:product-integrity", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:9"], + "scopeNote": "EU ESPR Art. 5 - Performance Standards." + }, + { + "@id": "topics:substance-control", + "@type": "skos:Concept", + "prefLabel": "Substance Control", + "definition": "Banning or restricting harmful materials and substances of concern in product composition and manufacturing.", + "notation": "04.03", + "broader": "topics:product-integrity", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:12"], + "scopeNote": "EU ESPR Annex I - Substance Restrictions." + }, + { + "@id": "topics:product-longevity", + "@type": "skos:Concept", + "prefLabel": "Product Longevity", + "definition": "Enhancing product durability, repairability, and lifespan to reduce premature obsolescence and waste.", + "notation": "04.04", + "broader": "topics:product-integrity", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:12"], + "scopeNote": "EU ESPR Art. 5 - Durability; UNTP circularity.design." + }, + { + "@id": "topics:standards-adherence", + "@type": "skos:Concept", + "prefLabel": "Standards Adherence", + "definition": "Compliance with applicable product certifications, industry standards, and regulatory requirements.", + "notation": "04.05", + "broader": "topics:product-integrity", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:9"], + "scopeNote": "EU ESPR Art. 4 - Ecodesign Requirements." + }, + { + "@id": "topics:supply-chain-traceability", + "@type": "skos:Concept", + "prefLabel": "Supply Chain Traceability", + "definition": "Tracking product origins, components, and transformations throughout the supply chain to enable transparency and accountability.", + "notation": "04.06", + "broader": "topics:product-integrity", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:12"], + "scopeNote": "EU ESPR Art. 12 - Digital Product Passport; UNTP governance.transparency." + }, + { + "@id": "topics:consumer-information", + "@type": "skos:Concept", + "prefLabel": "Consumer Information", + "definition": "Providing clear, accurate, and accessible product labeling and information to enable informed consumer choices.", + "notation": "04.07", + "broader": "topics:product-integrity", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:12"], + "scopeNote": "EU ESPR Art. 7 - Information Obligations." + }, + { + "@id": "topics:end-of-life-management", + "@type": "skos:Concept", + "prefLabel": "End-of-Life Management", + "definition": "Effective collection, recycling, and disposal processes for products at end of useful life, minimizing environmental impact.", + "notation": "04.08", + "broader": "topics:product-integrity", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:12"], + "scopeNote": "EU ESPR Art. 6 - End-of-Life Requirements." + }, + + { + "@id": "topics:circular-value-chains", + "@type": "skos:Concept", + "prefLabel": "Circular Value Chains", + "definition": "Advancing sustainability, circularity, and responsible practices throughout supply and production networks.", + "notation": "05", + "topConceptOf": "https://vocabulary.uncefact.org/conformity-topics/", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "scopeNote": "UN SDGs 8, 12, 17; OECD Guidelines Chapter II: General Policies and Chapter VI: Environment; EU ESPR Art. 8, 10.", + "relatedMatch": ["sdg:8", "sdg:12", "sdg:17"], + "narrower": [ + "topics:ethical-material-sourcing", + "topics:supplier-sustainability", + "topics:resource-circularity", + "topics:energy-optimization", + "topics:supply-chain-labor-rights", + "topics:origin-tracking", + "topics:supplier-development", + "topics:supply-chain-risk-reduction" + ] + }, + { + "@id": "topics:ethical-material-sourcing", + "@type": "skos:Concept", + "prefLabel": "Ethical Material Sourcing", + "definition": "Procuring raw materials through sustainable and responsible practices, avoiding conflict minerals and environmentally destructive extraction.", + "notation": "05.01", + "broader": "topics:circular-value-chains", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:12"], + "scopeNote": "EU ESPR Art. 10 - Supply Chain Due Diligence; UNTP governance.transparency." + }, + { + "@id": "topics:supplier-sustainability", + "@type": "skos:Concept", + "prefLabel": "Supplier Sustainability", + "definition": "Ensuring suppliers meet environmental, social, and governance requirements through assessment, monitoring, and collaboration.", + "notation": "05.02", + "broader": "topics:circular-value-chains", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:17"], + "scopeNote": "EU ESPR Art. 10 - Supply Chain Responsibility." + }, + { + "@id": "topics:resource-circularity", + "@type": "skos:Concept", + "prefLabel": "Resource Circularity", + "definition": "Promoting reuse, remanufacturing, and recycling of materials to create closed-loop resource flows.", + "notation": "05.03", + "broader": "topics:circular-value-chains", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:12"], + "scopeNote": "EU ESPR Art. 8 - Recycled Content; UNTP circularity.content." + }, + { + "@id": "topics:energy-optimization", + "@type": "skos:Concept", + "prefLabel": "Energy Optimization", + "definition": "Improving energy efficiency across supply chain operations including manufacturing, logistics, and warehousing.", + "notation": "05.04", + "broader": "topics:circular-value-chains", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:7"], + "scopeNote": "EU ESPR Art. 7 - Energy Efficiency." + }, + { + "@id": "topics:supply-chain-labor-rights", + "@type": "skos:Concept", + "prefLabel": "Supply Chain Labor Rights", + "definition": "Ensuring fair treatment of workers throughout the supply chain including subcontractors and informal workers.", + "notation": "05.05", + "broader": "topics:circular-value-chains", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:8"], + "scopeNote": "EU ESPR Annex I - Labor Standards." + }, + { + "@id": "topics:origin-tracking", + "@type": "skos:Concept", + "prefLabel": "Origin Tracking", + "definition": "Transparent documentation and verification of material and product origins throughout the supply chain.", + "notation": "05.06", + "broader": "topics:circular-value-chains", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:12"], + "scopeNote": "EU ESPR Art. 12 - Digital Product Passport." + }, + { + "@id": "topics:supplier-development", + "@type": "skos:Concept", + "prefLabel": "Supplier Development", + "definition": "Building supplier capacity and capability to meet sustainability requirements through training, support, and partnership.", + "notation": "05.07", + "broader": "topics:circular-value-chains", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:17"], + "scopeNote": "EU ESPR Art. 10 - Supply Chain Support." + }, + { + "@id": "topics:supply-chain-risk-reduction", + "@type": "skos:Concept", + "prefLabel": "Supply Chain Risk Reduction", + "definition": "Identifying, assessing, and mitigating environmental, social, and operational vulnerabilities in supply networks.", + "notation": "05.08", + "broader": "topics:circular-value-chains", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:12"], + "scopeNote": "EU ESPR Art. 10 - Risk Management." + }, + + { + "@id": "topics:economic-sustainability", + "@type": "skos:Concept", + "prefLabel": "Economic Sustainability", + "definition": "Balancing profitability with sustainable economic practices that create shared value for businesses and communities.", + "notation": "06", + "topConceptOf": "https://vocabulary.uncefact.org/conformity-topics/", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "scopeNote": "UN SDGs 8, 9; OECD Guidelines Chapter II: General Policies; EU ESPR Art. 5, 7, 10, 11.", + "relatedMatch": ["sdg:8", "sdg:9"], + "narrower": [ + "topics:business-resilience", + "topics:sustainable-investment", + "topics:green-innovation", + "topics:employment-opportunities", + "topics:regional-economic-growth", + "topics:resource-efficiency", + "topics:economic-risk-management", + "topics:supply-network-strength" + ] + }, + { + "@id": "topics:business-resilience", + "@type": "skos:Concept", + "prefLabel": "Business Resilience", + "definition": "Building long-term profitability and organizational resilience through sustainable business models and practices.", + "notation": "06.01", + "broader": "topics:economic-sustainability", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:8"], + "scopeNote": "EU ESPR Art. 11 - Governance for Sustainability." + }, + { + "@id": "topics:sustainable-investment", + "@type": "skos:Concept", + "prefLabel": "Sustainable Investment", + "definition": "Directing capital toward green initiatives, sustainable technologies, and projects with positive environmental and social outcomes.", + "notation": "06.02", + "broader": "topics:economic-sustainability", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:9"], + "scopeNote": "EU ESPR Art. 5 - Resource Efficiency." + }, + { + "@id": "topics:green-innovation", + "@type": "skos:Concept", + "prefLabel": "Green Innovation", + "definition": "Developing sustainable technologies, processes, and business models that reduce environmental impact while creating economic value.", + "notation": "06.03", + "broader": "topics:economic-sustainability", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:9"], + "scopeNote": "EU ESPR Art. 5 - Innovation Requirements." + }, + { + "@id": "topics:employment-opportunities", + "@type": "skos:Concept", + "prefLabel": "Employment Opportunities", + "definition": "Creating decent jobs and fostering inclusive economic participation through sustainable business growth.", + "notation": "06.04", + "broader": "topics:economic-sustainability", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:8"], + "scopeNote": "EU ESPR Art. 10 - Social Impact." + }, + { + "@id": "topics:regional-economic-growth", + "@type": "skos:Concept", + "prefLabel": "Regional Economic Growth", + "definition": "Supporting local economic development and equitable distribution of economic benefits in communities of operation.", + "notation": "06.05", + "broader": "topics:economic-sustainability", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:8"], + "scopeNote": "EU ESPR Art. 10 - Community Benefits; UNTP social.community." + }, + { + "@id": "topics:resource-efficiency", + "@type": "skos:Concept", + "prefLabel": "Resource Efficiency", + "definition": "Optimizing resource utilization to reduce costs and environmental impact while maintaining productivity.", + "notation": "06.06", + "broader": "topics:economic-sustainability", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:8"], + "scopeNote": "EU ESPR Art. 7 - Efficiency Standards." + }, + { + "@id": "topics:economic-risk-management", + "@type": "skos:Concept", + "prefLabel": "Economic Risk Management", + "definition": "Assessing and managing financial risks arising from environmental, social, and governance factors.", + "notation": "06.07", + "broader": "topics:economic-sustainability", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:8"], + "scopeNote": "EU ESPR Art. 11 - Governance." + }, + { + "@id": "topics:supply-network-strength", + "@type": "skos:Concept", + "prefLabel": "Supply Network Strength", + "definition": "Enhancing the stability, diversity, and resilience of value chain networks against disruption.", + "notation": "06.08", + "broader": "topics:economic-sustainability", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:9"], + "scopeNote": "EU ESPR Art. 10 - Supply Chain Resilience." + }, + + { + "@id": "topics:health-and-safety", + "@type": "skos:Concept", + "prefLabel": "Health and Safety Assurance", + "definition": "Prioritizing the health and safety of workers and communities through hazard prevention, preparedness, and wellbeing support.", + "notation": "07", + "topConceptOf": "https://vocabulary.uncefact.org/conformity-topics/", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "scopeNote": "UN SDG 3; OECD Guidelines Chapter V: Employment and Industrial Relations; EU ESPR Art. 5, 10 and Annex I.", + "relatedMatch": ["sdg:3"], + "narrower": [ + "topics:workplace-hazard-control", + "topics:emergency-readiness", + "topics:exposure-management", + "topics:living-conditions", + "topics:healthcare-access", + "topics:wellbeing-support", + "topics:nutrition-standards", + "topics:ergonomic-design" + ] + }, + { + "@id": "topics:workplace-hazard-control", + "@type": "skos:Concept", + "prefLabel": "Workplace Hazard Control", + "definition": "Systematic identification, assessment, and mitigation of workplace hazards to reduce risk of injury and illness, including incident reporting, investigation, and corrective action.", + "notation": "07.01", + "broader": "topics:health-and-safety", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:3"], + "scopeNote": "EU ESPR Art. 10 - Social Sustainability; UNTP social.safety." + }, + { + "@id": "topics:emergency-readiness", + "@type": "skos:Concept", + "prefLabel": "Emergency Readiness", + "definition": "Preparedness planning, training, and response capabilities for workplace emergencies including fire, chemical spills, and natural disasters.", + "notation": "07.02", + "broader": "topics:health-and-safety", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:3"], + "scopeNote": "EU ESPR Annex I - Safety Measures." + }, + { + "@id": "topics:exposure-management", + "@type": "skos:Concept", + "prefLabel": "Exposure Management", + "definition": "Controlling worker exposure to harmful chemical, biological, and physical agents through monitoring and protective measures.", + "notation": "07.03", + "broader": "topics:health-and-safety", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:3"], + "scopeNote": "EU ESPR Annex I - Substance Safety." + }, + { + "@id": "topics:living-conditions", + "@type": "skos:Concept", + "prefLabel": "Living Conditions", + "definition": "Ensuring safe, sanitary, and dignified accommodation for workers where employer-provided housing is applicable.", + "notation": "07.04", + "broader": "topics:health-and-safety", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:3"], + "scopeNote": "EU ESPR Art. 10 - Worker Welfare." + }, + { + "@id": "topics:healthcare-access", + "@type": "skos:Concept", + "prefLabel": "Healthcare Access", + "definition": "Providing access to medical support, occupational health services, and health insurance for workers.", + "notation": "07.05", + "broader": "topics:health-and-safety", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:3"], + "scopeNote": "EU ESPR Art. 10 - Health Provisions." + }, + { + "@id": "topics:wellbeing-support", + "@type": "skos:Concept", + "prefLabel": "Wellbeing Support", + "definition": "Addressing worker mental health, stress management, and overall wellbeing through support programs and workplace culture.", + "notation": "07.06", + "broader": "topics:health-and-safety", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:3"], + "scopeNote": "EU ESPR Art. 10 - Social Impact." + }, + { + "@id": "topics:nutrition-standards", + "@type": "skos:Concept", + "prefLabel": "Nutrition Standards", + "definition": "Ensuring safe, adequate, and nutritious food provisions for workers where employer-provided meals are applicable.", + "notation": "07.07", + "broader": "topics:health-and-safety", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:3"], + "scopeNote": "EU ESPR Art. 10 - Worker Welfare." + }, + { + "@id": "topics:ergonomic-design", + "@type": "skos:Concept", + "prefLabel": "Ergonomic Design", + "definition": "Designing safe physical work environments that minimize musculoskeletal strain and support worker comfort and productivity.", + "notation": "07.08", + "broader": "topics:health-and-safety", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:3"], + "scopeNote": "EU ESPR Art. 5 - Product Safety." + }, + + { + "@id": "topics:systemic-sustainability", + "@type": "skos:Concept", + "prefLabel": "Systemic Sustainability", + "definition": "Establishing management frameworks, policies, and processes for systematic improvement of environmental, social, and governance outcomes.", + "notation": "08", + "topConceptOf": "https://vocabulary.uncefact.org/conformity-topics/", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "scopeNote": "UN SDGs 12, 16; OECD Guidelines Chapter II: General Policies; EU ESPR Art. 4, 5, 10-12.", + "relatedMatch": ["sdg:12", "sdg:16"], + "narrower": [ + "topics:sustainability-policies", + "topics:risk-identification", + "topics:outcome-tracking", + "topics:capacity-building", + "topics:process-enhancement", + "topics:feedback-channels", + "topics:compliance-verification", + "topics:transparent-communication" + ] + }, + { + "@id": "topics:sustainability-policies", + "@type": "skos:Concept", + "prefLabel": "Sustainability Policies", + "definition": "Formal organizational commitments, policies, and targets for environmental, social, and governance performance.", + "notation": "08.01", + "broader": "topics:systemic-sustainability", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:16"], + "scopeNote": "EU ESPR Art. 11 - Governance Framework." + }, + { + "@id": "topics:risk-identification", + "@type": "skos:Concept", + "prefLabel": "Risk Identification", + "definition": "Systematic assessment and prioritization of environmental, social, and governance risks across operations and supply chains.", + "notation": "08.02", + "broader": "topics:systemic-sustainability", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:12"], + "scopeNote": "EU ESPR Art. 10 - Due Diligence." + }, + { + "@id": "topics:outcome-tracking", + "@type": "skos:Concept", + "prefLabel": "Outcome Tracking", + "definition": "Monitoring, measuring, and reporting on sustainability performance against defined targets and indicators.", + "notation": "08.03", + "broader": "topics:systemic-sustainability", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:12"], + "scopeNote": "EU ESPR Art. 12 - Reporting Requirements." + }, + { + "@id": "topics:capacity-building", + "@type": "skos:Concept", + "prefLabel": "Capacity Building", + "definition": "Training and developing stakeholder knowledge and skills to implement and maintain sustainability practices.", + "notation": "08.04", + "broader": "topics:systemic-sustainability", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:12"], + "scopeNote": "EU ESPR Art. 10 - Stakeholder Support." + }, + { + "@id": "topics:process-enhancement", + "@type": "skos:Concept", + "prefLabel": "Process Enhancement", + "definition": "Continuous improvement of operational processes to achieve better sustainability outcomes over time.", + "notation": "08.05", + "broader": "topics:systemic-sustainability", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:12"], + "scopeNote": "EU ESPR Art. 5 - Performance Improvement." + }, + { + "@id": "topics:feedback-channels", + "@type": "skos:Concept", + "prefLabel": "Feedback Channels", + "definition": "Accessible grievance mechanisms, whistleblower protections, and feedback systems for workers, communities, and stakeholders to raise concerns without fear of retaliation.", + "notation": "08.06", + "broader": "topics:systemic-sustainability", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:16"], + "scopeNote": "EU ESPR Art. 10 - Stakeholder Engagement." + }, + { + "@id": "topics:compliance-verification", + "@type": "skos:Concept", + "prefLabel": "Compliance Verification", + "definition": "Independent audits, inspections, and verification processes to confirm adherence to sustainability standards and regulations.", + "notation": "08.07", + "broader": "topics:systemic-sustainability", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:16"], + "scopeNote": "EU ESPR Art. 4 - Compliance Monitoring." + }, + { + "@id": "topics:transparent-communication", + "@type": "skos:Concept", + "prefLabel": "Transparent Communication", + "definition": "Public disclosure and reporting of sustainability policies, performance, and progress to stakeholders.", + "notation": "08.08", + "broader": "topics:systemic-sustainability", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:16"], + "scopeNote": "EU ESPR Art. 12 - Information Disclosure; UNTP governance.transparency." + }, + + { + "@id": "topics:trade-and-market-access", + "@type": "skos:Concept", + "prefLabel": "Trade and Market Access", + "definition": "Adherence to trade regulations, customs requirements, market access rules, and cross-border compliance frameworks.", + "notation": "09", + "topConceptOf": "https://vocabulary.uncefact.org/conformity-topics/", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "scopeNote": "WTO TBT and SPS Agreements; UNECE Trade Facilitation Recommendations; WCO Harmonized System.", + "relatedMatch": ["sdg:17"], + "narrower": [ + "topics:import-export-controls", + "topics:customs-classification", + "topics:rules-of-origin", + "topics:sanctions-compliance", + "topics:market-authorization", + "topics:trade-documentation", + "topics:tariff-and-duty-compliance", + "topics:mutual-recognition" + ] + }, + { + "@id": "topics:import-export-controls", + "@type": "skos:Concept", + "prefLabel": "Import and Export Controls", + "definition": "Compliance with cross-border trade restrictions, licensing requirements, and controlled goods regulations.", + "notation": "09.01", + "broader": "topics:trade-and-market-access", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:17"], + "scopeNote": "WTO Trade Facilitation Agreement; national export control regimes." + }, + { + "@id": "topics:customs-classification", + "@type": "skos:Concept", + "prefLabel": "Customs Classification", + "definition": "Accurate tariff classification and customs valuation of goods in accordance with the Harmonized System and national schedules.", + "notation": "09.02", + "broader": "topics:trade-and-market-access", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:17"], + "scopeNote": "WCO Harmonized System Convention." + }, + { + "@id": "topics:rules-of-origin", + "@type": "skos:Concept", + "prefLabel": "Rules of Origin", + "definition": "Verification of product origin to determine eligibility for preferential tariff treatment under trade agreements.", + "notation": "09.03", + "broader": "topics:trade-and-market-access", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:17"], + "scopeNote": "WTO Agreement on Rules of Origin; WCO Revised Kyoto Convention." + }, + { + "@id": "topics:sanctions-compliance", + "@type": "skos:Concept", + "prefLabel": "Sanctions Compliance", + "definition": "Adherence to international trade sanctions, embargoes, and restricted party screening requirements.", + "notation": "09.04", + "broader": "topics:trade-and-market-access", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:16"], + "scopeNote": "UN Security Council sanctions; national sanctions regimes." + }, + { + "@id": "topics:market-authorization", + "@type": "skos:Concept", + "prefLabel": "Market Authorization", + "definition": "Meeting regulatory requirements for market entry including product registration, type approval, and pre-market conformity assessment.", + "notation": "09.05", + "broader": "topics:trade-and-market-access", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:17"], + "scopeNote": "WTO TBT Agreement Art. 5 - Conformity Assessment Procedures." + }, + { + "@id": "topics:trade-documentation", + "@type": "skos:Concept", + "prefLabel": "Trade Documentation", + "definition": "Accuracy, completeness, and digital exchange of trade and customs documentation including certificates, invoices, and declarations.", + "notation": "09.06", + "broader": "topics:trade-and-market-access", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:17"], + "scopeNote": "UNECE Trade Facilitation Recommendations; UN/CEFACT standards." + }, + { + "@id": "topics:tariff-and-duty-compliance", + "@type": "skos:Concept", + "prefLabel": "Tariff and Duty Compliance", + "definition": "Correct assessment, declaration, and payment of applicable customs duties, taxes, and fees.", + "notation": "09.07", + "broader": "topics:trade-and-market-access", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:17"], + "scopeNote": "WCO Revised Kyoto Convention; national customs legislation." + }, + { + "@id": "topics:mutual-recognition", + "@type": "skos:Concept", + "prefLabel": "Mutual Recognition", + "definition": "Acceptance of conformity assessment results, certifications, and test reports across jurisdictions through mutual recognition agreements.", + "notation": "09.08", + "broader": "topics:trade-and-market-access", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:17"], + "scopeNote": "WTO TBT Agreement Art. 6; ILAC and IAF mutual recognition arrangements." + }, + + { + "@id": "topics:technical-conformity", + "@type": "skos:Concept", + "prefLabel": "Technical Conformity", + "definition": "Adherence to technical regulations, voluntary standards, and conformity assessment procedures that ensure product and process fitness for purpose.", + "notation": "10", + "topConceptOf": "https://vocabulary.uncefact.org/conformity-topics/", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "scopeNote": "WTO TBT Agreement; ISO/IEC 17000 series conformity assessment standards; Codex Alimentarius.", + "relatedMatch": ["sdg:9"], + "narrower": [ + "topics:technical-regulations", + "topics:voluntary-standards", + "topics:metrology-and-measurement", + "topics:testing-and-certification", + "topics:sanitary-and-phytosanitary", + "topics:interoperability-standards", + "topics:accessibility-requirements", + "topics:performance-specifications" + ] + }, + { + "@id": "topics:technical-regulations", + "@type": "skos:Concept", + "prefLabel": "Technical Regulations", + "definition": "Compliance with mandatory government-imposed technical requirements for products, processes, and production methods.", + "notation": "10.01", + "broader": "topics:technical-conformity", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:9"], + "scopeNote": "WTO TBT Agreement Art. 2 - Technical Regulations." + }, + { + "@id": "topics:voluntary-standards", + "@type": "skos:Concept", + "prefLabel": "Voluntary Standards", + "definition": "Adherence to consensus-based standards developed by recognized standards bodies for products, services, and management systems.", + "notation": "10.02", + "broader": "topics:technical-conformity", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:9"], + "scopeNote": "WTO TBT Agreement Art. 4 - Standards; ISO, IEC, ITU standards." + }, + { + "@id": "topics:metrology-and-measurement", + "@type": "skos:Concept", + "prefLabel": "Metrology and Measurement", + "definition": "Accuracy and traceability of measurements and calibrations to national and international measurement standards.", + "notation": "10.03", + "broader": "topics:technical-conformity", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:9"], + "scopeNote": "BIPM International System of Units; OIML Recommendations." + }, + { + "@id": "topics:testing-and-certification", + "@type": "skos:Concept", + "prefLabel": "Testing and Certification", + "definition": "Third-party conformity assessment including laboratory testing, product certification, and inspection by accredited bodies.", + "notation": "10.04", + "broader": "topics:technical-conformity", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:9"], + "scopeNote": "ISO/IEC 17065 Product Certification; ISO/IEC 17025 Testing Laboratories." + }, + { + "@id": "topics:sanitary-and-phytosanitary", + "@type": "skos:Concept", + "prefLabel": "Sanitary and Phytosanitary Measures", + "definition": "Compliance with food safety, animal health, and plant health standards designed to protect human, animal, and plant life.", + "notation": "10.05", + "broader": "topics:technical-conformity", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:3"], + "scopeNote": "WTO SPS Agreement; Codex Alimentarius; OIE; IPPC." + }, + { + "@id": "topics:interoperability-standards", + "@type": "skos:Concept", + "prefLabel": "Interoperability Standards", + "definition": "Conformity with standards ensuring compatibility, data exchange, and seamless interaction between systems, components, and services.", + "notation": "10.06", + "broader": "topics:technical-conformity", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:9"], + "scopeNote": "ISO/IEC JTC 1 information technology standards; W3C web standards." + }, + { + "@id": "topics:accessibility-requirements", + "@type": "skos:Concept", + "prefLabel": "Accessibility Requirements", + "definition": "Compliance with inclusive design and accessibility standards ensuring products and services are usable by people with diverse abilities.", + "notation": "10.07", + "broader": "topics:technical-conformity", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:10"], + "scopeNote": "ISO 21542 Accessibility; WCAG 2.1; EN 301 549." + }, + { + "@id": "topics:performance-specifications", + "@type": "skos:Concept", + "prefLabel": "Performance Specifications", + "definition": "Meeting defined functional, reliability, and performance benchmarks established by regulations, standards, or contractual requirements.", + "notation": "10.08", + "broader": "topics:technical-conformity", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:9"], + "scopeNote": "Industry-specific performance standards and testing protocols." + }, + + { + "@id": "topics:information-security", + "@type": "skos:Concept", + "prefLabel": "Information Security and Digital Trust", + "definition": "Protection of data, digital systems, and information assets, and the establishment of trust frameworks for digital interactions.", + "notation": "11", + "topConceptOf": "https://vocabulary.uncefact.org/conformity-topics/", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "scopeNote": "ISO/IEC 27001 Information Security; GDPR; eIDAS; NIST Cybersecurity Framework.", + "relatedMatch": ["sdg:9", "sdg:16"], + "narrower": [ + "topics:data-protection-and-privacy", + "topics:cybersecurity-controls", + "topics:digital-identity-and-trust", + "topics:access-management", + "topics:incident-response", + "topics:system-integrity", + "topics:encryption-and-data-security", + "topics:audit-and-accountability" + ] + }, + { + "@id": "topics:data-protection-and-privacy", + "@type": "skos:Concept", + "prefLabel": "Data Protection and Privacy", + "definition": "Safeguarding personal and sensitive data in compliance with privacy regulations, consent requirements, and ethical data handling principles.", + "notation": "11.01", + "broader": "topics:information-security", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:16"], + "scopeNote": "GDPR; ISO/IEC 27701 Privacy Information Management." + }, + { + "@id": "topics:cybersecurity-controls", + "@type": "skos:Concept", + "prefLabel": "Cybersecurity Controls", + "definition": "Implementation of technical and organizational security measures to protect digital infrastructure from threats and vulnerabilities.", + "notation": "11.02", + "broader": "topics:information-security", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:9"], + "scopeNote": "ISO/IEC 27001; NIST Cybersecurity Framework; IEC 62443." + }, + { + "@id": "topics:digital-identity-and-trust", + "@type": "skos:Concept", + "prefLabel": "Digital Identity and Trust", + "definition": "Verification and assurance of digital identities, credentials, and trust relationships in electronic transactions and communications.", + "notation": "11.03", + "broader": "topics:information-security", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:16"], + "scopeNote": "eIDAS Regulation; W3C Verifiable Credentials; UNTP Digital Identity Anchor." + }, + { + "@id": "topics:access-management", + "@type": "skos:Concept", + "prefLabel": "Access Management", + "definition": "Controls for authentication, authorization, and system access ensuring only authorized parties can access resources and data.", + "notation": "11.04", + "broader": "topics:information-security", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:16"], + "scopeNote": "ISO/IEC 27001 Annex A - Access Control." + }, + { + "@id": "topics:incident-response", + "@type": "skos:Concept", + "prefLabel": "Incident Response and Recovery", + "definition": "Preparedness planning, detection, response procedures, and recovery capabilities for security breaches and system failures.", + "notation": "11.05", + "broader": "topics:information-security", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:9"], + "scopeNote": "ISO/IEC 27035 Incident Management; NIST SP 800-61." + }, + { + "@id": "topics:system-integrity", + "@type": "skos:Concept", + "prefLabel": "System Integrity and Availability", + "definition": "Ensuring reliability, uptime, and integrity of digital systems through resilient architecture and continuity planning.", + "notation": "11.06", + "broader": "topics:information-security", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:9"], + "scopeNote": "ISO 22301 Business Continuity; ISO/IEC 27001 Availability Controls." + }, + { + "@id": "topics:encryption-and-data-security", + "@type": "skos:Concept", + "prefLabel": "Encryption and Data Security", + "definition": "Protection of data confidentiality and integrity in transit and at rest through cryptographic controls and key management.", + "notation": "11.07", + "broader": "topics:information-security", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:9"], + "scopeNote": "ISO/IEC 19790 Cryptographic Modules; NIST FIPS 140." + }, + { + "@id": "topics:audit-and-accountability", + "@type": "skos:Concept", + "prefLabel": "Audit Trail and Accountability", + "definition": "Logging, monitoring, and accountability mechanisms for digital activities to support compliance verification and forensic analysis.", + "notation": "11.08", + "broader": "topics:information-security", + "inScheme": "https://vocabulary.uncefact.org/conformity-topics/", + "relatedMatch": ["sdg:16"], + "scopeNote": "ISO/IEC 27001 Annex A - Logging and Monitoring." + } + ] +} diff --git a/tests/fixtures/valid/untp-dpp-battery-instance-0.7.0.json b/tests/fixtures/valid/untp-dpp-battery-instance-0.7.0.json new file mode 100644 index 0000000..5a39115 --- /dev/null +++ b/tests/fixtures/valid/untp-dpp-battery-instance-0.7.0.json @@ -0,0 +1,720 @@ +{ + "type": [ + "DigitalProductPassport", + "VerifiableCredential" + ], + "@context": [ + "https://www.w3.org/ns/credentials/v2", + "https://vocabulary.uncefact.org/untp/0.7.0/context/" + ], + "id": "https://credentials.sample-battery.example.com/dpp/bat-75kwh-2025", + "issuer": { + "type": [ + "CredentialIssuer" + ], + "id": "did:web:sample-battery.example.com", + "name": "Sample Battery Mfg GmbH", + "issuerAlsoKnownAs": [ + { + "type": [ + "Party" + ], + "id": "https://sample-register.example.com/companies/BAT-001", + "name": "Sample Battery Mfg GmbH", + "registeredId": "BAT-001", + "idScheme": { + "type": [ + "IdentifierScheme" + ], + "id": "https://sample-register.example.com", + "name": "Sample Commercial Register" + } + } + ] + }, + "validFrom": "2025-03-01T00:00:00Z", + "validUntil": "2035-03-01T00:00:00Z", + "name": "Digital Product Passport — 75 kWh Li-ion Battery Pack", + "issuingSoftware": { + "id": "https://sample-software-vendor.example.com/.well-known/untp/software/passport-builder/2026.04.1", + "name": "Sample Passport Builder", + "version": "2026.04.1", + "vendor": { + "id": "did:web:sample-software-vendor.example.com", + "name": "Sample Software Vendor Inc" + } + }, + "credentialSubject": { + "type": [ + "Product" + ], + "id": "https://id.sample-battery.example.com/product/bat-75kwh-2025", + "name": "75 kWh Li-ion Battery Pack", + "description": "75 kWh NMC 811 lithium-ion battery pack for electric vehicle applications. Assembled at the Sample Battery Factory in Salzgitter, Germany. Energy density 166 Wh/kg, designed for 1500+ charge cycles.", + "idScheme": { + "type": [ + "IdentifierScheme" + ], + "id": "https://id.sample-battery.example.com", + "name": "Sample Product Identifier Scheme" + }, + "modelNumber": "BAT-NMC811-75", + "batchNumber": "2025-SZG-0342", + "itemNumber": "BAT-75-2025-00471", + "idGranularity": "item", + "productCategory": [ + { + "code": "46410", + "name": "Primary cells and primary batteries", + "definition": "Primary cells and primary batteries and parts thereof.", + "schemeId": "https://unstats.un.org/unsd/classifications/Econ/cpc/", + "schemeName": "UN Central Product Classification (CPC)" + } + ], + "characteristics": { + "batteryChemistry": "NMC 811 (LiNi0.8Mn0.1Co0.1O2)", + "batteryCategory": "EV", + "ratedCapacity": { + "value": 150, + "unit": "Ah" + }, + "certifiedUsableEnergy": { + "value": 75, + "unit": "kWh" + }, + "nominalVoltage": { + "value": 400, + "unit": "V" + }, + "minimumVoltage": { + "value": 280, + "unit": "V" + }, + "maximumVoltage": { + "value": 450, + "unit": "V" + }, + "originalPowerCapability": { + "value": 250000, + "unit": "W" + }, + "maximumPermittedPower": { + "value": 270000, + "unit": "W" + }, + "initialInternalResistance": { + "cell": { + "value": 0.8, + "unit": "mOhm" + }, + "pack": { + "value": 45, + "unit": "mOhm" + } + }, + "expectedLifetimeYears": 15, + "expectedLifetimeCycles": 1500, + "capacityThresholdForExhaustion": 80, + "temperatureRangeIdleState": { + "lower": -20, + "upper": 50, + "unit": "CEL" + }, + "initialSelfDischargeRate": { + "value": 2, + "unit": "%/month" + }, + "initialRoundTripEnergyEfficiency": 95, + "extinguishingAgent": "Class D dry powder or CO2", + "warrantyPeriodMonths": 96 + }, + "relatedDocument": [ + { + "linkURL": "https://credentials.sample-vap-cab.example.com/dcc/battery-003", + "linkName": "RBA VAP Certification — Sample Battery Factory", + "mediaType": "application/ld+json", + "linkType": "https://test.uncefact.org/vocabulary/linkTypes/dcc" + }, + { + "linkURL": "https://docs.sample-battery.example.com/dismantling/BAT-NMC811-75-manual.pdf", + "linkName": "Dismantling and Disassembly Manual", + "mediaType": "application/pdf", + "linkType": "https://test.uncefact.org/vocabulary/linkTypes/dismantlingInfo" + }, + { + "linkURL": "https://docs.sample-battery.example.com/due-diligence/2025-report.pdf", + "linkName": "Supply Chain Due Diligence Report 2025", + "mediaType": "application/pdf", + "linkType": "https://test.uncefact.org/vocabulary/linkTypes/dueDiligenceReport" + }, + { + "linkURL": "https://docs.sample-battery.example.com/carbon-footprint/BAT-NMC811-75-study.pdf", + "linkName": "Carbon Footprint Study — 75 kWh Battery Pack", + "mediaType": "application/pdf", + "linkType": "https://test.uncefact.org/vocabulary/linkTypes/carbonFootprintStudy" + }, + { + "linkURL": "https://docs.sample-battery.example.com/spare-parts/BAT-NMC811-75.html", + "linkName": "Spare Parts and Service Information", + "mediaType": "text/html", + "linkType": "https://test.uncefact.org/vocabulary/linkTypes/sparePartsInfo" + }, + { + "linkURL": "https://docs.sample-battery.example.com/safety/BAT-NMC811-75-measures.pdf", + "linkName": "Safety Measures", + "mediaType": "application/pdf", + "linkType": "https://test.uncefact.org/vocabulary/linkTypes/safetyMeasures" + }, + { + "linkURL": "https://docs.sample-battery.example.com/end-of-life/battery-collection-guidance.pdf", + "linkName": "Battery Collection and End-of-Life Treatment Guidance", + "mediaType": "application/pdf", + "linkType": "https://test.uncefact.org/vocabulary/linkTypes/endOfLifeInfo" + }, + { + "linkURL": "https://docs.sample-battery.example.com/conformity/eu-doc-BAT-NMC811-75.pdf", + "linkName": "EU Declaration of Conformity", + "mediaType": "application/pdf", + "linkType": "https://test.uncefact.org/vocabulary/linkTypes/euDeclarationOfConformity" + } + ], + "relatedParty": [ + { + "role": "manufacturer", + "party": { + "type": [ + "Party" + ], + "id": "did:web:sample-battery.example.com", + "name": "Sample Battery Mfg GmbH", + "registeredId": "BAT-001", + "idScheme": { + "type": [ + "IdentifierScheme" + ], + "id": "https://sample-register.example.com", + "name": "Sample Commercial Register" + }, + "registrationCountry": { + "countryCode": "DE", + "countryName": "Germany" + } + } + } + ], + "producedAtFacility": { + "type": [ + "Facility" + ], + "id": "https://facility-register.example.com/fac-003", + "name": "Sample Battery Factory", + "registeredId": "fac-003", + "idScheme": { + "type": [ + "IdentifierScheme" + ], + "id": "https://facility-register.example.com", + "name": "UNTP Sample Facility Register" + } + }, + "productionDate": "2025-03-01", + "countryOfProduction": { + "countryCode": "DE", + "countryName": "Germany" + }, + "dimensions": { + "weight": { + "value": 450, + "unit": "KGM" + }, + "length": { + "value": 2100, + "unit": "MMT" + }, + "width": { + "value": 1500, + "unit": "MMT" + }, + "height": { + "value": 150, + "unit": "MMT" + } + }, + "materialProvenance": [ + { + "name": "Copper cathode", + "originCountry": { + "countryCode": "JP", + "countryName": "Japan" + }, + "materialType": { + "code": "41521", + "name": "Unwrought copper", + "definition": "Copper, unrefined; copper anodes for electrolytic refining; refined copper and copper alloys, unwrought.", + "schemeId": "https://unstats.un.org/unsd/classifications/Econ/cpc/", + "schemeName": "UN Central Product Classification (CPC)" + }, + "massFraction": 0.08, + "mass": { + "value": 36, + "unit": "KGM" + }, + "recycledMassFraction": 0.12, + "hazardous": false + }, + { + "name": "Cobalt sulphate", + "originCountry": { + "countryCode": "CD", + "countryName": "Congo (Democratic Republic of the)" + }, + "materialType": { + "code": "14210", + "name": "Cobalt ores and concentrates", + "definition": "Cobalt ores and concentrates.", + "schemeId": "https://unstats.un.org/unsd/classifications/Econ/cpc/", + "schemeName": "UN Central Product Classification (CPC)" + }, + "massFraction": 0.05, + "mass": { + "value": 22.5, + "unit": "KGM" + }, + "recycledMassFraction": 0.16, + "hazardous": false + }, + { + "name": "Lithium carbonate", + "originCountry": { + "countryCode": "CL", + "countryName": "Chile" + }, + "materialType": { + "code": "14290", + "name": "Other non-ferrous metal ores and concentrates", + "definition": "Lithium, beryllium, and other non-ferrous metal ores and concentrates.", + "schemeId": "https://unstats.un.org/unsd/classifications/Econ/cpc/", + "schemeName": "UN Central Product Classification (CPC)" + }, + "massFraction": 0.12, + "mass": { + "value": 54, + "unit": "KGM" + }, + "recycledMassFraction": 0.06, + "hazardous": false + }, + { + "name": "Nickel sulphate", + "originCountry": { + "countryCode": "ID", + "countryName": "Indonesia" + }, + "materialType": { + "code": "14230", + "name": "Nickel ores and concentrates", + "definition": "Nickel ores and concentrates.", + "schemeId": "https://unstats.un.org/unsd/classifications/Econ/cpc/", + "schemeName": "UN Central Product Classification (CPC)" + }, + "massFraction": 0.35, + "mass": { + "value": 157.5, + "unit": "KGM" + }, + "recycledMassFraction": 0.08, + "hazardous": false + }, + { + "name": "Graphite (anode material)", + "originCountry": { + "countryCode": "MZ", + "countryName": "Mozambique" + }, + "materialType": { + "code": "15310", + "name": "Natural graphite", + "definition": "Natural graphite.", + "schemeId": "https://unstats.un.org/unsd/classifications/Econ/cpc/", + "schemeName": "UN Central Product Classification (CPC)" + }, + "massFraction": 0.18, + "mass": { + "value": 81, + "unit": "KGM" + }, + "recycledMassFraction": 0, + "hazardous": false + }, + { + "name": "Other components (electrolyte, separator, casing, BMS)", + "originCountry": { + "countryCode": "DE", + "countryName": "Germany" + }, + "materialType": { + "code": "46410", + "name": "Primary cells and primary batteries", + "definition": "Primary cells and primary batteries and parts thereof.", + "schemeId": "https://unstats.un.org/unsd/classifications/Econ/cpc/", + "schemeName": "UN Central Product Classification (CPC)" + }, + "massFraction": 0.22, + "mass": { + "value": 99, + "unit": "KGM" + }, + "recycledMassFraction": 0.25, + "hazardous": false + } + ], + "packaging": { + "description": "Reinforced steel transit crate with foam inserts", + "dimensions": { + "weight": { + "value": 35, + "unit": "KGM" + }, + "length": { + "value": 2300, + "unit": "MMT" + }, + "width": { + "value": 1700, + "unit": "MMT" + }, + "height": { + "value": 350, + "unit": "MMT" + } + }, + "materialUsed": [ + { + "name": "Steel crate", + "originCountry": { + "countryCode": "DE", + "countryName": "Germany" + }, + "materialType": { + "code": "41211", + "name": "Flat-rolled products of iron or non-alloy steel", + "schemeId": "https://unstats.un.org/unsd/classifications/Econ/cpc/", + "schemeName": "UN Central Product Classification (CPC)" + }, + "massFraction": 0.85, + "recycledMassFraction": 0.7, + "hazardous": false + } + ] + }, + "productLabel": [ + { + "name": "CE Marking", + "description": "EU conformity marking for the battery pack", + "imageData": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==", + "mediaType": "image/png" + }, + { + "name": "Separate Collection Symbol", + "description": "Crossed-out wheeled bin indicating separate collection requirement per EU Battery Regulation Article 13", + "imageData": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==", + "mediaType": "image/png" + }, + { + "name": "Carbon Footprint Performance Class", + "description": "Battery carbon footprint class label (Class B) per EU Battery Regulation Article 7", + "imageData": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==", + "mediaType": "image/png" + } + ], + "performanceClaim": [ + { + "type": [ + "Claim" + ], + "id": "https://sample-battery.example.com/claims/battery-carbon-2025", + "name": "Battery Carbon Footprint", + "description": "Cradle-to-gate carbon footprint of the 75 kWh battery pack per kWh of capacity, covering all lifecycle stages as required by EU Battery Regulation Article 7.", + "referenceRegulation": [ + { + "type": [ + "Regulation" + ], + "id": "https://eur-lex.europa.eu/eli/reg/2023/1542/oj", + "name": "EU Battery Regulation (EU) 2023/1542" + } + ], + "referenceCriteria": [ + { + "type": [ + "Criterion" + ], + "id": "https://sample-scheme.responsiblebusiness.org/criteria/ghg-reporting/v8", + "name": "GHG Emissions Reporting (RBA Code of Conduct Section C.1)" + } + ], + "claimDate": "2025-03-01", + "claimedPerformance": [ + { + "metric": { + "type": [ + "PerformanceMetric" + ], + "id": "https://vocabulary.uncefact.org/performance-metric/product-carbon-footprint", + "name": "Product Carbon Footprint" + }, + "measure": { + "value": 61, + "unit": "KGM" + }, + "score": { + "code": "B", + "rank": 2, + "definition": "Carbon footprint performance class B per EU Battery Regulation" + } + } + ], + "evidence": [ + { + "linkURL": "https://credentials.sample-cab.example.com/dcc/carbon-verification-bat-75kwh", + "linkName": "Carbon Footprint Verification — Sample CAB", + "mediaType": "application/ld+json", + "linkType": "https://test.uncefact.org/vocabulary/linkTypes/dcc" + } + ], + "conformityTopic": [ + { + "type": [ + "ConformityTopic" + ], + "id": "https://vocabulary.uncefact.org/conformity-topic/greenhouse-gas-emissions", + "name": "Greenhouse Gas Emissions" + } + ] + }, + { + "type": [ + "Claim" + ], + "id": "https://sample-battery.example.com/claims/battery-recycled-2025", + "name": "Recycled Content — Battery Pack", + "description": "Recycled content percentages for critical raw materials (cobalt, lithium, nickel, lead) as required by EU Battery Regulation Article 8.", + "referenceRegulation": [ + { + "type": [ + "Regulation" + ], + "id": "https://eur-lex.europa.eu/eli/reg/2023/1542/oj", + "name": "EU Battery Regulation (EU) 2023/1542" + } + ], + "referenceCriteria": [ + { + "type": [ + "Criterion" + ], + "id": "https://sample-scheme.responsiblebusiness.org/criteria/recycled-content/v8", + "name": "Recycled Content Requirements (RBA Code of Conduct Section C.5)" + } + ], + "claimDate": "2025-03-01", + "claimedPerformance": [ + { + "metric": { + "type": [ + "PerformanceMetric" + ], + "id": "https://vocabulary.uncefact.org/performance-metric/recycled-content-percentage", + "name": "Recycled Content Percentage" + }, + "measure": { + "value": 16, + "unit": "P1" + } + }, + { + "metric": { + "type": [ + "PerformanceMetric" + ], + "id": "https://vocabulary.uncefact.org/performance-metric/recycled-content-percentage", + "name": "Cobalt Recycled Content" + }, + "measure": { + "value": 16, + "unit": "P1" + } + }, + { + "metric": { + "type": [ + "PerformanceMetric" + ], + "id": "https://vocabulary.uncefact.org/performance-metric/recycled-content-percentage", + "name": "Lithium Recycled Content" + }, + "measure": { + "value": 6, + "unit": "P1" + } + }, + { + "metric": { + "type": [ + "PerformanceMetric" + ], + "id": "https://vocabulary.uncefact.org/performance-metric/recycled-content-percentage", + "name": "Nickel Recycled Content" + }, + "measure": { + "value": 8, + "unit": "P1" + } + } + ], + "conformityTopic": [ + { + "type": [ + "ConformityTopic" + ], + "id": "https://vocabulary.uncefact.org/conformity-topic/recycled-material-integration", + "name": "Recycled Material Integration" + } + ] + }, + { + "type": [ + "Claim" + ], + "id": "https://sample-battery.example.com/claims/battery-efficiency-2025", + "name": "Round Trip Energy Efficiency", + "description": "Initial round trip energy efficiency and projected efficiency at 50% of cycle-life.", + "referenceRegulation": [ + { + "type": [ + "Regulation" + ], + "id": "https://eur-lex.europa.eu/eli/reg/2023/1542/oj", + "name": "EU Battery Regulation (EU) 2023/1542" + } + ], + "referenceCriteria": [ + { + "type": [ + "Criterion" + ], + "id": "https://sample-scheme.responsiblebusiness.org/criteria/energy-efficiency/v8", + "name": "Energy Efficiency (RBA Code of Conduct Section C.3)" + } + ], + "claimDate": "2025-03-01", + "claimedPerformance": [ + { + "metric": { + "type": [ + "PerformanceMetric" + ], + "id": "https://vocabulary.uncefact.org/performance-metric/energy-intensity", + "name": "Initial Round Trip Energy Efficiency" + }, + "measure": { + "value": 95, + "unit": "P1" + } + }, + { + "metric": { + "type": [ + "PerformanceMetric" + ], + "id": "https://vocabulary.uncefact.org/performance-metric/energy-intensity", + "name": "Round Trip Energy Efficiency at 50% Cycle-life" + }, + "measure": { + "value": 90, + "unit": "P1" + } + } + ], + "conformityTopic": [ + { + "type": [ + "ConformityTopic" + ], + "id": "https://vocabulary.uncefact.org/conformity-topic/energy-optimization", + "name": "Energy Optimization" + } + ] + }, + { + "type": [ + "Claim" + ], + "id": "https://sample-battery.example.com/claims/battery-due-diligence-2025", + "name": "Ethical Material Sourcing", + "description": "Compliance with supply chain due diligence obligations under EU Battery Regulation Articles 48-52 and OECD Due Diligence Guidance for Responsible Supply Chains of Minerals.", + "referenceRegulation": [ + { + "type": [ + "Regulation" + ], + "id": "https://eur-lex.europa.eu/eli/reg/2023/1542/oj", + "name": "EU Battery Regulation (EU) 2023/1542" + } + ], + "referenceStandard": [ + { + "type": [ + "Standard" + ], + "id": "https://www.oecd.org/corporate/mne/mining.htm", + "name": "OECD Due Diligence Guidance for Responsible Supply Chains of Minerals" + } + ], + "referenceCriteria": [ + { + "type": [ + "Criterion" + ], + "id": "https://sample-scheme.responsiblebusiness.org/criteria/responsible-minerals/v8", + "name": "Responsible Minerals Sourcing (RBA Code of Conduct Section C.7)" + } + ], + "claimDate": "2025-03-01", + "claimedPerformance": [ + { + "metric": { + "type": [ + "PerformanceMetric" + ], + "id": "https://vocabulary.uncefact.org/performance-metric/supplier-due-diligence-coverage", + "name": "Supplier Due Diligence Coverage" + }, + "score": { + "code": "compliant", + "rank": 1, + "definition": "Fully compliant with due diligence obligations" + } + } + ], + "evidence": [ + { + "linkURL": "https://credentials.sample-cab.example.com/dcc/due-diligence-bat-2025", + "linkName": "Third-party Due Diligence Assurance — Sample CAB", + "mediaType": "application/ld+json", + "linkType": "https://test.uncefact.org/vocabulary/linkTypes/dcc" + } + ], + "conformityTopic": [ + { + "type": [ + "ConformityTopic" + ], + "id": "https://vocabulary.uncefact.org/conformity-topic/ethical-material-sourcing", + "name": "Ethical Material Sourcing" + } + ] + } + ] + } +} diff --git a/tests/fixtures/valid/untp-dpp-cathode-instance-0.7.0.json b/tests/fixtures/valid/untp-dpp-cathode-instance-0.7.0.json new file mode 100644 index 0000000..2e87963 --- /dev/null +++ b/tests/fixtures/valid/untp-dpp-cathode-instance-0.7.0.json @@ -0,0 +1,284 @@ +{ + "type": [ + "DigitalProductPassport", + "VerifiableCredential" + ], + "@context": [ + "https://www.w3.org/ns/credentials/v2", + "https://vocabulary.uncefact.org/untp/0.7.0/context/" + ], + "id": "https://credentials.sample-refinery.example.com/dpp/cu-cathode-2025", + "issuer": { + "type": [ + "CredentialIssuer" + ], + "id": "did:web:sample-refinery.example.com", + "name": "Sample Copper Refinery Co. Ltd", + "issuerAlsoKnownAs": [ + { + "type": [ + "Party" + ], + "id": "https://www.sample-register.example.com/henkorireki-johoto.html?selHouzinNo=REF-001", + "name": "Sample Copper Refinery Co. Ltd", + "registeredId": "REF-001", + "idScheme": { + "type": [ + "IdentifierScheme" + ], + "id": "https://www.sample-register.example.com", + "name": "Japan Corporate Number (Houjin Bangou)" + } + } + ] + }, + "validFrom": "2025-03-01T00:00:00Z", + "validUntil": "2026-03-01T00:00:00Z", + "name": "Digital Product Passport — LME Grade A Copper Cathode", + "issuingSoftware": { + "id": "https://sample-software-vendor.example.com/.well-known/untp/software/passport-builder/2026.04.1", + "name": "Sample Passport Builder", + "version": "2026.04.1", + "vendor": { + "id": "did:web:sample-software-vendor.example.com", + "name": "Sample Software Vendor Inc" + } + }, + "credentialSubject": { + "type": [ + "Product" + ], + "id": "https://id.sample-refinery.example.com/product/cu-cathode-2025", + "name": "LME Grade A Copper Cathode", + "description": "LME Grade A copper cathode (Cu 99.99%) produced by electrolytic refining at Sample Copper Refinery. Each cathode weighs approximately 125 kg and meets London Metal Exchange delivery specifications.", + "idScheme": { + "type": [ + "IdentifierScheme" + ], + "id": "https://id.sample-refinery.example.com", + "name": "Sample Product Identifier Scheme" + }, + "modelNumber": "SR-CU-CATH-9999", + "batchNumber": "2025-Q1-0812", + "idGranularity": "model", + "productCategory": [ + { + "code": "41521", + "name": "Unwrought copper", + "definition": "Copper, unrefined; copper anodes for electrolytic refining; refined copper and copper alloys, unwrought.", + "schemeId": "https://unstats.un.org/unsd/classifications/Econ/cpc/", + "schemeName": "UN Central Product Classification (CPC)" + } + ], + "relatedDocument": [ + { + "linkURL": "https://credentials.sample-cab.example.com/dcc/smelter-002", + "linkName": "Coppermark Certification — Sample Copper Refinery", + "mediaType": "application/ld+json", + "linkType": "https://test.uncefact.org/vocabulary/linkTypes/dcc" + } + ], + "relatedParty": [ + { + "role": "manufacturer", + "party": { + "type": [ + "Party" + ], + "id": "did:web:sample-refinery.example.com", + "name": "Sample Copper Refinery Co. Ltd", + "registeredId": "REF-001", + "idScheme": { + "type": [ + "IdentifierScheme" + ], + "id": "https://www.sample-register.example.com", + "name": "Japan Corporate Number (Houjin Bangou)" + }, + "registrationCountry": { + "countryCode": "JP", + "countryName": "Japan" + } + } + } + ], + "producedAtFacility": { + "type": [ + "Facility" + ], + "id": "https://facility-register.example.com/fac-002", + "name": "Sample Copper Refinery", + "registeredId": "fac-002", + "idScheme": { + "type": [ + "IdentifierScheme" + ], + "id": "https://facility-register.example.com", + "name": "UNTP Sample Facility Register" + } + }, + "productionDate": "2025-03-01", + "countryOfProduction": { + "countryCode": "JP", + "countryName": "Japan" + }, + "dimensions": { + "weight": { + "value": 125, + "unit": "KGM" + } + }, + "materialProvenance": [ + { + "name": "Copper concentrate", + "originCountry": { + "countryCode": "ZM", + "countryName": "Zambia" + }, + "materialType": { + "code": "14110", + "name": "Copper ores and concentrates", + "definition": "Copper ores and concentrates obtained from mining operations.", + "schemeId": "https://unstats.un.org/unsd/classifications/Econ/cpc/", + "schemeName": "UN Central Product Classification (CPC)" + }, + "massFraction": 0.88, + "mass": { + "value": 110, + "unit": "KGM" + }, + "recycledMassFraction": 0, + "hazardous": false + }, + { + "name": "Recycled copper scrap", + "originCountry": { + "countryCode": "JP", + "countryName": "Japan" + }, + "materialType": { + "code": "41521", + "name": "Unwrought copper", + "definition": "Copper, unrefined; copper anodes for electrolytic refining; refined copper and copper alloys, unwrought.", + "schemeId": "https://unstats.un.org/unsd/classifications/Econ/cpc/", + "schemeName": "UN Central Product Classification (CPC)" + }, + "massFraction": 0.12, + "mass": { + "value": 15, + "unit": "KGM" + }, + "recycledMassFraction": 1, + "hazardous": false + } + ], + "performanceClaim": [ + { + "type": [ + "Claim" + ], + "id": "https://sample-refinery.example.com/claims/product-carbon-2025", + "name": "Product Carbon Footprint — Copper Cathode", + "description": "Cradle-to-gate carbon footprint of copper cathode per tonne produced at Sample smelter.", + "referenceCriteria": [ + { + "type": [ + "Criterion" + ], + "id": "https://sample-scheme.coppermark.org/criteria/ghg-management/v3", + "name": "GHG Emissions Management (Coppermark RRA Criterion 26)", + "conformityTopic": [ + { + "type": [ + "ConformityTopic" + ], + "id": "https://vocabulary.uncefact.org/conformity-topic/greenhouse-gas-emissions", + "name": "Greenhouse Gas Emissions", + "definition": "Assessment of direct and indirect greenhouse gas emissions across scopes 1, 2, and 3, including measurement, reporting, and reduction targets aligned with climate science." + } + ] + } + ], + "claimDate": "2025-03-01", + "claimedPerformance": [ + { + "metric": { + "type": [ + "PerformanceMetric" + ], + "id": "https://vocabulary.uncefact.org/performance-metric/product-carbon-footprint", + "name": "Product Carbon Footprint" + }, + "measure": { + "value": 3.8, + "unit": "KGM" + } + } + ], + "conformityTopic": [ + { + "type": [ + "ConformityTopic" + ], + "id": "https://vocabulary.uncefact.org/conformity-topic/greenhouse-gas-emissions", + "name": "Greenhouse Gas Emissions", + "definition": "Assessment of direct and indirect greenhouse gas emissions across scopes 1, 2, and 3, including measurement, reporting, and reduction targets aligned with climate science." + } + ] + }, + { + "type": [ + "Claim" + ], + "id": "https://sample-refinery.example.com/claims/product-recycled-2025", + "name": "Recycled Content — Copper Cathode", + "description": "Percentage of recycled copper content in cathode output at Sample smelter.", + "referenceCriteria": [ + { + "type": [ + "Criterion" + ], + "id": "https://sample-scheme.coppermark.org/criteria/recycled-feedstock/v3", + "name": "Recycled Feedstock Management (Coppermark RRA Criterion 31)", + "conformityTopic": [ + { + "type": [ + "ConformityTopic" + ], + "id": "https://vocabulary.uncefact.org/conformity-topic/recycled-material-integration", + "name": "Recycled Material Integration", + "definition": "Incorporation of recycled and reclaimed materials into products and processes, promoting circular material flows and reducing demand for virgin resources." + } + ] + } + ], + "claimDate": "2025-03-01", + "claimedPerformance": [ + { + "metric": { + "type": [ + "PerformanceMetric" + ], + "id": "https://vocabulary.uncefact.org/performance-metric/recycled-content-percentage", + "name": "Recycled Content Percentage" + }, + "measure": { + "value": 12, + "unit": "P1" + } + } + ], + "conformityTopic": [ + { + "type": [ + "ConformityTopic" + ], + "id": "https://vocabulary.uncefact.org/conformity-topic/recycled-material-integration", + "name": "Recycled Material Integration", + "definition": "Incorporation of recycled and reclaimed materials into products and processes, promoting circular material flows and reducing demand for virgin resources." + } + ] + } + ] + } +} diff --git a/tests/fixtures/valid/untp-dpp-instance-0.7.0.json b/tests/fixtures/valid/untp-dpp-instance-0.7.0.json new file mode 100644 index 0000000..b8f686c --- /dev/null +++ b/tests/fixtures/valid/untp-dpp-instance-0.7.0.json @@ -0,0 +1,263 @@ +{ + "type": [ + "DigitalProductPassport", + "VerifiableCredential" + ], + "@context": [ + "https://www.w3.org/ns/credentials/v2", + "https://vocabulary.uncefact.org/untp/0.7.0/context/" + ], + "id": "https://credentials.sample-mine.example.com/dpp/cu-conc-2025", + "issuer": { + "type": [ + "CredentialIssuer" + ], + "id": "did:web:sample-mine.example.com", + "name": "Sample Copper Mine Pty Ltd", + "issuerAlsoKnownAs": [ + { + "type": [ + "Party" + ], + "id": "https://sample-register.example.com/companies/MINE-001", + "name": "Sample Copper Mine Pty Ltd", + "registeredId": "MINE-001", + "idScheme": { + "type": [ + "IdentifierScheme" + ], + "id": "https://sample-register.example.com", + "name": "Patents and Companies Registration Agency (Zambia)" + } + } + ] + }, + "validFrom": "2025-03-01T00:00:00Z", + "validUntil": "2026-03-01T00:00:00Z", + "name": "Digital Product Passport — Copper Concentrate (Cu 30%)", + "issuingSoftware": { + "id": "https://sample-software-vendor.example.com/.well-known/untp/software/passport-builder/2026.04.1", + "name": "Sample Passport Builder", + "version": "2026.04.1", + "vendor": { + "id": "did:web:sample-software-vendor.example.com", + "name": "Sample Software Vendor Inc" + } + }, + "credentialSubject": { + "type": [ + "Product" + ], + "id": "https://id.sample-mine.example.com/product/cu-conc-2025", + "name": "Copper Concentrate (Cu 30%)", + "description": "Copper sulphide flotation concentrate with approximately 30% copper content, produced at Sample Copper Mine. Suitable for smelting to produce refined copper cathode.", + "idScheme": { + "type": [ + "IdentifierScheme" + ], + "id": "https://id.sample-mine.example.com", + "name": "Sample Product Identifier Scheme" + }, + "modelNumber": "SM-CU-CONC-30", + "batchNumber": "2025-Q1-4501", + "idGranularity": "model", + "productCategory": [ + { + "code": "14110", + "name": "Copper ores and concentrates", + "definition": "Copper ores and concentrates obtained from mining operations.", + "schemeId": "https://unstats.un.org/unsd/classifications/Econ/cpc/", + "schemeName": "UN Central Product Classification (CPC)" + } + ], + "relatedDocument": [ + { + "linkURL": "https://credentials.sample-cab.example.com/dcc/mine-001", + "linkName": "Coppermark Certification — Sample Copper Mine", + "mediaType": "application/ld+json", + "linkType": "https://test.uncefact.org/vocabulary/linkTypes/dcc" + } + ], + "relatedParty": [ + { + "role": "manufacturer", + "party": { + "type": [ + "Party" + ], + "id": "did:web:sample-mine.example.com", + "name": "Sample Copper Mine Pty Ltd", + "registeredId": "MINE-001", + "idScheme": { + "type": [ + "IdentifierScheme" + ], + "id": "https://sample-register.example.com", + "name": "Patents and Companies Registration Agency (Zambia)" + }, + "registrationCountry": { + "countryCode": "ZM", + "countryName": "Zambia" + } + } + } + ], + "producedAtFacility": { + "type": [ + "Facility" + ], + "id": "https://facility-register.example.com/fac-001", + "name": "Sample Copper Mine", + "registeredId": "fac-001", + "idScheme": { + "type": [ + "IdentifierScheme" + ], + "id": "https://facility-register.example.com", + "name": "UNTP Sample Facility Register" + } + }, + "productionDate": "2025-03-01", + "countryOfProduction": { + "countryCode": "ZM", + "countryName": "Zambia" + }, + "dimensions": { + "weight": { + "value": 1000, + "unit": "KGM" + } + }, + "materialProvenance": [ + { + "name": "Copper ore", + "originCountry": { + "countryCode": "ZM", + "countryName": "Zambia" + }, + "materialType": { + "code": "14110", + "name": "Copper ores and concentrates", + "definition": "Copper ores and concentrates obtained from mining operations.", + "schemeId": "https://unstats.un.org/unsd/classifications/Econ/cpc/", + "schemeName": "UN Central Product Classification (CPC)" + }, + "massFraction": 0.3, + "mass": { + "value": 300, + "unit": "KGM" + }, + "recycledMassFraction": 0, + "hazardous": false + } + ], + "performanceClaim": [ + { + "type": [ + "Claim" + ], + "id": "https://sample-mine.example.com/claims/product-carbon-2025", + "name": "Product Carbon Footprint — Copper Concentrate", + "description": "Cradle-to-gate carbon footprint of copper concentrate per tonne produced at Sample mine.", + "referenceCriteria": [ + { + "type": [ + "Criterion" + ], + "id": "https://sample-scheme.coppermark.org/criteria/ghg-management/v3", + "name": "GHG Emissions Management (Coppermark RRA Criterion 26)", + "conformityTopic": [ + { + "type": [ + "ConformityTopic" + ], + "id": "https://vocabulary.uncefact.org/conformity-topic/greenhouse-gas-emissions", + "name": "Greenhouse Gas Emissions", + "definition": "Assessment of direct and indirect greenhouse gas emissions across scopes 1, 2, and 3, including measurement, reporting, and reduction targets aligned with climate science." + } + ] + } + ], + "claimDate": "2025-03-01", + "claimedPerformance": [ + { + "metric": { + "type": [ + "PerformanceMetric" + ], + "id": "https://vocabulary.uncefact.org/performance-metric/product-carbon-footprint", + "name": "Product Carbon Footprint" + }, + "measure": { + "value": 2.1, + "unit": "KGM" + } + } + ], + "conformityTopic": [ + { + "type": [ + "ConformityTopic" + ], + "id": "https://vocabulary.uncefact.org/conformity-topic/greenhouse-gas-emissions", + "name": "Greenhouse Gas Emissions", + "definition": "Assessment of direct and indirect greenhouse gas emissions across scopes 1, 2, and 3, including measurement, reporting, and reduction targets aligned with climate science." + } + ] + }, + { + "type": [ + "Claim" + ], + "id": "https://sample-mine.example.com/claims/product-water-2025", + "name": "Water Intensity — Copper Concentrate", + "description": "Water consumption per tonne of copper concentrate produced at Sample mine.", + "referenceCriteria": [ + { + "type": [ + "Criterion" + ], + "id": "https://sample-scheme.coppermark.org/criteria/water-stewardship/v3", + "name": "Water Stewardship (Coppermark RRA Criterion 27)", + "conformityTopic": [ + { + "type": [ + "ConformityTopic" + ], + "id": "https://vocabulary.uncefact.org/conformity-topic/water-conservation", + "name": "Water Conservation", + "definition": "Efficient and responsible management of water resources, including reduction of water consumption, recycling, and protection of water quality in operations and supply chains." + } + ] + } + ], + "claimDate": "2025-03-01", + "claimedPerformance": [ + { + "metric": { + "type": [ + "PerformanceMetric" + ], + "id": "https://vocabulary.uncefact.org/performance-metric/water-intensity", + "name": "Water Intensity" + }, + "measure": { + "value": 15, + "unit": "MTQ" + } + } + ], + "conformityTopic": [ + { + "type": [ + "ConformityTopic" + ], + "id": "https://vocabulary.uncefact.org/conformity-topic/water-conservation", + "name": "Water Conservation", + "definition": "Efficient and responsible management of water resources, including reduction of water consumption, recycling, and protection of water quality in operations and supply chains." + } + ] + } + ] + } +} diff --git a/tests/integration/test_compat_roundtrip.py b/tests/integration/test_compat_roundtrip.py new file mode 100644 index 0000000..d072af9 --- /dev/null +++ b/tests/integration/test_compat_roundtrip.py @@ -0,0 +1,183 @@ +"""Phase 4 round-trip: upgrade every 0.6.x fixture and re-validate at 0.7. + +This integration test enforces the Phase 4 exit criterion from +``docs/plans/UNTP_0.7.0_MIGRATION.md``: + +> Every 0.6.x valid fixture either upgrades and re-validates cleanly, +> or emits a documented warning. + +The test takes each enveloped 0.6.x fixture under ``tests/fixtures/valid/``, +runs it through :func:`dppvalidator.compat.upgrade_0_6_to_0_7.upgrade`, +then attempts to construct the v0.7 ``DigitalProductPassport`` model +from the result. Validation outcomes split into three buckets: + +- **CLEAN**: model construction succeeds with zero warnings — the + fixture upgrades fully. +- **WARNED**: model construction succeeds but the shim emitted at + least one ``UPG`` warning — the fixture upgrades with documented + caveats. +- **REQUIRES_MANUAL**: the shim emitted ``UPG004``-class + required-field-missing warnings or model construction fails — the + fixture cannot fully upgrade without manual data fill-in. + +The test only fails when a fixture lands in REQUIRES_MANUAL **and** +the shim emitted no warnings to explain it — i.e. the shim silently +produced an invalid result. Documented failures (warnings emitted) +are surfaced via the captured "known limitation" list and persisted +for the migration guide. +""" + +from __future__ import annotations + +import json +from pathlib import Path +from typing import Any + +import pytest +from pydantic import ValidationError + +from dppvalidator.compat.upgrade_0_6_to_0_7 import ( + UPG_CODE_REQUIRED_FIELD_MISSING, + UpgradeSeverity, + upgrade, +) +from dppvalidator.models.v0_7.envelope import DigitalProductPassport + +_FIXTURE_DIR = Path(__file__).resolve().parents[1] / "fixtures" / "valid" + + +def _enveloped_fixtures() -> list[Path]: + """Return only enveloped 0.6.x DPP fixtures. + + Phase 5 vendored the upstream 0.7.0 samples into the same + ``valid/`` directory; those are not v0.6 inputs and must be + skipped — the shim is defined for v0.6 → v0.7 only. We detect + a v0.7 payload by its context URL (the only stable on-the-wire + marker; type arrays are version-shared). + """ + out: list[Path] = [] + for p in sorted(_FIXTURE_DIR.glob("*.json")): + try: + data = json.loads(p.read_text(encoding="utf-8")) + except json.JSONDecodeError: # pragma: no cover — fixtures should parse + continue + if not (isinstance(data, dict) and "@context" in data and "credentialSubject" in data): + continue + # Skip v0.7-shaped payloads (already in target shape). + ctx = data.get("@context") or [] + if any(isinstance(c, str) and "vocabulary.uncefact.org/untp/0.7" in c for c in ctx): + continue + out.append(p) + return out + + +def _classify(upgraded: dict[str, Any], warnings: list[Any]) -> tuple[str, str | None]: + """Categorise an upgrade outcome as CLEAN / WARNED / REQUIRES_MANUAL. + + Returns a tuple of (bucket, validation_error_message_or_None). + """ + has_required_missing = any(w.code == UPG_CODE_REQUIRED_FIELD_MISSING for w in warnings) + has_error_severity = any(w.severity == UpgradeSeverity.ERROR for w in warnings) + try: + DigitalProductPassport.model_validate(upgraded) + except ValidationError as exc: + # Any ValidationError pushes us into REQUIRES_MANUAL — caller + # uses the warning set to assess whether it was expected. + return "REQUIRES_MANUAL", str(exc) + if has_required_missing or has_error_severity: + return "REQUIRES_MANUAL", None + if warnings: + return "WARNED", None + return "CLEAN", None + + +@pytest.mark.parametrize( + "fixture_path", + _enveloped_fixtures(), + ids=lambda p: p.name, +) +def test_v06_fixture_round_trip(fixture_path: Path) -> None: + """Each enveloped 0.6.x fixture either re-validates or emits warnings. + + The shim is allowed to leave a fixture in REQUIRES_MANUAL state — + that's expected for fixtures missing v0.7-required fields like + ``Material.materialType``. What's NOT allowed is a fixture + silently failing to validate without any explanatory warning: that + indicates the shim has a transformation bug. + """ + src = json.loads(fixture_path.read_text(encoding="utf-8")) + upgraded, warnings = upgrade(src) + bucket, error_msg = _classify(upgraded, warnings) + if bucket == "REQUIRES_MANUAL": + # If we landed in REQUIRES_MANUAL and the shim emitted at least + # one warning, that's an *expected* outcome — the warning + # explains why. We still log enough to make the migration + # guide entry obvious. + assert warnings, ( + f"{fixture_path.name} failed to validate at 0.7 and the shim " + f"emitted no warnings — silent shim bug.\n" + f"Validation error: {error_msg}" + ) + + +def test_at_least_one_fixture_round_trips_cleanly() -> None: + """Smoke check that the CLEAN / WARNED path is reachable. + + A pure-acceptance integration test that's worthless unless we know + the shim *can* produce a valid output for *some* 0.6.x input. We + construct a hand-crafted fixture that has every v0.7-required + field already populated in v0.6 shape, then assert it round-trips + cleanly into a v0.7 model. + """ + src = { + "type": ["DigitalProductPassport", "VerifiableCredential"], + "@context": [ + "https://www.w3.org/ns/credentials/v2", + "https://test.uncefact.org/vocabulary/untp/dpp/0.6.1/", + ], + "id": "https://example.com/credentials/clean", + "name": "Clean fixture", + "issuer": { + "type": ["CredentialIssuer"], + "id": "did:example:1", + "name": "Example", + }, + "validFrom": "2024-01-01T00:00:00Z", + "credentialSubject": { + "type": ["ProductPassport"], + "id": "https://example.com/subject/1", + "granularityLevel": "model", + "product": { + "type": ["Product"], + "id": "https://example.com/products/1", + "name": "Clean product", + "idScheme": { + "type": ["IdentifierScheme"], + "id": "https://id.example.com", + "name": "Example scheme", + }, + "productCategory": { + "type": ["Classification"], + "schemeID": "https://unstats.un.org/cpc/", + "schemeName": "UN CPC", + "code": "12345", + "name": "Example category", + }, + "producedAtFacility": { + "type": ["Facility"], + "id": "https://facilities.example.com/1", + "name": "Example facility", + }, + "countryOfProduction": "DE", + }, + }, + } + upgraded, warnings = upgrade(src, country_lookup={"DE": "Germany"}) + # The model accepts the upgraded payload. + DigitalProductPassport.model_validate(upgraded) + # Only INFO-grade events fire — no required-field warnings, no + # blocking severities. + blocking = [w for w in warnings if w.severity != UpgradeSeverity.INFO] + assert not blocking, ( + f"Expected zero blocking warnings, got: {[(w.code, w.path, w.message) for w in blocking]}" + ) diff --git a/tests/integration/test_example_plugin.py b/tests/integration/test_example_plugin.py new file mode 100644 index 0000000..b557000 --- /dev/null +++ b/tests/integration/test_example_plugin.py @@ -0,0 +1,458 @@ +"""Phase 6 acceptance test: example plugin integration coverage. + +The exit criterion from §Phase 6 of +``docs/plans/UNTP_0.7.0_MIGRATION.md``: + +> ``pip install -e examples/dppvalidator_example_plugin && pytest +> tests/integration/test_example_plugin.py`` is green. + +This module makes the example plugin a CI-tested target so any +public-API regression in the core (renaming a model attribute, moving +a module, breaking the ``SemanticRule`` protocol) is caught +immediately. + +The plugin is treated as an editable optional dependency: if it isn't +installed when the suite runs, the tests skip with a clear pointer to +the install command rather than failing. This keeps the plugin +opt-in while still wiring it into nightly CI. +""" + +from __future__ import annotations + +import importlib +import importlib.util +from typing import Any + +import pytest + + +def _plugin_installed() -> bool: + """Return True when ``dppvalidator_example_plugin`` is importable.""" + return importlib.util.find_spec("dppvalidator_example_plugin") is not None + + +pytestmark = pytest.mark.skipif( + not _plugin_installed(), + reason=( + "example plugin not installed; run " + "`uv pip install -e examples/dppvalidator_example_plugin` first" + ), +) + + +# --------------------------------------------------------------------------- +# Fixtures: build v0.6 / v0.7 passports for the rules to chew on +# --------------------------------------------------------------------------- + + +@pytest.fixture +def v06_passport() -> Any: + """Construct a minimal v0.6 ``DigitalProductPassport`` instance. + + Uses the top-level alias ``dppvalidator.models.passport`` — + that's the import the plugin's v0.6 rule expects to see, and the + public-API stability rule in §4.1.8 of the migration plan + promises this entry point keeps working in 0.4.0. + """ + from dppvalidator.models.passport import DigitalProductPassport + + payload: dict[str, Any] = { + "@context": [ + "https://www.w3.org/ns/credentials/v2", + "https://test.uncefact.org/vocabulary/untp/dpp/0.6.1/", + ], + "type": ["DigitalProductPassport", "VerifiableCredential"], + "id": "https://example.com/credentials/test-v06", + "issuer": {"id": "did:example:1", "name": "Example"}, + "validFrom": "2024-01-01T00:00:00Z", + "credentialSubject": { + "type": ["ProductPassport"], + "id": "https://example.com/subject/1", + "product": { + "type": ["Product"], + "id": "https://example.com/products/1", + "name": "v0.6 sample product", + }, + "materialsProvenance": [ + {"name": "Cotton", "originCountry": "EG", "massFraction": 0.6}, + {"name": "Polyester", "originCountry": "DE", "massFraction": 0.4}, + ], + }, + } + return DigitalProductPassport.model_validate(payload) + + +@pytest.fixture +def v07_passport() -> Any: + """Construct a minimal v0.7 ``DigitalProductPassport`` instance.""" + from dppvalidator.models.v0_7.envelope import DigitalProductPassport + + payload: dict[str, Any] = { + "@context": [ + "https://www.w3.org/ns/credentials/v2", + "https://vocabulary.uncefact.org/untp/0.7.0/context/", + ], + "type": ["DigitalProductPassport", "VerifiableCredential"], + "id": "https://example.com/credentials/test-v07", + "name": "Sample DPP", + "issuer": { + "type": ["CredentialIssuer"], + "id": "did:example:1", + "name": "Example", + }, + "validFrom": "2024-01-01T00:00:00Z", + "credentialSubject": { + "type": ["Product"], + "id": "https://example.com/products/1", + "name": "v0.7 sample product", + "idScheme": { + "type": ["IdentifierScheme"], + "id": "https://example.com/schemes/internal", + "name": "Internal scheme", + }, + "idGranularity": "model", + "productCategory": [ + { + "type": ["Classification"], + "schemeId": "https://unstats.un.org/cpc/", + "schemeName": "UN CPC", + "code": "12345", + "name": "Example category", + } + ], + "producedAtFacility": { + "type": ["Facility"], + "id": "https://example.com/facilities/1", + "name": "Example facility", + }, + "countryOfProduction": {"countryCode": "DE", "countryName": "Germany"}, + "relatedParty": [ + { + "role": "brandOwner", + "party": { + "type": ["Party"], + "id": "did:example:brand", + "name": "Sample Brand Inc", + }, + } + ], + }, + } + return DigitalProductPassport.model_validate(payload) + + +# --------------------------------------------------------------------------- +# Public-API stability — the imports the plugin relies on still work +# --------------------------------------------------------------------------- + + +class TestPluginPublicApi: + """The imports baked into the plugin must remain stable.""" + + def test_top_level_passport_alias_imports(self) -> None: + """``dppvalidator.models.passport.DigitalProductPassport`` resolves.""" + from dppvalidator.models.passport import DigitalProductPassport + + assert DigitalProductPassport is not None + + def test_credential_subject_dot_product_dot_name_path_works(self, v06_passport: Any) -> None: + """The plugin reads ``passport.credential_subject.product.name``. + + Phase 3 moved the v0.6 models under ``models/v0_6/`` with a + thin shim at the top level. The shim must continue to expose + the original attribute path; otherwise the example plugin + breaks at attribute-access time, not import time. + """ + assert v06_passport.credential_subject is not None + assert v06_passport.credential_subject.product is not None + assert v06_passport.credential_subject.product.name == "v0.6 sample product" + + def test_v07_credential_subject_is_product_directly(self, v07_passport: Any) -> None: + """v0.7 credentialSubject *is* the Product (no envelope).""" + cs = v07_passport.credential_subject + assert cs is not None + # The v0.7 shape must NOT carry an outer ``.product`` attribute — + # plugin authors targeting v0.7 read the Product fields directly. + assert not hasattr(cs, "product") + assert cs.name == "v0.7 sample product" + + +# --------------------------------------------------------------------------- +# Entry-point discovery +# --------------------------------------------------------------------------- + + +class TestPluginDiscovery: + """The installed plugin's entry points are discoverable.""" + + def test_validator_entry_points_register(self) -> None: + from dppvalidator.plugins.discovery import discover_validators + + names = {name for name, _ in discover_validators()} + assert "brand_name" in names + assert "brand_name_v07" in names + assert "min_materials" in names + + def test_exporter_entry_points_register(self) -> None: + from dppvalidator.plugins.discovery import discover_exporters + + names = {name for name, _ in discover_exporters()} + assert "csv" in names + + def test_validator_classes_are_instantiable(self) -> None: + """Each discovered validator class can be constructed. + + ``discover_validators`` returns the class object (not an + instance). The engine instantiates them; a constructor that + requires arguments would break the registry without surfacing + a clear error. + """ + from dppvalidator.plugins.discovery import discover_validators + + for name, cls in discover_validators(): + instance = cls() + assert instance is not None, f"failed to construct {name}" + assert hasattr(instance, "rule_id") + assert hasattr(instance, "check") + + +# --------------------------------------------------------------------------- +# v0.6 rule behaviour — keep working +# --------------------------------------------------------------------------- + + +class TestV06BrandNameRule: + """The v0.6 ``BrandNameRule`` keeps its pre-Phase-3 behaviour.""" + + def test_no_violation_when_product_has_name(self, v06_passport: Any) -> None: + from dppvalidator_example_plugin.validators import BrandNameRule + + rule = BrandNameRule() + assert rule.check(v06_passport) == [] + + def test_violation_when_product_name_missing(self, v06_passport: Any) -> None: + from dppvalidator_example_plugin.validators import BrandNameRule + + # Pydantic v2 frozen=False on this model — we reach in directly. + v06_passport.credential_subject.product.name = "" + rule = BrandNameRule() + violations = rule.check(v06_passport) + assert len(violations) == 1 + path, _ = violations[0] + assert path == "$.credentialSubject.product.name" + + def test_applies_to_versions_pins_v06(self) -> None: + """Pins the per-version dispatch contract (Phase 6 / 0.4.0 polish).""" + from dppvalidator_example_plugin.validators import BrandNameRule + + assert BrandNameRule.applies_to_versions == ("0.6.0", "0.6.1") + + def test_silently_skips_v07_passports(self, v07_passport: Any) -> None: + """Defensive duck-typing: v0.7 input never crashes the rule. + + The engine's per-version dispatch normally filters this rule + out for v0.7 payloads (because ``applies_to_versions`` is + v0.6.x). This test exercises the **belt-and-braces path**: + if a caller bypasses dispatch (e.g. by invoking ``check()`` + directly), the rule must still no-op cleanly rather than + crash with ``AttributeError``. + """ + from dppvalidator_example_plugin.validators import BrandNameRule + + rule = BrandNameRule() + assert rule.check(v07_passport) == [] + + +class TestV06MinMaterialsRule: + """The v0.6 ``MinMaterialsRule`` keeps its pre-Phase-3 behaviour.""" + + def test_no_violation_with_two_materials(self, v06_passport: Any) -> None: + from dppvalidator_example_plugin.validators import MinMaterialsRule + + rule = MinMaterialsRule() + assert rule.check(v06_passport) == [] + + def test_warning_with_one_material(self, v06_passport: Any) -> None: + from dppvalidator_example_plugin.validators import MinMaterialsRule + + # Trim materials down to 1 to trigger the rule. + v06_passport.credential_subject.materials_provenance = ( + v06_passport.credential_subject.materials_provenance[:1] + ) + rule = MinMaterialsRule() + violations = rule.check(v06_passport) + assert len(violations) == 1 + + def test_applies_to_versions_pins_v06(self) -> None: + from dppvalidator_example_plugin.validators import MinMaterialsRule + + assert MinMaterialsRule.applies_to_versions == ("0.6.0", "0.6.1") + + def test_silently_skips_v07_passports(self, v07_passport: Any) -> None: + """Defensive duck-typing — same contract as ``BrandNameRule``.""" + from dppvalidator_example_plugin.validators import MinMaterialsRule + + rule = MinMaterialsRule() + assert rule.check(v07_passport) == [] + + +# --------------------------------------------------------------------------- +# v0.7 rule behaviour — new in Phase 6 +# --------------------------------------------------------------------------- + + +class TestV07BrandNameRule: + """The new ``BrandNameRuleV07`` ships v0.7-aware semantics.""" + + def test_rule_id_advertised(self) -> None: + from dppvalidator_example_plugin.brand_name_v07 import BrandNameRuleV07 + + rule = BrandNameRuleV07() + assert rule.rule_id == "SEM_BRAND_V07" + + def test_applies_to_versions_pins_v07(self) -> None: + from dppvalidator_example_plugin.brand_name_v07 import BrandNameRuleV07 + + assert BrandNameRuleV07.applies_to_versions == ("0.7.0",) + + def test_no_violation_when_product_has_name(self, v07_passport: Any) -> None: + from dppvalidator_example_plugin.brand_name_v07 import BrandNameRuleV07 + + rule = BrandNameRuleV07() + assert rule.check(v07_passport) == [] + + def test_no_violation_when_brand_owner_present_even_without_name( + self, v07_passport: Any + ) -> None: + from dppvalidator_example_plugin.brand_name_v07 import BrandNameRuleV07 + + # Drop the name; the brandOwner relatedParty alone should satisfy. + v07_passport.credential_subject.name = "" + rule = BrandNameRuleV07() + assert rule.check(v07_passport) == [] + + def test_violation_when_neither_name_nor_brand_owner(self, v07_passport: Any) -> None: + from dppvalidator_example_plugin.brand_name_v07 import BrandNameRuleV07 + + v07_passport.credential_subject.name = "" + v07_passport.credential_subject.related_party = [] + rule = BrandNameRuleV07() + violations = rule.check(v07_passport) + assert len(violations) == 1 + path, message = violations[0] + assert path == "$.credentialSubject" + assert "brandOwner" in message + + def test_silently_skips_v06_passports(self, v06_passport: Any) -> None: + """Handed a v0.6 passport, the v0.7 rule no-ops cleanly. + + This is the version-aware-rule pattern: rules co-exist in the + registry and self-filter on shape rather than crashing when the + wrong version flows through. + """ + from dppvalidator_example_plugin.brand_name_v07 import BrandNameRuleV07 + + rule = BrandNameRuleV07() + # Even though the v0.6 product.name is set, the v0.7 rule should + # not look at the wrapped product — it returns no violations + # because the shape didn't match. + assert rule.check(v06_passport) == [] + + +# --------------------------------------------------------------------------- +# Engine-level per-version dispatch (Phase 6 / 0.4.0 polish) +# --------------------------------------------------------------------------- + + +class TestEnginePerVersionPluginDispatch: + """The engine's plugin layer routes plugins by ``applies_to_versions``. + + Prior to the 0.4.0 polish round, all installed plugins ran for + every payload. v0.6 plugins reading ``credential_subject.product`` + crashed on v0.7 payloads, surfacing as ``PLG001`` warnings. The + fix wires ``schema_version`` through to ``run_all_validators`` so + plugins that pin to a specific version are silently skipped on + non-matching payloads. + + These tests pin the contract end-to-end through the engine. + """ + + def test_v07_engine_does_not_trip_v06_plugins(self) -> None: + """A v0.7 payload validated end-to-end produces zero PLG001 entries. + + Pre-fix this test would fail: v0.6 ``BrandNameRule`` and + ``MinMaterialsRule`` would crash on the v0.7 shape and emit + ``PLG001`` warnings. + """ + import json + from pathlib import Path + + from dppvalidator import ValidationEngine + + fixture = ( + Path(__file__).resolve().parents[1] + / "fixtures" + / "valid" + / "untp-dpp-instance-0.7.0.json" + ) + if not fixture.is_file(): + pytest.skip("v0.7 fixture not vendored") + data = json.loads(fixture.read_text(encoding="utf-8")) + + engine = ValidationEngine(schema_version="0.7.0") + result = engine.validate(data) + + plg001 = [ + issue + for issue in result.errors + result.warnings + result.info + if issue.code == "PLG001" + ] + assert plg001 == [], ( + "Plugin filter regressed — PLG001 entries leaked into a v0.7 " + "validation:\n" + "\n".join(f" [{e.code}] {e.path}: {e.message}" for e in plg001) + ) + + def test_v06_engine_runs_v06_plugins(self) -> None: + """The v0.6 plugins still run for v0.6 payloads (no regression).""" + import json + from pathlib import Path + + from dppvalidator import ValidationEngine + + fixture = ( + Path(__file__).resolve().parents[1] + / "fixtures" + / "valid" + / "untp-dpp-instance-0.6.1.json" + ) + if not fixture.is_file(): + pytest.skip("v0.6 fixture not vendored") + data = json.loads(fixture.read_text(encoding="utf-8")) + + engine = ValidationEngine(schema_version="0.6.1") + result = engine.validate(data) + + # No PLG001 (the rules don't crash on a valid v0.6 payload). + plg001 = [ + issue + for issue in result.errors + result.warnings + result.info + if issue.code == "PLG001" + ] + assert plg001 == [] + + +# --------------------------------------------------------------------------- +# CSV exporter — public-API smoke +# --------------------------------------------------------------------------- + + +class TestCSVExporterSmoke: + """The CSV exporter still exports a non-empty payload.""" + + def test_export_returns_string(self, v06_passport: Any) -> None: + from dppvalidator_example_plugin.exporters import CSVExporter + + exporter = CSVExporter() + out = exporter.export(v06_passport) + assert isinstance(out, str) + assert out diff --git a/tests/integration/test_real_world_samples.py b/tests/integration/test_real_world_samples.py new file mode 100644 index 0000000..772a0af --- /dev/null +++ b/tests/integration/test_real_world_samples.py @@ -0,0 +1,444 @@ +"""Integration tests using real-world DPP samples from various sources. + +Tests the validation pipeline against actual DPP instances from: +- UNTP (UN Trade Protocol) reference implementations +- Battery Pass data model examples +- NFC Forum DPP examples +- Catena-X/Tractus-X battery pass models +""" + +from __future__ import annotations + +import json +from pathlib import Path +from typing import TYPE_CHECKING, Any + +import pytest + +from dppvalidator.validators import ValidationEngine, ValidationResult + +if TYPE_CHECKING: + from collections.abc import Iterator + +SAMPLES_DIR = Path(__file__).parent.parent / "fixtures" / "samples" + + +def _load_sample(filename: str) -> dict[str, Any] | None: + """Load a sample file if it exists.""" + path = SAMPLES_DIR / filename + if not path.exists(): + return None + with open(path) as f: + data: dict[str, Any] = json.load(f) + return data + + +def _sample_files() -> Iterator[Path]: + """Yield all JSON sample files.""" + if not SAMPLES_DIR.exists(): + return + yield from SAMPLES_DIR.glob("*.json") + + +class TestUNTPSamples: + """Tests for UNTP (UN Trade Protocol) DPP samples. + + These are considered the reference implementation for DPP structure + and should pass validation with high confidence. + """ + + @pytest.fixture + def engine(self) -> ValidationEngine: + return ValidationEngine(schema_version="auto", layers=["model", "semantic"]) + + def test_untp_dpp_instance_0_6_0(self, engine: ValidationEngine): + """UNTP DPP v0.6.0 instance should validate successfully.""" + data = _load_sample("test_uncefact_org_untp-dpp-instance-0.6.0.json") + if data is None: + pytest.skip("Sample not available") + assert data is not None # Type narrowing + + result = engine.validate(data) + + assert isinstance(result, ValidationResult) + assert result.passport is not None, f"Failed to parse: {result.errors}" + # UNTP reference should pass with minimal errors + assert result.valid or len(result.errors) <= 2 + + def test_untp_dpp_instance_0_3_10(self, engine: ValidationEngine): + """UNTP DPP v0.3.10 instance should be parseable.""" + data = _load_sample("opensource_unicc_org_untp-digital-product-passport-v0.3.10.json") + if data is None: + pytest.skip("Sample not available") + assert data is not None # Type narrowing + + result = engine.validate(data) + + assert isinstance(result, ValidationResult) + # Older version may have structural differences + assert result.passport is not None or len(result.errors) > 0 + + def test_untp_digital_facility_record(self, engine: ValidationEngine): + """UNTP Digital Facility Record should be parseable as related credential.""" + data = _load_sample("opensource_unicc_org_untp-digital-facility-record-v0.3.9.json") + if data is None: + pytest.skip("Sample not available") + assert data is not None # Type narrowing + + result = engine.validate(data) + + # Facility records are different from DPPs but should still parse + assert isinstance(result, ValidationResult) + + def test_untp_digital_identity_anchor(self, engine: ValidationEngine): + """UNTP Digital Identity Anchor should be parseable.""" + data = _load_sample("test_uncefact_org_DigitalIdentityAnchor-instance-0.6.1.json") + if data is None: + pytest.skip("Sample not available") + assert data is not None # Type narrowing + + result = engine.validate(data) + + assert isinstance(result, ValidationResult) + # Identity anchors have different structure + assert result.passport is not None or len(result.errors) > 0 + + +class TestBatteryPassSamples: + """Tests for Battery Pass data model samples. + + These follow the EU Battery Regulation requirements and include + specific battery-related fields. + """ + + @pytest.fixture + def engine(self) -> ValidationEngine: + return ValidationEngine(schema_version="auto", layers=["model", "semantic"]) + + def test_battery_pass_general_product_info_payload(self, engine: ValidationEngine): + """Battery Pass GeneralProductInformation payload should be parseable.""" + data = _load_sample( + "BatteryPassDataModel_BatteryPass_GeneralProductInformation-payload.json" + ) + if data is None: + pytest.skip("Sample not available") + assert data is not None # Type narrowing + + result = engine.validate(data) + + assert isinstance(result, ValidationResult) + # Battery Pass has different structure, may not fully validate + # but should parse and identify key fields + if result.passport: + # Check if battery-specific fields are recognized + assert result.passport is not None + + def test_battery_pass_general_product_info_ld(self, engine: ValidationEngine): + """Battery Pass GeneralProductInformation JSON-LD should be parseable.""" + data = _load_sample("batterypass_BatteryPassDataModel_GeneralProductInformation-ld.json") + if data is None: + pytest.skip("Sample not available") + assert data is not None # Type narrowing + + result = engine.validate(data) + + assert isinstance(result, ValidationResult) + # JSON-LD with @graph structure may need special handling + if "@graph" in data: + # Verify we handle @graph structures + assert result.passport is not None or len(result.errors) > 0 + + def test_battery_pass_circularity_ld(self, engine: ValidationEngine): + """Battery Pass Circularity JSON-LD should be parseable.""" + data = _load_sample("batterypass_BatteryPassDataModel_Circularity-ld.json") + if data is None: + pytest.skip("Sample not available") + assert data is not None # Type narrowing + + result = engine.validate(data) + + assert isinstance(result, ValidationResult) + + def test_battery_pass_material_composition_ld(self, engine: ValidationEngine): + """Battery Pass MaterialComposition JSON-LD should be parseable.""" + data = _load_sample("batterypass_BatteryPassDataModel_MaterialComposition-ld.json") + if data is None: + pytest.skip("Sample not available") + assert data is not None # Type narrowing + + result = engine.validate(data) + + assert isinstance(result, ValidationResult) + + def test_battery_pass_carbon_footprint_ld(self, engine: ValidationEngine): + """Battery Pass CarbonFootprint JSON-LD should be parseable.""" + data = _load_sample("batterypass_BatteryPassDataModel_CarbonFootprintForBatteries-ld.json") + if data is None: + pytest.skip("Sample not available") + assert data is not None # Type narrowing + + result = engine.validate(data) + + assert isinstance(result, ValidationResult) + + +class TestCatenaXSamples: + """Tests for Catena-X/Tractus-X battery pass samples. + + These are industry consortium examples from automotive sector. + """ + + @pytest.fixture + def engine(self) -> ValidationEngine: + return ValidationEngine(schema_version="auto", layers=["model", "semantic"]) + + def test_tractus_x_battery_pass(self, engine: ValidationEngine): + """Tractus-X BatteryPass sample should be parseable.""" + data = _load_sample("eclipse-tractusx_sldt-semantic-models_BatteryPass.json") + if data is None: + pytest.skip("Sample not available") + assert data is not None # Type narrowing + + result = engine.validate(data) + + assert isinstance(result, ValidationResult) + # Catena-X has its own structure, validate basic parsing + if result.passport: + # Should extract some product information + assert result.passport is not None + + +class TestAlternativeDPPFormats: + """Tests for alternative DPP formats from various sources.""" + + @pytest.fixture + def engine(self) -> ValidationEngine: + return ValidationEngine(schema_version="auto", layers=["model", "semantic"]) + + def test_nfc_forum_long_dpp_example(self, engine: ValidationEngine): + """NFC Forum long DPP example should be parseable.""" + data = _load_sample("nfc-forum_org_long-dpp-example.json") + if data is None: + pytest.skip("Sample not available") + assert data is not None # Type narrowing + + result = engine.validate(data) + + assert isinstance(result, ValidationResult) + # NFC Forum example has different structure + # but contains product information + if result.passport is None and result.errors: + # Should identify what's missing + error_messages = [e.message for e in result.errors] + assert len(error_messages) > 0 + + def test_spherity_breathable_tshirt(self, engine: ValidationEngine): + """Spherity breathable t-shirt sample should be parseable.""" + data = _load_sample("schemas_testing_breathable-t-shirt.json") + if data is None: + pytest.skip("Sample not available") + assert data is not None # Type narrowing + + result = engine.validate(data) + + assert isinstance(result, ValidationResult) + + def test_enveloped_verifiable_credential(self, engine: ValidationEngine): + """Enveloped VC from S3 should be parseable.""" + data = _load_sample( + "untp-verifiable-credentials_s3_amazonaws_com_bc075c5f-2304-4b3f-bb24-46d9fa9a8e60.json" + ) + if data is None: + pytest.skip("Sample not available") + assert data is not None # Type narrowing + + result = engine.validate(data) + + assert isinstance(result, ValidationResult) + # Enveloped credentials need unwrapping + if data.get("type") == "EnvelopedVerifiableCredential": + # Should handle or report enveloped format + assert result.passport is not None or len(result.errors) > 0 + + +class TestBulkSampleValidation: + """Bulk tests across all downloaded samples.""" + + @pytest.fixture + def engine(self) -> ValidationEngine: + return ValidationEngine(schema_version="auto", layers=["model"]) + + def test_all_samples_are_valid_json(self): + """All sample files should be valid JSON.""" + for sample_path in _sample_files(): + with open(sample_path) as f: + try: + data = json.load(f) + assert isinstance(data, dict), f"{sample_path.name} is not a JSON object" + except json.JSONDecodeError as e: + pytest.fail(f"{sample_path.name} is not valid JSON: {e}") + + def test_all_samples_can_be_processed(self, engine: ValidationEngine): + """All samples should be processable without crashing.""" + for sample_path in _sample_files(): + with open(sample_path) as f: + data = json.load(f) + + # Should not raise any exceptions + result = engine.validate(data) + assert isinstance(result, ValidationResult), f"Failed on {sample_path.name}" + + @pytest.mark.parametrize( + "filename", + [ + "test_uncefact_org_untp-dpp-instance-0.6.0.json", + ], + ) + def test_excellent_samples_validate(self, engine: ValidationEngine, filename: str): + """Samples rated 'excellent' with matching schema version should pass validation.""" + data = _load_sample(filename) + if data is None: + pytest.skip(f"Sample {filename} not available") + assert data is not None # Type narrowing + + result = engine.validate(data) + + assert result.passport is not None, f"{filename}: {result.errors}" + + def test_older_untp_sample_processes_with_version_differences(self, engine: ValidationEngine): + """Older UNTP samples may have schema differences but should still process.""" + data = _load_sample("opensource_unicc_org_untp-digital-product-passport-v0.3.10.json") + if data is None: + pytest.skip("Sample not available") + assert data is not None # Type narrowing + + result = engine.validate(data) + + # Older versions may fail strict validation due to schema evolution + # but the error should be about extra/missing fields, not parsing failures + assert isinstance(result, ValidationResult) + if not result.valid: + # Errors should be model-level (schema differences), not parsing errors + assert all(e.layer == "model" for e in result.errors) + + @pytest.mark.parametrize( + "filename", + [ + "test_uncefact_org_DigitalIdentityAnchor-instance-0.6.1.json", + "opensource_unicc_org_untp-digital-facility-record-v0.3.9.json", + "BatteryPassDataModel_BatteryPass_GeneralProductInformation-payload.json", + "batterypass_BatteryPassDataModel_GeneralProductInformation-ld.json", + ], + ) + def test_good_samples_process(self, engine: ValidationEngine, filename: str): + """Samples rated 'good' should process without errors.""" + data = _load_sample(filename) + if data is None: + pytest.skip(f"Sample {filename} not available") + assert data is not None # Type narrowing + + result = engine.validate(data) + + assert isinstance(result, ValidationResult) + # Good samples may not fully validate but should parse + assert result.passport is not None or result.errors + + +class TestSampleStructureAnalysis: + """Tests that analyze the structure of real-world samples.""" + + def test_untp_samples_have_context(self): + """UNTP samples should have @context field.""" + untp_samples = [ + "test_uncefact_org_untp-dpp-instance-0.6.0.json", + "opensource_unicc_org_untp-digital-product-passport-v0.3.10.json", + ] + for filename in untp_samples: + data = _load_sample(filename) + if data is None: + continue + + assert "@context" in data, f"{filename} missing @context" + assert isinstance(data["@context"], list), f"{filename} @context should be list" + + def test_untp_samples_have_type(self): + """UNTP samples should have type field.""" + untp_samples = [ + "test_uncefact_org_untp-dpp-instance-0.6.0.json", + "opensource_unicc_org_untp-digital-product-passport-v0.3.10.json", + ] + for filename in untp_samples: + data = _load_sample(filename) + if data is None: + continue + + assert "type" in data, f"{filename} missing type" + types = data["type"] + assert "VerifiableCredential" in types, f"{filename} should be VerifiableCredential" + + def test_untp_samples_have_issuer(self): + """UNTP samples should have issuer field.""" + untp_samples = [ + "test_uncefact_org_untp-dpp-instance-0.6.0.json", + "opensource_unicc_org_untp-digital-product-passport-v0.3.10.json", + ] + for filename in untp_samples: + data = _load_sample(filename) + if data is None: + continue + + assert "issuer" in data, f"{filename} missing issuer" + issuer = data["issuer"] + assert "id" in issuer, f"{filename} issuer missing id" + + def test_untp_samples_have_credential_subject(self): + """UNTP samples should have credentialSubject field.""" + untp_samples = [ + "test_uncefact_org_untp-dpp-instance-0.6.0.json", + "opensource_unicc_org_untp-digital-product-passport-v0.3.10.json", + ] + for filename in untp_samples: + data = _load_sample(filename) + if data is None: + continue + + assert "credentialSubject" in data, f"{filename} missing credentialSubject" + + def test_battery_pass_samples_have_battery_fields(self): + """Battery Pass samples should contain battery-specific fields.""" + battery_sample = _load_sample( + "BatteryPassDataModel_BatteryPass_GeneralProductInformation-payload.json" + ) + if battery_sample is None: + pytest.skip("Battery Pass sample not available") + assert battery_sample is not None # Type narrowing + + battery_fields = ["batteryCategory", "batteryStatus", "batteryMass"] + sample_keys = list(battery_sample.keys()) + found = [f for f in battery_fields if f in sample_keys] + assert len(found) >= 2, f"Expected battery fields, found: {sample_keys}" + + +class TestValidationConsistency: + """Tests for validation consistency across similar samples.""" + + @pytest.fixture + def engine(self) -> ValidationEngine: + return ValidationEngine(schema_version="auto", layers=["model", "semantic"]) + + def test_same_version_samples_produce_consistent_results(self, engine: ValidationEngine): + """Samples from the same schema version should produce consistent validation.""" + # Test with two v0.6.x samples which should both validate + sample1 = _load_sample("test_uncefact_org_untp-dpp-instance-0.6.0.json") + sample2 = _load_sample("test_uncefact_org_DigitalIdentityAnchor-instance-0.6.1.json") + + if sample1 is None or sample2 is None: + pytest.skip("Samples not available") + assert sample1 is not None and sample2 is not None # Type narrowing + + result1 = engine.validate(sample1) + result2 = engine.validate(sample2) + + # Both are v0.6.x and should parse successfully + assert result1.passport is not None, f"v0.6.0 DPP failed: {result1.errors}" + assert result2.passport is not None, f"v0.6.1 DIA failed: {result2.errors}" diff --git a/tests/integration/test_validation_pipeline.py b/tests/integration/test_validation_pipeline.py index 09f940e..73df488 100644 --- a/tests/integration/test_validation_pipeline.py +++ b/tests/integration/test_validation_pipeline.py @@ -14,11 +14,11 @@ class TestValidFixtures: - """Integration tests validating known-good DPP fixtures.""" + """Integration tests validating known-good v0.6.x DPP fixtures.""" @pytest.fixture def engine(self) -> ValidationEngine: - """Create validation engine with all layers.""" + """Create validation engine with all layers (pinned to v0.6.1).""" return ValidationEngine(schema_version="0.6.1", layers=["model", "semantic"]) def test_minimal_dpp_passes_validation(self, engine): @@ -52,6 +52,73 @@ def test_untp_instance_passes_validation(self, engine): assert result.passport is not None or len(result.errors) > 0 +class TestValidV07Fixtures: + """Integration tests validating the vendored v0.7.0 upstream fixtures. + + Phase 5 vendored the upstream UNTP 0.7.0 sample DPPs into + ``tests/fixtures/valid/``. This class pins the contract that the + full validation pipeline (model + semantic layers) accepts each + of them — a regression here means either the v0.7 model has + drifted from the upstream schema, or a semantic rule started + rejecting a sample we previously accepted. + """ + + @pytest.fixture + def engine(self) -> ValidationEngine: + """Create validation engine with all layers (pinned to v0.7.0).""" + return ValidationEngine(schema_version="0.7.0", layers=["model", "semantic"]) + + @pytest.mark.parametrize( + "fixture_name", + [ + "untp-dpp-instance-0.7.0.json", + "untp-dpp-battery-instance-0.7.0.json", + "untp-dpp-cathode-instance-0.7.0.json", + ], + ) + def test_canonical_v07_fixture_passes_pipeline( + self, engine: ValidationEngine, fixture_name: str + ) -> None: + """Each canonical v0.7.0 sample validates cleanly through the pipeline. + + The vendored fixtures are bit-identical to the upstream samples + published at ``untp.unece.org/artefacts/samples/v0.7.0/dpp/``; + they're the highest-fidelity smoke test we have for the v0.7 + model + semantic-rule combination. + """ + fixture_path = FIXTURES_DIR / "valid" / fixture_name + if not fixture_path.exists(): + pytest.skip(f"v0.7 fixture not vendored: {fixture_name}") + + result = engine.validate_file(fixture_path) + + assert result.valid, f"{fixture_name} unexpectedly rejected:\n" + "\n".join( + f" [{e.code}] {e.path}: {e.message}" for e in result.errors + ) + assert result.passport is not None + + def test_v06_fixture_through_v07_engine_fails_with_VER001( + self, engine: ValidationEngine + ) -> None: + """Feeding a v0.6.x payload to a v0.7.0-pinned engine is fail-fast. + + Pins the VER001 contract from Phase 3.3 — the engine must not + silently coerce across versions. See + ``docs/errors/VER001.md`` for the user-facing remediation. + """ + fixture_path = FIXTURES_DIR / "valid" / "untp-dpp-instance-0.6.1.json" + if not fixture_path.exists(): + pytest.skip("v0.6 fixture not available") + + result = engine.validate_file(fixture_path) + + assert result.valid is False + assert any(e.code == "VER001" for e in result.errors), ( + "Expected VER001 (version mismatch) when v0.6.x payload " + "flows through a v0.7.0-pinned engine." + ) + + class TestInvalidFixtures: """Integration tests validating known-bad DPP fixtures.""" diff --git a/tests/integration/test_version_matrix.py b/tests/integration/test_version_matrix.py new file mode 100644 index 0000000..cb457bb --- /dev/null +++ b/tests/integration/test_version_matrix.py @@ -0,0 +1,178 @@ +"""Phase 5 acceptance test: validator-layer × UNTP-version matrix. + +This module enforces the Phase 5 exit criterion from +``docs/plans/UNTP_0.7.0_MIGRATION.md``: + +> Parametrised matrix is green; coverage report unchanged or improved. + +The matrix multiplies every supported validation layer (``schema``, +``model``, ``semantic``, ``jsonld``, ``deep``) by every supported UNTP +DPP version (currently ``0.6.1`` and ``0.7.0``) and asserts the +canonical happy-path fixture validates cleanly through each layer/version +combination. When a new UNTP version is registered, the matrix +automatically expands. + +Negative coverage — fixtures that *must* fail — lives next to this test +in ``tests/fixtures/invalid/0.7.0/`` with a parametrised "every invalid +fixture surfaces at least one error" check. +""" + +from __future__ import annotations + +import asyncio +import json +from pathlib import Path +from typing import Any + +import pytest + +from dppvalidator.validators import ValidationEngine + +_FIXTURE_ROOT = Path(__file__).resolve().parents[1] / "fixtures" +_VALID_DIR = _FIXTURE_ROOT / "valid" +_INVALID_07_DIR = _FIXTURE_ROOT / "invalid" / "0.7.0" + + +# --------------------------------------------------------------------------- +# Fixture discovery — keyed on the published UNTP version +# --------------------------------------------------------------------------- + + +_HAPPY_PATH_BY_VERSION: dict[str, Path] = { + "0.6.1": _VALID_DIR / "untp-dpp-instance-0.6.1.json", + "0.7.0": _VALID_DIR / "untp-dpp-instance-0.7.0.json", +} + + +def _matrix_versions() -> tuple[str, ...]: + """Read the matrix versions from the conftest registry helper.""" + from tests.conftest import all_matrix_versions + + return tuple(all_matrix_versions()) + + +def _layers_for( + version: str, # noqa: ARG001 — placeholder for future per-version filtering +) -> tuple[str, ...]: + """Layers that should execute against ``version``. + + All four sub-validators are exercised for both versions in Phase 5 + — the matrix is exhaustive on purpose. If a layer ever needs to + skip a version (e.g. an experimental layer not yet ported), this + is the place to express that. The deep validator is async and + handled separately. + """ + return ("schema", "model", "semantic", "jsonld") + + +# --------------------------------------------------------------------------- +# Happy-path matrix — sub-validator × version +# --------------------------------------------------------------------------- + + +def _layer_version_matrix() -> list[tuple[str, str, Path]]: + """Build the ``(layer, version, fixture)`` cartesian product.""" + out: list[tuple[str, str, Path]] = [] + for version in _matrix_versions(): + fixture = _HAPPY_PATH_BY_VERSION.get(version) + if fixture is None or not fixture.is_file(): + continue + for layer in _layers_for(version): + out.append((layer, version, fixture)) + return out + + +@pytest.mark.parametrize( + ("layer", "version", "fixture"), + _layer_version_matrix(), + ids=lambda val: val if isinstance(val, str) else val.name, +) +def test_layer_passes_for_canonical_fixture(layer: str, version: str, fixture: Path) -> None: + """Run a single layer against the canonical fixture for ``version``. + + Fails when the fixture is rejected — that's the matrix's purpose. + Each (layer, version) slot is a separate test ID so CI failures + point straight at the broken combination without ambiguity. + """ + engine = ValidationEngine(schema_version=version, layers=[layer]) + data = json.loads(fixture.read_text(encoding="utf-8")) + result = engine.validate(data) + assert result.valid, ( + f"{layer} layer rejected the canonical {version} fixture " + f"({fixture.name}):\n" + + "\n".join(f" [{e.code}] {e.path}: {e.message}" for e in result.errors) + ) + + +def test_full_pipeline_passes_for_each_version() -> None: + """The default-layer pipeline (schema+model+semantic) is green per version.""" + for version in _matrix_versions(): + fixture = _HAPPY_PATH_BY_VERSION.get(version) + if fixture is None or not fixture.is_file(): + pytest.skip(f"No happy-path fixture vendored for {version}") + engine = ValidationEngine(schema_version=version) + data = json.loads(fixture.read_text(encoding="utf-8")) + result = engine.validate(data) + assert result.valid, f"Full pipeline failed for {version} ({fixture.name}):\n" + "\n".join( + f" [{e.code}] {e.path}: {e.message}" for e in result.errors + ) + + +# --------------------------------------------------------------------------- +# Deep validator — async, exercised once per version +# --------------------------------------------------------------------------- + + +@pytest.mark.parametrize("version", _matrix_versions()) +def test_deep_validator_runs_per_version(version: str) -> None: + """The deep validator constructs and executes for each UNTP version. + + The fixture has no external links (``href``-bearing fields are + self-referential or absent), so deep traversal has nothing to fetch + and the result must be valid. We assert the *path* compiles per + version more than that the result is empty — Phase 3b's + ``LINK_PATHS_BY_VERSION`` dispatch is what's under test. + """ + fixture = _HAPPY_PATH_BY_VERSION.get(version) + if fixture is None or not fixture.is_file(): + pytest.skip(f"No happy-path fixture vendored for {version}") + data = json.loads(fixture.read_text(encoding="utf-8")) + engine = ValidationEngine(schema_version=version) + result = asyncio.run(engine.validate_deep(data)) + # The deep validator may surface no findings — what matters is that + # the per-version dispatch table produced a runnable validator. + assert result is not None + + +# --------------------------------------------------------------------------- +# Negative matrix — every invalid v0.7 fixture surfaces an error +# --------------------------------------------------------------------------- + + +def _invalid_07_fixtures() -> list[Path]: + return sorted(_INVALID_07_DIR.glob("*.json")) + + +@pytest.mark.parametrize( + "fixture", + _invalid_07_fixtures(), + ids=lambda p: p.name, +) +def test_invalid_v07_fixture_is_rejected(fixture: Path) -> None: + """Every fixture in ``invalid/0.7.0/`` must be flagged as invalid. + + Catches regressions where a schema or model relaxation accidentally + starts accepting a payload that's documented as invalid. + """ + data: dict[str, Any] = json.loads(fixture.read_text(encoding="utf-8")) + engine = ValidationEngine(schema_version="0.7.0") + result = engine.validate(data) + assert not result.valid, ( + f"{fixture.name} unexpectedly validated cleanly — the fixture is " + "documented as invalid; either the fixture or the validator " + "regressed." + ) + assert result.errors, ( + f"{fixture.name} returned valid=False but no errors — the engine " + "should always surface the cause." + ) diff --git a/tests/unit/test_cli_migrate.py b/tests/unit/test_cli_migrate.py new file mode 100644 index 0000000..ebd4536 --- /dev/null +++ b/tests/unit/test_cli_migrate.py @@ -0,0 +1,244 @@ +"""CLI tests for ``dppvalidator migrate`` and ``validate --upgrade-from``. + +Phase 4 wires the compat shim into two CLI surfaces: + +- ``dppvalidator migrate`` writes the upgraded JSON to ``-o`` / + ``--in-place`` / stdout; refuses to write when warnings fire unless + ``--accept-warnings`` is set; emits a sidecar ``.warnings.json`` file + whenever any non-info warning is recorded. +- ``dppvalidator validate --upgrade-from `` runs the shim before + validating, surfacing both upgrade warnings and validation issues in + one report. + +These tests exercise both surfaces end-to-end via the public ``main`` +entry point, so they double as integration coverage for the dispatch +table and arg-parser wiring. +""" + +from __future__ import annotations + +import json +from pathlib import Path +from typing import Any + +import pytest + +from dppvalidator.cli.main import main + + +@pytest.fixture +def v06_payload(tmp_path: Path) -> Path: + """Write a minimal v0.6 DPP fixture for the CLI to read. + + The fixture intentionally carries an unconditional v0.7-only + blocking transformation (``Product.registeredId`` → UPG001 lossy) + so we can exercise the warnings-block path. To exercise the + no-warnings path, individual tests pop the offending field before + invoking the CLI. + """ + data: dict[str, Any] = { + "type": ["DigitalProductPassport", "VerifiableCredential"], + "@context": [ + "https://www.w3.org/ns/credentials/v2", + "https://test.uncefact.org/vocabulary/untp/dpp/0.6.1/", + ], + "id": "https://example.com/credentials/test", + "name": "Sample DPP", # Pre-populated to avoid UPG002 from name synthesis. + "issuer": {"id": "did:example:1", "name": "Example"}, + "validFrom": "2024-01-01T00:00:00Z", + "credentialSubject": { + "type": ["ProductPassport"], + "id": "https://example.com/subject/1", + "product": { + "type": ["Product"], + "id": "https://example.com/products/1", + "name": "Sample", + "registeredId": "ABN-123", + }, + }, + } + path = tmp_path / "input.json" + path.write_text(json.dumps(data), encoding="utf-8") + return path + + +# --------------------------------------------------------------------------- +# `migrate` command +# --------------------------------------------------------------------------- + + +class TestMigrateCommand: + """Acceptance tests for ``dppvalidator migrate``.""" + + def test_writes_to_stdout_when_no_output_path( + self, v06_payload: Path, capsys: pytest.CaptureFixture[str] + ) -> None: + # Drop the registeredId line so the payload upgrades without + # blocking warnings (registeredId fires UPG001). + data = json.loads(v06_payload.read_text()) + data["credentialSubject"]["product"].pop("registeredId", None) + v06_payload.write_text(json.dumps(data), encoding="utf-8") + exit_code = main(["migrate", str(v06_payload)]) + captured = capsys.readouterr() + assert exit_code == 0 + # Stdout should contain serialised JSON of the upgraded payload. + assert "vocabulary.uncefact.org/untp/0.7" in captured.out + + def test_writes_to_explicit_output_file(self, v06_payload: Path, tmp_path: Path) -> None: + data = json.loads(v06_payload.read_text()) + data["credentialSubject"]["product"].pop("registeredId", None) + v06_payload.write_text(json.dumps(data), encoding="utf-8") + out = tmp_path / "upgraded.json" + exit_code = main(["migrate", str(v06_payload), "-o", str(out)]) + assert exit_code == 0 + assert out.is_file() + upgraded = json.loads(out.read_text()) + assert any("vocabulary.uncefact.org/untp/0.7" in c for c in upgraded["@context"]) + + def test_in_place_overwrites_input(self, v06_payload: Path) -> None: + data = json.loads(v06_payload.read_text()) + data["credentialSubject"]["product"].pop("registeredId", None) + v06_payload.write_text(json.dumps(data), encoding="utf-8") + exit_code = main(["migrate", str(v06_payload), "--in-place"]) + assert exit_code == 0 + upgraded = json.loads(v06_payload.read_text()) + assert any("vocabulary.uncefact.org/untp/0.7" in c for c in upgraded["@context"]) + + def test_in_place_and_output_are_mutually_exclusive( + self, v06_payload: Path, tmp_path: Path + ) -> None: + out = tmp_path / "x.json" + exit_code = main( + ["migrate", str(v06_payload), "--in-place", "-o", str(out)], + ) + assert exit_code != 0 + + def test_refuses_to_write_when_warnings_fire(self, v06_payload: Path, tmp_path: Path) -> None: + # The fixture has registeredId → UPG001 (lossy/warning) — without + # --accept-warnings the command must refuse. + out = tmp_path / "upgraded.json" + exit_code = main(["migrate", str(v06_payload), "-o", str(out)]) + assert exit_code == 1 + # Sidecar must always be written when blocking warnings fire. + sidecar = out.with_suffix(out.suffix + ".warnings.json") + assert sidecar.is_file(), "sidecar warnings file must be written" + # Main output file must NOT be written. + assert not out.is_file() + + def test_accept_warnings_lets_write_proceed(self, v06_payload: Path, tmp_path: Path) -> None: + out = tmp_path / "upgraded.json" + exit_code = main( + ["migrate", str(v06_payload), "-o", str(out), "--accept-warnings"], + ) + assert exit_code == 0 + assert out.is_file() + sidecar = out.with_suffix(out.suffix + ".warnings.json") + assert sidecar.is_file() + sidecar_data = json.loads(sidecar.read_text()) + assert sidecar_data["schema_version_from"].startswith("0.6") + assert any(w["code"].startswith("UPG") for w in sidecar_data["warnings"]) + + def test_rejects_unknown_source_version(self, v06_payload: Path) -> None: + exit_code = main( + ["migrate", str(v06_payload), "--from", "0.5.0"], + ) + assert exit_code != 0 + + def test_missing_input_file_returns_error(self, tmp_path: Path) -> None: + missing = tmp_path / "nope.json" + exit_code = main(["migrate", str(missing)]) + assert exit_code != 0 + + def test_invalid_json_input_returns_error(self, tmp_path: Path) -> None: + bad = tmp_path / "bad.json" + bad.write_text("{ this is not json", encoding="utf-8") + exit_code = main(["migrate", str(bad)]) + assert exit_code != 0 + + def test_in_place_with_stdin_is_rejected( + self, + monkeypatch: pytest.MonkeyPatch, + v06_payload: Path, # noqa: ARG002 — fixture only here for parser order + ) -> None: + # ``--in-place`` requires a real file path; ``-`` (stdin) cannot + # be written back to. + import io + + monkeypatch.setattr("sys.stdin", io.StringIO("{}")) + exit_code = main(["migrate", "-", "--in-place"]) + assert exit_code != 0 + + def test_stdin_input_works( + self, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str] + ) -> None: + """Reading from stdin and writing to stdout is the pipe-friendly path.""" + import io + + payload: dict[str, Any] = { + "type": ["DigitalProductPassport", "VerifiableCredential"], + "@context": [ + "https://www.w3.org/ns/credentials/v2", + "https://test.uncefact.org/vocabulary/untp/dpp/0.6.1/", + ], + "id": "https://example.com/credentials/test", + "name": "From stdin", + "issuer": {"id": "did:example:1", "name": "Example"}, + "validFrom": "2024-01-01T00:00:00Z", + "credentialSubject": { + "type": ["ProductPassport"], + "id": "https://example.com/subject/1", + "product": { + "type": ["Product"], + "id": "https://example.com/products/1", + "name": "Sample", + }, + }, + } + monkeypatch.setattr("sys.stdin", io.StringIO(json.dumps(payload))) + exit_code = main(["migrate", "-"]) + captured = capsys.readouterr() + assert exit_code == 0 + assert "vocabulary.uncefact.org/untp/0.7" in captured.out + + +# --------------------------------------------------------------------------- +# `validate --upgrade-from` flag +# --------------------------------------------------------------------------- + + +class TestValidateUpgradeFrom: + """``dppvalidator validate --upgrade-from`` runs the shim then validates.""" + + def test_flag_is_accepted(self, v06_payload: Path, capsys: pytest.CaptureFixture[str]) -> None: + # We don't care about the exit code (the upgraded fixture is + # likely still invalid against the v0.7 schema due to required + # fields the shim can't synthesise) — only that the flag is + # accepted and the shim runs. + main( + [ + "validate", + str(v06_payload), + "--upgrade-from", + "0.6.1", + "--schema-version", + "0.6.1", # not the target — we're only sanity-checking flag wiring + ] + ) + captured = capsys.readouterr() + # The upgrade-warnings header should appear in the output. + assert "Upgrade warnings" in captured.out or "UPG" in captured.out + + def test_no_flag_means_no_shim( + self, v06_payload: Path, capsys: pytest.CaptureFixture[str] + ) -> None: + main( + [ + "validate", + str(v06_payload), + "--schema-version", + "0.6.1", + ] + ) + captured = capsys.readouterr() + assert "Upgrade warnings" not in captured.out + assert "UPG" not in captured.out diff --git a/tests/unit/test_code_lists.py b/tests/unit/test_code_lists.py index b98d807..01dcb49 100644 --- a/tests/unit/test_code_lists.py +++ b/tests/unit/test_code_lists.py @@ -283,3 +283,83 @@ def test_unknown_chapter_returns_none(self) -> None: """Unknown chapter returns None.""" assert get_hs_chapter_description("9901") is None assert get_hs_chapter_description("0100") is None + + +class TestSchemeDetectors: + """``is_unece_rec46_scheme`` / ``is_hs_scheme`` — added with the + VOC003 / VOC004 scheme-aware fix. + + These detectors decide whether the textile-pilot code validators + should fire for a given ``Classification.schemeId`` value. + Conservative substring matching (case-folded) avoids false + positives on UN CPC, NACE, GS1 GPC, and other classifications. + """ + + def test_unece_rec46_recognises_canonical_url_forms(self) -> None: + from dppvalidator.vocabularies.code_lists import is_unece_rec46_scheme + + assert is_unece_rec46_scheme("https://vocabulary.uncefact.org/unecerec46/") + assert is_unece_rec46_scheme("urn:un:unece:uncefact:codelist:standard:UNECE:Material:46") + assert is_unece_rec46_scheme("https://example.com/unece-rec-46/codes") + assert is_unece_rec46_scheme("https://example.com/unece/rec46/") + + def test_unece_rec46_rejects_other_classifications(self) -> None: + from dppvalidator.vocabularies.code_lists import is_unece_rec46_scheme + + # UN CPC — the scheme used by every v0.7 fixture under + # tests/fixtures/valid/. This is the failure mode the fix + # exists to prevent. + assert not is_unece_rec46_scheme("https://unstats.un.org/unsd/classifications/Econ/cpc/") + # Other common non-Rec46 schemes. + assert not is_unece_rec46_scheme("https://gs1.org/voc/CategoryCode") + assert not is_unece_rec46_scheme("https://wcoomd.org/hs-nomenclature") + assert not is_unece_rec46_scheme("urn:nace:r2:2008") + + def test_unece_rec46_rejects_falsy_inputs(self) -> None: + from dppvalidator.vocabularies.code_lists import is_unece_rec46_scheme + + assert not is_unece_rec46_scheme(None) + assert not is_unece_rec46_scheme("") + + def test_unece_rec46_is_case_insensitive(self) -> None: + from dppvalidator.vocabularies.code_lists import is_unece_rec46_scheme + + assert is_unece_rec46_scheme("HTTPS://EXAMPLE.COM/UNECE-REC-46") + assert is_unece_rec46_scheme("Rec46") + + def test_hs_scheme_recognises_canonical_url_forms(self) -> None: + from dppvalidator.vocabularies.code_lists import is_hs_scheme + + assert is_hs_scheme("https://wcoomd.org/hs-nomenclature/2017") + assert is_hs_scheme("urn:un:unece:uncefact:codelist:standard:WCO:HS:2022") + assert is_hs_scheme("https://example.com/harmonized-system/") + assert is_hs_scheme("https://example.com/customs/hs/") + + def test_hs_scheme_rejects_other_classifications(self) -> None: + from dppvalidator.vocabularies.code_lists import is_hs_scheme + + # UN CPC again — this is what the v0.7 fixtures use; the fix + # prevents VOC004 from firing on these. + assert not is_hs_scheme("https://unstats.un.org/unsd/classifications/Econ/cpc/") + assert not is_hs_scheme("urn:nace:r2:2008") + assert not is_hs_scheme("https://example.com/cpc/") + + def test_hs_scheme_rejects_falsy_inputs(self) -> None: + from dppvalidator.vocabularies.code_lists import is_hs_scheme + + assert not is_hs_scheme(None) + assert not is_hs_scheme("") + + def test_hs_scheme_anchors_path_tokens(self) -> None: + """``/hs/`` and ``/hs-`` only match path segments, not arbitrary text. + + The leading slash anchors the substring so e.g. ``brands.example.com`` + doesn't get false-matched by the ``hs`` substring. + """ + from dppvalidator.vocabularies.code_lists import is_hs_scheme + + # Negative — the bare ``hs`` substring inside a domain shouldn't + # match (it's not in a path segment). + assert not is_hs_scheme("https://example.com/things/") + # Positive — actual HS path segment. + assert is_hs_scheme("https://example.com/customs/hs/2022/") diff --git a/tests/unit/test_compat_upgrade.py b/tests/unit/test_compat_upgrade.py new file mode 100644 index 0000000..dd1e97a --- /dev/null +++ b/tests/unit/test_compat_upgrade.py @@ -0,0 +1,711 @@ +"""Phase 4 acceptance tests: UNTP DPP v0.6.x → v0.7.0 compatibility shim. + +This module is the unit-level coverage for +``src/dppvalidator/compat/upgrade_0_6_to_0_7.py``. Each test pins a +single transformation step from §Phase 4 of +``docs/plans/UNTP_0.7.0_MIGRATION.md`` and asserts both the structural +rewrite and the structured warnings the shim emits. + +Tests are organised by step number so it's easy to map a failing case +back to the migration plan. +""" + +from __future__ import annotations + +import json +from pathlib import Path +from typing import Any + +import pytest + +from dppvalidator.compat import ( + UPG_CODE_LOSSY, + UPG_CODE_REQUIRED_FIELD_MISSING, + UPG_CODE_SYNTHESISED, + UPG_CODE_UNMAPPED_COUNTRY, + UpgradeSeverity, + UpgradeWarning, + active_version, + is_version, + upgrade, +) + + +def _minimal_v06_payload(**overrides: Any) -> dict[str, Any]: + """Return a minimal v0.6 ProductPassport payload for use in shim tests.""" + base: dict[str, Any] = { + "type": ["DigitalProductPassport", "VerifiableCredential"], + "@context": [ + "https://www.w3.org/ns/credentials/v2", + "https://test.uncefact.org/vocabulary/untp/dpp/0.6.1/", + ], + "id": "https://example.com/credentials/x", + "issuer": {"id": "did:example:1", "name": "Example"}, + "validFrom": "2024-01-01T00:00:00Z", + "credentialSubject": { + "type": ["ProductPassport"], + "id": "https://example.com/subject/1", + "product": { + "type": ["Product"], + "id": "https://example.com/products/1", + "name": "Sample Product", + }, + }, + } + base.update(overrides) + return base + + +# --------------------------------------------------------------------------- +# active_version() / is_version() +# --------------------------------------------------------------------------- + + +class TestActiveVersionHelpers: + """Phase 4 introduces ``active_version()`` / ``is_version()``.""" + + def test_active_version_matches_default_schema_version(self) -> None: + from dppvalidator.schemas.registry import DEFAULT_SCHEMA_VERSION + + assert active_version() == DEFAULT_SCHEMA_VERSION + + def test_is_version_true_for_active(self) -> None: + assert is_version(active_version()) is True + + def test_is_version_false_for_other(self) -> None: + assert is_version("9.9.9") is False + + +# --------------------------------------------------------------------------- +# Type / contract checks +# --------------------------------------------------------------------------- + + +class TestUpgradeContract: + """``upgrade()`` is pure, doesn't mutate input, and rejects bad shapes.""" + + def test_rejects_non_dict(self) -> None: + with pytest.raises(TypeError): + upgrade("not a dict") # type: ignore[arg-type] + + def test_does_not_mutate_input(self) -> None: + src = _minimal_v06_payload() + before = json.dumps(src, sort_keys=True) + upgrade(src) + after = json.dumps(src, sort_keys=True) + assert before == after, "upgrade() must not mutate its input" + + def test_returns_tuple_of_dict_and_warning_list(self) -> None: + out, warnings = upgrade(_minimal_v06_payload()) + assert isinstance(out, dict) + assert isinstance(warnings, list) + for w in warnings: + assert isinstance(w, UpgradeWarning) + + +# --------------------------------------------------------------------------- +# Step 1 — context URL substitution +# --------------------------------------------------------------------------- + + +class TestStep1ContextRewrite: + def test_v061_context_becomes_v07_context(self) -> None: + out, _ = upgrade(_minimal_v06_payload()) + assert "https://vocabulary.uncefact.org/untp/0.7.0/context/" in out["@context"] + assert "https://test.uncefact.org/vocabulary/untp/dpp/0.6.1/" not in out["@context"] + + def test_v060_context_is_also_rewritten(self) -> None: + src = _minimal_v06_payload() + src["@context"] = [ + "https://www.w3.org/ns/credentials/v2", + "https://test.uncefact.org/vocabulary/untp/dpp/0.6.0/", + ] + out, _ = upgrade(src) + assert "https://vocabulary.uncefact.org/untp/0.7.0/context/" in out["@context"] + + def test_w3c_vc_context_preserved(self) -> None: + out, _ = upgrade(_minimal_v06_payload()) + assert "https://www.w3.org/ns/credentials/v2" in out["@context"] + + def test_unknown_context_entries_pass_through(self) -> None: + src = _minimal_v06_payload() + src["@context"].append("https://example.com/extension") + out, _ = upgrade(src) + assert "https://example.com/extension" in out["@context"] + + +# --------------------------------------------------------------------------- +# Step 2 — envelope required fields +# --------------------------------------------------------------------------- + + +class TestStep2EnvelopeRequiredFields: + def test_synthesises_name_from_product_name(self) -> None: + src = _minimal_v06_payload() + src.pop("name", None) + out, warnings = upgrade(src) + assert out["name"] == "Sample Product" + codes = [w.code for w in warnings if w.path == "name"] + assert UPG_CODE_SYNTHESISED in codes + + def test_warns_when_name_cannot_be_synthesised(self) -> None: + src = _minimal_v06_payload() + src["credentialSubject"]["product"].pop("name", None) + out, warnings = upgrade(src) + codes = [(w.code, w.severity) for w in warnings if w.path == "name"] + assert (UPG_CODE_REQUIRED_FIELD_MISSING, UpgradeSeverity.ERROR) in codes + + def test_warns_when_validFrom_missing(self) -> None: + src = _minimal_v06_payload() + src.pop("validFrom", None) + _, warnings = upgrade(src) + codes = [w.code for w in warnings if w.path == "validFrom"] + assert UPG_CODE_REQUIRED_FIELD_MISSING in codes + + +# --------------------------------------------------------------------------- +# Step 3 — drop ProductPassport envelope +# --------------------------------------------------------------------------- + + +class TestStep3FlattenEnvelope: + def test_credential_subject_is_product(self) -> None: + out, _ = upgrade(_minimal_v06_payload()) + cs = out["credentialSubject"] + assert cs["type"] == ["Product"] + assert "product" not in cs + + def test_granularity_level_renamed(self) -> None: + src = _minimal_v06_payload() + src["credentialSubject"]["granularityLevel"] = "batch" + out, _ = upgrade(src) + assert out["credentialSubject"]["idGranularity"] == "batch" + + def test_serial_number_renamed_to_item_number(self) -> None: + src = _minimal_v06_payload() + src["credentialSubject"]["product"]["serialNumber"] = "SN-42" + out, _ = upgrade(src) + cs = out["credentialSubject"] + assert cs.get("itemNumber") == "SN-42" + assert "serialNumber" not in cs + + def test_passport_id_preserved_when_product_has_none(self) -> None: + src = _minimal_v06_payload() + src["credentialSubject"]["product"].pop("id", None) + src["credentialSubject"]["id"] = "https://example.com/subject/keep" + out, _ = upgrade(src) + assert out["credentialSubject"]["id"] == "https://example.com/subject/keep" + + +# --------------------------------------------------------------------------- +# Step 4 — materialsProvenance → materialProvenance +# --------------------------------------------------------------------------- + + +class TestStep4MaterialsProvenance: + def test_materials_provenance_renamed_and_moved(self) -> None: + src = _minimal_v06_payload() + src["credentialSubject"]["materialsProvenance"] = [ + {"name": "Cotton", "originCountry": "EG", "massFraction": 0.5} + ] + out, _ = upgrade(src) + cs = out["credentialSubject"] + assert "materialsProvenance" not in cs + assert isinstance(cs["materialProvenance"], list) + assert cs["materialProvenance"][0]["name"] == "Cotton" + + +# --------------------------------------------------------------------------- +# Step 5 — dueDiligenceDeclaration → relatedDocument[] +# --------------------------------------------------------------------------- + + +class TestStep5DueDiligenceDeclaration: + def test_due_diligence_link_lands_in_related_document(self) -> None: + src = _minimal_v06_payload() + src["credentialSubject"]["dueDiligenceDeclaration"] = { + "linkURL": "https://example.com/dd", + "linkName": "Custom", + } + out, _ = upgrade(src) + cs = out["credentialSubject"] + assert "dueDiligenceDeclaration" not in cs + rd = cs["relatedDocument"] + assert any("dd" in entry.get("linkURL", "") for entry in rd) + + +# --------------------------------------------------------------------------- +# Step 6 — conformityClaim → performanceClaim with field renames +# --------------------------------------------------------------------------- + + +class TestStep6ConformityClaimToPerformanceClaim: + def test_conformity_claim_array_becomes_performance_claim(self) -> None: + src = _minimal_v06_payload() + src["credentialSubject"]["conformityClaim"] = [ + { + "id": "https://example.com/claims/1", + "description": "Sample claim", + "assessmentDate": "2024-03-15", + "conformityTopic": "environment.emissions", + "declaredValue": [ + { + "metricName": "GHG intensity", + "metricValue": {"value": 1.5, "unit": "KGM"}, + "score": "AA", + }, + ], + }, + ] + out, _ = upgrade(src) + cs = out["credentialSubject"] + assert "conformityClaim" not in cs + pc = cs["performanceClaim"] + assert len(pc) == 1 + claim = pc[0] + assert claim["claimDate"] == "2024-03-15" + assert isinstance(claim["conformityTopic"], list) + assert claim["conformityTopic"][0]["name"] == "environment.emissions" + assert isinstance(claim["claimedPerformance"], list) + perf = claim["claimedPerformance"][0] + assert perf["metric"]["name"] == "GHG intensity" + assert perf["measure"] == {"value": 1.5, "unit": "KGM"} + assert perf["score"]["code"] == "AA" + + def test_claim_with_no_name_falls_back_to_description(self) -> None: + src = _minimal_v06_payload() + src["credentialSubject"]["conformityClaim"] = [ + {"id": "https://example.com/claims/1", "description": "Sample"}, + ] + out, _ = upgrade(src) + assert out["credentialSubject"]["performanceClaim"][0]["name"] == "Sample" + + +# --------------------------------------------------------------------------- +# Step 7 — scorecards → Claim entries on performanceClaim +# --------------------------------------------------------------------------- + + +class TestStep7ScorecardsAsClaims: + def test_emissions_scorecard_becomes_claim(self) -> None: + src = _minimal_v06_payload() + src["credentialSubject"]["emissionsScorecard"] = { + "carbonFootprint": 1.8, + "primarySourcedRatio": 0.3, + } + out, _ = upgrade(src) + pc = out["credentialSubject"]["performanceClaim"] + emissions = next( + c for c in pc if any(t["name"] == "Emissions" for t in c.get("conformityTopic", [])) + ) + names = {p["metric"]["name"] for p in emissions["claimedPerformance"]} + assert "carbonFootprint" in names + assert "primarySourcedRatio" in names + + def test_circularity_scorecard_link_becomes_evidence(self) -> None: + src = _minimal_v06_payload() + src["credentialSubject"]["circularityScorecard"] = { + "recyclableContent": 0.5, + "recyclingInformation": { + "linkURL": "https://example.com/recycle", + "linkName": "Recycling guide", + }, + } + out, _ = upgrade(src) + pc = out["credentialSubject"]["performanceClaim"] + circ = next( + c for c in pc if any(t["name"] == "Circularity" for t in c.get("conformityTopic", [])) + ) + assert any("recycle" in (e.get("linkURL") or "") for e in circ.get("evidence", [])) + + def test_traceability_information_array_explodes(self) -> None: + src = _minimal_v06_payload() + src["credentialSubject"]["traceabilityInformation"] = [ + {"valueChainProcess": "Spinning", "verifiedRatio": 0.5}, + {"valueChainProcess": "Weaving", "verifiedRatio": 0.7}, + ] + out, _ = upgrade(src) + pc = out["credentialSubject"]["performanceClaim"] + trace = [ + c for c in pc if any(t["name"] == "Traceability" for t in c.get("conformityTopic", [])) + ] + assert len(trace) == 2 + + +# --------------------------------------------------------------------------- +# Step 8 — wrap scalar Standard / Regulation +# --------------------------------------------------------------------------- + + +class TestStep8ClaimReferencesAreLists: + def test_scalar_standard_wrapped_into_list(self) -> None: + src = _minimal_v06_payload() + src["credentialSubject"]["conformityClaim"] = [ + { + "id": "https://example.com/claims/1", + "description": "x", + "referenceStandard": {"id": "https://std.example/A", "name": "Std A"}, + }, + ] + out, _ = upgrade(src) + rs = out["credentialSubject"]["performanceClaim"][0]["referenceStandard"] + assert isinstance(rs, list) + assert rs[0]["name"] == "Std A" + + +# --------------------------------------------------------------------------- +# Step 9 — wrap scalar country codes +# --------------------------------------------------------------------------- + + +class TestStep9CountryCodes: + def test_scalar_country_wrapped_to_object(self) -> None: + src = _minimal_v06_payload() + src["credentialSubject"]["product"]["countryOfProduction"] = "DE" + out, _ = upgrade(src) + cop = out["credentialSubject"]["countryOfProduction"] + assert cop == {"countryCode": "DE"} or ( + isinstance(cop, dict) and cop["countryCode"] == "DE" + ) + + def test_country_lookup_populates_name(self) -> None: + src = _minimal_v06_payload() + src["credentialSubject"]["product"]["countryOfProduction"] = "DE" + out, _ = upgrade(src, country_lookup={"DE": "Germany"}) + assert out["credentialSubject"]["countryOfProduction"]["countryName"] == "Germany" + + def test_unknown_country_fires_upg003(self) -> None: + src = _minimal_v06_payload() + src["credentialSubject"]["product"]["countryOfProduction"] = "XX" + _, warnings = upgrade(src) + assert any(w.code == UPG_CODE_UNMAPPED_COUNTRY for w in warnings) + + def test_known_country_without_lookup_fires_upg002_info(self) -> None: + src = _minimal_v06_payload() + src["credentialSubject"]["product"]["countryOfProduction"] = "DE" + _, warnings = upgrade(src) + info_warnings = [ + w for w in warnings if w.code == UPG_CODE_SYNTHESISED and "countryName" in w.path + ] + assert info_warnings, "Expected UPG002 info warning for missing country lookup" + assert all(w.severity == UpgradeSeverity.INFO for w in info_warnings) + + def test_material_origin_country_also_wrapped(self) -> None: + src = _minimal_v06_payload() + src["credentialSubject"]["materialsProvenance"] = [ + { + "name": "X", + "originCountry": "DE", + "massFraction": 0.5, + "materialType": {"schemeID": "s", "schemeName": "S", "code": "c", "name": "n"}, + } + ] + out, _ = upgrade(src, country_lookup={"DE": "Germany"}) + m = out["credentialSubject"]["materialProvenance"][0] + assert m["originCountry"] == {"countryCode": "DE", "countryName": "Germany"} + + +# --------------------------------------------------------------------------- +# Step 10 — wrap Product.productCategory +# --------------------------------------------------------------------------- + + +class TestStep10WrapProductCategory: + def test_scalar_category_wrapped(self) -> None: + src = _minimal_v06_payload() + src["credentialSubject"]["product"]["productCategory"] = { + "schemeID": "s", + "schemeName": "S", + "code": "c", + "name": "n", + } + out, _ = upgrade(src) + pc = out["credentialSubject"]["productCategory"] + assert isinstance(pc, list) and len(pc) == 1 + + def test_existing_list_passthrough(self) -> None: + src = _minimal_v06_payload() + src["credentialSubject"]["product"]["productCategory"] = [ + {"schemeID": "s", "schemeName": "S", "code": "c", "name": "n"}, + ] + out, _ = upgrade(src) + assert isinstance(out["credentialSubject"]["productCategory"], list) + + +# --------------------------------------------------------------------------- +# Step 11 — producedByParty → relatedParty[] +# --------------------------------------------------------------------------- + + +class TestStep11ProducedByParty: + def test_scalar_party_becomes_party_role_with_manufacturer_role(self) -> None: + src = _minimal_v06_payload() + src["credentialSubject"]["product"]["producedByParty"] = { + "id": "did:example:mfr", + "name": "Manufacturer", + } + out, _ = upgrade(src) + cs = out["credentialSubject"] + assert "producedByParty" not in cs + rp = cs["relatedParty"] + assert len(rp) == 1 + assert rp[0]["role"] == "manufacturer" + assert rp[0]["party"]["name"] == "Manufacturer" + + +# --------------------------------------------------------------------------- +# Step 12 — furtherInformation → relatedDocument[] +# --------------------------------------------------------------------------- + + +class TestStep12FurtherInformation: + def test_further_information_appended_to_related_document(self) -> None: + src = _minimal_v06_payload() + src["credentialSubject"]["product"]["furtherInformation"] = [ + {"linkURL": "https://example.com/info1"}, + {"linkURL": "https://example.com/info2"}, + ] + out, _ = upgrade(src) + cs = out["credentialSubject"] + assert "furtherInformation" not in cs + rd = cs["relatedDocument"] + urls = [r["linkURL"] for r in rd] + assert "https://example.com/info1" in urls + assert "https://example.com/info2" in urls + + +# --------------------------------------------------------------------------- +# Step 13 — drop Product.registeredId with warning +# --------------------------------------------------------------------------- + + +class TestStep13DropRegisteredId: + def test_registered_id_dropped_and_warns(self) -> None: + src = _minimal_v06_payload() + src["credentialSubject"]["product"]["registeredId"] = "ABN-123" + out, warnings = upgrade(src) + assert "registeredId" not in out["credentialSubject"] + assert any(w.code == UPG_CODE_LOSSY and "registeredId" in w.path for w in warnings) + + +# --------------------------------------------------------------------------- +# Step 14 — Material.symbol → Image +# --------------------------------------------------------------------------- + + +class TestStep14MaterialSymbol: + def test_undefined_placeholder_dropped(self) -> None: + src = _minimal_v06_payload() + src["credentialSubject"]["materialsProvenance"] = [ + { + "name": "X", + "originCountry": "DE", + "massFraction": 0.5, + "materialType": {"schemeID": "s", "schemeName": "S", "code": "c", "name": "n"}, + "symbol": "undefined", + }, + ] + out, warnings = upgrade(src) + m = out["credentialSubject"]["materialProvenance"][0] + assert "symbol" not in m + assert any( + w.code == UPG_CODE_LOSSY and "symbol" in w.path and w.severity == UpgradeSeverity.INFO + for w in warnings + ) + + def test_real_base64_becomes_image_object(self) -> None: + # 16+ chars, valid base64 (padded) — passes _looks_like_base64. + sample_b64 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg==" + src = _minimal_v06_payload() + src["credentialSubject"]["materialsProvenance"] = [ + { + "name": "X", + "originCountry": "DE", + "massFraction": 0.5, + "materialType": {"schemeID": "s", "schemeName": "S", "code": "c", "name": "n"}, + "symbol": sample_b64, + }, + ] + out, warnings = upgrade(src) + m = out["credentialSubject"]["materialProvenance"][0] + assert isinstance(m["symbol"], dict) + assert m["symbol"]["imageData"] == sample_b64 + assert m["symbol"]["mediaType"] == "image/png" + assert m["symbol"]["name"] + assert any(w.code == UPG_CODE_SYNTHESISED and "symbol" in w.path for w in warnings) + + +# --------------------------------------------------------------------------- +# Step 15 — strip type arrays on embedded objects +# --------------------------------------------------------------------------- + + +class TestStep15StripEmbeddedTypes: + def test_dimension_type_stripped(self) -> None: + src = _minimal_v06_payload() + src["credentialSubject"]["product"]["dimensions"] = { + "type": ["Dimension"], + "weight": {"type": ["Measure"], "value": 1.0, "unit": "KGM"}, + } + out, _ = upgrade(src) + dims = out["credentialSubject"]["dimensions"] + assert "type" not in dims + assert "type" not in dims["weight"] + + def test_product_type_preserved(self) -> None: + out, _ = upgrade(_minimal_v06_payload()) + assert out["credentialSubject"]["type"] == ["Product"] + + +# --------------------------------------------------------------------------- +# Step 16 — schemeID → schemeId rename +# --------------------------------------------------------------------------- + + +class TestStep16SchemeIdRename: + def test_scheme_id_renamed_recursively(self) -> None: + src = _minimal_v06_payload() + src["credentialSubject"]["product"]["productCategory"] = [ + {"schemeID": "https://example.com/scheme", "schemeName": "S", "code": "c", "name": "n"}, + ] + out, _ = upgrade(src) + cls = out["credentialSubject"]["productCategory"][0] + assert "schemeID" not in cls + assert cls["schemeId"] == "https://example.com/scheme" + + def test_rename_inside_nested_classification(self) -> None: + src = _minimal_v06_payload() + src["credentialSubject"]["materialsProvenance"] = [ + { + "name": "X", + "originCountry": "DE", + "massFraction": 0.5, + "materialType": { + "schemeID": "https://example.com/scheme", + "schemeName": "S", + "code": "c", + "name": "n", + }, + }, + ] + out, _ = upgrade(src) + mt = out["credentialSubject"]["materialProvenance"][0]["materialType"] + assert "schemeID" not in mt + assert mt["schemeId"] == "https://example.com/scheme" + + +# --------------------------------------------------------------------------- +# Step 17 — Material required-field detection +# --------------------------------------------------------------------------- + + +class TestStep17MaterialRequiredFields: + def test_missing_material_type_emits_upg004(self) -> None: + src = _minimal_v06_payload() + src["credentialSubject"]["materialsProvenance"] = [ + {"name": "X", "originCountry": "DE", "massFraction": 0.5}, + ] + _, warnings = upgrade(src) + assert any( + w.code == UPG_CODE_REQUIRED_FIELD_MISSING and "materialType" in w.path for w in warnings + ) + + def test_missing_mass_fraction_emits_upg004(self) -> None: + src = _minimal_v06_payload() + src["credentialSubject"]["materialsProvenance"] = [ + { + "name": "X", + "originCountry": "DE", + "materialType": {"schemeID": "s", "schemeName": "S", "code": "c", "name": "n"}, + }, + ] + _, warnings = upgrade(src) + assert any( + w.code == UPG_CODE_REQUIRED_FIELD_MISSING and "massFraction" in w.path for w in warnings + ) + + +# --------------------------------------------------------------------------- +# Round-trip: every 0.6.x valid fixture upgrades and either validates +# cleanly or emits structured warnings. +# --------------------------------------------------------------------------- + + +_VALID_FIXTURE_DIR = Path(__file__).resolve().parents[1] / "fixtures" / "valid" + + +def _enveloped_v06_fixtures() -> list[Path]: + """Return ``valid/*.json`` fixtures that carry a full VC envelope. + + Some legacy fixtures (e.g. ``product_passport_instance_0.6.1.json``) + are bare ``ProductPassport`` shapes without the W3C VC v2 envelope — + they were captured for direct ``ProductPassport`` model tests, not + for the engine's full credential pipeline. The shim correctly + no-ops on them (nothing to upgrade), but they don't fit the + envelope-oriented round-trip assertions below. + """ + out: list[Path] = [] + for path in sorted(_VALID_FIXTURE_DIR.glob("*.json")): + data = json.loads(path.read_text(encoding="utf-8")) + if not (isinstance(data, dict) and "@context" in data and "credentialSubject" in data): + continue + # Skip v0.7-shaped fixtures vendored in Phase 5 — the shim upgrades + # *from* v0.6 only, not v0.7. + ctx = data.get("@context") or [] + if any(isinstance(c, str) and "vocabulary.uncefact.org/untp/0.7" in c for c in ctx): + continue + out.append(path) + return out + + +@pytest.mark.parametrize( + "fixture_path", + _enveloped_v06_fixtures(), + ids=lambda p: p.name, +) +def test_v06_fixtures_round_trip_through_shim(fixture_path: Path) -> None: + """Every enveloped 0.6.x ``valid/*.json`` fixture upgrades without crashing. + + Per the migration plan exit criterion: "every 0.6.x valid fixture + either upgrades and re-validates cleanly, or emits a documented + warning". This test asserts the *upgrade itself* never crashes; + re-validation is covered by the model integration test below. + """ + src = json.loads(fixture_path.read_text(encoding="utf-8")) + out, warnings = upgrade(src) + assert isinstance(out, dict) + assert "@context" in out + for w in warnings: + assert w.code.startswith("UPG"), f"unexpected code: {w.code}" + assert w.path, "warning path must not be empty" + assert w.message, "warning message must not be empty" + + +@pytest.mark.parametrize( + "fixture_path", + _enveloped_v06_fixtures(), + ids=lambda p: p.name, +) +def test_v06_fixtures_upgrade_to_v07_context_url(fixture_path: Path) -> None: + """The shim always swaps the v0.6 context URL for the v0.7 one.""" + src = json.loads(fixture_path.read_text(encoding="utf-8")) + out, _ = upgrade(src) + contexts = out["@context"] + assert any("vocabulary.uncefact.org/untp/0.7" in c for c in contexts) + assert not any("test.uncefact.org/vocabulary/untp/dpp/0.6" in c for c in contexts) + + +def test_bare_product_passport_does_not_crash_shim() -> None: + """Bare ProductPassport shapes (no VC envelope) should no-op cleanly. + + The shim is defined for v0.6 ``DigitalProductPassport`` envelopes; + a bare ``ProductPassport`` lacks the envelope and shouldn't make + the shim raise. It just won't do anything substantive. + """ + bare_path = _VALID_FIXTURE_DIR / "product_passport_instance_0.6.1.json" + if not bare_path.is_file(): + pytest.skip("bare ProductPassport fixture not present") + src = json.loads(bare_path.read_text(encoding="utf-8")) + out, warnings = upgrade(src) + assert isinstance(out, dict) + assert isinstance(warnings, list) diff --git a/tests/unit/test_credential_verifier.py b/tests/unit/test_credential_verifier.py index e8ad1a9..7dd22e5 100644 --- a/tests/unit/test_credential_verifier.py +++ b/tests/unit/test_credential_verifier.py @@ -1,11 +1,9 @@ """Unit tests for credential verification behavior.""" import base64 -import json from typing import Any from unittest.mock import MagicMock -import pytest from cryptography.hazmat.primitives.asymmetric import ed25519 from dppvalidator.verifier.did import DIDDocument, DIDResolver, VerificationMethod @@ -302,11 +300,29 @@ def test_ed25519_proof_without_value_returns_none(self) -> None: result = verifier._verify_ed25519_proof({}, {"type": "Ed25519Signature2020"}, vm) assert result is None - @pytest.mark.xfail(reason="Flaky in CI - signature verification timing issue") def test_ed25519_proof_with_base64_signature(self) -> None: - """Ed25519 proof with base64 signature is decoded.""" - # Generate a key and sign - private_key = ed25519.Ed25519PrivateKey.generate() + """Ed25519 proof with base64 signature round-trips through the verifier. + + Round-trips the verifier's own canonicalisation: we ask the + verifier to produce ``verify-data`` for a (credential, proof + options) pair, sign **those** bytes with a deterministic Ed25519 + key, attach the signature as the ``proofValue``, and then call + the verifier — which must recompute the same canonical bytes and + report a valid signature. + + Uses a fixed seed so the signature's base64 encoding is + reproducible across runs; previously this test generated a + random key and flaked ~1.5% of runs when the signature happened + to base64-encode with a leading ``z`` (which collides with the + multibase base58btc prefix). See + ``test_ed25519_proof_with_leading_z_base64_signature`` for the + explicit regression covering that case. + """ + verifier = CredentialVerifier() + + # Deterministic key (32-byte seed). Reproducible across runs. + seed = bytes(range(32)) + private_key = ed25519.Ed25519PrivateKey.from_private_bytes(seed) public_key = private_key.public_key() vm = VerificationMethod( @@ -320,24 +336,87 @@ def test_ed25519_proof_with_base64_signature(self) -> None: }, ) - credential = {"id": "urn:uuid:test"} - proof_options = {"type": "Ed25519Signature2020", "created": "2024-01-01"} + # A credential with @context so URDNA2015 produces non-empty + # canonical bytes — otherwise the verifier and the signer would + # operate on different message bytes. + credential = { + "@context": ["https://www.w3.org/ns/credentials/v2"], + "id": "urn:uuid:test", + "type": ["VerifiableCredential"], + } + proof_options = { + "type": "Ed25519Signature2020", + "created": "2024-01-01T00:00:00Z", + "verificationMethod": vm.id, + "proofPurpose": "assertionMethod", + } - # Create the message that will be verified - cred_json = json.dumps(credential, sort_keys=True, separators=(",", ":")) - proof_json = json.dumps(proof_options, sort_keys=True, separators=(",", ":")) - message = (proof_json + cred_json).encode("utf-8") + # Sign exactly what the verifier will verify. + message = verifier._create_verify_data(credential, proof_options) + assert message, "verify-data must be non-empty for a credential with @context" - # Sign it signature = private_key.sign(message) - proof_value = base64.b64encode(signature).decode() + proof = {**proof_options, "proofValue": base64.b64encode(signature).decode()} - proof = {**proof_options, "proofValue": proof_value} + result = verifier._verify_ed25519_proof(credential, proof, vm) + assert result is True + def test_ed25519_proof_with_leading_z_base64_signature(self) -> None: + """Regression: base64 signatures with leading ``z`` are not misread as multibase. + + Standard base64 contains ``z`` in its alphabet, so ~1/64 of + Ed25519 signatures encode with a leading ``z`` — colliding with + the multibase base58btc prefix. The verifier must fall back to + base64 when base58 decode fails on such a value. + """ verifier = CredentialVerifier() - result = verifier._verify_ed25519_proof(credential, proof, vm) - # Should return True for valid signature + credential = { + "@context": ["https://www.w3.org/ns/credentials/v2"], + "id": "urn:uuid:test-leading-z", + "type": ["VerifiableCredential"], + } + proof_options = { + "type": "Ed25519Signature2020", + "created": "2024-01-01T00:00:00Z", + "verificationMethod": "did:key:z6Mk...#key-1", + "proofPurpose": "assertionMethod", + } + message = verifier._create_verify_data(credential, proof_options) + assert message + + # Search seed space deterministically for a (key, signature) + # pair whose base64-encoded signature starts with 'z'. Expected + # to find one within ~64 tries; cap at 1000 for safety. + private_key = None + signature = None + for seed_int in range(1000): + candidate_key = ed25519.Ed25519PrivateKey.from_private_bytes( + seed_int.to_bytes(32, "big") + ) + candidate_sig = candidate_key.sign(message) + if base64.b64encode(candidate_sig).decode().startswith("z"): + private_key = candidate_key + signature = candidate_sig + break + assert private_key is not None, "failed to find leading-z base64 signature in 1000 seeds" + assert signature is not None + + public_key = private_key.public_key() + vm = VerificationMethod( + id="did:key:z6Mk...#key-1", + type="Ed25519VerificationKey2020", + controller="did:key:z6Mk...", + public_key_jwk={ + "kty": "OKP", + "crv": "Ed25519", + "x": base64.urlsafe_b64encode(public_key.public_bytes_raw()).rstrip(b"=").decode(), + }, + ) + proof = {**proof_options, "proofValue": base64.b64encode(signature).decode()} + assert proof["proofValue"].startswith("z") + + result = verifier._verify_ed25519_proof(credential, proof, vm) assert result is True diff --git a/tests/unit/test_deep_validation_v07.py b/tests/unit/test_deep_validation_v07.py new file mode 100644 index 0000000..9654ad4 --- /dev/null +++ b/tests/unit/test_deep_validation_v07.py @@ -0,0 +1,212 @@ +"""Phase 3b acceptance tests: version-keyed deep-validation paths. + +This module covers the ``LINK_PATHS_BY_VERSION`` half of Phase 3b +(``docs/plans/UNTP_0.7.0_MIGRATION.md``): + +1. The dispatch table covers every key in :data:`SCHEMA_REGISTRY`. +2. :class:`DeepValidator` selects the correct path list when + ``follow_links`` is left at the default ``None``. +3. The ``[*]`` list-iteration token in v0.7 paths is normalised correctly + by ``_get_urls_at_path`` — paths like + ``credentialSubject.performanceClaim[*].evidence`` extract URLs from + every element of the list. +4. Backward-compat: the legacy ``DEFAULT_LINK_PATHS`` constant still + imports and matches the v0.6.1 list byte-for-byte. + +The crawl integration test (full async traversal) lives elsewhere — this +module exercises the *path table* without touching the network. +""" + +from __future__ import annotations + +import pytest + +from dppvalidator.schemas.registry import SCHEMA_REGISTRY +from dppvalidator.validators.deep import ( + DEFAULT_LINK_PATHS, + LINK_PATHS_BY_VERSION, + DeepValidator, +) + +# --------------------------------------------------------------------------- +# 1. Dispatch-table consistency +# --------------------------------------------------------------------------- + + +class TestLinkPathDispatchConsistency: + """``LINK_PATHS_BY_VERSION`` must cover every registered version.""" + + def test_every_registered_version_has_paths(self) -> None: + missing = sorted(set(SCHEMA_REGISTRY) - set(LINK_PATHS_BY_VERSION)) + assert not missing, ( + f"LINK_PATHS_BY_VERSION is missing {missing}. " + "Add entries in src/dppvalidator/validators/deep.py." + ) + + def test_no_orphan_path_lists(self) -> None: + extra = sorted(set(LINK_PATHS_BY_VERSION) - set(SCHEMA_REGISTRY)) + assert not extra, f"LINK_PATHS_BY_VERSION has entries {extra} not in SCHEMA_REGISTRY." + + def test_v07_paths_use_new_envelope_shape(self) -> None: + """v0.7 paths target the new envelope (no ``credentialSubject.product`` traversal).""" + v07_paths = LINK_PATHS_BY_VERSION["0.7.0"] + # Every path roots at credentialSubject directly (not credentialSubject.product). + for p in v07_paths: + assert "credentialSubject.product." not in p, ( + f"v0.7 path uses the v0.6 envelope shape: {p!r}" + ) + + def test_v06_paths_unchanged(self) -> None: + """v0.6 paths must not have drifted — they're a backward-compat surface.""" + assert LINK_PATHS_BY_VERSION["0.6.1"] == [ + "credentialSubject.traceabilityEvents", + "credentialSubject.conformityClaim", + "credentialSubject.product.traceabilityInfo", + "credentialSubject.materialsProvenance", + ] + + +# --------------------------------------------------------------------------- +# 2. DEFAULT_LINK_PATHS backward-compat +# --------------------------------------------------------------------------- + + +def test_default_link_paths_matches_v0_6_1() -> None: + """The old ``DEFAULT_LINK_PATHS`` constant still resolves to the v0.6.1 list. + + Anyone who imported it pre-Phase-3b sees the same value, just sourced + from the dispatch table now. + """ + assert LINK_PATHS_BY_VERSION["0.6.1"] == DEFAULT_LINK_PATHS + + +# --------------------------------------------------------------------------- +# 3. DeepValidator picks the right paths per version +# --------------------------------------------------------------------------- + + +class TestDeepValidatorVersionDispatch: + @pytest.mark.parametrize( + ("version", "expected"), + [ + ("0.6.0", LINK_PATHS_BY_VERSION["0.6.0"]), + ("0.6.1", LINK_PATHS_BY_VERSION["0.6.1"]), + ("0.7.0", LINK_PATHS_BY_VERSION["0.7.0"]), + ], + ) + def test_default_follow_links_per_version(self, version: str, expected: list[str]) -> None: + validator = DeepValidator(schema_version=version, max_depth=0) + assert validator.follow_links == expected + assert validator.schema_version == version + + def test_explicit_follow_links_override(self) -> None: + custom = ["credentialSubject.id"] + validator = DeepValidator( + schema_version="0.7.0", + follow_links=custom, + max_depth=0, + ) + assert validator.follow_links is custom + + def test_unknown_version_falls_back_to_default(self) -> None: + validator = DeepValidator(schema_version="9.9.9", max_depth=0) + assert validator.follow_links == DEFAULT_LINK_PATHS + + +# --------------------------------------------------------------------------- +# 4. ``[*]`` list-iteration token handling +# --------------------------------------------------------------------------- + + +class TestStarTokenHandling: + """The ``[*]`` token in v0.7 paths must not break path traversal.""" + + @pytest.fixture + def validator(self) -> DeepValidator: + return DeepValidator(schema_version="0.7.0", max_depth=0) + + def test_star_in_path_extracts_from_every_list_element(self, validator: DeepValidator) -> None: + """``performanceClaim[*].evidence[*].linkURL`` extracts URLs from each row.""" + data = { + "credentialSubject": { + "performanceClaim": [ + { + "evidence": [ + {"linkURL": "https://example.com/evidence/a.pdf"}, + {"linkURL": "https://example.com/evidence/b.pdf"}, + ], + }, + { + "evidence": [ + {"linkURL": "https://example.com/evidence/c.pdf"}, + ], + }, + ], + }, + } + urls = validator._get_urls_at_path(data, "credentialSubject.performanceClaim[*].evidence") + assert sorted(urls) == [ + "https://example.com/evidence/a.pdf", + "https://example.com/evidence/b.pdf", + "https://example.com/evidence/c.pdf", + ] + + def test_star_normalises_to_implicit_iteration(self, validator: DeepValidator) -> None: + """``foo[*].bar`` and ``foo.bar`` produce the same result. + + The ``[*]`` token is purely a readability aid; the underlying + resolver iterates lists implicitly. + """ + data = { + "credentialSubject": { + "relatedParty": [ + {"party": {"id": "https://example.com/party/1"}}, + {"party": {"id": "https://example.com/party/2"}}, + ], + }, + } + with_star = validator._get_urls_at_path(data, "credentialSubject.relatedParty[*].party.id") + without_star = validator._get_urls_at_path(data, "credentialSubject.relatedParty.party.id") + assert with_star == without_star + assert sorted(with_star) == [ + "https://example.com/party/1", + "https://example.com/party/2", + ] + + def test_v07_relatedDocument_extraction(self, validator: DeepValidator) -> None: + """The simple ``credentialSubject.relatedDocument`` v0.7 path works.""" + data = { + "credentialSubject": { + "relatedDocument": [ + {"linkURL": "https://example.com/spec.pdf", "name": "Spec"}, + {"linkURL": "https://example.com/care.pdf", "name": "Care"}, + ], + }, + } + urls = validator._get_urls_at_path(data, "credentialSubject.relatedDocument") + assert sorted(urls) == [ + "https://example.com/care.pdf", + "https://example.com/spec.pdf", + ] + + +# --------------------------------------------------------------------------- +# 5. Empty-data robustness +# --------------------------------------------------------------------------- + + +def test_extract_links_handles_empty_payload() -> None: + """The crawler returns no links for an empty document.""" + validator = DeepValidator(schema_version="0.7.0", max_depth=0) + links = validator._extract_links({}, depth=0) + assert links == [] + + +def test_extract_links_skips_missing_paths() -> None: + """Paths that don't resolve in the payload don't error — just yield no links.""" + validator = DeepValidator(schema_version="0.7.0", max_depth=0) + data = {"credentialSubject": {"id": "https://example.com/p/1"}} + # Only ``credentialSubject`` is populated; the v0.7 paths target deeper + # fields that aren't in this minimal doc. Should yield no links, no errors. + links = validator._extract_links(data, depth=0) + assert links == [] diff --git a/tests/unit/test_detection.py b/tests/unit/test_detection.py index afb66a1..9cb31f1 100644 --- a/tests/unit/test_detection.py +++ b/tests/unit/test_detection.py @@ -112,6 +112,85 @@ def test_handles_none_context(self) -> None: data = {"@context": None, "type": ["DigitalProductPassport"]} assert detect_schema_version(data) == "0.6.1" + # ---- UNTP 0.7.0 detection (Phase 1) ---------------------------------- + + def test_detect_from_context_url_070_production(self) -> None: + """Detect 0.7.0 from the production CloudFront context URL.""" + data = { + "@context": [ + "https://www.w3.org/ns/credentials/v2", + "https://vocabulary.uncefact.org/untp/0.7.0/context/", + ], + "type": ["DigitalProductPassport"], + } + assert detect_schema_version(data) == "0.7.0" + + def test_detect_from_context_url_070_no_trailing_slash(self) -> None: + """``/context`` (no trailing slash) is also valid.""" + data = { + "@context": [ + "https://www.w3.org/ns/credentials/v2", + "https://vocabulary.uncefact.org/untp/0.7.0/context", + ], + "type": ["DigitalProductPassport"], + } + assert detect_schema_version(data) == "0.7.0" + + def test_detect_from_schema_url_070_modern(self) -> None: + """Detect 0.7.0 from a modern ``…/v0.7.0/dpp/DigitalProductPassport.json`` URL.""" + data = { + "$schema": ( + "https://opensource.unicc.org/un/unece/uncefact/spec-untp/-/raw/" + "707cd5267deddede24bb74e453a758561972a109/artefacts/schema/v0.7.0/" + "dpp/DigitalProductPassport.json" + ), + "type": ["DigitalProductPassport"], + } + assert detect_schema_version(data) == "0.7.0" + + def test_detect_from_schema_url_070_cloudfront(self) -> None: + """Detect 0.7.0 from a CloudFront-style schema URL (no ``v`` prefix).""" + data = { + "$schema": ( + "https://vocabulary.uncefact.org/untp/0.7.0/schema/dpp/DigitalProductPassport.json" + ), + "type": ["DigitalProductPassport"], + } + assert detect_schema_version(data) == "0.7.0" + + def test_unregistered_modern_version_falls_back(self) -> None: + """An unregistered version in a modern URL falls back to default.""" + data = { + "$schema": ("https://example.com/foo/v9.9.9/bar/DigitalProductPassport.json"), + "type": ["DigitalProductPassport"], + } + # 9.9.9 is not in SCHEMA_REGISTRY so detection ignores it. + assert detect_schema_version(data) == "0.6.1" + + def test_detect_from_upstream_070_sample(self) -> None: + """Real-world: vendored 0.7.0 sample is detected as 0.7.0.""" + import json + from pathlib import Path + + sample_path = ( + Path(__file__).resolve().parents[1] + / "fixtures" + / "upstream" + / "v0.7.0" + / "samples" + / "DigitalProductPassport_instance.json" + ) + if not sample_path.is_file(): # pragma: no cover - vendored in Phase 0 + import pytest + + pytest.skip( + "Upstream 0.7.0 sample missing; vendor via Phase 0 of " + "docs/plans/UNTP_0.7.0_MIGRATION.md." + ) + with sample_path.open() as f: + data = json.load(f) + assert detect_schema_version(data) == "0.7.0" + class TestIsDppDocument: """Tests for is_dpp_document function.""" diff --git a/tests/unit/test_errors.py b/tests/unit/test_errors.py index e2e83a4..517aaea 100644 --- a/tests/unit/test_errors.py +++ b/tests/unit/test_errors.py @@ -75,10 +75,25 @@ def test_finds_similar_type_values(self): assert "DigitalProductPassport" in matches def test_finds_similar_granularity_values(self): - """Finds similar granularity level values.""" + """Finds similar granularity level values (v0.6 spelling).""" matches = find_similar_values("itme", "granularityLevel") assert "item" in matches + def test_finds_similar_id_granularity_values(self): + """Finds similar id granularity values (v0.7 spelling). + + ``idGranularity`` is the v0.7 rename of ``granularityLevel``; + it shares the same enum values, so typo suggestions must work + for users on either UNTP version. + """ + matches = find_similar_values("itme", "idGranularity") + assert "item" in matches + + def test_finds_similar_party_role_values(self): + """Finds similar PartyRole.role values (v0.7 only).""" + matches = find_similar_values("manufactrer", "role") + assert "manufacturer" in matches + def test_finds_similar_scope_values(self): """Finds similar operational scope values.""" matches = find_similar_values("Scope1", "operationalScope") @@ -145,10 +160,47 @@ def test_type_values_include_dpp(self): assert "VerifiableCredential" in KNOWN_VALUES["type"] def test_granularity_levels_defined(self): - """Granularity levels are defined.""" - assert "item" in KNOWN_VALUES["granularityLevel"] - assert "batch" in KNOWN_VALUES["granularityLevel"] - assert "model" in KNOWN_VALUES["granularityLevel"] + """Granularity levels are defined under both v0.6 and v0.7 spellings.""" + for spelling in ("granularityLevel", "idGranularity"): + assert "item" in KNOWN_VALUES[spelling] + assert "batch" in KNOWN_VALUES[spelling] + assert "model" in KNOWN_VALUES[spelling] + # Both spellings share the same value list — drift would mean + # one version's users get worse typo suggestions than the other. + assert KNOWN_VALUES["granularityLevel"] == KNOWN_VALUES["idGranularity"] + + def test_party_role_values_defined(self): + """v0.7 PartyRole.role enum is exposed for typo suggestions.""" + assert "manufacturer" in KNOWN_VALUES["role"] + assert "recycler" in KNOWN_VALUES["role"] + assert "brandOwner" in KNOWN_VALUES["role"] + + def test_party_role_values_match_v07_enum(self): + """``KNOWN_VALUES['role']`` mirrors ``PartyRoleEnum`` exactly. + + Drift catch: adding a new role to the enum without updating + the suggestion list silently degrades typo support for that + role. Pinning equality here forces both surfaces to evolve + together. + """ + from dppvalidator.models.v0_7.identifiers import PartyRoleEnum + + enum_values = {member.value for member in PartyRoleEnum} + known_values = set(KNOWN_VALUES["role"]) + assert enum_values == known_values, ( + "PartyRoleEnum and KNOWN_VALUES['role'] disagree: " + f"in enum but not known: {enum_values - known_values}; " + f"known but not in enum: {known_values - enum_values}" + ) + + def test_id_granularity_values_match_v07_enum(self): + """``KNOWN_VALUES['idGranularity']`` mirrors ``IdGranularity`` exactly.""" + from dppvalidator.models.v0_7.product import IdGranularity + + enum_values = {member.value for member in IdGranularity} + assert enum_values == set(KNOWN_VALUES["idGranularity"]), ( + "IdGranularity enum and KNOWN_VALUES['idGranularity'] are out of sync." + ) def test_operational_scopes_defined(self): """Operational scopes are defined.""" diff --git a/tests/unit/test_eudpp_export_v07.py b/tests/unit/test_eudpp_export_v07.py new file mode 100644 index 0000000..37ae9e6 --- /dev/null +++ b/tests/unit/test_eudpp_export_v07.py @@ -0,0 +1,226 @@ +"""Phase 3c acceptance tests: v0.7 EU DPP JSON-LD export. + +This module covers the exporter half of Phase 3c (see +``docs/plans/UNTP_0.7.0_MIGRATION.md``): + +1. :class:`EUDPPTermMapper` indexes the right column per version. +2. :class:`EUDPPJsonLDExporter` auto-detects the source UNTP version from + the passport class's module path, and an explicit ``schema_version`` + override takes precedence. +3. v0.7 round-trip: a v0.7 sample exports to EU DPP JSON-LD with v0.7 + spellings (``itemNumber``, ``materialProvenance``, ``idGranularity``) + correctly mapped to the same EU DPP URIs as their v0.6 counterparts. +4. ``gtin`` does not appear in the v0.7 export (correctly removed). +""" + +from __future__ import annotations + +import json +from pathlib import Path + +import pytest + +from dppvalidator.exporters.eudpp_jsonld import ( + EUDPPJsonLDExporter, + EUDPPTermMapper, + export_eudpp_jsonld_dict, + get_term_mapping_summary, + validate_eudpp_export, +) +from dppvalidator.models import v0_7 +from dppvalidator.schemas.registry import DEFAULT_SCHEMA_VERSION + +_UPSTREAM_DIR = Path(__file__).resolve().parents[1] / "fixtures" / "upstream" / "v0.7.0" + + +def _load_canonical() -> dict: + p = _UPSTREAM_DIR / "samples" / "DigitalProductPassport_instance.json" + if not p.is_file(): # pragma: no cover — vendored in Phase 0 + pytest.skip(f"Upstream sample missing: {p}") + with p.open(encoding="utf-8") as f: + return json.load(f) + + +@pytest.fixture +def passport_v07() -> v0_7.DigitalProductPassport: + return v0_7.DigitalProductPassport.model_validate(_load_canonical()) + + +# --------------------------------------------------------------------------- +# 1. EUDPPTermMapper picks the right column +# --------------------------------------------------------------------------- + + +class TestEudppTermMapperPerVersion: + """The mapper indexes the correct UNTP spellings per version.""" + + def test_v06_mapper_uses_canonical_spellings(self) -> None: + mapper = EUDPPTermMapper(schema_version="0.6.1") + assert mapper.map_key("serialNumber") == "uniqueProductID" + assert mapper.map_key("granularityLevel") == "granularity" + assert mapper.map_key("producedByParty") == "hasManufacturer" + assert mapper.map_key("gtin") == "GTIN" + + def test_v07_mapper_uses_renamed_spellings(self) -> None: + mapper = EUDPPTermMapper(schema_version="0.7.0") + assert mapper.map_key("itemNumber") == "uniqueProductID" + assert mapper.map_key("idGranularity") == "granularity" + assert mapper.map_key("relatedParty") == "hasManufacturer" + assert mapper.map_key("materialProvenance") == "hasMaterialProvenance" + assert mapper.map_key("performanceClaim") == "hasPerformanceClaim" + + def test_v07_mapper_does_not_recognise_v06_only_terms(self) -> None: + """v0.6-only terms (renamed in v0.7) don't resolve under the v0.7 mapper. + + ``map_key`` returns the input unchanged when the term isn't in the + mapper's index — that's the documented "passthrough" behaviour. + """ + mapper = EUDPPTermMapper(schema_version="0.7.0") + # ``serialNumber`` is the v0.6 spelling; v0.7 says ``itemNumber``. + assert mapper.map_key("serialNumber") == "serialNumber" + assert mapper.map_key("granularityLevel") == "granularityLevel" + assert mapper.map_key("materialsProvenance") == "materialsProvenance" + + def test_v07_mapper_omits_gtin(self) -> None: + """``gtin`` is removed in v0.7 → must not appear in the index.""" + mapper = EUDPPTermMapper(schema_version="0.7.0") + assert "gtin" not in mapper.mapped_keys + + def test_default_construction_matches_default_version(self) -> None: + """Constructing without args uses :data:`DEFAULT_SCHEMA_VERSION`.""" + m = EUDPPTermMapper() + assert m.schema_version == DEFAULT_SCHEMA_VERSION + + +# --------------------------------------------------------------------------- +# 2. EUDPPJsonLDExporter — explicit version + auto-detect +# --------------------------------------------------------------------------- + + +class TestExporterVersionDispatch: + """``EUDPPJsonLDExporter`` resolves the right mapper per call.""" + + def test_explicit_version_uses_pinned_mapper( + self, passport_v07: v0_7.DigitalProductPassport + ) -> None: + exporter = EUDPPJsonLDExporter(schema_version="0.7.0") + assert exporter.schema_version == "0.7.0" + result = exporter.export_dict(passport_v07) + assert "credentialSubject" in result + + def test_auto_detect_v07_from_module_path( + self, passport_v07: v0_7.DigitalProductPassport + ) -> None: + """A v0.7 passport auto-detects to the v0.7 mapper without explicit configuration.""" + exporter = EUDPPJsonLDExporter() + assert exporter.schema_version is None # not pinned + # Internal helper exposes the auto-detection result. + assert exporter._detect_version_from_passport(passport_v07) == "0.7.0" + + def test_auto_detect_unknown_falls_back_to_default(self) -> None: + """Passports outside the in-tree v0_X namespaces fall back to the default.""" + + class _Outsider: + credential_subject = None + + exporter = EUDPPJsonLDExporter() + assert ( + exporter._detect_version_from_passport(_Outsider()) # type: ignore[arg-type] + == DEFAULT_SCHEMA_VERSION + ) + + def test_explicit_version_overrides_auto_detect( + self, passport_v07: v0_7.DigitalProductPassport + ) -> None: + """An explicit ``schema_version='0.6.1'`` on a v0.7 passport overrides auto-detect. + + This is intentional — it lets callers force-export through the v0.6 + mapper for testing or downstream-compat scenarios. + """ + exporter = EUDPPJsonLDExporter(schema_version="0.6.1") + result = exporter.export_dict(passport_v07) + # The exporter pins to v0.6, so v0.7-only spellings (``itemNumber``, + # ``materialProvenance``, …) wouldn't be in the v0.6 index and pass + # through unchanged. Sanity-check that it didn't crash. + assert "credentialSubject" in result + + +# --------------------------------------------------------------------------- +# 3. v0.7 round-trip: spellings map to expected EU DPP URIs +# --------------------------------------------------------------------------- + + +class TestV07ExportRoundtrip: + """Full v0.7 export produces a JSON-LD doc with EU DPP terms.""" + + def test_v07_passport_exports_cleanly(self, passport_v07: v0_7.DigitalProductPassport) -> None: + result = export_eudpp_jsonld_dict(passport_v07) + # Validates as an EU DPP export (has @context, type, etc.). + issues = validate_eudpp_export(result) + assert issues == [], f"Unexpected validation issues: {issues}" + + def test_v07_renames_are_mapped(self, passport_v07: v0_7.DigitalProductPassport) -> None: + """v0.7 spellings on the source side land at the right EU DPP keys. + + The canonical 0.7.0 sample doesn't include every renamed field, but + it has ``materialProvenance``, ``idGranularity``, ``relatedParty`` + which all flow through. + """ + result = export_eudpp_jsonld_dict(passport_v07) + cs = result.get("credentialSubject") + assert isinstance(cs, dict) + # ``materialProvenance`` (v0.7) → ``hasMaterialProvenance`` (EU DPP) + assert "hasMaterialProvenance" in cs + # ``relatedParty`` (v0.7) → ``hasManufacturer`` (EU DPP) + assert "hasManufacturer" in cs + # ``idGranularity`` (v0.7) is also surfaced at the document root via + # the metadata helper. + assert result.get("granularity") == cs.get("granularity") + + def test_v07_export_does_not_carry_v06_only_fields( + self, passport_v07: v0_7.DigitalProductPassport + ) -> None: + """v0.7 source has no ``gtin``, ``serialNumber``, ``materialsProvenance`` keys.""" + result = export_eudpp_jsonld_dict(passport_v07) + cs = result.get("credentialSubject", {}) + # These are v0.6-only field names. Even before mapping, they shouldn't + # be present on a v0.7 source. + for v06_only in ("gtin", "serialNumber", "materialsProvenance"): + assert v06_only not in cs, f"v0.6-only field {v06_only!r} leaked into v0.7 export" + + def test_v07_export_uses_eudpp_namespace_in_type( + self, passport_v07: v0_7.DigitalProductPassport + ) -> None: + result = export_eudpp_jsonld_dict(passport_v07) + type_arr = result.get("type") + assert isinstance(type_arr, list) + assert "eudpp:DPP" in type_arr + + +# --------------------------------------------------------------------------- +# 4. get_term_mapping_summary per version +# --------------------------------------------------------------------------- + + +class TestTermMappingSummaryPerVersion: + """The summary helper takes a version and reflects the right column.""" + + def test_v06_summary_includes_legacy_terms(self) -> None: + summary = get_term_mapping_summary("0.6.1") + assert summary.get("serialNumber") == "uniqueProductID" + assert summary.get("granularityLevel") == "granularity" + assert summary.get("gtin") == "GTIN" + + def test_v07_summary_uses_renamed_terms(self) -> None: + summary = get_term_mapping_summary("0.7.0") + assert summary.get("itemNumber") == "uniqueProductID" + assert summary.get("idGranularity") == "granularity" + assert summary.get("relatedParty") == "hasManufacturer" + assert "gtin" not in summary + assert "serialNumber" not in summary + + def test_default_summary_is_default_version(self) -> None: + """Default invocation returns :data:`DEFAULT_SCHEMA_VERSION`'s view.""" + default = get_term_mapping_summary() + # Same as the explicit default-version call. + assert default == get_term_mapping_summary(DEFAULT_SCHEMA_VERSION) diff --git a/tests/unit/test_manifest_integrity.py b/tests/unit/test_manifest_integrity.py new file mode 100644 index 0000000..07aea52 --- /dev/null +++ b/tests/unit/test_manifest_integrity.py @@ -0,0 +1,217 @@ +"""Phase 5 acceptance test: every bundled artefact matches MANIFEST.json. + +Cardinal rule 4 from ``.claude/rules/untp-versioning.md``: + +> Bundled artefacts have a manifest. Every JSON Schema and JSON-LD +> context vendored under ``src/dppvalidator/schemas/data/`` or +> ``src/dppvalidator/vocabularies/data/`` MUST appear in +> ``src/dppvalidator/schemas/data/MANIFEST.json`` with version, source +> URL, SHA-256, and pull date. CI verifies the hashes. + +This module is the CI verification end of that contract: + +1. Every entry in MANIFEST.json points at a file that exists. +2. Each file's SHA-256 matches the manifest pin. +3. The manifest is well-formed (every entry has the required fields). +4. (Drift catch) Every bundled JSON Schema / JSON-LD artefact is + *covered* by the manifest — adding a vendored file without a + manifest entry fails the suite. +""" + +from __future__ import annotations + +import hashlib +import json +from pathlib import Path +from typing import Any + +import pytest + +_REPO_ROOT = Path(__file__).resolve().parents[2] +_MANIFEST_PATH = _REPO_ROOT / "src" / "dppvalidator" / "schemas" / "data" / "MANIFEST.json" + +# Required keys every artefact entry must have. +_REQUIRED_FIELDS: frozenset[str] = frozenset( + {"version", "kind", "path", "source_url", "sha256", "pulled_at"} +) + +# Vendored-data directories scanned by the drift-catch test. +_VENDORED_DIRS: tuple[Path, ...] = ( + _REPO_ROOT / "src" / "dppvalidator" / "schemas" / "data", + _REPO_ROOT / "src" / "dppvalidator" / "vocabularies" / "data", +) + +# File extensions that count as "vendored" for the drift-catch test. +# README.md and __init__.py are infrastructure, not data, and don't need +# manifest entries. +_TRACKED_EXTENSIONS: frozenset[str] = frozenset({".json", ".jsonld"}) + +# Known files that intentionally aren't tracked by MANIFEST.json. These +# fall into two buckets: +# +# 1. Project-curated lists that don't have a single upstream-pinned +# source (countries, units, materials, HS codes). +# 2. CIRPASS-schema files: the CIRPASS axis has its own version pin +# (``CIRPASS_SCHEMA_VERSION`` in ``schemas/cirpass_loader.py``); +# folding them into MANIFEST.json would conflate two version axes. +_UNTRACKED_FILES: frozenset[str] = frozenset( + { + "MANIFEST.json", # the manifest itself + # Project-curated vocab data. + "countries.json", + "units.json", + "materials.json", + "hs_codes.json", + # CIRPASS schemas — separate version axis (cirpass_loader.py). + "cirpass_dpp_schema.json", + "cirpass_dpp_openapi.json", + } +) + + +def _load_manifest() -> dict[str, Any]: + return json.loads(_MANIFEST_PATH.read_text(encoding="utf-8")) + + +def _normalise_for_hash(content: bytes) -> bytes: + """Match the SHA-256 normalisation used by the registry's verifier. + + ``SchemaVersion.verify_integrity`` normalises ``\\r\\n`` to ``\\n`` + before hashing so cross-platform line-ending differences don't + invalidate the pins. The manifest hashes are computed against the + same normalised bytes. + """ + return content.replace(b"\r\n", b"\n") + + +# --------------------------------------------------------------------------- +# Manifest is well-formed +# --------------------------------------------------------------------------- + + +class TestManifestStructure: + """Sanity checks on MANIFEST.json itself.""" + + def test_manifest_file_exists(self) -> None: + assert _MANIFEST_PATH.is_file(), f"missing manifest at {_MANIFEST_PATH}" + + def test_manifest_is_valid_json(self) -> None: + # Will raise if the manifest is malformed. + _load_manifest() + + def test_manifest_has_artefacts_array(self) -> None: + manifest = _load_manifest() + assert isinstance(manifest.get("artefacts"), list) + assert manifest["artefacts"], "artefacts array is empty" + + def test_every_entry_has_required_fields(self) -> None: + manifest = _load_manifest() + for entry in manifest["artefacts"]: + missing = _REQUIRED_FIELDS - set(entry.keys()) + assert not missing, ( + f"manifest entry for {entry.get('path')!r} missing fields: {sorted(missing)}" + ) + + def test_paths_are_repo_relative(self) -> None: + """Every ``path`` field is a relative path under ``src/``.""" + manifest = _load_manifest() + for entry in manifest["artefacts"]: + path = entry["path"] + assert not path.startswith("/"), f"path is absolute: {path!r}" + assert path.startswith("src/dppvalidator/"), ( + f"path is not under src/dppvalidator/: {path!r}" + ) + + +# --------------------------------------------------------------------------- +# Each manifest entry resolves to an existing file with matching hash +# --------------------------------------------------------------------------- + + +def _manifest_entries_with_files() -> list[tuple[str, Path, str]]: + """Materialise ``(label, file_path, expected_sha256)`` triples.""" + manifest = _load_manifest() + out: list[tuple[str, Path, str]] = [] + for entry in manifest["artefacts"]: + label = f"{entry['kind']}@{entry['version']}" + out.append( + (label, _REPO_ROOT / entry["path"], entry["sha256"]), + ) + return out + + +@pytest.mark.parametrize( + ("label", "file_path", "expected_sha256"), + _manifest_entries_with_files(), + ids=lambda v: v if isinstance(v, str) else getattr(v, "name", str(v)), +) +def test_manifest_entry_file_exists_and_hash_matches( + label: str, file_path: Path, expected_sha256: str +) -> None: + """The bundled file exists and its SHA-256 matches the manifest pin. + + Failure here means either: + - the file was edited without re-running the manifest update; or + - the manifest pin is stale. + + Either way: re-pull the upstream artefact, refresh the SHA in + MANIFEST.json, and re-run the suite. + """ + assert file_path.is_file(), f"manifest entry {label!r} points at missing file {file_path}" + raw = file_path.read_bytes() + actual = hashlib.sha256(_normalise_for_hash(raw)).hexdigest() + assert actual == expected_sha256, ( + f"SHA-256 mismatch for {label} ({file_path.name}):\n" + f" manifest: {expected_sha256}\n" + f" on disk: {actual}\n" + "Either the file was modified or the manifest is stale." + ) + + +# --------------------------------------------------------------------------- +# Drift catch: every vendored .json/.jsonld is in the manifest +# --------------------------------------------------------------------------- + + +def _all_vendored_files() -> list[Path]: + """Return every vendored file under the tracked data directories. + + Skips directory infrastructure (``__init__.py``, ``__pycache__``, + ``README.md``) and files in the explicit allow-list of + project-curated vocabularies (``countries.json`` etc.). + """ + out: list[Path] = [] + for root in _VENDORED_DIRS: + if not root.is_dir(): # pragma: no cover — repo layout is stable + continue + for path in root.rglob("*"): + if not path.is_file(): + continue + if path.suffix not in _TRACKED_EXTENSIONS: + continue + if path.name in _UNTRACKED_FILES: + continue + out.append(path) + return sorted(out) + + +def test_every_vendored_file_is_manifested() -> None: + """Every ``.json``/``.jsonld`` under data dirs has a manifest entry. + + Adding a new vendored upstream artefact without a manifest entry + silently bypasses CI hash verification — this guard catches that + drift. + """ + manifest = _load_manifest() + manifested_paths = {(_REPO_ROOT / e["path"]).resolve() for e in manifest["artefacts"]} + untracked: list[Path] = [] + for vendored in _all_vendored_files(): + if vendored.resolve() not in manifested_paths: + untracked.append(vendored.relative_to(_REPO_ROOT)) + assert not untracked, ( + f"{len(untracked)} vendored file(s) lack manifest entries:\n" + + "\n".join(f" - {p}" for p in untracked) + + "\nAdd them to src/dppvalidator/schemas/data/MANIFEST.json or " + "extend tests/unit/test_manifest_integrity.py:_UNTRACKED_FILES if " + "they're project-curated (not vendored)." + ) diff --git a/tests/unit/test_no_version_literals.py b/tests/unit/test_no_version_literals.py new file mode 100644 index 0000000..be0f5c4 --- /dev/null +++ b/tests/unit/test_no_version_literals.py @@ -0,0 +1,128 @@ +"""Guard test: forbid bare ``"X.Y.Z"`` version literals in source code. + +This test enforces the contract documented in +``docs/plans/UNTP_0.7.0_MIGRATION.md`` §Phase 1 / §7.1 (rule 1) and +``.claude/rules/untp-versioning.md`` (cardinal rule 1): + +> No bare UNTP/CIRPASS version literals. A string like ``"0.6.1"`` or +> ``"0.7.0"`` may only appear in ``schemas/registry.py``, +> ``exporters/contexts.py``, and ``schemas/cirpass_loader.py``. Everywhere +> else: look it up via ``SchemaRegistry``, ``ContextManager``, or +> ``dppvalidator.compat.active_version()``. + +When this test fails, the right fix is almost always to import +``DEFAULT_SCHEMA_VERSION`` from ``dppvalidator.schemas.registry`` and use it +as the constructor default / dataclass default / argparse default — not to +add the new file to ``ALLOWED``. +""" + +from __future__ import annotations + +import re +from pathlib import Path + +import pytest + +# Pattern: a "double-quoted" SemVer-shaped literal anywhere in a Python source +# line. We intentionally do NOT match version-like substrings that appear +# inside longer URLs (e.g. "https://.../0.6.1/..."), because the version is +# not the entire content between the surrounding quotes there. +_VERSION_LITERAL_PATTERN = re.compile(r'"\d+\.\d+\.\d+"') + +# Files allowed to contain bare version literals. These are all *registries* +# in the strict §7.1 sense ("source of truth per surface"): each one defines +# a single dispatch table whose entries MUST be literal version strings +# keyed to :data:`SCHEMA_REGISTRY`. Adding a new UNTP version is a one-line +# change in each. +# +# - ``schemas/registry.py`` : the ``SchemaVersion`` registry itself. +# - ``exporters/contexts.py`` : the JSON-LD ``ContextDefinition`` registry. +# - ``schemas/cirpass_loader.py`` : the CIRPASS-2 ``CIRPASS_SCHEMA_VERSION`` +# constant (separate version axis). +# - ``validators/model.py`` : the ``_MODEL_BY_VERSION`` Pydantic-class +# dispatch (Phase 3.3). +# - ``validators/deep.py`` : the ``LINK_PATHS_BY_VERSION`` deep-crawl +# path dispatch (Phase 3b). +# - ``validators/rules/__init__`` : the ``ALL_RULES_BY_VERSION`` semantic-rule +# dispatch (Phase 3b). +_ALLOWED_FILES = frozenset( + { + Path("src/dppvalidator/schemas/registry.py"), + Path("src/dppvalidator/exporters/contexts.py"), + Path("src/dppvalidator/schemas/cirpass_loader.py"), + Path("src/dppvalidator/validators/model.py"), + Path("src/dppvalidator/validators/deep.py"), + Path("src/dppvalidator/validators/rules/__init__.py"), + } +) + + +def _project_root() -> Path: + """Locate the repo root by walking up from this test file.""" + return Path(__file__).resolve().parents[2] + + +def _violations() -> list[tuple[Path, int, str, str]]: + """Collect every bare version-literal occurrence outside the allow-list. + + Returns: + Tuples of ``(relative_path, line_number, matched_literal, line_excerpt)``. + """ + root = _project_root() + src = root / "src" / "dppvalidator" + if not src.is_dir(): + # If the layout ever changes, fail loudly instead of silently passing. + raise RuntimeError(f"Expected source tree at {src}") + + out: list[tuple[Path, int, str, str]] = [] + for path in sorted(src.rglob("*.py")): + rel = path.relative_to(root) + if rel in _ALLOWED_FILES: + continue + for lineno, line in enumerate(path.read_text(encoding="utf-8").splitlines(), start=1): + for match in _VERSION_LITERAL_PATTERN.findall(line): + out.append((rel, lineno, match, line.strip())) + return out + + +def test_no_bare_version_literals_in_src() -> None: + """Every UNTP/CIRPASS version literal must live in one of the registries. + + See module docstring for the rationale and the fix. + """ + violations = _violations() + if not violations: + return + + pretty = "\n".join( + f" {rel}:{lineno}: {literal}\n {excerpt[:120]}" + for rel, lineno, literal, excerpt in violations + ) + allowed = "\n".join(f" - {p}" for p in sorted(_ALLOWED_FILES)) + pytest.fail( + f"\n{len(violations)} bare version literal(s) found outside the registry " + f"allow-list.\n\nViolations:\n{pretty}\n\nAllowed files:\n{allowed}\n\n" + "Fix: import DEFAULT_SCHEMA_VERSION from dppvalidator.schemas.registry " + "and reference that constant instead of hardcoding the string. " + "See docs/plans/UNTP_0.7.0_MIGRATION.md §Phase 1.\n", + ) + + +def test_allow_list_files_exist() -> None: + """Every allow-listed path must exist; otherwise the guard is a tautology.""" + root = _project_root() + missing = [p for p in _ALLOWED_FILES if not (root / p).is_file()] + assert not missing, ( + f"Allow-list references non-existent file(s): {missing}. " + "Update _ALLOWED_FILES to match the current source layout." + ) + + +def test_pattern_matches_expected_strings() -> None: + """Self-test: confirm the regex catches the kinds of literal we care about.""" + assert _VERSION_LITERAL_PATTERN.search('default = "0.6.1"') + assert _VERSION_LITERAL_PATTERN.search('schema_version: str = "0.7.0"') + # URL-embedded versions must NOT match — they're inside longer strings. + assert not _VERSION_LITERAL_PATTERN.search('"https://example.com/0.6.1/schema.json"') + # Two-part versions are not matched. + assert not _VERSION_LITERAL_PATTERN.search('python_requires = "3.10"') diff --git a/tests/unit/test_ontology_v07.py b/tests/unit/test_ontology_v07.py new file mode 100644 index 0000000..2092bd8 --- /dev/null +++ b/tests/unit/test_ontology_v07.py @@ -0,0 +1,183 @@ +"""Phase 3c acceptance tests: per-version ``TermMapping`` columns. + +This module covers the ontology half of Phase 3c (see +``docs/plans/UNTP_0.7.0_MIGRATION.md``): + +1. ``TermMapping.term_for(version)`` resolves the correct spelling per + version. Renames (``serialNumber`` → ``itemNumber``, ``producedByParty`` + → ``relatedParty``, ``granularityLevel`` → ``idGranularity``, + ``materialsProvenance`` → ``materialProvenance``, + ``conformityClaim`` → ``performanceClaim``) round-trip. +2. The :data:`TERM_REMOVED` sentinel collapses to ``None`` when consulted + per-version: ``gtin`` is gone in v0.7. +3. :class:`OntologyMapper` exposes the right per-version forward index via + :meth:`mapped_terms_for` and :meth:`find_mapping_for_term`. +4. The default (no-version) API still returns the v0.6 canonical spelling + so pre-Phase-3c callers and the existing test suite are unaffected. +""" + +from __future__ import annotations + +import pytest + +from dppvalidator.vocabularies.ontology import ( + TERM_MAPPINGS, + TERM_REMOVED, + OntologyMapper, + TermMapping, +) + +# --------------------------------------------------------------------------- +# 1. TermMapping.term_for() resolution +# --------------------------------------------------------------------------- + + +class TestTermMappingPerVersion: + """The new ``term_for()`` method resolves to the right spelling.""" + + @pytest.mark.parametrize( + ("v06", "v07"), + [ + ("serialNumber", "itemNumber"), + ("granularityLevel", "idGranularity"), + ("producedByParty", "relatedParty"), + ("materialsProvenance", "materialProvenance"), + ("conformityClaim", "performanceClaim"), + ], + ) + def test_renames_round_trip(self, v06: str, v07: str) -> None: + """Renamed fields resolve to the new spelling under v0.7.""" + mapper = OntologyMapper() + # Forward look-up by the v0.6 canonical key still works. + mapping = mapper.get_mapping(v06) + assert mapping is not None, f"missing mapping for v0.6 term {v06!r}" + assert mapping.term_for("0.6.1") == v06 + assert mapping.term_for("0.6.0") == v06 + assert mapping.term_for("0.7.0") == v07 + + def test_unchanged_fields_resolve_to_canonical(self) -> None: + """Fields with no version-specific rename use ``untp_term`` for both.""" + mapping = next(m for m in TERM_MAPPINGS if m.untp_term == "validFrom") + assert mapping.term_for("0.6.1") == "validFrom" + assert mapping.term_for("0.7.0") == "validFrom" + + def test_unknown_version_falls_back_to_canonical(self) -> None: + """Forward-compat: an unrecognised version returns the canonical spelling.""" + mapping = next(m for m in TERM_MAPPINGS if m.untp_term == "name") + assert mapping.term_for("9.9.9") == "name" + + +class TestTermRemovedSentinel: + """``TERM_REMOVED`` collapses to ``None`` when resolving per-version.""" + + def test_gtin_removed_in_v07(self) -> None: + """``gtin`` exists in v0.6 but is removed in v0.7.""" + mapping = next(m for m in TERM_MAPPINGS if m.untp_term == "gtin") + assert mapping.term_for("0.6.1") == "gtin" + assert mapping.term_for("0.7.0") is None + # Sentinel is exposed for explicit comparison. + assert mapping.untp_v0_7 == TERM_REMOVED + + def test_term_removed_value_is_a_string_marker(self) -> None: + """The sentinel is a string so ``slots=True`` types stay clean.""" + assert isinstance(TERM_REMOVED, str) + assert TERM_REMOVED.startswith("<") and TERM_REMOVED.endswith(">") + + +# --------------------------------------------------------------------------- +# 2. OntologyMapper version-keyed lookup +# --------------------------------------------------------------------------- + + +class TestOntologyMapperVersionDispatch: + """``OntologyMapper`` per-version forward index.""" + + def test_mapped_terms_for_v06_includes_gtin(self) -> None: + mapper = OntologyMapper() + terms = mapper.mapped_terms_for("0.6.1") + assert "gtin" in terms + assert "serialNumber" in terms + + def test_mapped_terms_for_v07_excludes_gtin_renames_serialNumber(self) -> None: + mapper = OntologyMapper() + terms = mapper.mapped_terms_for("0.7.0") + assert "gtin" not in terms, "gtin must be excluded from the v0.7 index" + assert "serialNumber" not in terms, "serialNumber → itemNumber rename" + assert "itemNumber" in terms + assert "materialProvenance" in terms + assert "performanceClaim" in terms + + def test_find_mapping_for_term_with_v07_spelling(self) -> None: + """Looking up ``itemNumber`` (v0.7 spelling) returns the same row as ``serialNumber`` (v0.6).""" + mapper = OntologyMapper() + v06 = mapper.find_mapping_for_term("serialNumber", version="0.6.1") + v07 = mapper.find_mapping_for_term("itemNumber", version="0.7.0") + assert v06 is not None and v07 is not None + assert v06 is v07, "both version-spellings must resolve to the same TermMapping row" + + def test_find_mapping_default_falls_back_to_v07(self) -> None: + """Without an explicit ``version``, v0.7-only spellings still resolve. + + This keeps ``find_mapping_for_term('itemNumber')`` working for callers + who don't yet pass a version argument. + """ + mapper = OntologyMapper() + result = mapper.find_mapping_for_term("itemNumber") + assert result is not None + assert result.cirpass_uri == "eudpp:uniqueProductID" + + def test_to_untp_with_version_returns_correct_spelling(self) -> None: + """``to_untp(uri, version)`` returns the spelling for the given version.""" + mapper = OntologyMapper() + uri = "eudpp:uniqueProductID" + assert mapper.to_untp(uri, version="0.6.1") == "serialNumber" + assert mapper.to_untp(uri, version="0.7.0") == "itemNumber" + + def test_to_untp_with_version_returns_none_for_removed_terms(self) -> None: + """``to_untp`` returns ``None`` when the term is removed in that version.""" + mapper = OntologyMapper() + # ``eudpp:GTIN`` was the v0.6 ``gtin`` field; gone in v0.7. + assert mapper.to_untp("eudpp:GTIN", version="0.6.1") == "gtin" + assert mapper.to_untp("eudpp:GTIN", version="0.7.0") is None + + def test_to_untp_without_version_preserves_legacy_behaviour(self) -> None: + """No-version call returns the canonical (v0.6) spelling.""" + mapper = OntologyMapper() + # Legacy callers (pre-Phase-3c) get the same result they always did. + assert mapper.to_untp("eudpp:uniqueProductID") == "serialNumber" + assert mapper.to_untp("eudpp:GTIN") == "gtin" + + +# --------------------------------------------------------------------------- +# 3. Backward compatibility: pre-Phase-3c API surface +# --------------------------------------------------------------------------- + + +class TestBackwardCompatibility: + """Pre-Phase-3c callers and tests must keep working.""" + + def test_to_cirpass_unchanged(self) -> None: + mapper = OntologyMapper() + assert mapper.to_cirpass("Product") == "eudpp:Product" + assert mapper.to_cirpass("serialNumber") == "eudpp:uniqueProductID" + assert mapper.to_cirpass("nope") is None + + def test_mapping_count_includes_v07_only_rows(self) -> None: + """The mapping count reflects the table size — including new rows added in Phase 3c.""" + mapper = OntologyMapper() + # Phase 3c added 2 new rows (materialsProvenance, conformityClaim). + assert mapper.mapping_count == len(TERM_MAPPINGS) + + def test_mapped_terms_default_returns_canonical_spellings(self) -> None: + mapper = OntologyMapper() + # Default ``mapped_terms`` is keyed on the canonical (v0.6) spelling. + assert "serialNumber" in mapper.mapped_terms + assert "itemNumber" not in mapper.mapped_terms + + def test_termmapping_dataclass_is_frozen_and_slotted(self) -> None: + """The dataclass shape is part of the public contract.""" + from dataclasses import FrozenInstanceError + + m = TermMapping(untp_term="x", cirpass_uri="eudpp:y", description="z") + with pytest.raises(FrozenInstanceError): + m.untp_term = "mutated" # type: ignore[misc] diff --git a/tests/unit/test_phase2_schema_and_jsonld.py b/tests/unit/test_phase2_schema_and_jsonld.py new file mode 100644 index 0000000..f1a07a9 --- /dev/null +++ b/tests/unit/test_phase2_schema_and_jsonld.py @@ -0,0 +1,188 @@ +"""Phase 2 acceptance tests: schema-only and JSON-LD-only validation of 0.7.0. + +This module covers the two exit-criterion checks for Phase 2 of +``docs/plans/UNTP_0.7.0_MIGRATION.md``: + +1. The bundled 0.7.0 JSON Schema validates the upstream canonical 0.7.0 + sample with **zero** schema errors. This proves the in-tree + ``untp-dpp-schema-0.7.0.json`` is byte-equivalent to the upstream and + does not need ``Product.json`` ($ref-resolution at validate time) — the + schema is self-contained. + +2. The bundled JSON-LD context for 0.7.0 is reachable through + ``BUNDLED_CONTEXT_URLS`` so JSON-LD expansion works **offline**. The same + coverage holds retroactively for 0.6.1 (Phase 2 also vendored that + context to remove the prior implicit network dependency). + +Pydantic-model and semantic-rule validation for 0.7.0 are deliberately out +of scope here — they land in Phase 3 / 3b. +""" + +from __future__ import annotations + +import json +from pathlib import Path + +import pytest + +from dppvalidator.schemas.registry import SCHEMA_REGISTRY +from dppvalidator.validators.jsonld_semantic import BUNDLED_CONTEXT_URLS, JSONLDValidator +from dppvalidator.validators.schema import SchemaValidator + +# Vendored upstream samples (Phase 0). Skip individually if any path is +# missing so a partial checkout still runs the rest of the suite. +_UPSTREAM_DIR = Path(__file__).resolve().parents[1] / "fixtures" / "upstream" / "v0.7.0" +_CANONICAL_SAMPLE = _UPSTREAM_DIR / "samples" / "DigitalProductPassport_instance.json" +_BATTERY_SAMPLE = _UPSTREAM_DIR / "samples" / "DigitalProductPassport_battery_instance.json" +_CATHODE_SAMPLE = _UPSTREAM_DIR / "samples" / "DigitalProductPassport_cathode_instance.json" + + +def _load(path: Path) -> dict: + if not path.is_file(): # pragma: no cover - vendored in Phase 0 + pytest.skip( + f"Upstream sample missing at {path}; vendor via Phase 0 of " + "docs/plans/UNTP_0.7.0_MIGRATION.md.", + ) + with path.open(encoding="utf-8") as f: + return json.load(f) + + +# ---------------------------------------------------------------------------- +# 1. Schema-only validation (Layer 1) +# ---------------------------------------------------------------------------- + + +class TestSchemaLayerForUntp070: + """Drive the bundled 0.7.0 JSON Schema against the upstream samples.""" + + @pytest.mark.parametrize( + ("label", "path"), + [ + ("canonical", _CANONICAL_SAMPLE), + ("battery", _BATTERY_SAMPLE), + ("cathode", _CATHODE_SAMPLE), + ], + ) + def test_upstream_sample_passes_schema_validation(self, label: str, path: Path) -> None: + """Every vendored upstream 0.7.0 sample validates against the bundled schema.""" + sample = _load(path) + validator = SchemaValidator(schema_version="0.7.0") + result = validator.validate(sample) + assert result.valid is True, ( + f"Sample {label} failed schema validation with {len(result.errors)} " + f"error(s); first: {result.errors[0].message if result.errors else '(none)'}" + ) + assert result.errors == [] + assert result.schema_version == "0.7.0" + + def test_registry_sha_matches_bundled_file(self) -> None: + """The registry SHA pin matches the on-disk bytes.""" + import hashlib + from importlib import resources + + schema = SCHEMA_REGISTRY["0.7.0"] + assert schema.sha256 is not None, "SCHEMA_REGISTRY['0.7.0'].sha256 must be pinned (Phase 2)" + f = resources.files("dppvalidator.schemas.data").joinpath("untp-dpp-schema-0.7.0.json") + actual = hashlib.sha256(f.read_bytes()).hexdigest() + assert actual == schema.sha256, ( + f"Bundled 0.7.0 schema SHA mismatch.\n" + f" expected: {schema.sha256}\n actual: {actual}\n" + "Re-vendor or update the registry pin." + ) + + def test_bundled_schema_has_expected_top_level_required_fields(self) -> None: + """0.7.0 makes ``validFrom``, ``name``, ``credentialSubject`` required.""" + validator = SchemaValidator(schema_version="0.7.0") + schema = validator._load_schema() + assert set(schema.get("required", [])) >= { + "@context", + "id", + "issuer", + "validFrom", + "name", + "credentialSubject", + } + + def test_missing_validFrom_now_fails_schema(self) -> None: + """A 0.7.0 payload missing ``validFrom`` must fail Layer 1.""" + sample = _load(_CANONICAL_SAMPLE) + broken = {k: v for k, v in sample.items() if k != "validFrom"} + validator = SchemaValidator(schema_version="0.7.0") + result = validator.validate(broken) + assert result.valid is False + assert any( + "validFrom" in e.message or e.path.endswith("validFrom") for e in result.errors + ), f"Expected a validFrom-related error; got: {[e.message for e in result.errors]}" + + +# ---------------------------------------------------------------------------- +# 2. JSON-LD layer offline-readiness (Layer 4) +# ---------------------------------------------------------------------------- + + +class TestJsonLdLayerOffline: + """The bundled UNTP contexts must satisfy JSON-LD expansion without network.""" + + def test_bundled_urls_cover_both_versions(self) -> None: + """Both the 0.6.1 and 0.7.0 UNTP context URLs are bundled.""" + assert "https://test.uncefact.org/vocabulary/untp/dpp/0.6.1/" in BUNDLED_CONTEXT_URLS + assert "https://vocabulary.uncefact.org/untp/0.7.0/context/" in BUNDLED_CONTEXT_URLS + # Plus the W3C VC v2 context that was already there pre-Phase 2. + assert "https://www.w3.org/ns/credentials/v2" in BUNDLED_CONTEXT_URLS + + def test_070_sample_jsonld_expands_with_no_undefined_terms(self) -> None: + """A real 0.7.0 sample expands cleanly through PyLD using bundled contexts.""" + sample = _load(_CANONICAL_SAMPLE) + validator = JSONLDValidator(schema_version="0.7.0", strict=False) + result = validator.validate(sample) + # JLD001 (missing @context) must NOT fire; JLD002 (undefined term) is + # advisory in non-strict mode but should not produce errors here. + assert result.errors == [], ( + f"JSON-LD expansion produced errors: {[(e.code, e.message) for e in result.errors]}" + ) + + def test_070_sample_jsonld_expands_without_network(self, monkeypatch) -> None: + """Expansion of the canonical 0.7.0 sample must not hit the network. + + We swap PyLD's default document loader with one that raises if it is + invoked, then run JSON-LD validation through the validator's + :class:`CachingDocumentLoader` (which pre-populates the cache from + ``BUNDLED_CONTEXT_URLS``). + """ + from pyld import jsonld + + sentinel = "NETWORK ACCESS UNEXPECTEDLY ATTEMPTED in JSON-LD layer" + + def _trip(url, options=None): # noqa: ARG001 + raise RuntimeError(f"{sentinel}: {url}") + + monkeypatch.setattr(jsonld, "get_document_loader", lambda: _trip) + + sample = _load(_CANONICAL_SAMPLE) + validator = JSONLDValidator(schema_version="0.7.0", strict=False) + result = validator.validate(sample) + # If the trip wired in by monkeypatch fired, the failure would surface + # as a JsonLdError caught and reported as a JLD000-class error. Assert + # we got a clean (non-erroring) run. + assert all(sentinel not in e.message for e in result.errors + result.warnings), ( + "Network was hit despite the bundled context being available." + ) + assert result.errors == [], ( + f"Offline 0.7.0 JSON-LD validation produced unexpected errors: " + f"{[(e.code, e.message) for e in result.errors]}" + ) + + def test_061_legacy_sample_jsonld_expands_without_network(self, monkeypatch) -> None: + """Same offline guarantee retroactively holds for the 0.6.1 fixture.""" + from pyld import jsonld + + def _trip(url, options=None): # noqa: ARG001 + raise RuntimeError(f"NETWORK HIT: {url}") + + monkeypatch.setattr(jsonld, "get_document_loader", lambda: _trip) + + legacy = Path(__file__).resolve().parents[1] / "fixtures" / "valid" / "minimal_dpp.json" + sample = _load(legacy) + validator = JSONLDValidator(schema_version="0.6.1", strict=False) + result = validator.validate(sample) + assert all("NETWORK HIT" not in e.message for e in result.errors + result.warnings) diff --git a/tests/unit/test_plugins.py b/tests/unit/test_plugins.py index 587cba6..c768608 100644 --- a/tests/unit/test_plugins.py +++ b/tests/unit/test_plugins.py @@ -259,6 +259,121 @@ def check(self, _p): # noqa: ARG002 assert len(errors) == 2 +class TestRunAllValidatorsVersionFilter: + """Per-version plugin dispatch — Phase 6 of UNTP 0.7.0 migration. + + Plugins that declare ``applies_to_versions`` are skipped when the + payload's resolved version doesn't match. Plugins without that + attribute keep running for every version (back-compat). + """ + + @pytest.fixture + def passport(self) -> DigitalProductPassport: + """Minimal passport — the rules below don't introspect it.""" + return DigitalProductPassport( + id="https://example.com/dpp", + issuer=CredentialIssuer(id="https://example.com/issuer", name="Test"), + ) + + def test_plugin_with_matching_applies_to_versions_runs(self, passport): + """Plugin runs when ``schema_version`` is in ``applies_to_versions``.""" + registry = PluginRegistry(auto_discover=False) + + class V07OnlyRule: + rule_id = "V07_ONLY" + severity = "warning" + applies_to_versions = ("0.7.0",) + + def check(self, _p): # noqa: ARG002 + return [("$", "v0.7-only violation")] + + registry.register_validator("v07_only", V07OnlyRule()) + errors = registry.run_all_validators(passport, schema_version="0.7.0") + assert len(errors) == 1 + assert errors[0].code == "V07_ONLY" + + def test_plugin_with_non_matching_applies_to_versions_is_skipped(self, passport): + """Plugin is silently skipped when ``schema_version`` doesn't match. + + Critically: the skipped plugin must NOT raise and must NOT + produce a PLG001 entry — it's filtered before ``check()`` + is called. + """ + registry = PluginRegistry(auto_discover=False) + + class V07OnlyRuleThatWouldCrashOnV06: + rule_id = "V07_ONLY" + severity = "warning" + applies_to_versions = ("0.7.0",) + + def check(self, passport): + # Reach for a v0.7 attribute. If the engine routed a + # v0.6 passport here this would AttributeError. + return [(passport.credential_subject.does_not_exist, "x")] + + registry.register_validator("v07_only", V07OnlyRuleThatWouldCrashOnV06()) + errors = registry.run_all_validators(passport, schema_version="0.6.1") + # Filter applied, ``check()`` never called — zero errors. + assert errors == [] + + def test_plugin_without_applies_to_versions_runs_for_every_version(self, passport): + """Plugins predating Phase 6 still run for every version.""" + registry = PluginRegistry(auto_discover=False) + + class LegacyRule: + rule_id = "LEGACY" + severity = "info" + # No ``applies_to_versions``. + + def check(self, _p): # noqa: ARG002 + return [("$", "legacy violation")] + + registry.register_validator("legacy", LegacyRule()) + for version in ("0.6.0", "0.6.1", "0.7.0", "9.9.9"): + errors = registry.run_all_validators(passport, schema_version=version) + assert len(errors) == 1, f"legacy plugin should run for {version}" + + def test_no_schema_version_arg_runs_every_plugin(self, passport): + """Calling without ``schema_version`` ignores the filter entirely. + + Pre-Phase-6 callers (e.g. anyone invoking + ``PluginRegistry.run_all_validators(passport)``) get the same + behaviour they always had. + """ + registry = PluginRegistry(auto_discover=False) + + class V07OnlyRule: + rule_id = "V07_ONLY" + severity = "warning" + applies_to_versions = ("0.7.0",) + + def check(self, _p): # noqa: ARG002 + return [("$", "v0.7-only violation")] + + registry.register_validator("v07", V07OnlyRule()) + errors = registry.run_all_validators(passport) # no schema_version= + assert len(errors) == 1, "back-compat path: no version → no filter" + + def test_filter_works_on_class_registration_too(self, passport): + """``applies_to_versions`` on a class is honoured (not just on instances).""" + registry = PluginRegistry(auto_discover=False) + + class V07ClassRule: + rule_id = "V07_CLASS" + severity = "warning" + applies_to_versions = ("0.7.0",) + + def check(self, _p): # noqa: ARG002 + return [("$", "x")] + + # Register the class, not an instance — the filter must still see it. + registry.register_validator("v07_class", V07ClassRule) + errors_06 = registry.run_all_validators(passport, schema_version="0.6.1") + errors_07 = registry.run_all_validators(passport, schema_version="0.7.0") + assert errors_06 == [] + assert len(errors_07) == 1 + + class TestDefaultRegistry: """Tests for default registry singleton.""" diff --git a/tests/unit/test_production_url.py b/tests/unit/test_production_url.py new file mode 100644 index 0000000..7d78036 --- /dev/null +++ b/tests/unit/test_production_url.py @@ -0,0 +1,162 @@ +"""Cross-check the production-mirror URL split for UNTP DPP schemas. + +Polish step A from the post-Phase-6 review: ``SchemaVersion`` now +carries a ``production_url`` field for the canonical +``untp.unece.org`` hosting alongside the SHA-pinned ``url``. This +module pins the new contract so a future refactor doesn't silently +drop the production link. + +Pins: + +1. ``SchemaVersion.production_url`` is exposed as a frozen-dataclass + attribute and defaults to ``None`` when not set. +2. ``SchemaRegistry.get_production_url(version)`` returns the + registry's recorded production URL or ``None``. +3. The 0.7.0 entry has a non-``None`` production URL pointing at + ``untp.unece.org``. +4. The ``production_url`` recorded in MANIFEST.json (under the + ``untp-dpp-schema@0.7.0`` artefact) matches the registry entry — + the two surfaces must not drift. +""" + +from __future__ import annotations + +import json +from pathlib import Path + +import pytest + +from dppvalidator.schemas.registry import ( + SCHEMA_REGISTRY, + SchemaRegistry, + SchemaVersion, +) + +_REPO_ROOT = Path(__file__).resolve().parents[2] +_MANIFEST_PATH = _REPO_ROOT / "src" / "dppvalidator" / "schemas" / "data" / "MANIFEST.json" +_EXPECTED_V07_PRODUCTION_URL = ( + "https://untp.unece.org/artefacts/schema/v0.7.0/dpp/DigitalProductPassport.json" +) + + +# --------------------------------------------------------------------------- +# 1. Dataclass shape +# --------------------------------------------------------------------------- + + +class TestSchemaVersionProductionUrlField: + """``SchemaVersion`` exposes ``production_url`` as a public attribute.""" + + def test_production_url_defaults_to_none(self) -> None: + sv = SchemaVersion( + version="9.9.9", + url="https://example.com/schema.json", + sha256=None, + context_urls=("https://www.w3.org/ns/credentials/v2",), + ) + assert sv.production_url is None + + def test_production_url_can_be_set(self) -> None: + sv = SchemaVersion( + version="9.9.9", + url="https://example.com/schema.json", + sha256=None, + context_urls=("https://www.w3.org/ns/credentials/v2",), + production_url="https://prod.example.com/schema.json", + ) + assert sv.production_url == "https://prod.example.com/schema.json" + + def test_production_url_field_is_frozen(self) -> None: + """The dataclass is frozen — assignment must raise.""" + from dataclasses import FrozenInstanceError + + sv = SchemaVersion( + version="9.9.9", + url="https://example.com/schema.json", + sha256=None, + context_urls=("https://www.w3.org/ns/credentials/v2",), + ) + with pytest.raises(FrozenInstanceError): + sv.production_url = "https://other.example.com/schema.json" # type: ignore[misc] + + +# --------------------------------------------------------------------------- +# 2. Registry accessor +# --------------------------------------------------------------------------- + + +class TestRegistryGetProductionUrl: + """``SchemaRegistry.get_production_url`` exposes the field.""" + + def test_returns_url_for_v07(self) -> None: + registry = SchemaRegistry() + url = registry.get_production_url("0.7.0") + assert url == _EXPECTED_V07_PRODUCTION_URL + + def test_returns_none_for_versions_without_production_url(self) -> None: + # 0.6.0 and 0.6.1 don't currently carry a production URL — the + # accessor should report None rather than raise. + registry = SchemaRegistry() + assert registry.get_production_url("0.6.0") is None + assert registry.get_production_url("0.6.1") is None + + def test_unknown_version_raises(self) -> None: + registry = SchemaRegistry() + with pytest.raises(ValueError, match="Unknown schema version"): + registry.get_production_url("9.9.9") + + +# --------------------------------------------------------------------------- +# 3. v0.7.0 entry in the registry table +# --------------------------------------------------------------------------- + + +class TestV07ProductionUrlContent: + """The 0.7.0 entry's production URL points at the right host.""" + + def test_registry_v07_production_url_matches_expected(self) -> None: + entry = SCHEMA_REGISTRY["0.7.0"] + assert entry.production_url == _EXPECTED_V07_PRODUCTION_URL + + def test_registry_v07_production_url_is_not_the_source_url(self) -> None: + """Production URL is distinct from SHA-pinned source URL. + + The whole point of the field is to record provenance separately + from integrity. Collapsing both into the same string would + defeat the abstraction. + """ + entry = SCHEMA_REGISTRY["0.7.0"] + assert entry.production_url != entry.url + + +# --------------------------------------------------------------------------- +# 4. MANIFEST ↔ registry agreement +# --------------------------------------------------------------------------- + + +class TestManifestRegistryAgreement: + """``production_url`` recorded in MANIFEST matches the registry entry. + + The two surfaces document the same fact ("where does this artefact + live in production?") and must not drift. CI catches drift + immediately rather than letting one fall stale unnoticed. + """ + + def test_v07_dpp_schema_manifest_carries_production_url(self) -> None: + manifest = json.loads(_MANIFEST_PATH.read_text(encoding="utf-8")) + v07_entry = next( + entry + for entry in manifest["artefacts"] + if entry["kind"] == "untp-dpp-schema" and entry["version"] == "0.7.0" + ) + assert v07_entry.get("production_url") == _EXPECTED_V07_PRODUCTION_URL + + def test_v07_dpp_schema_manifest_and_registry_agree(self) -> None: + manifest = json.loads(_MANIFEST_PATH.read_text(encoding="utf-8")) + v07_entry = next( + entry + for entry in manifest["artefacts"] + if entry["kind"] == "untp-dpp-schema" and entry["version"] == "0.7.0" + ) + registry_entry = SCHEMA_REGISTRY["0.7.0"] + assert v07_entry["production_url"] == registry_entry.production_url diff --git a/tests/unit/test_public_api_stability.py b/tests/unit/test_public_api_stability.py new file mode 100644 index 0000000..fa39f5b --- /dev/null +++ b/tests/unit/test_public_api_stability.py @@ -0,0 +1,168 @@ +"""Public-API stability tests. + +Phase 3 of docs/plans/UNTP_0.7.0_MIGRATION.md formalises the +public-import contract in §4.1.8 / §7.6: + +> Within a minor (0.X.0 → 0.X.Y): no class is removed from +> ``dppvalidator.models.__all__``; no parameter on a documented +> constructor is removed or renamed. + +These tests guard the import paths that third-party plugins depend on +(specifically ``examples/dppvalidator_example_plugin/``). They snapshot +the set of public names exported from ``dppvalidator.models`` and +``dppvalidator.models.passport`` and fail the build if any name +disappears within a minor. + +If you legitimately rename or remove a class, update both the snapshot +**and** the tracking issue's CHANGELOG entry under "Removed". +""" + +from __future__ import annotations + +import dppvalidator +from dppvalidator import models as models_pkg +from dppvalidator.models import ( + passport as passport_module, +) + +# Snapshot of the names guaranteed to be importable from +# ``dppvalidator.models`` through 0.4.x. Adding to this set is fine; removing +# from it requires bumping the validator's minor version (see plan §7.6). +EXPECTED_MODELS_EXPORTS = frozenset( + { + # Base & primitives + "UNTPBaseModel", + "UNTPStrictModel", + "Classification", + "FlexibleUri", + "Link", + "Measure", + "SecureLink", + "Characteristics", + "Dimension", + # Enums + "ConformityTopic", + "CriterionStatus", + "EncryptionMethod", + "GranularityLevel", + "HashMethod", + "OperationalScope", + # Identifiers + "Facility", + "IdentifierScheme", + "Party", + # Materials + "Material", + # Product + "Product", + # Claims (v0.6 names, kept through 0.4.x) + "Claim", + "Criterion", + "Metric", + "Regulation", + "Standard", + # Performance scorecards (v0.6 only — gone in v0.7) + "CircularityPerformance", + "EmissionsPerformance", + "TraceabilityPerformance", + # Credential / envelope + "CredentialIssuer", + "CredentialStatus", + "ProductPassport", + "DigitalProductPassport", + } +) + + +class TestModelsPackagePublicAPI: + def test_top_level_dppvalidator_exports(self) -> None: + """Names guaranteed at the package root.""" + for name in ( + "DigitalProductPassport", + "ValidationEngine", + "ValidationError", + "ValidationResult", + "DeepValidator", + "DeepValidationResult", + ): + assert hasattr(dppvalidator, name), ( + f"dppvalidator.{name} disappeared — see plan §7.6 stability contract." + ) + + def test_models_package_keeps_v06_exports(self) -> None: + """Every name in the snapshot must still be importable.""" + actual = set(models_pkg.__all__) + missing = EXPECTED_MODELS_EXPORTS - actual + assert not missing, ( + "\nThe following names were REMOVED from dppvalidator.models.__all__:\n " + + "\n ".join(sorted(missing)) + + "\n\nIf this is intentional, you are breaking the public-API " + "stability contract documented in §7.6 of the migration plan. " + "Either restore the re-export or bump the validator's minor " + "version and update CHANGELOG.md." + ) + + def test_models_package_actually_exposes_each_name(self) -> None: + """``__all__`` and ``hasattr`` must agree.""" + for name in EXPECTED_MODELS_EXPORTS: + assert hasattr(models_pkg, name), ( + f"dppvalidator.models lists {name!r} in __all__ but " + "``hasattr`` says it isn't there. The re-export shim is broken." + ) + + +class TestSubmoduleImportPaths: + """The example plugin imports `from dppvalidator.models.passport import …`. + + That submodule path must keep resolving to the v0.6 class through + the 0.4.x line. Phase 9 will switch it to v0.7; that's a separate + minor release. + """ + + def test_passport_module_re_exports_v0_6_class(self) -> None: + from dppvalidator.models.v0_6 import ( + DigitalProductPassport as V06DigitalProductPassport, + ) + + assert passport_module.DigitalProductPassport is V06DigitalProductPassport, ( + "dppvalidator.models.passport must re-export the v0.6 " + "DigitalProductPassport class through 0.4.x. Phase 9 (validator 0.5.0) " + "switches the default — this test will be updated then." + ) + + def test_v0_6_namespace_is_accessible(self) -> None: + from dppvalidator.models import v0_6 + + assert hasattr(v0_6, "DigitalProductPassport") + assert hasattr(v0_6, "Product") + assert hasattr(v0_6, "Material") + assert hasattr(v0_6, "ProductPassport") # gone in v0.7 + + def test_v0_7_namespace_is_accessible(self) -> None: + from dppvalidator.models import v0_7 + + assert hasattr(v0_7, "DigitalProductPassport") + assert hasattr(v0_7, "Product") + assert hasattr(v0_7, "Material") + # v0.7-only classes + assert hasattr(v0_7, "IssuingSoftware") + assert hasattr(v0_7, "Country") + assert hasattr(v0_7, "PartyRole") + assert hasattr(v0_7, "Performance") + + def test_v0_7_drops_v0_6_only_classes(self) -> None: + """Deliberate non-exports from v0_7.""" + from dppvalidator.models import v0_7 + + assert not hasattr(v0_7, "ProductPassport"), ( + "v0.7 should not have ProductPassport — credentialSubject is Product directly." + ) + assert not hasattr(v0_7, "EmissionsPerformance"), ( + "v0.7 should not have EmissionsPerformance — collapsed into Claim.claimedPerformance." + ) + assert not hasattr(v0_7, "SecureLink"), ( + "v0.7 should not have SecureLink — absorbed into Link." + ) + assert not hasattr(v0_7, "Metric"), ( + "v0.7 should not have Metric — split into Performance.metric/measure/score." + ) diff --git a/tests/unit/test_samples_classification.py b/tests/unit/test_samples_classification.py new file mode 100644 index 0000000..3513536 --- /dev/null +++ b/tests/unit/test_samples_classification.py @@ -0,0 +1,111 @@ +"""Phase 5: pinned version detection on the vendored real-world samples. + +The Phase 5 plan calls for re-running ``scripts/fetch_dpp_samples.py`` +and regenerating ``tests/fixtures/samples_report.md`` to confirm the +new detection logic classifies samples correctly. The fetch step is +network-bound; this module is the durable, offline form of the +"verify detection still works" check. + +It walks every sample under ``tests/fixtures/samples/``, asks +:func:`detect_schema_version` what version it thinks the payload is, +and asserts the answer is one registered in ``SCHEMA_REGISTRY``. The +specific classification per sample is also asserted against a pinned +expectation map so a regression in URL-pattern detection trips the +suite immediately. +""" + +from __future__ import annotations + +import json +from pathlib import Path + +import pytest + +from dppvalidator.schemas.registry import SCHEMA_REGISTRY +from dppvalidator.validators.detection import detect_schema_version + +_SAMPLES_DIR = Path(__file__).resolve().parents[1] / "fixtures" / "samples" + + +# Pinned classifications for the currently-vendored samples. Adding a +# new sample to ``tests/fixtures/samples/`` requires extending this map +# (or marking the file as not-applicable in +# ``_NOT_DPP_SAMPLES`` below). +_EXPECTED_VERSIONS: dict[str, str] = { + "BatteryPassDataModel_BatteryPass_GeneralProductInformation-payload.json": "0.6.1", + "batterypass_BatteryPassDataModel_CarbonFootprintForBatteries-ld.json": "0.6.1", + "batterypass_BatteryPassDataModel_Circularity-ld.json": "0.6.1", + "batterypass_BatteryPassDataModel_GeneralProductInformation-ld.json": "0.6.1", + "batterypass_BatteryPassDataModel_MaterialComposition-ld.json": "0.6.1", + "eclipse-tractusx_sldt-semantic-models_BatteryPass.json": "0.6.1", + "nfc-forum_org_long-dpp-example.json": "0.6.1", + "opensource_unicc_org_untp-digital-facility-record-v0.3.9.json": "0.6.1", + "opensource_unicc_org_untp-digital-product-passport-v0.3.10.json": "0.6.1", + "schemas_testing_breathable-t-shirt.json": "0.6.1", + "test_uncefact_org_DigitalIdentityAnchor-instance-0.6.1.json": "0.6.1", + "test_uncefact_org_untp-dpp-instance-0.6.0.json": "0.6.0", + "untp-verifiable-credentials_s3_amazonaws_com_bc075c5f-2304-4b3f-bb24-46d9fa9a8e60.json": "0.6.0", +} + + +def _sample_files() -> list[Path]: + """All sample files currently vendored for the detection test.""" + return sorted(_SAMPLES_DIR.glob("*.json")) + + +@pytest.mark.parametrize( + "sample_path", + _sample_files(), + ids=lambda p: p.name, +) +def test_sample_detects_to_known_version(sample_path: Path) -> None: + """Every sample must classify to a version present in the registry. + + A regression here means either: + - The detection regex no longer matches a URL it used to. + - A sample file's contents drifted (in which case re-pull it). + """ + data = json.loads(sample_path.read_text(encoding="utf-8")) + assert isinstance(data, dict), f"{sample_path.name} is not a JSON object" + detected = detect_schema_version(data) + assert detected in SCHEMA_REGISTRY, ( + f"{sample_path.name} detected as {detected!r}, which is not in " + f"SCHEMA_REGISTRY ({list(SCHEMA_REGISTRY)!r})." + ) + + +@pytest.mark.parametrize( + ("sample_path", "expected"), + [(_SAMPLES_DIR / name, version) for name, version in _EXPECTED_VERSIONS.items()], + ids=lambda val: val.name if isinstance(val, Path) else str(val), +) +def test_sample_pinned_classification(sample_path: Path, expected: str) -> None: + """Each sample's detected version matches the pinned expectation.""" + if not sample_path.is_file(): + pytest.skip(f"sample no longer present: {sample_path.name}") + data = json.loads(sample_path.read_text(encoding="utf-8")) + assert isinstance(data, dict) + detected = detect_schema_version(data) + assert detected == expected, ( + f"{sample_path.name}: expected {expected!r}, got {detected!r}. " + "Either the sample contents drifted or the detection regex changed; " + "update the pinned expectation if this is intentional." + ) + + +def test_every_sample_has_pinned_expectation() -> None: + """Drift catch: adding a sample without updating the pinned map fails. + + Without this check, a new sample could land with whatever + classification the detection happens to give it, masking a real + regression on existing samples. + """ + on_disk = {p.name for p in _sample_files()} + pinned = set(_EXPECTED_VERSIONS.keys()) + new_samples = on_disk - pinned + assert not new_samples, ( + f"Vendored sample(s) without pinned classification: " + f"{sorted(new_samples)}.\n" + "Add them to _EXPECTED_VERSIONS in this file with the version " + "they should detect to." + ) diff --git a/tests/unit/test_semantic_rules_v07.py b/tests/unit/test_semantic_rules_v07.py new file mode 100644 index 0000000..643dd5f --- /dev/null +++ b/tests/unit/test_semantic_rules_v07.py @@ -0,0 +1,221 @@ +"""Phase 3b acceptance tests: v0.7 semantic rules + version-keyed dispatch. + +This module covers the Phase 3b deliverables in +``docs/plans/UNTP_0.7.0_MIGRATION.md``: + +1. The :data:`ALL_RULES_BY_VERSION` dispatch is in lock-step with + :data:`SCHEMA_REGISTRY` — every registered version has a rule list. +2. The :class:`SemanticValidator` selects the right rule list when + ``rules`` is left at the default ``None``. +3. Each ported v0.7 rule fires on the new envelope shape (no false + positives from v0.6-shaped checks) — see the per-class smoke tests. +4. The dropped rule (``OperationalScopeRule`` / SEM007) is documented + in :data:`DROPPED_RULES_V0_6_TO_V0_7` but is **not** in + :data:`ALL_RULES_V0_7`. +""" + +from __future__ import annotations + +import json +from datetime import datetime, timezone +from pathlib import Path + +import pytest + +from dppvalidator.models import v0_7 +from dppvalidator.schemas.registry import SCHEMA_REGISTRY +from dppvalidator.validators.rules import ALL_RULES_BY_VERSION +from dppvalidator.validators.rules.v0_7 import ( + ALL_RULES_V0_7, + DROPPED_RULES_V0_6_TO_V0_7, + CircularityContentRule, + CIRPASSMandatoryAttributesRule, + ConformityClaimRule, + GranularitySerialNumberRule, + HazardousMaterialRule, + MassFractionSumRule, + ValidityDateRule, +) +from dppvalidator.validators.semantic import SemanticValidator + +_UPSTREAM_DIR = Path(__file__).resolve().parents[1] / "fixtures" / "upstream" / "v0.7.0" + + +def _load_canonical() -> dict: + p = _UPSTREAM_DIR / "samples" / "DigitalProductPassport_instance.json" + if not p.is_file(): # pragma: no cover — vendored in Phase 0 + pytest.skip(f"Upstream sample missing: {p}") + with p.open(encoding="utf-8") as f: + return json.load(f) + + +@pytest.fixture +def canonical_v07() -> v0_7.DigitalProductPassport: + return v0_7.DigitalProductPassport.model_validate(_load_canonical()) + + +# --------------------------------------------------------------------------- +# 1. Dispatch-table consistency +# --------------------------------------------------------------------------- + + +class TestRuleDispatchConsistency: + """``ALL_RULES_BY_VERSION`` must cover every key in ``SCHEMA_REGISTRY``.""" + + def test_every_registered_version_has_a_rule_set(self) -> None: + missing = sorted(set(SCHEMA_REGISTRY) - set(ALL_RULES_BY_VERSION)) + assert not missing, ( + f"ALL_RULES_BY_VERSION is missing entries for {missing}. " + "Add them in src/dppvalidator/validators/rules/__init__.py." + ) + + def test_no_orphan_rule_sets(self) -> None: + extra = sorted(set(ALL_RULES_BY_VERSION) - set(SCHEMA_REGISTRY)) + assert not extra, f"ALL_RULES_BY_VERSION has entries {extra} not in SCHEMA_REGISTRY." + + def test_v07_drops_operational_scope_rule(self) -> None: + """SEM007 is the canonical dropped rule — must be advertised in DROPPED_RULES.""" + assert "SEM007" in DROPPED_RULES_V0_6_TO_V0_7 + # And it must NOT appear in the v0.7 rule list. + rule_ids = {getattr(r, "rule_id", None) for r in ALL_RULES_V0_7} + assert "SEM007" not in rule_ids + + +# --------------------------------------------------------------------------- +# 2. SemanticValidator dispatch +# --------------------------------------------------------------------------- + + +class TestSemanticValidatorDispatch: + """The validator picks the right rule list when ``rules`` is None.""" + + def test_v06_uses_v06_rules(self) -> None: + v = SemanticValidator(schema_version="0.6.1") + ids = {getattr(r, "rule_id", None) for r in v.rules} + assert "SEM007" in ids, ( + "Expected the v0.6 rule set to include OperationalScopeRule (SEM007)." + ) + + def test_v07_uses_v07_rules(self) -> None: + v = SemanticValidator(schema_version="0.7.0") + ids = {getattr(r, "rule_id", None) for r in v.rules} + assert "SEM007" not in ids, ( + "v0.7 rule set must NOT include OperationalScopeRule — see DROPPED_RULES_V0_6_TO_V0_7." + ) + # Sanity: it should still include the ported rules. + assert {"SEM001", "SEM003", "CQ001"}.issubset(ids) + + def test_unknown_version_falls_back_to_default(self) -> None: + v = SemanticValidator(schema_version="9.9.9") + # Should fall back to ALL_RULES (v0.6.x default). + assert len(v.rules) > 0 + + def test_explicit_rules_override(self) -> None: + custom = [MassFractionSumRule()] + v = SemanticValidator(schema_version="0.7.0", rules=custom) + assert v.rules is custom + + +# --------------------------------------------------------------------------- +# 3. v0.7 rule semantics (no false positives, fires when expected) +# --------------------------------------------------------------------------- + + +class TestV07RulesNoFalsePositives: + """Ported rules walking the v0.7 shape produce zero violations on the + canonical sample (which is well-formed).""" + + @pytest.mark.parametrize( + "rule_cls", + [ + # NOTE: ``MassFractionSumRule`` is intentionally NOT in this set — + # the canonical 0.7.0 sample carries a partial-declaration material + # provenance (one material at 30 %), so SEM001 *should* fire as a + # warning. That's a "rule fires correctly", not a "false positive". + # See ``test_mass_fraction_rule_fires_on_partial_declaration`` below. + ValidityDateRule, + HazardousMaterialRule, + CircularityContentRule, + ConformityClaimRule, + GranularitySerialNumberRule, + CIRPASSMandatoryAttributesRule, + ], + ) + def test_rule_clean_on_canonical_sample( + self, + canonical_v07: v0_7.DigitalProductPassport, + rule_cls: type, + ) -> None: + rule = rule_cls() + violations = rule.check(canonical_v07) + assert violations == [], ( + f"{rule_cls.__name__} produced {len(violations)} violation(s) on " + f"a canonical 0.7.0 sample: {violations}" + ) + + def test_mass_fraction_rule_fires_on_partial_declaration( + self, canonical_v07: v0_7.DigitalProductPassport + ) -> None: + """SEM001 fires (as a warning) when material provenance sums to < 1.0. + + The canonical 0.7.0 sample declares one material at mass fraction + 0.3 — that's a documented partial declaration and SEM001 must + flag it so the user knows the material list is incomplete. + """ + rule = MassFractionSumRule() + violations = rule.check(canonical_v07) + assert len(violations) == 1 + path, message = violations[0] + assert path == "$.credentialSubject.materialProvenance" + assert "partial declaration" in message + # Severity is "warning" — not an error. + assert rule.severity == "warning" + + +class TestV07RulesFire: + """The ported rules still fire on the conditions they're meant to flag.""" + + def _envelope(self, **overrides): + sample = _load_canonical() + sample.update(overrides) + return v0_7.DigitalProductPassport.model_validate(sample) + + def test_validity_date_rule_fires_on_inverted_dates(self) -> None: + # Constructing the model directly bypasses the model-level invariant, + # but we can simulate the rule's view by building a stub object. + class _Stub: + valid_from = datetime(2030, 1, 1, tzinfo=timezone.utc) + valid_until = datetime(2025, 1, 1, tzinfo=timezone.utc) + + rule = ValidityDateRule() + violations = rule.check(_Stub()) + assert any("validFrom" in msg for _, msg in violations) + + def test_hazardous_material_rule_fires_when_no_safety_info(self) -> None: + # Build a stand-alone Material so we don't trip the model-level + # invariant — the rule walks the dotted path via getattr. + class _Mat: + name = "Lithium" + hazardous = True + material_safety_information = None + + class _Product: + material_provenance = [_Mat()] + + class _DPP: + credential_subject = _Product() + + rule = HazardousMaterialRule() + violations = rule.check(_DPP()) + assert violations and "Lithium" in violations[0][1] + + def test_conformity_claim_rule_warns_when_no_claims(self) -> None: + class _Product: + performance_claim = [] + + class _DPP: + credential_subject = _Product() + + rule = ConformityClaimRule() + violations = rule.check(_DPP()) + assert violations and "performance" in violations[0][1].lower() diff --git a/tests/unit/test_v07_models.py b/tests/unit/test_v07_models.py new file mode 100644 index 0000000..a1b947f --- /dev/null +++ b/tests/unit/test_v07_models.py @@ -0,0 +1,300 @@ +"""Acceptance tests for the UNTP v0.7.0 Pydantic models. + +Phase 3 of docs/plans/UNTP_0.7.0_MIGRATION.md introduced +``dppvalidator.models.v0_7``. These tests cover: + +1. The dispatch table is in lock-step with the schema registry — every + registered version has a model class. +2. Each vendored 0.7.0 sample (canonical, battery, cathode) parses + cleanly through ``v0_7.DigitalProductPassport``. +3. The cross-field invariants documented in §3.2 of the plan are wired + correctly (date order, granularity ↔ id required, hazardous ↔ safety + info, mass-fraction sum ≤ 1.0, performance has measure or score, + period ordering, country-code shape, image required fields). + +These tests deliberately do not exercise the engine (that's covered by +``test_phase3_engine.py``) — they pin the model layer's behaviour in +isolation so a regression there is identifiable. +""" + +from __future__ import annotations + +import json +from datetime import date, datetime, timedelta, timezone +from pathlib import Path + +import pytest +from pydantic import ValidationError + +from dppvalidator.models import v0_6, v0_7 +from dppvalidator.schemas.registry import SCHEMA_REGISTRY +from dppvalidator.validators.model import _MODEL_BY_VERSION + +_UPSTREAM_DIR = Path(__file__).resolve().parents[1] / "fixtures" / "upstream" / "v0.7.0" +_SAMPLES = { + "canonical": _UPSTREAM_DIR / "samples" / "DigitalProductPassport_instance.json", + "battery": _UPSTREAM_DIR / "samples" / "DigitalProductPassport_battery_instance.json", + "cathode": _UPSTREAM_DIR / "samples" / "DigitalProductPassport_cathode_instance.json", +} + + +def _load(name: str) -> dict: + path = _SAMPLES[name] + if not path.is_file(): # pragma: no cover - vendored in Phase 0 + pytest.skip(f"Vendor {path} via Phase 0 of the migration plan.") + with path.open(encoding="utf-8") as f: + return json.load(f) + + +# ---------------------------------------------------------------------------- +# Dispatch table contract +# ---------------------------------------------------------------------------- + + +class TestDispatchTable: + def test_dispatch_table_covers_every_registered_version(self) -> None: + """Every entry in SCHEMA_REGISTRY has a model in _MODEL_BY_VERSION.""" + missing = sorted(set(SCHEMA_REGISTRY) - set(_MODEL_BY_VERSION)) + assert not missing, ( + f"Versions registered without a Pydantic model: {missing}. " + "Add an entry to validators/model.py::_MODEL_BY_VERSION." + ) + + def test_dispatch_uses_v0_6_for_legacy_versions(self) -> None: + assert _MODEL_BY_VERSION["0.6.0"] is v0_6.DigitalProductPassport + assert _MODEL_BY_VERSION["0.6.1"] is v0_6.DigitalProductPassport + + def test_dispatch_uses_v0_7_for_modern_version(self) -> None: + assert _MODEL_BY_VERSION["0.7.0"] is v0_7.DigitalProductPassport + + def test_v0_6_and_v0_7_dpp_classes_are_distinct(self) -> None: + """The two version namespaces must not alias to the same class.""" + assert v0_6.DigitalProductPassport is not v0_7.DigitalProductPassport + + +# ---------------------------------------------------------------------------- +# Canonical sample parsing +# ---------------------------------------------------------------------------- + + +class TestUpstreamSampleParsing: + @pytest.mark.parametrize("name", sorted(_SAMPLES)) + def test_each_upstream_sample_parses_through_v0_7_model(self, name: str) -> None: + data = _load(name) + dpp = v0_7.DigitalProductPassport.model_validate(data) + assert dpp.id + assert dpp.name + assert isinstance(dpp.credential_subject, v0_7.Product) + assert dpp.credential_subject.name + assert dpp.credential_subject.id_granularity is not None + + def test_canonical_sample_round_trip_preserves_top_keys(self) -> None: + """Round-tripping through the model preserves the envelope shape.""" + data = _load("canonical") + dpp = v0_7.DigitalProductPassport.model_validate(data) + out = dpp.model_dump(by_alias=True, exclude_none=True) + assert out["id"] == data["id"] + assert out["name"] == data["name"] + assert "credentialSubject" in out + + +# ---------------------------------------------------------------------------- +# Cross-field invariants +# ---------------------------------------------------------------------------- + + +class TestEnvelopeInvariants: + def test_validfrom_must_precede_validuntil(self) -> None: + data = _load("canonical") + # Make validUntil precede validFrom + data["validFrom"] = "2030-01-01T00:00:00Z" + data["validUntil"] = "2025-01-01T00:00:00Z" + with pytest.raises(ValidationError, match="validFrom"): + v0_7.DigitalProductPassport.model_validate(data) + + def test_validfrom_required(self) -> None: + """v0.7.0 makes validFrom required (envelope-level).""" + data = _load("canonical") + del data["validFrom"] + with pytest.raises(ValidationError, match="validFrom"): + v0_7.DigitalProductPassport.model_validate(data) + + def test_name_required_and_non_empty(self) -> None: + data = _load("canonical") + data["name"] = "" + with pytest.raises(ValidationError): + v0_7.DigitalProductPassport.model_validate(data) + + +class TestProductInvariants: + def test_item_granularity_requires_item_number(self) -> None: + data = _load("canonical") + data["credentialSubject"]["idGranularity"] = "item" + data["credentialSubject"].pop("itemNumber", None) + with pytest.raises(ValidationError, match="itemNumber"): + v0_7.DigitalProductPassport.model_validate(data) + + def test_batch_granularity_requires_batch_number(self) -> None: + data = _load("canonical") + data["credentialSubject"]["idGranularity"] = "batch" + data["credentialSubject"].pop("batchNumber", None) + with pytest.raises(ValidationError, match="batchNumber"): + v0_7.DigitalProductPassport.model_validate(data) + + def test_mass_fraction_sum_above_unity_rejected(self) -> None: + data = _load("canonical") + # Pad materialProvenance until the sum exceeds 1.0 + materials = data["credentialSubject"]["materialProvenance"] + # Inject two materials whose mass fractions sum > 1.0 + proto = materials[0] + materials.append({**proto, "name": "Padding A", "massFraction": 0.7}) + materials.append({**proto, "name": "Padding B", "massFraction": 0.7}) + with pytest.raises(ValidationError, match="mass.?[Ff]raction"): + v0_7.DigitalProductPassport.model_validate(data) + + +class TestMaterialInvariants: + def _make_minimal_material(self, **overrides) -> dict: + base = { + "name": "Lithium", + "originCountry": {"countryCode": "AU", "countryName": "Australia"}, + "materialType": { + "schemeId": "https://example.com/scheme", + "schemeName": "Example", + "code": "Li", + "name": "Lithium", + }, + "massFraction": 0.5, + } + return {**base, **overrides} + + def test_hazardous_requires_safety_information(self) -> None: + with pytest.raises(ValidationError, match="materialSafetyInformation"): + v0_7.Material.model_validate(self._make_minimal_material(hazardous=True)) + + def test_hazardous_with_safety_info_passes(self) -> None: + m = self._make_minimal_material( + hazardous=True, + materialSafetyInformation={ + "linkURL": "https://example.com/sds.pdf", + "name": "SDS", + "mediaType": "application/pdf", + }, + ) + v0_7.Material.model_validate(m) + + +class TestPerformanceInvariants: + def test_at_least_one_of_measure_or_score(self) -> None: + with pytest.raises(ValidationError, match="measure.+score"): + v0_7.Performance.model_validate({"metric": {"name": "x"}}) + + def test_with_measure_only_is_ok(self) -> None: + v0_7.Performance.model_validate( + { + "metric": {"name": "co2"}, + "measure": {"value": 12.5, "unit": "KGM"}, + }, + ) + + def test_with_score_only_is_ok(self) -> None: + v0_7.Performance.model_validate( + { + "metric": {"name": "rating"}, + "score": {"code": "A"}, + }, + ) + + +class TestPeriodInvariants: + def test_start_after_end_rejected(self) -> None: + with pytest.raises(ValidationError, match="startDate"): + v0_7.Period.model_validate( + {"startDate": "2025-12-01", "endDate": "2025-01-01"}, + ) + + def test_open_ended_periods_ok(self) -> None: + v0_7.Period.model_validate({"startDate": "2025-01-01"}) # no end + v0_7.Period.model_validate({"endDate": "2025-12-31"}) # no start + + +class TestCountryInvariants: + def test_iso_3166_alpha2_required(self) -> None: + with pytest.raises(ValidationError, match="countryCode"): + v0_7.Country.model_validate({"countryCode": "USA"}) # 3 letters + with pytest.raises(ValidationError, match="countryCode"): + v0_7.Country.model_validate({"countryCode": "us"}) # lowercase + + def test_alpha2_uppercase_passes(self) -> None: + c = v0_7.Country.model_validate({"countryCode": "DE", "countryName": "Germany"}) + assert c.country_code == "DE" + + +# ---------------------------------------------------------------------------- +# Pydantic v2 patterns sanity check (per .claude/rules/dpp-domain.md) +# ---------------------------------------------------------------------------- + + +class TestPydanticV2Patterns: + def test_models_use_extra_allow_on_envelope(self) -> None: + """UNTPBaseModel ships extra='allow' so industry extensions flow through.""" + data = _load("canonical") + data["x:vendor-extension"] = {"key": "value"} + # Must not raise on extra + v0_7.DigitalProductPassport.model_validate(data) + + def test_model_dump_uses_aliases_for_jsonld_output(self) -> None: + """model_dump(by_alias=True) emits camelCase keys.""" + c = v0_7.Country.model_validate({"countryCode": "ES", "countryName": "Spain"}) + d = c.model_dump(by_alias=True, exclude_none=True) + assert "countryCode" in d + assert "countryName" in d + # snake_case must NOT be in the dumped output + assert "country_code" not in d + + +# ---------------------------------------------------------------------------- +# Smoke test on the constructor — Pydantic v2 import paths +# ---------------------------------------------------------------------------- + + +def test_construct_minimal_v07_dpp_programmatically() -> None: + """Build a minimal valid 0.7.0 DPP from scratch (not via JSON).""" + now = datetime.now(timezone.utc) + dpp = v0_7.DigitalProductPassport( + **{ + "@context": [ + "https://www.w3.org/ns/credentials/v2", + "https://vocabulary.uncefact.org/untp/0.7.0/context/", + ], + }, + id="https://example.com/credentials/dpp-1", + name="Smoke test DPP", + issuer=v0_7.CredentialIssuer(id="did:web:example.com", name="Example Co"), + validFrom=now, + validUntil=now + timedelta(days=365), + credentialSubject=v0_7.Product( + id="https://example.com/products/p1", + name="Widget", + idScheme=v0_7.IdentifierScheme( + id="https://example.com/scheme", + name="Example scheme", + ), + idGranularity=v0_7.product.IdGranularity.MODEL, + productCategory=[ + v0_7.Classification( + schemeId="https://unstats.un.org/cpc", + schemeName="UN CPC", + code="14110", + name="Widgets", + ), + ], + producedAtFacility=v0_7.Facility( + id="https://example.com/facility/1", + name="Main plant", + ), + countryOfProduction=v0_7.Country(countryCode="DE", countryName="Germany"), + productionDate=date(2025, 1, 1), + ), + ) + assert dpp.credential_subject.name == "Widget" diff --git a/tests/unit/test_validation_engine.py b/tests/unit/test_validation_engine.py index 70a3bc3..b7248ba 100644 --- a/tests/unit/test_validation_engine.py +++ b/tests/unit/test_validation_engine.py @@ -12,8 +12,14 @@ FIXTURES_DIR = Path(__file__).parent.parent / "fixtures" +@pytest.mark.dpp_version("0.6.1") class TestValidationEngine: - """Tests for ValidationEngine.""" + """Tests for ValidationEngine. + + Pinned to v0.6.1 via the class-level marker because the ``engine`` + fixture hardcodes ``schema_version="0.6.1"`` — see Phase 5 of + ``docs/plans/UNTP_0.7.0_MIGRATION.md`` for the matrix design. + """ @pytest.fixture def engine(self) -> ValidationEngine: @@ -120,6 +126,7 @@ def test_validate_product_passport_instance(self): assert passport.product.name == "EV battery 300Ah." +@pytest.mark.dpp_version("0.6.1") class TestValidationEngineExtended: """Extended tests for ValidationEngine.""" @@ -244,6 +251,7 @@ def test_semantic_layer_skipped_without_passport(self): class TestValidationEngineBehavior: """Behavior tests for ValidationEngine.""" + @pytest.mark.dpp_version("0.6.1") def test_engine_validates_real_dpp_structure(self, valid_dpp_data: dict): """Test engine validates a complete DPP with all components.""" engine = ValidationEngine(schema_version="0.6.1", layers=["model", "semantic"]) diff --git a/tests/unit/test_version_mismatch.py b/tests/unit/test_version_mismatch.py new file mode 100644 index 0000000..3d51147 --- /dev/null +++ b/tests/unit/test_version_mismatch.py @@ -0,0 +1,117 @@ +"""Acceptance tests for the VER001 version-mismatch error. + +Phase 3.3 of docs/plans/UNTP_0.7.0_MIGRATION.md introduced VER001 to +prevent the silent field-loss that Pydantic's ``extra='allow'`` setting +would otherwise allow when a payload's declared version doesn't match +the engine's configured version. + +Three behaviours under test: + +1. **Mismatch is rejected.** Engine configured for ``schema_version=A`` + + payload declaring ``B`` ⇒ single ``VER001`` error and stop. No + layer-level errors leak through. +2. **Auto-detect is unaffected.** Engine in ``schema_version='auto'`` + mode never raises VER001 — it adopts the declared version. +3. **Unmarked payloads pass through.** Engine configured for any + version + payload that declares no version (no ``$schema``, no + versioned UNTP context URL) is trusted, no VER001. +""" + +from __future__ import annotations + +import json +from pathlib import Path + +import pytest + +from dppvalidator.validators import ValidationEngine + +_FIXTURES = Path(__file__).resolve().parents[1] / "fixtures" +_V07_SAMPLE = _FIXTURES / "upstream" / "v0.7.0" / "samples" / "DigitalProductPassport_instance.json" +_V06_SAMPLE = _FIXTURES / "valid" / "full_dpp.json" + + +def _load(path: Path) -> dict: + if not path.is_file(): # pragma: no cover - vendored in Phase 0 + pytest.skip(f"Vendor {path} via Phase 0 of the migration plan.") + with path.open(encoding="utf-8") as f: + return json.load(f) + + +class TestVer001MismatchFailsFast: + def test_engine_06_rejects_v07_payload_with_VER001(self) -> None: + sample = _load(_V07_SAMPLE) + engine = ValidationEngine( + schema_version="0.6.1", + layers=["schema", "model", "jsonld"], + load_plugins=False, + ) + result = engine.validate(sample) + assert result.valid is False + assert len(result.errors) == 1, ( + f"Expected exactly one VER001 error, got " + f"{[(e.code, e.message[:60]) for e in result.errors]}" + ) + assert result.errors[0].code == "VER001" + assert "0.7.0" in result.errors[0].message + assert "0.6.1" in result.errors[0].message + + def test_engine_07_rejects_v06_payload_with_VER001(self) -> None: + sample = _load(_V06_SAMPLE) + engine = ValidationEngine( + schema_version="0.7.0", + layers=["schema", "model"], + load_plugins=False, + ) + result = engine.validate(sample) + assert result.valid is False + codes = [e.code for e in result.errors] + assert "VER001" in codes + # And we stopped before any layer ran (so no MDL/SCH errors). + assert all(c == "VER001" for c in codes) + + def test_VER001_carries_diagnostic_context(self) -> None: + sample = _load(_V07_SAMPLE) + engine = ValidationEngine(schema_version="0.6.1", layers=["schema"], load_plugins=False) + result = engine.validate(sample) + err = result.errors[0] + assert err.context is not None + assert err.context.get("declared_version") == "0.7.0" + assert err.context.get("configured_version") == "0.6.1" + + +class TestAutoDetectIgnoresVer001: + def test_auto_detect_adopts_declared_version_no_VER001(self) -> None: + sample = _load(_V07_SAMPLE) + engine = ValidationEngine( + schema_version="auto", + layers=["schema", "model", "jsonld"], + load_plugins=False, + ) + result = engine.validate(sample) + # Auto detection should set schema_version to 0.7.0 and validate + # cleanly — VER001 must not fire. + assert all(e.code != "VER001" for e in result.errors), ( + f"Unexpected VER001 in auto mode; errors: {[e.code for e in result.errors]}" + ) + assert result.schema_version == "0.7.0" + + +class TestUnmarkedPayloadsAccepted: + def test_payload_with_no_version_marker_does_not_trip_VER001(self) -> None: + # Strip every version marker (no $schema, no UNTP context URLs). + sample = { + "type": ["DigitalProductPassport", "VerifiableCredential"], + "@context": ["https://www.w3.org/ns/credentials/v2"], + "id": "https://example.com/credentials/dpp-1", + "issuer": {"id": "did:web:example.com", "name": "Example"}, + "validFrom": "2025-01-01T00:00:00Z", + "name": "Unmarked", + "credentialSubject": {}, + } + engine = ValidationEngine(schema_version="0.7.0", layers=["schema"], load_plugins=False) + result = engine.validate(sample) + assert all(e.code != "VER001" for e in result.errors), ( + "VER001 fired on a payload that declares no version — should " + "trust the user's configuration." + ) diff --git a/tests/unit/test_voc_rules_scheme_aware.py b/tests/unit/test_voc_rules_scheme_aware.py new file mode 100644 index 0000000..2d54627 --- /dev/null +++ b/tests/unit/test_voc_rules_scheme_aware.py @@ -0,0 +1,266 @@ +"""Regression tests: VOC003/VOC004 must be scheme-aware. + +Both rules check classification codes against textile-pilot +validators (UNECE Rec 46 for materials, HS chapters 50-63 for +products). Before the fix, they fired unconditionally on every +``Classification.code`` regardless of the scheme — producing false +positives for UN CPC, NACE, GS1 GPC, and any other classification. + +The fix gates the validator call on the ``schemeId``: + +- When ``schemeId`` is set AND matches the rule's scheme: validate. +- When ``schemeId`` is set AND does NOT match: skip silently. +- When ``schemeId`` is missing: fall back to pre-fix best-effort + behaviour (a length/digit heuristic for VOC004, validate + unconditionally for VOC003) for back-compat with legacy fixtures. + +These tests pin the contract end-to-end through both v0.6 and v0.7 +rule implementations. +""" + +from __future__ import annotations + +import json +from pathlib import Path +from typing import Any + +import pytest + +_FIXTURES = Path(__file__).resolve().parents[1] / "fixtures" / "valid" + + +# --------------------------------------------------------------------------- +# Real-fixture round-trip — the canonical regression +# --------------------------------------------------------------------------- + + +@pytest.mark.parametrize( + "fixture_name", + [ + "untp-dpp-instance-0.7.0.json", + "untp-dpp-battery-instance-0.7.0.json", + "untp-dpp-cathode-instance-0.7.0.json", + ], +) +def test_v07_un_cpc_fixtures_emit_no_voc003_or_voc004(fixture_name: str) -> None: + """v0.7 UN CPC fixtures must not trip the textile-pilot rules. + + All three vendored upstream samples use UN CPC scheme IDs + (``https://unstats.un.org/unsd/classifications/Econ/cpc/``). + Before the scheme-aware fix, every classification code in these + fixtures spuriously emitted VOC003 + VOC004 because the rules + blindly ran textile validators on UN CPC codes. The fix gates + the validation on the scheme — UN CPC isn't UNECE Rec 46 or HS, + so the rules skip silently. + """ + from dppvalidator.validators import ValidationEngine + + fixture_path = _FIXTURES / fixture_name + if not fixture_path.is_file(): + pytest.skip(f"fixture not vendored: {fixture_name}") + + data = json.loads(fixture_path.read_text(encoding="utf-8")) + engine = ValidationEngine(schema_version="0.7.0", layers=["semantic"]) + result = engine.validate(data) + + voc003 = [w for w in result.warnings + result.errors if w.code == "VOC003"] + voc004 = [w for w in result.warnings + result.errors if w.code == "VOC004"] + assert voc003 == [], ( + f"{fixture_name} unexpectedly emitted VOC003 (scheme-aware fix regressed):\n" + + "\n".join(f" {w.path}: {w.message}" for w in voc003) + ) + assert voc004 == [], ( + f"{fixture_name} unexpectedly emitted VOC004 (scheme-aware fix regressed):\n" + + "\n".join(f" {w.path}: {w.message}" for w in voc004) + ) + + +def test_v06_un_cpc_fixture_emits_no_voc003_or_voc004() -> None: + """The vendored v0.6.1 UN CPC fixture is also clean after the fix.""" + from dppvalidator.validators import ValidationEngine + + fixture_path = _FIXTURES / "untp-dpp-instance-0.6.1.json" + if not fixture_path.is_file(): + pytest.skip("v0.6 fixture not vendored") + + data = json.loads(fixture_path.read_text(encoding="utf-8")) + engine = ValidationEngine(schema_version="0.6.1", layers=["semantic"]) + result = engine.validate(data) + + voc003 = [w for w in result.warnings + result.errors if w.code == "VOC003"] + voc004 = [w for w in result.warnings + result.errors if w.code == "VOC004"] + assert voc003 == [] + assert voc004 == [] + + +# --------------------------------------------------------------------------- +# v0.7 unit-level: rule check() with constructed Material / Classification +# --------------------------------------------------------------------------- + + +def _build_v07_passport_with( + materials: list[dict[str, Any]] | None = None, categories: list[dict[str, Any]] | None = None +) -> Any: + """Construct a minimal v0.7 ``DigitalProductPassport`` for rule tests.""" + from dppvalidator.models.v0_7.envelope import DigitalProductPassport + + payload: dict[str, Any] = { + "@context": [ + "https://www.w3.org/ns/credentials/v2", + "https://vocabulary.uncefact.org/untp/0.7.0/context/", + ], + "type": ["DigitalProductPassport", "VerifiableCredential"], + "id": "https://example.com/credentials/test", + "name": "Test DPP", + "issuer": { + "type": ["CredentialIssuer"], + "id": "did:example:1", + "name": "Example", + }, + "validFrom": "2024-01-01T00:00:00Z", + "credentialSubject": { + "type": ["Product"], + "id": "https://example.com/products/1", + "name": "Test product", + "idScheme": { + "type": ["IdentifierScheme"], + "id": "https://example.com/scheme", + "name": "Internal", + }, + "idGranularity": "model", + "productCategory": categories + or [ + { + "type": ["Classification"], + "schemeId": "https://example.com/scheme", + "schemeName": "Internal", + "code": "X", + "name": "x", + } + ], + "producedAtFacility": { + "type": ["Facility"], + "id": "https://example.com/facilities/1", + "name": "Facility", + }, + "countryOfProduction": {"countryCode": "DE", "countryName": "Germany"}, + "materialProvenance": materials or [], + }, + } + return DigitalProductPassport.model_validate(payload) + + +class TestV07MaterialCodeRule: + """Scheme-gating on the v0.7 ``MaterialCodeRule`` (VOC003).""" + + def _material(self, *, scheme_id: str | None, code: str = "9999") -> dict[str, Any]: + material_type: dict[str, Any] = { + "type": ["Classification"], + "schemeName": "Test", + "code": code, + "name": "x", + } + if scheme_id is not None: + material_type["schemeId"] = scheme_id + return { + "name": "Test material", + "originCountry": {"countryCode": "DE", "countryName": "Germany"}, + "materialType": material_type, + "massFraction": 1.0, + } + + def test_un_cpc_scheme_skips_validation(self) -> None: + from dppvalidator.validators.rules.v0_7.base import MaterialCodeRule + + # ``9999`` is not a real UNECE Rec 46 code — but the rule + # must not fire because the scheme isn't Rec 46. + passport = _build_v07_passport_with( + materials=[ + self._material( + scheme_id="https://unstats.un.org/unsd/classifications/Econ/cpc/", + code="9999", + ), + ], + categories=[ + { + "type": ["Classification"], + "schemeId": "https://unstats.un.org/unsd/classifications/Econ/cpc/", + "schemeName": "UN CPC", + "code": "12345", + "name": "x", + }, + ], + ) + rule = MaterialCodeRule(material_validator=lambda code: code == "0000") + assert rule.check(passport) == [] + + def test_unece_rec46_scheme_fires_on_invalid_code(self) -> None: + from dppvalidator.validators.rules.v0_7.base import MaterialCodeRule + + passport = _build_v07_passport_with( + materials=[ + self._material( + scheme_id="https://vocabulary.uncefact.org/UNECERec46/", + code="9999", + ), + ], + categories=[ + { + "type": ["Classification"], + "schemeId": "https://vocabulary.uncefact.org/UNECERec46/", + "schemeName": "UNECE Rec 46", + "code": "12345", + "name": "x", + }, + ], + ) + # Stub validator: accepts only "0000". + rule = MaterialCodeRule(material_validator=lambda code: code == "0000") + violations = rule.check(passport) + assert len(violations) == 1 + assert "9999" in violations[0][1] + + +class TestV07HSCodeRule: + """Scheme-gating on the v0.7 ``HSCodeRule`` (VOC004).""" + + def _category(self, *, scheme_id: str | None, code: str = "12345") -> dict[str, Any]: + cat: dict[str, Any] = { + "type": ["Classification"], + "schemeName": "Test", + "code": code, + "name": "x", + } + if scheme_id is not None: + cat["schemeId"] = scheme_id + return cat + + def test_un_cpc_scheme_skips_validation(self) -> None: + from dppvalidator.validators.rules.v0_7.base import HSCodeRule + + passport = _build_v07_passport_with( + categories=[ + self._category( + scheme_id="https://unstats.un.org/unsd/classifications/Econ/cpc/", + code="9999", # would trip the textile validator if not gated + ), + ], + ) + rule = HSCodeRule(hs_validator=lambda _code: False) + assert rule.check(passport) == [] + + def test_hs_scheme_fires_on_invalid_code(self) -> None: + from dppvalidator.validators.rules.v0_7.base import HSCodeRule + + passport = _build_v07_passport_with( + categories=[ + self._category( + scheme_id="https://wcoomd.org/hs-nomenclature/2017", + code="9999", + ), + ], + ) + rule = HSCodeRule(hs_validator=lambda _code: False) + violations = rule.check(passport) + assert len(violations) == 1 + assert "9999" in violations[0][1] diff --git a/uv.lock b/uv.lock index 4dbd06f..69adc8f 100644 --- a/uv.lock +++ b/uv.lock @@ -483,62 +483,62 @@ toml = [ [[package]] name = "cryptography" -version = "46.0.4" +version = "48.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/78/19/f748958276519adf6a0c1e79e7b8860b4830dda55ccdf29f2719b5fc499c/cryptography-46.0.4.tar.gz", hash = "sha256:bfd019f60f8abc2ed1b9be4ddc21cfef059c841d86d710bb69909a688cbb8f59", size = 749301, upload-time = "2026-01-28T00:24:37.379Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/8d/99/157aae7949a5f30d51fcb1a9851e8ebd5c74bf99b5285d8bb4b8b9ee641e/cryptography-46.0.4-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:281526e865ed4166009e235afadf3a4c4cba6056f99336a99efba65336fd5485", size = 7173686, upload-time = "2026-01-28T00:23:07.515Z" }, - { url = "https://files.pythonhosted.org/packages/87/91/874b8910903159043b5c6a123b7e79c4559ddd1896e38967567942635778/cryptography-46.0.4-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5f14fba5bf6f4390d7ff8f086c566454bff0411f6d8aa7af79c88b6f9267aecc", size = 4275871, upload-time = "2026-01-28T00:23:09.439Z" }, - { url = "https://files.pythonhosted.org/packages/c0/35/690e809be77896111f5b195ede56e4b4ed0435b428c2f2b6d35046fbb5e8/cryptography-46.0.4-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:47bcd19517e6389132f76e2d5303ded6cf3f78903da2158a671be8de024f4cd0", size = 4423124, upload-time = "2026-01-28T00:23:11.529Z" }, - { url = "https://files.pythonhosted.org/packages/1a/5b/a26407d4f79d61ca4bebaa9213feafdd8806dc69d3d290ce24996d3cfe43/cryptography-46.0.4-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:01df4f50f314fbe7009f54046e908d1754f19d0c6d3070df1e6268c5a4af09fa", size = 4277090, upload-time = "2026-01-28T00:23:13.123Z" }, - { url = "https://files.pythonhosted.org/packages/0c/d8/4bb7aec442a9049827aa34cee1aa83803e528fa55da9a9d45d01d1bb933e/cryptography-46.0.4-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:5aa3e463596b0087b3da0dbe2b2487e9fc261d25da85754e30e3b40637d61f81", size = 4947652, upload-time = "2026-01-28T00:23:14.554Z" }, - { url = "https://files.pythonhosted.org/packages/2b/08/f83e2e0814248b844265802d081f2fac2f1cbe6cd258e72ba14ff006823a/cryptography-46.0.4-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:0a9ad24359fee86f131836a9ac3bffc9329e956624a2d379b613f8f8abaf5255", size = 4455157, upload-time = "2026-01-28T00:23:16.443Z" }, - { url = "https://files.pythonhosted.org/packages/0a/05/19d849cf4096448779d2dcc9bb27d097457dac36f7273ffa875a93b5884c/cryptography-46.0.4-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:dc1272e25ef673efe72f2096e92ae39dea1a1a450dd44918b15351f72c5a168e", size = 3981078, upload-time = "2026-01-28T00:23:17.838Z" }, - { url = "https://files.pythonhosted.org/packages/e6/89/f7bac81d66ba7cde867a743ea5b37537b32b5c633c473002b26a226f703f/cryptography-46.0.4-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:de0f5f4ec8711ebc555f54735d4c673fc34b65c44283895f1a08c2b49d2fd99c", size = 4276213, upload-time = "2026-01-28T00:23:19.257Z" }, - { url = "https://files.pythonhosted.org/packages/da/9f/7133e41f24edd827020ad21b068736e792bc68eecf66d93c924ad4719fb3/cryptography-46.0.4-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:eeeb2e33d8dbcccc34d64651f00a98cb41b2dc69cef866771a5717e6734dfa32", size = 4912190, upload-time = "2026-01-28T00:23:21.244Z" }, - { url = "https://files.pythonhosted.org/packages/a6/f7/6d43cbaddf6f65b24816e4af187d211f0bc536a29961f69faedc48501d8e/cryptography-46.0.4-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:3d425eacbc9aceafd2cb429e42f4e5d5633c6f873f5e567077043ef1b9bbf616", size = 4454641, upload-time = "2026-01-28T00:23:22.866Z" }, - { url = "https://files.pythonhosted.org/packages/9e/4f/ebd0473ad656a0ac912a16bd07db0f5d85184924e14fc88feecae2492834/cryptography-46.0.4-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:91627ebf691d1ea3976a031b61fb7bac1ccd745afa03602275dda443e11c8de0", size = 4405159, upload-time = "2026-01-28T00:23:25.278Z" }, - { url = "https://files.pythonhosted.org/packages/d1/f7/7923886f32dc47e27adeff8246e976d77258fd2aa3efdd1754e4e323bf49/cryptography-46.0.4-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:2d08bc22efd73e8854b0b7caff402d735b354862f1145d7be3b9c0f740fef6a0", size = 4666059, upload-time = "2026-01-28T00:23:26.766Z" }, - { url = "https://files.pythonhosted.org/packages/eb/a7/0fca0fd3591dffc297278a61813d7f661a14243dd60f499a7a5b48acb52a/cryptography-46.0.4-cp311-abi3-win32.whl", hash = "sha256:82a62483daf20b8134f6e92898da70d04d0ef9a75829d732ea1018678185f4f5", size = 3026378, upload-time = "2026-01-28T00:23:28.317Z" }, - { url = "https://files.pythonhosted.org/packages/2d/12/652c84b6f9873f0909374864a57b003686c642ea48c84d6c7e2c515e6da5/cryptography-46.0.4-cp311-abi3-win_amd64.whl", hash = "sha256:6225d3ebe26a55dbc8ead5ad1265c0403552a63336499564675b29eb3184c09b", size = 3478614, upload-time = "2026-01-28T00:23:30.275Z" }, - { url = "https://files.pythonhosted.org/packages/b9/27/542b029f293a5cce59349d799d4d8484b3b1654a7b9a0585c266e974a488/cryptography-46.0.4-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:485e2b65d25ec0d901bca7bcae0f53b00133bf3173916d8e421f6fddde103908", size = 7116417, upload-time = "2026-01-28T00:23:31.958Z" }, - { url = "https://files.pythonhosted.org/packages/f8/f5/559c25b77f40b6bf828eabaf988efb8b0e17b573545edb503368ca0a2a03/cryptography-46.0.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:078e5f06bd2fa5aea5a324f2a09f914b1484f1d0c2a4d6a8a28c74e72f65f2da", size = 4264508, upload-time = "2026-01-28T00:23:34.264Z" }, - { url = "https://files.pythonhosted.org/packages/49/a1/551fa162d33074b660dc35c9bc3616fefa21a0e8c1edd27b92559902e408/cryptography-46.0.4-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:dce1e4f068f03008da7fa51cc7abc6ddc5e5de3e3d1550334eaf8393982a5829", size = 4409080, upload-time = "2026-01-28T00:23:35.793Z" }, - { url = "https://files.pythonhosted.org/packages/b0/6a/4d8d129a755f5d6df1bbee69ea2f35ebfa954fa1847690d1db2e8bca46a5/cryptography-46.0.4-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:2067461c80271f422ee7bdbe79b9b4be54a5162e90345f86a23445a0cf3fd8a2", size = 4270039, upload-time = "2026-01-28T00:23:37.263Z" }, - { url = "https://files.pythonhosted.org/packages/4c/f5/ed3fcddd0a5e39321e595e144615399e47e7c153a1fb8c4862aec3151ff9/cryptography-46.0.4-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:c92010b58a51196a5f41c3795190203ac52edfd5dc3ff99149b4659eba9d2085", size = 4926748, upload-time = "2026-01-28T00:23:38.884Z" }, - { url = "https://files.pythonhosted.org/packages/43/ae/9f03d5f0c0c00e85ecb34f06d3b79599f20630e4db91b8a6e56e8f83d410/cryptography-46.0.4-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:829c2b12bbc5428ab02d6b7f7e9bbfd53e33efd6672d21341f2177470171ad8b", size = 4442307, upload-time = "2026-01-28T00:23:40.56Z" }, - { url = "https://files.pythonhosted.org/packages/8b/22/e0f9f2dae8040695103369cf2283ef9ac8abe4d51f68710bec2afd232609/cryptography-46.0.4-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:62217ba44bf81b30abaeda1488686a04a702a261e26f87db51ff61d9d3510abd", size = 3959253, upload-time = "2026-01-28T00:23:42.827Z" }, - { url = "https://files.pythonhosted.org/packages/01/5b/6a43fcccc51dae4d101ac7d378a8724d1ba3de628a24e11bf2f4f43cba4d/cryptography-46.0.4-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:9c2da296c8d3415b93e6053f5a728649a87a48ce084a9aaf51d6e46c87c7f2d2", size = 4269372, upload-time = "2026-01-28T00:23:44.655Z" }, - { url = "https://files.pythonhosted.org/packages/17/b7/0f6b8c1dd0779df2b526e78978ff00462355e31c0a6f6cff8a3e99889c90/cryptography-46.0.4-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:9b34d8ba84454641a6bf4d6762d15847ecbd85c1316c0a7984e6e4e9f748ec2e", size = 4891908, upload-time = "2026-01-28T00:23:46.48Z" }, - { url = "https://files.pythonhosted.org/packages/83/17/259409b8349aa10535358807a472c6a695cf84f106022268d31cea2b6c97/cryptography-46.0.4-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:df4a817fa7138dd0c96c8c8c20f04b8aaa1fac3bbf610913dcad8ea82e1bfd3f", size = 4441254, upload-time = "2026-01-28T00:23:48.403Z" }, - { url = "https://files.pythonhosted.org/packages/9c/fe/e4a1b0c989b00cee5ffa0764401767e2d1cf59f45530963b894129fd5dce/cryptography-46.0.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:b1de0ebf7587f28f9190b9cb526e901bf448c9e6a99655d2b07fff60e8212a82", size = 4396520, upload-time = "2026-01-28T00:23:50.26Z" }, - { url = "https://files.pythonhosted.org/packages/b3/81/ba8fd9657d27076eb40d6a2f941b23429a3c3d2f56f5a921d6b936a27bc9/cryptography-46.0.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9b4d17bc7bd7cdd98e3af40b441feaea4c68225e2eb2341026c84511ad246c0c", size = 4651479, upload-time = "2026-01-28T00:23:51.674Z" }, - { url = "https://files.pythonhosted.org/packages/00/03/0de4ed43c71c31e4fe954edd50b9d28d658fef56555eba7641696370a8e2/cryptography-46.0.4-cp314-cp314t-win32.whl", hash = "sha256:c411f16275b0dea722d76544a61d6421e2cc829ad76eec79280dbdc9ddf50061", size = 3001986, upload-time = "2026-01-28T00:23:53.485Z" }, - { url = "https://files.pythonhosted.org/packages/5c/70/81830b59df7682917d7a10f833c4dab2a5574cd664e86d18139f2b421329/cryptography-46.0.4-cp314-cp314t-win_amd64.whl", hash = "sha256:728fedc529efc1439eb6107b677f7f7558adab4553ef8669f0d02d42d7b959a7", size = 3468288, upload-time = "2026-01-28T00:23:55.09Z" }, - { url = "https://files.pythonhosted.org/packages/56/f7/f648fdbb61d0d45902d3f374217451385edc7e7768d1b03ff1d0e5ffc17b/cryptography-46.0.4-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:a9556ba711f7c23f77b151d5798f3ac44a13455cc68db7697a1096e6d0563cab", size = 7169583, upload-time = "2026-01-28T00:23:56.558Z" }, - { url = "https://files.pythonhosted.org/packages/d8/cc/8f3224cbb2a928de7298d6ed4790f5ebc48114e02bdc9559196bfb12435d/cryptography-46.0.4-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8bf75b0259e87fa70bddc0b8b4078b76e7fd512fd9afae6c1193bcf440a4dbef", size = 4275419, upload-time = "2026-01-28T00:23:58.364Z" }, - { url = "https://files.pythonhosted.org/packages/17/43/4a18faa7a872d00e4264855134ba82d23546c850a70ff209e04ee200e76f/cryptography-46.0.4-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3c268a3490df22270955966ba236d6bc4a8f9b6e4ffddb78aac535f1a5ea471d", size = 4419058, upload-time = "2026-01-28T00:23:59.867Z" }, - { url = "https://files.pythonhosted.org/packages/ee/64/6651969409821d791ba12346a124f55e1b76f66a819254ae840a965d4b9c/cryptography-46.0.4-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:812815182f6a0c1d49a37893a303b44eaac827d7f0d582cecfc81b6427f22973", size = 4278151, upload-time = "2026-01-28T00:24:01.731Z" }, - { url = "https://files.pythonhosted.org/packages/20/0b/a7fce65ee08c3c02f7a8310cc090a732344066b990ac63a9dfd0a655d321/cryptography-46.0.4-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:a90e43e3ef65e6dcf969dfe3bb40cbf5aef0d523dff95bfa24256be172a845f4", size = 4939441, upload-time = "2026-01-28T00:24:03.175Z" }, - { url = "https://files.pythonhosted.org/packages/db/a7/20c5701e2cd3e1dfd7a19d2290c522a5f435dd30957d431dcb531d0f1413/cryptography-46.0.4-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a05177ff6296644ef2876fce50518dffb5bcdf903c85250974fc8bc85d54c0af", size = 4451617, upload-time = "2026-01-28T00:24:05.403Z" }, - { url = "https://files.pythonhosted.org/packages/00/dc/3e16030ea9aa47b63af6524c354933b4fb0e352257c792c4deeb0edae367/cryptography-46.0.4-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:daa392191f626d50f1b136c9b4cf08af69ca8279d110ea24f5c2700054d2e263", size = 3977774, upload-time = "2026-01-28T00:24:06.851Z" }, - { url = "https://files.pythonhosted.org/packages/42/c8/ad93f14118252717b465880368721c963975ac4b941b7ef88f3c56bf2897/cryptography-46.0.4-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:e07ea39c5b048e085f15923511d8121e4a9dc45cee4e3b970ca4f0d338f23095", size = 4277008, upload-time = "2026-01-28T00:24:08.926Z" }, - { url = "https://files.pythonhosted.org/packages/00/cf/89c99698151c00a4631fbfcfcf459d308213ac29e321b0ff44ceeeac82f1/cryptography-46.0.4-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:d5a45ddc256f492ce42a4e35879c5e5528c09cd9ad12420828c972951d8e016b", size = 4903339, upload-time = "2026-01-28T00:24:12.009Z" }, - { url = "https://files.pythonhosted.org/packages/03/c3/c90a2cb358de4ac9309b26acf49b2a100957e1ff5cc1e98e6c4996576710/cryptography-46.0.4-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:6bb5157bf6a350e5b28aee23beb2d84ae6f5be390b2f8ee7ea179cda077e1019", size = 4451216, upload-time = "2026-01-28T00:24:13.975Z" }, - { url = "https://files.pythonhosted.org/packages/96/2c/8d7f4171388a10208671e181ca43cdc0e596d8259ebacbbcfbd16de593da/cryptography-46.0.4-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:dd5aba870a2c40f87a3af043e0dee7d9eb02d4aff88a797b48f2b43eff8c3ab4", size = 4404299, upload-time = "2026-01-28T00:24:16.169Z" }, - { url = "https://files.pythonhosted.org/packages/e9/23/cbb2036e450980f65c6e0a173b73a56ff3bccd8998965dea5cc9ddd424a5/cryptography-46.0.4-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:93d8291da8d71024379ab2cb0b5c57915300155ad42e07f76bea6ad838d7e59b", size = 4664837, upload-time = "2026-01-28T00:24:17.629Z" }, - { url = "https://files.pythonhosted.org/packages/0a/21/f7433d18fe6d5845329cbdc597e30caf983229c7a245bcf54afecc555938/cryptography-46.0.4-cp38-abi3-win32.whl", hash = "sha256:0563655cb3c6d05fb2afe693340bc050c30f9f34e15763361cf08e94749401fc", size = 3009779, upload-time = "2026-01-28T00:24:20.198Z" }, - { url = "https://files.pythonhosted.org/packages/3a/6a/bd2e7caa2facffedf172a45c1a02e551e6d7d4828658c9a245516a598d94/cryptography-46.0.4-cp38-abi3-win_amd64.whl", hash = "sha256:fa0900b9ef9c49728887d1576fd8d9e7e3ea872fa9b25ef9b64888adc434e976", size = 3466633, upload-time = "2026-01-28T00:24:21.851Z" }, - { url = "https://files.pythonhosted.org/packages/59/e0/f9c6c53e1f2a1c2507f00f2faba00f01d2f334b35b0fbfe5286715da2184/cryptography-46.0.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:766330cce7416c92b5e90c3bb71b1b79521760cdcfc3a6a1a182d4c9fab23d2b", size = 3476316, upload-time = "2026-01-28T00:24:24.144Z" }, - { url = "https://files.pythonhosted.org/packages/27/7a/f8d2d13227a9a1a9fe9c7442b057efecffa41f1e3c51d8622f26b9edbe8f/cryptography-46.0.4-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c236a44acfb610e70f6b3e1c3ca20ff24459659231ef2f8c48e879e2d32b73da", size = 4216693, upload-time = "2026-01-28T00:24:25.758Z" }, - { url = "https://files.pythonhosted.org/packages/c5/de/3787054e8f7972658370198753835d9d680f6cd4a39df9f877b57f0dd69c/cryptography-46.0.4-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:8a15fb869670efa8f83cbffbc8753c1abf236883225aed74cd179b720ac9ec80", size = 4382765, upload-time = "2026-01-28T00:24:27.577Z" }, - { url = "https://files.pythonhosted.org/packages/8a/5f/60e0afb019973ba6a0b322e86b3d61edf487a4f5597618a430a2a15f2d22/cryptography-46.0.4-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:fdc3daab53b212472f1524d070735b2f0c214239df131903bae1d598016fa822", size = 4216066, upload-time = "2026-01-28T00:24:29.056Z" }, - { url = "https://files.pythonhosted.org/packages/81/8e/bf4a0de294f147fee66f879d9bae6f8e8d61515558e3d12785dd90eca0be/cryptography-46.0.4-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:44cc0675b27cadb71bdbb96099cca1fa051cd11d2ade09e5cd3a2edb929ed947", size = 4382025, upload-time = "2026-01-28T00:24:30.681Z" }, - { url = "https://files.pythonhosted.org/packages/79/f4/9ceb90cfd6a3847069b0b0b353fd3075dc69b49defc70182d8af0c4ca390/cryptography-46.0.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:be8c01a7d5a55f9a47d1888162b76c8f49d62b234d88f0ff91a9fbebe32ffbc3", size = 3406043, upload-time = "2026-01-28T00:24:32.236Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/9f/a9/db8f313fdcd85d767d4973515e1db101f9c71f95fced83233de224673757/cryptography-48.0.0.tar.gz", hash = "sha256:5c3932f4436d1cccb036cb0eaef46e6e2db91035166f1ad6505c3c9d5a635920", size = 832984, upload-time = "2026-05-04T22:59:38.133Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/3d/01f6dd9190170a5a241e0e98c2d04be3664a9e6f5b9b872cde63aff1c3dd/cryptography-48.0.0-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:0c558d2cdffd8f4bbb30fc7134c74d2ca9a476f830bb053074498fbc86f41ed6", size = 8001587, upload-time = "2026-05-04T22:57:36.803Z" }, + { url = "https://files.pythonhosted.org/packages/b2/6e/e90527eef33f309beb811cf7c982c3aeffcce8e3edb178baa4ca3ae4a6fa/cryptography-48.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f5333311663ea94f75dd408665686aaf426563556bb5283554a3539177e03b8c", size = 4690433, upload-time = "2026-05-04T22:57:40.373Z" }, + { url = "https://files.pythonhosted.org/packages/90/04/673510ed51ddff56575f306cf1617d80411ee76831ccd3097599140efdfe/cryptography-48.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7995ef305d7165c3f11ae07f2517e5a4f1d5c18da1376a0a9ed496336b69e5f3", size = 4710620, upload-time = "2026-05-04T22:57:42.935Z" }, + { url = "https://files.pythonhosted.org/packages/14/d5/e9c4ef932c8d800490c34d8bd589d64a31d5890e27ec9e9ad532be893294/cryptography-48.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:40ba1f85eaa6959837b1d51c9767e230e14612eea4ef110ee8854ada22da1bf5", size = 4696283, upload-time = "2026-05-04T22:57:45.294Z" }, + { url = "https://files.pythonhosted.org/packages/0c/29/174b9dfb60b12d59ecfc6cfa04bc88c21b42a54f01b8aae09bb6e51e4c7f/cryptography-48.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:369a6348999f94bbd53435c894377b20ab95f25a9065c283570e70150d8abc3c", size = 5296573, upload-time = "2026-05-04T22:57:47.933Z" }, + { url = "https://files.pythonhosted.org/packages/95/38/0d29a6fd7d0d1373f0c0c88a04ba20e359b257753ac497564cd660fc1d55/cryptography-48.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a0e692c683f4df67815a2d258b324e66f4738bd7a96a218c826dce4f4bd05d8f", size = 4743677, upload-time = "2026-05-04T22:57:50.067Z" }, + { url = "https://files.pythonhosted.org/packages/30/be/eef653013d5c63b6a490529e0316f9ac14a37602965d4903efed1399f32b/cryptography-48.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:18349bbc56f4743c8b12dc32e2bccb2cf83ee8b69a3bba74ef8ae857e26b3d25", size = 4330808, upload-time = "2026-05-04T22:57:52.301Z" }, + { url = "https://files.pythonhosted.org/packages/84/9e/500463e87abb7a0a0f9f256ec21123ecde0a7b5541a15e840ea54551fd81/cryptography-48.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:7e8eac43dfca5c4cccc6dad9a80504436fca53bb9bc3100a2386d730fbe6b602", size = 4695941, upload-time = "2026-05-04T22:57:54.603Z" }, + { url = "https://files.pythonhosted.org/packages/e3/dc/7303087450c2ec9e7fbb750e17c2abfbc658f23cbd0e54009509b7cc4091/cryptography-48.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:9ccdac7d40688ecb5a3b4a604b8a88c8002e3442d6c60aead1db2a89a041560c", size = 5252579, upload-time = "2026-05-04T22:57:57.207Z" }, + { url = "https://files.pythonhosted.org/packages/d0/c0/7101d3b7215edcdc90c45da544961fd8ed2d6448f77577460fa75a8443f7/cryptography-48.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:bd72e68b06bb1e96913f97dd4901119bc17f39d4586a5adf2d3e47bc2b9d58b5", size = 4743326, upload-time = "2026-05-04T22:57:59.535Z" }, + { url = "https://files.pythonhosted.org/packages/ac/d8/5b833bad13016f562ab9d063d68199a4bd121d18458e439515601d3357ec/cryptography-48.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:59baa2cb386c4f0b9905bd6eb4c2a79a69a128408fd31d32ca4d7102d4156321", size = 4826672, upload-time = "2026-05-04T22:58:01.996Z" }, + { url = "https://files.pythonhosted.org/packages/98/e1/7074eb8bf3c135558c73fc2bcf0f5633f912e6fb87e868a55c454080ef09/cryptography-48.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:9249e3cd978541d665967ac2cb2787fd6a62bddf1e75b3e347a594d7dacf4f74", size = 4972574, upload-time = "2026-05-04T22:58:03.968Z" }, + { url = "https://files.pythonhosted.org/packages/04/70/e5a1b41d325f797f39427aa44ef8baf0be500065ab6d8e10369d850d4a4f/cryptography-48.0.0-cp311-abi3-win32.whl", hash = "sha256:9c459db21422be75e2809370b829a87eb37f74cd785fc4aa9ea1e5f43b47cda4", size = 3294868, upload-time = "2026-05-04T22:58:06.467Z" }, + { url = "https://files.pythonhosted.org/packages/f4/ac/8ac51b4a5fc5932eb7ee5c517ba7dc8cd834f0048962b6b352f00f41ebf9/cryptography-48.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:5b012212e08b8dd5edc78ef54da83dd9892fd9105323b3993eff6bea65dc21d7", size = 3817107, upload-time = "2026-05-04T22:58:08.845Z" }, + { url = "https://files.pythonhosted.org/packages/6b/84/70e3feea9feea87fd7cbe77efb2712ae1e3e6edf10749dc6e95f4e60e455/cryptography-48.0.0-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:3cb07a3ed6431663cd321ea8a000a1314c74211f823e4177fefa2255e057d1ec", size = 7986556, upload-time = "2026-05-04T22:58:11.172Z" }, + { url = "https://files.pythonhosted.org/packages/89/6e/18e07a618bb5442ba10cf4df16e99c071365528aa570dfcb8c02e25a303b/cryptography-48.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8c7378637d7d88016fa6791c159f698b3d3eed28ebf844ac36b9dc04a14dae18", size = 4684776, upload-time = "2026-05-04T22:58:13.712Z" }, + { url = "https://files.pythonhosted.org/packages/be/6a/4ea3b4c6c6759794d5ee2103c304a5076dc4b19ae1f9fe47dba439e159e9/cryptography-48.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cc90c0b39b2e3c65ef52c804b72e3c58f8a04ab2a1871272798e5f9572c17d20", size = 4698121, upload-time = "2026-05-04T22:58:16.448Z" }, + { url = "https://files.pythonhosted.org/packages/2f/59/6ff6ad6cae03bb887da2a5860b2c9805f8dac969ef01ce563336c49bd1d1/cryptography-48.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:76341972e1eff8b4bea859f09c0d3e64b96ce931b084f9b9b7db8ef364c30eff", size = 4690042, upload-time = "2026-05-04T22:58:18.544Z" }, + { url = "https://files.pythonhosted.org/packages/ca/b4/fc334ed8cfd705aca282fe4d8f5ae64a8e0f74932e9feecb344610cf6e4d/cryptography-48.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:55b7718303bf06a5753dcdccf2f3945cf18ad7bffde41b61226e4db31ab89a9c", size = 5282526, upload-time = "2026-05-04T22:58:20.75Z" }, + { url = "https://files.pythonhosted.org/packages/11/08/9f8c5386cc4cd90d8255c7cdd0f5baf459a08502a09de30dc51f553d38dc/cryptography-48.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:a64697c641c7b1b2178e573cbc31c7c6684cd56883a478d75143dbb7118036db", size = 4733116, upload-time = "2026-05-04T22:58:23.627Z" }, + { url = "https://files.pythonhosted.org/packages/b8/77/99307d7574045699f8805aa500fa0fb83422d115b5400a064ddd306d7750/cryptography-48.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:561215ea3879cb1cbbf272867e2efda62476f240fb58c64de6b393ae19246741", size = 4316030, upload-time = "2026-05-04T22:58:25.581Z" }, + { url = "https://files.pythonhosted.org/packages/fd/36/a608b98337af3cb2aff4818e406649d30572b7031918b04c87d979495348/cryptography-48.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:ad64688338ed4bc1a6618076ba75fd7194a5f1797ac60b47afe926285adb3166", size = 4689640, upload-time = "2026-05-04T22:58:27.747Z" }, + { url = "https://files.pythonhosted.org/packages/dd/a6/825010a291b4438aecc1f568bc428189fc1175515223632477c07dc0a6df/cryptography-48.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:906cbf0670286c6e0044156bc7d4af9cbb0ef6db9f73e52c3ec56ba6bdde5336", size = 5237657, upload-time = "2026-05-04T22:58:29.848Z" }, + { url = "https://files.pythonhosted.org/packages/b9/09/4e76a09b4caa29aad535ddc806f5d4c5d01885bd978bd984fbc6ca032cae/cryptography-48.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:ea8990436d914540a40ab24b6a77c0969695ed52f4a4874c5137ccf7045a7057", size = 4732362, upload-time = "2026-05-04T22:58:32.009Z" }, + { url = "https://files.pythonhosted.org/packages/18/78/444fa04a77d0cb95f417dda20d450e13c56ba8e5220fc892a1658f44f882/cryptography-48.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c18684a7f0cc9a3cb60328f496b8e3372def7c5d2df39ac267878b05565aaaae", size = 4819580, upload-time = "2026-05-04T22:58:34.254Z" }, + { url = "https://files.pythonhosted.org/packages/38/85/ea67067c70a1fd4be2c63d35eeed82658023021affccc7b17705f8527dd2/cryptography-48.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9be5aafa5736574f8f15f262adc81b2a9869e2cfe9014d52a44633905b40d52c", size = 4963283, upload-time = "2026-05-04T22:58:36.376Z" }, + { url = "https://files.pythonhosted.org/packages/75/54/cc6d0f3deac3e81c7f847e8a189a12b6cdd65059b43dad25d4316abd849a/cryptography-48.0.0-cp314-cp314t-win32.whl", hash = "sha256:c17dfe85494deaeddc5ce251aebd1d60bbe6afc8b62071bb0b469431a000124f", size = 3270954, upload-time = "2026-05-04T22:58:38.791Z" }, + { url = "https://files.pythonhosted.org/packages/49/67/cc947e288c0758a4e5473d1dcb743037ab7785541265a969240b8885441a/cryptography-48.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27241b1dc9962e056062a8eef1991d02c3a24569c95975bd2322a8a52c6e5e12", size = 3797313, upload-time = "2026-05-04T22:58:40.746Z" }, + { url = "https://files.pythonhosted.org/packages/f2/63/61d4a4e1c6b6bab6ce1e213cd36a24c415d90e76d78c5eb8577c5541d2e8/cryptography-48.0.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:58d00498e8933e4a194f3076aee1b4a97dfec1a6da444535755822fe5d8b0b86", size = 7983482, upload-time = "2026-05-04T22:58:43.769Z" }, + { url = "https://files.pythonhosted.org/packages/d5/ac/f5b5995b87770c693e2596559ffafe195b4033a57f14a82268a2842953f3/cryptography-48.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:614d0949f4790582d2cc25553abd09dd723025f0c0e7c67376a1d77196743d6e", size = 4683266, upload-time = "2026-05-04T22:58:46.064Z" }, + { url = "https://files.pythonhosted.org/packages/ec/c6/8b14f67e18338fbc4adb76f66c001f5c3610b3e2d1837f268f47a347dbbb/cryptography-48.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7ce4bfae76319a532a2dc68f82cc32f5676ee792a983187dac07183690e5c66f", size = 4696228, upload-time = "2026-05-04T22:58:48.22Z" }, + { url = "https://files.pythonhosted.org/packages/ea/73/f808fbae9514bd91b47875b003f13e284c8c6bdfd904b7944e803937eec1/cryptography-48.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:2eb992bbd4661238c5a397594c83f5b4dc2bc5b848c365c8f991b6780efcc5c7", size = 4689097, upload-time = "2026-05-04T22:58:50.9Z" }, + { url = "https://files.pythonhosted.org/packages/93/01/d86632d7d28db8ae83221995752eeb6639ffb374c2d22955648cf8d52797/cryptography-48.0.0-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:22a5cb272895dce158b2cacdfdc3debd299019659f42947dbdac6f32d68fe832", size = 5283582, upload-time = "2026-05-04T22:58:53.017Z" }, + { url = "https://files.pythonhosted.org/packages/02/e1/50edc7a50334807cc4791fc4a0ce7468b4a1416d9138eab358bfc9a3d70b/cryptography-48.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2b4d59804e8408e2fea7d1fbaf218e5ec984325221db76e6a241a9abd6cdd95c", size = 4730479, upload-time = "2026-05-04T22:58:55.611Z" }, + { url = "https://files.pythonhosted.org/packages/6f/af/99a582b1b1641ff5911ac559beb45097cf79efd4ead4657f578ef1af2d47/cryptography-48.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:984a20b0f62a26f48a3396c72e4bc34c66e356d356bf370053066b3b6d54634a", size = 4326481, upload-time = "2026-05-04T22:58:57.607Z" }, + { url = "https://files.pythonhosted.org/packages/90/ee/89aa26a06ef0a7d7611788ffd571a7c50e368cc6a4d5eef8b4884e866edb/cryptography-48.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:5a5ed8fde7a1d09376ca0b40e68cd59c69fe23b1f9768bd5824f54681626032a", size = 4688713, upload-time = "2026-05-04T22:59:00.077Z" }, + { url = "https://files.pythonhosted.org/packages/70/ba/bcb1b0bb7a33d4c7c0c4d4c7874b4a62ae4f56113a5f4baefa362dfb1f0f/cryptography-48.0.0-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:8cd666227ef7af430aa5914a9910e0ddd703e75f039cef0825cd0da71b6b711a", size = 5238165, upload-time = "2026-05-04T22:59:02.317Z" }, + { url = "https://files.pythonhosted.org/packages/c9/70/ca4003b1ce5ca3dc3186ada51908c8a9b9ff7d5cab83cc0d43ee14ec144f/cryptography-48.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:9071196d81abc88b3516ac8cdfad32e2b66dd4a5393a8e68a961e9161ddc6239", size = 4729947, upload-time = "2026-05-04T22:59:05.255Z" }, + { url = "https://files.pythonhosted.org/packages/44/a0/4ec7cf774207905aef1a8d11c3750d5a1db805eb380ee4e16df317870128/cryptography-48.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1e2d54c8be6152856a36f0882ab231e70f8ec7f14e93cf87db8a2ed056bf160c", size = 4822059, upload-time = "2026-05-04T22:59:07.802Z" }, + { url = "https://files.pythonhosted.org/packages/1e/75/a2e55f99c16fcac7b5d6c1eb19ad8e00799854d6be5ca845f9259eae1681/cryptography-48.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a5da777e32ffed6f85a7b2b3f7c5cbc88c146bfcd0a1d7baf5fcc6c52ee35dd4", size = 4960575, upload-time = "2026-05-04T22:59:09.851Z" }, + { url = "https://files.pythonhosted.org/packages/b8/23/6e6f32143ab5d8b36ca848a502c4bcd477ae75b9e1677e3530d669062578/cryptography-48.0.0-cp39-abi3-win32.whl", hash = "sha256:77a2ccbbe917f6710e05ba9adaa25fb5075620bf3ea6fb751997875aff4ae4bd", size = 3279117, upload-time = "2026-05-04T22:59:12.019Z" }, + { url = "https://files.pythonhosted.org/packages/9d/9a/0fea98a70cf1749d41d738836f6349d97945f7c89433a259a6c2642eefeb/cryptography-48.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:16cd65b9330583e4619939b3a3843eec1e6e789744bb01e7c7e2e62e33c239c8", size = 3792100, upload-time = "2026-05-04T22:59:14.884Z" }, + { url = "https://files.pythonhosted.org/packages/be/d2/024b5e06be9d44cb021fb0e1a03d34d63989cf56a0fe62f3dfbab695b9b4/cryptography-48.0.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:84cf79f0dc8b36ac5da873481716e87aef31fcfa0444f9e1d8b4b2cece142855", size = 3950391, upload-time = "2026-05-04T22:59:17.415Z" }, + { url = "https://files.pythonhosted.org/packages/bc/17/3861e17c56fa0fd37491a14a8673fdb77c57fc5693cafe745ea8b06dba75/cryptography-48.0.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:fdfef35d751d510fcef5252703621574364fec16418c4a1e5e1055248401054b", size = 4637126, upload-time = "2026-05-04T22:59:20.197Z" }, + { url = "https://files.pythonhosted.org/packages/f0/0a/7e226dbff530f21480727eb764973a7bff2b912f8e15cd4f129e71b56d1d/cryptography-48.0.0-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:0890f502ddf7d9c6426129c3f49f5c0a39278ed7cd6322c8755ffca6ee675a13", size = 4667270, upload-time = "2026-05-04T22:59:22.647Z" }, + { url = "https://files.pythonhosted.org/packages/3b/f2/5a72274ca9f1b2a8b44a662ee0bf1b435909deb473d6f97bcd035bcdbc71/cryptography-48.0.0-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:ecde28a596bead48b0cfd2a1b4416c3d43074c2d785e3a398d7ec1fc4d0f7fbb", size = 4636797, upload-time = "2026-05-04T22:59:24.912Z" }, + { url = "https://files.pythonhosted.org/packages/b4/e1/48cedb2fe63626e91ded1edad159e2a4fb8b6906c4425eb7749673077ce7/cryptography-48.0.0-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:4defde8685ae324a9eb9d818717e93b4638ef67070ac9bc15b8ca85f63048355", size = 4666800, upload-time = "2026-05-04T22:59:27.474Z" }, + { url = "https://files.pythonhosted.org/packages/a2/ca/7e8365deec19afb2b2c7be7c1c0aa8f99633b54e90c570999acda93260fc/cryptography-48.0.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:db63bf618e5dea46c07de12e900fe1cdd2541e6dc9dbae772a70b7d4d4765f6a", size = 3739536, upload-time = "2026-05-04T22:59:29.61Z" }, ] [[package]] @@ -614,7 +614,7 @@ wheels = [ [[package]] name = "dppvalidator" -version = "0.3.2" +version = "0.4.0" source = { editable = "." } dependencies = [ { name = "base58" }, @@ -645,6 +645,7 @@ dev = [ { name = "cyclonedx-bom" }, { name = "hypothesis" }, { name = "mutmut" }, + { name = "pip" }, { name = "pip-audit" }, { name = "pip-licenses" }, { name = "pre-commit" }, @@ -666,12 +667,12 @@ docs = [ [package.metadata] requires-dist = [ { name = "base58", specifier = ">=2.1.0" }, - { name = "cryptography", specifier = ">=43.0.0" }, + { name = "cryptography", specifier = ">=46.0.7" }, { name = "dppvalidator", extras = ["cli", "rdf"], marker = "extra == 'all'" }, { name = "httpx", specifier = ">=0.28.0" }, { name = "jsonschema", specifier = ">=4.23.0" }, { name = "pydantic", specifier = ">=2.12.5" }, - { name = "pyjwt", specifier = ">=2.9.0" }, + { name = "pyjwt", specifier = ">=2.12.0" }, { name = "pyld", specifier = ">=2.0.4" }, { name = "pyshacl", marker = "extra == 'rdf'", specifier = ">=0.25.0" }, { name = "rdflib", marker = "extra == 'rdf'", specifier = ">=7.0.0" }, @@ -684,11 +685,12 @@ dev = [ { name = "cyclonedx-bom", specifier = ">=7.0.0" }, { name = "hypothesis", specifier = ">=6.100.0" }, { name = "mutmut", specifier = ">=3.0.0" }, + { name = "pip", specifier = ">=26.1" }, { name = "pip-audit", specifier = ">=2.7.0" }, { name = "pip-licenses", specifier = ">=5.0.0" }, { name = "pre-commit", specifier = ">=3.6.0" }, { name = "pyshacl", specifier = ">=0.25.0" }, - { name = "pytest", specifier = ">=8.0.0" }, + { name = "pytest", specifier = ">=9.0.3" }, { name = "pytest-asyncio", specifier = ">=0.24.0" }, { name = "pytest-cov", specifier = ">=4.1.0" }, { name = "rdflib", specifier = ">=7.0.0" }, @@ -1068,126 +1070,120 @@ wheels = [ [[package]] name = "lxml" -version = "6.0.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/aa/88/262177de60548e5a2bfc46ad28232c9e9cbde697bd94132aeb80364675cb/lxml-6.0.2.tar.gz", hash = "sha256:cd79f3367bd74b317dda655dc8fcfa304d9eb6e4fb06b7168c5cf27f96e0cd62", size = 4073426, upload-time = "2025-09-22T04:04:59.287Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/db/8a/f8192a08237ef2fb1b19733f709db88a4c43bc8ab8357f01cb41a27e7f6a/lxml-6.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e77dd455b9a16bbd2a5036a63ddbd479c19572af81b624e79ef422f929eef388", size = 8590589, upload-time = "2025-09-22T04:00:10.51Z" }, - { url = "https://files.pythonhosted.org/packages/12/64/27bcd07ae17ff5e5536e8d88f4c7d581b48963817a13de11f3ac3329bfa2/lxml-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5d444858b9f07cefff6455b983aea9a67f7462ba1f6cbe4a21e8bf6791bf2153", size = 4629671, upload-time = "2025-09-22T04:00:15.411Z" }, - { url = "https://files.pythonhosted.org/packages/02/5a/a7d53b3291c324e0b6e48f3c797be63836cc52156ddf8f33cd72aac78866/lxml-6.0.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f952dacaa552f3bb8834908dddd500ba7d508e6ea6eb8c52eb2d28f48ca06a31", size = 4999961, upload-time = "2025-09-22T04:00:17.619Z" }, - { url = "https://files.pythonhosted.org/packages/f5/55/d465e9b89df1761674d8672bb3e4ae2c47033b01ec243964b6e334c6743f/lxml-6.0.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:71695772df6acea9f3c0e59e44ba8ac50c4f125217e84aab21074a1a55e7e5c9", size = 5157087, upload-time = "2025-09-22T04:00:19.868Z" }, - { url = "https://files.pythonhosted.org/packages/62/38/3073cd7e3e8dfc3ba3c3a139e33bee3a82de2bfb0925714351ad3d255c13/lxml-6.0.2-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:17f68764f35fd78d7c4cc4ef209a184c38b65440378013d24b8aecd327c3e0c8", size = 5067620, upload-time = "2025-09-22T04:00:21.877Z" }, - { url = "https://files.pythonhosted.org/packages/4a/d3/1e001588c5e2205637b08985597827d3827dbaaece16348c8822bfe61c29/lxml-6.0.2-cp310-cp310-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:058027e261afed589eddcfe530fcc6f3402d7fd7e89bfd0532df82ebc1563dba", size = 5406664, upload-time = "2025-09-22T04:00:23.714Z" }, - { url = "https://files.pythonhosted.org/packages/20/cf/cab09478699b003857ed6ebfe95e9fb9fa3d3c25f1353b905c9b73cfb624/lxml-6.0.2-cp310-cp310-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8ffaeec5dfea5881d4c9d8913a32d10cfe3923495386106e4a24d45300ef79c", size = 5289397, upload-time = "2025-09-22T04:00:25.544Z" }, - { url = "https://files.pythonhosted.org/packages/a3/84/02a2d0c38ac9a8b9f9e5e1bbd3f24b3f426044ad618b552e9549ee91bd63/lxml-6.0.2-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:f2e3b1a6bb38de0bc713edd4d612969dd250ca8b724be8d460001a387507021c", size = 4772178, upload-time = "2025-09-22T04:00:27.602Z" }, - { url = "https://files.pythonhosted.org/packages/56/87/e1ceadcc031ec4aa605fe95476892d0b0ba3b7f8c7dcdf88fdeff59a9c86/lxml-6.0.2-cp310-cp310-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d6690ec5ec1cce0385cb20896b16be35247ac8c2046e493d03232f1c2414d321", size = 5358148, upload-time = "2025-09-22T04:00:29.323Z" }, - { url = "https://files.pythonhosted.org/packages/fe/13/5bb6cf42bb228353fd4ac5f162c6a84fd68a4d6f67c1031c8cf97e131fc6/lxml-6.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f2a50c3c1d11cad0ebebbac357a97b26aa79d2bcaf46f256551152aa85d3a4d1", size = 5112035, upload-time = "2025-09-22T04:00:31.061Z" }, - { url = "https://files.pythonhosted.org/packages/e4/e2/ea0498552102e59834e297c5c6dff8d8ded3db72ed5e8aad77871476f073/lxml-6.0.2-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:3efe1b21c7801ffa29a1112fab3b0f643628c30472d507f39544fd48e9549e34", size = 4799111, upload-time = "2025-09-22T04:00:33.11Z" }, - { url = "https://files.pythonhosted.org/packages/6a/9e/8de42b52a73abb8af86c66c969b3b4c2a96567b6ac74637c037d2e3baa60/lxml-6.0.2-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:59c45e125140b2c4b33920d21d83681940ca29f0b83f8629ea1a2196dc8cfe6a", size = 5351662, upload-time = "2025-09-22T04:00:35.237Z" }, - { url = "https://files.pythonhosted.org/packages/28/a2/de776a573dfb15114509a37351937c367530865edb10a90189d0b4b9b70a/lxml-6.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:452b899faa64f1805943ec1c0c9ebeaece01a1af83e130b69cdefeda180bb42c", size = 5314973, upload-time = "2025-09-22T04:00:37.086Z" }, - { url = "https://files.pythonhosted.org/packages/50/a0/3ae1b1f8964c271b5eec91db2043cf8c6c0bce101ebb2a633b51b044db6c/lxml-6.0.2-cp310-cp310-win32.whl", hash = "sha256:1e786a464c191ca43b133906c6903a7e4d56bef376b75d97ccbb8ec5cf1f0a4b", size = 3611953, upload-time = "2025-09-22T04:00:39.224Z" }, - { url = "https://files.pythonhosted.org/packages/d1/70/bd42491f0634aad41bdfc1e46f5cff98825fb6185688dc82baa35d509f1a/lxml-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:dacf3c64ef3f7440e3167aa4b49aa9e0fb99e0aa4f9ff03795640bf94531bcb0", size = 4032695, upload-time = "2025-09-22T04:00:41.402Z" }, - { url = "https://files.pythonhosted.org/packages/d2/d0/05c6a72299f54c2c561a6c6cbb2f512e047fca20ea97a05e57931f194ac4/lxml-6.0.2-cp310-cp310-win_arm64.whl", hash = "sha256:45f93e6f75123f88d7f0cfd90f2d05f441b808562bf0bc01070a00f53f5028b5", size = 3680051, upload-time = "2025-09-22T04:00:43.525Z" }, - { url = "https://files.pythonhosted.org/packages/77/d5/becbe1e2569b474a23f0c672ead8a29ac50b2dc1d5b9de184831bda8d14c/lxml-6.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:13e35cbc684aadf05d8711a5d1b5857c92e5e580efa9a0d2be197199c8def607", size = 8634365, upload-time = "2025-09-22T04:00:45.672Z" }, - { url = "https://files.pythonhosted.org/packages/28/66/1ced58f12e804644426b85d0bb8a4478ca77bc1761455da310505f1a3526/lxml-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3b1675e096e17c6fe9c0e8c81434f5736c0739ff9ac6123c87c2d452f48fc938", size = 4650793, upload-time = "2025-09-22T04:00:47.783Z" }, - { url = "https://files.pythonhosted.org/packages/11/84/549098ffea39dfd167e3f174b4ce983d0eed61f9d8d25b7bf2a57c3247fc/lxml-6.0.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8ac6e5811ae2870953390452e3476694196f98d447573234592d30488147404d", size = 4944362, upload-time = "2025-09-22T04:00:49.845Z" }, - { url = "https://files.pythonhosted.org/packages/ac/bd/f207f16abf9749d2037453d56b643a7471d8fde855a231a12d1e095c4f01/lxml-6.0.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5aa0fc67ae19d7a64c3fe725dc9a1bb11f80e01f78289d05c6f62545affec438", size = 5083152, upload-time = "2025-09-22T04:00:51.709Z" }, - { url = "https://files.pythonhosted.org/packages/15/ae/bd813e87d8941d52ad5b65071b1affb48da01c4ed3c9c99e40abb266fbff/lxml-6.0.2-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:de496365750cc472b4e7902a485d3f152ecf57bd3ba03ddd5578ed8ceb4c5964", size = 5023539, upload-time = "2025-09-22T04:00:53.593Z" }, - { url = "https://files.pythonhosted.org/packages/02/cd/9bfef16bd1d874fbe0cb51afb00329540f30a3283beb9f0780adbb7eec03/lxml-6.0.2-cp311-cp311-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:200069a593c5e40b8f6fc0d84d86d970ba43138c3e68619ffa234bc9bb806a4d", size = 5344853, upload-time = "2025-09-22T04:00:55.524Z" }, - { url = "https://files.pythonhosted.org/packages/b8/89/ea8f91594bc5dbb879734d35a6f2b0ad50605d7fb419de2b63d4211765cc/lxml-6.0.2-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7d2de809c2ee3b888b59f995625385f74629707c9355e0ff856445cdcae682b7", size = 5225133, upload-time = "2025-09-22T04:00:57.269Z" }, - { url = "https://files.pythonhosted.org/packages/b9/37/9c735274f5dbec726b2db99b98a43950395ba3d4a1043083dba2ad814170/lxml-6.0.2-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:b2c3da8d93cf5db60e8858c17684c47d01fee6405e554fb55018dd85fc23b178", size = 4677944, upload-time = "2025-09-22T04:00:59.052Z" }, - { url = "https://files.pythonhosted.org/packages/20/28/7dfe1ba3475d8bfca3878365075abe002e05d40dfaaeb7ec01b4c587d533/lxml-6.0.2-cp311-cp311-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:442de7530296ef5e188373a1ea5789a46ce90c4847e597856570439621d9c553", size = 5284535, upload-time = "2025-09-22T04:01:01.335Z" }, - { url = "https://files.pythonhosted.org/packages/e7/cf/5f14bc0de763498fc29510e3532bf2b4b3a1c1d5d0dff2e900c16ba021ef/lxml-6.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2593c77efde7bfea7f6389f1ab249b15ed4aa5bc5cb5131faa3b843c429fbedb", size = 5067343, upload-time = "2025-09-22T04:01:03.13Z" }, - { url = "https://files.pythonhosted.org/packages/1c/b0/bb8275ab5472f32b28cfbbcc6db7c9d092482d3439ca279d8d6fa02f7025/lxml-6.0.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:3e3cb08855967a20f553ff32d147e14329b3ae70ced6edc2f282b94afbc74b2a", size = 4725419, upload-time = "2025-09-22T04:01:05.013Z" }, - { url = "https://files.pythonhosted.org/packages/25/4c/7c222753bc72edca3b99dbadba1b064209bc8ed4ad448af990e60dcce462/lxml-6.0.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:2ed6c667fcbb8c19c6791bbf40b7268ef8ddf5a96940ba9404b9f9a304832f6c", size = 5275008, upload-time = "2025-09-22T04:01:07.327Z" }, - { url = "https://files.pythonhosted.org/packages/6c/8c/478a0dc6b6ed661451379447cdbec77c05741a75736d97e5b2b729687828/lxml-6.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b8f18914faec94132e5b91e69d76a5c1d7b0c73e2489ea8929c4aaa10b76bbf7", size = 5248906, upload-time = "2025-09-22T04:01:09.452Z" }, - { url = "https://files.pythonhosted.org/packages/2d/d9/5be3a6ab2784cdf9accb0703b65e1b64fcdd9311c9f007630c7db0cfcce1/lxml-6.0.2-cp311-cp311-win32.whl", hash = "sha256:6605c604e6daa9e0d7f0a2137bdc47a2e93b59c60a65466353e37f8272f47c46", size = 3610357, upload-time = "2025-09-22T04:01:11.102Z" }, - { url = "https://files.pythonhosted.org/packages/e2/7d/ca6fb13349b473d5732fb0ee3eec8f6c80fc0688e76b7d79c1008481bf1f/lxml-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e5867f2651016a3afd8dd2c8238baa66f1e2802f44bc17e236f547ace6647078", size = 4036583, upload-time = "2025-09-22T04:01:12.766Z" }, - { url = "https://files.pythonhosted.org/packages/ab/a2/51363b5ecd3eab46563645f3a2c3836a2fc67d01a1b87c5017040f39f567/lxml-6.0.2-cp311-cp311-win_arm64.whl", hash = "sha256:4197fb2534ee05fd3e7afaab5d8bfd6c2e186f65ea7f9cd6a82809c887bd1285", size = 3680591, upload-time = "2025-09-22T04:01:14.874Z" }, - { url = "https://files.pythonhosted.org/packages/f3/c8/8ff2bc6b920c84355146cd1ab7d181bc543b89241cfb1ebee824a7c81457/lxml-6.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a59f5448ba2ceccd06995c95ea59a7674a10de0810f2ce90c9006f3cbc044456", size = 8661887, upload-time = "2025-09-22T04:01:17.265Z" }, - { url = "https://files.pythonhosted.org/packages/37/6f/9aae1008083bb501ef63284220ce81638332f9ccbfa53765b2b7502203cf/lxml-6.0.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e8113639f3296706fbac34a30813929e29247718e88173ad849f57ca59754924", size = 4667818, upload-time = "2025-09-22T04:01:19.688Z" }, - { url = "https://files.pythonhosted.org/packages/f1/ca/31fb37f99f37f1536c133476674c10b577e409c0a624384147653e38baf2/lxml-6.0.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a8bef9b9825fa8bc816a6e641bb67219489229ebc648be422af695f6e7a4fa7f", size = 4950807, upload-time = "2025-09-22T04:01:21.487Z" }, - { url = "https://files.pythonhosted.org/packages/da/87/f6cb9442e4bada8aab5ae7e1046264f62fdbeaa6e3f6211b93f4c0dd97f1/lxml-6.0.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:65ea18d710fd14e0186c2f973dc60bb52039a275f82d3c44a0e42b43440ea534", size = 5109179, upload-time = "2025-09-22T04:01:23.32Z" }, - { url = "https://files.pythonhosted.org/packages/c8/20/a7760713e65888db79bbae4f6146a6ae5c04e4a204a3c48896c408cd6ed2/lxml-6.0.2-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c371aa98126a0d4c739ca93ceffa0fd7a5d732e3ac66a46e74339acd4d334564", size = 5023044, upload-time = "2025-09-22T04:01:25.118Z" }, - { url = "https://files.pythonhosted.org/packages/a2/b0/7e64e0460fcb36471899f75831509098f3fd7cd02a3833ac517433cb4f8f/lxml-6.0.2-cp312-cp312-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:700efd30c0fa1a3581d80a748157397559396090a51d306ea59a70020223d16f", size = 5359685, upload-time = "2025-09-22T04:01:27.398Z" }, - { url = "https://files.pythonhosted.org/packages/b9/e1/e5df362e9ca4e2f48ed6411bd4b3a0ae737cc842e96877f5bf9428055ab4/lxml-6.0.2-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c33e66d44fe60e72397b487ee92e01da0d09ba2d66df8eae42d77b6d06e5eba0", size = 5654127, upload-time = "2025-09-22T04:01:29.629Z" }, - { url = "https://files.pythonhosted.org/packages/c6/d1/232b3309a02d60f11e71857778bfcd4acbdb86c07db8260caf7d008b08f8/lxml-6.0.2-cp312-cp312-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:90a345bbeaf9d0587a3aaffb7006aa39ccb6ff0e96a57286c0cb2fd1520ea192", size = 5253958, upload-time = "2025-09-22T04:01:31.535Z" }, - { url = "https://files.pythonhosted.org/packages/35/35/d955a070994725c4f7d80583a96cab9c107c57a125b20bb5f708fe941011/lxml-6.0.2-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:064fdadaf7a21af3ed1dcaa106b854077fbeada827c18f72aec9346847cd65d0", size = 4711541, upload-time = "2025-09-22T04:01:33.801Z" }, - { url = "https://files.pythonhosted.org/packages/1e/be/667d17363b38a78c4bd63cfd4b4632029fd68d2c2dc81f25ce9eb5224dd5/lxml-6.0.2-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fbc74f42c3525ac4ffa4b89cbdd00057b6196bcefe8bce794abd42d33a018092", size = 5267426, upload-time = "2025-09-22T04:01:35.639Z" }, - { url = "https://files.pythonhosted.org/packages/ea/47/62c70aa4a1c26569bc958c9ca86af2bb4e1f614e8c04fb2989833874f7ae/lxml-6.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6ddff43f702905a4e32bc24f3f2e2edfe0f8fde3277d481bffb709a4cced7a1f", size = 5064917, upload-time = "2025-09-22T04:01:37.448Z" }, - { url = "https://files.pythonhosted.org/packages/bd/55/6ceddaca353ebd0f1908ef712c597f8570cc9c58130dbb89903198e441fd/lxml-6.0.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:6da5185951d72e6f5352166e3da7b0dc27aa70bd1090b0eb3f7f7212b53f1bb8", size = 4788795, upload-time = "2025-09-22T04:01:39.165Z" }, - { url = "https://files.pythonhosted.org/packages/cf/e8/fd63e15da5e3fd4c2146f8bbb3c14e94ab850589beab88e547b2dbce22e1/lxml-6.0.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:57a86e1ebb4020a38d295c04fc79603c7899e0df71588043eb218722dabc087f", size = 5676759, upload-time = "2025-09-22T04:01:41.506Z" }, - { url = "https://files.pythonhosted.org/packages/76/47/b3ec58dc5c374697f5ba37412cd2728f427d056315d124dd4b61da381877/lxml-6.0.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:2047d8234fe735ab77802ce5f2297e410ff40f5238aec569ad7c8e163d7b19a6", size = 5255666, upload-time = "2025-09-22T04:01:43.363Z" }, - { url = "https://files.pythonhosted.org/packages/19/93/03ba725df4c3d72afd9596eef4a37a837ce8e4806010569bedfcd2cb68fd/lxml-6.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6f91fd2b2ea15a6800c8e24418c0775a1694eefc011392da73bc6cef2623b322", size = 5277989, upload-time = "2025-09-22T04:01:45.215Z" }, - { url = "https://files.pythonhosted.org/packages/c6/80/c06de80bfce881d0ad738576f243911fccf992687ae09fd80b734712b39c/lxml-6.0.2-cp312-cp312-win32.whl", hash = "sha256:3ae2ce7d6fedfb3414a2b6c5e20b249c4c607f72cb8d2bb7cc9c6ec7c6f4e849", size = 3611456, upload-time = "2025-09-22T04:01:48.243Z" }, - { url = "https://files.pythonhosted.org/packages/f7/d7/0cdfb6c3e30893463fb3d1e52bc5f5f99684a03c29a0b6b605cfae879cd5/lxml-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:72c87e5ee4e58a8354fb9c7c84cbf95a1c8236c127a5d1b7683f04bed8361e1f", size = 4011793, upload-time = "2025-09-22T04:01:50.042Z" }, - { url = "https://files.pythonhosted.org/packages/ea/7b/93c73c67db235931527301ed3785f849c78991e2e34f3fd9a6663ffda4c5/lxml-6.0.2-cp312-cp312-win_arm64.whl", hash = "sha256:61cb10eeb95570153e0c0e554f58df92ecf5109f75eacad4a95baa709e26c3d6", size = 3672836, upload-time = "2025-09-22T04:01:52.145Z" }, - { url = "https://files.pythonhosted.org/packages/53/fd/4e8f0540608977aea078bf6d79f128e0e2c2bba8af1acf775c30baa70460/lxml-6.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:9b33d21594afab46f37ae58dfadd06636f154923c4e8a4d754b0127554eb2e77", size = 8648494, upload-time = "2025-09-22T04:01:54.242Z" }, - { url = "https://files.pythonhosted.org/packages/5d/f4/2a94a3d3dfd6c6b433501b8d470a1960a20ecce93245cf2db1706adf6c19/lxml-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6c8963287d7a4c5c9a432ff487c52e9c5618667179c18a204bdedb27310f022f", size = 4661146, upload-time = "2025-09-22T04:01:56.282Z" }, - { url = "https://files.pythonhosted.org/packages/25/2e/4efa677fa6b322013035d38016f6ae859d06cac67437ca7dc708a6af7028/lxml-6.0.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1941354d92699fb5ffe6ed7b32f9649e43c2feb4b97205f75866f7d21aa91452", size = 4946932, upload-time = "2025-09-22T04:01:58.989Z" }, - { url = "https://files.pythonhosted.org/packages/ce/0f/526e78a6d38d109fdbaa5049c62e1d32fdd70c75fb61c4eadf3045d3d124/lxml-6.0.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bb2f6ca0ae2d983ded09357b84af659c954722bbf04dea98030064996d156048", size = 5100060, upload-time = "2025-09-22T04:02:00.812Z" }, - { url = "https://files.pythonhosted.org/packages/81/76/99de58d81fa702cc0ea7edae4f4640416c2062813a00ff24bd70ac1d9c9b/lxml-6.0.2-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb2a12d704f180a902d7fa778c6d71f36ceb7b0d317f34cdc76a5d05aa1dd1df", size = 5019000, upload-time = "2025-09-22T04:02:02.671Z" }, - { url = "https://files.pythonhosted.org/packages/b5/35/9e57d25482bc9a9882cb0037fdb9cc18f4b79d85df94fa9d2a89562f1d25/lxml-6.0.2-cp313-cp313-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:6ec0e3f745021bfed19c456647f0298d60a24c9ff86d9d051f52b509663feeb1", size = 5348496, upload-time = "2025-09-22T04:02:04.904Z" }, - { url = "https://files.pythonhosted.org/packages/a6/8e/cb99bd0b83ccc3e8f0f528e9aa1f7a9965dfec08c617070c5db8d63a87ce/lxml-6.0.2-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:846ae9a12d54e368933b9759052d6206a9e8b250291109c48e350c1f1f49d916", size = 5643779, upload-time = "2025-09-22T04:02:06.689Z" }, - { url = "https://files.pythonhosted.org/packages/d0/34/9e591954939276bb679b73773836c6684c22e56d05980e31d52a9a8deb18/lxml-6.0.2-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ef9266d2aa545d7374938fb5c484531ef5a2ec7f2d573e62f8ce722c735685fd", size = 5244072, upload-time = "2025-09-22T04:02:08.587Z" }, - { url = "https://files.pythonhosted.org/packages/8d/27/b29ff065f9aaca443ee377aff699714fcbffb371b4fce5ac4ca759e436d5/lxml-6.0.2-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:4077b7c79f31755df33b795dc12119cb557a0106bfdab0d2c2d97bd3cf3dffa6", size = 4718675, upload-time = "2025-09-22T04:02:10.783Z" }, - { url = "https://files.pythonhosted.org/packages/2b/9f/f756f9c2cd27caa1a6ef8c32ae47aadea697f5c2c6d07b0dae133c244fbe/lxml-6.0.2-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a7c5d5e5f1081955358533be077166ee97ed2571d6a66bdba6ec2f609a715d1a", size = 5255171, upload-time = "2025-09-22T04:02:12.631Z" }, - { url = "https://files.pythonhosted.org/packages/61/46/bb85ea42d2cb1bd8395484fd72f38e3389611aa496ac7772da9205bbda0e/lxml-6.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:8f8d0cbd0674ee89863a523e6994ac25fd5be9c8486acfc3e5ccea679bad2679", size = 5057175, upload-time = "2025-09-22T04:02:14.718Z" }, - { url = "https://files.pythonhosted.org/packages/95/0c/443fc476dcc8e41577f0af70458c50fe299a97bb6b7505bb1ae09aa7f9ac/lxml-6.0.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:2cbcbf6d6e924c28f04a43f3b6f6e272312a090f269eff68a2982e13e5d57659", size = 4785688, upload-time = "2025-09-22T04:02:16.957Z" }, - { url = "https://files.pythonhosted.org/packages/48/78/6ef0b359d45bb9697bc5a626e1992fa5d27aa3f8004b137b2314793b50a0/lxml-6.0.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:dfb874cfa53340009af6bdd7e54ebc0d21012a60a4e65d927c2e477112e63484", size = 5660655, upload-time = "2025-09-22T04:02:18.815Z" }, - { url = "https://files.pythonhosted.org/packages/ff/ea/e1d33808f386bc1339d08c0dcada6e4712d4ed8e93fcad5f057070b7988a/lxml-6.0.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:fb8dae0b6b8b7f9e96c26fdd8121522ce5de9bb5538010870bd538683d30e9a2", size = 5247695, upload-time = "2025-09-22T04:02:20.593Z" }, - { url = "https://files.pythonhosted.org/packages/4f/47/eba75dfd8183673725255247a603b4ad606f4ae657b60c6c145b381697da/lxml-6.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:358d9adae670b63e95bc59747c72f4dc97c9ec58881d4627fe0120da0f90d314", size = 5269841, upload-time = "2025-09-22T04:02:22.489Z" }, - { url = "https://files.pythonhosted.org/packages/76/04/5c5e2b8577bc936e219becb2e98cdb1aca14a4921a12995b9d0c523502ae/lxml-6.0.2-cp313-cp313-win32.whl", hash = "sha256:e8cd2415f372e7e5a789d743d133ae474290a90b9023197fd78f32e2dc6873e2", size = 3610700, upload-time = "2025-09-22T04:02:24.465Z" }, - { url = "https://files.pythonhosted.org/packages/fe/0a/4643ccc6bb8b143e9f9640aa54e38255f9d3b45feb2cbe7ae2ca47e8782e/lxml-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:b30d46379644fbfc3ab81f8f82ae4de55179414651f110a1514f0b1f8f6cb2d7", size = 4010347, upload-time = "2025-09-22T04:02:26.286Z" }, - { url = "https://files.pythonhosted.org/packages/31/ef/dcf1d29c3f530577f61e5fe2f1bd72929acf779953668a8a47a479ae6f26/lxml-6.0.2-cp313-cp313-win_arm64.whl", hash = "sha256:13dcecc9946dca97b11b7c40d29fba63b55ab4170d3c0cf8c0c164343b9bfdcf", size = 3671248, upload-time = "2025-09-22T04:02:27.918Z" }, - { url = "https://files.pythonhosted.org/packages/03/15/d4a377b385ab693ce97b472fe0c77c2b16ec79590e688b3ccc71fba19884/lxml-6.0.2-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:b0c732aa23de8f8aec23f4b580d1e52905ef468afb4abeafd3fec77042abb6fe", size = 8659801, upload-time = "2025-09-22T04:02:30.113Z" }, - { url = "https://files.pythonhosted.org/packages/c8/e8/c128e37589463668794d503afaeb003987373c5f94d667124ffd8078bbd9/lxml-6.0.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4468e3b83e10e0317a89a33d28f7aeba1caa4d1a6fd457d115dd4ffe90c5931d", size = 4659403, upload-time = "2025-09-22T04:02:32.119Z" }, - { url = "https://files.pythonhosted.org/packages/00/ce/74903904339decdf7da7847bb5741fc98a5451b42fc419a86c0c13d26fe2/lxml-6.0.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:abd44571493973bad4598a3be7e1d807ed45aa2adaf7ab92ab7c62609569b17d", size = 4966974, upload-time = "2025-09-22T04:02:34.155Z" }, - { url = "https://files.pythonhosted.org/packages/1f/d3/131dec79ce61c5567fecf82515bd9bc36395df42501b50f7f7f3bd065df0/lxml-6.0.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:370cd78d5855cfbffd57c422851f7d3864e6ae72d0da615fca4dad8c45d375a5", size = 5102953, upload-time = "2025-09-22T04:02:36.054Z" }, - { url = "https://files.pythonhosted.org/packages/3a/ea/a43ba9bb750d4ffdd885f2cd333572f5bb900cd2408b67fdda07e85978a0/lxml-6.0.2-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:901e3b4219fa04ef766885fb40fa516a71662a4c61b80c94d25336b4934b71c0", size = 5055054, upload-time = "2025-09-22T04:02:38.154Z" }, - { url = "https://files.pythonhosted.org/packages/60/23/6885b451636ae286c34628f70a7ed1fcc759f8d9ad382d132e1c8d3d9bfd/lxml-6.0.2-cp314-cp314-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:a4bf42d2e4cf52c28cc1812d62426b9503cdb0c87a6de81442626aa7d69707ba", size = 5352421, upload-time = "2025-09-22T04:02:40.413Z" }, - { url = "https://files.pythonhosted.org/packages/48/5b/fc2ddfc94ddbe3eebb8e9af6e3fd65e2feba4967f6a4e9683875c394c2d8/lxml-6.0.2-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b2c7fdaa4d7c3d886a42534adec7cfac73860b89b4e5298752f60aa5984641a0", size = 5673684, upload-time = "2025-09-22T04:02:42.288Z" }, - { url = "https://files.pythonhosted.org/packages/29/9c/47293c58cc91769130fbf85531280e8cc7868f7fbb6d92f4670071b9cb3e/lxml-6.0.2-cp314-cp314-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:98a5e1660dc7de2200b00d53fa00bcd3c35a3608c305d45a7bbcaf29fa16e83d", size = 5252463, upload-time = "2025-09-22T04:02:44.165Z" }, - { url = "https://files.pythonhosted.org/packages/9b/da/ba6eceb830c762b48e711ded880d7e3e89fc6c7323e587c36540b6b23c6b/lxml-6.0.2-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:dc051506c30b609238d79eda75ee9cab3e520570ec8219844a72a46020901e37", size = 4698437, upload-time = "2025-09-22T04:02:46.524Z" }, - { url = "https://files.pythonhosted.org/packages/a5/24/7be3f82cb7990b89118d944b619e53c656c97dc89c28cfb143fdb7cd6f4d/lxml-6.0.2-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8799481bbdd212470d17513a54d568f44416db01250f49449647b5ab5b5dccb9", size = 5269890, upload-time = "2025-09-22T04:02:48.812Z" }, - { url = "https://files.pythonhosted.org/packages/1b/bd/dcfb9ea1e16c665efd7538fc5d5c34071276ce9220e234217682e7d2c4a5/lxml-6.0.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9261bb77c2dab42f3ecd9103951aeca2c40277701eb7e912c545c1b16e0e4917", size = 5097185, upload-time = "2025-09-22T04:02:50.746Z" }, - { url = "https://files.pythonhosted.org/packages/21/04/a60b0ff9314736316f28316b694bccbbabe100f8483ad83852d77fc7468e/lxml-6.0.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:65ac4a01aba353cfa6d5725b95d7aed6356ddc0a3cd734de00124d285b04b64f", size = 4745895, upload-time = "2025-09-22T04:02:52.968Z" }, - { url = "https://files.pythonhosted.org/packages/d6/bd/7d54bd1846e5a310d9c715921c5faa71cf5c0853372adf78aee70c8d7aa2/lxml-6.0.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:b22a07cbb82fea98f8a2fd814f3d1811ff9ed76d0fc6abc84eb21527596e7cc8", size = 5695246, upload-time = "2025-09-22T04:02:54.798Z" }, - { url = "https://files.pythonhosted.org/packages/fd/32/5643d6ab947bc371da21323acb2a6e603cedbe71cb4c99c8254289ab6f4e/lxml-6.0.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:d759cdd7f3e055d6bc8d9bec3ad905227b2e4c785dc16c372eb5b5e83123f48a", size = 5260797, upload-time = "2025-09-22T04:02:57.058Z" }, - { url = "https://files.pythonhosted.org/packages/33/da/34c1ec4cff1eea7d0b4cd44af8411806ed943141804ac9c5d565302afb78/lxml-6.0.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:945da35a48d193d27c188037a05fec5492937f66fb1958c24fc761fb9d40d43c", size = 5277404, upload-time = "2025-09-22T04:02:58.966Z" }, - { url = "https://files.pythonhosted.org/packages/82/57/4eca3e31e54dc89e2c3507e1cd411074a17565fa5ffc437c4ae0a00d439e/lxml-6.0.2-cp314-cp314-win32.whl", hash = "sha256:be3aaa60da67e6153eb15715cc2e19091af5dc75faef8b8a585aea372507384b", size = 3670072, upload-time = "2025-09-22T04:03:38.05Z" }, - { url = "https://files.pythonhosted.org/packages/e3/e0/c96cf13eccd20c9421ba910304dae0f619724dcf1702864fd59dd386404d/lxml-6.0.2-cp314-cp314-win_amd64.whl", hash = "sha256:fa25afbadead523f7001caf0c2382afd272c315a033a7b06336da2637d92d6ed", size = 4080617, upload-time = "2025-09-22T04:03:39.835Z" }, - { url = "https://files.pythonhosted.org/packages/d5/5d/b3f03e22b3d38d6f188ef044900a9b29b2fe0aebb94625ce9fe244011d34/lxml-6.0.2-cp314-cp314-win_arm64.whl", hash = "sha256:063eccf89df5b24e361b123e257e437f9e9878f425ee9aae3144c77faf6da6d8", size = 3754930, upload-time = "2025-09-22T04:03:41.565Z" }, - { url = "https://files.pythonhosted.org/packages/5e/5c/42c2c4c03554580708fc738d13414801f340c04c3eff90d8d2d227145275/lxml-6.0.2-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:6162a86d86893d63084faaf4ff937b3daea233e3682fb4474db07395794fa80d", size = 8910380, upload-time = "2025-09-22T04:03:01.645Z" }, - { url = "https://files.pythonhosted.org/packages/bf/4f/12df843e3e10d18d468a7557058f8d3733e8b6e12401f30b1ef29360740f/lxml-6.0.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:414aaa94e974e23a3e92e7ca5b97d10c0cf37b6481f50911032c69eeb3991bba", size = 4775632, upload-time = "2025-09-22T04:03:03.814Z" }, - { url = "https://files.pythonhosted.org/packages/e4/0c/9dc31e6c2d0d418483cbcb469d1f5a582a1cd00a1f4081953d44051f3c50/lxml-6.0.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:48461bd21625458dd01e14e2c38dd0aea69addc3c4f960c30d9f59d7f93be601", size = 4975171, upload-time = "2025-09-22T04:03:05.651Z" }, - { url = "https://files.pythonhosted.org/packages/e7/2b/9b870c6ca24c841bdd887504808f0417aa9d8d564114689266f19ddf29c8/lxml-6.0.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:25fcc59afc57d527cfc78a58f40ab4c9b8fd096a9a3f964d2781ffb6eb33f4ed", size = 5110109, upload-time = "2025-09-22T04:03:07.452Z" }, - { url = "https://files.pythonhosted.org/packages/bf/0c/4f5f2a4dd319a178912751564471355d9019e220c20d7db3fb8307ed8582/lxml-6.0.2-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5179c60288204e6ddde3f774a93350177e08876eaf3ab78aa3a3649d43eb7d37", size = 5041061, upload-time = "2025-09-22T04:03:09.297Z" }, - { url = "https://files.pythonhosted.org/packages/12/64/554eed290365267671fe001a20d72d14f468ae4e6acef1e179b039436967/lxml-6.0.2-cp314-cp314t-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:967aab75434de148ec80597b75062d8123cadf2943fb4281f385141e18b21338", size = 5306233, upload-time = "2025-09-22T04:03:11.651Z" }, - { url = "https://files.pythonhosted.org/packages/7a/31/1d748aa275e71802ad9722df32a7a35034246b42c0ecdd8235412c3396ef/lxml-6.0.2-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d100fcc8930d697c6561156c6810ab4a508fb264c8b6779e6e61e2ed5e7558f9", size = 5604739, upload-time = "2025-09-22T04:03:13.592Z" }, - { url = "https://files.pythonhosted.org/packages/8f/41/2c11916bcac09ed561adccacceaedd2bf0e0b25b297ea92aab99fd03d0fa/lxml-6.0.2-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2ca59e7e13e5981175b8b3e4ab84d7da57993eeff53c07764dcebda0d0e64ecd", size = 5225119, upload-time = "2025-09-22T04:03:15.408Z" }, - { url = "https://files.pythonhosted.org/packages/99/05/4e5c2873d8f17aa018e6afde417c80cc5d0c33be4854cce3ef5670c49367/lxml-6.0.2-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:957448ac63a42e2e49531b9d6c0fa449a1970dbc32467aaad46f11545be9af1d", size = 4633665, upload-time = "2025-09-22T04:03:17.262Z" }, - { url = "https://files.pythonhosted.org/packages/0f/c9/dcc2da1bebd6275cdc723b515f93edf548b82f36a5458cca3578bc899332/lxml-6.0.2-cp314-cp314t-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b7fc49c37f1786284b12af63152fe1d0990722497e2d5817acfe7a877522f9a9", size = 5234997, upload-time = "2025-09-22T04:03:19.14Z" }, - { url = "https://files.pythonhosted.org/packages/9c/e2/5172e4e7468afca64a37b81dba152fc5d90e30f9c83c7c3213d6a02a5ce4/lxml-6.0.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e19e0643cc936a22e837f79d01a550678da8377d7d801a14487c10c34ee49c7e", size = 5090957, upload-time = "2025-09-22T04:03:21.436Z" }, - { url = "https://files.pythonhosted.org/packages/a5/b3/15461fd3e5cd4ddcb7938b87fc20b14ab113b92312fc97afe65cd7c85de1/lxml-6.0.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:1db01e5cf14345628e0cbe71067204db658e2fb8e51e7f33631f5f4735fefd8d", size = 4764372, upload-time = "2025-09-22T04:03:23.27Z" }, - { url = "https://files.pythonhosted.org/packages/05/33/f310b987c8bf9e61c4dd8e8035c416bd3230098f5e3cfa69fc4232de7059/lxml-6.0.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:875c6b5ab39ad5291588aed6925fac99d0097af0dd62f33c7b43736043d4a2ec", size = 5634653, upload-time = "2025-09-22T04:03:25.767Z" }, - { url = "https://files.pythonhosted.org/packages/70/ff/51c80e75e0bc9382158133bdcf4e339b5886c6ee2418b5199b3f1a61ed6d/lxml-6.0.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:cdcbed9ad19da81c480dfd6dd161886db6096083c9938ead313d94b30aadf272", size = 5233795, upload-time = "2025-09-22T04:03:27.62Z" }, - { url = "https://files.pythonhosted.org/packages/56/4d/4856e897df0d588789dd844dbed9d91782c4ef0b327f96ce53c807e13128/lxml-6.0.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:80dadc234ebc532e09be1975ff538d154a7fa61ea5031c03d25178855544728f", size = 5257023, upload-time = "2025-09-22T04:03:30.056Z" }, - { url = "https://files.pythonhosted.org/packages/0f/85/86766dfebfa87bea0ab78e9ff7a4b4b45225df4b4d3b8cc3c03c5cd68464/lxml-6.0.2-cp314-cp314t-win32.whl", hash = "sha256:da08e7bb297b04e893d91087df19638dc7a6bb858a954b0cc2b9f5053c922312", size = 3911420, upload-time = "2025-09-22T04:03:32.198Z" }, - { url = "https://files.pythonhosted.org/packages/fe/1a/b248b355834c8e32614650b8008c69ffeb0ceb149c793961dd8c0b991bb3/lxml-6.0.2-cp314-cp314t-win_amd64.whl", hash = "sha256:252a22982dca42f6155125ac76d3432e548a7625d56f5a273ee78a5057216eca", size = 4406837, upload-time = "2025-09-22T04:03:34.027Z" }, - { url = "https://files.pythonhosted.org/packages/92/aa/df863bcc39c5e0946263454aba394de8a9084dbaff8ad143846b0d844739/lxml-6.0.2-cp314-cp314t-win_arm64.whl", hash = "sha256:bb4c1847b303835d89d785a18801a883436cdfd5dc3d62947f9c49e24f0f5a2c", size = 3822205, upload-time = "2025-09-22T04:03:36.249Z" }, - { url = "https://files.pythonhosted.org/packages/e7/9c/780c9a8fce3f04690b374f72f41306866b0400b9d0fdf3e17aaa37887eed/lxml-6.0.2-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:e748d4cf8fef2526bb2a589a417eba0c8674e29ffcb570ce2ceca44f1e567bf6", size = 3939264, upload-time = "2025-09-22T04:04:32.892Z" }, - { url = "https://files.pythonhosted.org/packages/f5/5a/1ab260c00adf645d8bf7dec7f920f744b032f69130c681302821d5debea6/lxml-6.0.2-pp310-pypy310_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4ddb1049fa0579d0cbd00503ad8c58b9ab34d1254c77bc6a5576d96ec7853dba", size = 4216435, upload-time = "2025-09-22T04:04:34.907Z" }, - { url = "https://files.pythonhosted.org/packages/f2/37/565f3b3d7ffede22874b6d86be1a1763d00f4ea9fc5b9b6ccb11e4ec8612/lxml-6.0.2-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cb233f9c95f83707dae461b12b720c1af9c28c2d19208e1be03387222151daf5", size = 4325913, upload-time = "2025-09-22T04:04:37.205Z" }, - { url = "https://files.pythonhosted.org/packages/22/ec/f3a1b169b2fb9d03467e2e3c0c752ea30e993be440a068b125fc7dd248b0/lxml-6.0.2-pp310-pypy310_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bc456d04db0515ce3320d714a1eac7a97774ff0849e7718b492d957da4631dd4", size = 4269357, upload-time = "2025-09-22T04:04:39.322Z" }, - { url = "https://files.pythonhosted.org/packages/77/a2/585a28fe3e67daa1cf2f06f34490d556d121c25d500b10082a7db96e3bcd/lxml-6.0.2-pp310-pypy310_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2613e67de13d619fd283d58bda40bff0ee07739f624ffee8b13b631abf33083d", size = 4412295, upload-time = "2025-09-22T04:04:41.647Z" }, - { url = "https://files.pythonhosted.org/packages/7b/d9/a57dd8bcebd7c69386c20263830d4fa72d27e6b72a229ef7a48e88952d9a/lxml-6.0.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:24a8e756c982c001ca8d59e87c80c4d9dcd4d9b44a4cbeb8d9be4482c514d41d", size = 3516913, upload-time = "2025-09-22T04:04:43.602Z" }, - { url = "https://files.pythonhosted.org/packages/0b/11/29d08bc103a62c0eba8016e7ed5aeebbf1e4312e83b0b1648dd203b0e87d/lxml-6.0.2-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:1c06035eafa8404b5cf475bb37a9f6088b0aca288d4ccc9d69389750d5543700", size = 3949829, upload-time = "2025-09-22T04:04:45.608Z" }, - { url = "https://files.pythonhosted.org/packages/12/b3/52ab9a3b31e5ab8238da241baa19eec44d2ab426532441ee607165aebb52/lxml-6.0.2-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c7d13103045de1bdd6fe5d61802565f1a3537d70cd3abf596aa0af62761921ee", size = 4226277, upload-time = "2025-09-22T04:04:47.754Z" }, - { url = "https://files.pythonhosted.org/packages/a0/33/1eaf780c1baad88224611df13b1c2a9dfa460b526cacfe769103ff50d845/lxml-6.0.2-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0a3c150a95fbe5ac91de323aa756219ef9cf7fde5a3f00e2281e30f33fa5fa4f", size = 4330433, upload-time = "2025-09-22T04:04:49.907Z" }, - { url = "https://files.pythonhosted.org/packages/7a/c1/27428a2ff348e994ab4f8777d3a0ad510b6b92d37718e5887d2da99952a2/lxml-6.0.2-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:60fa43be34f78bebb27812ed90f1925ec99560b0fa1decdb7d12b84d857d31e9", size = 4272119, upload-time = "2025-09-22T04:04:51.801Z" }, - { url = "https://files.pythonhosted.org/packages/f0/d0/3020fa12bcec4ab62f97aab026d57c2f0cfd480a558758d9ca233bb6a79d/lxml-6.0.2-pp311-pypy311_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:21c73b476d3cfe836be731225ec3421fa2f048d84f6df6a8e70433dff1376d5a", size = 4417314, upload-time = "2025-09-22T04:04:55.024Z" }, - { url = "https://files.pythonhosted.org/packages/6c/77/d7f491cbc05303ac6801651aabeb262d43f319288c1ea96c66b1d2692ff3/lxml-6.0.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:27220da5be049e936c3aca06f174e8827ca6445a4353a1995584311487fc4e3e", size = 3518768, upload-time = "2025-09-22T04:04:57.097Z" }, +version = "6.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/28/30/9abc9e34c657c33834eaf6cd02124c61bdf5944d802aa48e69be8da3585d/lxml-6.1.0.tar.gz", hash = "sha256:bfd57d8008c4965709a919c3e9a98f76c2c7cb319086b3d26858250620023b13", size = 4197006, upload-time = "2026-04-18T04:32:51.613Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/6e/ee8fc0e01202eb3dd2b9e1ea4f0910d72425d35c66187c63931d7a3ea73f/lxml-6.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:41dcc4c7b10484257cbd6c37b83ddb26df2b0e5aff5ac00d095689015af868ec", size = 8540733, upload-time = "2026-04-18T04:27:33.185Z" }, + { url = "https://files.pythonhosted.org/packages/54/e8/325fe9b942824c773dffe1baf0c35b046a763851fdff4393af4450bceeb7/lxml-6.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a31286dbb5e74c8e9a5344465b77ab4c5bd511a253b355b5ca2fae7e579fafec", size = 4602805, upload-time = "2026-04-18T04:27:36.097Z" }, + { url = "https://files.pythonhosted.org/packages/2d/81/221aa3ea4a40370bb0358fa454cbe7e5a837e522f7630c24dfef3f9a73b0/lxml-6.1.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1bc4cc83fb7f66ffb16f74d6dd0162e144333fc36ebcce32246f80c8735b2551", size = 5002652, upload-time = "2026-04-18T04:27:30.603Z" }, + { url = "https://files.pythonhosted.org/packages/c6/e1/fdbfb9019542f1875c093576df7f37adc2983c8ba7ecf17e5f14490bc107/lxml-6.1.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:20cf4d0651987c906a2f5cba4e3a8d6ba4bfdf973cfe2a96c0d6053888ea2ecd", size = 5155332, upload-time = "2026-04-18T04:27:33.507Z" }, + { url = "https://files.pythonhosted.org/packages/56/b1/4087c782fff397cd03abf9c551069be59bb04a7e548c50fb7b9c4cdaca28/lxml-6.1.0-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ffb34ea45a82dd637c2c97ae1bbb920850c1e59bcae79ce1c15af531d83e7215", size = 5057226, upload-time = "2026-04-18T04:27:37.567Z" }, + { url = "https://files.pythonhosted.org/packages/5d/66/516c79dec8417f3a972327330254c0b5fac93d5c3ecfd8a5b43650a5a4d9/lxml-6.1.0-cp310-cp310-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a1d9b99e5b2597e4f5aed2484fef835256fa1b68a19e4265c97628ef4bf8bcf4", size = 5287588, upload-time = "2026-04-18T04:27:41.4Z" }, + { url = "https://files.pythonhosted.org/packages/94/1d/e578f4cbeb42b9df9f29b0d44a45a7cdfa3a5ae300dd59ec68e3602d29bb/lxml-6.1.0-cp310-cp310-manylinux_2_28_i686.whl", hash = "sha256:d43aa26dcda363f21e79afa0668f5029ed7394b3bb8c92a6927a3d34e8b610ea", size = 5412438, upload-time = "2026-04-18T04:27:45.589Z" }, + { url = "https://files.pythonhosted.org/packages/47/5b/2aa68307d6d15959e84d4882f9c04f2da63127eac463e1594166f681ef77/lxml-6.1.0-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:6262b87f9e5c1e5fe501d6c153247289af42eb44ad7660b9b3de17baaf92d6f6", size = 4770997, upload-time = "2026-04-18T04:27:49.853Z" }, + { url = "https://files.pythonhosted.org/packages/ae/c9/3e51fc1228310a836b4eb32595ae00154ab12197fca944676a3ab3b163ea/lxml-6.1.0-cp310-cp310-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d1392c569c032f78a11a25d1de1c43fff13294c793b39e19d84fade3045cbbc3", size = 5359678, upload-time = "2026-04-18T04:31:56.184Z" }, + { url = "https://files.pythonhosted.org/packages/b5/91/ab8bc834f977fbbd310e697b120787c153db026f9151e02a88d2645d4e5b/lxml-6.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:045e387d1f4f42a418380930fa3f45c73c9b392faf67e495e58902e68e8f44a7", size = 5107890, upload-time = "2026-04-18T04:32:00.387Z" }, + { url = "https://files.pythonhosted.org/packages/bb/10/8a143cfa3ac99cb5b0523ff6d0429a9c9dddf25ffeae09caa3866c7964d9/lxml-6.1.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:9f93d5b8b07f73e8c77e3c6556a3db269918390c804b5e5fcdd4858232cc8f16", size = 4803977, upload-time = "2026-04-18T04:32:05.099Z" }, + { url = "https://files.pythonhosted.org/packages/45/fd/ee02faf52fa39c2fe32f824628958b9aa86dff21343dc3161f0e3c6ccd15/lxml-6.1.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:de550d129f18d8ab819651ffe4f38b1b713c7e116707de3c0c6400d0ef34fbc1", size = 5350277, upload-time = "2026-04-18T04:32:09.176Z" }, + { url = "https://files.pythonhosted.org/packages/85/8c/b3481364b8554b5d36d540189a87fc71e94b0b01c24f8f152bd662dd2e45/lxml-6.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c08da09dc003c9e8c70e06b53a11db6fb3b250c21c4236b03c7d7b443c318e7a", size = 5309717, upload-time = "2026-04-18T04:32:13.303Z" }, + { url = "https://files.pythonhosted.org/packages/74/e8/a6b21927077a9127afa17473b6576b322616f34ac50ee4f577e763b75ec0/lxml-6.1.0-cp310-cp310-win32.whl", hash = "sha256:37448bf9c7d7adfc5254763901e2bbd6bb876228dfc1fc7f66e58c06368a7544", size = 3598491, upload-time = "2026-04-18T04:27:24.288Z" }, + { url = "https://files.pythonhosted.org/packages/ea/82/14dea800d041274d96c07d49ff9191f011d1427450850de19bf541e2cc12/lxml-6.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:2593a0a6621545b9095b71ad74ed4226eba438a7d9fc3712a99bdb15508cf93a", size = 4020906, upload-time = "2026-04-18T04:27:27.53Z" }, + { url = "https://files.pythonhosted.org/packages/f2/ba/d3539aaf4d9d21456b9a7b902816623227d05d63e7c5aafd8834c4b9bed6/lxml-6.1.0-cp310-cp310-win_arm64.whl", hash = "sha256:e80807d72f96b96ad5588cb85c75616e4f2795a7737d4630784c51497beb7776", size = 3667787, upload-time = "2026-04-18T04:27:29.407Z" }, + { url = "https://files.pythonhosted.org/packages/5e/5d/3bccad330292946f97962df9d5f2d3ae129cce6e212732a781e856b91e07/lxml-6.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:cec05be8c876f92a5aa07b01d60bbb4d11cfbdd654cad0561c0d7b5c043a61b9", size = 8526232, upload-time = "2026-04-18T04:27:40.389Z" }, + { url = "https://files.pythonhosted.org/packages/a7/51/adc8826570a112f83bb4ddb3a2ab510bbc2ccd62c1b9fe1f34fae2d90b57/lxml-6.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9c03e048b6ce8e77b09c734e931584894ecd58d08296804ca2d0b184c933ce50", size = 4595448, upload-time = "2026-04-18T04:27:44.208Z" }, + { url = "https://files.pythonhosted.org/packages/54/84/5a9ec07cbe1d2334a6465f863b949a520d2699a755738986dcd3b6b89e3f/lxml-6.1.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:942454ff253da14218f972b23dc72fa4edf6c943f37edd19cd697618b626fac5", size = 4923771, upload-time = "2026-04-18T04:32:17.402Z" }, + { url = "https://files.pythonhosted.org/packages/a7/23/851cfa33b6b38adb628e45ad51fb27105fa34b2b3ba9d1d4aa7a9428dfe0/lxml-6.1.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d036ee7b99d5148072ac7c9b847193decdfeac633db350363f7bce4fff108f0e", size = 5068101, upload-time = "2026-04-18T04:32:21.437Z" }, + { url = "https://files.pythonhosted.org/packages/b0/38/41bf99c2023c6b79916ba057d83e9db21d642f473cac210201222882d38b/lxml-6.1.0-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3ae5d8d5427f3cc317e7950f2da7ad276df0cfa37b8de2f5658959e618ea8512", size = 5002573, upload-time = "2026-04-18T04:32:25.373Z" }, + { url = "https://files.pythonhosted.org/packages/c2/20/053aa10bdc39747e1e923ce2d45413075e84f70a136045bb09e5eaca41d3/lxml-6.1.0-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:363e47283bde87051b821826e71dde47f107e08614e1aa312ba0c5711e77738c", size = 5202816, upload-time = "2026-04-18T04:32:29.393Z" }, + { url = "https://files.pythonhosted.org/packages/9a/da/bc710fad8bf04b93baee752c192eaa2210cd3a84f969d0be7830fea55802/lxml-6.1.0-cp311-cp311-manylinux_2_28_i686.whl", hash = "sha256:f504d861d9f2a8f94020130adac88d66de93841707a23a86244263d1e54682f5", size = 5329999, upload-time = "2026-04-18T04:32:34.019Z" }, + { url = "https://files.pythonhosted.org/packages/b3/cb/bf035dedbdf7fab49411aa52e4236f3445e98d38647d85419e6c0d2806b9/lxml-6.1.0-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:23a5dc68e08ed13331d61815c08f260f46b4a60fdd1640bbeb82cf89a9d90289", size = 4659643, upload-time = "2026-04-18T04:32:37.932Z" }, + { url = "https://files.pythonhosted.org/packages/5c/4f/22be31f33727a5e4c7b01b0a874503026e50329b259d3587e0b923cf964b/lxml-6.1.0-cp311-cp311-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f15401d8d3dbf239e23c818afc10c7207f7b95f9a307e092122b6f86dd43209a", size = 5265963, upload-time = "2026-04-18T04:32:41.881Z" }, + { url = "https://files.pythonhosted.org/packages/c8/2b/d44d0e5c79226017f4ab8c87a802ebe4f89f97e6585a8e4166dffcdd7b6e/lxml-6.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:fcf3da95e93349e0647d48d4b36a12783105bcc74cb0c416952f9988410846a3", size = 5045444, upload-time = "2026-04-18T04:32:44.512Z" }, + { url = "https://files.pythonhosted.org/packages/d3/c3/3f034fec1594c331a6dbf9491238fdcc9d66f68cc529e109ec75b97197e1/lxml-6.1.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:0d082495c5fcf426e425a6e28daaba1fcb6d8f854a4ff01effb1f1f381203eb9", size = 4712703, upload-time = "2026-04-18T04:32:47.16Z" }, + { url = "https://files.pythonhosted.org/packages/12/16/0b83fccc158218aca75a7aa33e97441df737950734246b9fffa39301603d/lxml-6.1.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:e3c4f84b24a1fcba435157d111c4b755099c6ff00a3daee1ad281817de75ed11", size = 5252745, upload-time = "2026-04-18T04:32:50.427Z" }, + { url = "https://files.pythonhosted.org/packages/dd/ee/12e6c1b39a77666c02eaa77f94a870aaf63c4ac3a497b2d52319448b01c6/lxml-6.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:976a6b39b1b13e8c354ad8d3f261f3a4ac6609518af91bdb5094760a08f132c4", size = 5226822, upload-time = "2026-04-18T04:32:53.437Z" }, + { url = "https://files.pythonhosted.org/packages/34/20/c7852904858b4723af01d2fc14b5d38ff57cb92f01934a127ebd9a9e51aa/lxml-6.1.0-cp311-cp311-win32.whl", hash = "sha256:857efde87d365706590847b916baff69c0bc9252dc5af030e378c9800c0b10e3", size = 3594026, upload-time = "2026-04-18T04:27:31.903Z" }, + { url = "https://files.pythonhosted.org/packages/02/05/d60c732b56da5085175c07c74b2df4e6d181b0c9a61e1691474f06ef4b39/lxml-6.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:183bfb45a493081943be7ea2b5adfc2b611e1cf377cefa8b8a8be404f45ef9a7", size = 4025114, upload-time = "2026-04-18T04:27:34.077Z" }, + { url = "https://files.pythonhosted.org/packages/c2/df/c84dcc175fd690823436d15b41cb920cd5ba5e14cd8bfb00949d5903b320/lxml-6.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:19f4164243fc206d12ed3d866e80e74f5bc3627966520da1a5f97e42c32a3f39", size = 3667742, upload-time = "2026-04-18T04:27:38.45Z" }, + { url = "https://files.pythonhosted.org/packages/d2/d4/9326838b59dc36dfae42eec9656b97520f9997eee1de47b8316aaeed169c/lxml-6.1.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d2f17a16cd8751e8eb233a7e41aecdf8e511712e00088bf9be455f604cd0d28d", size = 8570663, upload-time = "2026-04-18T04:27:48.253Z" }, + { url = "https://files.pythonhosted.org/packages/d8/a4/053745ce1f8303ccbb788b86c0db3a91b973675cefc42566a188637b7c40/lxml-6.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f0cea5b1d3e6e77d71bd2b9972eb2446221a69dc52bb0b9c3c6f6e5700592d93", size = 4624024, upload-time = "2026-04-18T04:27:52.594Z" }, + { url = "https://files.pythonhosted.org/packages/90/97/a517944b20f8fd0932ad2109482bee4e29fe721416387a363306667941f6/lxml-6.1.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fc46da94826188ed45cb53bd8e3fc076ae22675aea2087843d4735627f867c6d", size = 4930895, upload-time = "2026-04-18T04:32:56.29Z" }, + { url = "https://files.pythonhosted.org/packages/94/7c/e08a970727d556caa040a44773c7b7e3ad0f0d73dedc863543e9a8b931f2/lxml-6.1.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9147d8e386ec3b82c3b15d88927f734f565b0aaadef7def562b853adca45784a", size = 5093820, upload-time = "2026-04-18T04:32:58.94Z" }, + { url = "https://files.pythonhosted.org/packages/88/ee/2a5c2aa2c32016a226ca25d3e1056a8102ea6e1fe308bf50213586635400/lxml-6.1.0-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5715e0e28736a070f3f34a7ccc09e2fdcba0e3060abbcf61a1a5718ff6d6b105", size = 5005790, upload-time = "2026-04-18T04:33:01.272Z" }, + { url = "https://files.pythonhosted.org/packages/e3/38/a0db9be8f38ad6043ab9429487c128dd1d30f07956ef43040402f8da49e8/lxml-6.1.0-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4937460dc5df0cdd2f06a86c285c28afda06aefa3af949f9477d3e8df430c485", size = 5630827, upload-time = "2026-04-18T04:33:04.036Z" }, + { url = "https://files.pythonhosted.org/packages/31/ba/3c13d3fc24b7cacf675f808a3a1baabf43a30d0cd24c98f94548e9aa58eb/lxml-6.1.0-cp312-cp312-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bc783ee3147e60a25aa0445ea82b3e8aabb83b240f2b95d32cb75587ff781814", size = 5240445, upload-time = "2026-04-18T04:33:06.87Z" }, + { url = "https://files.pythonhosted.org/packages/55/ba/eeef4ccba09b2212fe239f46c1692a98db1878e0872ae320756488878a94/lxml-6.1.0-cp312-cp312-manylinux_2_28_i686.whl", hash = "sha256:40d9189f80075f2e1f88db21ef815a2b17b28adf8e50aaf5c789bfe737027f32", size = 5350121, upload-time = "2026-04-18T04:33:09.365Z" }, + { url = "https://files.pythonhosted.org/packages/7e/01/1da87c7b587c38d0cbe77a01aae3b9c1c49ed47d76918ef3db8fc151b1ca/lxml-6.1.0-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:05b9b8787e35bec69e68daf4952b2e6dfcfb0db7ecf1a06f8cdfbbac4eb71aad", size = 4694949, upload-time = "2026-04-18T04:33:11.628Z" }, + { url = "https://files.pythonhosted.org/packages/a1/88/7db0fe66d5aaf128443ee1623dec3db1576f3e4c17751ec0ef5866468590/lxml-6.1.0-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0f0f08beb0182e3e9a86fae124b3c47a7b41b7b69b225e1377db983802404e54", size = 5243901, upload-time = "2026-04-18T04:33:13.95Z" }, + { url = "https://files.pythonhosted.org/packages/00/a8/1346726af7d1f6fca1f11223ba34001462b0a3660416986d37641708d57c/lxml-6.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73becf6d8c81d4c76b1014dbd3584cb26d904492dcf73ca85dc8bff08dcd6d2d", size = 5048054, upload-time = "2026-04-18T04:33:16.965Z" }, + { url = "https://files.pythonhosted.org/packages/2e/b7/85057012f035d1a0c87e02f8c723ca3c3e6e0728bcf4cb62080b21b1c1e3/lxml-6.1.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:1ae225f66e5938f4fa29d37e009a3bb3b13032ac57eb4eb42afa44f6e4054e69", size = 4777324, upload-time = "2026-04-18T04:33:19.832Z" }, + { url = "https://files.pythonhosted.org/packages/75/6c/ad2f94a91073ef570f33718040e8e160d5fb93331cf1ab3ca1323f939e2d/lxml-6.1.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:690022c7fae793b0489aa68a658822cea83e0d5933781811cabbf5ea3bcfe73d", size = 5645702, upload-time = "2026-04-18T04:33:22.436Z" }, + { url = "https://files.pythonhosted.org/packages/3b/89/0bb6c0bd549c19004c60eea9dc554dd78fd647b72314ef25d460e0d208c6/lxml-6.1.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:63aeafc26aac0be8aff14af7871249e87ea1319be92090bfd632ec68e03b16a5", size = 5232901, upload-time = "2026-04-18T04:33:26.21Z" }, + { url = "https://files.pythonhosted.org/packages/a1/d9/d609a11fb567da9399f525193e2b49847b5a409cdebe737f06a8b7126bdc/lxml-6.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:264c605ab9c0e4aa1a679636f4582c4d3313700009fac3ec9c3412ed0d8f3e1d", size = 5261333, upload-time = "2026-04-18T04:33:28.984Z" }, + { url = "https://files.pythonhosted.org/packages/a6/3a/ac3f99ec8ac93089e7dd556f279e0d14c24de0a74a507e143a2e4b496e7c/lxml-6.1.0-cp312-cp312-win32.whl", hash = "sha256:56971379bc5ee8037c5a0f09fa88f66cdb7d37c3e38af3e45cf539f41131ac1f", size = 3596289, upload-time = "2026-04-18T04:27:42.819Z" }, + { url = "https://files.pythonhosted.org/packages/f2/a7/0a915557538593cb1bbeedcd40e13c7a261822c26fecbbdb71dad0c2f540/lxml-6.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:bba078de0031c219e5dd06cf3e6bf8fb8e6e64a77819b358f53bb132e3e03366", size = 3997059, upload-time = "2026-04-18T04:27:46.764Z" }, + { url = "https://files.pythonhosted.org/packages/92/96/a5dc078cf0126fbfbc35611d77ecd5da80054b5893e28fb213a5613b9e1d/lxml-6.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:c3592631e652afa34999a088f98ba7dfc7d6aff0d535c410bea77a71743f3819", size = 3659552, upload-time = "2026-04-18T04:27:51.133Z" }, + { url = "https://files.pythonhosted.org/packages/08/03/69347590f1cf4a6d5a4944bb6099e6d37f334784f16062234e1f892fdb1d/lxml-6.1.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a0092f2b107b69601adf562a57c956fbb596e05e3e6651cabd3054113b007e45", size = 8559689, upload-time = "2026-04-18T04:31:57.785Z" }, + { url = "https://files.pythonhosted.org/packages/3f/58/25e00bb40b185c974cfe156c110474d9a8a8390d5f7c92a4e328189bb60e/lxml-6.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:fc7140d7a7386e6b545d41b7358f4d02b656d4053f5fa6859f92f4b9c2572c4d", size = 4617892, upload-time = "2026-04-18T04:32:01.78Z" }, + { url = "https://files.pythonhosted.org/packages/f5/54/92ad98a94ac318dc4f97aaac22ff8d1b94212b2ae8af5b6e9b354bf825f7/lxml-6.1.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:419c58fc92cc3a2c3fa5f78c63dbf5da70c1fa9c1b25f25727ecee89a96c7de2", size = 4923489, upload-time = "2026-04-18T04:33:31.401Z" }, + { url = "https://files.pythonhosted.org/packages/15/3b/a20aecfab42bdf4f9b390590d345857ad3ffd7c51988d1c89c53a0c73faf/lxml-6.1.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:37fabd1452852636cf38ecdcc9dd5ca4bba7a35d6c53fa09725deeb894a87491", size = 5082162, upload-time = "2026-04-18T04:33:34.262Z" }, + { url = "https://files.pythonhosted.org/packages/45/26/2cdb3d281ac1bd175603e290cbe4bad6eff127c0f8de90bafd6f8548f0fd/lxml-6.1.0-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a2853c8b2170cc6cd54a6b4d50d2c1a8a7aeca201f23804b4898525c7a152cfc", size = 4993247, upload-time = "2026-04-18T04:33:36.674Z" }, + { url = "https://files.pythonhosted.org/packages/f6/05/d735aef963740022a08185c84821f689fc903acb3d50326e6b1e9886cc22/lxml-6.1.0-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8e369cbd690e788c8d15e56222d91a09c6a417f49cbc543040cba0fe2e25a79e", size = 5613042, upload-time = "2026-04-18T04:33:39.205Z" }, + { url = "https://files.pythonhosted.org/packages/ee/b8/ead7c10efff731738c72e59ed6eb5791854879fbed7ae98781a12006263a/lxml-6.1.0-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e69aa6805905807186eb00e66c6d97a935c928275182eb02ee40ba00da9623b2", size = 5228304, upload-time = "2026-04-18T04:33:41.647Z" }, + { url = "https://files.pythonhosted.org/packages/6b/10/e9842d2ec322ea65f0a7270aa0315a53abed06058b88ef1b027f620e7a5f/lxml-6.1.0-cp313-cp313-manylinux_2_28_i686.whl", hash = "sha256:4bd1bdb8a9e0e2dd229de19b5f8aebac80e916921b4b2c6ef8a52bc131d0c1f9", size = 5341578, upload-time = "2026-04-18T04:33:44.596Z" }, + { url = "https://files.pythonhosted.org/packages/89/54/40d9403d7c2775fa7301d3ddd3464689bfe9ba71acc17dfff777071b4fdc/lxml-6.1.0-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:cbd7b79cdcb4986ad78a2662625882747f09db5e4cd7b2ae178a88c9c51b3dfe", size = 4700209, upload-time = "2026-04-18T04:33:47.552Z" }, + { url = "https://files.pythonhosted.org/packages/85/b2/bbdcc2cf45dfc7dfffef4fd97e5c47b15919b6a365247d95d6f684ef5e82/lxml-6.1.0-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:43e4d297f11080ec9d64a4b1ad7ac02b4484c9f0e2179d9c4ef78e886e747b88", size = 5232365, upload-time = "2026-04-18T04:33:50.249Z" }, + { url = "https://files.pythonhosted.org/packages/48/5a/b06875665e53aaba7127611a7bed3b7b9658e20b22bc2dd217a0b7ab0091/lxml-6.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cc16682cc987a3da00aa56a3aa3075b08edb10d9b1e476938cfdbee8f3b67181", size = 5043654, upload-time = "2026-04-18T04:33:52.71Z" }, + { url = "https://files.pythonhosted.org/packages/e9/9c/e71a069d09641c1a7abeb30e693f828c7c90a41cbe3d650b2d734d876f85/lxml-6.1.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:d6d8efe71429635f0559579092bb5e60560d7b9115ee38c4adbea35632e7fa24", size = 4769326, upload-time = "2026-04-18T04:33:55.244Z" }, + { url = "https://files.pythonhosted.org/packages/cc/06/7a9cd84b3d4ed79adf35f874750abb697dec0b4a81a836037b36e47c091a/lxml-6.1.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7e39ab3a28af7784e206d8606ec0e4bcad0190f63a492bca95e94e5a4aef7f6e", size = 5635879, upload-time = "2026-04-18T04:33:58.509Z" }, + { url = "https://files.pythonhosted.org/packages/cc/f0/9d57916befc1e54c451712c7ee48e9e74e80ae4d03bdce49914e0aee42cd/lxml-6.1.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:9eb667bf50856c4a58145f8ca2d5e5be160191e79eb9e30855a476191b3c3495", size = 5224048, upload-time = "2026-04-18T04:34:00.943Z" }, + { url = "https://files.pythonhosted.org/packages/99/75/90c4eefda0c08c92221fe0753db2d6699a4c628f76ff4465ec20dea84cc1/lxml-6.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7f4a77d6f7edf9230cee3e1f7f6764722a41604ee5681844f18db9a81ea0ec33", size = 5250241, upload-time = "2026-04-18T04:34:03.365Z" }, + { url = "https://files.pythonhosted.org/packages/5e/73/16596f7e4e38fa33084b9ccbccc22a15f82a290a055126f2c1541236d2ff/lxml-6.1.0-cp313-cp313-win32.whl", hash = "sha256:28902146ffbe5222df411c5d19e5352490122e14447e98cd118907ee3fd6ee62", size = 3596938, upload-time = "2026-04-18T04:31:56.206Z" }, + { url = "https://files.pythonhosted.org/packages/8e/63/981401c5680c1eb30893f00a19641ac80db5d1e7086c62cb4b13ed813038/lxml-6.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:4a1503c56e4e2b38dc76f2f2da7bae69670c0f1933e27cfa34b2fa5876410b16", size = 3995728, upload-time = "2026-04-18T04:31:58.763Z" }, + { url = "https://files.pythonhosted.org/packages/e7/e8/c358a38ac3e541d16a1b527e4e9cb78c0419b0506a070ace11777e5e8404/lxml-6.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:e0af85773850417d994d019741239b901b22c6680206f46a34766926e466141d", size = 3658372, upload-time = "2026-04-18T04:32:03.629Z" }, + { url = "https://files.pythonhosted.org/packages/eb/45/cee4cf203ef0bab5c52afc118da61d6b460c928f2893d40023cfa27e0b80/lxml-6.1.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:ab863fd37458fed6456525f297d21239d987800c46e67da5ef04fc6b3dd93ac8", size = 8576713, upload-time = "2026-04-18T04:32:06.831Z" }, + { url = "https://files.pythonhosted.org/packages/8a/a7/eda05babeb7e046839204eaf254cd4d7c9130ce2bbf0d9e90ea41af5654d/lxml-6.1.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:6fd8b1df8254ff4fd93fd31da1fc15770bde23ac045be9bb1f87425702f61cc9", size = 4623874, upload-time = "2026-04-18T04:32:10.755Z" }, + { url = "https://files.pythonhosted.org/packages/e7/e9/db5846de9b436b91890a62f29d80cd849ea17948a49bf532d5278ee69a9e/lxml-6.1.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:47024feaae386a92a146af0d2aeed65229bf6fff738e6a11dda6b0015fb8fd03", size = 4949535, upload-time = "2026-04-18T04:34:06.657Z" }, + { url = "https://files.pythonhosted.org/packages/5a/ba/0d3593373dcae1d68f40dc3c41a5a92f2544e68115eb2f62319a4c2a6500/lxml-6.1.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3f00972f84450204cd5d93a5395965e348956aaceaadec693a22ec743f8ae3eb", size = 5086881, upload-time = "2026-04-18T04:34:09.556Z" }, + { url = "https://files.pythonhosted.org/packages/43/76/759a7484539ad1af0d125a9afe9c3fb5f82a8779fd1f5f56319d9e4ea2fd/lxml-6.1.0-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97faa0860e13b05b15a51fb4986421ef7a30f0b3334061c416e0981e9450ca4c", size = 5031305, upload-time = "2026-04-18T04:34:12.336Z" }, + { url = "https://files.pythonhosted.org/packages/dc/b9/c1f0daf981a11e47636126901fd4ab82429e18c57aeb0fc3ad2940b42d8b/lxml-6.1.0-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:972a6451204798675407beaad97b868d0c733d9a74dafefc63120b81b8c2de28", size = 5647522, upload-time = "2026-04-18T04:34:14.89Z" }, + { url = "https://files.pythonhosted.org/packages/31/e6/1f533dcd205275363d9ba3511bcec52fa2df86abf8abe6a5f2c599f0dc31/lxml-6.1.0-cp314-cp314-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fe022f20bc4569ec66b63b3fb275a3d628d9d32da6326b2982584104db6d3086", size = 5239310, upload-time = "2026-04-18T04:34:17.652Z" }, + { url = "https://files.pythonhosted.org/packages/c3/8c/4175fb709c78a6e315ed814ed33be3defd8b8721067e70419a6cf6f971da/lxml-6.1.0-cp314-cp314-manylinux_2_28_i686.whl", hash = "sha256:75c4c7c619a744f972f4451bf5adf6d0fb00992a1ffc9fd78e13b0bc817cc99f", size = 5350799, upload-time = "2026-04-18T04:34:20.529Z" }, + { url = "https://files.pythonhosted.org/packages/fd/77/6ffdebc5994975f0dde4acb59761902bd9d9bb84422b9a0bd239a7da9ca8/lxml-6.1.0-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:3648f20d25102a22b6061c688beb3a805099ea4beb0a01ce62975d926944d292", size = 4697693, upload-time = "2026-04-18T04:34:23.541Z" }, + { url = "https://files.pythonhosted.org/packages/f8/f1/565f36bd5c73294602d48e04d23f81ff4c8736be6ba5e1d1ec670ac9be80/lxml-6.1.0-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:77b9f99b17cbf14026d1e618035077060fc7195dd940d025149f3e2e830fbfcb", size = 5250708, upload-time = "2026-04-18T04:34:26.001Z" }, + { url = "https://files.pythonhosted.org/packages/5a/11/a68ab9dd18c5c499404deb4005f4bc4e0e88e5b72cd755ad96efec81d18d/lxml-6.1.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:32662519149fd7a9db354175aa5e417d83485a8039b8aaa62f873ceee7ea4cad", size = 5084737, upload-time = "2026-04-18T04:34:28.32Z" }, + { url = "https://files.pythonhosted.org/packages/ab/78/e8f41e2c74f4af564e6a0348aea69fb6daaefa64bc071ef469823d22cc18/lxml-6.1.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:73d658216fc173cf2c939e90e07b941c5e12736b0bf6a99e7af95459cfe8eabb", size = 4737817, upload-time = "2026-04-18T04:34:30.784Z" }, + { url = "https://files.pythonhosted.org/packages/06/2d/aa4e117aa2ce2f3b35d9ff246be74a2f8e853baba5d2a92c64744474603a/lxml-6.1.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:ac4db068889f8772a4a698c5980ec302771bb545e10c4b095d4c8be26749616f", size = 5670753, upload-time = "2026-04-18T04:34:33.675Z" }, + { url = "https://files.pythonhosted.org/packages/08/f5/dd745d50c0409031dbfcc4881740542a01e54d6f0110bd420fa7782110b8/lxml-6.1.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:45e9dfbd1b661eb64ba0d4dbe762bd210c42d86dd1e5bd2bdf89d634231beb43", size = 5238071, upload-time = "2026-04-18T04:34:36.12Z" }, + { url = "https://files.pythonhosted.org/packages/3e/74/ad424f36d0340a904665867dab310a3f1f4c96ff4039698de83b77f44c1f/lxml-6.1.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:89e8d73d09ac696a5ba42ec69787913d53284f12092f651506779314f10ba585", size = 5264319, upload-time = "2026-04-18T04:34:39.035Z" }, + { url = "https://files.pythonhosted.org/packages/53/36/a15d8b3514ec889bfd6aa3609107fcb6c9189f8dc347f1c0b81eded8d87c/lxml-6.1.0-cp314-cp314-win32.whl", hash = "sha256:ebe33f4ec1b2de38ceb225a1749a2965855bffeef435ba93cd2d5d540783bf2f", size = 3657139, upload-time = "2026-04-18T04:32:20.006Z" }, + { url = "https://files.pythonhosted.org/packages/1a/a4/263ebb0710851a3c6c937180a9a86df1206fdfe53cc43005aa2237fd7736/lxml-6.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:398443df51c538bd578529aa7e5f7afc6c292644174b47961f3bf87fe5741120", size = 4064195, upload-time = "2026-04-18T04:32:23.876Z" }, + { url = "https://files.pythonhosted.org/packages/80/68/2000f29d323b6c286de077ad20b429fc52272e44eae6d295467043e56012/lxml-6.1.0-cp314-cp314-win_arm64.whl", hash = "sha256:8c8984e1d8c4b3949e419158fda14d921ff703a9ed8a47236c6eb7a2b6cb4946", size = 3741870, upload-time = "2026-04-18T04:32:27.922Z" }, + { url = "https://files.pythonhosted.org/packages/30/e9/21383c7c8d43799f0da90224c0d7c921870d476ec9b3e01e1b2c0b8237c5/lxml-6.1.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:1081dd10bc6fa437db2500e13993abf7cc30716d0a2f40e65abb935f02ec559c", size = 8827548, upload-time = "2026-04-18T04:32:15.094Z" }, + { url = "https://files.pythonhosted.org/packages/a5/01/c6bc11cd587030dd4f719f65c5657960649fe3e19196c844c75bf32cd0d6/lxml-6.1.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:dabecc48db5f42ba348d1f5d5afdc54c6c4cc758e676926c7cd327045749517d", size = 4735866, upload-time = "2026-04-18T04:32:18.924Z" }, + { url = "https://files.pythonhosted.org/packages/f3/01/757132fff5f4acf25463b5298f1a46099f3a94480b806547b29ce5e385de/lxml-6.1.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e3dd5fe19c9e0ac818a9c7f132a5e43c1339ec1cbbfecb1a938bd3a47875b7c9", size = 4969476, upload-time = "2026-04-18T04:34:41.889Z" }, + { url = "https://files.pythonhosted.org/packages/fd/fb/1bc8b9d27ed64be7c8903db6c89e74dc8c2cd9ec630a7462e4654316dc5b/lxml-6.1.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9e7b0a4ca6dcc007a4cef00a761bba2dea959de4bd2df98f926b33c92ca5dfb9", size = 5103719, upload-time = "2026-04-18T04:34:44.797Z" }, + { url = "https://files.pythonhosted.org/packages/d5/e7/5bf82fa28133536a54601aae633b14988e89ed61d4c1eb6b899b023233aa/lxml-6.1.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d27bbe326c6b539c64b42638b18bc6003a8d88f76213a97ac9ed4f885efeab7", size = 5027890, upload-time = "2026-04-18T04:34:47.634Z" }, + { url = "https://files.pythonhosted.org/packages/2d/20/e048db5d4b4ea0366648aa595f26bb764b2670903fc585b87436d0a5032c/lxml-6.1.0-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4e425db0c5445ef0ad56b0eec54f89b88b2d884656e536a90b2f52aecb4ca86", size = 5596008, upload-time = "2026-04-18T04:34:51.503Z" }, + { url = "https://files.pythonhosted.org/packages/9a/c2/d10807bc8da4824b39e5bd01b5d05c077b6fd01bd91584167edf6b269d22/lxml-6.1.0-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4b89b098105b8599dc57adac95d1813409ac476d3c948a498775d3d0c6124bfb", size = 5224451, upload-time = "2026-04-18T04:34:54.263Z" }, + { url = "https://files.pythonhosted.org/packages/3c/15/2ebea45bea427e7f0057e9ce7b2d62c5aba20c6b001cca89ed0aadb3ad41/lxml-6.1.0-cp314-cp314t-manylinux_2_28_i686.whl", hash = "sha256:c4a699432846df86cc3de502ee85f445ebad748a1c6021d445f3e514d2cd4b1c", size = 5312135, upload-time = "2026-04-18T04:34:56.818Z" }, + { url = "https://files.pythonhosted.org/packages/31/e2/87eeae151b0be2a308d49a7ec444ff3eb192b14251e62addb29d0bf3778f/lxml-6.1.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:30e7b2ed63b6c8e97cca8af048589a788ab5c9c905f36d9cf1c2bb549f450d2f", size = 4639126, upload-time = "2026-04-18T04:34:59.704Z" }, + { url = "https://files.pythonhosted.org/packages/a3/51/8a3f6a20902ad604dd746ec7b4000311b240d389dac5e9d95adefd349e0c/lxml-6.1.0-cp314-cp314t-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:022981127642fe19866d2907d76241bb07ed21749601f727d5d5dd1ce5d1b773", size = 5232579, upload-time = "2026-04-18T04:35:02.658Z" }, + { url = "https://files.pythonhosted.org/packages/6d/d2/650d619bdbe048d2c3f2c31edb00e35670a5e2d65b4fe3b61bce37b19121/lxml-6.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:23cad0cc86046d4222f7f418910e46b89971c5a45d3c8abfad0f64b7b05e4a9b", size = 5084206, upload-time = "2026-04-18T04:35:05.175Z" }, + { url = "https://files.pythonhosted.org/packages/dd/8a/672ca1a3cbeabd1f511ca275a916c0514b747f4b85bdaae103b8fa92f307/lxml-6.1.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:21c3302068f50d1e8728c67c87ba92aa87043abee517aa2576cca1855326b405", size = 4758906, upload-time = "2026-04-18T04:35:08.098Z" }, + { url = "https://files.pythonhosted.org/packages/be/f1/ef4b691da85c916cb2feb1eec7414f678162798ac85e042fa164419ac05c/lxml-6.1.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:be10838781cb3be19251e276910cd508fe127e27c3242e50521521a0f3781690", size = 5620553, upload-time = "2026-04-18T04:35:11.23Z" }, + { url = "https://files.pythonhosted.org/packages/59/17/94e81def74107809755ac2782fdad4404420f1c92ca83433d117a6d5acf0/lxml-6.1.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:2173a7bffe97667bbf0767f8a99e587740a8c56fdf3befac4b09cb29a80276fd", size = 5229458, upload-time = "2026-04-18T04:35:14.254Z" }, + { url = "https://files.pythonhosted.org/packages/21/55/c4be91b0f830a871fc1b0d730943d56013b683d4671d5198260e2eae722b/lxml-6.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c6854e9cf99c84beb004eecd7d3a3868ef1109bf2b1df92d7bc11e96a36c2180", size = 5247861, upload-time = "2026-04-18T04:35:17.006Z" }, + { url = "https://files.pythonhosted.org/packages/c2/ca/77123e4d77df3cb1e968ade7b1f808f5d3a5c1c96b18a33895397de292c1/lxml-6.1.0-cp314-cp314t-win32.whl", hash = "sha256:00750d63ef0031a05331b9223463b1c7c02b9004cef2346a5b2877f0f9494dd2", size = 3897377, upload-time = "2026-04-18T04:32:07.656Z" }, + { url = "https://files.pythonhosted.org/packages/64/ce/3554833989d074267c063209bae8b09815e5656456a2d332b947806b05ff/lxml-6.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:80410c3a7e3c617af04de17caa9f9f20adaa817093293d69eae7d7d0522836f5", size = 4392701, upload-time = "2026-04-18T04:32:12.113Z" }, + { url = "https://files.pythonhosted.org/packages/2b/a0/9b916c68c0e57752c07f8f64b30138d9d4059dbeb27b90274dedbea128ff/lxml-6.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:26dd9f57ee3bd41e7d35b4c98a2ffd89ed11591649f421f0ec19f67d50ec67ac", size = 3817120, upload-time = "2026-04-18T04:32:15.803Z" }, + { url = "https://files.pythonhosted.org/packages/f2/88/55143966481409b1740a3ac669e611055f49efd68087a5ce41582325db3e/lxml-6.1.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:546b66c0dd1bb8d9fa89d7123e5fa19a8aff3a1f2141eb22df96112afb17b842", size = 3930134, upload-time = "2026-04-18T04:32:35.008Z" }, + { url = "https://files.pythonhosted.org/packages/b5/97/28b985c2983938d3cb696dd5501423afb90a8c3e869ef5d3c62569282c0f/lxml-6.1.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5cfa1a34df366d9dc0d5eaf420f4cf2bb1e1bebe1066d1c2fc28c179f8a4004c", size = 4210749, upload-time = "2026-04-18T04:36:03.626Z" }, + { url = "https://files.pythonhosted.org/packages/29/67/dfab2b7d58214921935ccea7ce9b3df9b7d46f305d12f0f532ac7cf6b804/lxml-6.1.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:db88156fcf544cdbf0d95588051515cfdfd4c876fc66444eb98bceb5d6db76de", size = 4318463, upload-time = "2026-04-18T04:36:06.309Z" }, + { url = "https://files.pythonhosted.org/packages/32/a2/4ac7eb32a4d997dd352c32c32399aae27b3f268d440e6f9cfa405b575d2f/lxml-6.1.0-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:07f98f5496f96bf724b1e3c933c107f0cbf2745db18c03d2e13a291c3afd2635", size = 4251124, upload-time = "2026-04-18T04:36:09.056Z" }, + { url = "https://files.pythonhosted.org/packages/33/ef/d6abd850bb4822f9b720cfe36b547a558e694881010ff7d012191e8769c6/lxml-6.1.0-pp311-pypy311_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4642e04449a1e164b5ff71ffd901ddb772dfabf5c9adf1b7be5dffe1212bc037", size = 4401758, upload-time = "2026-04-18T04:36:11.803Z" }, + { url = "https://files.pythonhosted.org/packages/40/44/3ee09a5b60cb44c4f2fbc1c9015cfd6ff5afc08f991cab295d3024dcbf2d/lxml-6.1.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:7da13bb6fbadfafb474e0226a30570a3445cfd47c86296f2446dafbd77079ace", size = 3508860, upload-time = "2026-04-18T04:32:48.619Z" }, ] [[package]] @@ -1697,11 +1693,11 @@ wheels = [ [[package]] name = "pip" -version = "25.3" +version = "26.1.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fe/6e/74a3f0179a4a73a53d66ce57fdb4de0080a8baa1de0063de206d6167acc2/pip-25.3.tar.gz", hash = "sha256:8d0538dbbd7babbd207f261ed969c65de439f6bc9e5dbd3b3b9a77f25d95f343", size = 1803014, upload-time = "2025-10-25T00:55:41.394Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b6/48/cb9b7a682f6fe01a4221e1728941dd4ac3cd9090a17db3779d6ff490b602/pip-26.1.1.tar.gz", hash = "sha256:d36762751d156a4ee895de8af39aa0abeeeb577f93a2eca6ab62467bbf0f8a78", size = 1840400, upload-time = "2026-05-04T19:02:21.248Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/44/3c/d717024885424591d5376220b5e836c2d5293ce2011523c9de23ff7bf068/pip-25.3-py3-none-any.whl", hash = "sha256:9655943313a94722b7774661c21049070f6bbb0a1516bf02f7c8d5d9201514cd", size = 1778622, upload-time = "2025-10-25T00:55:39.247Z" }, + { url = "https://files.pythonhosted.org/packages/3a/eb/fea4d1d51c49832120f7f285d07306db3960f423a2612c6057caf3e8196f/pip-26.1.1-py3-none-any.whl", hash = "sha256:99cb1c2899893b075ff56e4ed0af55669a955b49ad7fb8d8603ecdaf4ed653fb", size = 1812777, upload-time = "2026-05-04T19:02:18.9Z" }, ] [[package]] @@ -1965,20 +1961,23 @@ wheels = [ [[package]] name = "pygments" -version = "2.19.2" +version = "2.20.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, ] [[package]] name = "pyjwt" -version = "2.10.1" +version = "2.12.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e7/46/bd74733ff231675599650d3e47f361794b22ef3e3770998dda30d3b63726/pyjwt-2.10.1.tar.gz", hash = "sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953", size = 87785, upload-time = "2024-11-28T03:43:29.933Z" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c2/27/a3b6e5bf6ff856d2509292e95c8f57f0df7017cf5394921fc4e4ef40308a/pyjwt-2.12.1.tar.gz", hash = "sha256:c74a7a2adf861c04d002db713dd85f84beb242228e671280bf709d765b03672b", size = 102564, upload-time = "2026-03-13T19:27:37.25Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/61/ad/689f02752eeec26aed679477e80e632ef1b682313be70793d798c1d5fc8f/PyJWT-2.10.1-py3-none-any.whl", hash = "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb", size = 22997, upload-time = "2024-11-28T03:43:27.893Z" }, + { url = "https://files.pythonhosted.org/packages/e5/7a/8dd906bd22e79e47397a61742927f6747fe93242ef86645ee9092e610244/pyjwt-2.12.1-py3-none-any.whl", hash = "sha256:28ca37c070cad8ba8cd9790cd940535d40274d22f80ab87f3ac6a713e6e8454c", size = 29726, upload-time = "2026-03-13T19:27:35.677Z" }, ] [[package]] @@ -2035,7 +2034,7 @@ wheels = [ [[package]] name = "pytest" -version = "9.0.2" +version = "9.0.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, @@ -2046,9 +2045,9 @@ dependencies = [ { name = "pygments" }, { name = "tomli", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" }, + { url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" }, ] [[package]] @@ -2225,7 +2224,7 @@ wheels = [ [[package]] name = "requests" -version = "2.32.5" +version = "2.33.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, @@ -2233,9 +2232,9 @@ dependencies = [ { name = "idna" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5f/a4/98b9c7c6428a668bf7e42ebb7c79d576a1c3c1e3ae2d47e674b468388871/requests-2.33.1.tar.gz", hash = "sha256:18817f8c57c6263968bc123d237e3b8b08ac046f5456bd1e307ee8f4250d3517", size = 134120, upload-time = "2026-03-30T16:09:15.531Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, + { url = "https://files.pythonhosted.org/packages/d7/8e/7540e8a2036f79a125c1d2ebadf69ed7901608859186c856fa0388ef4197/requests-2.33.1-py3-none-any.whl", hash = "sha256:4e6d1ef462f3626a1f0a0a9c42dd93c63bad33f9f1c1937509b8c5c8718ab56a", size = 64947, upload-time = "2026-03-30T16:09:13.83Z" }, ] [[package]]