65 Commits
Author SHA1 Message Date
zemion 0b171fbdd4 feat: gate infrastructure changes on provider inventory
Security Audit / security-audit (push) Failing after 12m3s
Developer Meta-package Release / publish-package (push) Successful in 9s
Dependency Audit / dependency-audit (push) Successful in 1m43s
Deployment Installer / deployment-installer (push) Successful in 6s
2026-08-24 15:18:38 +02:00
zemion ed6790c057 Record resident permit browser evidence
Dependency Audit / dependency-audit (push) Successful in 1m41s
Deployment Installer / deployment-installer (push) Successful in 6s
Security Audit / security-audit (push) Failing after 12m6s
Developer Meta-package Release / publish-package (push) Successful in 9s
2026-08-24 14:03:57 +02:00
zemion 3766e26377 feat: publish stable product surface composition
Dependency Audit / dependency-audit (push) Successful in 1m43s
Deployment Installer / deployment-installer (push) Successful in 6s
Security Audit / security-audit (push) Failing after 12m29s
Developer Meta-package Release / publish-package (push) Successful in 10s
2026-08-24 13:41:00 +02:00
zemion 6c2b36af0f feat(inventory): enforce high-risk contextual help
Deployment Installer / deployment-installer (push) Successful in 6s
Security Audit / security-audit (push) Failing after 12m18s
Developer Meta-package Release / publish-package (push) Successful in 10s
Dependency Audit / dependency-audit (push) Successful in 1m40s
2026-08-24 11:40:21 +02:00
zemion 3f75ca8e48 chore: compose German documentation releases
Dependency Audit / dependency-audit (push) Successful in 1m37s
Deployment Installer / deployment-installer (push) Successful in 5s
Security Audit / security-audit (push) Failing after 11m47s
2026-08-24 01:47:06 +02:00
zemion a886a9b3de chore(release): compose complete German coverage
Deployment Installer / deployment-installer (push) Successful in 6s
Dependency Audit / dependency-audit (push) Successful in 1m48s
Security Audit / security-audit (push) Failing after 12m0s
2026-08-23 21:31:07 +02:00
zemion fe83290d56 chore(release): compose expanded German coverage
Dependency Audit / dependency-audit (push) Successful in 1m48s
Deployment Installer / deployment-installer (push) Successful in 7s
Security Audit / security-audit (push) Failing after 12m5s
2026-08-23 20:51:28 +02:00
zemion c50f699399 chore(release): compose German reference coverage
Dependency Audit / dependency-audit (push) Successful in 1m45s
Deployment Installer / deployment-installer (push) Successful in 6s
Security Audit / security-audit (push) Failing after 11m55s
2026-08-23 19:57:05 +02:00
zemion 59b45a0829 chore(release): compose autonomous integration contracts
Dependency Audit / dependency-audit (push) Successful in 1m48s
Deployment Installer / deployment-installer (push) Successful in 6s
Security Audit / security-audit (push) Failing after 11m57s
2026-08-23 18:14:11 +02:00
zemion 861abcc573 chore(release): compose integration foundations
Dependency Audit / dependency-audit (push) Successful in 1m46s
Deployment Installer / deployment-installer (push) Successful in 6s
Security Audit / security-audit (push) Failing after 11m53s
2026-08-23 11:21:04 +02:00
zemion 5e995fed88 chore(release): compose German documentation batch
Dependency Audit / dependency-audit (push) Successful in 1m37s
Deployment Installer / deployment-installer (push) Successful in 6s
Security Audit / security-audit (push) Failing after 11m4s
2026-08-23 02:14:42 +02:00
zemion 41ca242004 chore(release): compose Reporting and Search 0.1.19
Dependency Audit / dependency-audit (push) Successful in 1m41s
Deployment Installer / deployment-installer (push) Successful in 6s
Security Audit / security-audit (push) Failing after 11m51s
2026-08-22 21:14:24 +02:00
zemion 85caa8d337 chore(release): integrate structured documentation localization
Dependency Audit / dependency-audit (push) Successful in 1m45s
Deployment Installer / deployment-installer (push) Successful in 6s
Security Audit / security-audit (push) Failing after 12m2s
Developer Meta-package Release / publish-package (push) Successful in 10s
2026-08-22 20:45:41 +02:00
zemion 64640327ae chore(release): pin datasource lifecycle governance
Dependency Audit / dependency-audit (push) Successful in 1m47s
Security Audit / security-audit (push) Failing after 11m49s
Deployment Installer / deployment-installer (push) Successful in 6s
Developer Meta-package Release / publish-package (push) Successful in 9s
2026-08-22 19:37:54 +02:00
zemion cc7c2a91ee chore(release): pin Records 0.1.20
Deployment Installer / deployment-installer (push) Successful in 5s
Dependency Audit / dependency-audit (push) Successful in 1m44s
Security Audit / security-audit (push) Failing after 11m51s
2026-08-22 18:42:15 +02:00
zemion 7c92565d9d Compose v0.1.35 resident permit configuration package
Dependency Audit / dependency-audit (push) Successful in 1m44s
Developer Meta-package Release / publish-package (push) Successful in 9s
Deployment Installer / deployment-installer (push) Successful in 6s
Security Audit / security-audit (push) Failing after 11m59s
2026-08-22 18:05:58 +02:00
zemion 23bfe5e2f8 Compose Core v0.1.34 and Mail v0.1.22
Dependency Audit / dependency-audit (push) Successful in 1m46s
Deployment Installer / deployment-installer (push) Successful in 6s
Security Audit / security-audit (push) Failing after 11m54s
Developer Meta-package Release / publish-package (push) Successful in 9s
2026-08-22 17:30:27 +02:00
zemion cf2f7f6890 Compose Core v0.1.33 and Connectors v0.1.22
Dependency Audit / dependency-audit (push) Successful in 1m44s
Deployment Installer / deployment-installer (push) Successful in 7s
Security Audit / security-audit (push) Failing after 12m8s
2026-08-22 16:31:58 +02:00
zemion 23b601bc0d build: compose external knowledge connector slice
Security Audit / security-audit (push) Failing after 12m23s
Dependency Audit / dependency-audit (push) Successful in 1m45s
Deployment Installer / deployment-installer (push) Successful in 6s
2026-08-22 14:44:55 +02:00
zemion 99c52c2153 build: compose governed Wiki vertical slice
Dependency Audit / dependency-audit (push) Successful in 1m44s
Deployment Installer / deployment-installer (push) Successful in 6s
Security Audit / security-audit (push) Failing after 12m20s
2026-08-22 13:33:31 +02:00
zemion ca68d98806 build: release ticket operations vertical slice
Dependency Audit / dependency-audit (push) Successful in 1m40s
Deployment Installer / deployment-installer (push) Successful in 6s
Security Audit / security-audit (push) Failing after 11m44s
2026-08-22 12:24:55 +02:00
zemion 79c4cb067a build: release localized documentation batch
Dependency Audit / dependency-audit (push) Successful in 1m39s
Deployment Installer / deployment-installer (push) Successful in 6s
Security Audit / security-audit (push) Failing after 11m41s
2026-08-22 08:45:51 +02:00
zemion cd498dc1d8 build: release seed documentation coverage batch
Dependency Audit / dependency-audit (push) Successful in 1m40s
Deployment Installer / deployment-installer (push) Successful in 6s
Security Audit / security-audit (push) Failing after 11m35s
2026-08-22 08:12:04 +02:00
zemion e07b3487e3 build: release documentation coverage batch
Dependency Audit / dependency-audit (push) Successful in 1m41s
Deployment Installer / deployment-installer (push) Successful in 6s
Security Audit / security-audit (push) Failing after 11m23s
2026-08-22 07:40:10 +02:00
zemion 72279de2c0 chore: sync Tasks and Postbox documentation releases
Dependency Audit / dependency-audit (push) Successful in 1m42s
Deployment Installer / deployment-installer (push) Successful in 6s
Security Audit / security-audit (push) Failing after 11m43s
2026-08-22 07:07:49 +02:00
zemion 88543ab115 chore: sync REST and SOAP reference releases
Dependency Audit / dependency-audit (push) Successful in 1m40s
Deployment Installer / deployment-installer (push) Successful in 6s
Security Audit / security-audit (push) Failing after 11m18s
2026-08-22 06:38:20 +02:00
zemion 81fe0f4680 chore: sync Docs German reference release
Dependency Audit / dependency-audit (push) Successful in 1m36s
Deployment Installer / deployment-installer (push) Successful in 6s
Security Audit / security-audit (push) Failing after 11m3s
2026-08-22 06:12:49 +02:00
zemion fe784cc562 chore: sync Mail German help release
Dependency Audit / dependency-audit (push) Successful in 1m36s
Deployment Installer / deployment-installer (push) Successful in 6s
Security Audit / security-audit (push) Failing after 11m0s
2026-08-22 05:45:12 +02:00
zemion 9fe7ad2cb4 chore: sync Mail POP3 legacy import release
Dependency Audit / dependency-audit (push) Successful in 1m31s
Deployment Installer / deployment-installer (push) Successful in 5s
Security Audit / security-audit (push) Failing after 11m3s
2026-08-22 05:09:01 +02:00
zemion f1eebd849c chore: sync Campaign portable transfer release
Dependency Audit / dependency-audit (push) Successful in 1m35s
Deployment Installer / deployment-installer (push) Successful in 6s
Security Audit / security-audit (push) Failing after 10m52s
2026-08-22 03:58:31 +02:00
zemion 4eb90079d5 chore: sync IDM governance releases
Dependency Audit / dependency-audit (push) Successful in 1m36s
Deployment Installer / deployment-installer (push) Successful in 6s
Security Audit / security-audit (push) Failing after 10m55s
Developer Meta-package Release / publish-package (push) Successful in 8s
2026-08-22 03:30:25 +02:00
zemion 628714804b chore: sync Campaign workflow hand-off releases
Dependency Audit / dependency-audit (push) Successful in 1m37s
Deployment Installer / deployment-installer (push) Successful in 7s
Security Audit / security-audit (push) Failing after 11m13s
2026-08-22 02:35:25 +02:00
zemion ff9fa37a88 chore: sync Campaign work assignment release
Dependency Audit / dependency-audit (push) Successful in 1m35s
Deployment Installer / deployment-installer (push) Successful in 6s
Security Audit / security-audit (push) Failing after 10m59s
2026-08-22 01:30:26 +02:00
zemion 4c7552f0dd chore: sync Campaign collaboration release
Dependency Audit / dependency-audit (push) Successful in 1m36s
Deployment Installer / deployment-installer (push) Successful in 7s
Security Audit / security-audit (push) Failing after 11m27s
2026-08-22 00:20:56 +02:00
zemion a20e02291d chore: sync Files folder sync release
Dependency Audit / dependency-audit (push) Successful in 1m39s
Deployment Installer / deployment-installer (push) Successful in 5s
Security Audit / security-audit (push) Failing after 11m32s
2026-08-21 23:08:35 +02:00
zemion b6452c6f53 chore: sync IDM relationship administration release
Security Audit / security-audit (push) Failing after 11m22s
Dependency Audit / dependency-audit (push) Successful in 1m41s
Deployment Installer / deployment-installer (push) Successful in 6s
2026-08-21 22:06:29 +02:00
zemion 75103d49af chore: sync Access credential help release
Dependency Audit / dependency-audit (push) Successful in 1m42s
Deployment Installer / deployment-installer (push) Successful in 6s
Security Audit / security-audit (push) Failing after 11m33s
2026-08-21 21:35:56 +02:00
zemion a60b8b0752 chore: sync datasource visibility release
Dependency Audit / dependency-audit (push) Successful in 1m43s
Deployment Installer / deployment-installer (push) Successful in 6s
Security Audit / security-audit (push) Failing after 11m27s
2026-08-21 20:52:01 +02:00
zemion 7f2f896a0f chore: sync governed tabular origins release
Dependency Audit / dependency-audit (push) Successful in 1m45s
Deployment Installer / deployment-installer (push) Successful in 6s
Security Audit / security-audit (push) Failing after 12m11s
2026-08-21 19:30:01 +02:00
zemion 60e04a324c chore: sync Dataflow reusable definitions release
Dependency Audit / dependency-audit (push) Successful in 1m45s
Deployment Installer / deployment-installer (push) Successful in 6s
Security Audit / security-audit (push) Failing after 12m1s
2026-08-21 18:44:09 +02:00
zemion 6a74e53a1c chore: recognize authored help contexts and sync packages
Dependency Audit / dependency-audit (push) Successful in 1m44s
Deployment Installer / deployment-installer (push) Successful in 6s
Security Audit / security-audit (push) Failing after 11m33s
2026-08-21 17:36:24 +02:00
zemion 6517b6ac27 chore: synchronize autonomous slice release evidence
Dependency Audit / dependency-audit (push) Successful in 1m43s
Deployment Installer / deployment-installer (push) Successful in 7s
Security Audit / security-audit (push) Failing after 11m29s
2026-08-21 16:35:21 +02:00
zemion 26a66814b4 test(privacy): enforce workspace DSAR coverage
Dependency Audit / dependency-audit (push) Successful in 1m45s
Deployment Installer / deployment-installer (push) Successful in 6s
Security Audit / security-audit (push) Failing after 11m42s
2026-08-21 13:59:56 +02:00
zemion d08f9f0f2d chore: classify audit evidence lifecycle endpoint
Dependency Audit / dependency-audit (push) Successful in 1m49s
Deployment Installer / deployment-installer (push) Successful in 7s
Security Audit / security-audit (push) Failing after 11m51s
2026-08-20 18:00:21 +02:00
zemion 47c90400af chore: classify governed administration endpoints 2026-08-20 13:00:59 +02:00
zemion 5d4535f7b5 feat: generate evidence-based fit assessments 2026-08-20 12:58:53 +02:00
zemion 83ccb7f198 docs: expose scheduling enrollment policy defaults 2026-08-20 11:45:07 +02:00
zemion f407419d25 chore(inventory): declare Postbox client transform endpoint
Dependency Audit / dependency-audit (push) Successful in 1m35s
Deployment Installer / deployment-installer (push) Successful in 6s
Security Audit / security-audit (push) Successful in 10m33s
2026-08-20 03:42:58 +02:00
zemion e88dceb639 chore(webui): enforce semantic interface patterns
Dependency Audit / dependency-audit (push) Successful in 1m46s
Deployment Installer / deployment-installer (push) Successful in 6s
Security Audit / security-audit (push) Successful in 11m24s
2026-08-19 18:47:45 +02:00
zemion ef8fd45457 Enforce semantic page layout contracts
Dependency Audit / dependency-audit (push) Successful in 1m46s
Deployment Installer / deployment-installer (push) Successful in 7s
Security Audit / security-audit (push) Successful in 12m5s
2026-08-19 14:26:25 +02:00
zemion d0ff2f1510 Integrate Payments operator WebUI
Dependency Audit / dependency-audit (push) Successful in 1m54s
Deployment Installer / deployment-installer (push) Successful in 7s
Security Audit / security-audit (push) Successful in 11m59s
2026-08-19 13:33:51 +02:00
zemion f8b06887d2 feat: extend resident permit service journey
Dependency Audit / dependency-audit (push) Successful in 1m46s
Deployment Installer / deployment-installer (push) Successful in 9s
Security Audit / security-audit (push) Failing after 1s
2026-08-19 12:33:34 +02:00
zemion 69519a92b4 feat: prove assisted resident permit intake
Dependency Audit / dependency-audit (push) Successful in 1m40s
Deployment Installer / deployment-installer (push) Successful in 6s
Security Audit / security-audit (push) Successful in 10m37s
2026-08-19 02:45:53 +02:00
zemion e2f505eeab feat: enforce shared UI foundations and pin reference journey
Dependency Audit / dependency-audit (push) Successful in 1m57s
Deployment Installer / deployment-installer (push) Successful in 6s
Security Audit / security-audit (push) Successful in 12m24s
2026-08-18 21:32:25 +02:00
zemion a1b80eda27 Enforce the shared WebUI pattern language
Dependency Audit / dependency-audit (push) Successful in 1m43s
Deployment Installer / deployment-installer (push) Successful in 6s
Security Audit / security-audit (push) Successful in 11m17s
2026-08-18 13:17:22 +02:00
zemion 5bb8028147 Enforce shared metric and description layouts
Dependency Audit / dependency-audit (push) Successful in 1m39s
Deployment Installer / deployment-installer (push) Successful in 6s
Security Audit / security-audit (push) Successful in 11m3s
2026-08-18 11:30:39 +02:00
zemion 5efb0eea6f Enforce shared WebUI primitive adoption
Dependency Audit / dependency-audit (push) Successful in 1m38s
Deployment Installer / deployment-installer (push) Successful in 6s
Security Audit / security-audit (push) Successful in 10m55s
2026-08-18 10:42:55 +02:00
zemion 5e9234d4b6 chore: enforce shared workspace layouts
Dependency Audit / dependency-audit (push) Successful in 1m35s
Deployment Installer / deployment-installer (push) Successful in 6s
Security Audit / security-audit (push) Successful in 10m19s
2026-08-18 02:17:24 +02:00
zemion b76581a89a chore: enforce shared WebUI layouts
Dependency Audit / dependency-audit (push) Successful in 1m36s
Deployment Installer / deployment-installer (push) Successful in 6s
Security Audit / security-audit (push) Successful in 10m29s
2026-08-18 01:03:33 +02:00
zemion bec62f38d1 docs: refresh strategy status evidence
Dependency Audit / dependency-audit (push) Successful in 1m49s
Deployment Installer / deployment-installer (push) Successful in 6s
Security Audit / security-audit (push) Successful in 11m32s
2026-08-17 16:58:30 +02:00
zemion c66e1b768d docs: organize cross-product documentation
Dependency Audit / dependency-audit (push) Successful in 1m46s
Deployment Installer / deployment-installer (push) Successful in 9s
Security Audit / security-audit (push) Successful in 11m48s
2026-08-17 16:52:51 +02:00
zemion 209a43592f chore: declare new API endpoint surfaces
Dependency Audit / dependency-audit (push) Successful in 1m48s
Deployment Installer / deployment-installer (push) Successful in 6s
Security Audit / security-audit (push) Successful in 11m54s
2026-08-07 14:54:09 +02:00
zemion 50b81c9ca7 docs: describe receipt-bound module configuration
Dependency Audit / dependency-audit (push) Successful in 1m50s
Deployment Installer / deployment-installer (push) Successful in 7s
Security Audit / security-audit (push) Successful in 12m19s
2026-08-07 11:15:50 +02:00
zemion 612a44bc8e feat(deploy): project infrastructure capabilities
Dependency Audit / dependency-audit (push) Successful in 1m44s
Deployment Installer / deployment-installer (push) Successful in 6s
Security Audit / security-audit (push) Successful in 11m43s
2026-08-07 01:59:45 +02:00
zemion dce725636d feat(release): disclose module permissions in catalog 2026-08-07 01:59:18 +02:00
92 changed files with 7094 additions and 515 deletions
+2
View File
@@ -21,6 +21,8 @@ CELERY_ENABLED=true
REDIS_URL=redis://127.0.0.1:6379/0 REDIS_URL=redis://127.0.0.1:6379/0
CELERY_QUEUES=send_email,append_sent,notifications,calendar,dataflow,events,default CELERY_QUEUES=send_email,append_sent,notifications,calendar,dataflow,events,default
CALENDAR_OUTBOX_TERMINAL_RETENTION_DAYS=90 CALENDAR_OUTBOX_TERMINAL_RETENTION_DAYS=90
SCHEDULING_PUBLIC_SELF_ENROLLMENT_ENABLED=true
SCHEDULING_PUBLIC_SELF_ENROLLMENT_MAX_CAPACITY=10000
GOVOPLAN_CONNECTOR_ALLOW_PRIVATE_NETWORKS=false GOVOPLAN_CONNECTOR_ALLOW_PRIVATE_NETWORKS=false
GOVOPLAN_CONNECTOR_MAX_STRUCTURED_RESPONSE_BYTES=16777216 GOVOPLAN_CONNECTOR_MAX_STRUCTURED_RESPONSE_BYTES=16777216
+13 -46
View File
@@ -123,7 +123,7 @@ Synchronize module package workflows and inspect the registry release contract:
Package publication, exact artifact locking, and the optional `govoplan` Package publication, exact artifact locking, and the optional `govoplan`
developer meta-package are documented in developer meta-package are documented in
[Package Registry Releases](docs/PACKAGE_REGISTRY_RELEASES.md). [Package Registry Releases](docs/operations/PACKAGE_REGISTRY_RELEASES.md).
For reproducible release artifacts, set `SOURCE_DATE_EPOCH` to the release For reproducible release artifacts, set `SOURCE_DATE_EPOCH` to the release
commit timestamp (or pass an explicit timezone-qualified `--timestamp`): commit timestamp (or pass an explicit timezone-qualified `--timestamp`):
@@ -170,19 +170,19 @@ Create and validate a private, declarative installation bundle:
``` ```
The current executable slice and remaining production gates are documented in The current executable slice and remaining production gates are documented in
[Installation and Deployment Architecture](docs/INSTALLATION_AND_DEPLOYMENT_ARCHITECTURE.md). [Installation and Deployment Architecture](docs/operations/INSTALLATION_AND_DEPLOYMENT_ARCHITECTURE.md).
The canonical distinction between local source development, split source The canonical distinction between local source development, split source
integration, immutable single-host rehearsal, one-host production and integration, immutable single-host rehearsal, one-host production and
multi-host Kubernetes production is in multi-host Kubernetes production is in
[Deployment Profiles](docs/DEPLOYMENT_PROFILES.md). [Deployment Profiles](docs/operations/DEPLOYMENT_PROFILES.md).
Same-host replica balancing and the multi-host promotion boundary are documented Same-host replica balancing and the multi-host promotion boundary are documented
in [Scaling and Multi-Host Deployment](docs/SCALING_AND_MULTI_HOST_DEPLOYMENT.md). in [Scaling and Multi-Host Deployment](docs/operations/SCALING_AND_MULTI_HOST_DEPLOYMENT.md).
Create, update, pause, resume, verify and remove a local or multi-hypervisor K3s Create, update, pause, resume, verify and remove a local or multi-hypervisor K3s
VM target with the guarded lifecycle documented in VM target with the guarded lifecycle documented in
[Kubernetes VM Test Lab](docs/KUBERNETES_TEST_LAB.md). [Kubernetes VM Test Lab](docs/operations/KUBERNETES_TEST_LAB.md).
The recovery state machine, migration rollback boundary, and required restore The recovery state machine, migration rollback boundary, and required restore
drills are documented in drills are documented in
[Recovery and Rollback Guarantees](docs/RECOVERY_AND_ROLLBACK_GUARANTEES.md). [Recovery and Rollback Guarantees](docs/operations/RECOVERY_AND_ROLLBACK_GUARANTEES.md).
## Configuration ## Configuration
@@ -195,47 +195,14 @@ such as `~/.config/gitea/gitea.env` and be passed with `--env-file`.
## Structure ## Structure
The repository categories are documented in Start with the [documentation map](docs/README.md). It separates stable
`docs/REPOSITORY_STRUCTURE.md`. The machine-readable list lives in strategy, architecture, operations, project reference, pinned evidence, and
`repositories.json`; the clickable human-readable index is historical records and identifies the canonical source for each question.
`docs/REPOSITORY_INDEX.md`.
Meta ownership and module install/contract boundaries are documented in The machine-readable repository list lives in `repositories.json`; the
`docs/META_REPO_SCAN.md` and `docs/MODULE_CONTRACTS_AND_INSTALLS.md`. clickable directory is the
Frontend layout principles for module pages are documented in [Repository Index](docs/project/REPOSITORY_INDEX.md), and ownership boundaries
`docs/FRONTEND_LAYOUT_PRINCIPLES.md`. are in [Repository Structure](docs/project/REPOSITORY_STRUCTURE.md).
The provider-neutral datasource boundary and reusable Dataflow/Workflow graph
contract are documented in
`docs/DATASOURCE_AND_DEFINITION_GRAPH_ARCHITECTURE.md`.
The cross-product destination, stakeholder visions, configuration archetypes,
connected outcome stories, and capability horizons are documented in
the [Connected Governance Platform Roadmap](docs/CONNECTED_GOVERNANCE_PLATFORM_ROADMAP.md).
The reconciled institutional semantics, source-authority modes, module layers,
candidate Mandates/Services/Parties/Decisions boundaries, and migration
sequence are documented in the
[Institutional Governance Target Architecture](docs/INSTITUTIONAL_GOVERNANCE_TARGET_ARCHITECTURE.md).
The selected Campaign-to-Postbox-to-data-to-collaboration implementation path,
including stage gates and shared documentation expectations, is in the
[Reference Journey Program](docs/REFERENCE_JOURNEY_PROGRAM.md).
The administrator journey from a Core-baseline bootstrap through online module
installation, scale-out, and reversible environment promotion is defined in
[System Administrator Lifecycle User Story](docs/SYSTEM_ADMINISTRATOR_LIFECYCLE_USER_STORY.md).
The corresponding host deployment compiler, managed/external component choices,
reconfiguration semantics, and safe Web update boundary are defined in
[Installation and Deployment Architecture](docs/INSTALLATION_AND_DEPLOYMENT_ARCHITECTURE.md).
The concrete replica, worker-node, load-balancer, and shared-state topology is
defined in [Scaling and Multi-Host Deployment](docs/SCALING_AND_MULTI_HOST_DEPLOYMENT.md).
Durable deployment journals, Core recovery evidence, and the distinction
between pre-migration configuration restore and post-migration forward recovery
are defined in
[Recovery and Rollback Guarantees](docs/RECOVERY_AND_ROLLBACK_GUARANTEES.md).
The first Campaign-centric capability and infrastructure fit assessment is in
`docs/CAPABILITY_AND_INFRASTRUCTURE_FIT.md`. Its rerun tooling can collect and
verify a bounded installed composition; target, provider and production claims
remain separate, expiring attestations signed by independently scoped proof
authorities. The operational issuance, target-run, recovery-measurement, key
custody, and promotion-gate procedure is in
[Target Maturity Evidence Runbook](docs/TARGET_MATURITY_EVIDENCE_RUNBOOK.md).
# GovOPlaN Docker # GovOPlaN Docker
+1 -1
View File
@@ -3,7 +3,7 @@
This profile runs the shared services that production depends on while keeping This profile runs the shared services that production depends on while keeping
API, worker, scheduler, and WebUI code in the editable local repositories. API, worker, scheduler, and WebUI code in the editable local repositories.
It is the **split source integration** profile defined in It is the **split source integration** profile defined in
[`docs/DEPLOYMENT_PROFILES.md`](../../docs/DEPLOYMENT_PROFILES.md). It does not [`docs/operations/DEPLOYMENT_PROFILES.md`](../../docs/operations/DEPLOYMENT_PROFILES.md). It does not
exercise signed application images. Use an installer-generated evaluation exercise signed application images. Use an installer-generated evaluation
Compose bundle for an immutable Dockerized whole-product rehearsal. Compose bundle for an immutable Dockerized whole-product rehearsal.
-77
View File
@@ -1,77 +0,0 @@
# GovOPlaN Frontend Layout Principles
GovOPlaN modules should choose their page layout by the kind of work the user is
doing, not by the repository that owns the feature.
These concise layout choices are one canonical input to the broader
[`INTERFACE_PATTERN_LANGUAGE.md`](INTERFACE_PATTERN_LANGUAGE.md). The current
route and rollout evidence lives in
[`INTERFACE_SURFACE_INVENTORY.md`](INTERFACE_SURFACE_INVENTORY.md).
## Structured Data Directories
Use a full-available-space workspace for structured data directories: files,
addresses, calendars, records, mailboxes, document stores, and similar domains
where the primary task is browsing, selecting, filtering, inspecting, and acting
on related objects.
Principles:
- The module route should use the full available content area.
- Do not add a separate page heading row above the main workspace.
- Prefer persistent navigation panes, such as tree panels, source panels, folder
panels, calendar list panels, or mailbox folder panels.
- Keep collection navigation and collection-level actions close to the relevant
pane header.
- In a list-detail workspace such as Scheduling, keep related lists stacked in
the left pane and use the remaining main pane for view/create/edit. A single
Add action stays in the relevant list-pane header and opens the common main
editor; it does not create an additional menu or launcher.
- Use bounded widths for navigation/list panes and let the main detail/content
pane take the remaining space.
- Keep filtering controls inside the pane they affect.
- Use overlays, toasts, or floating alerts for transient messages so the
workspace height does not change.
This pattern is appropriate when the user is working inside one coherent data
domain and needs spatial continuity.
## Workflow And Configuration Surfaces
Use the standard heading/menu/card visual language for workflow structures,
settings, administration, dashboards, and pages that collect essentially
unrelated areas.
Principles:
- A page heading and subnavigation are appropriate when the page explains a
task, workflow stage, or administrative area.
- Cards are appropriate for repeated independent panels, settings groups,
summaries, and dashboard widgets.
- Collapsible panels and segmented controls are appropriate when a dense
configuration area needs controlled disclosure.
- A collapsible card whose sole content is a table gives that table the full
available card body; avoid nested cards, duplicate padding, inner max-widths,
and nested scrolling.
- Avoid forcing workflow/configuration pages into a file-explorer style unless
the primary interaction is genuinely directory browsing.
This pattern is appropriate when the user is comparing or configuring separate
concerns rather than navigating one structured object space.
## Shared Components
Reusable layout components belong in `govoplan-core` WebUI. Modules may consume
shared components from core, but must not import another module's private UI
components directly.
When a module-specific component becomes generally useful, promote it to core
with a parameterized API before reusing it elsewhere.
Non-self-explanatory fields use Core `FieldLabel`; documented omissions must
name their accessible-label source. Explicit Discard and dirty navigation use
the same Core unsaved-changes dialog. Table action sets retain unavailable row
actions as disabled controls and reserve empty-state slots so Add remains
aligned. Use central feedback/dialog components; `window.alert` is not an
authorized product surface unless a product-owner-approved exception is first
recorded in the Core decision ledger.
+108 -61
View File
@@ -1,80 +1,127 @@
# GovOPlaN Documentation Map # GovOPlaN Documentation
This directory contains cross-repository product, architecture, release, and This directory contains cross-repository product, architecture, delivery, and
operational documentation. The map below defines which document answers which project documentation. Start here instead of browsing every file.
question. A document not listed as the current status source must not present
volatile repository, issue, release, or maturity counts as current facts. ## Read First
| Need | Source |
| --- | --- |
| Understand the platform in ten minutes | [Platform Core Ideas](strategy/PLATFORM_CORE_IDEAS.md) |
| See the intended product sequence | [Roadmap](strategy/ROADMAP.md) |
| Check the reconciled state and material gaps | [Strategy Status](strategy/STRATEGY_STATUS.md) |
| Find active work, priority, or ownership | [Gitea issue workflow](project/GITEA_ISSUES.md) and Gitea issues |
| Understand the selected end-to-end proofs | [Reference Journey Program](strategy/REFERENCE_JOURNEY_PROGRAM.md) |
The first three documents are the normal entry points. Detailed architecture,
runbooks, evidence, and historical assessments support them; they are not
parallel roadmaps.
## Strategy ## Strategy
| Question | Canonical source | | Document | Role |
| --- | --- | | --- | --- |
| What are the stable ideas and boundaries of the platform? | [Platform Core Ideas](PLATFORM_CORE_IDEAS.md) | | [Platform Core Ideas](strategy/PLATFORM_CORE_IDEAS.md) | Stable purpose, principles, planes, distinctions, and non-goals |
| What product outcomes should GovOPlaN pursue? | [Connected Governance Platform Roadmap](CONNECTED_GOVERNANCE_PLATFORM_ROADMAP.md) | | [Roadmap](strategy/ROADMAP.md) | Concise product outcomes, horizons, and current sequence |
| Which institutional concepts and owners form the target architecture? | [Institutional Governance Target Architecture](INSTITUTIONAL_GOVERNANCE_TARGET_ARCHITECTURE.md) | | [Strategy Status](strategy/STRATEGY_STATUS.md) | Only prose source for current cross-product status |
| Which end-to-end proofs should guide implementation? | [Reference Journey Program](REFERENCE_JOURNEY_PROGRAM.md) | | [Reference Journey Program](strategy/REFERENCE_JOURNEY_PROGRAM.md) | Acceptance journeys and their gates |
| What is the reconciled state now? | [Strategy Status](STRATEGY_STATUS.md) | | [Product Input Register](strategy/PRODUCT_INPUT_REGISTER.md) | Normalized ideas and user-story source material |
| Which collected ideas and user stories inform the product direction? | [Product Input Register](PRODUCT_INPUT_REGISTER.md) | | [System Administrator Lifecycle](strategy/SYSTEM_ADMINISTRATOR_LIFECYCLE_USER_STORY.md) | Installation and lifecycle outcome story |
| [Detailed Connected-Platform Vision](strategy/reference/CONNECTED_GOVERNANCE_PLATFORM_ROADMAP.md) | Long-form stakeholder, configuration, and outcome catalogue |
The dated [Strategic Review](STRATEGIC_REVIEW_2026-08-05.md) explains why the ## Architecture
current reset and sequencing were chosen. It is an assessment record, not a
second live status page.
## Product Architecture
| Topic | Canonical source | | Topic | Canonical source |
| --- | --- | | --- | --- |
| Product-facing experience and hiding technical module boundaries | [Product Experience and Module Boundaries](PRODUCT_EXPERIENCE_AND_MODULE_BOUNDARIES.md) | | Institutional model and ownership | [Institutional Governance Target Architecture](architecture/INSTITUTIONAL_GOVERNANCE_TARGET_ARCHITECTURE.md) |
| Configurable product areas and task-local tools | [Quick Access and Product Areas](QUICK_ACCESS_AND_PRODUCT_AREAS.md) | | Product experience and technical boundaries | [Product Experience and Module Boundaries](architecture/PRODUCT_EXPERIENCE_AND_MODULE_BOUNDARIES.md) |
| Federation between autonomous installations | [Federated GovOPlaN Architecture](FEDERATED_GOVOPLAN_ARCHITECTURE.md) | | Shared interface and layout rules | [Interface Pattern Language](architecture/INTERFACE_PATTERN_LANGUAGE.md) |
| Institutional digital twin and continuous assurance | [Institutional Digital Twin](INSTITUTIONAL_DIGITAL_TWIN.md) | | Focused task views | [Views Architecture](architecture/VIEWS_ARCHITECTURE.md) |
| Assisted and non-digital channels | [Assisted and Non-Digital Channels](ASSISTED_AND_NON_DIGITAL_CHANNELS.md) | | Product areas and task-local tools | [Quick Access and Product Areas](architecture/QUICK_ACCESS_AND_PRODUCT_AREAS.md) |
| Cross-module temporal, purpose, retention, and institutional-context adoption | `govoplan-core/docs/INFORMATION_GOVERNANCE_ADOPTION.md` | | Platform self-description and configuration | [Platform Control Plane](architecture/PLATFORM_CONTROL_PLANE.md) |
| eAkte and digital-record ownership | `govoplan-records/docs/EAKTE_ARCHITECTURE.md` | | Data sources, definitions, and graph execution | [Datasource and Definition Graph Architecture](architecture/DATASOURCE_AND_DEFINITION_GRAPH_ARCHITECTURE.md) |
| Data source, definition, and transformation graph | [Datasource and Definition Graph Architecture](DATASOURCE_AND_DEFINITION_GRAPH_ARCHITECTURE.md) | | Federation between autonomous installations | [Federated GovOPlaN Architecture](architecture/FEDERATED_GOVOPLAN_ARCHITECTURE.md) |
| Focused task views | [Views Architecture](VIEWS_ARCHITECTURE.md) | | Institutional digital twin | [Institutional Digital Twin](architecture/INSTITUTIONAL_DIGITAL_TWIN.md) |
| Shared interface patterns | [Interface Pattern Language](INTERFACE_PATTERN_LANGUAGE.md) | | Assisted and non-digital participation | [Assisted and Non-Digital Channels](architecture/ASSISTED_AND_NON_DIGITAL_CHANNELS.md) |
## Runtime And Delivery Module-specific architecture remains in the owning repository. In particular,
information-governance adoption is in
`govoplan-core/docs/INFORMATION_GOVERNANCE_ADOPTION.md`, and the eAkte model is
in `govoplan-records/docs/EAKTE_ARCHITECTURE.md`.
- [Module Contracts and Installs](MODULE_CONTRACTS_AND_INSTALLS.md) ## Operations
- [Platform Control Plane](PLATFORM_CONTROL_PLANE.md)
- [Installation and Deployment Architecture](INSTALLATION_AND_DEPLOYMENT_ARCHITECTURE.md)
- [Kubernetes VM Test Lab](KUBERNETES_TEST_LAB.md)
- [Deployment Profiles](DEPLOYMENT_PROFILES.md)
- [Scaling and Multi-Host Deployment](SCALING_AND_MULTI_HOST_DEPLOYMENT.md)
- [Recovery and Rollback Guarantees](RECOVERY_AND_ROLLBACK_GUARANTEES.md)
- [Recovery Ledger Adoption](RECOVERY_LEDGER_ADOPTION.md)
- [Package Registry Releases](PACKAGE_REGISTRY_RELEASES.md)
## Evidence And Snapshots | Need | Source |
| --- | --- |
| Installation model and managed components | [Installation and Deployment Architecture](operations/INSTALLATION_AND_DEPLOYMENT_ARCHITECTURE.md) |
| Supported operating modes | [Deployment Profiles](operations/DEPLOYMENT_PROFILES.md) |
| Horizontal scaling and multi-host topology | [Scaling and Multi-Host Deployment](operations/SCALING_AND_MULTI_HOST_DEPLOYMENT.md) |
| Local Kubernetes evidence target | [Kubernetes VM Test Lab](operations/KUBERNETES_TEST_LAB.md) |
| Recovery guarantees and state machine | [Recovery and Rollback Guarantees](operations/RECOVERY_AND_ROLLBACK_GUARANTEES.md) |
| Recovery-ledger rollout | [Recovery Ledger Adoption](operations/RECOVERY_LEDGER_ADOPTION.md) |
| Backup evidence contract | [Backup and Restore Evidence](operations/BACKUP_AND_RESTORE_EVIDENCE.md) |
| Target handoff and independent evidence | [Production Target Handoff](operations/PRODUCTION_TARGET_HANDOFF.md) |
| Evidence collection and promotion | [Target Maturity Evidence Runbook](operations/TARGET_MATURITY_EVIDENCE_RUNBOOK.md) |
| Package publication and consumption | [Package Registry Releases](operations/PACKAGE_REGISTRY_RELEASES.md) |
| Release-console operation | [Release Console](operations/RELEASE_CONSOLE.md) |
| Module compatibility and install behavior | [Module Contracts and Installs](operations/MODULE_CONTRACTS_AND_INSTALLS.md) |
| Security-audit toolchain | [Security Audit](operations/SECURITY_AUDIT.md) |
These documents are intentionally dated or pinned. They may remain useful even ## Project Reference
after the product changes, but they do not override `STRATEGY_STATUS.md`.
- [Capability and Infrastructure Fit Assessment](CAPABILITY_AND_INFRASTRUCTURE_FIT.md), pinned to the 2026-07-22 Campaign composition - [Repository Index](project/REPOSITORY_INDEX.md) is the human-readable module
- [Strategic Review 2026-08-05](STRATEGIC_REVIEW_2026-08-05.md) and repository directory; `../repositories.json` is authoritative for tools.
- [Backup and Restore Evidence](BACKUP_AND_RESTORE_EVIDENCE.md) - [Repository Structure](project/REPOSITORY_STRUCTURE.md) defines ownership of
- [Production Target Handoff](PRODUCTION_TARGET_HANDOFF.md) meta, module, deployment, and website content.
- [Target Maturity Evidence Runbook](TARGET_MATURITY_EVIDENCE_RUNBOOK.md) - [Gitea Issues](project/GITEA_ISSUES.md) defines labels, templates, import, and
state-update conventions.
Machine-readable schemas and evidence files belong beside the document that ## Evidence And Archive
defines them. Generated inventories belong in `audit-reports/` and should not
be edited manually. Pinned evidence is retained under `evidence/`; completed reviews and migration
inventories are under `archive/`. They explain or prove a dated state and must
not be read as current product status.
- [Generated Campaign capability and infrastructure fit, 2026-07-22](evidence/snapshots/CAPABILITY_AND_INFRASTRUCTURE_FIT.generated.md)
- [Supporting narrative for the 2026-07-22 assessment](evidence/snapshots/CAPABILITY_AND_INFRASTRUCTURE_FIT.md)
- [Interface surface inventory, 2026-08-03](evidence/snapshots/INTERFACE_SURFACE_INVENTORY.md)
- [Strategic review, 2026-08-05](archive/2026-08/STRATEGIC_REVIEW_2026-08-05.md)
- [Meta repository scan, 2026-07-13](archive/2026-07/META_REPO_SCAN.md)
- [Meta repository migration audit](archive/2026-07/META_REPOSITORY_MIGRATION_AUDIT.md)
The JSON files at the root of this directory are machine-readable schemas,
evidence inputs, and project configuration. Their paths are intentionally
stable because tools and published schema identifiers consume them; they are
not additional reading-list entries.
Regenerate and verify the human fit report from its JSON input with:
```sh
./.venv/bin/python tools/assessments/generate-capability-fit-report.py
./.venv/bin/python tools/assessments/generate-capability-fit-report.py --check
```
## Maintenance Rules ## Maintenance Rules
1. Gitea issues are the only live work-state source. 1. Gitea issues are the only live source for work state, priority, and owner.
2. `STRATEGY_STATUS.md` is the only prose reconciliation of current portfolio 2. `strategy/STRATEGY_STATUS.md` is the only prose reconciliation of current
state. Refresh it from manifests, inventories, tests, and Gitea; do not copy portfolio state. Do not copy its volatile counts into durable documents.
its counts into durable architecture pages.
3. Durable documents state decisions, invariants, ownership, and acceptance 3. Durable documents state decisions, invariants, ownership, and acceptance
gates. They link to status and issues for implementation depth. gates. They link to Gitea for implementation detail.
4. Dated assessments retain their original composition and conclusion. Add a 4. Dated evidence and archive documents retain their original composition and
snapshot notice rather than silently updating their claims. conclusion. Add a snapshot notice instead of silently modernizing them.
5. Module-specific behavior and user/admin documentation remain in the owning 5. Module-specific behavior and user/admin documentation stay in the owning
repository. Meta documentation defines cross-module outcomes and contracts. repository. Meta documentation covers cross-module outcomes and contracts.
6. A new strategy document must replace, narrow, or link an existing source; 6. Do not add another top-level Markdown file. Place new content in the
it must not introduce a parallel roadmap. appropriate directory and add it to this map only when it has a distinct
7. The Product Input Register preserves external idea and story notes, but only canonical purpose.
Gitea issues carry live priority, ownership, and implementation state. 7. A new strategy document must replace, narrow, or become a reference for an
existing source; it must not introduce a parallel roadmap.
8. The Product Input Register preserves source ideas. Only a named journey,
package, or Gitea issue turns an idea into implementation work.
After moving or adding documentation, run:
```sh
./.venv/bin/python -m unittest tests.test_documentation_structure
```
@@ -72,6 +72,16 @@ An assisted session is a resumable work item, not a privileged bypass. It:
8. creates follow-up tasks when original documents, signatures, translation, 8. creates follow-up tasks when original documents, signatures, translation,
or verification remain outstanding. or verification remain outstanding.
The first executable slice is implemented in Forms Runtime for authenticated
assisted sessions. Administrators enable an exact published Form revision;
operators then record channel, party and representation references, authority,
purpose, notice, responsible function, language, accessibility needs, and
field-level source/confidence provenance. Read-back outcomes are append-only and
payload-bound. A draft correction changes the Form revision and invalidates the
prior confirmation for submission. The resident-parking-permit fixture proves
resume and submission enforcement; browser accessibility and target archive
evidence remain acceptance work.
The helper's normal account and represented function remain in the audit The helper's normal account and represented function remain in the audit
chain. Assistance never grants access to unrelated records about the person. chain. Assistance never grants access to unrelated records about the person.
@@ -12,21 +12,21 @@ The source concepts describe GovOPlaN as an operational governance platform for
public institutions. This document is the canonical repository version of that public institutions. This document is the canonical repository version of that
durable architectural direction. Its implementation table records the accepted durable architectural direction. Its implementation table records the accepted
2026-08-01 baseline; it is not a rolling status report. Current reconciliation 2026-08-01 baseline; it is not a rolling status report. Current reconciliation
lives in [Strategy Status](STRATEGY_STATUS.md), and Gitea issues remain the lives in [Strategy Status](../strategy/STRATEGY_STATUS.md), and Gitea issues remain the
source of truth for delivery state. source of truth for delivery state.
Read this together with: Read this together with:
- [Connected Governance Platform Roadmap](CONNECTED_GOVERNANCE_PLATFORM_ROADMAP.md) - [Connected Governance Platform Roadmap](../strategy/reference/CONNECTED_GOVERNANCE_PLATFORM_ROADMAP.md)
- [Platform Core Ideas](PLATFORM_CORE_IDEAS.md) - [Platform Core Ideas](../strategy/PLATFORM_CORE_IDEAS.md)
- [Strategy Status](STRATEGY_STATUS.md) - [Strategy Status](../strategy/STRATEGY_STATUS.md)
- [Reference Journey Program](REFERENCE_JOURNEY_PROGRAM.md) - [Reference Journey Program](../strategy/REFERENCE_JOURNEY_PROGRAM.md)
- [Module Contracts and Install Boundaries](MODULE_CONTRACTS_AND_INSTALLS.md) - [Module Contracts and Install Boundaries](../operations/MODULE_CONTRACTS_AND_INSTALLS.md)
- [Datasource and Definition Graph Architecture](DATASOURCE_AND_DEFINITION_GRAPH_ARCHITECTURE.md) - [Datasource and Definition Graph Architecture](DATASOURCE_AND_DEFINITION_GRAPH_ARCHITECTURE.md)
- [Capability and Infrastructure Fit](CAPABILITY_AND_INFRASTRUCTURE_FIT.md) - [Generated Capability and Infrastructure Fit](../evidence/snapshots/CAPABILITY_AND_INFRASTRUCTURE_FIT.generated.md)
- [Core Module Architecture](../../govoplan-core/docs/MODULE_ARCHITECTURE.md) - [Core Module Architecture](../../../govoplan-core/docs/MODULE_ARCHITECTURE.md)
- [Core External References and Integration Maturity](../../govoplan-core/docs/EXTERNAL_REFERENCES_AND_INTEGRATION_MATURITY.md) - [Core External References and Integration Maturity](../../../govoplan-core/docs/EXTERNAL_REFERENCES_AND_INTEGRATION_MATURITY.md)
- [Core Action, Effect, and Automation Layer](../../govoplan-core/docs/ACTION_EFFECT_AUTOMATION_LAYER.md) - [Core Action, Effect, and Automation Layer](../../../govoplan-core/docs/ACTION_EFFECT_AUTOMATION_LAYER.md)
## Decision ## Decision
@@ -2,12 +2,13 @@
This document is the cross-repository pattern language for GovOPlaN user This document is the cross-repository pattern language for GovOPlaN user
interfaces. It turns the existing ethical doctrine, binding UI/UX decisions, interfaces. It turns the existing ethical doctrine, binding UI/UX decisions,
layout principles, and module boundary into a common composition and review layout rules, and module boundary into a common composition and review grammar.
grammar. It does not replace those sources. This document also owns the former standalone frontend-layout principles.
The companion [interface surface inventory](INTERFACE_SURFACE_INVENTORY.md) The dated [interface surface inventory](../evidence/snapshots/INTERFACE_SURFACE_INVENTORY.md)
records which surfaces the current code contributes and where each surface records the 2026-08-03 rollout snapshot. Current implementation state belongs
enters the rollout. in Gitea and generated inventory evidence, not in this durable pattern
language.
## Source Of Truth And Precedence ## Source Of Truth And Precedence
@@ -19,14 +20,11 @@ Use the narrowest owning document when changing a rule:
2. `govoplan-core/docs/UI_UX_DECISION_LEDGER.md` owns accepted product decisions 2. `govoplan-core/docs/UI_UX_DECISION_LEDGER.md` owns accepted product decisions
such as progressive disclosure, adaptive forms, blocker language, guided such as progressive disclosure, adaptive forms, blocker language, guided
operations, and the platform theme contract. operations, and the platform theme contract.
3. `docs/FRONTEND_LAYOUT_PRINCIPLES.md` owns the high-level choice between a 3. `govoplan-core/docs/MODULE_ARCHITECTURE.md` owns the shell, route, navigation,
full-space structured-data workspace and a heading/menu/card workflow or
configuration surface.
4. `govoplan-core/docs/MODULE_ARCHITECTURE.md` owns the shell, route, navigation,
UI-capability, and shared-component boundaries. UI-capability, and shared-component boundaries.
5. This document owns the common pattern names, placement grammar, wording and 4. This document owns the high-level layout choice, common pattern names,
state conventions, focused-view composition, and definition of done across placement grammar, wording and state conventions, focused-view composition,
those sources. and definition of done across those sources.
If two rules appear to conflict, do not create a third local convention. Record If two rules appear to conflict, do not create a third local convention. Record
the conflict in the owning decision ledger, resolve it there, and update the the conflict in the owning decision ledger, resolve it there, and update the
@@ -53,6 +51,148 @@ rules:
- Preserve a stable way back to the containing object and the broader system. - Preserve a stable way back to the containing object and the broader system.
- Do not let navigation, selection, or a view switch imply consent. - Do not let navigation, selection, or a view switch imply consent.
## Shared Component And Layout Architecture
Core owns the reusable WebUI vocabulary; modules own domain composition and
behavior. Centralization follows four layers:
| Layer | Owner | Examples | Rule |
| --- | --- | --- | --- |
| Foundation | Core | theme tokens, spacing, typography, focus and responsive breakpoints | Modules consume the contract and do not redefine it. |
| Primitives | Core | buttons, fields, dialogs, alerts, cards, tables, loading, empty and blocked states | A matching primitive is reused rather than copied locally. |
| Structural layouts | Core | page frame and header, action region, workspace panes, toolbars, grids, form sections and dialog anatomy | Layout owns geometry, scroll, responsive collapse and accessibility, but no domain decisions. |
| Domain compositions | Owning module | a campaign review, mailbox, records explorer or operations dashboard | Modules select shared pieces, bind data and permissions, and retain domain wording and consequences. |
A component belongs in Core when it is used or expected in more than one
module and central ownership materially protects accessibility, responsive
behavior, localization, contextual help, theming, or interaction consistency.
A component stays module-owned when its API would otherwise encode a domain
entity, permission, workflow state, endpoint, or policy decision. Reuse does
not justify moving domain semantics into Core.
`PageLayout` is the standard frame for headed workflow, dashboard,
configuration, monitoring and explanatory pages. It owns the content inset,
sticky responsive header, title and rich-description geometry, route-action
placement, transient and custom notices, loading boundary and page help
identity. Its modes make scroll ownership explicit: `standalone` owns a page
viewport, `workspace` defers scrolling to a full-canvas content pane while
retaining the standard inset, and `embedded` owns neither scroll nor inset.
`WorkspaceLayout` is the standard full-canvas shell. Its `navigation` variant
owns module/resource subnavigation plus content; its `split` variant owns
collection/detail panes. It centralizes pane sizing, internal scroll,
responsive collapse/stacking, accessible pane labels and workspace help
identity. `WorkspaceFrame` is the outer full-height module frame and owns
container or application-viewport height, overflow, surface, landmark, help,
and accessible-name behavior. `PageHeader` remains available when an
exceptional canvas needs only the shared heading. Specialized layouts such as
`AdminPageLayout` compose these lower-level Core contracts; they do not repeat
markup or responsive CSS.
`PageActionBar` is the semantic action contract for headed pages;
`WorkspaceActionBar` applies the identical ordering and lifecycle rules to a
full canvas and its collection, detail, and editor panes. Reload is always the
leading action on a refreshable projection. Help and ordinary task actions
follow contextual controls; Create is the far-right collection action;
destructive actions occupy a named separated group; an editor ends with
Discard and Save, with Save at the far right. Editor state is explicit:
`clean`, `dirty`, `invalid`, `saving`, `save-failed`, or `conflict`. Lower-level
`ActionToolbar` remains appropriate for a section-local view switch or compact
control group, but it must not recreate page or pane action placement.
Composite workspaces whose selected contribution supplies its own semantic
heading may use `PageLayout` with its visible header delegated. This preserves
the central inset, loading boundary, help identity, and content frame without
adding a duplicate heading. It is not permission to recreate the page header
locally on ordinary headed pages.
Module CSS may arrange domain content inside a shared layout. It must not
override Core layout internals or copy the outer page, dialog, toolbar, form or
state skeleton under a module-prefixed name. If an archetype cannot be
expressed by the central API, extend the central contract or record a bounded
exception before introducing local structure.
Migration is incremental and enforceable:
1. inventory copied structures and register existing debt;
2. introduce the smallest domain-neutral Core contract with accessibility,
help, localization, theme and narrow-layout tests;
3. migrate representative Core and optional-module consumers;
4. reject new copies while removing registered debt in bounded module batches;
5. promote the next repeated structure only after its variants and extension
points are understood.
The current page-frame and workspace migration has no legacy exceptions. New
raw frames fail the focused layout contract instead of entering a new baseline.
The current structural vocabulary is:
- `ActionToolbar`, `ToolbarGroup`, and `ToolbarSpacer` own action alignment,
distribution, density, grouping, panel/section surfaces, accessible toolbar
naming, help identity, and responsive wrapping. Modules may add
domain-specific presentation; they do not recreate the flex/wrap skeleton.
- `PageActionBar` and `WorkspaceActionBar` own semantic ordering, Reload,
editor persistence state, destructive separation, and page/pane scope. A
module supplies action behavior, authority, blocker reasons, and wording;
it does not assemble another panel-header action convention.
- `WorkspaceFrame` and `WorkspaceLayout` own application-viewport framing,
surfaces, overflow, list/detail and navigation/content pane geometry,
accessible region identity, and responsive pane behavior. Modules own only
the domain regions placed inside those contracts.
- `FilterBar` owns submitted or live filter/search arrangement, wrapping,
width and surface. `SelectionList`, `SelectionListItem`, and
`SelectionListItemContent` own selectable resource navigation and its
title/description/leading-icon geometry. `CountBadge` owns compact numeric
emphasis. Modules retain filter behavior, selection state, and count meaning.
- `StatePanel` owns whole-surface, compact, inline and fill state presentation
for empty, unavailable, blocked, warning and recoverable-error compositions.
Modules provide the cause, consequence, permitted action and authority.
- `ContentGrid`, `FormGrid`, `FormLayout`, and `GridItem` own equal-column
geometry, standard gaps, alignment, spans, native form semantics, and named
responsive collapse points. A module-local grid remains appropriate only
when unequal tracks or domain visualization semantics are material.
- `ContentSection` owns repeated bordered or subtle content-section surfaces,
density, stacked flow and surrounding rhythm without prescribing a domain
heading or body schema.
- `FormSection` owns form-section heading, description, actions, content flow,
separation, and panel presentation. It does not own field values,
validation, permissions, or domain wording.
- `MetricGrid` owns the responsive grouping around `MetricCard`: fixed one-to-five
columns or auto-fit, minimum card width, density, surrounding rhythm, and a
named collapse point. `MetricCard.drilldown` provides an explicit link or
in-page action when an authorized underlying detail helps the user act; it
names that destination and preserves the current scope and filters. The card
itself is never the hidden click target. Derived, privacy-suppressed,
non-enumerable, and purely informational aggregates remain inert. Modules
provide the metric, tone, destination, and consequence; they do not recreate
the group grid or reach across module CSS to size it.
- `DescriptionList` and `DescriptionItem` own semantic property presentation.
The stacked variant supports compact multi-column facts; the inline variant
supports one-column term/value rows with a standard term width. Both own
density, wrapping, and responsive collapse while modules retain the terms,
values, provenance, and actions.
- `Dialog` owns size and administration variants, body padding, description,
notices, and fixed footer placement. `DialogActions`, `DialogForm`, and
`DialogSection` own the footer action flow, native form flow, and body
grouping used inside it. Modules compose fields and consequences rather than
recreating dialog anatomy.
- `DefinitionPalette`, `DefinitionPaletteGroup`, `DefinitionPaletteItem`, and
`DefinitionNodeIcon`, together with the shared definition-canvas classes,
own reusable graph-editor palette, canvas-control, node-icon, port and empty
overlay visuals. Workflow/Dataflow retain node types, shapes, edges,
validation and execution semantics. `FloatingStatus` owns the common
non-shifting activity overlay.
Raw toolbar tags, the former generic grid and property-list classes, retired
module-local shells/states/metrics/badges, raw dialog-form wrappers, and
module-local definitions of these contracts are rejected by the focused
workspace checks. Dialog widths matching the Core size scale must use `Dialog
size`; other local widths require a reviewed exception and may only decrease.
Remaining local layout is acceptable only for unequal-track domain editors,
visualizations, trees, timelines, data tables, or domain-specific multi-pane
interaction. Generic resemblance alone is not a reason to create one oversized
page template, while exact repeated structural anatomy must be promoted.
## Surface Archetypes ## Surface Archetypes
Choose an archetype from the task, then specialize it for the domain. A route Choose an archetype from the task, then specialize it for the domain. A route
@@ -95,15 +235,23 @@ one.
- Structured directories use the full available content space and persistent - Structured directories use the full available content space and persistent
panes. They do not add a decorative heading row that reduces working height. panes. They do not add a decorative heading row that reduces working height.
Give navigation and list panes bounded widths and let the main content or
detail pane consume the remaining space.
- In a list-detail workspace, related lists may be stacked in the left pane
while the main pane owns view, create, and edit. Keep one create action in
the relevant list heading instead of adding a second launcher or permanent
creation panel.
- Workflow, configuration, dashboard, and explanatory pages may use a heading. - Workflow, configuration, dashboard, and explanatory pages may use a heading.
The heading names the task or scoped object and contains only route-level The heading names the task or scoped object and contains only route-level
actions. actions. Use the Core `PageLayout` contract for the frame and `PageHeader`
only when a full-canvas archetype owns its own scrolling.
- Put a collection-wide create action in the heading of the collection it - Put a collection-wide create action in the heading of the collection it
affects. Use a short, specific label such as `Add` when the heading already affects. Use a short, specific label such as `Add` when the heading already
names the object. Do not duplicate that action in a permanently visible side names the object. Do not duplicate that action in a permanently visible side
panel. A side panel used as the creation surface appears for creation and is panel. A side panel used as the creation surface appears for creation and is
otherwise absent or returns to its documented non-creation purpose. otherwise absent or returns to its documented non-creation purpose.
- Put filters beside the list or pane they affect. Put bulk actions immediately - Put filters beside the list or pane they affect. Put collection, detail, and
editor-pane actions in `WorkspaceActionBar` with the matching scope. Put bulk actions immediately
above or beside the current selection. Put object actions with the object above or beside the current selection. Put object actions with the object
detail, not in the global title bar. detail, not in the global title bar.
- Full-page create and edit surfaces put their persistent action cluster in the - Full-page create and edit surfaces put their persistent action cluster in the
@@ -72,6 +72,23 @@ Each WebUI module should be able to announce:
The contract references surfaces. It does not permit Core or a product package The contract references surfaces. It does not permit Core or a product package
to import their implementation. to import their implementation.
The first versioned `product_surfaces` slice is now implemented in Core. It
binds a stable product identity and entry path to one or more owner routes,
View surfaces, presentations, capabilities, search sources, help contexts and
documentation topics. It also carries standard unavailable/degraded
explanations and migration aliases. Mail and Postbox contribute the first
shared identity, `communication.messages`: `/messages` and the migration alias
`/inbox` select the first currently authorized, View-visible owner while the
underlying `/mail` and `/postbox` deep links, custody and permissions remain
unchanged. Alias resolution emits a bounded client telemetry event before the
redirect.
Core's `ProductAvailabilityState` is the shared presentation primitive for
authorization, Policy, configuration, disabled, missing-capability, offline and
provider-degraded states. Product language is primary; exact module,
capability, provider and correlation provenance is available only in an
expandable technical section.
## Navigation Model ## Navigation Model
The default shell should prioritize: The default shell should prioritize:
@@ -132,9 +149,9 @@ first rail slice.
### Slice 1: inventory and aliases ### Slice 1: inventory and aliases
- classify every route, navigation item, widget, setting, search object, and - continue classifying every route, navigation item, widget, setting, search object, and
help context by product area and object type; help context by product area and object type;
- add product aliases without removing existing deep links; - extend the implemented product-surface aliases without removing existing deep links;
- flag raw module IDs in ordinary-user labels and errors. - flag raw module IDs in ordinary-user labels and errors.
### Slice 2: work-first shell ### Slice 2: work-first shell
@@ -41,12 +41,15 @@ The first production-shaped slice is implemented:
- the expanded left rail groups classified destinations while retaining - the expanded left rail groups classified destinations while retaining
Dashboard and every authorized unclassified destination under More tools. Dashboard and every authorized unclassified destination under More tools.
The remaining rollout is classification rather than a missing boundary: other The baseline classification is now manifest-declared for every ordinary
user-facing modules must announce their product areas and future bounded tools, user-facing module and enforced by the workspace manifest check. A separately
reference journeys need browser accessibility evidence, and richer active-object versioned launch-context contract carries bounded active-object, acting,
context should be added only through a separately versioned launch-context temporal, View and return references into full-page Quick Access fallbacks;
contract. Until classification is complete, authorized unclassified routes Cases publishes the first active-object reference. The remaining rollout is to
remain visible rather than disappearing. add useful bounded tools and active-object publishers only where a maintained
journey benefits, and to extend browser evidence to a pinned reference
composition. Authorized global and technical routes remain visible through
their dedicated shell entry or **All available tools**.
## Quick Access Boundary ## Quick Access Boundary
@@ -75,10 +78,11 @@ matters.
A Quick Access contribution declares: A Quick Access contribution declares:
- a stable id, category and human label; - contract version 1, a stable id, category and human label;
- icon, order and optional badge/summary provider; - icon, order and optional badge/summary provider;
- required permissions and optional dependencies; - required permissions and optional dependencies;
- accepted context references and produced return references; - global or active-object availability, accepted context-reference kinds and
produced result-reference kinds;
- an owner-rendered bounded WebUI surface and full-page fallback route; - an owner-rendered bounded WebUI surface and full-page fallback route;
- View surface, help context and availability explanation; - View surface, help context and availability explanation;
- whether the contribution supports preview, create, select or resume. - whether the contribution supports preview, create, select or resume.
@@ -89,6 +93,22 @@ resources and a safe return location. The owner reauthorizes every read and
effect. Credentials, protected content and permission decisions are never effect. Credentials, protected content and permission decisions are never
embedded in launch context. embedded in launch context.
Launch-context version 2 identifies reference contract version 1 and carries
the exact resolved View revision plus optional recommended and focused tool
ids. Recommendations affect order and emphasis only. Focus narrows the rail
only when at least one focused contribution survives module enablement,
configuration, context compatibility and authorization; otherwise the normal
effective rail remains available. Workflow gets the same behavior by resolving
the exact View revision instead of acquiring separate presentation authority.
An owner-rendered tool explicitly returns result contract version 1 as either
`completed` with an action and typed owner reference, or `cancelled` with a
reason. The shell correlates the result with the source and tool, rejects
cross-tenant or undeclared reference kinds, and does not interpret closing the
drawer as completion. Owner modules validate, persist, recover and audit their
own effects. The overlay leaves the host route mounted, so unsaved host-page
state is preserved; the full-page route remains the bounded-work fallback.
## Effective Configuration ## Effective Configuration
The effective rail is resolved from: The effective rail is resolved from:
@@ -122,9 +142,9 @@ workspace layouts do not resize unexpectedly; a later explicit pinned mode may
reserve layout width on sufficiently wide screens. reserve layout width on sufficiently wide screens.
The drawer preserves host-page state, has a deterministic focus return, closes The drawer preserves host-page state, has a deterministic focus return, closes
with Escape, supports keyboard traversal, and provides an explicit full-page with Escape, supports keyboard traversal, and provides explicit completion,
open action. Mobile and narrow layouts use the same category/configuration cancellation and full-page actions. Mobile and narrow layouts use the same
semantics in a bottom sheet or compact menu. category/configuration semantics in a bottom sheet or compact menu.
## Product Areas ## Product Areas
@@ -150,6 +170,25 @@ Familiar product nouns such as Calendar, Mail or Files may remain directly
pinned. The objective is not to hide every module name; it is to prevent pinned. The objective is not to hide every module name; it is to prevent
repository topology from determining a person's workflow. repository topology from determining a person's workflow.
The initial module classification is deliberately outcome-oriented:
| Product area | Contributing user-facing modules |
| --- | --- |
| Work | Approvals, Projects, Tasks, Workflow |
| Services and Cases | Cases, Forms, Forms Runtime, Portal |
| Records and Documents | Files, Records, Templates |
| Communication | Campaigns, Distribution Lists, Mail, Notifications, Postbox |
| Meetings and Decisions | Calendar, Committee, Scheduling, Voting |
| Data and Assurance | Dataflow, Datasources, Reporting, Risk Compliance |
| People and Responsibility | Address Book, IDM, Organizations |
Dashboard, Search, Documentation and Quick Access remain global shell
affordances. Access, Administration, Audit, Encryption, Identity Trust,
Operations, Policy, Tenancy and Views remain administrative or platform
surfaces available through their dedicated entry point or **All available
tools**. The manifest-shape check enforces both this explicit exception set and
the shared label, icon, description and ordering of every canonical area.
## Full Access And Provenance ## Full Access And Provenance
The existing permission-derived module rail remains available as **All The existing permission-derived module rail remains available as **All
@@ -141,7 +141,7 @@ Still intentionally separate:
Quick Access ordering and availability remain owned by Quick Access ordering and availability remain owned by
`govoplan-quick-access`; Views only narrow its declared surfaces for the active `govoplan-quick-access`; Views only narrow its declared surfaces for the active
task. Neither contract permits arbitrary layout or styling. See task. Neither contract permits arbitrary layout or styling. See
`docs/QUICK_ACCESS_AND_PRODUCT_AREAS.md` in the meta repository. `docs/architecture/QUICK_ACCESS_AND_PRODUCT_AREAS.md` in the meta repository.
## Gitea Work Packages ## Gitea Work Packages
@@ -1,5 +1,9 @@
# Meta Repository Migration Audit # Meta Repository Migration Audit
> **Archived migration record:** The ownership migration described here is
> complete. Current boundaries are defined by Repository Structure, module
> manifests, and the owning repositories.
This audit records which existing GovOPlaN files should move toward the This audit records which existing GovOPlaN files should move toward the
`govoplan` meta repository and which should remain with their current runtime `govoplan` meta repository and which should remain with their current runtime
owner. owner.
@@ -148,7 +152,7 @@ It should not own:
Known references reviewed after the server-side rename: Known references reviewed after the server-side rename:
- `govoplan/repositories.json` - `govoplan/repositories.json`
- `govoplan/docs/REPOSITORY_STRUCTURE.md` - `govoplan/docs/project/REPOSITORY_STRUCTURE.md`
- `govoplan/docker/README.md` - `govoplan/docker/README.md`
- `govoplan-core/docs/RELEASE_DEPENDENCIES.md` - `govoplan-core/docs/RELEASE_DEPENDENCIES.md`
- `govoplan-core/docs/MODULE_ARCHITECTURE.md` - `govoplan-core/docs/MODULE_ARCHITECTURE.md`
@@ -1,5 +1,8 @@
# Meta Repository Scan # Meta Repository Scan
> **Archived assessment:** This file records the 2026-07-13 repository state.
> Use `repositories.json` and the current documentation map for present state.
Scan date: 2026-07-13. Scan date: 2026-07-13.
This scan checked local repositories under `/mnt/DATA/git` listed in This scan checked local repositories under `/mnt/DATA/git` listed in
@@ -13,7 +16,7 @@ Checked-out repositories not listed in `repositories.json`: none.
Repositories listed in `repositories.json` but not checked out locally: none. Repositories listed in `repositories.json` but not checked out locally: none.
The human-readable link index is `docs/REPOSITORY_INDEX.md`; the JSON file The human-readable link index is `docs/project/REPOSITORY_INDEX.md`; the JSON file
remains the machine-readable source of truth. remains the machine-readable source of truth.
## Meta-Owned Content ## Meta-Owned Content
@@ -1,5 +1,8 @@
# Strategic Review - 2026-08-05 # Strategic Review - 2026-08-05
> **Archived assessment:** This review explains the 2026-08-05 strategy reset.
> It is not updated with later implementation or portfolio state.
## Assessment ## Assessment
GovOPlaN has not lost its central direction. The architecture now expresses a GovOPlaN has not lost its central direction. The architecture now expresses a
@@ -9,8 +12,8 @@ need is convergence: fewer simultaneous fronts, stronger cross-cutting
adoption, and end-to-end reference journeys that non-developers can complete. adoption, and end-to-end reference journeys that non-developers can complete.
This is a dated review. Current status belongs in This is a dated review. Current status belongs in
[Strategy Status](STRATEGY_STATUS.md); stable direction belongs in [Strategy Status](../../strategy/STRATEGY_STATUS.md); stable direction belongs in
[Platform Core Ideas](PLATFORM_CORE_IDEAS.md). [Platform Core Ideas](../../strategy/PLATFORM_CORE_IDEAS.md).
## What Is Already Strong ## What Is Already Strong
@@ -58,7 +61,7 @@ already reduce navigation and the complete technical rail remains useful for
power users. The correction is configurable product areas, task-focused Views power users. The correction is configurable product areas, task-focused Views
and a bounded Quick Access rail, while preserving deliberate access to every and a bounded Quick Access rail, while preserving deliberate access to every
authorized tool and technical provenance. The accepted design is maintained in authorized tool and technical provenance. The accepted design is maintained in
[Quick Access And Product Areas](QUICK_ACCESS_AND_PRODUCT_AREAS.md). [Quick Access And Product Areas](../../architecture/QUICK_ACCESS_AND_PRODUCT_AREAS.md).
### Status became duplicated ### Status became duplicated
+275 -5
View File
@@ -1,6 +1,6 @@
{ {
"$schema": "./capability-fit.schema.json", "$schema": "./capability-fit.schema.json",
"schema_version": "0.1.0", "schema_version": "0.2.0",
"assessment_id": "campaign-reference-2026-07-22", "assessment_id": "campaign-reference-2026-07-22",
"assessed_at": "2026-07-22", "assessed_at": "2026-07-22",
"scope": { "scope": {
@@ -13,16 +13,30 @@
"Workflow and workflow-driven user stories" "Workflow and workflow-driven user stories"
] ]
}, },
"facts": [
"The assessment is pinned to signed stable catalog sequence 202607220843 and the exact module commits listed below.",
"The Campaign authoring, validation, build, mock-delivery, managed-file, local-access, and local-audit paths have direct test or contract evidence.",
"The production-like development profile runs PostgreSQL and Redis in containers while application processes use editable source trees.",
"No installed-target, external-provider, reference-readiness, recovery, or production-approval evidence bundle is attached to this assessment."
],
"decisions": [
"Use Campaign as the first reference journey and flagship pilot scenario.",
"Keep Workflow and workflow-driven user stories planned and explicitly postponed for this assessment.",
"Use local GovOPlaN accounts for the bounded pilot; do not claim federated identity support.",
"Do not approve small production until installed-artifact, target mail, monitoring, backup/restore, and recovery proof checks pass."
],
"release": { "release": {
"kind": "tagged_release", "kind": "tagged_release",
"ref": "stable-catalog-202607220843", "ref": "stable-catalog-202607220843",
"meta_commit": "5447299289a1", "meta_commit": "5447299289a1",
"reproducible": true, "reproducible": true,
"configuration_packages": [], "configuration_packages": [
"none: environment-profile basis only"
],
"notes": [ "notes": [
"The live stable catalog has a valid Ed25519 signature trusted through release-key-1.", "The live stable catalog has a valid Ed25519 signature trusted through release-key-1.",
"Core v0.1.13 and Campaign v0.1.10 are tagged and package-integrated; this is not target-environment or production approval.", "Core v0.1.13 and Campaign v0.1.10 are tagged and package-integrated; this is not target-environment or production approval.",
"No configuration revision or configuration package is pinned yet." "The absence of a configuration package is pinned explicitly as an environment-profile-only basis; this remains a promotion gap."
] ]
}, },
"composition": [ "composition": [
@@ -188,6 +202,125 @@
} }
] ]
}, },
"scenarios": [
{
"id": "campaign-pilot",
"label": "Controlled Campaign pilot",
"status": "partial",
"recommendation": "Proceed with a bounded internal pilot after its provider, privacy, workload, and recovery proof checks are assigned and passed.",
"composition": [
"core",
"tenancy",
"organizations",
"identity",
"access",
"admin",
"dashboard",
"policy",
"audit",
"campaigns",
"files",
"mail",
"docs",
"ops"
],
"topology": [
"One supervised GovOPlaN API process and one immutable built WebUI behind deployment-owned TLS termination",
"One PostgreSQL database and a durable single-node or shared managed-file path",
"One persistent private Redis broker and one supervised Celery worker when asynchronous delivery is enabled",
"One dedicated non-production SMTP/IMAP account with a restricted safe-recipient policy",
"External health checks, centralized logs, protected secret injection, and coordinated backup storage"
],
"conditions": [
"Use one internal tenant or office and controlled operators.",
"Keep recipient volume non-critical until measured.",
"Enable Addresses only when reusable recipient lists or CardDAV are explicitly in scope.",
"Do not enable or claim Workflow from this assessment."
]
},
{
"id": "small-production-candidate",
"label": "Small-production candidate",
"status": "partial",
"recommendation": "Do not approve production until every listed operational gate has target evidence and the residual risks have named owners.",
"composition": [
"core",
"tenancy",
"organizations",
"identity",
"access",
"admin",
"dashboard",
"policy",
"audit",
"campaigns",
"files",
"mail",
"docs",
"ops"
],
"topology": [
"Immutable separately supervised WebUI, API, and worker artifacts behind monitored reverse-proxy TLS",
"Dedicated or managed PostgreSQL with measured coordinated backup and isolated restore",
"Persistent authenticated Redis with queue-age, queue-depth, and worker-health alerts",
"Durable shared or S3-compatible object storage with versioning, lifecycle, and restore evidence",
"Target-native secret management, centralized monitoring/logging/audit export, and an exercised incident and disaster-recovery procedure"
],
"conditions": [
"Pin and promote a configuration package instead of relying on an environment-only basis.",
"Pass installed-release, target SMTP/IMAP, accessibility, privacy, security, operations, and recovery evidence gates.",
"Agree availability, RPO, RTO, retention, support, and procurement requirements.",
"Run only one scheduler unless distributed leadership or locking is proved."
]
}
],
"functional_context": {
"required_modules": [
"core",
"tenancy",
"organizations",
"identity",
"access",
"admin",
"dashboard",
"policy",
"audit",
"campaigns",
"files",
"mail",
"docs",
"ops"
],
"optional_modules": [
"addresses"
],
"external_systems": [
"Deployment-owned reverse proxy and TLS certificate lifecycle",
"Target SMTP/IMAP service and its DNS, certificate, throttling, bounce, and reply policies",
"Target-native secret store, monitoring/logging platform, backup storage, and incident-response process"
],
"missing_contracts": [
"End-to-end federated identity provider and lifecycle contract",
"Target monitoring, alert delivery, and central audit/SIEM acceptance contract",
"Production configuration-package promotion and approval evidence"
],
"policy_decisions": [
"Recipient allow-list, permitted sender, attachment, retention, and external-disclosure policy",
"Identity, MFA, break-glass, service-account, and joiner/mover/leaver policy",
"Availability, RPO, RTO, support, procurement, and residual-risk ownership"
],
"manual_workarounds": [
"Use controlled local accounts while federation remains outside the verified slice",
"Use one supervised scheduler where periodic work is unavoidable",
"Keep provider reconciliation and production promotion under explicit operator review"
],
"blockers": [
"No promoted configuration package is pinned",
"No installed-target or target SMTP/IMAP proof is attached",
"No coherent target backup/restore or disaster-recovery drill with measured RPO/RTO is attached",
"No target privacy, security, accessibility, operations, or production-approval evidence is attached"
]
},
"questionnaire": { "questionnaire": {
"scope_outcomes": [ "scope_outcomes": [
{ {
@@ -203,6 +336,20 @@
"state": "answered", "state": "answered",
"answer": "No; Workflow is planned and explicitly postponed.", "answer": "No; Workflow is planned and explicitly postponed.",
"evidence": [] "evidence": []
},
{
"id": "scope.users_tenants_organizations",
"question": "Which users, roles, tenants, organization units, and delegated functions participate?",
"state": "assumed",
"answer": "One internal tenant or office with controlled Campaign operators; detailed organization and delegation shape remains target-specific.",
"evidence": []
},
{
"id": "outcome.acceptance",
"question": "What constitutes pilot success and production acceptance?",
"state": "answered",
"answer": "Pilot success requires the bounded Campaign journey and proof checks; production additionally requires installed-artifact, provider, privacy, security, operations, recovery, and approval evidence.",
"evidence": []
} }
], ],
"data_policy": [ "data_policy": [
@@ -219,6 +366,13 @@
"state": "not_assessed", "state": "not_assessed",
"answer": null, "answer": null,
"evidence": [] "evidence": []
},
{
"id": "data.privacy_security_disclosure",
"question": "Which privacy, security, residency, minimization, access, and external-disclosure constraints apply?",
"state": "not_assessed",
"answer": null,
"evidence": []
} }
], ],
"identity_integrations": [ "identity_integrations": [
@@ -235,22 +389,50 @@
"state": "not_assessed", "state": "not_assessed",
"answer": null, "answer": null,
"evidence": [] "evidence": []
},
{
"id": "identity.protocols_lifecycle",
"question": "Which identity protocols, MFA, joiner/mover/leaver, service-account, and break-glass rules are mandatory?",
"state": "not_assessed",
"answer": null,
"evidence": []
},
{
"id": "integration.protocols_network",
"question": "Which connector protocols, versions, directions, authentication, certificate, rate-limit, egress, and degraded-mode requirements apply?",
"state": "not_assessed",
"answer": null,
"evidence": []
} }
], ],
"workload_growth": [ "workload_growth": [
{ {
"id": "workload.campaign", "id": "workload.campaign_volume_peaks",
"question": "What are Campaign frequency, recipients per Campaign, send window, import size and attachment volume?", "question": "What are Campaign frequency, recipients per Campaign, send window, import size and attachment volume?",
"state": "not_assessed", "state": "not_assessed",
"answer": null, "answer": null,
"evidence": [] "evidence": []
}, },
{ {
"id": "workload.platform", "id": "workload.tenants_users_concurrency",
"question": "What are tenant, named-user, active-user, concurrent-user, and peak-request assumptions?",
"state": "not_assessed",
"answer": null,
"evidence": []
},
{
"id": "workload.files_jobs_audit_growth_retention",
"question": "What are tenant, user, concurrency, file, database, queue and audit growth assumptions?", "question": "What are tenant, user, concurrency, file, database, queue and audit growth assumptions?",
"state": "not_assessed", "state": "not_assessed",
"answer": null, "answer": null,
"evidence": [] "evidence": []
},
{
"id": "workload.connector_traffic_batches",
"question": "What connector traffic, scheduled-job, batch, queue-depth, queue-age, and external-rate-limit peaks apply?",
"state": "not_assessed",
"answer": null,
"evidence": []
} }
], ],
"availability_operations": [ "availability_operations": [
@@ -267,6 +449,13 @@
"state": "not_assessed", "state": "not_assessed",
"answer": null, "answer": null,
"evidence": [] "evidence": []
},
{
"id": "hosting.network_constraints",
"question": "Which hosting, network-zone, egress, proxy, DNS, NTP, certificate-authority, residency, or disconnected-operation constraints apply?",
"state": "not_assessed",
"answer": null,
"evidence": []
} }
], ],
"procurement_decisions": [ "procurement_decisions": [
@@ -754,6 +943,63 @@
"recommendation": "Use target-native secret injection and document rotation/recovery.", "recommendation": "Use target-native secret injection and document rotation/recovery.",
"proof_check": "Rotate a non-production credential and recover from a protected backup." "proof_check": "Rotate a non-production credential and recover from a protected backup."
}, },
{
"id": "identity.access",
"requirement": "Authenticate users and enforce tenant-scoped authorization through the selected identity mode.",
"status": "verified",
"evidence": [
{
"kind": "test",
"scope": "committed_source",
"locator": "govoplan-access/tests/test_auth_dependencies.py"
},
{
"kind": "test",
"scope": "committed_source",
"locator": "govoplan-core/tests/test_api_smoke.py#cookie-session-csrf"
}
],
"conditions": [
"The bounded pilot accepts local GovOPlaN accounts."
],
"gaps": [
"Target MFA, federation, provisioning, and joiner/mover/leaver requirements are not assessed."
],
"risks": [
"A local-only identity topology may not satisfy institutional production policy."
],
"recommendation": "Use controlled local pilot accounts and assess the mandatory production identity topology separately.",
"proof_check": "Exercise login, role change, account suspension, protected bootstrap, and break-glass recovery in the target."
},
{
"id": "connectors.mail",
"requirement": "Reach the selected SMTP/IMAP and other external connector endpoints under explicit network and provider policy.",
"status": "available_unconfigured",
"evidence": [
{
"kind": "test",
"scope": "current_workspace",
"locator": "govoplan-mail/tests",
"note": "Protocol adapters have direct tests; no target provider was exercised"
},
{
"kind": "documentation",
"scope": "documented_model",
"locator": "govoplan-campaign/docs/CAMPAIGN_DELIVERY_RUNBOOK.md"
}
],
"conditions": [
"The deployment supplies DNS, egress, proxy, CA trust, scoped service accounts, and provider limits."
],
"gaps": [
"No target endpoint, TLS chain, throttling, sender policy, bounce/reply path, or disclosure agreement is assessed."
],
"risks": [
"Provider rejection, delay, or ambiguous outcomes can affect delivery and evidence completeness."
],
"recommendation": "Use a dedicated safe provider account for the pilot and require target interoperability evidence before production.",
"proof_check": "Exercise target-like SMTP acceptance, IMAP append, throttling, outage, retry, and reconciliation through the approved network path."
},
{ {
"id": "operations.monitoring", "id": "operations.monitoring",
"requirement": "Detect API, database, worker, queue, storage and delivery degradation.", "requirement": "Detect API, database, worker, queue, storage and delivery degradation.",
@@ -780,6 +1026,30 @@
"recommendation": "Integrate external monitoring before small production.", "recommendation": "Integrate external monitoring before small production.",
"proof_check": "Trigger each readiness/delivery failure and verify an actionable alert." "proof_check": "Trigger each readiness/delivery failure and verify an actionable alert."
}, },
{
"id": "operations.audit",
"requirement": "Retain, monitor, review, and where required export security and business audit evidence.",
"status": "partial",
"evidence": [
{
"kind": "test",
"scope": "current_workspace",
"locator": "govoplan-audit/tests",
"note": "Local audit persistence and retry behavior are exercised"
}
],
"conditions": [
"Local database audit evidence is part of coordinated backup and access review."
],
"gaps": [
"Target retention enforcement, tamper-evident export, SIEM integration, alerting, and privileged review are not verified."
],
"risks": [
"Local evidence alone may not meet institutional security, records, or incident-response requirements."
],
"recommendation": "Define the target audit retention, export, monitoring, and review controls before production approval.",
"proof_check": "Exercise privileged-event review, retention, export failure/retry, and target SIEM or archive ingestion."
},
{ {
"id": "operations.backup_restore", "id": "operations.backup_restore",
"requirement": "Back up and restore database, files, configuration and keys as a coherent service.", "requirement": "Back up and restore database, files, configuration and keys as a coherent service.",
+66 -1
View File
@@ -9,9 +9,13 @@
"assessment_id", "assessment_id",
"assessed_at", "assessed_at",
"scope", "scope",
"facts",
"decisions",
"release", "release",
"composition", "composition",
"deployment_profile", "deployment_profile",
"scenarios",
"functional_context",
"questionnaire", "questionnaire",
"capabilities", "capabilities",
"infrastructure", "infrastructure",
@@ -28,7 +32,7 @@
"format": "uri-reference" "format": "uri-reference"
}, },
"schema_version": { "schema_version": {
"const": "0.1.0" "const": "0.2.0"
}, },
"assessment_id": { "assessment_id": {
"$ref": "#/$defs/non_empty_string" "$ref": "#/$defs/non_empty_string"
@@ -54,6 +58,8 @@
} }
} }
}, },
"facts": { "$ref": "#/$defs/string_list" },
"decisions": { "$ref": "#/$defs/string_list" },
"release": { "release": {
"type": "object", "type": "object",
"additionalProperties": false, "additionalProperties": false,
@@ -95,6 +101,33 @@
} }
} }
}, },
"scenarios": {
"type": "array",
"minItems": 2,
"items": { "$ref": "#/$defs/scenario" }
},
"functional_context": {
"type": "object",
"additionalProperties": false,
"required": [
"required_modules",
"optional_modules",
"external_systems",
"missing_contracts",
"policy_decisions",
"manual_workarounds",
"blockers"
],
"properties": {
"required_modules": { "$ref": "#/$defs/string_list" },
"optional_modules": { "$ref": "#/$defs/string_list" },
"external_systems": { "$ref": "#/$defs/string_list" },
"missing_contracts": { "$ref": "#/$defs/string_list" },
"policy_decisions": { "$ref": "#/$defs/string_list" },
"manual_workarounds": { "$ref": "#/$defs/string_list" },
"blockers": { "$ref": "#/$defs/string_list" }
}
},
"questionnaire": { "questionnaire": {
"type": "object", "type": "object",
"additionalProperties": false, "additionalProperties": false,
@@ -239,6 +272,37 @@
} }
} }
}, },
"scenario": {
"type": "object",
"additionalProperties": false,
"required": [
"id",
"label",
"status",
"recommendation",
"composition",
"topology",
"conditions"
],
"properties": {
"id": { "$ref": "#/$defs/non_empty_string" },
"label": { "$ref": "#/$defs/non_empty_string" },
"status": { "$ref": "#/$defs/status" },
"recommendation": { "$ref": "#/$defs/non_empty_string" },
"composition": {
"type": "array",
"minItems": 1,
"uniqueItems": true,
"items": { "$ref": "#/$defs/non_empty_string" }
},
"topology": {
"type": "array",
"minItems": 1,
"items": { "$ref": "#/$defs/non_empty_string" }
},
"conditions": { "$ref": "#/$defs/string_list" }
}
},
"assessed_item": { "assessed_item": {
"type": "object", "type": "object",
"additionalProperties": false, "additionalProperties": false,
@@ -259,6 +323,7 @@
"status": { "$ref": "#/$defs/status" }, "status": { "$ref": "#/$defs/status" },
"evidence": { "evidence": {
"type": "array", "type": "array",
"minItems": 1,
"items": { "$ref": "#/$defs/evidence" } "items": { "$ref": "#/$defs/evidence" }
}, },
"conditions": { "$ref": "#/$defs/string_list" }, "conditions": { "$ref": "#/$defs/string_list" },
@@ -0,0 +1,74 @@
# Shared WebUI Primitive Inventory
This 2026-08-18 inventory records the implementation state after the
product-wide structural consolidation and second duplicate-rule audit. It is
evidence for enforcement, not a substitute for the normative
[interface pattern language](../architecture/INTERFACE_PATTERN_LANGUAGE.md).
## Implemented And Enforced
| Contract | Adoption evidence | Ownership now enforced |
| --- | ---: | --- |
| `ActionToolbar` and groups | 50 source files | Raw module-prefixed toolbar elements and local toolbar definitions are rejected. Distribution, wrapping, density, grouping and panel/section surfaces are Core-owned. |
| `PageLayout` / `WorkspaceLayout` / `WorkspaceFrame` | 25 / 16 / 17 source files | Headed page anatomy, full-height viewport frames and navigation/list-detail panes no longer repeat inset, heading, notices, loading, shell height, surface, overflow or pane geometry. The raw page-frame and raw workspace exception baselines are both empty. |
| `FilterBar` | 14 source files | Catalogue and pane search/filter rows share width, surface, layout and wrapping. |
| `SelectionList` family | 19 source files | Resource navigation shares selection, title/description, leading-icon and truncation anatomy. |
| `StatePanel` | 25 source files | Whole-surface, compact and fill empty/blocked/error states replace module-local state shells. |
| `CountBadge` | 8 source files | Notification, folder, search, postbox and graph counts use one compact badge contract. |
| `ContentSection` | 5 source files | Repeated bordered/subtle editor sections and compact provenance panels share surface, density, flow and rhythm. |
| `ContentGrid` | 22 source files | Equal-column content geometry and former dashboard/settings/assignment copies are Core-owned. |
| `FormGrid` and `FormLayout` | 53 source files | Former generic/admin grids and equal-column dialog/editor copies use named collapse points and native form semantics. |
| `MetricGrid` / `MetricCard` | 31 / 33 source files | Module-local metric helpers, grids and card visual definitions were removed. |
| `DescriptionList` and `DescriptionItem` | 28 source files | Former generic property grids use semantic `dl`/`dt`/`dd` composition with central density and collapse. |
| `DefinitionPalette`, node/canvas visuals and `FloatingStatus` | 2 Dataflow/Workflow consumers each | The copied graph palette, canvas controls, minimap, node icon/port, empty overlay and activity overlay definitions are Core-owned; graph semantics remain local. |
| `DialogActions`, `DialogForm`, `DialogSection` | every Core footer / 6 / 6 source files | Footer action flow, native dialog form flow and dialog content grouping are Core-owned. |
| Standard dialog sizing | 61 reviewed specialized selectors | Any width matching the Core 460/560/680/1040/1440px scale must use `Dialog size`; the remaining decrease-only exceptions are explicit. |
`tools/checks/check-shared-webui-primitives.py` verifies Core exports and
ownership, representative consumers, the absence of the retired raw anatomy,
and composition of every `Dialog` footer through `DialogActions`.
`tools/checks/check-shared-webui-layouts.py` additionally requires the reviewed
Core and module consumers and rejects any raw page or workspace frame; there
are no remaining allow-listed layout exceptions.
## Dialog Width Classification
The remaining 61 width selectors do not duplicate the Core 460/560/680/1040/
1440px scale. They cover bounded editor widths between scale steps, high-density
definition and governance editors, preview/chooser canvases, message and file
overlays with coupled height behavior, and responsive full-canvas workflows.
Their exact selector set lives in
`tools/checks/shared-webui-dialog-width-exceptions.txt`. The focused check fails
for a new selector, a stale baseline entry, or any local width that duplicates
the Core scale.
## Audit Result And Deliberate Local Ownership
The second scan compared exact CSS declaration bodies and JSX anatomy across
every WebUI module after migration. All repeated generic structural candidates
found in that pass were promoted: viewport frames, catalogue/list shells,
filters, selectable lists, state panels, count badges, section frames,
equal-column grids, section headers, metrics, and definition-editor chrome.
The final legacy-baseline pass also migrated Access administration, Core
Settings, Docs, Mail bounce processing, and Organizations to the shared page
and workspace layouts and removed their copied responsive geometry.
The remaining cross-module declaration matches are not independent component
anatomy. They are small token-based rules such as ellipsis, muted captions,
uppercase terms, or flex-column containment applied to different semantic
elements. Moving those rules into a component would erase meaning; their
visual values already come from Core tokens. Remaining larger local layouts
are deliberately domain-owned:
- unequal-track editors, import mappings and schema/data tables;
- calendar time grids, charts, graph node shapes and graph edge semantics;
- file/mail/postbox/records explorer panes whose interaction contracts differ;
- timelines, evidence histories, recipient compositions and policy-specific
detail sections;
- compact list-row internals that cannot preserve their semantics through
`SelectionListItemContent`.
A future candidate is promoted only when a new audit identifies repeated
structure plus the same responsive, accessibility and interaction contract.
The enforcement script prevents regression for the patterns centralized in
this pass and maintains the reviewed dialog-width baseline.
@@ -0,0 +1,323 @@
# GovOPlaN Capability and IT-Infrastructure Fit Assessment
> Generated from [`capability-fit-current.json`](../../capability-fit-current.json).
> Edit and validate the machine-readable assessment, then regenerate this file;
> do not maintain conclusions independently in Markdown.
This is an evidence-based fit assessment, not a production approval or
security certification. Repository or manifest existence alone never counts
as an implemented capability. Unknown target requirements remain explicitly
`not_assessed`.
## Assessment record
| Field | Value |
| --- | --- |
| Assessment ID | `campaign-reference-2026-07-22` |
| Schema version | `govoplan.fit-assessment/0.2.0` |
| Assessed on | 2026-07-22 |
| Scope | Campaign-centric internal pilot and small-production candidate |
| Release | `stable-catalog-202607220843` (tagged_release) |
| Meta commit | `5447299289a1` |
| Deployment profile | `production-like-dev` · `partial` |
| Configuration packages | `none: environment-profile basis only` |
| Canonical input SHA-256 | `5a23f17c5289c5a89d2e92445f2c8b2eef54e3753f1392ebf300aff5508f0bfe` |
## Controlled status vocabulary
| Status | Meaning |
| --- | --- |
| `verified` | Implemented and directly exercised by evidence appropriate to the stated scope. |
| `available_unconfigured` | Implemented with supporting evidence, but not configured and exercised in the target. |
| `partial` | A useful subset exists, but a material part of the requirement is missing or unproved. |
| `scaffold` | Contracts or structure exist, but the end-to-end capability is not usable. |
| `external_system` | The deployment or another system must supply the capability. |
| `planned` | Only a concept, backlog item, or design direction exists. |
| `not_fit` | Evidence shows that the assessed composition cannot meet the requirement. |
| `not_assessed` | The requirement or target environment is not sufficiently known. |
## Scope and reference journeys
Reference journeys:
- Internal operator authors, validates, builds, queues, sends and reconciles an email Campaign with managed attachments
- Operator inspects delivery and audit evidence
Explicitly postponed:
- Workflow and workflow-driven user stories
## Facts
- The assessment is pinned to signed stable catalog sequence 202607220843 and the exact module commits listed below.
- The Campaign authoring, validation, build, mock-delivery, managed-file, local-access, and local-audit paths have direct test or contract evidence.
- The production-like development profile runs PostgreSQL and Redis in containers while application processes use editable source trees.
- No installed-target, external-provider, reference-readiness, recovery, or production-approval evidence bundle is attached to this assessment.
## Decisions
- Use Campaign as the first reference journey and flagship pilot scenario.
- Keep Workflow and workflow-driven user stories planned and explicitly postponed for this assessment.
- Use local GovOPlaN accounts for the bounded pilot; do not claim federated identity support.
- Do not approve small production until installed-artifact, target mail, monitoring, backup/restore, and recovery proof checks pass.
## Assumptions
- The pilot can use local accounts and one internal tenant or office.
- A dedicated non-production SMTP/IMAP account and safe recipients are available.
- Pilot load fits one API and one worker until measured otherwise.
- Durable local storage is acceptable for the pilot.
## Unresolved decisions
- What are the target organization's data classes, legal bases, retention and external-disclosure rules?
- Which identity, mail, file, address and monitoring systems are mandatory?
- What are Campaign volume, concurrency, growth, availability, RPO and RTO?
- Who owns each external runtime component and operational control?
- Which accessibility, security, support and procurement constraints are mandatory?
## Pinned release and composition
Release reproducible: **yes**.
Release notes:
- The live stable catalog has a valid Ed25519 signature trusted through release-key-1.
- Core v0.1.13 and Campaign v0.1.10 are tagged and package-integrated; this is not target-environment or production approval.
- The absence of a configuration package is pinned explicitly as an environment-profile-only basis; this remains a promotion gap.
| Module | Repository and commit | Manifest version | Enabled | Role |
| --- | --- | --- | --- | --- |
| `core` | `govoplan-core` @ `d487726f4d2c` | `0.1.13` | yes | API, registry, migrations, sessions, kernel contracts and shared WebUI |
| `tenancy` | `govoplan-tenancy` @ `efbec827616b` | `0.1.8` | yes | Tenant context and lifecycle |
| `organizations` | `govoplan-organizations` @ `39c081c4fb8f` | `0.1.8` | yes | Organization model |
| `identity` | `govoplan-identity` @ `7a1710af896f` | `0.1.8` | yes | Normalized internal identity directory |
| `access` | `govoplan-access` @ `f1d64d247e12` | `0.1.11` | yes | Local authentication, sessions, API keys and RBAC |
| `admin` | `govoplan-admin` @ `11ecf362a36d` | `0.1.8` | yes | Administration surfaces |
| `dashboard` | `govoplan-dashboard` @ `4b960ad37f0d` | `0.1.8` | yes | Module-aware home surface |
| `policy` | `govoplan-policy` @ `1063622d311a` | `0.1.9` | yes | Policy explanation and configuration boundary |
| `audit` | `govoplan-audit` @ `d3d2c60d7dc1` | `0.1.8` | yes | Database audit records and retrying audit outbox |
| `campaigns` | `govoplan-campaign` @ `735e874bd03c` | `0.1.10` | yes | Campaign authoring, build, delivery control and reporting |
| `files` | `govoplan-files` @ `2b34f6e30578` | `0.1.9` | yes | Managed files and Campaign attachments |
| `mail` | `govoplan-mail` @ `3e2302909022` | `0.1.10` | yes | SMTP and IMAP profiles and transports |
| `calendar` | `govoplan-calendar` @ `9bcf41bb1fbb` | `0.1.8` | yes | Optional calendar outside the Campaign pilot minimum |
| `docs` | `govoplan-docs` @ `be52b716caed` | `0.1.10` | yes | Configured-system documentation |
| `ops` | `govoplan-ops` @ `341773a4ff8a` | `0.1.8` | yes | Readiness and deployment-profile visibility |
| `addresses` | `govoplan-addresses` @ `93dddbb8c52a` | `0.1.9` | no | Optional reusable recipient sources and CardDAV |
## Deployment profile
Status: `partial`
PostgreSQL and Redis run in containers while API, WebUI, worker and scheduler run from editable source trees.
Evidence:
- configuration/current_workspace: govoplan/dev/production-like/docker-compose.yml
- documentation/documented_model: govoplan/dev/production-like/README.md
## Recommended scenarios
### Controlled Campaign pilot
Status: `partial`
Proceed with a bounded internal pilot after its provider, privacy, workload, and recovery proof checks are assigned and passed.
Composition: `core`, `tenancy`, `organizations`, `identity`, `access`, `admin`, `dashboard`, `policy`, `audit`, `campaigns`, `files`, `mail`, `docs`, `ops`.
Topology:
- One supervised GovOPlaN API process and one immutable built WebUI behind deployment-owned TLS termination
- One PostgreSQL database and a durable single-node or shared managed-file path
- One persistent private Redis broker and one supervised Celery worker when asynchronous delivery is enabled
- One dedicated non-production SMTP/IMAP account with a restricted safe-recipient policy
- External health checks, centralized logs, protected secret injection, and coordinated backup storage
Conditions:
- Use one internal tenant or office and controlled operators.
- Keep recipient volume non-critical until measured.
- Enable Addresses only when reusable recipient lists or CardDAV are explicitly in scope.
- Do not enable or claim Workflow from this assessment.
### Small-production candidate
Status: `partial`
Do not approve production until every listed operational gate has target evidence and the residual risks have named owners.
Composition: `core`, `tenancy`, `organizations`, `identity`, `access`, `admin`, `dashboard`, `policy`, `audit`, `campaigns`, `files`, `mail`, `docs`, `ops`.
Topology:
- Immutable separately supervised WebUI, API, and worker artifacts behind monitored reverse-proxy TLS
- Dedicated or managed PostgreSQL with measured coordinated backup and isolated restore
- Persistent authenticated Redis with queue-age, queue-depth, and worker-health alerts
- Durable shared or S3-compatible object storage with versioning, lifecycle, and restore evidence
- Target-native secret management, centralized monitoring/logging/audit export, and an exercised incident and disaster-recovery procedure
Conditions:
- Pin and promote a configuration package instead of relying on an environment-only basis.
- Pass installed-release, target SMTP/IMAP, accessibility, privacy, security, operations, and recovery evidence gates.
- Agree availability, RPO, RTO, retention, support, and procurement requirements.
- Run only one scheduler unless distributed leadership or locking is proved.
## Functional matrix context
### Required modules
- core
- tenancy
- organizations
- identity
- access
- admin
- dashboard
- policy
- audit
- campaigns
- files
- mail
- docs
- ops
### Optional modules
- addresses
### External systems and connectors
- Deployment-owned reverse proxy and TLS certificate lifecycle
- Target SMTP/IMAP service and its DNS, certificate, throttling, bounce, and reply policies
- Target-native secret store, monitoring/logging platform, backup storage, and incident-response process
### Missing contracts
- End-to-end federated identity provider and lifecycle contract
- Target monitoring, alert delivery, and central audit/SIEM acceptance contract
- Production configuration-package promotion and approval evidence
### Policy decisions
- Recipient allow-list, permitted sender, attachment, retention, and external-disclosure policy
- Identity, MFA, break-glass, service-account, and joiner/mover/leaver policy
- Availability, RPO, RTO, support, procurement, and residual-risk ownership
### Manual workarounds
- Use controlled local accounts while federation remains outside the verified slice
- Use one supervised scheduler where periodic work is unavoidable
- Keep provider reconciliation and production promotion under explicit operator review
### Blockers
- No promoted configuration package is pinned
- No installed-target or target SMTP/IMAP proof is attached
- No coherent target backup/restore or disaster-recovery drill with measured RPO/RTO is attached
- No target privacy, security, accessibility, operations, or production-approval evidence is attached
## Assessment questionnaire
Every required area remains visible even when its target answer is unknown.
| Area | Question | State | Answer | Evidence |
| --- | --- | --- | --- | --- |
| Scope Outcomes | Which journey is assessed? | `answered` | An internal operator authors, validates, builds, queues, sends and reconciles a Campaign with managed attachments. | — |
| Scope Outcomes | Is Workflow in scope? | `answered` | No; Workflow is planned and explicitly postponed. | — |
| Scope Outcomes | Which users, roles, tenants, organization units, and delegated functions participate? | `assumed` | One internal tenant or office with controlled Campaign operators; detailed organization and delegation shape remains target-specific. | — |
| Scope Outcomes | What constitutes pilot success and production acceptance? | `answered` | Pilot success requires the bounded Campaign journey and proof checks; production additionally requires installed-artifact, provider, privacy, security, operations, recovery, and approval evidence. | — |
| Data Policy | Which data classes and legal bases apply? | `not_assessed` | — | — |
| Data Policy | What retention, deletion, archive and legal-hold rules apply? | `not_assessed` | — | — |
| Data Policy | Which privacy, security, residency, minimization, access, and external-disclosure constraints apply? | `not_assessed` | — | — |
| Identity Integrations | May the pilot use local GovOPlaN accounts? | `assumed` | Yes; federation is outside the verified composition. | — |
| Identity Integrations | Which target SMTP/IMAP service and policy apply? | `not_assessed` | — | — |
| Identity Integrations | Which identity protocols, MFA, joiner/mover/leaver, service-account, and break-glass rules are mandatory? | `not_assessed` | — | — |
| Identity Integrations | Which connector protocols, versions, directions, authentication, certificate, rate-limit, egress, and degraded-mode requirements apply? | `not_assessed` | — | — |
| Workload Growth | What are Campaign frequency, recipients per Campaign, send window, import size and attachment volume? | `not_assessed` | — | — |
| Workload Growth | What are tenant, named-user, active-user, concurrent-user, and peak-request assumptions? | `not_assessed` | — | — |
| Workload Growth | What are tenant, user, concurrency, file, database, queue and audit growth assumptions? | `not_assessed` | — | — |
| Workload Growth | What connector traffic, scheduled-job, batch, queue-depth, queue-age, and external-rate-limit peaks apply? | `not_assessed` | — | — |
| Availability Operations | What availability, RPO and RTO are required? | `not_assessed` | — | — |
| Availability Operations | Who operates database, queue, storage, TLS, secrets, monitoring, backup and incident response? | `not_assessed` | — | — |
| Availability Operations | Which hosting, network-zone, egress, proxy, DNS, NTP, certificate-authority, residency, or disconnected-operation constraints apply? | `not_assessed` | — | — |
| Procurement Decisions | Which licensing, accessibility, security, certification, support and procurement conditions are mandatory? | `not_assessed` | — | — |
## Functional capability matrix
| Requirement | Status | Evidence | Conditions and gaps | Recommendation and proof |
| --- | --- | --- | --- | --- |
| **platform.composition**<br>Compose enabled backend and WebUI modules without hard optional-module dependencies. | `verified` | test/committed_source: govoplan-core/tests/test_module_system.py; contract/current_workspace: govoplan/tools/checks/check-contracts.py (43 modules, 33 providers, 19 requirements, no issues) | Condition: Package integration is verified; repeat checks on the installed target composition.; Gap: No target deployment acceptance is recorded.; Risk: A reproducible module graph can still be installed or configured incorrectly. | Use the signed stable catalog and verify the minimal Campaign composition after installation.<br>**Proof:** Run contract, migration, API and WebUI module-permutation gates on the installed release. |
| **access.local**<br>Provide tenant-scoped local accounts, sessions, API keys and RBAC. | `verified` | test/committed_source: govoplan-access/tests/test_auth_dependencies.py; test/committed_source: govoplan-core/tests/test_api_smoke.py#cookie-session-csrf | Condition: Pilot accepts local accounts.; Gap: MFA and federated lifecycle are not part of this conclusion.; Risk: Manual account lifecycle may not satisfy production identity policy. | Use controlled local pilot accounts and define break-glass/bootstrap rules.<br>**Proof:** Exercise joiner, role change, suspension and protected-owner recovery. |
| **campaign.journey**<br>Author, validate, build, queue, send, reconcile and report a Campaign with frozen execution evidence. | `verified` | test/committed_source: govoplan-core/tests/test_api_smoke.py#campaign-create-validate-build-mock-send; test/committed_source: govoplan-campaign/tests (Campaign v0.1.10 is exactly the catalog-selected tagged source); configuration/committed_source: https://govoplan.add-ideas.de/catalogs/v1/channels/stable.json#sequence-202607220843 (Core v0.1.13 and Campaign v0.1.10 have matching catalogued Python and WebUI refs) | Condition: This verifies implementation paths, not target-provider delivery.; Gap: Usability and target-provider acceptance remain separate.; Risk: Package integration does not prove provider behavior or production operations. | Use the catalogued Campaign release for usability and target-provider acceptance.<br>**Proof:** Run the complete journey with safe data and the target-like mail service. |
| **files.managed_attachments**<br>Store and resolve managed Campaign attachments on durable storage. | `verified` | test/current_workspace: govoplan-files/tests (14 tests passed); test/current_workspace: govoplan-campaign/tests/test_attachment_building.py | Condition: Deployment provides a durable storage root.; Gap: Target backup and restore are not verified.; Risk: Node-local storage prevents safe independent API scaling. | Use durable local storage for the pilot and assess object/shared storage before scaling.<br>**Proof:** Back up and restore files together with database references. |
| **mail.smtp_imap**<br>Send Campaign mail through SMTP and optionally append sent messages through IMAP. | `available_unconfigured` | test/current_workspace: govoplan-mail/tests (22 tests passed); documentation/documented_model: govoplan-campaign/docs/CAMPAIGN_DELIVERY_RUNBOOK.md | Condition: Use a dedicated non-production service account and safe recipients.; Gap: No target provider, TLS chain, throttling or bounce/reply process was exercised.; Risk: Ambiguous provider outcomes can cause duplicate-send risk if reconciled incorrectly. | Run target-like interoperability and failure drills before production use.<br>**Proof:** Prove SMTP acceptance, IMAP append, throttling and outcome reconciliation. |
| **addresses.recipient_sources**<br>Select reusable address lists as Campaign recipient sources. | `available_unconfigured` | test/current_workspace: govoplan-addresses/tests (14 tests passed) | Condition: Enable the Addresses module explicitly.; Gap: Addresses is disabled in the pinned root profile.; Risk: Recipient governance may differ between source data and frozen Campaign evidence. | Enable only when reusable lists are a pilot requirement.<br>**Proof:** Build a Campaign from a source list and verify immutable recipient provenance. |
| **audit.local**<br>Retain tenant/system audit evidence and retry governed audit events. | `verified` | test/current_workspace: govoplan-audit/tests (5 tests passed) | Condition: Conclusion covers local database evidence only.; Gap: No central sink, retention enforcement or tamper-evident archive is verified.; Risk: Local audit evidence may not satisfy organizational records or SIEM requirements. | Define retention and export requirements before production approval.<br>**Proof:** Exercise privileged-event review, retention and any required external export. |
| **identity.federation**<br>Integrate external LDAP/AD, OIDC/SAML or SCIM identity infrastructure. | `scaffold` | documentation/documented_model: govoplan-idm/README.md | Gap: No end-to-end provider connector or federated login is verified.; Risk: Federation-dependent organizations cannot use the current pilot composition without extra implementation. | Use local pilot accounts or assess and implement the selected provider path.<br>**Proof:** Run provider metadata, login/provisioning, deprovisioning and failure tests. |
| **compliance.export_control**<br>Screen persons and organizations against embargo/sanctions lists with review evidence. | `planned` | issue/documented_model: https://git.add-ideas.de/GovOPlaN/govoplan/issues/12 | Gap: No provider, list provenance, match policy, review flow or legal evidence exists.; Risk: The current composition must not be represented as performing export-control screening. | Keep outside pilot claims until the user story is implemented and legally validated.<br>**Proof:** Validate list ingestion, versioning, matching, false-positive review and audit evidence. |
| **workflow**<br>Orchestrate the journey through Workflow. | `planned` | observation/documented_model: Assessment scope (Explicitly postponed) | Gap: Workflow is outside this assessment.; Risk: Including it would overstate the assessed composition. | Do not enable or claim Workflow for this reference pilot.<br>**Proof:** Reassess in a later Workflow-focused composition. |
## Infrastructure matrix
| Requirement | Status | Evidence | Conditions and gaps | Recommendation and proof |
| --- | --- | --- | --- | --- |
| **runtime.web_api**<br>Serve matching WebUI and API artifacts with health endpoints. | `verified` | test/committed_source: govoplan-core/tests/test_module_system.py; route/committed_source: govoplan-core/src/govoplan_core/server/fastapi.py#/health | Condition: Materialize the matching catalogued artifacts in the target.; Gap: No production image or service bundle is supplied by the profile.; Risk: Editable source processes are unsuitable as a production artifact. | Install matching catalogued WebUI/API refs and supervise them as immutable artifacts.<br>**Proof:** Deploy the built artifacts and run health/module-route checks. |
| **runtime.worker**<br>Run durable asynchronous Campaign jobs. | `available_unconfigured` | configuration/current_workspace: govoplan/tools/launch/launch-production-like-dev.sh | Condition: Redis and a supervised worker are required when Celery is enabled.; Gap: Target heartbeat, restart and queue-age alerting are not proved.; Risk: Queued work can stall silently without monitoring. | Start one worker for the pilot and split queues only after measurement.<br>**Proof:** Interrupt and restart a worker while preserving job/reconciliation safety. |
| **runtime.scheduler**<br>Run periodic recovery and cleanup safely. | `partial` | test/committed_source: govoplan-calendar/tests/test_outbox.py (Committed and pushed after the catalogued Calendar v0.1.8 tag) | Condition: Calendar outbox and recovery work is remote-integrated source but not stable-package-integrated.; Gap: No distributed leader election or target supervision is established.; Risk: Multiple schedulers can duplicate periodic dispatch without locking. | Omit from the Campaign-only pilot or run one supervised instance.<br>**Proof:** Prove missed-schedule recovery and single-leader behavior. |
| **data.postgresql**<br>Persist application state in PostgreSQL with explicit migrations. | `verified` | configuration/committed_source: govoplan/dev/postgres; test/committed_source: govoplan/tools/checks/postgres-integration-check.py | Condition: Target database remains deployment-owned.; Gap: HA, patching, WAL policy and capacity are not assessed.; Risk: A single unprotected database is a system-wide failure point. | Use managed or dedicated PostgreSQL with explicit migration and backup controls.<br>**Proof:** Run migrations and restore a target-like database. |
| **queue.redis**<br>Provide the Celery broker and queue persistence. | `available_unconfigured` | configuration/current_workspace: govoplan/dev/production-like/docker-compose.yml#redis | Gap: Authentication, TLS, eviction, HA and queue-loss policy are not assessed.; Risk: Broker loss or eviction can delay work even when database business state survives. | Configure private persistent Redis and monitor queue age/depth.<br>**Proof:** Exercise broker interruption and worker recovery. |
| **storage.local**<br>Persist managed files on a durable single-node/shared path. | `verified` | contract/committed_source: govoplan-files/src/govoplan_files/backend/storage/backends.py | Condition: Path is durable, private, writable and backed up.; Gap: Node-local storage cannot support independent API replicas.; Risk: Files can be lost or become inconsistent with database state. | Use for a bounded pilot only with coordinated backup.<br>**Proof:** Restore files and verify all database references. |
| **storage.object**<br>Use S3-compatible storage for independently scalable file persistence. | `partial` | test/current_workspace: govoplan-files/tests/test_connector_providers.py | Gap: No chosen target service or storage-backend interoperability drill.; Risk: Provider semantics, CA or lifecycle mismatch can break file access/retention. | Select and exercise the target object store before horizontal scaling.<br>**Proof:** Upload, retrieve, version, back up and restore representative objects. |
| **edge.proxy_tls**<br>Terminate HTTPS and enforce proxy/security policy. | `external_system` | route/committed_source: govoplan-ops/src/govoplan_ops/backend/api/v1/routes.py#deployment-security | Gap: No proxy, certificates, renewal, header or request-limit configuration is shipped here.; Risk: Incorrect proxy/cookie/CORS configuration can expose sessions or block legitimate use. | Supply and monitor the edge through the target platform.<br>**Proof:** Run external TLS/header/cookie/CORS and upload-limit tests. |
| **security.secret_store**<br>Inject and rotate master, database, mail and connector secrets. | `external_system` | configuration/committed_source: govoplan/.env.example | Gap: No target secret manager or rotation drill is selected.; Risk: Loss of the master key makes encrypted credentials unavailable; leakage compromises connectors. | Use target-native secret injection and document rotation/recovery.<br>**Proof:** Rotate a non-production credential and recover from a protected backup. |
| **identity.access**<br>Authenticate users and enforce tenant-scoped authorization through the selected identity mode. | `verified` | test/committed_source: govoplan-access/tests/test_auth_dependencies.py; test/committed_source: govoplan-core/tests/test_api_smoke.py#cookie-session-csrf | Condition: The bounded pilot accepts local GovOPlaN accounts.; Gap: Target MFA, federation, provisioning, and joiner/mover/leaver requirements are not assessed.; Risk: A local-only identity topology may not satisfy institutional production policy. | Use controlled local pilot accounts and assess the mandatory production identity topology separately.<br>**Proof:** Exercise login, role change, account suspension, protected bootstrap, and break-glass recovery in the target. |
| **connectors.mail**<br>Reach the selected SMTP/IMAP and other external connector endpoints under explicit network and provider policy. | `available_unconfigured` | test/current_workspace: govoplan-mail/tests (Protocol adapters have direct tests; no target provider was exercised); documentation/documented_model: govoplan-campaign/docs/CAMPAIGN_DELIVERY_RUNBOOK.md | Condition: The deployment supplies DNS, egress, proxy, CA trust, scoped service accounts, and provider limits.; Gap: No target endpoint, TLS chain, throttling, sender policy, bounce/reply path, or disclosure agreement is assessed.; Risk: Provider rejection, delay, or ambiguous outcomes can affect delivery and evidence completeness. | Use a dedicated safe provider account for the pilot and require target interoperability evidence before production.<br>**Proof:** Exercise target-like SMTP acceptance, IMAP append, throttling, outage, retry, and reconciliation through the approved network path. |
| **operations.monitoring**<br>Detect API, database, worker, queue, storage and delivery degradation. | `partial` | route/committed_source: govoplan-ops/src/govoplan_ops/backend/api/v1/routes.py#/ops/readiness; contract/committed_source: govoplan-core/src/govoplan_core/server/fastapi.py#slow-request-logging | Gap: No metrics exporter, log collector, dashboards, alert routes or SLO is verified.; Risk: Failures and queue backlog can remain unnoticed. | Integrate external monitoring before small production.<br>**Proof:** Trigger each readiness/delivery failure and verify an actionable alert. |
| **operations.audit**<br>Retain, monitor, review, and where required export security and business audit evidence. | `partial` | test/current_workspace: govoplan-audit/tests (Local audit persistence and retry behavior are exercised) | Condition: Local database audit evidence is part of coordinated backup and access review.; Gap: Target retention enforcement, tamper-evident export, SIEM integration, alerting, and privileged review are not verified.; Risk: Local evidence alone may not meet institutional security, records, or incident-response requirements. | Define the target audit retention, export, monitoring, and review controls before production approval.<br>**Proof:** Exercise privileged-event review, retention, export failure/retry, and target SIEM or archive ingestion. |
| **operations.backup_restore**<br>Back up and restore database, files, configuration and keys as a coherent service. | `partial` | documentation/documented_model: govoplan-core/docs/DEPLOYMENT_OPERATOR_GUIDE.md; issue/documented_model: https://git.add-ideas.de/GovOPlaN/govoplan-core/issues/29 | Gap: No target full-service restore drill or measured RPO/RTO exists.; Risk: Partial restore can produce missing files, unusable secrets or inconsistent evidence. | Treat Core #29 and a target restore drill as a production gate.<br>**Proof:** Restore the whole service into an isolated environment and measure it. |
| **operations.disaster_recovery**<br>Recover the service after site or dependency loss within agreed RPO/RTO. | `not_assessed` | absence/current_workspace: No target DR plan or exercise evidence supplied | Gap: RPO/RTO, off-site copies, recovery order, failover, communications and exercise schedule are unknown.; Risk: Service and evidence may be unrecoverable after a major incident. | Define and exercise DR before any availability commitment.<br>**Proof:** Run a documented end-to-end recovery exercise. |
## Data flows and trust boundaries
| Flow | From → to | Data | Trust boundary | Controls |
| --- | --- | --- | --- | --- |
| `browser.api` | User browser → Reverse proxy and GovOPlaN WebUI/API | Session and CSRF cookies; Campaign content; Recipient personal data; Managed files | Client/public to application | HTTPS; Exact CORS origins; Secure cookies; Tenant and RBAC enforcement; Request limits |
| `api.database` | GovOPlaN API and workers → PostgreSQL | Tenant and identity records; Campaign drafts, snapshots and jobs; Connector metadata; Audit evidence | Application to primary state store | Dedicated database identity; Private or encrypted transport; Migrations; Backup and retention |
| `api.queue.worker` | GovOPlaN API → Redis and Celery worker | Job identifiers; Queue routing and retry metadata | Request plane to asynchronous processing plane | Private authenticated broker; Bounded payloads; Idempotent claims; Queue monitoring |
| `worker.mail` | GovOPlaN Campaign worker → External SMTP and IMAP services | Recipient addresses; Message bodies; Attachments; Sent-message copy | GovOPlaN to external communication provider | Scoped service account; TLS and CA policy; Sender and recipient policy; Rate limits; Outcome reconciliation |
| `worker.connectors` | GovOPlaN connector worker → External address, file, object or calendar service | Addresses; Files and provenance; Calendar resources | GovOPlaN to organizational/external content systems | Explicit sync direction; Scoped credentials; Endpoint allow-list; Provenance; Conflict and reconciliation policy |
## Risks and residual risks
| Risk | Impact | Treatment | Owner | Residual risk |
| --- | --- | --- | --- | --- |
| **risk.reproducibility**<br>The signed package selection is reproducible but has not been accepted as an installed target composition. | Installation or configuration drift can still produce uncertain deployed behavior. | Materialize the signed catalog in an isolated target and run installed-artifact acceptance gates. | unassigned | Module and environment differences still require release-environment verification. |
| **risk.delivery_provider**<br>Target SMTP/IMAP behavior and failure modes are unproved. | Failed, delayed or duplicate communication and incomplete evidence. | Run target-like interoperability, throttling and uncertainty drills. | unassigned | External provider outages and ambiguous outcomes remain operational risks. |
| **risk.recovery**<br>Backup/restore and disaster recovery are not demonstrated across all state and keys. | Irrecoverable or inconsistent service after loss. | Complete Core #29 and an isolated full-service restore/DR exercise. | unassigned | Recovery time and data loss remain bounded by the selected external infrastructure. |
## Recommendations
- Proceed only with a controlled internal Campaign pilot after the bounded proof checks pass.
- Use the minimal composition and enable Addresses only for an explicit reusable-recipient journey.
- Do not claim Workflow, export-control screening, identity federation or production DR as implemented.
- Treat installed-release acceptance, target mail proof, monitoring and a coherent restore drill as production gates.
## Proof-of-concept and promotion checks
1. Materialize the signed catalog into an isolated installation and rerun contract, migration and module-permutation gates against the installed artifacts.
2. Collect the isolated installation with the bounded installed-composition evidence contract; require exact enabled package/module versions, complete RECORD verification and immutable provenance anchored to this assessment.
3. Run a safe target-like Campaign through SMTP acceptance, IMAP append, reporting and audit.
4. Drill worker, Redis and ambiguous-delivery failures without duplicate sends.
5. Restore PostgreSQL, managed files, configuration and encrypted credentials and measure RPO/RTO.
6. Validate proxy/TLS, cookies/CORS, account bootstrap, secret redaction, monitoring and alert delivery.
7. Measure representative Campaign/file/queue/database load and external throttling.
8. Require separately issued, expiring and independently scope-authorized evidence before marking target environment, external provider or production approval proof as checked.
## Generation contract
This report is deterministic output from the schema-validated JSON companion.
The generator rejects duplicate JSON keys, schema drift, secret-bearing field
names, stale checked-in output, and oversized inputs. A new assessment or
release changes the canonical input hash and requires review of the affected
evidence and conclusions through the release-aware reassessment tool.
@@ -1,8 +1,14 @@
# GovOPlaN Capability and IT-Infrastructure Fit Assessment # Supporting Narrative: 2026-07-22 Capability and Infrastructure Assessment
> **Canonical report:** The schema-validated human report is generated from the
> machine-readable input at
> [`CAPABILITY_AND_INFRASTRUCTURE_FIT.generated.md`](CAPABILITY_AND_INFRASTRUCTURE_FIT.generated.md).
> This file retains the original hand-authored evidence narrative and operator
> guidance; it is not maintained as a second set of conclusions.
> **Pinned historical evidence:** This document assesses the exact 2026-07-22 > **Pinned historical evidence:** This document assesses the exact 2026-07-22
> Campaign composition below. It is intentionally not updated to describe later > Campaign composition below. It is intentionally not updated to describe later
> main-branch work. Use [Strategy Status](STRATEGY_STATUS.md) for the current > main-branch work. Use [Strategy Status](../../strategy/STRATEGY_STATUS.md) for the current
> cross-product reconciliation and create a new dated fit assessment for a new > cross-product reconciliation and create a new dated fit assessment for a new
> target composition. > target composition.
@@ -17,16 +23,16 @@
| Configuration basis | Root `.env.example` and the production-like development profile | | Configuration basis | Root `.env.example` and the production-like development profile |
| Scope | Campaign-centric internal pilot and small-production candidate | | Scope | Campaign-centric internal pilot and small-production candidate |
| Explicitly postponed | Workflow and workflow-driven user stories | | Explicitly postponed | Workflow and workflow-driven user stories |
| Machine-readable companion | [`capability-fit-current.json`](capability-fit-current.json) | | Machine-readable companion | [`capability-fit-current.json`](../../capability-fit-current.json) |
| Input schema | [`capability-fit.schema.json`](capability-fit.schema.json) | | Input schema | [`capability-fit.schema.json`](../../capability-fit.schema.json) |
**Snapshot notice:** this assessment remains valid only for the pinned **Snapshot notice:** this assessment remains valid only for the pinned
2026-07-22 composition above. Workflow Engine, the optional Workflow editor, 2026-07-22 composition above. Workflow Engine, the optional Workflow editor,
Datasources, Dataflow, Search, encryption contracts, and other later main-branch Datasources, Dataflow, Search, encryption contracts, and other later main-branch
work must not be inferred into this evidence record. The current product work must not be inferred into this evidence record. The current product
direction and implemented-state reconciliation are documented separately in direction and implemented-state reconciliation are documented separately in
the [Institutional Governance Target Architecture](INSTITUTIONAL_GOVERNANCE_TARGET_ARCHITECTURE.md) the [Institutional Governance Target Architecture](../../architecture/INSTITUTIONAL_GOVERNANCE_TARGET_ARCHITECTURE.md)
and [Strategy Status](STRATEGY_STATUS.md). and [Strategy Status](../../strategy/STRATEGY_STATUS.md).
This is a fit assessment, not a production approval or security certification. This is a fit assessment, not a production approval or security certification.
It deliberately does not infer implementation from a repository, issue, or It deliberately does not infer implementation from a repository, issue, or
@@ -404,7 +410,7 @@ that observation to the assessment and signed catalog:
``` ```
The collector follows the strict version `0.4.0` The collector follows the strict version `0.4.0`
[`installed-composition-evidence.schema.json`](installed-composition-evidence.schema.json) [`installed-composition-evidence.schema.json`](../../installed-composition-evidence.schema.json)
contract. It enumerates all installed distributions whose normalized name starts contract. It enumerates all installed distributions whose normalized name starts
with `govoplan-`, compares the enabled assessed package and module-manifest with `govoplan-`, compares the enabled assessed package and module-manifest
versions, and identifies missing, duplicate and extra GovOPlaN distributions. versions, and identifies missing, duplicate and extra GovOPlaN distributions.
@@ -487,14 +493,14 @@ unchecked boundary.
Installed evidence cannot establish target acceptance, accessibility, privacy, Installed evidence cannot establish target acceptance, accessibility, privacy,
security, operations, recovery, an external provider, or production use. These security, operations, recovery, an external provider, or production use. These
scopes use a separate, expiring scopes use a separate, expiring
[`capability-fit-boundary-evidence.schema.json`](capability-fit-boundary-evidence.schema.json) [`capability-fit-boundary-evidence.schema.json`](../../capability-fit-boundary-evidence.schema.json)
bundle. The bundle is bound to the assessment ID, assessment release and exact bundle. The bundle is bound to the assessment ID, assessment release and exact
installed-evidence SHA-256 digest. It contains only opaque subject/control/result installed-evidence SHA-256 digest. It contains only opaque subject/control/result
IDs and content hashes, not endpoints, credentials, people or raw result files. IDs and content hashes, not endpoints, credentials, people or raw result files.
Boundary evidence is accepted only when at least one Ed25519 signature validates Boundary evidence is accepted only when at least one Ed25519 signature validates
against a separately provisioned against a separately provisioned
[`capability-fit-proof-authority-keyring.schema.json`](capability-fit-proof-authority-keyring.schema.json). [`capability-fit-proof-authority-keyring.schema.json`](../../capability-fit-proof-authority-keyring.schema.json).
Each authority key explicitly lists the scopes it may attest. Target, Each authority key explicitly lists the scopes it may attest. Target,
accessibility, privacy, security, operations, recovery, and provider claims use accessibility, privacy, security, operations, recovery, and provider claims use
`passed` or `failed`; production claims use `approved` or `rejected`. `passed` or `failed`; production claims use `approved` or `rejected`.
@@ -511,7 +517,7 @@ the tool's deterministic canonicalization.
`tools/assessments/boundary-evidence.py` is the bounded issuance path. It `tools/assessments/boundary-evidence.py` is the bounded issuance path. It
accepts a private target-run manifest conforming to accepts a private target-run manifest conforming to
[`capability-fit-boundary-run.schema.json`](capability-fit-boundary-run.schema.json), [`capability-fit-boundary-run.schema.json`](../../capability-fit-boundary-run.schema.json),
hashes each retained result file without following a final-component symlink, hashes each retained result file without following a final-component symlink,
and excludes all paths and raw results from the signed receipt. Issuance is and excludes all paths and raw results from the signed receipt. Issuance is
refused unless an independently trusted catalog, exact installed payload, refused unless an independently trusted catalog, exact installed payload,
@@ -521,7 +527,7 @@ is authorized for the full proof interval; catalog and installer key reuse is
rejected. The command immediately verifies its own result and atomically writes rejected. The command immediately verifies its own result and atomically writes
both the proof and a sanitized review. The complete operator procedure and both the proof and a sanitized review. The complete operator procedure and
recovery measurement definition are in recovery measurement definition are in
[`TARGET_MATURITY_EVIDENCE_RUNBOOK.md`](TARGET_MATURITY_EVIDENCE_RUNBOOK.md). [`TARGET_MATURITY_EVIDENCE_RUNBOOK.md`](../../operations/TARGET_MATURITY_EVIDENCE_RUNBOOK.md).
```bash ```bash
./.venv/bin/python tools/assessments/capability-fit.py \ ./.venv/bin/python tools/assessments/capability-fit.py \
@@ -599,7 +605,7 @@ separate from production approval and from provider-specific acceptance.
Both authority keyrings are governance trust roots. Installer receipt keys use Both authority keyrings are governance trust roots. Installer receipt keys use
the strict the strict
[`installer-receipt-authority-keyring.schema.json`](installer-receipt-authority-keyring.schema.json) [`installer-receipt-authority-keyring.schema.json`](../../installer-receipt-authority-keyring.schema.json)
contract and may attest only `installed_release_origin`; their public material contract and may attest only `installed_release_origin`; their public material
must not be reused by catalog or boundary-proof authorities. Do not download or must not be reused by catalog or boundary-proof authorities. Do not download or
generate them from the proof bundle being checked. The checker rejects generate them from the proof bundle being checked. The checker rejects
@@ -623,9 +629,9 @@ journey, source tests, or signed release metadata.
## Evidence used in this slice ## Evidence used in this slice
- [Production-like profile](../dev/production-like/README.md) and - [Production-like profile](../../../dev/production-like/README.md) and
[Compose dependencies](../dev/production-like/docker-compose.yml) [Compose dependencies](../../../dev/production-like/docker-compose.yml)
- [Module contracts and install boundaries](MODULE_CONTRACTS_AND_INSTALLS.md) - [Module contracts and install boundaries](../../operations/MODULE_CONTRACTS_AND_INSTALLS.md)
- [Core deployment operator guide](https://git.add-ideas.de/GovOPlaN/govoplan-core/src/branch/main/docs/DEPLOYMENT_OPERATOR_GUIDE.md) - [Core deployment operator guide](https://git.add-ideas.de/GovOPlaN/govoplan-core/src/branch/main/docs/DEPLOYMENT_OPERATOR_GUIDE.md)
- [Ops scalability profiles](https://git.add-ideas.de/GovOPlaN/govoplan-ops/src/branch/main/docs/SCALABILITY_PROFILES.md) - [Ops scalability profiles](https://git.add-ideas.de/GovOPlaN/govoplan-ops/src/branch/main/docs/SCALABILITY_PROFILES.md)
- Actual module manifests in the pinned repositories and the static contract - Actual module manifests in the pinned repositories and the static contract
@@ -0,0 +1,91 @@
# DSAR Provider Coverage
This generated matrix is enforced by `tools/checks/check-dsar-coverage.py`.
A migration-owning module must register and document its canonical DSAR provider.
Every other active module requires a reviewed explanation of why it owns no
persistent subject-data store. Adding a migration invalidates that explanation.
- Active modules: 72
- Registered and documented DSAR providers: 48
- Reviewed no-store rationales: 24
- Unexplained coverage gaps: 0
| Module | Repository | Persistence | Coverage | Rationale |
| --- | --- | --- | --- | --- |
| `access` | `govoplan-access` | Migration-owned | Provider | Provider `privacy.dsar.access` is registered and documented. |
| `addresses` | `govoplan-addresses` | Migration-owned | Provider | Provider `privacy.dsar.addresses` is registered and documented. |
| `admin` | `govoplan-admin` | Migration-owned | Provider | Provider `privacy.dsar.admin` is registered and documented. |
| `approvals` | `govoplan-approvals` | Migration-owned | Provider | Provider `privacy.dsar.approvals` is registered and documented. |
| `assets` | `govoplan-assets` | No module migration | Reviewed no-store rationale | Contract-only module: asset persistence and lifecycle APIs are not implemented; reassess before adding a migration-owned store. |
| `audit` | `govoplan-audit` | Migration-owned | Provider | Provider `privacy.dsar.audit` is registered and documented. |
| `booking` | `govoplan-booking` | No module migration | Reviewed no-store rationale | Contract-only module: booking persistence and reservation workflows are not implemented; reassess before adding a migration-owned store. |
| `calendar` | `govoplan-calendar` | Migration-owned | Provider | Provider `privacy.dsar.calendar` is registered and documented. |
| `campaigns` | `govoplan-campaign` | Migration-owned | Provider | Provider `privacy.dsar.campaigns` is registered and documented. |
| `cases` | `govoplan-cases` | Migration-owned | Provider | Provider `privacy.dsar.cases` is registered and documented. |
| `certificates` | `govoplan-certificates` | No module migration | Reviewed no-store rationale | Contract-only module: certificate issuance and revocation persistence are not implemented; reassess before adding a migration-owned store. |
| `committee` | `govoplan-committee` | Migration-owned | Provider | Provider `privacy.dsar.committee` is registered and documented. |
| `connectors` | `govoplan-connectors` | Migration-owned | Provider | Provider `privacy.dsar.connectors` is registered and documented. |
| `consultation` | `govoplan-consultation` | No module migration | Reviewed no-store rationale | Contract-only module: consultation submissions and evaluation persistence are not implemented; reassess before adding a migration-owned store. |
| `contracts` | `govoplan-contracts` | No module migration | Reviewed no-store rationale | Contract-only module: contract, amendment, and obligation persistence are not implemented; reassess before adding a migration-owned store. |
| `dashboard` | `govoplan-dashboard` | Migration-owned | Provider | Provider `privacy.dsar.dashboard` is registered and documented. |
| `dataflow` | `govoplan-dataflow` | Migration-owned | Provider | Provider `privacy.dsar.dataflow` is registered and documented. |
| `datasources` | `govoplan-datasources` | Migration-owned | Provider | Provider `privacy.dsar.datasources` is registered and documented. |
| `decisions` | `govoplan-decisions` | Migration-owned | Provider | Provider `privacy.dsar.decisions` is registered and documented. |
| `dist_lists` | `govoplan-dist-lists` | Migration-owned | Provider | Provider `privacy.dsar.dist_lists` is registered and documented. |
| `dms` | `govoplan-dms` | No module migration | Reviewed no-store rationale | Stateless integration-preview module: DMS retains no document, person, credential, or provider-response store; Files and Records remain the subject-data owners. Reassess before persisting a target binding, plan, receipt, or diagnostic. |
| `docs` | `govoplan-docs` | Migration-owned | Provider | Provider `privacy.dsar.docs` is registered and documented. |
| `encryption` | `govoplan-encryption` | Migration-owned | Provider | Provider `privacy.dsar.encryption` is registered and documented. |
| `erp` | `govoplan-erp` | No module migration | Reviewed no-store rationale | Stateless integration-contract module: ERP retains no invoice, payable, plan, booking observation, provider response, or credential store; Procurement, Payments, Ledger, Files, and Audit remain the subject-data owners. Reassess before persisting a target binding, plan, receipt, reconciliation decision, or diagnostic. |
| `evaluation` | `govoplan-evaluation` | No module migration | Reviewed no-store rationale | Contract-only module: evaluation runs, responses, and scores are not persisted; reassess before adding a migration-owned store. |
| `facilities` | `govoplan-facilities` | No module migration | Reviewed no-store rationale | Contract-only module: facility and maintenance persistence are not implemented; reassess before adding a migration-owned store. |
| `files` | `govoplan-files` | Migration-owned | Provider | Provider `privacy.dsar.files` is registered and documented. |
| `fit_connect` | `govoplan-fit-connect` | No module migration | Reviewed no-store rationale | Stateless transport-contract module: FIT-Connect retains no submission, attachment, receipt, acknowledgement plan, key, provider response, or diagnostic store; the owning Service, Forms, Cases, Files, and Audit workflows remain responsible for subject data. Reassess before persisting any ingress or event-log evidence. |
| `forms` | `govoplan-forms` | Migration-owned | Provider | Provider `privacy.dsar.forms` is registered and documented. |
| `forms_runtime` | `govoplan-forms-runtime` | Migration-owned | Provider | Provider `privacy.dsar.forms_runtime` is registered and documented. |
| `grants` | `govoplan-grants` | No module migration | Reviewed no-store rationale | Contract-only module: grant applications, awards, and monitoring are not persisted; reassess before adding a migration-owned store. |
| `helpdesk` | `govoplan-helpdesk` | Migration-owned | Provider | Provider `privacy.dsar.helpdesk` is registered and documented. |
| `identity` | `govoplan-identity` | Migration-owned | Provider | Provider `privacy.dsar.identity` is registered and documented. |
| `identity_trust` | `govoplan-identity-trust` | Migration-owned | Provider | Provider `privacy.dsar.identity_trust` is registered and documented. |
| `idm` | `govoplan-idm` | Migration-owned | Provider | Provider `privacy.dsar.idm` is registered and documented. |
| `inspections` | `govoplan-inspections` | No module migration | Reviewed no-store rationale | Contract-only module: inspections, findings, and measures are not persisted; reassess before adding a migration-owned store. |
| `learning` | `govoplan-learning` | No module migration | Reviewed no-store rationale | Contract-only module: learning offers, enrollment, and completion are not persisted; reassess before adding a migration-owned store. |
| `mail` | `govoplan-mail` | Migration-owned | Provider | Provider `privacy.dsar.mail` is registered and documented. |
| `mandates` | `govoplan-mandates` | Migration-owned | Provider | Provider `privacy.dsar.mandates` is registered and documented. |
| `notifications` | `govoplan-notifications` | Migration-owned | Provider | Provider `privacy.dsar.notifications` is registered and documented. |
| `ops` | `govoplan-ops` | No module migration | Reviewed no-store rationale | Projection-only module: Ops reads bounded platform and provider status; durable recovery evidence remains owned by Core and domain modules. |
| `organizations` | `govoplan-organizations` | Migration-owned | Provider | Provider `privacy.dsar.organizations` is registered and documented. |
| `parties` | `govoplan-parties` | Migration-owned | Provider | Provider `privacy.dsar.parties` is registered and documented. |
| `payments` | `govoplan-payments` | Migration-owned | Provider | Provider `privacy.dsar.payments` is registered and documented. |
| `permits` | `govoplan-permits` | No module migration | Reviewed no-store rationale | Contract-only module: permit applications, assessments, and decisions are not persisted; reassess before adding a migration-owned store. |
| `policy` | `govoplan-policy` | Migration-owned | Provider | Provider `privacy.dsar.policy` is registered and documented. |
| `poll` | `govoplan-poll` | Migration-owned | Provider | Provider `privacy.dsar.poll` is registered and documented. |
| `portal` | `govoplan-portal` | No module migration | Reviewed no-store rationale | Projection-only module: Portal stores no applicant records; Services, Forms Runtime, Cases, and Postbox own and export authoritative subject data. |
| `postbox` | `govoplan-postbox` | Migration-owned | Provider | Provider `privacy.dsar.postbox` is registered and documented. |
| `procurement` | `govoplan-procurement` | No module migration | Reviewed no-store rationale | Contract-only module: procurement procedures, tenders, and awards are not persisted; reassess before adding a migration-owned store. |
| `projects` | `govoplan-projects` | Migration-owned | Provider | Provider `privacy.dsar.projects` is registered and documented. |
| `quick_access` | `govoplan-quick-access` | Migration-owned | Provider | Provider `privacy.dsar.quick_access` is registered and documented. |
| `records` | `govoplan-records` | Migration-owned | Provider | Provider `privacy.dsar.records` is registered and documented. |
| `reporting` | `govoplan-reporting` | Migration-owned | Provider | Provider `privacy.dsar.reporting` is registered and documented. |
| `resources` | `govoplan-resources` | No module migration | Reviewed no-store rationale | Contract-only module: resource catalog and allocation persistence are not implemented; reassess before adding a migration-owned store. |
| `rest` | `govoplan-rest` | No module migration | Reviewed no-store rationale | Transport-only module: REST binds explicitly published functions and owns no domain or subject-data store. |
| `risk_compliance` | `govoplan-risk-compliance` | Migration-owned | Provider | Provider `privacy.dsar.risk_compliance` is registered and documented. |
| `scheduling` | `govoplan-scheduling` | Migration-owned | Provider | Provider `privacy.dsar.scheduling` is registered and documented. |
| `search` | `govoplan-search` | Migration-owned | Provider | Provider `privacy.dsar.search` is registered and documented. |
| `services` | `govoplan-services` | Migration-owned | Provider | Provider `privacy.dsar.services` is registered and documented. |
| `soap` | `govoplan-soap` | No module migration | Reviewed no-store rationale | Transport-only module: SOAP binds explicitly published operations and owns no domain or subject-data store. |
| `tasks` | `govoplan-tasks` | Migration-owned | Provider | Provider `privacy.dsar.tasks` is registered and documented. |
| `templates` | `govoplan-templates` | Migration-owned | Provider | Provider `privacy.dsar.templates` is registered and documented. |
| `tenancy` | `govoplan-tenancy` | No module migration | Reviewed no-store rationale | Orchestration module: tenant lifecycle and settings use Core-owned storage; Access covers account and membership subject data. |
| `tickets` | `govoplan-tickets` | Migration-owned | Provider | Provider `privacy.dsar.tickets` is registered and documented. |
| `transparency` | `govoplan-transparency` | No module migration | Reviewed no-store rationale | Contract-only module: requests, disclosure reviews, and publications are not persisted; reassess before adding a migration-owned store. |
| `views` | `govoplan-views` | Migration-owned | Provider | Provider `privacy.dsar.views` is registered and documented. |
| `voting` | `govoplan-voting` | Migration-owned | Provider | Provider `privacy.dsar.voting` is registered and documented. |
| `wiki` | `govoplan-wiki` | Migration-owned | Provider | Provider `privacy.dsar.wiki` is registered and documented. |
| `workflow` | `govoplan-workflow` | No module migration | Reviewed no-store rationale | Presentation-only module: Workflow edits and projects Workflow Engine state; Workflow Engine owns persistence and DSAR coverage. |
| `workflow_engine` | `govoplan-workflow-engine` | Migration-owned | Provider | Provider `privacy.dsar.workflow_engine` is registered and documented. |
| `xrechnung` | `govoplan-xrechnung` | No module migration | Reviewed no-store rationale | Stateless validation-contract module: XRechnung persists no invoice, report, diagnostic, or handoff; the invoking Files, Procurement, or Payments workflow remains the subject-data owner. Reassess before adding a validation store. |
Provider search, export minimization, retention, and erasure behavior remains
documented and tested by each owning module. This matrix verifies adoption and
ownership coverage; Core continues to test disabled providers, partial failure,
retry, authorization evidence, and horizontally coordinated execution.
@@ -1,19 +1,24 @@
# GovOPlaN Interface Surface Inventory And Rollout # GovOPlaN Interface Surface Inventory And Rollout
> **Pinned snapshot:** This inventory records the source-derived state reviewed
> on 2026-08-03. It is retained as evidence, not maintained as the current
> rollout ledger. Generate a new inventory and use Gitea issues for current
> implementation state.
This is the initial evidence inventory for the product-wide interface pattern This is the initial evidence inventory for the product-wide interface pattern
language. It records code contributions, not an assertion that every listed language. It records code contributions, not an assertion that every listed
surface is complete, enabled in a deployment, usable, or compliant. surface is complete, enabled in a deployment, usable, or compliant.
The applicable design contract is The applicable design contract is
[`INTERFACE_PATTERN_LANGUAGE.md`](INTERFACE_PATTERN_LANGUAGE.md). [`INTERFACE_PATTERN_LANGUAGE.md`](../../architecture/INTERFACE_PATTERN_LANGUAGE.md).
## Snapshot And Method ## Snapshot And Method
The source-derived inventory command is documented in The source-derived inventory command is documented in
[`PLATFORM_CONTROL_PLANE.md`](PLATFORM_CONTROL_PLANE.md). It produces [`PLATFORM_CONTROL_PLANE.md`](../../architecture/PLATFORM_CONTROL_PLANE.md). It produces
machine-readable field, label, translation, route, API-reference, and module machine-readable field, label, translation, route, API-reference, and module
manifest evidence. This hand-maintained document remains the reviewed product manifest evidence. This hand-maintained document is the reviewed interpretation
interpretation and rollout ledger; generated evidence does not replace it. of that snapshot; generated evidence does not retroactively change it.
Snapshot refreshed: 2026-08-03. Snapshot refreshed: 2026-08-03.
@@ -104,7 +109,9 @@ semantics as authenticated navigation routes.
| `/scheduling/public/:requestId/:token` | Scheduling | Public signed token | Public participation | Scheduling #8 complete in `c17cbda` | | `/scheduling/public/:requestId/:token` | Scheduling | Public signed token | Public participation | Scheduling #8 complete in `c17cbda` |
| `/search` | Search | `search:result:read` | Keyboard-first global/context overlay and full results fallback | Search pattern migration complete in [Search #4](https://git.add-ideas.de/GovOPlaN/govoplan-search/issues/4); durable evidence in `govoplan-search/docs/INTERFACE_PATTERN_MIGRATION.md` | | `/search` | Search | `search:result:read` | Keyboard-first global/context overlay and full results fallback | Search pattern migration complete in [Search #4](https://git.add-ideas.de/GovOPlaN/govoplan-search/issues/4); durable evidence in `govoplan-search/docs/INTERFACE_PATTERN_MIGRATION.md` |
| `/templates` | Templates | Template read/write/publish/render/admin | Governed library, immutable-revision editor, compatibility preview, and render evidence | Templates pattern migration complete in [Templates #5](https://git.add-ideas.de/GovOPlaN/govoplan-templates/issues/5), commit `72fafa2` | | `/templates` | Templates | Template read/write/publish/render/admin | Governed library, immutable-revision editor, compatibility preview, and render evidence | Templates pattern migration complete in [Templates #5](https://git.add-ideas.de/GovOPlaN/govoplan-templates/issues/5), commit `72fafa2` |
| `/tickets` | Tickets | `tickets:ticket:read` | Governed operational queue/detail workspace with distinct report, triage, assignment, resolution, comment, reference and removal boundaries | Tickets vertical slice and pattern migration complete in [Tickets #1](https://git.add-ideas.de/GovOPlaN/govoplan-tickets/issues/1), release `v0.1.20` |
| `/voting` | Voting | `voting:ballot:read` | Governed ballot workspace | Voting pattern migration complete in [Voting #1](https://git.add-ideas.de/GovOPlaN/govoplan-voting/issues/1), commit `2625990` | | `/voting` | Voting | `voting:ballot:read` | Governed ballot workspace | Voting pattern migration complete in [Voting #1](https://git.add-ideas.de/GovOPlaN/govoplan-voting/issues/1), commit `2625990` |
| `/wiki` | Wiki | `wiki:page:read` | Governed space-tree/page workspace with draft editing, immutable revision comparison, publication, comments, typed references and archival | Native Wiki vertical slice and pattern migration implemented in [Wiki #1](https://git.add-ideas.de/GovOPlaN/govoplan-wiki/issues/1), release `v0.1.20` |
| `/workflow` | Workflow | Definition read or instance admin | Native BPMN editor, governed revision actions and execution evidence | Workflow pattern migration complete in [Workflow #15](https://git.add-ideas.de/GovOPlaN/govoplan-workflow/issues/15); durable evidence in `govoplan-workflow/docs/INTERFACE_PATTERN_MIGRATION.md` | | `/workflow` | Workflow | Definition read or instance admin | Native BPMN editor, governed revision actions and execution evidence | Workflow pattern migration complete in [Workflow #15](https://git.add-ideas.de/GovOPlaN/govoplan-workflow/issues/15); durable evidence in `govoplan-workflow/docs/INTERFACE_PATTERN_MIGRATION.md` |
## Final Module Closure Evidence ## Final Module Closure Evidence
@@ -318,8 +325,8 @@ The generated manifest snapshot reports no WebUI package for:
`govoplan-learning`, `govoplan-mandates`, `govoplan-parties`, `govoplan-learning`, `govoplan-mandates`, `govoplan-parties`,
`govoplan-permits`, `govoplan-poll`, `govoplan-procurement`, `govoplan-permits`, `govoplan-poll`, `govoplan-procurement`,
`govoplan-records`, `govoplan-resources`, `govoplan-rest`, `govoplan-records`, `govoplan-resources`, `govoplan-rest`,
`govoplan-services`, `govoplan-soap`, `govoplan-tickets`, `govoplan-services`, `govoplan-soap`, `govoplan-transparency`, and
`govoplan-transparency`, `govoplan-wiki`, and `govoplan-workflow-engine`. `govoplan-workflow-engine`.
Tenancy does provide composed administration surfaces despite having no direct Tenancy does provide composed administration surfaces despite having no direct
route. This section is only negative package evidence; connector-only, route. This section is only negative package evidence; connector-only,
@@ -16,8 +16,8 @@ deployment/evidence boundary.
The machine-readable contracts are: The machine-readable contracts are:
- [`backup-evidence.schema.json`](backup-evidence.schema.json); - [`backup-evidence.schema.json`](../backup-evidence.schema.json);
- [`backup-evidence-keyring.schema.json`](backup-evidence-keyring.schema.json). - [`backup-evidence-keyring.schema.json`](../backup-evidence-keyring.schema.json).
One evidence document is bound to the installation id, deployment profile, One evidence document is bound to the installation id, deployment profile,
topology subject, exact signed release manifest, image digests, and composition topology subject, exact signed release manifest, image digests, and composition
@@ -8,7 +8,7 @@ receives a working base system. Re-running the same tool repairs or
reconfigures that installation instead of creating unrelated state. reconfigures that installation instead of creating unrelated state.
The canonical product journey remains The canonical product journey remains
[System Administrator Lifecycle User Story](SYSTEM_ADMINISTRATOR_LIFECYCLE_USER_STORY.md). [System Administrator Lifecycle User Story](../strategy/SYSTEM_ADMINISTRATOR_LIFECYCLE_USER_STORY.md).
This document defines the deployer boundary and the first executable slice. This document defines the deployer boundary and the first executable slice.
The execution, topology, component-ownership and assurance modes are defined The execution, topology, component-ownership and assurance modes are defined
canonically in [Deployment Profiles](DEPLOYMENT_PROFILES.md). In particular, canonically in [Deployment Profiles](DEPLOYMENT_PROFILES.md). In particular,
@@ -99,6 +99,8 @@ The private installation directory contains:
| `existing-proxy.json` | Exact upstream, trusted-source, header, and health contract for an operator-owned proxy | | `existing-proxy.json` | Exact upstream, trusted-source, header, and health contract for an operator-owned proxy |
| `plan.json` | Latest desired-state diff and readiness findings | | `plan.json` | Latest desired-state diff and readiness findings |
| `receipt.json` | Last successfully applied immutable identities | | `receipt.json` | Last successfully applied immutable identities |
| `infrastructure-capabilities.json` | Deterministic non-secret capability states, endpoint metadata, secret references, consumers, and resumable post-install tasks |
| `infrastructure-dependency-inventory.json` | Owner-only, short-lived Ops evidence of actual module-owned configuration and data that depend on infrastructure capabilities |
| `distribution-manifest.json` | Canonical signed runtime/image selection adopted by the installer | | `distribution-manifest.json` | Canonical signed runtime/image selection adopted by the installer |
| `distribution-keyring.json` | Explicitly installed public trust anchor for runtime releases | | `distribution-keyring.json` | Explicitly installed public trust anchor for runtime releases |
| `backup-evidence.json` | Signed provider-neutral coordinated backup and isolated-restore receipt | | `backup-evidence.json` | Signed provider-neutral coordinated backup and isolated-restore receipt |
@@ -110,7 +112,27 @@ The private installation directory contains:
| `.deployment.lock` | Same-host operation exclusion | | `.deployment.lock` | Same-host operation exclusion |
The specification contract is The specification contract is
[`installation-spec.schema.json`](installation-spec.schema.json). [`installation-spec.schema.json`](../installation-spec.schema.json).
The API, workers, scheduler, and Ops read the capability receipt through the
same bounded Core validator. Configuration-package providers receive that typed
receipt in preflight context. Mail uses `mail.smtp` to offer an idempotent SMTP
profile plan and accepts only an existing credential-envelope reference; Files
uses `files.storage` to prove that the deployment-owned local/S3 runtime binding
already matches. Files deliberately blocks drift instead of rewriting process
environment or initiating an implicit object migration. Invalid receipts fail
closed, while a deployment without a mounted receipt continues to run but
cannot apply receipt-bound configuration fragments.
Enabled modules may also register a Core infrastructure-dependency provider.
The authorized Ops endpoint aggregates those providers without importing their
tables. Mail reports persisted SMTP endpoints, credential-binding counts and
legacy profiles; Files reports its runtime storage binding plus persisted blob
counts and byte totals grouped by backend. Ops reports the active PostgreSQL,
Redis coordination, ingress, and load-balancing runtime bindings. Provider output contains stable
references, bounded numeric metrics and required migration actions, never
credentials, endpoint secrets, tenant identifiers or file keys. A provider
failure makes the entire inventory incomplete.
Build the same dependency-free tool as one downloadable artifact: Build the same dependency-free tool as one downloadable artifact:
@@ -199,9 +221,9 @@ it can initialize a new volume; the actual HAProxy process retains the image's
non-root identity and runs read-only with all capabilities dropped. non-root identity and runs read-only with all capabilities dropped.
The manifest contract is The manifest contract is
[`runtime-distribution-manifest.schema.json`](runtime-distribution-manifest.schema.json), [`runtime-distribution-manifest.schema.json`](../runtime-distribution-manifest.schema.json),
and its separately distributed trust-anchor contract is and its separately distributed trust-anchor contract is
[`runtime-distribution-keyring.schema.json`](runtime-distribution-keyring.schema.json). [`runtime-distribution-keyring.schema.json`](../runtime-distribution-keyring.schema.json).
Publication is immutable: an existing Gitea release asset must have the same Publication is immutable: an existing Gitea release asset must have the same
size and SHA-256 digest or publication fails. size and SHA-256 digest or publication fails.
@@ -408,16 +430,30 @@ the supported topology and promotion path.
## Reconfiguration Semantics ## Reconfiguration Semantics
`installation.json` is desired state. `receipt.json` is the last successfully `installation.json` is desired state. `receipt.json` is the last successfully
applied state. `plan` compares their canonical hashes and service sets. applied state. `plan` compares their canonical hashes, service sets, and
infrastructure capability projections.
- Adding a managed component creates its service and persistent volume. - Adding a managed component creates its service and persistent volume.
- Removing a component removes its service container on apply. - Removing a component removes its service container on apply.
- Reconfiguring, replacing or removing a capability adds a review action that
names the prior and desired state/source, declared consumers, actual
provider-reported dependency records and each required migration action.
- The deployer blocks that change when provider inventory is missing,
incomplete, more than five minutes old, from another installation, timestamped
in the future, or does not cover every impacted capability. It never treats
installer-declared consumers as proof that persisted module state is absent.
- The inventory reports impact; it does not migrate or delete module-owned
configuration or data. Complete the reported preparation and collect again
immediately before apply.
- Volumes are retained by default; deleting data requires a separate, - Volumes are retained by default; deleting data requires a separate,
deliberately destructive workflow. deliberately destructive workflow.
- Existing generated credentials are retained unless an explicit future rotate - Existing generated credentials are retained unless an explicit future rotate
operation is requested. operation is requested.
- Private configuration changes are represented by a keyed fingerprint in the - Private configuration changes are represented by a keyed fingerprint in the
plan and receipt; plaintext values are never copied there. plan and receipt; plaintext values are never copied there.
- Capability documents contain sanitized scheme/host/port metadata and stable
`env:` references only. Credential values and secret-bearing URLs remain in
`secrets.env` or module-owned credential envelopes.
- Managed-to-external transitions require the new endpoint in the same - Managed-to-external transitions require the new endpoint in the same
operation. operation.
- Migrations run as a one-shot service before API/worker replacement. - Migrations run as a one-shot service before API/worker replacement.
@@ -430,6 +466,35 @@ applied state. `plan` compares their canonical hashes and service sets.
- Health must recover before a new receipt and applied-state snapshot are - Health must recover before a new receipt and applied-state snapshot are
committed. committed.
Compose mounts the capability document read-only into API and worker runtime
containers. The Kubernetes export projects the same document through a
dedicated ConfigMap and read-only file mount. Ops validates the bounded schema
before displaying configured, externally supplied, available-unconfigured, or
unavailable states and any pending post-install tasks.
Collect current dependency evidence with an API key whose principal has one of
the Ops read scopes:
```sh
export GOVOPLAN_OPS_API_KEY='<short-lived operator API key>'
python3 govoplan-deploy.pyz collect-infrastructure-inventory \
--directory /srv/govoplan/example
python3 govoplan-deploy.pyz doctor \
--directory /srv/govoplan/example
python3 govoplan-deploy.pyz apply \
--directory /srv/govoplan/example
unset GOVOPLAN_OPS_API_KEY
```
The command defaults to
`<public-url>/api/v1/ops/infrastructure/dependencies`; `--ops-url` may select an
explicit HTTPS endpoint (plain HTTP is accepted only on loopback). `apply`
refreshes the inventory automatically when `GOVOPLAN_OPS_API_KEY` is present.
Otherwise an already collected, current inventory may be used. The API key is
sent only as `X-API-Key`, is never written to the bundle, and the inventory file
is owner-readable only. Because it contains operational references and counts,
handle it as private evidence even though it contains no secret material.
Every apply operation is journalled before image pulls or runtime mutation. A Every apply operation is journalled before image pulls or runtime mutation. A
failure before migration may restore a verified previous bundle. Once migration failure before migration may restore a verified previous bundle. Once migration
starts, recovery is forward-only unless an independently verified database starts, recovery is forward-only unless an independently verified database
@@ -217,6 +217,11 @@ requirements. Missing dependency/interface providers and unsupported update
windows are surfaced before an operator adds the entry to a plan; installer windows are surfaced before an operator adds the entry to a plan; installer
preflight remains authoritative. preflight remains authoritative.
Catalog entries also carry the permission definitions declared by the tagged
module manifest. Admin groups and exposes their scopes before an install or
update is planned. This is disclosure only: installing a module does not grant
its permissions to an account, role, group, tenant, or service account.
Package lifecycle and availability are intentionally separate: Package lifecycle and availability are intentionally separate:
- install, update, and uninstall change the instance-wide package composition; - install, update, and uninstall change the instance-wide package composition;
@@ -2,7 +2,7 @@
The Core recovery ledger is a platform primitive, not automatic protection for The Core recovery ledger is a platform primitive, not automatic protection for
module-owned effects. The canonical, machine-checked inventory is module-owned effects. The canonical, machine-checked inventory is
[`recovery-operation-inventory.json`](recovery-operation-inventory.json). [`recovery-operation-inventory.json`](../recovery-operation-inventory.json).
## Classification Rules ## Classification Rules
@@ -277,4 +277,4 @@ test against the target ingress controller and network implementation.
This proves the bounded stateless-node-loss slice only. Session continuity, This proves the bounded stateless-node-loss slice only. Session continuity,
accepted-job redelivery, state-service failover, and coordinated restore remain accepted-job redelivery, state-service failover, and coordinated restore remain
separate target exercises whose signed evidence is governed by separate target exercises whose signed evidence is governed by
`docs/TARGET_MATURITY_EVIDENCE_RUNBOOK.md` and GovOPlaN #37. `docs/operations/TARGET_MATURITY_EVIDENCE_RUNBOOK.md` and GovOPlaN #37.
@@ -79,7 +79,7 @@ development depend on submodule updates.
Module release tags also publish wheels and WebUI tarballs to the organization Module release tags also publish wheels and WebUI tarballs to the organization
PyPI/npm registries. The meta release resolves exact versions into a hash-bound PyPI/npm registries. The meta release resolves exact versions into a hash-bound
package lock before producing the signed OCI runtime. See package lock before producing the signed OCI runtime. See
`docs/PACKAGE_REGISTRY_RELEASES.md`. Git tags remain source provenance; package `docs/operations/PACKAGE_REGISTRY_RELEASES.md`. Git tags remain source provenance; package
registries are reusable artifact transport; the signed runtime manifest and registries are reusable artifact transport; the signed runtime manifest and
digest-pinned images remain production authority. digest-pinned images remain production authority.
@@ -6,7 +6,7 @@ This document preserves and normalizes product ideas and user-story notes that
inform GovOPlaN without turning a private note file into a second backlog. inform GovOPlaN without turning a private note file into a second backlog.
Gitea issues remain the source of live work state; the stable platform direction Gitea issues remain the source of live work state; the stable platform direction
remains in [Platform Core Ideas](PLATFORM_CORE_IDEAS.md), the remains in [Platform Core Ideas](PLATFORM_CORE_IDEAS.md), the
[Connected Governance Platform Roadmap](CONNECTED_GOVERNANCE_PLATFORM_ROADMAP.md), [Connected Governance Platform Roadmap](reference/CONNECTED_GOVERNANCE_PLATFORM_ROADMAP.md),
and the [Reference Journey Program](REFERENCE_JOURNEY_PROGRAM.md). and the [Reference Journey Program](REFERENCE_JOURNEY_PROGRAM.md).
The register was reconciled on 2026-08-06 from: The register was reconciled on 2026-08-06 from:
@@ -11,7 +11,7 @@ is not an automatic dependency of every journey.
The institutional semantics and source-authority model applied to these stages The institutional semantics and source-authority model applied to these stages
are defined in the are defined in the
[Institutional Governance Target Architecture](INSTITUTIONAL_GOVERNANCE_TARGET_ARCHITECTURE.md). [Institutional Governance Target Architecture](../architecture/INSTITUTIONAL_GOVERNANCE_TARGET_ARCHITECTURE.md).
The stages are ordered, but they are not monolithic releases. Each stage is The stages are ordered, but they are not monolithic releases. Each stage is
delivered as small, reviewable, green increments and is complete only when its delivered as small, reviewable, green increments and is complete only when its
@@ -39,6 +39,32 @@ identify the journey it improves, or provide security, operability, recovery,
accessibility, or usability evidence that those journeys require. Work that accessibility, or usability evidence that those journeys require. Work that
does neither stays in the backlog until a concrete consumer exists. does neither stays in the backlog until a concrete consumer exists.
The maintained service-to-decision scenario is the German resident parking
permit (`Anwohnerparkausweis`), pinned by
`tests/fixtures/resident_parking_permit_journey.json`. It replaces generic
permit examples as acceptance evidence and fixes the service, exact Form
revision, digital and assisted intake, Case and Workflow handoff, formal
Decision, Postbox delivery, and Records target. Changing this flagship scenario
is a product decision; implementations may add further scenarios without
weakening or silently replacing its acceptance gates.
The reference fixes an email-link applicant-status profile. The exact
published Form revision names the linked email field and bounded expiry/request
limits. Submission issues a tracking grant, a matching request delegates mail
delivery to Notifications using a hash-only short-lived secret, and Portal
presents only the public lifecycle projection. Forms Runtime's module tests
also cover authenticated-only and permanent-link variants; the flagship keeps
email-link mode because it exercises identity minimization, delivery,
revocation, resend, expiry, and non-enumerating failure behavior in one slice.
The Case-to-payment handoff now has an executable first contract as well. The
flagship requests a fixed EUR obligation through `payments.requests`, retains
the Case and Workflow context references, proves exact replay, and reconciles a
full offline receipt against a Files-owned immutable evidence reference. This
does not simulate online checkout or accounting: provider callbacks, partial
payments, corrections, refunds, Ledger posting, and XRechnung remain separate
governed slices.
The Records vertical now supplies the journey's native file plan, immutable The Records vertical now supplies the journey's native file plan, immutable
record and item revisions, chronology, close/reopen, retention calculation, record and item revisions, chronology, close/reopen, retention calculation,
holds, appraisal, independent disposition approval, recovery-ledger evidence, holds, appraisal, independent disposition approval, recovery-ledger evidence,
@@ -48,8 +74,12 @@ all three contribute metadata-only native Search projections that can be
rebuilt from authoritative state. The executable fixtures prove those native rebuilt from authoritative state. The executable fixtures prove those native
transitions without claiming archival custody. A persisted Workflow Engine transitions without claiming archival custody. A persisted Workflow Engine
handoff is now reloaded through the Tasks aggregation surface and remains handoff is now reloaded through the Tasks aggregation surface and remains
visible until the authoritative Workflow transition completes. The journey visible until the authoritative Workflow transition completes. Authenticated
still needs pinned-composition reconstruction evidence and one target-tested assisted intake now uses the same exact Form revision and validation as digital
intake while retaining purpose, authority, party, channel, accessibility,
source, correction, and payload-bound read-back evidence across a session
restart. The journey still needs browser accessibility evidence for both
channels, pinned-composition reconstruction evidence, and one target-tested
archive profile. archive profile.
## Why this sequence ## Why this sequence
+100
View File
@@ -0,0 +1,100 @@
# GovOPlaN Roadmap
## Purpose
GovOPlaN should become the connective, governance-aware operating layer of an
institution: people complete services and work without learning the module
graph, while the institution can explain authority, policy, source data,
effects, evidence, and recovery.
This is the concise product roadmap. It states durable outcomes and sequence,
not release dates or issue state. Use [Strategy Status](STRATEGY_STATUS.md) for
the current reconciliation and Gitea issues for active work. The
[detailed connected-platform vision](reference/CONNECTED_GOVERNANCE_PLATFORM_ROADMAP.md)
retains stakeholder perspectives, configuration archetypes, and the complete
outcome-story catalogue.
## Product Promise
GovOPlaN will:
1. model institutional context, responsibility, authority, and time;
2. turn incoming information into owned, reviewable human and machine work;
3. connect native and external systems without obscuring the source of truth;
4. preserve decisions, effects, records, corrections, and recovery evidence;
5. support digital, assisted, paper, message, calendar, and system channels as
paths through the same governed work; and
6. package successful configurations so institutions can adopt them without
code forks or loss of local autonomy.
It will not replace every specialist system, copy all data into one master
database, infer authority from membership, or claim production maturity from
repository breadth.
## Outcome Horizons
| Horizon | Outcome | Completion evidence |
| --- | --- | --- |
| Trustworthy baseline | A pinned composition can be installed, upgraded, operated, explained, and recovered. | Signed artifacts, clean install/upgrade, provider failure tests, restore drill, coherent UI, and target evidence |
| Connected work | Intake becomes accountable work with context, assignment, review, communication, and evidence. | One digital and assisted service reaches a decision and eAkte without losing responsibility or state |
| Reusable products | Complete service, communication, and data outcomes ship as governed configuration packages. | Two materially different deployments adapt packages without code forks |
| Institutional assurance | Records, transparency, privacy, risk, regulated review, and reporting connect to real operations. | A consequential decision can be reconstructed, corrected, retained, and disclosed under policy |
| Federated ecosystem | Autonomous installations exchange signed data and configuration across explicit trust boundaries. | Paired-instance exchange, reconciliation, supported deployment profiles, and independent evidence |
## Current Sequence
The sequence is outcome-led. Shared foundation work enters when one of these
proofs needs it.
1. **Enforce the platform quality contract.** German is the reference locale;
help, accessibility, temporal browsing, purpose-aware access, retention,
institutional context, optional-module combinations, and recovery behavior
become measurable release gates.
2. **Complete governed communication.** Prove recipient selection, Campaign,
Files, Mail, function-bound Postbox delivery, acknowledgement, uncertain
outcomes, correction, filing, and recovery against a named target.
3. **Complete the monthly-data and sanctions journey.** Acquire immutable
source snapshots, validate and reconcile data interactively, preserve
lineage and review, publish reports and files, and deliver accepted results.
4. **Complete inclusive service to decision.** Accept digital or assisted
input, establish actor and purpose, persist human handoffs, decide, notify,
and reconstruct the exact eAkte under current authorization.
5. **Complete discovery and external coexistence.** Finish native PostgreSQL
search coverage, prove reauthorization and reindexing, then prove one
external product connector and one paired GovOPlaN federation exchange.
6. **Prove production operation.** Complete multi-host, provider, restore,
accessibility, volume, key-custody, and independently signed target
evidence before raising maturity claims.
## Continuous Foundation
Every journey applies the same boundaries:
- modules cooperate through versioned Core contracts and typed references;
- permissions, policy, institutional context, purpose, and current authority
are evaluated before presenting or acting on data;
- requested actions, durable intent, observed effects, unknown outcomes,
retries, reconciliation, and correction remain distinct;
- Workflow Engine coordinates stable module-owned actions and human handoffs;
it does not become a second owner of domain state;
- Files owns managed bytes, Records owns institutional filing and retention,
and source systems retain explicitly declared authority;
- focused views and product areas reduce interface complexity without granting
access or hiding material consequences;
- configuration packages include terminology, forms, policies, workflows,
views, reports, providers, documentation, migration, and evidence; and
- maturity advances from scaffold to vertical slice, reference-ready,
supported, and LTS only with evidence appropriate to each claim.
## Decision Rule
A roadmap item should answer all of the following before implementation:
1. Which real journey and actor outcome does it improve?
2. Which module or external system owns each object and source of truth?
3. Which institutional, temporal, purpose, and policy context applies?
4. Which effects, evidence, retention, failure, and recovery states result?
5. Which package and target evidence will prove the outcome?
If those answers are missing, retain the idea in the Product Input Register or
Gitea discovery work rather than opening an unbounded implementation program.
@@ -4,9 +4,9 @@
| Field | Value | | Field | Value |
| --- | --- | | --- | --- |
| Reconciled on | 2026-08-06 | | Reconciled on | 2026-08-17 |
| Source scope | Local workspace manifests, source inventory, focused journey checks, signed release evidence, and live Gitea issue state | | Source scope | Local workspace manifests, source inventory, focused journey checks, signed release evidence, and live Gitea issue state |
| Stable direction | [Platform Core Ideas](PLATFORM_CORE_IDEAS.md) and [Connected Governance Platform Roadmap](CONNECTED_GOVERNANCE_PLATFORM_ROADMAP.md) | | Stable direction | [Platform Core Ideas](PLATFORM_CORE_IDEAS.md) and [Roadmap](ROADMAP.md) |
| Collected product input | [Product Input Register](PRODUCT_INPUT_REGISTER.md) | | Collected product input | [Product Input Register](PRODUCT_INPUT_REGISTER.md) |
| Delivery source | Gitea issues | | Delivery source | Gitea issues |
@@ -16,11 +16,14 @@ evidence remain authoritative for specific maturity claims.
## Portfolio Snapshot ## Portfolio Snapshot
- 66 source module manifests were loadable and architecture-declared. - 67 source module manifests were loadable and architecture-declared.
- 48 modules declared `vertical_slice`; 18 declared `scaffold`. - 50 modules declared `vertical_slice`; 17 declared `scaffold`.
- No module declared `reference_ready`, `supported`, or `lts`. - No module declared `reference_ready`, `supported`, or `lts`.
- The live portfolio had 133 open issues, including 37 priority-P1 items. - The coordinated package version was `0.1.18`, with version alignment passing
- 118 open issues had no milestone, so issue labels do not yet express a across all 78 release repositories.
- The live portfolio had 137 open issues: 42 priority-P1, 92 priority-P2, and
3 priority-P3 items. Every open issue had labels.
- 129 open issues had no milestone, so issue labels do not yet express a
reliable completion sequence on their own. reliable completion sequence on their own.
- Three product package manifests existed: governed communication, governed - Three product package manifests existed: governed communication, governed
data and assurance, and service to decision. None had crossed the complete data and assurance, and service to decision. None had crossed the complete
@@ -31,20 +34,21 @@ document.
## Interface And Contract Evidence ## Interface And Contract Evidence
The 2026-08-06 source inventory found: The 2026-08-17 source inventory found:
- 1,307 UI fields and 1,291 UI actions; - 1,344 UI fields and 1,331 UI actions;
- 8,157 stable interface declarations with no duplicate IDs; - 8,412 stable interface declarations with no duplicate IDs;
- 43 frontend routes and 920 backend endpoints; - 43 frontend routes and 943 backend endpoints;
- no public WebUI surfaces missing runtime declarations; - no public WebUI surfaces missing runtime declarations;
- no stale runtime route declarations; - no stale runtime route declarations;
- no unclassified endpoint without a static UI reference; - no unclassified endpoint without a static UI reference;
- all 1,307 fields with a resolvable F1 context; 1,143 remain candidates for - all 1,344 fields with a resolvable F1 context; 175 have statically specific
richer field-specific content beyond page/module fallback; help and 1,169 remain candidates for richer field-specific content beyond
page/module fallback;
- German (`de`) as the complete reference locale and no used key missing from - German (`de`) as the complete reference locale and no used key missing from
the required German or English catalogs; the required German or English catalogs;
- 3 module information-governance dimensions classified as `enforced`, 1 as - 3 module information-governance dimensions classified as `enforced`, 1 as
`partial`, and 260 as `contract_only`. `partial`, and 264 as `contract_only`.
This is an honest platform-wide baseline, not a claim that temporal, This is an honest platform-wide baseline, not a claim that temporal,
purpose, retention, and institutional-context adoption is complete. purpose, retention, and institutional-context adoption is complete.
@@ -94,7 +98,7 @@ reconciliation, governed export/delivery, and browser-level handoff evidence.
| --- | --- | --- | | --- | --- | --- |
| No reference-ready product package | The platform cannot yet make a bounded supported-product claim | Complete one named target composition and evidence bundle | | No reference-ready product package | The platform cannot yet make a bounded supported-product claim | Complete one named target composition and evidence bundle |
| Human-work spine is only an MVP | Tasks aggregates explicit work plus Workflow, Approval, and unread Postbox projections, but broad domain coverage, deadline escalation, assignment lifecycle, and focused product UX remain | Extend source providers through the three reference journeys and prove overdue/reassignment behavior in browser tests | | Human-work spine is only an MVP | Tasks aggregates explicit work plus Workflow, Approval, and unread Postbox projections, but broad domain coverage, deadline escalation, assignment lifecycle, and focused product UX remain | Extend source providers through the three reference journeys and prove overdue/reassignment behavior in browser tests |
| Records/eAkte target integration incomplete | Native lifecycle, retention, holds, approval, recovery, and transfer simulation are implemented, but real custody is not proved | Target-test one archive/xdomea profile and complete the assisted reference journey | | Records/eAkte target integration incomplete | Native lifecycle, retention, holds, approval, recovery, and transfer simulation are implemented, but real custody is not proved | Target-test one archive/xdomea profile and browser-test the now server-enforced assisted reference journey |
| Cross-cutting governance adoption uneven | Historical and purpose-sensitive behavior varies by module | Enforced adoption declarations and route/query/effect migration | | Cross-cutting governance adoption uneven | Historical and purpose-sensitive behavior varies by module | Enforced adoption declarations and route/query/effect migration |
| Explicit help/accessibility depth incomplete | German/reference and F1 association gates now pass, but generic fallback remains too common | High-risk German help content and browser/a11y matrix | | Explicit help/accessibility depth incomplete | German/reference and F1 association gates now pass, but generic fallback remains too common | High-risk German help content and browser/a11y matrix |
| Real federation absent | Cross-institution exchange remains connector-specific | Paired-instance signed exchange and reconciliation proof | | Real federation absent | Cross-institution exchange remains connector-specific | Paired-instance signed exchange and reconciliation proof |
@@ -107,8 +111,9 @@ reconciliation, governed export/delivery, and browser-level handoff evidence.
2. Complete governed communication and Postbox against a named target. 2. Complete governed communication and Postbox against a named target.
3. Complete the monthly-data flow and use it as the data foundation for 3. Complete the monthly-data flow and use it as the data foundation for
sanctions screening. sanctions screening.
4. Complete the browser and resumable-work proof for the digital and assisted 4. Complete the browser proof for the digital and assisted service-to-decision
service-to-decision journey with its existing exact eAkte filing contracts. journey; server-side assisted resume, provenance, correction, and read-back
enforcement now complement its existing exact eAkte filing contracts.
5. Complete native PostgreSQL search coverage for remaining journey-owned 5. Complete native PostgreSQL search coverage for remaining journey-owned
objects and prove reauthorization and reindex operations at target volume; objects and prove reauthorization and reindex operations at target volume;
keep OpenSearch optional. Communication, Records, service-to-decision, keep OpenSearch optional. Communication, Records, service-to-decision,
@@ -1,30 +1,32 @@
# GovOPlaN Connected Governance Platform Roadmap # GovOPlaN Detailed Connected-Platform Vision
## Purpose and status ## Purpose and status
This document describes the long-term product destination for GovOPlaN from an This reference catalogue describes the long-term product destination from an
outcome and stakeholder perspective. It answers what a completely connected outcome and stakeholder perspective. It preserves the detailed perspectives,
governance platform should enable, how the same platform can be configured for configuration archetypes, stories, horizons, and maturity notes behind the
different institutions, and which capability horizons lead from the current concise [Roadmap](../ROADMAP.md).
baseline to that destination.
It is a durable direction, not a release promise or a substitute for issue It is not a release promise, live plan, or second status source. The concise
tracking. Live work state belongs in Gitea issues. The roadmap owns the current durable sequence, Strategy Status owns the reconciled
state, and Gitea issues own work state. Where dated detail here differs from
those sources, those sources take precedence. The
[Core master roadmap](https://git.add-ideas.de/GovOPlaN/govoplan-core/src/branch/main/docs/GOVOPLAN_MASTER_ROADMAP.md) [Core master roadmap](https://git.add-ideas.de/GovOPlaN/govoplan-core/src/branch/main/docs/GOVOPLAN_MASTER_ROADMAP.md)
remains the technical module and wave sequence; this document supplies the remains the technical module and wave sequence; this document supplies the
cross-product vision that sequence serves. cross-product vision that sequence serves.
Read it together with: Read it together with:
- the [institutional governance target architecture](INSTITUTIONAL_GOVERNANCE_TARGET_ARCHITECTURE.md) - the [concise product roadmap](../ROADMAP.md)
- the [selected reference-journey program](REFERENCE_JOURNEY_PROGRAM.md) - the [institutional governance target architecture](../../architecture/INSTITUTIONAL_GOVERNANCE_TARGET_ARCHITECTURE.md)
- the [current strategy status](STRATEGY_STATUS.md) - the [selected reference-journey program](../REFERENCE_JOURNEY_PROGRAM.md)
- the [pinned Campaign capability and infrastructure fit assessment](CAPABILITY_AND_INFRASTRUCTURE_FIT.md) - the [current strategy status](../STRATEGY_STATUS.md)
- the [interface pattern language](INTERFACE_PATTERN_LANGUAGE.md) - the [generated, pinned Campaign capability and infrastructure fit assessment](../../evidence/snapshots/CAPABILITY_AND_INFRASTRUCTURE_FIT.generated.md)
- the [interface surface inventory](INTERFACE_SURFACE_INVENTORY.md) - the [interface pattern language](../../architecture/INTERFACE_PATTERN_LANGUAGE.md)
- the [module contract and install model](MODULE_CONTRACTS_AND_INSTALLS.md) - the [interface surface inventory](../../evidence/snapshots/INTERFACE_SURFACE_INVENTORY.md)
- the [repository and module index](REPOSITORY_INDEX.md) - the [module contract and install model](../../operations/MODULE_CONTRACTS_AND_INSTALLS.md)
- the [Gitea issue workflow](GITEA_ISSUES.md) - the [repository and module index](../../project/REPOSITORY_INDEX.md)
- the [Gitea issue workflow](../../project/GITEA_ISSUES.md)
### How to read this roadmap ### How to read this roadmap
@@ -42,14 +44,15 @@ Read it together with:
- Use [Near-term portfolio order](#near-term-portfolio-order) for the bridge to - Use [Near-term portfolio order](#near-term-portfolio-order) for the bridge to
implementation and [Product decisions](#product-decisions-to-make-progressively) implementation and [Product decisions](#product-decisions-to-make-progressively)
for choices that can remain deferred. for choices that can remain deferred.
- Use the [dated strategic review](STRATEGIC_REVIEW_2026-08-05.md) to understand - Use the [dated strategic review](../../archive/2026-08/STRATEGIC_REVIEW_2026-08-05.md) to understand
why the current convergence and reference-journey order was chosen. why the current convergence and reference-journey order was chosen.
### Planning ownership ### Planning ownership
| Question | Canonical source | | Question | Canonical source |
| --- | --- | | --- | --- |
| What product should GovOPlaN become, for whom, in which configurations, and through which outcome horizons? | This meta roadmap | | What product should GovOPlaN become and in which durable sequence? | The concise Roadmap |
| Which stakeholder perspectives, configuration archetypes, and detailed outcome stories inform that direction? | This reference catalogue |
| Which module owns a capability, which technical wave should deliver it, and what implementation gates apply? | The Core master roadmap and owning-module concepts | | Which module owns a capability, which technical wave should deliver it, and what implementation gates apply? | The Core master roadmap and owning-module concepts |
| What is actively planned, blocked, implemented, or closed now? | Gitea issues and the dated reconciliation in `STRATEGY_STATUS.md` | | What is actively planned, blocked, implemented, or closed now? | Gitea issues and the dated reconciliation in `STRATEGY_STATUS.md` |
| What can a named composition credibly claim in a target environment? | A dated capability/infrastructure fit assessment | | What can a named composition credibly claim in a target environment? | A dated capability/infrastructure fit assessment |
@@ -110,7 +113,7 @@ safe modules -> connected work -> reusable services -> institutional assurance -
``` ```
The active implementation path is the The active implementation path is the
[Reference Journey Program](REFERENCE_JOURNEY_PROGRAM.md), selected on [Reference Journey Program](../REFERENCE_JOURNEY_PROGRAM.md), selected on
2026-07-21. Its five stages do not replace these product horizons: they are the 2026-07-21. Its five stages do not replace these product horizons: they are the
ordered demonstrations through which the shared platform contracts and horizon ordered demonstrations through which the shared platform contracts and horizon
gates are to be proved. Connector safety, identity/function semantics, gates are to be proved. Connector safety, identity/function semantics,
@@ -266,7 +269,7 @@ Diagnostics minimize personal data and link to governed evidence when deeper
inspection is authorized. inspection is authorized.
The complete installation and lifecycle journey is specified in the The complete installation and lifecycle journey is specified in the
[System Administrator Lifecycle User Story](SYSTEM_ADMINISTRATOR_LIFECYCLE_USER_STORY.md): [System Administrator Lifecycle User Story](../SYSTEM_ADMINISTRATOR_LIFECYCLE_USER_STORY.md):
one-command Core-baseline bootstrap, signed online module installation and updates, one-command Core-baseline bootstrap, signed online module installation and updates,
stateless scale-out, versioned configuration transfer, undo, and reproducible stateless scale-out, versioned configuration transfer, undo, and reproducible
environment-promotion recipes. environment-promotion recipes.
@@ -950,7 +953,7 @@ first analytical product prove Horizons 2 and 3; governed BI adds assurance and
ecosystem capabilities across Horizons 35; collaborative documents combine ecosystem capabilities across Horizons 35; collaborative documents combine
the evidence spine, service packages, and records assurance across Horizons the evidence spine, service packages, and records assurance across Horizons
24. The detailed mapping and gates are in the 24. The detailed mapping and gates are in the
[Reference Journey Program](REFERENCE_JOURNEY_PROGRAM.md). [Reference Journey Program](../REFERENCE_JOURNEY_PROGRAM.md).
### Current baseline: modular pilot foundations ### Current baseline: modular pilot foundations
@@ -976,7 +979,7 @@ checkouts.
Priorities: Priorities:
1. Deliver the first slices of the 1. Deliver the first slices of the
[System Administrator Lifecycle User Story](SYSTEM_ADMINISTRATOR_LIFECYCLE_USER_STORY.md): [System Administrator Lifecycle User Story](../SYSTEM_ADMINISTRATOR_LIFECYCLE_USER_STORY.md):
a verified full-package distribution with only the Core baseline active, a verified full-package distribution with only the Core baseline active,
first-run control plane, read-only online first-run control plane, read-only online
module directory, and durable plan/confirm/install progress. module directory, and durable plan/confirm/install progress.
@@ -1220,7 +1223,7 @@ provides all applicable evidence below.
## Near-term portfolio order ## Near-term portfolio order
This order is now selected. Detailed slices and gates are in the This order is now selected. Detailed slices and gates are in the
[Reference Journey Program](REFERENCE_JOURNEY_PROGRAM.md). Workflow Engine and [Reference Journey Program](../REFERENCE_JOURNEY_PROGRAM.md). Workflow Engine and
the optional editor may support these stages, but Workflow work enters the the optional editor may support these stages, but Workflow work enters the
portfolio only through an explicit bounded package or reference journey. portfolio only through an explicit bounded package or reference journey.
@@ -1357,6 +1360,6 @@ evidence remains—and the product can prove that explanation at runtime.
The volatile release and backlog appendix that originally accompanied this The volatile release and backlog appendix that originally accompanied this
roadmap has been removed so the durable direction cannot become a competing roadmap has been removed so the durable direction cannot become a competing
status source. The [Strategic Review 2026-08-05](STRATEGIC_REVIEW_2026-08-05.md) status source. The [Strategic Review 2026-08-05](../../archive/2026-08/STRATEGIC_REVIEW_2026-08-05.md)
retains the dated assessment and reasoning. Current reconciliation belongs in retains the dated assessment and reasoning. Current reconciliation belongs in
[Strategy Status](STRATEGY_STATUS.md), and live work state belongs in Gitea. [Strategy Status](../STRATEGY_STATUS.md), and live work state belongs in Gitea.
+74 -69
View File
@@ -4,84 +4,89 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "govoplan" name = "govoplan"
version = "0.1.18" version = "0.1.42"
description = "Developer convenience package for a versioned GovOPlaN composition" description = "Developer convenience package for a versioned GovOPlaN composition"
readme = "README.md" readme = "README.md"
requires-python = ">=3.12" requires-python = ">=3.12"
license = { text = "AGPL-3.0-or-later" } license = { text = "AGPL-3.0-or-later" }
dependencies = [ dependencies = [
"govoplan-core[server]==0.1.18", "govoplan-core[server]==0.1.42",
"govoplan-tenancy==0.1.18", "govoplan-tenancy==0.1.20",
"govoplan-organizations==0.1.18", "govoplan-organizations==0.1.20",
"govoplan-identity==0.1.18", "govoplan-identity==0.1.20",
"govoplan-idm==0.1.18", "govoplan-idm==0.1.24",
"govoplan-access==0.1.18", "govoplan-access==0.1.23",
"govoplan-admin==0.1.18", "govoplan-admin==0.1.22",
"govoplan-policy==0.1.18", "govoplan-policy==0.1.22",
"govoplan-audit==0.1.18", "govoplan-audit==0.1.20",
"govoplan-dashboard==0.1.18", "govoplan-dashboard==0.1.20",
"govoplan-files==0.1.18", "govoplan-files==0.1.24",
"govoplan-mail==0.1.18", "govoplan-mail==0.1.26",
"govoplan-campaign==0.1.18", "govoplan-campaign==0.1.27",
"govoplan-calendar==0.1.18", "govoplan-calendar==0.1.22",
"govoplan-docs==0.1.18", "govoplan-docs==0.1.22",
"govoplan-ops==0.1.18", "govoplan-ops==0.1.21",
] ]
[project.optional-dependencies] [project.optional-dependencies]
full = [ full = [
"govoplan-addresses==0.1.18", "govoplan-addresses==0.1.21",
"govoplan-approvals==0.1.18", "govoplan-approvals==0.1.20",
"govoplan-assets==0.1.18", "govoplan-assets==0.1.20",
"govoplan-booking==0.1.18", "govoplan-booking==0.1.20",
"govoplan-cases==0.1.18", "govoplan-cases==0.1.22",
"govoplan-certificates==0.1.18", "govoplan-certificates==0.1.20",
"govoplan-committee==0.1.18", "govoplan-committee==0.1.20",
"govoplan-connectors==0.1.18", "govoplan-connectors==0.1.25",
"govoplan-consultation==0.1.18", "govoplan-consultation==0.1.20",
"govoplan-contracts==0.1.18", "govoplan-contracts==0.1.20",
"govoplan-dataflow==0.1.18", "govoplan-dataflow==0.1.23",
"govoplan-datasources==0.1.18", "govoplan-datasources==0.1.24",
"govoplan-decisions==0.1.18", "govoplan-decisions==0.1.19",
"govoplan-dist-lists==0.1.18", "govoplan-dist-lists==0.1.20",
"govoplan-encryption==0.1.18", "govoplan-dms==0.1.20",
"govoplan-evaluation==0.1.18", "govoplan-encryption==0.1.19",
"govoplan-facilities==0.1.18", "govoplan-erp==0.1.20",
"govoplan-forms==0.1.18", "govoplan-evaluation==0.1.20",
"govoplan-forms-runtime==0.1.18", "govoplan-facilities==0.1.20",
"govoplan-grants==0.1.18", "govoplan-fit-connect==0.1.20",
"govoplan-helpdesk==0.1.18", "govoplan-forms==0.1.22",
"govoplan-identity-trust==0.1.18", "govoplan-forms-runtime==0.1.20",
"govoplan-inspections==0.1.18", "govoplan-grants==0.1.20",
"govoplan-learning==0.1.18", "govoplan-helpdesk==0.1.21",
"govoplan-mandates==0.1.18", "govoplan-identity-trust==0.1.20",
"govoplan-notifications==0.1.18", "govoplan-inspections==0.1.20",
"govoplan-parties==0.1.18", "govoplan-learning==0.1.20",
"govoplan-permits==0.1.18", "govoplan-mandates==0.1.19",
"govoplan-poll==0.1.18", "govoplan-notifications==0.1.19",
"govoplan-portal==0.1.18", "govoplan-parties==0.1.19",
"govoplan-postbox==0.1.18", "govoplan-payments==0.1.21",
"govoplan-procurement==0.1.18", "govoplan-permits==0.1.20",
"govoplan-projects==0.1.18", "govoplan-poll==0.1.20",
"govoplan-quick-access==0.1.18", "govoplan-portal==0.1.21",
"govoplan-records==0.1.19", "govoplan-postbox==0.1.22",
"govoplan-reporting==0.1.18", "govoplan-procurement==0.1.20",
"govoplan-resources==0.1.18", "govoplan-projects==0.1.19",
"govoplan-rest==0.1.18", "govoplan-quick-access==0.1.20",
"govoplan-risk-compliance==0.1.18", "govoplan-records==0.1.22",
"govoplan-scheduling==0.1.18", "govoplan-reporting==0.1.20",
"govoplan-search==0.1.18", "govoplan-resources==0.1.20",
"govoplan-services==0.1.18", "govoplan-rest==0.1.19",
"govoplan-soap==0.1.18", "govoplan-risk-compliance==0.1.20",
"govoplan-tasks==0.1.19", "govoplan-scheduling==0.1.21",
"govoplan-templates==0.1.18", "govoplan-search==0.1.19",
"govoplan-tickets==0.1.18", "govoplan-services==0.1.19",
"govoplan-transparency==0.1.18", "govoplan-soap==0.1.19",
"govoplan-views==0.1.18", "govoplan-tasks==0.1.21",
"govoplan-voting==0.1.18", "govoplan-templates==0.1.21",
"govoplan-wiki==0.1.18", "govoplan-tickets==0.1.22",
"govoplan-workflow==0.1.18", "govoplan-transparency==0.1.20",
"govoplan-workflow-engine==0.1.18", "govoplan-views==0.1.21",
"govoplan-voting==0.1.20",
"govoplan-wiki==0.1.22",
"govoplan-workflow==0.1.22",
"govoplan-workflow-engine==0.1.21",
"govoplan-xrechnung==0.1.21",
] ]
[project.urls] [project.urls]
+54 -3
View File
@@ -9,6 +9,34 @@ resolution -> approval/deliberation -> formal Decision -> observed delivery
effect -> record and review references effect -> record and review references
``` ```
The maintained concrete scenario is a German resident parking permit
(`Anwohnerparkausweis`). Its versioned fixture is
`tests/fixtures/resident_parking_permit_journey.json`. It pins the service,
exact Form revision, resident inputs, digital and assisted channels, Case type,
human review handoff, formal outcome, Postbox delivery channel, and Records
filing/retention target. Generic permit wording is no longer acceptance
evidence for this package.
The package is now executable rather than metadata-only. Its Access fragments
create the bounded resident-permit clerk role, collect only the tenant-local
responsibility group key and name, create that group, and bind the role. The
Forms-owned fragment carries a digest-bound German-reference application schema
and imports it as a tenant-local draft with source provenance. Reapplying the
same source digest is a no-op; replacing an unrelated local definition remains
blocked unless the reviewed package explicitly selects a new revision. Normal
Forms review and publication are still required before the definition can serve
new applications. The Workflow Engine-owned fragment materializes and activates
the tenant review baseline, resolves the chosen responsibility group into each
human handoff, and preserves the evidence, decision, and EUR 30 payment-review
steps as a replay-safe contributed definition.
Services, Cases, Payments, Tasks, and the optional delivery and Records modules
already execute the pinned journey through their runtime
contracts, but their reusable configuration fragments are not yet claimed by
this package. Until those module-owned configuration providers are added, the
package preflight deliberately distinguishes the installed runtime composition
from the Access, Forms, and Workflow configurations it can currently materialize.
An installed Forms and Forms Runtime pair adds an alternative governed entry An installed Forms and Forms Runtime pair adds an alternative governed entry
path before case/workflow handoff: path before case/workflow handoff:
@@ -17,6 +45,14 @@ Service discovery -> exact Form revision -> validated draft/submission
-> receipt and handoff evidence -> Case or Workflow owner -> receipt and handoff evidence -> Case or Workflow owner
``` ```
The assisted path now creates an authenticated, resumable session against that
same exact Form revision. It records channel, affected and represented parties,
authority, purpose, notice, responsible function, language, accessibility
support, and field provenance. Submission fails closed until an immutable
read-back outcome matches the current revision, values, attachments, and
signatures. Saving a correction therefore requires a fresh confirmation rather
than silently reusing old evidence.
Services, Cases, Parties, Mandates, Committee, and Decisions retain immutable Services, Cases, Parties, Mandates, Committee, and Decisions retain immutable
provider-owned revisions for the parts they own. Portal, Cases, and Committee provider-owned revisions for the parts they own. Portal, Cases, and Committee
consume capabilities for cross-module semantics only. The package does not consume capabilities for cross-module semantics only. The package does not
@@ -51,11 +87,26 @@ The executable fixture in
`tests/test_institutional_governance_journey.py` proves SQL-backed Service, `tests/test_institutional_governance_journey.py` proves SQL-backed Service,
Case, Party, Mandate, Committee meeting/agendum/vote/minute, and Decision state. Case, Party, Mandate, Committee meeting/agendum/vote/minute, and Decision state.
`tests/test_institutional_service_journey.py` separately proves exact Portal `tests/test_institutional_service_journey.py` separately proves exact Portal
Form launch, persisted submission provenance, idempotent replay, and a durable Form launch, persisted submission provenance, idempotent replay, resumable
Workflow handoff that remains visible through Tasks after the database session assisted intake with enforced read-back evidence, and a durable Workflow handoff
is reopened and disappears only after the Workflow Engine records completion. that remains visible through Tasks after the database session is reopened and
disappears only after the Workflow Engine records completion.
Core's production-component browser conformance suite additionally executes the
German self-service and assisted Anwohnerparkausweis paths at desktop and mobile
widths. It proves native keyboard order, accessible names and landmarks, WCAG
2.1 A/AA automation, responsive geometry, first-draft persistence, and mixed
per-field person/document/system provenance. Physical screen-reader spot checks
remain target-environment release evidence.
Module-level Records source tests prove exact Form submission, Case revision, Module-level Records source tests prove exact Form submission, Case revision,
and Decision revision filing. Target-environment browser accessibility, and Decision revision filing. Target-environment browser accessibility,
production identity and delivery, a named archive profile, and recovery evidence production identity and delivery, a named archive profile, and recovery evidence
are still required before this product package may claim `reference_ready` are still required before this product package may claim `reference_ready`
maturity. maturity.
The generic package orchestrator stops at the first provider apply or health
blocker. Access and Forms may commit in separate provider transactions, so the
operator must retain the reviewed pre-apply database snapshot until verification
is complete. The Admin result reports no-op, snapshot-required, or partial-apply
recovery state and never describes this as atomic cross-module undo. Exported
fragments carry source/module/operator/scope provenance; supplied values and
credentials are not serialized into that provenance.
@@ -8,36 +8,366 @@
"category": "institutional-governance", "category": "institutional-governance",
"license": "AGPL-3.0-or-later", "license": "AGPL-3.0-or-later",
"required_modules": [ "required_modules": [
{"module_id": "access"},
{"module_id": "audit"}, {"module_id": "audit"},
{"module_id": "cases"}, {"module_id": "cases"},
{"module_id": "decisions"}, {"module_id": "decisions"},
{"module_id": "forms"},
{"module_id": "forms_runtime"},
{"module_id": "mandates"}, {"module_id": "mandates"},
{"module_id": "parties"}, {"module_id": "parties"},
{"module_id": "payments"},
{"module_id": "policy"}, {"module_id": "policy"},
{"module_id": "portal"}, {"module_id": "portal"},
{"module_id": "services"} {"module_id": "services"},
{"module_id": "tasks"},
{"module_id": "workflow_engine"}
], ],
"required_capabilities": [ "required_capabilities": [
"access.configuration",
"cases.party_context", "cases.party_context",
"cases.service_intake", "cases.service_intake",
"decisions.registry", "decisions.registry",
"forms.configuration",
"forms.definitions",
"mandates.resolver", "mandates.resolver",
"parties.resolver", "parties.resolver",
"payments.requests",
"portal.service_directory", "portal.service_directory",
"services.availability", "services.availability",
"services.definitions" "services.definitions",
"workflow.configuration"
], ],
"optional_modules": [ "optional_modules": [
{"module_id": "approvals"}, {"module_id": "approvals"},
{"module_id": "committee"}, {"module_id": "committee"},
{"module_id": "files"}, {"module_id": "files"},
{"module_id": "forms"},
{"module_id": "forms_runtime"},
{"module_id": "postbox"}, {"module_id": "postbox"},
{"module_id": "records"}, {"module_id": "records"},
{"module_id": "search"}, {"module_id": "search"}
{"module_id": "tasks"}, ],
{"module_id": "workflow_engine"} "data_requirements": [
{
"key": "responsible_group_slug",
"label": "Responsible permit group key",
"data_type": "string",
"required": true,
"secret": false,
"description": "Tenant-local stable key for the group that reviews resident parking permit applications."
},
{
"key": "responsible_group_name",
"label": "Responsible permit group name",
"data_type": "string",
"required": true,
"secret": false,
"description": "Human-readable tenant-local name shown for the responsible permit group."
}
],
"fragments": [
{
"module_id": "access",
"fragment_type": "roles",
"fragment_id": "resident-parking-permit-clerk",
"payload": {
"items": [
{
"slug": "resident-parking-permit-clerk",
"name": "Resident parking permit clerk",
"description": "Reviews resident parking permit submissions, workflow handoffs, cases, decisions, and payment evidence.",
"permissions": [
"cases:case:read",
"cases:case:create",
"cases:case:update",
"decisions:decision:read",
"decisions:decision:write",
"forms:definition:read",
"forms_runtime:workspace:read",
"forms_runtime:workspace:write",
"payments:payment:read",
"payments:payment:write",
"tasks:item:read",
"tasks:item:write",
"workflow:definition:read",
"workflow:instance:read",
"workflow:instance:start",
"workflow:instance:transition"
]
}
]
}
},
{
"module_id": "access",
"fragment_type": "groups",
"fragment_id": "resident-parking-permit-responsibility",
"payload": {
"items": [
{
"slug": {"$data": "responsible_group_slug"},
"name": {"$data": "responsible_group_name"},
"description": "Tenant-local responsibility group for the resident parking permit reference journey."
}
]
}
},
{
"module_id": "access",
"fragment_type": "group_role_assignments",
"fragment_id": "resident-parking-permit-clerk-assignment",
"payload": {
"items": [
{
"group": {"$data": "responsible_group_slug"},
"role": "resident-parking-permit-clerk"
}
]
}
},
{
"module_id": "forms",
"fragment_type": "definition",
"fragment_id": "resident-parking-permit-application",
"payload": {
"on_conflict": "new_revision",
"change_reason": "Install the reviewed resident parking permit reference form.",
"fragment": {
"kind": "govoplan.forms.definition",
"contract_version": "0.1.0",
"definition": {
"reference": {
"kind": "form",
"owner_module": "forms",
"object_id": "resident-parking-permit-application",
"tenant_id": "reference-package",
"version": "3",
"valid_at": null,
"label": null
},
"key": "resident-parking-permit-application",
"temporal": {
"revision": "3",
"valid_from": null,
"valid_to": null,
"recorded_at": "2026-08-22T00:00:00+00:00",
"superseded_at": null,
"change_reason": "Reference package revision."
},
"title": "Resident parking permit",
"description": "Apply for a resident parking permit through a digital or assisted channel.",
"fields": [
{
"key": "applicant_name",
"label": "Name",
"value_type": "text",
"required": true,
"help_text": null,
"options": [],
"constraints": {"min_length": 2, "max_length": 200},
"default_value": null
},
{
"key": "applicant_email",
"label": "Email",
"value_type": "text",
"required": true,
"help_text": null,
"options": [],
"constraints": {"format": "email"},
"default_value": null
},
{
"key": "residence_address",
"label": "Primary residence",
"value_type": "text",
"required": true,
"help_text": null,
"options": [],
"constraints": {"max_length": 500},
"default_value": null
},
{
"key": "licence_plate",
"label": "Licence plate",
"value_type": "text",
"required": true,
"help_text": null,
"options": [],
"constraints": {"max_length": 20},
"default_value": null
}
],
"publication_state": "published",
"allow_drafts": true,
"max_attachments": 4,
"signature_requirement": "none",
"policy_refs": [
"law:resident-parking-permit",
"records:resident-parking-permit"
],
"handoff_kinds": ["case", "workflow"],
"metadata": {},
"pages": [
{
"key": "application",
"title": "Application",
"description": null,
"sections": [
{
"key": "applicant-and-vehicle",
"title": "Applicant and vehicle",
"description": null,
"field_keys": [
"applicant_name",
"applicant_email",
"residence_address",
"licence_plate"
]
}
]
}
],
"localizations": [
{
"locale": "de",
"title": "Anwohnerparkausweis beantragen",
"description": "Einen Anwohnerparkausweis digital oder mit Unterstützung beantragen.",
"field_labels": {
"applicant_name": "Name",
"applicant_email": "E-Mail-Adresse",
"residence_address": "Hauptwohnsitz",
"licence_plate": "Kennzeichen"
},
"field_help_texts": {},
"option_labels": {},
"page_titles": {"application": "Antrag"},
"section_titles": {
"applicant-and-vehicle": "Antragstellende Person und Fahrzeug"
}
}
],
"fallback_locale": "de"
},
"definition_sha256": "7dc108002d532c07e5e7f3b14029a9d4deb3836ebb65d97fb6b51166a70e0ed4",
"provenance": {
"owner_module": "forms",
"tenant_id": "reference-package",
"form_id": "resident-parking-permit-application",
"revision": "3",
"exported_at": "2026-08-22T12:00:00+00:00",
"exported_by": "GovOPlaN reference package"
}
}
}
},
{
"module_id": "workflow_engine",
"fragment_type": "workflow_definitions",
"fragment_id": "resident-parking-permit-workflow",
"payload": {
"schema_version": 1,
"origin_module_id": "configuration_package.service_to_decision",
"origin_module_version": "0.1.0",
"items": [
{
"definition_key": "resident-parking-permit-review",
"name": "Resident parking permit review",
"description": "Review evidence, record the formal decision, and verify payment evidence for the resident parking permit reference journey.",
"scope_type": "tenant",
"allow_start": true,
"allow_reuse": true,
"allow_automation": false,
"execution_mode": "guided",
"activate_on_install": true,
"graph": {
"schema_version": 1,
"nodes": [
{
"id": "start",
"type": "workflow.start.manual",
"label": "Application received",
"config": {"input_schema_ref": "form:resident-parking-permit-application"}
},
{
"id": "review-evidence",
"type": "workflow.review",
"label": "Review application evidence",
"config": {
"title": "Review resident parking permit evidence",
"reviewer": {
"kind": "group",
"id": {"$data": "responsible_group_slug"},
"label": {"$data": "responsible_group_name"}
},
"due_after": "P14D",
"required_evidence": [
"identity",
"primary_residence",
"vehicle_registration"
],
"view_surface_ids": []
}
},
{
"id": "record-decision",
"type": "workflow.activity",
"label": "Record formal decision",
"config": {
"title": "Record the resident parking permit decision",
"instructions": "Record the operative result, reasoning, legal basis, remedy, and exact evidence references through the Decisions capability.",
"assignee": {
"kind": "group",
"id": {"$data": "responsible_group_slug"},
"label": {"$data": "responsible_group_name"}
},
"due_after": "P7D",
"view_surface_ids": []
}
},
{
"id": "verify-payment",
"type": "workflow.activity",
"label": "Verify payment evidence",
"config": {
"title": "Verify the resident parking permit fee",
"instructions": "Verify the EUR 30.00 obligation, immutable receipt evidence, currency, amount, and transaction reference before completion.",
"assignee": {
"kind": "group",
"id": {"$data": "responsible_group_slug"},
"label": {"$data": "responsible_group_name"}
},
"due_after": "P14D",
"view_surface_ids": []
}
},
{
"id": "completed",
"type": "workflow.end.completed",
"label": "Permit journey complete",
"config": {"output_mapping": {}}
}
],
"edges": [
{"id": "start-review", "source": "start", "target": "review-evidence"},
{"id": "review-decision", "source": "review-evidence", "source_port": "approved", "target": "record-decision"},
{"id": "decision-payment", "source": "record-decision", "target": "verify-payment"},
{"id": "payment-completed", "source": "verify-payment", "target": "completed"}
],
"metadata": {
"reference_journey": "resident-parking-permit",
"locale": "de-DE",
"payment_amount_minor": 3000,
"payment_currency": "EUR"
}
},
"metadata": {
"reference_package": "product.service-to-decision",
"form_id": "resident-parking-permit-application"
}
}
]
}
}
], ],
"evidence": [ "evidence": [
{ {
@@ -45,6 +375,11 @@
"reference": "packages/product/service-to-decision/README.md", "reference": "packages/product/service-to-decision/README.md",
"summary": "Defines the package boundary, authority path, recovery contract, and known operational limits." "summary": "Defines the package boundary, authority path, recovery contract, and known operational limits."
}, },
{
"kind": "target_test",
"reference": "tests/fixtures/resident_parking_permit_journey.json",
"summary": "Pins the resident parking permit actors, channels, exact inputs, work item, formal outcome, filing target, and remaining manual acceptance gates."
},
{ {
"kind": "target_test", "kind": "target_test",
"reference": "tests/test_institutional_governance_journey.py", "reference": "tests/test_institutional_governance_journey.py",
+1
View File
@@ -30,6 +30,7 @@
-e ../govoplan-parties -e ../govoplan-parties
-e ../govoplan-mandates -e ../govoplan-mandates
-e ../govoplan-decisions -e ../govoplan-decisions
-e ../govoplan-payments
-e ../govoplan-connectors -e ../govoplan-connectors
-e ../govoplan-datasources -e ../govoplan-datasources
-e ../govoplan-dataflow -e ../govoplan-dataflow
+15 -15
View File
@@ -1,18 +1,18 @@
# Whole-product release install from immutable, independently versioned module tags. # Whole-product release install from immutable, independently versioned module tags.
# Only add a module after its referenced tag has been published. # Only add a module after its referenced tag has been published.
../govoplan-core[server] ../govoplan-core[server]
govoplan-tenancy @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-tenancy.git@v0.1.18 govoplan-tenancy @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-tenancy.git@v0.1.20
govoplan-organizations @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-organizations.git@v0.1.18 govoplan-organizations @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-organizations.git@v0.1.20
govoplan-identity @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-identity.git@v0.1.18 govoplan-identity @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-identity.git@v0.1.20
govoplan-idm @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-idm.git@v0.1.18 govoplan-idm @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-idm.git@v0.1.24
govoplan-access @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-access.git@v0.1.18 govoplan-access @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-access.git@v0.1.23
govoplan-admin @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-admin.git@v0.1.18 govoplan-admin @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-admin.git@v0.1.22
govoplan-policy @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-policy.git@v0.1.18 govoplan-policy @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-policy.git@v0.1.22
govoplan-audit @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-audit.git@v0.1.18 govoplan-audit @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-audit.git@v0.1.20
govoplan-dashboard @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-dashboard.git@v0.1.18 govoplan-dashboard @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-dashboard.git@v0.1.20
govoplan-files @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-files.git@v0.1.18 govoplan-files @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-files.git@v0.1.24
govoplan-mail @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-mail.git@v0.1.18 govoplan-mail @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-mail.git@v0.1.26
govoplan-campaign @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-campaign.git@v0.1.18 govoplan-campaign @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-campaign.git@v0.1.27
govoplan-calendar @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-calendar.git@v0.1.18 govoplan-calendar @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-calendar.git@v0.1.22
govoplan-docs @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-docs.git@v0.1.18 govoplan-docs @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-docs.git@v0.1.22
govoplan-ops @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-ops.git@v0.1.18 govoplan-ops @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-ops.git@v0.1.21
+103
View File
@@ -0,0 +1,103 @@
{
"id": "resident-parking-permit-berlin-style-reference",
"title": "Resident parking permit",
"title_de": "Anwohnerparkausweis",
"locale": "de-DE",
"service": {
"object_id": "resident-parking-permit",
"key": "resident_parking_permit.apply",
"version": "6",
"audience": "resident",
"required_evidence_types": [
"application",
"identity",
"primary_residence",
"vehicle_registration"
],
"channels": ["portal", "assisted"]
},
"form": {
"object_id": "resident-parking-permit-application",
"version": "3",
"fields": {
"applicant_name": "Ada Lovelace",
"applicant_email": "ada.lovelace@example.test",
"residence_address": "Musterstrasse 17, 10115 Berlin",
"licence_plate": "B-AL 1843"
}
},
"status_access": {
"mode": "email_link",
"email_field_key": "applicant_email",
"token_ttl_seconds": 1800,
"request_limit_per_hour": 3
},
"assisted_intake": {
"channel": "counter",
"affected_party_ref": "party:resident-ada-lovelace",
"represented_party_ref": null,
"authority_basis": "self",
"purpose": "Apply for a resident parking permit.",
"legal_basis_ref": "law:resident-parking-permit",
"consent_basis": "in-person-confirmation",
"notice_given": true,
"responsible_function_ref": "function:parking-permits",
"language": "de",
"accessibility_needs": ["plain-language"],
"confirmation_method": "written_preview",
"confirmation_outcome": "confirmed"
},
"case": {
"type_key": "resident-parking-permit-application",
"number": "RPP-2026-0001",
"initial_status": "intake",
"decided_status": "decided",
"deadline_days": 30
},
"workflow": {
"definition_name": "Resident parking permit decision",
"work_item_title": "Decide the resident parking permit application",
"instructions": "Review identity, primary residence, vehicle evidence, and the effective local rule before recording the decision."
},
"decision": {
"type": "resident-parking-permit",
"operative_result": "Resident parking permit granted.",
"reasoning": "Identity, primary residence, vehicle registration, and the effective local rule were verified.",
"delivery_channel": "postbox",
"remedy": "review:administrative-court"
},
"payment": {
"mode": "manual",
"amount_minor": 3000,
"currency": "EUR",
"subject": "Resident parking permit fee",
"due_days": 14,
"evidence_owner": "files"
},
"records": {
"file_plan_key": "traffic.resident-parking-permits",
"retention_policy_ref": "records:resident-parking-permit"
},
"acceptance": {
"automated": [
"The published service and exact form revision drive digital intake.",
"An authenticated assisted session uses the same exact form and validation rules while retaining purpose, authority, channel, party, accessibility, source, correction, and read-back provenance.",
"The configured applicant email issues a short-lived, hash-only status link through Notifications and exposes only the bounded status timeline.",
"An idempotent replay returns the same persisted submission.",
"The human review handoff survives a database-session restart and remains visible in Tasks until completion.",
"The production self-service and assisted WebUI paths preserve keyboard order, accessible names, WCAG 2.1 A/AA automation, and responsive geometry at desktop and mobile widths.",
"The assisted operator can assign independent source, confidence, and governed declaring-party, document, or system references to every populated field before immutable read-back.",
"The formal decision retains party, mandate, legal-basis, evidence, delivery, review, and exact revision references.",
"The Case-bound payment handoff creates a replay-safe obligation and accepts a full manual receipt only with exact amount, currency, transaction reference, and immutable evidence.",
"Forms Runtime, Cases, and Decisions can expose exact snapshots for explicit eAkte filing."
],
"manual_or_target": [
"Perform physical screen-reader spot checks for the digital journey at desktop and mobile widths.",
"Perform physical screen-reader spot checks for the assisted operator journey at desktop and mobile widths.",
"Open, resend, expire, and revoke the applicant status link with keyboard and screen reader at desktop and mobile widths.",
"Verify the configured Postbox or external delivery provider, including unknown outcome and reconciliation.",
"Restore the pinned composition and reconstruct the exact form, case, decision, delivery evidence, and eAkte chronology.",
"Transfer through a named archive profile and retain independently signed target evidence."
]
}
}
+124
View File
@@ -0,0 +1,124 @@
from __future__ import annotations
from copy import deepcopy
import json
from pathlib import Path
import sys
import tempfile
import unittest
META_ROOT = Path(__file__).resolve().parents[1]
ASSESSMENT_TOOLS_ROOT = META_ROOT / "tools" / "assessments"
RELEASE_TOOLS_ROOT = META_ROOT / "tools" / "release"
for tools_root in (ASSESSMENT_TOOLS_ROOT, RELEASE_TOOLS_ROOT):
if str(tools_root) not in sys.path:
sys.path.insert(0, str(tools_root))
from govoplan_assessment.report_generator import ( # noqa: E402
AssessmentGenerationError,
load_bounded_json,
render_assessment_markdown,
validate_report_input,
)
class CapabilityFitGenerationTests(unittest.TestCase):
@classmethod
def setUpClass(cls) -> None:
cls.assessment = json.loads(
(META_ROOT / "docs" / "capability-fit-current.json").read_text("utf-8")
)
cls.schema = json.loads(
(META_ROOT / "docs" / "capability-fit.schema.json").read_text("utf-8")
)
def test_current_assessment_renders_every_conclusion_from_one_input(self) -> None:
validate_report_input(assessment=self.assessment, schema=self.schema)
first = render_assessment_markdown(self.assessment)
second = render_assessment_markdown(deepcopy(self.assessment))
self.assertEqual(first, second)
self.assertIn("## Facts", first)
self.assertIn("## Decisions", first)
self.assertIn("## Unresolved decisions", first)
self.assertIn("## Risks and residual risks", first)
self.assertIn("## Proof-of-concept and promotion checks", first)
self.assertIn("### Controlled Campaign pilot", first)
self.assertIn("### Small-production candidate", first)
self.assertIn("## Functional matrix context", first)
self.assertIn("### Manual workarounds", first)
self.assertIn("### Blockers", first)
self.assertIn("Workflow and workflow-driven user stories", first)
for status in self.schema["$defs"]["status"]["enum"]:
self.assertIn(f"`{status}`", first)
for collection in ("capabilities", "infrastructure", "data_flows"):
for item in self.assessment[collection]:
self.assertIn(item["id"], first)
infrastructure_ids = {
item["id"] for item in self.assessment["infrastructure"]
}
self.assertTrue(
{
"runtime.web_api",
"runtime.worker",
"runtime.scheduler",
"data.postgresql",
"queue.redis",
"storage.local",
"storage.object",
"edge.proxy_tls",
"identity.access",
"security.secret_store",
"connectors.mail",
"operations.monitoring",
"operations.audit",
"operations.backup_restore",
"operations.disaster_recovery",
}.issubset(infrastructure_ids)
)
def test_questionnaire_retains_all_required_fit_dimensions(self) -> None:
ids = {
item["id"]
for answers in self.assessment["questionnaire"].values()
for item in answers
}
self.assertTrue(
{
"outcome.reference_journey",
"scope.users_tenants_organizations",
"data.classification",
"data.retention",
"data.privacy_security_disclosure",
"identity.protocols_lifecycle",
"integration.protocols_network",
"workload.tenants_users_concurrency",
"workload.campaign_volume_peaks",
"workload.files_jobs_audit_growth_retention",
"workload.connector_traffic_batches",
"availability.rto_rpo",
"hosting.network_constraints",
"operations.ownership",
"procurement.constraints",
}.issubset(ids)
)
def test_duplicate_keys_and_sensitive_fields_fail_closed(self) -> None:
with tempfile.TemporaryDirectory() as directory:
duplicate = Path(directory) / "duplicate.json"
duplicate.write_text('{"id": 1, "id": 2}', encoding="utf-8")
with self.assertRaisesRegex(AssessmentGenerationError, "Duplicate JSON key"):
load_bounded_json(duplicate, label="assessment")
unsafe = deepcopy(self.assessment)
unsafe["password"] = "must-not-render"
permissive = deepcopy(self.schema)
permissive["additionalProperties"] = True
with self.assertRaisesRegex(AssessmentGenerationError, "sensitive field"):
validate_report_input(assessment=unsafe, schema=permissive)
if __name__ == "__main__":
unittest.main()
+167 -1
View File
@@ -5,9 +5,17 @@ import json
from pathlib import Path from pathlib import Path
import unittest import unittest
from sqlalchemy import create_engine
from sqlalchemy.orm import Session
from govoplan_core.core.configuration_packages import ( from govoplan_core.core.configuration_packages import (
ConfigurationApplyResult,
ConfigurationExportResult,
ConfigurationPackageManifest, ConfigurationPackageManifest,
ConfigurationPlanItem,
ConfigurationPreflightContext, ConfigurationPreflightContext,
ConfigurationPreflightResult,
ConfigurationProviderDescription,
configuration_package_claim_issues, configuration_package_claim_issues,
dry_run_configuration_package, dry_run_configuration_package,
) )
@@ -54,10 +62,19 @@ class ConfigurationPackageArtifactTests(unittest.TestCase):
f"Missing repository for {requirement.module_id}", f"Missing repository for {requirement.module_id}",
) )
provider_module_ids = tuple(
sorted({fragment.module_id for fragment in manifest.fragments})
)
providers = tuple(_ArtifactProvider(module_id) for module_id in provider_module_ids)
supplied_data = {
str(item["key"]): _sample_value(item)
for item in manifest.data_requirements
}
result = dry_run_configuration_package( result = dry_run_configuration_package(
manifest, manifest,
(), providers,
ConfigurationPreflightContext( ConfigurationPreflightContext(
supplied_data=supplied_data,
installed_modules={ installed_modules={
item.module_id: item.version or "workspace" item.module_id: item.version or "workspace"
for item in manifest.required_modules for item in manifest.required_modules
@@ -74,6 +91,155 @@ class ConfigurationPackageArtifactTests(unittest.TestCase):
self.assertIn("product.governed-data-assurance", package_ids) self.assertIn("product.governed-data-assurance", package_ids)
self.assertIn("product.service-to-decision", package_ids) self.assertIn("product.service-to-decision", package_ids)
def test_service_to_decision_package_imports_its_form_as_an_idempotent_local_draft(self) -> None:
from govoplan_forms.backend.configuration_provider import (
_apply_definition,
_preflight_definition,
)
from govoplan_forms.backend.db.models import FormDefinitionRevision
from govoplan_forms.backend.service import get_form_definition
package = ConfigurationPackageManifest.from_mapping(json.loads(
(PACKAGE_ROOT / "product/service-to-decision/package.json").read_text(
encoding="utf-8"
)
))
fragment = next(
item
for item in package.fragments
if item.module_id == "forms" and item.fragment_type == "definition"
)
context = ConfigurationPreflightContext(
tenant_id="tenant-reference-test",
operator_user_id="operator-1",
operator_scopes=frozenset({"system:governance:write"}),
)
engine = create_engine("sqlite+pysqlite:///:memory:")
FormDefinitionRevision.__table__.create(engine)
session = Session(engine)
try:
preflight = _preflight_definition(session, fragment, context)
applied = _apply_definition(session, fragment, context)
session.commit()
replay = _apply_definition(session, fragment, context)
imported = get_form_definition(
session,
type("Principal", (), {"tenant_id": "tenant-reference-test"})(),
form_id="resident-parking-permit-application",
)
finally:
session.close()
engine.dispose()
self.assertFalse(any(item.severity == "blocker" for item in preflight.diagnostics))
self.assertEqual("create", preflight.plan[0].action)
self.assertEqual(1, len(applied.created_refs))
self.assertEqual({}, replay.created_refs)
self.assertIsNotNone(imported)
assert imported is not None
self.assertEqual("tenant-reference-test", imported.reference.tenant_id)
self.assertEqual("draft", imported.publication_state)
self.assertEqual("de", imported.fallback_locale)
def test_service_to_decision_package_materializes_its_tenant_workflow_idempotently(self) -> None:
from govoplan_core.core.configuration_packages import _resolve_fragment_data_references
from govoplan_workflow_engine.backend.configuration_provider import (
apply_workflow_definitions,
preflight_workflow_definitions,
)
from govoplan_workflow_engine.backend.db.models import (
WorkflowDefinition,
WorkflowDefinitionRevision,
)
package = ConfigurationPackageManifest.from_mapping(json.loads(
(PACKAGE_ROOT / "product/service-to-decision/package.json").read_text(
encoding="utf-8"
)
))
fragment = next(
item
for item in package.fragments
if item.module_id == "workflow_engine"
)
context = ConfigurationPreflightContext(
tenant_id="tenant-reference-test",
supplied_data={
"responsible_group_slug": "traffic-permits",
"responsible_group_name": "Traffic permits",
},
)
resolved = _resolve_fragment_data_references(
fragment,
context.supplied_data,
)
engine = create_engine("sqlite+pysqlite:///:memory:")
WorkflowDefinition.__table__.create(engine)
WorkflowDefinitionRevision.__table__.create(engine)
session = Session(engine)
try:
preflight = preflight_workflow_definitions(session, resolved, context)
applied = apply_workflow_definitions(
session,
resolved,
context,
registry=None,
)
replay = apply_workflow_definitions(
session,
resolved,
context,
registry=None,
)
session.commit()
finally:
session.close()
engine.dispose()
self.assertFalse(any(item.severity == "blocker" for item in preflight.diagnostics))
self.assertEqual("create", preflight.plan[0].action)
self.assertIn("resident-parking-permit-review", applied.created_refs)
self.assertEqual({}, replay.created_refs)
self.assertEqual({}, replay.updated_refs)
class _ArtifactProvider:
def __init__(self, module_id: str) -> None:
self.module_id = module_id
def describe(self) -> ConfigurationProviderDescription:
return ConfigurationProviderDescription(module_id=self.module_id)
def preflight(self, fragment, context) -> ConfigurationPreflightResult:
del context
return ConfigurationPreflightResult(plan=(ConfigurationPlanItem(
action="create",
module_id=fragment.module_id,
fragment_type=fragment.fragment_type,
fragment_id=fragment.fragment_id,
),))
def apply(self, fragment, supplied_data, context) -> ConfigurationApplyResult:
del fragment, supplied_data, context
return ConfigurationApplyResult()
def export(self, selection, context) -> ConfigurationExportResult:
del selection, context
return ConfigurationExportResult()
def health(self, import_result, context):
del import_result, context
return ()
def _sample_value(requirement: dict[str, object]) -> object:
data_type = str(requirement.get("data_type") or requirement.get("type") or "string")
if data_type == "boolean":
return False
if data_type in {"integer", "number"}:
return 1
return f"fixture-{requirement['key']}"
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()
+283 -2
View File
@@ -1,8 +1,10 @@
from __future__ import annotations from __future__ import annotations
from contextlib import redirect_stderr, redirect_stdout from contextlib import redirect_stderr, redirect_stdout
from datetime import UTC, datetime, timedelta
import io import io
import json import json
import os
from pathlib import Path from pathlib import Path
import stat import stat
import subprocess import subprocess
@@ -33,6 +35,11 @@ from govoplan_deploy.bundle import ( # noqa: E402
) )
from govoplan_deploy.cli import _receipt_uses_direct_web_port, main # noqa: E402 from govoplan_deploy.cli import _receipt_uses_direct_web_port, main # noqa: E402
import govoplan_deploy.cli as deployment_cli # noqa: E402 import govoplan_deploy.cli as deployment_cli # noqa: E402
from govoplan_deploy.capabilities import ( # noqa: E402
capability_change_impacts,
infrastructure_capability_document,
infrastructure_dependency_inventory_from_mapping,
)
from govoplan_deploy.cluster_evidence import ( # noqa: E402 from govoplan_deploy.cluster_evidence import ( # noqa: E402
collect_kubernetes_evidence, collect_kubernetes_evidence,
) )
@@ -83,6 +90,41 @@ def _kubernetes_test_deployment(component: str, replicas: int) -> dict:
} }
def _dependency_inventory(
installation_id: str,
*,
generated_at: datetime | None = None,
) -> dict:
return {
"schema_version": 1,
"installation_id": installation_id,
"generated_at": (generated_at or datetime.now(UTC)).isoformat(),
"complete": True,
"inspected_capability_ids": ["coordination.redis", "mail.smtp"],
"providers": [
{
"module_id": "mail",
"state": "complete",
"capability_ids": ["mail.smtp"],
"dependency_count": 1,
}
],
"dependencies": [
{
"capability_id": "mail.smtp",
"module_id": "mail",
"dependency_type": "smtp_endpoint",
"dependency_ref": "endpoint:17",
"state": "active",
"scope": "system",
"summary": "Persisted SMTP endpoint has one credential binding.",
"metrics": {"credential_binding_count": 1},
"required_action": "Rebind or migrate this SMTP endpoint.",
}
],
}
class DeploymentInstallerTests(unittest.TestCase): class DeploymentInstallerTests(unittest.TestCase):
def test_kubernetes_evidence_requires_two_node_spread_and_safe_runtime( def test_kubernetes_evidence_requires_two_node_spread_and_safe_runtime(
self, self,
@@ -414,6 +456,27 @@ class DeploymentInstallerTests(unittest.TestCase):
self.assertNotIn("db-secret", rendered) self.assertNotIn("db-secret", rendered)
self.assertNotIn("redis-secret", rendered) self.assertNotIn("redis-secret", rendered)
self.assertNotIn("object-secret", rendered) self.assertNotIn("object-secret", rendered)
capability_config = next(
item
for item in manifest["items"]
if item["kind"] == "ConfigMap"
and item["metadata"]["name"].endswith("infrastructure-capabilities")
)
capability_payload = json.loads(
capability_config["data"]["infrastructure-capabilities.json"]
)
self.assertEqual(1, capability_payload["schema_version"])
self.assertNotIn("db-secret", json.dumps(capability_payload))
api_container = deployments["govoplan-cluster-api"]["spec"]["template"]["spec"]["containers"][0]
self.assertIn(
{
"name": "deployment-capabilities",
"mountPath": "/etc/govoplan/deployment/infrastructure-capabilities.json",
"subPath": "infrastructure-capabilities.json",
"readOnly": True,
},
api_container["volumeMounts"],
)
self.assertNotIn("PersistentVolumeClaim", kinds) self.assertNotIn("PersistentVolumeClaim", kinds)
self.assertNotIn("StatefulSet", kinds) self.assertNotIn("StatefulSet", kinds)
self.assertEqual(3, deployments["govoplan-cluster-api"]["spec"]["replicas"]) self.assertEqual(3, deployments["govoplan-cluster-api"]["spec"]["replicas"])
@@ -723,6 +786,10 @@ class DeploymentInstallerTests(unittest.TestCase):
compose["services"]["load-balancer"]["ports"], compose["services"]["load-balancer"]["ports"],
) )
self.assertNotIn("ports", compose["services"]["web"]) self.assertNotIn("ports", compose["services"]["web"])
self.assertIn(
"./infrastructure-capabilities.json:/etc/govoplan/deployment/infrastructure-capabilities.json:ro",
compose["services"]["api"]["volumes"],
)
self.assertEqual(1, compose["services"]["api"]["scale"]) self.assertEqual(1, compose["services"]["api"]["scale"])
self.assertEqual(1, compose["services"]["web"]["scale"]) self.assertEqual(1, compose["services"]["web"]["scale"])
@@ -948,6 +1015,85 @@ class DeploymentInstallerTests(unittest.TestCase):
reconciled["GARAGE_RPC_SECRET"], reconciled["GARAGE_RPC_SECRET"],
) )
def test_infrastructure_capability_document_exposes_refs_not_secrets(self) -> None:
spec = default_spec(
installation_id="govoplan-shared",
postgres_mode="external",
redis_mode="external",
storage_mode="s3",
mail_mode="external-relay",
module_set="full",
)
values = initial_secrets(
spec,
supplied={
"DATABASE_URL": "postgresql+psycopg://user:database-secret@db.example.test/govoplan",
"REDIS_URL": "rediss://:redis-secret@redis.example.test/0",
"FILE_STORAGE_S3_ENDPOINT_URL": "https://s3.example.test",
"FILE_STORAGE_S3_REGION": "eu-test-1",
"FILE_STORAGE_S3_ACCESS_KEY_ID": "object-key",
"FILE_STORAGE_S3_SECRET_ACCESS_KEY": "object-secret",
"FILE_STORAGE_S3_BUCKET": "govoplan",
},
)
document = infrastructure_capability_document(spec, values)
rendered = json.dumps(document, sort_keys=True)
capabilities = {item["id"]: item for item in document["capabilities"]}
self.assertNotIn("database-secret", rendered)
self.assertNotIn("redis-secret", rendered)
self.assertNotIn("object-secret", rendered)
self.assertNotIn("object-key", rendered)
self.assertEqual("externally_supplied", capabilities["database.postgresql"]["state"])
self.assertEqual("db.example.test", capabilities["database.postgresql"]["endpoint"]["host"])
self.assertEqual(["env:DATABASE_URL"], capabilities["database.postgresql"]["secret_refs"])
self.assertEqual("available_unconfigured", capabilities["mail.smtp"]["state"])
self.assertEqual("mail.smtp-profile", document["post_install_tasks"][0]["id"])
def test_capability_impact_detects_external_endpoint_rebinding(self) -> None:
spec = default_spec(postgres_mode="external", module_set="full")
previous = infrastructure_capability_document(
spec,
{"DATABASE_URL": "postgresql://user:old-secret@old-db.example.test/govoplan"},
)
desired = infrastructure_capability_document(
spec,
{"DATABASE_URL": "postgresql://user:new-secret@new-db.example.test/govoplan"},
)
impacts = {
item.capability_id: item
for item in capability_change_impacts(previous, desired)
}
self.assertEqual("reconfigure", impacts["database.postgresql"].action)
self.assertIn("changed endpoint binding", impacts["database.postgresql"].detail)
self.assertNotIn("old-secret", impacts["database.postgresql"].detail)
self.assertNotIn("new-secret", impacts["database.postgresql"].detail)
def test_capability_impact_includes_provider_dependency_evidence(self) -> None:
previous_spec = default_spec(mail_mode="test-mail", module_set="full")
desired_spec = default_spec(mail_mode="disabled", module_set="full")
inventory = infrastructure_dependency_inventory_from_mapping(
_dependency_inventory(previous_spec.installation_id)
)
impacts = {
item.capability_id: item
for item in capability_change_impacts(
infrastructure_capability_document(previous_spec, {}),
infrastructure_capability_document(desired_spec, {}),
dependency_inventory=inventory,
)
}
mail = impacts["mail.smtp"]
self.assertTrue(mail.inventory_inspected)
self.assertEqual("endpoint:17", mail.actual_dependencies[0].dependency_ref)
self.assertIn("mail:endpoint:17", mail.detail)
self.assertIn("Rebind or migrate", mail.required_action)
def test_replica_counts_drive_compose_and_load_balancer_discovery(self) -> None: def test_replica_counts_drive_compose_and_load_balancer_discovery(self) -> None:
spec = default_spec( spec = default_spec(
storage_mode="garage", storage_mode="garage",
@@ -1077,10 +1223,14 @@ class DeploymentInstallerTests(unittest.TestCase):
with tempfile.TemporaryDirectory(prefix="govoplan-deploy-test-") as directory: with tempfile.TemporaryDirectory(prefix="govoplan-deploy-test-") as directory:
paths = bundle_paths(Path(directory)) paths = bundle_paths(Path(directory))
paths.root.chmod(0o700) paths.root.chmod(0o700)
first_spec = default_spec(mail_mode="test-mail") first_spec = default_spec(mail_mode="test-mail", module_set="full")
first_environment = initial_secrets(first_spec) first_environment = initial_secrets(first_spec)
write_env(paths.env, first_environment) write_env(paths.env, first_environment)
first_plan = build_plan(first_spec, paths, include_host_checks=False) first_plan = build_plan(first_spec, paths, include_host_checks=False)
first_capabilities = infrastructure_capability_document(
first_spec,
first_environment,
)
atomic_write( atomic_write(
paths.receipt, paths.receipt,
canonical_json( canonical_json(
@@ -1091,12 +1241,17 @@ class DeploymentInstallerTests(unittest.TestCase):
first_plan.desired_environment_fingerprint first_plan.desired_environment_fingerprint
), ),
"services": list(render_compose(first_spec)["services"]), "services": list(render_compose(first_spec)["services"]),
"infrastructure_capabilities": first_capabilities,
} }
), ),
mode=0o600, mode=0o600,
) )
second_spec = default_spec(redis_mode="disabled", mail_mode="disabled") second_spec = default_spec(
redis_mode="disabled",
mail_mode="disabled",
module_set="full",
)
write_env( write_env(
paths.env, paths.env,
reconcile_runtime_environment(second_spec, first_environment), reconcile_runtime_environment(second_spec, first_environment),
@@ -1112,6 +1267,79 @@ class DeploymentInstallerTests(unittest.TestCase):
{"redis", "worker", "scheduler", "test-mail"}, {"redis", "worker", "scheduler", "test-mail"},
removed, removed,
) )
impacts = {
item.capability_id: item
for item in second_plan.capability_impacts
}
self.assertEqual("remove", impacts["coordination.redis"].action)
self.assertEqual("remove", impacts["mail.smtp"].action)
self.assertIn("mail", impacts["mail.smtp"].dependent_modules)
self.assertTrue(
any(
check.id == "capability.change.mail.smtp"
and check.level == "warning"
for check in second_plan.checks
)
)
self.assertTrue(second_plan.blocked)
self.assertTrue(
any(
check.id == "capability.dependency_inventory.missing"
and check.level == "error"
for check in second_plan.checks
)
)
atomic_write(
paths.dependency_inventory,
canonical_json(_dependency_inventory(second_spec.installation_id)),
mode=0o600,
)
evidenced_plan = build_plan(
second_spec,
paths,
include_host_checks=False,
)
self.assertFalse(
any(
check.level == "error"
and check.id.startswith("capability.dependency_inventory.")
for check in evidenced_plan.checks
)
)
self.assertEqual(
"endpoint:17",
{
item.capability_id: item
for item in evidenced_plan.capability_impacts
}["mail.smtp"].actual_dependencies[0].dependency_ref,
)
self.assertTrue(
any(
check.id == "capability.dependency_inventory.current"
and check.level == "ok"
for check in evidenced_plan.checks
)
)
stale = _dependency_inventory(
second_spec.installation_id,
generated_at=datetime.now(UTC) - timedelta(minutes=6),
)
atomic_write(
paths.dependency_inventory,
canonical_json(stale),
mode=0o600,
)
stale_plan = build_plan(second_spec, paths, include_host_checks=False)
self.assertTrue(stale_plan.blocked)
self.assertTrue(
any(
check.id == "capability.dependency_inventory.stale"
for check in stale_plan.checks
)
)
def test_secret_change_is_planned_without_exposing_secret_values(self) -> None: def test_secret_change_is_planned_without_exposing_secret_values(self) -> None:
with tempfile.TemporaryDirectory(prefix="govoplan-deploy-test-") as directory: with tempfile.TemporaryDirectory(prefix="govoplan-deploy-test-") as directory:
@@ -1221,6 +1449,54 @@ class DeploymentInstallerTests(unittest.TestCase):
)[0], )[0],
) )
def test_cli_collects_bounded_private_dependency_inventory(self) -> None:
with tempfile.TemporaryDirectory(prefix="govoplan-deploy-test-") as directory:
root = Path(directory) / "installation"
self.assertEqual(
0,
run_cli(
[
"init",
"--non-interactive",
"--directory",
str(root),
]
)[0],
)
payload = _dependency_inventory("govoplan-local")
response = MagicMock()
response.__enter__.return_value = response
response.geturl.return_value = "https://ops.example.test/inventory"
response.read.return_value = json.dumps(payload).encode("utf-8")
fetch = MagicMock(return_value=response)
with (
patch.dict(os.environ, {"TEST_OPS_KEY": "secret-api-key"}),
patch.object(deployment_cli, "urlopen", fetch),
):
result, stdout, stderr = run_cli(
[
"collect-infrastructure-inventory",
"--directory",
str(root),
"--ops-url",
"https://ops.example.test/inventory",
"--api-key-env",
"TEST_OPS_KEY",
]
)
self.assertEqual(0, result, stderr)
self.assertIn("1 record(s)", stdout)
evidence_path = root / "infrastructure-dependency-inventory.json"
self.assertEqual(0o600, stat.S_IMODE(evidence_path.stat().st_mode))
self.assertNotIn(
"secret-api-key",
evidence_path.read_text(encoding="utf-8"),
)
request = fetch.call_args.args[0]
self.assertEqual("secret-api-key", request.get_header("X-api-key"))
def test_cli_requires_external_url_when_switching_from_managed(self) -> None: def test_cli_requires_external_url_when_switching_from_managed(self) -> None:
with tempfile.TemporaryDirectory(prefix="govoplan-deploy-test-") as directory: with tempfile.TemporaryDirectory(prefix="govoplan-deploy-test-") as directory:
root = Path(directory) / "installation" root = Path(directory) / "installation"
@@ -1480,6 +1756,11 @@ class DeploymentInstallerTests(unittest.TestCase):
receipt["listen"], receipt["listen"],
) )
self.assertNotIn("installer", receipt["services"]) self.assertNotIn("installer", receipt["services"])
self.assertEqual(
1,
receipt["infrastructure_capabilities"]["schema_version"],
)
self.assertTrue((root / "infrastructure-capabilities.json").is_file())
def test_installation_root_symlink_is_rejected(self) -> None: def test_installation_root_symlink_is_rejected(self) -> None:
if not hasattr(Path, "symlink_to"): if not hasattr(Path, "symlink_to"):
+61
View File
@@ -0,0 +1,61 @@
from __future__ import annotations
import re
import unittest
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
DOCS = ROOT / "docs"
MARKDOWN_LINK = re.compile(
r"!?\[[^\n]*?\]\((?P<destination><[^>]+>|[^)\s]+)"
)
class DocumentationStructureTests(unittest.TestCase):
def test_documentation_root_has_one_human_entry_point(self) -> None:
self.assertEqual(
[path.name for path in sorted(DOCS.glob("*.md"))],
["README.md"],
)
def test_documentation_front_doors_exist(self) -> None:
expected = (
DOCS / "strategy" / "PLATFORM_CORE_IDEAS.md",
DOCS / "strategy" / "ROADMAP.md",
DOCS / "strategy" / "STRATEGY_STATUS.md",
DOCS / "strategy" / "REFERENCE_JOURNEY_PROGRAM.md",
)
self.assertFalse([path for path in expected if not path.is_file()])
def test_local_markdown_links_resolve(self) -> None:
broken: list[str] = []
sources = [ROOT / "README.md", *sorted(DOCS.rglob("*.md"))]
for source in sources:
for line_number, line in enumerate(
source.read_text(encoding="utf-8").splitlines(),
start=1,
):
for match in MARKDOWN_LINK.finditer(line):
destination = match.group("destination")
if destination.startswith("<") and destination.endswith(">"):
destination = destination[1:-1]
path_text = destination.split("#", 1)[0]
if (
not path_text
or path_text.startswith(("/", "mailto:", "data:"))
or "://" in path_text
):
continue
target = (source.parent / path_text).resolve()
if not target.is_relative_to(ROOT):
continue
if not target.exists():
broken.append(
f"{source.relative_to(ROOT)}:{line_number}: {destination}"
)
self.assertEqual(broken, [])
if __name__ == "__main__":
unittest.main()
+39 -29
View File
@@ -2,6 +2,8 @@ from __future__ import annotations
from dataclasses import dataclass, replace from dataclasses import dataclass, replace
from datetime import UTC, datetime, timedelta from datetime import UTC, datetime, timedelta
import json
from pathlib import Path
import unittest import unittest
from sqlalchemy import create_engine from sqlalchemy import create_engine
@@ -66,6 +68,11 @@ from govoplan_services.backend.service import SqlServiceDefinitionProvider, reco
NOW = datetime(2026, 8, 1, 10, 0, tzinfo=UTC) NOW = datetime(2026, 8, 1, 10, 0, tzinfo=UTC)
JOURNEY = json.loads(
(Path(__file__).parent / "fixtures/resident_parking_permit_journey.json").read_text(
encoding="utf-8"
)
)
def _reference( def _reference(
@@ -177,31 +184,31 @@ class InstitutionalGovernanceJourneyTests(unittest.TestCase):
service = ServiceDefinition( service = ServiceDefinition(
reference=_reference( reference=_reference(
"service", "service",
"permit-service", JOURNEY["service"]["object_id"],
owner="services", owner="services",
version="5", version=JOURNEY["service"]["version"],
), ),
key="permit.apply", key=JOURNEY["service"]["key"],
temporal=TemporalRevision( temporal=TemporalRevision(
revision="5", revision=JOURNEY["service"]["version"],
valid_from=NOW - timedelta(days=1), valid_from=NOW - timedelta(days=1),
valid_to=NOW + timedelta(days=30), valid_to=NOW + timedelta(days=30),
recorded_at=NOW - timedelta(days=2), recorded_at=NOW - timedelta(days=2),
change_reason="Service published.", change_reason="Service published.",
), ),
title="Apply for a permit", title=JOURNEY["title"],
audience=("resident",), audience=("resident",),
legal_bases=(legal_basis,), legal_bases=(legal_basis,),
required_evidence_types=("application", "identity"), required_evidence_types=tuple(JOURNEY["service"]["required_evidence_types"]),
channels=("portal", "postbox"), channels=tuple(JOURNEY["service"]["channels"]) + ("postbox",),
responsible_organization_ref=organization, responsible_organization_ref=organization,
responsible_function_ref=function, responsible_function_ref=function,
mandate_ref=mandate_ref, mandate_ref=mandate_ref,
jurisdiction_refs=(jurisdiction,), jurisdiction_refs=(jurisdiction,),
bindings=( bindings=(
ServiceBinding("case", "permit-application"), ServiceBinding("case", JOURNEY["case"]["type_key"]),
ServiceBinding("workflow", "workflow:permit-review"), ServiceBinding("workflow", "workflow:resident-parking-permit-review"),
ServiceBinding("result", "decision:permit"), ServiceBinding("result", f"decision:{JOURNEY['decision']['type']}"),
), ),
remedy_refs=("review:administrative-court",), remedy_refs=("review:administrative-court",),
publication_state="published", publication_state="published",
@@ -230,37 +237,40 @@ class InstitutionalGovernanceJourneyTests(unittest.TestCase):
upsert_case_status( upsert_case_status(
session, session,
principal, principal,
status_key="intake", status_key=JOURNEY["case"]["initial_status"],
label="Intake", label="Intake",
) )
upsert_case_status( upsert_case_status(
session, session,
principal, principal,
status_key="decided", status_key=JOURNEY["case"]["decided_status"],
label="Decided", label="Decided",
category="decided", category="decided",
) )
upsert_case_type( upsert_case_type(
session, session,
principal, principal,
type_key="permit-application", type_key=JOURNEY["case"]["type_key"],
label="Permit application", label=JOURNEY["title"],
initial_status_key="intake", initial_status_key=JOURNEY["case"]["initial_status"],
allowed_status_keys=("intake", "decided"), allowed_status_keys=(
JOURNEY["case"]["initial_status"],
JOURNEY["case"]["decided_status"],
),
) )
case_record = create_case_from_intake( case_record = create_case_from_intake(
session, session,
principal, principal,
plan=intake, plan=intake,
case_number="PERMIT-2026-0001", case_number=JOURNEY["case"]["number"],
title="Permit application", title=JOURNEY["title"],
status_key=None, status_key=JOURNEY["case"]["initial_status"],
opened_at=NOW, opened_at=NOW,
recorded_at=NOW, recorded_at=NOW,
change_reason="Portal application received.", change_reason="Portal application received.",
idempotency_key="journey-case-create", idempotency_key="journey-case-create",
evidence_refs=(application_evidence,), evidence_refs=(application_evidence,),
deadline_at=NOW + timedelta(days=30), deadline_at=NOW + timedelta(days=JOURNEY["case"]["deadline_days"]),
) )
applicant = _reference("party", "applicant", owner="parties") applicant = _reference("party", "applicant", owner="parties")
@@ -333,7 +343,7 @@ class InstitutionalGovernanceJourneyTests(unittest.TestCase):
change_reason="Permit authority delegated.", change_reason="Permit authority delegated.",
), ),
task_types=("committee.formal_decision",), task_types=("committee.formal_decision",),
authority_types=("permit",), authority_types=(JOURNEY["decision"]["type"],),
organization_unit_refs=(organization,), organization_unit_refs=(organization,),
function_refs=(function,), function_refs=(function,),
jurisdiction_refs=(jurisdiction,), jurisdiction_refs=(jurisdiction,),
@@ -380,7 +390,7 @@ class InstitutionalGovernanceJourneyTests(unittest.TestCase):
object_id="item-1", object_id="item-1",
revision=1, revision=1,
state="deliberating", state="deliberating",
title="Permit application", title=JOURNEY["title"],
parent_id=meeting.object_id, parent_id=meeting.object_id,
recorded_at=NOW, recorded_at=NOW,
change_reason="Agenda item entered deliberation.", change_reason="Agenda item entered deliberation.",
@@ -398,7 +408,7 @@ class InstitutionalGovernanceJourneyTests(unittest.TestCase):
object_id="vote-1", object_id="vote-1",
revision=1, revision=1,
state="closed", state="closed",
title="Vote on permit application", title=f"Vote on {JOURNEY['title'].lower()}",
parent_id=agenda.object_id, parent_id=agenda.object_id,
recorded_at=NOW, recorded_at=NOW,
change_reason="Vote result accepted.", change_reason="Vote result accepted.",
@@ -445,7 +455,7 @@ class InstitutionalGovernanceJourneyTests(unittest.TestCase):
effective_at=NOW, effective_at=NOW,
meeting_ref="meeting-1", meeting_ref="meeting-1",
agenda_item_ref="item-1", agenda_item_ref="item-1",
decision_type="permit", decision_type=JOURNEY["decision"]["type"],
subject_refs=(case_record.reference,), subject_refs=(case_record.reference,),
organization_unit_ref=organization, organization_unit_ref=organization,
function_ref=function, function_ref=function,
@@ -461,8 +471,8 @@ class InstitutionalGovernanceJourneyTests(unittest.TestCase):
), ),
fact_evidence=(application_evidence, address_evidence), fact_evidence=(application_evidence, address_evidence),
legal_bases=(legal_basis,), legal_bases=(legal_basis,),
operative_result="Permit granted.", operative_result=JOURNEY["decision"]["operative_result"],
reasoning="The application satisfies the effective rule.", reasoning=JOURNEY["decision"]["reasoning"],
case_ref=case_record.reference, case_ref=case_record.reference,
jurisdiction_refs=(jurisdiction,), jurisdiction_refs=(jurisdiction,),
party_refs=(applicant, representative), party_refs=(applicant, representative),
@@ -475,7 +485,7 @@ class InstitutionalGovernanceJourneyTests(unittest.TestCase):
classification="restricted", classification="restricted",
purposes=("permit-decision", "party-delivery"), purposes=("permit-decision", "party-delivery"),
legal_basis_refs=("permit-law:3@2026-01",), legal_basis_refs=("permit-law:3@2026-01",),
retention_policy_ref="records:permit", retention_policy_ref=JOURNEY["records"]["retention_policy_ref"],
disclosure_state="partly_disclosable", disclosure_state="partly_disclosable",
), ),
), ),
@@ -568,7 +578,7 @@ class InstitutionalGovernanceJourneyTests(unittest.TestCase):
get_case(session, principal, case_id="case-1"), get_case(session, principal, case_id="case-1"),
) )
self.assertEqual( self.assertEqual(
"decided", JOURNEY["case"]["decided_status"],
get_workspace_object( get_workspace_object(
session, session,
principal, principal,
@@ -583,7 +593,7 @@ class InstitutionalGovernanceJourneyTests(unittest.TestCase):
self.assertEqual("confirmed", reconstruction["observed_effects"][0]["state"]) self.assertEqual("confirmed", reconstruction["observed_effects"][0]["state"])
self.assertEqual("audit:delivery-1", reconstruction["observed_effects"][0]["audit_event_refs"][0]) self.assertEqual("audit:delivery-1", reconstruction["observed_effects"][0]["audit_event_refs"][0])
self.assertEqual("application-1", reconstruction["fact_evidence"][0]["evidence_id"]) self.assertEqual("application-1", reconstruction["fact_evidence"][0]["evidence_id"])
self.assertEqual("The application satisfies the effective rule.", reconstruction["reasoning"]) self.assertEqual(JOURNEY["decision"]["reasoning"], reconstruction["reasoning"])
self.assertEqual("review:administrative-court", reconstruction["review_refs"][0]) self.assertEqual("review:administrative-court", reconstruction["review_refs"][0])
+397 -28
View File
@@ -2,7 +2,10 @@ from __future__ import annotations
from dataclasses import dataclass from dataclasses import dataclass
from datetime import UTC, datetime, timedelta from datetime import UTC, datetime, timedelta
import json
from pathlib import Path
from types import SimpleNamespace from types import SimpleNamespace
from urllib.parse import parse_qs, urlparse
import unittest import unittest
from sqlalchemy import create_engine from sqlalchemy import create_engine
@@ -13,6 +16,7 @@ from govoplan_core.core.access import PrincipalRef
from govoplan_core.core.institutional import ( from govoplan_core.core.institutional import (
CAPABILITY_FORM_DEFINITIONS, CAPABILITY_FORM_DEFINITIONS,
CAPABILITY_SERVICE_DEFINITIONS, CAPABILITY_SERVICE_DEFINITIONS,
EvidenceReference,
FormDefinition, FormDefinition,
FormFieldDefinition, FormFieldDefinition,
InstitutionalReference, InstitutionalReference,
@@ -21,6 +25,11 @@ from govoplan_core.core.institutional import (
TemporalRevision, TemporalRevision,
service_launch_capability, service_launch_capability,
) )
from govoplan_core.core.notifications import CAPABILITY_NOTIFICATIONS_DISPATCH
from govoplan_core.core.payments import (
ManualPaymentReconciliationCommand,
PaymentRequestCommand,
)
from govoplan_core.core.runtime_coordination import ( from govoplan_core.core.runtime_coordination import (
DistributedLease, DistributedLease,
RuntimeIdentity, RuntimeIdentity,
@@ -42,15 +51,30 @@ from govoplan_forms.backend.service import (
record_form_definition, record_form_definition,
) )
from govoplan_forms_runtime.backend.db.models import ( from govoplan_forms_runtime.backend.db.models import (
FormAssistedConfirmation,
FormInstanceEvent, FormInstanceEvent,
FormInstanceIdentity, FormInstanceIdentity,
FormInstanceRevision, FormInstanceRevision,
FormIntakeProfile,
FormIntakeSession,
FormStatusAccessGrant,
FormStatusAccessPolicy,
FormStatusAccessToken,
) )
from govoplan_forms_runtime.backend.intake import FormIntakeService
from govoplan_forms_runtime.backend.service import ( from govoplan_forms_runtime.backend.service import (
FormRuntimeError,
FormRuntimeService, FormRuntimeService,
FormsServiceLauncher, FormsServiceLauncher,
) )
from govoplan_forms_runtime.backend.status_access import FormStatusAccessService
from govoplan_portal.backend.service_directory import PortalServiceDirectory from govoplan_portal.backend.service_directory import PortalServiceDirectory
from govoplan_payments.backend.db.models import (
PaymentEvent,
PaymentObligation,
PaymentReconciliation,
)
from govoplan_payments.backend.service import SqlPaymentRequestProvider
from govoplan_tasks.backend.aggregation import aggregate_work_items from govoplan_tasks.backend.aggregation import aggregate_work_items
from govoplan_workflow_engine.backend.db.models import ( from govoplan_workflow_engine.backend.db.models import (
WorkflowDefinition, WorkflowDefinition,
@@ -82,6 +106,11 @@ from govoplan_workflow_engine.backend.work_items import WorkflowWorkItemProvider
NOW = datetime(2026, 8, 1, 10, 0, tzinfo=UTC) NOW = datetime(2026, 8, 1, 10, 0, tzinfo=UTC)
JOURNEY = json.loads(
(Path(__file__).parent / "fixtures/resident_parking_permit_journey.json").read_text(
encoding="utf-8"
)
)
def _service() -> ServiceDefinition: def _service() -> ServiceDefinition:
@@ -89,24 +118,24 @@ def _service() -> ServiceDefinition:
reference=InstitutionalReference( reference=InstitutionalReference(
kind="service", kind="service",
owner_module="portal", owner_module="portal",
object_id="permit", object_id=JOURNEY["service"]["object_id"],
tenant_id="tenant-1", tenant_id="tenant-1",
version="5", version=JOURNEY["service"]["version"],
), ),
key="permit.apply", key=JOURNEY["service"]["key"],
temporal=TemporalRevision( temporal=TemporalRevision(
revision="5", revision=JOURNEY["service"]["version"],
valid_from=NOW - timedelta(days=1), valid_from=NOW - timedelta(days=1),
valid_to=NOW + timedelta(days=1), valid_to=NOW + timedelta(days=1),
recorded_at=NOW - timedelta(days=2), recorded_at=NOW - timedelta(days=2),
), ),
title="Apply for a permit", title=JOURNEY["title"],
audience=("resident",), audience=(JOURNEY["service"]["audience"],),
required_evidence_types=("application",), required_evidence_types=tuple(JOURNEY["service"]["required_evidence_types"]),
bindings=( bindings=(
ServiceBinding("capability", CAPABILITY_CASES_SERVICE_INTAKE), ServiceBinding("capability", CAPABILITY_CASES_SERVICE_INTAKE),
ServiceBinding("case", "permit-application"), ServiceBinding("case", JOURNEY["case"]["type_key"]),
ServiceBinding("workflow", "workflow:permit-review"), ServiceBinding("workflow", "workflow:resident-parking-permit-review"),
), ),
publication_state="published", publication_state="published",
) )
@@ -161,9 +190,29 @@ class _FormRegistry(_Registry):
self.capabilities[service_launch_capability("form")] = FormsServiceLauncher( self.capabilities[service_launch_capability("form")] = FormsServiceLauncher(
self self
) )
self.notifications = _NotificationProvider()
self.capabilities[CAPABILITY_NOTIFICATIONS_DISPATCH] = self.notifications
def has(self, module_id: str) -> bool: def has(self, module_id: str) -> bool:
return module_id in {"portal", "forms", "forms_runtime"} return module_id in {"portal", "forms", "forms_runtime", "notifications"}
class _NotificationProvider:
def __init__(self) -> None:
self.requests: list[object] = []
def tenant_id_for_notification(self, session, *, notification_id):
return "tenant-1"
def enqueue_notification(self, session, request, *, enqueue_delivery=True):
self.requests.append(request)
return {"id": f"notification-{len(self.requests)}"}
def deliver_notification(self, session, *, notification_id):
return {"id": notification_id}
def deliver_pending(self, session, *, tenant_id=None, limit=50):
return {"delivered": 0}
class _WorkflowTaskRegistry: class _WorkflowTaskRegistry:
@@ -230,8 +279,19 @@ class InstitutionalServiceJourneyTests(unittest.TestCase):
self.assertTrue(entries[0].available) self.assertTrue(entries[0].available)
self.assertIs(definition, entries[0].definition) self.assertIs(definition, entries[0].definition)
self.assertEqual(definition.reference, plan.service_ref) self.assertEqual(definition.reference, plan.service_ref)
self.assertEqual("5", plan.context.service_ref.version) self.assertEqual(JOURNEY["service"]["version"], plan.context.service_ref.version)
self.assertEqual("workflow:permit-review", plan.workflow_refs[0]) self.assertEqual("workflow:resident-parking-permit-review", plan.workflow_refs[0])
def test_reference_fixture_names_remaining_manual_target_evidence(self) -> None:
self.assertEqual("Anwohnerparkausweis", JOURNEY["title_de"])
self.assertEqual("de-DE", JOURNEY["locale"])
self.assertEqual("email_link", JOURNEY["status_access"]["mode"])
self.assertEqual("manual", JOURNEY["payment"]["mode"])
automated = JOURNEY["acceptance"]["automated"]
self.assertEqual(10, len(automated))
self.assertTrue(any("desktop and mobile" in item for item in automated))
self.assertTrue(any("independent source" in item for item in automated))
self.assertEqual(6, len(JOURNEY["acceptance"]["manual_or_target"]))
def test_portal_launches_exact_form_revision_and_persists_submission(self) -> None: def test_portal_launches_exact_form_revision_and_persists_submission(self) -> None:
engine = create_engine("sqlite+pysqlite:///:memory:") engine = create_engine("sqlite+pysqlite:///:memory:")
@@ -252,19 +312,19 @@ class InstitutionalServiceJourneyTests(unittest.TestCase):
reference=InstitutionalReference( reference=InstitutionalReference(
kind="form", kind="form",
owner_module="forms", owner_module="forms",
object_id="permit-application", object_id=JOURNEY["form"]["object_id"],
tenant_id="tenant-1", tenant_id="tenant-1",
version="3", version="3",
), ),
key="permit-application", key=JOURNEY["form"]["object_id"],
temporal=TemporalRevision( temporal=TemporalRevision(
revision="3", revision="3",
valid_from=NOW - timedelta(days=1), valid_from=NOW - timedelta(days=1),
valid_to=NOW + timedelta(days=1), valid_to=NOW + timedelta(days=1),
recorded_at=NOW - timedelta(days=2), recorded_at=NOW - timedelta(days=2),
change_reason="Publish the permit application.", change_reason="Publish the resident parking permit application.",
), ),
title="Permit application", title=JOURNEY["title"],
fields=( fields=(
FormFieldDefinition( FormFieldDefinition(
key="applicant_name", key="applicant_name",
@@ -272,6 +332,25 @@ class InstitutionalServiceJourneyTests(unittest.TestCase):
required=True, required=True,
constraints={"min_length": 2}, constraints={"min_length": 2},
), ),
FormFieldDefinition(
key="applicant_email",
label="Applicant email",
value_type="email",
required=True,
constraints={"min_length": 5},
),
FormFieldDefinition(
key="residence_address",
label="Primary residence address",
required=True,
constraints={"min_length": 5},
),
FormFieldDefinition(
key="licence_plate",
label="Vehicle licence plate",
required=True,
constraints={"min_length": 3},
),
), ),
publication_state="published", publication_state="published",
allow_drafts=True, allow_drafts=True,
@@ -286,18 +365,18 @@ class InstitutionalServiceJourneyTests(unittest.TestCase):
reference=InstitutionalReference( reference=InstitutionalReference(
kind="service", kind="service",
owner_module="services", owner_module="services",
object_id="permit", object_id=JOURNEY["service"]["object_id"],
tenant_id="tenant-1", tenant_id="tenant-1",
version="6", version=JOURNEY["service"]["version"],
), ),
key="permit.apply", key=JOURNEY["service"]["key"],
temporal=TemporalRevision( temporal=TemporalRevision(
revision="6", revision=JOURNEY["service"]["version"],
valid_from=NOW - timedelta(days=1), valid_from=NOW - timedelta(days=1),
valid_to=NOW + timedelta(days=1), valid_to=NOW + timedelta(days=1),
recorded_at=NOW - timedelta(days=2), recorded_at=NOW - timedelta(days=2),
), ),
title="Apply for a permit", title=JOURNEY["title"],
audience=("public",), audience=("public",),
bindings=(binding,), bindings=(binding,),
publication_state="published", publication_state="published",
@@ -311,7 +390,7 @@ class InstitutionalServiceJourneyTests(unittest.TestCase):
reference=service.reference, reference=service.reference,
requested_at=NOW, requested_at=NOW,
idempotency_key="portal-form-launch-1", idempotency_key="portal-form-launch-1",
parameters={"applicant_name": "Ada Lovelace"}, parameters=JOURNEY["form"]["fields"],
) )
replay = directory.launch_service( replay = directory.launch_service(
session, session,
@@ -319,7 +398,7 @@ class InstitutionalServiceJourneyTests(unittest.TestCase):
reference=service.reference, reference=service.reference,
requested_at=NOW, requested_at=NOW,
idempotency_key="portal-form-launch-1", idempotency_key="portal-form-launch-1",
parameters={"applicant_name": "Ada Lovelace"}, parameters=JOURNEY["form"]["fields"],
) )
instance = FormRuntimeService(registry).get_instance( instance = FormRuntimeService(registry).get_instance(
session, session,
@@ -348,11 +427,231 @@ class InstitutionalServiceJourneyTests(unittest.TestCase):
) )
self.assertEqual(NOW, instance.definition_ref.valid_at) self.assertEqual(NOW, instance.definition_ref.valid_at)
self.assertEqual(service.reference, instance.service_ref) self.assertEqual(service.reference, instance.service_ref)
self.assertEqual("Ada Lovelace", instance.values["applicant_name"]) self.assertEqual(JOURNEY["form"]["fields"], instance.values)
finally: finally:
session.close() session.close()
engine.dispose() engine.dispose()
def test_assisted_intake_reuses_exact_form_and_persists_readback_provenance(
self,
) -> None:
engine = create_engine("sqlite+pysqlite:///:memory:")
for table in (
FormDefinitionRevision.__table__,
FormInstanceIdentity.__table__,
FormInstanceRevision.__table__,
FormInstanceEvent.__table__,
FormIntakeProfile.__table__,
FormIntakeSession.__table__,
FormAssistedConfirmation.__table__,
FormStatusAccessPolicy.__table__,
FormStatusAccessGrant.__table__,
FormStatusAccessToken.__table__,
):
table.create(engine)
sessions = sessionmaker(bind=engine)
principal = _Principal()
assisted = JOURNEY["assisted_intake"]
try:
with sessions() as session:
form = record_form_definition(
session,
principal,
definition=FormDefinition(
reference=InstitutionalReference(
kind="form",
owner_module="forms",
object_id=JOURNEY["form"]["object_id"],
tenant_id="tenant-1",
version=JOURNEY["form"]["version"],
),
key=JOURNEY["form"]["object_id"],
temporal=TemporalRevision(
revision=JOURNEY["form"]["version"],
recorded_at=NOW - timedelta(days=2),
change_reason="Publish the resident parking permit application.",
),
title=JOURNEY["title"],
fields=tuple(
FormFieldDefinition(
key=key,
label=key.replace("_", " ").title(),
value_type=(
"email" if key == "applicant_email" else "text"
),
required=True,
constraints={"min_length": 2},
)
for key in JOURNEY["form"]["fields"]
),
publication_state="published",
allow_drafts=True,
handoff_kinds=("case",),
),
)
registry = _FormRegistry(_service())
status_access = JOURNEY["status_access"]
FormStatusAccessService(registry).upsert_policy(
session,
principal,
definition_ref=form.reference,
mode=status_access["mode"],
enabled=True,
email_field_key=status_access["email_field_key"],
token_ttl_seconds=status_access["token_ttl_seconds"],
request_limit_per_hour=status_access[
"request_limit_per_hour"
],
recorded_at=NOW,
)
intake = FormIntakeService(registry)
profile = intake.create_profile(
session,
principal,
definition_ref=form.reference,
mode="assisted",
custodian_ref=assisted["responsible_function_ref"],
recorded_at=NOW,
)
started = intake.start_assisted(
session,
principal,
profile_id=profile.profile_id,
values=JOURNEY["form"]["fields"],
channel=assisted["channel"],
affected_party_ref=assisted["affected_party_ref"],
represented_party_ref=assisted["represented_party_ref"],
authority_basis=assisted["authority_basis"],
purpose=assisted["purpose"],
legal_basis_ref=assisted["legal_basis_ref"],
consent_basis=assisted["consent_basis"],
notice_given=assisted["notice_given"],
responsible_function_ref=assisted["responsible_function_ref"],
language=assisted["language"],
accessibility_needs=assisted["accessibility_needs"],
field_sources={
key: {
"source": "person_statement",
"confidence": "stated",
"declared_by_ref": assisted["affected_party_ref"],
}
for key in JOURNEY["form"]["fields"]
},
idempotency_key="resident-permit-assisted-start",
recorded_at=NOW + timedelta(minutes=1),
)
self.assertEqual(form.reference, started.instance.definition_ref)
self.assertEqual(JOURNEY["form"]["fields"], started.instance.values)
self.assertEqual(
assisted["purpose"], started.instance.metadata["intake"]["purpose"]
)
instance_id = started.instance.instance_id
session.commit()
with sessions() as resumed:
runtime = FormRuntimeService(registry)
current = runtime.get_instance(
resumed,
principal,
instance_id=instance_id,
)
self.assertIsNotNone(current)
with self.assertRaisesRegex(FormRuntimeError, "read-back confirmation"):
runtime.submit_instance(
resumed,
principal,
instance_id=instance_id,
expected_revision=current.revision,
values=current.values,
attachment_refs=(),
signature_refs=(),
idempotency_key="resident-permit-assisted-unconfirmed",
recorded_at=NOW + timedelta(minutes=2),
)
confirmation = FormIntakeService(registry).record_assisted_confirmation(
resumed,
principal,
instance_id=instance_id,
expected_revision=current.revision,
values=current.values,
attachment_refs=(),
signature_refs=(),
outcome=assisted["confirmation_outcome"],
method=assisted["confirmation_method"],
confirmed_by_ref=assisted["affected_party_ref"],
confirmed_at=NOW + timedelta(minutes=3),
idempotency_key="resident-permit-assisted-readback",
field_sources={
key: {
"source": "person_statement",
"confidence": "stated",
"declared_by_ref": assisted["affected_party_ref"],
}
for key in JOURNEY["form"]["fields"]
},
)
submitted = runtime.submit_instance(
resumed,
principal,
instance_id=instance_id,
expected_revision=current.revision,
values=current.values,
attachment_refs=(),
signature_refs=(),
idempotency_key="resident-permit-assisted-submit",
recorded_at=NOW + timedelta(minutes=4),
)
resumed.commit()
self.assertEqual("submitted", submitted.status)
self.assertEqual(current.revision, confirmation.instance_revision)
self.assertEqual(assisted["affected_party_ref"], confirmation.confirmed_by_ref)
status_service = FormStatusAccessService(registry)
access = status_service.access_summary_for_instance(
resumed,
tenant_id="tenant-1",
instance_id=instance_id,
)
self.assertIsNotNone(access)
tracking_id = str(access["tracking_id"])
challenge = status_service.public_access_challenge(
resumed,
tracking_id=tracking_id,
)
self.assertEqual("email_link", challenge["mode"])
self.assertFalse(
status_service.request_email_link(
resumed,
tracking_id=tracking_id,
email="wrong@example.test",
requested_at=NOW + timedelta(minutes=5),
)
)
self.assertTrue(
status_service.request_email_link(
resumed,
tracking_id=tracking_id,
email=JOURNEY["form"]["fields"]["applicant_email"],
requested_at=NOW + timedelta(minutes=6),
)
)
notification = registry.notifications.requests[-1]
query = parse_qs(urlparse(notification.action_url).query)
projection = status_service.get_public_projection(
resumed,
tracking_id=tracking_id,
token=query["token"][0],
observed_at=NOW + timedelta(minutes=7),
)
self.assertEqual("submitted", projection["status"])
self.assertEqual(JOURNEY["title"], projection["title"])
self.assertEqual(
["submitted"],
[item["status"] for item in projection["timeline"]],
)
self.assertNotIn("values", projection)
finally:
engine.dispose()
def test_workflow_handoff_survives_session_reopen_and_projects_into_tasks( def test_workflow_handoff_survives_session_reopen_and_projects_into_tasks(
self, self,
) -> None: ) -> None:
@@ -392,7 +691,7 @@ class InstitutionalServiceJourneyTests(unittest.TestCase):
tenant_id="tenant-1", tenant_id="tenant-1",
actor_id="account-1", actor_id="account-1",
payload=WorkflowDefinitionCreateRequest( payload=WorkflowDefinitionCreateRequest(
name="Permit decision", name=JOURNEY["workflow"]["definition_name"],
graph=WorkflowGraph( graph=WorkflowGraph(
nodes=[ nodes=[
WorkflowNode( WorkflowNode(
@@ -403,8 +702,8 @@ class InstitutionalServiceJourneyTests(unittest.TestCase):
id="review", id="review",
type="workflow.activity", type="workflow.activity",
config={ config={
"title": "Decide the permit application", "title": JOURNEY["workflow"]["work_item_title"],
"instructions": "Review the filed evidence and record the decision.", "instructions": JOURNEY["workflow"]["instructions"],
"assignee": "account:account-1", "assignee": "account:account-1",
"due_after": "2d", "due_after": "2d",
}, },
@@ -464,7 +763,7 @@ class InstitutionalServiceJourneyTests(unittest.TestCase):
self.assertEqual(1, work.total) self.assertEqual(1, work.total)
self.assertEqual(step_id, work.items[0].id) self.assertEqual(step_id, work.items[0].id)
self.assertEqual( self.assertEqual(
"Decide the permit application", JOURNEY["workflow"]["work_item_title"],
work.items[0].title, work.items[0].title,
) )
self.assertTrue(work.items[0].action_url.startswith("/workflow?")) self.assertTrue(work.items[0].action_url.startswith("/workflow?"))
@@ -496,6 +795,76 @@ class InstitutionalServiceJourneyTests(unittest.TestCase):
bind_process_runtime_identity(None) bind_process_runtime_identity(None)
engine.dispose() engine.dispose()
def test_case_bound_payment_handoff_is_replay_safe_and_evidence_bound(
self,
) -> None:
engine = create_engine("sqlite+pysqlite:///:memory:")
for table in (
PaymentObligation.__table__,
PaymentReconciliation.__table__,
PaymentEvent.__table__,
):
table.create(engine)
session = Session(engine)
provider = SqlPaymentRequestProvider()
payment = JOURNEY["payment"]
try:
command = PaymentRequestCommand(
tenant_id="tenant-1",
source_module="cases",
source_resource_type="case",
source_resource_id="case-1",
amount_minor=payment["amount_minor"],
currency=payment["currency"],
subject=payment["subject"],
idempotency_key="resident-permit-case-1-fee",
requested_at=NOW + timedelta(days=1),
requested_by_ref="workflow:resident-parking-permit-review",
due_at=NOW + timedelta(days=1 + payment["due_days"]),
context_refs={
"case": "case-1",
"workflow": "workflow:resident-parking-permit-review",
},
)
requested = provider.request_payment(session, command)
replay = provider.request_payment(session, command)
self.assertEqual(requested["payment_id"], replay["payment_id"])
self.assertTrue(replay["replayed"])
self.assertEqual("case-1", requested["source"]["resource_id"])
paid = provider.reconcile_manual_payment(
session,
ManualPaymentReconciliationCommand(
tenant_id="tenant-1",
payment_id=str(requested["payment_id"]),
amount_minor=payment["amount_minor"],
currency=payment["currency"],
transaction_reference="BANK-RPP-2026-0001",
evidence_ref=EvidenceReference(
kind="document",
owner_module=payment["evidence_owner"],
evidence_id="file-payment-rpp-1",
tenant_id="tenant-1",
version="1",
checksum="b" * 64,
),
idempotency_key="resident-permit-bank-receipt-1",
received_at=NOW + timedelta(days=2),
recorded_at=NOW + timedelta(days=2, minutes=5),
recorded_by_ref="account:payment-officer-1",
),
)
session.commit()
self.assertEqual("paid", paid["status"])
self.assertEqual(
"BANK-RPP-2026-0001",
paid["reconciliation"]["transaction_reference"],
)
self.assertEqual(2, len(paid["events"]))
finally:
session.close()
engine.dispose()
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()
+8 -3
View File
@@ -57,18 +57,23 @@ class PackageRegistryReleaseTests(unittest.TestCase):
self.assertEqual(ARTIFACTS._canonical_sha256(unsigned), digest) self.assertEqual(ARTIFACTS._canonical_sha256(unsigned), digest)
def test_full_profile_is_derived_from_the_developer_meta_package(self) -> None: def test_full_profile_is_derived_from_the_developer_meta_package(self) -> None:
core_version = tomllib.loads(
(ROOT.parent / "govoplan-core/pyproject.toml").read_text(
encoding="utf-8"
)
)["project"]["version"]
selected = PACKAGE_SET.parse_meta_package( selected = PACKAGE_SET.parse_meta_package(
ROOT / "packages/govoplan-meta/pyproject.toml", ROOT / "packages/govoplan-meta/pyproject.toml",
core_version="0.1.18", core_version=core_version,
) )
by_name = {item["name"]: item for item in selected} by_name = {item["name"]: item for item in selected}
self.assertIn("govoplan-core", by_name) self.assertIn("govoplan-core", by_name)
self.assertIn("govoplan-records", by_name) self.assertIn("govoplan-records", by_name)
self.assertEqual("0.1.19", by_name["govoplan-tasks"]["version"]) self.assertEqual("0.1.21", by_name["govoplan-tasks"]["version"])
payload = PACKAGE_SET.generate_package_set( payload = PACKAGE_SET.generate_package_set(
core_version="0.1.18", core_version=core_version,
requirements=ROOT / "requirements-release.txt", requirements=ROOT / "requirements-release.txt",
workspace=ROOT.parent, workspace=ROOT.parent,
profile="full", profile="full",
+9 -4
View File
@@ -22,11 +22,16 @@ class PackageSetDispatchTests(unittest.TestCase):
def test_meta_package_resolves_to_exact_tagged_repository_targets(self) -> None: def test_meta_package_resolves_to_exact_tagged_repository_targets(self) -> None:
targets = MODULE.package_targets() targets = MODULE.package_targets()
self.assertEqual(66, len(targets)) self.assertEqual(73, len(targets))
self.assertEqual(66, len({target.distribution for target in targets})) self.assertEqual(73, len({target.distribution for target in targets}))
by_name = {target.distribution: target for target in targets} by_name = {target.distribution: target for target in targets}
self.assertEqual("v0.1.14", by_name["govoplan-core"].tag) self.assertEqual("v0.1.38", by_name["govoplan-core"].tag)
self.assertEqual("v0.1.8", by_name["govoplan-access"].tag) self.assertEqual("v0.1.22", by_name["govoplan-access"].tag)
self.assertEqual("v0.1.20", by_name["govoplan-dms"].tag)
self.assertEqual("v0.1.20", by_name["govoplan-erp"].tag)
self.assertEqual("v0.1.20", by_name["govoplan-fit-connect"].tag)
self.assertEqual("v0.1.23", by_name["govoplan-idm"].tag)
self.assertEqual("v0.1.21", by_name["govoplan-xrechnung"].tag)
self.assertTrue(by_name["govoplan-core"].tag_exists) self.assertTrue(by_name["govoplan-core"].tag_exists)
self.assertTrue(by_name["govoplan-access"].has_webui) self.assertTrue(by_name["govoplan-access"].has_webui)
self.assertEqual( self.assertEqual(
@@ -169,6 +169,69 @@ class PlatformInterfaceInventoryTests(unittest.TestCase):
), ),
) )
def test_high_risk_help_baseline_is_validated(self) -> None:
with tempfile.TemporaryDirectory() as directory:
path = Path(directory) / "help-baseline.json"
path.write_text(
json.dumps(
{
"schema_version": 1,
"maximum_missing_exact_help": 3,
}
),
encoding="utf-8",
)
self.assertEqual(
3,
inventory._load_high_risk_help_baseline(path)[
"maximum_missing_exact_help"
],
)
path.write_text(
json.dumps(
{
"schema_version": 1,
"maximum_missing_exact_help": -1,
}
),
encoding="utf-8",
)
with self.assertRaisesRegex(ValueError, "non-negative integer"):
inventory._load_high_risk_help_baseline(path)
def test_declaration_strict_mode_rejects_high_risk_help_regression(
self,
) -> None:
result = {
"translation_health": {"missing_catalog_entries": []},
"api": {
"unclassified_endpoints": [],
"stale_endpoint_declarations": [],
},
"declaration_health": {},
"help_health": {
"invalid_risk_annotations": [],
"unresolved_exact_high_risk_help": [],
"high_risk_help_without_german": [],
"missing_exact_high_risk_help": [{"id": "example.delete"}],
"baseline_maximum_missing": 0,
"baseline_regression": True,
},
}
self.assertEqual(
[
"1 high-risk controls lack exact F1 help; baseline permits at most 0"
],
inventory._strict_failures(
result,
check_translations=False,
check_endpoints=False,
check_declarations=True,
),
)
def test_fastapi_route_scanner_includes_router_prefix(self) -> None: def test_fastapi_route_scanner_includes_router_prefix(self) -> None:
tree = ast.parse( tree = ast.parse(
""" """
+27 -2
View File
@@ -5,7 +5,7 @@ import tomllib
import unittest import unittest
from pathlib import Path from pathlib import Path
from govoplan_core.core.modules import ModuleManifest from govoplan_core.core.modules import ModuleManifest, PermissionDefinition
from govoplan_core.core.provider_governance import ( from govoplan_core.core.provider_governance import (
ExternalProviderDeclaration, ExternalProviderDeclaration,
ModuleArchitectureDeclaration, ModuleArchitectureDeclaration,
@@ -73,7 +73,7 @@ class ReleaseCatalogEntrySynthesisTests(unittest.TestCase):
), ),
ModuleMaturityEvidence( ModuleMaturityEvidence(
kind="documentation", kind="documentation",
reference="docs/INSTITUTIONAL_GOVERNANCE_TARGET_ARCHITECTURE.md", reference="docs/architecture/INSTITUTIONAL_GOVERNANCE_TARGET_ARCHITECTURE.md",
summary="Defines the provider declaration contract.", summary="Defines the provider declaration contract.",
), ),
), ),
@@ -88,6 +88,18 @@ class ReleaseCatalogEntrySynthesisTests(unittest.TestCase):
id="example", id="example",
name="Example", name="Example",
version="1.2.3", version="1.2.3",
permissions=(
PermissionDefinition(
scope="example:records:read",
label="Read records",
description="Read example records.",
category="Records",
level="tenant",
module_id="example",
resource="records",
action="read",
),
),
architecture=architecture, architecture=architecture,
external_providers=(provider,), external_providers=(provider,),
), ),
@@ -112,6 +124,19 @@ class ReleaseCatalogEntrySynthesisTests(unittest.TestCase):
"default_authority_mode" "default_authority_mode"
], ],
) )
self.assertEqual(
{
"scope": "example:records:read",
"label": "Read records",
"description": "Read example records.",
"category": "Records",
"level": "tenant",
"resource": "records",
"action": "read",
"deprecated": False,
},
entry["permissions"][0],
)
def test_selective_update_synthesizes_initial_entries_from_package_manifests(self) -> None: def test_selective_update_synthesizes_initial_entries_from_package_manifests(self) -> None:
payload: dict[str, object] = { payload: dict[str, object] = {
@@ -0,0 +1,126 @@
#!/usr/bin/env python3
"""Generate the human capability-fit report from its machine-readable input."""
from __future__ import annotations
import argparse
import os
from pathlib import Path
import sys
import tempfile
META_ROOT = Path(__file__).resolve().parents[2]
ASSESSMENT_TOOLS_ROOT = META_ROOT / "tools" / "assessments"
RELEASE_TOOLS_ROOT = META_ROOT / "tools" / "release"
for tools_root in (ASSESSMENT_TOOLS_ROOT, RELEASE_TOOLS_ROOT):
if str(tools_root) not in sys.path:
sys.path.insert(0, str(tools_root))
from govoplan_assessment.report_generator import ( # noqa: E402
AssessmentGenerationError,
load_bounded_json,
render_assessment_markdown,
validate_report_input,
)
DEFAULT_ASSESSMENT = META_ROOT / "docs" / "capability-fit-current.json"
DEFAULT_SCHEMA = META_ROOT / "docs" / "capability-fit.schema.json"
DEFAULT_OUTPUT = (
META_ROOT
/ "docs"
/ "evidence"
/ "snapshots"
/ "CAPABILITY_AND_INFRASTRUCTURE_FIT.generated.md"
)
MAX_OUTPUT_BYTES = 16 * 1024 * 1024
def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Render a deterministic human report from one capability-fit JSON input."
)
parser.add_argument("--assessment", type=Path, default=DEFAULT_ASSESSMENT)
parser.add_argument("--schema", type=Path, default=DEFAULT_SCHEMA)
parser.add_argument("--output", type=Path, default=DEFAULT_OUTPUT)
parser.add_argument(
"--check",
action="store_true",
help="Fail when the output is missing or differs instead of writing it.",
)
return parser.parse_args(argv)
def main(argv: list[str] | None = None) -> int:
args = parse_args(argv)
try:
assessment = load_bounded_json(args.assessment, label="assessment")
schema = load_bounded_json(args.schema, label="assessment schema")
validate_report_input(assessment=assessment, schema=schema)
rendered = render_assessment_markdown(assessment)
encoded = rendered.encode("utf-8")
if len(encoded) > MAX_OUTPUT_BYTES:
raise AssessmentGenerationError(
f"Generated report exceeds the {MAX_OUTPUT_BYTES}-byte output limit"
)
if args.check:
try:
current = args.output.read_bytes()
except OSError:
current = None
if current != encoded:
print(
f"Capability-fit report is stale: {args.output}",
file=sys.stderr,
)
return 2
print(f"Capability-fit report is current: {args.output}")
return 0
_atomic_write(args.output, encoded)
print(f"Generated capability-fit report: {args.output}")
return 0
except AssessmentGenerationError as exc:
print(str(exc), file=sys.stderr)
return 1
def _atomic_write(path: Path, content: bytes) -> None:
if not path.parent.is_dir():
raise AssessmentGenerationError(
f"Output parent directory does not exist: {path.parent}"
)
if path.is_symlink():
raise AssessmentGenerationError("Output path must not be a symbolic link")
descriptor = -1
temporary_name = ""
try:
descriptor, temporary_name = tempfile.mkstemp(
prefix=".govoplan-fit-report-",
suffix=".tmp",
dir=path.parent,
)
os.fchmod(descriptor, 0o644)
with os.fdopen(descriptor, "wb", closefd=True) as handle:
descriptor = -1
handle.write(content)
handle.flush()
os.fsync(handle.fileno())
os.replace(temporary_name, path)
temporary_name = ""
except OSError as exc:
raise AssessmentGenerationError(
f"Could not write generated report atomically: {exc}"
) from exc
finally:
if descriptor >= 0:
os.close(descriptor)
if temporary_name:
try:
os.unlink(temporary_name)
except FileNotFoundError:
pass
if __name__ == "__main__":
raise SystemExit(main())
@@ -0,0 +1,551 @@
"""Deterministically render one validated capability-fit assessment as Markdown."""
from __future__ import annotations
import hashlib
import json
from pathlib import Path
from typing import Any, Iterable, Mapping, Sequence
from jsonschema import Draft202012Validator, FormatChecker
from jsonschema.exceptions import SchemaError
MAX_ASSESSMENT_BYTES = 16 * 1024 * 1024
STATUS_DEFINITIONS = (
(
"verified",
"Implemented and directly exercised by evidence appropriate to the stated scope.",
),
(
"available_unconfigured",
"Implemented with supporting evidence, but not configured and exercised in the target.",
),
(
"partial",
"A useful subset exists, but a material part of the requirement is missing or unproved.",
),
(
"scaffold",
"Contracts or structure exist, but the end-to-end capability is not usable.",
),
(
"external_system",
"The deployment or another system must supply the capability.",
),
(
"planned",
"Only a concept, backlog item, or design direction exists.",
),
(
"not_fit",
"Evidence shows that the assessed composition cannot meet the requirement.",
),
(
"not_assessed",
"The requirement or target environment is not sufficiently known.",
),
)
class AssessmentGenerationError(ValueError):
"""The assessment cannot be safely validated or rendered."""
def load_bounded_json(path: Path, *, label: str) -> dict[str, Any]:
try:
size = path.stat().st_size
except OSError as exc:
raise AssessmentGenerationError(f"Could not inspect {label}: {exc}") from exc
if size > MAX_ASSESSMENT_BYTES:
raise AssessmentGenerationError(
f"{label} exceeds the {MAX_ASSESSMENT_BYTES}-byte input limit"
)
try:
payload = json.loads(
path.read_text(encoding="utf-8"),
object_pairs_hook=_unique_object,
)
except (OSError, UnicodeError, json.JSONDecodeError) as exc:
raise AssessmentGenerationError(f"Could not read {label}: {exc}") from exc
if not isinstance(payload, dict):
raise AssessmentGenerationError(f"{label} must contain one JSON object")
return payload
def validate_report_input(
*,
assessment: Mapping[str, Any],
schema: Mapping[str, Any],
) -> None:
try:
Draft202012Validator.check_schema(schema)
except SchemaError as exc:
raise AssessmentGenerationError(
f"Assessment schema is invalid: {exc.message}"
) from exc
errors = sorted(
Draft202012Validator(
schema,
format_checker=FormatChecker(),
).iter_errors(assessment),
key=lambda item: tuple(str(part) for part in item.absolute_path),
)
if errors:
details = "; ".join(
f"{_json_path(error.absolute_path)}: {error.message}"
for error in errors[:20]
)
raise AssessmentGenerationError(f"Assessment does not match schema: {details}")
_validate_references(assessment)
_reject_sensitive_keys(assessment)
def render_assessment_markdown(assessment: Mapping[str, Any]) -> str:
"""Return stable Markdown derived only from a validated assessment object."""
assessment_hash = hashlib.sha256(
json.dumps(
assessment,
sort_keys=True,
separators=(",", ":"),
ensure_ascii=True,
).encode("utf-8")
).hexdigest()
scope = _mapping(assessment["scope"])
release = _mapping(assessment["release"])
profile = _mapping(assessment["deployment_profile"])
lines = [
"# GovOPlaN Capability and IT-Infrastructure Fit Assessment",
"",
"> Generated from [`capability-fit-current.json`](../../capability-fit-current.json).",
"> Edit and validate the machine-readable assessment, then regenerate this file;",
"> do not maintain conclusions independently in Markdown.",
"",
"This is an evidence-based fit assessment, not a production approval or",
"security certification. Repository or manifest existence alone never counts",
"as an implemented capability. Unknown target requirements remain explicitly",
"`not_assessed`.",
"",
"## Assessment record",
"",
"| Field | Value |",
"| --- | --- |",
f"| Assessment ID | `{_cell(assessment['assessment_id'])}` |",
f"| Schema version | `govoplan.fit-assessment/{_cell(assessment['schema_version'])}` |",
f"| Assessed on | {_cell(assessment['assessed_at'])} |",
f"| Scope | {_cell(scope['title'])} |",
f"| Release | `{_cell(release['ref'])}` ({_cell(release['kind'])}) |",
f"| Meta commit | `{_cell(release['meta_commit'])}` |",
f"| Deployment profile | `{_cell(profile['id'])}` · `{_cell(profile['status'])}` |",
f"| Configuration packages | {_inline_list(release['configuration_packages'], code=True)} |",
f"| Canonical input SHA-256 | `{assessment_hash}` |",
"",
"## Controlled status vocabulary",
"",
"| Status | Meaning |",
"| --- | --- |",
]
lines.extend(
f"| `{status}` | {_cell(description)} |"
for status, description in STATUS_DEFINITIONS
)
lines.extend(
[
"",
"## Scope and reference journeys",
"",
"Reference journeys:",
"",
*_bullets(scope["reference_journeys"]),
"",
"Explicitly postponed:",
"",
*_bullets(scope["postponed"]),
"",
"## Facts",
"",
*_bullets(assessment["facts"]),
"",
"## Decisions",
"",
*_bullets(assessment["decisions"]),
"",
"## Assumptions",
"",
*_bullets(assessment["assumptions"]),
"",
"## Unresolved decisions",
"",
*_bullets(assessment["open_questions"]),
"",
"## Pinned release and composition",
"",
f"Release reproducible: **{'yes' if release['reproducible'] else 'no'}**.",
"",
]
)
lines.extend(_notes(release.get("notes", [])))
lines.extend(
[
"",
"| Module | Repository and commit | Manifest version | Enabled | Role |",
"| --- | --- | --- | --- | --- |",
]
)
for module_value in assessment["composition"]:
module = _mapping(module_value)
lines.append(
"| `{}` | `{}` @ `{}` | `{}` | {} | {} |".format(
_cell(module["module_id"]),
_cell(module["repository"]),
_cell(module["commit"]),
_cell(module["manifest_version"]),
"yes" if module["enabled"] else "no",
_cell(module["role"]),
)
)
lines.extend(
[
"",
"## Deployment profile",
"",
f"Status: `{_cell(profile['status'])}`",
"",
_text(profile["description"]),
"",
"Evidence:",
"",
*_bullets(_evidence_labels(profile["evidence"])),
"",
"## Recommended scenarios",
"",
]
)
for scenario_value in assessment["scenarios"]:
scenario = _mapping(scenario_value)
lines.extend(
[
f"### {_text(scenario['label'])}",
"",
f"Status: `{_cell(scenario['status'])}`",
"",
_text(scenario["recommendation"]),
"",
f"Composition: {_inline_list(scenario['composition'], code=True)}.",
"",
"Topology:",
"",
*_bullets(scenario["topology"]),
"",
"Conditions:",
"",
*_bullets(scenario["conditions"]),
"",
]
)
functional_context = _mapping(assessment["functional_context"])
lines.extend(
[
"## Functional matrix context",
"",
"### Required modules",
"",
*_bullets(functional_context["required_modules"]),
"",
"### Optional modules",
"",
*_bullets(functional_context["optional_modules"]),
"",
"### External systems and connectors",
"",
*_bullets(functional_context["external_systems"]),
"",
"### Missing contracts",
"",
*_bullets(functional_context["missing_contracts"]),
"",
"### Policy decisions",
"",
*_bullets(functional_context["policy_decisions"]),
"",
"### Manual workarounds",
"",
*_bullets(functional_context["manual_workarounds"]),
"",
"### Blockers",
"",
*_bullets(functional_context["blockers"]),
"",
]
)
lines.extend(
[
"## Assessment questionnaire",
"",
"Every required area remains visible even when its target answer is unknown.",
"",
"| Area | Question | State | Answer | Evidence |",
"| --- | --- | --- | --- | --- |",
]
)
questionnaire = _mapping(assessment["questionnaire"])
for area, answers in questionnaire.items():
for answer_value in _sequence(answers):
answer = _mapping(answer_value)
raw_answer = answer["answer"]
answer_text = (
_inline_list(raw_answer)
if isinstance(raw_answer, list)
else _text(raw_answer) if raw_answer is not None else ""
)
lines.append(
"| {} | {} | `{}` | {} | {} |".format(
_cell(area.replace("_", " ").title()),
_cell(answer["question"]),
_cell(answer["state"]),
_cell(answer_text),
_cell("; ".join(_evidence_labels(answer["evidence"])) or ""),
)
)
lines.extend(_assessed_matrix("Functional capability matrix", assessment["capabilities"]))
lines.extend(_assessed_matrix("Infrastructure matrix", assessment["infrastructure"]))
lines.extend(
[
"## Data flows and trust boundaries",
"",
"| Flow | From → to | Data | Trust boundary | Controls |",
"| --- | --- | --- | --- | --- |",
]
)
for flow_value in assessment["data_flows"]:
flow = _mapping(flow_value)
lines.append(
"| `{}` | {}{} | {} | {} | {} |".format(
_cell(flow["id"]),
_cell(flow["from"]),
_cell(flow["to"]),
_cell(_inline_list(flow["data"])),
_cell(flow["trust_boundary"]),
_cell(_inline_list(flow["controls"])),
)
)
lines.extend(
[
"",
"## Risks and residual risks",
"",
"| Risk | Impact | Treatment | Owner | Residual risk |",
"| --- | --- | --- | --- | --- |",
]
)
for risk_value in assessment["risks"]:
risk = _mapping(risk_value)
lines.append(
"| **{}**<br>{} | {} | {} | {} | {} |".format(
_cell(risk["id"]),
_cell(risk["statement"]),
_cell(risk["impact"]),
_cell(risk["treatment"]),
_cell(risk["owner"] or "unassigned"),
_cell(risk["residual_risk"]),
)
)
lines.extend(
[
"",
"## Recommendations",
"",
*_bullets(assessment["recommendations"]),
"",
"## Proof-of-concept and promotion checks",
"",
*_numbered(assessment["proof_checks"]),
"",
"## Generation contract",
"",
"This report is deterministic output from the schema-validated JSON companion.",
"The generator rejects duplicate JSON keys, schema drift, secret-bearing field",
"names, stale checked-in output, and oversized inputs. A new assessment or",
"release changes the canonical input hash and requires review of the affected",
"evidence and conclusions through the release-aware reassessment tool.",
"",
]
)
return "\n".join(lines)
def _assessed_matrix(title: str, values: object) -> list[str]:
lines = [
"",
f"## {title}",
"",
"| Requirement | Status | Evidence | Conditions and gaps | Recommendation and proof |",
"| --- | --- | --- | --- | --- |",
]
for item_value in _sequence(values):
item = _mapping(item_value)
conditions = [f"Condition: {value}" for value in item["conditions"]]
gaps = [f"Gap: {value}" for value in item["gaps"]]
risks = [f"Risk: {value}" for value in item["risks"]]
lines.append(
"| **{}**<br>{} | `{}` | {} | {} | {}<br>**Proof:** {} |".format(
_cell(item["id"]),
_cell(item["requirement"]),
_cell(item["status"]),
_cell("; ".join(_evidence_labels(item["evidence"])) or "Explicit absence of evidence"),
_cell("; ".join([*conditions, *gaps, *risks]) or ""),
_cell(item["recommendation"] or ""),
_cell(item["proof_check"] or ""),
)
)
return lines
def _evidence_labels(values: object) -> list[str]:
labels: list[str] = []
for value in _sequence(values):
item = _mapping(value)
label = f"{item['kind']}/{item['scope']}: {item['locator']}"
if item.get("note"):
label += f" ({item['note']})"
labels.append(label)
return labels
def _unique_object(pairs: list[tuple[str, Any]]) -> dict[str, Any]:
result: dict[str, Any] = {}
for key, value in pairs:
if key in result:
raise AssessmentGenerationError(f"Duplicate JSON key: {key!r}")
result[key] = value
return result
def _reject_sensitive_keys(value: object, path: tuple[str, ...] = ()) -> None:
forbidden = {
"access_token",
"api_key",
"credential_value",
"password",
"private_key",
"refresh_token",
"secret",
}
if isinstance(value, Mapping):
for key, nested in value.items():
normalized = str(key).strip().casefold()
if normalized in forbidden:
raise AssessmentGenerationError(
f"Assessment contains forbidden sensitive field {_json_path((*path, str(key)))}"
)
_reject_sensitive_keys(nested, (*path, str(key)))
elif isinstance(value, Sequence) and not isinstance(value, (str, bytes)):
for index, nested in enumerate(value):
_reject_sensitive_keys(nested, (*path, str(index)))
def _validate_references(assessment: Mapping[str, Any]) -> None:
composition = [_mapping(item) for item in _sequence(assessment["composition"])]
module_ids = [str(item["module_id"]) for item in composition]
if len(module_ids) != len(set(module_ids)):
raise AssessmentGenerationError("Composition contains duplicate module IDs")
modules = {str(item["module_id"]): item for item in composition}
context = _mapping(assessment["functional_context"])
required = {str(item) for item in _sequence(context["required_modules"])}
optional = {str(item) for item in _sequence(context["optional_modules"])}
unknown_context = (required | optional) - set(modules)
if unknown_context:
raise AssessmentGenerationError(
"Functional context references unknown modules: "
+ ", ".join(sorted(unknown_context))
)
if required & optional:
raise AssessmentGenerationError(
"Functional context cannot mark a module both required and optional"
)
for scenario_value in _sequence(assessment["scenarios"]):
scenario = _mapping(scenario_value)
referenced = {str(item) for item in _sequence(scenario["composition"])}
unknown = referenced - set(modules)
if unknown:
raise AssessmentGenerationError(
f"Scenario {scenario['id']!r} references unknown modules: "
+ ", ".join(sorted(unknown))
)
disabled = sorted(
module_id
for module_id in referenced
if not bool(modules[module_id]["enabled"])
)
if disabled:
raise AssessmentGenerationError(
f"Scenario {scenario['id']!r} references disabled modules: "
+ ", ".join(disabled)
)
for collection in ("capabilities", "infrastructure", "data_flows", "risks"):
identifiers = [
str(_mapping(item)["id"])
for item in _sequence(assessment[collection])
]
if len(identifiers) != len(set(identifiers)):
raise AssessmentGenerationError(
f"Assessment contains duplicate {collection} IDs"
)
def _mapping(value: object) -> Mapping[str, Any]:
if not isinstance(value, Mapping):
raise AssessmentGenerationError("Validated assessment contains a non-object value")
return value
def _sequence(value: object) -> Sequence[Any]:
if not isinstance(value, Sequence) or isinstance(value, (str, bytes)):
raise AssessmentGenerationError("Validated assessment contains a non-list value")
return value
def _text(value: object) -> str:
return str(value).strip()
def _cell(value: object) -> str:
return _text(value).replace("|", "\\|").replace("\r", " ").replace("\n", " ")
def _inline_list(values: object, *, code: bool = False) -> str:
items = [_text(item) for item in _sequence(values)]
if not items:
return "none"
if code:
return ", ".join(f"`{_cell(item)}`" for item in items)
return "; ".join(items)
def _bullets(values: object) -> list[str]:
items = [_text(item) for item in _sequence(values)]
return [f"- {item}" for item in items] or ["- None recorded."]
def _numbered(values: object) -> list[str]:
return [f"{index}. {_text(item)}" for index, item in enumerate(_sequence(values), 1)]
def _notes(values: object) -> list[str]:
items = _bullets(values)
return ["Release notes:", "", *items]
def _json_path(parts: Iterable[object]) -> str:
suffix = "".join(f"[{part}]" if str(part).isdigit() else f".{part}" for part in parts)
return f"${suffix}"
__all__ = (
"AssessmentGenerationError",
"MAX_ASSESSMENT_BYTES",
"load_bounded_json",
"render_assessment_markdown",
"validate_report_input",
)
+265
View File
@@ -0,0 +1,265 @@
#!/usr/bin/env python3
"""Require DSAR coverage or a reviewed no-store rationale for every module."""
from __future__ import annotations
import argparse
import importlib
import json
import re
import sys
from dataclasses import dataclass
from pathlib import Path
META_ROOT = Path(__file__).resolve().parents[2]
EXEMPTIONS_PATH = Path(__file__).with_name("dsar-coverage-exemptions.json")
REPORT_PATH = (
META_ROOT
/ "docs"
/ "evidence"
/ "snapshots"
/ "DSAR_PROVIDER_COVERAGE.generated.md"
)
MODULE_NAME_PATTERN = re.compile(r"[A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)*")
REQUIRED_DOCUMENTATION_TYPES = frozenset({"admin"})
@dataclass(frozen=True, slots=True)
class CoverageRow:
module_id: str
repository: str
migration_owned: bool
capability: str | None
rationale: str
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
"--workspace-root",
type=Path,
default=None,
help="Directory containing GovOPlaN repositories.",
)
parser.add_argument(
"--render",
action="store_true",
help="Print the current matrix instead of comparing the checked-in report.",
)
args = parser.parse_args()
catalog = json.loads((META_ROOT / "repositories.json").read_text(encoding="utf-8"))
workspace_root = (args.workspace_root or Path(catalog["default_parent"])).resolve()
exemptions = _exemptions()
manifests, load_errors = _load_manifests(
workspace_root=workspace_root,
repositories=tuple(catalog["repositories"]),
)
errors = list(load_errors)
rows: list[CoverageRow] = []
manifest_ids = {manifest.id for _, manifest in manifests}
stale_exemptions = sorted(set(exemptions) - manifest_ids)
if stale_exemptions:
errors.append(
"DSAR coverage exemptions reference unknown modules: "
+ ", ".join(stale_exemptions)
)
for repository, manifest in manifests:
expected = f"privacy.dsar.{manifest.id}"
provided = {
item.name
for item in manifest.provides_interfaces
if item.name.startswith("privacy.dsar.")
}
factories = {
name
for name in manifest.capability_factories
if name.startswith("privacy.dsar.")
}
migration_owned = manifest.migration_spec is not None
rationale = exemptions.get(manifest.id)
if provided != factories:
errors.append(
f"{repository}: DSAR interface/factory mismatch: "
f"interfaces={sorted(provided)!r}, factories={sorted(factories)!r}"
)
if provided and provided != {expected}:
errors.append(
f"{repository}: expected only {expected!r}, found {sorted(provided)!r}"
)
capability = (
expected if expected in provided and expected in factories else None
)
if migration_owned and capability is None:
errors.append(
f"{repository}: migration-owning module {manifest.id!r} must provide "
f"and register {expected!r}"
)
if migration_owned and rationale is not None:
errors.append(
f"{repository}: migration-owning module {manifest.id!r} cannot use a "
"no-store DSAR exemption"
)
if not migration_owned and capability is None and rationale is None:
errors.append(
f"{repository}: module {manifest.id!r} needs a DSAR provider or an "
"explicit reviewed no-store rationale"
)
if capability is not None and rationale is not None:
errors.append(
f"{repository}: module {manifest.id!r} has both DSAR coverage and a "
"stale exemption"
)
if capability is not None:
if capability not in manifest.capability_documentation:
errors.append(
f"{repository}: {capability!r} lacks capability documentation"
)
matching_topics = tuple(
topic
for topic in manifest.documentation
if "data-subject-request" in topic.id
)
if not matching_topics or not any(
REQUIRED_DOCUMENTATION_TYPES.issubset(topic.documentation_types)
for topic in matching_topics
):
errors.append(
f"{repository}: DSAR coverage needs a static administrator "
"data-subject-requests DocumentationTopic"
)
rows.append(
CoverageRow(
module_id=manifest.id,
repository=repository,
migration_owned=migration_owned,
capability=capability,
rationale=(
f"Provider `{capability}` is registered and documented."
if capability
else rationale or "MISSING"
),
)
)
report = _report(rows)
if args.render:
print(report, end="")
elif not REPORT_PATH.is_file():
errors.append(f"DSAR coverage report is missing: {REPORT_PATH}")
elif REPORT_PATH.read_text(encoding="utf-8") != report:
errors.append(
"DSAR coverage report is stale; review changes and replace it with "
"the output of tools/checks/check-dsar-coverage.py --render"
)
if errors:
print("\n".join(errors), file=sys.stderr)
return 1
provider_count = sum(row.capability is not None for row in rows)
print(
"DSAR coverage check passed: "
f"{provider_count} providers, {len(rows) - provider_count} reviewed "
f"no-store rationales, {len(rows)} active modules."
)
return 0
def _exemptions() -> dict[str, str]:
values = json.loads(EXEMPTIONS_PATH.read_text(encoding="utf-8"))
if not isinstance(values, dict) or any(
not isinstance(key, str) or not isinstance(value, str) or not value.strip()
for key, value in values.items()
):
raise ValueError("DSAR coverage exemptions must be non-empty string mappings.")
return {key: value.strip() for key, value in values.items()}
def _load_manifests(*, workspace_root: Path, repositories: tuple[dict, ...]):
sources: list[Path] = []
candidates: list[tuple[str, Path, Path]] = []
for repository in repositories:
source = workspace_root / repository["path"] / "src"
if not source.is_dir():
continue
sources.append(source)
candidates.extend(
(repository["name"], source, path)
for path in sorted(source.glob("*/backend/manifest.py"))
)
core_source = workspace_root / "govoplan-core" / "src"
sys.path[:0] = [
str(core_source),
*(str(source) for source in sources if source != core_source),
]
manifests = []
errors = []
for repository, source, path in candidates:
module_name = ".".join(path.relative_to(source).with_suffix("").parts)
if MODULE_NAME_PATTERN.fullmatch(module_name) is None:
errors.append(f"{repository}: unsafe manifest module name {module_name!r}")
continue
try:
module = importlib.import_module(module_name)
manifests.append((repository, module.get_manifest()))
except Exception as exc: # pragma: no cover - emitted as check evidence
errors.append(f"{repository}: could not load {module_name}: {exc}")
return manifests, errors
def _report(rows: list[CoverageRow]) -> str:
ordered = sorted(rows, key=lambda row: row.module_id)
providers = sum(row.capability is not None for row in ordered)
lines = [
"# DSAR Provider Coverage",
"",
"This generated matrix is enforced by `tools/checks/check-dsar-coverage.py`.",
"A migration-owning module must register and document its canonical DSAR provider.",
"Every other active module requires a reviewed explanation of why it owns no",
"persistent subject-data store. Adding a migration invalidates that explanation.",
"",
f"- Active modules: {len(ordered)}",
f"- Registered and documented DSAR providers: {providers}",
f"- Reviewed no-store rationales: {len(ordered) - providers}",
"- Unexplained coverage gaps: 0",
"",
"| Module | Repository | Persistence | Coverage | Rationale |",
"| --- | --- | --- | --- | --- |",
]
for row in ordered:
lines.append(
"| "
+ " | ".join(
(
f"`{row.module_id}`",
f"`{row.repository}`",
"Migration-owned" if row.migration_owned else "No module migration",
"Provider" if row.capability else "Reviewed no-store rationale",
row.rationale.replace("|", "\\|"),
)
)
+ " |"
)
lines.extend(
(
"",
"Provider search, export minimization, retention, and erasure behavior remains",
"documented and tested by each owning module. This matrix verifies adoption and",
"ownership coverage; Core continues to test disabled providers, partial failure,",
"retry, authorization evidence, and horizontally coordinated execution.",
"",
)
)
return "\n".join(lines)
if __name__ == "__main__":
raise SystemExit(main())
+24
View File
@@ -38,6 +38,7 @@ cd "$ROOT"
GOVOPLAN_CORE_ROOT="$ROOT" PYTHON="$PYTHON" CHECK_TESTCLIENT_DEPRECATIONS=1 bash "$META_ROOT/tools/checks/check-dependency-hygiene.sh" GOVOPLAN_CORE_ROOT="$ROOT" PYTHON="$PYTHON" CHECK_TESTCLIENT_DEPRECATIONS=1 bash "$META_ROOT/tools/checks/check-dependency-hygiene.sh"
"$PYTHON" "$META_ROOT/tools/checks/check-contracts.py" --no-impact "$PYTHON" "$META_ROOT/tools/checks/check-contracts.py" --no-impact
PYTHONDONTWRITEBYTECODE=1 "$PYTHON" "$META_ROOT/tools/checks/check-manifest-shapes.py" --require-architecture PYTHONDONTWRITEBYTECODE=1 "$PYTHON" "$META_ROOT/tools/checks/check-manifest-shapes.py" --require-architecture
PYTHONDONTWRITEBYTECODE=1 "$PYTHON" "$META_ROOT/tools/checks/check-dsar-coverage.py"
cd "$META_ROOT" cd "$META_ROOT"
"$PYTHON" tools/inventory/platform-interface-inventory.py --strict-declarations --strict-endpoints "$PYTHON" tools/inventory/platform-interface-inventory.py --strict-declarations --strict-endpoints
@@ -46,6 +47,8 @@ cd "$META_ROOT"
"$PYTHON" -m unittest tests.test_module_package_workflows tests.test_package_registry_release "$PYTHON" -m unittest tests.test_module_package_workflows tests.test_package_registry_release
"$PYTHON" -m unittest tests.test_deployment_installer "$PYTHON" -m unittest tests.test_deployment_installer
"$PYTHON" -m unittest tests.test_capability_fit_evidence "$PYTHON" -m unittest tests.test_capability_fit_evidence
"$PYTHON" -m unittest tests.test_capability_fit_generation tests.test_capability_fit_review
"$PYTHON" tools/assessments/generate-capability-fit-report.py --check
"$PYTHON" -m unittest tests.test_configuration_package_artifacts "$PYTHON" -m unittest tests.test_configuration_package_artifacts
"$PYTHON" -m unittest tests.test_institutional_governance_journey "$PYTHON" -m unittest tests.test_institutional_governance_journey
"$PYTHON" -m unittest tests.test_institutional_service_journey "$PYTHON" -m unittest tests.test_institutional_service_journey
@@ -89,6 +92,9 @@ PY
"$PYTHON" -c 'import govoplan_core.db.bootstrap; import govoplan_access.backend.admin.service; import govoplan_addresses.backend.manifest; import govoplan_files.backend.router; import govoplan_mail.backend.sending.imap; print("targeted backend imports passed")' "$PYTHON" -c 'import govoplan_core.db.bootstrap; import govoplan_access.backend.admin.service; import govoplan_addresses.backend.manifest; import govoplan_files.backend.router; import govoplan_mail.backend.sending.imap; print("targeted backend imports passed")'
"$META_ROOT/tools/checks/check_dependency_boundaries.py" "$META_ROOT/tools/checks/check_dependency_boundaries.py"
"$PYTHON" "$META_ROOT/tools/checks/check-shared-webui-layouts.py"
"$PYTHON" "$META_ROOT/tools/checks/check-shared-webui-primitives.py"
"$PYTHON" "$META_ROOT/tools/checks/check-shared-webui-foundations.py"
"$PYTHON" -m unittest tests.test_module_system "$PYTHON" -m unittest tests.test_module_system
"$PYTHON" -m unittest discover -s /mnt/DATA/git/govoplan-connectors/tests "$PYTHON" -m unittest discover -s /mnt/DATA/git/govoplan-connectors/tests
"$PYTHON" -m unittest discover -s /mnt/DATA/git/govoplan-datasources/tests "$PYTHON" -m unittest discover -s /mnt/DATA/git/govoplan-datasources/tests
@@ -100,6 +106,7 @@ PY
"$PYTHON" -m unittest discover -s /mnt/DATA/git/govoplan-dashboard/tests "$PYTHON" -m unittest discover -s /mnt/DATA/git/govoplan-dashboard/tests
"$PYTHON" -m unittest discover -s /mnt/DATA/git/govoplan-postbox/tests "$PYTHON" -m unittest discover -s /mnt/DATA/git/govoplan-postbox/tests
"$PYTHON" -m unittest discover -s /mnt/DATA/git/govoplan-portal/tests "$PYTHON" -m unittest discover -s /mnt/DATA/git/govoplan-portal/tests
"$PYTHON" -m unittest discover -s /mnt/DATA/git/govoplan-payments/tests
"$PYTHON" -m unittest discover -s /mnt/DATA/git/govoplan-forms/tests "$PYTHON" -m unittest discover -s /mnt/DATA/git/govoplan-forms/tests
"$PYTHON" -m unittest discover -s /mnt/DATA/git/govoplan-forms-runtime/tests "$PYTHON" -m unittest discover -s /mnt/DATA/git/govoplan-forms-runtime/tests
"$PYTHON" -m unittest discover -s /mnt/DATA/git/govoplan-cases/tests "$PYTHON" -m unittest discover -s /mnt/DATA/git/govoplan-cases/tests
@@ -108,6 +115,7 @@ PY
"$PYTHON" -m unittest discover -s /mnt/DATA/git/govoplan-approvals/tests "$PYTHON" -m unittest discover -s /mnt/DATA/git/govoplan-approvals/tests
"$PYTHON" -m unittest discover -s /mnt/DATA/git/govoplan-identity-trust/tests "$PYTHON" -m unittest discover -s /mnt/DATA/git/govoplan-identity-trust/tests
"$PYTHON" -m unittest discover -s /mnt/DATA/git/govoplan-encryption/tests "$PYTHON" -m unittest discover -s /mnt/DATA/git/govoplan-encryption/tests
"$PYTHON" -m pytest -q /mnt/DATA/git/govoplan-wiki/tests
"$PYTHON" -m pytest -q /mnt/DATA/git/govoplan-campaign/tests/test_approval_gate.py "$PYTHON" -m pytest -q /mnt/DATA/git/govoplan-campaign/tests/test_approval_gate.py
"$PYTHON" "$META_ROOT/tools/checks/check-datasource-composition.py" "$PYTHON" "$META_ROOT/tools/checks/check-datasource-composition.py"
"$PYTHON" "$META_ROOT/tools/checks/check-sanctions-screening-composition.py" "$PYTHON" "$META_ROOT/tools/checks/check-sanctions-screening-composition.py"
@@ -115,9 +123,16 @@ PY
"$PYTHON" -m unittest tests.test_api_smoke.ApiSmokeTests.test_mailbox_message_listing_reports_total_count "$PYTHON" -m unittest tests.test_api_smoke.ApiSmokeTests.test_mailbox_message_listing_reports_total_count
cd "$ROOT/webui" cd "$ROOT/webui"
"$NPM" run test:layout-primitives
"$NPM" run test:mail-components "$NPM" run test:mail-components
"$NPM" run test:module-capabilities "$NPM" run test:module-capabilities
"$NPM" run test:module-permutations "$NPM" run test:module-permutations
"$NPM" run test:conformance
"$WEBUI_BIN/tsc" -p /mnt/DATA/git/govoplan-payments/webui/tsconfig.json
cd /mnt/DATA/git/govoplan-payments/webui
"$NPM" run test:interface-pattern
cd /mnt/DATA/git/govoplan-dataflow/webui cd /mnt/DATA/git/govoplan-dataflow/webui
"$NPM" run test:structure "$NPM" run test:structure
@@ -131,6 +146,9 @@ cd /mnt/DATA/git/govoplan-workflow/webui
cd /mnt/DATA/git/govoplan-dashboard/webui cd /mnt/DATA/git/govoplan-dashboard/webui
"$NPM" run test:dashboard-layout "$NPM" run test:dashboard-layout
cd /mnt/DATA/git/govoplan-approvals/webui
"$NPM" run test:workspace-layout
cd /mnt/DATA/git/govoplan-postbox/webui cd /mnt/DATA/git/govoplan-postbox/webui
"$NPM" run test:ui-structure "$NPM" run test:ui-structure
@@ -140,3 +158,9 @@ cd /mnt/DATA/git/govoplan-mail/webui
cd /mnt/DATA/git/govoplan-campaign/webui cd /mnt/DATA/git/govoplan-campaign/webui
"$NPM" run test:policy-ui "$NPM" run test:policy-ui
"$NPM" run test:template-preview "$NPM" run test:template-preview
"$NPM" run test:accessibility-contract
"$NPM" run test:campaign-collaboration
"$NPM" run test:campaign-work
cd /mnt/DATA/git/govoplan-wiki/webui
"$NPM" run test:interface-pattern
+117
View File
@@ -17,6 +17,70 @@ MODULE_NAME_PATTERN = re.compile(
r"[A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)*" r"[A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)*"
) )
REQUIRED_DOCUMENTATION_TYPES = frozenset({"admin", "user"}) REQUIRED_DOCUMENTATION_TYPES = frozenset({"admin", "user"})
CANONICAL_PRODUCT_AREAS = {
"work": (
"i18n:govoplan-core.product_area.work",
"list-checks",
"i18n:govoplan-core.product_area.work_description",
10,
),
"services-cases": (
"i18n:govoplan-core.product_area.services_cases",
"landmark",
"i18n:govoplan-core.product_area.services_cases_description",
20,
),
"records-documents": (
"i18n:govoplan-core.product_area.records_documents",
"folder",
"i18n:govoplan-core.product_area.records_documents_description",
30,
),
"communication": (
"i18n:govoplan-core.product_area.communication",
"mail",
"i18n:govoplan-core.product_area.communication_description",
40,
),
"meetings-decisions": (
"i18n:govoplan-core.product_area.meetings_decisions",
"calendar",
"i18n:govoplan-core.product_area.meetings_decisions_description",
50,
),
"data-assurance": (
"i18n:govoplan-core.product_area.data_assurance",
"database-zap",
"i18n:govoplan-core.product_area.data_assurance_description",
60,
),
"people-responsibility": (
"i18n:govoplan-core.product_area.people_responsibility",
"users",
"i18n:govoplan-core.product_area.people_responsibility_description",
70,
),
}
# These surfaces are intentionally global, administrative, security-policy, or
# shell infrastructure. They remain discoverable through their dedicated shell
# affordance or through "All available tools" instead of a business area.
PRODUCT_AREA_EXEMPT_MODULES = frozenset(
{
"access",
"admin",
"audit",
"dashboard",
"docs",
"encryption",
"identity_trust",
"ops",
"policy",
"quick_access",
"search",
"tenancy",
"views",
}
)
def main() -> int: def main() -> int:
@@ -113,6 +177,42 @@ def main() -> int:
) )
continue continue
frontend = manifest.frontend
has_user_facing_surface = frontend is not None and bool(
frontend.routes
or frontend.public_routes
or frontend.nav_items
or frontend.settings_routes
)
if (
has_user_facing_surface
and not frontend.product_areas
and manifest.id not in PRODUCT_AREA_EXEMPT_MODULES
):
errors.append(
f"{repository_name}: user-facing module {manifest.id!r} has no "
"ProductAreaContribution and is not an explicit global/technical exemption"
)
if frontend is not None:
for contribution in frontend.product_areas:
expected = CANONICAL_PRODUCT_AREAS.get(contribution.id)
actual = (
contribution.label,
contribution.icon,
contribution.description,
contribution.order,
)
if expected is None:
errors.append(
f"{repository_name}: module {manifest.id!r} uses unknown product "
f"area {contribution.id!r}"
)
elif actual != expected:
errors.append(
f"{repository_name}: module {manifest.id!r} redefines canonical "
f"product area {contribution.id!r}; expected {expected!r}, found {actual!r}"
)
repository_root = manifest_path.parents[3] repository_root = manifest_path.parents[3]
if manifest.architecture is None: if manifest.architecture is None:
if args.require_architecture: if args.require_architecture:
@@ -142,6 +242,23 @@ def main() -> int:
print("\n".join(errors), file=sys.stderr) print("\n".join(errors), file=sys.stderr)
return 1 return 1
contributed_product_areas = {
contribution.id
for manifest in manifests
if manifest.frontend is not None
for contribution in manifest.frontend.product_areas
}
missing_product_areas = sorted(
set(CANONICAL_PRODUCT_AREAS) - contributed_product_areas
)
if missing_product_areas:
print(
"Canonical product areas have no contributing module: "
+ ", ".join(missing_product_areas),
file=sys.stderr,
)
return 1
registry = PlatformRegistry() registry = PlatformRegistry()
try: try:
for manifest in manifests: for manifest in manifests:
+2
View File
@@ -26,6 +26,8 @@ cd "$ROOT"
"$PYTHON" "$META_ROOT/tools/checks/check_dependency_boundaries.py" "$PYTHON" "$META_ROOT/tools/checks/check_dependency_boundaries.py"
cd "$META_ROOT" cd "$META_ROOT"
"$PYTHON" -m unittest tests.test_capability_fit_generation
"$PYTHON" tools/assessments/generate-capability-fit-report.py --check
"$PYTHON" -m unittest tests.test_configuration_package_artifacts "$PYTHON" -m unittest tests.test_configuration_package_artifacts
PYTHONPATH="$META_ROOT/../govoplan-portal/src:$META_ROOT/../govoplan-forms/src:$META_ROOT/../govoplan-forms-runtime/src:$META_ROOT/../govoplan-cases/src:$ROOT/src${PYTHONPATH:+:$PYTHONPATH}" \ PYTHONPATH="$META_ROOT/../govoplan-portal/src:$META_ROOT/../govoplan-forms/src:$META_ROOT/../govoplan-forms-runtime/src:$META_ROOT/../govoplan-cases/src:$ROOT/src${PYTHONPATH:+:$PYTHONPATH}" \
"$PYTHON" -m unittest tests.test_institutional_service_journey "$PYTHON" -m unittest tests.test_institutional_service_journey
@@ -262,6 +262,8 @@ cd "$WORK_ROOT/govoplan-campaign/webui"
"$NPM" run test:policy-ui "$NPM" run test:policy-ui
"$NPM" run test:template-preview "$NPM" run test:template-preview
"$NPM" run test:import-utils "$NPM" run test:import-utils
"$NPM" run test:campaign-collaboration
"$NPM" run test:campaign-work
echo echo
echo "Release integration check passed." echo "Release integration check passed."
@@ -0,0 +1,118 @@
#!/usr/bin/env python3
"""Enforce Core ownership of WebUI visual foundations."""
from __future__ import annotations
import pathlib
import re
import sys
META_ROOT = pathlib.Path(__file__).resolve().parents[2]
REPOS_ROOT = META_ROOT.parent
TOKENS_PATH = REPOS_ROOT / "govoplan-core/webui/src/styles/tokens.css"
RAW_HEX_COLOR = re.compile(r"#[0-9a-fA-F]{3,8}\b")
RAW_COLOR_FUNCTION = re.compile(r"\b(?:rgb|rgba|hsl|hsla)\((?!\s*var\()", re.IGNORECASE)
RADIUS_DECLARATION = re.compile(r"border-radius\s*:\s*([^;}]+)")
MEDIA_MAX_WIDTH = re.compile(r"@media[^\n{]*\(max-width\s*:\s*(\d+)px\)")
RESPONSIVE_BANDS = {560, 600, 680, 760, 900, 1100, 1280}
REQUIRED_TOKENS = {
"--radius-hairline",
"--radius-tight",
"--radius-xs",
"--radius-sm",
"--radius-compact",
"--radius-md",
"--radius-lg",
"--radius-xl",
"--radius-round",
"--radius-pill",
"--shadow-drawer-side",
"--shadow-drawer-bottom",
"--action-primary-bg",
"--action-primary-border",
"--action-primary-text",
"--action-danger-bg",
"--action-danger-text",
"--badge-accent-text",
"--data-category-blue",
"--data-category-green",
"--data-category-amber",
"--data-category-purple",
"--data-category-rose",
*(f"--data-series-{index}" for index in range(1, 9)),
}
def line_number(source: str, offset: int) -> int:
return source.count("\n", 0, offset) + 1
def css_files() -> list[pathlib.Path]:
files: list[pathlib.Path] = []
for repository in sorted(REPOS_ROOT.glob("govoplan-*")):
styles = repository / "webui/src"
if styles.is_dir():
files.extend(sorted(styles.rglob("*.css")))
return files
def display_path(path: pathlib.Path) -> str:
return str(path.relative_to(REPOS_ROOT))
def main() -> int:
errors: list[str] = []
tokens = TOKENS_PATH.read_text(encoding="utf-8")
for token in sorted(REQUIRED_TOKENS):
if f"{token}:" not in tokens:
errors.append(f"{display_path(TOKENS_PATH)}: missing required foundation token {token}")
files = css_files()
for path in files:
source = path.read_text(encoding="utf-8")
owns_literals = path == TOKENS_PATH
if not owns_literals:
for pattern, label in (
(RAW_HEX_COLOR, "raw color"),
(RAW_COLOR_FUNCTION, "raw color function"),
):
for match in pattern.finditer(source):
errors.append(
f"{display_path(path)}:{line_number(source, match.start())}: "
f"{label} must use a Core theme token"
)
for match in RADIUS_DECLARATION.finditer(source):
value = match.group(1).strip()
if "var(" not in value and value not in {"0", "inherit", "initial", "unset"}:
errors.append(
f"{display_path(path)}:{line_number(source, match.start())}: "
f"border radius {value!r} must use a Core radius token"
)
for match in MEDIA_MAX_WIDTH.finditer(source):
width = int(match.group(1))
if width not in RESPONSIVE_BANDS:
errors.append(
f"{display_path(path)}:{line_number(source, match.start())}: "
f"{width}px is not a shared responsive band; use one of "
f"{', '.join(f'{value}px' for value in sorted(RESPONSIVE_BANDS))}"
)
if errors:
print("\n".join(errors))
return 1
print(
"Shared WebUI foundation contract passed for "
f"{len(files)} stylesheets and {len(RESPONSIVE_BANDS)} responsive bands."
)
return 0
if __name__ == "__main__":
raise SystemExit(main())
+208
View File
@@ -0,0 +1,208 @@
#!/usr/bin/env python3
"""Keep raw module page frames from growing while shared layouts are adopted."""
from __future__ import annotations
import pathlib
import re
import sys
META_ROOT = pathlib.Path(__file__).resolve().parents[2]
REPOS_ROOT = META_ROOT.parent
BASELINE_PATH = pathlib.Path(__file__).with_name("shared-webui-layout-baseline.txt")
WORKSPACE_BASELINE_PATH = pathlib.Path(__file__).with_name(
"shared-webui-workspace-baseline.txt"
)
RAW_PAGE_FRAME = 'className="content-pad workspace-data-page'
RAW_WORKSPACE = re.compile(r'<div\s+className="workspace(?:\s|\")')
PAGE_LAYOUT_USAGE = re.compile(r"<PageLayout\b")
SEMANTIC_PAGE_LAYOUT_USAGE = re.compile(
r"<PageLayout\s+(?:\n\s*)?archetype="
)
LOCAL_PAGE_LAYOUT = re.compile(r"\b(?:function|class|const)\s+PageLayout\b")
LOCAL_WORKSPACE_LAYOUT = re.compile(
r"\b(?:function|class|const)\s+WorkspaceLayout\b"
)
CENTRAL_LAYOUT = pathlib.Path("govoplan-core/webui/src/components/PageLayout.tsx")
CENTRAL_WORKSPACE_LAYOUT = pathlib.Path(
"govoplan-core/webui/src/components/WorkspaceLayout.tsx"
)
CENTRAL_ACTION_BARS = {
pathlib.Path("govoplan-core/webui/src/components/PageActionBar.tsx"),
pathlib.Path("govoplan-core/webui/src/components/WorkspaceActionBar.tsx"),
}
SEMANTIC_ACTION_USAGE = re.compile(r"<(?:Page|Workspace)ActionBar\b")
EDITOR_ACTION_USAGE = re.compile(
r"<(?:Page|Workspace)ActionBar\b[\s\S]{0,1200}?variant=\"editor\""
)
PANEL_HEADER_ACTION_TOOLBAR = re.compile(
r"<ActionToolbar\b[^>]*\bsurface=\"panel-header\""
)
UNSAVED_GUARD_MARKERS = (
"useUnsavedDraftGuard",
"useCampaignDraftEditor",
"useRegisterUnsavedChanges",
"semantic-editor-guard:",
)
def baseline_paths(path: pathlib.Path) -> set[pathlib.Path]:
return {
pathlib.Path(line.strip())
for line in path.read_text(encoding="utf-8").splitlines()
if line.strip() and not line.lstrip().startswith("#")
}
def source_paths() -> list[pathlib.Path]:
paths: list[pathlib.Path] = []
for repository in sorted(REPOS_ROOT.glob("govoplan*")):
source_root = repository / "webui" / "src"
if source_root.is_dir():
paths.extend(sorted(source_root.rglob("*.tsx")))
return paths
def relative(path: pathlib.Path) -> pathlib.Path:
return path.relative_to(REPOS_ROOT)
def main() -> int:
sources = source_paths()
source_text = {relative(path): path.read_text(encoding="utf-8") for path in sources}
page_consumers = {
path for path, text in source_text.items()
if path != CENTRAL_LAYOUT and PAGE_LAYOUT_USAGE.search(text)
}
workspace_consumers = {
path for path, text in source_text.items()
if path != CENTRAL_WORKSPACE_LAYOUT and "<WorkspaceLayout" in text
}
action_consumers = {
path for path, text in source_text.items()
if path not in CENTRAL_ACTION_BARS and SEMANTIC_ACTION_USAGE.search(text)
}
editor_consumers = {
path for path, text in source_text.items()
if path not in CENTRAL_ACTION_BARS and EDITOR_ACTION_USAGE.search(text)
}
raw_frames = {path for path, text in source_text.items() if RAW_PAGE_FRAME in text}
baseline = baseline_paths(BASELINE_PATH)
available_baseline = {
path for path in baseline if (REPOS_ROOT / path.parts[0]).is_dir()
}
errors: list[str] = []
unexpected = sorted(raw_frames - available_baseline)
if unexpected:
errors.append("New raw page frames must use @govoplan/core-webui PageLayout:")
errors.extend(f"- {path}" for path in unexpected)
resolved = sorted(available_baseline - raw_frames)
if resolved:
errors.append("Remove migrated page frames from the shared-layout baseline:")
errors.extend(f"- {path}" for path in resolved)
raw_workspaces = {
path for path, text in source_text.items() if RAW_WORKSPACE.search(text)
}
workspace_baseline = baseline_paths(WORKSPACE_BASELINE_PATH)
available_workspace_baseline = {
path
for path in workspace_baseline
if (REPOS_ROOT / path.parts[0]).is_dir()
}
unexpected_workspaces = sorted(raw_workspaces - available_workspace_baseline)
if unexpected_workspaces:
errors.append("New raw workspaces must use @govoplan/core-webui WorkspaceLayout:")
errors.extend(f"- {path}" for path in unexpected_workspaces)
resolved_workspaces = sorted(available_workspace_baseline - raw_workspaces)
if resolved_workspaces:
errors.append("Remove migrated workspaces from the shared-workspace baseline:")
errors.extend(f"- {path}" for path in resolved_workspaces)
for path, text in source_text.items():
if path != CENTRAL_LAYOUT and LOCAL_PAGE_LAYOUT.search(text):
errors.append(f"Module-local PageLayout definition is not allowed: {path}")
if path != CENTRAL_WORKSPACE_LAYOUT and LOCAL_WORKSPACE_LAYOUT.search(text):
errors.append(f"Module-local WorkspaceLayout definition is not allowed: {path}")
page_layout_count = len(PAGE_LAYOUT_USAGE.findall(text))
semantic_layout_count = len(SEMANTIC_PAGE_LAYOUT_USAGE.findall(text))
if page_layout_count and semantic_layout_count != page_layout_count:
errors.append(
"Every PageLayout must declare its semantic archetype immediately "
f"after the component name: {path} ({semantic_layout_count}/{page_layout_count})"
)
if (
page_layout_count
and "actions=" in text
and path != pathlib.Path("govoplan-core/webui/src/components/admin/AdminPageLayout.tsx")
and "<PageActionBar" not in text
and "semantic-page-actions: delegated" not in text
):
errors.append(
f"Headed page actions must use the semantic PageActionBar: {path}"
)
if "<PageActionBar" in text and "consequentialActions=" in text:
errors.append(
f"Ambiguous consequential action slots are forbidden; use destructiveActions: {path}"
)
if (
path not in CENTRAL_ACTION_BARS
and PANEL_HEADER_ACTION_TOOLBAR.search(text)
):
errors.append(
"Panel-header actions must use WorkspaceActionBar so their ordering, "
f"state, and destructive separation remain semantic: {path}"
)
if "<WorkspaceFrame" in text and not SEMANTIC_ACTION_USAGE.search(text):
errors.append(
f"WorkspaceFrame routes must declare a semantic page or pane action bar: {path}"
)
for path in editor_consumers:
text = source_text[path]
for required in ('variant="editor"', "state=", "discardAction=", "saveAction="):
if required not in text:
errors.append(f"Editor page is missing {required}: {path}")
if not any(guard in text for guard in UNSAVED_GUARD_MARKERS):
errors.append(f"Editor page is missing an unsaved-change guard: {path}")
for path, text in source_text.items():
if "destructiveActions=" in text and 'variant="danger"' not in text:
errors.append(f"Destructive page actions must contain a danger action: {path}")
core_index = REPOS_ROOT / "govoplan-core/webui/src/index.ts"
if core_index.exists() and "PageLayout, PageHeader" not in core_index.read_text(encoding="utf-8"):
errors.append("Core must export PageLayout and PageHeader from @govoplan/core-webui.")
if core_index.exists() and "WorkspaceLayout" not in core_index.read_text(encoding="utf-8"):
errors.append("Core must export WorkspaceLayout from @govoplan/core-webui.")
if core_index.exists() and "PageActionBar" not in core_index.read_text(encoding="utf-8"):
errors.append("Core must export PageActionBar from @govoplan/core-webui.")
if core_index.exists() and "WorkspaceActionBar" not in core_index.read_text(encoding="utf-8"):
errors.append("Core must export WorkspaceActionBar from @govoplan/core-webui.")
if errors:
print("\n".join(errors), file=sys.stderr)
return 1
print(
"Shared WebUI layout contract passed: "
f"{len(page_consumers)} discovered page consumers, "
f"{sum(len(PAGE_LAYOUT_USAGE.findall(text)) for text in source_text.values())} semantic pages, "
f"{len(action_consumers)} semantic action consumers, "
f"{len(editor_consumers)} guarded editor consumers, "
f"{len(raw_frames)} registered legacy page-frame files; "
f"{len(workspace_consumers)} discovered workspace consumers, "
f"{len(raw_workspaces)} registered legacy workspace files."
)
return 0
if __name__ == "__main__":
raise SystemExit(main())
@@ -0,0 +1,447 @@
#!/usr/bin/env python3
"""Enforce Core ownership of repeated WebUI layout and dialog anatomy."""
from __future__ import annotations
import pathlib
import re
import sys
META_ROOT = pathlib.Path(__file__).resolve().parents[2]
REPOS_ROOT = META_ROOT.parent
CORE_INDEX = pathlib.Path("govoplan-core/webui/src/index.ts")
DIALOG_WIDTH_EXCEPTIONS = META_ROOT / "tools/checks/shared-webui-dialog-width-exceptions.txt"
RAW_ELEMENT = re.compile(
r'<(?P<tag>div|span|header|section|form)\b(?P<attrs>[^>]*?)'
r'\bclassName="(?P<classes>[^"]+)"',
re.DOTALL,
)
LEGACY_LAYOUT_TOKENS = {
"form-grid",
"admin-form-grid",
"dashboard-grid",
"settings-grid",
"admin-dialog",
"admin-dialog-wide",
"admin-details-grid",
"detail-list",
"metric-grid",
}
CENTRAL_COMPONENTS = {
"PageActionBar": pathlib.Path(
"govoplan-core/webui/src/components/PageActionBar.tsx"
),
"ActionToolbar": pathlib.Path(
"govoplan-core/webui/src/components/ActionToolbar.tsx"
),
"ToolbarGroup": pathlib.Path(
"govoplan-core/webui/src/components/ActionToolbar.tsx"
),
"ToolbarSpacer": pathlib.Path(
"govoplan-core/webui/src/components/ActionToolbar.tsx"
),
"ContentGrid": pathlib.Path(
"govoplan-core/webui/src/components/ContentGrid.tsx"
),
"ContentSection": pathlib.Path(
"govoplan-core/webui/src/components/ContentSection.tsx"
),
"FormGrid": pathlib.Path("govoplan-core/webui/src/components/ContentGrid.tsx"),
"FormLayout": pathlib.Path(
"govoplan-core/webui/src/components/ContentGrid.tsx"
),
"GridItem": pathlib.Path("govoplan-core/webui/src/components/ContentGrid.tsx"),
"FormSection": pathlib.Path(
"govoplan-core/webui/src/components/FormSection.tsx"
),
"DialogActions": pathlib.Path(
"govoplan-core/webui/src/components/DialogAnatomy.tsx"
),
"DialogForm": pathlib.Path(
"govoplan-core/webui/src/components/DialogAnatomy.tsx"
),
"DialogSection": pathlib.Path(
"govoplan-core/webui/src/components/DialogAnatomy.tsx"
),
"DescriptionList": pathlib.Path(
"govoplan-core/webui/src/components/DescriptionList.tsx"
),
"DescriptionItem": pathlib.Path(
"govoplan-core/webui/src/components/DescriptionList.tsx"
),
"MetricGrid": pathlib.Path(
"govoplan-core/webui/src/components/MetricGrid.tsx"
),
"MetricCard": pathlib.Path(
"govoplan-core/webui/src/components/MetricCard.tsx"
),
"FilterBar": pathlib.Path(
"govoplan-core/webui/src/components/FilterBar.tsx"
),
"StatePanel": pathlib.Path(
"govoplan-core/webui/src/components/StatePanel.tsx"
),
"CountBadge": pathlib.Path(
"govoplan-core/webui/src/components/CountBadge.tsx"
),
"SelectionList": pathlib.Path(
"govoplan-core/webui/src/components/SelectionList.tsx"
),
"SelectionListItem": pathlib.Path(
"govoplan-core/webui/src/components/SelectionList.tsx"
),
"SelectionListItemContent": pathlib.Path(
"govoplan-core/webui/src/components/SelectionList.tsx"
),
"WorkspaceLayout": pathlib.Path(
"govoplan-core/webui/src/components/WorkspaceLayout.tsx"
),
"WorkspaceFrame": pathlib.Path(
"govoplan-core/webui/src/components/WorkspaceFrame.tsx"
),
"DefinitionPalette": pathlib.Path(
"govoplan-core/webui/src/components/DefinitionPalette.tsx"
),
"DefinitionPaletteGroup": pathlib.Path(
"govoplan-core/webui/src/components/DefinitionPalette.tsx"
),
"DefinitionPaletteItem": pathlib.Path(
"govoplan-core/webui/src/components/DefinitionPalette.tsx"
),
"DefinitionNodeIcon": pathlib.Path(
"govoplan-core/webui/src/components/DefinitionNodeIcon.tsx"
),
"FloatingStatus": pathlib.Path(
"govoplan-core/webui/src/components/FloatingStatus.tsx"
),
}
REQUIRED_CONSUMERS = {
"PageActionBar": (
pathlib.Path("govoplan-payments/webui/src/features/payments/PaymentsPage.tsx"),
),
"ActionToolbar": (
pathlib.Path("govoplan-core/webui/src/components/WysiwygEditor.tsx"),
pathlib.Path("govoplan-calendar/webui/src/features/calendar/CalendarPage.tsx"),
pathlib.Path("govoplan-files/webui/src/features/files/FilesPage.tsx"),
pathlib.Path("govoplan-templates/webui/src/features/templates/TemplatesPage.tsx"),
),
"ContentGrid": (
pathlib.Path("govoplan-core/webui/src/features/settings/SettingsPage.tsx"),
pathlib.Path("govoplan-campaign/webui/src/features/campaigns/GlobalSettingsPage.tsx"),
pathlib.Path("govoplan-notifications/webui/src/features/notifications/NotificationSettingsPanel.tsx"),
),
"ContentSection": (
pathlib.Path("govoplan-datasources/webui/src/features/datasources/DatasourcesPage.tsx"),
pathlib.Path("govoplan-dist-lists/webui/src/features/distributionLists/DistributionListsPage.tsx"),
pathlib.Path("govoplan-templates/webui/src/features/templates/TemplatesPage.tsx"),
),
"FormGrid": (
pathlib.Path("govoplan-core/webui/src/components/mail/MailServerSettingsPanel.tsx"),
pathlib.Path("govoplan-calendar/webui/src/features/calendar/CalendarEventDialog.tsx"),
pathlib.Path("govoplan-forms/webui/src/features/forms/FormDefinitionDialog.tsx"),
pathlib.Path("govoplan-postbox/webui/src/features/postbox/PostboxAdminPanel.tsx"),
),
"FormSection": (
pathlib.Path("govoplan-addresses/webui/src/features/addressbook/AddressBookPage.tsx"),
pathlib.Path("govoplan-quick-access/webui/src/features/settings/QuickAccessSettingsPanel.tsx"),
),
"DialogForm": (
pathlib.Path("govoplan-addresses/webui/src/features/addressbook/AddressBookPage.tsx"),
pathlib.Path("govoplan-calendar/webui/src/features/calendar/CalendarEventDialog.tsx"),
pathlib.Path("govoplan-records/webui/src/features/records/RecordsPage.tsx"),
),
"DialogSection": (
pathlib.Path("govoplan-datasources/webui/src/features/datasources/DatasourcesPage.tsx"),
pathlib.Path("govoplan-files/webui/src/features/files/components/FileShareDialog.tsx"),
pathlib.Path("govoplan-templates/webui/src/features/templates/TemplatesPage.tsx"),
),
"DescriptionList": (
pathlib.Path("govoplan-access/webui/src/features/admin/UsersPanel.tsx"),
pathlib.Path("govoplan-campaign/webui/src/features/campaigns/CampaignReportPage.tsx"),
pathlib.Path("govoplan-docs/webui/src/features/docs/DocsPage.tsx"),
pathlib.Path("govoplan-policy/webui/src/features/policy/ViewPoliciesPanel.tsx"),
),
"MetricGrid": (
pathlib.Path("govoplan-core/webui/src/features/dashboard/DashboardPage.tsx"),
pathlib.Path("govoplan-admin/webui/src/features/admin/ModuleManagementPanel.tsx"),
pathlib.Path("govoplan-campaign/webui/src/features/operator/OperatorQueuePage.tsx"),
pathlib.Path("govoplan-notifications/webui/src/features/notifications/NotificationSummaryWidget.tsx"),
),
"MetricCard": (
pathlib.Path("govoplan-admin/webui/src/features/admin/AdminOverviewPanel.tsx"),
pathlib.Path("govoplan-approvals/webui/src/features/approvals/ApprovalsPage.tsx"),
pathlib.Path("govoplan-campaign/webui/src/features/campaigns/ReviewSendPage.tsx"),
pathlib.Path("govoplan-voting/webui/src/features/voting/VotingPage.tsx"),
),
"FilterBar": (
pathlib.Path("govoplan-cases/webui/src/features/cases/CasesPage.tsx"),
pathlib.Path("govoplan-dataflow/webui/src/features/dataflow/DataflowPage.tsx"),
pathlib.Path("govoplan-records/webui/src/features/records/RecordsPage.tsx"),
pathlib.Path("govoplan-tasks/webui/src/features/tasks/TasksPage.tsx"),
),
"StatePanel": (
pathlib.Path("govoplan-committee/webui/src/features/committee/CommitteePage.tsx"),
pathlib.Path("govoplan-notifications/webui/src/features/notifications/NotificationCenterPage.tsx"),
pathlib.Path("govoplan-postbox/webui/src/features/postbox/PostboxPage.tsx"),
pathlib.Path("govoplan-risk-compliance/webui/src/features/riskCompliance/RiskCompliancePage.tsx"),
),
"CountBadge": (
pathlib.Path("govoplan-core/webui/src/layout/Titlebar.tsx"),
pathlib.Path("govoplan-mail/webui/src/features/mail/MailboxPage.tsx"),
pathlib.Path("govoplan-search/webui/src/features/search/SearchPage.tsx"),
),
"SelectionListItemContent": (
pathlib.Path("govoplan-approvals/webui/src/features/approvals/ApprovalsPage.tsx"),
pathlib.Path("govoplan-datasources/webui/src/features/datasources/DatasourcesPage.tsx"),
pathlib.Path("govoplan-voting/webui/src/features/voting/VotingPage.tsx"),
),
"WorkspaceLayout": (
pathlib.Path("govoplan-dataflow/webui/src/features/dataflow/DataflowPage.tsx"),
pathlib.Path("govoplan-notifications/webui/src/features/notifications/NotificationCenterPage.tsx"),
pathlib.Path("govoplan-workflow/webui/src/features/workflow/WorkflowPage.tsx"),
),
"WorkspaceFrame": (
pathlib.Path("govoplan-cases/webui/src/features/cases/CasesPage.tsx"),
pathlib.Path("govoplan-portal/webui/src/features/portal/PortalPage.tsx"),
pathlib.Path("govoplan-records/webui/src/features/records/RecordsPage.tsx"),
),
"DefinitionPalette": (
pathlib.Path("govoplan-dataflow/webui/src/features/dataflow/DataflowPage.tsx"),
pathlib.Path("govoplan-workflow/webui/src/features/workflow/WorkflowPage.tsx"),
),
"DefinitionNodeIcon": (
pathlib.Path("govoplan-dataflow/webui/src/features/dataflow/DataflowNode.tsx"),
pathlib.Path("govoplan-workflow/webui/src/features/workflow/WorkflowNode.tsx"),
),
"FloatingStatus": (
pathlib.Path("govoplan-dataflow/webui/src/features/dataflow/DataflowPage.tsx"),
pathlib.Path("govoplan-workflow/webui/src/features/workflow/WorkflowPage.tsx"),
),
}
LEGACY_CSS_SELECTOR = re.compile(
r"(?<![-\w])\.(?:admin-details-grid|detail-list|metric-grid)(?![-\w])"
)
RETIRED_LOCAL_CSS_CLASSES = {
"admin-assignment-grid",
"approval-metrics",
"dataflow-node-count",
"dataflow-shell",
"datasources-metrics",
"datasources-shell",
"dist-lists-metrics",
"dist-lists-shell",
"notifications-count",
"notifications-empty-state",
"notifications-shell",
"review-flow-execution-summary",
"review-flow-fact-grid",
"risk-metrics",
"search-filter-count",
"tasks-empty-detail",
"tasks-shell",
"templates-shell",
"voting-metrics",
"voting-shell",
"workflow-shell",
"dataflow-palette-items",
"workflow-palette-items",
"dataflow-working-indicator",
"workflow-working-indicator",
"datasources-detail-section",
"dist-lists-section",
"templates-section",
}
LOCAL_METRIC_HELPER = re.compile(r"\b(?:function\s+Metric\b|const\s+Metric\s*=)")
CSS_BLOCK = re.compile(r"([^{}]+)\{([^{}]*)\}")
CSS_COMMENT = re.compile(r"/\*.*?\*/", re.DOTALL)
DIALOG_CLASS = re.compile(r"\.([A-Za-z0-9_-]*(?:dialog|modal)[A-Za-z0-9_-]*)", re.IGNORECASE)
DIALOG_WIDTH = re.compile(r"(?:^|;)\s*(?:width|max-width)\s*:", re.MULTILINE)
DIALOG_WIDTH_VALUE = re.compile(
r"(?:^|;)\s*(?:width|max-width)\s*:\s*([^;]+)", re.MULTILINE
)
STANDARD_DIALOG_WIDTH = re.compile(r"\b(?:460|560|680|1040|1440)px\b")
DIALOG_INTERNAL_SUFFIXES = (
"-actions",
"-body",
"-close",
"-content",
"-field",
"-fields",
"-footer",
"-form",
"-header",
"-title",
)
def source_paths() -> list[pathlib.Path]:
paths: list[pathlib.Path] = []
for repository in sorted(REPOS_ROOT.glob("govoplan*")):
source_root = repository / "webui" / "src"
if source_root.is_dir():
paths.extend(sorted(source_root.rglob("*.tsx")))
return paths
def css_paths() -> list[pathlib.Path]:
paths: list[pathlib.Path] = []
for repository in sorted(REPOS_ROOT.glob("govoplan*")):
source_root = repository / "webui" / "src"
if source_root.is_dir():
paths.extend(sorted(source_root.rglob("*.css")))
return paths
def relative(path: pathlib.Path) -> pathlib.Path:
return path.relative_to(REPOS_ROOT)
def raw_reason(classes: str) -> str | None:
tokens = classes.split()
legacy = sorted(set(tokens) & LEGACY_LAYOUT_TOKENS)
if legacy:
return f"legacy shared layout class {', '.join(legacy)}"
toolbars = [
token
for token in tokens
if token == "admin-toolbar-row" or token.endswith("-toolbar")
]
if toolbars:
return f"raw toolbar class {', '.join(toolbars)}"
dialog_forms = [token for token in tokens if token.endswith("dialog-form")]
if dialog_forms:
return f"raw dialog form class {', '.join(dialog_forms)}"
return None
def normalized_css_selector(selector: str) -> str:
return " ".join(selector.split())
def dialog_width_exceptions(styles: dict[pathlib.Path, str]) -> dict[str, str]:
exceptions: dict[str, str] = {}
central_dialog_styles = pathlib.Path("govoplan-core/webui/src/styles/dialogs.css")
for path, content in styles.items():
if path == central_dialog_styles:
continue
without_comments = CSS_COMMENT.sub("", content)
for match in CSS_BLOCK.finditer(without_comments):
selector = normalized_css_selector(match.group(1))
declarations = match.group(2)
if not DIALOG_WIDTH.search(declarations):
continue
dialog_classes = DIALOG_CLASS.findall(selector)
if not dialog_classes:
continue
if all(name.lower().endswith(DIALOG_INTERNAL_SUFFIXES) for name in dialog_classes):
continue
signature = f"{path}|{selector}"
exceptions[signature] = declarations
return exceptions
def exception_baseline() -> set[str]:
if not DIALOG_WIDTH_EXCEPTIONS.exists():
return set()
return {
line.strip()
for line in DIALOG_WIDTH_EXCEPTIONS.read_text(encoding="utf-8").splitlines()
if line.strip() and not line.lstrip().startswith("#")
}
def main() -> int:
sources = source_paths()
source_text = {relative(path): path.read_text(encoding="utf-8") for path in sources}
styles = css_paths()
style_text = {relative(path): path.read_text(encoding="utf-8") for path in styles}
errors: list[str] = []
for path, content in source_text.items():
for match in RAW_ELEMENT.finditer(content):
reason = raw_reason(match.group("classes"))
if reason is None:
continue
line = content.count("\n", 0, match.start()) + 1
errors.append(
f"Raw {match.group('tag')} repeats shared anatomy ({reason}): {path}:{line}"
)
for path, content in style_text.items():
uncommented = CSS_COMMENT.sub("", content)
legacy_match = LEGACY_CSS_SELECTOR.search(uncommented)
if legacy_match:
line = content.count("\n", 0, legacy_match.start()) + 1
errors.append(f"Legacy shared layout selector is not allowed: {path}:{line}")
for class_name in sorted(RETIRED_LOCAL_CSS_CLASSES):
retired = re.search(rf"(?<![-\w])\.{re.escape(class_name)}(?![-\w])\s*(?:,|\{{)", uncommented)
if retired:
line = content.count("\n", 0, retired.start()) + 1
errors.append(f"Retired module-local shared anatomy selector is not allowed: {path}:{line} ({class_name})")
dialog_exceptions = dialog_width_exceptions(style_text)
baseline = exception_baseline()
for signature in sorted(set(dialog_exceptions) - baseline):
errors.append(f"Unreviewed local dialog width; use Dialog size or register a justified exception: {signature}")
for signature in sorted(baseline - set(dialog_exceptions)):
errors.append(f"Stale dialog width exception can be removed: {signature}")
for signature, declarations in sorted(dialog_exceptions.items()):
width_values = " ".join(DIALOG_WIDTH_VALUE.findall(declarations))
standard = STANDARD_DIALOG_WIDTH.search(width_values)
if standard:
errors.append(f"Local dialog width duplicates Core size {standard.group(0)}: {signature}")
for name, owner in CENTRAL_COMPONENTS.items():
definition = re.compile(
rf"\b(?:function|class)\s+{name}\b|\bconst\s+{name}\s*="
)
for path, content in source_text.items():
if path != owner and definition.search(content):
errors.append(f"Module-local {name} definition is not allowed: {path}")
for path, content in source_text.items():
if LOCAL_METRIC_HELPER.search(content):
errors.append(f"Module-local Metric helper is not allowed; compose MetricCard directly: {path}")
usage_counts: dict[str, int] = {}
for name in CENTRAL_COMPONENTS:
usage = re.compile(rf"<{name}\b")
usage_counts[name] = sum(bool(usage.search(content)) for content in source_text.values())
for name, consumers in REQUIRED_CONSUMERS.items():
for path in consumers:
absolute = REPOS_ROOT / path
if not absolute.exists():
continue
if f"<{name}" not in absolute.read_text(encoding="utf-8"):
errors.append(f"Required shared {name} consumer regressed: {path}")
core_index_path = REPOS_ROOT / CORE_INDEX
if core_index_path.exists():
core_index = core_index_path.read_text(encoding="utf-8")
for name in CENTRAL_COMPONENTS:
if not re.search(rf"\b{name}\b", core_index):
errors.append(f"Core must export {name} from @govoplan/core-webui.")
dialog_path = REPOS_ROOT / "govoplan-core/webui/src/components/Dialog.tsx"
if dialog_path.exists():
dialog_text = dialog_path.read_text(encoding="utf-8")
if "<DialogActions" not in dialog_text:
errors.append("Every Core Dialog footer must compose DialogActions.")
if errors:
print("\n".join(errors), file=sys.stderr)
return 1
migrated = ", ".join(
f"{name}={usage_counts[name]} files"
for name in ("PageActionBar", "ActionToolbar", "WorkspaceFrame", "WorkspaceLayout", "FilterBar", "StatePanel", "SelectionList", "CountBadge", "DefinitionPalette", "DefinitionNodeIcon", "FloatingStatus", "ContentSection", "ContentGrid", "FormGrid", "FormSection", "DialogForm", "DialogSection", "MetricGrid", "MetricCard", "DescriptionList")
)
print(f"Shared WebUI primitive contract passed: {migrated}; {len(dialog_exceptions)} reviewed dialog width exceptions; no raw legacy anatomy.")
return 0
if __name__ == "__main__":
raise SystemExit(main())
@@ -0,0 +1,26 @@
{
"assets": "Contract-only module: asset persistence and lifecycle APIs are not implemented; reassess before adding a migration-owned store.",
"booking": "Contract-only module: booking persistence and reservation workflows are not implemented; reassess before adding a migration-owned store.",
"certificates": "Contract-only module: certificate issuance and revocation persistence are not implemented; reassess before adding a migration-owned store.",
"consultation": "Contract-only module: consultation submissions and evaluation persistence are not implemented; reassess before adding a migration-owned store.",
"contracts": "Contract-only module: contract, amendment, and obligation persistence are not implemented; reassess before adding a migration-owned store.",
"dms": "Stateless integration-preview module: DMS retains no document, person, credential, or provider-response store; Files and Records remain the subject-data owners. Reassess before persisting a target binding, plan, receipt, or diagnostic.",
"erp": "Stateless integration-contract module: ERP retains no invoice, payable, plan, booking observation, provider response, or credential store; Procurement, Payments, Ledger, Files, and Audit remain the subject-data owners. Reassess before persisting a target binding, plan, receipt, reconciliation decision, or diagnostic.",
"evaluation": "Contract-only module: evaluation runs, responses, and scores are not persisted; reassess before adding a migration-owned store.",
"facilities": "Contract-only module: facility and maintenance persistence are not implemented; reassess before adding a migration-owned store.",
"fit_connect": "Stateless transport-contract module: FIT-Connect retains no submission, attachment, receipt, acknowledgement plan, key, provider response, or diagnostic store; the owning Service, Forms, Cases, Files, and Audit workflows remain responsible for subject data. Reassess before persisting any ingress or event-log evidence.",
"grants": "Contract-only module: grant applications, awards, and monitoring are not persisted; reassess before adding a migration-owned store.",
"inspections": "Contract-only module: inspections, findings, and measures are not persisted; reassess before adding a migration-owned store.",
"learning": "Contract-only module: learning offers, enrollment, and completion are not persisted; reassess before adding a migration-owned store.",
"ops": "Projection-only module: Ops reads bounded platform and provider status; durable recovery evidence remains owned by Core and domain modules.",
"permits": "Contract-only module: permit applications, assessments, and decisions are not persisted; reassess before adding a migration-owned store.",
"portal": "Projection-only module: Portal stores no applicant records; Services, Forms Runtime, Cases, and Postbox own and export authoritative subject data.",
"procurement": "Contract-only module: procurement procedures, tenders, and awards are not persisted; reassess before adding a migration-owned store.",
"resources": "Contract-only module: resource catalog and allocation persistence are not implemented; reassess before adding a migration-owned store.",
"rest": "Transport-only module: REST binds explicitly published functions and owns no domain or subject-data store.",
"soap": "Transport-only module: SOAP binds explicitly published operations and owns no domain or subject-data store.",
"tenancy": "Orchestration module: tenant lifecycle and settings use Core-owned storage; Access covers account and membership subject data.",
"transparency": "Contract-only module: requests, disclosure reviews, and publications are not persisted; reassess before adding a migration-owned store.",
"workflow": "Presentation-only module: Workflow edits and projects Workflow Engine state; Workflow Engine owns persistence and DSAR coverage.",
"xrechnung": "Stateless validation-contract module: XRechnung persists no invoice, report, diagnostic, or handoff; the invoking Files, Procurement, or Payments workflow remains the subject-data owner. Reassess before adding a validation store."
}
@@ -0,0 +1,65 @@
# Reviewed module-specific Dialog and modal width selectors.
#
# Standard Core widths (460/560/680/1040/1440px) are never valid here.
# Remove an entry when its consumer moves to Dialog size; additions require
# an explicit design review and an inventory explanation.
govoplan-addresses/webui/src/styles/addresses.css|.address-governance-dialog .dialog-panel
govoplan-addresses/webui/src/styles/addresses.css|.address-import-dialog
govoplan-addresses/webui/src/styles/addresses.css|.address-sync-dialog
govoplan-addresses/webui/src/styles/addresses.css|.dialog-panel.address-contact-dialog
govoplan-addresses/webui/src/styles/addresses.css|.dialog-panel.address-member-dialog
govoplan-addresses/webui/src/styles/addresses.css|.dialog-panel.address-quality-dialog, .address-quality-dialog .dialog-panel
govoplan-approvals/webui/src/styles/approvals.css|.approval-request-dialog, .approval-template-dialog
govoplan-approvals/webui/src/styles/approvals.css|.approval-template-history-dialog
govoplan-calendar/webui/src/styles/calendar.css|.calendar-delete-dialog
govoplan-calendar/webui/src/styles/calendar.css|.calendar-migration-dialog
govoplan-calendar/webui/src/styles/calendar.css|.calendar-outbox-dialog
govoplan-calendar/webui/src/styles/calendar.css|.calendar-vevent-dialog
govoplan-campaign/webui/src/styles/campaign-workspace.css|.attachment-linking-detail-modal
govoplan-campaign/webui/src/styles/campaign-workspace.css|.attachment-rules-modal
govoplan-campaign/webui/src/styles/campaign-workspace.css|.campaign-content-library-dialog
govoplan-campaign/webui/src/styles/campaign-workspace.css|.campaign-copy-dialog
govoplan-campaign/webui/src/styles/campaign-workspace.css|.campaign-schedule-dialog
govoplan-campaign/webui/src/styles/campaign-workspace.css|.dialog-panel-wide
govoplan-campaign/webui/src/styles/campaign-workspace.css|.message-preview-modal
govoplan-campaign/webui/src/styles/campaign-workspace.css|.recipient-address-editor-modal
govoplan-campaign/webui/src/styles/campaign-workspace.css|.recipient-import-modal
govoplan-campaign/webui/src/styles/campaign-workspace.css|.template-action-dialog
govoplan-campaign/webui/src/styles/campaign-workspace.css|.template-expression-dialog
govoplan-campaign/webui/src/styles/campaign-workspace.css|.template-preview-modal
govoplan-cases/webui/src/styles/cases.css|.case-decision-dialog
govoplan-cases/webui/src/styles/cases.css|.case-share-dialog
govoplan-committee/webui/src/styles/committee.css|.committee-ballot-dialog
govoplan-committee/webui/src/styles/committee.css|.committee-record-dialog
govoplan-core/webui/src/styles/components.css|.dialog-panel.wysiwyg-editor-dialog
govoplan-core/webui/src/styles/components.css|.guided-config-dialog
govoplan-core/webui/src/styles/components.css|.password-generator-dialog
govoplan-core/webui/src/styles/layout.css|.concurrency-conflict-dialog
govoplan-dashboard/webui/src/styles/dashboard.css|.dashboard-widget-config-dialog
govoplan-dataflow/webui/src/styles/dataflow.css|.dataflow-decision-dialog
govoplan-dataflow/webui/src/styles/dataflow.css|.dataflow-definition-dialog
govoplan-dataflow/webui/src/styles/dataflow.css|.dataflow-run-dialog
govoplan-dataflow/webui/src/styles/dataflow.css|.dataflow-source-dialog
govoplan-dataflow/webui/src/styles/dataflow.css|.dataflow-triggers-dialog
govoplan-datasources/webui/src/styles/datasources.css|.datasources-add-dialog
govoplan-datasources/webui/src/styles/datasources.css|.datasources-governance-dialog
govoplan-dist-lists/webui/src/styles/dist-lists.css|.dist-lists-entry-dialog
govoplan-dist-lists/webui/src/styles/dist-lists.css|.dist-lists-explanation-dialog
govoplan-files/webui/src/styles/file-manager.css|.file-dialog
govoplan-files/webui/src/styles/file-manager.css|.file-dialog:has(.archive-preview)
govoplan-files/webui/src/styles/file-manager.css|.file-dialog:has(.connector-sync-grid)
govoplan-files/webui/src/styles/file-manager.css|.file-dialog:has(.file-share-dialog-content)
govoplan-files/webui/src/styles/file-manager.css|.managed-file-chooser-dialog
govoplan-forms-runtime/webui/src/styles/forms-runtime.css|.form-intake-dialog
govoplan-forms/webui/src/styles/forms.css|.form-definition-dialog
govoplan-organizations/webui/src/styles/organizations.css|.organization-upgrade-dialog
govoplan-postbox/webui/src/styles/postbox.css|.postbox-dialog
govoplan-postbox/webui/src/styles/postbox.css|.postbox-message-dialog
govoplan-postbox/webui/src/styles/postbox.css|.postbox-template-dialog
govoplan-projects/webui/src/styles/projects.css|.project-editor-dialog
govoplan-records/webui/src/styles/records.css|.records-dialog
govoplan-reporting/webui/src/styles/reporting.css|.reporting-drill-dialog
govoplan-views/webui/src/styles/views.css|.views-assignment-dialog
govoplan-workflow/webui/src/styles/workflow.css|.workflow-definition-dialog
govoplan-workflow/webui/src/styles/workflow.css|.workflow-runs-dialog
govoplan-workflow/webui/src/styles/workflow.css|.workflow-standard-comparison-dialog
@@ -0,0 +1,2 @@
# Raw page-frame exceptions. Keep this file empty: module pages use the Core
# PageLayout contract and new exceptions are rejected by the layout checker.
@@ -0,0 +1,2 @@
# Raw workspace exceptions. Keep this file empty: module workspaces use the Core
# WorkspaceLayout contract and new exceptions are rejected by the layout checker.
+12 -1
View File
@@ -26,6 +26,8 @@ CADDY_CONFIG_FILENAME = "Caddyfile"
EXISTING_PROXY_FILENAME = "existing-proxy.json" EXISTING_PROXY_FILENAME = "existing-proxy.json"
PLAN_FILENAME = "plan.json" PLAN_FILENAME = "plan.json"
RECEIPT_FILENAME = "receipt.json" RECEIPT_FILENAME = "receipt.json"
CAPABILITIES_FILENAME = "infrastructure-capabilities.json"
DEPENDENCY_INVENTORY_FILENAME = "infrastructure-dependency-inventory.json"
MANIFEST_FILENAME = "distribution-manifest.json" MANIFEST_FILENAME = "distribution-manifest.json"
KEYRING_FILENAME = "distribution-keyring.json" KEYRING_FILENAME = "distribution-keyring.json"
BACKUP_EVIDENCE_FILENAME = "backup-evidence.json" BACKUP_EVIDENCE_FILENAME = "backup-evidence.json"
@@ -88,6 +90,7 @@ RUNTIME_ENV_KEYS = (
"DEV_BOOTSTRAP_ENABLED", "DEV_BOOTSTRAP_ENABLED",
"GOVOPLAN_ALLOW_PROCESS_LOCAL_LOGIN_THROTTLE", "GOVOPLAN_ALLOW_PROCESS_LOCAL_LOGIN_THROTTLE",
"GOVOPLAN_DEPLOYMENT_SPEC_PATH", "GOVOPLAN_DEPLOYMENT_SPEC_PATH",
"GOVOPLAN_DEPLOYMENT_CAPABILITIES_PATH",
"FILE_STORAGE_BACKEND", "FILE_STORAGE_BACKEND",
"FILE_STORAGE_LOCAL_ROOT", "FILE_STORAGE_LOCAL_ROOT",
"FILE_STORAGE_S3_ENDPOINT_URL", "FILE_STORAGE_S3_ENDPOINT_URL",
@@ -113,6 +116,8 @@ class BundlePaths:
existing_proxy: Path existing_proxy: Path
plan: Path plan: Path
receipt: Path receipt: Path
capabilities: Path
dependency_inventory: Path
manifest: Path manifest: Path
keyring: Path keyring: Path
backup_evidence: Path backup_evidence: Path
@@ -137,6 +142,8 @@ def bundle_paths(root: Path) -> BundlePaths:
existing_proxy=resolved / EXISTING_PROXY_FILENAME, existing_proxy=resolved / EXISTING_PROXY_FILENAME,
plan=resolved / PLAN_FILENAME, plan=resolved / PLAN_FILENAME,
receipt=resolved / RECEIPT_FILENAME, receipt=resolved / RECEIPT_FILENAME,
capabilities=resolved / CAPABILITIES_FILENAME,
dependency_inventory=resolved / DEPENDENCY_INVENTORY_FILENAME,
manifest=resolved / MANIFEST_FILENAME, manifest=resolved / MANIFEST_FILENAME,
keyring=resolved / KEYRING_FILENAME, keyring=resolved / KEYRING_FILENAME,
backup_evidence=resolved / BACKUP_EVIDENCE_FILENAME, backup_evidence=resolved / BACKUP_EVIDENCE_FILENAME,
@@ -296,6 +303,7 @@ def reconcile_runtime_environment(
"DEV_AUTO_MIGRATE_ENABLED": "false", "DEV_AUTO_MIGRATE_ENABLED": "false",
"DEV_BOOTSTRAP_ENABLED": "false", "DEV_BOOTSTRAP_ENABLED": "false",
"GOVOPLAN_DEPLOYMENT_SPEC_PATH": "/etc/govoplan/deployment/installation.json", "GOVOPLAN_DEPLOYMENT_SPEC_PATH": "/etc/govoplan/deployment/installation.json",
"GOVOPLAN_DEPLOYMENT_CAPABILITIES_PATH": "/etc/govoplan/deployment/infrastructure-capabilities.json",
} }
) )
if redis.mode == "disabled": if redis.mode == "disabled":
@@ -370,7 +378,10 @@ def render_compose(spec: InstallationSpec) -> dict[str, object]:
deployment_mount = ( deployment_mount = (
f"./{SPEC_FILENAME}:/etc/govoplan/deployment/installation.json:ro" f"./{SPEC_FILENAME}:/etc/govoplan/deployment/installation.json:ro"
) )
data_mounts = [deployment_mount] capabilities_mount = (
f"./{CAPABILITIES_FILENAME}:/etc/govoplan/deployment/infrastructure-capabilities.json:ro"
)
data_mounts = [deployment_mount, capabilities_mount]
if spec.components.storage.mode == "local": if spec.components.storage.mode == "local":
data_mounts.append("files-data:/var/lib/govoplan/files") data_mounts.append("files-data:/var/lib/govoplan/files")
@@ -0,0 +1,751 @@
"""Non-secret infrastructure capability projection and change impact."""
from __future__ import annotations
from dataclasses import asdict, dataclass
from datetime import UTC, datetime
from typing import Mapping
from urllib.parse import urlsplit
from .model import InstallationSpec
CAPABILITY_DOCUMENT_SCHEMA_VERSION = 1
CAPABILITY_STATES = frozenset(
{
"configured",
"available_unconfigured",
"externally_supplied",
"unavailable",
}
)
DEPENDENCY_INVENTORY_SCHEMA_VERSION = 1
DEPENDENCY_STATES = frozenset(
{"active", "inactive", "data_present", "pending_work", "runtime_binding"}
)
@dataclass(frozen=True, slots=True)
class InfrastructureCapability:
id: str
label: str
state: str
source: str
detail: str
endpoint: Mapping[str, object]
secret_refs: tuple[str, ...]
dependent_modules: tuple[str, ...]
def to_dict(self) -> dict[str, object]:
value = asdict(self)
value["endpoint"] = dict(self.endpoint)
value["secret_refs"] = list(self.secret_refs)
value["dependent_modules"] = list(self.dependent_modules)
return value
@dataclass(frozen=True, slots=True)
class CapabilityChangeImpact:
capability_id: str
action: str
previous_state: str
desired_state: str
previous_source: str
desired_source: str
dependent_modules: tuple[str, ...]
detail: str
required_action: str
actual_dependencies: tuple["CapabilityDependency", ...] = ()
inventory_inspected: bool = False
def to_dict(self) -> dict[str, object]:
value = asdict(self)
value["dependent_modules"] = list(self.dependent_modules)
value["actual_dependencies"] = [
item.to_dict() for item in self.actual_dependencies
]
return value
@dataclass(frozen=True, slots=True)
class CapabilityDependency:
capability_id: str
module_id: str
dependency_type: str
dependency_ref: str
state: str
scope: str
summary: str
metrics: Mapping[str, int]
required_action: str
def to_dict(self) -> dict[str, object]:
return {
"capability_id": self.capability_id,
"module_id": self.module_id,
"dependency_type": self.dependency_type,
"dependency_ref": self.dependency_ref,
"state": self.state,
"scope": self.scope,
"summary": self.summary,
"metrics": dict(sorted(self.metrics.items())),
"required_action": self.required_action,
}
@dataclass(frozen=True, slots=True)
class InfrastructureDependencyInventory:
installation_id: str
generated_at: datetime
complete: bool
inspected_capability_ids: tuple[str, ...]
provider_count: int
dependencies: tuple[CapabilityDependency, ...]
def dependencies_for(
self,
capability_id: str,
) -> tuple[CapabilityDependency, ...]:
return tuple(
item for item in self.dependencies if item.capability_id == capability_id
)
def infrastructure_dependency_inventory_from_mapping(
value: object,
) -> InfrastructureDependencyInventory:
if (
not isinstance(value, Mapping)
or value.get("schema_version") != DEPENDENCY_INVENTORY_SCHEMA_VERSION
):
raise ValueError("Infrastructure dependency inventory schema is unsupported.")
installation_id = _inventory_text(value, "installation_id", maximum=100)
generated_at_text = _inventory_text(value, "generated_at", maximum=100)
try:
generated_at = datetime.fromisoformat(generated_at_text.replace("Z", "+00:00"))
except ValueError as exc:
raise ValueError(
"Infrastructure dependency inventory timestamp is invalid."
) from exc
if generated_at.tzinfo is None:
raise ValueError("Infrastructure dependency inventory timestamp needs a timezone.")
generated_at = generated_at.astimezone(UTC)
complete = value.get("complete")
if type(complete) is not bool:
raise ValueError("Infrastructure dependency inventory completion state is invalid.")
inspected = _inventory_string_list(
value.get("inspected_capability_ids"),
maximum_items=100,
maximum_length=120,
)
if len(inspected) != len(set(inspected)):
raise ValueError("Infrastructure dependency inventory repeats a capability id.")
providers = value.get("providers")
if not isinstance(providers, list) or len(providers) > 100:
raise ValueError("Infrastructure dependency provider reports are invalid.")
provider_states: list[str] = []
provider_declarations: dict[str, tuple[str, ...]] = {}
provider_counts: dict[str, int] = {}
for provider in providers:
if not isinstance(provider, Mapping):
raise ValueError("Infrastructure dependency provider report is invalid.")
module_id = _inventory_text(provider, "module_id", maximum=120)
if module_id in provider_declarations:
raise ValueError("Infrastructure dependency provider is repeated.")
state = _inventory_text(provider, "state", maximum=40)
if state not in {"complete", "error"}:
raise ValueError("Infrastructure dependency provider state is invalid.")
provider_states.append(state)
count = provider.get("dependency_count")
if type(count) is not int or count < 0:
raise ValueError("Infrastructure dependency provider count is invalid.")
capability_ids = _inventory_string_list(
provider.get("capability_ids"),
maximum_items=30,
maximum_length=120,
)
if len(capability_ids) != len(set(capability_ids)):
raise ValueError("Infrastructure dependency provider capability is repeated.")
provider_declarations[module_id] = capability_ids
provider_counts[module_id] = count
if complete and any(state != "complete" for state in provider_states):
raise ValueError("Complete dependency inventory contains a failed provider.")
raw_dependencies = value.get("dependencies")
if not isinstance(raw_dependencies, list) or len(raw_dependencies) > 10_000:
raise ValueError("Infrastructure dependency records are invalid.")
dependencies = tuple(_inventory_dependency(item) for item in raw_dependencies)
if any(
capability_id not in inspected
for capability_ids in provider_declarations.values()
for capability_id in capability_ids
):
raise ValueError(
"Infrastructure dependency provider was not covered by the inspection."
)
if any(item.capability_id not in inspected for item in dependencies):
raise ValueError("Dependency record was not covered by the inventory inspection.")
identities = {
(item.capability_id, item.module_id, item.dependency_type, item.dependency_ref)
for item in dependencies
}
if len(identities) != len(dependencies):
raise ValueError("Infrastructure dependency inventory repeats a record.")
observed_counts = {module_id: 0 for module_id in provider_counts}
for dependency in dependencies:
declarations = provider_declarations.get(dependency.module_id)
if declarations is None or dependency.capability_id not in declarations:
raise ValueError(
"Infrastructure dependency is outside its provider declaration."
)
observed_counts[dependency.module_id] += 1
if observed_counts != provider_counts:
raise ValueError("Infrastructure dependency provider count does not match records.")
return InfrastructureDependencyInventory(
installation_id=installation_id,
generated_at=generated_at,
complete=complete,
inspected_capability_ids=inspected,
provider_count=len(providers),
dependencies=dependencies,
)
def infrastructure_capability_document(
spec: InstallationSpec,
environment: Mapping[str, str],
) -> dict[str, object]:
"""Project installer choices without copying credentials or secret URLs."""
capabilities = tuple(
sorted(
(
_postgres_capability(spec, environment),
_redis_capability(spec, environment),
_mail_capability(spec),
_storage_capability(spec, environment),
_load_balancer_capability(spec),
_ingress_capability(spec),
),
key=lambda item: item.id,
)
)
tasks = _post_install_tasks(spec, capabilities)
return {
"schema_version": CAPABILITY_DOCUMENT_SCHEMA_VERSION,
"installation_id": spec.installation_id,
"profile": spec.profile,
"capabilities": [item.to_dict() for item in capabilities],
"post_install_tasks": tasks,
}
def capability_change_impacts(
previous_document: object,
desired_document: Mapping[str, object],
*,
dependency_inventory: InfrastructureDependencyInventory | None = None,
) -> tuple[CapabilityChangeImpact, ...]:
previous = _capability_map(previous_document)
desired = _capability_map(desired_document)
if not previous:
return ()
impacts: list[CapabilityChangeImpact] = []
for capability_id in sorted(set(previous) | set(desired)):
before = previous.get(capability_id)
after = desired.get(capability_id)
if before is None or after is None:
continue
previous_state = str(before.get("state") or "unavailable")
desired_state = str(after.get("state") or "unavailable")
previous_source = str(before.get("source") or "unknown")
desired_source = str(after.get("source") or "unknown")
previous_endpoint = _endpoint_signature(before.get("endpoint"))
desired_endpoint = _endpoint_signature(after.get("endpoint"))
previous_secret_refs = tuple(
sorted(_string_items(before.get("secret_refs")))
)
desired_secret_refs = tuple(
sorted(_string_items(after.get("secret_refs")))
)
if (
previous_state == desired_state
and previous_source == desired_source
and previous_endpoint == desired_endpoint
and previous_secret_refs == desired_secret_refs
):
continue
action = (
"remove"
if previous_state != "unavailable" and desired_state == "unavailable"
else "replace"
if previous_source != desired_source
else "reconfigure"
)
dependents = tuple(
sorted(
{
*(_string_items(before.get("dependent_modules"))),
*(_string_items(after.get("dependent_modules"))),
}
)
)
dependent_label = ", ".join(dependents) or "no declared module consumers"
binding_change = _binding_change_label(
previous_endpoint,
desired_endpoint,
previous_secret_refs,
desired_secret_refs,
)
actual_dependencies = (
dependency_inventory.dependencies_for(capability_id)
if dependency_inventory is not None
else ()
)
inventory_inspected = bool(
dependency_inventory is not None
and capability_id in dependency_inventory.inspected_capability_ids
)
if inventory_inspected and actual_dependencies:
references = ", ".join(
f"{item.module_id}:{item.dependency_ref}"
for item in actual_dependencies
)
inventory_detail = (
f" Provider inventory reports {len(actual_dependencies)} persisted "
f"dependency record(s): {references}."
)
elif inventory_inspected:
inventory_detail = (
" Provider inventory reports no persisted module-owned dependencies."
)
else:
inventory_detail = " Provider inventory did not inspect this capability."
dependency_actions = tuple(
dict.fromkeys(
item.required_action
for item in actual_dependencies
if item.required_action.strip()
)
)
required_action = (
"Review module-owned configuration and data migration or recovery "
"evidence before apply."
)
if dependency_actions:
required_action = f"{required_action} {' '.join(dependency_actions)}"
impacts.append(
CapabilityChangeImpact(
capability_id=capability_id,
action=action,
previous_state=previous_state,
desired_state=desired_state,
previous_source=previous_source,
desired_source=desired_source,
dependent_modules=dependents,
detail=(
f"{capability_id} changes from {previous_state}/{previous_source} "
f"to {desired_state}/{desired_source}{binding_change}; "
f"declared consumers: {dependent_label}.{inventory_detail}"
),
required_action=required_action,
actual_dependencies=actual_dependencies,
inventory_inspected=inventory_inspected,
)
)
return tuple(impacts)
def _postgres_capability(
spec: InstallationSpec,
environment: Mapping[str, str],
) -> InfrastructureCapability:
managed = spec.components.postgres.mode == "managed"
endpoint = (
{"scheme": "postgresql", "host": "postgres", "port": 5432}
if managed
else _redacted_endpoint(environment.get("DATABASE_URL", ""), default_port=5432)
)
return InfrastructureCapability(
id="database.postgresql",
label="PostgreSQL database",
state="configured" if managed else "externally_supplied",
source="installer-managed" if managed else "operator-supplied",
detail=(
"The installer manages the database service."
if managed
else "The deployment binds an externally operated PostgreSQL service."
),
endpoint=endpoint,
secret_refs=("env:POSTGRES_PASSWORD",) if managed else ("env:DATABASE_URL",),
dependent_modules=("core", *tuple(sorted(spec.enabled_modules))),
)
def _redis_capability(
spec: InstallationSpec,
environment: Mapping[str, str],
) -> InfrastructureCapability:
mode = spec.components.redis.mode
consumers = _enabled_consumers(
spec,
{
"campaigns",
"dataflow",
"files",
"mail",
"notifications",
"scheduling",
"workflow_engine",
},
include_core=True,
)
if mode == "disabled":
return InfrastructureCapability(
id="coordination.redis",
label="Redis coordination and queues",
state="unavailable",
source="disabled",
detail="Distributed queues and coordination are disabled.",
endpoint={},
secret_refs=(),
dependent_modules=consumers,
)
managed = mode == "managed"
endpoint = (
{"scheme": "redis", "host": "redis", "port": 6379}
if managed
else _redacted_endpoint(environment.get("REDIS_URL", ""), default_port=6379)
)
return InfrastructureCapability(
id="coordination.redis",
label="Redis coordination and queues",
state="configured" if managed else "externally_supplied",
source="installer-managed" if managed else "operator-supplied",
detail=(
"The installer manages the Redis service."
if managed
else "The deployment binds an externally operated Redis service."
),
endpoint=endpoint,
secret_refs=("env:REDIS_PASSWORD",) if managed else ("env:REDIS_URL",),
dependent_modules=consumers,
)
def _mail_capability(spec: InstallationSpec) -> InfrastructureCapability:
mode = spec.components.mail.mode
consumers = _enabled_consumers(
spec,
{"campaigns", "mail", "notifications"},
)
if mode == "disabled":
return InfrastructureCapability(
id="mail.smtp",
label="SMTP delivery",
state="unavailable",
source="disabled",
detail="No SMTP infrastructure was selected.",
endpoint={},
secret_refs=(),
dependent_modules=consumers,
)
if mode == "test-mail":
return InfrastructureCapability(
id="mail.smtp",
label="SMTP delivery",
state="available_unconfigured",
source="installer-managed-test",
detail="GreenMail is reachable, but Mail still owns profile and credential configuration.",
endpoint={"scheme": "smtp", "host": "test-mail", "port": 3025},
secret_refs=(),
dependent_modules=consumers,
)
return InfrastructureCapability(
id="mail.smtp",
label="SMTP delivery",
state="available_unconfigured",
source="operator-supplied",
detail="An external relay was selected; Mail still needs a reviewed server and credential binding.",
endpoint={},
secret_refs=(),
dependent_modules=consumers,
)
def _storage_capability(
spec: InstallationSpec,
environment: Mapping[str, str],
) -> InfrastructureCapability:
mode = spec.components.storage.mode
consumers = _enabled_consumers(
spec,
{"campaigns", "files", "records", "templates"},
)
if mode == "local":
return InfrastructureCapability(
id="files.storage",
label="Managed file content storage",
state="configured",
source="host-local",
detail="Files use the installer-managed local persistent volume.",
endpoint={"kind": "filesystem", "reference": "volume:files-data"},
secret_refs=(),
dependent_modules=consumers,
)
if mode == "garage":
return InfrastructureCapability(
id="files.storage",
label="Managed file content storage",
state="configured",
source="installer-managed-garage",
detail="Files use the installer-managed single-node Garage service.",
endpoint={"scheme": "http", "host": "garage", "port": 3900},
secret_refs=(
"env:FILE_STORAGE_S3_ACCESS_KEY_ID",
"env:FILE_STORAGE_S3_SECRET_ACCESS_KEY",
"env:GARAGE_RPC_SECRET",
),
dependent_modules=consumers,
)
return InfrastructureCapability(
id="files.storage",
label="Managed file content storage",
state="externally_supplied",
source="operator-supplied-s3",
detail="Files use an externally operated S3-compatible service.",
endpoint=_redacted_endpoint(
environment.get("FILE_STORAGE_S3_ENDPOINT_URL", ""),
default_port=443,
),
secret_refs=(
"env:FILE_STORAGE_S3_ACCESS_KEY_ID",
"env:FILE_STORAGE_S3_SECRET_ACCESS_KEY",
),
dependent_modules=consumers,
)
def _load_balancer_capability(spec: InstallationSpec) -> InfrastructureCapability:
return InfrastructureCapability(
id="runtime.load_balancing",
label="Application load balancing",
state="configured",
source="installer-managed",
detail=(
f"HAProxy balances {spec.replicas.web} WebUI and {spec.replicas.api} API replica(s)."
),
endpoint={"scheme": "http", "host": "load-balancer", "port": 8080},
secret_refs=(),
dependent_modules=("core", "ops"),
)
def _ingress_capability(spec: InstallationSpec) -> InfrastructureCapability:
mode = spec.ingress.mode
endpoint = _redacted_endpoint(spec.public_url, default_port=443)
if mode == "unconfigured":
state = "unavailable"
source = "unconfigured"
detail = "No supported public ingress boundary is configured."
elif mode == "existing-proxy":
state = "externally_supplied"
source = "operator-supplied-proxy"
detail = "An externally operated reverse proxy provides public ingress."
else:
state = "configured"
source = "installer-managed" if mode == "managed" else "host-local"
detail = "The installer has a bounded public ingress configuration."
return InfrastructureCapability(
id="network.ingress",
label="Public HTTP ingress",
state=state,
source=source,
detail=detail,
endpoint=endpoint,
secret_refs=(),
dependent_modules=("core", "ops"),
)
def _post_install_tasks(
spec: InstallationSpec,
capabilities: tuple[InfrastructureCapability, ...],
) -> list[dict[str, object]]:
by_id = {item.id: item for item in capabilities}
tasks: list[dict[str, object]] = []
mail = by_id["mail.smtp"]
if mail.state == "available_unconfigured" and "mail" in spec.enabled_modules:
tasks.append(
{
"id": "mail.smtp-profile",
"resume_key": f"{spec.installation_id}:mail.smtp-profile:v1",
"capability_id": mail.id,
"state": "pending",
"owner_module": "mail",
"summary": "Create or select a Mail SMTP server and credential envelope.",
"required_inputs": [
"server endpoint",
"transport security policy",
"credential envelope reference when authentication is required",
],
"secret_boundary": "credential-envelope-reference-only",
}
)
ingress = by_id["network.ingress"]
if ingress.state == "unavailable":
tasks.append(
{
"id": "network.configure-ingress",
"resume_key": f"{spec.installation_id}:network.configure-ingress:v1",
"capability_id": ingress.id,
"state": "pending",
"owner_module": "ops",
"summary": "Select managed ingress or bind an existing reverse proxy.",
"required_inputs": ["public URL", "TLS and proxy trust boundary"],
"secret_boundary": "no-secret-material",
}
)
return tasks
def _enabled_consumers(
spec: InstallationSpec,
candidates: set[str],
*,
include_core: bool = False,
) -> tuple[str, ...]:
consumers = candidates.intersection(spec.enabled_modules)
if include_core:
consumers.add("core")
return tuple(sorted(consumers))
def _redacted_endpoint(value: str, *, default_port: int) -> dict[str, object]:
try:
parsed = urlsplit(value)
host = parsed.hostname
port = parsed.port or default_port
except ValueError:
return {"reference": "unresolved"}
if not parsed.scheme or not host:
return {"reference": "unresolved"}
return {"scheme": parsed.scheme, "host": host, "port": port}
def _capability_map(value: object) -> dict[str, Mapping[str, object]]:
if not isinstance(value, Mapping):
return {}
raw_items = value.get("capabilities")
if not isinstance(raw_items, list):
return {}
result: dict[str, Mapping[str, object]] = {}
for item in raw_items:
if not isinstance(item, Mapping):
continue
capability_id = str(item.get("id") or "").strip()
state = str(item.get("state") or "").strip()
if capability_id and state in CAPABILITY_STATES:
result[capability_id] = item
return result
def _string_items(value: object) -> tuple[str, ...]:
if not isinstance(value, list):
return ()
return tuple(str(item).strip() for item in value if str(item).strip())
def _endpoint_signature(value: object) -> tuple[tuple[str, str], ...]:
if not isinstance(value, Mapping):
return ()
return tuple(
sorted(
(str(key), str(raw))
for key, raw in value.items()
if isinstance(key, str) and isinstance(raw, (str, int, bool))
)
)
def _binding_change_label(
previous_endpoint: tuple[tuple[str, str], ...],
desired_endpoint: tuple[tuple[str, str], ...],
previous_secret_refs: tuple[str, ...],
desired_secret_refs: tuple[str, ...],
) -> str:
changes: list[str] = []
if previous_endpoint != desired_endpoint:
changes.append("endpoint binding")
if previous_secret_refs != desired_secret_refs:
changes.append("secret-reference binding")
return f" with changed {' and '.join(changes)}" if changes else ""
def _inventory_text(
value: Mapping[str, object],
key: str,
*,
maximum: int,
) -> str:
raw = value.get(key)
if not isinstance(raw, str):
raise ValueError(f"Infrastructure dependency inventory {key} is invalid.")
result = raw.strip()
if not result or len(result) > maximum or any(ord(char) < 32 for char in result):
raise ValueError(f"Infrastructure dependency inventory {key} is invalid.")
return result
def _inventory_string_list(
value: object,
*,
maximum_items: int,
maximum_length: int,
) -> tuple[str, ...]:
if not isinstance(value, list) or len(value) > maximum_items:
raise ValueError("Infrastructure dependency inventory list is invalid.")
items: list[str] = []
for raw in value:
if not isinstance(raw, str):
raise ValueError("Infrastructure dependency inventory list is invalid.")
item = raw.strip()
if (
not item
or len(item) > maximum_length
or any(ord(char) < 32 for char in item)
):
raise ValueError("Infrastructure dependency inventory list is invalid.")
items.append(item)
return tuple(items)
def _inventory_dependency(value: object) -> CapabilityDependency:
if not isinstance(value, Mapping):
raise ValueError("Infrastructure dependency record is invalid.")
state = _inventory_text(value, "state", maximum=40)
if state not in DEPENDENCY_STATES:
raise ValueError("Infrastructure dependency state is invalid.")
raw_metrics = value.get("metrics")
if not isinstance(raw_metrics, Mapping) or len(raw_metrics) > 20:
raise ValueError("Infrastructure dependency metrics are invalid.")
metrics: dict[str, int] = {}
for raw_key, raw_count in raw_metrics.items():
if not isinstance(raw_key, str):
raise ValueError("Infrastructure dependency metric name is invalid.")
key = raw_key.strip()
if not key or len(key) > 80 or any(ord(char) < 32 for char in key):
raise ValueError("Infrastructure dependency metric name is invalid.")
if type(raw_count) is not int or raw_count < 0:
raise ValueError("Infrastructure dependency metric value is invalid.")
metrics[key] = raw_count
return CapabilityDependency(
capability_id=_inventory_text(value, "capability_id", maximum=120),
module_id=_inventory_text(value, "module_id", maximum=120),
dependency_type=_inventory_text(value, "dependency_type", maximum=120),
dependency_ref=_inventory_text(value, "dependency_ref", maximum=240),
state=state,
scope=_inventory_text(value, "scope", maximum=120),
summary=_inventory_text(value, "summary", maximum=1000),
metrics=metrics,
required_action=_inventory_text(value, "required_action", maximum=1000),
)
+138 -1
View File
@@ -18,7 +18,8 @@ import sys
import time import time
from typing import Iterator, Mapping, Sequence from typing import Iterator, Mapping, Sequence
from urllib.error import URLError from urllib.error import URLError
from urllib.request import urlopen from urllib.parse import urlsplit
from urllib.request import Request, urlopen
from .backup_evidence import ( from .backup_evidence import (
DEFAULT_MAX_BACKUP_AGE_SECONDS, DEFAULT_MAX_BACKUP_AGE_SECONDS,
@@ -28,6 +29,7 @@ from .backup_evidence import (
) )
from .bundle import ( from .bundle import (
BACKUP_RUNTIME_ENV_KEYS, BACKUP_RUNTIME_ENV_KEYS,
BundlePaths,
atomic_write, atomic_write,
bundle_paths, bundle_paths,
canonical_json, canonical_json,
@@ -45,6 +47,11 @@ from .bundle import (
service_names, service_names,
write_env, write_env,
) )
from .capabilities import (
InfrastructureDependencyInventory,
infrastructure_capability_document,
infrastructure_dependency_inventory_from_mapping,
)
from .cluster_evidence import collect_kubernetes_evidence from .cluster_evidence import collect_kubernetes_evidence
from .distribution import ( from .distribution import (
MAX_KEYRING_BYTES, MAX_KEYRING_BYTES,
@@ -80,6 +87,7 @@ from .kubernetes import (
write_secret_creation_hint, write_secret_creation_hint,
) )
from .planning import ( from .planning import (
MAX_DEPENDENCY_INVENTORY_BYTES,
DeploymentPlan, DeploymentPlan,
build_plan, build_plan,
release_change_requires_backup, release_change_requires_backup,
@@ -151,6 +159,39 @@ def build_parser() -> argparse.ArgumentParser:
default=120.0, default=120.0,
help="Maximum time to wait for the public health endpoint.", help="Maximum time to wait for the public health endpoint.",
) )
apply_parser.add_argument(
"--ops-url",
help=(
"Dependency inventory URL; defaults to "
"<public-url>/api/v1/ops/infrastructure/dependencies."
),
)
apply_parser.add_argument(
"--api-key-env",
default="GOVOPLAN_OPS_API_KEY",
help=(
"Environment variable containing an API key authorized to read "
"Ops dependency inventory."
),
)
collect_inventory = subparsers.add_parser(
"collect-infrastructure-inventory",
help="Collect current module-owned capability dependencies from Ops.",
)
_directory_argument(collect_inventory)
collect_inventory.add_argument(
"--ops-url",
help=(
"Dependency inventory URL; defaults to "
"<public-url>/api/v1/ops/infrastructure/dependencies."
),
)
collect_inventory.add_argument(
"--api-key-env",
default="GOVOPLAN_OPS_API_KEY",
help="Environment variable containing an authorized Ops API key.",
)
status = subparsers.add_parser( status = subparsers.add_parser(
"status", help="Show desired state and current Compose process state." "status", help="Show desired state and current Compose process state."
@@ -424,6 +465,8 @@ def main(argv: Sequence[str] | None = None) -> int:
return _render_or_doctor(args) return _render_or_doctor(args)
if args.command == "apply": if args.command == "apply":
return _apply(args) return _apply(args)
if args.command == "collect-infrastructure-inventory":
return _collect_infrastructure_inventory(args)
if args.command == "status": if args.command == "status":
return _status(args) return _status(args)
if args.command == "verify-release": if args.command == "verify-release":
@@ -585,6 +628,25 @@ def _apply(args: argparse.Namespace) -> int:
) )
secrets = reconcile_runtime_environment(spec, read_env(paths.env)) secrets = reconcile_runtime_environment(spec, read_env(paths.env))
secrets = _write_bundle(spec, paths, secrets) secrets = _write_bundle(spec, paths, secrets)
preliminary_plan = build_plan(spec, paths, include_host_checks=False)
api_key_env = str(
getattr(args, "api_key_env", "GOVOPLAN_OPS_API_KEY")
).strip()
api_key = os.environ.get(api_key_env, "").strip()
if preliminary_plan.capability_impacts and api_key:
try:
_collect_dependency_inventory(
spec,
paths,
ops_url=getattr(args, "ops_url", None),
api_key=api_key,
)
print("Refreshed infrastructure dependency inventory from Ops.")
except (OSError, ValueError, json.JSONDecodeError) as exc:
print(
f"warning: could not refresh dependency inventory: {exc}",
file=sys.stderr,
)
plan = build_plan(spec, paths, include_host_checks=True) plan = build_plan(spec, paths, include_host_checks=True)
_write_plan(paths.plan, plan) _write_plan(paths.plan, plan)
effective_errors = [ effective_errors = [
@@ -612,6 +674,8 @@ def _apply(args: argparse.Namespace) -> int:
if effective_errors: if effective_errors:
_print_plan(plan) _print_plan(plan)
raise ValueError("deployment plan is blocked; resolve doctor errors first") raise ValueError("deployment plan is blocked; resolve doctor errors first")
if plan.capability_impacts:
_print_plan(plan)
docker = shutil.which("docker") docker = shutil.which("docker")
if docker is None: if docker is None:
raise ValueError("Docker CLI is required for apply") raise ValueError("Docker CLI is required for apply")
@@ -760,6 +824,70 @@ def _apply(args: argparse.Namespace) -> int:
return 0 return 0
def _collect_infrastructure_inventory(args: argparse.Namespace) -> int:
paths = bundle_paths(args.directory)
spec = load_spec(paths.spec)
api_key_env = str(args.api_key_env).strip()
api_key = os.environ.get(api_key_env, "").strip()
if not api_key:
raise ValueError(f"{api_key_env} must contain an authorized Ops API key")
inventory = _collect_dependency_inventory(
spec,
paths,
ops_url=args.ops_url,
api_key=api_key,
)
state = "complete" if inventory.complete else "incomplete"
print(
f"Collected {state} provider dependency inventory with "
f"{len(inventory.dependencies)} record(s) at {paths.dependency_inventory}."
)
return 0 if inventory.complete else 1
def _collect_dependency_inventory(
spec: InstallationSpec,
paths: BundlePaths,
*,
ops_url: str | None,
api_key: str,
) -> InfrastructureDependencyInventory:
url = str(ops_url or "").strip() or (
spec.public_url.rstrip("/")
+ "/api/v1/ops/infrastructure/dependencies"
)
_validate_ops_inventory_url(url)
request = Request(
url,
headers={"Accept": "application/json", "X-API-Key": api_key},
)
with urlopen(request, timeout=15) as response: # noqa: S310
_validate_ops_inventory_url(response.geturl())
encoded = response.read(MAX_DEPENDENCY_INVENTORY_BYTES + 1)
if len(encoded) > MAX_DEPENDENCY_INVENTORY_BYTES:
raise ValueError("Ops dependency inventory exceeds its size limit")
value = json.loads(encoded)
inventory = infrastructure_dependency_inventory_from_mapping(value)
if inventory.installation_id != spec.installation_id:
raise ValueError(
"Ops dependency inventory belongs to a different installation"
)
ensure_private_directory(paths.root)
atomic_write(paths.dependency_inventory, canonical_json(value), mode=0o600)
return inventory
def _validate_ops_inventory_url(url: str) -> None:
parsed = urlsplit(url)
if not parsed.hostname or parsed.username or parsed.password or parsed.fragment:
raise ValueError("Ops dependency inventory URL is invalid")
loopback = parsed.hostname in {"localhost", "127.0.0.1", "::1"}
if parsed.scheme != "https" and not (parsed.scheme == "http" and loopback):
raise ValueError(
"Ops dependency inventory URL requires HTTPS except on loopback"
)
def _status(args: argparse.Namespace) -> int: def _status(args: argparse.Namespace) -> int:
paths = bundle_paths(args.directory) paths = bundle_paths(args.directory)
spec = load_spec(paths.spec) spec = load_spec(paths.spec)
@@ -1287,6 +1415,10 @@ def _deployment_receipt(
"agent": "cli", "agent": "cli",
"web_updates": False, "web_updates": False,
}, },
"infrastructure_capabilities": infrastructure_capability_document(
spec,
secrets,
),
} }
@@ -1464,6 +1596,11 @@ def _write_bundle(
runtime_environment.update(_backup_runtime_environment(spec, paths)) runtime_environment.update(_backup_runtime_environment(spec, paths))
atomic_write(paths.spec, canonical_json(spec.to_dict()), mode=0o600) atomic_write(paths.spec, canonical_json(spec.to_dict()), mode=0o600)
write_env(paths.env, runtime_environment) write_env(paths.env, runtime_environment)
atomic_write(
paths.capabilities,
canonical_json(infrastructure_capability_document(spec, runtime_environment)),
mode=0o644,
)
atomic_write(paths.compose, canonical_json(render_compose(spec)), mode=0o600) atomic_write(paths.compose, canonical_json(render_compose(spec)), mode=0o600)
atomic_write( atomic_write(
paths.load_balancer_config, paths.load_balancer_config,
@@ -11,6 +11,7 @@ from typing import Any, Mapping
from urllib.parse import urlsplit from urllib.parse import urlsplit
from .bundle import BACKUP_RUNTIME_ENV_KEYS from .bundle import BACKUP_RUNTIME_ENV_KEYS
from .capabilities import infrastructure_capability_document
from .model import InstallationSpec, image_is_digest_pinned from .model import InstallationSpec, image_is_digest_pinned
@@ -96,6 +97,7 @@ def render_kubernetes(
public_host = urlsplit(spec.public_url).hostname or "localhost" public_host = urlsplit(spec.public_url).hostname or "localhost"
labels = {"app.kubernetes.io/name": "govoplan", "app.kubernetes.io/instance": name} labels = {"app.kubernetes.io/name": "govoplan", "app.kubernetes.io/instance": name}
config_name = f"{name}-runtime" config_name = f"{name}-runtime"
capabilities_config_name = f"{name}-infrastructure-capabilities"
service_account = f"{name}-runtime" service_account = f"{name}-runtime"
config = { config = {
key: str(environment[key]) key: str(environment[key])
@@ -142,6 +144,22 @@ def render_kubernetes(
"metadata": {"name": config_name, "namespace": namespace, "labels": labels}, "metadata": {"name": config_name, "namespace": namespace, "labels": labels},
"data": dict(sorted(config.items())), "data": dict(sorted(config.items())),
}, },
{
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": {
"name": capabilities_config_name,
"namespace": namespace,
"labels": labels,
},
"data": {
"infrastructure-capabilities.json": json.dumps(
infrastructure_capability_document(spec, environment),
sort_keys=True,
separators=(",", ":"),
)
},
},
_deployment( _deployment(
name=f"{name}-api", name=f"{name}-api",
namespace=namespace, namespace=namespace,
@@ -161,6 +179,7 @@ def render_kubernetes(
"--proxy-headers", "--proxy-headers",
), ),
config_name=config_name, config_name=config_name,
capabilities_config_name=capabilities_config_name,
secret_name=secret_name, secret_name=secret_name,
s3_ca_secret_name=s3_ca_secret_name, s3_ca_secret_name=s3_ca_secret_name,
service_account=service_account, service_account=service_account,
@@ -187,6 +206,7 @@ def render_kubernetes(
image=spec.release.web_image, image=spec.release.web_image,
command=(), command=(),
config_name=None, config_name=None,
capabilities_config_name=None,
secret_name=None, secret_name=None,
s3_ca_secret_name=None, s3_ca_secret_name=None,
service_account=service_account, service_account=service_account,
@@ -247,6 +267,7 @@ def render_kubernetes(
"INFO", "INFO",
), ),
config_name=config_name, config_name=config_name,
capabilities_config_name=capabilities_config_name,
secret_name=secret_name, secret_name=secret_name,
s3_ca_secret_name=s3_ca_secret_name, s3_ca_secret_name=s3_ca_secret_name,
service_account=service_account, service_account=service_account,
@@ -303,6 +324,7 @@ def render_kubernetes(
"/tmp/celerybeat-schedule", "/tmp/celerybeat-schedule",
), ),
config_name=config_name, config_name=config_name,
capabilities_config_name=capabilities_config_name,
secret_name=secret_name, secret_name=secret_name,
s3_ca_secret_name=s3_ca_secret_name, s3_ca_secret_name=s3_ca_secret_name,
service_account=service_account, service_account=service_account,
@@ -652,6 +674,7 @@ def _deployment(
image: str, image: str,
command: tuple[str, ...], command: tuple[str, ...],
config_name: str | None, config_name: str | None,
capabilities_config_name: str | None,
secret_name: str | None, secret_name: str | None,
s3_ca_secret_name: str | None, s3_ca_secret_name: str | None,
service_account: str, service_account: str,
@@ -682,6 +705,13 @@ def _deployment(
) )
if secret_name: if secret_name:
environment.extend(_secret_environment(secret_name)) environment.extend(_secret_environment(secret_name))
if capabilities_config_name:
environment.append(
{
"name": "GOVOPLAN_DEPLOYMENT_CAPABILITIES_PATH",
"value": "/etc/govoplan/deployment/infrastructure-capabilities.json",
}
)
if s3_ca_secret_name: if s3_ca_secret_name:
environment.append( environment.append(
{"name": "AWS_CA_BUNDLE", "value": "/etc/govoplan/trust/s3-ca.crt"} {"name": "AWS_CA_BUNDLE", "value": "/etc/govoplan/trust/s3-ca.crt"}
@@ -757,6 +787,29 @@ def _deployment(
if s3_ca_secret_name: if s3_ca_secret_name:
pod_spec["volumes"].append(_s3_ca_volume(s3_ca_secret_name)) pod_spec["volumes"].append(_s3_ca_volume(s3_ca_secret_name))
container["volumeMounts"].append(_s3_ca_volume_mount()) container["volumeMounts"].append(_s3_ca_volume_mount())
if capabilities_config_name:
pod_spec["volumes"].append(
{
"name": "deployment-capabilities",
"configMap": {
"name": capabilities_config_name,
"items": [
{
"key": "infrastructure-capabilities.json",
"path": "infrastructure-capabilities.json",
}
],
},
}
)
container["volumeMounts"].append(
{
"name": "deployment-capabilities",
"mountPath": "/etc/govoplan/deployment/infrastructure-capabilities.json",
"subPath": "infrastructure-capabilities.json",
"readOnly": True,
}
)
if config_name: if config_name:
pod_spec["containers"][0]["envFrom"] = [{"configMapRef": {"name": config_name}}] pod_spec["containers"][0]["envFrom"] = [{"configMapRef": {"name": config_name}}]
if config_name and secret_name: if config_name and secret_name:
@@ -3,6 +3,7 @@
from __future__ import annotations from __future__ import annotations
from dataclasses import asdict, dataclass from dataclasses import asdict, dataclass
from datetime import UTC, datetime
import hashlib import hashlib
import json import json
import os import os
@@ -35,6 +36,13 @@ from .bundle import (
render_existing_proxy_contract, render_existing_proxy_contract,
service_names, service_names,
) )
from .capabilities import (
CapabilityChangeImpact,
InfrastructureDependencyInventory,
capability_change_impacts,
infrastructure_capability_document,
infrastructure_dependency_inventory_from_mapping,
)
from .distribution import ( from .distribution import (
MAX_KEYRING_BYTES, MAX_KEYRING_BYTES,
MAX_MANIFEST_BYTES, MAX_MANIFEST_BYTES,
@@ -83,6 +91,8 @@ class DeploymentPlan:
desired_environment_fingerprint: str desired_environment_fingerprint: str
actions: tuple[PlanAction, ...] actions: tuple[PlanAction, ...]
checks: tuple[Check, ...] checks: tuple[Check, ...]
infrastructure_capabilities: Mapping[str, object]
capability_impacts: tuple[CapabilityChangeImpact, ...]
@property @property
def blocked(self) -> bool: def blocked(self) -> bool:
@@ -97,10 +107,15 @@ class DeploymentPlan:
"blocked": self.blocked, "blocked": self.blocked,
"actions": [action.to_dict() for action in self.actions], "actions": [action.to_dict() for action in self.actions],
"checks": [check.to_dict() for check in self.checks], "checks": [check.to_dict() for check in self.checks],
"infrastructure_capabilities": dict(self.infrastructure_capabilities),
"capability_impacts": [item.to_dict() for item in self.capability_impacts],
} }
CommandRunner = Callable[[Sequence[str], Path], subprocess.CompletedProcess[str]] CommandRunner = Callable[[Sequence[str], Path], subprocess.CompletedProcess[str]]
MAX_DEPENDENCY_INVENTORY_BYTES = 2 * 1024 * 1024
DEPENDENCY_INVENTORY_MAX_AGE_SECONDS = 300
DEPENDENCY_INVENTORY_MAX_FUTURE_SECONDS = 60
def build_plan( def build_plan(
@@ -122,6 +137,18 @@ def build_plan(
spec_digest = digest_json(spec.to_dict()) spec_digest = digest_json(spec.to_dict())
compose_digest = digest_json(compose) compose_digest = digest_json(compose)
environment_digest = environment_fingerprint(read_env(paths.env)) environment_digest = environment_fingerprint(read_env(paths.env))
infrastructure_capabilities = infrastructure_capability_document(
spec,
read_env(paths.env),
)
dependency_inventory, dependency_inventory_error = (
_read_dependency_inventory(paths.dependency_inventory)
)
capability_impacts = capability_change_impacts(
previous.get("infrastructure_capabilities"),
infrastructure_capabilities,
dependency_inventory=dependency_inventory,
)
actions: list[PlanAction] = [] actions: list[PlanAction] = []
if not previous: if not previous:
@@ -166,12 +193,21 @@ def build_plan(
"Remove the service container; retained volumes are not deleted.", "Remove the service container; retained volumes are not deleted.",
) )
) )
for impact in capability_impacts:
actions.append(
PlanAction(
"review",
f"capability:{impact.capability_id}",
impact.detail,
)
)
if ( if (
previous previous
and previous_spec_digest == spec_digest and previous_spec_digest == spec_digest
and previous_compose_digest == compose_digest and previous_compose_digest == compose_digest
and previous_environment_fingerprint == environment_digest and previous_environment_fingerprint == environment_digest
and previous_services == desired_services and previous_services == desired_services
and not capability_impacts
): ):
actions.append( actions.append(
PlanAction( PlanAction(
@@ -180,6 +216,23 @@ def build_plan(
) )
checks = list(static_checks(spec, paths)) checks = list(static_checks(spec, paths))
checks.extend(
Check(
id=f"capability.change.{impact.capability_id}",
level="warning",
message=impact.detail,
action=impact.required_action,
)
for impact in capability_impacts
)
checks.extend(
_dependency_inventory_checks(
spec,
capability_impacts,
dependency_inventory,
dependency_inventory_error,
)
)
if include_host_checks: if include_host_checks:
checks.extend(host_checks(spec, paths, command_runner=command_runner)) checks.extend(host_checks(spec, paths, command_runner=command_runner))
return DeploymentPlan( return DeploymentPlan(
@@ -189,6 +242,8 @@ def build_plan(
desired_environment_fingerprint=environment_digest, desired_environment_fingerprint=environment_digest,
actions=tuple(actions), actions=tuple(actions),
checks=tuple(checks), checks=tuple(checks),
infrastructure_capabilities=infrastructure_capabilities,
capability_impacts=capability_impacts,
) )
@@ -1150,6 +1205,106 @@ def _read_receipt(path: Path) -> Mapping[str, object]:
return value if isinstance(value, dict) else {} return value if isinstance(value, dict) else {}
def _read_dependency_inventory(
path: Path,
) -> tuple[InfrastructureDependencyInventory | None, str]:
if not path.exists():
return None, "missing"
try:
value = load_bounded_json(
path,
maximum_bytes=MAX_DEPENDENCY_INVENTORY_BYTES,
)
return infrastructure_dependency_inventory_from_mapping(value), ""
except (DistributionError, ValueError) as exc:
return None, str(exc)
def _dependency_inventory_checks(
spec: InstallationSpec,
impacts: tuple[CapabilityChangeImpact, ...],
inventory: InfrastructureDependencyInventory | None,
inventory_error: str,
) -> tuple[Check, ...]:
if not impacts:
return ()
collect_action = (
"Run govoplan-deploy collect-infrastructure-inventory with an Ops API "
"key, then review the capability impacts before apply."
)
if inventory is None:
if inventory_error == "missing":
message = "Current provider dependency inventory is missing."
check_id = "capability.dependency_inventory.missing"
else:
message = f"Provider dependency inventory is invalid: {inventory_error}"
check_id = "capability.dependency_inventory.invalid"
return (Check(check_id, "error", message, collect_action),)
if inventory.installation_id != spec.installation_id:
return (
Check(
"capability.dependency_inventory.installation",
"error",
"Provider dependency inventory belongs to a different installation.",
collect_action,
),
)
if not inventory.complete:
return (
Check(
"capability.dependency_inventory.incomplete",
"error",
"Provider dependency inventory is incomplete because at least one provider failed.",
"Resolve the provider failure and collect the inventory again.",
),
)
age_seconds = (datetime.now(UTC) - inventory.generated_at).total_seconds()
if age_seconds < -DEPENDENCY_INVENTORY_MAX_FUTURE_SECONDS:
return (
Check(
"capability.dependency_inventory.future",
"error",
"Provider dependency inventory timestamp is in the future.",
"Correct host clock skew and collect the inventory again.",
),
)
if age_seconds > DEPENDENCY_INVENTORY_MAX_AGE_SECONDS:
return (
Check(
"capability.dependency_inventory.stale",
"error",
"Provider dependency inventory is older than five minutes.",
collect_action,
),
)
impacted_ids = {item.capability_id for item in impacts}
missing_ids = sorted(impacted_ids - set(inventory.inspected_capability_ids))
if missing_ids:
return (
Check(
"capability.dependency_inventory.coverage",
"error",
"Provider dependency inventory did not inspect impacted capabilities: "
+ ", ".join(missing_ids)
+ ".",
collect_action,
),
)
matching_dependencies = sum(
len(inventory.dependencies_for(capability_id))
for capability_id in impacted_ids
)
return (
Check(
"capability.dependency_inventory.current",
"ok",
"Current provider inventory inspected every impacted capability and "
f"reported {matching_dependencies} persisted dependency record(s) from "
f"{inventory.provider_count} provider(s).",
),
)
def _memory_bytes() -> int | None: def _memory_bytes() -> int | None:
try: try:
for line in Path("/proc/meminfo").read_text(encoding="utf-8").splitlines(): for line in Path("/proc/meminfo").read_text(encoding="utf-8").splitlines():
@@ -33,6 +33,8 @@ _BUNDLE_FILES = (
"backup-keyring.json", "backup-keyring.json",
"backup-verification.json", "backup-verification.json",
"receipt.json", "receipt.json",
"infrastructure-capabilities.json",
"infrastructure-dependency-inventory.json",
) )
+1 -1
View File
@@ -247,7 +247,7 @@ def is_excluded_repo_file(path: pathlib.Path) -> bool:
return True return True
if "testing_plan" in name or "test_plan" in name: if "testing_plan" in name or "test_plan" in name:
return True return True
if text.endswith("/docs/GITEA_ISSUES.md"): if text.endswith(("/docs/GITEA_ISSUES.md", "/docs/project/GITEA_ISSUES.md")):
return True return True
if text.endswith("/docs/GOVOPLAN_MASTER_ROADMAP.md"): if text.endswith("/docs/GOVOPLAN_MASTER_ROADMAP.md"):
return True return True
+1
View File
@@ -34,6 +34,7 @@ DEFAULT_EXCLUDES = (
"!**/.cache/**", "!**/.cache/**",
"!.gitea/**", "!.gitea/**",
"!docs/GITEA_ISSUES.md", "!docs/GITEA_ISSUES.md",
"!docs/project/GITEA_ISSUES.md",
"!tools/gitea/gitea-todo-import.py", "!tools/gitea/gitea-todo-import.py",
"!tools/gitea/gitea-sync-labels.py", "!tools/gitea/gitea-sync-labels.py",
"!tools/gitea/gitea-codex-note.py", "!tools/gitea/gitea-codex-note.py",
@@ -280,6 +280,13 @@
"rationale": "The module WebUI constructs this endpoint through a mounted router prefix, generic action, or provider path.", "rationale": "The module WebUI constructs this endpoint through a mounted router prefix, generic action, or provider path.",
"repository": "govoplan-audit" "repository": "govoplan-audit"
}, },
{
"category": "intentionally_headless",
"method": "GET",
"path": "/admin/audit/evidence-bundles/{}",
"rationale": "Evidence export clients can poll the persisted request/result lifecycle before downloading; the current synchronous administration action downloads ready bundles directly.",
"repository": "govoplan-audit"
},
{ {
"category": "ui_reachable", "category": "ui_reachable",
"method": "GET", "method": "GET",
@@ -357,6 +364,27 @@
"rationale": "The local bootstrap handoff reads only minimum first-run readiness before a normal authenticated shell exists.", "rationale": "The local bootstrap handoff reads only minimum first-run readiness before a normal authenticated shell exists.",
"repository": "govoplan-core" "repository": "govoplan-core"
}, },
{
"category": "ui_reachable",
"method": "POST",
"path": "/admin/privacy/data-subject-requests/{}/erasure-plan",
"rationale": "The Data-subject requests administration panel builds this revision-guarded action URL from the selected request identifier.",
"repository": "govoplan-core"
},
{
"category": "ui_reachable",
"method": "POST",
"path": "/admin/privacy/data-subject-requests/{}/execute",
"rationale": "The Data-subject requests administration panel builds this explicitly confirmed action URL from the selected request identifier.",
"repository": "govoplan-core"
},
{
"category": "ui_reachable",
"method": "POST",
"path": "/admin/privacy/data-subject-requests/{}/search",
"rationale": "The Data-subject requests administration panel builds this provider-search action URL from the selected request identifier.",
"repository": "govoplan-core"
},
{ {
"category": "public_integration", "category": "public_integration",
"method": "GET", "method": "GET",
@@ -448,6 +476,188 @@
"rationale": "This capability-first module intentionally exposes a headless API for other modules and integrations.", "rationale": "This capability-first module intentionally exposes a headless API for other modules and integrations.",
"repository": "govoplan-connectors" "repository": "govoplan-connectors"
}, },
{
"category": "ui_reachable",
"method": "GET",
"path": "/connectors/governed/configurations",
"rationale": "The Connector governance administration page lists tenant configurations through this endpoint.",
"repository": "govoplan-connectors"
},
{
"category": "ui_reachable",
"method": "POST",
"path": "/connectors/governed/configurations",
"rationale": "The Connector governance administration page creates tenant configurations through this endpoint.",
"repository": "govoplan-connectors"
},
{
"category": "ui_reachable",
"method": "PUT",
"path": "/connectors/governed/configurations/{}",
"rationale": "The Connector governance administration page constructs this revision-guarded URL from the selected configuration identifier.",
"repository": "govoplan-connectors"
},
{
"category": "ui_reachable",
"method": "POST",
"path": "/connectors/governed/configurations/{}/dry-runs",
"rationale": "The Connector governance administration page constructs this bounded dry-run URL from the selected configuration identifier.",
"repository": "govoplan-connectors"
},
{
"category": "ui_reachable",
"method": "POST",
"path": "/connectors/governed/configurations/{}/simulations",
"rationale": "The Connector governance administration page constructs this bounded simulation URL from the selected configuration identifier.",
"repository": "govoplan-connectors"
},
{
"category": "ui_reachable",
"method": "GET",
"path": "/connectors/governed/definitions",
"rationale": "The Connector governance administration page lists governed connector definitions through this endpoint.",
"repository": "govoplan-connectors"
},
{
"category": "ui_reachable",
"method": "POST",
"path": "/connectors/governed/definitions",
"rationale": "The Connector governance administration page publishes immutable connector-definition revisions through this endpoint.",
"repository": "govoplan-connectors"
},
{
"category": "ui_reachable",
"method": "GET",
"path": "/connectors/governed/runs",
"rationale": "The Connector governance administration page lists dry-run and simulation evidence through this endpoint.",
"repository": "govoplan-connectors"
},
{
"category": "ui_reachable",
"method": "POST",
"path": "/connectors/governed/runs/{}/review",
"rationale": "The Connector governance administration page constructs this review-decision URL from the selected run identifier.",
"repository": "govoplan-connectors"
},
{
"category": "ui_reachable",
"method": "GET",
"path": "/connectors/knowledge/profiles",
"rationale": "The External knowledge administration page lists MediaWiki and BlueSpice profiles through this endpoint.",
"repository": "govoplan-connectors"
},
{
"category": "ui_reachable",
"method": "POST",
"path": "/connectors/knowledge/profiles",
"rationale": "The External knowledge administration page creates a mapped, ACL-governed knowledge profile through this endpoint.",
"repository": "govoplan-connectors"
},
{
"category": "ui_reachable",
"method": "PUT",
"path": "/connectors/knowledge/profiles/{}",
"rationale": "The External knowledge administration page constructs this revision-guarded profile URL from the selected profile identifier.",
"repository": "govoplan-connectors"
},
{
"category": "ui_reachable",
"method": "POST",
"path": "/connectors/knowledge/profiles/{}/discover",
"rationale": "The External knowledge administration page discovers product, version, capabilities, namespaces, and diagnostics through this endpoint.",
"repository": "govoplan-connectors"
},
{
"category": "ui_reachable",
"method": "POST",
"path": "/connectors/knowledge/profiles/{}/migration-dry-runs",
"rationale": "The External knowledge administration page runs a bounded, non-writing native-Wiki migration preview through this endpoint.",
"repository": "govoplan-connectors"
},
{
"category": "ui_reachable",
"method": "GET",
"path": "/connectors/knowledge/profiles/{}/objects",
"rationale": "The External knowledge administration page lists synchronized, identity-stable knowledge snapshots through this endpoint.",
"repository": "govoplan-connectors"
},
{
"category": "ui_reachable",
"method": "POST",
"path": "/connectors/knowledge/profiles/{}/pages/{}/publish",
"rationale": "The External knowledge administration page constructs this revision-checked external publication URL from the selected profile and page identifiers.",
"repository": "govoplan-connectors"
},
{
"category": "ui_reachable",
"method": "POST",
"path": "/connectors/knowledge/profiles/{}/sync",
"rationale": "The External knowledge administration page runs keyed full backfills and recent-change deltas through this endpoint.",
"repository": "govoplan-connectors"
},
{
"category": "ui_reachable",
"method": "GET",
"path": "/connectors/knowledge/runs",
"rationale": "The External knowledge administration page lists synchronization, migration-preview, and publication evidence through this endpoint.",
"repository": "govoplan-connectors"
},
{
"category": "ui_reachable",
"method": "GET",
"path": "/connectors/service-desk/profiles",
"rationale": "The External service desk administration page lists Znuny/OTRS profiles through this endpoint.",
"repository": "govoplan-connectors"
},
{
"category": "ui_reachable",
"method": "POST",
"path": "/connectors/service-desk/profiles",
"rationale": "The External service desk administration page creates route-, queue-, field-, authority-, and ACL-governed profiles through this endpoint.",
"repository": "govoplan-connectors"
},
{
"category": "ui_reachable",
"method": "PUT",
"path": "/connectors/service-desk/profiles/{}",
"rationale": "The External service desk administration page constructs this revision-guarded profile URL from the selected profile identifier.",
"repository": "govoplan-connectors"
},
{
"category": "ui_reachable",
"method": "POST",
"path": "/connectors/service-desk/profiles/{}/discover",
"rationale": "The External service desk administration page discovers product, version, maturity, capabilities, route health, and diagnostics through this endpoint.",
"repository": "govoplan-connectors"
},
{
"category": "ui_reachable",
"method": "GET",
"path": "/connectors/service-desk/profiles/{}/objects",
"rationale": "The External service desk administration page lists synchronized identity-stable, ACL-governed external ticket projections through this endpoint.",
"repository": "govoplan-connectors"
},
{
"category": "ui_reachable",
"method": "POST",
"path": "/connectors/service-desk/profiles/{}/sync",
"rationale": "The External service desk administration page runs keyed bounded full and delta synchronization through this endpoint.",
"repository": "govoplan-connectors"
},
{
"category": "ui_reachable",
"method": "POST",
"path": "/connectors/service-desk/profiles/{}/tickets/{}/update",
"rationale": "The External service desk ticket list opens a separated revision-checked governed update dialog and constructs this URL from stable profile and provider ticket identifiers.",
"repository": "govoplan-connectors"
},
{
"category": "ui_reachable",
"method": "GET",
"path": "/connectors/service-desk/runs",
"rationale": "The External service desk administration page lists synchronization and external-mutation recovery evidence through this endpoint.",
"repository": "govoplan-connectors"
},
{ {
"category": "intentionally_headless", "category": "intentionally_headless",
"method": "GET", "method": "GET",
@@ -483,6 +693,13 @@
"rationale": "This capability-first module intentionally exposes a headless API for other modules and integrations.", "rationale": "This capability-first module intentionally exposes a headless API for other modules and integrations.",
"repository": "govoplan-connectors" "repository": "govoplan-connectors"
}, },
{
"category": "intentionally_headless",
"method": "POST",
"path": "/connectors/tabular-sources/files",
"rationale": "This capability-first module intentionally exposes a headless API for other modules and integrations.",
"repository": "govoplan-connectors"
},
{ {
"category": "intentionally_headless", "category": "intentionally_headless",
"method": "POST", "method": "POST",
@@ -490,6 +707,13 @@
"rationale": "This capability-first module intentionally exposes a headless API for other modules and integrations.", "rationale": "This capability-first module intentionally exposes a headless API for other modules and integrations.",
"repository": "govoplan-connectors" "repository": "govoplan-connectors"
}, },
{
"category": "intentionally_headless",
"method": "POST",
"path": "/connectors/tabular-sources/sql",
"rationale": "This capability-first module intentionally exposes a headless API for other modules and integrations.",
"repository": "govoplan-connectors"
},
{ {
"category": "intentionally_headless", "category": "intentionally_headless",
"method": "DELETE", "method": "DELETE",
@@ -504,6 +728,13 @@
"rationale": "This capability-first module intentionally exposes a headless API for other modules and integrations.", "rationale": "This capability-first module intentionally exposes a headless API for other modules and integrations.",
"repository": "govoplan-connectors" "repository": "govoplan-connectors"
}, },
{
"category": "intentionally_headless",
"method": "POST",
"path": "/connectors/tabular-sources/{}/refresh",
"rationale": "This capability-first module intentionally exposes a headless API for other modules and integrations.",
"repository": "govoplan-connectors"
},
{ {
"category": "public_integration", "category": "public_integration",
"method": "GET", "method": "GET",
@@ -672,6 +903,90 @@
"rationale": "The module WebUI constructs this endpoint through a mounted router prefix, generic action, or provider path.", "rationale": "The module WebUI constructs this endpoint through a mounted router prefix, generic action, or provider path.",
"repository": "govoplan-docs" "repository": "govoplan-docs"
}, },
{
"category": "ui_reachable",
"method": "GET",
"path": "/semantic/entries",
"rationale": "The Docs semantic authoring UI calls this mounted sub-router through the /docs prefix, which static endpoint matching cannot compose.",
"repository": "govoplan-docs"
},
{
"category": "ui_reachable",
"method": "POST",
"path": "/semantic/entries",
"rationale": "The Docs semantic authoring UI calls this mounted sub-router through the /docs prefix, which static endpoint matching cannot compose.",
"repository": "govoplan-docs"
},
{
"category": "ui_reachable",
"method": "GET",
"path": "/semantic/entries/{}",
"rationale": "The Docs semantic authoring UI constructs the entry identifier dynamically on the mounted semantic sub-router.",
"repository": "govoplan-docs"
},
{
"category": "ui_reachable",
"method": "PUT",
"path": "/semantic/entries/{}",
"rationale": "The Docs semantic authoring UI constructs the entry identifier dynamically on the mounted semantic sub-router.",
"repository": "govoplan-docs"
},
{
"category": "ui_reachable",
"method": "GET",
"path": "/semantic/entries/{}/history",
"rationale": "The Docs semantic authoring UI constructs immutable history paths dynamically on the mounted semantic sub-router.",
"repository": "govoplan-docs"
},
{
"category": "ui_reachable",
"method": "POST",
"path": "/semantic/entries/{}/publish",
"rationale": "The Docs semantic authoring UI constructs lifecycle action paths dynamically on the mounted semantic sub-router.",
"repository": "govoplan-docs"
},
{
"category": "ui_reachable",
"method": "POST",
"path": "/semantic/entries/{}/retire",
"rationale": "The Docs semantic authoring UI constructs lifecycle action paths dynamically on the mounted semantic sub-router.",
"repository": "govoplan-docs"
},
{
"category": "ui_reachable",
"method": "POST",
"path": "/semantic/entries/{}/supersede",
"rationale": "The Docs semantic authoring UI constructs lifecycle action paths dynamically on the mounted semantic sub-router.",
"repository": "govoplan-docs"
},
{
"category": "ui_reachable",
"method": "GET",
"path": "/semantic/export",
"rationale": "The Docs semantic authoring UI calls tenant export on the mounted semantic sub-router.",
"repository": "govoplan-docs"
},
{
"category": "ui_reachable",
"method": "GET",
"path": "/semantic/policy",
"rationale": "The Docs semantic authoring UI reads publication policy through the mounted semantic sub-router.",
"repository": "govoplan-docs"
},
{
"category": "ui_reachable",
"method": "PUT",
"path": "/semantic/policy",
"rationale": "The Docs semantic authoring UI saves publication policy through the mounted semantic sub-router.",
"repository": "govoplan-docs"
},
{
"category": "ui_reachable",
"method": "GET",
"path": "/semantic/subjects",
"rationale": "The Docs semantic authoring UI discovers authorized module subjects through the mounted semantic sub-router.",
"repository": "govoplan-docs"
},
{ {
"category": "ui_reachable", "category": "ui_reachable",
"method": "GET", "method": "GET",
@@ -805,6 +1120,13 @@
"rationale": "A short-lived purpose-bound bearer grant lets the public Forms Runtime surface stream one attachment directly to Files without granting general Files access.", "rationale": "A short-lived purpose-bound bearer grant lets the public Forms Runtime surface stream one attachment directly to Files without granting general Files access.",
"repository": "govoplan-files" "repository": "govoplan-files"
}, },
{
"category": "ui_reachable",
"method": "GET",
"path": "/files/{}/versions/{}/download",
"rationale": "Postbox resolves immutable Files evidence references to this exact-version download URL; Files performs the final tenant and access check.",
"repository": "govoplan-files"
},
{ {
"category": "ui_reachable", "category": "ui_reachable",
"method": "POST", "method": "POST",
@@ -896,6 +1218,20 @@
"rationale": "Retained compatibility endpoint; current module surfaces use the replacement contract.", "rationale": "Retained compatibility endpoint; current module surfaces use the replacement contract.",
"repository": "govoplan-identity" "repository": "govoplan-identity"
}, },
{
"category": "ui_reachable",
"method": "POST",
"path": "/identity/identities/{}/activate",
"rationale": "The Identity administration workspace constructs this explicit lifecycle-action URL from the selected identity identifier.",
"repository": "govoplan-identity"
},
{
"category": "ui_reachable",
"method": "POST",
"path": "/identity/identities/{}/deactivate",
"rationale": "The Identity administration workspace constructs this explicit lifecycle-action URL from the selected identity identifier.",
"repository": "govoplan-identity"
},
{ {
"category": "intentionally_headless", "category": "intentionally_headless",
"method": "POST", "method": "POST",
@@ -1043,6 +1379,13 @@
"rationale": "Operational worker, scheduler, reconciliation, or health endpoint; it is not a direct user surface.", "rationale": "Operational worker, scheduler, reconciliation, or health endpoint; it is not a direct user surface.",
"repository": "govoplan-notifications" "repository": "govoplan-notifications"
}, },
{
"category": "intentionally_headless",
"method": "GET",
"path": "/ops/infrastructure/dependencies",
"rationale": "Authorized host-deployer preflight consumes this provider inventory directly; it is private operational evidence rather than a product page.",
"repository": "govoplan-ops"
},
{ {
"category": "worker_internal", "category": "worker_internal",
"method": "GET", "method": "GET",
@@ -1118,6 +1461,34 @@
"rationale": "This capability-first module intentionally exposes a headless API for other modules and integrations.", "rationale": "This capability-first module intentionally exposes a headless API for other modules and integrations.",
"repository": "govoplan-parties" "repository": "govoplan-parties"
}, },
{
"category": "ui_reachable",
"method": "GET",
"path": "/payments/requests",
"rationale": "The Payments workspace lists obligations, preserves stale data on refresh failure, and offers explicit retry.",
"repository": "govoplan-payments"
},
{
"category": "ui_reachable",
"method": "POST",
"path": "/payments/requests",
"rationale": "The guided Payments request dialog creates fixed, source-bound obligations with an explicit replay key.",
"repository": "govoplan-payments"
},
{
"category": "intentionally_headless",
"method": "GET",
"path": "/payments/requests/{}",
"rationale": "The operator list response already carries the complete detail projection; the single-object endpoint remains available to capability consumers without duplicating a detail fetch in the workspace.",
"repository": "govoplan-payments"
},
{
"category": "ui_reachable",
"method": "POST",
"path": "/payments/requests/{}/manual-reconciliations",
"rationale": "The separate manual reconciliation dialog fixes amount and currency and captures transaction and immutable evidence references.",
"repository": "govoplan-payments"
},
{ {
"category": "intentionally_headless", "category": "intentionally_headless",
"method": "DELETE", "method": "DELETE",
@@ -1321,6 +1692,13 @@
"rationale": "Postbox delivery, access-decision, and routing-preview APIs are consumed by Campaign and other module capabilities.", "rationale": "Postbox delivery, access-decision, and routing-preview APIs are consumed by Campaign and other module capabilities.",
"repository": "govoplan-postbox" "repository": "govoplan-postbox"
}, },
{
"category": "public_integration",
"method": "POST",
"path": "/postbox/admin/postboxes/{}/protection-transitions/{}/transform",
"rationale": "An approved external encryption client uses this governed endpoint to submit per-message E2EE or plaintext transforms with digest and authority evidence; the ordinary server-rendered WebUI never handles E2EE private keys.",
"repository": "govoplan-postbox"
},
{ {
"category": "intentionally_headless", "category": "intentionally_headless",
"method": "POST", "method": "POST",
@@ -1757,68 +2135,60 @@
"repository": "govoplan-campaign" "repository": "govoplan-campaign"
}, },
{ {
"category": "missing_ui", "category": "ui_reachable",
"method": "GET", "method": "GET",
"path": "/relationships", "path": "/relationships",
"rationale": "IDM documents typed relationships but its current administration page exposes function assignments only.", "rationale": "The IDM relationship administration grid lists effective-dated identity relationships and their lifecycle state.",
"repository": "govoplan-idm", "repository": "govoplan-idm"
"tracking_issue": "https://git.add-ideas.de/GovOPlaN/govoplan-idm/issues/11"
}, },
{ {
"category": "missing_ui", "category": "ui_reachable",
"method": "POST", "method": "POST",
"path": "/relationships", "path": "/relationships",
"rationale": "IDM documents typed relationships but its current administration page exposes function assignments only.", "rationale": "Authorized IDM administrators create relationships through the searchable relationship editor.",
"repository": "govoplan-idm", "repository": "govoplan-idm"
"tracking_issue": "https://git.add-ideas.de/GovOPlaN/govoplan-idm/issues/11"
}, },
{ {
"category": "missing_ui", "category": "ui_reachable",
"method": "PATCH", "method": "PATCH",
"path": "/relationships/{}", "path": "/relationships/{}",
"rationale": "IDM documents typed relationships but its current administration page exposes function assignments only.", "rationale": "Authorized IDM administrators update a loaded relationship with its optimistic revision.",
"repository": "govoplan-idm", "repository": "govoplan-idm"
"tracking_issue": "https://git.add-ideas.de/GovOPlaN/govoplan-idm/issues/11"
}, },
{ {
"category": "missing_ui", "category": "ui_reachable",
"method": "POST", "method": "POST",
"path": "/relationships/{}/revoke", "path": "/relationships/{}/revoke",
"rationale": "IDM documents typed relationships but its current administration page exposes function assignments only.", "rationale": "The IDM relationship grid exposes a separated destructive action with mandatory reason and confirmation.",
"repository": "govoplan-idm", "repository": "govoplan-idm"
"tracking_issue": "https://git.add-ideas.de/GovOPlaN/govoplan-idm/issues/11"
}, },
{ {
"category": "missing_ui", "category": "ui_reachable",
"method": "GET", "method": "GET",
"path": "/typed-groups", "path": "/typed-groups",
"rationale": "IDM documents typed groups but its current administration page exposes function assignments only.", "rationale": "The IDM typed-group administration grid lists active and optionally inactive groups.",
"repository": "govoplan-idm", "repository": "govoplan-idm"
"tracking_issue": "https://git.add-ideas.de/GovOPlaN/govoplan-idm/issues/11"
}, },
{ {
"category": "missing_ui", "category": "ui_reachable",
"method": "POST", "method": "POST",
"path": "/typed-groups", "path": "/typed-groups",
"rationale": "IDM documents typed groups but its current administration page exposes function assignments only.", "rationale": "Authorized IDM administrators create typed groups through the group editor.",
"repository": "govoplan-idm", "repository": "govoplan-idm"
"tracking_issue": "https://git.add-ideas.de/GovOPlaN/govoplan-idm/issues/11"
}, },
{ {
"category": "missing_ui", "category": "ui_reachable",
"method": "PATCH", "method": "PATCH",
"path": "/typed-groups/{}", "path": "/typed-groups/{}",
"rationale": "IDM documents typed groups but its current administration page exposes function assignments only.", "rationale": "Authorized IDM administrators edit group metadata, lifecycle state, and provenance with optimistic revision checks.",
"repository": "govoplan-idm", "repository": "govoplan-idm"
"tracking_issue": "https://git.add-ideas.de/GovOPlaN/govoplan-idm/issues/11"
}, },
{ {
"category": "missing_ui", "category": "ui_reachable",
"method": "GET", "method": "GET",
"path": "/typed-groups/{}/memberships", "path": "/typed-groups/{}/memberships",
"rationale": "IDM lacks the typed-group membership explanation surface for this existing API.", "rationale": "The IDM membership inspector resolves a group at a selected time and shows included and excluded decisions.",
"repository": "govoplan-idm", "repository": "govoplan-idm"
"tracking_issue": "https://git.add-ideas.de/GovOPlaN/govoplan-idm/issues/11"
}, },
{ {
"category": "intentionally_headless", "category": "intentionally_headless",
+127 -4
View File
@@ -56,11 +56,35 @@ const labelAttributes = new Set([
"title" "title"
]); ]);
const helpAttributes = new Set([ const helpAttributes = new Set([
"data-help-context-id",
"description", "description",
"help", "help",
"helpContextId",
"helperText", "helperText",
"helpText" "helpText"
]); ]);
const exactHelpAttributes = new Set([
"data-help-context-id",
"helpContextId"
]);
const helpRiskAttributes = new Set([
"data-help-risk",
"helpRisk"
]);
const reviewedHelpRiskAttributes = new Set([
"data-help-risk-reviewed",
"helpRiskReviewed"
]);
const supportedHelpRisks = new Set([
"authority",
"credential",
"disclosure",
"encryption",
"external-effect",
"irreversible",
"policy",
"retention"
]);
const actionComponentPattern = /(?:Action|Button|Link)$/; const actionComponentPattern = /(?:Action|Button|Link)$/;
const contributionTypes = new Map([ const contributionTypes = new Map([
["AdminSectionsUiCapability", "admin_section"], ["AdminSectionsUiCapability", "admin_section"],
@@ -198,20 +222,43 @@ function inspectSource(repository, sourceRoot, sourcePath) {
const parentAttributes = parentFormField const parentAttributes = parentFormField
? jsxAttributes(parentFormField) ? jsxAttributes(parentFormField)
: new Map(); : new Map();
const scopedAncestorAttributes = nearestScopedHelpAttributes(node);
const label = const label =
attributes.get("label") ?? attributes.get("label") ??
attributes.get("aria-label") ?? attributes.get("aria-label") ??
parentAttributes.get("label") ?? parentAttributes.get("label") ??
null; null;
const help = firstAttribute(attributes, helpAttributes) ?? const help = firstAttribute(attributes, helpAttributes) ??
firstAttribute(parentAttributes, helpAttributes); firstAttribute(parentAttributes, helpAttributes) ??
firstAttribute(scopedAncestorAttributes, helpAttributes);
const hasHelp = hasAnyAttribute(attributes, helpAttributes) || const hasHelp = hasAnyAttribute(attributes, helpAttributes) ||
hasAnyAttribute(parentAttributes, helpAttributes); hasAnyAttribute(parentAttributes, helpAttributes) ||
hasAnyAttribute(scopedAncestorAttributes, helpAttributes);
const hasExactHelp = hasAnyAttribute(attributes, exactHelpAttributes) ||
hasAnyAttribute(parentAttributes, exactHelpAttributes) ||
hasAnyAttribute(scopedAncestorAttributes, exactHelpAttributes);
const helpContextId = firstAttribute(attributes, exactHelpAttributes) ??
firstAttribute(parentAttributes, exactHelpAttributes) ??
firstAttribute(scopedAncestorAttributes, exactHelpAttributes);
const explicitId = firstAttribute( const explicitId = firstAttribute(
attributes, attributes,
new Set(["interfaceId", "data-interface-id", "id", "name", "field"]) new Set(["interfaceId", "data-interface-id", "id", "name", "field"])
); );
const context = nearestNamedContext(node); const context = nearestNamedContext(node);
const risk = helpRiskFor({
component,
context,
file: relativeFile,
label,
explicitId,
name: attributes.get("name") ?? attributes.get("id") ?? attributes.get("field") ?? null,
explicitRisk: firstAttribute(attributes, helpRiskAttributes) ??
firstAttribute(parentAttributes, helpRiskAttributes) ??
firstAttribute(scopedAncestorAttributes, helpRiskAttributes)
});
const riskReviewed = firstAttribute(attributes, reviewedHelpRiskAttributes) ??
firstAttribute(parentAttributes, reviewedHelpRiskAttributes) ??
firstAttribute(scopedAncestorAttributes, reviewedHelpRiskAttributes);
const stableId = sourceIdentity( const stableId = sourceIdentity(
"field", "field",
node, node,
@@ -235,7 +282,14 @@ function inspectSource(repository, sourceRoot, sourcePath) {
help: help ?? null, help: help ?? null,
helpId: hasHelp ? `${stableId}.help` : null, helpId: hasHelp ? `${stableId}.help` : null,
helpDynamic: hasHelp && help === null, helpDynamic: hasHelp && help === null,
helpCandidate: !hasHelp helpCandidate: !hasHelp,
helpExact: hasExactHelp,
helpContextId,
helpContextDynamic: hasExactHelp && helpContextId === null,
helpRisk: risk.value,
helpRiskSource: risk.source,
helpRiskReviewed: riskReviewed,
highRiskHelpMissing: risk.value !== null && !hasExactHelp && riskReviewed !== "standard"
}); });
} }
@@ -259,6 +313,19 @@ function inspectSource(repository, sourceRoot, sourcePath) {
new Set(["interfaceId", "data-interface-id", "id", "name"]) new Set(["interfaceId", "data-interface-id", "id", "name"])
); );
const context = nearestNamedContext(node); const context = nearestNamedContext(node);
const hasHelp = hasAnyAttribute(attributes, helpAttributes);
const hasExactHelp = hasAnyAttribute(attributes, exactHelpAttributes);
const helpContextId = firstAttribute(attributes, exactHelpAttributes);
const risk = helpRiskFor({
component,
context,
file: relativeFile,
label,
explicitId,
name: attributes.get("name") ?? attributes.get("id") ?? null,
explicitRisk: firstAttribute(attributes, helpRiskAttributes)
});
const riskReviewed = firstAttribute(attributes, reviewedHelpRiskAttributes);
result.actions.push({ result.actions.push({
...locate(node), ...locate(node),
id: sourceIdentity( id: sourceIdentity(
@@ -271,7 +338,15 @@ function inspectSource(repository, sourceRoot, sourcePath) {
idSource: explicitId === null ? "source_anchor" : "explicit", idSource: explicitId === null ? "source_anchor" : "explicit",
context, context,
component, component,
label label,
helpExact: hasExactHelp,
helpContextId,
helpContextDynamic: hasExactHelp && helpContextId === null,
helpDynamic: hasHelp && firstAttribute(attributes, helpAttributes) === null,
helpRisk: risk.value,
helpRiskSource: risk.source,
helpRiskReviewed: riskReviewed,
highRiskHelpMissing: risk.value !== null && !hasExactHelp && riskReviewed !== "standard"
}); });
} }
@@ -352,6 +427,26 @@ function inspectSource(repository, sourceRoot, sourcePath) {
return null; return null;
} }
function nearestScopedHelpAttributes(node) {
let current = node.parent;
while (current) {
if (ts.isJsxElement(current)) {
const attributes = jsxAttributes(current.openingElement);
if (attributes.get("data-help-scope") === "field") return attributes;
}
if (
ts.isFunctionDeclaration(current) ||
ts.isMethodDeclaration(current) ||
ts.isArrowFunction(current) ||
ts.isFunctionExpression(current)
) {
return new Map();
}
current = current.parent;
}
return new Map();
}
function jsxAttributes(node) { function jsxAttributes(node) {
const mapped = new Map(); const mapped = new Map();
for (const attribute of node.attributes.properties) { for (const attribute of node.attributes.properties) {
@@ -577,6 +672,34 @@ function hasAnyAttribute(attributes, names) {
return false; return false;
} }
function helpRiskFor({ component, context, file, label, explicitId, name, explicitRisk }) {
if (typeof explicitRisk === "string") {
return supportedHelpRisks.has(explicitRisk)
? { value: explicitRisk, source: "explicit" }
: { value: null, source: "invalid_explicit" };
}
const value = [component, context, file, label, explicitId, name]
.filter((item) => typeof item === "string")
.join(" ")
.toLowerCase()
.replace(/^i18n:/g, "")
.replace(/[._-]+/g, " ");
const patterns = [
["irreversible", /\b(delete|destroy|erase|purge|dispose|disposition|revoke|withdraw|shred)\b/],
["credential", /\b(credential|password|secret|token|api key|private key)\b/],
["retention", /\b(retention|legal hold|archive lifecycle)\b/],
["encryption", /\b(encrypt|encryption|decrypt|decryption|signing key|signature key)\b/],
["disclosure", /\b(disclose|disclosure|publish|share externally|public export)\b/],
["external-effect", /\b(send|deliver|transfer|refund|payment execution|webhook execution)\b/],
["authority", /\b(grant permission|role assignment|approve|reject|formal decision|mandate)\b/],
["policy", /\b(policy apply|policy override|enforcement mode)\b/]
];
for (const [risk, pattern] of patterns) {
if (pattern.test(value)) return { value: risk, source: "inferred" };
}
return { value: null, source: null };
}
function slug(value) { function slug(value) {
const normalized = value const normalized = value
.toLowerCase() .toLowerCase()
@@ -0,0 +1,5 @@
{
"schema_version": 1,
"maximum_missing_exact_help": 0,
"rationale": "The source-derived high-risk queue for Core issue #284 is fully resolved. Strict declarations reject any new high-risk control without an exact, manifest-declared, German-complete F1 context."
}
+186 -2
View File
@@ -31,6 +31,9 @@ ENDPOINT_SURFACE_CATEGORIES = {
DEFAULT_ENDPOINT_DECLARATIONS = ( DEFAULT_ENDPOINT_DECLARATIONS = (
META_ROOT / "tools" / "inventory" / "endpoint-surface-declarations.json" META_ROOT / "tools" / "inventory" / "endpoint-surface-declarations.json"
) )
DEFAULT_HIGH_RISK_HELP_BASELINE = (
META_ROOT / "tools" / "inventory" / "high-risk-help-baseline.json"
)
REQUIRED_LOCALES = ("de", "en") REQUIRED_LOCALES = ("de", "en")
REFERENCE_LOCALE = "de" REFERENCE_LOCALE = "de"
@@ -75,6 +78,12 @@ def main() -> int:
default=DEFAULT_ENDPOINT_DECLARATIONS, default=DEFAULT_ENDPOINT_DECLARATIONS,
help="Versioned endpoint-surface declaration registry.", help="Versioned endpoint-surface declaration registry.",
) )
parser.add_argument(
"--high-risk-help-baseline",
type=Path,
default=DEFAULT_HIGH_RISK_HELP_BASELINE,
help="Versioned upper bound for high-risk controls without exact F1 help.",
)
args = parser.parse_args() args = parser.parse_args()
catalog = json.loads((META_ROOT / "repositories.json").read_text(encoding="utf-8")) catalog = json.loads((META_ROOT / "repositories.json").read_text(encoding="utf-8"))
@@ -85,11 +94,15 @@ def main() -> int:
endpoint_declarations = _load_endpoint_declarations( endpoint_declarations = _load_endpoint_declarations(
args.endpoint_declarations.resolve() args.endpoint_declarations.resolve()
) )
high_risk_help_baseline = _load_high_risk_help_baseline(
args.high_risk_help_baseline.resolve()
)
inventory = _assemble_inventory( inventory = _assemble_inventory(
webui=webui, webui=webui,
backend_endpoints=backend_endpoints, backend_endpoints=backend_endpoints,
manifests=manifests, manifests=manifests,
endpoint_declarations=endpoint_declarations, endpoint_declarations=endpoint_declarations,
high_risk_help_baseline=high_risk_help_baseline,
runtime_snapshot=( runtime_snapshot=(
_load_runtime_snapshot(args.runtime_snapshot.resolve()) _load_runtime_snapshot(args.runtime_snapshot.resolve())
if args.runtime_snapshot is not None if args.runtime_snapshot is not None
@@ -164,6 +177,28 @@ def _strict_failures(
f"{len(declaration_health['stale_runtime_routes'])} runtime route " f"{len(declaration_health['stale_runtime_routes'])} runtime route "
"declarations have no WebUI implementation" "declarations have no WebUI implementation"
) )
help_health = inventory.get("help_health", {})
if check_declarations and help_health.get("invalid_risk_annotations"):
failures.append(
f"{len(help_health['invalid_risk_annotations'])} controls use an "
"unsupported contextual-help risk class"
)
if check_declarations and help_health.get("baseline_regression"):
failures.append(
f"{len(help_health['missing_exact_high_risk_help'])} high-risk "
"controls lack exact F1 help; baseline permits at most "
f"{help_health['baseline_maximum_missing']}"
)
if check_declarations and help_health.get("unresolved_exact_high_risk_help"):
failures.append(
f"{len(help_health['unresolved_exact_high_risk_help'])} high-risk "
"controls reference no manifest DocumentationTopic help context"
)
if check_declarations and help_health.get("high_risk_help_without_german"):
failures.append(
f"{len(help_health['high_risk_help_without_german'])} high-risk "
"controls resolve to documentation without complete German content"
)
runtime_comparison = inventory.get("runtime_comparison") runtime_comparison = inventory.get("runtime_comparison")
if ( if (
check_declarations check_declarations
@@ -355,6 +390,29 @@ def _extract_manifests(
} }
for permission in manifest.permissions for permission in manifest.permissions
], ],
"documentation": [
{
"id": topic.id,
"help_contexts": sorted(
{
str(context)
for context in topic.metadata.get(
"help_contexts", ()
)
if isinstance(context, str) and context.strip()
}
),
"german_complete": (
isinstance(topic.translations.get("de"), dict)
and all(
isinstance(topic.translations["de"].get(field), str)
and topic.translations["de"][field].strip()
for field in ("title", "summary", "body")
)
),
}
for topic in manifest.documentation
],
"architecture": ( "architecture": (
manifest.architecture.to_dict() manifest.architecture.to_dict()
if manifest.architecture is not None if manifest.architecture is not None
@@ -400,6 +458,7 @@ def _assemble_inventory(
backend_endpoints: list[dict[str, Any]], backend_endpoints: list[dict[str, Any]],
manifests: list[dict[str, Any]], manifests: list[dict[str, Any]],
endpoint_declarations: dict[tuple[str, str, str], dict[str, Any]], endpoint_declarations: dict[tuple[str, str, str], dict[str, Any]],
high_risk_help_baseline: dict[str, Any] | None = None,
runtime_snapshot: dict[str, Any] | None = None, runtime_snapshot: dict[str, Any] | None = None,
) -> dict[str, Any]: ) -> dict[str, Any]:
frontend_refs = webui["frontendApiReferences"] frontend_refs = webui["frontendApiReferences"]
@@ -471,8 +530,47 @@ def _assemble_inventory(
if any(key not in catalog_keys.get(locale, set()) for locale in expected_locales) if any(key not in catalog_keys.get(locale, set()) for locale in expected_locales)
] ]
fields = webui["fields"] fields = webui["fields"]
actions = webui.get("actions", [])
help_candidates = [field for field in fields if field["helpCandidate"]] help_candidates = [field for field in fields if field["helpCandidate"]]
dynamic_help = [field for field in fields if field.get("helpDynamic")] dynamic_help = [field for field in fields if field.get("helpDynamic")]
controls = [*fields, *actions]
high_risk_controls = [item for item in controls if item.get("helpRisk")]
missing_exact_high_risk_help = [
item for item in controls if item.get("highRiskHelpMissing")
]
invalid_risk_annotations = [
item
for item in controls
if item.get("helpRiskSource") == "invalid_explicit"
]
documentation_contexts = {
context: {
"module_id": manifest["id"],
"topic_id": topic["id"],
"german_complete": topic["german_complete"],
}
for manifest in manifests
for topic in manifest.get("documentation", [])
for context in topic.get("help_contexts", [])
}
unresolved_exact_high_risk_help = [
item
for item in high_risk_controls
if item.get("helpExact")
and not item.get("helpContextDynamic")
and item.get("helpContextId") not in documentation_contexts
]
high_risk_help_without_german = [
item
for item in high_risk_controls
if item.get("helpContextId") in documentation_contexts
and not documentation_contexts[item["helpContextId"]]["german_complete"]
]
baseline_maximum_missing = (
high_risk_help_baseline["maximum_missing_exact_help"]
if high_risk_help_baseline is not None
else None
)
governance_adoption = Counter( governance_adoption = Counter(
dimension["adoption"] dimension["adoption"]
for manifest in manifests for manifest in manifests
@@ -499,10 +597,34 @@ def _assemble_inventory(
"modules": manifests, "modules": manifests,
"interface_declarations": source_declarations, "interface_declarations": source_declarations,
"declaration_health": declaration_health, "declaration_health": declaration_health,
"help_health": {
"supported_risk_classes": sorted(
{
str(item["helpRisk"])
for item in high_risk_controls
if item.get("helpRisk")
}
),
"high_risk_controls": high_risk_controls,
"missing_exact_high_risk_help": missing_exact_high_risk_help,
"invalid_risk_annotations": invalid_risk_annotations,
"unresolved_exact_high_risk_help": unresolved_exact_high_risk_help,
"high_risk_help_without_german": high_risk_help_without_german,
"dynamic_owner_context_controls": [
item
for item in high_risk_controls
if item.get("helpContextDynamic")
],
"baseline_maximum_missing": baseline_maximum_missing,
"baseline_regression": (
baseline_maximum_missing is not None
and len(missing_exact_high_risk_help) > baseline_maximum_missing
),
},
"runtime_comparison": runtime_comparison, "runtime_comparison": runtime_comparison,
"ui": { "ui": {
"fields": fields, "fields": fields,
"actions": webui.get("actions", []), "actions": actions,
"labels": webui["labels"], "labels": webui["labels"],
"visible_text": webui["visibleText"], "visible_text": webui["visibleText"],
"routes": webui["routes"], "routes": webui["routes"],
@@ -554,7 +676,19 @@ def _assemble_inventory(
"ui_fields_with_resolvable_f1_context": len(fields), "ui_fields_with_resolvable_f1_context": len(fields),
"help_review_candidates": len(help_candidates), "help_review_candidates": len(help_candidates),
"dynamic_help_references": len(dynamic_help), "dynamic_help_references": len(dynamic_help),
"ui_actions": len(webui.get("actions", [])), "ui_actions": len(actions),
"high_risk_controls": len(high_risk_controls),
"high_risk_controls_with_exact_help": (
len(high_risk_controls) - len(missing_exact_high_risk_help)
),
"high_risk_controls_missing_exact_help": len(
missing_exact_high_risk_help
),
"invalid_help_risk_annotations": len(invalid_risk_annotations),
"unresolved_exact_high_risk_help": len(
unresolved_exact_high_risk_help
),
"high_risk_help_without_german": len(high_risk_help_without_german),
"interface_declarations": len(source_declarations), "interface_declarations": len(source_declarations),
"duplicate_interface_ids": len(declaration_health["duplicate_ids"]), "duplicate_interface_ids": len(declaration_health["duplicate_ids"]),
"undeclared_source_surfaces": len( "undeclared_source_surfaces": len(
@@ -885,6 +1019,10 @@ def _render_markdown(inventory: dict[str, Any]) -> str:
for item in inventory["api"]["unreferenced_by_static_webui_scan"] for item in inventory["api"]["unreferenced_by_static_webui_scan"]
) )
classification_counts = inventory["api"]["classification_counts"] classification_counts = inventory["api"]["classification_counts"]
high_risk_by_repository = Counter(
item["repository"]
for item in inventory["help_health"]["missing_exact_high_risk_help"]
)
lines = [ lines = [
"# GovOPlaN Platform Interface Inventory", "# GovOPlaN Platform Interface Inventory",
"", "",
@@ -900,6 +1038,12 @@ def _render_markdown(inventory: dict[str, Any]) -> str:
f"- Fields with a resolvable F1 context: {summary['ui_fields_with_resolvable_f1_context']}", f"- Fields with a resolvable F1 context: {summary['ui_fields_with_resolvable_f1_context']}",
f"- Fields with dynamic help references: {summary['dynamic_help_references']}", f"- Fields with dynamic help references: {summary['dynamic_help_references']}",
f"- Help review candidates: {summary['help_review_candidates']}", f"- Help review candidates: {summary['help_review_candidates']}",
f"- High-risk controls: {summary['high_risk_controls']}",
f"- High-risk controls with exact F1 help: {summary['high_risk_controls_with_exact_help']}",
f"- High-risk controls missing exact F1 help: {summary['high_risk_controls_missing_exact_help']}",
f"- Invalid help-risk annotations: {summary['invalid_help_risk_annotations']}",
f"- High-risk exact contexts missing a manifest topic: {summary['unresolved_exact_high_risk_help']}",
f"- High-risk contexts without complete German topic content: {summary['high_risk_help_without_german']}",
f"- Stable interface declarations: {summary['interface_declarations']}", f"- Stable interface declarations: {summary['interface_declarations']}",
f"- Duplicate interface IDs: {summary['duplicate_interface_ids']}", f"- Duplicate interface IDs: {summary['duplicate_interface_ids']}",
f"- WebUI surfaces missing runtime declarations: {summary['undeclared_source_surfaces']}", f"- WebUI surfaces missing runtime declarations: {summary['undeclared_source_surfaces']}",
@@ -930,6 +1074,23 @@ def _render_markdown(inventory: dict[str, Any]) -> str:
f"| `{repository}` | {count} |" f"| `{repository}` | {count} |"
for repository, count in sorted(help_by_repository.items()) for repository, count in sorted(help_by_repository.items())
) )
lines.extend(
[
"",
"## High-risk Contextual-help Debt",
"",
"Inferred or explicitly classified high-risk controls require an exact",
"F1 context. `data-help-risk-reviewed=\"standard\"` records a reviewed",
"false positive. The versioned baseline makes this queue non-regressing.",
"",
"| Repository | Missing exact contexts |",
"| --- | ---: |",
]
)
lines.extend(
f"| `{repository}` | {count} |"
for repository, count in sorted(high_risk_by_repository.items())
)
lines.extend( lines.extend(
[ [
"", "",
@@ -1005,6 +1166,29 @@ def endpoint_key(endpoint: dict[str, Any]) -> tuple[str, str, str]:
) )
def _load_high_risk_help_baseline(path: Path) -> dict[str, Any]:
try:
payload = json.loads(path.read_text(encoding="utf-8"))
except FileNotFoundError as exc:
raise ValueError(
f"High-risk contextual-help baseline does not exist: {path}"
) from exc
except json.JSONDecodeError as exc:
raise ValueError(
f"High-risk contextual-help baseline is invalid JSON: {exc}"
) from exc
if not isinstance(payload, dict) or payload.get("schema_version") != 1:
raise ValueError(
"High-risk contextual-help baseline must use schema_version 1."
)
maximum = payload.get("maximum_missing_exact_help")
if not isinstance(maximum, int) or isinstance(maximum, bool) or maximum < 0:
raise ValueError(
"High-risk contextual-help baseline maximum must be a non-negative integer."
)
return payload
def _load_endpoint_declarations( def _load_endpoint_declarations(
path: Path, path: Path,
) -> dict[tuple[str, str, str], dict[str, Any]]: ) -> dict[tuple[str, str, str], dict[str, Any]]:
@@ -172,6 +172,23 @@ def manifest_catalog_entry(
entry["dependencies"] = list(manifest.dependencies) entry["dependencies"] = list(manifest.dependencies)
if manifest.optional_dependencies: if manifest.optional_dependencies:
entry["optional_dependencies"] = list(manifest.optional_dependencies) entry["optional_dependencies"] = list(manifest.optional_dependencies)
if manifest.permissions:
entry["permissions"] = [
{
"scope": permission.scope,
"label": permission.label,
"description": permission.description,
"category": permission.category,
"level": permission.level,
"resource": permission.resource,
"action": permission.action,
"deprecated": permission.deprecated,
}
for permission in sorted(
manifest.permissions,
key=lambda item: item.scope,
)
]
if manifest.architecture is not None: if manifest.architecture is not None:
entry["architecture"] = manifest.architecture.to_dict() entry["architecture"] = manifest.architecture.to_dict()
entry["information_governance"] = manifest.information_governance.to_dict() entry["information_governance"] = manifest.information_governance.to_dict()
+4
View File
@@ -571,6 +571,10 @@ run_manifest_shape_gate() {
"$META_ROOT/tools/checks/check-manifest-shapes.py" \ "$META_ROOT/tools/checks/check-manifest-shapes.py" \
--workspace-root "$PARENT" \ --workspace-root "$PARENT" \
--require-architecture --require-architecture
run env PYTHONDONTWRITEBYTECODE=1 \
"$PYTHON" \
"$META_ROOT/tools/checks/check-dsar-coverage.py" \
--workspace-root "$PARENT"
} }
run_migration_release_audit() { run_migration_release_audit() {