From a66585b8b31f094bef3082abc4f3ba0c0cc47e85 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 7 Jul 2026 12:27:44 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20MQA=20quality=20scoring=20=E2=80=94=20P?= =?UTF-8?q?hase=202=20vocabulary=20checks=20(v2.14.0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Score the MQA 'vocab'-type metrics offline (no new form fields needed since all inputs exist as of v2.12.0): - Format/media-type from vocabulary (10): format is part of the controlled EU file-type set - Non-proprietary format (20) + machine-readable format (20): from new flags in config/dct-format-list.php (aligned with the EU vocabularies) - License from vocabulary (10): license URI is in the known license set (config/licenses.txt / standard licenses) - Access rights from vocabulary (5): value comes from the EU access-right vocab The assessable maximum rises from ~230 to ~295 of 405 (all offline). Only URL reachability (80) and DCAT-AP SHACL (30) remain "not assessed" (Phase 3, opt-in). - config/dct-format-list.php: add machine_readable + non_proprietary per format - ODW_Fields::get_format_meta(): public accessor for a format's config entry - ODW_Quality::check_vocab_metric() + license/access-right vocab helpers - Meta-box "not assessed" hint updated (vocabulary now assessed) - i18n: updated string added to en_US Version 2.14.0. Tests: 149 pass, PHPStan clean, PHPCS exit 0. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01JB1xUQM892bVZ4Yv3MZjvq --- CHANGELOG.md | 25 ++++++ README.md | 2 +- config/dct-format-list.php | 118 +++++++++++++++++++--------- includes/class-fields.php | 11 +++ includes/class-quality.php | 99 +++++++++++++++++++++-- languages/open-data-wizard-en_US.mo | Bin 48517 -> 48722 bytes languages/open-data-wizard-en_US.po | 3 + open-data-wizard.php | 4 +- tests/test-fields.php | 26 ++++++ 9 files changed, 242 insertions(+), 46 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ca78d3..31d1d2c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,31 @@ und dieses Projekt folgt [Semantic Versioning](https://semver.org/spec/v2.0.0.ht --- +## [2.14.0] — 2026-07-07 + +MQA-Qualitäts-Scoring **Phase 2**: Vokabular-Prüfungen (offline). Siehe `docs/MQA-KONZEPT.md`. + +### ✨ Added — Vokabular-Metriken bewertet +- Die MQA-Metriken vom Typ `vocab` werden jetzt bewertet (offline, ohne neue Felder): + - **Format/Media-Type aus Vokabular** (10) — Format ist Teil des kontrollierten EU-Vokabulars. + - **Nicht-proprietäres Format** (20) und **Maschinenlesbares Format** (20) — aus den neuen Flags + in `config/dct-format-list.php`. + - **Lizenz aus Vokabular** (10) — Lizenz-URI ist im bekannten Lizenz-Set (`config/licenses.txt` + bzw. Standardlizenzen). + - **Zugriffsrechte aus Vokabular** (5) — Wert stammt aus dem EU-Access-Right-Vokabular. +- Das bewertbare Maximum steigt damit von ~230 auf **~295 von 405 Punkten** (rein offline). +- `config/dct-format-list.php` um `machine_readable` und `non_proprietary` je Format erweitert + (orientiert an den EU-Vokabularen). + +### 🔧 Verbleibend (Phase 3, opt-in) +- Nur noch **URL-Erreichbarkeit** (80) und **DCAT-AP-SHACL-Konformität** (30) sind „nicht bewertet". + +### ✅ Tests +- Neuer Test für die Format-Flags (`get_format_meta`: CSV offen/maschinenlesbar, XLSX proprietär, + „Sonstiges" ohne Vokabular). + +--- + ## [2.13.0] — 2026-07-07 Neues Qualitäts-Scoring nach der EU-MQA-Methodik (Phase 1). Siehe `docs/MQA-KONZEPT.md`. diff --git a/README.md b/README.md index b160337..18ca9bb 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ ![PHP Version](https://img.shields.io/badge/PHP-%3E%3D%208.1-8892BF?style=flat-square&logo=php&logoColor=white) ![WordPress](https://img.shields.io/badge/WordPress-compatible-21759B?style=flat-square&logo=wordpress&logoColor=white) ![DCAT-AP](https://img.shields.io/badge/DCAT--AP-3.0-brightgreen?style=flat-square) -![Version](https://img.shields.io/badge/Version-2.13.0-brightgreen?style=flat-square) +![Version](https://img.shields.io/badge/Version-2.14.0-brightgreen?style=flat-square) ![PRs Welcome](https://img.shields.io/badge/PRs-willkommen-brightgreen?style=flat-square) 📖 [Dokumentation](DOCUMENTATION.md) · 📐 [Technische Spezifikation](TECHNICAL-SPEC.md) · 📝 [Changelog](CHANGELOG.md) · 🛡️ [Security](SECURITY.md) · ⚖️ [Lizenz](LICENSE) diff --git a/config/dct-format-list.php b/config/dct-format-list.php index 4907c6a..4358a9d 100644 --- a/config/dct-format-list.php +++ b/config/dct-format-list.php @@ -2,9 +2,15 @@ /** * Dateiformatliste für DCAT-AP Distributionen * - * Jeder Eintrag: Kurzbezeichnung => ['mime' => MIME-Typ, 'eu_uri' => EU-Publications-Office-Kürzel] + * Jeder Eintrag: Kurzbezeichnung => [ + * 'mime' => MIME-Typ, + * 'eu_uri' => EU-Publications-Office-Kürzel (leer = kein EU-Standard-URI), + * 'machine_readable' => bool (MQA: maschinenlesbares Format), + * 'non_proprietary' => bool (MQA: nicht-proprietäres Format), + * ] * eu_uri wird zu http://publications.europa.eu/resource/authority/file-type/{eu_uri} aufgelöst. - * Leere eu_uri bedeutet: kein EU-Standard-URI vorhanden → Kurzbezeichnung als Fallback. + * Die MQA-Flags orientieren sich an den EU-Vokabularen für maschinenlesbare bzw. + * nicht-proprietäre Formate (data.europa.eu) und werden für das Qualitäts-Scoring genutzt. * * @package OpenDataWizard */ @@ -15,75 +21,111 @@ return array( 'CSV' => array( - 'mime' => 'text/csv', - 'eu_uri' => 'CSV', + 'mime' => 'text/csv', + 'eu_uri' => 'CSV', + 'machine_readable' => true, + 'non_proprietary' => true, ), 'JSON' => array( - 'mime' => 'application/json', - 'eu_uri' => 'JSON', + 'mime' => 'application/json', + 'eu_uri' => 'JSON', + 'machine_readable' => true, + 'non_proprietary' => true, ), 'XLSX' => array( - 'mime' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', - 'eu_uri' => 'XLSX', + 'mime' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + 'eu_uri' => 'XLSX', + 'machine_readable' => true, + 'non_proprietary' => false, ), 'ODS' => array( - 'mime' => 'application/vnd.oasis.opendocument.spreadsheet', - 'eu_uri' => 'ODS', + 'mime' => 'application/vnd.oasis.opendocument.spreadsheet', + 'eu_uri' => 'ODS', + 'machine_readable' => true, + 'non_proprietary' => true, ), 'PDF' => array( - 'mime' => 'application/pdf', - 'eu_uri' => 'PDF', + 'mime' => 'application/pdf', + 'eu_uri' => 'PDF', + 'machine_readable' => false, + 'non_proprietary' => true, ), 'XML' => array( - 'mime' => 'application/xml', - 'eu_uri' => 'XML', + 'mime' => 'application/xml', + 'eu_uri' => 'XML', + 'machine_readable' => true, + 'non_proprietary' => true, ), 'GeoJSON' => array( - 'mime' => 'application/geo+json', - 'eu_uri' => 'GEOJSON', + 'mime' => 'application/geo+json', + 'eu_uri' => 'GEOJSON', + 'machine_readable' => true, + 'non_proprietary' => true, ), 'GML' => array( - 'mime' => 'application/gml+xml', - 'eu_uri' => 'GML', + 'mime' => 'application/gml+xml', + 'eu_uri' => 'GML', + 'machine_readable' => true, + 'non_proprietary' => true, ), 'SHP' => array( - 'mime' => 'application/x-esri-shape', - 'eu_uri' => 'SHP', + 'mime' => 'application/x-esri-shape', + 'eu_uri' => 'SHP', + 'machine_readable' => true, + 'non_proprietary' => false, ), 'GPKG' => array( - 'mime' => 'application/geopackage+sqlite3', - 'eu_uri' => 'GPKG', + 'mime' => 'application/geopackage+sqlite3', + 'eu_uri' => 'GPKG', + 'machine_readable' => true, + 'non_proprietary' => true, ), 'KML' => array( - 'mime' => 'application/vnd.google-earth.kml+xml', - 'eu_uri' => 'KML', + 'mime' => 'application/vnd.google-earth.kml+xml', + 'eu_uri' => 'KML', + 'machine_readable' => true, + 'non_proprietary' => true, ), 'ZIP' => array( - 'mime' => 'application/zip', - 'eu_uri' => 'ZIP', + 'mime' => 'application/zip', + 'eu_uri' => 'ZIP', + 'machine_readable' => false, + 'non_proprietary' => true, ), 'TIFF' => array( - 'mime' => 'image/tiff', - 'eu_uri' => 'TIFF', + 'mime' => 'image/tiff', + 'eu_uri' => 'TIFF', + 'machine_readable' => true, + 'non_proprietary' => true, ), 'RDF' => array( - 'mime' => 'application/rdf+xml', - 'eu_uri' => 'RDF_XML', + 'mime' => 'application/rdf+xml', + 'eu_uri' => 'RDF_XML', + 'machine_readable' => true, + 'non_proprietary' => true, ), 'TURTLE' => array( - 'mime' => 'text/turtle', - 'eu_uri' => 'TURTLE', + 'mime' => 'text/turtle', + 'eu_uri' => 'TURTLE', + 'machine_readable' => true, + 'non_proprietary' => true, ), 'N-TRIPLES' => array( - 'mime' => 'application/n-triples', - 'eu_uri' => 'N_TRIPLES', + 'mime' => 'application/n-triples', + 'eu_uri' => 'N_TRIPLES', + 'machine_readable' => true, + 'non_proprietary' => true, ), 'JSON-LD' => array( - 'mime' => 'application/ld+json', - 'eu_uri' => 'JSON_LD', + 'mime' => 'application/ld+json', + 'eu_uri' => 'JSON_LD', + 'machine_readable' => true, + 'non_proprietary' => true, ), 'Sonstiges' => array( - 'mime' => '', - 'eu_uri' => '', + 'mime' => '', + 'eu_uri' => '', + 'machine_readable' => false, + 'non_proprietary' => false, ), ); diff --git a/includes/class-fields.php b/includes/class-fields.php index 7af53e6..b594d72 100644 --- a/includes/class-fields.php +++ b/includes/class-fields.php @@ -800,6 +800,17 @@ public static function get_format_mime( string $format ): string { return $list[ $format ]['mime'] ?? $format; } + /** + * Returns the config entry for a format label (mime, eu_uri and MQA flags). + * + * @param string $format Short format label (e.g. "CSV"). + * @return array{mime?: string, eu_uri?: string, machine_readable?: bool, non_proprietary?: bool} + */ + public static function get_format_meta( string $format ): array { + $list = self::load_format_list(); + return $list[ $format ] ?? array(); + } + /** * Maps a short format label to its EU Publications Office file-type URI. * diff --git a/includes/class-quality.php b/includes/class-quality.php index a6aafdc..84dc18a 100644 --- a/includes/class-quality.php +++ b/includes/class-quality.php @@ -190,12 +190,18 @@ public static function calculate( int $post_id ): array { * @return bool|null true = erfüllt, false = nicht erfüllt, null = nicht bewertet (Vokabular/Netzwerk/SHACL). */ private static function evaluate_metric( array $metric, \WP_Post $post ): ?bool { - if ( 'present' !== ( $metric['type'] ?? '' ) ) { - // Vokabular-, Erreichbarkeits- und SHACL-Prüfungen folgen in späteren Phasen. - return null; + $type = (string) ( $metric['type'] ?? '' ); + + if ( 'present' === $type ) { + return self::check_metric( (string) $metric['check'], $post ); + } + + if ( 'vocab' === $type ) { + return self::check_vocab_metric( (string) $metric['check'], $post ); } - return self::check_metric( (string) $metric['check'], $post ); + // Erreichbarkeits- und SHACL-Prüfungen folgen in Phase 3. + return null; } /** @@ -272,6 +278,89 @@ private static function check_metric( string $check, \WP_Post $post ): bool { return false; } + /** + * „Stammt der Wert aus einem kontrollierten Vokabular?"-Prüfung (MQA Phase 2). + * + * @param string $check Check-Schlüssel aus der Metrik-Definition. + * @param \WP_Post $post Dataset post object. + * @return bool True wenn der Wert einem kontrollierten Vokabular entspricht. + */ + private static function check_vocab_metric( string $check, \WP_Post $post ): bool { + if ( ! class_exists( 'ODW_Fields' ) ) { + return false; + } + + $id = $post->ID; + + switch ( $check ) { + case 'format_vocab': + $format = (string) carbon_get_post_meta( $id, 'odw_format' ); + if ( '' === $format ) { + return false; + } + return '' !== (string) ( ODW_Fields::get_format_meta( $format )['eu_uri'] ?? '' ); + + case 'format_nonproprietary': + $format = (string) carbon_get_post_meta( $id, 'odw_format' ); + return true === ( ODW_Fields::get_format_meta( $format )['non_proprietary'] ?? false ); + + case 'format_machine_readable': + $format = (string) carbon_get_post_meta( $id, 'odw_format' ); + return true === ( ODW_Fields::get_format_meta( $format )['machine_readable'] ?? false ); + + case 'license_vocab': + return self::license_in_vocab( self::effective_license( $id ) ); + + case 'access_rights_vocab': + $value = (string) carbon_get_post_meta( $id, 'odw_access_rights' ); + if ( '' === $value ) { + return false; + } + foreach ( ODW_Fields::load_vocabulary( 'access-right' ) as $entry ) { + if ( $entry['value'] === $value ) { + return true; + } + } + return false; + } + + return false; + } + + /** + * Effektive Lizenz-URI (bei „sonstige" die eigene URI). + * + * @param int $id Post ID. + * @return string + */ + private static function effective_license( int $id ): string { + $lic = (string) carbon_get_post_meta( $id, 'odw_license' ); + if ( 'sonstige' === $lic ) { + return (string) carbon_get_post_meta( $id, 'odw_license_custom' ); + } + return $lic; + } + + /** + * Prüft, ob eine Lizenz-URI in einem bekannten Lizenz-Vokabular enthalten ist. + * + * @param string $uri Lizenz-URI. + * @return bool + */ + private static function license_in_vocab( string $uri ): bool { + if ( '' === $uri || 'sonstige' === $uri ) { + return false; + } + + $options = ODW_Fields::get_license_options(); + if ( isset( $options[ $uri ] ) ) { + return true; + } + + $extended = ODW_Fields::load_license_list(); + return isset( $extended[ $uri ] ); + } + /** * Ermittelt die MQA-Bewertungsstufe aus erreichten/bewertbaren Punkten. * Die MQA-Schwellen (351/221/121 von 405) werden proportional auf das @@ -498,7 +587,7 @@ public static function render_meta_box( \WP_Post $post ): void { echo esc_html( sprintf( /* translators: %d: number of points not yet assessed */ - __( '%d Punkte werden derzeit nicht bewertet (URL-Erreichbarkeit, Vokabular- und DCAT-AP-SHACL-Prüfung folgen).', 'open-data-wizard' ), + __( '%d Punkte werden derzeit nicht bewertet (URL-Erreichbarkeit und DCAT-AP-SHACL-Prüfung folgen).', 'open-data-wizard' ), $not_assessed ) ); diff --git a/languages/open-data-wizard-en_US.mo b/languages/open-data-wizard-en_US.mo index 0dd6d7837ee211b107a9207f14a2ff9e2194084d..8dadac79acf0f5ad25aeb8db8679bc9cde2a2bd6 100644 GIT binary patch delta 8037 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 delta 8013 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 zcof~yassertArrayHasKey( 'Bayern', $options ); } + /** + * Format metadata exposes the MQA machine-readable / non-proprietary flags. + */ + public function test_get_format_meta_exposes_mqa_flags(): void { + require_once ODW_PLUGIN_DIR . 'includes/class-fields.php'; + + $csv = ODW_Fields::get_format_meta( 'CSV' ); + $this->assertTrue( $csv['machine_readable'] ); + $this->assertTrue( $csv['non_proprietary'] ); + $this->assertNotSame( '', $csv['eu_uri'] ); + + // XLSX is machine-readable but proprietary. + $xlsx = ODW_Fields::get_format_meta( 'XLSX' ); + $this->assertTrue( $xlsx['machine_readable'] ); + $this->assertFalse( $xlsx['non_proprietary'] ); + + // "Sonstiges" is neither and has no EU vocabulary URI. + $other = ODW_Fields::get_format_meta( 'Sonstiges' ); + $this->assertFalse( $other['machine_readable'] ); + $this->assertFalse( $other['non_proprietary'] ); + $this->assertSame( '', $other['eu_uri'] ); + + // Unknown format returns an empty array. + $this->assertSame( array(), ODW_Fields::get_format_meta( 'NOPE' ) ); + } + /** * License descriptions provide plain-language text for the standard licenses. */