diff --git a/.github/workflows/validate-badges.yml b/.github/workflows/validate-badges.yml new file mode 100644 index 0000000..6372414 --- /dev/null +++ b/.github/workflows/validate-badges.yml @@ -0,0 +1,37 @@ +name: Validate public badge registry + +on: + pull_request: + paths: + - "assets/data/badges-public.json" + - "assets/badges/**" + - "schema/badges-public.schema.json" + - "scripts/validate_badges.py" + - ".github/workflows/validate-badges.yml" + push: + branches: + - gh-pages + - paciobadge + paths: + - "assets/data/badges-public.json" + - "assets/badges/**" + - "schema/badges-public.schema.json" + - "scripts/validate_badges.py" + - ".github/workflows/validate-badges.yml" + +jobs: + validate-badges: + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.x" + + - name: Validate public badge registry + run: python scripts/validate_badges.py diff --git a/README.md b/README.md index 8795f9b..6bc5730 100644 --- a/README.md +++ b/README.md @@ -1 +1,4 @@ -# pacio-website-v2.0 \ No newline at end of file +# PACIO Project website + +Badge-verification publishing and privacy guidance is in +[docs/badge-system/README.md](docs/badge-system/README.md). diff --git a/advance-directives.html b/advance-directives.html index c501f51..397bbd4 100644 --- a/advance-directives.html +++ b/advance-directives.html @@ -40,6 +40,7 @@ + diff --git a/announcements.html b/announcements.html index b67abc7..7d60bae 100644 --- a/announcements.html +++ b/announcements.html @@ -40,6 +40,7 @@ + @@ -57,6 +58,21 @@

PACIO Announcements

2026

