diff --git a/package.json b/package.json index d436e4c..850084a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@govoplan/search-webui", - "version": "0.1.18", + "version": "0.1.19", "private": true, "type": "module", "main": "webui/src/index.ts", diff --git a/pyproject.toml b/pyproject.toml index e5857b5..f7b35aa 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,13 +4,13 @@ build-backend = "setuptools.build_meta" [project] name = "govoplan-search" -version = "0.1.18" +version = "0.1.19" description = "Permission-aware global and contextual search for GovOPlaN." readme = "README.md" requires-python = ">=3.12" license = "AGPL-3.0-or-later" authors = [{ name = "GovOPlaN" }] -dependencies = ["govoplan-core>=0.1.18"] +dependencies = ["govoplan-core>=0.1.37"] [tool.setuptools.packages.find] where = ["src"] diff --git a/src/govoplan_search/__init__.py b/src/govoplan_search/__init__.py index bc54423..feb2d72 100644 --- a/src/govoplan_search/__init__.py +++ b/src/govoplan_search/__init__.py @@ -1,3 +1,3 @@ """GovOPlaN search module.""" -__version__ = "0.1.18" +__version__ = "0.1.19" diff --git a/src/govoplan_search/backend/manifest.py b/src/govoplan_search/backend/manifest.py index 6356046..5bc05b2 100644 --- a/src/govoplan_search/backend/manifest.py +++ b/src/govoplan_search/backend/manifest.py @@ -12,6 +12,7 @@ from govoplan_core.core.module_guards import ( ) from govoplan_core.core.modules import ( CapabilityDocumentation, + DocumentationCondition, DocumentationLink, DocumentationTopic, FrontendModule, @@ -39,7 +40,7 @@ from govoplan_search.backend.dsar_provider import ( MODULE_ID = "search" MODULE_NAME = "Search" -MODULE_VERSION = "0.1.18" +MODULE_VERSION = "0.1.19" READ_SCOPE = "search:result:read" INDEX_SCOPE = "search:index:write" ADMIN_SCOPE = "search:index:admin" @@ -235,6 +236,56 @@ manifest = ModuleManifest( "search.data-subject-requests", "search.admin.index", ], + "consequence_classes": { + "purge_derived_index": ( + "Deletes matching derived documents and queued changes idempotently." + ), + "preserve_authoritative_source": ( + "Does not correct or erase the authoritative object owned by its source module." + ), + "review_acl_only_matches": ( + "Requires source-authority review when only an ACL projection identifies the subject." + ), + }, + }, + translations={ + "de": { + "title": "Datenschutzanfragen zum abgeleiteten Suchindex", + "summary": ( + "Abgeleitete Indexkopien entfernen, ohne Search als führende Quelle " + "der indexierten Objekte zu behandeln." + ), + "body": ( + "Search gleicht innerhalb des exakten Mandanten nur ausdrückliche Index- " + "oder Änderungskennungen und quellanbietergeführte Referenzen ab. Selektoren " + "für Konten, Identitäten und Mitgliedschaften legen ausschließlich minimierte " + "Berechtigungsprojektionen offen und begründen niemals die Eigentümerschaft am " + "indexierten Quellobjekt. Indexierter Titel, Zusammenfassung, Inhalt, Suchtext, " + "URL, Schlüsselwörter, Metadaten, externe Referenzinhalte, Berechtigungswerte, " + "Prüfsummen, Cursor, eingereihte Dokumente und Fehler bleiben ausgeschlossen. " + "Abgeleitete Dokumente und eingereihte Änderungen können idempotent gelöscht " + "werden. Treffer allein über Berechtigungsprojektionen erfordern eine Prüfung " + "durch die führende Quelle. Berichtigung oder Löschung muss im Eigentümermodul " + "erfolgen, bevor Search neu aufgebaut wird; andernfalls darf die Quelle den " + "abgeleiteten Eintrag erneut veröffentlichen." + ), + } + }, + structured_translation_version="1", + structured_translations={ + "de": { + "consequence_classes": { + "purge_derived_index": ( + "Löscht passende abgeleitete Dokumente und eingereihte Änderungen idempotent." + ), + "preserve_authoritative_source": ( + "Berichtigt oder löscht nicht das führende Objekt des jeweiligen Quellmoduls." + ), + "review_acl_only_matches": ( + "Erfordert eine Prüfung durch die führende Quelle, wenn die betroffene Person nur über eine Berechtigungsprojektion erkannt wird." + ), + } + } }, links=( DocumentationLink( @@ -270,14 +321,98 @@ manifest = ModuleManifest( documentation_types=("admin", "user"), audience=("administrator", "user"), related_modules=("connectors", "views"), + conditions=(DocumentationCondition(required_scopes=(READ_SCOPE,)),), metadata={ - "kind": "reference", + "kind": "workflow", "help_contexts": [ "search.global", "search.results", "search.filters", "search.admin.index", ], + "purpose": ( + "Find currently authorized native and connected objects without broadening source permissions." + ), + "prerequisites": [ + "The actor has Search read access and the source module authorizes each returned object now.", + "At least one enabled source provider has published or can supply a searchable projection.", + ], + "steps": [ + "Open Search from the title bar, F3, or Ctrl/Cmd+K and enter a precise term.", + "Narrow the result set by type, source, or context without changing source authorization.", + "Review partial-provider and quarantine diagnostics before relying on completeness.", + "Open an authorized result; Search rechecks the provider-owned object permission before disclosure.", + "Administrators may process queued changes, reconcile providers, or start a bounded rebuild when diagnostics require it.", + ], + "limitations": [ + "Search is a derived discovery layer and is never authoritative for source content or access decisions.", + "A provider failure can make results incomplete but never permits unsafe results to bypass ACL filtering.", + ], + "operational_consequences": { + "process_queue": "Applies pending derived index changes under current tenant and provider boundaries.", + "reconcile": "Compares enabled source coverage and retains unresolved changes in quarantine.", + "rebuild": "Recreates bounded derived projections while source data remains authoritative.", + }, + "verification": [ + "Every displayed result names its source and remains openable by the current principal.", + "Partial-provider failures and quarantined changes remain visible as diagnostics.", + "Administrative rebuild status can be reconciled against the enabled provider inventory.", + ], + }, + translations={ + "de": { + "title": "Globale und kontextbezogene Suche", + "summary": ( + "Berechtigte native und angebundene Objekte über eine gemeinsame, " + "berechtigungsbewusste Oberfläche finden." + ), + "body": ( + "Search verwendet den eingebauten Datenbankindex und kann optionale Anbieter " + "zusammenführen. Quellmodule melden durchsuchbare Typen, Kontextbereiche und " + "berechtigungsgeprüfte Indexeinträge. Externe Suchmaschinen bleiben optionale " + "Adapter. Der Suchbefehl in der Titelleiste, F3 oder Strg/Cmd+K öffnet die per " + "Tastatur bedienbare Suchüberlagerung; Filter erweitern niemals die " + "Quellberechtigungen der aktuellen Person. Anbieterausfälle werden als partielle " + "Diagnosen angezeigt, ohne sichere Ergebnisse zu verwerfen. Administratoren " + "können die Abdeckung nativer Quellen prüfen, eingereihte Änderungen verarbeiten, " + "aktivierte Module abgleichen und begrenzte Neuaufbauten aus der Administration " + "starten. Änderungen in Quarantäne bleiben sichtbar, bis sie repariert und " + "abgeglichen wurden." + ), + } + }, + structured_translation_version="1", + structured_translations={ + "de": { + "purpose": ( + "Aktuell berechtigte native und angebundene Objekte finden, ohne Quellberechtigungen zu erweitern." + ), + "prerequisites": [ + "Die handelnde Person darf Search lesen und das Quellmodul autorisiert jedes zurückgegebene Objekt weiterhin.", + "Mindestens ein aktivierter Quellanbieter hat eine durchsuchbare Projektion veröffentlicht oder kann sie bereitstellen.", + ], + "steps": [ + "Search über die Titelleiste, F3 oder Strg/Cmd+K öffnen und einen präzisen Suchbegriff eingeben.", + "Die Ergebnisse nach Typ, Quelle oder Kontext eingrenzen, ohne die Quellautorisierung zu verändern.", + "Diagnosen zu partiellen Anbieterausfällen und Quarantäne prüfen, bevor Vollständigkeit angenommen wird.", + "Ein berechtigtes Ergebnis öffnen; Search prüft vor der Offenlegung erneut die Berechtigung am quellengeführten Objekt.", + "Administratoren können bei entsprechenden Diagnosen eingereihte Änderungen verarbeiten, Anbieter abgleichen oder einen begrenzten Neuaufbau starten.", + ], + "limitations": [ + "Search ist eine abgeleitete Auffindbarkeitsschicht und niemals führend für Quellinhalte oder Zugriffsentscheidungen.", + "Ein Anbieterausfall kann Ergebnisse unvollständig machen, erlaubt aber niemals das Umgehen der Berechtigungsfilterung.", + ], + "operational_consequences": { + "process_queue": "Verarbeitet ausstehende abgeleitete Indexänderungen innerhalb der aktuellen Mandanten- und Anbietergrenzen.", + "reconcile": "Vergleicht die Abdeckung aktivierter Quellen und hält ungeklärte Änderungen in Quarantäne.", + "rebuild": "Erstellt begrenzte abgeleitete Projektionen neu, während die Quelldaten führend bleiben.", + }, + "verification": [ + "Jedes angezeigte Ergebnis nennt seine Quelle und kann von der aktuellen Person weiterhin geöffnet werden.", + "Partielle Anbieterausfälle und Änderungen in Quarantäne bleiben als Diagnosen sichtbar.", + "Der administrative Neuaufbaustatus lässt sich mit dem Inventar aktivierter Anbieter abgleichen.", + ], + } }, links=( DocumentationLink( @@ -295,10 +430,20 @@ manifest = ModuleManifest( maturity="vertical_slice", documentation_ref="README.md", test_ref="tests/test_postgres_search.py", - known_limits=("The built-in PostgreSQL index is implemented; optional OpenSearch target evidence is not.",), + known_limits=( + "The built-in PostgreSQL index is implemented; optional OpenSearch target evidence is not.", + ), supported_authority_modes=("external_mirror",), - owned_concepts=("derived search index", "search ACL projection", "index change queue"), - non_owned_concepts=("source object", "source authorization", "external search engine"), + owned_concepts=( + "derived search index", + "search ACL projection", + "index change queue", + ), + non_owned_concepts=( + "source object", + "source authorization", + "external search engine", + ), recovery_docs=("README.md",), security_docs=("README.md",), operations_docs=("README.md",), diff --git a/tests/test_documentation.py b/tests/test_documentation.py new file mode 100644 index 0000000..0742822 --- /dev/null +++ b/tests/test_documentation.py @@ -0,0 +1,31 @@ +from __future__ import annotations + +import unittest + +from govoplan_core.core.modules import ( + documentation_structured_translation_issues, + user_workflow_scope_condition_issues, +) +from govoplan_search.backend.manifest import manifest + + +class SearchDocumentationTests(unittest.TestCase): + def test_public_topics_have_complete_german_reference_content(self) -> None: + self.assertEqual(2, len(manifest.documentation)) + for topic in manifest.documentation: + translation = topic.translations.get("de", {}) + self.assertTrue( + all(translation.get(key) for key in ("title", "summary", "body")) + ) + self.assertEqual((), documentation_structured_translation_issues(topic)) + + def test_documentation_has_scope_conditioned_workflow_and_reference(self) -> None: + kinds = {topic.metadata.get("kind") for topic in manifest.documentation} + self.assertIn("workflow", kinds) + self.assertIn("reference", kinds) + for topic in manifest.documentation: + self.assertEqual((), user_workflow_scope_condition_issues(topic)) + + +if __name__ == "__main__": + unittest.main() diff --git a/webui/package.json b/webui/package.json index aba471b..719010b 100644 --- a/webui/package.json +++ b/webui/package.json @@ -1,6 +1,6 @@ { "name": "@govoplan/search-webui", - "version": "0.1.18", + "version": "0.1.19", "private": true, "type": "module", "main": "src/index.ts",