From 017acffd2cbc940f50369fea854b52a047074fc9 Mon Sep 17 00:00:00 2001 From: Etienne Pichot Damon Date: Wed, 30 Sep 2026 13:21:14 -0400 Subject: [PATCH 1/4] feat(specifications): add unlisted specification registry built from data files --- .claude/launch.json | 11 + .../ISSUE_TEMPLATE/specification-change.yml | 57 + .../workflows/check_specifications_data.yml | 33 + Makefile | 16 +- README.md | 33 + config/en/mkdocs.yml | 6 + data/README.md | 192 ++ data/licences.csv | 18 + data/organizations/apds.md | 10 + data/organizations/apple.md | 9 + data/organizations/asam.md | 9 + data/organizations/bcs.md | 10 + data/organizations/bts-usdot.md | 9 + data/organizations/cen.md | 10 + data/organizations/codeforamerica.md | 8 + data/organizations/david-wasserman.md | 7 + data/organizations/etalab.md | 9 + data/organizations/european-commission.md | 9 + data/organizations/fabmob.md | 10 + data/organizations/iso.md | 9 + data/organizations/ite.md | 10 + data/organizations/itxpt.md | 10 + data/organizations/masstrails.md | 9 + data/organizations/mobilitydata.md | 9 + data/organizations/mobivoc.md | 9 + data/organizations/mtc.md | 10 + data/organizations/nc-bpaid.md | 9 + data/organizations/nemtac.md | 9 + data/organizations/oasis.md | 9 + data/organizations/ogc.md | 10 + data/organizations/omf.md | 10 + data/organizations/osm-us.md | 10 + data/organizations/osmf.md | 10 + data/organizations/overture.md | 9 + data/organizations/peopleforbikes.md | 9 + data/organizations/psrc.md | 10 + data/organizations/sae.md | 9 + data/organizations/sfcta.md | 9 + data/organizations/suti.md | 9 + data/organizations/tcat-uw.md | 10 + data/organizations/tides-board.md | 8 + data/organizations/tods-board.md | 9 + data/organizations/tomp-wg.md | 9 + data/organizations/trb.md | 10 + data/organizations/uic.md | 9 + data/organizations/uk-dft.md | 10 + data/organizations/vdv-ets.md | 9 + data/organizations/vta.md | 10 + data/organizations/zephyr.md | 9 + data/principles.csv | 8 + data/specifications/apds.md | 37 + data/specifications/asam-opendrive.md | 40 + data/specifications/beckn.md | 38 + data/specifications/cds-m.md | 39 + data/specifications/cds.md | 40 + data/specifications/connect-os.md | 40 + data/specifications/curblr.md | 27 + data/specifications/fast-trips.md | 25 + data/specifications/gatis.md | 40 + data/specifications/gbfs.md | 40 + data/specifications/gcfs.md | 18 + data/specifications/gmns.md | 44 + data/specifications/gofs.md | 41 + data/specifications/gtfs-flex.md | 46 + data/specifications/gtfs-realtime.md | 52 + data/specifications/gtfs-schedule.md | 49 + data/specifications/gtns.md | 27 + data/specifications/imdf.md | 40 + data/specifications/inspire.md | 30 + data/specifications/iso-20524-2.md | 42 + data/specifications/iso-4448.md | 38 + data/specifications/iso-7874.md | 40 + data/specifications/itxpt.md | 38 + data/specifications/ixsi.md | 27 + data/specifications/mat.md | 38 + data/specifications/mds.md | 45 + data/specifications/mobivoc.md | 23 + data/specifications/mqtt.md | 37 + data/specifications/mtlfs.md | 37 + data/specifications/nchrp-07-31.md | 38 + data/specifications/nchrp-08-177.md | 38 + data/specifications/nemtac-1000.md | 38 + data/specifications/netex.md | 41 + data/specifications/ojp.md | 41 + data/specifications/opensidewalks.md | 40 + data/specifications/opentrails.md | 34 + data/specifications/opra.md | 42 + data/specifications/osdm.md | 40 + data/specifications/osm-bna-guide.md | 40 + data/specifications/osm-us-pedestrian-wg.md | 40 + data/specifications/osm.md | 41 + data/specifications/overture-maps.md | 47 + data/specifications/rdex.md | 27 + data/specifications/sae-j2735.md | 40 + .../schema-amenagements-cyclables.md | 40 + .../schema-stationnement-cyclable.md | 39 + data/specifications/shared-row.md | 41 + data/specifications/siri.md | 43 + .../statewide-trails-data-standard-ma.md | 40 + data/specifications/suti.md | 38 + data/specifications/tides.md | 43 + data/specifications/tods.md | 42 + data/specifications/tomp-api.md | 39 + data/specifications/transxchange.md | 41 + data/specifications/vdv.md | 41 + hooks/specifications.py | 907 +++++++++ overrides/assets/javascript/specifications.js | 454 +++++ .../assets/stylesheets/specifications.css | 1764 +++++++++++++++++ overrides/main.html | 10 + overrides/sitemap.xml | 14 + scripts/check_specs.py | 74 + scripts/spec_api.py | 384 ++++ scripts/spec_data.py | 727 +++++++ 113 files changed, 7200 insertions(+), 1 deletion(-) create mode 100644 .claude/launch.json create mode 100644 .github/ISSUE_TEMPLATE/specification-change.yml create mode 100644 .github/workflows/check_specifications_data.yml create mode 100644 data/README.md create mode 100644 data/licences.csv create mode 100644 data/organizations/apds.md create mode 100644 data/organizations/apple.md create mode 100644 data/organizations/asam.md create mode 100644 data/organizations/bcs.md create mode 100644 data/organizations/bts-usdot.md create mode 100644 data/organizations/cen.md create mode 100644 data/organizations/codeforamerica.md create mode 100644 data/organizations/david-wasserman.md create mode 100644 data/organizations/etalab.md create mode 100644 data/organizations/european-commission.md create mode 100644 data/organizations/fabmob.md create mode 100644 data/organizations/iso.md create mode 100644 data/organizations/ite.md create mode 100644 data/organizations/itxpt.md create mode 100644 data/organizations/masstrails.md create mode 100644 data/organizations/mobilitydata.md create mode 100644 data/organizations/mobivoc.md create mode 100644 data/organizations/mtc.md create mode 100644 data/organizations/nc-bpaid.md create mode 100644 data/organizations/nemtac.md create mode 100644 data/organizations/oasis.md create mode 100644 data/organizations/ogc.md create mode 100644 data/organizations/omf.md create mode 100644 data/organizations/osm-us.md create mode 100644 data/organizations/osmf.md create mode 100644 data/organizations/overture.md create mode 100644 data/organizations/peopleforbikes.md create mode 100644 data/organizations/psrc.md create mode 100644 data/organizations/sae.md create mode 100644 data/organizations/sfcta.md create mode 100644 data/organizations/suti.md create mode 100644 data/organizations/tcat-uw.md create mode 100644 data/organizations/tides-board.md create mode 100644 data/organizations/tods-board.md create mode 100644 data/organizations/tomp-wg.md create mode 100644 data/organizations/trb.md create mode 100644 data/organizations/uic.md create mode 100644 data/organizations/uk-dft.md create mode 100644 data/organizations/vdv-ets.md create mode 100644 data/organizations/vta.md create mode 100644 data/organizations/zephyr.md create mode 100644 data/principles.csv create mode 100644 data/specifications/apds.md create mode 100644 data/specifications/asam-opendrive.md create mode 100644 data/specifications/beckn.md create mode 100644 data/specifications/cds-m.md create mode 100644 data/specifications/cds.md create mode 100644 data/specifications/connect-os.md create mode 100644 data/specifications/curblr.md create mode 100644 data/specifications/fast-trips.md create mode 100644 data/specifications/gatis.md create mode 100644 data/specifications/gbfs.md create mode 100644 data/specifications/gcfs.md create mode 100644 data/specifications/gmns.md create mode 100644 data/specifications/gofs.md create mode 100644 data/specifications/gtfs-flex.md create mode 100644 data/specifications/gtfs-realtime.md create mode 100644 data/specifications/gtfs-schedule.md create mode 100644 data/specifications/gtns.md create mode 100644 data/specifications/imdf.md create mode 100644 data/specifications/inspire.md create mode 100644 data/specifications/iso-20524-2.md create mode 100644 data/specifications/iso-4448.md create mode 100644 data/specifications/iso-7874.md create mode 100644 data/specifications/itxpt.md create mode 100644 data/specifications/ixsi.md create mode 100644 data/specifications/mat.md create mode 100644 data/specifications/mds.md create mode 100644 data/specifications/mobivoc.md create mode 100644 data/specifications/mqtt.md create mode 100644 data/specifications/mtlfs.md create mode 100644 data/specifications/nchrp-07-31.md create mode 100644 data/specifications/nchrp-08-177.md create mode 100644 data/specifications/nemtac-1000.md create mode 100644 data/specifications/netex.md create mode 100644 data/specifications/ojp.md create mode 100644 data/specifications/opensidewalks.md create mode 100644 data/specifications/opentrails.md create mode 100644 data/specifications/opra.md create mode 100644 data/specifications/osdm.md create mode 100644 data/specifications/osm-bna-guide.md create mode 100644 data/specifications/osm-us-pedestrian-wg.md create mode 100644 data/specifications/osm.md create mode 100644 data/specifications/overture-maps.md create mode 100644 data/specifications/rdex.md create mode 100644 data/specifications/sae-j2735.md create mode 100644 data/specifications/schema-amenagements-cyclables.md create mode 100644 data/specifications/schema-stationnement-cyclable.md create mode 100644 data/specifications/shared-row.md create mode 100644 data/specifications/siri.md create mode 100644 data/specifications/statewide-trails-data-standard-ma.md create mode 100644 data/specifications/suti.md create mode 100644 data/specifications/tides.md create mode 100644 data/specifications/tods.md create mode 100644 data/specifications/tomp-api.md create mode 100644 data/specifications/transxchange.md create mode 100644 data/specifications/vdv.md create mode 100644 hooks/specifications.py create mode 100644 overrides/assets/javascript/specifications.js create mode 100644 overrides/assets/stylesheets/specifications.css create mode 100644 overrides/main.html create mode 100644 overrides/sitemap.xml create mode 100644 scripts/check_specs.py create mode 100644 scripts/spec_api.py create mode 100644 scripts/spec_data.py diff --git a/.claude/launch.json b/.claude/launch.json new file mode 100644 index 0000000..499000f --- /dev/null +++ b/.claude/launch.json @@ -0,0 +1,11 @@ +{ + "version": "0.0.1", + "configurations": [ + { + "name": "mkdocs-en", + "runtimeExecutable": "mkdocs", + "runtimeArgs": ["serve", "-f", "config/en/mkdocs.yml", "--dev-addr", "127.0.0.1:8000", "--watch", "overrides", "--watch", "hooks", "--watch", "data", "--watch", "scripts"], + "port": 8000 + } + ] +} diff --git a/.github/ISSUE_TEMPLATE/specification-change.yml b/.github/ISSUE_TEMPLATE/specification-change.yml new file mode 100644 index 0000000..c5ce3c5 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/specification-change.yml @@ -0,0 +1,57 @@ +name: Specification registry change request +description: Correct or add an entry in the MDIP specification registry (/specifications). +title: "Change request: " +labels: ["specification-registry"] +body: + - type: markdown + attributes: + value: | + Each specification and organization in the registry is one file in + [`data/`](https://github.com/MobilityData/Mobility-Data-Interoperability-Principles/tree/main/data), described in + [`data/README.md`](https://github.com/MobilityData/Mobility-Data-Interoperability-Principles/blob/main/data/README.md). + The quickest way to change an entry is "Suggest a change" on its page, which opens + its file for editing. Use this form to report a problem or ask a question instead. + + Every change is discussed here in public before it is merged. + - type: input + id: spec_id + attributes: + label: Specification id + description: The file name in `data/specifications/`, without `.md`, for example `gtfs-schedule`. Leave empty to propose a new specification. + validations: + required: false + - type: dropdown + id: change_type + attributes: + label: Type of change + options: + - Correct an existing entry + - Add a new specification + - Add or correct an organization + - Question about the methodology or scoring + validations: + required: true + - type: textarea + id: change + attributes: + label: What should change + description: Name the field (for example `principles.open_governance.verdict` or `homepage_url`) and give the value you propose. + validations: + required: true + - type: textarea + id: evidence + attributes: + label: Evidence + description: Links that support the change, such as a licence file, governance charter, changelog or adopters list. + validations: + required: true + - type: dropdown + id: relationship + attributes: + label: Your relationship to the specification + options: + - I am part of the maintaining organization + - I implement or use the specification + - Other + validations: + required: true diff --git a/.github/workflows/check_specifications_data.yml b/.github/workflows/check_specifications_data.yml new file mode 100644 index 0000000..8b95a43 --- /dev/null +++ b/.github/workflows/check_specifications_data.yml @@ -0,0 +1,33 @@ +name: Check specification registry data + +# Validates the files in data/ against the vocabularies in scripts/spec_data.py, so a +# change to the registry is checked before it is merged. +on: + pull_request: + paths: + - 'data/**' + - 'scripts/spec_data.py' + - 'scripts/check_specs.py' + - 'scripts/spec_api.py' + push: + branches: + - main + paths: + - 'data/**' + - 'scripts/spec_data.py' + - 'scripts/check_specs.py' + - 'scripts/spec_api.py' + workflow_dispatch: + +jobs: + check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: '3.12' + - name: Install PyYAML + run: pip install pyyaml + - name: Validate the registry files + run: python3 scripts/check_specs.py --export "$RUNNER_TEMP/export" diff --git a/Makefile b/Makefile index bf34cb1..a7ff82e 100644 --- a/Makefile +++ b/Makefile @@ -10,10 +10,15 @@ setup: pip3 install --force-reinstall -r requirements.txt && \ pip3 install --upgrade --force-reinstall mkdocs-material +# --watch: mkdocs only watches docs/ and the config by itself, so the +# specification registry's sources (hooks/, scripts/, data/) have to be named. Note that mkdocs caches +# the hook module -- an edit to hooks/ or scripts/ triggers a rebuild but the +# old code runs, so restart the server after changing those. serve: clean @echo "Starting MkDocs server..." @trap 'echo "Stopping MkDocs server..."; pkill -f "mkdocs serve"' SIGINT SIGTERM; \ - mkdocs serve -f config/en/mkdocs.yml --dev-addr 127.0.0.1:8000 --watch overrides + mkdocs serve -f config/en/mkdocs.yml --dev-addr 127.0.0.1:8000 \ + --watch overrides --watch hooks --watch scripts --watch data build: clean mkdir -p generated # Ensure the folder exists @@ -21,3 +26,12 @@ build: clean killserve: pkill -f "mkdocs serve" + +# --- Specification registry (/specifications) ------------------------------- +# The pages and the /api/ files are built from data/ (see data/README.md). + +specs-check: ## Validate the registry files in data/ and print the display order + python3 scripts/check_specs.py + +specs-export: ## Write the API files (JSON catalogue, full export CSV) to generated/ + python3 scripts/check_specs.py --export generated diff --git a/README.md b/README.md index a0fe7eb..c03278b 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,39 @@ Over 60 public and private signatories have committed to implementing the Princi - [VIA Metropolitan Transit San Antonio](https://www.viainfo.net/) - [Washington State Department of Transportation (WSDOT) Public Transportation Division](https://wsdot.wa.gov/) +## Specification registry (`/specifications`) + +The `/specifications` page, one page per specification under +`/specifications//`, and a read-only JSON API under `/api/` are generated +at build time from the files in [`data/`](data/README.md). GitHub is the +source of truth and the place to contribute: each specification and each +organization is one Markdown file with YAML front matter, and "Suggest a +change" on a specification's page opens that file in GitHub's editor. + +The section is unlisted for now: it is not in the nav bar, the site search or +the sitemap, and its pages carry ``. + +| Path | What it is | +| --- | --- | +| `data/specifications/.md` | One specification: fields in front matter, then `# Description`. | +| `data/organizations/.md` | One organization, its role and a link to its logo. | +| `data/principles.csv` | The criteria and what earns each verdict. | +| `data/licences.csv` | The licence ids specifications may use. | +| `data/README.md` | Every field and allowed value, the ranking, and the API. | +| `scripts/spec_data.py` | Loads, validates and joins the files. | +| `scripts/spec_api.py` | Builds the `/api/` files: catalogue, full records, export CSV, the OpenAPI description, and its Swagger UI page at `/api/docs/`. | +| `scripts/check_specs.py` | `make specs-check`: validation, also run on every pull request. | +| `hooks/specifications.py` | MkDocs hook that renders the pages and publishes the API. | + +```bash +make specs-check # validate the data; fails on any unknown value or key +make specs-export # write the API files to generated/api/ +make serve # check the result +``` + +`make serve` caches the hook module, so restart the server after editing +`hooks/` or `scripts/`: it rebuilds, but with the previous code. + ## Building the site locally 1. In Terminal, change the directory to one where you wish to build the site. diff --git a/config/en/mkdocs.yml b/config/en/mkdocs.yml index 50f8468..51e473e 100644 --- a/config/en/mkdocs.yml +++ b/config/en/mkdocs.yml @@ -20,6 +20,9 @@ theme: logo: assets/images/logo.png favicon: assets/images/favicon.png +hooks: + - ../../hooks/specifications.py + plugins: - search: lang: @@ -57,10 +60,12 @@ extra: extra_javascript: - https://unpkg.com/mermaid@8.5.0/dist/mermaid.min.js - assets/javascript/fix-md-search.js + - assets/javascript/specifications.js extra_css: - https://use.fontawesome.com/releases/v5.13.0/css/all.css - assets/stylesheets/additional.css + - assets/stylesheets/specifications.css markdown_extensions: - attr_list @@ -117,6 +122,7 @@ nav: - Glossary: definitions.md not_in_nav: | + specifications/* admin_draft_review.md coalition.md cosignatories.md diff --git a/data/README.md b/data/README.md new file mode 100644 index 0000000..0745650 --- /dev/null +++ b/data/README.md @@ -0,0 +1,192 @@ +# MDIP specification registry data + +These files are the source of truth for the `/specifications` pages of the +site and for the read-only API under `/api/`. They are kept on GitHub so that +every change is proposed, reviewed and recorded in public. Nothing under +`/specifications` or `/api` is edited by hand: both are generated from this +folder on every build. + +| Path | What it holds | +| --- | --- | +| [`specifications/.md`](specifications/) | One file per specification | +| [`organizations/.md`](organizations/) | One file per organization | +| [`principles.csv`](principles.csv) | The assessment criteria and what earns each verdict | +| [`licences.csv`](licences.csv) | The licence ids a specification may use | + +## How to contribute + +- **Change an entry**: on the specification's page, "Suggest a change" opens + its file in GitHub's editor. Without write access to the repository, GitHub + offers to fork it and open a pull request, which is where the change is + reviewed. +- **Add a specification**: "Propose a specification" on the registry page + opens a new file with every field laid out. Name the file after the + specification's id. +- **Report a problem or ask a question**: open a + [change request](https://github.com/MobilityData/Mobility-Data-Interoperability-Principles/issues/new?template=specification-change.yml). + +Run `make specs-check` before pushing a change made locally; the same check +runs on every pull request and fails on any value, or any key, it does not +recognise. A change is published once it is merged into `main`. + +## File format + +Each entry is a Markdown file: YAML front matter between two `---` lines for +the structured fields, then a `# Description` section in Markdown. + +- The file name is the id, and part of the page URL + (`/specifications//`). Use lower case, with words joined by `-` or `_`, + and do not rename a file without a reason. +- Leave a field out, or empty, when it is not known. +- Put a URL or a date in quotes only if your editor insists; both are read + either way. Dates are `YYYY-MM-DD`. +- Text that spans several lines uses a YAML block (`rationale: |`) followed by + the indented lines. + +### `specifications/.md` + +```yaml +--- +short_name: GTFS Schedule +full_name: General Transit Feed Specification Schedule +homepage_url: https://gtfs.org/ +status: compliant +modes: [public_transport] +maintainers: [mobilitydata] +licences: +- id: Apache-2.0 + scopes: [specification] +- id: CC-BY-3.0 + scopes: [documentation] +first_release_year: 2006 +adoption: + lifecycle: active + tier: hundred_plus + adopters_count: 2950 + adopters_count_updated: '2026-09-29' + adopters_registry_url: https://mobilitydatabase.org/feeds?gtfs=true + confirmed_by_maintainer: true +principles: + cost_restriction_free: + verdict: compliant + rationale: Apache 2.0 + evidence_url: https://github.com/google/transit + # ... one block per principle in principles.csv +--- + +# Description + +Travel information: Transit schedules, geography, fares. +``` + +| Field | Values | +| --- | --- | +| `short_name` | Required. | +| `full_name` | Free text. | +| `homepage_url` | URL. | +| `status` | The coalition's overall finding: `compliant`, `partially_compliant`, `not_compliant`, `inactive` or `under_review`. | +| `modes` | Any of `public_transport`, `microtransit_drt`, `taxi_ridehail`, `bicycle`, `pedestrian_accessibility`, `parking`. Empty shows as "Unclassified". | +| `maintainers` | Organization ids: file names in `organizations/`. | +| `licences` | A list of licences, each with an `id` from `licences.csv` and optional `scopes`: `specification`, `documentation`, `code`, `data`. Empty means no licence is stated. | +| `licence_evidence_url` | URL of the licence text, when it is not the licence's own page. | +| `first_release_year` | Four-digit year. | +| `adoption.lifecycle` | `active`, `early_adoption`, `pilot_adoption`, `pre_adoption` or `inactive`. | +| `adoption.tier` | `hundred_plus`, `early`, `pilot` or `none`. | +| `adoption.adopters_count` | Whole number of known adopters. | +| `adoption.adopters_count_updated` | Date the count was taken. | +| `adoption.adopters_registry_url` | URL of a public list of adopters. | +| `adoption.confirmed_by_maintainer` | `true` or `false`; leave out when unknown. | +| `principles..verdict` | One block per principle in `principles.csv`: `compliant`, `partially_compliant`, `not_compliant`, `unknown` or `not_assessed` (the default when left out). | +| `principles..rationale` | Why the verdict was given. Published as written. | +| `principles..evidence_url` | URL supporting the verdict. | +| `# Description` | What the specification is for, in Markdown. Its first paragraph is the summary in the registry list. | + +`make specs-check` warns, without failing, when `status` disagrees with the +verdicts: `compliant` when every principle is compliant, `not_compliant` when +any principle is not compliant, and `partially_compliant` otherwise. `status` +is still published as recorded, because it is the coalition's decision. + +### `organizations/.md` + +```yaml +--- +name: MobilityData +type: nonprofit +roles: [specification_maintainer] +url: https://mobilitydata.org/ +logo_url: https://mobilitydata.org/.../logo.png +--- + +# Description +``` + +| Field | Values | +| --- | --- | +| `name` | Required. Full name. | +| `short_name` | Optional shorter name for compact places such as filters. | +| `type` | `standards-body`, `nonprofit`, `government`, `industry-consortium`, `academic`, `company`, `community` or `individual`. | +| `roles` | The organization's roles in the registry. Today only `specification_maintainer`. | +| `url` | Home page. | +| `logo_url` | Link to the organization's logo online. When it is empty, or the image does not load, the site shows the organization's initials instead. | +| `# Description` | Optional, in Markdown. | + +## `principles.csv` + +Each `principle` row is one criterion of an open standard in the MDIP +[definition](../docs/en/definitions.md#open_standard). Every specification +file has a `principles.` block for each of them. + +| Column | Meaning | +| --- | --- | +| `id` | Criterion id, used as the key under `principles:` in specification files. | +| `kind` | `principle`, `bonus` or `adoption`. The last two only feed the ranking below. | +| `order` | Display order. | +| `name`, `definition` | What the criterion is. | +| `compliant_when`, `partially_compliant_when`, `not_compliant_when` | What earns each verdict. Shown under "MDIP criteria" on every specification page. | +| `compliant_score`, `partially_compliant_score`, `not_compliant_score` | Points used by the ranking. | + +## Ranking + +The registry lists specifications with the most known adopters +(`adoption.adopters_count`) first. Those without a count follow, ordered by a +ranking score. The score only decides that order: it is not shown on the site +and not published in the API. + +| Criterion | Points | +| --- | --- | +| Each of the five principles | +1 compliant, 0 partially compliant, -1 not compliant. `unknown` and `not_assessed` count 0. | +| Fully compliant | +1 when all five principles are compliant, otherwise 0. | +| Adoption | +1 for `hundred_plus`, 0 for `early` or `pilot`, -1 for `none`. No tier counts 0. | + +The maximum is 7. A specification with no assessed principle sorts after +every assessed one. Ties are broken by status, then by name. +`make specs-check` prints the resulting order with each score. + +## `licences.csv` + +| Column | Values | +| --- | --- | +| `id` | Licence id, used in specification files. Use the [SPDX id](https://spdx.org/licenses/) where one exists. | +| `name` | Full name. | +| `category` | `open`, `restricted` (published openly but with a limiting clause), `closed` (purchase or membership required) or `unknown`. | +| `url` | Licence text. | +| `note` | Optional short explanation shown with the licence. | + +## API + +Every build publishes the joined data as static files under +`https://interoperablemobility.org/api/`. `make specs-export` writes the same +files to `generated/api/` without building the site. + +| File | Contents | +| --- | --- | +| `index.json` | The endpoints and every vocabulary with its labels. | +| `catalogue.json` | One short record per specification, in display order. | +| `specifications.json` | Every specification with its organizations, licences and verdicts joined in. | +| `specifications/.json` | One specification. | +| `organizations.json` | Organizations, with the specifications each one maintains. | +| `principles.json` | The criteria and what earns each verdict. | +| `licences.json` | The licence list. | +| `export.csv` | Every specification as one flat table. | +| `openapi.json` | OpenAPI 3 description of every file above. | +| `docs/` | Interactive documentation (Swagger UI) for `openapi.json`. Not indexed by search engines. | diff --git a/data/licences.csv b/data/licences.csv new file mode 100644 index 0000000..9dcbe1a --- /dev/null +++ b/data/licences.csv @@ -0,0 +1,18 @@ +id,name,category,url,note +Apache-2.0,Apache License 2.0,open,https://www.apache.org/licenses/LICENSE-2.0, +MIT,MIT License,open,https://opensource.org/license/mit, +GPL-3.0,GNU General Public License v3.0,open,https://www.gnu.org/licenses/gpl-3.0.html, +CC0-1.0,Creative Commons Zero v1.0 Universal,open,https://creativecommons.org/publicdomain/zero/1.0/, +CC-BY-3.0,Creative Commons Attribution 3.0,open,https://creativecommons.org/licenses/by/3.0/, +CC-BY-4.0,Creative Commons Attribution 4.0,open,https://creativecommons.org/licenses/by/4.0/, +CC-BY-SA-2.0,Creative Commons Attribution-ShareAlike 2.0,open,https://creativecommons.org/licenses/by-sa/2.0/, +CC-BY-NC-SA-4.0,Creative Commons Attribution-NonCommercial-ShareAlike 4.0,restricted,https://creativecommons.org/licenses/by-nc-sa/4.0/,Non-commercial use only. +CC-BY-ND-4.0,Creative Commons Attribution-NoDerivatives 4.0,restricted,https://creativecommons.org/licenses/by-nd/4.0/,Redistribution of modified versions is not permitted. +ODbL-1.0,Open Data Commons Open Database License v1.0,open,https://opendatacommons.org/licenses/odbl/1-0/, +CDLA-Permissive-2.0,Community Data License Agreement - Permissive v2.0,open,https://cdla.dev/permissive-2-0/, +OGL-3.0,Open Government Licence v3.0 (UK),open,https://www.nationalarchives.gov.uk/doc/open-government-licence/version/3/, +Public-Domain,Public domain,open,https://en.wikipedia.org/wiki/Public_domain, +Custom-Open,Custom open terms,open,,"Published under the maintainer's own terms, which permit free use." +Crown-Copyright,Crown Copyright,restricted,https://www.nationalarchives.gov.uk/information-management/re-using-public-sector-information/uk-government-licensing-framework/crown-copyright/, +Members-Only,Membership or accreditation required,closed,,"The text is obtainable only by members, subscribers or accredited parties." +Proprietary,Proprietary / all rights reserved,closed,,Copyright-protected; the text must be purchased or is not licensed for reuse. diff --git a/data/organizations/apds.md b/data/organizations/apds.md new file mode 100644 index 0000000..cf583aa --- /dev/null +++ b/data/organizations/apds.md @@ -0,0 +1,10 @@ +--- +name: Alliance for Parking Data Standards +short_name: APDS +type: industry-consortium +roles: [specification_maintainer] +url: https://www.allianceforparkingdatastandards.org/ +logo_url: https://allianceforparkingdatastandards.org/wp-content/uploads/2025/01/cropped-APDS-logo-white-text-180x180.png +--- + +# Description diff --git a/data/organizations/apple.md b/data/organizations/apple.md new file mode 100644 index 0000000..096209e --- /dev/null +++ b/data/organizations/apple.md @@ -0,0 +1,9 @@ +--- +name: Apple +type: company +roles: [specification_maintainer] +url: https://www.apple.com/ +logo_url: https://www.apple.com/ac/structured-data/images/open_graph_logo.png?202609042047 +--- + +# Description diff --git a/data/organizations/asam.md b/data/organizations/asam.md new file mode 100644 index 0000000..46fb74f --- /dev/null +++ b/data/organizations/asam.md @@ -0,0 +1,9 @@ +--- +name: ASAM e.V. +type: industry-consortium +roles: [specification_maintainer] +url: https://www.asam.net/ +logo_url: https://www.asam.net/typo3conf/ext/asam_cms/Resources/Public/Icons/apple-icon-180x180.png +--- + +# Description diff --git a/data/organizations/bcs.md b/data/organizations/bcs.md new file mode 100644 index 0000000..a85620c --- /dev/null +++ b/data/organizations/bcs.md @@ -0,0 +1,10 @@ +--- +name: Bundesverband CarSharing +short_name: bcs +type: industry-consortium +roles: [specification_maintainer] +url: https://www.carsharing.de/ +logo_url: https://carsharing.de/themes/custom/bcs23/images/favicon/apple-touch-icon.png +--- + +# Description diff --git a/data/organizations/bts-usdot.md b/data/organizations/bts-usdot.md new file mode 100644 index 0000000..c06bba1 --- /dev/null +++ b/data/organizations/bts-usdot.md @@ -0,0 +1,9 @@ +--- +name: Bureau of Transportation Statistics, US Department of Transportation +short_name: BTS (USDOT) +type: government +roles: [specification_maintainer] +url: https://www.bts.gov/ +--- + +# Description diff --git a/data/organizations/cen.md b/data/organizations/cen.md new file mode 100644 index 0000000..0156e59 --- /dev/null +++ b/data/organizations/cen.md @@ -0,0 +1,10 @@ +--- +name: European Committee for Standardization +short_name: CEN +type: standards-body +roles: [specification_maintainer] +url: https://www.cencenelec.eu/ +logo_url: https://www.cencenelec.eu/dist/images/public/favicon.ico +--- + +# Description diff --git a/data/organizations/codeforamerica.md b/data/organizations/codeforamerica.md new file mode 100644 index 0000000..0d8d5dc --- /dev/null +++ b/data/organizations/codeforamerica.md @@ -0,0 +1,8 @@ +--- +name: Code for America +type: nonprofit +roles: [specification_maintainer] +url: https://codeforamerica.org/ +--- + +# Description diff --git a/data/organizations/david-wasserman.md b/data/organizations/david-wasserman.md new file mode 100644 index 0000000..cfa971d --- /dev/null +++ b/data/organizations/david-wasserman.md @@ -0,0 +1,7 @@ +--- +name: David Wasserman +type: individual +roles: [specification_maintainer] +--- + +# Description diff --git a/data/organizations/etalab.md b/data/organizations/etalab.md new file mode 100644 index 0000000..8255f8f --- /dev/null +++ b/data/organizations/etalab.md @@ -0,0 +1,9 @@ +--- +name: Etalab +type: government +roles: [specification_maintainer] +url: https://www.etalab.gouv.fr/ +logo_url: https://www.data.gouv.fr/nuxt_images/favicon.svg +--- + +# Description diff --git a/data/organizations/european-commission.md b/data/organizations/european-commission.md new file mode 100644 index 0000000..460eae5 --- /dev/null +++ b/data/organizations/european-commission.md @@ -0,0 +1,9 @@ +--- +name: European Commission +type: government +roles: [specification_maintainer] +url: https://ec.europa.eu/ +logo_url: https://commission.europa.eu/themes/contrib/oe_theme/images/favicons/ec/favicon.svg +--- + +# Description diff --git a/data/organizations/fabmob.md b/data/organizations/fabmob.md new file mode 100644 index 0000000..c43fb71 --- /dev/null +++ b/data/organizations/fabmob.md @@ -0,0 +1,10 @@ +--- +name: La Fabrique des Mobilites +short_name: FabMob +type: nonprofit +roles: [specification_maintainer] +url: https://lafabriquedesmobilites.fr/ +logo_url: https://lafabriquedesmobilites.fr/images/favicon2.png +--- + +# Description diff --git a/data/organizations/iso.md b/data/organizations/iso.md new file mode 100644 index 0000000..37ccf6e --- /dev/null +++ b/data/organizations/iso.md @@ -0,0 +1,9 @@ +--- +name: International Organization for Standardization +short_name: ISO +type: standards-body +roles: [specification_maintainer] +url: https://www.iso.org/ +--- + +# Description diff --git a/data/organizations/ite.md b/data/organizations/ite.md new file mode 100644 index 0000000..e9e1597 --- /dev/null +++ b/data/organizations/ite.md @@ -0,0 +1,10 @@ +--- +name: Institute of Transportation Engineers +short_name: ITE +type: nonprofit +roles: [specification_maintainer] +url: https://www.ite.org/ +logo_url: https://www.ite.org/ITEORG/includes/themes/MuraBootstrap3/images/favicon.ico +--- + +# Description diff --git a/data/organizations/itxpt.md b/data/organizations/itxpt.md new file mode 100644 index 0000000..d5a08d5 --- /dev/null +++ b/data/organizations/itxpt.md @@ -0,0 +1,10 @@ +--- +name: Information Technology for Public Transport +short_name: ITxPT +type: industry-consortium +roles: [specification_maintainer] +url: https://itxpt.org/ +logo_url: https://itxpt.org/wp-content/uploads/2022/10/ITxPT-favicon-120x120-1.png +--- + +# Description diff --git a/data/organizations/masstrails.md b/data/organizations/masstrails.md new file mode 100644 index 0000000..6db1f66 --- /dev/null +++ b/data/organizations/masstrails.md @@ -0,0 +1,9 @@ +--- +name: MassTrails Coordinating Body +type: government +roles: [specification_maintainer] +url: https://www.mass.gov/masstrails +logo_url: https://www.mass.gov/favicon.ico +--- + +# Description diff --git a/data/organizations/mobilitydata.md b/data/organizations/mobilitydata.md new file mode 100644 index 0000000..aa7ba76 --- /dev/null +++ b/data/organizations/mobilitydata.md @@ -0,0 +1,9 @@ +--- +name: MobilityData +type: nonprofit +roles: [specification_maintainer] +url: https://mobilitydata.org/ +logo_url: https://mobilitydata.org/wp-content/uploads/2021/04/cropped-flaticon_logo-18-180x180.png +--- + +# Description diff --git a/data/organizations/mobivoc.md b/data/organizations/mobivoc.md new file mode 100644 index 0000000..8094a04 --- /dev/null +++ b/data/organizations/mobivoc.md @@ -0,0 +1,9 @@ +--- +name: Open Mobility Vocabulary +short_name: MobiVoc +type: community +roles: [specification_maintainer] +url: https://www.mobivoc.org/ +--- + +# Description diff --git a/data/organizations/mtc.md b/data/organizations/mtc.md new file mode 100644 index 0000000..87fcc34 --- /dev/null +++ b/data/organizations/mtc.md @@ -0,0 +1,10 @@ +--- +name: Metropolitan Transportation Commission +short_name: MTC +type: government +roles: [specification_maintainer] +url: https://mtc.ca.gov/ +logo_url: https://mtc.ca.gov/themes/custom/mtcca/favicon.ico +--- + +# Description diff --git a/data/organizations/nc-bpaid.md b/data/organizations/nc-bpaid.md new file mode 100644 index 0000000..5c17f26 --- /dev/null +++ b/data/organizations/nc-bpaid.md @@ -0,0 +1,9 @@ +--- +name: National Collaboration on Bicycle, Pedestrian, and Accessibility Infrastructure Data +short_name: NC-BPAID +type: government +roles: [specification_maintainer] +url: https://www.bts.gov/ncbpaid +--- + +# Description diff --git a/data/organizations/nemtac.md b/data/organizations/nemtac.md new file mode 100644 index 0000000..ac219a1 --- /dev/null +++ b/data/organizations/nemtac.md @@ -0,0 +1,9 @@ +--- +name: Non-Emergency Medical Transportation Accreditation Commission +short_name: NEMTAC +type: nonprofit +roles: [specification_maintainer] +url: https://nemtac.org/ +--- + +# Description diff --git a/data/organizations/oasis.md b/data/organizations/oasis.md new file mode 100644 index 0000000..862daa4 --- /dev/null +++ b/data/organizations/oasis.md @@ -0,0 +1,9 @@ +--- +name: OASIS Open +type: standards-body +roles: [specification_maintainer] +url: https://www.oasis-open.org/ +logo_url: https://www.oasis-open.org/wp-content/uploads/2026/07/OASIS-favicon-512x512-1-240x240.png +--- + +# Description diff --git a/data/organizations/ogc.md b/data/organizations/ogc.md new file mode 100644 index 0000000..ef5fadf --- /dev/null +++ b/data/organizations/ogc.md @@ -0,0 +1,10 @@ +--- +name: Open Geospatial Consortium +short_name: OGC +type: standards-body +roles: [specification_maintainer] +url: https://www.ogc.org/ +logo_url: https://cdn-ileijba.nitrocdn.com/RzFQtWVJhnUlNPowswzWHfpcmwWDSHQE/assets/images/source/rev-780680b/www.ogc.org/wp-content/uploads/2026/02/OGC-new-fevicon.svg +--- + +# Description diff --git a/data/organizations/omf.md b/data/organizations/omf.md new file mode 100644 index 0000000..77cc5c0 --- /dev/null +++ b/data/organizations/omf.md @@ -0,0 +1,10 @@ +--- +name: Open Mobility Foundation +short_name: OMF +type: nonprofit +roles: [specification_maintainer] +url: https://www.openmobilityfoundation.org/ +logo_url: https://www.openmobilityfoundation.org/wp-content/uploads/2019/06/favicon.ico +--- + +# Description diff --git a/data/organizations/osm-us.md b/data/organizations/osm-us.md new file mode 100644 index 0000000..190f1b9 --- /dev/null +++ b/data/organizations/osm-us.md @@ -0,0 +1,10 @@ +--- +name: OpenStreetMap US +short_name: OSM US +type: nonprofit +roles: [specification_maintainer] +url: https://openstreetmap.us/ +logo_url: https://openstreetmap.us/img/favicon/apple-icon-180x180.png +--- + +# Description diff --git a/data/organizations/osmf.md b/data/organizations/osmf.md new file mode 100644 index 0000000..fecf78d --- /dev/null +++ b/data/organizations/osmf.md @@ -0,0 +1,10 @@ +--- +name: OpenStreetMap Foundation +short_name: OSMF +type: community +roles: [specification_maintainer] +url: https://osmfoundation.org/ +logo_url: https://osmfoundation.org/osm_logo.svg +--- + +# Description diff --git a/data/organizations/overture.md b/data/organizations/overture.md new file mode 100644 index 0000000..20ca8ae --- /dev/null +++ b/data/organizations/overture.md @@ -0,0 +1,9 @@ +--- +name: Overture Maps Foundation +type: industry-consortium +roles: [specification_maintainer] +url: https://overturemaps.org/ +logo_url: https://overturemaps.org/wp-content/uploads/sites/16/2022/12/cropped-Favicon-300x300.png +--- + +# Description diff --git a/data/organizations/peopleforbikes.md b/data/organizations/peopleforbikes.md new file mode 100644 index 0000000..549816a --- /dev/null +++ b/data/organizations/peopleforbikes.md @@ -0,0 +1,9 @@ +--- +name: PeopleForBikes +type: nonprofit +roles: [specification_maintainer] +url: https://www.peopleforbikes.org/ +logo_url: https://www.peopleforbikes.org/logo.svg +--- + +# Description diff --git a/data/organizations/psrc.md b/data/organizations/psrc.md new file mode 100644 index 0000000..449b984 --- /dev/null +++ b/data/organizations/psrc.md @@ -0,0 +1,10 @@ +--- +name: Puget Sound Regional Council +short_name: PSRC +type: government +roles: [specification_maintainer] +url: https://www.psrc.org/ +logo_url: https://www.psrc.org/themes/custom/psrc/images/apple-touch-icon.png +--- + +# Description diff --git a/data/organizations/sae.md b/data/organizations/sae.md new file mode 100644 index 0000000..733398f --- /dev/null +++ b/data/organizations/sae.md @@ -0,0 +1,9 @@ +--- +name: SAE International +type: standards-body +roles: [specification_maintainer] +url: https://www.sae.org/ +logo_url: https://www.sae.org/favicon.ico +--- + +# Description diff --git a/data/organizations/sfcta.md b/data/organizations/sfcta.md new file mode 100644 index 0000000..0e8d983 --- /dev/null +++ b/data/organizations/sfcta.md @@ -0,0 +1,9 @@ +--- +name: San Francisco County Transportation Authority +short_name: SFCTA +type: government +roles: [specification_maintainer] +url: https://www.sfcta.org/ +--- + +# Description diff --git a/data/organizations/suti.md b/data/organizations/suti.md new file mode 100644 index 0000000..f2e4f0b --- /dev/null +++ b/data/organizations/suti.md @@ -0,0 +1,9 @@ +--- +name: SUTI +type: industry-consortium +roles: [specification_maintainer] +url: https://suti.se/ +logo_url: https://media2.suti.se/2020/02/SUTI-logo-square.png +--- + +# Description diff --git a/data/organizations/tcat-uw.md b/data/organizations/tcat-uw.md new file mode 100644 index 0000000..b0b58e9 --- /dev/null +++ b/data/organizations/tcat-uw.md @@ -0,0 +1,10 @@ +--- +name: Taskar Center for Accessible Technology, University of Washington +short_name: Taskar Center +type: academic +roles: [specification_maintainer] +url: https://tcat.cs.washington.edu/ +logo_url: https://i0.wp.com/tcat.cs.washington.edu/wp-content/uploads/2017/10/cropped-TCATicon-2.png?fit=180%2C180&ssl=1 +--- + +# Description diff --git a/data/organizations/tides-board.md b/data/organizations/tides-board.md new file mode 100644 index 0000000..d849618 --- /dev/null +++ b/data/organizations/tides-board.md @@ -0,0 +1,8 @@ +--- +name: TIDES Board +type: community +roles: [specification_maintainer] +url: https://tides-transit.org/ +--- + +# Description diff --git a/data/organizations/tods-board.md b/data/organizations/tods-board.md new file mode 100644 index 0000000..1067ead --- /dev/null +++ b/data/organizations/tods-board.md @@ -0,0 +1,9 @@ +--- +name: TODS Board +type: community +roles: [specification_maintainer] +url: http://tods-transit.org/ +logo_url: https://tods-transit.org/assets/images/favicon.ico +--- + +# Description diff --git a/data/organizations/tomp-wg.md b/data/organizations/tomp-wg.md new file mode 100644 index 0000000..f3da592 --- /dev/null +++ b/data/organizations/tomp-wg.md @@ -0,0 +1,9 @@ +--- +name: TOMP Working Group +short_name: TOMP-WG +type: industry-consortium +roles: [specification_maintainer] +url: https://tomp-api.org/ +--- + +# Description diff --git a/data/organizations/trb.md b/data/organizations/trb.md new file mode 100644 index 0000000..0572434 --- /dev/null +++ b/data/organizations/trb.md @@ -0,0 +1,10 @@ +--- +name: Transportation Research Board of the National Academies of Sciences, Engineering, and Medicine +short_name: TRB +type: academic +roles: [specification_maintainer] +url: https://www.nationalacademies.org/trb/transportation-research-board +logo_url: https://www.nationalacademies.org/favicon.svg +--- + +# Description diff --git a/data/organizations/uic.md b/data/organizations/uic.md new file mode 100644 index 0000000..337832e --- /dev/null +++ b/data/organizations/uic.md @@ -0,0 +1,9 @@ +--- +name: International Union of Railways +short_name: UIC +type: industry-consortium +roles: [specification_maintainer] +url: https://uic.org/ +--- + +# Description diff --git a/data/organizations/uk-dft.md b/data/organizations/uk-dft.md new file mode 100644 index 0000000..0dfa220 --- /dev/null +++ b/data/organizations/uk-dft.md @@ -0,0 +1,10 @@ +--- +name: UK Department for Transport +short_name: DfT +type: government +roles: [specification_maintainer] +url: https://www.gov.uk/government/organisations/department-for-transport +logo_url: https://www.gov.uk/assets/collections/favicon-d962d21b5bb443f546c097ea21b567cde639adef7370da45be5e349ba8d62d33.svg +--- + +# Description diff --git a/data/organizations/vdv-ets.md b/data/organizations/vdv-ets.md new file mode 100644 index 0000000..8cae1c1 --- /dev/null +++ b/data/organizations/vdv-ets.md @@ -0,0 +1,9 @@ +--- +name: VDV eTicket Service +type: company +roles: [specification_maintainer] +url: https://www.eticket-deutschland.de/ +logo_url: https://www.eticket-deutschland.de/favicon.ico +--- + +# Description diff --git a/data/organizations/vta.md b/data/organizations/vta.md new file mode 100644 index 0000000..8b8687a --- /dev/null +++ b/data/organizations/vta.md @@ -0,0 +1,10 @@ +--- +name: Santa Clara Valley Transportation Authority +short_name: VTA +type: government +roles: [specification_maintainer] +url: https://www.vta.org/ +logo_url: https://www.vta.org/themes/custom/de_theme/src/img/favicons/favicon.svg +--- + +# Description diff --git a/data/organizations/zephyr.md b/data/organizations/zephyr.md new file mode 100644 index 0000000..27461ba --- /dev/null +++ b/data/organizations/zephyr.md @@ -0,0 +1,9 @@ +--- +name: Zephyr Foundation +type: nonprofit +roles: [specification_maintainer] +url: https://zephyrtransport.org/ +logo_url: https://zephyrtransport.org/img/favicon.ico +--- + +# Description diff --git a/data/principles.csv b/data/principles.csv new file mode 100644 index 0000000..dc1d527 --- /dev/null +++ b/data/principles.csv @@ -0,0 +1,8 @@ +id,kind,order,name,definition,compliant_when,compliant_score,partially_compliant_when,partially_compliant_score,not_compliant_when,not_compliant_score +cost_restriction_free,principle,1,Free of cost restrictions,Free from cost and restrictions to use.,"The specification can be obtained, implemented and redistributed at no cost, under an open licence, with no membership, registration or accreditation required.",1,"The specification is free of charge, but obtaining or using it requires registration or membership, or its licence adds terms that limit use (for example non-commercial or no-derivatives clauses).",0,"Obtaining or implementing the specification requires a purchase, a paid membership or a subscription, or its licence does not permit implementation or redistribution.",-1 +publicly_documented,principle,2,Publicly documented,Publicly documented in its entirety.,"The complete specification text is published on a public web page or repository and can be read by anyone, without an account or payment.",1,"Part of the specification is public, but some sections, profiles or schemas are only available on request, after registration, or for a fee.",0,"The specification text is not publicly available, or only a summary or abstract is public.",-1 +independent_maintainer,principle,3,Independent maintainer,Actively maintained by an independent organization with no business interest in the contents of the standard.,"An identified organization actively maintains the specification, and its business interests are not, and would not reasonably appear to be, affected by the contents of the specification.",1,"A maintainer exists, but its independence is limited: the governing body is led by industry participants, a single company or agency keeps significant control, or maintenance is irregular.",0,"No organization actively maintains the specification, or the maintainer's own business interests are directly affected by its contents.",-1 +structured_releases,principle,4,Structured releases,"Contains structured releases, versions, or changelogs.",The specification is published in numbered versions with a public changelog or release notes.,1,"Changes are publicly visible (for example in a repository's commit history), but there are no numbered releases or no changelog.",0,No versions or change history are published.,-1 +open_governance,principle,5,Open governance,"Has an open community of contributors and users, and transparent, inclusive governance.","Anyone can propose changes and take part in decisions on equal terms, through a documented, public process.",1,"Anyone can contribute or comment in public, but decisions are taken by a closed group, or the decision process is not documented.",0,"Taking part in changes or decisions requires a paid membership, or decisions are taken without any public process.",-1 +fully_compliant,bonus,6,Fully compliant,Bonus for a specification that meets every principle.,All five principles are compliant.,1,,,At least one principle is not compliant.,0 +adoption,adoption,7,Adoption,Scale of known real-world deployments (adoption.tier).,hundred_plus: 100 or more known adopters.,1,early or pilot: early adopters or pilot deployments.,0,none: no known adopters.,-1 diff --git a/data/specifications/apds.md b/data/specifications/apds.md new file mode 100644 index 0000000..edbc79c --- /dev/null +++ b/data/specifications/apds.md @@ -0,0 +1,37 @@ +--- +short_name: APDS +full_name: Alliance for Parking Data Standards +homepage_url: https://www.allianceforparkingdatastandards.org/about +status: not_compliant +maintainers: [apds] +licences: +- id: MIT +adoption: + lifecycle: early_adoption + tier: early +principles: + cost_restriction_free: + verdict: compliant + rationale: 'Yes' + evidence_url: https://allianceforparkingdatastandards.org/faq/ + publicly_documented: + verdict: not_compliant + rationale: No - login required + evidence_url: https://allianceforparkingdatastandards.org/ + independent_maintainer: + verdict: not_compliant + rationale: No - Industry Consortium + evidence_url: https://allianceforparkingdatastandards.org/about-us/ + structured_releases: + verdict: compliant + rationale: Yes (Changes recorded on GitHub) + evidence_url: https://github.com/parkingdata/spec + open_governance: + verdict: not_compliant + rationale: 'No' + evidence_url: https://allianceforparkingdatastandards.org/change-process/ +--- + +# Description + +Support for parking facilities diff --git a/data/specifications/asam-opendrive.md b/data/specifications/asam-opendrive.md new file mode 100644 index 0000000..09460ff --- /dev/null +++ b/data/specifications/asam-opendrive.md @@ -0,0 +1,40 @@ +--- +short_name: ASAM OpenDRIVE +full_name: Association for Standardization of Automation and Measuring Systems +homepage_url: https://www.asam.net/standards/detail/opendrive +status: partially_compliant +modes: [taxi_ridehail, microtransit_drt] +maintainers: [asam] +licences: +- id: Members-Only +first_release_year: 2005 +adoption: + lifecycle: active + tier: hundred_plus + confirmed_by_maintainer: true +principles: + cost_restriction_free: + verdict: partially_compliant + rationale: Free to download and use/redistribute under ASAM's own license terms, but access requires either ASAM membership (annual dues scaled to company size) or providing registration details as a non-member; it is not licensed under a conventional open-source license (e.g., MIT/CC)." + evidence_url: https://www.asam.net/standards/detail/opendrive/#:~:text=(The%20download%20of%20the%20standard%20ASAM%20OpenDRIVE,as%20features%20along%20the%20roads%2C%20like%20signals. + publicly_documented: + verdict: compliant + rationale: Yes -- the full specification text is published and downloadable (registration required for non-members) at ASAM's publications site; there is no separate paywalled 'full version'. + evidence_url: https://publications.pages.asam.net/standards/ASAM_OpenDRIVE/ASAM_OpenDRIVE_Specification/v1.9.0/specification/00_preface/00_introduction.html + independent_maintainer: + verdict: partially_compliant + rationale: No in the neutral/public-interest sense -- ASAM e.V. is an industry-membership association (~400 member companies, predominantly automotive OEMs, Tier-1 suppliers, and tool vendors) that funds and governs the standard through paid membership. + evidence_url: https://www.asam.net/about-asam/organization/ + structured_releases: + verdict: compliant + rationale: Yes -- versioned releases with documented changes + evidence_url: https://www.asam.net/standards/detail/opendrive/older/ + open_governance: + verdict: partially_compliant + rationale: Partially -- non-members can read/download the standard, but proposing changes and voting on standard development is generally reserved for dues-paying ASAM members + evidence_url: https://code.asam.net/simulation/openx/-/blob/main/README.md?ref_type=heads +--- + +# Description + +XML-based open format specification (file extension .xodr) for describing the static logic of road networks -- roads, lanes, junctions, signals, and roadside objects -- so that road descriptions can be exchanged between different driving simulators for ADAS/automated-driving development and validation. However the ASAM is the unique owner of the standard itself. diff --git a/data/specifications/beckn.md b/data/specifications/beckn.md new file mode 100644 index 0000000..bcecacc --- /dev/null +++ b/data/specifications/beckn.md @@ -0,0 +1,38 @@ +--- +short_name: Beckn +homepage_url: https://beckn.io/ +status: partially_compliant +modes: [taxi_ridehail] +licences: +- id: CC-BY-NC-SA-4.0 +first_release_year: 2025 +adoption: + lifecycle: early_adoption + tier: early + confirmed_by_maintainer: false +principles: + cost_restriction_free: + verdict: compliant + rationale: CC BY-NC-SA 4.0 + evidence_url: https://github.com/beckn/protocol-specifications-v2?tab=License-1-ov-file + publicly_documented: + verdict: compliant + rationale: 'Yes' + evidence_url: https://github.com/beckn/protocol-specifications-v2/blob/main/docs/README.md + independent_maintainer: + verdict: compliant + rationale: Yes - Global non profit + evidence_url: https://networksforhumanity.org/ + structured_releases: + verdict: partially_compliant + rationale: Changes visible on GitHub + evidence_url: https://github.com/beckn/protocol-specifications-v2 + open_governance: + verdict: compliant + rationale: Open and documented governance + evidence_url: https://github.com/beckn/protocol-specifications-v2/blob/main/GOVERNANCE.md +--- + +# Description + +An open protocol intended to facilitate the direct exchange of value between consumers and providers (peer-to-peer) without needing a central platform diff --git a/data/specifications/cds-m.md b/data/specifications/cds-m.md new file mode 100644 index 0000000..e6f4283 --- /dev/null +++ b/data/specifications/cds-m.md @@ -0,0 +1,39 @@ +--- +short_name: CDS-M +full_name: City Data Standard for Mobility +homepage_url: https://cds-m.com/ +status: partially_compliant +maintainers: [omf] +licences: +- id: Apache-2.0 +first_release_year: 2022 +adoption: + lifecycle: early_adoption + tier: early + confirmed_by_maintainer: true +principles: + cost_restriction_free: + verdict: compliant + rationale: Apache 2.0 + evidence_url: https://github.com/CDSM-WG/CDS-M + publicly_documented: + verdict: compliant + rationale: 'Yes' + evidence_url: https://github.com/CDSM-WG/CDS-M + independent_maintainer: + verdict: compliant + rationale: Yes, led and financed by a coalition of cities + evidence_url: https://www.openmobilityfoundation.org/faq/ + structured_releases: + verdict: compliant + rationale: Changelog available on GitHub + evidence_url: https://github.com/CDSM-WG/CDS-M/pulls?q=is%3Apr+state%3Aclosed + open_governance: + verdict: partially_compliant + rationale: Open contribution on GitHub. + evidence_url: https://github.com/CDSM-WG/CDS-M +--- + +# Description + +Agreement on the use of data across different standards/specifications diff --git a/data/specifications/cds.md b/data/specifications/cds.md new file mode 100644 index 0000000..ae2a89a --- /dev/null +++ b/data/specifications/cds.md @@ -0,0 +1,40 @@ +--- +short_name: CDS +full_name: Curb Data Specification +homepage_url: https://github.com/openmobilityfoundation/curb-data-specification +status: compliant +modes: [parking, microtransit_drt, taxi_ridehail] +maintainers: [omf] +licences: +- id: CC-BY-4.0 +first_release_year: 2021 +adoption: + lifecycle: active + tier: early + confirmed_by_maintainer: true +principles: + cost_restriction_free: + verdict: compliant + rationale: 'Yes' + evidence_url: https://github.com/openmobilityfoundation/curb-data-specification?tab=License-1-ov-file + publicly_documented: + verdict: compliant + rationale: 'Yes' + evidence_url: https://github.com/openmobilityfoundation/curb-data-specification/ + independent_maintainer: + verdict: compliant + rationale: Yes, led and financed by a coalition of cities + evidence_url: https://www.openmobilityfoundation.org/faq/ + structured_releases: + verdict: compliant + rationale: 'Yes' + evidence_url: https://github.com/openmobilityfoundation/curb-data-specification/blob/main/ReleaseNotes.md + open_governance: + verdict: compliant + rationale: Open contribution on GitHub and working groups. Final release need OMF approbation + evidence_url: https://github.com/openmobilityfoundation/governance/blob/main/technical/ReleaseGuidelines.md#approval-by-the-open-mobility-foundation +--- + +# Description + +A data specification to help cities manage their curb zone and parking programs and surrounding areas, and measure the utilization and impact. diff --git a/data/specifications/connect-os.md b/data/specifications/connect-os.md new file mode 100644 index 0000000..3f60491 --- /dev/null +++ b/data/specifications/connect-os.md @@ -0,0 +1,40 @@ +--- +short_name: Connect-OS +full_name: Open Sidewalks +homepage_url: https://uwescience.github.io/DSSG2016-Sidewalks/ +status: partially_compliant +modes: [pedestrian_accessibility] +maintainers: [tcat-uw] +licences: +- id: CC-BY-ND-4.0 +first_release_year: 2022 +adoption: + lifecycle: early_adoption + tier: early + confirmed_by_maintainer: false +principles: + cost_restriction_free: + verdict: compliant + rationale: Yes, CC BY-ND 4.0 + evidence_url: https://github.com/OpenSidewalks/OpenSidewalks-Schema?tab=License-1-ov-file + publicly_documented: + verdict: compliant + rationale: 'Yes' + evidence_url: https://github.com/OpenSidewalks/OpenSidewalks-Schema/blob/main/README.md + independent_maintainer: + verdict: compliant + rationale: Yes (Academic) + evidence_url: https://sidewalks.washington.edu/get-involved/ + structured_releases: + verdict: compliant + rationale: Yes (Changes recorded on GitHub) + evidence_url: https://github.com/OpenSidewalks/OpenSidewalks-Schema + open_governance: + verdict: partially_compliant + rationale: Open participation on GtHub, but no official governance + evidence_url: https://sidewalks.washington.edu/get-involved/ +--- + +# Description + +Aims to make pedestian pathways, particularly sidewalks, part of the open data transportation network. diff --git a/data/specifications/curblr.md b/data/specifications/curblr.md new file mode 100644 index 0000000..809e31f --- /dev/null +++ b/data/specifications/curblr.md @@ -0,0 +1,27 @@ +--- +short_name: CurbLR +homepage_url: https://curblr.org/ +status: inactive +licences: +- id: CC0-1.0 + scopes: [specification] +- id: MIT + scopes: [code] +adoption: + lifecycle: inactive +principles: + cost_restriction_free: + verdict: not_assessed + publicly_documented: + verdict: not_assessed + independent_maintainer: + verdict: not_assessed + structured_releases: + verdict: not_compliant + open_governance: + verdict: not_assessed +--- + +# Description + +Standard for curb regulations diff --git a/data/specifications/fast-trips.md b/data/specifications/fast-trips.md new file mode 100644 index 0000000..a47ce3a --- /dev/null +++ b/data/specifications/fast-trips.md @@ -0,0 +1,25 @@ +--- +short_name: Fast-Trips +homepage_url: http://fast-trips.mtc.ca.gov/ +status: inactive +maintainers: [mtc, psrc, sfcta] +licences: +- id: Apache-2.0 +adoption: + lifecycle: inactive +principles: + cost_restriction_free: + verdict: not_assessed + publicly_documented: + verdict: not_assessed + independent_maintainer: + verdict: not_assessed + structured_releases: + verdict: not_assessed + open_governance: + verdict: not_assessed +--- + +# Description + +Want to analyze human transporation behavior to help lessen urban congestion diff --git a/data/specifications/gatis.md b/data/specifications/gatis.md new file mode 100644 index 0000000..faaff43 --- /dev/null +++ b/data/specifications/gatis.md @@ -0,0 +1,40 @@ +--- +short_name: GATIS +full_name: National Collaboration on Geospatial Bike, Pedestrian, and Accessibility Infrastructure Data +homepage_url: https://github.com/dotbts/BPA +status: partially_compliant +modes: [bicycle, pedestrian_accessibility] +maintainers: [bts-usdot, nc-bpaid] +licences: +- id: CC0-1.0 +first_release_year: 2024 +adoption: + lifecycle: pre_adoption + tier: none + confirmed_by_maintainer: false +principles: + cost_restriction_free: + verdict: compliant + rationale: Yes, CC0 1.0 Universal Public Domain + evidence_url: https://github.com/dotbts/BPA?tab=License-1-ov-file + publicly_documented: + verdict: compliant + rationale: Yes, viewable on the GATIS Explorer + evidence_url: https://dotbts.github.io/BPA/gatis_explorer/pages/presence_tables.html + independent_maintainer: + verdict: partially_compliant + rationale: No / Hybrid Model. The steward is a cross-sector public-private-academic collaboration (NC-BPAID), but it is heavily backed, hosted, and led by a federal government agency (USDOT/BTS) + evidence_url: https://github.com/dotbts/BPA/blob/main/documents/drafts/CollaborationFramework.md + structured_releases: + verdict: compliant + rationale: Yes. Managed publicly on the dotbts/BPA GitHub Repository. + evidence_url: https://github.com/dotbts/BPA/wiki + open_governance: + verdict: compliant + rationale: Yes. Development relies on collaborative working groups and voting processes that include government agencies, academic institutions, non-profit advocates, private tech firms, and individual contributors + evidence_url: https://github.com/dotbts/BPA/blob/main/documents/drafts/CollaborationFramework.md +--- + +# Description + +Nationwide geospatial data layers capturing the extent, connectivity, and condition of bicycle, pedestrian, and accessibility infrastructure in public spaces. diff --git a/data/specifications/gbfs.md b/data/specifications/gbfs.md new file mode 100644 index 0000000..a629653 --- /dev/null +++ b/data/specifications/gbfs.md @@ -0,0 +1,40 @@ +--- +short_name: GBFS +full_name: General Bikeshare Feed Specification +homepage_url: https://gbfs.org/ +status: compliant +modes: [bicycle] +maintainers: [mobilitydata] +licences: +- id: Apache-2.0 +first_release_year: 2015 +adoption: + lifecycle: active + tier: hundred_plus + confirmed_by_maintainer: true +principles: + cost_restriction_free: + verdict: compliant + rationale: 'Yes' + evidence_url: https://gbfs.org/documentation/faq/ + publicly_documented: + verdict: compliant + rationale: 'Yes' + evidence_url: https://gbfs.org/documentation/reference/ + independent_maintainer: + verdict: compliant + rationale: 'Yes' + evidence_url: https://gbfs.org/documentation/process/ + structured_releases: + verdict: compliant + rationale: 'Yes' + evidence_url: https://github.com/MobilityData/gbfs/releases + open_governance: + verdict: compliant + rationale: 'Yes' + evidence_url: https://gbfs.org/documentation/process/ +--- + +# Description + +A standardized data feed specification for real-time information on the status and availability of shared mobility and bikeshare systems. diff --git a/data/specifications/gcfs.md b/data/specifications/gcfs.md new file mode 100644 index 0000000..f7b840f --- /dev/null +++ b/data/specifications/gcfs.md @@ -0,0 +1,18 @@ +--- +short_name: General Carpooling Feed Specification - Actionfigure +full_name: General Carpooling Feed Specification - Actionfigure +status: under_review +principles: + cost_restriction_free: + verdict: not_assessed + publicly_documented: + verdict: not_assessed + independent_maintainer: + verdict: not_assessed + structured_releases: + verdict: not_assessed + open_governance: + verdict: not_assessed +--- + +# Description diff --git a/data/specifications/gmns.md b/data/specifications/gmns.md new file mode 100644 index 0000000..0b949b4 --- /dev/null +++ b/data/specifications/gmns.md @@ -0,0 +1,44 @@ +--- +short_name: GMNS +full_name: General Modeling Network Specification +homepage_url: https://github.com/zephyr-data-specs/GMNS +status: not_compliant +modes: [bicycle, pedestrian_accessibility, public_transport] +maintainers: [zephyr] +licences: +- id: Apache-2.0 + scopes: [specification] +- id: MIT + scopes: [documentation] +first_release_year: 2018 +adoption: + lifecycle: early_adoption + tier: early +principles: + cost_restriction_free: + verdict: compliant + rationale: Apache 2.0 + evidence_url: https://rosap.ntl.bts.gov/view/dot/44136 + publicly_documented: + verdict: compliant + rationale: Everything is available on GitHub and GitHub Pages. + evidence_url: https://github.com/zephyr-data-specs/GMNS + independent_maintainer: + verdict: partially_compliant + rationale: |- + Board does include a minority of seats for industry. + But not maintained by an independant organization. + evidence_url: https://zephyrtransport.org/ + structured_releases: + verdict: compliant + rationale: Yes, formal version tagging is maintained on GitHub. + evidence_url: https://github.com/zephyr-data-specs/GMNS/releases + open_governance: + verdict: not_compliant + rationale: Operates with an open-source, community-driven governance model via the Zephyr Foundation. However, voting is only possible as a member. + evidence_url: https://zephyrtransport.org/ +--- + +# Description + +Provides a common machine- and human-readable format for sharing routable road and multi-modal network files. Used primarily for static and dynamic transportation planning and operations modeling. diff --git a/data/specifications/gofs.md b/data/specifications/gofs.md new file mode 100644 index 0000000..6a2abb7 --- /dev/null +++ b/data/specifications/gofs.md @@ -0,0 +1,41 @@ +--- +short_name: GOFS +full_name: General On-Demand Feed Specification (GOFS) +homepage_url: https://github.com/MobilityData/GOFS/blob/main/governance.md +status: compliant +modes: [microtransit_drt, taxi_ridehail] +maintainers: [mobilitydata] +licences: +- id: Apache-2.0 + scopes: [specification, documentation] +first_release_year: 2021 +adoption: + lifecycle: early_adoption + tier: early + confirmed_by_maintainer: true +principles: + cost_restriction_free: + verdict: compliant + rationale: Yes, Apache 2.0 + evidence_url: https://github.com/MobilityData/GOFS + publicly_documented: + verdict: compliant + rationale: Yes Specification, JSON schema, and governance process are all published in full on GitHub + evidence_url: https://github.com/MobilityData/GOFS/blob/main/reference.md + independent_maintainer: + verdict: compliant + rationale: Yes MobilityData is an independent nonprofit; the Maintainer role facilitates but does not vote on changes. + evidence_url: https://mobilitydata.org/history/ + structured_releases: + verdict: compliant + rationale: Yes Structured MAJOR/MINOR release cycle with changelog via GitHub releases. + evidence_url: https://github.com/MobilityData/GOFS + open_governance: + verdict: compliant + rationale: Anyone can propose changes and anyone can vote; requires unanimous consensus including at least one producer and one consumer vote — the same open governance model used by GBFS/GTFS. + evidence_url: https://github.com/MobilityData/GOFS/blob/main/governance.md +--- + +# Description + +A lightweight, open data format for purely on-demand transport services (no fixed routes/schedule) to describe their offering for consumption by transport applications diff --git a/data/specifications/gtfs-flex.md b/data/specifications/gtfs-flex.md new file mode 100644 index 0000000..fd8254b --- /dev/null +++ b/data/specifications/gtfs-flex.md @@ -0,0 +1,46 @@ +--- +short_name: GTFS' Flex features +full_name: GTFS-Flex (flexible / demand-responsive service extensions to core GTFS). +homepage_url: https://gtfs.org/ +status: compliant +modes: [microtransit_drt] +maintainers: [mobilitydata] +licences: +- id: Apache-2.0 + scopes: [specification] +- id: CC-BY-3.0 + scopes: [documentation] +first_release_year: 2020 +adoption: + lifecycle: early_adoption + tier: early + confirmed_by_maintainer: true +principles: + cost_restriction_free: + verdict: compliant + rationale: Apache 2.0 + evidence_url: https://github.com/google/transit?tab=Apache-2.0-1-ov-file + publicly_documented: + verdict: compliant + rationale: Apache 2.0 + evidence_url: https://creativecommons.org/licenses/by/3.0/ + independent_maintainer: + verdict: compliant + rationale: |- + Official Maintainer: MobilityData. + + (GitHub repo still held by Google. Google delegated admin rights to MobilityData.) + evidence_url: https://gtfs.org/community/governance/gtfs-schedule-governance/roles/ + structured_releases: + verdict: compliant + rationale: Changelog + evidence_url: https://gtfs.org/documentation/schedule/change-history/revision-history/ + open_governance: + verdict: compliant + rationale: Anyone can vote with equal rights. + evidence_url: https://gtfs.org/community/governance/gtfs-schedule-governance/roles/ +--- + +# Description + +Extends core GTFS Schedule to describe flexible and demand-responsive service: booking rules, flexible service zones/areas, and continuous stops/pickup-dropoff windows diff --git a/data/specifications/gtfs-realtime.md b/data/specifications/gtfs-realtime.md new file mode 100644 index 0000000..0d337a6 --- /dev/null +++ b/data/specifications/gtfs-realtime.md @@ -0,0 +1,52 @@ +--- +short_name: GTFS Realtime +full_name: General Transit Feed Specification Realtime +homepage_url: https://gtfs.org/ +status: compliant +modes: [public_transport] +maintainers: [mobilitydata] +licences: +- id: Apache-2.0 + scopes: [specification] +- id: CC-BY-3.0 + scopes: [documentation] +first_release_year: 2011 +adoption: + lifecycle: active + tier: hundred_plus + adopters_count: 632 + adopters_count_updated: '2026-09-29' + adopters_registry_url: https://mobilitydatabase.org/feeds?gtfs_rt=true + confirmed_by_maintainer: true +principles: + cost_restriction_free: + verdict: compliant + rationale: Apache 2.0 + evidence_url: https://github.com/google/transit?tab=Apache-2.0-1-ov-file + publicly_documented: + verdict: compliant + rationale: CC-BY-3.0 + evidence_url: https://creativecommons.org/licenses/by/3.0/ + independent_maintainer: + verdict: compliant + rationale: |- + Defacto Steward: MobilityData. + + (GitHub repo still held by Google. Google delegated admin rights to MobilityData.) + evidence_url: https://gtfs.org/community/governance/gtfs-realtime-amendment-process/ + structured_releases: + verdict: compliant + rationale: Changelog + evidence_url: https://gtfs.org/documentation/realtime/change-history/revision-history/ + open_governance: + verdict: compliant + rationale: |- + Anyone can vote with equal rights. + + Note: Interface between governance and actual protobuf file implementation + evidence_url: https://gtfs.org/community/governance/gtfs-realtime-amendment-process/ +--- + +# Description + +Extends GTFS to support live updates. diff --git a/data/specifications/gtfs-schedule.md b/data/specifications/gtfs-schedule.md new file mode 100644 index 0000000..5b0d95d --- /dev/null +++ b/data/specifications/gtfs-schedule.md @@ -0,0 +1,49 @@ +--- +short_name: GTFS Schedule +full_name: General Transit Feed Specification Schedule +homepage_url: https://gtfs.org/ +status: compliant +modes: [public_transport] +maintainers: [mobilitydata] +licences: +- id: Apache-2.0 + scopes: [specification] +- id: CC-BY-3.0 + scopes: [documentation] +first_release_year: 2006 +adoption: + lifecycle: active + tier: hundred_plus + adopters_count: 2950 + adopters_count_updated: '2026-09-29' + adopters_registry_url: https://mobilitydatabase.org/feeds?gtfs=true + confirmed_by_maintainer: true +principles: + cost_restriction_free: + verdict: compliant + rationale: Apache 2.0 + evidence_url: https://github.com/google/transit?tab=Apache-2.0-1-ov-file + publicly_documented: + verdict: compliant + rationale: Apache 2.0 + evidence_url: https://creativecommons.org/licenses/by/3.0/ + independent_maintainer: + verdict: compliant + rationale: |- + Official Maintainer: MobilityData. + + (GitHub repo still held by Google. Google delegated admin rights to MobilityData.) + evidence_url: https://gtfs.org/community/governance/gtfs-schedule-governance/roles/ + structured_releases: + verdict: compliant + rationale: Changelog + evidence_url: https://gtfs.org/documentation/schedule/change-history/revision-history/ + open_governance: + verdict: compliant + rationale: Anyone can vote with equal rights. + evidence_url: https://gtfs.org/community/governance/gtfs-schedule-governance/roles/ +--- + +# Description + +Travel information: Transit schedules, geography, fares. diff --git a/data/specifications/gtns.md b/data/specifications/gtns.md new file mode 100644 index 0000000..a8214eb --- /dev/null +++ b/data/specifications/gtns.md @@ -0,0 +1,27 @@ +--- +short_name: GTNS +full_name: General Travel Network Specification +homepage_url: https://zephyrtransport.org/trb17projects/7-general-travel-network-specification/ +status: inactive +modes: [public_transport] +maintainers: [zephyr] +first_release_year: 2020 +adoption: + lifecycle: inactive + tier: none +principles: + cost_restriction_free: + verdict: not_assessed + publicly_documented: + verdict: not_assessed + independent_maintainer: + verdict: not_assessed + structured_releases: + verdict: not_assessed + open_governance: + verdict: not_assessed +--- + +# Description + +Page no longer accessible diff --git a/data/specifications/imdf.md b/data/specifications/imdf.md new file mode 100644 index 0000000..47c3d2b --- /dev/null +++ b/data/specifications/imdf.md @@ -0,0 +1,40 @@ +--- +short_name: IMDF +full_name: Indoor Mapping Data Format +homepage_url: https://register.apple.com/resources/imdf/ +status: partially_compliant +modes: [pedestrian_accessibility] +maintainers: [ogc, apple] +licences: +- id: Custom-Open +first_release_year: 2019 +adoption: + lifecycle: active + tier: hundred_plus + confirmed_by_maintainer: true +principles: + cost_restriction_free: + verdict: compliant + rationale: 'Yes' + evidence_url: https://docs.ogc.org/cs/20-094/index.html + publicly_documented: + verdict: compliant + rationale: Yes, full schemas, relationship rules, feature definitions, and validator tools are publicly accessible online. + evidence_url: https://docs.ogc.org/cs/20-094/index.html + independent_maintainer: + verdict: partially_compliant + rationale: No, it was originally developed as a proprietary specification by Apple Inc. before being submitted to the independent OGC for community standardization. + evidence_url: https://docs.ogc.org/cs/20-094/index.html + structured_releases: + verdict: compliant + rationale: 'Yes' + evidence_url: https://docs.ogc.org/cs/20-094/CHANGELOG/index.html + open_governance: + verdict: partially_compliant + rationale: Yes, governance is managed under the member-driven OGC consensus process alongside Apple community feedback. + evidence_url: https://github.com/OGC-tracker/OGC-Standards-Tracker/ +--- + +# Description + +Indoor mapping, primarily used to create accurate, geo-referenced digital maps of indoor spaces. It’s often adopted for use in buildings like airports, malls, hospitals, universities, and other complex indoor environments diff --git a/data/specifications/inspire.md b/data/specifications/inspire.md new file mode 100644 index 0000000..61378dd --- /dev/null +++ b/data/specifications/inspire.md @@ -0,0 +1,30 @@ +--- +short_name: INSPIRE +homepage_url: https://eur-lex.europa.eu/FR/legal-content/summary/the-eu-s-infrastructure-for-spatial-information-inspire.html +status: inactive +maintainers: [european-commission] +licences: +- id: CC-BY-4.0 +first_release_year: 2007 +adoption: + lifecycle: early_adoption + tier: early +principles: + cost_restriction_free: + verdict: not_assessed + rationale: 'Yes' + publicly_documented: + verdict: not_assessed + independent_maintainer: + verdict: not_assessed + structured_releases: + verdict: not_assessed + open_governance: + verdict: not_assessed + rationale: Change requests through GitHub + evidence_url: https://op.europa.eu/en/web/eu-vocabularies/inspire +--- + +# Description + +Works with a variety of different geospatial data. there is a general focus on environmental components. not exclusively focused on travel. diff --git a/data/specifications/iso-20524-2.md b/data/specifications/iso-20524-2.md new file mode 100644 index 0000000..034acf5 --- /dev/null +++ b/data/specifications/iso-20524-2.md @@ -0,0 +1,42 @@ +--- +short_name: ISO GDF 5.1 +full_name: |- + ISO 20524-2:2020 + Intelligent transport systems — Geographic Data Files (GDF) GDF5.1 Public Right of Way +homepage_url: https://www.iso.org/standard/72494.html +status: not_compliant +modes: [bicycle, pedestrian_accessibility] +maintainers: [iso, cen] +licences: +- id: Proprietary +first_release_year: 1980 +adoption: + lifecycle: pre_adoption + tier: none + confirmed_by_maintainer: false +principles: + cost_restriction_free: + verdict: not_compliant + rationale: Accessing the full specification text requires a paid purchase from ISO or a national standards body + evidence_url: https://www.iso.org/copyright.html + publicly_documented: + verdict: not_compliant + rationale: No. Payment needed. + evidence_url: https://www.iso.org/standard/72494.html + independent_maintainer: + verdict: compliant + rationale: Yes. Maintained by the International Organization for Standardization (ISO), an independent, international non-governmental standards entity. + evidence_url: https://www.iso.org/home.html + structured_releases: + verdict: not_compliant + rationale: Tracked via ISO lifecycle stage codes (currently at stage 30.99) + evidence_url: https://www.iso.org/stage-codes.html#60_00 + open_governance: + verdict: not_compliant + rationale: No, governance is open to national bodies only. + evidence_url: https://www.iso.org/get-involved.html +--- + +# Description + +Standardizes geographic data formats for Intelligent Transport Systems, ensuring interoperability between automated driving systems, Cooperative ITS, and multi-modal transport. diff --git a/data/specifications/iso-4448.md b/data/specifications/iso-4448.md new file mode 100644 index 0000000..0cac518 --- /dev/null +++ b/data/specifications/iso-4448.md @@ -0,0 +1,38 @@ +--- +short_name: ISO 4448 +full_name: Intelligent transport systems — Public-area mobile robots (PMR) +homepage_url: https://www.iso.org/standard/81068.html +status: not_compliant +maintainers: [iso, cen] +licences: +- id: Proprietary +first_release_year: 2024 +adoption: + lifecycle: inactive + confirmed_by_maintainer: false +principles: + cost_restriction_free: + verdict: not_compliant + rationale: No. Payment needed. + evidence_url: https://www.iso.org/copyright.html + publicly_documented: + verdict: not_compliant + rationale: Accessing the full specification text requires a paid purchase from ISO or a national standards body + evidence_url: https://www.iso.org/standard/81068.html + independent_maintainer: + verdict: compliant + rationale: Yes. Maintained by the International Organization for Standardization (ISO), an independent, international non-governmental standards entity. + evidence_url: https://www.iso.org/home.html + structured_releases: + verdict: not_compliant + rationale: Tracked via ISO lifecycle stage codes + evidence_url: https://www.iso.org/standard/81068.html + open_governance: + verdict: not_compliant + rationale: No, governance is open to national bodies only. + evidence_url: https://www.iso.org/get-involved.html +--- + +# Description + +Standard for managing real-time mobility flows among automated vehicles and devices at sidewalks and kerbs. diff --git a/data/specifications/iso-7874.md b/data/specifications/iso-7874.md new file mode 100644 index 0000000..1c09e64 --- /dev/null +++ b/data/specifications/iso-7874.md @@ -0,0 +1,40 @@ +--- +short_name: ISO 7874 +full_name: Mobility integration multimodal pricing related rules +homepage_url: https://www.iso.org/standard/84091.html +status: not_compliant +modes: [microtransit_drt] +maintainers: [iso] +licences: +- id: Proprietary + scopes: [specification, documentation] +adoption: + lifecycle: pre_adoption + tier: none + confirmed_by_maintainer: false +principles: + cost_restriction_free: + verdict: not_compliant + rationale: Accessing the full specification text requires a paid purchase from ISO or a national standards body + evidence_url: https://www.iso.org/copyright.html + publicly_documented: + verdict: not_compliant + rationale: No. Payment needed. + evidence_url: https://www.iso.org/standard/82966.html#lifecycle + independent_maintainer: + verdict: compliant + rationale: Yes. Maintained by the International Organization for Standardization (ISO), an independent, international non-governmental standards entity. + evidence_url: https://www.iso.org/home.html + structured_releases: + verdict: not_compliant + rationale: Tracked via ISO lifecycle stage codes (currently at stage 30.99) + evidence_url: https://www.iso.org/stage-codes.html#60_00 + open_governance: + verdict: not_compliant + rationale: No, governance is open to national bodies only. + evidence_url: https://www.iso.org/get-involved.html +--- + +# Description + +Defines the overarching conceptual framework and multi-modal fare/pricing guidelines for integrating public transport, micro-mobility, and on-demand transit into unified digital ecosystems (MaaS/MOD) diff --git a/data/specifications/itxpt.md b/data/specifications/itxpt.md new file mode 100644 index 0000000..972d649 --- /dev/null +++ b/data/specifications/itxpt.md @@ -0,0 +1,38 @@ +--- +short_name: ITxPT +full_name: Information Technology for Public Transport +homepage_url: https://itxpt.org/ +status: not_compliant +modes: [public_transport] +maintainers: [itxpt] +licences: +- id: Proprietary + scopes: [documentation, specification] +first_release_year: 2013 +adoption: + lifecycle: active + tier: hundred_plus + confirmed_by_maintainer: true +principles: + cost_restriction_free: + verdict: not_compliant + rationale: Free, but restricted. + publicly_documented: + verdict: not_compliant + rationale: Login is required for released versions. In-development versions are restricted ITxPT members. + independent_maintainer: + verdict: partially_compliant + rationale: Industry represtentation appears to be prioritzed over public sector. + structured_releases: + verdict: compliant + rationale: Release history + evidence_url: https://wiki.itxpt.org/docs/Specifications_release_history + open_governance: + verdict: not_compliant + rationale: 'Different membership levels provide different levels of governance access: https://itxpt.org/membership/' + evidence_url: https://itxpt.org/membership/ +--- + +# Description + +IT architecture for public transport, currently focused on on-vehicle communication diff --git a/data/specifications/ixsi.md b/data/specifications/ixsi.md new file mode 100644 index 0000000..e8bd67c --- /dev/null +++ b/data/specifications/ixsi.md @@ -0,0 +1,27 @@ +--- +short_name: IXSI +homepage_url: https://github.com/RWTH-i5-IDSG/ixsi +status: inactive +modes: [public_transport] +maintainers: [bcs] +licences: +- id: Apache-2.0 + scopes: [code] +- id: CC-BY-3.0 + scopes: [documentation] +principles: + cost_restriction_free: + verdict: not_assessed + publicly_documented: + verdict: not_assessed + independent_maintainer: + verdict: not_assessed + structured_releases: + verdict: not_assessed + open_governance: + verdict: not_assessed +--- + +# Description + +Carshare focus. Shares information about vehicles, parking saces, and eletric charging spaces availability diff --git a/data/specifications/mat.md b/data/specifications/mat.md new file mode 100644 index 0000000..aa17117 --- /dev/null +++ b/data/specifications/mat.md @@ -0,0 +1,38 @@ +--- +short_name: MAT +full_name: Multimodal and Accessible Transportation (MAT) Standards- Phase 2 +homepage_url: https://www.ite.org/technical-resources/topics/standards/mat-and-vru-cybersecurity/multimodal-and-accessible-transportation-mat-standards-phase-2/ +status: not_compliant +modes: [bicycle, pedestrian_accessibility] +maintainers: [ite] +first_release_year: 2021 +adoption: + lifecycle: pre_adoption + tier: none + confirmed_by_maintainer: false +principles: + cost_restriction_free: + verdict: unknown + rationale: 'Yes' + evidence_url: https://www.ite.org/technical-resources/topics/standards/mat-and-cybersecurity/ + publicly_documented: + verdict: compliant + rationale: Yes, accessible via the ITE Library + evidence_url: https://www.ite.org/technical-resources/topics/standards/mat-and-cybersecurity/ + independent_maintainer: + verdict: compliant + rationale: Yes (Independent non-profit professional association) + evidence_url: https://www.ite.org/navigation/technical-resources/resources1/standards/ + structured_releases: + verdict: not_compliant + rationale: 'No' + evidence_url: https://www.ite.org/technical-resources/topics/standards/mat-and-cybersecurity/ + open_governance: + verdict: partially_compliant + rationale: Yes. Building a governance foundation for a stakeholder-led coordination committee to generate consensus around standards development activities. + evidence_url: https://www.ite.org/ITEORG/assets/File/Standards/ITEStandardDevProcess-time%20frames.pdf +--- + +# Description + +Address gaps in standards and deployment guidance for multimodal and accessible transportation and vulnerable road user technologies when integrating with intelligent transportation systems environments. diff --git a/data/specifications/mds.md b/data/specifications/mds.md new file mode 100644 index 0000000..f65b393 --- /dev/null +++ b/data/specifications/mds.md @@ -0,0 +1,45 @@ +--- +short_name: MDS +full_name: Mobility Data Specification +homepage_url: https://github.com/openmobilityfoundation/mobility-data-specification/tree/release-2.1.0 +status: compliant +modes: [microtransit_drt, taxi_ridehail, bicycle] +maintainers: [omf] +licences: +- id: CC-BY-4.0 + scopes: [specification] +licence_evidence_url: https://github.com/openmobilityfoundation/mobility-data-specification/blob/main/LICENSE +first_release_year: 2018 +adoption: + lifecycle: active + tier: hundred_plus + adopters_count: 258 + adopters_count_updated: '2026-09-28' + adopters_registry_url: https://www.openmobilityfoundation.org/mds-users/ + confirmed_by_maintainer: true +principles: + cost_restriction_free: + verdict: compliant + rationale: 'Yes' + evidence_url: https://www.openmobilityfoundation.org/about-mds/ + publicly_documented: + verdict: compliant + rationale: 'Yes' + evidence_url: https://github.com/openmobilityfoundation/mobility-data-specification/blob/main/LICENSE + independent_maintainer: + verdict: compliant + rationale: Yes, led and financed by a coalition of cities + evidence_url: https://www.openmobilityfoundation.org/faq/ + structured_releases: + verdict: compliant + rationale: 'Yes' + evidence_url: https://github.com/openmobilityfoundation/mobility-data-specification/releases + open_governance: + verdict: compliant + rationale: Open contribution on GitHub and working groups. Final release need OMF approbation + evidence_url: https://github.com/openmobilityfoundation/governance/blob/main/technical/ReleaseGuidelines.md#approval-by-the-open-mobility-foundation +--- + +# Description + +A data specification to enable right-of-way regulation, digital policy, geofencing, and two-way communication between mobility companies and public agencies worldwide for any regulated, shared vehicle. diff --git a/data/specifications/mobivoc.md b/data/specifications/mobivoc.md new file mode 100644 index 0000000..15ec50b --- /dev/null +++ b/data/specifications/mobivoc.md @@ -0,0 +1,23 @@ +--- +short_name: MobiVoc +homepage_url: https://github.com/vocol/mobivoc +status: inactive +maintainers: [mobivoc] +licences: +- id: CC-BY-4.0 +principles: + cost_restriction_free: + verdict: not_assessed + publicly_documented: + verdict: not_assessed + independent_maintainer: + verdict: not_assessed + structured_releases: + verdict: not_assessed + open_governance: + verdict: not_assessed +--- + +# Description + +The goal is to establish a standard vocabulary for data mobility technologies diff --git a/data/specifications/mqtt.md b/data/specifications/mqtt.md new file mode 100644 index 0000000..5b73de8 --- /dev/null +++ b/data/specifications/mqtt.md @@ -0,0 +1,37 @@ +--- +short_name: MQTT +full_name: Message Queuing Telemetry Transport +homepage_url: https://mqtt.org/ +status: not_compliant +maintainers: [oasis] +first_release_year: 1999 +adoption: + lifecycle: active + tier: hundred_plus + confirmed_by_maintainer: true +principles: + cost_restriction_free: + verdict: compliant + rationale: Yes, however, the OASIS name is under copyright. + evidence_url: https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc3901001 + publicly_documented: + verdict: compliant + rationale: 'Yes' + evidence_url: https://docs.oasis-open.org/mqtt/mqtt/v5.0/mqtt-v5.0.html + independent_maintainer: + verdict: compliant + rationale: 'Yes' + evidence_url: https://www.oasis-open.org/org/ + structured_releases: + verdict: compliant + rationale: 'Yes' + evidence_url: https://docs.oasis-open.org/mqtt/mqtt/v5.0/mqtt-v5.0.html + open_governance: + verdict: not_compliant + rationale: No, only payed members can contribute + evidence_url: https://www.oasis-open.org/join-a-tc/ +--- + +# Description + +MQTT for Sensor Networks is aimed at embedded devices on non-TCP/IP networks, such as Zigbee. MQTT-SN is a publish/subscribe messaging protocol for wireless sensor networks (WSN), with the aim of extending the MQTT protocol beyond the reach of TCP/IP infrastructure for Sensor and Actuator solutions. diff --git a/data/specifications/mtlfs.md b/data/specifications/mtlfs.md new file mode 100644 index 0000000..db6efcf --- /dev/null +++ b/data/specifications/mtlfs.md @@ -0,0 +1,37 @@ +--- +short_name: MTLFS +full_name: Managed and Tolled Lanes Feed Specification +homepage_url: https://github.com/vta/Managed-and-Tolled-Lanes-Feed-Specification +status: not_compliant +maintainers: [vta] +licences: +- id: MIT +first_release_year: 2016 +adoption: + lifecycle: inactive +principles: + cost_restriction_free: + verdict: compliant + rationale: Yes for the specification text/code itself -- released under the MIT License, which permits free use, modification, and redistribution. + evidence_url: https://github.com/vta/Managed-and-Tolled-Lanes-Feed-Specification?tab=MIT-1-ov-file + publicly_documented: + verdict: partially_compliant + rationale: 'Documented only as an unfinished draft/proposal: the spec (mtlfs.md) and supporting materials (including a 2018 conference poster) are publicly posted on GitHub, but it was never finalized into a ratified standard.' + evidence_url: https://github.com/vta/Managed-and-Tolled-Lanes-Feed-Specification/blob/master/README.md + independent_maintainer: + verdict: not_compliant + rationale: No -- authored and hosted by a single public transit/toll agency (VTA) + evidence_url: https://github.com/vta + structured_releases: + verdict: partially_compliant + rationale: No -- the repository has no formal release tags or versioned releases. + evidence_url: https://github.com/vta/Managed-and-Tolled-Lanes-Feed-Specification + open_governance: + verdict: not_compliant + rationale: No formal governance process identified -- this is a single-agency proposal open only for public GitHub issue comments (17 open, unresolved issues) + evidence_url: https://github.com/vta/Managed-and-Tolled-Lanes-Feed-Specification +--- + +# Description + +A proposed open data-feed format for managed/tolled highway lanes: defines agency, toll-program, facility, gantry-location, and real-time toll-pricing data so that third-party applications (trip planners, payment tools, smartphone apps) can consume live tolling information. diff --git a/data/specifications/nchrp-07-31.md b/data/specifications/nchrp-07-31.md new file mode 100644 index 0000000..3b4902e --- /dev/null +++ b/data/specifications/nchrp-07-31.md @@ -0,0 +1,38 @@ +--- +short_name: NCHRP 07-31 +full_name: "NCHRP 07-31 \nState DOT and Tribal Use of Active Transportation Data: Practices, Sources, Needs, and Gaps" +homepage_url: https://apps.trb.org/cmsfeed/TRBNetProjectDisplay.asp?ProjectID=4946 +status: not_compliant +modes: [bicycle, pedestrian_accessibility] +maintainers: [trb] +first_release_year: 2021 +adoption: + lifecycle: pre_adoption + tier: none + confirmed_by_maintainer: false +principles: + cost_restriction_free: + verdict: unknown + rationale: Unknown + evidence_url: https://nitc.trec.stage.rc.pdx.edu/research/project/1599/State_DOT_and_Tribal_Use_of_Active_Transportation_Data:_Practices,_Sources,_Needs,_and_Gaps_(NCHRP_07-31) + publicly_documented: + verdict: not_compliant + rationale: No information yet + evidence_url: https://nitc.trec.stage.rc.pdx.edu/research/project/1599/State_DOT_and_Tribal_Use_of_Active_Transportation_Data:_Practices,_Sources,_Needs,_and_Gaps_(NCHRP_07-31) + independent_maintainer: + verdict: compliant + rationale: Yes (Administered by TRB under the independent National Academies of Sciences, Engineering, and Medicine) + evidence_url: https://trec.pdx.edu/ + structured_releases: + verdict: not_compliant + rationale: 'No' + evidence_url: https://nitc.trec.stage.rc.pdx.edu/research/project/1599/State_DOT_and_Tribal_Use_of_Active_Transportation_Data:_Practices,_Sources,_Needs,_and_Gaps_(NCHRP_07-31) + open_governance: + verdict: not_compliant + rationale: No documented governance process + evidence_url: https://nitc.trec.stage.rc.pdx.edu/research/project/1599/State_DOT_and_Tribal_Use_of_Active_Transportation_Data:_Practices,_Sources,_Needs,_and_Gaps_(NCHRP_07-31) +--- + +# Description + +Develop a playbook for state DOTs and tribes on the use of active transportation data and draft data standardization formats to improve data quality and facilitate data sharing nationally. diff --git a/data/specifications/nchrp-08-177.md b/data/specifications/nchrp-08-177.md new file mode 100644 index 0000000..adc3536 --- /dev/null +++ b/data/specifications/nchrp-08-177.md @@ -0,0 +1,38 @@ +--- +short_name: NCHRP 08-177 +full_name: NCHRP 08-177 Digitizing Bicycle and Pedestrian Treatments for Promoting Active Transportation Equity and Safety +homepage_url: https://apps.trb.org/cmsfeed/TRBNetProjectDisplay.asp?ProjectID=5496 +status: not_compliant +modes: [bicycle, pedestrian_accessibility] +maintainers: [trb] +first_release_year: 2011 +adoption: + lifecycle: pre_adoption + tier: none + confirmed_by_maintainer: false +principles: + cost_restriction_free: + verdict: compliant + rationale: 'Yes' + evidence_url: https://highways.dot.gov/safety/data-analysis-tools/rsdp/rsdp-tools/national-cooperative-highway-research-program-nchrp + publicly_documented: + verdict: compliant + rationale: Yes (Published as an open research guide and toolkit on the TRB website) + evidence_url: https://highways.dot.gov/safety/data-analysis-tools/rsdp/rsdp-tools/national-cooperative-highway-research-program-nchrp + independent_maintainer: + verdict: compliant + rationale: Yes, administered by the TRB under the National Academies, a private, non-governmental institution providing independent advice. + evidence_url: https://www.nationalacademies.org/units/TRB-Transporta-23-P-586 + structured_releases: + verdict: not_compliant + rationale: 'No' + evidence_url: https://apps.trb.org/cmsfeed/TRBNetProjectDisplay.asp?ProjectID=5496 + open_governance: + verdict: partially_compliant + rationale: Partially. Research and TRB guidelines. + evidence_url: https://apps.trb.org/cmsfeed/TRBNetProjectDisplay.asp?ProjectID=5496 +--- + +# Description + +Research project and data collection toolkit for digitizing bicycle and pedestrian treatments to promote active transportation equity and safety. diff --git a/data/specifications/nemtac-1000.md b/data/specifications/nemtac-1000.md new file mode 100644 index 0000000..6dbb582 --- /dev/null +++ b/data/specifications/nemtac-1000.md @@ -0,0 +1,38 @@ +--- +short_name: NEMTAC 1000 +full_name: NEMTAC 1000 +homepage_url: https://nemtac.co/standard-hub/ +status: not_compliant +modes: [microtransit_drt] +maintainers: [nemtac] +first_release_year: 2018 +adoption: + lifecycle: active + tier: early + confirmed_by_maintainer: false +principles: + cost_restriction_free: + verdict: not_compliant + rationale: Accreditation needed (minimum $600) + evidence_url: https://nemtac.co/accreditation/ + publicly_documented: + verdict: not_compliant + rationale: No, requires purchase to access the full specification. + evidence_url: https://nemtac.co/wp-content/uploads/2024/10/NEMTAC-1002-202X-Modes-of-Transportation-Public-Comment-Draft.pdf + independent_maintainer: + verdict: compliant + rationale: 'Yes' + evidence_url: https://nemtac.co/about-nemtac/ + structured_releases: + verdict: not_compliant + rationale: 'No' + evidence_url: https://nemtac.co/standard-hub/ + open_governance: + verdict: not_compliant + rationale: Governance is mentionned, but not fully documented. + evidence_url: https://nemtac.co/about-nemtac/ +--- + +# Description + +Non-Emergency Medical Transportation Service Levels of Service Standard diff --git a/data/specifications/netex.md b/data/specifications/netex.md new file mode 100644 index 0000000..cb3e012 --- /dev/null +++ b/data/specifications/netex.md @@ -0,0 +1,41 @@ +--- +short_name: NeTEx +full_name: Network, Timetables and Fare Exchange, CEN TS 16614-1 to 3 (SG9) +homepage_url: https://transmodel-cen.eu/index.php/netex/ +status: not_compliant +modes: [microtransit_drt, public_transport, taxi_ridehail] +maintainers: [cen] +licences: +- id: GPL-3.0 + scopes: [specification] +- id: Apache-2.0 +first_release_year: 2009 +adoption: + lifecycle: active + tier: early +principles: + cost_restriction_free: + verdict: compliant + rationale: GPL 3.0 + evidence_url: https://github.com/NeTEx-CEN/NeTEx/blob/v2.0/LICENSE + publicly_documented: + verdict: partially_compliant + rationale: Unable to find full public documentation. + evidence_url: https://transmodel-cen.eu/index.php/netex/ + independent_maintainer: + verdict: not_compliant + rationale: Governmental institution + evidence_url: https://transmodel-cen.eu/index.php/legal-context/ + structured_releases: + verdict: compliant + rationale: Versioning + evidence_url: http://github.com/NeTEx-CEN/NeTEx/releases + open_governance: + verdict: not_compliant + rationale: One vote per EU and some neighbouring countries. - Working Group participation requirements are unclear, potentail for industry conflict. + evidence_url: https://www.cencenelec.eu/european-standardization/cen-and-cenelec/ +--- + +# Description + +Exchange of network, schedules, and complex fare data. diff --git a/data/specifications/ojp.md b/data/specifications/ojp.md new file mode 100644 index 0000000..26c0300 --- /dev/null +++ b/data/specifications/ojp.md @@ -0,0 +1,41 @@ +--- +short_name: OJP +full_name: Open Journey Planner +homepage_url: https://transmodel-cen.eu/index.php/ojp/ +status: not_compliant +modes: [public_transport] +maintainers: [cen] +licences: +- id: Proprietary +- id: Apache-2.0 + scopes: [specification] +first_release_year: 2017 +adoption: + lifecycle: active + tier: pilot +principles: + cost_restriction_free: + verdict: unknown + rationale: No licence specified + evidence_url: https://transmodel-cen.eu/index.php/ojp/ + publicly_documented: + verdict: partially_compliant + rationale: Only the technical XML schema and API endpoints are published. + evidence_url: https://github.com/VDVde/OJP + independent_maintainer: + verdict: not_compliant + rationale: Governmental institution + evidence_url: https://www.itsstandards.eu/ + structured_releases: + verdict: compliant + rationale: Yes, major releases (OJP 1.0, 2.0) are published via CEN and GitHub. + evidence_url: https://github.com/VDVde/OJP/blob/develop/CHANGELOG.md + open_governance: + verdict: not_compliant + rationale: Unclear, no information on adoped changes after a pull request. + evidence_url: https://www.cencenelec.eu/european-standardization/cen-and-cenelec/ +--- + +# Description + +Defines an open API standard for distributed journey planning. It allows different systems and agencies to collaborate and calculate cross-border or multi-modal trips without needing a central data repository. diff --git a/data/specifications/opensidewalks.md b/data/specifications/opensidewalks.md new file mode 100644 index 0000000..65b20d9 --- /dev/null +++ b/data/specifications/opensidewalks.md @@ -0,0 +1,40 @@ +--- +short_name: OpenSidewalks +full_name: OpenSidewalks +homepage_url: https://github.com/OpenSidewalks/OpenSidewalks-Schema +status: compliant +modes: [bicycle, pedestrian_accessibility] +maintainers: [tcat-uw] +licences: +- id: ODbL-1.0 +- id: CC-BY-SA-2.0 +first_release_year: 2016 +adoption: + lifecycle: early_adoption + tier: early +principles: + cost_restriction_free: + verdict: compliant + rationale: Yes, CC BY-ND 4.0 + evidence_url: https://github.com/OpenSidewalks/OpenSidewalks-Schema + publicly_documented: + verdict: compliant + rationale: Yes (Data dictionary and schemas fully published on web) + evidence_url: https://sidewalks.washington.edu/2024/05/30/schema/ + independent_maintainer: + verdict: compliant + rationale: Yes (Academic research center at University of Washington) + evidence_url: https://tcat.cs.washington.edu/ + structured_releases: + verdict: compliant + rationale: Yes (Version control and schema revision logs) + evidence_url: https://github.com/OpenSidewalks/OpenSidewalks-Schema#schema-versions + open_governance: + verdict: compliant + rationale: Yes (Academic-led open research and public collaboration) + evidence_url: https://github.com/OpenSidewalks/OpenSidewalks-Schema +--- + +# Description + +Internationally distributed pedestrian/bike transport graph layer, including infrastructure data for accessibility, safety, and pedestrian preferences. Includes extended attributes for advanced accessibility and route planning features. diff --git a/data/specifications/opentrails.md b/data/specifications/opentrails.md new file mode 100644 index 0000000..e51247a --- /dev/null +++ b/data/specifications/opentrails.md @@ -0,0 +1,34 @@ +--- +short_name: OpenTrails +full_name: Open Trail System Specification +status: inactive +modes: [pedestrian_accessibility] +maintainers: [codeforamerica] +licences: +- id: CC0-1.0 +first_release_year: 2014 +adoption: + lifecycle: inactive + tier: none + confirmed_by_maintainer: false +principles: + cost_restriction_free: + verdict: compliant + rationale: 'Yes' + publicly_documented: + verdict: compliant + rationale: Yes, schemas and documentation are openly available on GitHub. + independent_maintainer: + verdict: compliant + rationale: Yes (Independent civic tech non-profit organization) + structured_releases: + verdict: compliant + rationale: 'Yes' + open_governance: + verdict: compliant + rationale: Yes (Open community participation and development on GitHub) +--- + +# Description + +Data standard for describing and sharing information about recreational trails, trailheads, and park systems. diff --git a/data/specifications/opra.md b/data/specifications/opra.md new file mode 100644 index 0000000..9ca9620 --- /dev/null +++ b/data/specifications/opra.md @@ -0,0 +1,42 @@ +--- +short_name: OpRa +full_name: Operational Raw Data +homepage_url: http://transmodel-cen.eu/opra +status: not_compliant +modes: [public_transport] +maintainers: [cen] +licences: +- id: GPL-3.0 + scopes: [specification] +first_release_year: 2018 +adoption: + lifecycle: pre_adoption + tier: none + confirmed_by_maintainer: false +principles: + cost_restriction_free: + verdict: compliant + rationale: GPL 3.0 + evidence_url: https://github.com/OpRa-CEN/OpRa/blob/1.0rc/LICENSE + publicly_documented: + verdict: not_compliant + rationale: Unable to find full public documentation. + independent_maintainer: + verdict: partially_compliant + rationale: |- + CEN + + Working Group participation requirements are unclear, potentail for industry conflict. + structured_releases: + verdict: compliant + rationale: Versioning is planned. + evidence_url: https://github.com/OpRa-CEN/OpRa/releases + open_governance: + verdict: not_compliant + rationale: One vote per EU and some neighbouring countries. + evidence_url: https://www.cencenelec.eu/european-standardization/cen-and-cenelec/ +--- + +# Description + +Archive of observed data about transit operations for future planning diff --git a/data/specifications/osdm.md b/data/specifications/osdm.md new file mode 100644 index 0000000..1d31231 --- /dev/null +++ b/data/specifications/osdm.md @@ -0,0 +1,40 @@ +--- +short_name: OSDM +full_name: Open Sales and Distribution Model +homepage_url: https://osdm.io/ +status: partially_compliant +modes: [public_transport] +maintainers: [uic] +licences: +- id: Apache-2.0 +first_release_year: 2021 +adoption: + lifecycle: active + tier: early + confirmed_by_maintainer: true +principles: + cost_restriction_free: + verdict: compliant + rationale: Apache-2.0 license + evidence_url: https://github.com/UnionInternationalCheminsdeFer/OSDM?tab=Apache-2.0-1-ov-file + publicly_documented: + verdict: compliant + rationale: 'Yes' + evidence_url: https://osdm.io/spec/getting-started/ + independent_maintainer: + verdict: partially_compliant + rationale: No - Industry Consortium + evidence_url: https://www.uic.org/ + structured_releases: + verdict: compliant + rationale: 'Yes' + evidence_url: https://osdm.io/ + open_governance: + verdict: partially_compliant + rationale: Governance is documented. Changes can be proposed by anyone, but implementation and decision is limited to some stakeholders. + evidence_url: https://osdm.io/organization/ +--- + +# Description + +Open sales & distribution API for public transportation. diff --git a/data/specifications/osm-bna-guide.md b/data/specifications/osm-bna-guide.md new file mode 100644 index 0000000..0a1fb15 --- /dev/null +++ b/data/specifications/osm-bna-guide.md @@ -0,0 +1,40 @@ +--- +short_name: OSM BNA Guide +full_name: OpenStreetMap Editing Guide for the Bicycle Network Analysis +homepage_url: https://docs.google.com/document/d/11q2PzR5r8BYafKYmJypqElxPC6ddYXRUPNFsv3jHw-A/edit +status: compliant +modes: [bicycle, pedestrian_accessibility] +maintainers: [peopleforbikes] +licences: +- id: MIT +- id: Custom-Open +first_release_year: 2018 +adoption: + lifecycle: early_adoption + tier: early +principles: + cost_restriction_free: + verdict: compliant + rationale: Yes (Free open-source tools and datasets) + evidence_url: https://peopleforbikes.github.io/brokenspoke-analyzer/about.html + publicly_documented: + verdict: compliant + rationale: Yes (Complete methodology and open-source codebase on GitHub) + evidence_url: https://peopleforbikes.github.io/brokenspoke-analyzer/about.html + independent_maintainer: + verdict: compliant + rationale: Yes (Managed by non-profit industry coalition PeopleForBikes) + evidence_url: https://www.peopleforbikes.org/ + structured_releases: + verdict: compliant + rationale: Yes (Versioned releases on GitHub) and changelog + evidence_url: https://github.com/PeopleForBikes/brokenspoke-analyzer/blob/main/CHANGELOG.md + open_governance: + verdict: compliant + rationale: Yes (Transparent methodology with open feedback) + evidence_url: https://peopleforbikes.github.io/code-of-conduct/ +--- + +# Description + +Guide for editing OpenStreetMap data to support the Bicycle Network Analysis, used to evaluate connectivity and safety of bicycle infrastructure. diff --git a/data/specifications/osm-us-pedestrian-wg.md b/data/specifications/osm-us-pedestrian-wg.md new file mode 100644 index 0000000..5f5020d --- /dev/null +++ b/data/specifications/osm-us-pedestrian-wg.md @@ -0,0 +1,40 @@ +--- +short_name: OSM US Ped WG +full_name: OpenStreetMap US Pedestrian Working Group +homepage_url: https://openstreetmap.us/news/2024/02/pedestrian-working-group/ +status: compliant +modes: [pedestrian_accessibility] +maintainers: [osm-us] +licences: +- id: CC-BY-SA-2.0 +first_release_year: 2024 +adoption: + lifecycle: pre_adoption + tier: none + confirmed_by_maintainer: false +principles: + cost_restriction_free: + verdict: compliant + rationale: Yes, CC-BY-SA 2.0 + evidence_url: https://wiki.openstreetmap.org/wiki/Foundation/Local_Chapters/United_States/Pedestrian_Working_Group/Schema + publicly_documented: + verdict: compliant + rationale: Yes, all tagging practices and guidelines are publicly documented on the OpenStreetMap Wiki. + evidence_url: https://wiki.openstreetmap.org/wiki/Foundation/Local_Chapters/United_States/Pedestrian_Working_Group/Guide#Mapping_Guidelines + independent_maintainer: + verdict: compliant + rationale: Yes, managed by an independent 501(c)(3) nonprofit organization (OSM US). + evidence_url: https://en.wikipedia.org/wiki/OpenStreetMap + structured_releases: + verdict: compliant + rationale: Yes. Versioned snapshot subpages and draft pages for the schema and guide are maintained on the OSM Wiki. + evidence_url: https://wiki.openstreetmap.org/wiki/Foundation/Local_Chapters/United_States/Pedestrian_Working_Group/Guide + open_governance: + verdict: compliant + rationale: Yes. Composed of volunteers who collaborate openly through public monthly meetings and Slack channels. + evidence_url: https://wiki.openstreetmap.org/wiki/Foundation/Local_Chapters/United_States/Pedestrian_Working_Group#Get_Involved! +--- + +# Description + +US working group focused on improving pedestrian data and standards in OpenStreetMap, with aims to enhance mapping accuracy for pedestrian infrastructure. diff --git a/data/specifications/osm.md b/data/specifications/osm.md new file mode 100644 index 0000000..0e7eb78 --- /dev/null +++ b/data/specifications/osm.md @@ -0,0 +1,41 @@ +--- +short_name: OSM +full_name: OpenStreetMap Pedestrian and Active Mobility Data Model +homepage_url: https://www.openstreetmap.org/about +status: compliant +modes: [bicycle, pedestrian_accessibility] +maintainers: [osmf] +licences: +- id: ODbL-1.0 +- id: CC-BY-SA-2.0 +first_release_year: 2004 +adoption: + lifecycle: active + tier: hundred_plus + confirmed_by_maintainer: true +principles: + cost_restriction_free: + verdict: compliant + rationale: Yes, free to use and distribute under ODbL terms (requires attribution and share-alike for modified derivative databases). + evidence_url: https://www.openstreetmap.org/ + publicly_documented: + verdict: compliant + rationale: Yes, all tagging schemes, feature proposals, data models, and API endpoints are fully documented on the open OSM Wiki. + evidence_url: https://wiki.openstreetmap.org/wiki/Map_features + independent_maintainer: + verdict: compliant + rationale: Yes, managed by a non-profit international foundation (OSMF) completely independent of single corporate or state control. + evidence_url: https://blog.openstreetmap.org/faq/ + structured_releases: + verdict: compliant + rationale: Yes, real-time minute/hourly/daily replication diffs, changeset logs, and community wiki proposal history track all updates continuously. + evidence_url: https://wiki.openstreetmap.org/w/index.php?title=Map_features&action=history + open_governance: + verdict: compliant + rationale: Yes, tagging standards and operational decisions are governed through open community RFC proposals, public voting, and OSMF community elections. + evidence_url: https://community.openstreetmap.org/t/how-is-core-software-governance-managed-in-openstreetmap-and-who-decides-which-projects-are-included/131621 +--- + +# Description + +A collaborative project to create a free editable map of the world, widely used for various applications, including routing, research, and geographic analysis. diff --git a/data/specifications/overture-maps.md b/data/specifications/overture-maps.md new file mode 100644 index 0000000..46d28d0 --- /dev/null +++ b/data/specifications/overture-maps.md @@ -0,0 +1,47 @@ +--- +short_name: Overture Maps +full_name: Overture Maps Foundation Transportation Data Specification +homepage_url: https://docs.overturemaps.org/guides/transportation/ +status: not_compliant +modes: [bicycle, pedestrian_accessibility] +maintainers: [overture] +licences: +- id: CC-BY-4.0 + scopes: [specification] +- id: CDLA-Permissive-2.0 + scopes: [data] +- id: ODbL-1.0 + scopes: [data] +first_release_year: 2023 +adoption: + lifecycle: active + tier: hundred_plus + confirmed_by_maintainer: true +principles: + cost_restriction_free: + verdict: compliant + rationale: CC-BY-4.0 + evidence_url: https://github.com/OvertureMaps/schema + publicly_documented: + verdict: compliant + rationale: Yes (Full schemas and specifications on GitHub and developer portal) + evidence_url: https://github.com/OvertureMaps/schema + independent_maintainer: + verdict: compliant + rationale: Yes (Independent non-profit under Linux Foundation) + evidence_url: https://overturemaps.org/about/who-we-are/ + structured_releases: + verdict: compliant + rationale: Yes (Monthly data releases and schema change tracking) + evidence_url: https://docs.overturemaps.org/release-calendar/ + open_governance: + verdict: not_compliant + rationale: |- + Contribution is open, but voting is possible as member only. + + All members are welcome to attend and contribute, but voting rights and team leadership roles are reserved for Steering and General members. +--- + +# Description + +Create an interoperable open map that allows for the use of multiple data inventories and standards. diff --git a/data/specifications/rdex.md b/data/specifications/rdex.md new file mode 100644 index 0000000..38fb4b9 --- /dev/null +++ b/data/specifications/rdex.md @@ -0,0 +1,27 @@ +--- +short_name: RDEX+ +homepage_url: https://rdex.fabmob.io/ +status: inactive +modes: [microtransit_drt] +maintainers: [fabmob] +licences: +- id: Apache-2.0 +adoption: + lifecycle: early_adoption + tier: early +principles: + cost_restriction_free: + verdict: not_assessed + publicly_documented: + verdict: not_assessed + independent_maintainer: + verdict: not_assessed + structured_releases: + verdict: not_assessed + open_governance: + verdict: not_assessed +--- + +# Description + +Facilitates data exchange between different carpooling agencies diff --git a/data/specifications/sae-j2735.md b/data/specifications/sae-j2735.md new file mode 100644 index 0000000..3532855 --- /dev/null +++ b/data/specifications/sae-j2735.md @@ -0,0 +1,40 @@ +--- +short_name: SAE J2735 +full_name: V2X Communications Message Set Dictionary +homepage_url: https://www.sae.org/standards/content/j2735_202007/ +status: not_compliant +modes: [bicycle, pedestrian_accessibility] +maintainers: [sae] +licences: +- id: Proprietary +first_release_year: 2009 +adoption: + lifecycle: pre_adoption + tier: none + confirmed_by_maintainer: false +principles: + cost_restriction_free: + verdict: not_compliant + rationale: 'No' + evidence_url: https://www.sae.org/standards/j2735_202007-v2x-communications-message-set-dictionary + publicly_documented: + verdict: not_compliant + rationale: No, requires purchase to access the full specification. + evidence_url: https://www.sae.org/standards/j2735_202007-v2x-communications-message-set-dictionary + independent_maintainer: + verdict: compliant + rationale: Yes (Independent non-profit professional association) + evidence_url: https://www.sae.org + structured_releases: + verdict: compliant + rationale: Yes. Version history and tracked revisions are managed via official SAE standard release documentation. + evidence_url: https://www.sae.org/standards/j2735_202007-v2x-communications-message-set-dictionary + open_governance: + verdict: not_compliant + rationale: Unclear + evidence_url: https://www.sae.org/standards/development +--- + +# Description + +This SAE standard specifies a message set, and its data frames and data elements, for use by applications that use vehicle-to-everything (V2X) communications systems. diff --git a/data/specifications/schema-amenagements-cyclables.md b/data/specifications/schema-amenagements-cyclables.md new file mode 100644 index 0000000..9d94832 --- /dev/null +++ b/data/specifications/schema-amenagements-cyclables.md @@ -0,0 +1,40 @@ +--- +short_name: Schéma de données d'aménagements cyclables (France) +full_name: Schéma de données d'aménagements cyclables +homepage_url: https://schema.data.gouv.fr/etalab/schema-amenagements-cyclables/ +status: not_compliant +modes: [bicycle] +maintainers: [etalab] +licences: +- id: ODbL-1.0 +first_release_year: 2020 +adoption: + lifecycle: active + tier: hundred_plus + confirmed_by_maintainer: true +principles: + cost_restriction_free: + verdict: compliant + rationale: 'Yes' + evidence_url: https://schema.data.gouv.fr/etalab/schema-amenagements-cyclables/ + publicly_documented: + verdict: compliant + rationale: 'Yes' + evidence_url: https://schema.data.gouv.fr/etalab/schema-amenagements-cyclables/0.3.5/documentation.html + independent_maintainer: + verdict: not_compliant + rationale: No (governmental) + evidence_url: https://schema.data.gouv.fr/etalab/schema-amenagements-cyclables/ + structured_releases: + verdict: compliant + rationale: 'Yes' + evidence_url: https://schema.data.gouv.fr/etalab/schema-amenagements-cyclables/0.3.5/CHANGELOG.html + open_governance: + verdict: compliant + rationale: 'Yes' + evidence_url: https://schema.data.gouv.fr/contribuer.html +--- + +# Description + +Schema for bike lane infrastructure. diff --git a/data/specifications/schema-stationnement-cyclable.md b/data/specifications/schema-stationnement-cyclable.md new file mode 100644 index 0000000..496807d --- /dev/null +++ b/data/specifications/schema-stationnement-cyclable.md @@ -0,0 +1,39 @@ +--- +short_name: Schéma de données pour le stationnement cyclable (France) +full_name: Schéma de données pour le stationnement cyclable +homepage_url: https://schema.data.gouv.fr/etalab/schema-stationnement-cyclable/ +status: not_compliant +modes: [bicycle] +maintainers: [etalab] +licences: +- id: ODbL-1.0 +first_release_year: 2020 +adoption: + lifecycle: early_adoption + tier: early +principles: + cost_restriction_free: + verdict: compliant + rationale: 'Yes' + evidence_url: https://schema.data.gouv.fr/etalab/schema-stationnement-cyclable/ + publicly_documented: + verdict: compliant + rationale: 'Yes' + evidence_url: https://schema.data.gouv.fr/etalab/schema-stationnement-cyclable/ + independent_maintainer: + verdict: not_compliant + rationale: 'No' + evidence_url: https://schema.data.gouv.fr/etalab/schema-stationnement-cyclable/ + structured_releases: + verdict: compliant + rationale: 'Yes' + evidence_url: https://schema.data.gouv.fr/etalab/schema-stationnement-cyclable/ + open_governance: + verdict: compliant + rationale: 'Yes' + evidence_url: https://schema.data.gouv.fr/contribuer.html +--- + +# Description + +Schema for bike parking data. diff --git a/data/specifications/shared-row.md b/data/specifications/shared-row.md new file mode 100644 index 0000000..04f153d --- /dev/null +++ b/data/specifications/shared-row.md @@ -0,0 +1,41 @@ +--- +short_name: shared-row +full_name: shared-row +homepage_url: https://github.com/d-wasserman/shared-row +status: not_compliant +modes: [bicycle, pedestrian_accessibility] +maintainers: [david-wasserman] +licences: +- id: Apache-2.0 +- id: CC-BY-4.0 +first_release_year: 2020 +adoption: + lifecycle: pre_adoption + tier: none + confirmed_by_maintainer: false +principles: + cost_restriction_free: + verdict: compliant + rationale: 'Yes' + evidence_url: https://github.com/d-wasserman/shared-row?tab=License-1-ov-file + publicly_documented: + verdict: compliant + rationale: Yes, available on GitHub + evidence_url: https://github.com/d-wasserman/shared-row/tree/main/specification + independent_maintainer: + verdict: not_compliant + rationale: No (individual) + evidence_url: https://github.com/d-wasserman + structured_releases: + verdict: compliant + rationale: Yes (Changes recorded on GitHub) + evidence_url: https://github.com/d-wasserman/shared-row/tree/main + open_governance: + verdict: compliant + rationale: Clear and open governance through GitHub + evidence_url: https://github.com/d-wasserman/shared-row/blob/main/CONTRIBUTING.md +--- + +# Description + +Data standard for shared-road infrastructure with designated pedestrian/bicycle areas. diff --git a/data/specifications/siri.md b/data/specifications/siri.md new file mode 100644 index 0000000..b623e87 --- /dev/null +++ b/data/specifications/siri.md @@ -0,0 +1,43 @@ +--- +short_name: SIRI +full_name: Service Interface for Real Time Information +homepage_url: https://transmodel-cen.eu/index.php/siri/ +status: not_compliant +modes: [public_transport] +maintainers: [cen] +licences: +- id: Crown-Copyright + scopes: [specification] +first_release_year: 2006 +adoption: + lifecycle: active + tier: hundred_plus + confirmed_by_maintainer: true +principles: + cost_restriction_free: + verdict: unknown + rationale: No licence specified + evidence_url: https://github.com/TransmodelEcosystem/SIRI + publicly_documented: + verdict: not_compliant + rationale: Schemas available, no full and clear documentation available. + evidence_url: https://transmodel-cen.eu/index.php/uml-documents/ + independent_maintainer: + verdict: partially_compliant + rationale: |- + CEN - Working Group participation requirements are unclear + + Governmental entity (EU) + structured_releases: + verdict: compliant + rationale: Versioning + evidence_url: https://github.com/SIRI-CEN/SIRI/releases + open_governance: + verdict: not_compliant + rationale: Unclear, no information on adoped changes after a pull request. + evidence_url: https://www.cencenelec.eu/european-standardization/cen-and-cenelec/ +--- + +# Description + +Exchange of realtime information about schedules, vehicles, transfers, and additional informational messages about operational status diff --git a/data/specifications/statewide-trails-data-standard-ma.md b/data/specifications/statewide-trails-data-standard-ma.md new file mode 100644 index 0000000..4ea7e4d --- /dev/null +++ b/data/specifications/statewide-trails-data-standard-ma.md @@ -0,0 +1,40 @@ +--- +short_name: Statewide Trails Data Standard (Massachusetts) +full_name: Statewide Trails Data Standard +homepage_url: https://mapc365.sharepoint.com/:w:/s/DigitalServices/Ef9sIFA3T_hLi_Tmaf2975cB6dUfTLtjuT3K4kcK8d7VLg?e=gKq2v6 +status: not_compliant +modes: [bicycle, pedestrian_accessibility] +maintainers: [masstrails] +licences: +- id: Public-Domain +first_release_year: 2023 +adoption: + lifecycle: pre_adoption + tier: none + confirmed_by_maintainer: false +principles: + cost_restriction_free: + verdict: compliant + rationale: 'Yes' + evidence_url: https://mapc365.sharepoint.com/sites/DigitalServices/Shared%20Documents/Forms/AllItems.aspx?id=%2Fsites%2FDigitalServices%2FShared%20Documents%2FProjects%2FTrails%20Database%2FTrails%20DB%20%5BShared%5D%2FData%20Standard%2FStatewide%20Trails%20Data%20Standard%20v1%2E0%2E1%5FFor%20website%2Epdf&parent=%2Fsites%2FDigitalServices%2FShared%20Documents%2FProjects%2FTrails%20Database%2FTrails%20DB%20%5BShared%5D%2FData%20Standard&p=true&ga=1 + publicly_documented: + verdict: compliant + rationale: Yes, viewable on the MA Statewide Trails Database Initiative website + evidence_url: https://ma-statewide-trails-database-hub-massgis.hub.arcgis.com/pages/trails-standard~8a9e63c5a54d4eefb9d37b6d7b74ff37 + independent_maintainer: + verdict: not_compliant + rationale: No. The steward is an inter-agency initiative led by the Massachusetts state government. + evidence_url: https://ma-statewide-trails-database-hub-massgis.hub.arcgis.com/pages/trails-standard~8a9e63c5a54d4eefb9d37b6d7b74ff37 + structured_releases: + verdict: not_compliant + rationale: 'No' + evidence_url: https://ma-statewide-trails-database-hub-massgis.hub.arcgis.com/pages/trails-standard~8a9e63c5a54d4eefb9d37b6d7b74ff37 + open_governance: + verdict: not_compliant + rationale: No. Governance not documented and closed access. + evidence_url: https://ma-statewide-trails-database-hub-massgis.hub.arcgis.com/pages/ca929ce609fd46f4ab7106ad3bc3fb91 +--- + +# Description + +Schema offering a way of describing trails and sharing trails data that is simple at its core and useful for general (i.e., statewide) purposes, while remaining flexible/extensible enough for more specialized/local use cases. diff --git a/data/specifications/suti.md b/data/specifications/suti.md new file mode 100644 index 0000000..66199ea --- /dev/null +++ b/data/specifications/suti.md @@ -0,0 +1,38 @@ +--- +short_name: SUTI +full_name: Standardized Utilization of Transport Information +homepage_url: https://suti.se/ +status: not_compliant +modes: [microtransit_drt] +maintainers: [suti] +first_release_year: 2008 +adoption: + lifecycle: pre_adoption + tier: early + confirmed_by_maintainer: false +principles: + cost_restriction_free: + verdict: unknown + rationale: No licence specified + evidence_url: https://suti.se/369-2/ + publicly_documented: + verdict: compliant + rationale: 'Yes' + evidence_url: https://github.com/SUTI-se/SUTI + independent_maintainer: + verdict: compliant + rationale: SUTI is a non-profit organization founded in 2002. SUTI is owned by its members + evidence_url: https://suti.se/facts/ + structured_releases: + verdict: compliant + rationale: 'Yes' + evidence_url: https://github.com/SUTI-se/SUTI/releases + open_governance: + verdict: not_compliant + rationale: No public governance document + evidence_url: https://suti.se/year-by-year/ +--- + +# Description + +SUTI is a Swedish abbreviation that translates to standardized exchange of transport information in a demand responsive transport (DRT) context. In English SUTI can be read as Standardized Utilization of Transport Information. The aim is to develop and maintain a standard for communication between a Client and a Provider in a DRT environment. diff --git a/data/specifications/tides.md b/data/specifications/tides.md new file mode 100644 index 0000000..2f5a60b --- /dev/null +++ b/data/specifications/tides.md @@ -0,0 +1,43 @@ +--- +short_name: TIDES +full_name: Transit Integrated Data Exchange Specification +homepage_url: https://tides-transit.org/main/ +status: compliant +modes: [public_transport] +maintainers: [tides-board, mobilitydata] +licences: +- id: Apache-2.0 + scopes: [specification] +- id: CC-BY-4.0 + scopes: [documentation] +first_release_year: 2024 +adoption: + lifecycle: pilot_adoption + tier: pilot + confirmed_by_maintainer: true +principles: + cost_restriction_free: + verdict: compliant + rationale: Apache 2.0 + evidence_url: https://github.com/TIDES-transit/TIDES + publicly_documented: + verdict: compliant + rationale: CC-BY-4.0 + evidence_url: http://creativecommons.org/licenses/by/4.0/?ref=chooser-v1 + independent_maintainer: + verdict: compliant + rationale: Board does include a minority of seats for industry. + evidence_url: https://tides-transit.org/main/governance/ + structured_releases: + verdict: compliant + rationale: Versioning, Revision History + evidence_url: https://github.com/TIDES-transit/TIDES/releases + open_governance: + verdict: compliant + rationale: Anyone can become a contributor and vote. + evidence_url: https://tides-transit.org/main/governance/ +--- + +# Description + +Standard format for archiving transit operational data (AFC, AVL, APC). diff --git a/data/specifications/tods.md b/data/specifications/tods.md new file mode 100644 index 0000000..f7cfe6c --- /dev/null +++ b/data/specifications/tods.md @@ -0,0 +1,42 @@ +--- +short_name: TODS +full_name: Transit Operational Data Standard +homepage_url: http://tods-transit.org/ +status: compliant +modes: [public_transport] +maintainers: [tods-board, mobilitydata] +licences: +- id: Apache-2.0 + scopes: [specification] +- id: CC-BY-4.0 + scopes: [documentation] +first_release_year: 2022 +adoption: + lifecycle: early_adoption + tier: early +principles: + cost_restriction_free: + verdict: compliant + rationale: Apache 2.0 + evidence_url: https://github.com/MobilityData/transit-operational-data-standard/blob/main/LICENSE-APACHE-2.0 + publicly_documented: + verdict: compliant + rationale: CC-BY-4.0 + evidence_url: https://github.com/MobilityData/transit-operational-data-standard/blob/main/LICENSE-CC-BY-4.0 + independent_maintainer: + verdict: compliant + rationale: Board does include a minority of seats for industry. + evidence_url: https://tods-transit.org/governance/governance/ + structured_releases: + verdict: compliant + rationale: Versioning, Revision History + evidence_url: https://tods-transit.org/spec/revision-history/ + open_governance: + verdict: compliant + rationale: Anyone can become a contributor and vote. + evidence_url: https://tods-transit.org/governance/governance/ +--- + +# Description + +Extends GTFS for personnel and non-revenue service information used in scheduled transit operations. diff --git a/data/specifications/tomp-api.md b/data/specifications/tomp-api.md new file mode 100644 index 0000000..311cc52 --- /dev/null +++ b/data/specifications/tomp-api.md @@ -0,0 +1,39 @@ +--- +short_name: TOMP API +full_name: Transport Operator Mobility-as-a-service (TOMP) +homepage_url: https://github.com/TOMP-WG/TOMP-API +status: partially_compliant +modes: [microtransit_drt, taxi_ridehail] +maintainers: [tomp-wg] +licences: +- id: Apache-2.0 +first_release_year: 2019 +adoption: + lifecycle: early_adoption + tier: hundred_plus +principles: + cost_restriction_free: + verdict: compliant + rationale: Apache 2.0 + evidence_url: https://github.com/TOMP-WG/TOMP-API?tab=Apache-2.0-1-ov-file + publicly_documented: + verdict: compliant + rationale: Human-readable docs and the OpenAPI (machine-readable) specification are both published in full on GitHub (TOMP-WG/TOMP-API). + evidence_url: https://app.swaggerhub.com/apis-docs/TOMP-API-WG/transport-operator_maas_provider_api/ + independent_maintainer: + verdict: partially_compliant + rationale: TOMP-WG operates under the Maas Alliance ecosystem - governance is industry-participant led rather than a fully independent standards body. + evidence_url: https://mobility-alliance.org/about-us + structured_releases: + verdict: compliant + rationale: Versioned GitHub releases. + evidence_url: https://github.com/TOMP-WG/TOMP-API/wiki/Versioning-and-releases + open_governance: + verdict: compliant + rationale: Open to public GitHub contributions, but formal working-group decision-making is limited to participating members/companies. + evidence_url: https://github.com/TOMP-WG/TOMP-API/blob/master/CONTRIBUTING.md +--- + +# Description + +The TOMP-API is an interoperable open standard for technical communication between transport operators and mobility as a service providers. Compatible with GBFS, NeTEx. diff --git a/data/specifications/transxchange.md b/data/specifications/transxchange.md new file mode 100644 index 0000000..27d80c2 --- /dev/null +++ b/data/specifications/transxchange.md @@ -0,0 +1,41 @@ +--- +short_name: TransXChange +full_name: TransXChange (TXC) +homepage_url: http://gov.uk/government/collections/transxchange +status: inactive +modes: [public_transport] +maintainers: [uk-dft] +licences: +- id: OGL-3.0 + scopes: [specification, documentation] +first_release_year: 2001 +adoption: + lifecycle: active + tier: hundred_plus + confirmed_by_maintainer: true +principles: + cost_restriction_free: + verdict: compliant + rationale: Yes. 100% free to use, integrate, and redistribute commercially without restrictions under the Open Government Licence. + evidence_url: https://assets.publishing.service.gov.uk/media/5a7482f3ed915d0e8bf18e16/1-1_Overview.pdf + publicly_documented: + verdict: compliant + rationale: Yes. Complete XML schemas, technical documentation, implementation guides (UML), and sample files are publicly accessible on GOV.UK without registration. + evidence_url: https://www.gov.uk/government/publications/national-public-transport-access-node-schema + independent_maintainer: + verdict: not_compliant + rationale: Government Entity. + evidence_url: https://www.gov.uk/government/collections/transxchange + structured_releases: + verdict: compliant + rationale: Yes. Major public releases. No release since 2015 + evidence_url: https://assets.publishing.service.gov.uk/media/5a74a3ce40f0b61df4777456/transxchange-publisher-guidance.pdf + open_governance: + verdict: not_compliant + rationale: No public governance document - last updated in 2016 + evidence_url: https://assets.publishing.service.gov.uk/media/5a74a3ce40f0b61df4777456/transxchange-publisher-guidance.pdf +--- + +# Description + +UK national XML-based standard for modeling and exchanging bus routes, stops, and timetables, used for passenger information systems and regulatory registration (Electronic Bus Service Registration / EBSR). diff --git a/data/specifications/vdv.md b/data/specifications/vdv.md new file mode 100644 index 0000000..68c3540 --- /dev/null +++ b/data/specifications/vdv.md @@ -0,0 +1,41 @@ +--- +short_name: VDV +full_name: Verband Deutscher Verkehrsunternehmen - Kernapplikation (VDV-KA) +homepage_url: http://eticket-deutschland.de +status: not_compliant +modes: [public_transport] +maintainers: [vdv-ets] +licences: +- id: Proprietary + scopes: [documentation, specification] +first_release_year: 2002 +adoption: + lifecycle: active + tier: hundred_plus + confirmed_by_maintainer: true +principles: + cost_restriction_free: + verdict: not_compliant + rationale: No. Specifications require paid manufacturer subscriptions (((etiKIT) ranging from €359 to €1,790/month for commercial use, alongside mandatory participation/framework agreements. + evidence_url: https://www.eticket-deutschland.de/en/about-us/ + publicly_documented: + verdict: not_compliant + rationale: No. High-level overviews and glossaries are public, but complete technical specifications, test tools, and XML schemas are restricted behind login walls (ASM Tool). + evidence_url: https://www.eticket-deutschland.de/en/eticket/vdv-ka-and-eticore/ + independent_maintainer: + verdict: not_compliant + rationale: Industry Consortium / Sector Subsidiary. Maintained by VDV ETS, an entity owned and controlled by German transport associations and public transport operators under the umbrella of the VDV. + evidence_url: https://www.eticket-deutschland.de/en/eticket/vdv-ka-and-eticore/ + structured_releases: + verdict: compliant + rationale: Yes. Maintained across active releases (VDV-KA Release 1.12.1 currently live; (((etiCORE / KA 3.0.0-rc in active transition). + evidence_url: https://www.eticket-deutschland.de/en/eticket/vdv-ka-and-eticore/ + open_governance: + verdict: not_compliant + rationale: Restricted. Governance is tied to German transport association participation and paid manufacturer tiers. Access to development groups and voting influence requires formal membership and contractual accession. + evidence_url: https://www.eticket-deutschland.de/en/eticket/vdv-ka-and-eticore/ +--- + +# Description + +Nationwide open data and interface standard for electronic ticketing and electronic fare management (EFM) in public transport across smart cards, 2D barcodes, mobile NFC ticketing, and background processing systems. diff --git a/hooks/specifications.py b/hooks/specifications.py new file mode 100644 index 0000000..8cedd7b --- /dev/null +++ b/hooks/specifications.py @@ -0,0 +1,907 @@ +"""Generate /specifications and one page per specification at build time. + +The pages are produced with `File.generated`, so nothing is written into +docs/en and there are no generated Markdown files to keep in sync in git. The +single source of truth is the set of CSV files in data/, which are reviewed +and changed on GitHub; `scripts/spec_data.py` validates and joins them, and +`scripts/spec_api.py` turns the result into the static JSON API under /api/. + +The section is deliberately unlisted for now: it is left out of the nav bar, +the site search and the sitemap, and every page asks search engines not to +index it (see INDEX_META and overrides/main.html). + +Everything the listing page needs is rendered server-side: every row and every +filter control exists in the HTML, and the JavaScript only hides, sorts and +counts them. With JavaScript off the page is still a complete, readable list. + +Specifications still under review are rendered into a collapsed section after +the main list: they carry no finding yet, so they would only add noise to the +default view. + +Icons are read out of the Material theme's own bundled Material Design Icons +set and inlined, so there are no emoji and no icon font to load. +""" + +from __future__ import annotations + +import json +import os +import re +import sys +from html import escape +from urllib.parse import quote + +from mkdocs.structure.files import File + +ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +sys.path.insert(0, os.path.join(ROOT, "scripts")) + +import spec_api # noqa: E402 (path set above) +import spec_data # noqa: E402 + +SECTION = "specifications" + +_ICON_CACHE = {} + + +def icon(name, cls="", title=None): + """Inline one Material Design Icon from the theme's bundled set.""" + if name not in _ICON_CACHE: + import material + path = os.path.join( + os.path.dirname(material.__file__), "templates", ".icons", "material", name + ".svg" + ) + with open(path) as fh: + _ICON_CACHE[name] = fh.read().strip() + svg = _ICON_CACHE[name] + attrs = ' class="mdip-i %s" aria-hidden="true" focusable="false"' % escape(cls or "") + if title: + attrs = (' class="mdip-i %s" role="img" aria-label="%s" focusable="false"' + % (escape(cls or ""), escape(title))) + return svg.replace("' % (mono, escape(org["logo_url"]))) + + +def org_hue(key): + h = 0 + for ch in key: + h = (h * 31 + ord(ch)) % 360 + return h + + +def monogram(name): + words = [w for w in name.replace("-", " ").split() if w[:1].isalnum()] + if not words: + return "?" + if len(words) == 1: + return words[0][:2].upper() + return (words[0][:1] + words[1][:1]).upper() + + +def pill(tone, label, icon_name=None, cls=""): + return ('%s%s' + % (escape(tone), escape(cls), icon(icon_name or TONE_ICON.get(tone, "help-circle")), + escape(label))) + + +def meter(met, total): + dots = [] + for i in range(total): + dots.append('' % ("on" if i < met else "off")) + return ('%s' + % (met, total, "".join(dots))) + + +def ext_link(url, label, icon_name="open-in-new", cls=""): + return ('%s%s%s' + % (escape(cls), escape(url), icon("link-variant"), escape(label), + icon(icon_name))) + + +# -------------------------------------------------------------------------- +# Contributing on GitHub +# -------------------------------------------------------------------------- + +ISSUE_TEMPLATE = "specification-change.yml" + +# What "Propose a specification" opens in GitHub's editor: a new file in +# data/specifications/ with every field already laid out. +NEW_SPEC_TEMPLATE = """--- +short_name: +full_name: +homepage_url: +status: under_review +modes: [] +maintainers: [] +licences: +- id: + scopes: [specification] +first_release_year: +adoption: + lifecycle: + tier: +principles: +%s--- + +# Description + +What the specification is for, in a sentence or two. +""" + + +def issue_url(spec=None): + """A new issue from the change-request form, pre-filled where possible.""" + params = [("template", ISSUE_TEMPLATE)] + if spec: + params += [("title", "Change request: %s" % spec["short_name"]), ("spec_id", spec["id"])] + return spec_data.github_url("issues/new") + "?" + "&".join( + "%s=%s" % (k, quote(v)) for k, v in params) + + +def new_spec_url(data): + blocks = "".join(" %s:\n verdict: not_assessed\n rationale:\n evidence_url:\n" + % c["key"] for c in data["criteria"]) + return "%s?filename=%s&value=%s" % ( + spec_data.github_url("new", "data/" + spec_data.SPECS_DIR), + quote("new-specification-id.md"), quote(NEW_SPEC_TEMPLATE % blocks)) + + +def render_contribute(data, spec=None): + """Where to change what the page shows. + + On a specification page, "Suggest a change" opens that specification's + own file in GitHub's editor. Someone without write access is offered a + fork and a pull request, so every change is proposed and reviewed in + public. + """ + if spec: + title = "Something to update about %s?" % spec["short_name"] + text = ("If any information on this page is missing, outdated or incorrect, " + "suggest an edit on GitHub. The coalition reviews every change in public " + "before it appears here.") + primary = (spec_data.github_url("edit", spec["source_path"]), "pencil-outline", + "Suggest a change") + secondary = (issue_url(spec), "message-alert-outline", "Report an issue") + else: + title = "Help keep this registry accurate" + text = ("Every specification and organization on this page is a file kept in the " + "open on GitHub. Anyone can suggest a change, or propose a specification " + "that is missing.") + primary = (new_spec_url(data), "plus", "Propose a specification") + secondary = (spec_data.github_url("tree", "data"), "database-outline", "Browse the data") + return ('
%s

%s

%s

' + '
' + '%s%s' + '%s%s' + "
" + % (icon("github", "contribute__ico"), escape(title), escape(text), + escape(primary[0]), icon(primary[1]), escape(primary[2]), + escape(secondary[0]), icon(secondary[1]), escape(secondary[2]))) + + +# -------------------------------------------------------------------------- +# Listing page +# -------------------------------------------------------------------------- + +def render_row(spec, base): + """One row of the registry list. + + A row, not a card: the listing is a long inventory that people scan down a + single column, comparing one specification's status against the next, and + rows put those values in the same place on every line. Nothing here is + tinted by status -- the verdict is carried by the icon and the pill, which + is enough, and a coloured edge on 55 rows reads as decoration. + """ + parts = ['
  • ' + % (escape(spec["id"]), escape(spec["status"]))] + parts.append('' % escape(spec["slug"])) + + parts.append('') + parts.append('%s' + % escape(spec["short_name"])) + if spec["full_name"] and spec["full_name"] != spec["short_name"]: + parts.append('%s' % escape(spec["full_name"])) + parts.append("") + + if spec["function"]: + parts.append('%s' % escape(spec["function"])) + + parts.append('') + if spec["maintainers"]: + for org in spec["maintainers"][:2]: + parts.append('%s%s' + % (org_logo(org, base), escape(org["short"]))) + if len(spec["maintainers"]) > 2: + parts.append('+%d' + % (len(spec["maintainers"]) - 2)) + else: + parts.append('%sNo maintainer recorded' + "" % icon("help-circle")) + for mode in spec["modes"]: + parts.append('%s' % escape(mode)) + for lic in spec["licences"]: + parts.append('%s' + % (escape(lic["category"]), + escape(lic["id"] if lic["id"] != "Other" else lic["name"][:28]))) + parts.append("") + + parts.append('') + parts.append(pill(spec["status_tone"], spec["status_label"])) + facts = [] + if spec["adopters_count"]: + facts.append("%d adopters" % spec["adopters_count"]) + elif spec["adoption_tier_label"]: + facts.append(spec["adoption_tier_label"]) + if spec["first_release_year"]: + facts.append("since %d" % spec["first_release_year"]) + if facts: + parts.append('%s' % escape(" · ".join(facts))) + parts.append("") + + parts.append('%s' % icon("chevron-right")) + parts.append("
  • ") + return "".join(parts) + + +def render_facet(key, facet, base, organizations): + if not facet["options"]: + return "" + out = ['
    ' + % (escape(key), " open" if key in OPEN_BY_DEFAULT else "")] + out.append('' + % (' title="%s"' % escape(facet["hint"]) if facet.get("hint") else "")) + out.append(icon(FACET_ICON.get(key, "tune-variant"), "facet__icon")) + out.append('%s' % escape(facet["label"])) + out.append('') + out.append(icon("chevron-down", "facet__chev")) + out.append("") + out.append('
    ') + if facet.get("searchable"): + out.append('' + % (escape(facet["label"].lower()), icon("magnify"), + escape(facet["label"].lower()))) + out.append('
      ') + for opt in facet["options"]: + oid = "f-%s-%s" % (key, opt["value"]) + logo = "" + if key == "maintainer": + org = organizations.get(opt["value"]) + if org: + logo = org_logo(org, base) + out.append('
    • ') + out.append('' + % (escape(oid), escape(key), escape(opt["value"]))) + out.append('' + % (escape(oid), logo, escape(opt["label"]), opt["count"])) + out.append("
    • ") + out.append("
    ") + out.append('') + out.append("
    ") + return "".join(out) + + +def render_index(data, base): + m = data["meta"] + specs = data["specs"] + o = [] + + o.append('
    ') + + # ---- hero + o.append('
    ') + o.append('') + o.append('
    ') + o.append('

    %sMDIP registry

    ' + % icon("clipboard-text-search-outline")) + o.append("

    Mobility specifications

    ") + o.append('

    Every specification the coalition has assessed against ' + 'the %d MDIP openness criteria: what it covers, who maintains it, how it is ' + 'licensed, and where the evidence for each finding comes from.

    ' + % len(data["criteria"])) + o.append('
      ') + for value, label, ico in ( + (m["count"], "assessed", "table-search"), + (m["compliant_count"], "MDIP compliant", "shield-check"), + (m["confirmed_count"], "maintainer-confirmed", "account-check"), + (m["organization_count"], "organizations", "domain"), + ): + o.append('
    • %s' + '%s%s
    • ' + % (icon(ico), value, escape(label))) + o.append("
    ") + o.append("
    ") + + # ---- layout + o.append('
    ') + + o.append('' % icon("filter-variant")) + + o.append('
    ') + o.append('
    ') + o.append('

    %sFilters

    ' % icon("filter-variant")) + o.append('" % icon("close")) + o.append('' % icon("close")) + o.append("
    ") + + o.append('' % icon("magnify")) + + o.append('
    ') + for key in FACET_ORDER: + if key in data["facets"]: + o.append(render_facet(key, data["facets"][key], base, data["organizations"])) + + o.append('
    ') + o.append('

    %sBy individual criterion

    ' + % icon("shield-check")) + for c in data["criteria"]: + key = "criterion_" + c["key"] + if key in data["facets"]: + o.append(render_facet(key, data["facets"][key], base, data["organizations"])) + o.append("
    ") + o.append("
    ") + + # ---- results + deferred = [sp for sp in specs if sp["status"] in DEFERRED_STATUSES] + main = [sp for sp in specs if sp["status"] not in DEFERRED_STATUSES] + + o.append('
    ') + o.append('
    ') + o.append('

    %dof %d' + "

    " % (len(main), len(specs))) + o.append('") + o.append("
    ") + o.append('') + + o.append('
      ') + for spec in main: + o.append(render_row(spec, base)) + o.append("
    ") + + if deferred: + o.append('
    ') + o.append('%s' + 'Not yet assessed' + '%d%s' + % (icon("progress-clock"), len(deferred), icon("chevron-down"))) + o.append('

    These specifications are in the registry but ' + 'the coalition has not finished reviewing them against the MDIP criteria, so ' + 'no finding is published yet.

    ') + o.append('
      ') + for spec in deferred: + o.append(render_row(spec, base)) + o.append("
    ") + o.append("
    ") + + o.append('' + % icon("text-box-search-outline")) + o.append(render_contribute(data)) + o.append('

    %sReuse this data: API ' + 'documentation · catalogue (JSON) ' + '· full export (CSV)

    ' + % (icon("api"), base, base, base)) + o.append("
    ") + + o.append("
    ") # layout + + o.append('' + % json.dumps(compact(data), separators=(",", ":")).replace("") + return "\n".join(o) + + +def render_rules(data): + """What earns each verdict, from data/principles.csv. + + Shown under the criteria on every specification page, so the reasoning + behind a verdict sits next to the rule it was judged against. Only the + principles are listed: the bonus and adoption rows of principles.csv + feed the ordering of the registry, which is documented in data/README.md + rather than presented on the page. + """ + o = ['
    '] + o.append('%sHow each criterion is assessed' + % icon("book-open-outline")) + o.append('
    ' + "" + "") + for r in data["criteria"]: + o.append('' + % (escape(r["label"]), escape(r["blurb"]))) + for outcome in spec_data.OUTCOMES: + o.append("" % escape(r["when"].get(outcome) or "")) + o.append("") + o.append('
    CriterionCompliantPartially compliantNot compliant
    %s' + '%s%s

    The rules are kept in ' + 'principles.csv on GitHub.

    ' + % escape(spec_data.github_url("blob", "data/" + spec_data.PRINCIPLES_CSV))) + return "".join(o) + + +def compact(data): + """Small record per spec for client-side filtering, sorting and search.""" + out = [] + for s in data["specs"]: + text = " ".join(filter(None, [ + s["short_name"], s["full_name"], s["function"], s["id"], + " ".join(m["name"] for m in s["maintainers"]), + " ".join(m["short"] for m in s["maintainers"]), + " ".join(l["name"] for l in s["licences"]), + " ".join(l["id"] for l in s["licences"]), + " ".join(s["modes"]), + ])).lower() + rec = { + "id": s["id"], + "mode": s["modes"], + "status": [s["status"]], + "confirmed": [s["confirmed"]], + "lifecycle": [s["lifecycle"]] if s["lifecycle"] else [], + "adoption_tier": [s["adoption_tier"]] if s["adoption_tier"] else [], + "licence": sorted(set(s["licence_ids"])), + "licence_category": sorted(set(l["category"] for l in s["licences"])), + "maintainer": s["maintainer_ids"], + "maintainer_type": s["maintainer_types"], + "decade": ([str(s["first_release_year"] // 10 * 10) + "s"] + if s["first_release_year"] else []), + "t": text, + "sort": { + # Mirrors spec_data.default_order so the client re-sort lands + # back on exactly the server-rendered order. + "default": [1 if s["adopters_count"] is None else 0, -(s["adopters_count"] or 0), + -(s["score"] if s["score"] is not None else -999), + s["status_rank"], s["short_name"].lower()], + "criteria": [-s["criteria_met"], s["status_rank"]], + "name": [s["short_name"].lower()], + "newest": [-(s["first_release_year"] or 0)], + "oldest": [s["first_release_year"] or 9999], + }, + } + for c in s["criteria"]: + rec["criterion_" + c["key"]] = [c["verdict"]] + out.append(rec) + return {"specs": out} + + +# -------------------------------------------------------------------------- +# Detail page +# -------------------------------------------------------------------------- + +def paras(text, cls=""): + """Turn the registry's free text into paragraphs, keeping its line breaks.""" + blocks = [b.strip() for b in (text or "").replace("\r\n", "\n").split("\n\n") if b.strip()] + if not blocks: + return "" + return "".join( + '

    %s

    ' % (escape(cls), escape(b).replace("\n", "
    ")) + for b in blocks + ) + + +# Rationales that only restate the verdict. A quarter of the cells in the +# registry are one of these, and "Compliant -- because: Yes" is worth neither a +# paragraph nor a disclosure to open. +_RESTATEMENTS = { + "yes", "no", "n a", "na", "none", "tbd", "unknown", "partial", "partially", + "compliant", "not compliant", "not assessed", "yes yes", "no no", +} + + +def is_restatement(rationale): + folded = " ".join(re.sub(r"[^a-z0-9]+", " ", (rationale or "").lower()).split()) + return not folded or folded in _RESTATEMENTS + + +def description_html(text): + """The entry's `# Description` section, rendered as Markdown. + + Contributors write it on GitHub, so raw HTML in it is escaped rather than + passed through: the page renders what GitHub previews, and nothing else. + """ + import markdown + return markdown.markdown(escape(text, quote=False), extensions=["sane_lists"]) + + +def dl_row(ico, label, value_html): + return ('
    %s%s
    %s
    ' + % (icon(ico), escape(label), value_html)) + + +def render_detail(spec, data, base): + o = ['
    '] + + o.append('%sAll specifications' + % (escape(base + SECTION + "/"), icon("arrow-left"))) + + # ---- hero + o.append('
    ' % escape(spec["status_tone"])) + o.append('') + o.append('
    ') + o.append('
    %s%s
    ' + % (pill(spec["status_tone"], spec["status_label"]), + meter(spec["criteria_met"], spec["criteria_total"]))) + o.append("

    %s

    " % escape(spec["short_name"])) + if spec["full_name"] and spec["full_name"] != spec["short_name"]: + o.append('

    %s

    ' % escape(spec["full_name"])) + if spec["description"]: + o.append('
    %s
    ' % description_html(spec["description"])) + + o.append('
    ') + if spec["homepage_url"]: + o.append('%sSpecification ' + "homepage%s" + % (escape(spec["homepage_url"]), icon("web"), icon("open-in-new"))) + if spec["adopters_registry"]: + o.append('%sAdopters registry%s' + % (escape(spec["adopters_registry"]), icon("account-group"), icon("open-in-new"))) + o.append("
    ") + + if spec["maintainers"]: + o.append('
    Maintained by' + "") + for org in spec["maintainers"]: + o.append('%s%s' + % (org_logo(org, base), escape(org["name"]))) + o.append("
    ") + o.append("
    ") + + # ---- in-page nav + sections = [("glance", "At a glance", "information-outline"), + ("criteria", "MDIP criteria", "shield-check"), + ("licensing", "Licensing", "scale-balance")] + if spec["adoption_tier_label"] or spec["adopters_count"] or spec["adopters_registry"]: + sections.append(("adoption", "Adoption", "account-group")) + if spec["maintainers"]: + sections.append(("maintainers", "Maintainers", "domain")) + o.append('") + + o.append('
    ') + + # ---- at a glance + o.append('
    ') + o.append('

    %sAt a glance

    ' % icon("information-outline")) + o.append('
    ') + o.append(dl_row("routes", "Mode", " ".join( + '%s' % escape(m) for m in spec["modes"]))) + o.append(dl_row("shield-check", "MDIP status", + pill(spec["status_tone"], spec["status_label"]) + + '%d of %d criteria met' + % (spec["criteria_met"], spec["criteria_total"]))) + o.append(dl_row("account-check", "Adoption confirmed by maintainer", + pill(spec["confirmed_tone"], spec["confirmed_label"]))) + if spec["lifecycle_label"]: + o.append(dl_row("chart-timeline-variant", "Lifecycle", + '%s%s' + % (escape(spec["lifecycle_label"]), escape(spec["lifecycle_hint"] or "")))) + if spec["adoption_tier_label"]: + o.append(dl_row("account-group", "Adoption", + '%s' % escape(spec["adoption_tier_label"]))) + if spec["first_release_year"]: + o.append(dl_row("calendar-blank", "First release", + "%d" % spec["first_release_year"])) + o.append(dl_row("scale-balance", "Licence", " ".join( + '%s' + % (escape(l["category"]), escape(l["id"] if l["id"] != "Other" else l["name"])) + for l in spec["licences"]))) + if spec["homepage_url"]: + o.append(dl_row("web", "Homepage", + '%s%s' + % (escape(spec["homepage_url"]), escape(spec["homepage_url"]), + icon("open-in-new")))) + o.append('
    %sRegistry id
    ' + '
    %s
    ' % (icon("tag-outline"), escape(spec["id"]))) + o.append("
    ") + + # ---- criteria + o.append('
    ') + o.append('

    %sMDIP criteria%s' + "

    " % (icon("shield-check"), + meter(spec["criteria_met"], spec["criteria_total"]))) + o.append('
    ') + for c in spec["criteria"]: + # Reasoning worth reading is folded away, because five open rationales + # is a wall of text and the verdict alone answers the question most + # readers arrive with. A rationale that only restates the verdict is + # dropped instead of folded, so a row never offers a disclosure that + # opens onto the word "Yes"; its evidence link sits in the row. + reasoning = "" if is_restatement(c["rationale"]) else c["rationale"] + foldable = bool(reasoning) + o.append('<%s class="crit crit--%s">' + % ("details" if foldable else "div", escape(c["tone"]))) + o.append('<%s class="crit__head">' % ("summary" if foldable else "div")) + o.append('%s' + % icon(TONE_ICON.get(c["tone"], "help-circle"))) + o.append('%s' + '%s' + % (escape(c["label"]), escape(c["blurb"]))) + o.append('%s' % pill(c["tone"], c["verdict_label"])) + if foldable: + o.append('%s' % icon("chevron-down")) + elif c["evidence_url"]: + o.append('%s' + % ext_link(c["evidence_url"], "Source", cls="srclink--crit")) + o.append("" % ("summary" if foldable else "div")) + if foldable: + o.append('
    ') + o.append(paras(reasoning, "crit__rationale")) + if c["evidence_url"]: + o.append(ext_link(c["evidence_url"], "Source", cls="srclink--crit")) + o.append("
    ") + o.append("" % ("details" if foldable else "div")) + o.append("
    ") + o.append(render_rules(data)) + o.append("
    ") + + # ---- licensing + o.append('
    ') + o.append('

    %sLicensing

    ' % icon("scale-balance")) + o.append('
    ') + for lic in spec["licences"]: + o.append('
    ' % escape(lic["category"])) + o.append('
    ') + # Short id as the heading, full name underneath: the ids are what the + # cards and the licence filter use, and a long CC name would otherwise + # wrap and strand the external-link icon on its own line. + short = lic["id"] if lic["id"] != "Other" else lic["name"] + if lic["url"]: + o.append('

    %s%s

    ' + % (escape(lic["url"]), escape(short), icon("open-in-new"))) + else: + o.append("

    %s

    " % escape(short)) + if lic["scopes"]: + o.append('%s' % " ".join( + '%s' % escape(SCOPE_LABEL[s]) + for s in lic["scopes"])) + o.append("
    ") + if lic["name"] != short: + o.append('

    %s

    ' % escape(lic["name"])) + if lic["note"]: + o.append('

    %s%s

    ' + % (icon("information-outline"), escape(lic["note"]))) + o.append("
    ") + o.append("
    ") + for url in spec["licence_evidence"]: + o.append('

    %s

    ' % ext_link(url, "Licence text")) + o.append("
    ") + + # ---- adoption + if spec["adoption_tier_label"] or spec["adopters_count"] or spec["adopters_registry"]: + o.append('
    ') + o.append('

    %sAdoption

    ' % icon("account-group")) + o.append('
    ') + if spec["adoption_tier_label"]: + o.append(dl_row("counter", "Adoption tier", + '%s' % escape(spec["adoption_tier_label"]))) + o.append(dl_row("account-check", "Confirmed by maintainer", + pill(spec["confirmed_tone"], spec["confirmed_label"]))) + if spec["adopters_count"]: + note = "" + if spec["adopters_count_updated"]: + note = ('as of %s' + % escape(spec["adopters_count_updated"])) + o.append(dl_row("counter", "Known adopters", + "%d%s" % (spec["adopters_count"], note))) + if spec["adopters_registry"]: + o.append(dl_row("database-outline", "Adopters registry", + ext_link(spec["adopters_registry"], "Open registry"))) + o.append("
    ") + + # ---- maintainers + if spec["maintainers"]: + o.append('
    ') + o.append('

    %sMaintainers

    ' % icon("domain")) + o.append('
    ') + for org in spec["maintainers"]: + o.append('
    %s
    ' + "

    %s

    %s

    " % (org_logo(org, base), escape(org["name"]), + escape(org["type_label"]))) + if org["url"]: + o.append('%s%s' + % (escape(org["url"]), escape(short_url(org["url"])), + icon("open-in-new"))) + o.append("
    ") + o.append("
    ") + + related = [s for s in data["specs"] + if s["id"] != spec["id"] + and set(s["maintainer_ids"]) & set(spec["maintainer_ids"])] + if related: + o.append('") + o.append("
    ") + + o.append("
    ") # body + + o.append(render_contribute(data, spec)) + + o.append("
    ") + return "\n".join(o) + + +SCOPE_LABEL = spec_data.LICENCE_SCOPES + + +def short_url(url): + u = url.split("://", 1)[-1] + return u[:-1] if u.endswith("/") else u + + +# -------------------------------------------------------------------------- +# MkDocs entry point +# -------------------------------------------------------------------------- + +INDEX_META = """--- +title: Specifications +description: >- + Open mobility specifications assessed by the MDIP coalition against five + criteria for openness: cost, documentation, maintainer independence, + governance and release process. +robots: noindex +search: + exclude: true +hide: + - navigation + - toc +--- + +""" + +DETAIL_META = """--- +title: %(title)s +description: %(description)s +robots: noindex +search: + exclude: true +hide: + - navigation + - toc +--- + +""" + + +def _one_line(text): + return " ".join((text or "").split()) + + +def _collapse(html): + """Keep the payload free of blank lines. + + Python-Markdown ends a raw HTML block at the first blank line, so a stray + "\\n\\n" inside a value from the CSV would make the rest of + the page render as escaped markup. + """ + out = [] + for line in html.split("\n"): + if line.strip(): + out.append(line) + return "\n".join(out) + + +def on_files(files, config): + data = spec_data.build_dataset() + + for issue in data["issues"]: + log("specifications: %s in %s: %s" + % (issue["level"], issue["where"], issue["detail"][:160])) + + index = INDEX_META + _collapse(render_index(data, "../")) + files.append(File.generated(config, SECTION + "/index.md", content=index)) + + for spec in data["specs"]: + title = spec["short_name"] + desc = _one_line(spec["function"] or spec["full_name"] or title) + if len(desc) > 300: + desc = desc[:297].rstrip() + "..." + head = DETAIL_META % { + "title": json.dumps(title), + "description": json.dumps(desc), + } + body = _collapse(render_detail(spec, data, "../../")) + files.append(File.generated( + config, "%s/%s.md" % (SECTION, spec["slug"]), content=head + body)) + + for path, text in spec_api.build_api(data, config.get("site_url")).items(): + files.append(File.generated(config, path, content=text)) + + return files + + +def log(message): + import logging + logging.getLogger("mkdocs.hooks.specifications").warning(message) diff --git a/overrides/assets/javascript/specifications.js b/overrides/assets/javascript/specifications.js new file mode 100644 index 0000000..68955d0 --- /dev/null +++ b/overrides/assets/javascript/specifications.js @@ -0,0 +1,454 @@ +/* + * MDIP specification registry - progressive enhancement. + * + * Every row and every filter control is already in the HTML when this runs; + * the script only hides, reorders and counts them. With JavaScript off the + * page is still the complete list, which is why nothing here renders markup + * for the specifications themselves. + * + * Filter semantics: OR inside one facet, AND across facets. State lives in the + * query string so a filtered view can be linked to and survives back/forward. + */ +(function () { + "use strict"; + + var FACETS = [ + "mode", "status", "confirmed", "lifecycle", "adoption_tier", + "licence", "licence_category", "maintainer", "maintainer_type", "decade", + "criterion_cost_restriction_free", "criterion_publicly_documented", + "criterion_independent_maintainer", "criterion_open_governance", + "criterion_structured_releases" + ]; + + var SEP = "::"; + var DEFAULT_SORT = "default"; + + function ready(fn) { + if (document.readyState !== "loading") { fn(); } + else { document.addEventListener("DOMContentLoaded", fn); } + } + + ready(function () { + if (document.querySelector(".mdip-specs, .mdip-spec")) { + document.body.classList.add("mdip-fullbleed"); + } + initStickyOffset(); + initList(); + initDetail(); + initDisclosureAnchors(); + }); + + /* A link to a collapsed
    by id (the detail pages link to + #scoring) should land on it open, not on a closed summary line. */ + function initDisclosureAnchors() { + function openTarget() { + var id = location.hash.slice(1); + var el = id && document.getElementById(id); + if (el && el.tagName === "DETAILS" && !el.open) { + el.open = true; + el.scrollIntoView(); + } + } + openTarget(); + window.addEventListener("hashchange", openTarget); + } + + /* -------------------------------------------------------- sticky offset */ + + /* + * Publish Material's header height as --hdr on the registry roots, so the + * bars that stick under it (the filter panel, the in-page nav) start exactly + * at its bottom edge instead of guessing. + * + * It cannot be a constant: the header is one row on a phone, a taller row + * once the custom logo has space, and a row plus a tab strip above + * Material's tabs breakpoint -- and `navigation.tabs.sticky` keeps the tab + * strip inside the sticky header, so it counts. The CSS default (one row) + * is the no-JS fallback and is right at the width where JS is most likely + * to be missing. + */ + function initStickyOffset() { + var roots = Array.prototype.slice.call( + document.querySelectorAll(".mdip-specs, .mdip-spec") + ); + var header = document.querySelector(".md-header"); + if (!roots.length || !header) { return; } + + var last = -1; + var queued = false; + + function publish() { + queued = false; + var h = Math.round(header.getBoundingClientRect().height); + if (h === last || h <= 0) { return; } + last = h; + roots.forEach(function (root) { root.style.setProperty("--hdr", h + "px"); }); + } + + function schedule() { + if (queued) { return; } + queued = true; + requestAnimationFrame(publish); + } + + publish(); + if ("ResizeObserver" in window) { + new ResizeObserver(schedule).observe(header); + } else { + window.addEventListener("resize", schedule); + } + /* A late web font can change the header's height after first paint. */ + if (document.fonts && document.fonts.ready) { document.fonts.ready.then(schedule); } + } + + /* ---------------------------------------------------------------- listing */ + + function initList() { + var root = document.querySelector("[data-specs-root]"); + if (!root) { return; } + + var payload = root.querySelector("[data-specs-json]"); + var data = {}; + try { + JSON.parse(payload.textContent).specs.forEach(function (s) { data[s.id] = s; }); + } catch (e) { + return; // leave the server-rendered list untouched + } + + var panel = root.querySelector("[data-panel]"); + var grid = root.querySelector("[data-grid]"); + /* Specifications with no published finding live in a second, collapsed + list under the first. Both are filtered and sorted as one set; each row + is only ever re-inserted into the list it was rendered into. */ + var deferBox = root.querySelector("[data-defer]"); + var deferGrid = root.querySelector("[data-grid-defer]"); + var deferCountEl = root.querySelector("[data-defer-count]"); + var rowsOf = function (list) { + return list ? Array.prototype.slice.call(list.querySelectorAll(".spec-row")) : []; + }; + var mainRows = rowsOf(grid); + var deferRows = rowsOf(deferGrid); + var cards = mainRows.concat(deferRows); + var shownEl = root.querySelector("[data-shown]"); + var emptyEl = root.querySelector("[data-empty]"); + var chipsEl = root.querySelector("[data-chips]"); + var sortEl = root.querySelector("[data-sort]"); + var qEl = root.querySelector("[data-q]"); + var activeCountEl = root.querySelector("[data-active-count]"); + var boxes = Array.prototype.slice.call(panel.querySelectorAll('input[type="checkbox"]')); + var labelOf = {}; + + boxes.forEach(function (b) { + var name = b.parentNode.querySelector(".facet__name"); + labelOf[b.name + SEP + b.value] = name ? name.textContent : b.value; + }); + + var suppressPush = false; + + function selection() { + var sel = {}; + boxes.forEach(function (b) { + if (!b.checked) { return; } + (sel[b.name] = sel[b.name] || []).push(b.value); + }); + return sel; + } + + function matches(rec, sel, q) { + for (var key in sel) { + if (!Object.prototype.hasOwnProperty.call(sel, key)) { continue; } + var want = sel[key]; + var have = rec[key] || []; + var hit = false; + for (var i = 0; i < want.length; i++) { + if (have.indexOf(want[i]) !== -1) { hit = true; break; } + } + if (!hit) { return false; } + } + if (q) { + var terms = q.split(/\s+/); + for (var j = 0; j < terms.length; j++) { + if (rec.t.indexOf(terms[j]) === -1) { return false; } + } + } + return true; + } + + function apply(push) { + var sel = selection(); + var q = (qEl.value || "").trim().toLowerCase(); + var shown = 0; + + cards.forEach(function (card) { + var rec = data[card.getAttribute("data-spec")]; + var ok = rec ? matches(rec, sel, q) : true; + card.hidden = !ok; + if (ok) { shown++; } + }); + + var shownMain = mainRows.filter(visible).length; + var shownDefer = deferRows.filter(visible).length; + + shownEl.textContent = shown; + emptyEl.hidden = shown !== 0; + grid.hidden = shownMain === 0; + if (deferBox) { + deferBox.hidden = shownDefer === 0; + deferCountEl.textContent = shownDefer; + /* Filtering to a status that only exists down there would otherwise + look like an empty result. */ + if (shownDefer > 0 && shownMain === 0) { deferBox.open = true; } + } + + var nActive = 0; + for (var k in sel) { + if (Object.prototype.hasOwnProperty.call(sel, k)) { nActive += sel[k].length; } + } + if (q) { nActive++; } + + activeCountEl.hidden = nActive === 0; + activeCountEl.textContent = nActive; + + Array.prototype.forEach.call(root.querySelectorAll("[data-clear-all]"), function (b) { + if (b.classList.contains("ghost")) { b.hidden = nActive === 0; } + }); + + Array.prototype.forEach.call(panel.querySelectorAll(".facet"), function (facet) { + var key = facet.getAttribute("data-facet"); + var badge = facet.querySelector("[data-facet-count]"); + var n = (sel[key] || []).length; + badge.hidden = n === 0; + badge.textContent = n; + if (n > 0) { facet.open = true; } + }); + + renderChips(sel, q); + sortCards(); + if (push !== false) { pushState(sel, q); } + } + + function renderChips(sel, q) { + chipsEl.innerHTML = ""; + var any = false; + + if (q) { + any = true; + chipsEl.appendChild(chip('"' + q + '"', function () { + qEl.value = ""; + apply(); + })); + } + Object.keys(sel).forEach(function (key) { + sel[key].forEach(function (value) { + any = true; + chipsEl.appendChild(chip(labelOf[key + SEP + value] || value, function () { + boxes.forEach(function (b) { + if (b.name === key && b.value === value) { b.checked = false; } + }); + apply(); + })); + }); + }); + chipsEl.hidden = !any; + } + + function chip(text, onRemove) { + var el = document.createElement("span"); + el.className = "chip"; + var label = document.createElement("span"); + label.textContent = text; + var btn = document.createElement("button"); + btn.type = "button"; + btn.setAttribute("aria-label", "Remove filter " + text); + btn.innerHTML = ''; + btn.addEventListener("click", onRemove); + el.appendChild(label); + el.appendChild(btn); + return el; + } + + function visible(row) { return !row.hidden; } + + /* Sort keys are arrays: compare element by element, and fall back to the + name so two specifications with identical keys keep a stable order. */ + function compareKeys(ka, kb) { + var n = Math.min(ka.length, kb.length); + for (var i = 0; i < n; i++) { + if (ka[i] < kb[i]) { return -1; } + if (ka[i] > kb[i]) { return 1; } + } + return 0; + } + + function sortCards() { + var mode = sortEl.value; + [[grid, mainRows], [deferGrid, deferRows]].forEach(function (pair) { + var list = pair[0]; + if (!list) { return; } + pair[1].filter(visible).sort(function (a, b) { + var ra = data[a.getAttribute("data-spec")]; + var rb = data[b.getAttribute("data-spec")]; + if (!ra || !rb) { return 0; } + var order = compareKeys(ra.sort[mode] || [], rb.sort[mode] || []); + if (order !== 0) { return order; } + return compareKeys(ra.sort.name, rb.sort.name); + }).forEach(function (row) { list.appendChild(row); }); + }); + } + + /* -- url state -------------------------------------------------------- */ + + function pushState(sel, q) { + if (suppressPush) { return; } + var params = new URLSearchParams(); + FACETS.forEach(function (key) { + (sel[key] || []).forEach(function (v) { params.append(key, v); }); + }); + if (q) { params.set("q", q); } + if (sortEl.value !== DEFAULT_SORT) { params.set("sort", sortEl.value); } + var qs = params.toString(); + history.replaceState(null, "", location.pathname + (qs ? "?" + qs : "")); + } + + function readState() { + var params = new URLSearchParams(location.search); + suppressPush = true; + boxes.forEach(function (b) { b.checked = false; }); + FACETS.forEach(function (key) { + var values = params.getAll(key); + if (!values.length) { return; } + boxes.forEach(function (b) { + if (b.name === key && values.indexOf(b.value) !== -1) { b.checked = true; } + }); + }); + qEl.value = params.get("q") || ""; + /* An old link may carry a sort mode that no longer exists; setting it + would blank the select and sortCards would compare undefined keys. */ + var s = params.get("sort"); + if (s && sortEl.querySelector('option[value="' + CSS.escape(s) + '"]')) { + sortEl.value = s; + } else { + sortEl.value = DEFAULT_SORT; + } + suppressPush = false; + } + + /* -- facet option search ---------------------------------------------- */ + + Array.prototype.forEach.call(panel.querySelectorAll("[data-facet-search]"), function (input) { + var facet = input.closest(".facet"); + var items = Array.prototype.slice.call(facet.querySelectorAll(".facet__item")); + var none = facet.querySelector(".facet__none"); + input.addEventListener("input", function () { + var needle = input.value.trim().toLowerCase(); + var hits = 0; + items.forEach(function (item) { + var text = item.textContent.toLowerCase(); + var box = item.querySelector("input"); + var ok = !needle || text.indexOf(needle) !== -1 || box.checked; + item.hidden = !ok; + if (ok) { hits++; } + }); + none.hidden = hits !== 0; + }); + }); + + /* -- drawer ------------------------------------------------------------ */ + + var openBtn = root.querySelector("[data-drawer-open]"); + var closeBtn = root.querySelector("[data-drawer-close]"); + + function setDrawer(open) { + if (open) { panel.setAttribute("data-open", ""); } + else { panel.removeAttribute("data-open"); } + document.body.classList.toggle("mdip-drawer-open", open); + openBtn.setAttribute("aria-expanded", open ? "true" : "false"); + if (open) { qEl.focus(); } + } + + openBtn.setAttribute("aria-expanded", "false"); + openBtn.setAttribute("aria-controls", "specs-filters"); + openBtn.addEventListener("click", function () { setDrawer(true); }); + closeBtn.addEventListener("click", function () { setDrawer(false); }); + document.addEventListener("keydown", function (e) { + if (e.key === "Escape" && panel.hasAttribute("data-open")) { setDrawer(false); } + }); + document.addEventListener("click", function (e) { + if (!panel.hasAttribute("data-open")) { return; } + if (panel.contains(e.target) || openBtn.contains(e.target)) { return; } + setDrawer(false); + }); + + /* -- wiring ------------------------------------------------------------ */ + + panel.addEventListener("change", function (e) { + if (e.target.type === "checkbox") { apply(); } + }); + panel.addEventListener("submit", function (e) { e.preventDefault(); }); + + var qTimer = null; + qEl.addEventListener("input", function () { + clearTimeout(qTimer); + qTimer = setTimeout(apply, 140); + }); + + sortEl.addEventListener("change", function () { apply(); }); + + Array.prototype.forEach.call(root.querySelectorAll("[data-clear-all]"), function (btn) { + btn.addEventListener("click", function () { + boxes.forEach(function (b) { b.checked = false; }); + qEl.value = ""; + Array.prototype.forEach.call( + panel.querySelectorAll("[data-facet-search]"), + function (i) { i.value = ""; i.dispatchEvent(new Event("input")); } + ); + apply(); + setDrawer(false); + }); + }); + + window.addEventListener("popstate", function () { + readState(); + apply(false); + }); + + readState(); + apply(false); + } + + /* ----------------------------------------------------------------- detail */ + + function initDetail() { + var nav = document.querySelector(".spec-nav"); + if (!nav || !("IntersectionObserver" in window)) { return; } + + var links = Array.prototype.slice.call(nav.querySelectorAll("a")); + var byId = {}; + var sections = []; + + links.forEach(function (a) { + var id = a.getAttribute("href").slice(1); + var section = document.getElementById(id); + if (section) { byId[id] = a; sections.push(section); } + }); + + var seen = {}; + var observer = new IntersectionObserver(function (entries) { + entries.forEach(function (entry) { + seen[entry.target.id] = entry.isIntersecting; + }); + for (var i = 0; i < sections.length; i++) { + var id = sections[i].id; + if (seen[id]) { + links.forEach(function (a) { a.classList.remove("is-active"); }); + byId[id].classList.add("is-active"); + break; + } + } + }, { rootMargin: "-30% 0px -60% 0px" }); + + sections.forEach(function (s) { observer.observe(s); }); + } +})(); diff --git a/overrides/assets/stylesheets/specifications.css b/overrides/assets/stylesheets/specifications.css new file mode 100644 index 0000000..e6bd190 --- /dev/null +++ b/overrides/assets/stylesheets/specifications.css @@ -0,0 +1,1764 @@ +/* ========================================================================== + MDIP SPECIFICATION REGISTRY + /specifications and /specifications/ + + Scoped under .mdip-specs (listing) and .mdip-spec (detail) so nothing here + can leak into the rest of the documentation. Tokens are declared once on + those roots and re-declared for Material's slate scheme. + ========================================================================== */ + +.mdip-specs, +.mdip-spec { + --brand: #1b58a4; + --brand-600: #17497f; + --brand-300: #4892ec; + + --bg: #ffffff; + --bg-sunk: #f4f6fb; + --surface: #ffffff; + --surface-2: #f8fafd; + --line: #e3e8f2; + --line-strong: #cdd6e8; + + --ink: #10192b; + --ink-2: #44506a; + --ink-3: #646e88; + + --pass: #0f7b4f; + --pass-bg: #e6f5ee; + --warn: #a65c00; + --warn-bg: #fdf1e0; + --fail: #b3261e; + --fail-bg: #fcebe9; + --info: #1b58a4; + --info-bg: #e8f0fb; + --muted: #5d6880; + --muted-bg: #eef1f6; + + --r-s: 8px; + --r-m: 14px; + --r-l: 20px; + + --sh-1: 0 1px 2px rgba(16, 25, 43, .06), 0 1px 3px rgba(16, 25, 43, .04); + --sh-2: 0 4px 12px rgba(16, 25, 43, .07), 0 2px 4px rgba(16, 25, 43, .04); + --sh-3: 0 18px 40px -12px rgba(16, 25, 43, .22), 0 4px 12px rgba(16, 25, 43, .06); + + --ease: cubic-bezier(.22, 1, .36, 1); + --panel-w: 16rem; + + /* Material's .md-header is 2.4rem tall and sticks at top: 0. Anything else + that sticks has to start exactly there or the page shows through the gap + between the two bars. */ + --hdr: 2.4rem; + + color: var(--ink); + font-family: "Poppins", system-ui, -apple-system, "Segoe UI", sans-serif; + font-size: .78rem; + line-height: 1.55; +} + +[data-md-color-scheme="slate"] .mdip-specs, +[data-md-color-scheme="slate"] .mdip-spec { + --bg: #11131a; + --bg-sunk: #0c0e14; + --surface: #191c26; + --surface-2: #1f2330; + --line: #2b3040; + --line-strong: #3b4257; + + --ink: #e9edf6; + --ink-2: #b3bcd0; + --ink-3: #8c96ad; + + --pass: #5fd6a0; + --pass-bg: #113026; + --warn: #f0b258; + --warn-bg: #33250f; + --fail: #f4897f; + --fail-bg: #381a18; + --info: #7fb2f5; + --info-bg: #12233d; + --muted: #a3acc0; + --muted-bg: #232735; + + --sh-1: 0 1px 2px rgba(0, 0, 0, .4); + --sh-2: 0 4px 14px rgba(0, 0, 0, .45); + --sh-3: 0 20px 44px -12px rgba(0, 0, 0, .6); +} + +/* Let the two registry pages use the whole content column. `hide: navigation` + already removes the sidebar; this removes Material's article gutters too. + :has() covers the no-JS case, + + +the body class covers browsers without it. */ +.md-content__inner:has(> .mdip-specs), +.md-content__inner:has(> .mdip-spec), +body.mdip-fullbleed .md-content__inner { + margin: 0; + padding: 0; + max-width: none; +} + +.md-content__inner:has(> .mdip-specs)::before, +.md-content__inner:has(> .mdip-spec)::before, +body.mdip-fullbleed .md-content__inner::before { + display: none; +} + +body.mdip-fullbleed .md-main__inner { margin-top: 0; } + +.mdip-specs *, +.mdip-spec * { box-sizing: border-box; } + +:is(.mdip-specs, .mdip-spec) .u-sr { + position: absolute; + width: 1px; height: 1px; + padding: 0; margin: -1px; + overflow: hidden; clip: rect(0 0 0 0); + white-space: nowrap; border: 0; +} + +:is(.mdip-specs, .mdip-spec) .mdip-i { + width: 1.15em; + height: 1.15em; + fill: currentColor; + flex: none; + vertical-align: -.22em; +} + +/* -------------------------------------------------------------------------- + Hero + -------------------------------------------------------------------------- */ + +:is(.mdip-specs, .mdip-spec) .specs-hero, +:is(.mdip-specs, .mdip-spec) .spec-hero { + position: relative; + overflow: hidden; + padding: 1.9rem 1.1rem 1.5rem; + background: var(--bg-sunk); + border-bottom: 1px solid var(--line); + isolation: isolate; +} + +:is(.mdip-specs, .mdip-spec) .specs-hero__glow, +:is(.mdip-specs, .mdip-spec) .spec-hero__glow { + position: absolute; + inset: -55% -10% auto -10%; + height: 150%; + z-index: -1; + background: + radial-gradient(42% 46% at 18% 32%, rgba(27, 88, 164, .22), transparent 70%), + radial-gradient(38% 42% at 78% 18%, rgba(72, 146, 236, .20), transparent 72%), + radial-gradient(30% 38% at 52% 78%, rgba(15, 123, 79, .12), transparent 70%); + filter: blur(12px); + animation: mdip-drift 26s var(--ease) infinite alternate; +} + +:is(.mdip-specs, .mdip-spec) [data-md-color-scheme="slate"] .specs-hero__glow, +:is(.mdip-specs, .mdip-spec) [data-md-color-scheme="slate"] .spec-hero__glow { + background: + radial-gradient(42% 46% at 18% 32%, rgba(72, 146, 236, .26), transparent 70%), + radial-gradient(38% 42% at 78% 18%, rgba(27, 88, 164, .30), transparent 72%), + radial-gradient(30% 38% at 52% 78%, rgba(95, 214, 160, .12), transparent 70%); +} + +@keyframes mdip-drift { + from { transform: translate3d(-3%, -2%, 0) scale(1); } + to { transform: translate3d(4%, 3%, 0) scale(1.12); } +} + +:is(.mdip-specs, .mdip-spec) .specs-hero__in, +:is(.mdip-specs, .mdip-spec) .spec-hero__in { + max-width: 62rem; + margin: 0 auto; +} + +:is(.mdip-specs, .mdip-spec) .specs-hero__eyebrow { + display: inline-flex; + align-items: center; + gap: .4em; + margin: 0 0 .7rem; + padding: .28em .8em .28em .6em; + border: 1px solid var(--line-strong); + border-radius: 999px; + background: color-mix(in srgb, var(--surface) 72%, transparent); + backdrop-filter: blur(8px); + -webkit-backdrop-filter: blur(8px); + color: var(--brand); + font-size: .62rem; + font-weight: 600; + letter-spacing: .1em; + text-transform: uppercase; +} + +:is(.mdip-specs, .mdip-spec) [data-md-color-scheme="slate"] .specs-hero__eyebrow { color: var(--info); } + +.mdip-specs h1, +.mdip-spec h1 { + margin: 0 0 .45rem; + font-family: "Raleway", system-ui, sans-serif; + font-size: clamp(1.9rem, 6vw, 2.9rem); + font-weight: 800; + line-height: 1.06; + letter-spacing: -.02em; + color: var(--ink); +} + +:is(.mdip-specs, .mdip-spec) .specs-hero__lede, +:is(.mdip-specs, .mdip-spec) .spec-hero__lede { + margin: 0; + max-width: 46rem; + color: var(--ink-2); + font-size: .78rem; + line-height: 1.55; +} +/* The lede is the entry's Markdown description, so it can hold several + paragraphs and lists. */ +:is(.mdip-specs, .mdip-spec) .spec-hero__lede > * { margin: 0; } +:is(.mdip-specs, .mdip-spec) .spec-hero__lede > * + * { margin-top: .6em; } +:is(.mdip-specs, .mdip-spec) .spec-hero__lede ul { padding-left: 1.2em; } +:is(.mdip-specs, .mdip-spec) .spec-hero__lede ul li { list-style: disc; } + +:is(.mdip-specs, .mdip-spec) .specs-stats { + display: grid; + gap: .6rem; + grid-template-columns: repeat(2, minmax(0, 1fr)); + margin: 1.6rem 0 0; + padding: 0; + list-style: none; +} + +:is(.mdip-specs, .mdip-spec) .stat { + position: relative; + display: flex; + flex-direction: column; + gap: .1rem; + padding: .8rem .85rem; + border: 1px solid var(--line); + border-radius: var(--r-m); + background: color-mix(in srgb, var(--surface) 78%, transparent); + backdrop-filter: blur(10px); + -webkit-backdrop-filter: blur(10px); + box-shadow: var(--sh-1); +} + +:is(.mdip-specs, .mdip-spec) .stat__ico { color: var(--brand); opacity: .85; } +:is(.mdip-specs, .mdip-spec) [data-md-color-scheme="slate"] .stat__ico { color: var(--info); } + +:is(.mdip-specs, .mdip-spec) .stat__v { + font-family: "Raleway", system-ui, sans-serif; + font-size: 1.65rem; + font-weight: 800; + line-height: 1.1; + letter-spacing: -.02em; +} + +:is(.mdip-specs, .mdip-spec) .stat__l { + color: var(--ink-3); + font-size: .64rem; + line-height: 1.35; +} + +@media (min-width: 55em) { + :is(.mdip-specs, .mdip-spec) .specs-hero, + :is(.mdip-specs, .mdip-spec) .spec-hero { padding: 3.4rem 2rem 2.6rem; } + :is(.mdip-specs, .mdip-spec) .specs-stats { grid-template-columns: repeat(4, minmax(0, 1fr)); gap: .8rem; } + :is(.mdip-specs, .mdip-spec) .specs-hero__lede, + :is(.mdip-specs, .mdip-spec) .spec-hero__lede { font-size: .86rem; line-height: 1.62; } +} + +/* -------------------------------------------------------------------------- + Shared atoms: pills, + + +tags, + + +meters, + + +logos, + + +buttons + -------------------------------------------------------------------------- */ + +:is(.mdip-specs, .mdip-spec) .pill { + display: inline-flex; + align-items: center; + gap: .35em; + padding: .24em .7em .24em .5em; + border-radius: 999px; + font-size: .64rem; + font-weight: 600; + line-height: 1.5; + white-space: nowrap; +} + +:is(.mdip-specs, .mdip-spec) .pill--pass { background: var(--pass-bg); color: var(--pass); } +:is(.mdip-specs, .mdip-spec) .pill--warn { background: var(--warn-bg); color: var(--warn); } +:is(.mdip-specs, .mdip-spec) .pill--fail { background: var(--fail-bg); color: var(--fail); } +:is(.mdip-specs, .mdip-spec) .pill--info { background: var(--info-bg); color: var(--info); } +:is(.mdip-specs, .mdip-spec) .pill--muted { background: var(--muted-bg); color: var(--muted); } + +:is(.mdip-specs, .mdip-spec) .tag { + display: inline-flex; + align-items: center; + padding: .18em .55em; + border: 1px solid var(--line-strong); + border-radius: var(--r-s); + background: var(--surface-2); + color: var(--ink-2); + font-size: .62rem; + font-weight: 500; + white-space: nowrap; +} + +:is(.mdip-specs, .mdip-spec) .tag--mode { border-color: color-mix(in srgb, var(--brand) 28%, var(--line)); color: var(--brand); } +:is(.mdip-specs, .mdip-spec) [data-md-color-scheme="slate"] .tag--mode { color: var(--info); } + +:is(.mdip-specs, .mdip-spec) .tag--lic { font-variant-numeric: tabular-nums; } +:is(.mdip-specs, .mdip-spec) .tag--lic-open { border-color: color-mix(in srgb, var(--pass) 34%, var(--line)); color: var(--pass); } +:is(.mdip-specs, .mdip-spec) .tag--lic-restricted { border-color: color-mix(in srgb, var(--warn) 34%, var(--line)); color: var(--warn); } +:is(.mdip-specs, .mdip-spec) .tag--lic-closed { border-color: color-mix(in srgb, var(--fail) 34%, var(--line)); color: var(--fail); } +:is(.mdip-specs, .mdip-spec) .tag--lic-unknown { color: var(--ink-3); } +:is(.mdip-specs, .mdip-spec) .tag--scope { background: var(--muted-bg); border-color: transparent; } + +:is(.mdip-specs, .mdip-spec) .meter { display: inline-flex; gap: 3px; align-items: center; } +:is(.mdip-specs, .mdip-spec) .meter i { + width: 6px; height: 6px; + border-radius: 50%; + background: var(--line-strong); + transition: transform .3s var(--ease); +} +:is(.mdip-specs, .mdip-spec) .meter i.on { background: var(--brand); } +:is(.mdip-specs, .mdip-spec) [data-md-color-scheme="slate"] .meter i.on { background: var(--info); } + +:is(.mdip-specs, .mdip-spec) .org-logo { + width: 1.15rem; + height: 1.15rem; + flex: none; + object-fit: contain; + border-radius: 3px; +} + +:is(.mdip-specs, .mdip-spec) .org-logo--mono { + display: inline-flex; + align-items: center; + justify-content: center; + border-radius: 4px; + background: hsl(var(--org-hue, 215) 46% 92%); + color: hsl(var(--org-hue, 215) 52% 30%); + font-size: .5rem; + font-weight: 700; + letter-spacing: -.02em; +} + +:is(.mdip-specs, .mdip-spec) [data-md-color-scheme="slate"] .org-logo--mono { + background: hsl(var(--org-hue, 215) 30% 26%); + color: hsl(var(--org-hue, 215) 60% 82%); +} + +:is(.mdip-specs, .mdip-spec) .org-logo--stack { position: relative; display: inline-flex; } +:is(.mdip-specs, .mdip-spec) .org-logo--stack > .org-logo--mono { width: 100%; height: 100%; } +:is(.mdip-specs, .mdip-spec) .org-logo__img { + position: absolute; + inset: 0; + width: 100%; + height: 100%; + object-fit: contain; + border-radius: inherit; + background: var(--surface); +} + +/* -- assessment rules (under the criteria on each specification page) ----- */ + +:is(.mdip-specs, .mdip-spec) .method__body { padding: .7rem .8rem .8rem; } +:is(.mdip-specs, .mdip-spec) .method__src { margin: .6rem 0 0; color: var(--ink-3); font-size: .62rem; } +:is(.mdip-specs, .mdip-spec) .crits + .method { margin-top: .7rem; } +:is(.mdip-specs, .mdip-spec) .method__scroll { overflow-x: auto; } +.md-typeset :is(.mdip-specs, .mdip-spec) table.method__t { + display: table; + width: 100%; + margin: 0; + border: 0; + border-collapse: collapse; + box-shadow: none; + font-size: .64rem; +} +.md-typeset :is(.mdip-specs, .mdip-spec) table.method__t { min-width: 40rem; } +.md-typeset :is(.mdip-specs, .mdip-spec) .method__t :is(th, td) { + padding: .45rem .55rem; + border: 0; + border-top: 1px solid var(--line); + background: none; + color: var(--ink-2); + line-height: 1.5; + text-align: left; + vertical-align: top; +} +.md-typeset :is(.mdip-specs, .mdip-spec) .method__t thead th { + border-top: 0; + color: var(--ink-3); + font-size: .58rem; + font-weight: 600; + letter-spacing: .04em; + text-transform: uppercase; +} +.md-typeset :is(.mdip-specs, .mdip-spec) .method__t tbody th { width: 22%; } +:is(.mdip-specs, .mdip-spec) .method__name { display: block; color: var(--ink); font-weight: 700; } +:is(.mdip-specs, .mdip-spec) .method__def { display: block; color: var(--ink-3); font-weight: 400; font-size: .6rem; } +.md-typeset :is(.mdip-specs, .mdip-spec) .method__na { color: var(--ink-3); font-style: italic; } + +:is(.mdip-specs, .mdip-spec) .org-chip { + display: inline-flex; + align-items: center; + gap: .35em; + max-width: 100%; + color: var(--ink-2); + font-size: .66rem; + font-weight: 500; +} +:is(.mdip-specs, .mdip-spec) .org-chip > span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +:is(.mdip-specs, .mdip-spec) .org-chip--none { color: var(--ink-3); } + +:is(.mdip-specs, .mdip-spec) .org-chip--lg { font-size: .72rem; } +:is(.mdip-specs, .mdip-spec) .org-chip--lg .org-logo { width: 1.5rem; height: 1.5rem; } +:is(.mdip-specs, .mdip-spec) .org-chip--lg .org-logo--mono { font-size: .62rem; } + +:is(.mdip-specs, .mdip-spec) .btn { + display: inline-flex; + align-items: center; + gap: .4em; + padding: .5em .95em; + border: 1px solid var(--line-strong); + border-radius: 999px; + background: var(--surface); + color: var(--ink); + font-family: inherit; + font-size: .7rem; + font-weight: 600; + cursor: pointer; + text-decoration: none; + box-shadow: var(--sh-1); + transition: transform .18s var(--ease), box-shadow .18s var(--ease), + background-color .18s var(--ease), border-color .18s var(--ease); +} +:is(.mdip-specs, .mdip-spec) .btn:hover { transform: translateY(-1px); box-shadow: var(--sh-2); border-color: var(--brand); } +:is(.mdip-specs, .mdip-spec) .btn--primary { background: var(--brand); border-color: var(--brand); color: #fff; } +:is(.mdip-specs, .mdip-spec) .btn--primary:hover { background: var(--brand-600); border-color: var(--brand-600); } +.mdip-spec .btn, +.mdip-specs .btn { font-weight: 600; } + +:is(.mdip-specs, .mdip-spec) .ghost { + display: inline-flex; + align-items: center; + gap: .3em; + padding: .25em .55em; + border: 0; + border-radius: var(--r-s); + background: transparent; + color: var(--ink-3); + font: inherit; + font-size: .64rem; + cursor: pointer; + transition: background-color .15s var(--ease), color .15s var(--ease); +} +:is(.mdip-specs, .mdip-spec) .ghost:hover { background: var(--muted-bg); color: var(--ink); } + +:is(.mdip-specs, .mdip-spec) .srclink { + display: inline-flex; + align-items: center; + gap: .35em; + padding: .3em .7em; + border: 1px solid var(--line-strong); + border-radius: 999px; + background: var(--surface-2); + color: var(--brand); + font-size: .64rem; + font-weight: 600; + text-decoration: none; + transition: border-color .16s var(--ease), background-color .16s var(--ease), + transform .16s var(--ease); +} +:is(.mdip-specs, .mdip-spec) .srclink:hover { border-color: var(--brand); background: var(--info-bg); transform: translateY(-1px); } +:is(.mdip-specs, .mdip-spec) .srclink .mdip-i:last-child { width: .85em; height: .85em; opacity: .6; } +:is(.mdip-specs, .mdip-spec) [data-md-color-scheme="slate"] .srclink { color: var(--info); } + +.mdip-specs a, +.mdip-spec a { color: var(--brand); } +[data-md-color-scheme="slate"] .mdip-specs a, +[data-md-color-scheme="slate"] .mdip-spec a { color: var(--info); } + +.mdip-specs :focus-visible, +.mdip-spec :focus-visible { + outline: 2px solid var(--brand-300); + outline-offset: 2px; + border-radius: 4px; +} + +/* -------------------------------------------------------------------------- + Listing layout + -------------------------------------------------------------------------- */ + +:is(.mdip-specs, .mdip-spec) .specs-layout { + display: block; + max-width: 96rem; + margin: 0 auto; + padding: 1.1rem; +} + +@media (min-width: 60em) { + :is(.mdip-specs, .mdip-spec) .specs-layout { + display: grid; + grid-template-columns: var(--panel-w) minmax(0, 1fr); + gap: 1.2rem; + padding: 1.2rem 2rem 3rem; + align-items: start; + } +} + +/* -- filter panel ---------------------------------------------------------- */ + +:is(.mdip-specs, .mdip-spec) .specs-panel { + display: flex; + flex-direction: column; + min-height: 0; +} + +@media (min-width: 60em) { + :is(.mdip-specs, .mdip-spec) .specs-panel { + position: sticky; + top: calc(var(--hdr) + .8rem); + max-height: calc(100vh - var(--hdr) - 1.6rem); + padding: .55rem .6rem; + border: 1px solid var(--line); + border-radius: var(--r-m); + background: var(--surface); + box-shadow: var(--sh-1); + } +} + +:is(.mdip-specs, .mdip-spec) .specs-panel__head { + margin-bottom: .1rem; + display: flex; + align-items: center; + gap: .5rem; + padding: .1rem .2rem .7rem; +} + +:is(.mdip-specs, .mdip-spec) .specs-panel__head h2 { + display: flex; + align-items: center; + gap: .4em; + margin: 0; + flex: 1; + font-family: "Raleway", system-ui, sans-serif; + font-size: .82rem; + font-weight: 700; + color: var(--ink); +} + +:is(.mdip-specs, .mdip-spec) .specs-panel__close { + display: none; + border: 0; + background: transparent; + color: var(--ink-2); + cursor: pointer; + padding: .25rem; +} + +:is(.mdip-specs, .mdip-spec) .specs-find, +:is(.mdip-specs, .mdip-spec) .facet__find { + position: relative; + display: flex; + align-items: center; + gap: .4em; + padding: .45em .7em; + border: 1px solid var(--line-strong); + border-radius: 999px; + background: var(--surface); + color: var(--ink-3); + transition: border-color .16s var(--ease), box-shadow .16s var(--ease); +} +:is(.mdip-specs, .mdip-spec) .specs-find:focus-within, +:is(.mdip-specs, .mdip-spec) .facet__find:focus-within { + border-color: var(--brand); + box-shadow: 0 0 0 3px color-mix(in srgb, var(--brand) 15%, transparent); +} +:is(.mdip-specs, .mdip-spec) .specs-find input, +:is(.mdip-specs, .mdip-spec) .facet__find input { + flex: 1; + min-width: 0; + border: 0; + background: none; + color: var(--ink); + font: inherit; + font-size: .7rem; + outline: none; +} +:is(.mdip-specs, .mdip-spec) .facet__find { margin: 0 0 .5rem; padding: .32em .6em; border-radius: var(--r-s); } +:is(.mdip-specs, .mdip-spec) .facet__find input { font-size: .66rem; } + +:is(.mdip-specs, .mdip-spec) .specs-panel__scroll { + margin-top: .7rem; + overflow-y: auto; + overflow-x: hidden; + overscroll-behavior: contain; + scrollbar-width: thin; +} +@media (min-width: 60em) { + :is(.mdip-specs, .mdip-spec) .specs-panel__scroll { padding-right: .25rem; } +} + +/* -- one facet ------------------------------------------------------------- */ + +:is(.mdip-specs, .mdip-spec) .facet { + border-bottom: 1px solid var(--line); +} +:is(.mdip-specs, .mdip-spec) .facet:last-child { border-bottom: 0; } + +:is(.mdip-specs, .mdip-spec) .facet__head { + padding-block: .38em; + display: flex; + align-items: center; + min-width: 0; + gap: .45em; + padding: .62rem .2rem; + cursor: pointer; + list-style: none; + color: var(--ink); + font-size: .7rem; + font-weight: 600; + user-select: none; + transition: color .15s var(--ease); +} +:is(.mdip-specs, .mdip-spec) .facet__head::-webkit-details-marker { display: none; } +:is(.mdip-specs, .mdip-spec) .facet__head:hover { color: var(--brand); } +:is(.mdip-specs, .mdip-spec) .facet__icon { color: var(--ink-3); } +:is(.mdip-specs, .mdip-spec) .facet__label { + flex: 1; + min-width: 0; + overflow-wrap: anywhere; +} + +:is(.mdip-specs, .mdip-spec) .facet__badge { + min-width: 1.15rem; + padding: 0 .35em; + border-radius: 999px; + background: var(--brand); + color: #fff; + font-size: .58rem; + font-weight: 700; + text-align: center; + line-height: 1.15rem; +} + +:is(.mdip-specs, .mdip-spec) .facet__chev { + color: var(--ink-3); + transition: transform .25s var(--ease); +} +:is(.mdip-specs, .mdip-spec) .facet[open] > .facet__head .facet__chev { transform: rotate(180deg); } + +:is(.mdip-specs, .mdip-spec) .facet__body { padding: 0 .2rem .45rem; animation: mdip-reveal .28s var(--ease); } + +@keyframes mdip-reveal { + from { opacity: 0; transform: translateY(-4px); } + to { opacity: 1; transform: none; } +} + + +:is(.mdip-specs, .mdip-spec) .facet__list { + margin: 0; + padding: 0; + list-style: none; + max-height: 15rem; + overflow-y: auto; + scrollbar-width: thin; +} + +:is(.mdip-specs, .mdip-spec) .facet__item input { + position: absolute; + opacity: 0; + width: 1px; height: 1px; +} + +:is(.mdip-specs, .mdip-spec) .facet__item label { + padding-block: .22em; + display: flex; + align-items: center; + gap: .45em; + padding: .3em .45em; + border-radius: var(--r-s); + color: var(--ink-2); + font-size: .68rem; + cursor: pointer; + transition: background-color .14s var(--ease), color .14s var(--ease); +} + +:is(.mdip-specs, .mdip-spec) .facet__item label::before { + content: ""; + flex: none; + width: .82rem; + height: .82rem; + border: 1.5px solid var(--line-strong); + border-radius: 4px; + background: var(--surface); + transition: background-color .16s var(--ease), border-color .16s var(--ease); +} + +:is(.mdip-specs, .mdip-spec) .facet__item label:hover { background: var(--muted-bg); color: var(--ink); } + +:is(.mdip-specs, .mdip-spec) .facet__item input:checked + label { color: var(--ink); font-weight: 600; } +:is(.mdip-specs, .mdip-spec) .facet__item input:checked + label::before { + border-color: var(--brand); + background: var(--brand) url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23fff'%3E%3Cpath d='M9 20.42 2.79 14.21l2.83-2.83L9 14.77l9.88-9.89 2.83 2.83z'/%3E%3C/svg%3E") center/.6rem no-repeat; +} +:is(.mdip-specs, .mdip-spec) .facet__item input:focus-visible + label::before { + outline: 2px solid var(--brand-300); + outline-offset: 2px; +} + +:is(.mdip-specs, .mdip-spec) .facet__name { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + +:is(.mdip-specs, .mdip-spec) .facet__n { + color: var(--ink-3); + font-size: .6rem; + font-variant-numeric: tabular-nums; +} + +:is(.mdip-specs, .mdip-spec) .facet__none { margin: .3rem .45em; color: var(--ink-3); font-size: .62rem; } + +:is(.mdip-specs, .mdip-spec) .facet-group { + margin-top: .5rem; + padding: .6rem .5rem .2rem; + border: 1px dashed var(--line-strong); + border-radius: var(--r-m); + background: var(--surface-2); +} +:is(.mdip-specs, .mdip-spec) .facet-group__title { + display: flex; + align-items: center; + gap: .4em; + margin: 0 0 .2rem; + color: var(--ink-3); + font-size: .6rem; + font-weight: 700; + letter-spacing: .07em; + text-transform: uppercase; +} +:is(.mdip-specs, .mdip-spec) .facet-group .facet:last-child { border-bottom: 0; } + +/* -- mobile drawer --------------------------------------------------------- */ + +/* On small screens this is a floating action button: pinned to the thumb, and + out of the flow so it can never sit on top of the result count. */ +:is(.mdip-specs, .mdip-spec) .specs-drawer-btn { + position: fixed; + right: 1rem; + bottom: 1rem; + z-index: 8; + display: inline-flex; + align-items: center; + gap: .4em; + padding: .7em 1.15em; + border: 1px solid var(--line-strong); + border-radius: 999px; + background: color-mix(in srgb, var(--surface) 88%, transparent); + backdrop-filter: blur(10px); + -webkit-backdrop-filter: blur(10px); + color: var(--ink); + font: inherit; + font-size: .72rem; + font-weight: 600; + cursor: pointer; + box-shadow: var(--sh-3); +} + +:is(.mdip-specs, .mdip-spec) .specs-drawer-btn__n { + min-width: 1.15rem; + padding: 0 .3em; + border-radius: 999px; + background: var(--brand); + color: #fff; + font-size: .58rem; + line-height: 1.15rem; + text-align: center; +} + +@media (min-width: 60em) { + :is(.mdip-specs, .mdip-spec) .specs-drawer-btn { display: none; } +} + +/* room for the floating button under the last card */ +@media (max-width: 59.99em) { + :is(.mdip-specs, .mdip-spec) .specs-results { padding-bottom: 4rem; } +} + +@media (max-width: 59.99em) { + :is(.mdip-specs, .mdip-spec) .specs-panel { + position: fixed; + inset: 0 0 0 auto; + z-index: 10; + width: min(21rem, 92vw); + padding: .9rem .8rem 1.4rem; + background: var(--surface); + box-shadow: var(--sh-3); + transform: translateX(101%); + visibility: hidden; + transition: transform .34s var(--ease), visibility .34s; + } + :is(.mdip-specs, .mdip-spec) .specs-panel[data-open] { transform: none; visibility: visible; } + :is(.mdip-specs, .mdip-spec) .specs-panel__close { display: block; } + :is(.mdip-specs, .mdip-spec) .specs-panel__scroll { flex: 1; } + + body.mdip-drawer-open { overflow: hidden; } + body.mdip-drawer-open::after { + content: ""; + position: fixed; + inset: 0; + z-index: 9; + background: rgba(10, 16, 28, .45); + backdrop-filter: blur(2px); + -webkit-backdrop-filter: blur(2px); + animation: mdip-fade .3s var(--ease); + } +} + +@keyframes mdip-fade { from { opacity: 0; } to { opacity: 1; } } + +/* -- results --------------------------------------------------------------- */ + +:is(.mdip-specs, .mdip-spec) .specs-toolbar { + display: flex; + align-items: center; + gap: .8rem; + flex-wrap: wrap; + margin-bottom: .55rem; +} + +:is(.mdip-specs, .mdip-spec) .specs-count { + flex: 1; + display: inline-flex; + align-items: baseline; + gap: .3em; + margin: 0; + color: var(--ink-3); + font-size: .66rem; +} +:is(.mdip-specs, .mdip-spec) .specs-count strong { + color: var(--ink); + font-size: .82rem; + font-weight: 700; + font-variant-numeric: tabular-nums; +} + +:is(.mdip-specs, .mdip-spec) .specs-sort { + display: inline-flex; + align-items: center; + gap: .35em; + padding: .3em .5em .3em .7em; + border: 1px solid var(--line-strong); + border-radius: 999px; + background: var(--surface); + color: var(--ink-3); + font-size: .68rem; +} +:is(.mdip-specs, .mdip-spec) .specs-sort select { + border: 0; + background: none; + color: var(--ink); + font: inherit; + font-size: .68rem; + font-weight: 600; + cursor: pointer; + outline: none; +} + +:is(.mdip-specs, .mdip-spec) .specs-chips { + display: flex; + flex-wrap: wrap; + gap: .35rem; + margin-bottom: .9rem; +} + +:is(.mdip-specs, .mdip-spec) .chip { + display: inline-flex; + align-items: center; + gap: .3em; + padding: .22em .3em .22em .65em; + border: 1px solid color-mix(in srgb, var(--brand) 32%, var(--line)); + border-radius: 999px; + background: var(--info-bg); + color: var(--brand); + font-size: .62rem; + font-weight: 600; + animation: mdip-pop .22s var(--ease); +} +:is(.mdip-specs, .mdip-spec) [data-md-color-scheme="slate"] .chip { color: var(--info); } +:is(.mdip-specs, .mdip-spec) .chip button { + display: inline-flex; + padding: .1em; + border: 0; + border-radius: 999px; + background: transparent; + color: inherit; + cursor: pointer; + opacity: .65; +} +:is(.mdip-specs, .mdip-spec) .chip button:hover { opacity: 1; background: color-mix(in srgb, var(--brand) 16%, transparent); } +:is(.mdip-specs, .mdip-spec) .chip .mdip-i { width: .85em; height: .85em; } + +@keyframes mdip-pop { + from { opacity: 0; transform: scale(.9); } + to { opacity: 1; transform: none; } +} + +/* The registry is a list, not a gallery: one column at every width, rows + divided by hairlines inside a single bordered container. Scanning 55 rows + for a status works when the status sits in the same place on every line, + which a reflowing card grid cannot promise. */ +:is(.mdip-specs, .mdip-spec) .specs-list { + margin: 0; + padding: 0; + list-style: none; + border: 1px solid var(--line); + border-radius: var(--r-m); + background: var(--surface); + box-shadow: var(--sh-1); + overflow: hidden; +} + +:is(.mdip-specs, .mdip-spec) .spec-row + .spec-row { border-top: 1px solid var(--line); } +:is(.mdip-specs, .mdip-spec) .spec-row[hidden] { display: none; } + +:is(.mdip-specs, .mdip-spec) .spec-row__link { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + grid-template-areas: "main go" "side go"; + align-items: start; + gap: .3rem .7rem; + padding: .7rem .8rem; + /* additional.css bolds every ; a row wrapping a block of body copy opts + back out. */ + font-weight: 400; + color: inherit; + text-decoration: none; + transition: background-color .16s var(--ease); +} +:is(.mdip-specs, .mdip-spec) .spec-row__link:hover { background: var(--surface-2); } +:is(.mdip-specs, .mdip-spec) .spec-row__link:hover .spec-row__go { opacity: 1; transform: none; } + +@media (min-width: 52em) { + :is(.mdip-specs, .mdip-spec) .spec-row__link { + grid-template-columns: minmax(0, 1fr) 11.5rem auto; + grid-template-areas: "main side go"; + align-items: center; + gap: .9rem; + padding: .7rem 1rem; + } +} + +:is(.mdip-specs, .mdip-spec) .spec-row__main { + grid-area: main; + display: flex; + flex-direction: column; + gap: .18rem; + min-width: 0; +} + +:is(.mdip-specs, .mdip-spec) .spec-row__head { + display: flex; + flex-wrap: wrap; + align-items: baseline; + gap: .1rem .5rem; +} +:is(.mdip-specs, .mdip-spec) .spec-row__name { + font-family: "Raleway", system-ui, sans-serif; + font-size: .92rem; + font-weight: 700; + line-height: 1.25; + letter-spacing: -.01em; + color: var(--ink); +} +:is(.mdip-specs, .mdip-spec) .spec-row__full { color: var(--ink-3); font-size: .64rem; line-height: 1.4; } + +:is(.mdip-specs, .mdip-spec) .spec-row__fn { + color: var(--ink-2); + font-size: .68rem; + line-height: 1.5; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; +} + +:is(.mdip-specs, .mdip-spec) .spec-row__meta { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: .25rem .55rem; + margin-top: .12rem; +} + +:is(.mdip-specs, .mdip-spec) .spec-row__side { + grid-area: side; + display: flex; + flex-wrap: wrap; + align-items: center; + gap: .3rem .5rem; +} +@media (min-width: 52em) { + :is(.mdip-specs, .mdip-spec) .spec-row__side { + flex-direction: column; + align-items: flex-end; + gap: .22rem; + text-align: right; + } +} + +:is(.mdip-specs, .mdip-spec) .spec-row__facts { color: var(--ink-3); font-size: .6rem; line-height: 1.4; } + +:is(.mdip-specs, .mdip-spec) .spec-row__go { + grid-area: go; + align-self: center; + color: var(--brand); + opacity: .3; + transform: translateX(-2px); + transition: opacity .16s var(--ease), transform .16s var(--ease); +} +[data-md-color-scheme="slate"] :is(.mdip-specs, .mdip-spec) .spec-row__go { color: var(--info); } + +/* Stacked, the row is tall and the chevron ends up marooned beside its + middle. The whole row is the link; drop the hint. */ +@media (max-width: 51.99em) { + :is(.mdip-specs, .mdip-spec) .spec-row__go { display: none; } + :is(.mdip-specs, .mdip-spec) .spec-row__link { + grid-template-columns: minmax(0, 1fr); + grid-template-areas: "main" "side"; + } +} + +/* -- specifications with no published finding ------------------------------ */ + +:is(.mdip-specs, .mdip-spec) .specs-defer { margin-top: .9rem; } +:is(.mdip-specs, .mdip-spec) .specs-defer[hidden] { display: none; } + +:is(.mdip-specs, .mdip-spec) .specs-defer__head { + display: flex; + align-items: center; + gap: .45em; + padding: .5em .7em; + border-radius: var(--r-s); + color: var(--ink-3); + font-size: .68rem; + font-weight: 600; + cursor: pointer; + list-style: none; +} +:is(.mdip-specs, .mdip-spec) .specs-defer__head::-webkit-details-marker { display: none; } +:is(.mdip-specs, .mdip-spec) .specs-defer__head:hover { background: var(--muted-bg); color: var(--ink-2); } +:is(.mdip-specs, .mdip-spec) .specs-defer__t { flex: 1; } +:is(.mdip-specs, .mdip-spec) .specs-defer__n { + min-width: 1.25rem; + padding: 0 .35em; + border-radius: 999px; + background: var(--muted-bg); + color: var(--ink-3); + font-size: .58rem; + line-height: 1.25rem; + text-align: center; + font-variant-numeric: tabular-nums; +} +:is(.mdip-specs, .mdip-spec) .specs-defer__head .mdip-i:last-child { transition: transform .2s var(--ease); } +:is(.mdip-specs, .mdip-spec) .specs-defer[open] > .specs-defer__head .mdip-i:last-child { transform: rotate(180deg); } +:is(.mdip-specs, .mdip-spec) .specs-defer__note { + margin: 0 .7em .55rem; + color: var(--ink-3); + font-size: .64rem; + line-height: 1.5; + max-width: 46rem; +} +:is(.mdip-specs, .mdip-spec) .specs-list--defer { background: var(--surface-2); box-shadow: none; } + +/* -- change requests ------------------------------------------------------- */ + +:is(.mdip-specs, .mdip-spec) .specs-foot { + display: flex; + align-items: flex-start; + gap: .45em; + margin: 1.1rem 0 0; + color: var(--ink-3); + font-size: .66rem; + line-height: 1.55; +} +:is(.mdip-specs, .mdip-spec) .specs-foot .mdip-i { flex: none; margin-top: .18em; } + +:is(.mdip-specs, .mdip-spec) .contribute { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: .7rem 1rem; + margin: 2.2rem 0 0; + padding: 1rem 1.1rem; + border: 1px solid var(--line); + border-radius: var(--r-m); + background: var(--surface-2); +} +:is(.mdip-specs, .mdip-spec) .contribute__ico { flex: none; width: 1.5rem; height: 1.5rem; color: var(--ink-3); } +:is(.mdip-specs, .mdip-spec) .contribute__t { flex: 1 1 16rem; min-width: 0; } +:is(.mdip-specs, .mdip-spec) .contribute__t h2 { + margin: 0; + font-family: "Raleway", system-ui, sans-serif; + font-size: .9rem; + font-weight: 700; + color: var(--ink); +} +:is(.mdip-specs, .mdip-spec) .contribute__a { display: flex; flex-wrap: wrap; gap: .5rem; } +:is(.mdip-specs, .mdip-spec) .contribute__t p { margin: .2rem 0 0; color: var(--ink-2); font-size: .68rem; line-height: 1.55; } + +:is(.mdip-specs, .mdip-spec) .specs-empty { + padding: 3rem 1rem; + border: 1px dashed var(--line-strong); + border-radius: var(--r-l); + background: var(--surface-2); + text-align: center; + color: var(--ink-3); +} +:is(.mdip-specs, .mdip-spec) .specs-empty .mdip-i { width: 2.2rem; height: 2.2rem; opacity: .4; } +:is(.mdip-specs, .mdip-spec) .specs-empty h2 { + margin: .6rem 0 .3rem; + font-family: "Raleway", system-ui, sans-serif; + font-size: 1rem; + color: var(--ink); +} +:is(.mdip-specs, .mdip-spec) .specs-empty p { margin: 0 0 1rem; font-size: .72rem; } + +/* -------------------------------------------------------------------------- + Detail page + -------------------------------------------------------------------------- */ + +:is(.mdip-specs, .mdip-spec) .spec-back { + display: inline-flex; + align-items: center; + gap: .35em; + margin: 1rem 1.1rem .2rem; + padding: .3em .8em .3em .55em; + border: 1px solid var(--line); + border-radius: 999px; + background: var(--surface); + color: var(--ink-2) !important; + font-size: .66rem; + font-weight: 600; + text-decoration: none; + transition: transform .18s var(--ease), border-color .18s var(--ease); +} +:is(.mdip-specs, .mdip-spec) .spec-back:hover { transform: translateX(-2px); border-color: var(--brand); } + +@media (min-width: 55em) { :is(.mdip-specs, .mdip-spec) .spec-back { margin: 1.3rem 2rem .2rem; } } + +:is(.mdip-specs, .mdip-spec) .spec-hero { border-radius: 0; margin-top: .6rem; border-top: 1px solid var(--line); } + +:is(.mdip-specs, .mdip-spec) .spec-hero__top { + display: flex; + align-items: center; + gap: .7rem; + flex-wrap: wrap; + margin-bottom: .8rem; +} + +:is(.mdip-specs, .mdip-spec) .spec-hero__full { + margin: 0 0 .7rem; + color: var(--ink-2); + font-size: .88rem; + font-weight: 500; +} + +:is(.mdip-specs, .mdip-spec) .spec-hero__actions { + display: flex; + flex-wrap: wrap; + gap: .5rem; + margin-top: 1.2rem; +} + +:is(.mdip-specs, .mdip-spec) .spec-hero__orgs { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: .45rem 1.1rem; + margin-top: 1.3rem; + padding-top: 1rem; + border-top: 1px solid var(--line); +} +:is(.mdip-specs, .mdip-spec) .spec-hero__orgs-l { + color: var(--ink-3); + font-size: .6rem; + font-weight: 700; + letter-spacing: .08em; + text-transform: uppercase; +} + +:is(.mdip-specs, .mdip-spec) .spec-nav { + position: sticky; + top: var(--hdr); + z-index: 4; + border-bottom: 1px solid var(--line); + background: color-mix(in srgb, var(--bg) 84%, transparent); + backdrop-filter: blur(12px) saturate(1.3); + -webkit-backdrop-filter: blur(12px) saturate(1.3); +} +:is(.mdip-specs, .mdip-spec) .spec-nav ul { + display: flex; + gap: .2rem; + max-width: 62rem; + margin: 0 auto; + padding: .3rem 1.1rem; + list-style: none; + overflow-x: auto; + scrollbar-width: none; +} +:is(.mdip-specs, .mdip-spec) .spec-nav ul::-webkit-scrollbar { display: none; } +:is(.mdip-specs, .mdip-spec) .spec-nav a { + display: inline-flex; + align-items: center; + gap: .35em; + padding: .45em .7em; + border-radius: var(--r-s); + color: var(--ink-2) !important; + font-size: .66rem; + font-weight: 600; + white-space: nowrap; + text-decoration: none; + transition: background-color .16s var(--ease), color .16s var(--ease); +} +:is(.mdip-specs, .mdip-spec) .spec-nav a:hover, +:is(.mdip-specs, .mdip-spec) .spec-nav a.is-active { background: var(--info-bg); color: var(--brand) !important; } +@media (min-width: 55em) { :is(.mdip-specs, .mdip-spec) .spec-nav ul { padding: .3rem 2rem; } } + +:is(.mdip-specs, .mdip-spec) .spec-body { + max-width: 62rem; + margin: 0 auto; + padding: 1.6rem 1.1rem 2rem; +} +@media (min-width: 55em) { :is(.mdip-specs, .mdip-spec) .spec-body { padding: 2.2rem 2rem 3rem; } } + +:is(.mdip-specs, .mdip-spec) .spec-section { + /* header + the sticky in-page nav, so an anchor lands under both. */ + scroll-margin-top: calc(var(--hdr) + 3.2rem); + margin-bottom: 2.2rem; +} + +:is(.mdip-specs, .mdip-spec) .spec-h2 { + display: flex; + align-items: center; + gap: .45em; + margin: 0 0 .8rem; + padding-bottom: .5rem; + border-bottom: 1px solid var(--line); + font-family: "Raleway", system-ui, sans-serif; + font-size: 1.15rem; + font-weight: 700; + letter-spacing: -.01em; + color: var(--ink); +} +:is(.mdip-specs, .mdip-spec) .spec-h2 .mdip-i { color: var(--brand); opacity: .8; } +:is(.mdip-specs, .mdip-spec) [data-md-color-scheme="slate"] .spec-h2 .mdip-i { color: var(--info); } + + +/* -- at a glance ----------------------------------------------------------- */ + +:is(.mdip-specs, .mdip-spec) .glance { + display: grid; + gap: 1px; + margin: 0; + border: 1px solid var(--line); + border-radius: var(--r-m); + background: var(--line); + overflow: hidden; +} + +:is(.mdip-specs, .mdip-spec) .glance__row { + display: grid; + gap: .2rem .9rem; + padding: .7rem .9rem; + background: var(--surface); +} + +@media (min-width: 42em) { + :is(.mdip-specs, .mdip-spec) .glance__row { grid-template-columns: 15rem minmax(0, 1fr); align-items: baseline; } +} + +:is(.mdip-specs, .mdip-spec) .glance dt { + display: flex; + align-items: center; + gap: .4em; + color: var(--ink-3); + font-size: .66rem; + font-weight: 600; +} +:is(.mdip-specs, .mdip-spec) .glance dt .mdip-i { opacity: .7; } + +:is(.mdip-specs, .mdip-spec) .glance dd { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: .35rem .5rem; + margin: 0; + color: var(--ink); + font-size: .72rem; + overflow-wrap: anywhere; +} +:is(.mdip-specs, .mdip-spec) .glance dd code { + padding: .1em .4em; + border-radius: 4px; + background: var(--muted-bg); + font-size: .68rem; +} +:is(.mdip-specs, .mdip-spec) .glance__note { color: var(--ink-3); font-size: .64rem; } + +/* -- criteria -------------------------------------------------------------- */ + +/* Five criteria, five rows. The verdict answers the question; the reasoning + folds away behind the row so the section stays a summary rather than a wall + of five rationales. Tone lives in the icon and the pill only. */ +:is(.mdip-specs, .mdip-spec) .crits { + border: 1px solid var(--line); + border-radius: var(--r-m); + background: var(--surface); + box-shadow: var(--sh-1); + overflow: hidden; +} + +:is(.mdip-specs, .mdip-spec) .crit { border: 0; border-radius: 0; background: none; box-shadow: none; } +:is(.mdip-specs, .mdip-spec) .crit + .crit { border-top: 1px solid var(--line); } + +:is(.mdip-specs, .mdip-spec) .crit__head { + display: grid; + grid-template-columns: auto minmax(0, 1fr) auto; + grid-template-areas: "ico title chev" ". pill chev"; + align-items: start; + gap: .25rem .55rem; + padding: .55rem .75rem; + list-style: none; +} +:is(.mdip-specs, .mdip-spec) .crit__head::-webkit-details-marker { display: none; } +:is(.mdip-specs, .mdip-spec) details.crit > .crit__head { cursor: pointer; } +:is(.mdip-specs, .mdip-spec) details.crit > .crit__head:hover { background: var(--surface-2); } + +@media (min-width: 42em) { + :is(.mdip-specs, .mdip-spec) .crit__head { + grid-template-columns: auto minmax(0, 1fr) auto auto; + grid-template-areas: "ico title pill chev"; + align-items: center; + gap: .7rem; + } +} + +:is(.mdip-specs, .mdip-spec) .crit__icon { grid-area: ico; display: inline-flex; padding-top: .1rem; } +:is(.mdip-specs, .mdip-spec) .crit__icon .mdip-i { width: 1.05rem; height: 1.05rem; } +:is(.mdip-specs, .mdip-spec) .crit--pass .crit__icon { color: var(--pass); } +:is(.mdip-specs, .mdip-spec) .crit--warn .crit__icon { color: var(--warn); } +:is(.mdip-specs, .mdip-spec) .crit--fail .crit__icon { color: var(--fail); } +:is(.mdip-specs, .mdip-spec) .crit--muted .crit__icon { color: var(--muted); } +@media (min-width: 42em) { :is(.mdip-specs, .mdip-spec) .crit__icon { padding-top: 0; } } + +:is(.mdip-specs, .mdip-spec) .crit__title { + grid-area: title; + display: flex; + flex-direction: column; + gap: .05rem; + min-width: 0; +} +:is(.mdip-specs, .mdip-spec) .crit__label { + font-family: "Raleway", system-ui, sans-serif; + font-size: .78rem; + font-weight: 700; + line-height: 1.3; + color: var(--ink); +} +:is(.mdip-specs, .mdip-spec) .crit__blurb { color: var(--ink-3); font-size: .62rem; line-height: 1.45; } + +:is(.mdip-specs, .mdip-spec) .crit__verdict { grid-area: pill; justify-self: start; display: inline-flex; align-items: center; gap: .4rem; } +@media (min-width: 42em) { :is(.mdip-specs, .mdip-spec) .crit__verdict { justify-self: end; } } + +:is(.mdip-specs, .mdip-spec) .crit__src { + grid-area: chev; + align-self: center; + justify-self: start; +} +@media (min-width: 42em) { :is(.mdip-specs, .mdip-spec) .crit__src { justify-self: end; } } +:is(.mdip-specs, .mdip-spec) .crit__src .srclink { + padding: .22em .5em; + font-size: .6rem; +} + +:is(.mdip-specs, .mdip-spec) .crit__chev { + grid-area: chev; + align-self: center; + color: var(--ink-3); + opacity: .55; + transition: transform .2s var(--ease); +} +:is(.mdip-specs, .mdip-spec) .crit__chev .mdip-i { width: .95rem; height: .95rem; } +:is(.mdip-specs, .mdip-spec) details.crit[open] > .crit__head .crit__chev { transform: rotate(180deg); } + +:is(.mdip-specs, .mdip-spec) .crit__body { + padding: .6rem .8rem .7rem 2.35rem; + border-top: 1px solid var(--line); + background: var(--surface-2); + animation: mdip-reveal .24s var(--ease); +} +:is(.mdip-specs, .mdip-spec) .crit__rationale { + margin: 0 0 .5rem; + color: var(--ink-2); + font-size: .68rem; + line-height: 1.6; +} +:is(.mdip-specs, .mdip-spec) .crit__rationale:last-child { margin-bottom: 0; } + +:is(.mdip-specs, .mdip-spec) .spec-h2__n { + margin-left: auto; + color: var(--ink-3); + font-family: "Poppins", system-ui, sans-serif; + font-size: .66rem; + font-weight: 600; + font-variant-numeric: tabular-nums; + letter-spacing: 0; + text-transform: none; +} + +/* -- licensing ------------------------------------------------------------- */ + +:is(.mdip-specs, .mdip-spec) .lics { display: grid; gap: .6rem; } +@media (min-width: 48em) { :is(.mdip-specs, .mdip-spec) .lics { grid-template-columns: repeat(2, minmax(0, 1fr)); } } + +:is(.mdip-specs, .mdip-spec) .lic { + padding: .8rem .9rem; + border: 1px solid var(--line); + border-radius: var(--r-m); + background: var(--surface); + box-shadow: var(--sh-1); +} + +:is(.mdip-specs, .mdip-spec) .lic__head { display: flex; flex-wrap: wrap; align-items: baseline; gap: .4rem .6rem; } +:is(.mdip-specs, .mdip-spec) .lic__head h3 { + margin: 0; + flex: 1; + font-family: "Raleway", system-ui, sans-serif; + font-size: .8rem; + font-weight: 700; + color: var(--ink); +} +:is(.mdip-specs, .mdip-spec) .lic__head h3 a { color: inherit !important; text-decoration: none; } +:is(.mdip-specs, .mdip-spec) .lic__head h3 a:hover { color: var(--brand) !important; } +:is(.mdip-specs, .mdip-spec) .lic__head h3 a { + display: inline-flex; + align-items: baseline; + gap: .3em; +} +:is(.mdip-specs, .mdip-spec) .lic__head h3 .mdip-i { width: .8em; height: .8em; opacity: .45; } + +:is(.mdip-specs, .mdip-spec) .lic__name { + margin: .15rem 0 0; + color: var(--ink-3); + font-size: .66rem; + line-height: 1.4; +} +:is(.mdip-specs, .mdip-spec) .lic__scopes { display: flex; gap: .25rem; flex-wrap: wrap; } + +:is(.mdip-specs, .mdip-spec) .lic__note { + display: flex; + gap: .4em; + margin: .5rem 0 0; + color: var(--ink-3); + font-size: .64rem; + line-height: 1.5; +} +:is(.mdip-specs, .mdip-spec) .lic__evidence { margin: .7rem 0 0; } + +:is(.mdip-specs, .mdip-spec) .disclosure { + margin-top: .9rem; + border: 1px solid var(--line); + border-radius: var(--r-m); + background: var(--surface-2); +} +:is(.mdip-specs, .mdip-spec) .disclosure summary { + display: flex; + align-items: center; + gap: .4em; + padding: .55rem .8rem; + color: var(--ink-3); + font-size: .64rem; + font-weight: 600; + cursor: pointer; + list-style: none; +} +:is(.mdip-specs, .mdip-spec) .disclosure summary::-webkit-details-marker { display: none; } +:is(.mdip-specs, .mdip-spec) .disclosure summary:hover { color: var(--ink); } +:is(.mdip-specs, .mdip-spec) .disclosure[open] summary { border-bottom: 1px solid var(--line); } +:is(.mdip-specs, .mdip-spec) .disclosure__t { + margin: 0; + padding: .7rem .8rem; + color: var(--ink-2); + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-size: .64rem; + line-height: 1.6; + overflow-wrap: anywhere; +} +:is(.mdip-specs, .mdip-spec) .disclosure__t + .disclosure__t { padding-top: 0; } + +/* -- maintainers ----------------------------------------------------------- */ + +:is(.mdip-specs, .mdip-spec) .orgs { display: grid; gap: .6rem; } +@media (min-width: 48em) { :is(.mdip-specs, .mdip-spec) .orgs { grid-template-columns: repeat(2, minmax(0, 1fr)); } } + +:is(.mdip-specs, .mdip-spec) .orgcard { + display: flex; + align-items: center; + gap: .8rem; + padding: .8rem .9rem; + border: 1px solid var(--line); + border-radius: var(--r-m); + background: var(--surface); + box-shadow: var(--sh-1); + transition: border-color .2s var(--ease), box-shadow .2s var(--ease); +} +:is(.mdip-specs, .mdip-spec) .orgcard:hover { border-color: color-mix(in srgb, var(--brand) 34%, var(--line)); box-shadow: var(--sh-2); } +:is(.mdip-specs, .mdip-spec) .orgcard .org-logo { width: 2.4rem; height: 2.4rem; } +:is(.mdip-specs, .mdip-spec) .orgcard .org-logo--mono { font-size: .9rem; border-radius: 8px; } +:is(.mdip-specs, .mdip-spec) .orgcard__t { min-width: 0; } +:is(.mdip-specs, .mdip-spec) .orgcard__t h3 { + margin: 0; + font-family: "Raleway", system-ui, sans-serif; + font-size: .78rem; + font-weight: 700; + line-height: 1.3; + color: var(--ink); +} +:is(.mdip-specs, .mdip-spec) .orgcard__t p { margin: 0; color: var(--ink-3); font-size: .62rem; } +:is(.mdip-specs, .mdip-spec) .orgcard__t a { + display: inline-flex; + align-items: center; + gap: .25em; + margin-top: .15rem; + font-size: .64rem; + font-weight: 600; + overflow-wrap: anywhere; +} +:is(.mdip-specs, .mdip-spec) .orgcard__t a .mdip-i { width: .8em; height: .8em; opacity: .55; } + +:is(.mdip-specs, .mdip-spec) .related { margin-top: 1.1rem; } +:is(.mdip-specs, .mdip-spec) .related h3 { + display: flex; + align-items: center; + gap: .4em; + margin: 0 0 .5rem; + color: var(--ink-3); + font-size: .6rem; + font-weight: 700; + letter-spacing: .07em; + text-transform: uppercase; +} +:is(.mdip-specs, .mdip-spec) .related ul { display: flex; flex-wrap: wrap; gap: .4rem; margin: 0; padding: 0; list-style: none; } +:is(.mdip-specs, .mdip-spec) .related a { + display: inline-flex; + align-items: center; + gap: .35em; + padding: .3em .75em; + border: 1px solid var(--line-strong); + border-radius: 999px; + background: var(--surface); + color: var(--ink) !important; + font-size: .66rem; + font-weight: 600; + text-decoration: none; + transition: transform .18s var(--ease), border-color .18s var(--ease); +} +:is(.mdip-specs, .mdip-spec) .related a:hover { transform: translateY(-1px); border-color: var(--brand); } +:is(.mdip-specs, .mdip-spec) .related__i--pass { color: var(--pass); } +:is(.mdip-specs, .mdip-spec) .related__i--warn { color: var(--warn); } +:is(.mdip-specs, .mdip-spec) .related__i--fail { color: var(--fail); } +:is(.mdip-specs, .mdip-spec) .related__i--info { color: var(--info); } +:is(.mdip-specs, .mdip-spec) .related__i--muted { color: var(--muted); } + +:is(.mdip-specs, .mdip-spec) .spec-foot { + display: flex; + gap: .5em; + max-width: 62rem; + margin: 0 auto; + padding: 1.2rem 1.1rem 2.4rem; + color: var(--ink-3); + font-size: .64rem; + line-height: 1.55; +} +:is(.mdip-specs, .mdip-spec) .spec-foot p { margin: 0; } +@media (min-width: 55em) { :is(.mdip-specs, .mdip-spec) .spec-foot { padding: 1.2rem 2rem 3rem; } } + +/* -------------------------------------------------------------------------- + Material list overrides + + Material styles every list inside .md-typeset with + `.md-typeset ul:not([hidden]) { display: flow-root }` and sets the marker on + `.md-typeset ul li`. Both outrank a plain class selector, so the few lists + that carry layout restate it here using the same :not([hidden]) idiom. + -------------------------------------------------------------------------- */ + +/* Material also indents every list: `[dir=ltr] .md-typeset ul { margin-left: + .625em }` plus padding, which pushes the filter options out of alignment + with their headings. Reset the gutters first, then restate the few lists + that carry layout or a margin of their own. */ +:is(.mdip-specs, .mdip-spec) ul:not([hidden]), +:is(.mdip-specs, .mdip-spec) ol:not([hidden]) { + margin: 0; + padding: 0; + list-style: none; +} + +:is(.mdip-specs, .mdip-spec) ul.specs-stats:not([hidden]) { + display: grid; + margin: 1.2rem 0 0; +} +@media (min-width: 55em) { + :is(.mdip-specs, .mdip-spec) ul.specs-stats:not([hidden]) { margin: 1.6rem 0 0; } +} +:is(.mdip-specs, .mdip-spec) ol.specs-list:not([hidden]) { display: block; } +:is(.mdip-specs, .mdip-spec) ul.facet__list:not([hidden]) { display: block; } +:is(.mdip-specs, .mdip-spec) .related ul:not([hidden]) { display: flex; } +:is(.mdip-specs, .mdip-spec) .spec-nav ul:not([hidden]) { + display: flex; + margin: 0 auto; + padding: .3rem 1.1rem; +} +@media (min-width: 55em) { + :is(.mdip-specs, .mdip-spec) .spec-nav ul:not([hidden]) { padding: .3rem 2rem; } +} + +/* Several controls set `display` explicitly, which beats the UA's [hidden] + rule; the script toggles `hidden`, so it has to win. */ +:is(.mdip-specs, .mdip-spec) [hidden] { display: none !important; } + +:is(.mdip-specs, .mdip-spec) ul li, +:is(.mdip-specs, .mdip-spec) ol li { + margin: 0; + list-style: none; +} + +:is(.mdip-specs, .mdip-spec) ul li::marker, +:is(.mdip-specs, .mdip-spec) ol li::marker { content: ""; } + +:is(.mdip-specs, .mdip-spec) li.spec-row { display: block; } +:is(.mdip-specs, .mdip-spec) li.spec-row[hidden] { display: none; } + +/* -------------------------------------------------------------------------- + Material
    overrides + + pymdownx.details makes every
    in .md-typeset look like a collapsible + admonition: tinted box, left border, and an icon plus chevron drawn as + summary::before / ::after. `[dir=ltr] .md-typeset summary` is 0-2-1, so the + filter disclosures restate their own look here at 0-3-1 and strip the two + pseudo-elements. + -------------------------------------------------------------------------- */ + +:is(.mdip-specs, .mdip-spec) details.facet, +:is(.mdip-specs, .mdip-spec) details.disclosure, +:is(.mdip-specs, .mdip-spec) details.crit, +:is(.mdip-specs, .mdip-spec) details.specs-defer { + display: block; + overflow: visible; + padding: 0; + border-radius: 0; + background: none; + box-shadow: none; + font-size: inherit; +} + +:is(.mdip-specs, .mdip-spec) details.crit { border: 0; } +:is(.mdip-specs, .mdip-spec) details.crit + .crit { border-top: 1px solid var(--line); } +:is(.mdip-specs, .mdip-spec) details.specs-defer { border: 0; margin-top: .9rem; } + +:is(.mdip-specs, .mdip-spec) details.facet { + border: 0; + border-bottom: 1px solid var(--line); + border-radius: 0; + background: none; +} +:is(.mdip-specs, .mdip-spec) details.facet:last-child { border-bottom: 0; } + +:is(.mdip-specs, .mdip-spec) details.disclosure { + margin-top: .9rem; + border: 1px solid var(--line); + border-radius: var(--r-m); + background: var(--surface-2); +} + +:is(.mdip-specs, .mdip-spec) details.facet > summary, +:is(.mdip-specs, .mdip-spec) details.disclosure > summary, +:is(.mdip-specs, .mdip-spec) details.crit > summary, +:is(.mdip-specs, .mdip-spec) details.specs-defer > summary { + display: flex; + align-items: center; + /* Material bleeds the admonition title past its box with `margin: 0 -.6rem`, + which pushed the chevron outside the filter panel. */ + margin: 0; + min-height: 0; + overflow: visible; + border: 0; + border-radius: 0; + background: none; + font-weight: 600; + list-style: none; +} + +:is(.mdip-specs, .mdip-spec) details.facet > summary { + gap: .45em; + padding: .42rem .2rem; + color: var(--ink); + font-size: .68rem; +} +:is(.mdip-specs, .mdip-spec) details.disclosure > summary { + gap: .4em; + padding: .55rem .8rem; + color: var(--ink-3); + font-size: .64rem; +} +:is(.mdip-specs, .mdip-spec) details.disclosure[open] > summary { + border-bottom: 1px solid var(--line); +} + +:is(.mdip-specs, .mdip-spec) details.facet > summary::before, +:is(.mdip-specs, .mdip-spec) details.facet > summary::after, +:is(.mdip-specs, .mdip-spec) details.disclosure > summary::before, +:is(.mdip-specs, .mdip-spec) details.disclosure > summary::after, +:is(.mdip-specs, .mdip-spec) details.crit > summary::before, +:is(.mdip-specs, .mdip-spec) details.crit > summary::after, +:is(.mdip-specs, .mdip-spec) details.specs-defer > summary::before, +:is(.mdip-specs, .mdip-spec) details.specs-defer > summary::after { + display: none; + content: none; +} + +/* Both of these are grids, so they must not inherit the flex row above. + The padding has to be restated too: Material reserves 2rem on the left of + every summary for the admonition icon it is not drawing here, which + otherwise indents the collapsible criterion rows past the plain ones. */ +:is(.mdip-specs, .mdip-spec) details.crit > summary.crit__head { + display: grid; + padding: .55rem .75rem; +} +:is(.mdip-specs, .mdip-spec) details.specs-defer > summary { gap: .45em; padding: .5em .7em; font-size: .68rem; } + +/* -------------------------------------------------------------------------- + Motion preference + -------------------------------------------------------------------------- */ + +@media (prefers-reduced-motion: reduce) { + .mdip-specs *, + .mdip-spec * { + animation-duration: .001ms !important; + animation-iteration-count: 1 !important; + transition-duration: .001ms !important; + } + :is(.mdip-specs, .mdip-spec) .specs-hero__glow, + :is(.mdip-specs, .mdip-spec) .spec-hero__glow { animation: none; } +} diff --git a/overrides/main.html b/overrides/main.html new file mode 100644 index 0000000..416a262 --- /dev/null +++ b/overrides/main.html @@ -0,0 +1,10 @@ +{% extends "base.html" %} + +{#- A page can opt out of search engine indexing with `robots: noindex` in its + front matter. The specification registry uses this while it is unlisted. -#} +{% block extrahead %} + {{ super() }} + {%- if page and page.meta and page.meta.robots %} + + {%- endif %} +{% endblock %} diff --git a/overrides/sitemap.xml b/overrides/sitemap.xml new file mode 100644 index 0000000..9bd293b --- /dev/null +++ b/overrides/sitemap.xml @@ -0,0 +1,14 @@ + +{#- MkDocs' own sitemap template, plus one rule: pages whose front matter sets + `robots: noindex` are left out, so an unlisted page is not advertised to + search engines here either. -#} + +{%- for file in pages -%} + {% if not file.page.is_link and (file.page.abs_url or file.page.canonical_url) and not (file.page.meta and 'noindex' in (file.page.meta.robots or '')) %} + + {% if file.page.canonical_url %}{{ file.page.canonical_url|e }}{% else %}{{ file.page.abs_url|e }}{% endif %} + {% if file.page.update_date %}{{file.page.update_date}}{% endif %} + + {%- endif -%} +{% endfor %} + diff --git a/scripts/check_specs.py b/scripts/check_specs.py new file mode 100644 index 0000000..2e8a9d0 --- /dev/null +++ b/scripts/check_specs.py @@ -0,0 +1,74 @@ +"""Validate the specification registry in data/ and summarise it. + +Run with `make specs-check`; the pull request workflow runs it too. Errors +(an unknown vocabulary value or key, a maintainer with no organization file, +a malformed URL) exit with status 1. Warnings are printed but do not fail: +they point at data that is valid but worth a second look, such as a recorded +status the verdicts do not support. + +`--export DIR` also writes the API files (see spec_api.py) into DIR, which is +how to get the full export without building the site. +""" + +from __future__ import annotations + +import os +import sys +from collections import Counter + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + +import spec_api # noqa: E402 +import spec_data # noqa: E402 + +SITE_URL = "https://interoperablemobility.org/" + + +def main(argv): + data = spec_data.build_dataset() + specs = data["specs"] + + print("%d specifications, %d organizations, %d principles" + % (len(specs), len(data["organizations"]), len(data["criteria"]))) + for label, n in Counter(s["status_label"] for s in specs).most_common(): + print(" %-22s %d" % (label, n)) + + print("\nDisplay order (known adopters first, then ranking score):") + for s in specs: + score = "-" if s["score"] is None else "%+d" % s["score"] + count = "" if s["adopters_count"] is None else "%d adopters" % s["adopters_count"] + print(" %4s %-36s %-20s %s" % (score, s["short_name"][:36], s["status_label"], count)) + + for key in sorted(k for k in data["organizations"] + if not any(k in s["maintainer_ids"] for s in specs)): + data["issues"].append({"level": "warning", + "where": spec_data.entry_path(spec_data.ORGS_DIR, key), + "detail": "not referenced by any specification"}) + + if "--export" in argv: + out = argv[argv.index("--export") + 1] + files = spec_api.build_api(data, SITE_URL) + for path, text in files.items(): + dest = os.path.join(out, path) + os.makedirs(os.path.dirname(dest), exist_ok=True) + with open(dest, "w", encoding="utf-8") as fh: + fh.write(text) + print("\nWrote %d API files to %s" % (len(files), os.path.join(out, spec_api.API_DIR))) + + errors = [i for i in data["issues"] if i["level"] == "error"] + warnings = [i for i in data["issues"] if i["level"] == "warning"] + for title, items in (("WARNINGS", warnings), ("ERRORS", errors)): + if items: + print("\n%s (%d)" % (title, len(items))) + for i in items: + print(" %s: %s" % (i["where"], i["detail"])) + + if errors: + print("\nFix the errors above; data/README.md lists the allowed values.") + return 1 + print("\nOK: no errors.") + return 0 + + +if __name__ == "__main__": + sys.exit(main(sys.argv)) diff --git a/scripts/spec_api.py b/scripts/spec_api.py new file mode 100644 index 0000000..6681b66 --- /dev/null +++ b/scripts/spec_api.py @@ -0,0 +1,384 @@ +"""The registry's read-only JSON API, written as static files at build time. + +`build_api` takes the joined dataset from spec_data.build_dataset() and +returns {published path: text}. The MkDocs hook publishes those files under +/api/, and `make specs-export` writes the same files to a local folder. + + api/index.json what is here, and the vocabularies + api/catalogue.json one short record per specification + api/specifications.json every specification, fully joined + api/specifications/.json one specification + api/organizations.json organizations and what they maintain + api/principles.json the assessment criteria and their rules + api/licences.json the licence list + api/export.csv every specification as one flat table + api/openapi.json OpenAPI 3 description of all of the above + api/docs/ Swagger UI page for openapi.json (not indexed) + +The ranking score is deliberately not published: it only orders the listing. +""" + +from __future__ import annotations + +import csv +import datetime as _dt +import io +import json + +import spec_data + +API_DIR = "api" +API_VERSION = 1 + + +def _page(site_url, spec): + return "%sspecifications/%s/" % (site_url, spec["slug"]) + + +def _api(site_url, path): + return "%s%s/%s" % (site_url, API_DIR, path) + + +def _source(path): + return spec_data.github_url("blob", path) + + +def _org_ref(org): + return {"id": org["id"], "name": org["name"], "short_name": org["short"]} + + +def full_record(spec, site_url): + return { + "id": spec["id"], + "short_name": spec["short_name"], + "full_name": spec["full_name"] or None, + "description": spec["description"] or None, + "homepage_url": spec["homepage_url"], + "status": {"id": spec["status"], "label": spec["status_label"]}, + "modes": spec["mode_ids"], + "maintainers": [dict(_org_ref(o), type=o["type"], url=o["url"]) + for o in spec["maintainers"]], + "licences": [{"id": l["id"], "name": l["name"], "category": l["category"], + "url": l["url"], "scopes": l["scopes"]} for l in spec["licences"]], + "licence_evidence_urls": spec["licence_evidence"], + "first_release_year": spec["first_release_year"], + "adoption": { + "lifecycle": spec["lifecycle"], + "tier": spec["adoption_tier"], + "adopters_count": spec["adopters_count"], + "adopters_count_updated": spec["adopters_count_updated"], + "adopters_registry_url": spec["adopters_registry"], + "confirmed_by_maintainer": {"yes": True, "no": False}.get(spec["confirmed"]), + }, + "principles": [{"id": c["key"], "name": c["label"], "verdict": c["verdict"], + "rationale": c["rationale"] or None, + "evidence_url": c["evidence_url"]} for c in spec["criteria"]], + "principles_met": spec["criteria_met"], + "page_url": _page(site_url, spec), + "source_url": _source(spec["source_path"]), + } + + +def catalogue_record(spec, site_url): + return { + "id": spec["id"], + "short_name": spec["short_name"], + "full_name": spec["full_name"] or None, + "summary": spec["function"] or None, + "status": spec["status"], + "modes": spec["mode_ids"], + "maintainers": [_org_ref(o) for o in spec["maintainers"]], + "licences": [l["id"] for l in spec["licences"]], + "lifecycle": spec["lifecycle"], + "adoption_tier": spec["adoption_tier"], + "adopters_count": spec["adopters_count"], + "page_url": _page(site_url, spec), + "detail_url": _api(site_url, "specifications/%s.json" % spec["id"]), + } + + +def export_csv(records): + """One row per specification; lists are joined with "; ".""" + cols = ["id", "short_name", "full_name", "status", "modes", "maintainer_ids", + "maintainer_names", "licences", "homepage_url", "first_release_year", + "lifecycle", "adoption_tier", "adopters_count", "adopters_count_updated", + "adopters_registry_url", "confirmed_by_maintainer"] + principle_ids = [p["id"] for p in records[0]["principles"]] if records else [] + for pid in principle_ids: + cols += [pid + "_verdict", pid + "_rationale", pid + "_evidence_url"] + cols += ["description", "page_url", "source_url"] + + buf = io.StringIO() + w = csv.DictWriter(buf, cols, lineterminator="\n") + w.writeheader() + for r in records: + a = r["adoption"] + row = { + "id": r["id"], "short_name": r["short_name"], "full_name": r["full_name"], + "status": r["status"]["id"], "modes": "; ".join(r["modes"]), + "maintainer_ids": "; ".join(m["id"] for m in r["maintainers"]), + "maintainer_names": "; ".join(m["name"] for m in r["maintainers"]), + "licences": "; ".join( + l["id"] + (" (%s)" % ", ".join(l["scopes"]) if l["scopes"] else "") + for l in r["licences"] if l["id"] != "Unspecified"), + "homepage_url": r["homepage_url"], "first_release_year": r["first_release_year"], + "lifecycle": a["lifecycle"], "adoption_tier": a["tier"], + "adopters_count": a["adopters_count"], + "adopters_count_updated": a["adopters_count_updated"], + "adopters_registry_url": a["adopters_registry_url"], + "confirmed_by_maintainer": {True: "yes", False: "no"}.get( + a["confirmed_by_maintainer"], "unknown"), + "description": r["description"], "page_url": r["page_url"], + "source_url": r["source_url"], + } + for p in r["principles"]: + row[p["id"] + "_verdict"] = p["verdict"] + row[p["id"] + "_rationale"] = p["rationale"] + row[p["id"] + "_evidence_url"] = p["evidence_url"] + w.writerow(row) + return buf.getvalue() + + +def _enum(vocab): + return {"type": "string", "enum": list(vocab)} + + +def _nullable(schema): + return dict(schema, nullable=True) + + +def openapi(site_url): + """OpenAPI 3.0 description of the files build_api publishes. + + Enums are read from the vocabularies in spec_data, so the documentation + cannot drift from what the validator accepts. + """ + V = spec_data + s, i, url = {"type": "string"}, {"type": "integer"}, {"type": "string", "format": "uri"} + ref = lambda name: {"$ref": "#/components/schemas/" + name} # noqa: E731 + arr = lambda item: {"type": "array", "items": item} # noqa: E731 + envelope = {"api_version": i, "generated": dict(s, format="date")} + + def obj(props, required=()): + out = {"type": "object", "properties": props} + if required: + out["required"] = list(required) + return out + + schemas = { + "OrganizationRef": obj({"id": s, "name": s, "short_name": s}, ("id", "name")), + "Licence": obj({"id": s, "name": s, "category": _enum(V.LICENCE_CATEGORIES), + "url": _nullable(url), "note": _nullable(s)}, ("id", "name", "category")), + "SpecificationLicence": obj({"id": s, "name": s, "category": _enum(V.LICENCE_CATEGORIES), + "url": _nullable(url), + "scopes": arr(_enum(V.LICENCE_SCOPES))}), + "PrincipleVerdict": obj({"id": s, "name": s, "verdict": _enum(V.VERDICT), + "rationale": _nullable(s), "evidence_url": _nullable(url)}, + ("id", "verdict")), + "Adoption": obj({ + "lifecycle": _nullable(_enum(V.LIFECYCLE)), + "tier": _nullable(_enum(V.ADOPTION_TIER)), + "adopters_count": _nullable(i), + "adopters_count_updated": _nullable(dict(s, format="date")), + "adopters_registry_url": _nullable(url), + "confirmed_by_maintainer": _nullable({"type": "boolean"}), + }), + "CatalogueRecord": obj({ + "id": s, "short_name": s, "full_name": _nullable(s), "summary": _nullable(s), + "status": _enum(V.STATUS), "modes": arr(_enum(V.MODES)), + "maintainers": arr(ref("OrganizationRef")), "licences": arr(s), + "lifecycle": _nullable(_enum(V.LIFECYCLE)), + "adoption_tier": _nullable(_enum(V.ADOPTION_TIER)), + "adopters_count": _nullable(i), "page_url": url, "detail_url": url, + }, ("id", "short_name", "status")), + "Specification": obj({ + "id": s, "short_name": s, "full_name": _nullable(s), + "description": dict(_nullable(s), description="Markdown."), + "homepage_url": _nullable(url), + "status": obj({"id": _enum(V.STATUS), "label": s}), + "modes": arr(_enum(V.MODES)), + "maintainers": arr(obj({"id": s, "name": s, "short_name": s, + "type": _enum(V.ORG_TYPES), "url": _nullable(url)})), + "licences": arr(ref("SpecificationLicence")), + "licence_evidence_urls": arr(url), + "first_release_year": _nullable(i), + "adoption": ref("Adoption"), + "principles": arr(ref("PrincipleVerdict")), + "principles_met": i, + "page_url": url, + "source_url": dict(url, description="The entry's file on GitHub."), + }, ("id", "short_name", "status", "principles")), + "Organization": obj({ + "id": s, "name": s, "short_name": s, "type": _enum(V.ORG_TYPES), + "roles": arr(_enum(V.ORG_ROLES)), "url": _nullable(url), "logo_url": _nullable(url), + "description": _nullable(s), "specifications": arr(s), "source_url": url, + }, ("id", "name", "type")), + "Principle": obj({"id": s, "order": i, "name": s, "definition": s, + "compliant_when": s, "partially_compliant_when": s, + "not_compliant_when": s}, ("id", "name")), + } + + def get(summary, description, schema, tag, media="application/json", params=None): + op = {"summary": summary, "description": description, "tags": [tag], + "responses": {"200": {"description": "OK", + "content": {media: {"schema": schema}}}}} + if params: + op["parameters"] = params + return {"get": op} + + order = "Specifications are listed in the registry's display order: most known adopters first." + paths = { + "/index.json": get("Endpoints and vocabularies", + "Every endpoint, and each vocabulary with its labels.", + obj(dict(envelope, description=s, order=s, + endpoints={"type": "object", "additionalProperties": url}, + vocabularies={"type": "object"})), "Registry"), + "/catalogue.json": get("Catalogue", "One short record per specification. " + order, + obj(dict(envelope, count=i, + specifications=arr(ref("CatalogueRecord")))), + "Specifications"), + "/specifications.json": get("All specifications", + "Every specification with its organizations, licences and " + "verdicts joined in. " + order, + obj(dict(envelope, count=i, + specifications=arr(ref("Specification")))), + "Specifications"), + "/specifications/{id}.json": get( + "One specification", "A single specification, fully joined.", + obj(dict(envelope, specification=ref("Specification"))), "Specifications", + params=[{"name": "id", "in": "path", "required": True, "schema": s, + "description": "Specification id, for example gtfs-schedule.", + "example": "gtfs-schedule"}]), + "/organizations.json": get("Organizations", + "Organizations, with the ids of the specifications each one " + "maintains.", + obj(dict(envelope, organizations=arr(ref("Organization")))), + "Organizations"), + "/principles.json": get("Principles", "The MDIP criteria and what earns each verdict.", + obj(dict(envelope, principles=arr(ref("Principle")))), + "Methodology"), + "/licences.json": get("Licences", "The licences specifications may be released under.", + obj(dict(envelope, licences=arr(ref("Licence")))), "Methodology"), + "/export.csv": get("Full export (CSV)", + "Every specification as one flat table, one row per specification. " + "Lists are joined with \"; \". " + order, + dict(s, format="csv"), "Specifications", media="text/csv"), + } + + return { + "openapi": "3.0.3", + "info": { + "title": "MDIP specification registry API", + "version": str(API_VERSION), + "description": ( + "Read-only, static API of the MDIP specification registry. Every file is " + "regenerated from the registry's source files on GitHub at each build of the " + "site; there is no authentication and no rate limit. To change the data, edit " + "the source files: %s" % spec_data.github_url("tree", "data")), + }, + "externalDocs": {"description": "Data format and contribution guide", + "url": spec_data.github_url("blob", "data/README.md")}, + "servers": [{"url": site_url + API_DIR}], + "tags": [{"name": n} for n in ("Specifications", "Organizations", "Methodology", + "Registry")], + "paths": paths, + "components": {"schemas": schemas}, + } + + +# Swagger UI for openapi.json. A standalone page rather than a site page, so +# the explorer gets the full width; it asks search engines not to index it, +# like the rest of the registry while it is unlisted. +DOCS_HTML = """ + + + + + +MDIP specification registry API + + + + + +
    + + + + +""" + + +def _json(obj): + return json.dumps(obj, ensure_ascii=False, indent=2) + "\n" + + +def build_api(data, site_url): + """Return {published path: text} for every API file.""" + site_url = (site_url or "/").rstrip("/") + "/" + specs = data["specs"] + records = [full_record(s, site_url) for s in specs] + generated = _dt.date.today().isoformat() + envelope = {"api_version": API_VERSION, "generated": generated} + + maintained = {} + for s in specs: + for mid in s["maintainer_ids"]: + maintained.setdefault(mid, []).append(s["id"]) + + files = {} + files["catalogue.json"] = _json(dict(envelope, count=len(specs), specifications=[ + catalogue_record(s, site_url) for s in specs])) + files["specifications.json"] = _json(dict(envelope, count=len(records), + specifications=records)) + for r in records: + files["specifications/%s.json" % r["id"]] = _json(dict(envelope, specification=r)) + files["organizations.json"] = _json(dict(envelope, organizations=[{ + "id": o["id"], "name": o["name"], "short_name": o["short"], "type": o["type"], + "roles": o["roles"], "url": o["url"], "logo_url": o["logo_url"], + "description": o["description"] or None, + "specifications": maintained.get(o["id"], []), + "source_url": _source(o["source_path"]), + } for o in data["organizations"].values()])) + files["principles.json"] = _json(dict(envelope, principles=[{ + "id": p["key"], "order": p["order"], "name": p["label"], "definition": p["blurb"], + "compliant_when": p["when"]["compliant"], + "partially_compliant_when": p["when"]["partially_compliant"], + "not_compliant_when": p["when"]["not_compliant"], + } for p in data["criteria"]])) + files["licences.json"] = _json(dict(envelope, licences=list(data["licences"].values()))) + files["export.csv"] = export_csv(records) + files["openapi.json"] = _json(openapi(site_url)) + + files["index.json"] = _json(dict(envelope, description=( + "Read-only API of the MDIP specification registry. Generated from the files in " + "%s on every build." % spec_data.github_url("tree", "data")), + order="catalogue.json and specifications.json list specifications in the " + "registry's display order: most known adopters first.", + endpoints=dict((path, _api(site_url, path)) for path in sorted(files) + if not path.startswith("specifications/")), + documentation=_api(site_url, "docs/"), + vocabularies={ + "status": dict((k, v["label"]) for k, v in spec_data.STATUS.items()), + "verdict": dict((k, v["label"]) for k, v in spec_data.VERDICT.items()), + "modes": dict(spec_data.MODES), + "lifecycle": dict((k, v["label"]) for k, v in spec_data.LIFECYCLE.items()), + "adoption_tier": dict((k, v["label"]) for k, v in spec_data.ADOPTION_TIER.items()), + "organization_type": dict(spec_data.ORG_TYPES), + "organization_role": dict(spec_data.ORG_ROLES), + "licence_category": dict(spec_data.LICENCE_CATEGORIES), + "licence_scope": dict(spec_data.LICENCE_SCOPES), + })) + files["docs/index.html"] = DOCS_HTML + return dict((API_DIR + "/" + k, v) for k, v in files.items()) diff --git a/scripts/spec_data.py b/scripts/spec_data.py new file mode 100644 index 0000000..6fc388c --- /dev/null +++ b/scripts/spec_data.py @@ -0,0 +1,727 @@ +"""Load, validate, join and score the MDIP specification registry. + +The registry lives in this repository under data/, and GitHub is where it is +reviewed and changed: + + specifications/.md one file per specification + organizations/.md one file per organization (maintainers, for now) + principles.csv the assessment methodology and the ranking scores + licences.csv the licence ids a specification may use + +Each Markdown file is YAML front matter for the structured fields, followed by +a `# Description` section in Markdown. The file name is the id. Every value +outside the free-text fields comes from a fixed vocabulary, defined below and +documented in data/README.md. Nothing is guessed: an unknown value or key is +reported as an error by `Registry`, which `make specs-check` and the pull +request workflow run, so a mistake is caught in review instead of being +quietly reinterpreted at build time. + +The score computed here only orders the listing. It is not published on the +site or in the API; data/README.md documents how it is calculated. +""" + +from __future__ import annotations + +import csv +import datetime as _dt +import glob +import os +import re +from collections import Counter, OrderedDict + +import yaml + +ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +DATA_DIR = os.path.join(ROOT, "data") + +SPECS_DIR = "specifications" +ORGS_DIR = "organizations" +PRINCIPLES_CSV = "principles.csv" +LICENCES_CSV = "licences.csv" + +GITHUB_REPO = "MobilityData/Mobility-Data-Interoperability-Principles" +GITHUB_BRANCH = "main" + +# Keys a front matter block may contain. Anything else is reported, so a typo +# ("maintainer:" for "maintainers:") fails the check instead of vanishing. +SPEC_KEYS = ("short_name", "full_name", "homepage_url", "status", "modes", "maintainers", + "licences", "licence_evidence_url", "first_release_year", "adoption", "principles") +ADOPTION_KEYS = ("lifecycle", "tier", "adopters_count", "adopters_count_updated", + "adopters_registry_url", "confirmed_by_maintainer") +PRINCIPLE_KEYS = ("verdict", "rationale", "evidence_url") +LICENCE_KEYS = ("id", "scopes") +ORG_KEYS = ("name", "short_name", "type", "roles", "url", "logo_url") + +# -------------------------------------------------------------------------- +# Vocabularies. The keys are what the CSV files contain. +# -------------------------------------------------------------------------- + +STATUS = OrderedDict([ + ("compliant", {"label": "MDIP Compliant", "tone": "pass", "rank": 0}), + ("partially_compliant", {"label": "Partially compliant", "tone": "warn", "rank": 1}), + ("not_compliant", {"label": "Not compliant", "tone": "fail", "rank": 2}), + ("inactive", {"label": "Inactive project", "tone": "muted", "rank": 3}), + ("under_review", {"label": "Under review", "tone": "info", "rank": 4}), +]) + +VERDICT = OrderedDict([ + ("compliant", {"label": "Compliant", "tone": "pass"}), + ("partially_compliant", {"label": "Partially compliant", "tone": "warn"}), + ("not_compliant", {"label": "Not compliant", "tone": "fail"}), + ("unknown", {"label": "Unknown", "tone": "muted"}), + ("not_assessed", {"label": "Not assessed", "tone": "muted"}), +]) + +# Verdicts that carry a score from principles.csv. The other two score 0. +SCORED_VERDICTS = ("compliant", "partially_compliant", "not_compliant") + +LIFECYCLE = OrderedDict([ + ("active", {"label": "Active", "hint": "In active maintenance with a live user base."}), + ("early_adoption", {"label": "Early adoption", "hint": "Published and in use by early adopters."}), + ("pilot_adoption", {"label": "Pilot adoption", "hint": "Being trialled in a small number of deployments."}), + ("pre_adoption", {"label": "Pre-adoption", "hint": "Published but not yet deployed in production."}), + ("inactive", {"label": "Inactive", "hint": "No longer maintained."}), +]) + +# `outcome` ties each tier to a column of the adoption row in principles.csv. +ADOPTION_TIER = OrderedDict([ + ("hundred_plus", {"label": "100+ adopters", "outcome": "compliant"}), + ("early", {"label": "Early adopters", "outcome": "partially_compliant"}), + ("pilot", {"label": "Pilot deployments", "outcome": "partially_compliant"}), + ("none", {"label": "No known adopters", "outcome": "not_compliant"}), +]) + +CONFIRMED = OrderedDict([ + ("yes", {"label": "Confirmed by maintainer", "tone": "pass"}), + ("no", {"label": "Not confirmed", "tone": "fail"}), + ("unknown", {"label": "Unknown", "tone": "muted"}), +]) + +MODES = OrderedDict([ + ("public_transport", "Public Transport"), + ("microtransit_drt", "Microtransit/DRT"), + ("taxi_ridehail", "Taxi/Ridehail"), + ("bicycle", "Bicycle"), + ("pedestrian_accessibility", "Pedestrian/Accessibility"), + ("parking", "Parking"), +]) +UNCLASSIFIED = "Unclassified" + +ORG_TYPES = OrderedDict([ + ("standards-body", "Standards body"), + ("nonprofit", "Non-profit"), + ("government", "Government"), + ("industry-consortium", "Industry consortium"), + ("academic", "Academic"), + ("company", "Company"), + ("community", "Community"), + ("individual", "Individual"), +]) + +ORG_ROLES = OrderedDict([ + ("specification_maintainer", "Specification maintainer"), +]) + +LICENCE_CATEGORIES = OrderedDict([ + ("open", "Open"), + ("restricted", "Open with restrictions"), + ("closed", "Purchase or membership required"), + ("unknown", "Not stated"), +]) + +LICENCE_SCOPES = OrderedDict([ + ("specification", "Specification"), + ("documentation", "Documentation"), + ("code", "Code"), + ("data", "Data"), +]) + +PRINCIPLE_KINDS = ("principle", "bonus", "adoption") +OUTCOMES = ("compliant", "partially_compliant", "not_compliant") + +_URL_RE = re.compile(r"^https?://\S+$") +_ID_RE = re.compile(r"^[a-z0-9]+(?:[-_][a-z0-9]+)*$") +_DATE_RE = re.compile(r"^\d{4}-\d{2}-\d{2}$") +_HEADING_RE = re.compile(r"^#\s+(.+?)\s*$", re.M) + + +def github_url(kind, path=""): + """Link to the repository: kind is blob, edit, new, tree or issues/new.""" + if kind == "issues/new": + return "https://github.com/%s/issues/new" % GITHUB_REPO + return "https://github.com/%s/%s/%s/%s" % (GITHUB_REPO, kind, GITHUB_BRANCH, path) + + +def entry_path(folder, key): + """Repository path of one entry, e.g. data/specifications/gtfs-schedule.md.""" + return "data/%s/%s.md" % (folder, key) + + +def split(value): + return [v.strip() for v in (value or "").split(";") if v.strip()] + + +def as_list(value): + if value is None: + return [] + return value if isinstance(value, list) else [value] + + +def as_text(value): + """YAML turns 2026-09-29 into a date and 2006 into an int; want text.""" + if value is None: + return "" + if isinstance(value, (_dt.date, _dt.datetime)): + return value.isoformat() + return str(value).strip() + + +def read_entry(path): + """Split one Markdown file into (front matter, sections). + + `sections` maps each top-level `# Heading` to the Markdown under it, so + `# Description` and any section added later are read the same way. + """ + with open(path, encoding="utf-8") as fh: + text = fh.read().replace("\r\n", "\n") + if not text.startswith("---\n"): + raise ValueError("does not start with a --- front matter block") + end = text.find("\n---\n", 4) + if end < 0: + raise ValueError("front matter block is not closed with ---") + front = yaml.safe_load(text[4:end]) or {} + if not isinstance(front, dict): + raise ValueError("front matter is not a set of key: value pairs") + body = text[end + 5:] + sections = OrderedDict() + marks = list(_HEADING_RE.finditer(body)) + for n, m in enumerate(marks): + stop = marks[n + 1].start() if n + 1 < len(marks) else len(body) + sections[m.group(1)] = body[m.end():stop].strip() + return front, sections + + +def read_entries(folder): + for path in sorted(glob.glob(os.path.join(DATA_DIR, folder, "*.md"))): + yield os.path.splitext(os.path.basename(path))[0], path + + +def read_csv(name): + path = os.path.join(DATA_DIR, name) + with open(path, newline="", encoding="utf-8") as fh: + reader = csv.DictReader(fh) + rows = [dict((k, (v or "").strip()) for k, v in row.items() if k) for row in reader] + return reader.fieldnames or [], rows + + +class Registry(object): + """The four files, validated and joined.""" + + def __init__(self): + self.issues = [] + self.principles, self.bonus, self.adoption = self._load_principles() + self.licences = self._load_licences() + self.organizations = self._load_organizations() + self.specs = self._load_specs() + + # -- reporting --------------------------------------------------------- + def error(self, where, detail): + self.issues.append({"level": "error", "where": where, "detail": detail}) + + def warning(self, where, detail): + self.issues.append({"level": "warning", "where": where, "detail": detail}) + + def _url(self, where, column, value): + if value and not _URL_RE.match(value): + self.error(where, "%s is not an http(s) URL: %r" % (column, value)) + + def _keys(self, where, block, allowed, label=""): + if not isinstance(block, dict): + self.error(where, "%s must be a set of key: value pairs" % (label or "block")) + return {} + for key in block: + if key not in allowed: + self.error(where, "unknown key %s%r (expected one of %s)" + % (label + "." if label else "", key, ", ".join(allowed))) + return block + + def _read(self, folder, key, path): + where = entry_path(folder, key) + if not _ID_RE.match(key): + self.error(where, "file name must be lower-case letters, digits and - or _") + try: + front, sections = read_entry(path) + except (ValueError, yaml.YAMLError) as exc: + self.error(where, "cannot be read: %s" % str(exc).splitlines()[0]) + return where, None, None + for heading in sections: + if heading != "Description": + self.warning(where, "section %r is not published (only # Description is)" + % heading) + return where, front, sections.get("Description", "") + + # -- principles.csv ---------------------------------------------------- + def _load_principles(self): + _cols, rows = read_csv(PRINCIPLES_CSV) + principles, bonus, adoption = [], None, None + seen = set() + for row in rows: + where = "%s:%s" % (PRINCIPLES_CSV, row.get("id") or "?") + if row["id"] in seen: + self.error(where, "duplicate id") + seen.add(row["id"]) + if row["kind"] not in PRINCIPLE_KINDS: + self.error(where, "kind must be one of %s" % ", ".join(PRINCIPLE_KINDS)) + continue + scores = {} + for outcome in OUTCOMES: + raw = row.get(outcome + "_score", "") + if raw == "": + scores[outcome] = None + continue + try: + scores[outcome] = int(raw) + except ValueError: + self.error(where, "%s_score is not an integer: %r" % (outcome, raw)) + scores[outcome] = 0 + try: + order = int(row.get("order") or 0) + except ValueError: + self.error(where, "order is not an integer") + order = 0 + entry = { + "key": row["id"], + "kind": row["kind"], + "order": order, + "label": row["name"], + "blurb": row["definition"], + "when": dict((o, row.get(o + "_when", "")) for o in OUTCOMES), + "scores": scores, + } + if row["kind"] == "principle": + principles.append(entry) + elif row["kind"] == "bonus": + bonus = entry + else: + adoption = entry + principles.sort(key=lambda p: p["order"]) + if not principles: + self.error(PRINCIPLES_CSV, "no principle rows") + return principles, bonus, adoption + + # -- licences.csv ------------------------------------------------------ + def _load_licences(self): + _cols, rows = read_csv(LICENCES_CSV) + out = OrderedDict() + for row in rows: + where = "%s:%s" % (LICENCES_CSV, row.get("id") or "?") + if row["id"] in out: + self.error(where, "duplicate id") + if row["category"] not in LICENCE_CATEGORIES: + self.error(where, "category must be one of %s" % ", ".join(LICENCE_CATEGORIES)) + self._url(where, "url", row.get("url")) + out[row["id"]] = { + "id": row["id"], + "name": row["name"], + "category": row["category"], + "url": row.get("url") or None, + "note": row.get("note") or None, + } + return out + + # -- organizations/*.md ----------------------------------------------- + def _load_organizations(self): + out = OrderedDict() + for key, path in read_entries(ORGS_DIR): + where, front, description = self._read(ORGS_DIR, key, path) + if front is None: + continue + self._keys(where, front, ORG_KEYS) + name = as_text(front.get("name")) + if not name: + self.error(where, "name is empty") + org_type = as_text(front.get("type")) + if org_type not in ORG_TYPES: + self.error(where, "type must be one of %s" % ", ".join(ORG_TYPES)) + roles = [as_text(r) for r in as_list(front.get("roles"))] + for role in roles: + if role not in ORG_ROLES: + self.error(where, "roles must be among %s" % ", ".join(ORG_ROLES)) + url, logo = as_text(front.get("url")), as_text(front.get("logo_url")) + self._url(where, "url", url) + self._url(where, "logo_url", logo) + out[key] = { + "id": key, + "name": name or key, + "short": as_text(front.get("short_name")) or name or key, + "url": url or None, + "logo_url": logo or None, + "type": org_type, + "type_label": ORG_TYPES.get(org_type, "Other"), + "roles": roles, + "description": description, + "source_path": entry_path(ORGS_DIR, key), + } + return out + + # -- specifications/*.md ---------------------------------------------- + def _parse_licences(self, where, entries): + out = [] + for entry in as_list(entries): + if isinstance(entry, str): + entry = {"id": entry} + entry = self._keys(where, entry, LICENCE_KEYS, "licences") + lid = as_text(entry.get("id")) + scopes = [as_text(x) for x in as_list(entry.get("scopes"))] + for sc in scopes: + if sc not in LICENCE_SCOPES: + self.error(where, "licence scope must be one of %s: %r" + % (", ".join(LICENCE_SCOPES), sc)) + meta = self.licences.get(lid) + if meta is None: + self.error(where, "licence %r is not in %s" % (lid, LICENCES_CSV)) + meta = {"id": lid, "name": lid, "category": "unknown", "url": None, "note": None} + out.append(dict(meta, scopes=[x for x in scopes if x in LICENCE_SCOPES])) + if not out: + out.append({"id": "Unspecified", "name": "No licence stated", "category": "unknown", + "url": None, "note": None, "scopes": []}) + return out + + def _vocab(self, where, column, value, vocab, allow_empty=False): + if value == "" and allow_empty: + return None + if value not in vocab: + self.error(where, "%s must be one of %s, got %r" + % (column, ", ".join(vocab), value)) + return None + return value + + def _load_specs(self): + specs = [] + for key, path in read_entries(SPECS_DIR): + where, front, description = self._read(SPECS_DIR, key, path) + if front is not None: + specs.append(self._clean_spec(key, where, front, description)) + specs.sort(key=default_order) + return specs + + def _clean_spec(self, sid, where, front, description): + self._keys(where, front, SPEC_KEYS) + short_name = as_text(front.get("short_name")) + if not short_name: + self.error(where, "short_name is empty") + + status = self._vocab(where, "status", as_text(front.get("status")), STATUS) \ + or "under_review" + status_meta = STATUS[status] + + mode_ids = [] + for m in as_list(front.get("modes")): + if self._vocab(where, "modes", as_text(m), MODES): + mode_ids.append(as_text(m)) + modes = [MODES[m] for m in mode_ids] or [UNCLASSIFIED] + + maintainers = [] + for key in as_list(front.get("maintainers")): + key = as_text(key) + org = self.organizations.get(key) + if org is None: + self.error(where, "maintainer %r has no file %s" % (key, entry_path(ORGS_DIR, key))) + continue + if "specification_maintainer" not in org["roles"]: + self.warning(where, "maintainer %r does not have the specification_maintainer " + "role" % key) + maintainers.append(org) + + given = self._keys(where, front.get("principles") or {}, [p["key"] for p in self.principles], + "principles") + criteria = [] + for p in self.principles: + block = self._keys(where, given.get(p["key"]) or {}, PRINCIPLE_KEYS, + "principles." + p["key"]) + verdict = self._vocab(where, "principles.%s.verdict" % p["key"], + as_text(block.get("verdict")) or "not_assessed", VERDICT) \ + or "not_assessed" + evidence = as_text(block.get("evidence_url")) + self._url(where, "principles.%s.evidence_url" % p["key"], evidence) + points = p["scores"].get(verdict) if verdict in SCORED_VERDICTS else 0 + criteria.append({ + "key": p["key"], + "label": p["label"], + "blurb": p["blurb"], + "verdict": verdict, + "verdict_label": VERDICT[verdict]["label"], + "tone": VERDICT[verdict]["tone"], + "rationale": as_text(block.get("rationale")), + "evidence_url": evidence or None, + "points": points or 0, + }) + + adoption = self._keys(where, front.get("adoption") or {}, ADOPTION_KEYS, "adoption") + lifecycle = self._vocab(where, "adoption.lifecycle", as_text(adoption.get("lifecycle")), + LIFECYCLE, True) + tier = self._vocab(where, "adoption.tier", as_text(adoption.get("tier")), + ADOPTION_TIER, True) + raw_confirmed = adoption.get("confirmed_by_maintainer") + if raw_confirmed not in (None, True, False): + self.error(where, "adoption.confirmed_by_maintainer must be true or false") + confirmed = {True: "yes", False: "no"}.get(raw_confirmed, "unknown") + + adopters_count = adoption.get("adopters_count") + if adopters_count is not None and (not isinstance(adopters_count, int) + or isinstance(adopters_count, bool) + or adopters_count < 0): + self.error(where, "adoption.adopters_count must be a whole number") + adopters_count = None + updated = as_text(adoption.get("adopters_count_updated")) + if updated and not _DATE_RE.match(updated): + self.error(where, "adoption.adopters_count_updated must be a YYYY-MM-DD date") + registry = as_text(adoption.get("adopters_registry_url")) + self._url(where, "adoption.adopters_registry_url", registry) + + year = front.get("first_release_year") + if year is not None and not (isinstance(year, int) and 1900 <= year <= 2100): + self.error(where, "first_release_year must be a four-digit year") + year = None + + homepage = as_text(front.get("homepage_url")) + self._url(where, "homepage_url", homepage) + evidence = [as_text(u) for u in as_list(front.get("licence_evidence_url"))] + for url in evidence: + self._url(where, "licence_evidence_url", url) + + licences = self._parse_licences(where, front.get("licences")) + score = self.score(criteria, tier) + met = sum(1 for c in criteria if c["verdict"] == "compliant") + self._check_status(where, status, criteria) + + return { + "id": sid, + "slug": sid, + "short_name": short_name or sid, + "full_name": as_text(front.get("full_name")), + "description": description, + "function": plain_summary(description), + "homepage_url": homepage or None, + "modes": modes, + "mode_ids": mode_ids, + "status": status, + "status_label": status_meta["label"], + "status_tone": status_meta["tone"], + "status_rank": status_meta["rank"], + "criteria": criteria, + "criteria_met": met, + "criteria_total": len(criteria), + "score": score["total"], + "confirmed": confirmed, + "confirmed_label": CONFIRMED[confirmed]["label"], + "confirmed_tone": CONFIRMED[confirmed]["tone"], + "lifecycle": lifecycle, + "lifecycle_label": LIFECYCLE[lifecycle]["label"] if lifecycle else None, + "lifecycle_hint": LIFECYCLE[lifecycle]["hint"] if lifecycle else None, + "adoption_tier": tier, + "adoption_tier_label": ADOPTION_TIER[tier]["label"] if tier else None, + "adopters_registry": registry or None, + "adopters_count": adopters_count, + "adopters_count_updated": updated or None, + "first_release_year": year, + "licences": licences, + "licence_ids": [l["id"] for l in licences], + "licence_evidence": evidence, + "maintainers": maintainers, + "maintainer_ids": [m["id"] for m in maintainers], + "maintainer_types": sorted(set(m["type"] for m in maintainers)), + "source_path": entry_path(SPECS_DIR, sid), + } + + def _check_status(self, where, status, criteria): + """Flag a recorded status that the verdicts do not support. + + `status` is the coalition's decision and is published as recorded -- + "inactive" and "under_review" cannot be derived from verdicts at all. + For the three assessed outcomes, though, the verdicts imply one answer, + and a mismatch is worth a second look in review. + """ + if status not in ("compliant", "partially_compliant", "not_compliant"): + return + verdicts = [c["verdict"] for c in criteria] + if all(v == "compliant" for v in verdicts): + expected = "compliant" + elif "not_compliant" in verdicts: + expected = "not_compliant" + elif all(v in ("compliant", "partially_compliant") for v in verdicts): + expected = "partially_compliant" + else: + return # unknown or not assessed verdicts: no single answer + if expected != status: + self.warning(where, "status is %s but the principle verdicts imply %s" + % (status, expected)) + + # -- scoring ----------------------------------------------------------- + def score(self, criteria, tier): + """Ranking score, per principles.csv. Used for ordering only. + + Returns None when no principle has been assessed, so an unreviewed + specification sorts after every assessed one. + """ + parts = [{"key": c["key"], "label": c["label"], "outcome": c["verdict_label"], + "points": c["points"]} for c in criteria] + assessed = any(c["verdict"] in SCORED_VERDICTS for c in criteria) + maximum = sum(max(v for v in p["scores"].values() if v is not None) + for p in self.principles) + + if self.bonus: + full = all(c["verdict"] == "compliant" for c in criteria) + outcome = "compliant" if full else "not_compliant" + parts.append({"key": self.bonus["key"], "label": self.bonus["label"], + "outcome": "Yes" if full else "No", + "points": self.bonus["scores"].get(outcome) or 0}) + maximum += max(v for v in self.bonus["scores"].values() if v is not None) + + if self.adoption: + if tier: + outcome = ADOPTION_TIER[tier]["outcome"] + points = self.adoption["scores"].get(outcome) or 0 + label = ADOPTION_TIER[tier]["label"] + else: + points, label = 0, "Not recorded" + parts.append({"key": self.adoption["key"], "label": self.adoption["label"], + "outcome": label, "points": points}) + maximum += max(v for v in self.adoption["scores"].values() if v is not None) + + total = sum(p["points"] for p in parts) if assessed else None + return {"total": total, "max": maximum, "parts": parts} + + +def plain_summary(markdown_text): + """First paragraph of a Markdown description, as plain text for listings.""" + first = (markdown_text or "").strip().split("\n\n", 1)[0] + first = re.sub(r"!?\[([^\]]*)\]\([^)]*\)", r"\1", first) # links, images + first = re.sub(r"[*_`]+", "", first) + return " ".join(first.split()) + + +def default_order(spec): + """The registry's reading order. + + Specifications with the most known adopters come first; those without a + recorded count follow, highest score first. Status, then name, break the + remaining ties so the order is stable between builds. + """ + return ( + spec["adopters_count"] is None, + -(spec["adopters_count"] or 0), + -(spec["score"] if spec["score"] is not None else -999), + spec["status_rank"], + spec["short_name"].lower(), + ) + + +def _facet(counter, order=None, labels=None): + items = [] + for value, count in counter.items(): + items.append({"value": value, "label": (labels or {}).get(value, value), "count": count}) + if order: + pos = dict((v, i) for i, v in enumerate(order)) + items.sort(key=lambda i: (pos.get(i["value"], len(order)), -i["count"], i["label"].lower())) + else: + items.sort(key=lambda i: (-i["count"], i["label"].lower())) + return items + + +def build_dataset(): + reg = Registry() + specs = reg.specs + + counters = dict((k, Counter()) for k in ( + "mode", "status", "confirmed", "lifecycle", "adoption_tier", "licence", + "licence_category", "maintainer", "maintainer_type", "decade")) + for p in reg.principles: + counters["criterion_" + p["key"]] = Counter() + + for s in specs: + for m in s["modes"]: + counters["mode"][m] += 1 + counters["status"][s["status"]] += 1 + counters["confirmed"][s["confirmed"]] += 1 + if s["lifecycle"]: + counters["lifecycle"][s["lifecycle"]] += 1 + if s["adoption_tier"]: + counters["adoption_tier"][s["adoption_tier"]] += 1 + for lid in set(s["licence_ids"]): + counters["licence"][lid] += 1 + for cat in set(l["category"] for l in s["licences"]): + counters["licence_category"][cat] += 1 + for mid in s["maintainer_ids"]: + counters["maintainer"][mid] += 1 + for t in s["maintainer_types"]: + counters["maintainer_type"][t] += 1 + if s["first_release_year"]: + counters["decade"][str(s["first_release_year"] // 10 * 10) + "s"] += 1 + for c in s["criteria"]: + counters["criterion_" + c["key"]][c["verdict"]] += 1 + + licence_labels = dict((lid, m["name"]) for lid, m in reg.licences.items()) + licence_labels["Unspecified"] = "No licence stated" + org_labels = dict((k, o["short"]) for k, o in reg.organizations.items()) + verdict_labels = dict((k, v["label"]) for k, v in VERDICT.items()) + + facets = OrderedDict() + facets["mode"] = {"label": "Mode", "hint": "Transport modes the specification covers.", + "options": _facet(counters["mode"], list(MODES.values()) + [UNCLASSIFIED])} + facets["status"] = {"label": "MDIP status", "hint": "Overall outcome of the MDIP assessment.", + "options": _facet(counters["status"], list(STATUS), + dict((k, v["label"]) for k, v in STATUS.items()))} + facets["confirmed"] = {"label": "Adoption confirmed by maintainer", + "hint": "Whether the maintainer confirmed real-world adoption.", + "options": _facet(counters["confirmed"], list(CONFIRMED), + dict((k, v["label"]) for k, v in CONFIRMED.items()))} + facets["lifecycle"] = {"label": "Lifecycle", "hint": "How far along the specification is.", + "options": _facet(counters["lifecycle"], list(LIFECYCLE), + dict((k, v["label"]) for k, v in LIFECYCLE.items()))} + facets["adoption_tier"] = {"label": "Adoption", "hint": "Scale of known deployments.", + "options": _facet(counters["adoption_tier"], list(ADOPTION_TIER), + dict((k, v["label"]) + for k, v in ADOPTION_TIER.items()))} + facets["licence"] = {"label": "Licence", + "hint": "Licences the specification, its documentation, code or data " + "are released under.", + "searchable": True, + "options": _facet(counters["licence"], None, licence_labels)} + facets["licence_category"] = {"label": "Licence type", + "hint": "Whether the licence puts any gate in front of use.", + "options": _facet(counters["licence_category"], + list(LICENCE_CATEGORIES), + dict(LICENCE_CATEGORIES))} + facets["maintainer"] = {"label": "Maintainer organization", + "hint": "Organization responsible for the specification.", + "searchable": True, + "options": _facet(counters["maintainer"], None, org_labels)} + facets["maintainer_type"] = {"label": "Maintainer type", + "hint": "What kind of body maintains the specification.", + "options": _facet(counters["maintainer_type"], list(ORG_TYPES), + dict(ORG_TYPES))} + for p in reg.principles: + facets["criterion_" + p["key"]] = { + "label": p["label"], "hint": p["blurb"], "group": "criteria", + "options": _facet(counters["criterion_" + p["key"]], list(VERDICT), verdict_labels)} + facets["decade"] = {"label": "First released", "hint": "Decade of the first public release.", + "options": _facet(counters["decade"], + sorted(counters["decade"], reverse=True))} + + return { + "meta": { + "count": len(specs), + "organization_count": len([k for k, v in counters["maintainer"].items() if v]), + "compliant_count": counters["status"].get("compliant", 0), + "confirmed_count": counters["confirmed"].get("yes", 0), + }, + "facets": facets, + "organizations": reg.organizations, + "criteria": reg.principles, + "licences": reg.licences, + "bonus": reg.bonus, + "adoption": reg.adoption, + "specs": specs, + "issues": reg.issues, + } From a8a808f167cdfdc605919bdbf82efbcb594029dd Mon Sep 17 00:00:00 2001 From: Etienne Pichot Damon <79003454+etienne0101@users.noreply.github.com> Date: Wed, 30 Sep 2026 16:57:40 -0400 Subject: [PATCH 2/4] Update README --- README.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/README.md b/README.md index c03278b..34743d4 100644 --- a/README.md +++ b/README.md @@ -38,9 +38,6 @@ source of truth and the place to contribute: each specification and each organization is one Markdown file with YAML front matter, and "Suggest a change" on a specification's page opens that file in GitHub's editor. -The section is unlisted for now: it is not in the nav bar, the site search or -the sitemap, and its pages carry ``. - | Path | What it is | | --- | --- | | `data/specifications/.md` | One specification: fields in front matter, then `# Description`. | From 467011fa590429b39c31be870cd66f15788108ef Mon Sep 17 00:00:00 2001 From: Etienne Pichot Damon Date: Thu, 1 Oct 2026 11:29:27 -0400 Subject: [PATCH 3/4] add info on specs review --- .claude/launch.json | 6 ++++ hooks/specifications.py | 4 +++ .../assets/stylesheets/specifications.css | 31 ++++++++++++++++--- 3 files changed, 37 insertions(+), 4 deletions(-) diff --git a/.claude/launch.json b/.claude/launch.json index 499000f..530f8ca 100644 --- a/.claude/launch.json +++ b/.claude/launch.json @@ -6,6 +6,12 @@ "runtimeExecutable": "mkdocs", "runtimeArgs": ["serve", "-f", "config/en/mkdocs.yml", "--dev-addr", "127.0.0.1:8000", "--watch", "overrides", "--watch", "hooks", "--watch", "data", "--watch", "scripts"], "port": 8000 + }, + { + "name": "mkdocs-en-alt", + "runtimeExecutable": "mkdocs", + "runtimeArgs": ["serve", "-f", "config/en/mkdocs.yml", "--dev-addr", "127.0.0.1:8001"], + "port": 8001 } ] } diff --git a/hooks/specifications.py b/hooks/specifications.py index 8cedd7b..b0d5794 100644 --- a/hooks/specifications.py +++ b/hooks/specifications.py @@ -358,6 +358,10 @@ def render_index(data, base): o.append('
    ') + o.append('

    %sThe evaluation of the specifications ' + 'listed here is a work in progress. Findings may change as assessments are ' + 'reviewed.

    ' % icon("progress-wrench")) + # ---- hero o.append('
    ') o.append('') diff --git a/overrides/assets/stylesheets/specifications.css b/overrides/assets/stylesheets/specifications.css index e6bd190..2994f0c 100644 --- a/overrides/assets/stylesheets/specifications.css +++ b/overrides/assets/stylesheets/specifications.css @@ -150,19 +150,42 @@ body.mdip-fullbleed .md-main__inner { margin-top: 0; } background: radial-gradient(42% 46% at 18% 32%, rgba(27, 88, 164, .22), transparent 70%), radial-gradient(38% 42% at 78% 18%, rgba(72, 146, 236, .20), transparent 72%), - radial-gradient(30% 38% at 52% 78%, rgba(15, 123, 79, .12), transparent 70%); + radial-gradient(34% 40% at 52% 78%, rgba(250, 240, 214, .85), transparent 70%); filter: blur(12px); animation: mdip-drift 26s var(--ease) infinite alternate; } -:is(.mdip-specs, .mdip-spec) [data-md-color-scheme="slate"] .specs-hero__glow, -:is(.mdip-specs, .mdip-spec) [data-md-color-scheme="slate"] .spec-hero__glow { +[data-md-color-scheme="slate"] :is(.mdip-specs, .mdip-spec) .specs-hero__glow, +[data-md-color-scheme="slate"] :is(.mdip-specs, .mdip-spec) .spec-hero__glow { background: radial-gradient(42% 46% at 18% 32%, rgba(72, 146, 236, .26), transparent 70%), radial-gradient(38% 42% at 78% 18%, rgba(27, 88, 164, .30), transparent 72%), - radial-gradient(30% 38% at 52% 78%, rgba(95, 214, 160, .12), transparent 70%); + radial-gradient(34% 40% at 52% 78%, rgba(245, 232, 200, .07), transparent 70%); } +/* -------------------------------------------------------------------------- + Work-in-progress notice above the listing hero + -------------------------------------------------------------------------- */ + +.mdip-specs .specs-notice { + display: flex; + align-items: center; + justify-content: center; + gap: .45em; + margin: 0; + padding: .45rem 1.1rem; + background: var(--surface-2); + border-bottom: 1px solid var(--line); + color: var(--ink-3); + font-size: .64rem; + line-height: 1.4; + text-align: center; +} + +.mdip-specs .specs-notice .mdip-i { color: var(--brand); } + +[data-md-color-scheme="slate"] .mdip-specs .specs-notice .mdip-i { color: var(--info); } + @keyframes mdip-drift { from { transform: translate3d(-3%, -2%, 0) scale(1); } to { transform: translate3d(4%, 3%, 0) scale(1.12); } From 2c908613e355f566a86e16a208bb1133e6d6a848 Mon Sep 17 00:00:00 2001 From: Etienne Pichot Damon Date: Thu, 1 Oct 2026 11:41:17 -0400 Subject: [PATCH 4/4] Update specifications.css --- overrides/assets/stylesheets/specifications.css | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/overrides/assets/stylesheets/specifications.css b/overrides/assets/stylesheets/specifications.css index 2994f0c..54aa65a 100644 --- a/overrides/assets/stylesheets/specifications.css +++ b/overrides/assets/stylesheets/specifications.css @@ -150,7 +150,7 @@ body.mdip-fullbleed .md-main__inner { margin-top: 0; } background: radial-gradient(42% 46% at 18% 32%, rgba(27, 88, 164, .22), transparent 70%), radial-gradient(38% 42% at 78% 18%, rgba(72, 146, 236, .20), transparent 72%), - radial-gradient(34% 40% at 52% 78%, rgba(250, 240, 214, .85), transparent 70%); + radial-gradient(34% 40% at 52% 78%, rgba(255, 255, 255, .9), transparent 70%); filter: blur(12px); animation: mdip-drift 26s var(--ease) infinite alternate; } @@ -160,7 +160,7 @@ body.mdip-fullbleed .md-main__inner { margin-top: 0; } background: radial-gradient(42% 46% at 18% 32%, rgba(72, 146, 236, .26), transparent 70%), radial-gradient(38% 42% at 78% 18%, rgba(27, 88, 164, .30), transparent 72%), - radial-gradient(34% 40% at 52% 78%, rgba(245, 232, 200, .07), transparent 70%); + radial-gradient(34% 40% at 52% 78%, rgba(255, 255, 255, .05), transparent 70%); } /* --------------------------------------------------------------------------