diff --git a/css/styles.css b/css/styles.css index c96597e..4098cac 100755 --- a/css/styles.css +++ b/css/styles.css @@ -10097,6 +10097,7 @@ img::-moz-selection { font-size: 0.95rem; color: #fff; letter-spacing: 0.0625em; + white-space: nowrap; } #mainNav .navbar-nav .nav-item .nav-link.active, #mainNav .navbar-nav .nav-item .nav-link:hover { color: #f2af0d; @@ -10155,7 +10156,7 @@ body:not(:has(header.masthead)) #mainNav:not(.navbar-shrink) { height: 2.5rem; } #mainNav .navbar-nav .nav-item { - margin-right: 1rem; + margin-right: 0.5rem; } #mainNav .navbar-nav .nav-item:last-child { margin-right: 0; @@ -10627,4 +10628,4 @@ ul.news li { div.video { margin-bottom: 2em; -} \ No newline at end of file +} diff --git a/docs/badge-system/README.md b/docs/badge-system/README.md new file mode 100644 index 0000000..9a3838f --- /dev/null +++ b/docs/badge-system/README.md @@ -0,0 +1,64 @@ +# PACIO Badge Verification + +This directory documents the public badge verification pieces used by the +PACIO Project website. + +## Public Components + +- `/verification/index.html` provides the public badge lookup page. +- `/assets/data/badges-public.json` contains sanitized public badge records. +- `/assets/js/badge-verification.js` performs client-side lookup by badge ID + and filtering by event, year, badge category, and company name. +- `/assets/css/badge-verification.css` styles the verification page. +- `/schema/badges-public.schema.json` documents the public registry shape. +- `/.github/workflows/validate-badges.yml` validates public badge data changes. + +## Private Components + +Do not publish the private badge administration tracker, requester contact +information, eligibility evidence, internal reviewer notes, delivery logs, or +badge-generation credentials in this repository. + +Badge generation should run from a private PACIO-controlled environment. Only +sanitized public verification records should be committed here. Generated badge +SVG files should remain in the private generation output and be delivered to +recipients as attachments. + +Public registry updates should be staged only after the private generation +package has been reviewed. The private tooling uses a separate +`publish-reviewed` step to copy the reviewed `badges-public.json` into this +repository. + +## Public Record Rules + +Public badge records may confirm the badge ID, recipient display name, public +company name, key contributors, badge category, event ID, event name, event +date, status, replacement badge links, and last updated date. + +Public badge records must not include specific implementation guides tested, +detailed test results, certification claims, conformance claims, endorsement +language, production-readiness claims, private contact information, or internal +review notes. + +## Event Organization + +Badge IDs are globally unique and stable in the format +`PACIO-BDG-YYYY-####`. Event grouping is handled separately with `eventId` in +the format `YYYY-MM-short-event-name`, for example +`2026-07-cms-connectathon`. + +The verification page supports direct badge links such as +`/verification/?id=PACIO-BDG-2026-0002` and browsing filters for event, year, +category, and company name. + +## Correction And Replacement Display + +Participant request details stay private. The public registry may show only the +resulting public status: + +- `Corrected` means the badge record was updated while keeping the same Badge + ID. +- `Retired` or `Revoked` means the badge is no longer the active recognition + record. +- `supersedesBadgeId` and `supersededByBadgeId` may be used to connect a public + replacement badge with the older badge it replaced. diff --git a/functional_status.html b/functional_status.html index ab98f52..eb80f60 100644 --- a/functional_status.html +++ b/functional_status.html @@ -40,6 +40,7 @@ + diff --git a/index.html b/index.html index 06c278d..cbe63f1 100755 --- a/index.html +++ b/index.html @@ -22,7 +22,7 @@ - + @@ -41,6 +41,7 @@ + @@ -156,7 +157,7 @@

Use Cases

- + Transitions of Care @@ -194,6 +195,11 @@

Published by HL7!

Announcements

+
+

+ July 14–16, 2026
The PACIO Project Community participated in a CMS HL7 FHIR Connectathon track, advancing interoperability testing across the PACIO Transitions of Care (ToC), Advance Healthcare Directive Interoperability (ADI), and Personal Functioning and Engagement (PFE) Implementation Guides. Videos, presentation slides, and additional implementation details are available on the PACIO Connectathon Tracks page. +

+

January 13-15, 2026
The PACIO Project Community participated in the January 2026 HL7® FHIR® Connectathon, demonstrating testing across multiple PACIO implementation guides and workflows. Testing included PROMIS-10 data capture and sharing using the PACIO Personal Functioning and Engagement (PFE) Implementation Guide, medication reconciliation and Medication Action Plan creation using the Standardized Medication Profile (SMP) Implementation Guide, and REMS workflow integration using the PACIO Transitions of Care (ToC) Implementation Guide and SMP. Additional demonstrations included AI-assisted ToC data generation, PointClickCare FHIR sandbox testing, and PACIO-focused Inferno Test Kits for automated conformance testing. diff --git a/news.html b/news.html index 6d93368..8b8334c 100644 --- a/news.html +++ b/news.html @@ -40,6 +40,7 @@

+ diff --git a/pfe.html b/pfe.html index 20046da..10fe88e 100644 --- a/pfe.html +++ b/pfe.html @@ -40,6 +40,7 @@ + diff --git a/privacy.html b/privacy.html index fe27c31..f933d3e 100644 --- a/privacy.html +++ b/privacy.html @@ -33,6 +33,7 @@ + diff --git a/quality_measures.html b/quality_measures.html index 3825f84..0a1b42d 100644 --- a/quality_measures.html +++ b/quality_measures.html @@ -40,6 +40,7 @@ + diff --git a/re-assessment-timepoints.html b/re-assessment-timepoints.html index d4c6ef9..4762263 100644 --- a/re-assessment-timepoints.html +++ b/re-assessment-timepoints.html @@ -40,6 +40,7 @@ + diff --git a/schema/badges-public.schema.json b/schema/badges-public.schema.json new file mode 100644 index 0000000..c44a8ac --- /dev/null +++ b/schema/badges-public.schema.json @@ -0,0 +1,81 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://pacioproject.org/schema/badges-public.schema.json", + "title": "PACIO Public Badge Registry", + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": [ + "badgeId", + "eventId", + "status", + "recipientDisplayName", + "recipientType", + "badgeCategory", + "eventName", + "eventDateRange", + "verificationUrl", + "lastUpdated" + ], + "properties": { + "badgeId": { + "type": "string", + "pattern": "^PACIO-BDG-[0-9]{4}-[0-9]{4}$" + }, + "eventId": { + "type": "string", + "pattern": "^20[0-9]{2}-[0-9]{2}-[a-z0-9]+(-[a-z0-9]+)*$" + }, + "status": { + "type": "string", + "enum": ["Active", "Corrected", "Revoked", "Retired"] + }, + "recipientDisplayName": { + "type": "string", + "minLength": 1 + }, + "recipientType": { + "type": "string", + "enum": ["Individual", "Team", "Organization"] + }, + "companyName": { + "type": "string" + }, + "badgeCategory": { + "type": "string", + "enum": ["Observer", "Contributor", "FHIR Implementer"] + }, + "eventName": { + "type": "string", + "minLength": 1 + }, + "eventDateRange": { + "type": "string", + "minLength": 1 + }, + "keyContributors": { + "type": "string" + }, + "supersedesBadgeId": { + "type": "string", + "pattern": "^PACIO-BDG-[0-9]{4}-[0-9]{4}$" + }, + "supersededByBadgeId": { + "type": "string", + "pattern": "^PACIO-BDG-[0-9]{4}-[0-9]{4}$" + }, + "verificationUrl": { + "type": "string", + "pattern": "^https://pacioproject\\.org/verification/\\?id=PACIO-BDG-[0-9]{4}-[0-9]{4}$" + }, + "lastUpdated": { + "type": "string", + "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$" + }, + "publicNote": { + "type": "string" + } + } + } +} diff --git a/scripts/validate_badges.py b/scripts/validate_badges.py new file mode 100644 index 0000000..a6ab3d3 --- /dev/null +++ b/scripts/validate_badges.py @@ -0,0 +1,177 @@ +#!/usr/bin/env python3 +"""Validate the public PACIO badge registry before publication. + +This script intentionally uses only the Python standard library so it can run in +GitHub Actions without dependency installation. It validates the sanitized +public registry, not the private badge administration tracker. +""" + +from __future__ import annotations + +import json +import re +import sys +from pathlib import Path +from urllib.parse import parse_qs, urlparse + + +ROOT = Path(__file__).resolve().parents[1] +REGISTRY_PATH = ROOT / "assets" / "data" / "badges-public.json" + +REQUIRED_FIELDS = { + "badgeId", + "eventId", + "status", + "recipientDisplayName", + "recipientType", + "badgeCategory", + "eventName", + "eventDateRange", + "verificationUrl", + "lastUpdated", +} +OPTIONAL_FIELDS = {"companyName", "keyContributors", "supersedesBadgeId", "supersededByBadgeId", "publicNote"} +ALLOWED_FIELDS = REQUIRED_FIELDS | OPTIONAL_FIELDS +ALLOWED_STATUSES = {"Active", "Corrected", "Revoked", "Retired"} +ALLOWED_RECIPIENT_TYPES = {"Individual", "Team", "Organization"} +ALLOWED_CATEGORIES = {"Observer", "Contributor", "FHIR Implementer"} +BADGE_ID_PATTERN = re.compile(r"^PACIO-BDG-\d{4}-\d{4}$") +EVENT_ID_PATTERN = re.compile(r"^20\d{2}-\d{2}-[a-z0-9]+(?:-[a-z0-9]+)*$") +DATE_PATTERN = re.compile(r"^\d{4}-\d{2}-\d{2}$") +EMAIL_PATTERN = re.compile(r"[\w.+-]+@[\w.-]+\.[A-Za-z]{2,}") +PHONE_PATTERN = re.compile(r"(? None: + print(f"ERROR: {message}", file=sys.stderr) + raise SystemExit(1) + + +def load_registry() -> list[dict]: + try: + data = json.loads(REGISTRY_PATH.read_text(encoding="utf-8")) + except FileNotFoundError: + fail(f"Registry file not found: {REGISTRY_PATH}") + except json.JSONDecodeError as exc: + fail(f"Registry JSON is invalid: {exc}") + + if not isinstance(data, list): + fail("Registry must be a JSON array.") + return data + + +def validate_url(record: dict, index: int) -> None: + badge_id = record["badgeId"] + url = record["verificationUrl"] + parsed = urlparse(url) + if parsed.scheme != "https" or parsed.netloc != "pacioproject.org" or parsed.path != "/verification/": + fail(f"Record {index}: verificationUrl must use https://pacioproject.org/verification/.") + query_id = parse_qs(parsed.query).get("id", [None])[0] + if query_id != badge_id: + fail(f"Record {index}: verificationUrl id must match badgeId.") + + +def validate_public_text(record: dict, index: int) -> None: + combined = " ".join(str(value).lower() for value in record.values()) + for phrase in PROHIBITED_TEXT: + if phrase in combined: + fail(f"Record {index}: prohibited claim language found: {phrase}") + + +def validate_sensitive_public_text(record: dict, index: int) -> None: + for field, value in record.items(): + if not isinstance(value, str): + continue + if EMAIL_PATTERN.search(value): + fail(f"Record {index}: {field} must not include email addresses.") + if PHONE_PATTERN.search(value): + fail(f"Record {index}: {field} must not include phone numbers.") + + +def validate_record(record: object, index: int, seen_ids: set[str]) -> None: + if not isinstance(record, dict): + fail(f"Record {index}: each registry item must be an object.") + + unknown = set(record) - ALLOWED_FIELDS + if unknown: + fail(f"Record {index}: unknown public field(s): {', '.join(sorted(unknown))}") + + prohibited = set(record) & PROHIBITED_PUBLIC_FIELDS + if prohibited: + fail(f"Record {index}: prohibited private field(s): {', '.join(sorted(prohibited))}") + + missing = REQUIRED_FIELDS - set(record) + if missing: + fail(f"Record {index}: missing required field(s): {', '.join(sorted(missing))}") + + badge_id = record["badgeId"] + if not isinstance(badge_id, str) or not BADGE_ID_PATTERN.match(badge_id): + fail(f"Record {index}: badgeId must match PACIO-BDG-YYYY-####.") + if badge_id in seen_ids: + fail(f"Record {index}: duplicate badgeId: {badge_id}") + seen_ids.add(badge_id) + + event_id = record["eventId"] + if not isinstance(event_id, str) or not EVENT_ID_PATTERN.match(event_id): + fail(f"Record {index}: eventId must match YYYY-MM-short-event-name.") + + if record["status"] not in ALLOWED_STATUSES: + fail(f"Record {index}: invalid status: {record['status']}") + if record["recipientType"] not in ALLOWED_RECIPIENT_TYPES: + fail(f"Record {index}: invalid recipientType: {record['recipientType']}") + if record["badgeCategory"] not in ALLOWED_CATEGORIES: + fail(f"Record {index}: invalid badgeCategory: {record['badgeCategory']}") + for field in ("supersedesBadgeId", "supersededByBadgeId"): + related_badge_id = record.get(field, "") + if related_badge_id: + if not isinstance(related_badge_id, str) or not BADGE_ID_PATTERN.match(related_badge_id): + fail(f"Record {index}: {field} must match PACIO-BDG-YYYY-####.") + if related_badge_id == badge_id: + fail(f"Record {index}: {field} must not match badgeId.") + if not DATE_PATTERN.match(record["lastUpdated"]): + fail(f"Record {index}: lastUpdated must use YYYY-MM-DD.") + + for field in REQUIRED_FIELDS: + if not isinstance(record[field], str) or not record[field].strip(): + fail(f"Record {index}: {field} must be a non-empty string.") + for field in OPTIONAL_FIELDS: + if field in record and not isinstance(record[field], str): + fail(f"Record {index}: {field} must be a string when provided.") + validate_url(record, index) + validate_public_text(record, index) + validate_sensitive_public_text(record, index) + + +def main() -> None: + registry = load_registry() + seen_ids: set[str] = set() + for index, record in enumerate(registry, start=1): + validate_record(record, index, seen_ids) + print(f"Validated {len(registry)} public badge record(s).") + + +if __name__ == "__main__": + main() diff --git a/smp.html b/smp.html index 72766bc..18fd666 100644 --- a/smp.html +++ b/smp.html @@ -40,6 +40,7 @@ + diff --git a/splasch.html b/splasch.html index 7801e4d..69a2fcb 100644 --- a/splasch.html +++ b/splasch.html @@ -40,6 +40,7 @@ + diff --git a/terms-of-use.html b/terms-of-use.html index 28f7f62..7e018d5 100644 --- a/terms-of-use.html +++ b/terms-of-use.html @@ -40,6 +40,7 @@ + diff --git a/transitions-of-care.html b/transitions-of-care.html index ca5fa91..0cddc9f 100644 --- a/transitions-of-care.html +++ b/transitions-of-care.html @@ -40,6 +40,7 @@ + diff --git a/verification/index.html b/verification/index.html new file mode 100644 index 0000000..232db57 --- /dev/null +++ b/verification/index.html @@ -0,0 +1,227 @@ + + + + + + + + + + + PACIO Badge Verification + + + + + + + + + + + + + + + +
+
+
+
+

PACIO Badge Verification

+
+
+
+

+ PACIO badges recognize participation and engagement in PACIO testing events. + A badge confirms that the listed recipient participated in the listed event and category. + It does not represent certification, conformance, endorsement, production readiness, or successful completion of a specific test suite. +

+

+ Enter a badge ID below, or open a direct verification link such as + https://pacioproject.org/verification/?id=PACIO-BDG-2026-0001. +

+
+
+ +
+
+
+
+
+

Find a Badge

+

Verify by Badge ID

+
+
+ +
+ + + +
+

Type a badge ID to filter the table. Click Verify to open the matching badge record and highlight it.

+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+
+ +
+ + + +
+
+
+

Public Registry

+

Badge Records

+
+

+
+

+ Use the Badge ID field and filters above to browse the registry. Direct verification links filter this table automatically. +

+
+ + + + + + + + + + + + + +
Badge IDEventEvent DateCategoryCompany NameKey ContributorsStatus
+
+ +
+ +
+

Badge Meaning

+

+ PACIO badges are event-based recognition artifacts. They do not include specific implementation guides tested, + detailed testing results, certification claims, conformance claims, endorsement language, or production-readiness claims. +

+
+
+
+
+
+
+ +