diff --git a/alembic/dev_versions/a36d8e4f9b12_german_reference_locale.py b/alembic/dev_versions/a36d8e4f9b12_german_reference_locale.py new file mode 100644 index 0000000..73c12ac --- /dev/null +++ b/alembic/dev_versions/a36d8e4f9b12_german_reference_locale.py @@ -0,0 +1,23 @@ +from __future__ import annotations + +from importlib.util import module_from_spec, spec_from_file_location +from pathlib import Path + + +_path = ( + Path(__file__).resolve().parents[1] + / "versions" + / "a36d8e4f9b12_german_reference_locale.py" +) +_spec = spec_from_file_location("govoplan_german_reference_locale_migration", _path) +if _spec is None or _spec.loader is None: + raise RuntimeError(f"Unable to load migration implementation from {_path}") +_module = module_from_spec(_spec) +_spec.loader.exec_module(_module) + +revision = _module.revision +down_revision = _module.down_revision +branch_labels = _module.branch_labels +depends_on = _module.depends_on +upgrade = _module.upgrade +downgrade = _module.downgrade diff --git a/alembic/versions/a36d8e4f9b12_german_reference_locale.py b/alembic/versions/a36d8e4f9b12_german_reference_locale.py new file mode 100644 index 0000000..beb91c7 --- /dev/null +++ b/alembic/versions/a36d8e4f9b12_german_reference_locale.py @@ -0,0 +1,44 @@ +"""adopt German as the untouched system reference locale + +Revision ID: a36d8e4f9b12 +Revises: f25c9d3e7a01 +Create Date: 2026-08-05 00:00:00.000000 +""" + +from __future__ import annotations + +from alembic import op +import sqlalchemy as sa + + +revision = "a36d8e4f9b12" +down_revision = "f25c9d3e7a01" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + bind = op.get_bind() + if "core_system_settings" not in set(sa.inspect(bind).get_table_names()): + return + + settings = sa.table( + "core_system_settings", + sa.column("id", sa.String), + sa.column("default_locale", sa.String), + sa.column("created_at", sa.DateTime(timezone=True)), + sa.column("updated_at", sa.DateTime(timezone=True)), + ) + bind.execute( + settings.update() + .where(settings.c.id == "global") + .where(settings.c.default_locale == "en") + .where(settings.c.created_at == settings.c.updated_at) + .values(default_locale="de", updated_at=sa.func.now()) + ) + + +def downgrade() -> None: + # Locale selection is user-visible state. A downgrade must not overwrite a + # German value that may have been selected explicitly after this migration. + pass diff --git a/docs/CONTEXTUAL_HELP_CONTRACT.md b/docs/CONTEXTUAL_HELP_CONTRACT.md index edfde49..3e60185 100644 --- a/docs/CONTEXTUAL_HELP_CONTRACT.md +++ b/docs/CONTEXTUAL_HELP_CONTRACT.md @@ -28,6 +28,12 @@ manifest, allowing the shell to link directly to an exact topic when possible. Docs still performs the authoritative audience, permission, configured-state, and documentation-type filtering. +Core also maps explicit route, navigation, settings, and View surface IDs to +the module's static user or administrator documentation baseline. This makes a +page association complete by default and gives every derived field/action +context a useful fallback. Exact `metadata.help_contexts` remain the preferred +authoring mechanism for consequential or unfamiliar controls. + When there is no exact topic, Docs resolves the page fallback and then the first visible topic owned by the module. If Docs is unavailable, the shell opens the hosted documentation with the same context parameters. diff --git a/docs/DOCUMENTATION_MAP.md b/docs/DOCUMENTATION_MAP.md index 94116af..bcf447a 100644 --- a/docs/DOCUMENTATION_MAP.md +++ b/docs/DOCUMENTATION_MAP.md @@ -18,7 +18,9 @@ operator, and roadmap pages. | External references and integration maturity | `EXTERNAL_REFERENCES_AND_INTEGRATION_MATURITY.md` | Stable external identity and cumulative connector maturity; configured source authority is defined by the meta target architecture. | | Institutional context and governed references | `INSTITUTIONAL_CONTEXT_CONTRACT.md` | Shared temporal, actor/representation, institution, mandate, service, party, decision, evidence, legal-basis, information-governance, presentation, and geo DTO/provider contracts. | | Temporal data read context | `TEMPORAL_DATA_CONTEXT.md` | Valid-time and recorded-time titlebar selection, HTTP/cache contract, security boundary, and module-adoption rule. | +| Cross-module information governance adoption | `INFORMATION_GOVERNANCE_ADOPTION.md` | Manifest evidence and enforcement rules for temporal browsing, purpose-aware access, retention, and institutional context. | | Context-sensitive F1 help | `CONTEXTUAL_HELP_CONTRACT.md` | Focus, route, module-manifest documentation contexts, Docs projection, and hosted fallback. | +| German localization and help quality gate | `LOCALIZATION_AND_HELP_QUALITY.md` | German reference locale, new-installation default, catalog completeness, automatic page associations, and explicit-help review priorities. | | Postbox E2EE target architecture | `POSTBOX_E2EE_ARCHITECTURE.md` | Strategic encrypted postbox/mailbox model, key ownership, role mailbox semantics, and retraction limits. | | Shared state, runtime coordination, and recovery | `STATE_AND_RECOVERY_CONTRACT.md` | State profiles, object storage, node registration/drain, fenced leases, migration ordering, and recovery evidence. | | Module lifecycle recovery | `MODULE_LIFECYCLE_RECOVERY.md` | Installer/live-graph recovery modes, deployment fence, evidence, retry blocking, and operator reconciliation. | @@ -39,6 +41,8 @@ operator, and roadmap pages. | Topic | Canonical document | Notes | | --- | --- | --- | | Product roadmap and module routing | `GOVOPLAN_MASTER_ROADMAP.md` | Product-level sequencing, implementation gates, issue routing, and missing-module decisions. | +| Stable platform ideas | `govoplan/docs/PLATFORM_CORE_IDEAS.md` | Cross-product thesis, canonical distinctions, product experience rule, maturity rule, and decision test. | +| Current cross-product reconciliation | `govoplan/docs/STRATEGY_STATUS.md` | The only current prose status source; generated evidence and Gitea remain authoritative inputs. | | Institutional governance target | `govoplan/docs/INSTITUTIONAL_GOVERNANCE_TARGET_ARCHITECTURE.md` | Cross-product semantic layers, source-authority modes, candidate Mandates/Services/Parties/Decisions boundaries, and migration sequence. | | UI/UX decisions | `UI_UX_DECISION_LEDGER.md` | Binding guided-UI decisions, open decisions, impact index, and review checklist. | | Core interface migration | `INTERFACE_PATTERN_MIGRATION.md` | Core-owned settings, credential, retention, lifecycle, and shared-component evidence for the product pattern language. | diff --git a/docs/INFORMATION_GOVERNANCE_ADOPTION.md b/docs/INFORMATION_GOVERNANCE_ADOPTION.md new file mode 100644 index 0000000..4523772 --- /dev/null +++ b/docs/INFORMATION_GOVERNANCE_ADOPTION.md @@ -0,0 +1,131 @@ +# Information Governance Adoption + +## Platform Rule + +Temporal browsing, purpose-aware access, retention, and institutional context +are platform-wide information-governance dimensions. Every module receives the +same contract by default. A module may claim `partial` or `enforced` only with +repository-owned object scope, evidence, and limitations; it may claim +`not_applicable` only when the dimension genuinely does not apply. + +Historical business data is always authorized under the current security +state. No module may use a historical permission, membership, role, function +assignment, or policy projection to weaken present-day access. + +Platform-wide adoption is tracked in +[GovOPlaN #40](https://git.add-ideas.de/GovOPlaN/govoplan/issues/40), with +temporal reads detailed in +[GovOPlaN #39](https://git.add-ideas.de/GovOPlaN/govoplan/issues/39). + +## Manifest Declaration + +`ModuleManifest.information_governance` publishes four dimensions: + +- `temporal_browsing`; +- `purpose_aware_access`; +- `retention`; +- `institutional_context`. + +Each dimension declares: + +- adoption: `not_applicable`, `contract_only`, `partial`, or `enforced`; +- object types covered; +- repository-local test/documentation evidence; +- the remaining limitation for `contract_only` or `partial`. + +The default is intentionally `contract_only`. It applies the platform rule +without pretending that existing domain queries and effects already enforce +it. `reference_ready`, `supported`, and `lts` modules cannot retain an +applicable dimension below `enforced`. + +## Read Contract + +For every persistent domain object, the owner classifies the read: + +1. **Current-only:** historical semantics do not exist and the API says so. +2. **Valid-time:** select facts effective now or at the requested instant. +3. **Bitemporal:** additionally select only revisions known by `recorded_at`. +4. **All-validity:** return effective revisions in a bounded history view. + +The Core temporal middleware supplies the request context. Owners apply it in +repositories or query helpers, include it in cache keys, return evaluated +context, and test current/at/all plus recorded-time boundaries. Search, +reporting, exports, selectors, counts, and drill-through must use the same +projection as the owning list/detail API. + +## Purpose-Aware Access Contract + +Permission establishes a technical action ceiling. Purpose-aware access asks +whether this actor, represented capacity, case/work item, legal basis, and +declared use may access this object now. + +- A client-supplied purpose is an assertion, never authority by itself. +- The owner or Policy capability validates the purpose and returns explainable + provenance. +- Sensitive access can require case assignment, mandate, reason capture, + approval, or break-glass evidence. +- Search, selectors, reporting, exports, background jobs, and connectors apply + the same decision. +- Audit records the validated purpose identifier and decision reference, not + unnecessary content. + +## Retention Contract + +Every persistent object declares an owner, retention class or policy reference, +trigger, start instant, hold behavior, review/disposition action, and evidence. +Retention is not a generic timestamp deletion job. + +- Domain owners enumerate and execute their own effects through a typed + retention provider. +- Policy resolves inherited ceilings and simulation. +- Records owns record disposition; Files owns byte/object effects; Audit owns + audit-detail behavior; external providers declare their own effect and + recovery semantics. +- Dry-run, legal hold, exact revision, idempotency, outcome unknown, + reconciliation, correction, and destruction evidence are mandatory for + consequential removal. + +## Institutional Context Contract + +Consequential objects and effects carry the relevant tenant, institution, +organization unit, function, mandate/jurisdiction, service/case/work item, +party/representation, decision, and record references. Context is minimized to +what the operation needs. Organizational membership is not itself permission +or mandate. + +Events, automation intents, audit evidence, records, and external effects retain +the same governed context envelope or an exact reference to it. Consumers must +not reconstruct authority later from mutable current structures. + +## Adoption Order + +1. Inventory every domain list/detail/search/export/effect and classify all + four dimensions. +2. Migrate institutional owners first: Access, IDM, Organizations, Mandates, + Services, Parties, Cases, Approvals, Committee, Decisions, Voting, and + Records. +3. Migrate communication and content: Addresses, Distribution Lists, Campaign, + Postbox, Mail, Calendar, Files, Templates, and Forms Runtime. +4. Migrate data projections: Connectors, Datasources, Dataflow, Reporting, + Search, Risk Compliance, and Dashboard. +5. Migrate workflow/task/background/provider operations and prove that no + asynchronous path drops context. +6. Advance manifest claims only after owner tests and browser/reference-journey + evidence pass. + +The generated platform inventory reports adoption counts and module details. +Gitea tracks individual migrations; the declaration is evidence and a maturity +gate, not a substitute for implementation. + +## Definition Of Enforced + +A dimension is `enforced` only when: + +- all declared object types and public reads/effects use it; +- list/detail/count/search/export/worker behavior is consistent; +- cache and pagination semantics cannot cross contexts; +- absence, invalid values, and inaccessible referenced context fail safely; +- tests cover current, historical, unauthorized, replay, and module-absence + combinations appropriate to the dimension; +- user/admin documentation explains behavior and limitations; +- the manifest cites those tests and docs. diff --git a/docs/LOCALIZATION_AND_HELP_QUALITY.md b/docs/LOCALIZATION_AND_HELP_QUALITY.md new file mode 100644 index 0000000..ac64c40 --- /dev/null +++ b/docs/LOCALIZATION_AND_HELP_QUALITY.md @@ -0,0 +1,60 @@ +# Localization And Contextual Help Quality + +## Reference Language + +German (`de`) is GovOPlaN's first-class reference target. Every translation key +used by a shipped WebUI must exist in German and English. German completeness is +a release gate; English remains the source-code fallback language so existing +literal labels and external developer APIs do not change semantics. + +New installations and tenants default to German. Existing system, tenant, and +user preferences are preserved. The available-language and policy model can +still select another default or disable a package at the relevant scope. + +Explicit high-risk help content and browser acceptance are tracked in +[Core #284](https://git.add-ideas.de/GovOPlaN/govoplan-core/issues/284). + +The platform inventory recognizes both inline locale objects and generated +catalogs declared as `const de` / `const en`. Its strict mode requires both +locales and reports `de` explicitly as the reference locale. + +## Help Resolution + +Every focusable field and action receives a stable derived F1 identity from the +shared shell, even when the component has no dedicated help text. Resolution +falls back from field/action to dialog or page and then to the module's visible +documentation baseline. + +Backend manifests publish explicit topic associations first. Core additionally +associates declared route, navigation, settings, and View surface IDs with the +module's static user or administrator documentation baseline. Feature modules +should still add exact `metadata.help_contexts` entries for consequential, +unfamiliar, policy-controlled, destructive, security-sensitive, or legally +meaningful fields and actions. + +The generated `help_review_candidates` list is therefore a content-depth queue, +not a list of controls on which F1 cannot work. It should prioritize: + +1. effect, deletion, delivery, retention, disclosure, encryption, and recovery; +2. identity, representation, mandate, institutional context, and purpose; +3. valid-time versus recorded-time selection; +4. provider authority, synchronization, conflict, and outcome unknown; +5. fields whose consequences are not evident from their label. + +## Verification + +```bash +cd /mnt/DATA/git/govoplan +/mnt/DATA/git/govoplan/.venv/bin/python \ + tools/inventory/platform-interface-inventory.py \ + --strict --strict-declarations --strict-endpoints +``` + +The check must report: + +- reference locale `de` present and complete; +- no used key missing from `de` or `en`; +- every field has a resolvable F1 context; +- no duplicate stable IDs; +- no undeclared public WebUI surface; +- no stale runtime route or endpoint declaration. diff --git a/docs/STATE_AND_RECOVERY_CONTRACT.md b/docs/STATE_AND_RECOVERY_CONTRACT.md index 4101c1c..e1745c3 100644 --- a/docs/STATE_AND_RECOVERY_CONTRACT.md +++ b/docs/STATE_AND_RECOVERY_CONTRACT.md @@ -93,10 +93,17 @@ Alembic, and post-migration tasks. The lock is session-scoped and therefore released if the migration process dies. Runtime roles run `govoplan_core.commands.wait_for_database`. It waits until -the database has exactly the configured Core/module Alembic heads and never -upgrades schema. This permits a migration Job and runtime Deployments to be +the database has exactly the configured, dependency-resolved Core/module +Alembic heads and never upgrades schema. Cross-module `depends_on` revisions +therefore do not leave runtime roles waiting for a branch marker Alembic has +correctly consumed. This permits a migration Job and runtime Deployments to be submitted together while keeping startup fail-closed. +Runtime coordination records the installed `govoplan-core` distribution +version for API, worker and scheduler roles. FastAPI/OpenAPI metadata versions +are presentation metadata and must not be used as deployable software identity; +mixing the two would create a false version-skew readiness failure. + ## Recovery Ledger `govoplan_core.core.recovery` provides a durable operation and evidence diff --git a/docs/migration-release-baselines.json b/docs/migration-release-baselines.json index 11b0c78..7b3c2c7 100644 --- a/docs/migration-release-baselines.json +++ b/docs/migration-release-baselines.json @@ -1065,6 +1065,408 @@ "release": "0.1.15", "squash_policy": "reviewed-manual", "track": "release" + }, + { + "heads": [ + { + "owner": "govoplan-notifications", + "revision": "6e2f91ab4c70" + }, + { + "owner": "govoplan-poll", + "revision": "6e7f8a9b0c1d" + }, + { + "owner": "govoplan-dashboard", + "revision": "7b9d2f4a6c8e" + }, + { + "owner": "govoplan-voting", + "revision": "8b9c0d1e2f3a" + }, + { + "owner": "govoplan-mail", + "revision": "93b4c5d6e7f8" + }, + { + "owner": "govoplan-core", + "revision": "a36d8e4f9b12" + }, + { + "owner": "govoplan-forms-runtime", + "revision": "a3d5f7b9c1e2" + }, + { + "owner": "govoplan-templates", + "revision": "a3f7c9d2e1b4" + }, + { + "owner": "govoplan-organizations", + "revision": "a61e4d9c72b8" + }, + { + "owner": "govoplan-mandates", + "revision": "a8b1c2d3e4f5" + }, + { + "owner": "govoplan-audit", + "revision": "a8d1e4f7b2c5" + }, + { + "owner": "govoplan-approvals", + "revision": "a91c4e72b5d8" + }, + { + "owner": "govoplan-policy", + "revision": "a9c4e7b2d5f8" + }, + { + "owner": "govoplan-idm", + "revision": "b1c2d3e4f5a6" + }, + { + "owner": "govoplan-search", + "revision": "b2c3d4e5f607" + }, + { + "owner": "govoplan-datasources", + "revision": "b8d2f5a0c3e7" + }, + { + "owner": "govoplan-views", + "revision": "b8e4c1f7a2d9" + }, + { + "owner": "govoplan-risk-compliance", + "revision": "b9c0d1e2f3a4" + }, + { + "owner": "govoplan-services", + "revision": "b9c2d3e4f5a6" + }, + { + "owner": "govoplan-parties", + "revision": "c0d3e4f5a6b7" + }, + { + "owner": "govoplan-identity-trust", + "revision": "c3f5a7b9d1e2" + }, + { + "owner": "govoplan-projects", + "revision": "c4a1e8f2d6b9" + }, + { + "owner": "govoplan-addresses", + "revision": "c5d7e8f9a0b1" + }, + { + "owner": "govoplan-access", + "revision": "c7e0a3d6f9b2" + }, + { + "owner": "govoplan-reporting", + "revision": "c8d5e2f6a9b3" + }, + { + "owner": "govoplan-scheduling", + "revision": "c9d4e7f1a2b3" + }, + { + "owner": "govoplan-decisions", + "revision": "d1e4f5a6b7c8" + }, + { + "owner": "govoplan-calendar", + "revision": "d24e5f607182" + }, + { + "owner": "govoplan-committee", + "revision": "d8b9f0a1c2e3" + }, + { + "owner": "govoplan-postbox", + "revision": "d8e3f6a9b2c5" + }, + { + "owner": "govoplan-campaign", + "revision": "e3c8f4a5b6d7" + }, + { + "owner": "govoplan-workflow-engine", + "revision": "e4a1f8c2d7b6" + }, + { + "owner": "govoplan-encryption", + "revision": "e5b7c9d1f3a4" + }, + { + "owner": "govoplan-dist-lists", + "revision": "e7c3a9d1b5f2" + }, + { + "owner": "govoplan-files", + "revision": "f1a2b3c4d5e7" + }, + { + "owner": "govoplan-dataflow", + "revision": "f6c2a9d4e7b1" + }, + { + "owner": "govoplan-cases", + "revision": "f6d3a8b1c4e7" + }, + { + "owner": "govoplan-connectors", + "revision": "f7c8d9e0a1b2" + } + ], + "owner_heads": [ + { + "owner": "govoplan-access", + "revisions": [ + "c7e0a3d6f9b2" + ] + }, + { + "owner": "govoplan-addresses", + "revisions": [ + "c5d7e8f9a0b1" + ] + }, + { + "owner": "govoplan-approvals", + "revisions": [ + "a91c4e72b5d8" + ] + }, + { + "owner": "govoplan-audit", + "revisions": [ + "a8d1e4f7b2c5" + ] + }, + { + "owner": "govoplan-calendar", + "revisions": [ + "d24e5f607182" + ] + }, + { + "owner": "govoplan-campaign", + "revisions": [ + "e3c8f4a5b6d7" + ] + }, + { + "owner": "govoplan-cases", + "revisions": [ + "f6d3a8b1c4e7" + ] + }, + { + "owner": "govoplan-committee", + "revisions": [ + "d8b9f0a1c2e3" + ] + }, + { + "owner": "govoplan-connectors", + "revisions": [ + "f7c8d9e0a1b2" + ] + }, + { + "owner": "govoplan-core", + "revisions": [ + "a36d8e4f9b12" + ] + }, + { + "owner": "govoplan-dashboard", + "revisions": [ + "7b9d2f4a6c8e" + ] + }, + { + "owner": "govoplan-dataflow", + "revisions": [ + "f6c2a9d4e7b1" + ] + }, + { + "owner": "govoplan-datasources", + "revisions": [ + "b8d2f5a0c3e7" + ] + }, + { + "owner": "govoplan-decisions", + "revisions": [ + "d1e4f5a6b7c8" + ] + }, + { + "owner": "govoplan-dist-lists", + "revisions": [ + "e7c3a9d1b5f2" + ] + }, + { + "owner": "govoplan-encryption", + "revisions": [ + "e5b7c9d1f3a4" + ] + }, + { + "owner": "govoplan-files", + "revisions": [ + "f1a2b3c4d5e7" + ] + }, + { + "owner": "govoplan-forms", + "revisions": [ + "e1f2a3b4c5d6" + ] + }, + { + "owner": "govoplan-forms-runtime", + "revisions": [ + "a3d5f7b9c1e2" + ] + }, + { + "owner": "govoplan-identity", + "revisions": [ + "5c6d7e8f9a10" + ] + }, + { + "owner": "govoplan-identity-trust", + "revisions": [ + "c3f5a7b9d1e2" + ] + }, + { + "owner": "govoplan-idm", + "revisions": [ + "b1c2d3e4f5a6" + ] + }, + { + "owner": "govoplan-mail", + "revisions": [ + "93b4c5d6e7f8" + ] + }, + { + "owner": "govoplan-mandates", + "revisions": [ + "a8b1c2d3e4f5" + ] + }, + { + "owner": "govoplan-notifications", + "revisions": [ + "6e2f91ab4c70" + ] + }, + { + "owner": "govoplan-organizations", + "revisions": [ + "a61e4d9c72b8" + ] + }, + { + "owner": "govoplan-parties", + "revisions": [ + "c0d3e4f5a6b7" + ] + }, + { + "owner": "govoplan-policy", + "revisions": [ + "a9c4e7b2d5f8" + ] + }, + { + "owner": "govoplan-poll", + "revisions": [ + "6e7f8a9b0c1d" + ] + }, + { + "owner": "govoplan-postbox", + "revisions": [ + "d8e3f6a9b2c5" + ] + }, + { + "owner": "govoplan-projects", + "revisions": [ + "c4a1e8f2d6b9" + ] + }, + { + "owner": "govoplan-reporting", + "revisions": [ + "c8d5e2f6a9b3" + ] + }, + { + "owner": "govoplan-risk-compliance", + "revisions": [ + "b9c0d1e2f3a4" + ] + }, + { + "owner": "govoplan-scheduling", + "revisions": [ + "c9d4e7f1a2b3" + ] + }, + { + "owner": "govoplan-search", + "revisions": [ + "b2c3d4e5f607" + ] + }, + { + "owner": "govoplan-services", + "revisions": [ + "b9c2d3e4f5a6" + ] + }, + { + "owner": "govoplan-templates", + "revisions": [ + "a3f7c9d2e1b4" + ] + }, + { + "owner": "govoplan-views", + "revisions": [ + "b8e4c1f7a2d9" + ] + }, + { + "owner": "govoplan-voting", + "revisions": [ + "8b9c0d1e2f3a" + ] + }, + { + "owner": "govoplan-workflow-engine", + "revisions": [ + "e4a1f8c2d7b6" + ] + } + ], + "recorded_at": "2026-08-05T17:51:25Z", + "release": "0.1.16", + "squash_policy": "reviewed-manual", + "track": "release" } ], "version": 1 diff --git a/pyproject.toml b/pyproject.toml index 2fbf04a..09fbcef 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "govoplan-core" -version = "0.1.15" +version = "0.1.16" description = "Reusable GovOPlaN platform core, access, tenancy, and RBAC components." readme = "README.md" requires-python = ">=3.12" diff --git a/src/govoplan_core/admin/models.py b/src/govoplan_core/admin/models.py index 803c7ba..6735e3e 100644 --- a/src/govoplan_core/admin/models.py +++ b/src/govoplan_core/admin/models.py @@ -12,7 +12,7 @@ class SystemSettings(Base, TimestampMixin): __tablename__ = "core_system_settings" id: Mapped[str] = mapped_column(String(36), primary_key=True, default="global") - default_locale: Mapped[str] = mapped_column(String(20), default="en", nullable=False) + default_locale: Mapped[str] = mapped_column(String(20), default="de", nullable=False) allow_tenant_custom_groups: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False) allow_tenant_custom_roles: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False) allow_tenant_api_keys: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False) @@ -20,4 +20,3 @@ class SystemSettings(Base, TimestampMixin): __all__ = ["SystemSettings"] - diff --git a/src/govoplan_core/api/v1/schemas.py b/src/govoplan_core/api/v1/schemas.py index e9fef32..6848b11 100644 --- a/src/govoplan_core/api/v1/schemas.py +++ b/src/govoplan_core/api/v1/schemas.py @@ -84,7 +84,7 @@ class TenantInfo(BaseModel): slug: str name: str is_active: bool = True - default_locale: str = "en" + default_locale: str = "de" enabled_language_codes: list[str] = Field(default_factory=list) @@ -210,7 +210,7 @@ class AuthProfileResponse(BaseModel): active_tenant: TenantInfo available_languages: list[LanguageInfo] = Field(default_factory=list) enabled_language_codes: list[str] = Field(default_factory=list) - default_language: str = "en" + default_language: str = "de" profile_loaded: bool = True @@ -239,7 +239,7 @@ class LoginResponse(BaseModel): principal: PrincipalContextInfo | None = None available_languages: list[LanguageInfo] = Field(default_factory=list) enabled_language_codes: list[str] = Field(default_factory=list) - default_language: str = "en" + default_language: str = "de" profile_loaded: bool = True roles_loaded: bool = True groups_loaded: bool = True @@ -257,7 +257,7 @@ class MeResponse(BaseModel): principal: PrincipalContextInfo | None = None available_languages: list[LanguageInfo] = Field(default_factory=list) enabled_language_codes: list[str] = Field(default_factory=list) - default_language: str = "en" + default_language: str = "de" profile_loaded: bool = True roles_loaded: bool = True groups_loaded: bool = True diff --git a/src/govoplan_core/core/first_admin.py b/src/govoplan_core/core/first_admin.py index b9b25f6..105393e 100644 --- a/src/govoplan_core/core/first_admin.py +++ b/src/govoplan_core/core/first_admin.py @@ -356,7 +356,7 @@ def consume_first_admin_credential( tenant = Tenant( slug=clean_tenant_slug, name=clean_tenant_name, - default_locale="en", + default_locale="de", settings={}, is_active=True, ) diff --git a/src/govoplan_core/core/information_governance.py b/src/govoplan_core/core/information_governance.py new file mode 100644 index 0000000..dba9a60 --- /dev/null +++ b/src/govoplan_core/core/information_governance.py @@ -0,0 +1,236 @@ +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import Literal, Mapping, cast + + +InformationGovernanceAdoption = Literal[ + "not_applicable", + "contract_only", + "partial", + "enforced", +] + +INFORMATION_GOVERNANCE_ADOPTION_ORDER: tuple[InformationGovernanceAdoption, ...] = ( + "not_applicable", + "contract_only", + "partial", + "enforced", +) + + +class InformationGovernanceDeclarationError(ValueError): + pass + + +@dataclass(frozen=True, slots=True) +class InformationGovernanceDimension: + """Truthful module-level adoption claim for one cross-cutting dimension.""" + + adoption: InformationGovernanceAdoption = "contract_only" + object_types: tuple[str, ...] = () + evidence: tuple[str, ...] = () + limitation: str | None = None + + def __post_init__(self) -> None: + if self.adoption not in INFORMATION_GOVERNANCE_ADOPTION_ORDER: + raise InformationGovernanceDeclarationError( + f"Unsupported information-governance adoption: {self.adoption!r}." + ) + for field_name in ("object_types", "evidence"): + values = getattr(self, field_name) + if len(values) != len(set(values)) or any(not item.strip() for item in values): + raise InformationGovernanceDeclarationError( + f"Information-governance {field_name.replace('_', ' ')} must " + "contain unique non-empty values." + ) + if self.adoption == "enforced" and not self.evidence: + raise InformationGovernanceDeclarationError( + "An enforced information-governance dimension requires evidence." + ) + if self.adoption in {"partial", "enforced"} and not self.object_types: + raise InformationGovernanceDeclarationError( + "Partial and enforced information-governance dimensions must " + "name their covered object types." + ) + if self.adoption == "not_applicable" and self.object_types: + raise InformationGovernanceDeclarationError( + "A non-applicable information-governance dimension cannot declare object types." + ) + if self.adoption in {"contract_only", "partial"} and not str( + self.limitation or "" + ).strip(): + raise InformationGovernanceDeclarationError( + "Contract-only and partial adoption must state the current limitation." + ) + + def to_dict(self) -> dict[str, object]: + return { + "adoption": self.adoption, + "object_types": list(self.object_types), + "evidence": list(self.evidence), + "limitation": self.limitation, + } + + +def _contract_only_dimension() -> InformationGovernanceDimension: + return InformationGovernanceDimension( + adoption="contract_only", + limitation=( + "The platform contract applies, but module-specific adoption evidence " + "has not been declared." + ), + ) + + +@dataclass(frozen=True, slots=True) +class ModuleInformationGovernance: + """Cross-cutting data-use requirements and honest adoption evidence.""" + + temporal_browsing: InformationGovernanceDimension = field( + default_factory=_contract_only_dimension + ) + purpose_aware_access: InformationGovernanceDimension = field( + default_factory=_contract_only_dimension + ) + retention: InformationGovernanceDimension = field( + default_factory=_contract_only_dimension + ) + institutional_context: InformationGovernanceDimension = field( + default_factory=_contract_only_dimension + ) + current_authorization_for_historical_reads: bool = True + contract_version: str = "1" + + def __post_init__(self) -> None: + if self.contract_version != "1": + raise InformationGovernanceDeclarationError( + "Unsupported module information-governance contract version." + ) + if not self.current_authorization_for_historical_reads: + raise InformationGovernanceDeclarationError( + "Historical reads must always use current authorization." + ) + for name, dimension in self.dimensions.items(): + if not isinstance(dimension, InformationGovernanceDimension): + raise InformationGovernanceDeclarationError( + f"Information-governance dimension {name!r} has an invalid value." + ) + + @property + def dimensions(self) -> Mapping[str, InformationGovernanceDimension]: + return { + "temporal_browsing": self.temporal_browsing, + "purpose_aware_access": self.purpose_aware_access, + "retention": self.retention, + "institutional_context": self.institutional_context, + } + + def to_dict(self) -> dict[str, object]: + return { + "contract_version": self.contract_version, + "current_authorization_for_historical_reads": ( + self.current_authorization_for_historical_reads + ), + "dimensions": { + name: dimension.to_dict() + for name, dimension in self.dimensions.items() + }, + } + + +def information_governance_from_mapping( + value: Mapping[str, object], +) -> ModuleInformationGovernance: + raw_dimensions = value.get("dimensions") + if not isinstance(raw_dimensions, Mapping): + raise InformationGovernanceDeclarationError( + "Information-governance dimensions must be an object." + ) + + def dimension(name: str) -> InformationGovernanceDimension: + raw_dimension = raw_dimensions.get(name) + if not isinstance(raw_dimension, Mapping): + raise InformationGovernanceDeclarationError( + f"Information-governance dimension {name!r} must be an object." + ) + + def text_tuple(field_name: str) -> tuple[str, ...]: + raw_values = raw_dimension.get(field_name, ()) + if not isinstance(raw_values, (list, tuple)): + raise InformationGovernanceDeclarationError( + f"Information-governance {name}.{field_name} must be a list." + ) + if any(not isinstance(item, str) for item in raw_values): + raise InformationGovernanceDeclarationError( + f"Information-governance {name}.{field_name} must contain strings." + ) + return tuple(raw_values) + + raw_limitation = raw_dimension.get("limitation") + raw_adoption = raw_dimension.get("adoption") or "contract_only" + if not isinstance(raw_adoption, str): + raise InformationGovernanceDeclarationError( + f"Information-governance {name}.adoption must be a string." + ) + if raw_limitation is not None and not isinstance(raw_limitation, str): + raise InformationGovernanceDeclarationError( + f"Information-governance {name}.limitation must be a string." + ) + return InformationGovernanceDimension( + adoption=cast( + InformationGovernanceAdoption, + raw_adoption, + ), + object_types=text_tuple("object_types"), + evidence=text_tuple("evidence"), + limitation=raw_limitation, + ) + + current_authorization = value.get( + "current_authorization_for_historical_reads", + True, + ) + if not isinstance(current_authorization, bool): + raise InformationGovernanceDeclarationError( + "current_authorization_for_historical_reads must be boolean." + ) + return ModuleInformationGovernance( + contract_version=str(value.get("contract_version") or "1"), + current_authorization_for_historical_reads=current_authorization, + temporal_browsing=dimension("temporal_browsing"), + purpose_aware_access=dimension("purpose_aware_access"), + retention=dimension("retention"), + institutional_context=dimension("institutional_context"), + ) + + +def information_governance_maturity_issues( + declaration: ModuleInformationGovernance, + *, + maturity: str | None, +) -> tuple[str, ...]: + if maturity not in {"reference_ready", "supported", "lts"}: + return () + incomplete = [ + name + for name, dimension in declaration.dimensions.items() + if dimension.adoption not in {"not_applicable", "enforced"} + ] + if not incomplete: + return () + return ( + f"Maturity {maturity!r} requires enforced or explicitly non-applicable " + "information governance for: " + ", ".join(incomplete), + ) + + +__all__ = [ + "INFORMATION_GOVERNANCE_ADOPTION_ORDER", + "InformationGovernanceAdoption", + "InformationGovernanceDeclarationError", + "InformationGovernanceDimension", + "ModuleInformationGovernance", + "information_governance_from_mapping", + "information_governance_maturity_issues", +] diff --git a/src/govoplan_core/core/module_package_catalog.py b/src/govoplan_core/core/module_package_catalog.py index 3eecb99..e1e3ac1 100644 --- a/src/govoplan_core/core/module_package_catalog.py +++ b/src/govoplan_core/core/module_package_catalog.py @@ -17,6 +17,10 @@ from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey, Ed25519PublicKey from govoplan_core.core.versioning import format_version_range, version_range_is_valid, version_satisfies_range +from govoplan_core.core.information_governance import ( + information_governance_from_mapping, + information_governance_maturity_issues, +) from govoplan_core.core.provider_governance import ( external_provider_from_mapping, module_architecture_from_mapping, @@ -630,6 +634,7 @@ def _normalize_catalog_item(value: Any) -> dict[str, object]: "tags": _string_list(value.get("tags")), } raw_architecture = value.get("architecture") + architecture_maturity: str | None = None if raw_architecture is not None: if not isinstance(raw_architecture, Mapping): raise ValueError( @@ -647,6 +652,27 @@ def _normalize_catalog_item(value: Any) -> dict[str, object]: + "; ".join(issues) ) item["architecture"] = architecture.to_dict() + architecture_maturity = architecture.maturity + raw_information_governance = value.get("information_governance") + if raw_information_governance is not None: + if not isinstance(raw_information_governance, Mapping): + raise ValueError( + "Module package catalog information_governance for " + f"{module_id!r} must be an object." + ) + information_governance = information_governance_from_mapping( + raw_information_governance + ) + governance_issues = information_governance_maturity_issues( + information_governance, + maturity=architecture_maturity, + ) + if governance_issues: + raise ValueError( + "Module package catalog information_governance for " + f"{module_id!r} is invalid: " + "; ".join(governance_issues) + ) + item["information_governance"] = information_governance.to_dict() raw_providers = value.get("external_providers") if raw_providers is not None: if not isinstance(raw_providers, list): diff --git a/src/govoplan_core/core/modules.py b/src/govoplan_core/core/modules.py index 94128d7..79395df 100644 --- a/src/govoplan_core/core/modules.py +++ b/src/govoplan_core/core/modules.py @@ -4,6 +4,7 @@ from collections.abc import Callable, Iterable, Mapping, Sequence from dataclasses import dataclass, field from typing import Any, Literal, Protocol, TYPE_CHECKING +from govoplan_core.core.information_governance import ModuleInformationGovernance from govoplan_core.core.ownership import OwnershipProviderRegistration from govoplan_core.core.provider_governance import ( ExternalProviderDeclaration, @@ -446,6 +447,9 @@ class ModuleManifest: ..., ] = () architecture: ModuleArchitectureDeclaration | None = None + information_governance: ModuleInformationGovernance = field( + default_factory=ModuleInformationGovernance + ) external_providers: tuple[ExternalProviderDeclaration, ...] = () external_provider_state_providers: tuple[ ExternalProviderStateProviderRegistration, diff --git a/src/govoplan_core/core/registry.py b/src/govoplan_core/core/registry.py index b0ce653..510def7 100644 --- a/src/govoplan_core/core/registry.py +++ b/src/govoplan_core/core/registry.py @@ -32,6 +32,9 @@ from govoplan_core.core.module_entitlements import ( current_tenant_execution_context, tenant_execution_scope, ) +from govoplan_core.core.information_governance import ( + information_governance_maturity_issues, +) from govoplan_core.core.ownership import ( OwnershipProviderRegistration, ResourceOwnershipProvider, @@ -788,6 +791,13 @@ def _validate_architecture_declarations(manifest: ModuleManifest) -> None: raise RegistryError( f"Module {manifest.id!r} architecture declaration: {issue}" ) + for issue in information_governance_maturity_issues( + manifest.information_governance, + maturity=architecture.maturity if architecture is not None else None, + ): + raise RegistryError( + f"Module {manifest.id!r} information-governance declaration: {issue}" + ) provider_ids: set[str] = set() declared_capabilities = { diff --git a/src/govoplan_core/db/bootstrap.py b/src/govoplan_core/db/bootstrap.py index c6484a2..9463822 100644 --- a/src/govoplan_core/db/bootstrap.py +++ b/src/govoplan_core/db/bootstrap.py @@ -73,7 +73,7 @@ def bootstrap_dev_data( ) -> BootstrapResult: tenant = session.query(Tenant).filter(Tenant.slug == tenant_slug).one_or_none() if tenant is None: - tenant = Tenant(slug=tenant_slug, name="Default Tenant", default_locale="en", settings={}) + tenant = Tenant(slug=tenant_slug, name="Default Tenant", default_locale="de", settings={}) session.add(tenant) session.flush() diff --git a/src/govoplan_core/db/migrations.py b/src/govoplan_core/db/migrations.py index 9bfd0de..cb7045f 100644 --- a/src/govoplan_core/db/migrations.py +++ b/src/govoplan_core/db/migrations.py @@ -685,7 +685,10 @@ def configured_migration_heads( manifest_factories=manifest_factories, migration_track=migration_track, ) - return tuple(sorted(ScriptDirectory.from_config(config).get_heads())) + scripts = ScriptDirectory.from_config(config) + return tuple( + sorted(revision.revision for revision in scripts.get_revisions("heads")) + ) def database_is_at_configured_heads( diff --git a/src/govoplan_core/i18n.py b/src/govoplan_core/i18n.py index 5f45a16..6b7274b 100644 --- a/src/govoplan_core/i18n.py +++ b/src/govoplan_core/i18n.py @@ -4,10 +4,12 @@ import re from typing import Any, Iterable I18N_SETTINGS_KEY = "i18n" +REFERENCE_LANGUAGE_CODE = "de" +SOURCE_LANGUAGE_CODE = "en" DEFAULT_LANGUAGE_PACKAGES: tuple[dict[str, str], ...] = ( - {"code": "en", "label": "English", "native_label": "English"}, {"code": "de", "label": "German", "native_label": "Deutsch"}, + {"code": "en", "label": "English", "native_label": "English"}, ) @@ -66,7 +68,7 @@ def normalize_enabled_language_codes( available_languages: Iterable[dict[str, Any]], *, default_locale: object = None, - fallback_codes: Iterable[str] = ("en", "de"), + fallback_codes: Iterable[str] = (REFERENCE_LANGUAGE_CODE, SOURCE_LANGUAGE_CODE), ) -> list[str]: available_codes = [normalize_language_code(item.get("code")) for item in available_languages if isinstance(item, dict)] available = {code for code in available_codes if code} @@ -88,8 +90,10 @@ def normalize_enabled_language_codes( if enabled: return enabled - if "en" in available: - return ["en"] + if REFERENCE_LANGUAGE_CODE in available: + return [REFERENCE_LANGUAGE_CODE] + if SOURCE_LANGUAGE_CODE in available: + return [SOURCE_LANGUAGE_CODE] return available_codes[:1] @@ -126,7 +130,7 @@ def preferred_language_code(user_settings: dict[str, Any] | None, allowed_codes: default = resolve_language_code(default_locale, enabled) if default: return default - return enabled[0] if enabled else "en" + return enabled[0] if enabled else REFERENCE_LANGUAGE_CODE def update_i18n_settings(settings: dict[str, Any] | None, **values: Any) -> dict[str, Any]: @@ -141,10 +145,13 @@ def system_i18n_payload(settings_item: Any | None) -> dict[str, object]: settings = getattr(settings_item, "settings", None) packages = system_language_packages(settings) available_codes = [item["code"] for item in packages] - default_language = resolve_language_code(getattr(settings_item, "default_locale", None), available_codes) or "en" + default_language = ( + resolve_language_code(getattr(settings_item, "default_locale", None), available_codes) + or REFERENCE_LANGUAGE_CODE + ) enabled = system_enabled_language_codes(settings, default_locale=default_language) if default_language not in enabled: - default_language = enabled[0] if enabled else "en" + default_language = enabled[0] if enabled else REFERENCE_LANGUAGE_CODE return { "available_languages": packages, "enabled_languages": enabled, diff --git a/src/govoplan_core/server/default_config.py b/src/govoplan_core/server/default_config.py index a80d3e5..3a25af1 100644 --- a/src/govoplan_core/server/default_config.py +++ b/src/govoplan_core/server/default_config.py @@ -1,6 +1,7 @@ from __future__ import annotations from contextlib import asynccontextmanager +from importlib.metadata import PackageNotFoundError, version from fastapi import Depends, FastAPI from fastapi import HTTPException, status @@ -20,6 +21,13 @@ from govoplan_core.core.runtime_coordination import ( from govoplan_core.settings import Settings, settings +def _core_distribution_version() -> str: + try: + return version("govoplan-core") + except PackageNotFoundError: + return "development" + + def _dev_bootstrap_needs_create_all(database_url: str) -> bool: try: return make_url(database_url).get_backend_name() == "sqlite" @@ -61,7 +69,7 @@ async def lifespan(app: FastAPI): ) runtime_agent = RuntimeNodeAgent( settings=settings, - software_version=app.version, + software_version=_core_distribution_version(), module_ids=module_ids, metadata={"process": "api"}, identity=configured_identity @@ -93,7 +101,7 @@ def register_health_details( ): app.state.govoplan_runtime_identity = runtime_identity( active_settings, - software_version=app.version, + software_version=_core_distribution_version(), module_ids=module_ids, ) bind_process_runtime_identity(app.state.govoplan_runtime_identity) diff --git a/src/govoplan_core/server/platform.py b/src/govoplan_core/server/platform.py index 0079577..900ab2f 100644 --- a/src/govoplan_core/server/platform.py +++ b/src/govoplan_core/server/platform.py @@ -142,23 +142,58 @@ def _frontend_view_surfaces(manifest: ModuleManifest) -> list[dict[str, object]] def _documentation_help_contexts(manifest: ModuleManifest) -> list[dict[str, object]]: contexts: list[dict[str, object]] = [] seen: set[str] = set() + + def append_context(context_id: str, topic) -> None: + if not context_id or context_id in seen: + return + seen.add(context_id) + contexts.append( + { + "id": context_id, + "topic_id": topic.id, + "title": topic.title, + "documentation_types": list(topic.documentation_types), + } + ) + for topic in manifest.documentation: raw_contexts = topic.metadata.get("help_contexts", ()) if isinstance(raw_contexts, str) or not isinstance(raw_contexts, (list, tuple, set)): continue for raw_context in raw_contexts: context_id = str(raw_context).strip() - if not context_id or context_id in seen: - continue - seen.add(context_id) - contexts.append( - { - "id": context_id, - "topic_id": topic.id, - "title": topic.title, - "documentation_types": list(topic.documentation_types), - } - ) + append_context(context_id, topic) + + frontend = manifest.frontend + if frontend is None or not manifest.documentation: + return contexts + + ordered_topics = sorted(manifest.documentation, key=lambda item: (item.order, item.id)) + + def baseline_topic(documentation_type: str): + return next( + ( + topic + for topic in ordered_topics + if documentation_type in topic.documentation_types + ), + ordered_topics[0], + ) + + user_topic = baseline_topic("user") + admin_topic = baseline_topic("admin") + for route in frontend.routes: + if route.surface_id: + append_context(route.surface_id, user_topic) + for route in frontend.settings_routes: + if route.surface_id: + append_context(route.surface_id, admin_topic) + for item in frontend.nav_items: + if item.surface_id: + append_context(item.surface_id, user_topic) + for surface in frontend.view_surfaces: + topic = admin_topic if ".admin." in surface.id else user_topic + append_context(surface.id, topic) return contexts @@ -253,6 +288,7 @@ def create_platform_router(settings: object | None = None) -> APIRouter: if manifest.architecture is not None else None ), + "information_governance": manifest.information_governance.to_dict(), "external_providers": [ declaration.to_dict() for declaration in manifest.external_providers diff --git a/src/govoplan_core/tenancy/scope.py b/src/govoplan_core/tenancy/scope.py index e50f5da..e6b94b6 100644 --- a/src/govoplan_core/tenancy/scope.py +++ b/src/govoplan_core/tenancy/scope.py @@ -33,7 +33,7 @@ class Tenant: slug: Mapped[str] = mapped_column(String(100), unique=True, nullable=False, index=True) name: Mapped[str] = mapped_column(String(255), nullable=False) description: Mapped[str | None] = mapped_column(Text) - default_locale: Mapped[str] = mapped_column(String(20), default="en", nullable=False) + default_locale: Mapped[str] = mapped_column(String(20), default="de", nullable=False) settings: Mapped[dict[str, Any]] = mapped_column(JSON, default=dict, nullable=False) allow_custom_groups: Mapped[bool | None] = mapped_column(Boolean, nullable=True) allow_custom_roles: Mapped[bool | None] = mapped_column(Boolean, nullable=True) diff --git a/tests/test_database_migrations.py b/tests/test_database_migrations.py index fadd970..8182f96 100644 --- a/tests/test_database_migrations.py +++ b/tests/test_database_migrations.py @@ -184,6 +184,12 @@ class DatabaseMigrationTests(unittest.TestCase): system_settings_count = connection.execute( text("SELECT COUNT(*) FROM core_system_settings WHERE id = 'global'"), ).scalar_one() + default_locale = connection.execute( + text( + "SELECT default_locale FROM core_system_settings " + "WHERE id = 'global'" + ), + ).scalar_one() current = database_migration_heads(connection) self.assertEqual( @@ -204,6 +210,7 @@ class DatabaseMigrationTests(unittest.TestCase): self.assertIn("file_assets", tables) self.assertIn("mail_server_profiles", tables) self.assertEqual(system_settings_count, 1) + self.assertEqual(default_locale, "de") self.assertEqual( role_flags, {"system_owner": True, "system_admin": False, "system_auditor": False}, @@ -211,6 +218,42 @@ class DatabaseMigrationTests(unittest.TestCase): finally: engine.dispose() + def test_german_reference_migration_preserves_explicit_english(self) -> None: + with tempfile.TemporaryDirectory(prefix="govoplan-locale-migration-test-") as directory: + database = Path(directory) / "locale.db" + url = f"sqlite:///{database}" + config = alembic_config(database_url=url, enabled_modules=()) + command.upgrade(config, "f25c9d3e7a01") + + engine = create_engine(url) + try: + with engine.begin() as connection: + connection.execute( + text( + "UPDATE core_system_settings " + "SET default_locale = 'en', " + "updated_at = '2040-01-01 00:00:00' " + "WHERE id = 'global'" + ) + ) + finally: + engine.dispose() + + command.upgrade(config, "heads") + + engine = create_engine(url) + try: + with engine.connect() as connection: + default_locale = connection.execute( + text( + "SELECT default_locale FROM core_system_settings " + "WHERE id = 'global'" + ) + ).scalar_one() + self.assertEqual(default_locale, "en") + finally: + engine.dispose() + def test_default_module_baselines_apply_to_fresh_database(self) -> None: with tempfile.TemporaryDirectory(prefix="govoplan-default-baseline-test-") as directory: database = Path(directory) / "default.db" diff --git a/tests/test_i18n.py b/tests/test_i18n.py new file mode 100644 index 0000000..9e1dbe3 --- /dev/null +++ b/tests/test_i18n.py @@ -0,0 +1,29 @@ +from __future__ import annotations + +import unittest + +from govoplan_core.i18n import ( + REFERENCE_LANGUAGE_CODE, + normalize_enabled_language_codes, + normalize_language_packages, + preferred_language_code, + system_i18n_payload, +) + + +class I18nReferenceLanguageTests(unittest.TestCase): + def test_german_is_the_new_installation_reference_default(self) -> None: + packages = normalize_language_packages() + + self.assertEqual("de", REFERENCE_LANGUAGE_CODE) + self.assertEqual(["de", "en"], [item["code"] for item in packages]) + self.assertEqual( + ["de", "en"], + normalize_enabled_language_codes(None, packages), + ) + self.assertEqual("de", system_i18n_payload(None)["default_language"]) + self.assertEqual("de", preferred_language_code(None, ())) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_information_governance.py b/tests/test_information_governance.py new file mode 100644 index 0000000..8b2de57 --- /dev/null +++ b/tests/test_information_governance.py @@ -0,0 +1,68 @@ +from __future__ import annotations + +import unittest + +from govoplan_core.core.information_governance import ( + InformationGovernanceDeclarationError, + InformationGovernanceDimension, + ModuleInformationGovernance, + information_governance_from_mapping, + information_governance_maturity_issues, +) + + +class InformationGovernanceTests(unittest.TestCase): + def test_default_applies_contract_without_claiming_enforcement(self) -> None: + declaration = ModuleInformationGovernance() + + self.assertTrue(declaration.current_authorization_for_historical_reads) + self.assertEqual( + {"contract_only"}, + {item.adoption for item in declaration.dimensions.values()}, + ) + self.assertEqual("1", declaration.to_dict()["contract_version"]) + + def test_enforced_adoption_requires_evidence(self) -> None: + with self.assertRaisesRegex( + InformationGovernanceDeclarationError, + "requires evidence", + ): + InformationGovernanceDimension(adoption="enforced") + + with self.assertRaisesRegex( + InformationGovernanceDeclarationError, + "covered object types", + ): + InformationGovernanceDimension( + adoption="enforced", + evidence=("tests/test_information_governance.py",), + ) + + def test_catalog_shape_round_trips(self) -> None: + declaration = ModuleInformationGovernance() + + self.assertEqual( + declaration, + information_governance_from_mapping(declaration.to_dict()), + ) + + def test_reference_ready_rejects_unproved_dimensions(self) -> None: + issues = information_governance_maturity_issues( + ModuleInformationGovernance(), + maturity="reference_ready", + ) + + self.assertEqual(1, len(issues)) + self.assertIn("temporal_browsing", issues[0]) + self.assertIn("purpose_aware_access", issues[0]) + self.assertEqual( + (), + information_governance_maturity_issues( + ModuleInformationGovernance(), + maturity="vertical_slice", + ), + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_module_system.py b/tests/test_module_system.py index b707610..f4a655c 100644 --- a/tests/test_module_system.py +++ b/tests/test_module_system.py @@ -106,6 +106,7 @@ from govoplan_core.core.module_package_catalog import ( sign_module_package_catalog, validate_module_package_catalog, ) +from govoplan_core.core.information_governance import ModuleInformationGovernance from govoplan_core.core.modules import DocumentationTopic, FrontendModule, FrontendRoute, MigrationRetirementPlan, ModuleCompatibility, ModuleMigrationTask, ModuleMigrationTaskContext, ModuleMigrationTaskResult, ModuleUninstallGuardResult, PublicFrontendRoute from govoplan_core.core.module_guards import drop_table_retirement_provider from govoplan_core.core.modules import MigrationSpec, ModuleInterfaceProvider, ModuleInterfaceRequirement, ModuleManifest, PermissionDefinition, RoleTemplate @@ -119,7 +120,8 @@ from govoplan_core.security.module_permissions import scopes_grant_compatible from govoplan_core.security.permissions import scope_grants from govoplan_core.server.app import create_app from govoplan_core.server.config import GovoplanServerConfig -from govoplan_core.server.platform import create_platform_router +from govoplan_core.server.platform import _documentation_help_contexts, create_platform_router +from govoplan_core.core.views import ViewSurface from govoplan_core.server.registry import available_module_manifests, build_platform_registry from govoplan_core.server.route_validation import RouteCollisionError from govoplan_core.tenancy.scope import Tenant, create_scope_tables @@ -597,6 +599,70 @@ class ModuleSystemTests(unittest.TestCase): [item["path"] for item in public_module["frontend"]["public_routes"]], ) + def test_declared_surfaces_receive_static_documentation_fallbacks(self) -> None: + manifest = ModuleManifest( + id="example", + name="Example", + version="test", + documentation=( + DocumentationTopic( + id="example.user", + title="Example user guide", + summary="User guide.", + documentation_types=("user",), + order=20, + ), + DocumentationTopic( + id="example.admin", + title="Example administrator guide", + summary="Administrator guide.", + documentation_types=("admin",), + order=10, + ), + ), + frontend=FrontendModule( + module_id="example", + routes=( + FrontendRoute( + path="/example", + component="ExamplePage", + surface_id="example.workspace", + ), + ), + settings_routes=( + FrontendRoute( + path="/admin?section=example", + component="ExampleAdmin", + surface_id="example.admin.settings", + ), + ), + view_surfaces=( + ViewSurface( + id="example.workspace", + module_id="example", + kind="route", + label="Example workspace", + ), + ViewSurface( + id="example.admin.settings", + module_id="example", + kind="section", + label="Example settings", + ), + ), + ), + ) + + contexts = { + item["id"]: item for item in _documentation_help_contexts(manifest) + } + + self.assertEqual("example.user", contexts["example.workspace"]["topic_id"]) + self.assertEqual( + "example.admin", + contexts["example.admin.settings"]["topic_id"], + ) + def test_registry_rejects_duplicate_public_frontend_routes(self) -> None: registry = PlatformRegistry() for module_id in ("first", "second"): @@ -3300,6 +3366,7 @@ finally: "recovery_notes": "Restore rehearsal completed for the release candidate.", "provides_interfaces": [{"name": "files.spaces", "version": "1.2.0"}], "requires_interfaces": [{"name": "access.directory", "version_min": "1.0.0", "optional": True}], + "information_governance": ModuleInformationGovernance().to_dict(), "artifact_integrity": { "python": { "ref": "govoplan-files==0.1.4", @@ -3345,6 +3412,12 @@ finally: [{"name": "access.directory", "optional": True, "version_min": "1.0.0"}], catalog[0]["requires_interfaces"], ) + self.assertEqual( + "contract_only", + catalog[0]["information_governance"]["dimensions"]["retention"][ + "adoption" + ], + ) self.assertEqual("0" * 64, catalog[0]["artifact_integrity"]["python"]["sha256"]) validation = validate_module_package_catalog(catalog_path) diff --git a/tests/test_runtime_agents.py b/tests/test_runtime_agents.py index 13fe561..0588e52 100644 --- a/tests/test_runtime_agents.py +++ b/tests/test_runtime_agents.py @@ -40,6 +40,22 @@ class _Consumer: self.added.append(queue) +def test_api_runtime_identity_uses_the_installed_distribution_version( + monkeypatch, +) -> None: + from govoplan_core.server import default_config + + monkeypatch.setattr( + default_config, + "version", + lambda distribution: "0.1.15" + if distribution == "govoplan-core" + else "unexpected", + ) + + assert default_config._core_distribution_version() == "0.1.15" + + def test_api_runtime_agent_fails_readiness_on_heartbeat_error() -> None: agent = RuntimeNodeAgent( settings=SimpleNamespace( diff --git a/tests/test_wait_for_database.py b/tests/test_wait_for_database.py index 695d04f..d717521 100644 --- a/tests/test_wait_for_database.py +++ b/tests/test_wait_for_database.py @@ -1,12 +1,34 @@ from __future__ import annotations import unittest -from unittest.mock import patch +from types import SimpleNamespace +from unittest.mock import MagicMock, patch from govoplan_core.commands.wait_for_database import main +from govoplan_core.db.migrations import configured_migration_heads class WaitForDatabaseTests(unittest.TestCase): + def test_configured_heads_resolve_cross_branch_dependencies(self) -> None: + scripts = MagicMock() + scripts.get_revisions.return_value = ( + SimpleNamespace(revision="head-b"), + SimpleNamespace(revision="head-a"), + ) + + with ( + patch("govoplan_core.db.migrations.alembic_config"), + patch( + "govoplan_core.db.migrations.ScriptDirectory.from_config", + return_value=scripts, + ), + ): + heads = configured_migration_heads("sqlite://") + + self.assertEqual(("head-a", "head-b"), heads) + scripts.get_revisions.assert_called_once_with("heads") + scripts.get_heads.assert_not_called() + def test_waits_until_exact_configured_heads_are_visible(self) -> None: with ( patch( diff --git a/webui/package-lock.json b/webui/package-lock.json index 345beb3..ed7d112 100644 --- a/webui/package-lock.json +++ b/webui/package-lock.json @@ -1,12 +1,12 @@ { "name": "@govoplan/core-webui", - "version": "0.1.15", + "version": "0.1.16", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@govoplan/core-webui", - "version": "0.1.15", + "version": "0.1.16", "dependencies": { "@govoplan/access-webui": "file:../../govoplan-access/webui", "@govoplan/addresses-webui": "file:../../govoplan-addresses/webui", @@ -73,12 +73,12 @@ }, "../../govoplan-access/webui": { "name": "@govoplan/access-webui", - "version": "0.1.15", + "version": "0.1.16", "devDependencies": { "typescript": "^5.7.2" }, "peerDependencies": { - "@govoplan/core-webui": "^0.1.15", + "@govoplan/core-webui": "^0.1.16", "lucide-react": "^1.23.0", "react": ">=19.2.7 <20", "react-dom": ">=19.2.7 <20", @@ -92,9 +92,9 @@ }, "../../govoplan-addresses/webui": { "name": "@govoplan/addresses-webui", - "version": "0.1.15", + "version": "0.1.16", "peerDependencies": { - "@govoplan/core-webui": "^0.1.15", + "@govoplan/core-webui": "^0.1.16", "lucide-react": "^1.23.0", "react": ">=19.2.7 <20", "react-dom": ">=19.2.7 <20", @@ -108,12 +108,12 @@ }, "../../govoplan-admin/webui": { "name": "@govoplan/admin-webui", - "version": "0.1.15", + "version": "0.1.16", "devDependencies": { "typescript": "^5.7.2" }, "peerDependencies": { - "@govoplan/core-webui": "^0.1.15", + "@govoplan/core-webui": "^0.1.16", "lucide-react": "^1.23.0", "react": ">=19.2.7 <20", "react-dom": ">=19.2.7 <20", @@ -127,9 +127,9 @@ }, "../../govoplan-approvals/webui": { "name": "@govoplan/approvals-webui", - "version": "0.1.15", + "version": "0.1.16", "peerDependencies": { - "@govoplan/core-webui": "^0.1.15", + "@govoplan/core-webui": "^0.1.16", "lucide-react": "^1.23.0", "react": ">=19.2.7 <20", "react-dom": ">=19.2.7 <20" @@ -142,9 +142,9 @@ }, "../../govoplan-audit/webui": { "name": "@govoplan/audit-webui", - "version": "0.1.15", + "version": "0.1.16", "peerDependencies": { - "@govoplan/core-webui": "^0.1.15", + "@govoplan/core-webui": "^0.1.16", "lucide-react": "^1.23.0", "react": ">=19.2.7 <20", "react-dom": ">=19.2.7 <20", @@ -158,9 +158,9 @@ }, "../../govoplan-calendar/webui": { "name": "@govoplan/calendar-webui", - "version": "0.1.15", + "version": "0.1.16", "peerDependencies": { - "@govoplan/core-webui": "^0.1.15", + "@govoplan/core-webui": "^0.1.16", "@vitejs/plugin-react": "^5.2.0", "lucide-react": "^1.23.0", "react": ">=19.2.7 <20", @@ -177,7 +177,7 @@ }, "../../govoplan-campaign/webui": { "name": "@govoplan/campaign-webui", - "version": "0.1.15", + "version": "0.1.16", "dependencies": { "read-excel-file": "9.2.0" }, @@ -185,7 +185,7 @@ "typescript": "^5.7.2" }, "peerDependencies": { - "@govoplan/core-webui": "^0.1.15", + "@govoplan/core-webui": "^0.1.16", "lucide-react": "^1.23.0", "react": ">=19.2.7 <20", "react-dom": ">=19.2.7 <20", @@ -199,9 +199,9 @@ }, "../../govoplan-cases/webui": { "name": "@govoplan/cases-webui", - "version": "0.1.15", + "version": "0.1.16", "peerDependencies": { - "@govoplan/core-webui": "^0.1.15", + "@govoplan/core-webui": "^0.1.16", "lucide-react": "^1.23.0", "react": ">=19.2.7 <20", "react-dom": ">=19.2.7 <20", @@ -215,9 +215,9 @@ }, "../../govoplan-committee/webui": { "name": "@govoplan/committee-webui", - "version": "0.1.15", + "version": "0.1.16", "peerDependencies": { - "@govoplan/core-webui": "^0.1.15", + "@govoplan/core-webui": "^0.1.16", "lucide-react": "^1.23.0", "react": ">=19.2.7 <20", "react-dom": ">=19.2.7 <20", @@ -231,9 +231,9 @@ }, "../../govoplan-dashboard/webui": { "name": "@govoplan/dashboard-webui", - "version": "0.1.15", + "version": "0.1.16", "peerDependencies": { - "@govoplan/core-webui": "^0.1.15", + "@govoplan/core-webui": "^0.1.16", "lucide-react": "^1.23.0", "react": ">=19.2.7 <20", "react-dom": ">=19.2.7 <20", @@ -247,9 +247,9 @@ }, "../../govoplan-dataflow/webui": { "name": "@govoplan/dataflow-webui", - "version": "0.1.15", + "version": "0.1.16", "peerDependencies": { - "@govoplan/core-webui": "^0.1.15", + "@govoplan/core-webui": "^0.1.16", "@xyflow/react": "^12.11.2", "lucide-react": "^1.23.0", "react": ">=19.2.7 <20", @@ -265,9 +265,9 @@ }, "../../govoplan-datasources/webui": { "name": "@govoplan/datasources-webui", - "version": "0.1.15", + "version": "0.1.16", "peerDependencies": { - "@govoplan/core-webui": "^0.1.15", + "@govoplan/core-webui": "^0.1.16", "lucide-react": "^1.23.0", "react": ">=19.2.7 <20", "react-dom": ">=19.2.7 <20", @@ -282,9 +282,9 @@ }, "../../govoplan-dist-lists/webui": { "name": "@govoplan/dist-lists-webui", - "version": "0.1.15", + "version": "0.1.16", "peerDependencies": { - "@govoplan/core-webui": "^0.1.15", + "@govoplan/core-webui": "^0.1.16", "lucide-react": "^1.23.0", "react": ">=19.2.7 <20", "react-dom": ">=19.2.7 <20", @@ -299,9 +299,9 @@ }, "../../govoplan-docs/webui": { "name": "@govoplan/docs-webui", - "version": "0.1.15", + "version": "0.1.16", "peerDependencies": { - "@govoplan/core-webui": "^0.1.15", + "@govoplan/core-webui": "^0.1.16", "@vitejs/plugin-react": "^5.2.0", "lucide-react": "^1.23.0", "react": ">=19.2.7 <20", @@ -318,9 +318,9 @@ }, "../../govoplan-encryption/webui": { "name": "@govoplan/encryption-webui", - "version": "0.1.15", + "version": "0.1.16", "peerDependencies": { - "@govoplan/core-webui": "^0.1.15", + "@govoplan/core-webui": "^0.1.16", "lucide-react": "^1.23.0", "react": ">=19.2.7 <20", "react-dom": ">=19.2.7 <20" @@ -333,9 +333,9 @@ }, "../../govoplan-files/webui": { "name": "@govoplan/files-webui", - "version": "0.1.15", + "version": "0.1.16", "peerDependencies": { - "@govoplan/core-webui": "^0.1.15", + "@govoplan/core-webui": "^0.1.16", "@vitejs/plugin-react": "^5.2.0", "lucide-react": "^1.23.0", "react": ">=19.2.7 <20", @@ -352,9 +352,9 @@ }, "../../govoplan-forms-runtime/webui": { "name": "@govoplan/forms-runtime-webui", - "version": "0.1.15", + "version": "0.1.16", "peerDependencies": { - "@govoplan/core-webui": "^0.1.15", + "@govoplan/core-webui": "^0.1.16", "lucide-react": "^1.23.0", "react": ">=19.2.7 <20", "react-dom": ">=19.2.7 <20", @@ -368,9 +368,9 @@ }, "../../govoplan-forms/webui": { "name": "@govoplan/forms-webui", - "version": "0.1.15", + "version": "0.1.16", "peerDependencies": { - "@govoplan/core-webui": "^0.1.15", + "@govoplan/core-webui": "^0.1.16", "lucide-react": "^1.23.0", "react": ">=19.2.7 <20", "react-dom": ">=19.2.7 <20" @@ -383,9 +383,9 @@ }, "../../govoplan-identity-trust/webui": { "name": "@govoplan/identity-trust-webui", - "version": "0.1.15", + "version": "0.1.16", "peerDependencies": { - "@govoplan/core-webui": "^0.1.15", + "@govoplan/core-webui": "^0.1.16", "lucide-react": "^1.23.0", "react": ">=19.2.7 <20", "react-dom": ">=19.2.7 <20" @@ -398,9 +398,9 @@ }, "../../govoplan-idm/webui": { "name": "@govoplan/idm-webui", - "version": "0.1.15", + "version": "0.1.16", "peerDependencies": { - "@govoplan/core-webui": "^0.1.15", + "@govoplan/core-webui": "^0.1.16", "@vitejs/plugin-react": "^5.2.0", "lucide-react": "^1.23.0", "react": ">=19.2.7 <20", @@ -417,12 +417,12 @@ }, "../../govoplan-mail/webui": { "name": "@govoplan/mail-webui", - "version": "0.1.15", + "version": "0.1.16", "devDependencies": { "typescript": "^5.7.2" }, "peerDependencies": { - "@govoplan/core-webui": "^0.1.15", + "@govoplan/core-webui": "^0.1.16", "lucide-react": "^1.23.0", "react": ">=19.2.7 <20", "react-dom": ">=19.2.7 <20", @@ -436,9 +436,9 @@ }, "../../govoplan-notifications/webui": { "name": "@govoplan/notifications-webui", - "version": "0.1.15", + "version": "0.1.16", "peerDependencies": { - "@govoplan/core-webui": "^0.1.15", + "@govoplan/core-webui": "^0.1.16", "@vitejs/plugin-react": "^5.2.0", "lucide-react": "^1.23.0", "react": ">=19.2.7 <20", @@ -455,9 +455,9 @@ }, "../../govoplan-ops/webui": { "name": "@govoplan/ops-webui", - "version": "0.1.15", + "version": "0.1.16", "peerDependencies": { - "@govoplan/core-webui": "^0.1.15", + "@govoplan/core-webui": "^0.1.16", "@vitejs/plugin-react": "^5.2.0", "lucide-react": "^1.23.0", "react": ">=19.2.7 <20", @@ -474,9 +474,9 @@ }, "../../govoplan-organizations/webui": { "name": "@govoplan/organizations-webui", - "version": "0.1.15", + "version": "0.1.16", "peerDependencies": { - "@govoplan/core-webui": "^0.1.15", + "@govoplan/core-webui": "^0.1.16", "@vitejs/plugin-react": "^5.2.0", "lucide-react": "^1.23.0", "react": ">=19.2.7 <20", @@ -493,9 +493,9 @@ }, "../../govoplan-policy/webui": { "name": "@govoplan/policy-webui", - "version": "0.1.15", + "version": "0.1.16", "peerDependencies": { - "@govoplan/core-webui": "^0.1.15", + "@govoplan/core-webui": "^0.1.16", "lucide-react": "^1.23.0", "react": ">=19.2.7 <20", "react-dom": ">=19.2.7 <20", @@ -509,9 +509,9 @@ }, "../../govoplan-portal/webui": { "name": "@govoplan/portal-webui", - "version": "0.1.15", + "version": "0.1.16", "peerDependencies": { - "@govoplan/core-webui": "^0.1.15", + "@govoplan/core-webui": "^0.1.16", "lucide-react": "^1.23.0", "react": ">=19.2.7 <20", "react-dom": ">=19.2.7 <20", @@ -525,9 +525,9 @@ }, "../../govoplan-postbox/webui": { "name": "@govoplan/postbox-webui", - "version": "0.1.15", + "version": "0.1.16", "peerDependencies": { - "@govoplan/core-webui": "^0.1.15", + "@govoplan/core-webui": "^0.1.16", "lucide-react": "^1.23.0", "react": ">=19.2.7 <20", "react-dom": ">=19.2.7 <20", @@ -541,9 +541,9 @@ }, "../../govoplan-projects/webui": { "name": "@govoplan/projects-webui", - "version": "0.1.15", + "version": "0.1.16", "peerDependencies": { - "@govoplan/core-webui": "^0.1.15", + "@govoplan/core-webui": "^0.1.16", "lucide-react": "^1.23.0", "react": ">=19.2.7 <20", "react-dom": ">=19.2.7 <20", @@ -557,9 +557,9 @@ }, "../../govoplan-reporting/webui": { "name": "@govoplan/reporting-webui", - "version": "0.1.15", + "version": "0.1.16", "peerDependencies": { - "@govoplan/core-webui": "^0.1.15", + "@govoplan/core-webui": "^0.1.16", "lucide-react": "^1.23.0", "react": ">=19.2.7 <20", "react-dom": ">=19.2.7 <20", @@ -573,9 +573,9 @@ }, "../../govoplan-risk-compliance/webui": { "name": "@govoplan/risk-compliance-webui", - "version": "0.1.15", + "version": "0.1.16", "peerDependencies": { - "@govoplan/core-webui": "^0.1.15", + "@govoplan/core-webui": "^0.1.16", "lucide-react": "^1.23.0", "react": ">=19.2.7 <20", "react-dom": ">=19.2.7 <20", @@ -589,9 +589,9 @@ }, "../../govoplan-scheduling/webui": { "name": "@govoplan/scheduling-webui", - "version": "0.1.15", + "version": "0.1.16", "peerDependencies": { - "@govoplan/core-webui": "^0.1.15", + "@govoplan/core-webui": "^0.1.16", "@vitejs/plugin-react": "^5.2.0", "lucide-react": "^1.23.0", "react": ">=19.2.7 <20", @@ -608,9 +608,9 @@ }, "../../govoplan-search/webui": { "name": "@govoplan/search-webui", - "version": "0.1.15", + "version": "0.1.16", "peerDependencies": { - "@govoplan/core-webui": "^0.1.15", + "@govoplan/core-webui": "^0.1.16", "lucide-react": "^1.23.0", "react": ">=19.2.7 <20", "react-dom": ">=19.2.7 <20", @@ -624,9 +624,9 @@ }, "../../govoplan-templates/webui": { "name": "@govoplan/templates-webui", - "version": "0.1.15", + "version": "0.1.16", "peerDependencies": { - "@govoplan/core-webui": "^0.1.15", + "@govoplan/core-webui": "^0.1.16", "lucide-react": "^1.23.0", "react": ">=19.2.7 <20", "react-dom": ">=19.2.7 <20", @@ -641,9 +641,9 @@ }, "../../govoplan-tenancy/webui": { "name": "@govoplan/tenancy-webui", - "version": "0.1.15", + "version": "0.1.16", "peerDependencies": { - "@govoplan/core-webui": "^0.1.15", + "@govoplan/core-webui": "^0.1.16", "lucide-react": "^1.23.0", "react": ">=19.2.7 <20", "react-dom": ">=19.2.7 <20" @@ -656,9 +656,9 @@ }, "../../govoplan-views/webui": { "name": "@govoplan/views-webui", - "version": "0.1.15", + "version": "0.1.16", "peerDependencies": { - "@govoplan/core-webui": "^0.1.15", + "@govoplan/core-webui": "^0.1.16", "lucide-react": "^1.23.0", "react": ">=19.2.7 <20", "react-dom": ">=19.2.7 <20", @@ -672,9 +672,9 @@ }, "../../govoplan-voting/webui": { "name": "@govoplan/voting-webui", - "version": "0.1.15", + "version": "0.1.16", "peerDependencies": { - "@govoplan/core-webui": "^0.1.15", + "@govoplan/core-webui": "^0.1.16", "lucide-react": "^1.23.0", "react": ">=19.2.7 <20", "react-dom": ">=19.2.7 <20" @@ -687,9 +687,9 @@ }, "../../govoplan-workflow/webui": { "name": "@govoplan/workflow-webui", - "version": "0.1.15", + "version": "0.1.16", "peerDependencies": { - "@govoplan/core-webui": "^0.1.15", + "@govoplan/core-webui": "^0.1.16", "@xyflow/react": "^12.11.2", "lucide-react": "^1.23.0", "react": ">=19.2.7 <20", diff --git a/webui/package-lock.release.json b/webui/package-lock.release.json index 66f5c10..b0fc2a5 100644 --- a/webui/package-lock.release.json +++ b/webui/package-lock.release.json @@ -1,26 +1,26 @@ { "name": "@govoplan/core-webui", - "version": "0.1.15", + "version": "0.1.16", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@govoplan/core-webui", - "version": "0.1.15", + "version": "0.1.16", "dependencies": { - "@govoplan/access-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-access.git#v0.1.15", - "@govoplan/admin-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-admin.git#v0.1.15", - "@govoplan/audit-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-audit.git#v0.1.15", - "@govoplan/calendar-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-calendar.git#v0.1.15", - "@govoplan/campaign-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-campaign.git#v0.1.15", - "@govoplan/dashboard-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-dashboard.git#v0.1.15", - "@govoplan/docs-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-docs.git#v0.1.15", - "@govoplan/files-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-files.git#v0.1.15", - "@govoplan/idm-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-idm.git#v0.1.15", - "@govoplan/mail-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-mail.git#v0.1.15", - "@govoplan/ops-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-ops.git#v0.1.15", - "@govoplan/organizations-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-organizations.git#v0.1.15", - "@govoplan/policy-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-policy.git#v0.1.15", + "@govoplan/access-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-access.git#v0.1.16", + "@govoplan/admin-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-admin.git#v0.1.16", + "@govoplan/audit-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-audit.git#v0.1.16", + "@govoplan/calendar-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-calendar.git#v0.1.16", + "@govoplan/campaign-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-campaign.git#v0.1.16", + "@govoplan/dashboard-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-dashboard.git#v0.1.16", + "@govoplan/docs-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-docs.git#v0.1.16", + "@govoplan/files-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-files.git#v0.1.16", + "@govoplan/idm-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-idm.git#v0.1.16", + "@govoplan/mail-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-mail.git#v0.1.16", + "@govoplan/ops-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-ops.git#v0.1.16", + "@govoplan/organizations-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-organizations.git#v0.1.16", + "@govoplan/policy-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-policy.git#v0.1.16", "@tiptap/core": "^3.29.2", "@tiptap/extension-image": "^3.29.2", "@tiptap/pm": "^3.29.2", @@ -753,10 +753,10 @@ "optional": true }, "node_modules/@govoplan/access-webui": { - "version": "0.1.15", - "resolved": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-access.git#794fcf90f8d759eb140d3b5b0a880482912a79be", + "version": "0.1.16", + "resolved": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-access.git#a7ed28f6a3cf9e3f5f549db9b0e0785910a8c41a", "peerDependencies": { - "@govoplan/core-webui": "^0.1.15", + "@govoplan/core-webui": "^0.1.16", "lucide-react": "^1.23.0", "react": ">=19.2.7 <20", "react-dom": ">=19.2.7 <20", @@ -769,10 +769,10 @@ } }, "node_modules/@govoplan/admin-webui": { - "version": "0.1.15", - "resolved": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-admin.git#57e2a34c894e20afa46558b83d167e36a1db26c0", + "version": "0.1.16", + "resolved": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-admin.git#3842987a4ed2b550a3e3c6932db52fa09995e5b4", "peerDependencies": { - "@govoplan/core-webui": "^0.1.15", + "@govoplan/core-webui": "^0.1.16", "lucide-react": "^1.23.0", "react": ">=19.2.7 <20", "react-dom": ">=19.2.7 <20", @@ -785,10 +785,10 @@ } }, "node_modules/@govoplan/audit-webui": { - "version": "0.1.15", - "resolved": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-audit.git#6f095eb56293fa4bc3f3ca5a52d36491c1713b00", + "version": "0.1.16", + "resolved": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-audit.git#4613b54c06863689eade1012632bd2398d15fef1", "peerDependencies": { - "@govoplan/core-webui": "^0.1.15", + "@govoplan/core-webui": "^0.1.16", "lucide-react": "^1.23.0", "react": ">=19.2.7 <20", "react-dom": ">=19.2.7 <20", @@ -801,10 +801,10 @@ } }, "node_modules/@govoplan/calendar-webui": { - "version": "0.1.15", - "resolved": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-calendar.git#f6faaf196da1891997c5ad7065bbba69aacd018d", + "version": "0.1.16", + "resolved": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-calendar.git#128c78597edbc6a6fcac844fe1c08b2f9917a872", "peerDependencies": { - "@govoplan/core-webui": "^0.1.15", + "@govoplan/core-webui": "^0.1.16", "@vitejs/plugin-react": "^5.2.0", "lucide-react": "^1.23.0", "react": ">=19.2.7 <20", @@ -820,13 +820,13 @@ } }, "node_modules/@govoplan/campaign-webui": { - "version": "0.1.15", - "resolved": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-campaign.git#ea0efe661f10474e0a8df0bffde75f3a78577f27", + "version": "0.1.16", + "resolved": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-campaign.git#2db99eaf6a36b433093d37f53cef0125f4b98599", "dependencies": { "read-excel-file": "9.2.0" }, "peerDependencies": { - "@govoplan/core-webui": "^0.1.15", + "@govoplan/core-webui": "^0.1.16", "lucide-react": "^1.23.0", "react": ">=19.2.7 <20", "react-dom": ">=19.2.7 <20", @@ -839,10 +839,10 @@ } }, "node_modules/@govoplan/dashboard-webui": { - "version": "0.1.15", - "resolved": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-dashboard.git#c2c7ba8fb2090ea0e6abcb2f3cd46916a9016b1e", + "version": "0.1.16", + "resolved": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-dashboard.git#77924e7a2c8366be39c9305477e085802a578d0b", "peerDependencies": { - "@govoplan/core-webui": "^0.1.15", + "@govoplan/core-webui": "^0.1.16", "lucide-react": "^1.23.0", "react": ">=19.2.7 <20", "react-dom": ">=19.2.7 <20", @@ -855,10 +855,10 @@ } }, "node_modules/@govoplan/docs-webui": { - "version": "0.1.15", - "resolved": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-docs.git#1f5050530589da651a49e8d617bf0825e6332388", + "version": "0.1.16", + "resolved": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-docs.git#8cbab781bf9126062efc6127c89c7eed308955de", "peerDependencies": { - "@govoplan/core-webui": "^0.1.15", + "@govoplan/core-webui": "^0.1.16", "@vitejs/plugin-react": "^5.2.0", "lucide-react": "^1.23.0", "react": ">=19.2.7 <20", @@ -874,10 +874,10 @@ } }, "node_modules/@govoplan/files-webui": { - "version": "0.1.15", - "resolved": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-files.git#0293e49defd34a958584c9665ba006d22db497e1", + "version": "0.1.16", + "resolved": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-files.git#edee29af51fba34c569defc98a977a0ed80c5718", "peerDependencies": { - "@govoplan/core-webui": "^0.1.15", + "@govoplan/core-webui": "^0.1.16", "@vitejs/plugin-react": "^5.2.0", "lucide-react": "^1.23.0", "react": ">=19.2.7 <20", @@ -893,10 +893,10 @@ } }, "node_modules/@govoplan/idm-webui": { - "version": "0.1.15", - "resolved": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-idm.git#bd72a3f277ce4f214906e276ee1553f4fd23364d", + "version": "0.1.16", + "resolved": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-idm.git#6e9393c9c7606e91ced908dceef69fd18cf2abe7", "peerDependencies": { - "@govoplan/core-webui": "^0.1.15", + "@govoplan/core-webui": "^0.1.16", "@vitejs/plugin-react": "^5.2.0", "lucide-react": "^1.23.0", "react": ">=19.2.7 <20", @@ -912,10 +912,10 @@ } }, "node_modules/@govoplan/mail-webui": { - "version": "0.1.15", - "resolved": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-mail.git#622f63b4b5fd427128b1a6a9df0ecbe357059d69", + "version": "0.1.16", + "resolved": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-mail.git#a040de4fe5d9af4a5f434a87d2c5fa8b79e6a818", "peerDependencies": { - "@govoplan/core-webui": "^0.1.15", + "@govoplan/core-webui": "^0.1.16", "lucide-react": "^1.23.0", "react": ">=19.2.7 <20", "react-dom": ">=19.2.7 <20", @@ -928,10 +928,10 @@ } }, "node_modules/@govoplan/ops-webui": { - "version": "0.1.15", - "resolved": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-ops.git#ce074044056827f253eaa9bae920136ebdd8ada8", + "version": "0.1.16", + "resolved": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-ops.git#02e5d91d565bf7d42cc1b550f18e73621a93a6fa", "peerDependencies": { - "@govoplan/core-webui": "^0.1.15", + "@govoplan/core-webui": "^0.1.16", "@vitejs/plugin-react": "^5.2.0", "lucide-react": "^1.23.0", "react": ">=19.2.7 <20", @@ -947,10 +947,10 @@ } }, "node_modules/@govoplan/organizations-webui": { - "version": "0.1.15", - "resolved": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-organizations.git#aab7f25e866b9738390e2d3026e784d3ad9c3a54", + "version": "0.1.16", + "resolved": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-organizations.git#18e4df348a3cf69602b2d2491e6ae28c26551b59", "peerDependencies": { - "@govoplan/core-webui": "^0.1.15", + "@govoplan/core-webui": "^0.1.16", "@vitejs/plugin-react": "^5.2.0", "lucide-react": "^1.23.0", "react": ">=19.2.7 <20", @@ -966,10 +966,10 @@ } }, "node_modules/@govoplan/policy-webui": { - "version": "0.1.15", - "resolved": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-policy.git#44d72914aea27a74511fd50f445c450217e3cd5f", + "version": "0.1.16", + "resolved": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-policy.git#107cc3b654b7b97ec441650bcc3bac6a7accc8a0", "peerDependencies": { - "@govoplan/core-webui": "^0.1.15", + "@govoplan/core-webui": "^0.1.16", "lucide-react": "^1.23.0", "react": ">=19.2.7 <20", "react-dom": ">=19.2.7 <20", @@ -2059,9 +2059,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.400", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.400.tgz", - "integrity": "sha512-96EWDNjM59SYflgeV5Ylsf4EMiq1a25YjCnJH7cxn/AF2H3pILRweaUnoLax0yKHWdpOzY6JKEu45e8irqZIHA==", + "version": "1.5.401", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.401.tgz", + "integrity": "sha512-H6ViHN68nGYlChEvlIU67fn8O2/tpbWQPwck98yaJmh+08LSvHiydzDQ6oXNccLU3kNRVIRS9A4mA7CG+i6fLQ==", "license": "ISC" }, "node_modules/esbuild": { @@ -2330,9 +2330,9 @@ } }, "node_modules/prosemirror-commands": { - "version": "1.7.1", - "resolved": "https://registry.npmjs.org/prosemirror-commands/-/prosemirror-commands-1.7.1.tgz", - "integrity": "sha512-rT7qZnQtx5c0/y/KlYaGvtG411S97UaL6gdp6RIZ23DLHanMYLyfGBV5DtSnZdthQql7W+lEVbpSfwtO8T+L2w==", + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/prosemirror-commands/-/prosemirror-commands-1.7.2.tgz", + "integrity": "sha512-q6Q6szxqdu9Xd6EcdKsqXghu5nQdZTpB4Q9yd04WRc7/jt763e/rT60Owh0L1GYY+T46o5rD+9lEN36dZS43tw==", "license": "MIT", "dependencies": { "prosemirror-model": "^1.0.0", diff --git a/webui/package.json b/webui/package.json index a891d99..0d6946b 100644 --- a/webui/package.json +++ b/webui/package.json @@ -1,6 +1,6 @@ { "name": "@govoplan/core-webui", - "version": "0.1.15", + "version": "0.1.16", "private": true, "type": "module", "main": "src/index.ts", diff --git a/webui/package.release.json b/webui/package.release.json index 4682df3..0cdd5bc 100644 --- a/webui/package.release.json +++ b/webui/package.release.json @@ -1,6 +1,6 @@ { "name": "@govoplan/core-webui", - "version": "0.1.15", + "version": "0.1.16", "private": true, "type": "module", "main": "src/index.ts", @@ -26,19 +26,19 @@ "preview": "vite preview --host 127.0.0.1 --port 4173" }, "dependencies": { - "@govoplan/access-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-access.git#v0.1.15", - "@govoplan/admin-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-admin.git#v0.1.15", - "@govoplan/audit-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-audit.git#v0.1.15", - "@govoplan/calendar-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-calendar.git#v0.1.15", - "@govoplan/dashboard-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-dashboard.git#v0.1.15", - "@govoplan/docs-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-docs.git#v0.1.15", - "@govoplan/files-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-files.git#v0.1.15", - "@govoplan/idm-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-idm.git#v0.1.15", - "@govoplan/mail-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-mail.git#v0.1.15", - "@govoplan/campaign-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-campaign.git#v0.1.15", - "@govoplan/organizations-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-organizations.git#v0.1.15", - "@govoplan/ops-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-ops.git#v0.1.15", - "@govoplan/policy-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-policy.git#v0.1.15", + "@govoplan/access-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-access.git#v0.1.16", + "@govoplan/admin-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-admin.git#v0.1.16", + "@govoplan/audit-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-audit.git#v0.1.16", + "@govoplan/calendar-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-calendar.git#v0.1.16", + "@govoplan/dashboard-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-dashboard.git#v0.1.16", + "@govoplan/docs-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-docs.git#v0.1.16", + "@govoplan/files-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-files.git#v0.1.16", + "@govoplan/idm-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-idm.git#v0.1.16", + "@govoplan/mail-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-mail.git#v0.1.16", + "@govoplan/campaign-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-campaign.git#v0.1.16", + "@govoplan/organizations-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-organizations.git#v0.1.16", + "@govoplan/ops-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-ops.git#v0.1.16", + "@govoplan/policy-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-policy.git#v0.1.16", "@tiptap/core": "^3.29.2", "@tiptap/extension-image": "^3.29.2", "@tiptap/pm": "^3.29.2", diff --git a/webui/src/App.tsx b/webui/src/App.tsx index d193313..e220142 100644 --- a/webui/src/App.tsx +++ b/webui/src/App.tsx @@ -644,7 +644,7 @@ function normalizeAuthInfo(response: AuthPayload): AuthInfo { principal, available_languages: response.available_languages, enabled_language_codes: profileLoaded ? response.enabled_language_codes ?? activeTenant.enabled_language_codes ?? [] : undefined, - default_language: profileLoaded ? response.default_language ?? user.preferred_language ?? activeTenant.default_locale ?? "en" : undefined, + default_language: profileLoaded ? response.default_language ?? user.preferred_language ?? activeTenant.default_locale ?? "de" : undefined, profile_loaded: profileLoaded, roles_loaded: rolesLoaded, groups_loaded: groupsLoaded diff --git a/webui/src/i18n/LanguageContext.tsx b/webui/src/i18n/LanguageContext.tsx index 18ad913..7c6aac7 100644 --- a/webui/src/i18n/LanguageContext.tsx +++ b/webui/src/i18n/LanguageContext.tsx @@ -32,14 +32,15 @@ type PlatformLanguageProviderProps = { const STORAGE_KEY = "govoplan.language"; const PRIMARY_LANGUAGE = "en"; +const REFERENCE_LANGUAGE = "de"; const I18N_MESSAGE_SEPARATOR = "::"; const TRANSLATABLE_ATTRIBUTES = ["aria-label", "alt", "placeholder", "title"]; const SKIP_TRANSLATION_SELECTOR = "script, style, code, pre, textarea, [data-i18n-skip]"; const textNodeOriginals = new WeakMap(); export const DEFAULT_AVAILABLE_LANGUAGES: PlatformLanguage[] = [ -{ code: "en", label: "i18n:govoplan-core.english.649df08a", nativeLabel: "i18n:govoplan-core.language_native_english" }, -{ code: "de", label: "i18n:govoplan-core.german.da91388c", nativeLabel: "i18n:govoplan-core.language_native_german" }]; +{ code: "de", label: "i18n:govoplan-core.german.da91388c", nativeLabel: "i18n:govoplan-core.language_native_german" }, +{ code: "en", label: "i18n:govoplan-core.english.649df08a", nativeLabel: "i18n:govoplan-core.language_native_english" }]; export const DEFAULT_TRANSLATIONS: PlatformTranslations = { @@ -186,13 +187,17 @@ function languagesForCodes(languages: PlatformLanguage[], codes?: string[] | nul function preferredLanguage(languages: PlatformLanguage[], preferred?: string | null): string { const codes = new Set(languages.map((item) => item.code)); - const requested = normalizeLanguageCode(preferred ?? storedLanguage() ?? browserLanguage()); - if (codes.has(requested)) return requested; - const primary = primaryLanguageCode(requested); - const primaryMatch = languages.find((item) => primaryLanguageCode(item.code) === primary); - if (primaryMatch) return primaryMatch.code; + const requestedValue = preferred ?? storedLanguage() ?? browserLanguage(); + if (requestedValue) { + const requested = normalizeLanguageCode(requestedValue); + if (codes.has(requested)) return requested; + const primary = primaryLanguageCode(requested); + const primaryMatch = languages.find((item) => primaryLanguageCode(item.code) === primary); + if (primaryMatch) return primaryMatch.code; + } + if (codes.has(REFERENCE_LANGUAGE)) return REFERENCE_LANGUAGE; if (codes.has(PRIMARY_LANGUAGE)) return PRIMARY_LANGUAGE; - return languages[0]?.code ?? PRIMARY_LANGUAGE; + return languages[0]?.code ?? REFERENCE_LANGUAGE; } function primaryLanguageCode(value: string): string { diff --git a/webui/src/types.ts b/webui/src/types.ts index 8196df4..930a605 100644 --- a/webui/src/types.ts +++ b/webui/src/types.ts @@ -1124,6 +1124,16 @@ export type PlatformModuleInfo = { target_tested_providers: string[]; documentation: Record; } | null; + information_governance?: { + contract_version: string; + current_authorization_for_historical_reads: boolean; + dimensions: Record; + }; external_providers?: Array<{ contract_version: string; id: string;