102 Commits
Author SHA1 Message Date
zemion 6edaaadf37 release: align governed tenant erasure
Dependency Audit / dependency-audit (push) Successful in 1m44s
Deployment Installer / deployment-installer (push) Successful in 6s
Security Audit / security-audit (push) Failing after 12m4s
Developer Meta-package Release / publish-package (push) Successful in 10s
2026-08-24 16:33:16 +02:00
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
zemion 78811f7f6e Synchronize public module directory publication
Dependency Audit / dependency-audit (push) Successful in 1m50s
Deployment Installer / deployment-installer (push) Successful in 6s
Security Audit / security-audit (push) Successful in 11m58s
2026-08-06 22:42:25 +02:00
zemion 09046e6e62 Publish complete signed module catalogs
Dependency Audit / dependency-audit (push) Successful in 1m46s
Deployment Installer / deployment-installer (push) Successful in 5s
Security Audit / security-audit (push) Successful in 11m49s
2026-08-06 21:13:33 +02:00
zemion f3cfd1bccc Integrate Quick Access product presentation
Dependency Audit / dependency-audit (push) Successful in 1m50s
Deployment Installer / deployment-installer (push) Successful in 6s
Security Audit / security-audit (push) Successful in 11m40s
2026-08-06 19:02:56 +02:00
zemion 241db623c7 Refresh live strategy issue counts
Dependency Audit / dependency-audit (push) Successful in 1m54s
Deployment Installer / deployment-installer (push) Successful in 6s
Security Audit / security-audit (push) Successful in 12m9s
2026-08-06 16:40:46 +02:00
zemion b269791c48 Reconcile product inputs and prove durable work handoffs
Dependency Audit / dependency-audit (push) Successful in 1m48s
Deployment Installer / deployment-installer (push) Successful in 6s
Security Audit / security-audit (push) Successful in 12m2s
2026-08-06 16:06:18 +02:00
zemion 69ba1037bf Prove reference journey compositions
Dependency Audit / dependency-audit (push) Successful in 1m45s
Deployment Installer / deployment-installer (push) Successful in 6s
Security Audit / security-audit (push) Successful in 11m16s
2026-08-06 12:42:20 +02:00
zemion 8bdf7b5f7e docs(records): record lifecycle and endpoint coverage
Dependency Audit / dependency-audit (push) Successful in 1m38s
Deployment Installer / deployment-installer (push) Successful in 6s
Security Audit / security-audit (push) Successful in 10m32s
2026-08-06 05:36:29 +02:00
zemion d6fdd7ddf5 chore(inventory): track remaining records administration surfaces
Dependency Audit / dependency-audit (push) Successful in 1m41s
Deployment Installer / deployment-installer (push) Successful in 6s
Security Audit / security-audit (push) Successful in 10m47s
2026-08-06 01:43:37 +02:00
zemion 7b0ab31adf Document GovOPlaN deployment profiles
Dependency Audit / dependency-audit (push) Successful in 1m45s
Deployment Installer / deployment-installer (push) Successful in 7s
Security Audit / security-audit (push) Successful in 11m33s
2026-08-05 22:42:27 +02:00
zemion 389df7c3d5 Drain API pods before Kubernetes shutdown
Dependency Audit / dependency-audit (push) Successful in 1m51s
Deployment Installer / deployment-installer (push) Successful in 6s
Security Audit / security-audit (push) Successful in 11m37s
2026-08-05 21:55:32 +02:00
zemion 3b9ae901dd Harden Kubernetes lab CA profile [skip ci] 2026-08-05 21:17:32 +02:00
zemion bf2f02891f Release v0.1.18
Dependency Audit / dependency-audit (push) Successful in 1m48s
Deployment Installer / deployment-installer (push) Successful in 6s
Security Audit / security-audit (push) Successful in 11m31s
Developer Meta-package Release / publish-package (push) Successful in 10s
2026-08-05 21:07:52 +02:00
zemion 7ec0bbb826 Release v0.1.17
Dependency Audit / dependency-audit (push) Failing after 1m50s
Deployment Installer / deployment-installer (push) Successful in 6s
Security Audit / security-audit (push) Successful in 11m34s
Developer Meta-package Release / publish-package (push) Successful in 10s
2026-08-05 20:34:17 +02:00
zemion 3ca068f76a Release v0.1.16
Deployment Installer / deployment-installer (push) Successful in 6s
Security Audit / security-audit (push) Successful in 11m18s
Dependency Audit / dependency-audit (push) Failing after 1m47s
Developer Meta-package Release / publish-package (push) Successful in 11s
2026-08-05 19:52:32 +02:00
zemion 61463a24cb Bind runtime releases to protected source tags [skip ci] 2026-08-04 16:43:54 +02:00
zemion 8262215fcd Keep runtime builder path stable [skip ci] 2026-08-04 16:34:07 +02:00
zemion 8aba74e01e Make meta-package release retries tag-safe [skip ci] 2026-08-04 16:27:47 +02:00
zemion a24c94435e Release v0.1.15
Dependency Audit / dependency-audit (push) Failing after 1m49s
Deployment Installer / deployment-installer (push) Successful in 6s
Security Audit / security-audit (push) Successful in 10m50s
Developer Meta-package Release / publish-package (push) Failing after 4s
2026-08-04 15:20:50 +02:00
zemion 774793976c Support legacy module version declarations
Deployment Installer / deployment-installer (push) Successful in 6s
Dependency Audit / dependency-audit (push) Failing after 1m42s
Security Audit / security-audit (push) Successful in 10m43s
2026-08-04 15:08:46 +02:00
zemion 492449a4e2 Align all release version declarations
Dependency Audit / dependency-audit (push) Failing after 1m44s
Deployment Installer / deployment-installer (push) Successful in 6s
Security Audit / security-audit (push) Failing after 10m42s
2026-08-04 15:06:36 +02:00
zemion 8e890b37ed Validate candidate migration baseline during release
Dependency Audit / dependency-audit (push) Failing after 1m42s
Deployment Installer / deployment-installer (push) Successful in 6s
Security Audit / security-audit (push) Successful in 10m38s
2026-08-04 15:01:50 +02:00
zemion 077735bc24 Bind migration heads to coordinated releases
Deployment Installer / deployment-installer (push) Successful in 6s
Dependency Audit / dependency-audit (push) Failing after 1m44s
Security Audit / security-audit (push) Successful in 10m36s
2026-08-04 14:55:58 +02:00
zemion d9522d3cc4 Publish release tags in dependency order
Dependency Audit / dependency-audit (push) Failing after 1m44s
Deployment Installer / deployment-installer (push) Successful in 6s
Security Audit / security-audit (push) Successful in 10m53s
2026-08-04 14:54:54 +02:00
zemion bad0ea37a7 Automate exact package-set publication
Dependency Audit / dependency-audit (push) Failing after 1m46s
Deployment Installer / deployment-installer (push) Successful in 6s
Security Audit / security-audit (push) Successful in 10m46s
2026-08-04 14:32:06 +02:00
zemion 9ffd46fe22 Make package publication retries hash-safe
Dependency Audit / dependency-audit (push) Failing after 1m48s
Deployment Installer / deployment-installer (push) Successful in 6s
Security Audit / security-audit (push) Successful in 10m35s
2026-08-04 14:18:49 +02:00
zemion be51a9c347 Document production target evidence handoff
Dependency Audit / dependency-audit (push) Failing after 1m46s
Deployment Installer / deployment-installer (push) Successful in 6s
Security Audit / security-audit (push) Successful in 10m40s
2026-08-04 14:00:45 +02:00
zemion e36a6573bf Harden package release workflows for Gitea 2026-08-04 14:00:32 +02:00
zemion 629bfec1f1 Cover datasource publication change events
Dependency Audit / dependency-audit (push) Failing after 1m40s
Deployment Installer / deployment-installer (push) Successful in 5s
Security Audit / security-audit (push) Successful in 10m48s
2026-08-04 12:05:48 +02:00
zemion 9e956eec6f Classify first-run bootstrap endpoints
Dependency Audit / dependency-audit (push) Failing after 1m44s
Deployment Installer / deployment-installer (push) Successful in 6s
Security Audit / security-audit (push) Successful in 10m52s
2026-08-04 10:07:24 +02:00
zemion 9bb2c808a6 Enforce declared platform interface inventory
Dependency Audit / dependency-audit (push) Failing after 1m36s
Deployment Installer / deployment-installer (push) Successful in 6s
Security Audit / security-audit (push) Successful in 10m2s
2026-08-04 05:20:47 +02:00
zemion f7590a7b8b Add registry-backed module package releases
Dependency Audit / dependency-audit (push) Failing after 1m37s
Deployment Installer / deployment-installer (push) Successful in 6s
Security Audit / security-audit (push) Successful in 10m21s
2026-08-04 04:14:28 +02:00
zemion 1a68565ba0 Reconcile encryption interface surfaces
Dependency Audit / dependency-audit (push) Failing after 1m40s
Deployment Installer / deployment-installer (push) Successful in 6s
Security Audit / security-audit (push) Successful in 10m33s
2026-08-04 01:28:03 +02:00
zemion d9f67b5c26 Reconcile implemented platform interface surfaces
Dependency Audit / dependency-audit (push) Failing after 1m39s
Deployment Installer / deployment-installer (push) Successful in 5s
Security Audit / security-audit (push) Successful in 10m32s
2026-08-04 01:04:40 +02:00
zemion 1cfdaec250 inventory: mark campaign archive UI reachable
Dependency Audit / dependency-audit (push) Successful in 1m44s
Deployment Installer / deployment-installer (push) Successful in 6s
Security Audit / security-audit (push) Successful in 10m54s
2026-08-03 20:39:16 +02:00
zemion 62501d399a ci: enforce endpoint inventory independently
Dependency Audit / dependency-audit (push) Successful in 1m46s
Deployment Installer / deployment-installer (push) Successful in 6s
Security Audit / security-audit (push) Successful in 10m43s
2026-08-03 20:29:14 +02:00
zemion ce5528e3b8 Record first verified runtime distribution 2026-08-03 20:17:29 +02:00
152 changed files with 19076 additions and 1076 deletions
+4 -2
View File
@@ -15,12 +15,14 @@ GOVOPLAN_DB_MAX_OVERFLOW=10
GOVOPLAN_DB_POOL_TIMEOUT_SECONDS=30
GOVOPLAN_DB_POOL_RECYCLE_SECONDS=1800
ENABLED_MODULES=tenancy,organizations,identity,idm,access,admin,dashboard,policy,audit,files,templates,mail,campaigns,calendar,poll,scheduling,connectors,datasources,dataflow,dist_lists,workflow_engine,workflow,views,search,risk_compliance,postbox,notifications,services,parties,mandates,decisions,portal,cases,committee,docs,ops
ENABLED_MODULES=tenancy,organizations,identity,idm,access,admin,dashboard,policy,audit,files,templates,mail,campaigns,calendar,poll,scheduling,connectors,datasources,dataflow,dist_lists,workflow_engine,workflow,tasks,views,quick_access,search,risk_compliance,postbox,notifications,services,parties,mandates,decisions,portal,cases,committee,docs,ops
CELERY_ENABLED=true
REDIS_URL=redis://127.0.0.1:6379/0
CELERY_QUEUES=send_email,append_sent,notifications,calendar,dataflow,events,default
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_MAX_STRUCTURED_RESPONSE_BYTES=16777216
@@ -57,4 +59,4 @@ DEV_MAILBOX_API_ENABLED=false
GOVOPLAN_MODULE_PACKAGE_CATALOG_URL=https://govoplan.add-ideas.de/catalogs/v1/channels/stable.json
GOVOPLAN_MODULE_PACKAGE_CATALOG_TRUSTED_KEYS_FILE=/etc/govoplan/catalog-keyring.json
GOVOPLAN_MODULE_PACKAGE_CATALOG_APPROVED_CHANNEL=stable
GOVOPLAN_MODULE_PACKAGE_CATALOG_APPROVED_CHANNELS=stable
+2 -2
View File
@@ -55,9 +55,9 @@ jobs:
- name: Install WebUI release dependencies with test scripts
working-directory: govoplan
run: bash tools/release/install-webui-release-dependencies.sh ../govoplan-core/webui
- name: Validate platform endpoint surface declarations
- name: Validate platform interface and endpoint declarations
working-directory: govoplan
run: .venv/bin/python tools/inventory/platform-interface-inventory.py --strict
run: .venv/bin/python tools/inventory/platform-interface-inventory.py --strict-declarations --strict-endpoints
- name: Validate Search against PostgreSQL
working-directory: govoplan
env:
@@ -0,0 +1,178 @@
name: Developer Meta-package Release
on:
push:
tags:
- "v*"
workflow_dispatch:
inputs:
version:
description: Existing protected release version without leading v
required: true
type: string
jobs:
publish-package:
runs-on: ubuntu-latest
env:
GITEA_REPOSITORY: ${{ gitea.repository }}
TRIGGER_TAG: ${{ gitea.ref_name }}
REQUESTED_VERSION: ${{ inputs.version }}
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
with:
fetch-depth: 0
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065
with:
python-version: "3.12"
- name: Validate protected release tag and package version
run: |
python - <<'PY'
import os
from pathlib import Path
import subprocess
import tomllib
requested_version = os.environ.get("REQUESTED_VERSION", "").strip()
tag = f"v{requested_version}" if requested_version else os.environ["TRIGGER_TAG"]
if not tag.startswith("v") or not tag[1:]:
raise SystemExit("release tag is missing")
project_text = subprocess.check_output(
["git", "show", f"{tag}:packages/govoplan-meta/pyproject.toml"],
text=True,
)
project = tomllib.loads(project_text)["project"]
if tag != f"v{project['version']}":
raise SystemExit("meta-package version does not match the release tag")
tag_commit = subprocess.check_output(
["git", "rev-parse", f"refs/tags/{tag}^{{commit}}"], text=True
).strip()
if subprocess.run(
["git", "merge-base", "--is-ancestor", tag_commit, "origin/main"]
).returncode:
raise SystemExit("release tag is not contained in main")
if not requested_version:
head_commit = subprocess.check_output(
["git", "rev-parse", "HEAD"], text=True
).strip()
if head_commit != tag_commit:
raise SystemExit("tag-triggered checkout does not match the release tag")
with Path(os.environ["GITEA_ENV"]).open("a", encoding="utf-8") as env_file:
env_file.write(f"RELEASE_TAG={tag}\n")
subprocess.run(["git", "checkout", "--detach", tag_commit], check=True)
PY
- name: Build developer package
run: |
set -euo pipefail
python -m pip install --disable-pip-version-check build==1.5.0 twine==7.0.0
python -m build --wheel --outdir dist packages/govoplan-meta
python -m twine check dist/*.whl
python - <<'PY'
import hashlib
import json
import os
from pathlib import Path
import subprocess
wheels = tuple(Path("dist").glob("*.whl"))
if len(wheels) != 1:
raise SystemExit("meta release must contain exactly one wheel")
wheel = wheels[0]
evidence = {
"schema_version": "1",
"repository": os.environ["GITEA_REPOSITORY"],
"tag": os.environ["RELEASE_TAG"],
"commit": subprocess.check_output(
["git", "rev-parse", "HEAD"], text=True
).strip(),
"artifacts": [
{
"filename": wheel.name,
"sha256": hashlib.sha256(wheel.read_bytes()).hexdigest(),
"size": wheel.stat().st_size,
}
],
}
Path("dist/package-artifacts.json").write_text(
json.dumps(evidence, indent=2, sort_keys=True) + "\n", encoding="utf-8"
)
PY
- name: Retain package hash evidence
uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32
with:
name: developer-meta-package
path: dist/package-artifacts.json
- name: Check immutable registry state
env:
PACKAGE_TOKEN: ${{ secrets.GOVOPLAN_PACKAGE_TOKEN }}
run: |
set -euo pipefail
test -n "$PACKAGE_TOKEN"
python - <<'PY'
import hashlib
import json
import os
from pathlib import Path
import tomllib
from urllib.error import HTTPError
from urllib.parse import quote
from urllib.request import Request, urlopen
project = tomllib.loads(
Path("packages/govoplan-meta/pyproject.toml").read_text(encoding="utf-8")
)["project"]
wheels = tuple(Path("dist").glob("*.whl"))
if len(wheels) != 1:
raise SystemExit("meta release must contain exactly one wheel")
wheel = wheels[0]
digest = hashlib.sha256(wheel.read_bytes()).hexdigest()
package_url = "/".join(
(
"https://git.add-ideas.de/api/v1/packages/GovOPlaN",
"pypi",
quote(str(project["name"]), safe=""),
quote(str(project["version"]), safe=""),
"files",
)
)
request = Request(
package_url,
headers={
"Accept": "application/json",
"Authorization": f"token {os.environ['PACKAGE_TOKEN']}",
},
)
publish = True
try:
with urlopen(request, timeout=30) as response:
files = json.load(response)
except HTTPError as exc:
if exc.code != 404:
raise
else:
if not isinstance(files, list) or len(files) != 1:
raise SystemExit("immutable meta-package has an unexpected file set")
if files[0].get("sha256") != digest:
raise SystemExit(
"immutable meta-package already exists with a different SHA-256"
)
publish = False
with Path(os.environ["GITEA_ENV"]).open("a", encoding="utf-8") as env_file:
env_file.write(f"PUBLISH_PYPI={int(publish)}\n")
PY
- name: Publish developer package
env:
PACKAGE_USERNAME: ${{ secrets.GOVOPLAN_PACKAGE_USERNAME }}
PACKAGE_TOKEN: ${{ secrets.GOVOPLAN_PACKAGE_TOKEN }}
run: |
set -euo pipefail
test -n "$PACKAGE_USERNAME"
test -n "$PACKAGE_TOKEN"
if [[ "$PUBLISH_PYPI" == 1 ]]; then
TWINE_USERNAME="$PACKAGE_USERNAME" TWINE_PASSWORD="$PACKAGE_TOKEN" \
python -m twine upload --non-interactive \
--repository-url https://git.add-ideas.de/api/packages/GovOPlaN/pypi \
dist/*.whl
else
echo "Exact developer meta-package is already present; skipping immutable retry."
fi
+6
View File
@@ -25,6 +25,12 @@ jobs:
- name: Bootstrap GovOPlaN repositories
working-directory: govoplan
run: python tools/repo/bootstrap-repositories.py --parent .. --transport public-https --reuse-checkout-auth --exclude-repo addideas-govoplan-website
- name: Validate package publication contracts
working-directory: govoplan
run: |
python tools/repo/sync-module-package-workflows.py --check
python tools/release/generate-developer-meta-package.py --check
python -m unittest tests.test_module_package_workflows tests.test_package_registry_release
- name: Install backend release integration dependencies
working-directory: govoplan
run: |
+40 -6
View File
@@ -92,6 +92,17 @@ jobs:
if image_pattern.fullmatch(os.environ[name]) is None:
raise SystemExit(f"{name} must be an exact sha256 image reference")
PY
- name: Resolve immutable release source
working-directory: govoplan
env:
VERSION: ${{ inputs.version }}
run: |
git fetch --force --no-tags origin "refs/tags/v$VERSION:refs/tags/v$VERSION"
mkdir -p runtime-output
git rev-parse "v$VERSION^{commit}" > runtime-output/release-source-commit
grep -Eq '^[0-9a-f]{40}$' runtime-output/release-source-commit
git show "v$VERSION:requirements-release.txt" > runtime-output/requirements-release.source.txt
git show "v$VERSION:packages/govoplan-meta/pyproject.toml" > runtime-output/govoplan-meta.source.toml
- name: Use HTTPS for GovOPlaN repositories
run: |
git config --global --add url."https://git.add-ideas.de/GovOPlaN/govoplan".insteadOf "git@git.add-ideas.de:GovOPlaN/govoplan"
@@ -101,12 +112,31 @@ jobs:
run: python tools/repo/bootstrap-repositories.py --parent .. --transport public-https --reuse-checkout-auth --exclude-repo addideas-govoplan-website
- name: Build release wheel roots and WebUI
working-directory: govoplan
env:
VERSION: ${{ inputs.version }}
GOVOPLAN_PACKAGE_USERNAME: ${{ secrets.GOVOPLAN_PACKAGE_USERNAME }}
GOVOPLAN_PACKAGE_TOKEN: ${{ secrets.GOVOPLAN_PACKAGE_TOKEN }}
run: |
python -m venv .runtime-build
.runtime-build/bin/python -m pip install --upgrade pip wheel cryptography
mkdir -p runtime-output/local-wheels
.runtime-build/bin/python -m pip wheel --no-deps --wheel-dir runtime-output/local-wheels --requirement requirements-release.txt
bash tools/release/install-webui-release-dependencies.sh ../govoplan-core/webui
.runtime-build/bin/python -m pip install --upgrade pip cryptography
.runtime-build/bin/python tools/release/generate-release-package-set.py \
--version "$VERSION" \
--profile full \
--requirements runtime-output/requirements-release.source.txt \
--meta-package runtime-output/govoplan-meta.source.toml \
--output runtime-output/release-packages.json
.runtime-build/bin/python tools/release/resolve-package-artifacts.py \
--package-set runtime-output/release-packages.json \
--wheelhouse runtime-output/local-wheels \
--webui-packages runtime-output/webui-packages \
--lock-output runtime-output/package-artifacts.lock.json \
--requirements-output runtime-output/requirements-release.packages.txt \
--python .runtime-build/bin/python
PYTHON="$PWD/.runtime-build/bin/python" \
GOVOPLAN_WEBUI_PACKAGE_LOCK="$PWD/runtime-output/package-artifacts.lock.json" \
GOVOPLAN_WEBUI_PACKAGE_DIR="$PWD/runtime-output/webui-packages" \
GOVOPLAN_WEBUI_INSTALL_ALL_PACKAGES=true \
bash tools/release/install-webui-release-dependencies.sh ../govoplan-core/webui
npm --prefix ../govoplan-core/webui run build
.runtime-build/bin/python tools/release/prepare-runtime-context.py \
--wheelhouse runtime-output/local-wheels \
@@ -240,7 +270,6 @@ jobs:
working-directory: govoplan
env:
VERSION: ${{ inputs.version }}
SOURCE_COMMIT: ${{ gitea.sha }}
SIGNING_KEY: ${{ secrets.RUNTIME_DISTRIBUTION_SIGNING_KEY }}
SIGNING_KEY_ID: ${{ secrets.RUNTIME_DISTRIBUTION_SIGNING_KEY_ID }}
TRUSTED_KEYRING: ${{ secrets.RUNTIME_DISTRIBUTION_KEYRING }}
@@ -251,6 +280,7 @@ jobs:
GARAGE_IMAGE: ${{ inputs.garage_image }}
TEST_MAIL_IMAGE: ${{ inputs.test_mail_image }}
run: |
SOURCE_COMMIT="$(cat runtime-output/release-source-commit)"
test -n "$SIGNING_KEY"
test -n "$SIGNING_KEY_ID"
test -n "$TRUSTED_KEYRING"
@@ -264,6 +294,7 @@ jobs:
--web-metadata runtime-output/web-metadata.json \
--deployer runtime-output/govoplan-deploy.pyz \
--deployer-url "$ARTIFACT_BASE/govoplan-deploy.pyz" \
--package-lock runtime-output/package-artifacts.lock.json \
--artifact-base-url "$ARTIFACT_BASE" \
--source-commit "$SOURCE_COMMIT" \
--version "$VERSION" \
@@ -347,9 +378,9 @@ jobs:
working-directory: govoplan
env:
VERSION: ${{ inputs.version }}
SOURCE_COMMIT: ${{ gitea.sha }}
GITEA_RELEASE_TOKEN: ${{ secrets.GOVOPLAN_RELEASE_TOKEN }}
run: |
SOURCE_COMMIT="$(cat runtime-output/release-source-commit)"
python tools/release/publish-runtime-release.py \
--tag "v$VERSION" \
--target-commit "$SOURCE_COMMIT" \
@@ -361,6 +392,9 @@ jobs:
--asset runtime-output/distribution-manifest.json.sha256 \
--asset runtime-output/distribution-keyring.json \
--asset runtime-output/context-amd64/composition.json \
--asset runtime-output/release-packages.json \
--asset runtime-output/package-artifacts.lock.json \
--asset runtime-output/requirements-release.packages.txt \
--asset runtime-output/evidence/api-sbom.cdx.json \
--asset runtime-output/evidence/web-sbom.cdx.json \
--asset runtime-output/evidence/api-provenance.json \
+4
View File
@@ -9,7 +9,11 @@ tools/release/runtime/*
!tools/release/runtime/Dockerfile.api
!tools/release/runtime/Dockerfile.web
!tools/release/runtime/nginx.conf
!tools/release/runtime/web-entrypoint.sh
__pycache__/
build/
dist/
*.egg-info/
audit-reports/
coverage/
htmlcov/
+29 -43
View File
@@ -113,6 +113,18 @@ Generate the CycloneDX dependency inventory from a resolved release environment:
./.venv/bin/python tools/release/generate-release-sbom.py --python ./.venv/bin/python
```
Synchronize module package workflows and inspect the registry release contract:
```sh
./.venv/bin/python tools/repo/sync-module-package-workflows.py --check
./.venv/bin/python tools/release/generate-release-package-set.py \
--output /tmp/govoplan-release-packages.json
```
Package publication, exact artifact locking, and the optional `govoplan`
developer meta-package are documented in
[Package Registry Releases](docs/operations/PACKAGE_REGISTRY_RELEASES.md).
For reproducible release artifacts, set `SOURCE_DATE_EPOCH` to the release
commit timestamp (or pass an explicit timezone-qualified `--timestamp`):
@@ -158,12 +170,19 @@ Create and validate a private, declarative installation bundle:
```
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
integration, immutable single-host rehearsal, one-host production and
multi-host Kubernetes production is in
[Deployment Profiles](docs/operations/DEPLOYMENT_PROFILES.md).
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
VM target with the guarded lifecycle documented in
[Kubernetes VM Test Lab](docs/operations/KUBERNETES_TEST_LAB.md).
The recovery state machine, migration rollback boundary, and required restore
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
@@ -176,47 +195,14 @@ such as `~/.config/gitea/gitea.env` and be passed with `--env-file`.
## Structure
The repository categories are documented in
`docs/REPOSITORY_STRUCTURE.md`. The machine-readable list lives in
`repositories.json`; the clickable human-readable index is
`docs/REPOSITORY_INDEX.md`.
Start with the [documentation map](docs/README.md). It separates stable
strategy, architecture, operations, project reference, pinned evidence, and
historical records and identifies the canonical source for each question.
Meta ownership and module install/contract boundaries are documented in
`docs/META_REPO_SCAN.md` and `docs/MODULE_CONTRACTS_AND_INSTALLS.md`.
Frontend layout principles for module pages are documented in
`docs/FRONTEND_LAYOUT_PRINCIPLES.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 Core-only 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).
The machine-readable repository list lives in `repositories.json`; the
clickable directory is the
[Repository Index](docs/project/REPOSITORY_INDEX.md), and ownership boundaries
are in [Repository Structure](docs/project/REPOSITORY_STRUCTURE.md).
# GovOPlaN Docker
+4
View File
@@ -2,6 +2,10 @@
This profile runs the shared services that production depends on while keeping
API, worker, scheduler, and WebUI code in the editable local repositories.
It is the **split source integration** profile defined in
[`docs/operations/DEPLOYMENT_PROFILES.md`](../../docs/operations/DEPLOYMENT_PROFILES.md). It does not
exercise signed application images. Use an installer-generated evaluation
Compose bundle for an immutable Dockerized whole-product rehearsal.
It provides:
-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.
+127
View File
@@ -0,0 +1,127 @@
# GovOPlaN Documentation
This directory contains cross-repository product, architecture, delivery, and
project documentation. Start here instead of browsing every file.
## 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
| Document | Role |
| --- | --- |
| [Platform Core Ideas](strategy/PLATFORM_CORE_IDEAS.md) | Stable purpose, principles, planes, distinctions, and non-goals |
| [Roadmap](strategy/ROADMAP.md) | Concise product outcomes, horizons, and current sequence |
| [Strategy Status](strategy/STRATEGY_STATUS.md) | Only prose source for current cross-product status |
| [Reference Journey Program](strategy/REFERENCE_JOURNEY_PROGRAM.md) | Acceptance journeys and their gates |
| [Product Input Register](strategy/PRODUCT_INPUT_REGISTER.md) | Normalized ideas and user-story source material |
| [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 |
## Architecture
| Topic | Canonical source |
| --- | --- |
| Institutional model and ownership | [Institutional Governance Target Architecture](architecture/INSTITUTIONAL_GOVERNANCE_TARGET_ARCHITECTURE.md) |
| Product experience and technical boundaries | [Product Experience and Module Boundaries](architecture/PRODUCT_EXPERIENCE_AND_MODULE_BOUNDARIES.md) |
| Shared interface and layout rules | [Interface Pattern Language](architecture/INTERFACE_PATTERN_LANGUAGE.md) |
| Focused task views | [Views Architecture](architecture/VIEWS_ARCHITECTURE.md) |
| Product areas and task-local tools | [Quick Access and Product Areas](architecture/QUICK_ACCESS_AND_PRODUCT_AREAS.md) |
| Platform self-description and configuration | [Platform Control Plane](architecture/PLATFORM_CONTROL_PLANE.md) |
| Data sources, definitions, and graph execution | [Datasource and Definition Graph Architecture](architecture/DATASOURCE_AND_DEFINITION_GRAPH_ARCHITECTURE.md) |
| Federation between autonomous installations | [Federated GovOPlaN Architecture](architecture/FEDERATED_GOVOPLAN_ARCHITECTURE.md) |
| Institutional digital twin | [Institutional Digital Twin](architecture/INSTITUTIONAL_DIGITAL_TWIN.md) |
| Assisted and non-digital participation | [Assisted and Non-Digital Channels](architecture/ASSISTED_AND_NON_DIGITAL_CHANNELS.md) |
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`.
## Operations
| 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) |
## Project Reference
- [Repository Index](project/REPOSITORY_INDEX.md) is the human-readable module
and repository directory; `../repositories.json` is authoritative for tools.
- [Repository Structure](project/REPOSITORY_STRUCTURE.md) defines ownership of
meta, module, deployment, and website content.
- [Gitea Issues](project/GITEA_ISSUES.md) defines labels, templates, import, and
state-update conventions.
## Evidence And Archive
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
1. Gitea issues are the only live source for work state, priority, and owner.
2. `strategy/STRATEGY_STATUS.md` is the only prose reconciliation of current
portfolio state. Do not copy its volatile counts into durable documents.
3. Durable documents state decisions, invariants, ownership, and acceptance
gates. They link to Gitea for implementation detail.
4. Dated evidence and archive documents retain their original composition and
conclusion. Add a snapshot notice instead of silently modernizing them.
5. Module-specific behavior and user/admin documentation stay in the owning
repository. Meta documentation covers cross-module outcomes and contracts.
6. Do not add another top-level Markdown file. Place new content in the
appropriate directory and add it to this map only when it has a distinct
canonical purpose.
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
```
@@ -0,0 +1,177 @@
# Assisted and Non-Digital Channels
## Purpose
GovOPlaN must support people who cannot or do not use a self-service portal.
Telephone, paper, in-person service, authorized representation, mobile staff,
interpreters, and temporary offline work are not exceptional side systems.
They are governed channels into the same service, case, workflow, record, and
decision.
The goal is equivalent institutional treatment, not forced channel identity.
The system preserves which channel was used and which evidence is available
without giving digitally confident users stronger substantive rights.
The first end-to-end journey is tracked in
[GovOPlaN #42](https://git.add-ideas.de/GovOPlaN/govoplan/issues/42).
## Actor Model
Every assisted interaction distinguishes:
- the affected person or organization;
- the real staff member or external helper entering information;
- the represented party and representation basis;
- an interpreter, witness, guardian, or support person where relevant;
- the responsible institutional function;
- the channel and location;
- the person who reviewed or confirmed the captured information.
"Entered by" is not "declared by". "Declared by" is not "verified by".
Authentication assurance, representation authority, and evidence quality are
separate fields.
## Channel-Neutral Intake Contract
All channels create the same versioned service/form submission contract with
additional provenance:
- service, form, schema, language, and accessibility version;
- valid and recorded time;
- channel (`portal`, `counter`, `telephone`, `paper`, `email`, `mobile`,
`representative`, `offline_import`, or configured extension);
- affected and represented parties;
- capture actor and responsible function;
- consent, notice, purpose, legal basis, and information source;
- field-level source and confidence where staff transcribed or inferred data;
- attachments, scans, originals, signatures, recordings, and attestations as
governed evidence references;
- read-back/confirmation result and correction path;
- receipt and chosen return channels;
- duplicate/matching assessment and any manual resolution.
Forms Runtime owns the submission lifecycle. Parties owns procedural capacity
and representation. Identity/Addresses own subject and contact references.
Cases owns the matter. Records owns filing and retention. Audit preserves the
action/effect evidence.
## Assisted Session
An assisted session is a resumable work item, not a privileged bypass. It:
1. selects service, language, channel, affected party, and represented capacity;
2. shows the staff member only fields and evidence relevant to the service;
3. explains why sensitive data is requested and what evidence quality is
required;
4. records source per value when information comes from speech, paper, an
existing register, or staff observation;
5. validates and previews consequences before submission;
6. supports read-back, correction, confirmation, and a second-person check
where policy requires it;
7. generates an accessible receipt through the requested channel;
8. creates follow-up tasks when original documents, signatures, translation,
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
chain. Assistance never grants access to unrelated records about the person.
## Paper And Scanning
- Register receipt before scanning so custody and deadlines do not depend on
successful OCR.
- Store the original scan or external archive reference with digest, pages,
capture device/provider, time, operator, and quality assessment.
- Treat OCR and extracted fields as derived data with confidence and source
coordinates. A person confirms consequential values.
- Support separation, ordering, missing-page, duplicate, malware, and
readability review.
- File the resulting document and submission into the appropriate eAkte;
retain or return the physical original according to policy.
- Produce cover sheets, barcodes, and return instructions through Templates,
not a separate print domain.
## Telephone And In-Person Handling
- Show a scripted but adaptable interview from the same Form definition.
- Record how identity and representation were checked; do not equate caller ID
with identity proof.
- Require explicit confirmation of consequential declarations and capture the
method (read-back, signed summary, one-time code, witness, later letter).
- Record call audio only when a lawful, declared profile permits it; an
interaction note is the default.
- Make interrupted sessions resumable without exposing prior answers to an
unauthorized caller or visitor.
## Offline And Mobile Work
Offline packages are encrypted, device-bound, time-limited, purpose-limited,
and contain only the required forms/reference data. Synchronization uses
idempotent intents and exposes conflicts rather than last-write-wins. Device
loss, expiry, revocation, duplicate submission, clock drift, and outcome
unknown have explicit recovery paths.
## Outbound Non-Digital Delivery
Campaign and Postbox model one delivery intent with channel choices and policy:
- portal/postbox delivery;
- email;
- print and postal fulfillment through a managed provider or local handoff;
- in-person collection;
- telephone notification followed by durable confirmation;
- accessible or language-specific variants.
Distribution preferences are purpose- and service-specific, effective-dated,
and may be overridden only by a documented legal or urgent-delivery rule. A
fallback occurs only before a channel has accepted the effect unless policy
explicitly authorizes duplicate delivery. Receipts distinguish creation,
provider acceptance, dispatch, delivery, return, and acknowledgement.
## Accessibility And Equality
- The person can request language, easy-language, large-print, screen-reader,
sign-language, relay, interpreter, or representative support without those
preferences becoming a general-purpose profile visible everywhere.
- Staff interfaces support keyboard-only capture, clear focus, error summary,
read-back, and printable/offline alternatives.
- Channel choice and need for assistance must not be used as an adverse risk
signal.
- Reports compare completion, wait, correction, abandonment, and outcome by
channel only under a declared equality/service-quality purpose and with
privacy thresholds.
## Security And Abuse Controls
- purpose-aware field access and session timeout;
- current authority checks for every read and effect;
- dual control for high-risk identity, payment, address, or representation
changes;
- immutable source/attestation evidence and correction history;
- rate and anomaly controls that do not silently reject a person;
- explicit safe handling of domestic-abuse, protected-address, witness, or
sealed-record cases;
- no secret answers or full documents in ordinary operational logs.
## First Reference Journey
Implement the permit-to-payment/service-to-decision journey through three
equivalent starts:
1. self-service portal submission;
2. staff-assisted counter/telephone submission;
3. paper receipt, scan, extraction, confirmation, and filing.
All three must create the same Case and Workflow contract, preserve different
provenance, support correction, produce a receipt, file an eAkte, reach the same
decision rules, and prove accessibility, privacy, recovery, and channel
fallback in browser and operator tests.
@@ -0,0 +1,155 @@
# Federated GovOPlaN Architecture
## Purpose
Federation lets autonomous GovOPlaN installations exchange data,
configuration, work, messages, records, and evidence without sharing a database
or surrendering local policy. It is institution-to-institution cooperation,
not multi-tenancy across an untrusted network.
The first implementation should prove a bounded exchange between two
installations. A new federation module is not justified until the shared
protocol has at least two independent consumers. Core owns neutral envelopes
and trust contracts; Connectors owns transport providers; domain modules own
the objects and effects they exchange.
Implementation is tracked in
[GovOPlaN #41](https://git.add-ideas.de/GovOPlaN/govoplan/issues/41).
## Invariants
1. Every installation remains authoritative for its tenants, identities,
policies, keys, records, and local mappings.
2. A remote identity or permission never becomes a local authorization claim.
3. Every exchange declares purpose, legal/organizational basis, classification,
minimization, retention expectation, and permitted onward use.
4. Every object reference identifies origin instance, owner tenant, object type,
object ID, exact revision, and source-authority mode.
5. Payloads and receipts are signed; sensitive transports use mutually
authenticated encrypted channels.
6. Acceptance, rejection, outcome unknown, retry, revocation, correction, and
reconciliation are durable states.
7. Local policy may reject or narrow a remote request. It cannot silently claim
to have accepted an effect that did not occur.
8. Federation works asynchronously and can exchange signed offline bundles
where continuous connectivity is unavailable.
## Trust Domains
An instance publishes a signed, versioned federation descriptor containing:
- stable instance and operator identity;
- supported protocol and schema versions;
- signing and transport key identifiers with rotation history;
- accepted object and exchange profiles;
- endpoint locations and size/rate limits;
- support, incident, revocation, and data-protection contacts;
- evidence and conformance references.
Pairing is a two-sided administrative workflow. Each side verifies the other,
maps the remote institution to a local trusted-party record, selects permitted
profiles and purposes, sets policy ceilings, and records approvals. Trust is
directional and profile-specific; trusting signed Postbox delivery does not
automatically permit case transfer or configuration import.
## Exchange Envelope
Every request, response, receipt, correction, and revocation uses one neutral
envelope with:
- message ID, correlation ID, causation ID, creation and expiry;
- origin and destination instance/institution/tenant references;
- real actor and represented institutional capacity where disclosure is
permitted;
- exchange profile and semantic schema version;
- exact domain object references and content digests;
- purpose, legal basis, classification, data categories, retention expectation,
onward-transfer constraint, and subject notice status;
- requested action and idempotency key;
- encryption recipients and signature chain;
- attachment/object manifests rather than unbounded embedded blobs;
- previous-envelope references for correction, replacement, or revocation.
The envelope is evidence, not a universal domain object. Each owner validates
and imports or links its own payload.
## Exchange Profiles
| Profile | First owners | Behavior |
| --- | --- | --- |
| Postbox delivery | Postbox, Campaign, Notifications | Address or derive a remote function-bound postbox, obtain acceptance receipt, and track acknowledgement where permitted |
| Case handoff | Cases, Parties, Services, Workflow Engine | Offer exact context and evidence; destination accepts into a new local case and returns the mapping |
| Record transfer | Records, Files, DMS, Audit | Transfer or offer a signed record package with file-plan, metadata, content digests, holds, and disposition constraints |
| Decision/evidence reference | Decisions, Committee, Audit | Publish a protected exact outcome or verifiable reference without transferring unrelated case content |
| Data product publication | Datasources, Dataflow, Reporting | Publish immutable governed materializations with schema, quality, freshness, lineage, and use constraints |
| Configuration package | Core, Policy, Views, Workflow, Forms, Templates | Exchange signed definitions; destination assesses compatibility, maps values, derives locally, and never imports secrets |
| Search discovery | Search and domain providers | Return permission-filtered metadata or a handoff link; never expose raw remote indexes as local authority |
## State Machine
```text
draft -> authorized -> queued -> transmitted -> received
| |
v v
outcome_unknown rejected
|
received -> validating -> accepted -> applied -> acknowledged
| | |
v v v
rejected accepted_ reconciled
pending
```
Acceptance means the destination durably owns the received intent. It does not
mean the requested domain effect completed. Receipts distinguish transport,
validation, acceptance, application, and human acknowledgement.
## Conflict And Autonomy
- Incoming native objects become local references, mirrors, or newly owned
objects according to the profile. They do not overwrite local authority by
ID coincidence.
- Local mappings are effective-dated and auditable.
- Corrections create a linked revision. They do not erase what the destination
previously observed.
- Revocation is a request and evidence event; the destination applies its own
legal and retention rules.
- Configuration imports use assessment and derivation. A remote package cannot
weaken local policy or install code implicitly.
- A disconnected partner remains a visible pending/failed state; work can be
rerouted through an approved alternative channel.
## Security And Privacy
- Use mTLS for paired online transports and signed envelopes for end-to-end
origin evidence.
- Encrypt payload objects for the destination, with key rotation and outcome-
unknown recovery; transport encryption alone is insufficient for queued
bundles.
- Do not put bearer credentials, local permission scopes, or reusable secrets
in an exchange.
- Rate-limit and size-bound discovery and transfer; quarantine unknown schemas
and active content.
- Evaluate current local authorization at every effect even when the envelope
describes historical authority.
- Log metadata separately from protected content so operators can reconcile
without broad content access.
- Subject access, correction, restriction, legal hold, and deletion requests
become federated workflows with local decisions and receipts, not remote
direct database operations.
## First Reference Proof
1. Pair two disposable installations with independent tenants, keys, and
policies.
2. Exchange signed descriptors and approve only the Postbox delivery profile.
3. Deliver one Campaign message to a remote function-bound Postbox.
4. Prove replay safety, rejection, timeout/outcome unknown, retry,
acknowledgement, correction, key rotation, and revoked trust.
5. Export the complete evidence bundle and restore both sides from backup.
6. Add configuration-package exchange only after the delivery proof passes.
The result is a provider-neutral federation contract. A future dedicated
module becomes appropriate only when pairing, trust administration, exchange
queues, and evidence have a lifecycle independent of Connectors and the first
domain owner.
@@ -0,0 +1,149 @@
# Institutional Digital Twin
## Definition
The institutional digital twin is a governed, time-aware projection of how an
institution is constituted and operates. It connects structure, authority,
services, work, information, technology, obligations, controls, evidence, and
outcomes without becoming a second source of truth.
The twin is not one editable graph database and not an employee-surveillance
system. Domain modules and external systems keep ownership. The twin stores or
materializes exact references, declared relationships, provenance, confidence,
and projection versions. Changes flow through owner actions.
Implementation is tracked in
[GovOPlaN #43](https://git.add-ideas.de/GovOPlaN/govoplan/issues/43).
## Questions It Should Answer
- Which unit and function is responsible for a service, decision, record,
system, dataset, control, or risk at a given valid and recorded time?
- Which mandates and policies permit or constrain an action?
- Which processes, providers, staff capacities, data sources, and records are
required to deliver a service?
- What is affected if a system, provider, organizational unit, role, package,
or legal rule changes?
- Where are responsibilities missing, conflicting, expired, or concentrated?
- Which controls are evidenced, stale, failed, or dependent on an unverified
assertion?
- How do actual process traces differ from defined workflows?
- Which public outcomes can be explained from protected internal evidence?
## Projection Planes
| Plane | Meaning |
| --- | --- |
| Current | Valid now, reconstructed from owner projections and current provider state |
| Historical | Valid at and recorded by selected instants, with present-day security enforced |
| Planned | Approved or proposed future structures, services, policies, projects, and package changes |
| Observed | Events, process traces, service measures, incidents, effects, and evidence actually recorded |
| Scenario | Non-authoritative simulation of a proposed change and its estimated consequences |
The UI must label these planes unambiguously. Scenario output never becomes an
institutional fact until an authorized owner action accepts it.
## Canonical Graph
Nodes are stable institutional references, including institution, tenant,
unit, function, assignment, mandate, jurisdiction, service, case, party, task,
workflow, approval, decision, record, file, message, appointment, dataset,
report, provider, system, control, risk, project, asset, and configuration
package.
Edges have:
- owner and source authority;
- relationship type and direction;
- valid-from/valid-to and recorded/superseded times;
- exact source revision and evidence digest;
- institution/tenant boundary;
- purpose and visibility classification;
- confidence and derivation method for inferred relationships;
- correction and replacement references.
Inferred edges are never displayed as owner assertions. They remain
explainable analytical products with source lineage.
## Ownership And Implementation
- Core owns neutral institutional references, temporal context, provider
registration, and graph projection contracts.
- Domain modules publish bounded nodes and edges through provider interfaces.
- Search indexes discoverable identities and links.
- Reporting materializes governed analytical projections.
- Dataflow computes derived relationships, quality checks, and scenarios.
- Policy evaluates visibility, purpose, retention, and allowed scenario/action
transitions.
- Audit supplies observed events and evidence references.
- Projects supplies planned change and benefit relationships.
- Views renders role- and task-focused twin perspectives.
- Workflow Engine coordinates accepted changes but does not edit owner tables.
No new digital-twin module is required for the first slice. A dedicated owner
is justified later if persisted scenario models, graph revisions, and
cross-domain projection lifecycle become independent product objects.
## Beyond The Current Platform
### Continuous assurance
Controls become versioned assertions with evidence requirements, evaluation
frequency, responsible function, exception workflow, and freshness. Dataflow
and provider checks evaluate them continuously; Policy decides whether a stale
or failed control advises, requires review, or blocks an effect.
### Process mining and conformance
Governed event histories can derive actual paths, wait times, rework, and
exceptions. Comparison to Workflow definitions should improve procedures, not
rank individuals. Access to personal or small-cohort detail is purpose-limited
and separately governed.
### Change-impact simulation
A proposed organizational, provider, policy, or package change can be assessed
against dependencies, mandates, open work, records, controls, capacity, and
recovery plans before activation. Results identify uncertainty rather than
inventing precision.
### Federated institutional models
Installations can exchange signed public or partner-specific subsets of their
service, mandate, provider, and evidence graph. Every side maps the references
locally and retains autonomy. Federation does not create one supranational
master graph.
### Accountable assistance
Assistance may summarize context, identify missing evidence, draft a decision
or workflow, propose mappings, and explain policy. Every output records model,
inputs, constraints, uncertainty, human review, and accepted edits. Assistance
does not become the acting authority.
### Public evidence chains
Transparency packages can publish a minimized chain from rule and aggregate
facts to decision and observed outcome, with digests proving relation to
protected evidence. Public verification does not require disclosure of the
underlying personal data.
## Guardrails
- Do not infer competence, misconduct, intent, or personal performance from
graph proximity or incomplete events.
- Do not centralize protected content merely to make graph queries easier.
- Do not use historical authorization to expose data now prohibited.
- Do not let a scenario engine write domain state directly.
- Do not hide source authority, freshness, uncertainty, or missing evidence.
- Do not retain analytical detail longer than the declared purpose requires.
## Delivery Slices
1. Publish exact institutional reference/edge providers for the service-to-
decision and monthly-data journeys.
2. Build a current/historical dependency explorer with source and access
explanations.
3. Add planned Project/package changes and bounded impact reports.
4. Add control evidence/freshness and process conformance for one journey.
5. Prove a minimized federated projection and a public evidence package.
@@ -9,20 +9,24 @@ concepts prepared outside the repositories:
- `software_big_picture.md`
The source concepts describe GovOPlaN as an operational governance platform for
public institutions. This document merges that direction with the implemented
platform state as of 2026-08-01. It is the canonical repository version of the
direction. Gitea issues remain the source of truth for delivery state.
public institutions. This document is the canonical repository version of that
durable architectural direction. Its implementation table records the accepted
2026-08-01 baseline; it is not a rolling status report. Current reconciliation
lives in [Strategy Status](../strategy/STRATEGY_STATUS.md), and Gitea issues remain the
source of truth for delivery state.
Read this together with:
- [Connected Governance Platform Roadmap](CONNECTED_GOVERNANCE_PLATFORM_ROADMAP.md)
- [Reference Journey Program](REFERENCE_JOURNEY_PROGRAM.md)
- [Module Contracts and Install Boundaries](MODULE_CONTRACTS_AND_INSTALLS.md)
- [Connected Governance Platform Roadmap](../strategy/reference/CONNECTED_GOVERNANCE_PLATFORM_ROADMAP.md)
- [Platform Core Ideas](../strategy/PLATFORM_CORE_IDEAS.md)
- [Strategy Status](../strategy/STRATEGY_STATUS.md)
- [Reference Journey Program](../strategy/REFERENCE_JOURNEY_PROGRAM.md)
- [Module Contracts and Install Boundaries](../operations/MODULE_CONTRACTS_AND_INSTALLS.md)
- [Datasource and Definition Graph Architecture](DATASOURCE_AND_DEFINITION_GRAPH_ARCHITECTURE.md)
- [Capability and Infrastructure Fit](CAPABILITY_AND_INFRASTRUCTURE_FIT.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 Action, Effect, and Automation Layer](../../govoplan-core/docs/ACTION_EFFECT_AUTOMATION_LAYER.md)
- [Generated Capability and Infrastructure Fit](../evidence/snapshots/CAPABILITY_AND_INFRASTRUCTURE_FIT.generated.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 Action, Effect, and Automation Layer](../../../govoplan-core/docs/ACTION_EFFECT_AUTOMATION_LAYER.md)
## Decision
@@ -70,7 +74,12 @@ compositions described here are now implemented. Subsequent work is
**product depth and stronger maturity evidence**, not another runtime rewrite
or an unimplemented architecture boundary.
## Implementation status (2026-08-01)
## Accepted implementation baseline (2026-08-01)
This section is retained as the dated baseline against which the architecture
decision was accepted. Later implementation must be reconciled in
`STRATEGY_STATUS.md` rather than editing individual rows here into a competing
status report.
The architecture contract is implemented as a bounded, executable vertical
slice. The portfolio declarations and provider governance gates apply to the
@@ -79,7 +88,7 @@ were proven now have independent persistent owners:
| Area | Implemented state | Remaining rollout |
| --- | --- | --- |
| Module portfolio metadata | Core validates versioned architecture layer/kind, maturity evidence, known limits, ownership boundaries, authority modes, reference packages, target-tested providers, and migration/upgrade/recovery/security/operations documentation. | Complete for all 62 source manifests. Focused and release checks enforce `--require-architecture`; a new module cannot enter the workspace without truthful declaration and repository-local evidence. |
| Module portfolio metadata | Core validates versioned architecture layer/kind, maturity evidence, known limits, ownership boundaries, authority modes, reference packages, target-tested providers, and migration/upgrade/recovery/security/operations documentation. | Complete for the source manifests in the 2026-08-01 snapshot. Focused and release checks enforce `--require-architecture`; current portfolio counts belong in `STRATEGY_STATUS.md`. |
| External providers | Core validates provider objects/field groups, operations, integration maturity, source authority, bounded reads, freshness/health, idempotency, conflicts, outcome-unknown handling, evidence, correction, reconciliation, outage, classification, purpose, retention, and secret handling. Addresses/CardDAV, Files remote storage, Mail SMTP/IMAP, Calendar CalDAV/ICS/Graph/EWS, and Connectors tabular/sanctions providers declare the contract and tenant-bounded secret-free runtime state. | Registry validation rejects any declared external provider without a sanitized state provider. Future adapters must cross the same gate before activation. |
| Institutional context | Core provides versioned temporal, actor/representation, institution/unit/function/task/mandate/jurisdiction/service/case/party/work-item/workflow/approval/decision/record, legal-basis, evidence, information-governance, external-source, presentation, and geographic references. Events, automation actions, audit records, and the transactional Audit outbox preserve the envelope. | Owning modules must progressively require the relevant subset for consequential operations. |
| Semantic provider contracts | Provider-neutral DTOs and protocols cover Mandate resolution, versioned Service definitions, procedure Parties/representation, and formal Decisions. `govoplan-mandates`, `govoplan-services`, `govoplan-parties`, and `govoplan-decisions` now persist immutable revisions behind those contracts with tenant isolation, bounded reads, replay safety, OCC, migrations, uninstall guards, permissions, APIs, capability documentation, and recovery documentation. | The owners are deliberately headless. Procedure-specific UI remains with consuming modules. |
@@ -391,7 +400,9 @@ submodule, configuration fragment, package, or profile.
- This reconciliation is canonical in the meta repository and mirrored to the
Gitea wiki.
- All 62 source manifests carry validated evidence-based architecture metadata.
- All source manifests in the accepted 2026-08-01 baseline carried validated
evidence-based architecture metadata; current counts belong in
`STRATEGY_STATUS.md`.
- External-reference, action/effect, operational-health, ownership, policy,
audit, and documentation primitives compose into one enforced provider
declaration and sanitized runtime-state contract.
@@ -446,7 +457,7 @@ submodule, configuration fragment, package, or profile.
recovery, accessibility, privacy, security, and operator evidence. This is a
maturity gate, not missing architecture implementation.
## What remains after the executable architecture slice
## What remains within the accepted 2026-08-01 architecture slice
The remaining work is not another Core or cross-module architecture rewrite.
It falls into two explicitly different categories, neither of which can be
@@ -487,12 +498,38 @@ persistence, migrations, recovery/disable semantics, documentation and focused
tests. Their remaining tickets concern concrete providers, deeper adapters and
target evidence, not an unresolved institutional architecture boundary.
Everything else described as architecture in this document now has a
Everything else described in the accepted baseline of this document now has a
repository owner, versioned contract, bounded implementation, migration and
recovery boundary where state exists, documentation, and executable evidence.
Further work in those modules is product breadth, UX depth, provider adoption,
and evidence renewal.
## Strategic extensions accepted after the baseline
The completed baseline does not imply that institutional product architecture
can no longer grow. The 2026-08-05 strategic review accepted four extensions
that consume the existing contracts without reopening the kernel or moving
domain ownership into Core:
- [Product Experience and Module Boundaries](PRODUCT_EXPERIENCE_AND_MODULE_BOUNDARIES.md)
separates technical package topology from stable task/object/product
surfaces; implementation is tracked in Core #283.
- [Federated GovOPlaN Architecture](FEDERATED_GOVOPLAN_ARCHITECTURE.md)
defines governed exchange between autonomous installations; implementation
is tracked in GovOPlaN #41.
- [Assisted and Non-Digital Channels](ASSISTED_AND_NON_DIGITAL_CHANNELS.md)
makes channel inclusion part of the service-to-decision journey; the first
reference proof is tracked in GovOPlaN #42.
- [Institutional Digital Twin](INSTITUTIONAL_DIGITAL_TWIN.md) defines a
time-aware, policy-filtered projection over owner data; implementation is
tracked in GovOPlaN #43.
The eAkte depth required by those journeys is owned by Records and specified in
`govoplan-records/docs/EAKTE_ARCHITECTURE.md`, tracked in Records #1. These are
new product-depth programs with bounded contracts and acceptance journeys, not
evidence that the original institutional semantics or module architecture
failed.
## Delivery tracking
The completed cross-repository architecture epic is
@@ -2,12 +2,13 @@
This document is the cross-repository pattern language for GovOPlaN user
interfaces. It turns the existing ethical doctrine, binding UI/UX decisions,
layout principles, and module boundary into a common composition and review
grammar. It does not replace those sources.
layout rules, and module boundary into a common composition and review grammar.
This document also owns the former standalone frontend-layout principles.
The companion [interface surface inventory](INTERFACE_SURFACE_INVENTORY.md)
records which surfaces the current code contributes and where each surface
enters the rollout.
The dated [interface surface inventory](../evidence/snapshots/INTERFACE_SURFACE_INVENTORY.md)
records the 2026-08-03 rollout snapshot. Current implementation state belongs
in Gitea and generated inventory evidence, not in this durable pattern
language.
## 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
such as progressive disclosure, adaptive forms, blocker language, guided
operations, and the platform theme contract.
3. `docs/FRONTEND_LAYOUT_PRINCIPLES.md` owns the high-level choice between a
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,
3. `govoplan-core/docs/MODULE_ARCHITECTURE.md` owns the shell, route, navigation,
UI-capability, and shared-component boundaries.
5. This document owns the common pattern names, placement grammar, wording and
state conventions, focused-view composition, and definition of done across
those sources.
4. This document owns the high-level layout choice, common pattern names,
placement grammar, wording and state conventions, focused-view composition,
and definition of done across those sources.
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
@@ -53,6 +51,148 @@ rules:
- Preserve a stable way back to the containing object and the broader system.
- 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
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
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.
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
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
panel. A side panel used as the creation surface appears for creation and is
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
detail, not in the global title bar.
- Full-page create and edit surfaces put their persistent action cluster in the
@@ -25,6 +25,7 @@ releases, module boundaries, migrations, and security controls.
| Labels and translations | Generated translation catalogs plus source usage |
| Fields and help coverage | Shared form components plus generated TypeScript AST inventory |
| API use by the WebUI | Typed API clients plus generated static reference inventory |
| Stable platform interface IDs | Typed manifest/WebUI declarations plus line-independent source anchors for low-level controls |
| Effective configuration | Owning module data plus Policy provenance |
Runtime introspection is authoritative for an installed system. Static source
@@ -45,9 +46,13 @@ The command writes:
- `audit-reports/platform-inventory/platform-interface-inventory.json`
- `audit-reports/platform-inventory/platform-interface-inventory.md`
Use `--strict` in CI. In addition to translation coverage, strict mode requires
every backend endpoint without a statically visible WebUI path to have an exact
entry in
Use `--strict` for the combined translation, endpoint, and declaration audit.
Use `--strict-declarations` for duplicate/stale/undeclared interface checks
without making existing translation coverage a release blocker. Use
`--strict-endpoints` in the endpoint-surface CI gate so unrelated translation
catalog work cannot disable route classification enforcement. Both strict modes
require every backend endpoint without a statically visible WebUI path to have
an exact entry in
`tools/inventory/endpoint-surface-declarations.json`. The registry is keyed by
repository, HTTP method, and canonical version-independent path. It accepts:
@@ -73,6 +78,16 @@ It combines:
2. TypeScript AST extraction of fields, label attributes, visible text,
translations, frontend routes, navigation, capabilities, and API references
3. Python AST extraction of FastAPI route decorators and router prefixes
4. normalized runtime declarations from every loaded `ModuleManifest`
The declaration set covers routes, navigation, View surfaces, fields, actions,
help references, translations, admin/settings sections, widgets, search
objects, permissions, provided interfaces, and backend capabilities. Typed
module contributions keep their declared IDs. Shared controls may declare
`interfaceId` and `helpTopicId`; otherwise the extractor assigns a deterministic
source anchor based on repository, file, component context, control type, and
semantic label rather than a line number. The JSON records which identity
source was used.
The JSON includes exact repository, file, and line evidence. A missing-help
entry is a review candidate because dynamic parent components may supply help.
@@ -80,9 +95,40 @@ A backend route without a static frontend reference is also a review candidate:
public APIs, workers, callbacks, health checks, connectors, and dynamic URL
assembly are valid explanations.
`--strict` currently enforces only translation-catalog completeness. Endpoint
and help classifications need narrow reviewed baselines before they can become
release gates.
The module matrix enforces endpoint and interface declarations with
`--strict-endpoints --strict-declarations`.
Combined `--strict` additionally fails when used translation keys are absent
from generated locale catalogs. Help-text findings remain review candidates
rather than a release gate because dynamic parent components can supply help.
## Runtime Comparison
Core exposes a sanitized read-only catalog at
`GET /api/v1/platform/interface-catalog`. Access requires
`admin:module:read` or `system:settings:read`. Tenant module entitlements are
applied before serialization, so the response describes only the effective
installed combination. It contains IDs, paths, authorization metadata,
versions, counts, and canonical digests; it excludes factories, callbacks,
credentials, and mutable runtime state.
Capture and compare a running installation:
```bash
curl --fail --silent \
-H "Authorization: Bearer $GOVOPLAN_ACCESS_TOKEN" \
"$GOVOPLAN_URL/api/v1/platform/interface-catalog" \
> /tmp/govoplan-runtime-interface.json
./.venv/bin/python tools/inventory/platform-interface-inventory.py \
--runtime-snapshot /tmp/govoplan-runtime-interface.json \
--strict-declarations \
--strict-endpoints
```
The comparison accepts any installed subset. Every module present in the
runtime response must have the same contract version, module version, and
declaration digest as the static release inventory. Unknown, duplicate, or
mismatched runtime modules fail strict declaration mode.
## Admin Information Architecture
@@ -136,13 +182,20 @@ Custom code, new routes, arbitrary SQL, and executable workflow nodes remain
release artifacts. Modeling them as ordinary configuration would create an
unreviewed code-execution and migration channel.
## Next Enforcement Slices
## Enforced Contract
1. Require every WebUI module route and admin/settings contribution to have
matching manifest metadata or a reviewed exception.
2. Add stable field IDs and optional help-topic IDs to shared field components.
3. Classify each statically unreferenced backend endpoint by consumer type.
4. Compare a running installation's OpenAPI and module registry against the
release inventory.
5. Publish the sanitized installed-system structure through Ops/Docs for
authorized administrators.
1. Public WebUI routes and View surfaces must reconcile with runtime manifest
metadata; stale runtime routes and source-only public surfaces fail CI.
2. Duplicate stable IDs fail CI. Shared controls support explicit field/action
and help-topic identities; fallback anchors remain visible review evidence.
3. Every statically unreferenced backend endpoint has an exact reviewed
consumer classification, and stale classifications fail CI.
4. Runtime module combinations can be compared exactly with static release
evidence through versioned per-module digests.
5. Runtime introspection is authorized, tenant-filtered, and read-only. It is
safe for Ops/Docs projection but is not a generic configuration or code
mutation channel.
Generated JSON and Markdown remain build/audit artifacts. Do not hand-edit or
use them as a backlog; change the owning manifest, typed WebUI contribution,
translation/help declaration, or exact endpoint classification instead.
@@ -0,0 +1,190 @@
# Product Experience and Module Boundaries
## Problem
GovOPlaN's runtime modularity is a strength, but the implementation structure
is exposed too directly in the product. Ordinary users encounter module names,
one top-level route per module, one navigation item per repository, package and
provider identifiers, and errors framed as missing modules. This makes the
system look like a toolbox of adjacent applications instead of one operating
environment for institutional work.
The correction is not a monolithic frontend and not hidden provenance. It is a
separate product information architecture assembled from typed module
contributions.
Implementation is tracked in
[Core #283](https://git.add-ideas.de/GovOPlaN/govoplan-core/issues/283).
The accepted configurable product-area and task-local tool design is defined
in [Quick Access And Product Areas](QUICK_ACCESS_AND_PRODUCT_AREAS.md).
## Current Exposure Inventory
| Surface | Direct exposure | Appropriate audience | Product-facing alternative |
| --- | --- | --- | --- |
| Side rail | One icon and route for many installed modules | Administrators and power users | Work areas, services, inboxes, records, communication, data and assurance |
| Route paths | Technical owners such as `/dataflow`, `/forms`, or `/postbox` | Deep links and diagnostics | Stable product aliases and journey routes that resolve to owner surfaces |
| Dashboard | Installed module count and module-owned widget library | Operators | Outcome, obligation, work, exception, and service widgets |
| Administration | Package names, database state, capabilities, providers | Module and system administrators | Guided product/package configuration with technical details on demand |
| Errors | "Module/capability not installed" | Diagnostics | Explain the unavailable outcome, responsible administrator, and enabling path |
| Documentation | Topics grouped primarily by module | Administrators | Task, role, service, and object documentation with module provenance secondary |
| Permissions | Module-namespaced scopes | Access administrators | Human-readable responsibility bundles; exact scopes remain inspectable |
| Search | Provider/module as a result facet | Advanced filtering | Object type, institution, time, purpose, case/service, and source authority |
| Workflow | Steps can expose target route/module details | Workflow designers | User-facing action and expected result; technical binding in definition details |
| Connector state | Provider IDs and source types | Integration owners | Named source, authority, freshness, health, last effect, and recovery state |
## Boundary Decision
Three layers remain distinct:
1. **Technical module layer:** package ownership, dependencies, capabilities,
permissions, migrations, routes, and provider identifiers.
2. **Product composition layer:** work areas, object types, journeys, commands,
inboxes, configuration packages, and role-based defaults.
3. **Presentation projection:** active view, tenant policy, current task,
temporal context, language, accessibility preferences, and device layout.
Modules own implementation and contribute typed product metadata. Core
assembles it. Views filters it. Policy constrains it. Access authorizes the
underlying actions. No consumer imports another optional module's UI directly.
## Product Surface Contract
Each WebUI module should be able to announce:
- `product_areas`: stable areas to which a route, command, widget, or object
belongs;
- `object_types`: user-facing nouns, icons, search context, detail route, and
owner provenance;
- `work_item_sources`: open work, exceptions, deadlines, and responsible
capacity;
- `journey_actions`: launch, resume, review, correct, decide, publish, and
reconcile commands;
- `workspace_surfaces`: embeddable but owner-rendered list, detail, editor, and
status surfaces;
- `configuration_contributions`: guided settings with consequence and
prerequisite metadata;
- `help_contexts`: user/admin documentation for the product identity as well as
the technical owner;
- `technical_provenance`: module, interface version, capability, and provider
identifiers shown only in details and evidence.
The contract references surfaces. It does not permit Core or a product package
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
The default shell should prioritize:
1. global search and create/resume commands;
2. personal and function-bound work;
3. configured product areas;
4. pinned user destinations;
5. administration and technical module inspection when authorized.
The baseline product areas are Work, Services and Cases, Records and
Documents, Communication, Meetings and Decisions, Data and Assurance, and
People and Responsibility. They are configurable system/tenant defaults and
Views projections, not hard-coded repository groups. Empty areas disappear;
single-destination areas may link directly; familiar tools may remain pinned.
The complete permission-derived module rail remains available as **All
available tools**. Its ability to scroll is useful and is not itself the
product defect. The defect is requiring people to infer a task or outcome from
repository topology.
Task-local Work, Calendar, Messages and Files tools may be contributed to the
optional `govoplan-quick-access` rail. Messages composes Mail, Postbox and
future governed chat presentation without merging their channel semantics or
state.
A module route remains a valid deep link. A product area may combine links and
owner-rendered surfaces from several modules. When a required contribution is
absent, the area explains the missing outcome rather than rendering a broken
placeholder.
Views remain the projection mechanism. They may select product areas, routes,
sections, commands, widgets, and fields. A view must not grant a permission or
change data semantics. Policy can force, allow, or prohibit a surface at system,
tenant, group, or user scope.
## Error And Provenance Language
Normal errors answer:
- what the person was trying to achieve;
- why it is unavailable or failed;
- whether data was saved or an external effect may have occurred;
- who can resolve it and where;
- the correlation/evidence reference.
An expandable technical section may then identify the module, capability,
provider, request, and version. This keeps the product intelligible without
hiding operational truth.
## Migration
Core's product-area and Quick Access contracts, the optional Quick Access
module, the first five providers and immutable View presentation revisions are
implemented. The migration below now concerns broader classification and
product-language adoption; it is not a prerequisite for safely enabling the
first rail slice.
### Slice 1: inventory and aliases
- continue classifying every route, navigation item, widget, setting, search object, and
help context by product area and object type;
- extend the implemented product-surface aliases without removing existing deep links;
- flag raw module IDs in ordinary-user labels and errors.
### Slice 2: work-first shell
- provide a generic work/exception/deadline aggregation capability;
- make work areas and configured packages the default navigation;
- move the complete module catalogue to administration and an optional power-
user surface.
- implement the configurable Quick Access rail through Core-mediated
contributions, system/tenant/user resolution and View/Policy ceilings.
### Slice 3: composite journeys
- let product packages define journey launch/resume actions and default views;
- let Workflow Engine activate a view and focus an owner surface without
controlling authorization;
- expose provider provenance and technical bindings on demand.
### Slice 4: enforceability
- make product classification mandatory for user-visible manifest surfaces;
- reject duplicate product identities and missing owner routes in CI;
- add browser tests proving that reference users can complete a journey without
knowing module names.
## Acceptance Criteria
- An ordinary user can describe every primary navigation item as work or an
institutional object, not as a package.
- A product package can remove irrelevant navigation while retaining deep-link
and help integrity.
- Missing optional modules produce an actionable product explanation.
- Administrators can still inspect exact module, capability, provider, schema,
and evidence provenance.
- Module permutation tests prove that no product surface assumes an optional
owner is installed.
@@ -0,0 +1,232 @@
# Quick Access And Product Areas
## Purpose
GovOPlaN presents institutional work without requiring ordinary users to
understand the installed package graph. Two complementary projections provide
that experience:
- **product areas** group destinations, objects, work and actions by the
outcome a person recognizes;
- **Quick Access** keeps a small set of task-local tools available without
leaving the current page, case, record or Workflow context.
Technical modules remain the implementation, release and provenance boundary.
Product areas and Quick Access are presentation contracts over those owners;
they do not copy domain state or bypass authorization.
Implementation is tracked by Core #283 and #285, GovOPlaN's product-experience
umbrella, Views, Policy and `govoplan-quick-access`.
The repository and product name is `govoplan-quick-access`, with module id
`quick_access`. `govoplan-qar` was rejected because the abbreviation hides the
purpose in package catalogues, diagnostics, permissions and operations.
## Implementation Status
The first production-shaped slice is implemented:
- Core validates and publishes versioned `product_areas` and
`quick_access_tools` manifest contracts;
- `govoplan-quick-access` derives its live catalogue from installed modules,
persists optimistic-concurrency-protected system, tenant and user profiles,
and resolves blocked, forced, ordered and stale preferences;
- the shell hosts the optional right rail and one composed drawer with keyboard
dismissal, focus return, responsive mobile behavior and full-page fallbacks;
- Tasks, Calendar, Mail, Postbox and Files contribute the first owner-rendered
tools; Mail and Postbox remain separate sections inside Messages;
- immutable View revisions now carry grouped/flat navigation, product-area
order and optional labels. Scoped Views therefore configure product
presentation for system, tenant, group, user and Workflow contexts;
- the expanded left rail groups classified destinations while retaining
Dashboard and every authorized unclassified destination under More tools.
The baseline classification is now manifest-declared for every ordinary
user-facing module and enforced by the workspace manifest check. A separately
versioned launch-context contract carries bounded active-object, acting,
temporal, View and return references into full-page Quick Access fallbacks;
Cases publishes the first active-object reference. The remaining rollout is to
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
Core owns a versioned contribution contract. Feature modules may register a
tool when they have a useful bounded surface. They do not import Quick Access.
`govoplan-quick-access` owns configuration, effective resolution, ordering,
the right-side rail and its drawer. Views may narrow tools for the current
task. Policy may constrain availability and customization. Access and each
owner's backend remain authoritative.
The initial categories are:
| Category | Typical contributions |
| --- | --- |
| Work | Explicit Tasks, Workflow handoffs, approvals, deadlines and exceptions |
| Calendar | Today/upcoming agenda, event creation and scheduling launch |
| Messages | Mail, function-bound Postbox messages and future governed chat providers |
| Files | Contextual/recent files, attachment selection and upload |
Messages is one shell category but not one data model. Mail, Postbox and future
chat providers retain their channel semantics, custody, policy, audit and
delivery behavior. The drawer identifies the channel where that distinction
matters.
## Contribution Contract
A Quick Access contribution declares:
- contract version 1, a stable id, category and human label;
- icon, order and optional badge/summary provider;
- required permissions and optional dependencies;
- 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;
- View surface, help context and availability explanation;
- whether the contribution supports preview, create, select or resume.
The shell passes only bounded references: tenant, acting context, temporal
read context, active task/Workflow, current institutional object, selected
resources and a safe return location. The owner reauthorizes every read and
effect. Credentials, protected content and permission decisions are never
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
The effective rail is resolved from:
1. installed and enabled modules and their registered contributions;
2. system availability, forced entries and ordering defaults;
3. tenant availability, forced entries and ordering defaults;
4. group and user View/Policy ceilings where configured;
5. the user's enabled categories, entries and ordering;
6. the active View and optional Workflow-step narrowing overlay;
7. current authorization and contribution availability.
Lower scopes may narrow or reorder allowed entries but cannot enable a tool
blocked above them. A forced entry cannot be removed below its source. User
configuration stores stable contribution ids; unavailable or retired ids are
retained as explained stale preferences without rendering broken controls.
Configuration screens derive their available choices from the live registry.
Installing or enabling a contributing module adds its permitted choices;
disabling it removes the runtime tool while preserving harmless preferences.
If Quick Access is absent, contributors behave exactly as before.
## Interaction Model
Desktop uses a narrow right-side rail with at most four initial category
buttons and an overflow when an administrator or user adds more categories.
Selecting a category opens one fixed, owner-neutral drawer. Contributions are
shown inside that drawer as tabs, sections or commands according to the
category contract. The default drawer overlays content so DataGrid and fixed
workspace layouts do not resize unexpectedly; a later explicit pinned mode may
reserve layout width on sufficiently wide screens.
The drawer preserves host-page state, has a deterministic focus return, closes
with Escape, supports keyboard traversal, and provides explicit completion,
cancellation and full-page actions. Mobile and narrow layouts use the same
category/configuration semantics in a bottom sheet or compact menu.
## Product Areas
Product areas are stable configurable identities, not repositories. The
recommended baseline is:
- Work;
- Services and Cases;
- Records and Documents;
- Communication;
- Meetings and Decisions;
- Data and Assurance;
- People and Responsibility.
Modules contribute routes, objects, actions, widgets, work sources and help to
one or more areas. Product packages and administrators may define sensible
system and tenant defaults. Views select, order, rename or narrow allowed
areas, and users may personalize them within Policy ceilings. An empty area is
omitted. An area with one destination may open it directly. A multi-destination
area provides a useful work/recent/action surface rather than another menu.
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
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
The existing permission-derived module rail remains available as **All
available tools** for power users and deliberate escape from a focused View.
It contains only currently authorized destinations. Technical module,
capability, provider and package provenance remains visible in administration,
diagnostics, evidence and expandable details.
Search, deep links and help distinguish three states:
- available in the active View;
- authorized but outside the active View, with a temporary escape or View
switch;
- unavailable because of authorization, Policy, configuration or a missing
capability, with an actionable explanation.
## Delivery Order
1. Define Core product-area and Quick Access contracts and validation.
2. Implement `govoplan-quick-access` configuration, effective resolution and
shell capability.
3. Contribute Work, Calendar, Messages and Files bounded surfaces.
4. Add configurable product-area defaults through Views and product packages.
5. Migrate navigation, breadcrumbs, search, errors, documentation, dashboard
and administration toward product terminology.
6. Prove keyboard, focus, responsive, optional-module and reference-journey
behavior before making it the ordinary-user default.
## Acceptance Criteria
- A user can configure allowed Quick Access categories and ordering without
gaining authority.
- System and tenant administrators can make entries available, forced or
unavailable with provenance.
- Mail, Postbox and another future channel can share Messages presentation
while retaining independent state and channel semantics.
- A reference journey can use a bounded tool and return without losing host
state or Workflow context.
- Product areas remain useful under sparse and rich permission sets and under
optional-module permutations.
- All available tools and technical provenance remain deliberately reachable.
@@ -7,6 +7,11 @@ responsibility, or workflow step. A View can reduce the visible modules,
navigation entries, routes, page sections, and commands to the interface
needed for the current job.
Views also project configurable product areas and Quick Access contributions.
They may select, order, rename or hide permitted presentation identities but
do not move ownership or merge Mail, Postbox, Files, Calendar, Tasks or other
domain state.
Views are optional. If `govoplan-views` is not installed or enabled, the normal
permission-derived interface remains unchanged.
@@ -119,6 +124,12 @@ Implemented in the initial Views slice:
prevention
- surface declarations for every currently installed module that contributes a
WebUI, including finer-grained shared administration and settings surfaces
- immutable presentation settings for grouped or flat navigation, product-area
order and product-area labels; the shell resolves these settings through the
same system, tenant, group, user and Workflow-aware View projection
- live product-area identities from module manifests, with authorized
unclassified destinations retained under More tools during incremental
adoption
Still intentionally separate:
@@ -127,6 +138,11 @@ Still intentionally separate:
- read-only and layout-replacement projections beyond the version `1`
visible/hidden contract
Quick Access ordering and availability remain owned by
`govoplan-quick-access`; Views only narrow its declared surfaces for the active
task. Neither contract permits arbitrary layout or styling. See
`docs/architecture/QUICK_ACCESS_AND_PRODUCT_AREAS.md` in the meta repository.
## Gitea Work Packages
- `govoplan#17`: task-focused Views user story
@@ -1,5 +1,9 @@
# 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
`govoplan` meta repository and which should remain with their current runtime
owner.
@@ -148,7 +152,7 @@ It should not own:
Known references reviewed after the server-side rename:
- `govoplan/repositories.json`
- `govoplan/docs/REPOSITORY_STRUCTURE.md`
- `govoplan/docs/project/REPOSITORY_STRUCTURE.md`
- `govoplan/docker/README.md`
- `govoplan-core/docs/RELEASE_DEPENDENCIES.md`
- `govoplan-core/docs/MODULE_ARCHITECTURE.md`
@@ -1,5 +1,8 @@
# 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.
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.
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.
## Meta-Owned Content
@@ -0,0 +1,135 @@
# 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
GovOPlaN has not lost its central direction. The architecture now expresses a
coherent institutional governance platform, but architecture and repository
breadth have advanced faster than complete, usable outcomes. The immediate
need is convergence: fewer simultaneous fronts, stronger cross-cutting
adoption, and end-to-end reference journeys that non-developers can complete.
This is a dated review. Current status belongs in
[Strategy Status](../../strategy/STRATEGY_STATUS.md); stable direction belongs in
[Platform Core Ideas](../../strategy/PLATFORM_CORE_IDEAS.md).
## What Is Already Strong
- A modular runtime with manifests, capabilities, interfaces, migrations,
optional integrations, signed releases, and permutation checks.
- Explicit institutional semantics for identity, representation,
organization, function, mandate, service, case, party, approval, decision,
evidence, and record references.
- Governed communication foundations spanning Campaign, Mail, Files, Postbox,
Addresses, Distribution Lists, Templates, Audit, and Policy.
- Governed data foundations spanning Connectors, Datasources, Dataflow,
Reporting, Search, and immutable provenance.
- Bitemporal browsing, views, contextual documentation, action/effect
contracts, event delivery, recovery ledgers, and stateless deployment
contracts.
- A credible deployment and release foundation with signed artifacts and
reproducible composition evidence.
## Where The Program Veered
### Repository breadth preceded product proof
Logical modularity often became a repository before a reference journey proved
that an independent release boundary was required. Scaffolds are useful as
ownership markers, but their number makes the product appear broader and more
complete than its supported outcomes.
### Foundations outran reference gates
Later-stage contracts such as federation, encryption, formal governance,
deployment evidence, and broad module metadata were developed while basic
human-work and records journeys remained incomplete. Those foundations are not
wasted; they now need to be consumed by a small number of demonstrable
products.
### The module graph leaked into the experience
Navigation, routes, administration, errors, documentation, and configuration
often present module names and package structure directly. This is appropriate
for operators, but ordinary users should see work, services, records, and
outcomes.
This is not primarily a rail-length or scrolling problem. Sparse permissions
already reduce navigation and the complete technical rail remains useful for
power users. The correction is configurable product areas, task-focused Views
and a bounded Quick Access rail, while preserving deliberate access to every
authorized tool and technical provenance. The accepted design is maintained in
[Quick Access And Product Areas](../../architecture/QUICK_ACCESS_AND_PRODUCT_AREAS.md).
### Status became duplicated
Roadmaps, target architecture, fit assessments, issue comments, and release
documents each contained partial implementation snapshots. Their stable
decisions remain valuable, but volatile counts and maturity claims diverged.
### Too much work remained active simultaneously
The issue portfolio had many high-priority and in-progress items without
milestones. This reduces the signal of both labels and roadmap order and makes
completion harder to demonstrate.
## Where GovOPlaN Has Not Gone Far Enough
1. No composition has yet crossed the full `reference_ready` gate.
2. The human-work spine is incomplete: work queues, tasks, handoffs, deadlines,
reminders, escalation, and resumption need a coherent user experience.
3. Records and document management remain too shallow for a public-sector
operating platform.
4. Real target integrations and GovOPlaN-to-GovOPlaN federation are not yet
proven.
5. Temporal browsing, purpose-aware access, retention, and institutional
context exist as contracts but are not adopted uniformly by domain reads
and effects.
6. German completeness, contextual help, accessibility, responsive behavior,
and browser-level journey testing are not yet release gates everywhere.
7. Multi-host, backup/restore, provider interoperability, and independent
signed target evidence still require real environments and operators.
## Important Omissions
- a named first institution, bounded users, volumes, and operating constraints;
- measurable usability outcomes, not only functional tests;
- installable sector packages and migration/exit demonstrations;
- support, upgrade, deprecation, and LTS promises;
- complete assisted, paper, telephone, and in-person channel handling;
- a native eAkte/records model that can also overlay an external DMS or archive.
## Opportunities Beyond The Original Idea
- an institutional digital twin that exposes responsibilities, dependencies,
obligations, services, work, data, controls, and change impact over time;
- continuous assurance that evaluates controls and evidence as work happens;
- process mining and conformance analysis over governed event histories;
- federated product packages and inter-institution case/evidence exchange;
- accountable assistance that drafts and explains without obscuring authority;
- public evidence chains that disclose decisions and provenance without
exposing protected source data.
## Recommended Reset
1. Freeze new repositories unless a real journey proves an independent owner,
release lifecycle, security boundary, or optional installation need.
2. Use one generated maturity/status dashboard and one current status document.
3. Complete governed communication and function-bound Postbox against a real
target.
4. Complete the monthly-data journey, then sanctions screening on the same
data foundations.
5. Complete one browser-driven service-to-decision journey, including assisted
intake and records.
6. Make eAkte/records the next major product-depth program.
7. Tie feature work to a reference journey, a security/recovery gate, or a
measured usability defect.
## Success Criterion
The reset succeeds when a public institution can install a signed composition,
configure a named procedure, complete it through digital and assisted channels,
connect an external source, reconstruct the authority and evidence, recover it
after failure, and transfer or retire it without custom code.
+275 -5
View File
@@ -1,6 +1,6 @@
{
"$schema": "./capability-fit.schema.json",
"schema_version": "0.1.0",
"schema_version": "0.2.0",
"assessment_id": "campaign-reference-2026-07-22",
"assessed_at": "2026-07-22",
"scope": {
@@ -13,16 +13,30 @@
"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": {
"kind": "tagged_release",
"ref": "stable-catalog-202607220843",
"meta_commit": "5447299289a1",
"reproducible": true,
"configuration_packages": [],
"configuration_packages": [
"none: environment-profile basis only"
],
"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.",
"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": [
@@ -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": {
"scope_outcomes": [
{
@@ -203,6 +336,20 @@
"state": "answered",
"answer": "No; Workflow is planned and explicitly postponed.",
"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": [
@@ -219,6 +366,13 @@
"state": "not_assessed",
"answer": null,
"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": [
@@ -235,22 +389,50 @@
"state": "not_assessed",
"answer": null,
"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": [
{
"id": "workload.campaign",
"id": "workload.campaign_volume_peaks",
"question": "What are Campaign frequency, recipients per Campaign, send window, import size and attachment volume?",
"state": "not_assessed",
"answer": null,
"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?",
"state": "not_assessed",
"answer": null,
"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": [
@@ -267,6 +449,13 @@
"state": "not_assessed",
"answer": null,
"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": [
@@ -754,6 +943,63 @@
"recommendation": "Use target-native secret injection and document rotation/recovery.",
"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",
"requirement": "Detect API, database, worker, queue, storage and delivery degradation.",
@@ -780,6 +1026,30 @@
"recommendation": "Integrate external monitoring before small production.",
"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",
"requirement": "Back up and restore database, files, configuration and keys as a coherent service.",
+66 -1
View File
@@ -9,9 +9,13 @@
"assessment_id",
"assessed_at",
"scope",
"facts",
"decisions",
"release",
"composition",
"deployment_profile",
"scenarios",
"functional_context",
"questionnaire",
"capabilities",
"infrastructure",
@@ -28,7 +32,7 @@
"format": "uri-reference"
},
"schema_version": {
"const": "0.1.0"
"const": "0.2.0"
},
"assessment_id": {
"$ref": "#/$defs/non_empty_string"
@@ -54,6 +58,8 @@
}
}
},
"facts": { "$ref": "#/$defs/string_list" },
"decisions": { "$ref": "#/$defs/string_list" },
"release": {
"type": "object",
"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": {
"type": "object",
"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": {
"type": "object",
"additionalProperties": false,
@@ -259,6 +323,7 @@
"status": { "$ref": "#/$defs/status" },
"evidence": {
"type": "array",
"minItems": 1,
"items": { "$ref": "#/$defs/evidence" }
},
"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,4 +1,16 @@
# 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
> Campaign composition below. It is intentionally not updated to describe later
> 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
> target composition.
## Assessment record
@@ -11,16 +23,16 @@
| Configuration basis | Root `.env.example` and the production-like development profile |
| Scope | Campaign-centric internal pilot and small-production candidate |
| Explicitly postponed | Workflow and workflow-driven user stories |
| Machine-readable companion | [`capability-fit-current.json`](capability-fit-current.json) |
| Input schema | [`capability-fit.schema.json`](capability-fit.schema.json) |
| Machine-readable companion | [`capability-fit-current.json`](../../capability-fit-current.json) |
| Input schema | [`capability-fit.schema.json`](../../capability-fit.schema.json) |
**Snapshot notice:** this assessment remains valid only for the pinned
2026-07-22 composition above. Workflow Engine, the optional Workflow editor,
Datasources, Dataflow, Search, encryption contracts, and other later main-branch
work must not be inferred into this evidence record. The current product
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/STRATEGY_STATUS.md).
This is a fit assessment, not a production approval or security certification.
It deliberately does not infer implementation from a repository, issue, or
@@ -398,7 +410,7 @@ that observation to the assessment and signed catalog:
```
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
with `govoplan-`, compares the enabled assessed package and module-manifest
versions, and identifies missing, duplicate and extra GovOPlaN distributions.
@@ -481,14 +493,14 @@ unchecked boundary.
Installed evidence cannot establish target acceptance, accessibility, privacy,
security, operations, recovery, an external provider, or production use. These
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
installed-evidence SHA-256 digest. It contains only opaque subject/control/result
IDs and content hashes, not endpoints, credentials, people or raw result files.
Boundary evidence is accepted only when at least one Ed25519 signature validates
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,
accessibility, privacy, security, operations, recovery, and provider claims use
`passed` or `failed`; production claims use `approved` or `rejected`.
@@ -505,7 +517,7 @@ the tool's deterministic canonicalization.
`tools/assessments/boundary-evidence.py` is the bounded issuance path. It
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,
and excludes all paths and raw results from the signed receipt. Issuance is
refused unless an independently trusted catalog, exact installed payload,
@@ -515,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
both the proof and a sanitized review. The complete operator procedure and
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
./.venv/bin/python tools/assessments/capability-fit.py \
@@ -593,7 +605,7 @@ separate from production approval and from provider-specific acceptance.
Both authority keyrings are governance trust roots. Installer receipt keys use
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
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
@@ -617,9 +629,9 @@ journey, source tests, or signed release metadata.
## Evidence used in this slice
- [Production-like profile](../dev/production-like/README.md) and
[Compose dependencies](../dev/production-like/docker-compose.yml)
- [Module contracts and install boundaries](MODULE_CONTRACTS_AND_INSTALLS.md)
- [Production-like profile](../../../dev/production-like/README.md) and
[Compose dependencies](../../../dev/production-like/docker-compose.yml)
- [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)
- [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
@@ -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: 49
- Reviewed no-store rationales: 23
- 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` | Migration-owned | Provider | Provider `privacy.dsar.tenancy` is registered and documented. |
| `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
> **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
language. It records code contributions, not an assertion that every listed
surface is complete, enabled in a deployment, usable, or compliant.
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
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
manifest evidence. This hand-maintained document remains the reviewed product
interpretation and rollout ledger; generated evidence does not replace it.
manifest evidence. This hand-maintained document is the reviewed interpretation
of that snapshot; generated evidence does not retroactively change it.
Snapshot refreshed: 2026-08-03.
@@ -58,12 +63,12 @@ Inventory states:
| Surface | Owner and code evidence | Audience/access evidence | Primary task and target archetype | Audit / rollout |
| --- | --- | --- | --- | --- |
| Public landing and login | `govoplan-core` `PublicLandingPage`; rendered while no authenticated principal exists | Unauthenticated; maintenance and backend-reachability context are shell inputs | Understand the service and authenticate; public entry | Unreviewed; later public-entry audit |
| Session/bootstrap state | `govoplan-core` `App.tsx` and `AppShell` | All browser sessions during bootstrap | Understand that session/platform state is loading; state contract | Unreviewed; core shell |
| `/` authenticated redirect | `govoplan-core` chooses the first visible navigation destination | Authenticated; result depends on visible nav contributions | Enter the actor's first accessible service area; navigation behavior, not a content page | Unreviewed; focused-view/default-route work must preserve this fallback |
| `/dashboard` fallback | `govoplan-core` `DashboardPage` only when the Dashboard module is absent | Authenticated; no route-specific scope in core | Cross-module starting point; dashboard | Unreviewed; compare with module dashboard before shared changes |
| Public landing and login | `govoplan-core` `PublicLandingPage`; rendered while no authenticated principal exists | Unauthenticated; maintenance and backend-reachability context are shell inputs | Understand the service and authenticate; public entry | Core shell contract complete under Core #227: semantic entry/login, uniform reachable/offline/maintenance feedback, keyboard focus, responsive layout and privacy-safe pre-authentication state |
| Session/bootstrap state | `govoplan-core` `App.tsx` and `AppShell` | All browser sessions during bootstrap | Understand that session/platform state is loading; state contract | Core shell contract complete: loading, unreachable, maintenance, authentication-required and module-load failure states use shared status/alert boundaries without erasing the shell |
| `/` authenticated redirect | `govoplan-core` chooses the first visible navigation destination | Authenticated; result depends on visible nav contributions | Enter the actor's first accessible service area; navigation behavior, not a content page | Core route/module-permutation contract complete; permission, module, View and fallback filtering precede navigation and do not execute a domain action |
| `/dashboard` fallback | `govoplan-core` `DashboardPage` only when the Dashboard module is absent | Authenticated; no route-specific scope in core | Cross-module starting point; dashboard | Core fallback and Dashboard module permutations complete; fallback remains usable without the optional Dashboard module |
| `/settings` | `govoplan-core` `SettingsPage` | Authenticated; contributed sections and integrations filter internally | Profile, UI/workspace preference, local connection, and user-scoped integration settings; configuration | Core-owned pattern migration complete in [Core #225](https://git.add-ideas.de/GovOPlaN/govoplan-core/issues/225), commit `fa32cca` |
| Shell chrome | `AppShell`, `Titlebar`, `IconRail`, `BreadcrumbBar`, `HelpMenu`, language menu, unsaved-change provider | Public/authenticated variants; nav filtered later | Tenant/actor context, global navigation, help, language, session and maintenance state | Unreviewed; platform-owned prerequisite for focused views |
| Shell chrome | `AppShell`, `Titlebar`, `IconRail`, `BreadcrumbBar`, `HelpMenu`, language menu, unsaved-change provider | Public/authenticated variants; nav filtered later | Tenant/actor context, global navigation, help, language, session and maintenance state | Core shell contract complete under Core #227/#225 and Views #2: semantic global controls, scroll-safe rail, visible maintenance state, guarded navigation, configured Docs fallback, optional Search, responsive/theme/i18n checks and module permutations |
## Direct Module Route Contributions
@@ -78,15 +83,15 @@ semantics as authenticated navigation routes.
| `/address-book` | Addresses | `addresses:contact:read` | Governed source directory, contact/list detail, external-provider operation, governance facts, and reversible correction | Addresses pattern migration complete in [Addresses #23](https://git.add-ideas.de/GovOPlaN/govoplan-addresses/issues/23), commit `f9a7185` |
| `/approvals` | Approvals | `approvals:workspace:read` | Work queue/guided decision | Approvals pattern migration complete in [Approvals #3](https://git.add-ideas.de/GovOPlaN/govoplan-approvals/issues/3), commit `24e9559` |
| `/calendar` | Calendar | `calendar:event:read` | Full-height calendar workspace with filterable collection/agenda sidebar, continuous and bounded date views, guarded VEVENT and source editors, synchronized-source status, durable outbox recovery, and destructive remote-move evidence | Calendar pattern migration complete in [Calendar #22](https://git.add-ideas.de/GovOPlaN/govoplan-calendar/issues/22), commit `d7fd944` |
| `/campaigns`, `/campaigns/:campaignId/*`, `/campaigns/queue`, `/campaigns/reports`, `/templates` | Campaign | Campaign read/report/control scopes; template route has no route guard | List-detail, guided review, monitoring, reporting | [Campaign #74](https://git.add-ideas.de/GovOPlaN/govoplan-campaign/issues/74) |
| `/operator` | Campaign | Campaign read plus queue/control scope | Compatibility redirect to `/campaigns/queue` | Campaign #74; retire under the compatibility policy |
| `/campaigns`, `/campaigns/:campaignId/*`, `/campaigns/queue`, `/campaigns/reports` | Campaign | Campaign read/report/control scopes | List-detail, guided review, monitoring, reporting | Campaign pattern pilot complete in [Campaign #74](https://git.add-ideas.de/GovOPlaN/govoplan-campaign/issues/74); bounded product features such as watched-folder policy remain independently tracked |
| `/operator` | Campaign | Campaign read plus queue/control scope | Compatibility redirect to `/campaigns/queue` | Campaign #74 complete; redirect remains declared for saved links and is retired under the compatibility policy rather than through the UI migration |
| `/cases`, `/cases/:caseId` | Cases | `cases:case:read` | Governed case directory and detail workspace with guarded OCC lifecycle editor, provider-owned references, immutable timeline/history, and confirmed object-access editor | Cases pattern migration complete in [Cases #4](https://git.add-ideas.de/GovOPlaN/govoplan-cases/issues/4), commit `43b4cc8` |
| `/committee` | Committee | `committee:workspace:read` | Governed workspace | Committee pattern migration complete in [Committee #2](https://git.add-ideas.de/GovOPlaN/govoplan-committee/issues/2), commit `e64af30` |
| `/dashboard` | Dashboard | No route-specific scope | View-specific personal workspace with module/permission-filtered widget library, guarded four-column composition, nested widget settings, server/browser fallback, and optimistic layout persistence | Dashboard pattern migration complete in [Dashboard #3](https://git.add-ideas.de/GovOPlaN/govoplan-dashboard/issues/3), commit `da3947f` |
| `/dataflow` | Dataflow | Pipeline read/admin | Governed library, guarded graph/constrained-SQL definition editor, typed node inspector, bounded intermediate preview, automation triggers, and durable run/deployment evidence | Dataflow pattern migration complete in [Dataflow #20](https://git.add-ideas.de/GovOPlaN/govoplan-dataflow/issues/20), commit `109ddcd` |
| `/datasources` | Datasources | Catalogue read/source admin | Governed catalogue, staging preflight, optional-origin directory, authority editor, and immutable evidence | Datasources pattern migration complete in [Datasources #7](https://git.add-ideas.de/GovOPlaN/govoplan-datasources/issues/7), commit `6406ce7` |
| `/distribution-lists` | Distribution Lists | List read/write/admin | Governed directory, immutable-revision editor, expansion preview, and evidence register | Distribution Lists pattern migration complete in [Distribution Lists #8](https://git.add-ideas.de/GovOPlaN/govoplan-dist-lists/issues/8), commit `6cdd804` |
| `/docs` | Docs | Documentation or settings read | Documentation/reference | [Docs #15](https://git.add-ideas.de/GovOPlaN/govoplan-docs/issues/15) |
| `/docs` | Docs | Documentation or settings read | Documentation/reference | Configured-system workflow/reference/pattern help complete in [Docs #15](https://git.add-ideas.de/GovOPlaN/govoplan-docs/issues/15), commit `abe2f78` |
| `/files` | Files | `files:file:read` | Directory/explorer | Files pattern migration complete in [Files #42](https://git.add-ideas.de/GovOPlaN/govoplan-files/issues/42), commit `d8ae506` |
| `/forms` | Forms | `forms:definition:read` | Definition library/editor | Forms pattern migration complete in [Forms #4](https://git.add-ideas.de/GovOPlaN/govoplan-forms/issues/4), commit `e505536` |
| `/forms-runtime`, `/forms-runtime/:instanceId` | Forms Runtime | Participate or workspace read | Guided form execution | Forms Runtime pattern migration complete in [Forms Runtime #5](https://git.add-ideas.de/GovOPlaN/govoplan-forms-runtime/issues/5), commit `07dd35b` |
@@ -95,17 +100,38 @@ semantics as authenticated navigation routes.
| `/notifications` | Notifications | `notifications:notification:read` | Inbox/list-detail with guarded recipient state, confirmed local cancellation/dispatch, and sanitized delivery evidence | Notifications pattern migration complete in [Notifications #4](https://git.add-ideas.de/GovOPlaN/govoplan-notifications/issues/4), commit `ad6a31f` |
| `/ops` | Ops | Operations or settings read | Monitoring/evidence with contextual run, drain, readiness-blocker, and recovery guidance | Ops pattern migration complete in [Ops #4](https://git.add-ideas.de/GovOPlaN/govoplan-ops/issues/4), commit `2b32643` |
| `/organizations` | Organizations | Model/unit/function or settings read | Directory/hierarchy editor | Organizations pattern migration complete in [Organizations #7](https://git.add-ideas.de/GovOPlaN/govoplan-organizations/issues/7), commit `97acfcb` |
| `/portal` | Portal | `portal:service:read` | Service portal | [Portal #2](https://git.add-ideas.de/GovOPlaN/govoplan-portal/issues/2) |
| `/portal` | Portal | `portal:service:read` | Explained service directory and governed handoff | Portal pattern migration complete in [Portal #2](https://git.add-ideas.de/GovOPlaN/govoplan-portal/issues/2); durable evidence in `govoplan-portal/docs/INTERFACE_PATTERN_MIGRATION.md` |
| `/postbox` | Postbox | `postbox:postbox:read` | Inbox/list-detail | Postbox pattern migration complete in [Postbox #26](https://git.add-ideas.de/GovOPlaN/govoplan-postbox/issues/26), commit `a97eb3b` |
| `/projects` | Projects | `projects:project:read` | List-detail/project workspace | [Projects #2](https://git.add-ideas.de/GovOPlaN/govoplan-projects/issues/2) |
| `/reporting`, `/reports` | Reporting | `reporting:definition:read` | Reporting/definition library | [Reporting #8](https://git.add-ideas.de/GovOPlaN/govoplan-reporting/issues/8) |
| `/projects` | Projects | `projects:project:read` | Revisioned list-detail/project workspace | Projects pattern migration complete in [Projects #2](https://git.add-ideas.de/GovOPlaN/govoplan-projects/issues/2); durable evidence in `govoplan-projects/docs/INTERFACE_PATTERN_MIGRATION.md` |
| `/reporting`, `/reports` | Reporting | `reporting:definition:read` | Governed report catalogue, analytical workspace and evidence | Reporting pattern migration complete in [Reporting #8](https://git.add-ideas.de/GovOPlaN/govoplan-reporting/issues/8); durable evidence in `govoplan-reporting/docs/INTERFACE_PATTERN_MIGRATION.md` |
| `/risk-compliance` | Risk Compliance | Workspace or sanctions read | Immutable source evidence, version-pinned screening, list-detail review, and revisioned assurance graph with explicit blockers and consequences | Risk Compliance pattern migration complete in [Risk Compliance #8](https://git.add-ideas.de/GovOPlaN/govoplan-risk-compliance/issues/8), commit `24d80a6` |
| `/scheduling` | Scheduling | `scheduling:schedule:read` | List-detail/guided decision | Scheduling pattern migration complete in [Scheduling #8](https://git.add-ideas.de/GovOPlaN/govoplan-scheduling/issues/8), commit `c17cbda` |
| `/scheduling/public/:requestId/:token` | Scheduling | Public signed token | Public participation | Scheduling #8 complete in `c17cbda` |
| `/search` | Search | `search:result:read` | Search overlay/results | [Search #4](https://git.add-ideas.de/GovOPlaN/govoplan-search/issues/4) |
| `/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` |
| `/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` |
| `/workflow` | Workflow | Definition read or instance admin | Graph editor/execution evidence | [Workflow #15](https://git.add-ideas.de/GovOPlaN/govoplan-workflow/issues/15) |
| `/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` |
## Final Module Closure Evidence
The final five module-owned work packages complete the 2026-08-03 rollout
snapshot. Their module documents are the durable detailed inventories; the
table below records the cross-product closure evidence.
| Owner | Dominant archetype and consequential boundary | Focused evidence |
| --- | --- | --- |
| Workflow | Definition list-detail plus specialized native BPMN editor; save/activate/archive/delete/reset and instance transitions remain revisioned, confirmed, and Engine-owned | Shared dialogs/status/alerts/help, dirty-navigation guard, keyboard palette insertion, edge inspector alternative, responsive/reduced-motion contract, TypeScript and focused structure test |
| Search | Focus-contained global/context overlay plus URL-stable full results; filters only narrow permission-aware source results | F3/Ctrl/Cmd+K, listbox keyboard navigation, provider-partial diagnostics, shared controls/help, narrow layout and focused overlay/interface tests |
| Reporting | Three-region governed analytical workspace; runs, schedules, exports and publications retain purpose, permission, source and policy provenance | Shared grid/dialog/status/help, keyboard-explainable Run blockers, responsive task order, provider/semantic backend tests and focused interface test |
| Projects | Revisioned list-detail planning workspace; visibility and saves are ACL/OCC-governed and retain a change reason | Shared dialog/status/help/field labels, save errors attached to the editor, semantic list controls, responsive/focus contract and focused interface test |
| Portal | Explained service directory and exact-revision provider handoff; Portal never owns the launched case/form/workflow effect | Shared status/alert/toggle/help/blocker controls, stable disabled Open action with actor/action/destination, guarded navigation, responsive layout and focused interface test |
Future WebUI modules and newly added routes are not grandfathered by this
snapshot. They must meet the same surface definition of done in their owning
feature issue and pass the source/runtime inventory gates; they do not reopen
this finite migration program unless the pattern contract itself changes.
## Manifest And Runtime Route Alignment
@@ -155,10 +181,10 @@ enabled and the actor passes the declared filters.
| `/settings` | Core host | Profile; interface; workspace; local connection | Personal configuration with adaptive forms and immediate feedback | Pattern migration complete in Core #225 (`fa32cca`) |
| `/settings` | Files and Mail named capabilities | User-scoped file connections and mail profiles/policy | Optional integration regions disappear cleanly when capability absent | Files #42, Mail #20 and Core #225 complete |
| `/admin` and `/settings` | `govoplan-views` `admin.sections`, `settings.sections`, and `views.runtime` | System/tenant definition and assignment editors, personal/group editors, global selector | Versioned presentation projection with inheritance, lockout safeguards, optional directory targets, and no authorization effect | Pattern migration, contextual help, localized selector/editor, guarded drafts, explained inherited/permission/capability states, and focused evidence complete in Views #2 (`c125f33`) |
| `/settings` | `govoplan-notifications` `settings.sections` | Notification preferences | Personal configuration | Unreviewed |
| `/dashboard` | Dashboard host and `dashboard.widgets` | Installed-modules widget; Ops health widget when Ops contributes it | Widget ordering, staleness, permissions, destination behavior | Unreviewed |
| `/settings` | `govoplan-notifications` `settings.sections` | Notification preferences | Personal configuration | Pattern migration, contextual help, permission/target explanation, typed toggles and focused evidence complete in Notifications #4 (`ad6a31f`) |
| `/dashboard` | Dashboard host and `dashboard.widgets` | Installed-modules widget; Ops health widget when Ops contributes it | Widget ordering, staleness, permissions, destination behavior | Pattern migration, view-aware composition, keyboard/drag alternatives, responsive packing, module filtering and focused evidence complete in Dashboard #3 (`da3947f`) |
| `/organizations` | IDM `organizations.functionActions` | Action leading to assignment view filtered by IDM scopes | Cross-module context action through explicit capability | IDM pattern migration complete in IDM #12 (`d864317`) |
| Campaign attachments/import | Files `files.fileExplorer` | Folder tree, managed chooser, file listing/pattern resolution/sharing | Optional domain composition without sibling-private imports | Pilot audit under Campaign #74 |
| Campaign attachments/import | Files `files.fileExplorer` | Folder tree, managed chooser, file listing/pattern resolution/sharing | Optional domain composition without sibling-private imports | Campaign #74 pilot complete; watched-folder and duplicate-attachment product policy remain independent Campaign #60/#61 features |
| Campaign review/send | Mail runtime `mail.devMailbox` | Mock-mail verification when backend advertises runtime capability | Optional review stage with unavailable/optional states | Explicit intervention and review-progress vocabulary delivered in Campaign #63; send modes/progress delivered in #62/#79 |
Other named capability exports (`files.connectors`, `organizations.functionPicker`,
@@ -262,26 +288,25 @@ prove that the composition or states satisfy the pattern.
| Surface / code evidence | Primary task | Target pattern | Material consequence/state | Known issue / rollout |
| --- | --- | --- | --- | --- |
| Campaign list (`CampaignListPage`) | Find, compare, create, open | List-detail entry | Campaign lifecycle/status and creation | Audit in [#74](https://git.add-ideas.de/GovOPlaN/govoplan-campaign/issues/74); guided entry [#35](https://git.add-ideas.de/GovOPlaN/govoplan-campaign/issues/35) |
| Overview (`CampaignOverviewPage`) | Understand/edit campaign identity, version, access, lifecycle | Object overview plus adaptive edit | Lock/archive/delete/access changes need real consequence and reversibility wording | #74 remaining audit |
| Fields (`CampaignFieldsPage`) | Define recipient/template field schema | Structured editor | Schema changes can invalidate recipient/template data | #74 audit |
| Attachments/files (`AttachmentsDataPage`, `AttachmentRulesOverlay`) | Select sources and attachment/ZIP rules | Directory chooser plus adaptive rule editor | Missing or mismatched files affect built messages | #74; attachment-detail [#59](https://git.add-ideas.de/GovOPlaN/govoplan-campaign/issues/59) |
| Recipients (`RecipientDataPage`) | Select/import/map/edit recipients, address fields and per-recipient values/files | Import/mapping plus list-detail editor | Personal data, validation, bulk activation, file links | Consolidated editor delivered in #67; #74 remaining audit and guided entry #35 |
| Template (`TemplateDataPage`, placeholder/expression dialogs) | Author subject/body and preview substitutions | Adaptive editor plus stable preview | Generated communication content and unresolved expressions | #74; stable overlay [#73](https://git.add-ideas.de/GovOPlaN/govoplan-campaign/issues/73) |
| Mail settings (`MailSettingsPage` settings view) | Select/configure campaign mail transport | Adaptive configuration | Credentials, SMTP/IMAP destinations, test outcomes | #74; align with Core #225 mail pattern |
| Campaign settings (`GlobalSettingsPage` settings view) | Configure campaign behavior | Adaptive configuration | Can alter validation/build/send behavior | #74 audit |
| Mail policy (`MailSettingsPage` policy view) | Inspect/override effective mail policy | Effective policy/provenance editor | Inheritance and locks affect allowed delivery | #74; Core #225 policy pattern |
| Campaign policy (`GlobalSettingsPage` policy view) | Inspect/override campaign policy | Effective policy/provenance editor | Inheritance, actor authority, and blocked edits | #74; Core #225 policy pattern |
| Review/send (`ReviewSendPage`) | Validate, build, mock-test, confirm/send, inspect results | Guided review/decision plus durable progress | External communication, bounded synchronous execution, persisted queue mode, partial effects, retries, evidence | Blocking/non-blocking interventions and reviewed/remaining evidence delivered in [#63](https://git.add-ideas.de/GovOPlaN/govoplan-campaign/issues/63); bounded synchronous and explicit/persisted queued modes delivered in [#62](https://git.add-ideas.de/GovOPlaN/govoplan-campaign/issues/62) and [#79](https://git.add-ideas.de/GovOPlaN/govoplan-campaign/issues/79); #74 audit remains |
| Campaign list (`CampaignListPage`) | Find, compare, create, open | List-detail entry | Campaign lifecycle/status and creation | #74 and guided entry [#35](https://git.add-ideas.de/GovOPlaN/govoplan-campaign/issues/35) complete |
| Overview (`CampaignOverviewPage`) | Understand/edit campaign identity, version, access, lifecycle | Object overview plus adaptive edit | Lock/archive/delete/access changes expose consequence, reversibility, owner/access and lifecycle evidence | #74 complete; lifecycle policy is independently extended in Campaign #26 |
| Fields (`CampaignFieldsPage`) | Define recipient/template field schema | Structured editor | Schema changes can invalidate recipient/template data | #74 complete |
| Attachments/files (`AttachmentsDataPage`, `AttachmentRulesOverlay`) | Select sources and attachment/ZIP rules | Directory chooser plus adaptive rule editor | Missing or mismatched files affect built messages | #74 and attachment-detail [#59](https://git.add-ideas.de/GovOPlaN/govoplan-campaign/issues/59) complete |
| Recipients (`RecipientDataPage`) | Select/import/map/edit recipients, address fields and per-recipient values/files | Import/mapping plus list-detail editor | Personal data, validation, bulk activation, file links | Consolidated editor #67, guided entry #35 and #74 audit complete; independent bulk action #68 remains product scope |
| Template (`TemplateDataPage`, placeholder/expression dialogs) | Author subject/body and preview substitutions | Adaptive editor plus stable preview | Generated communication content and unresolved expressions | #74 and stable overlay [#73](https://git.add-ideas.de/GovOPlaN/govoplan-campaign/issues/73) complete |
| Mail settings (`MailSettingsPage` settings view) | Select/configure campaign mail transport | Adaptive configuration | Credentials, SMTP/IMAP destinations, test outcomes | #74 and Core #225 shared mail pattern complete; final credential hierarchy remains Mail #10 |
| Campaign settings (`GlobalSettingsPage` settings view) | Configure campaign behavior | Adaptive configuration | Can alter validation/build/send behavior | #74 complete |
| Mail policy (`MailSettingsPage` policy view) | Inspect/override effective mail policy | Effective policy/provenance editor | Inheritance and locks affect allowed delivery | #74 and Core #225 effective-policy pattern complete |
| Campaign policy (`GlobalSettingsPage` policy view) | Inspect/override campaign policy | Effective policy/provenance editor | Inheritance, actor authority, and blocked edits | #74 and Core #225 effective-policy pattern complete |
| Review/send (`ReviewSendPage`) | Validate, build, mock-test, confirm/send, inspect results | Guided review/decision plus durable progress | External communication, bounded synchronous execution, persisted queue mode, partial effects, retries, evidence | Interventions #63, send/progress #62/#79 and #74 wording/accessibility audit complete |
| Message and attachment detail overlays | Inspect one built/mock message and its attachment links | Stable detail/review dialog | Personal data, exact outbound content, reviewed state | Delivered and verified in [#59](https://git.add-ideas.de/GovOPlaN/govoplan-campaign/issues/59) and [#73](https://git.add-ideas.de/GovOPlaN/govoplan-campaign/issues/73) |
| Campaign report (`CampaignReportPage`) | Filter and inspect delivery outcomes | Reporting/list-detail | Partial, failed, explicitly excluded/skipped, SMTP/IMAP outcomes and retries | Server-owned filtering and counts delivered in [#65](https://git.add-ideas.de/GovOPlaN/govoplan-campaign/issues/65) with the full-result DataGrid contract from [Core #263](https://git.add-ideas.de/GovOPlaN/govoplan-core/issues/263); excluded semantics in [#66](https://git.add-ideas.de/GovOPlaN/govoplan-campaign/issues/66) |
| Audit (`CampaignAuditPage`) | Inspect campaign evidence/history | Provenance timeline/report | Actor/action/effect trace | #74 audit |
| JSON (`CampaignJsonView`) | Inspect expert representation | Advanced diagnostics/reference | Raw data may contain personal/configuration values; not a primary editor | #74 privacy/redaction audit |
| Audit (`CampaignAuditPage`) | Reach campaign evidence/history | Explained provenance handoff | Campaign emits platform evidence; Audit owns reading, retention and bundles | #74 complete as an explicit Audit handoff; object-scoped projection may follow Audit #3 without a sibling-private import |
| JSON (`CampaignJsonView`) | Inspect/download expert representation | Advanced diagnostics/reference | Full authorized configuration may contain personal data but no inline transport secrets | #74 privacy audit complete with explicit sensitivity warning and campaign-read boundary |
| Create wizard (`CreateWizard`) | Seed a campaign through basics, sender, fields, recipients, template, attachments, review, send | Guided setup | Current steps mix creation and later consequential delivery; completion semantics need audit | Guided first campaign [#35](https://git.add-ideas.de/GovOPlaN/govoplan-campaign/issues/35) |
| Review/send wizard routes | Alternate guided review/send shells | Guided review | Tracked routes exist; implementation relationship to `ReviewSendPage` must be established, not guessed | #74 inventory decision |
| Operator queue (`OperatorQueuePage`) | Monitor jobs and intervene | Monitoring/work queue | Campaign/version/job identity, historical active-version discovery, fixed action positions, authority-aware disabled states, exact non-overlapping queue counts, server-paged jobs, bounded refresh, retry/queue/reconcile per version, campaign-wide pause/resume/cancel, and leave/return progress | Durable operator controls delivered in [#78](https://git.add-ideas.de/GovOPlaN/govoplan-campaign/issues/78); #74 wording/accessibility audit remains |
| Review/send wizard routes | Focus the canonical review or send stage | Guided review | Thin wrappers render the same `ReviewSendPage` with a stable initial stage; no parallel workflow state exists | #74 inventory decision complete |
| Operator queue (`OperatorQueuePage`) | Monitor jobs and intervene | Monitoring/work queue | Campaign/version/job identity, historical active-version discovery, fixed action positions, authority-aware disabled states, exact non-overlapping queue counts, server-paged jobs, bounded refresh, retry/queue/reconcile per version, campaign-wide pause/resume/cancel, and leave/return progress | Durable controls #78 and #74 wording/accessibility audit complete |
| Aggregate reports (`AggregateReportsPage`) | Compare cross-campaign delivery outcomes | Privacy-preserving aggregate reporting | Tenant/campaign ACL, deployment/tenant small-cell policy, complementary and overlapping-cell suppression, explicit denominator, and no recipient detail/diagnostics/export/drill-down | Separate aggregate-reader surface delivered in [#80](https://git.add-ideas.de/GovOPlaN/govoplan-campaign/issues/80); not parity with the permission-gated per-campaign detail report |
| Templates route (`TemplatesPage`) | Browse template records | Directory/list-detail | Template availability and later generated outputs | #74 audit; verify missing route guard intent |
The five review stages currently named in code are `Validate and inspect`,
`Build and review`, `Mock send and verify`, `Confirm and send`, and `Delivery
@@ -300,8 +325,8 @@ The generated manifest snapshot reports no WebUI package for:
`govoplan-learning`, `govoplan-mandates`, `govoplan-parties`,
`govoplan-permits`, `govoplan-poll`, `govoplan-procurement`,
`govoplan-records`, `govoplan-resources`, `govoplan-rest`,
`govoplan-services`, `govoplan-soap`, `govoplan-tickets`,
`govoplan-transparency`, `govoplan-wiki`, and `govoplan-workflow-engine`.
`govoplan-services`, `govoplan-soap`, `govoplan-transparency`, and
`govoplan-workflow-engine`.
Tenancy does provide composed administration surfaces despite having no direct
route. This section is only negative package evidence; connector-only,
@@ -313,7 +338,7 @@ make every module symmetrical.
| Order | Scope | Current evidence | Target | Owner / issue | Verification gate | Status |
| --- | --- | --- | --- | --- | --- | --- |
| 0 | Product grammar and route inventory | Doctrine, ledger, layout rules, module contract, current route sources | One reconciled pattern language and evidence inventory | Meta [#11](https://git.add-ideas.de/GovOPlaN/govoplan/issues/11) | Docs links/diff checks; issue/wiki sync after integration | Initial slice in this document |
| 0 | Product grammar and route inventory | Doctrine, ledger, layout rules, module contract, current route sources | One reconciled pattern language and evidence inventory | Meta [#11](https://git.add-ideas.de/GovOPlaN/govoplan/issues/11) | Reviewed route/component inventory, module documents, manifest shapes and focused contracts | Complete 2026-08-03 |
| 1 | Campaign baseline integration | Recipient-editor WIP and tracker state have been reconciled with remote `main` | Integrated, testable baseline before migration claims | Campaign #67 and tracker cleanup | Backend and focused WebUI suites; issue evidence | Complete 2026-07-22 |
| 2 | Campaign previews/details | Stable shared dialog with bounded scrolling and fixed responsive preview workspace | Stable header/body/footer, accessible long-content detail | Campaign #59 and #73 | Review-preview and overlay structure tests | Complete 2026-07-22 |
| 3 | Campaign review/interventions | Five domain-owned stages use central blocker and guided-review primitives; validation/build warnings name action, actor, and destination; hard blockers, individual review, and group review remain distinct; reviewed/remaining counts survive reload through build-bound review evidence | Clear stages, outcomes, blockers, next actor/action, reviewed evidence | Campaign #63 | `reviewProgress` state tests, shared-component structure contract, TypeScript build, configured-system help topic, and Campaign documentation tests | Complete 2026-08-03 (`d635f3a`; Core primitives and contextual help `b823a22`) |
@@ -322,12 +347,12 @@ make every module symmetrical.
| 6 | Campaign operator recovery | A durable campaign/version queue page exposes historical work, exact non-overlapping state counts, persisted mode, permission-safe controls, server-paged job evidence, bounded refresh and active-state recovery | Fixed-position actions, disabled explanations, leave/return state, version-scoped retry/queue/reconcile and explicit campaign-wide pause/resume/cancel | Campaign #78 | Queue model/structure, historical-version, permission, paging, recovery-control, stale-response and delta tests | Complete 2026-07-22 (`21f3014`, `99d44ee`, `735e874`) |
| 7 | Campaign aggregate reports | A separate aggregate-reader projection and UI expose only policy-suppressed business totals with a stable status domain | Explicit denominator and exclusions, deployment floor plus tenant-strengthened small-cell threshold, complementary and overlapping-cell suppression, no detail/export/diagnostics | Campaign #80 | Aggregate query, cross-metric suppression, route/role/ACL, stable filter and UI structure tests | Complete 2026-07-22 (`06125cc`, `fc36aee`, `8ee87b7`, `ac3329c`, `1225802`) |
| 8 | Campaign excluded outcomes | Excluded build rows become explicit skipped transport outcomes and remain protected from queue/cancel/retry ambiguity | One durable source-to-job-to-report meaning with guarded historical normalization | Campaign #66 | Builder/persistence, migration, query/count, queue-control and report-explanation tests | Complete 2026-07-22 (`7229fb8`) |
| 9 | Guided first campaign | Existing wizard routes and ordinary workspace overlap | Task-oriented entry that hands off clearly to normal editing/review | Campaign #35 | First-run flow, resume/back, validation, optional modules, no implicit send | P1 after core pilot patterns stabilize |
| 9 | Guided first campaign | Eight-stage creation flow persists current step/draft and hands off to ordinary review/delivery preparation | Task-oriented entry that hands off clearly to normal editing/review | Campaign #35 | First-run flow, resume/back, partial validation, immutable-history and optional-module behavior, no implicit send | Complete 2026-07-30 |
| 10 | Prove/extract generic primitives | Shared consequence, focus, help, blocker, unsaved-change, confirmation, connection-tree and effective-policy contracts now have Core and multiple module consumers | Keep Core behavior-only and leave domain composition in owning modules | Core #225 plus bounded follow-ups | Core behavior/accessibility tests and module-permutation tests | Complete 2026-08-03 (`fa32cca`; Files `d8ae506`; Mail `7844d9c`) |
| 11 | Configured-system pattern help | Role/config-aware workflow, reference, pattern, and system topics are projected by Docs; shared route, field, blocker, and action links resolve to configured Docs or the hosted fallback | Stable configured-system guidance without feature-to-Docs imports | Docs #15 | Docs suite, shared component tests, Campaign review tests, 46 module permutations, full-product bundle budget | Complete 2026-08-03 (Docs `abe2f78`; Core `b823a22`; Campaign `d635f3a`) |
| 12 | Admin/configuration family | Core host/settings/credential/retention contracts, shared primitives, module lifecycle, Files, Mail, Policy, Access, Admin, Tenancy, Views, and Organizations are integrated and verified | Continue the same consequence/provenance grammar only through bounded module-owned migrations | Core #225 and module children | Per-surface state/accessibility/consequence evidence | Core #225 complete `fa32cca`; Access `1409dbf`; Files `d8ae506`; Mail `7844d9c`; Policy `f964ed7`; Admin `d428f33`; Tenancy `e76fe16`; Views `c125f33`; Organizations `97acfcb` |
| 13 | Remaining module surfaces | 33 bounded module-owned issues cover every WebUI contributor not already tracked by Campaign #74 or completed Docs #15 | Per-module audit and migration, ordered by user task and consequence rather than a bulk rewrite | Issues linked in the direct-route and composed-surface sections | Module-focused tests, manifest shapes, contextual Docs, and applicable definition-of-done gates | Scheduling `c17cbda`, Audit `6d3fcc1`, Access `1409dbf`, Files `d8ae506`, Mail `7844d9c`, Policy `f964ed7`, Admin `d428f33`, Tenancy `e76fe16`, Views `c125f33`, Organizations `97acfcb`, Postbox `a97eb3b`, IDM `d864317`, Committee `e64af30`, Approvals `24e9559`, Forms Runtime `07dd35b`, Forms `e505536`, Voting `2625990`, Distribution Lists `6cdd804`, Templates `72fafa2`, Addresses `f9a7185`, Datasources `6406ce7`, Dataflow `109ddcd`, Dashboard `da3947f`, Cases `43b4cc8`, Calendar `d7fd944`, Ops `2b32643`, Notifications `ad6a31f`, and Risk Compliance `24d80a6` complete |
| 14 | Manifest/runtime alignment | Several executable routes are absent from manifest metadata | Declared alignment or explicit validated exception | Core contract issue to create | Automated manifest/module route check and configured Docs verification | Discovery follow-up |
| 13 | Remaining module surfaces | 33 bounded module-owned issues cover every WebUI contributor not already tracked by Campaign #74 or completed Docs #15 | Per-module audit and migration, ordered by user task and consequence rather than a bulk rewrite | Issues linked in the direct-route and composed-surface sections | Module-focused tests, manifest shapes, contextual Docs, and applicable definition-of-done gates | Complete: prior 28 recorded commits plus Workflow #15, Search #4, Reporting #8, Projects #2 and Portal #2 verified 2026-08-03 |
| 14 | Manifest/runtime alignment | Authenticated canonical routes align; public signed-token and compatibility routes are explicit exceptions | Stable declarations reconcile with source and any effective runtime module combination | [Meta #25](https://git.add-ideas.de/GovOPlaN/govoplan/issues/25) | Strict duplicate/stale/undeclared declaration CI, per-module digests, and authorized read-only runtime inventory | Complete 2026-08-04 |
Workflow remains outside this rollout matrix because it has its own runtime and
editor workstream, not because it is postponed. Focused views can be specified,
+12
View File
@@ -353,6 +353,12 @@
"description": "GovOPlaN Projects module behavior or integration.",
"exclusive": false
},
{
"name": "module/records",
"color": "0052cc",
"description": "GovOPlaN Records and eAkte lifecycle behavior or integration.",
"exclusive": false
},
{
"name": "module/reporting",
"color": "c2e0c6",
@@ -365,6 +371,12 @@
"description": "GovOPlaN Risk Compliance module behavior or integration.",
"exclusive": false
},
{
"name": "module/quick-access",
"color": "c5def5",
"description": "GovOPlaN configurable task-local Quick Access behavior and integrations.",
"exclusive": false
},
{
"name": "module/search",
"color": "bfdadc",
@@ -16,8 +16,8 @@ deployment/evidence boundary.
The machine-readable contracts are:
- [`backup-evidence.schema.json`](backup-evidence.schema.json);
- [`backup-evidence-keyring.schema.json`](backup-evidence-keyring.schema.json).
- [`backup-evidence.schema.json`](../backup-evidence.schema.json);
- [`backup-evidence-keyring.schema.json`](../backup-evidence-keyring.schema.json).
One evidence document is bound to the installation id, deployment profile,
topology subject, exact signed release manifest, image digests, and composition
+120
View File
@@ -0,0 +1,120 @@
# GovOPlaN Deployment Profiles
## Purpose
GovOPlaN distinguishes how code is executed, where it is placed, and how mature
the target is. These are separate concerns:
- **execution basis:** editable source trees or an immutable signed release;
- **topology:** local processes, one-host containers, or a multi-host
orchestrator;
- **component ownership:** installer-managed or externally supplied state and
infrastructure services; and
- **assurance state:** development, rehearsal/acceptance, or approved
production.
PostgreSQL, Redis, object storage, mail and ingress choices are component
bindings inside a profile. They do not create a new application topology by
themselves.
## Canonical Profiles
| Profile | Entry point | Application execution | State services | Intended use | Explicit boundary |
| --- | --- | --- | --- | --- | --- |
| Local source development | `tools/launch/launch-dev.sh` | Editable Uvicorn/Vite processes with reload | Local development bindings, optionally the shared PostgreSQL helper | Fast module and UI work | No production packaging, isolation, availability or capacity claim |
| Split source integration | `tools/launch/launch-production-like-dev.sh` | Editable API, WebUI, worker and scheduler processes | Containerized PostgreSQL/Redis by default; environment bindings may point at developer-owned services | Queue, migration, Redis and split-role integration while retaining source reload | “Production-like” describes behavior, not immutable artifacts or a production security boundary |
| Immutable single-host rehearsal | `govoplan-deploy init/apply --profile evaluation` | Signed API/WebUI images and generated Compose roles | Bounded managed components or explicit external bindings | Test the downloadable artifacts, installer, migrations, load balancer and component choices | All containers and managed services may share one host and failure domain; evaluation conveniences are not production controls |
| Single-host production | `govoplan-deploy init/apply --profile self-hosted` | Signed API/WebUI images behind generated HAProxy and selected TLS ingress | Durable local/single-node managed services where accepted, or external services | Small and medium installations whose accepted availability boundary is one host | Multiple containers add capacity and rolling-process resilience, but do not survive host loss |
| Multi-host Kubernetes production | `govoplan-deploy render-kubernetes` or the guarded K3s lab/acceptance workflow | Immutable API, WebUI and queue-specific worker Deployments across failure domains | External PostgreSQL, Redis and S3-compatible storage; external secret and ingress control | Institution-scale availability and horizontal application-tier capacity | Production claims require independent nodes, HA state services, load/capacity evidence and signed recovery evidence |
Docker Compose services are containers or replicas, not Kubernetes pods. The
immutable single-host rehearsal is the appropriate Dockerized whole-product
test when source reload is not required.
The K3s VM lab has two modes over the same Kubernetes profile:
- `rehearsal` may place VMs on one physical hypervisor and proves bounded
orchestration behavior;
- `acceptance` requires independently controlled worker failure domains and can
contribute target evidence.
## Module composition and availability
Official immutable API and WebUI images carry the verified `full` package
profile. This is package availability, not runtime activation and not a license
or tenant entitlement. The signed distribution manifest records the complete
package composition; the desired module graph selects which installed modules
are active; tenant module policy applies unavailable/available/forced ceilings;
and Views/Policy control group and user presentation.
Local and single-host profiles may use the supervised installer to download a
signed catalog artifact into a private digest cache and mutate the local package
environment during maintenance. A multi-host/shared-state profile must never
change one replica in place. Its Admin install plan is a composition request:
publish and roll out a new signed image whose package lock contains the target,
then activate the module graph after all replicas report the same composition.
## Component Choices
The installer may manage a component where its bounded profile is appropriate,
or consume an operator-provided service:
| Component | Managed boundary | External/BYO boundary |
| --- | --- | --- |
| PostgreSQL | Single-host Compose database | Stable primary-aware endpoint supplied by a PostgreSQL provider/operator |
| Redis | Single-host persistent Redis | Tested HA Redis endpoint compatible with queues, throttling and coordination |
| File/object storage | Durable local storage or single-node Garage | Shared, redundant S3-compatible storage |
| Mail | Development GreenMail only | Institution/provider SMTP and IMAP services |
| Ingress/TLS | Generated Caddy on one host | Existing reverse proxy or Kubernetes ingress and secret management |
Switching to an external component changes ownership and evidence requirements;
it does not remove GovOPlaN's health, capacity, backup and recovery checks.
## Scaling Responsibilities
GovOPlaN scales application roles, while the infrastructure control plane owns
machines and state-service replication:
| Concern | Scaling model | Owner |
| --- | --- | --- |
| API and WebUI | Increase replicas behind health-aware Services/Ingress | GovOPlaN deployment desired state, reconciled by Compose or Kubernetes |
| Background work | Add queue-specific worker replicas and bounded concurrency | GovOPlaN deployment desired state and worker-pool configuration |
| Scheduler, migrations and module lifecycle | Singleton execution protected by database leases/fencing | GovOPlaN; these roles are never scaled as unfenced active-active workers |
| Kubernetes worker/control nodes | Add, drain, replace and upgrade machines; optionally use a cluster autoscaler | Kubernetes/platform operator, not the GovOPlaN application |
| PostgreSQL | Replication, failover, backups, connection pooling and stable writer endpoint | Database operator/provider; GovOPlaN currently consumes the stable endpoint and does not route arbitrary reads to replicas |
| Redis | Replication/failover, persistence, eviction and TLS/authentication | Redis operator/provider |
| S3-compatible storage | Placement, replication, repair and capacity | Storage operator/provider |
Administrators should eventually be able to review and change permitted
application replica and worker-pool desired state through the Ops surface.
Creating physical machines, database replicas or storage members remains an
orchestrator/provider action. GovOPlaN must observe their health and block unsafe
changes rather than becoming a second infrastructure scheduler.
Every scale change must recalculate the database connection budget, preserve
queue coverage, verify software/module-composition consistency and respect
drain and fencing state.
## What Has Been Proven
The current implementation and the signed `v0.1.18` rehearsal prove that the
application tier can run as stateless API, WebUI and worker replicas against
logically shared state. Two Kubernetes worker VMs hosted API and WebUI replicas,
and an API pod was replaced without an observed public-readiness failure.
This is not yet proof of general “large organization fit.” That claim also
requires:
- representative concurrent-user, dataset, report and background-job load
tests with latency and saturation budgets;
- independent physical failure domains and real ingress/network behavior;
- HA PostgreSQL, Redis and object storage with failover drills;
- session, accepted-job and provider-effect continuity under node and service
loss;
- coordinated backup/isolated restore, measured RTO/RPO and semantic recovery;
- observability, alerting, capacity forecasting and sustained soak evidence.
The profile therefore proves the architecture is horizontally deployable. A
specific institution is production-fit only after its target topology and load
envelope have produced the governed evidence described in
`TARGET_MATURITY_EVIDENCE_RUNBOOK.md`.
@@ -8,8 +8,12 @@ receives a working base system. Re-running the same tool repairs or
reconfigures that installation instead of creating unrelated state.
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.
The execution, topology, component-ownership and assurance modes are defined
canonically in [Deployment Profiles](DEPLOYMENT_PROFILES.md). In particular,
the editable production-like developer launcher is distinct from both an
immutable Compose rehearsal and a supported one-host production deployment.
## First Executable Slice
@@ -95,6 +99,8 @@ The private installation directory contains:
| `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 |
| `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-keyring.json` | Explicitly installed public trust anchor for runtime releases |
| `backup-evidence.json` | Signed provider-neutral coordinated backup and isolated-restore receipt |
@@ -106,7 +112,27 @@ The private installation directory contains:
| `.deployment.lock` | Same-host operation exclusion |
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:
@@ -195,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.
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
[`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
size and SHA-256 digest or publication fails.
@@ -221,26 +247,39 @@ references and archive hashes; mutable tags or incomplete bundles are rejected.
## Current Production Gates
The tool deliberately reports blockers instead of pretending the source tree is
a production distribution:
The first immutable production-distribution baseline is published as
[`v0.1.14`](https://git.add-ideas.de/GovOPlaN/govoplan/releases/tag/v0.1.14)
from source commit `1f039dd39c1ce2672f4978c8abc6dff862ef1445`. Runtime
Distribution [run #459](https://git.add-ideas.de/GovOPlaN/govoplan/actions/runs/459)
proved migrations, schema compatibility, non-root API/Web readiness, and worker
delivery/shutdown on both `linux/amd64` and `linux/arm64`. Its signed manifest
has SHA-256
`d703267e01855dee63200cb20921c91c3f95fbff550c8ca76e9a35cba3f69109`
and pins these runtime indexes:
1. **First publication.** The protected workflow and fail-closed artifact
contracts are implemented, but a release operator must configure the Gitea
registry/release tokens and runtime Ed25519 key, publish the first pinned
release, and retain its amd64/arm64 readiness evidence.
3. **First administrator.** Production needs a one-time, restricted enrollment
- API: `git.add-ideas.de/govoplan/runtime-api@sha256:197ed01790986f2bc927eaa5d8348fa118702e5d2dc05feb851fc2643c23764a`
- WebUI: `git.add-ideas.de/govoplan/runtime-web@sha256:e936cca124f1fad29a067834cf17627d4c236410fdc3fa129e0ccb26b8193812`
The signed bootstrap has SHA-256
`1ff946fba82b0895d153b23352d06e30fe18388450dfd37fed6fb9912310efc5`
and key id `runtime-distribution-2026-01`. The managed-ingress boundary passed
the same publication run and the independently dispatchable Runtime Ingress
Drill [run #458](https://git.add-ideas.de/GovOPlaN/govoplan/actions/runs/458).
Every later release must renew this evidence; the following target-specific
gates remain:
1. **First administrator.** Production needs a one-time, restricted enrollment
identity. The development bootstrap must not be enabled in production.
4. **Image/module composition.** The deployer now enforces the signed
2. **Image/module composition.** The deployer enforces the signed
composition. A selected module not shipped by that release cannot be
enabled.
5. **Deployment agent.** Web updates need a separate privileged reconciler with
3. **Deployment agent.** Web updates need a separate privileged reconciler with
a typed command allowlist. The API and browser must never receive the Docker
socket or arbitrary shell access.
6. **Ingress reachability evidence.** Managed Caddy ingress and the
4. **Target reachability evidence.** Managed Caddy ingress and the
existing-proxy contract are implemented. A production claim still requires
running `doctor` from the target host after public DNS/firewall changes and
retaining the first successful container drill and public TLS/readiness
evidence.
retaining public TLS/readiness evidence for that deployment.
`apply --allow-unverified-images` is therefore restricted to the evaluation
profile. It explicitly acknowledges both mutable image identities and
@@ -391,16 +430,30 @@ the supported topology and promotion path.
## Reconfiguration Semantics
`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.
- 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,
deliberately destructive workflow.
- Existing generated credentials are retained unless an explicit future rotate
operation is requested.
- Private configuration changes are represented by a keyed fingerprint in the
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
operation.
- Migrations run as a one-shot service before API/worker replacement.
@@ -413,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
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
failure before migration may restore a verified previous bundle. Once migration
starts, recovery is forward-only unless an independently verified database
+343
View File
@@ -0,0 +1,343 @@
# Kubernetes VM Test Lab
`tools/lab/govoplan-lab.py` creates and operates an amd64 Ubuntu/K3s test
environment on local or SSH-accessible libvirt hypervisors. It provides the
commands requested for the complete VM lifecycle:
| Command | Effect |
| --- | --- |
| `doctor` | Validate the strict inventory and, with `--online`, every hypervisor. |
| `create --apply` | Download checksum-pinned cloud images, create VM overlays and boot the declared VMs. |
| `deploy --apply` | Verify the signed GovOPlaN release, deploy shared state, install pinned K3s and apply GovOPlaN. |
| `update --apply` | Pull newly pinned state images, update K3s serially and roll the selected GovOPlaN release. |
| `status` | Show libvirt VM state, Kubernetes nodes and GovOPlaN pods. |
| `pause --apply` | Gracefully shut down workers, control planes and shared state while retaining disks. |
| `resume --apply` | Start the retained environment in dependency order and wait for readiness. |
| `verify` | Collect sanitized live-cluster evidence and optionally perform the API-pod-loss drill. |
| `destroy --apply --confirm <lab>` | Delete only the lab-owned domains and overlays; local evidence is retained by default. |
Every mutating command is a dry run unless `--apply` is present. Destruction
also requires the exact lab name. Generated credentials, CA keys, manifests and
evidence are written below the configured `state_directory` with owner-only
permissions. Keep that directory outside the repository and include it in the
workstation backup policy. Existing domains are reused or removed only when
their GovOPlaN ownership description and both expected lab disk paths match.
## What The Lab Proves
The supplied inventories describe two different assurance levels:
- `tools/lab/govoplan-lab.example.toml` creates four VMs on one libvirt host.
It is suitable for development, deployment rehearsal, migration testing,
application-pod replacement and recovery-tool exercises. It cannot close
GovOPlaN #27 because one physical host remains one failure domain.
- `tools/lab/govoplan-lab.acceptance.example.toml` places the two workers on
different hypervisors and puts the control and state VMs on a third. It can
produce the bounded stateless application-tier evidence required by #27 when
the declared hypervisors are genuinely independent physical failure domains.
Both examples use one control-plane VM and one state VM. This keeps the bounded
#27 target economical, but it does not prove control-plane or state-service
high availability. For control-plane failover, declare exactly three control
nodes on independent hosts. PostgreSQL, Redis and object-storage failover must
be tested against independently operated HA services; the lab's single state
VM is intentionally a replaceable integration fixture.
Approximate minimum capacity for the four-VM profile is 10 vCPUs, 16 GiB RAM
and 192 GiB of thin-provisioned disk. A six-VM profile with three controls needs
additional capacity. Do not overcommit memory on an acceptance target.
## 1. Prepare The Hypervisors
On each Ubuntu/Debian libvirt host:
```bash
sudo apt-get update
sudo apt-get install -y \
qemu-kvm libvirt-daemon-system libvirt-clients virtinst cloud-image-utils curl
sudo systemctl enable --now libvirtd
```
Use a dedicated lab-administration account. Remote hypervisors are managed over
SSH and the lifecycle invokes `sudo -n` there, so that account needs bounded
non-interactive permission for libvirt, image and cloud-init operations.
`NOPASSWD: ALL` is acceptable only on isolated lab hypervisors.
On a local hypervisor, put the workstation account in the `libvirt` group and
point `vm_image_directory` at a directory writable by that account and
traversable by `libvirt-qemu`. The lifecycle connects explicitly to
`qemu:///system` and does not require passwordless local sudo. Log out and back
in after a new group assignment before running `doctor --online`. Create the
configured image directory before running the doctor; it deliberately rejects
a missing or non-writable storage root instead of silently falling back to a
different filesystem.
Create a dedicated SSH key on the management workstation:
```bash
ssh-keygen -t ed25519 -f "$HOME/.ssh/govoplan-lab" \
-C "GovOPlaN Kubernetes lab"
```
Install its public key for every remote hypervisor account. The same public key
is injected into the VMs. The lifecycle keeps its own `ssh_known_hosts` file,
uses `accept-new` for first contact, and rejects changed host keys until a
lab-owned VM is deliberately recreated.
### Network contract
The configured `bridge` must exist on every selected hypervisor. All VM
addresses are static. Reserve them outside DHCP allocation and ensure that the
management workstation can route directly to every VM address; the lifecycle
does not tunnel VM traffic through the hypervisor SSH connection.
Permit only these flows inside the lab network:
| Port | Source and destination | Purpose |
| --- | --- | --- |
| TCP 22 | management workstation to every VM/hypervisor | Provisioning and evidence collection |
| TCP 6443 | all K3s nodes and management path to controls | Kubernetes API |
| UDP 8472 | K3s node to K3s node | Default Flannel VXLAN; never expose publicly |
| TCP 10250 | K3s node to K3s node | Kubelet metrics and API |
| TCP 2379-2380 | control to control, only with three controls | Embedded etcd |
| TCP 80/443 | test clients to K3s nodes | Traefik/ServiceLB ingress |
| TCP 5432/6379/9443 | K3s nodes to the state VM | PostgreSQL, Redis and TLS-protected Garage S3 |
| TCP 3025/3143 | approved test clients/workers to the state VM | GreenMail SMTP/IMAP test endpoints |
The official
[K3s networking requirements](https://docs.k3s.io/installation/requirements#networking)
remain authoritative. Restrict state ports to the lab network even though the
generated integration stack binds them on the state VM.
## 2. Create The Inventory
Start with the one-host rehearsal:
```bash
install -d -m 0700 "$HOME/.config/govoplan/labs"
cp tools/lab/govoplan-lab.example.toml \
"$HOME/.config/govoplan/labs/development.toml"
chmod 0600 "$HOME/.config/govoplan/labs/development.toml"
```
Edit at least the bridge, network, static addresses and SSH key paths. For a
multi-host run, copy the acceptance example and replace every example hostname,
failure-domain declaration and network value. Strict parsing rejects unknown
keys, mutable HTTP inputs, malformed checksums, duplicate addresses/MACs and an
acceptance inventory that collapses workers onto one declared hypervisor or
failure domain.
Cloud image, K3s binary, K3s installer and GovOPlaN release inputs are URL plus
SHA-256 pairs. Updating means changing those reviewed pins and then running the
`update` command; the tool deliberately does not follow `latest` aliases.
The one-host example uses the dedicated `govoplan-lab` NAT network. Its DHCP
pool ends at `192.168.123.99`; the static lab addresses start at
`192.168.123.201`. Define and start it once on the local hypervisor:
```bash
virsh --connect qemu:///system net-define \
tools/lab/libvirt/govoplan-lab-network.xml
virsh --connect qemu:///system net-autostart govoplan-lab
virsh --connect qemu:///system net-start govoplan-lab
```
Re-running those commands is unnecessary when `virsh net-info govoplan-lab`
already reports an active, persistent network. The lab destroy command leaves
this reusable network in place.
## 3. Validate And Create The VMs
```bash
LAB="$HOME/.config/govoplan/labs/development.toml"
PYTHON="/mnt/DATA/git/govoplan/.venv/bin/python"
"$PYTHON" tools/lab/govoplan-lab.py --config "$LAB" doctor --online
"$PYTHON" tools/lab/govoplan-lab.py --config "$LAB" create
"$PYTHON" tools/lab/govoplan-lab.py --config "$LAB" create --apply
```
The preview is safe to run repeatedly. Creation reuses a domain whose exact
lab-owned name already exists and otherwise creates a thin qcow2 overlay under
`vm_image_directory/<lab>/<node>`.
## 4. Deploy GovOPlaN
If `git.add-ideas.de` requires authentication for release images, export a
read-only package/container-registry identity for this shell. A Gitea package
token can be used as the password:
```bash
export GOVOPLAN_LAB_REGISTRY_USERNAME='package-reader'
read -r -s GOVOPLAN_LAB_REGISTRY_PASSWORD
export GOVOPLAN_LAB_REGISTRY_PASSWORD
```
Then preview and apply:
```bash
"$PYTHON" tools/lab/govoplan-lab.py --config "$LAB" deploy
"$PYTHON" tools/lab/govoplan-lab.py --config "$LAB" deploy --apply
"$PYTHON" tools/lab/govoplan-lab.py --config "$LAB" status
unset GOVOPLAN_LAB_REGISTRY_PASSWORD
```
Deployment verifies the downloaded release manifest and keyring by pinned
digest and by the existing GovOPlaN signature policy. It deploys PostgreSQL,
Redis, single-node Garage and GreenMail on the state VM. The API, WebUI, workers
and scheduler run in K3s from digest-pinned release images. A private lab CA
protects both ingress and S3; backend pods receive only the CA Secret and keep
TLS verification enabled. The CA profile carries critical `CA:TRUE` and
`keyCertSign,cRLSign` constraints. `deploy` and `update` rotate older lab CAs
that do not satisfy that profile and reissue the ingress/S3 certificate.
The final output identifies two local files below `state_directory`:
- `hosts` maps the public GovOPlaN and S3 test names to their VM addresses;
- `pki/ca.crt` is the private lab CA certificate.
Add the host mappings to the test client's resolver and trust the CA only on
devices used for this lab. On Debian/Ubuntu:
```bash
STATE="$HOME/.local/share/govoplan/labs/govoplan-k8s-lab"
cat "$STATE/hosts"
sudo install -m 0644 "$STATE/pki/ca.crt" \
/usr/local/share/ca-certificates/govoplan-k8s-lab.crt
sudo update-ca-certificates
```
Review mappings before adding them to `/etc/hosts`; the lifecycle does not edit
the workstation's trust or resolver configuration. Reinstall `pki/ca.crt` in
the client trust store after an automatic CA rotation.
### Enroll the first administrator
The production runtime does not create a default password. Issue one expiring,
single-use first-administrator credential inside an API pod and copy its
owner-only artifact out immediately:
```bash
KUBECTL="$STATE/bin/kubectl"
POD="$($KUBECTL -n govoplan get pods \
-l app.kubernetes.io/component=api \
-o jsonpath='{.items[0].metadata.name}')"
ARTIFACT="$STATE/first-admin-enrollment.json"
umask 077
$KUBECTL -n govoplan exec "$POD" -- \
python -m govoplan_core.commands.first_admin issue \
--reason 'initial Kubernetes lab enrollment' \
--output /tmp/first-admin-enrollment.json
$KUBECTL -n govoplan exec "$POD" -- \
cat /tmp/first-admin-enrollment.json > "$ARTIFACT"
$KUBECTL -n govoplan exec "$POD" -- \
rm -f /tmp/first-admin-enrollment.json
chmod 0600 "$ARTIFACT"
```
Submit the token from that artifact once to
`/api/v1/bootstrap/first-admin` with the administrator email, display name,
password, tenant slug and tenant name. The password must contain at least 12
characters. The lab command performs that exchange without placing either the
token or password in process arguments, rejects redirects, and removes the
artifact only after HTTP 201:
```bash
"$PYTHON" tools/lab/govoplan-lab.py --config "$LAB" enroll-admin \
--email 'owner@example.org' \
--display-name 'System Owner' \
--tenant-slug default \
--tenant-name 'Default Tenant'
"$PYTHON" tools/lab/govoplan-lab.py --config "$LAB" enroll-admin \
--email 'owner@example.org' \
--display-name 'System Owner' \
--tenant-slug default \
--tenant-name 'Default Tenant' \
--apply
```
The public lab hostname must already resolve on the management workstation;
the command verifies TLS through the generated private CA directly.
## 5. Collect #27 Evidence
Create a short-lived API key authorized to read the Ops status endpoint. In the
current Access administration UI, open **Tenant API keys** and select only
**View tenant settings** (`admin:settings:read`); the Ops endpoint explicitly
accepts that compatibility scope. A dedicated operator credential may instead
use `ops:operations:read`. Then run:
```bash
export GOVOPLAN_OPS_API_KEY='short-lived-value'
"$PYTHON" tools/lab/govoplan-lab.py --config "$LAB" verify \
--exercise-api-pod-loss
unset GOVOPLAN_OPS_API_KEY
```
The verifier requires ready API and WebUI pods across at least two Kubernetes
nodes, all Deployments available, consistent runtime composition, queue
coverage and a valid database-connection budget. During the optional drill it
deletes one ready API pod, probes public readiness and waits for replacement.
It writes sanitized output to
`state_directory/evidence/kubernetes-multi-host.json` and never stores the API
key. A rehearsal inventory prints an explicit warning that its result is not
independent-failure-domain evidence.
Retain these private artifacts together for review:
1. `inventory.json` and the reviewed inventory TOML;
2. the adopted release manifest/keyring and installation receipt;
3. `kubernetes.json`;
4. the Kubernetes verifier output;
5. private cluster logs for the approved drill window;
6. the operator's out-of-band evidence that the worker hypervisors are
independent physical hosts or availability zones.
GovOPlaN #37 additionally requires independent assessment and production
approval keys. Running its evidence jobs in containers is supported, but a
container does not create an independent authority. Follow
`TARGET_MATURITY_EVIDENCE_RUNBOOK.md` after the #27 drill passes.
## 6. Update, Pause, Resume And Remove
After reviewing and changing pinned image/K3s/release values in the inventory:
```bash
"$PYTHON" tools/lab/govoplan-lab.py --config "$LAB" update
"$PYTHON" tools/lab/govoplan-lab.py --config "$LAB" update --apply
```
Workers are cordoned, drained, updated and uncordoned one at a time. K3s
controls are reconciled serially. The release-specific migration Job remains
subject to GovOPlaN's signed backup-evidence gate. The lab update command is not
a substitute for creating recovery evidence before a destructive state-schema
change.
To stop compute use without deleting disks:
```bash
"$PYTHON" tools/lab/govoplan-lab.py --config "$LAB" pause --apply
"$PYTHON" tools/lab/govoplan-lab.py --config "$LAB" resume --apply
```
To remove VM resources while preserving local evidence:
```bash
"$PYTHON" tools/lab/govoplan-lab.py --config "$LAB" destroy
"$PYTHON" tools/lab/govoplan-lab.py --config "$LAB" destroy \
--apply --confirm govoplan-k8s-lab
```
Add `--purge-local-state` only after evidence and recovery material have been
retained elsewhere. That option deletes the generated local CA, secrets,
manifests and evidence as well as the VMs.
## Acceptance Boundary
This tool supplies reproducible infrastructure and executes the bounded
stateless-node drill. It does not certify the truth of operator-entered failure
domains, provide HA PostgreSQL/Redis/Garage, create production backup evidence,
or approve its own results. Those boundaries are deliberate: #27 can close
after a passing run on independently controlled hosts; broader production
maturity remains governed by #35, #37 and the target evidence runbook.
@@ -0,0 +1,263 @@
# Package Registry Releases
GovOPlaN publishes reusable module artifacts through Gitea's native PyPI and
npm registries. These packages improve developer installation, release
resolution, cacheability, and artifact inspection. They do not replace the
signed runtime distribution: the signed manifest and digest-pinned OCI images
remain the production deployment authority.
## Publication boundary
Every repository with a `pyproject.toml` contains
`.gitea/workflows/module-package-release.yml`. The meta repository owns the
canonical template and installs it with:
```bash
python tools/repo/sync-module-package-workflows.py --write
python tools/repo/sync-module-package-workflows.py --check
```
The workflow runs for `v*` tags and may be dispatched manually for an existing
tag. The organization preflight verifies that every package repository protects
the `v*` namespace. Before building, the workflow itself verifies that:
- the tagged commit is contained in `main`;
- the tag, Python project version, and optional WebUI package version agree;
- package names remain in the `govoplan-*` and `@govoplan/*-webui` namespaces.
The workflow binds the repository explicitly from the Gitea Actions context.
Do not rely on GitHub-compatible environment variables being injected by the
runner image; Gitea runners may expose only the context values. Gitea 1.24 job
tokens cannot read repository tag-protection settings, so package jobs must not
receive a broad administrator token merely to repeat the organization preflight.
Run the following before the first publication and after repository or tag-rule
changes:
```bash
python tools/gitea/gitea-configure-package-releases.py
```
Preview and dispatch the exact wheel/WebUI versions selected by the developer
meta-package with:
```bash
python tools/gitea/gitea-dispatch-package-set.py \
--env-file ~/.config/gitea/gitea.env
python tools/gitea/gitea-dispatch-package-set.py \
--env-file ~/.config/gitea/gitea.env \
--apply
```
The dispatcher reads exact versions from `packages/govoplan-meta/pyproject.toml`,
inspects the selected tag to determine whether a WebUI package is expected,
skips complete registry pairs and does not duplicate an active workflow. Use
`--repository govoplan-core` for a bounded dispatch or `--verify-existing` to
rebuild and hash-verify versions already present in both registries.
For coordinated lockstep tags, `push-release-tag.sh` pushes module tags first,
Core next, and the meta tag last. This is a dependency guarantee for a
single-capacity Actions runner: the developer package cannot run before its
exact Core and module versions have entered the queue.
The same release entry point first validates the migration graph, then records
the reviewed current Alembic heads under the target release version and reruns
the strict migration audit before it changes package versions, commits, or
tags. The default preflight intentionally does not require those heads to exist
in the previous release baseline. A failed candidate-baseline check therefore
cannot produce a protected package release.
The source gate validates `pyproject.toml`, the module version declaration
(`MODULE_VERSION` or the top-level `ModuleManifest.version`), public package
`__version__`, and WebUI metadata before creating tags. Release-tag artifact
checks run only after the candidate tags and immutable WebUI lock have been
created locally.
Release-lock regeneration resolves a fresh immutable lock from the reviewed
candidate manifests; it does not seed resolution from the previous release
lock. This prevents removed transitive packages and stale peer metadata from
blocking or contaminating the new release. Candidate resolution also uses an
isolated temporary npm cache, so a locally replaced tag cannot reuse metadata
from a failed, unpushed release attempt.
Modules that retain the same WebUI package identity in both a root publish
manifest and `webui/package.json` use the WebUI manifest as the canonical peer
contract. The coordinated release synchronizes `peerDependencies` and
`peerDependenciesMeta` into the publish manifest before creating the module
tag, then synchronizes each lockfile root from the final package metadata. A
distinct root package remains independent.
It builds one wheel and, where applicable, one npm tarball. The workflow records
the source tag, source commit, filename, size, and SHA-256 in
`package-artifacts.json` before publishing. Gitea rejects a second upload of the
same package version, so correction requires a new version rather than artifact
replacement.
A retry after partial publication is safe. Before upload, the workflow reads the
native package registry file record and compares its SHA-256 with the artifact
rebuilt from the protected tag. An exact existing artifact is skipped; a
same-version artifact with another digest or an unexpected file set fails
closed. This permits a failed npm publication to resume without weakening
package immutability or accepting `--skip-existing` blindly.
The npm tarball is always published through an explicit local `./dist/...`
path. Without that prefix, npm may interpret a relative tarball name as a Git
package shorthand before it ever contacts the configured registry.
Published WebUI packages contain registry-compatible dependencies only. The
workflow converts an internal dependency pinned to a protected `vX.Y.Z` Git tag
into the exact `X.Y.Z` registry version and rejects unresolved `file:` or Git
dependencies. Repository development metadata may therefore keep local or Git
references without leaking them into the published package contract.
Historical `add-ideas` and current `GovOPlaN` organization URLs are accepted
for immutable tagged releases; both normalize to the same exact registry
dependency and no branch or unversioned Git reference is accepted.
## One-time Gitea setup
Protect `v*` tags in every package repository and the meta repository. Allow
only the `Owners` team to create or delete those tags.
```bash
set -a
. ~/.config/gitea/gitea.env
set +a
python tools/gitea/gitea-configure-package-releases.py --apply
```
Create a dedicated personal access token with only `write:package` scope and
store these organization-level Actions secrets on `GovOPlaN`:
- `GOVOPLAN_PACKAGE_USERNAME`: account owning the package token;
- `GOVOPLAN_PACKAGE_TOKEN`: dedicated package-write token.
Do not use an administrator or general release token. Gitea 1.24 does not grant
package publication to the automatic Actions job token. Organization secrets
allow the same least-privilege credential to serve every module workflow.
## Exact release consumption
`tools/release/generate-release-package-set.py` supports two explicit package
profiles. `base` translates the reviewed roots in `requirements-release.txt`;
`full` reads the exact `govoplan[full]` dependency set from the developer
meta-package. Both profiles resolve every version tag to its commit and verify
the package metadata from that exact Git tree. The official module directory
and immutable runtime distribution use `full`, so every publicly released
module can be discovered without rebuilding the application image.
`tools/release/resolve-package-artifacts.py` then downloads exactly those wheel
and WebUI versions from Gitea. It reads the identity embedded in every wheel and
npm tarball, rejects missing, duplicate, unexpected, or oversized artifacts,
and writes `package-artifacts.lock.json` with credential-free HTTPS download
URLs, SHA-256 values, and npm registry integrity values. The resolver verifies
that the bytes downloaded by `npm pack` match the registry's own integrity
record. Credentials are accepted only through environment variables and are
never written to the lock. Python resolution ignores ambient pip configuration
and extra indexes for GovOPlaN roots, preventing an internal package name from
being selected from an undeclared registry.
The runtime distribution workflow uses the verified full-profile wheelhouse
directly and installs every selected module WebUI tarball only after matching
it to the lock. It publishes the package set, package lock, and hash-locked
requirements as release assets.
The WebUI installer receives the absolute runtime-build interpreter path so its
directory changes cannot escape the isolated release environment.
Gitea 1.24 dispatches this workflow from a branch, but that branch is only the
workflow implementation. The job fetches and peels the protected `v<version>`
tag explicitly and materializes both `requirements-release.txt` and the
developer meta-package from that Git tree. It then binds the signed distribution
source and Gitea release assets to the same exact commit. A post-tag workflow
repair can therefore retry publication without changing the released package
composition or relabelling the later branch commit as released source.
The package-lock SHA-256 is part of the signed distribution manifest. Runtime
finalization also requires the lock's package versions and hashes to match the
wheel composition embedded in the images. OCI assembly remains network-free
after package and third-party dependency resolution.
The source refs remain in the module catalog for source provenance and release
planning. Production installation consumes the signed runtime images rather
than invoking `pip`, `npm`, or Git on the target host.
## Public module directory
`tools/release/publish-release-catalog.sh` resolves the selected package set and
registry lock before it creates a catalog. Catalog entries are synthesized from
the exact tagged module manifests, never from a hand-maintained module list or
the current workspace. Each entry binds its Python wheel and optional WebUI
tarball to the registry URL, filename, size, SHA-256, package identity, source
tag, and source commit before the complete catalog is signed.
The same publication transaction regenerates and prunes the browsable static
directory under `public/catalogs/v1/modules/`. It writes a global
`modules/index.json`, one `<module>/index.json`, and one
`<module>/<version>/manifest.json` for every entry in the signed channel.
These files are derived from that exact signed payload and keyring; stale JSON
from an older partial catalog is removed while unrelated static assets are left
untouched. The signed channel remains the trust anchor, while the module
directory provides stable discovery URLs for browsers and external tooling.
Official GovOPlaN modules are open-source directory entries and do not require
license entitlements. The generic `license_features` contract remains available
for third-party package directories, support/configuration packages, or future
deployment-specific presets. A catalog entry is gated only when that entry
explicitly declares such features.
Core carries the public stable catalog URL and its independently pinned trust
anchor. In the absence of an operator-configured catalog, Admin discovers the
official directory automatically. Selecting an entry creates a reviewed
install/update plan; the trusted installer downloads the exact signed artifacts
into a private digest cache, verifies size and hash, and installs only from that
cache. A saved plan is rejected if any package ref, artifact identity, catalog
channel, sequence, or signing-key identity differs from the currently validated
catalog.
The Admin directory can be searched by module, package, repository, or tag and
filtered by available, installed, update, and blocked/withdrawn states. It
shows the source revision, artifact digest, release notes, and configuration
requirements. Missing dependency/interface providers and unsupported update
windows are surfaced before an operator adds the entry to a plan; installer
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:
- install, update, and uninstall change the instance-wide package composition;
- enable and disable change the active instance runtime graph;
- tenant module entitlements define unavailable, available, and forced modules;
- group/user presentation is governed through Views and Policy; and
- enabling a capability module does not opt data into that capability.
Single-process or single-host installations may execute a supervised package
plan locally. Shared-state and Kubernetes profiles reject node-local package
mutation: operators compose and roll out a new signed full-profile runtime image
instead. This prevents replicas from drifting while retaining the same Admin
catalog and preflight experience.
## Developer meta-package
`packages/govoplan-meta` builds the optional `govoplan` package. Its default
dependencies mirror the reviewed runtime roots; `govoplan[full]` adds all
currently packageable workspace modules. Regenerate it after changing release
requirements or package versions:
```bash
python tools/release/generate-developer-meta-package.py
python tools/release/generate-developer-meta-package.py --check
```
`push-release-tag.sh` performs this synchronization before release commits and
tags. The meta-package is for editable/developer setup and composition tests. It
does not enable modules, apply migrations, provision services, or establish
backup and recovery evidence.
If the tag-triggered developer meta-package job fails before publication, rerun
`publish-developer-meta-package.yml` with the existing protected version. The
manual path validates that tag against `main`, checks out its exact commit, and
publishes only when the registry does not already contain the same wheel hash.
Generic Packages are intentionally not used. Add that transport only when a
consumer needs an artifact format unsupported by PyPI, npm, Gitea Releases, or
the OCI registry.
@@ -0,0 +1,190 @@
# Production Target And Independent Evidence Handoff
This runbook identifies the external inputs needed to finish
[GovOPlaN #27](https://git.add-ideas.de/GovOPlaN/govoplan/issues/27) and
[GovOPlaN #37](https://git.add-ideas.de/GovOPlaN/govoplan/issues/37). The
repository can render, inspect and sign evidence for a target, but it cannot
manufacture an independent failure domain or an independent approval authority.
## GovOPlaN #27: real two-node target
The bounded acceptance target is two independently schedulable worker nodes.
The API and WebUI must each have ready replicas on both nodes, all Deployments
must be available, the active module composition and software versions must be
consistent, every configured queue must have a worker, and the database
connection budget must pass. The validation then deletes one ready API pod and
requires replacement without an observed readiness outage.
Two virtual machines on different physical hosts or availability zones meet the
failure-domain intent. Two containers, VMs or Kubernetes nodes on one physical
host are useful development targets but do not close #27. A two-worker cluster
also does not prove control-plane high availability. For a self-managed
production cluster, use three control-plane nodes plus at least two workers; a
managed control plane plus two workers is the shorter path.
### What the target owner must provide
Provide these through a secure handoff, not an issue, chat message or Git:
1. A kubeconfig path with access to the target, for example
`~/.config/govoplan/targets/<target>.kubeconfig`, mode `0600`.
2. A stable installation ID, public HTTPS hostname, namespace, ingress class and
TLS-secret or certificate-manager arrangement.
3. Two independently schedulable workers and permission to place API and WebUI
replicas on both.
4. External, logically shared PostgreSQL, Redis and S3 endpoints with trusted
CA material and network reachability from every worker. Do not co-locate the
only copies of these services on the two workers used for the failure drill.
5. The six runtime secret values required by the generated manifest:
`MASTER_KEY_B64`, `DATABASE_URL`, `GOVOPLAN_DATABASE_URL_PGTOOLS`,
`REDIS_URL`, `FILE_STORAGE_S3_ACCESS_KEY_ID` and
`FILE_STORAGE_S3_SECRET_ACCESS_KEY`.
6. A short-lived GovOPlaN API key limited to `ops:operations:read`, supplied in
`GOVOPLAN_OPS_API_KEY` only for evidence collection.
7. An approved drill window and permission to delete one API pod.
If no Kubernetes target exists, provide hostnames/IP addresses for the machines,
an SSH user and key path, the internal/external DNS plan, and the permitted
firewall ports. Those inputs are sufficient to provision a k3s target. They are
not sufficient to claim control-plane HA unless three control-plane failure
domains are present.
The repository now supplies the strict libvirt/K3s lifecycle and example
inventories for this handoff in
[`KUBERNETES_TEST_LAB.md`](KUBERNETES_TEST_LAB.md). Its `acceptance` mode
rejects a declared topology unless the workers and shared-state fixture occupy
different hypervisor and failure-domain identifiers. Reviewers must still
verify that those identifiers correspond to genuinely independent hosts.
### Separate deployment and evidence authorities
The deployment identity may create and update the namespace, Secret,
ConfigMap, Deployments, Services, Jobs, PodDisruptionBudgets and Ingress. The
evidence collector only needs:
- cluster scope: `get` and `list` for `nodes`;
- target namespace: `get` and `list` for `pods` and `deployments`;
- target namespace during the approved drill: `delete` for `pods`.
Use separate kubeconfig contexts or service accounts when the same person does
not hold both roles.
### Render, apply and verify
Use the signed, digest-pinned installation bundle selected for the target:
```bash
export KUBECONFIG="$HOME/.config/govoplan/targets/<target>.kubeconfig"
python tools/deployment/govoplan-deploy.py render-kubernetes \
--directory /srv/govoplan/<installation-id> \
--namespace govoplan \
--secret-name govoplan-runtime \
--tls-secret-name govoplan-tls \
--s3-ca-secret-name govoplan-s3-ca \
--ingress-class-name nginx \
--output /srv/govoplan/<installation-id>/kubernetes.json
kubectl apply -f /srv/govoplan/<installation-id>/kubernetes.json
kubectl -n govoplan wait --for=condition=available deployment --all --timeout=10m
export GOVOPLAN_OPS_API_KEY="$(cat /run/secrets/govoplan-ops-evidence-key)"
python tools/deployment/govoplan-deploy.py verify-kubernetes \
--directory /srv/govoplan/<installation-id> \
--namespace govoplan \
--exercise-api-pod-loss \
--output /srv/govoplan/<installation-id>/evidence/kubernetes-multi-host.json
unset GOVOPLAN_OPS_API_KEY
```
The verifier emits sanitized JSON and exits nonzero if the topology, runtime,
queue, connection-budget or pod-loss checks fail. Preserve the private cluster
logs and manifest alongside the sanitized result in the controlled evidence
store.
## GovOPlaN #37: controlled signed target evidence
Yes, collection, review and signing can run in containers. A container provides
repeatability and process isolation; it does not create independent authority.
The production approver must control a different private key from the target
operator/assessor and must review the evidence before signing the
`production_approval` scope.
Use at least these three key boundaries:
1. **Installer authority:** signs installed-release-origin receipts only.
2. **Target assessment authority:** signs the permitted target, accessibility,
privacy, security, operations and recovery scopes.
3. **Production approval authority:** independently signs only
`production_approval` after reviewing the other evidence.
Do not reuse release-catalog keys for any of these roles. Keep private Ed25519
keys outside Git, Gitea, GovOPlaN application storage and chat. Publish only the
public keyrings. The proof issuer already rejects key reuse across release,
installer and proof trust domains.
### Generate independently held keys
Each authority runs this command in its own `0700` directory. The generator
refuses existing output paths and writes both files as `0600`:
```bash
install -d -m 0700 "$HOME/.config/govoplan/authority-keys"
python tools/assessments/generate-authority-keypair.py \
--purpose proof \
--key-id authority:target-2026 \
--scope target_environment \
--scope accessibility \
--scope privacy \
--scope security \
--scope operations \
--scope recovery \
--private-key "$HOME/.config/govoplan/authority-keys/target-2026.pem" \
--keyring "$HOME/.config/govoplan/authority-keys/target-2026-public.json"
```
The independent production approver generates another key with only
`--scope production_approval`. An installer authority uses `--purpose installer`
and no `--scope`. Merge public key entries into the separately controlled
keyrings only after the responsible authorities verify fingerprints out of
band.
### Container boundary
Use two one-shot jobs or containers:
- **Collector/assessor:** network access, read-only source and trust mounts,
read/write private evidence output, and the narrowly scoped kubeconfig. It
must not receive the production-approval private key.
- **Production approver:** `--network none`, read-only assessment/evidence/trust
mounts, a read-only secret mount containing only the approval key, and a
separate output mount. It must not receive deployment credentials.
Build or select the assessment image by digest and record that digest in the
evidence log. A representative runtime shape is:
```bash
docker run --rm --network none --read-only --tmpfs /tmp \
--user "$(id -u):$(id -g)" \
--mount type=bind,src="$PWD/evidence",dst=/evidence,readonly \
--mount type=bind,src="$PWD/trust",dst=/trust,readonly \
--mount type=bind,src="$HOME/.config/govoplan/authority-keys",dst=/run/keys,readonly \
--mount type=bind,src="$PWD/approved",dst=/output \
<assessment-image>@sha256:<digest> \
<assessment command>
```
The current evidence commands and required scopes are documented in
[`TARGET_MATURITY_EVIDENCE_RUNBOOK.md`](TARGET_MATURITY_EVIDENCE_RUNBOOK.md).
The final proof must cover `target_environment`, `accessibility`, `privacy`,
`security`, `operations`, `recovery` and independent `production_approval`, and
must bind to the verified installed composition and installer receipt.
## Completion boundary
#27 can close after the real target produces a passing pod-loss result. #37 can
close after an independently approved, schema-valid proof is generated for that
same installed composition and the public authority keyrings, proof and private
evidence custody references are recorded. Neither issue should close from a
single-host simulation or a self-approved signature.
@@ -2,7 +2,7 @@
The Core recovery ledger is a platform primitive, not automatic protection for
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
@@ -1,5 +1,11 @@
# Scaling And Multi-Host Deployment
For the exact external handoff, least-privilege collector permissions and live
two-node acceptance procedure, see
[`PRODUCTION_TARGET_HANDOFF.md`](PRODUCTION_TARGET_HANDOFF.md).
For a reproducible local or multi-hypervisor libvirt/K3s target, use
[`KUBERNETES_TEST_LAB.md`](KUBERNETES_TEST_LAB.md).
## Implemented Contract
GovOPlaN now supports a stateless application tier backed by logically shared
@@ -70,6 +76,7 @@ python tools/deployment/govoplan-deploy.py render-kubernetes \
--namespace govoplan \
--secret-name govoplan-runtime \
--tls-secret-name govoplan-tls \
--s3-ca-secret-name govoplan-s3-ca \
--ingress-class-name nginx \
--output /srv/govoplan/default/kubernetes.json
```
@@ -86,11 +93,25 @@ command prints the exact required key contract. Review the generated
`FORWARDED_ALLOW_IPS` value and replace it with the exact ingress-proxy network
before production use.
When an external S3 endpoint is signed by a private CA, create the optional CA
Secret with a `ca.crt` key and pass `--s3-ca-secret-name`. The renderer mounts
that Secret read-only and sets `AWS_CA_BUNDLE` for API, worker, scheduler,
migration and database-wait containers. It does not disable certificate
verification or replace the WebUI trust store.
The generated containers run as non-root with a read-only root filesystem and
an ephemeral `/tmp`. Runtime Deployments wait for the exact configured database
migration heads before starting. The API exposes `/health/ready`, which fails
while that API node is draining or cannot prove its runtime-coordination
heartbeat.
an ephemeral `/tmp`. Celery Beat keeps its replaceable schedule database there;
durable schedule definitions remain in shared state. The WebUI resolves its
configured API Service when the container starts, so Kubernetes deployments do
not inherit the Compose-only `load-balancer` hostname. Runtime Deployments wait
for the exact dependency-resolved database migration heads before starting.
The API exposes `/health/ready`, which fails while that API node is draining or
cannot prove its runtime-coordination heartbeat.
Replicated API, WebUI, and worker Deployments use a hard hostname-spread
constraint scoped to the current pod-template hash. A rollout therefore keeps
each replica set distributed across independently schedulable nodes instead of
allowing all replacement pods to settle on one node after the old set exits.
## Runtime Coordination
@@ -249,7 +270,11 @@ record under the installation evidence directory and never retains the API key.
Use `--exercise-api-pod-loss` in an approved drill window to delete one API pod,
observe the public readiness path continuously, and record its replacement.
Generated API workloads use a ten-second pre-stop drain so Kubernetes can remove
the terminating endpoint from ingress and service routing before Uvicorn exits.
Do not remove or shorten this drain without repeating the public-path pod-loss
test against the target ingress controller and network implementation.
This proves the bounded stateless-node-loss slice only. Session continuity,
accepted-job redelivery, state-service failover, and coordinated restore remain
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.
@@ -1,5 +1,9 @@
# Target Maturity Evidence Runbook
For authority-key generation, container isolation and the concrete inputs that
must be supplied by the target owner and independent production approver, see
[`PRODUCTION_TARGET_HANDOFF.md`](PRODUCTION_TARGET_HANDOFF.md).
This runbook turns retained target-environment results into a sanitized,
signed GovOPlaN capability-fit proof. It does not make a deployment suitable,
certified, supported, or production-approved by itself. The proof records what
@@ -76,6 +76,13 @@ one place. If a deployment profile later needs pinned SHAs for every repository,
generate that lock as a release artifact instead of making day-to-day
development depend on submodule updates.
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
package lock before producing the signed OCI runtime. See
`docs/operations/PACKAGE_REGISTRY_RELEASES.md`. Git tags remain source provenance; package
registries are reusable artifact transport; the signed runtime manifest and
digest-pinned images remain production authority.
## Docker Placement
Whole-product Docker and production-like deployment composition belongs in
@@ -13,6 +13,7 @@
"expires_at",
"revoked",
"deployer",
"package_lock",
"images",
"dependencies",
"composition",
@@ -27,6 +28,7 @@
"expires_at": { "type": "string", "format": "date-time" },
"revoked": { "const": false },
"deployer": { "$ref": "#/$defs/artifact" },
"package_lock": { "$ref": "#/$defs/artifact" },
"images": {
"type": "object",
"additionalProperties": false,
+198
View File
@@ -0,0 +1,198 @@
# GovOPlaN Platform Core Ideas
## Purpose
GovOPlaN is an institutional governance and operations layer. Its central
promise is:
> Model the institution, orchestrate its work, connect its systems, and
> preserve why and under whose authority it acted.
The platform should let people complete a real task without understanding its
repository or module graph. It should let institutions retain control over
their data, procedures, providers, and deployment while still sharing
interoperable definitions and evidence.
This document is the stable summary of the ideas that every product package,
module, interface, and integration must preserve. Current implementation state
lives in [Strategy Status](STRATEGY_STATUS.md).
## Ten Core Ideas
### 1. Institutional context before application context
Work happens for a tenant, institution, organizational unit, function,
mandate, jurisdiction, service, case, and represented party. The real actor
and represented capacity remain distinct. Application permissions alone do not
prove institutional competence.
### 2. Governance is executable
Policy is not explanatory prose around an operation. Consequential actions
must expose applicable rules, authority, purpose, expected effects, review
requirements, recovery behavior, and evidence. Inheritance may tighten a rule
but must not silently loosen an upstream constraint.
### 3. Time has two independent meanings
Valid time answers when a fact applied. Recorded time answers what the system
knew at a point in history. Historical browsing changes the business-data
projection, never the current authorization context. Corrections and
supersession remain visible rather than rewriting history.
### 4. One context, many owners
Cases, tasks, decisions, records, messages, files, appointments, reports, and
external objects remain owned by their domain modules or source systems. Stable
references create one navigable context without a universal copied master
record or cross-module table access.
### 5. Native and connected operation are peers
For every integration, GovOPlaN states whether it is authoritative, mirrors an
external source, synchronizes governed fields, adds a governance overlay, or
keeps a link only. An external system can be used today and replaced later
without losing provenance or institutional control.
### 6. Human work is a first-class system object
An intake becomes owned, reviewable work. A person can see the current context,
next responsible action, reason, deadline, consequence, and completion
evidence. Workflow Engine coordinates machine and human transitions; focused
views guide people through the relevant platform surfaces.
Tasks owns explicit work items and the unified work inbox. Workflow Engine owns
process execution and resumable handoffs. Notifications attract attention, and
domain modules retain their business objects. These boundaries prevent an
inbox, workflow, or notification from becoming a second copy of institutional
state.
### 7. Views reduce complexity without changing authority
The interface is a task- and role-sensitive projection of installed
capabilities. Views, dashboards, search, documentation, and workflow-guided
surfaces may hide irrelevant functions, but they never grant access. Users can
escape a focused mode when policy permits and can always understand why
something is unavailable.
Configurable product areas organize authorized capabilities around work,
services, records, communication, meetings, data and institutional
responsibility. The optional Quick Access rail presents task-local Work,
Calendar, Messages and Files contributions without merging their owners or
turning presentation settings into permissions.
### 8. Evidence and recovery are part of the operation
Intent, exact input versions, approvals, external effects, receipts,
outcome-unknown states, reconciliation, corrections, retention, and recovery
belong to one evidence chain. A retry must be idempotent; rollback claims must
distinguish reversible local state from effects already observed elsewhere.
### 9. Inclusion is multi-channel, not portal-only
Public portal, postbox, mail, telephone, paper, in-person assistance, APIs, and
external systems are channels around the same governed work. Assisted entry
records who entered information, for whom, from which source, with which
attestation, and how the affected person receives a usable receipt and
correction path.
Responsive, mobile, desktop, and embedded launch surfaces are additional ways
to enter the same governed context, not separate products with weaker authority
or evidence. Common task-local actions may open in bounded overlays while their
owning modules retain validation, policy, and persistence.
### 10. Successful configurations are portable products
Modules are ingredients. A usable product is a signed configuration package
with terminology, forms, policies, workflows, views, reports, provider
profiles, documentation, migration rules, and evidence. Institutions derive
local packages without forking code or weakening inherited constraints.
## Platform Planes
The planes below are ownership lenses, not navigation groups or mandatory
deployment tiers.
| Plane | Responsibility |
| --- | --- |
| Experience | Shell, views, dashboard, search, help, accessibility, and task-focused composition |
| Participation and channels | Portal, postbox, mail, campaigns, calendar, scheduling, consultation, and assisted channels |
| Human work and procedure | Services, forms/runtime, cases, tasks, approvals, workflow execution, and domain procedures |
| Content, records, and evidence | Files, templates, DMS, eAkte/records, audit, reporting, transparency, and publication |
| Institutional governance | Identity, access, tenancy, organizations, functions, mandates, policy, trust, and formal decisions |
| Data and integration | Connectors, datasources, dataflow, search, external references, provider health, and reconciliation |
| Runtime and assurance | Module composition, operations, deployment, recovery, security evidence, and signed packages |
Collected product ideas and normalized actor outcomes are preserved in the
[Product Input Register](PRODUCT_INPUT_REGISTER.md). They enter implementation
only through a named journey, package, or explicit discovery issue.
## Canonical Distinctions
The platform must not collapse these pairs:
- identity vs account vs represented capacity;
- role/permission vs function/mandate/competence;
- valid time vs recorded time;
- purpose for use vs general technical access;
- document content vs managed file bytes vs institutional record;
- task vs workflow definition vs workflow instance;
- approval vs formal decision;
- message intent vs transport delivery vs recipient acknowledgement;
- source authority vs connector maturity;
- current state vs historical evidence;
- correction/compensation vs erasure of an observed effect;
- a module boundary vs a user-visible product boundary.
## Product Experience Rule
The normal user interface speaks in services, work, records, messages,
meetings, decisions, and outcomes. Module names, provider IDs, capability names,
package coordinates, and schema details are technical provenance. They are
visible to administrators and in expandable diagnostics, but they are not the
primary information architecture for ordinary work.
The complete permission-derived tool catalogue remains deliberately available
to power users. Product areas and Quick Access provide sensible system and
tenant defaults plus governed user personalization; they do not make familiar
tools harder to reach merely to conceal modular implementation.
## Maturity Rule
A repository, route, model, or unit test does not make a capability complete.
Claims advance only with evidence appropriate to the claim:
1. `scaffold`: boundary and documentation exist;
2. `vertical_slice`: useful behavior has focused tests;
3. `reference_ready`: an end-to-end reference journey passed target,
accessibility, privacy, security, operations, and recovery evidence;
4. `supported`: upgrades, interoperability, support procedures, and release
guarantees are defined;
5. `lts`: compatibility and maintenance windows are contractual.
## Deliberate Non-Goals
GovOPlaN does not aim to:
- replace every specialist system, ERP, DMS, groupware, or data tool;
- make one database authoritative for every connected fact;
- expose every installed capability to every person;
- infer authority from organizational membership alone;
- make historical browsing weaken current security;
- treat AI output as an unaccountable institutional decision;
- create a repository for every noun in the information model;
- claim production maturity from local development evidence.
## Decision Test
A proposed feature fits the platform when it improves at least one real
institutional journey and can answer:
1. Who owns the object and source of truth?
2. In which institutional and temporal context does it apply?
3. For which declared purpose may it be used?
4. Which policy and authority permit the action?
5. What effect, evidence, retention, and recovery behavior result?
6. How can it operate with an external owner without losing autonomy?
7. How will a person discover and complete it without learning the module
graph?
+203
View File
@@ -0,0 +1,203 @@
# GovOPlaN Product Input Register
## Purpose
This document preserves and normalizes product ideas and user-story notes that
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
remains in [Platform Core Ideas](PLATFORM_CORE_IDEAS.md), the
[Connected Governance Platform Roadmap](reference/CONNECTED_GOVERNANCE_PLATFORM_ROADMAP.md),
and the [Reference Journey Program](REFERENCE_JOURNEY_PROGRAM.md).
The register was reconciled on 2026-08-06 from:
- `/mnt/DATA/Nextcloud/ADD ideas UG/Products/govoplan/ideas.md`;
- `/mnt/DATA/Nextcloud/ADD ideas UG/Products/govoplan/user_stories.txt`.
The source notes remain useful as the original capture. This maintained version
uses consistent terminology, makes ownership explicit, and records where an
idea enters the product program.
## Product Themes
### Operable and scalable installation
An operator should be able to install, update, reconfigure, scale, back up,
restore, pause, and retire GovOPlaN through one explainable control plane.
Existing infrastructure may be reused or managed components may be provisioned.
The WebUI and CLI must invoke the same governed operations, show the planned and
completed effects, preserve recovery evidence, and never claim rollback for an
external effect that cannot actually be reversed.
This theme is owned by Core, Admin, Ops, Policy, Files, and the signed product
package. It is tracked primarily by GovOPlaN #13 and the production evidence
issues. It advances in parallel with, but does not replace, actor-facing
reference journeys.
### Focused, consistent work
People should see the work and tools relevant to the current task, not the
installed module graph. Views may be defined by administrators, groups, or
users within policy. Workflow instances may pin a governed View. Contextual
help, predictable action placement, consistent central components, visible
intermediate results, and plain institutional terminology are product
requirements.
Small task-local actions such as writing a Mail or Postbox message, completing
a Template, or manipulating Files should be launchable without abandoning the
current context. These actions remain owned by their modules and use bounded
overlays or workspaces; the shell supplies discovery and return context rather
than reimplementing them.
The accepted first presentation is the optional, configurable Quick Access
rail: Work, Calendar, Messages and Files. Messages may compose Mail, Postbox
and future chat contributions while preserving their separate authority and
channel semantics. System and tenant administrators govern availability and
forced entries; users select categories and ordering within those ceilings.
Modules register typed contributions through Core and continue to work when
Quick Access is absent.
This theme is owned by Core experience contracts, Views, Dashboard, Tasks,
Workflow Engine, Quick Access, Docs, and the contributing feature modules. The
first proof is the resumable service-to-decision/eAkte journey in GovOPlaN #42.
### Governed human work
An intake or event becomes owned work with a responsible actor or function,
priority, deadline, current action, consequence, source context, and completion
evidence. Tasks owns explicit work items and the unified work inbox. Workflow
Engine owns process execution, waits, retries, and handoffs. Domain modules own
the business objects and commands. Notifications attract attention but do not
replace durable work state.
This distinction applies to service requests, technical support, approvals,
data reconciliation, campaigns, meetings, decisions, records, and failed
automation. It is the immediate shared implementation priority because users
must be able to leave work and resume it safely.
### Institutional responsibility and workforce context
Organization units, functions, mandates, assignments, delegations, and acting
context determine institutional responsibility. Presence, absence, illness,
availability, and similar status are effective-dated operational facts used to
route work, suppress or redirect notifications, explain planning, and trigger
policy. They are not merely profile decorations and they do not replace the IDM
lifecycle status of an identity or account.
Time recording, absence management, sickness reporting, return-to-work
management, and applicant management form a possible workforce package. The
first implementation must be driven by a real journey and legal/privacy
profile; no new module boundary is implied solely by this register.
### Integration-first and provider-neutral operation
GovOPlaN should integrate tightly with software already used by an institution
and offer native alternatives only where that produces a better governed
outcome. Core-mediated provider contracts expose stable, vendor-neutral
capabilities; adapters encapsulate specific products. Authority, synchronized
fields, conflict behavior, health, credential custody, provenance, and
retirement must be explicit.
The LBV Baden-Wuerttemberg idea is retained as a candidate workforce/payroll
integration profile and as a test of provider-neutral contracts. Desktop and
groupware integration for Microsoft Office, Outlook, LibreOffice, Thunderbird,
file managers, Windows, Unix, and macOS should use standards, deep links,
protocol handlers, synchronization, and governed connectors before custom
desktop software is introduced.
### Inclusive channels and device surfaces
Portal, Postbox, Mail, telephone, paper, in-person assistance, API, desktop,
and mobile are channels around the same governed work. A responsive or native
mobile surface must not create a second authority or data model. Assisted work
records representation, source, attestation, receipt, correction, and delivery
choice. People may opt into permitted distribution channels while policy keeps
mandatory channels and legal delivery requirements explicit.
Video meetings, chat, instant messaging, and forums are retained as governed
collaboration-channel candidates. The default direction is integration with an
established provider through typed message, meeting, participant, evidence, and
retention contracts before building another communications stack.
### Meetings, deliberation, decisions, and voting
An institutional meeting spans scheduling, participants and mandates,
documents, agenda, discussion, formal motions, votes, decisions, minutes,
follow-up work, publication, and eligible expense settlement. Committee owns
the meeting and deliberation semantics while Calendar, Scheduling, Files,
Templates, Decisions, Tasks, Reporting, Ledger, and Voting contribute optional
capabilities.
Voting requiring certified assurance remains a provider program. POLYAS is the
first external profile; a native provider may progress only through the
controlled assurance and certification program already tracked in Voting.
### Controlled data work and understandable reporting
People should manipulate data through immutable inputs, previewed operations,
intermediate materializations, reversible definition changes, durable review
decisions, quality rules, and complete lineage. Reports expose their definitions
and source revisions so controllers can understand and change how a result is
produced. Technical support may package controlled workflows that let
non-technical users safely operate otherwise hidden data.
The monthly-data journey is the first proof. Sanctions screening follows on the
same source, snapshot, transformation, review, reporting, workflow, and
delivery contracts.
### Institutional memory and consequence
Decisions should be prepared, discussed, made, communicated, implemented, and
filed with their authority and consequences visible. A record/eAkte provides
the familiar administrative context across exact source revisions without
copying ownership from Cases, Decisions, Files, Forms, Campaign, Postbox, or
other modules. The institutional digital twin may later use governed
projections to model and simulate organizational change, but simulation output
never becomes authority without an explicit adoption decision.
## Normalized Story Catalogue
The following catalogue preserves the intent of the collected notes. It is an
orientation index, not a completion checklist.
| Actor and desired outcome | Product owner or composition | First proof |
| --- | --- | --- |
| Operator installs, updates, scales, backs up, restores, and rolls back through one explainable workflow | Core, Admin, Ops, signed package | GovOPlaN #13 and target-evidence lane |
| System and tenant module administrators govern module availability and lifecycle | Core, Admin, Policy, Tenancy | Module entitlement and lifecycle composition |
| User works in a decluttered, consistent and task-sensitive interface | Views, Core, Dashboard, Workflow, Docs | Service-to-decision workspace |
| Policy maker defines inherited, explainable and enforced rules | Policy plus every consequential owner | Information-governance adoption gate |
| Controller and auditor reconstruct results, rules, evidence and correction paths | Audit, Reporting, Records, Dataflow | Monthly-data and eAkte journeys |
| User sees institutional terminology, current progress, intermediate results and consequences | Domain owner, Tasks, Workflow, Views | All reference journey acceptance tests |
| Voting body and voter obtain independently assured democratic voting | Voting, Committee, Identity Trust, Encryption | POLYAS profile and controlled native-provider program |
| Support staff packages safe guided manipulation of hidden data | Workflow, Dataflow, Tasks, Views | Monthly reconciliation workflow |
| Data worker performs controlled, understandable and recoverable transformations | Datasources, Connectors, Dataflow, Reporting | GovOPlaN #8 |
| Decision maker prepares, deliberates, decides, records and follows consequences | Committee, Decisions, Tasks, Records, Reporting | Service-to-decision journey |
| Management delegates responsibility and receives governed activity reports | Organizations, IDM, Access, Policy, Reporting | Function-bound Postbox and work inbox |
| Institution models and simulates organizational change | Organizations, Policy, Dataflow, Reporting, Digital Twin | Later governed digital-twin package |
| Sender distributes generated files to functions without knowing incumbents | Campaign, Distribution Lists, Postbox, Organizations, IDM | Governed communication package |
| Function holder receives current and policy-selected historical work and information | IDM, Access, Postbox, Tasks, Records | Postbox reassignment/history tests |
| Administrative worker accesses one familiar eAkte context across exact owned objects | Records and record-source providers | GovOPlaN #42 and Records #8 |
| User invokes common message, template and file actions without leaving the current task | Core shell, Views, Workflow and contributing modules | Task-local action contract and service workspace |
## Idea Preservation Map
| Original idea cluster | Preserved direction |
| --- | --- |
| Time recording, absence, sickness, reintegration, applicant management | Governed workforce-context journey; effective-dated status and privacy profile before module expansion |
| LBV BW interface | Candidate provider-neutral workforce/payroll connector profile |
| Abstract interfaces | Versioned Core contracts with product adapters and explicit source authority |
| Desktop and groupware integration | Standards, connectors, deep launch and synchronization before custom clients |
| GovOPlaN app/mobile-first pages | Responsive shared semantics; native shell only when a proven journey needs device capabilities |
| Video, chat, instant messaging and forum | Optional governed collaboration providers with retention/evidence contracts |
| Somacos Session-style meeting management | Committee-led meeting composition across Calendar, Files, Decisions, Templates, Tasks, Reporting and Ledger |
| Stronger software integration | Integration-first roadmap rule and first full external product connectors |
## Maintenance
When a source idea becomes actionable:
1. link it to a named reference journey or explicit discovery issue;
2. identify the owning module and external authority;
3. create or update the Gitea issue with acceptance criteria;
4. keep live status out of this document;
5. update this register only when the durable interpretation changes.
@@ -11,13 +11,77 @@ is not an automatic dependency of every journey.
The institutional semantics and source-authority model applied to these stages
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
delivered as small, reviewable, green increments and is complete only when its
user journey, failure behavior, documentation, and operator evidence work in a
pinned composition.
## 2026 outcome reset
Repository completion is not product completion. From 2026-08-05 onward, work
is accepted primarily through three maintained real-life journeys:
1. **Governed communication:** select accountable recipients, prepare content
and attachments, approve, deliver through Mail and/or a function-bound
Postbox, reconcile uncertain outcomes, and file the evidence.
2. **Inclusive service-to-decision:** accept a request through a digital or
assisted channel, establish identity and purpose, guide the case through
human and automatic work, decide, notify, and file the resulting eAkte.
3. **Monthly data and sanctions:** acquire immutable source snapshots, validate
and reconcile them interactively, preserve decisions and lineage, produce
reports and files, and deliver the accepted result through Campaign.
The staged program below remains the architectural build order. These journeys
are the acceptance lens across those stages. Every significant feature should
identify the journey it improves, or provide security, operability, recovery,
accessibility, or usability evidence that those journeys require. Work that
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
record and item revisions, chronology, close/reopen, retention calculation,
holds, appraisal, independent disposition approval, recovery-ledger evidence,
and archive-neutral package simulation. Forms Runtime, Cases, and Decisions
expose exact, permission-rechecked source revisions for explicit filing, and
all three contribute metadata-only native Search projections that can be
rebuilt from authoritative state. The executable fixtures prove those native
transitions without claiming archival custody. A persisted Workflow Engine
handoff is now reloaded through the Tasks aggregation surface and remains
visible until the authoritative Workflow transition completes. Authenticated
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.
## Why this sequence
The sequence grows one connected product rather than advancing repositories in
@@ -86,6 +150,23 @@ journey needs and supplies contracts shared by all five stages.
execution. Database, broker, cache, and worker channels are constrained by
deployment network policy and authenticated transport rather than treated as
tenant connector profiles.
10. **Information governance.** Temporal browsing, purpose-aware access,
retention/legal-hold behavior, and institutional acting context are applied
to every owned object type. Historical reads use current authorization.
Module manifests state `contract_only`, `partial`, `enforced`, or
`not_applicable` adoption with evidence; supported maturity is blocked until
every applicable dimension is enforced.
11. **Durable human work.** Tasks aggregates explicit work and module-owned
attention items; Workflow Engine persists process state and handoffs;
Notifications attracts attention; Views focuses the relevant surfaces.
Leaving or refreshing the browser never becomes the only record that work
remains unfinished.
12. **Task-local tools.** Mail, Postbox, Templates, Files, and other common
actions may contribute bounded launch surfaces with return context. The
shell and Workflow compose them without copying their data or validation.
The optional Quick Access module presents configurable Work, Calendar,
Messages and Files categories; system/tenant/user settings and View/Policy
ceilings resolve their availability and ordering.
## Documentation contract for every reference stage
@@ -108,6 +189,9 @@ Every demonstrated journey provides:
provenance, evidence, retention, and destructive actions.
- **Acceptance view:** runnable examples, expected results, failure injection,
and release gates.
- **Channel and records view:** assisted/non-digital intake and output,
representation, provenance, filing, retention, legal hold, and archive
consequences where the journey creates evidence or a record.
The Docs module selects and links these views according to installed
capabilities and actor context. Feature repositories remain the source of
@@ -436,6 +520,9 @@ or the external editor the document-lifecycle owner.
link, callback, webhook, file, identity, or data row.
- Do not claim a stage complete from local unit tests. Use pinned composition,
target integration, failure drills, adaptive docs, and operator evidence.
- Do not claim a module complete while its relevant information-governance
dimensions remain `contract_only` or while the reference journey lacks an
assisted-channel and records outcome where those are applicable.
- A later stage may prototype contracts while the preceding gate is being
proven, but it may not redefine an owning module's boundary by convenience.
+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.
+137
View File
@@ -0,0 +1,137 @@
# GovOPlaN Strategy Status
## Status Record
| Field | Value |
| --- | --- |
| Reconciled on | 2026-08-17 |
| 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 [Roadmap](ROADMAP.md) |
| Collected product input | [Product Input Register](PRODUCT_INPUT_REGISTER.md) |
| Delivery source | Gitea issues |
This is the only prose source for current cross-product status. It is a
reconciliation, not a release certification. Module manifests and target
evidence remain authoritative for specific maturity claims.
## Portfolio Snapshot
- 67 source module manifests were loadable and architecture-declared.
- 50 modules declared `vertical_slice`; 17 declared `scaffold`.
- No module declared `reference_ready`, `supported`, or `lts`.
- The coordinated package version was `0.1.18`, with version alignment passing
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.
- Three product package manifests existed: governed communication, governed
data and assurance, and service to decision. None had crossed the complete
target-evidence gate.
These counts are dated. Refresh them rather than copying them into another
document.
## Interface And Contract Evidence
The 2026-08-17 source inventory found:
- 1,344 UI fields and 1,331 UI actions;
- 8,412 stable interface declarations with no duplicate IDs;
- 43 frontend routes and 943 backend endpoints;
- no public WebUI surfaces missing runtime declarations;
- no stale runtime route declarations;
- no unclassified endpoint without a static UI reference;
- all 1,344 fields with a resolvable F1 context; 175 have statically specific
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
the required German or English catalogs;
- 3 module information-governance dimensions classified as `enforced`, 1 as
`partial`, and 264 as `contract_only`.
This is an honest platform-wide baseline, not a claim that temporal,
purpose, retention, and institutional-context adoption is complete.
## Credible Current Outcomes
### Platform foundation
Module discovery, optional dependency validation, migrations, shared WebUI,
tenant and access foundations, signed catalogs/packages, event delivery,
recovery contracts, contextual help, views, temporal titlebar context, and
stateless-runtime patterns are implemented and tested at varying depths.
### Governed communication
Campaign authoring, recipient data, attachments, templates, mail profiles,
mock/real delivery paths, audit evidence, reporting, distribution-list
composition, and optional Postbox delivery form the deepest product cluster.
Target provider, accessibility, recovery, and high-volume evidence still
prevent a reference-ready claim.
### Institutional service and decision
Services, Forms, Forms Runtime, Cases, Parties, Mandates, Approvals, Committee,
Voting, Decisions, Portal, Postbox, and Audit have an executable service-to-
decision fixture. Public and invitation intake can retain Files-backed
evidence; Forms submissions, Cases, and formal Decisions can be explicitly
filed as exact eAkte source revisions and reconstructed through permission-
rechecked native Search projections. A durable Workflow Engine handoff now
survives session restart and appears through the Tasks work inbox until the
authoritative transition completes. Browser-complete assisted intake, broader
work projections and escalation, production identity and delivery, a named
archive profile, and target evidence remain.
### Governed data and assurance
Connectors, Datasources, Dataflow, Reporting, Search, Policy, Risk Compliance,
and Workflow provide source governance, immutable snapshots, transformation,
quality, semantic reporting, and provenance foundations. The monthly-data and
sanctions compositions now prove immutable connector snapshots, pinned
Dataflow publication, Risk Compliance review, and rescreening in process. The
journeys still need target connector profiles, complete interactive
reconciliation, governed export/delivery, and browser-level handoff evidence.
## Material Gaps
| Gap | Consequence | Next proof |
| --- | --- | --- |
| 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 |
| 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 |
| 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 |
| External production evidence incomplete | Scale, restore, interoperability and custody claims remain conditional | Real target drills and independent signed evidence |
## Active Strategic Order
1. Establish German, help, temporal, purpose, retention, and institutional
context as enforceable platform quality contracts.
2. Complete governed communication and Postbox against a named target.
3. Complete the monthly-data flow and use it as the data foundation for
sanctions screening.
4. Complete the browser proof for the digital and assisted service-to-decision
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
objects and prove reauthorization and reindex operations at target volume;
keep OpenSearch optional. Communication, Records, service-to-decision,
Dataflow, Reporting, Risk Compliance, and Datasource catalogue sources now
exist.
6. Prove one external product connector and one GovOPlaN federation exchange.
7. Finish multi-host, restore, provider, accessibility, and independent signed
target evidence before increasing maturity claims.
## Refresh Procedure
Refresh this page only from evidence:
1. run `tools/checks/check-manifest-shapes.py`;
2. run `tools/inventory/platform-interface-inventory.py --strict
--strict-declarations --strict-endpoints`;
3. run the selected reference-journey checks;
4. inspect signed release and target evidence;
5. query live Gitea issue/milestone state;
6. update the dated values and material gaps here;
7. retain prior assessments as dated evidence rather than rewriting them.
@@ -4,7 +4,8 @@
> As a system administrator, I can execute one shell command that downloads a
> verified GovOPlaN distribution and starts a completely configured Core control
> plane without optional modules. In the WebUI I can browse compatible signed
> plane with the official package directory available but only the protected
> baseline active. In the WebUI I can browse compatible signed
> module releases, select the modules for this installation, and follow every
> download, validation, migration, installation, activation, and health-check
> step. When an update is available, I can review its impact and confirm it.
@@ -25,7 +26,8 @@ The canonical backlog item is
- **Core control plane:** the smallest bootable distribution: Core API, Core
WebUI, PostgreSQL, Redis, installer worker, migration runner, and durable
storage configuration. No optional GovOPlaN module package is installed.
storage configuration. An immutable image may carry the full verified package
profile, but optional modules are not active or tenant-entitled by implication.
- **Bootstrap administrator:** a single-use, time-limited installation identity
that may access only first-run and module-lifecycle functions. It is retired
when the selected identity/access configuration becomes healthy.
@@ -55,7 +57,9 @@ The canonical backlog item is
5. It prints the local URL and one-time bootstrap credential. Re-running the
command is idempotent and shows or repairs the existing installation rather
than creating another identity or database.
6. No optional module is installed or enabled at this point.
6. Only the protected baseline is enabled. Installed package availability does
not grant permissions, tenant entitlement, View visibility, or capability
opt-in.
### Module selection, installation, and update
@@ -141,12 +145,16 @@ The canonical backlog item is
Implementation status as of the current source tree:
- Slice 1 now has the source-controlled production artifact boundary: offline
per-architecture wheel resolution, non-root API/Web image definitions,
multi-architecture OCI publication, signed composition/SBOM/provenance,
immutable Gitea assets, a signed one-file deployer, and fail-closed manifest
adoption. The first real published release and cross-architecture runtime
evidence remain release-operator work rather than source-code claims.
- Slice 1 has a published production-artifact baseline. Immutable
[`v0.1.14`](https://git.add-ideas.de/GovOPlaN/govoplan/releases/tag/v0.1.14)
binds source commit `1f039dd39c1ce2672f4978c8abc6dff862ef1445`, a signed
one-file deployer, exact API/Web and managed-dependency image digests,
composition, SBOMs, and provenance. Runtime Distribution
[run #459](https://git.add-ideas.de/GovOPlaN/govoplan/actions/runs/459)
passed migrations, schema checks, non-root API/Web readiness, and worker
delivery/shutdown on both amd64 and arm64. Each future release must renew the
evidence, and a real installation must still produce topology-specific
ingress, failover, backup, and recovery receipts.
- Slice 6 has a working application-tier foundation: state profiles, shared
object storage, runtime node registration/heartbeats/drain, fenced scheduler,
migration serialization, exact-head startup waiting, Ops visibility, and a
@@ -156,19 +164,24 @@ Implementation status as of the current source tree:
ledger and deployment operation journal. Automatic database backup and broad
adoption by module-owned external effects remain open work.
1. **Reproducible Core-only distribution.** Publish pinned multi-architecture
images, signed distribution manifest, Core-only Compose profile, bootstrap
1. **Reproducible Core-baseline distribution.** Publish pinned multi-architecture
full-package images, signed distribution manifest, Core-baseline Compose profile, bootstrap
preflight, generated secrets, readiness, and idempotent rerun/repair.
2. **First-run control plane.** Add the restricted bootstrap administrator,
one-time enrollment, initial catalog/keyring configuration, and retirement
after durable administrator access is established.
3. **Read-only online module directory.** Move the existing catalog and module
directory contracts into the installed Core WebUI with compatibility,
provenance, release-note, and update-state presentation.
4. **Durable module plan and install.** Reuse the existing installer queue,
locks, signed-package validator, rollback drill, and run evidence behind a
plan/confirm/progress UI. Add initial catalog-entry synthesis and artifact
acquisition where the current release console still assumes local sources.
3. **Read-only online module directory (implemented foundation).** Admin falls
back to the signed public stable directory, presents installed/update state,
searchable availability/blocker filters, immutable source/artifact
provenance, configuration requirements, release notes, and technical
compatibility. Withdrawn releases remain visible but cannot be planned.
Operator-configured catalogs remain an explicit override.
4. **Durable module plan and install (implemented local boundary).** Catalog
selection creates a reviewed plan; the installer queue, lock, preflight,
maintenance gate, digest-verified artifact cache, rollback drill, and run
evidence remain separate from the API process. Shared deployments convert
the same intent into a new immutable release composition instead of mutating
one replica.
5. **Safe module update.** Add drain/maintenance coordination, backup gate,
migration compatibility window, reconnectable progress, health verification,
retry/recovery, and update notification.
@@ -190,7 +203,8 @@ Implementation status as of the current source tree:
## Explicit non-goals for the first distribution slice
- Shipping optional modules in the Core image.
- Activating, tenant-entitling, or exposing optional modules merely because the
immutable image carries their verified packages.
- Exporting secrets or production business data with configuration.
- Pretending every schema migration can be reversed automatically.
- Building a proprietary orchestrator instead of supporting Compose and a
@@ -1,29 +1,32 @@
# GovOPlaN Connected Governance Platform Roadmap
# GovOPlaN Detailed Connected-Platform Vision
## Purpose and status
This document describes the long-term product destination for GovOPlaN from an
outcome and stakeholder perspective. It answers what a completely connected
governance platform should enable, how the same platform can be configured for
different institutions, and which capability horizons lead from the current
baseline to that destination.
This reference catalogue describes the long-term product destination from an
outcome and stakeholder perspective. It preserves the detailed perspectives,
configuration archetypes, stories, horizons, and maturity notes behind the
concise [Roadmap](../ROADMAP.md).
It is a durable direction, not a release promise or a substitute for issue
tracking. Live work state belongs in Gitea issues. The
It is not a release promise, live plan, or second status source. The concise
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)
remains the technical module and wave sequence; this document supplies the
cross-product vision that sequence serves.
Read it together with:
- the [institutional governance target architecture](INSTITUTIONAL_GOVERNANCE_TARGET_ARCHITECTURE.md)
- the [selected reference-journey program](REFERENCE_JOURNEY_PROGRAM.md)
- the [current capability and infrastructure fit assessment](CAPABILITY_AND_INFRASTRUCTURE_FIT.md)
- the [interface pattern language](INTERFACE_PATTERN_LANGUAGE.md)
- the [interface surface inventory](INTERFACE_SURFACE_INVENTORY.md)
- the [module contract and install model](MODULE_CONTRACTS_AND_INSTALLS.md)
- the [repository and module index](REPOSITORY_INDEX.md)
- the [Gitea issue workflow](GITEA_ISSUES.md)
- the [concise product roadmap](../ROADMAP.md)
- the [institutional governance target architecture](../../architecture/INSTITUTIONAL_GOVERNANCE_TARGET_ARCHITECTURE.md)
- the [selected reference-journey program](../REFERENCE_JOURNEY_PROGRAM.md)
- the [current strategy status](../STRATEGY_STATUS.md)
- the [generated, pinned Campaign capability and infrastructure fit assessment](../../evidence/snapshots/CAPABILITY_AND_INFRASTRUCTURE_FIT.generated.md)
- the [interface pattern language](../../architecture/INTERFACE_PATTERN_LANGUAGE.md)
- the [interface surface inventory](../../evidence/snapshots/INTERFACE_SURFACE_INVENTORY.md)
- the [module contract and install model](../../operations/MODULE_CONTRACTS_AND_INSTALLS.md)
- the [repository and module index](../../project/REPOSITORY_INDEX.md)
- the [Gitea issue workflow](../../project/GITEA_ISSUES.md)
### How to read this roadmap
@@ -41,16 +44,17 @@ Read it together with:
- Use [Near-term portfolio order](#near-term-portfolio-order) for the bridge to
implementation and [Product decisions](#product-decisions-to-make-progressively)
for choices that can remain deferred.
- Use the [dated snapshot appendix](#snapshot-appendix-2026-07-20) only to
understand which live backlog and release facts informed this revision.
- 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.
### Planning ownership
| 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 |
| What is actively planned, blocked, implemented, or closed now? | Gitea issues |
| 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 |
The horizons and near-term order below express product outcomes and portfolio
@@ -109,7 +113,7 @@ safe modules -> connected work -> reusable services -> institutional assurance -
```
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
ordered demonstrations through which the shared platform contracts and horizon
gates are to be proved. Connector safety, identity/function semantics,
@@ -265,8 +269,8 @@ Diagnostics minimize personal data and link to governed evidence when deeper
inspection is authorized.
The complete installation and lifecycle journey is specified in the
[System Administrator Lifecycle User Story](SYSTEM_ADMINISTRATOR_LIFECYCLE_USER_STORY.md):
one-command Core-only bootstrap, signed online module installation and updates,
[System Administrator Lifecycle User Story](../SYSTEM_ADMINISTRATOR_LIFECYCLE_USER_STORY.md):
one-command Core-baseline bootstrap, signed online module installation and updates,
stateless scale-out, versioned configuration transfer, undo, and reproducible
environment-promotion recipes.
@@ -949,7 +953,7 @@ first analytical product prove Horizons 2 and 3; governed BI adds assurance and
ecosystem capabilities across Horizons 35; collaborative documents combine
the evidence spine, service packages, and records assurance across Horizons
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
@@ -975,8 +979,9 @@ checkouts.
Priorities:
1. Deliver the first slices of the
[System Administrator Lifecycle User Story](SYSTEM_ADMINISTRATOR_LIFECYCLE_USER_STORY.md):
a verified Core-only distribution, first-run control plane, read-only online
[System Administrator Lifecycle User Story](../SYSTEM_ADMINISTRATOR_LIFECYCLE_USER_STORY.md):
a verified full-package distribution with only the Core baseline active,
first-run control plane, read-only online
module directory, and durable plan/confirm/install progress.
2. Pin and publish a compatible Core/WebUI/module composition and first
reference configuration package.
@@ -1218,7 +1223,7 @@ provides all applicable evidence below.
## Near-term portfolio order
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
portfolio only through an explicit bounded package or reference journey.
@@ -1351,71 +1356,10 @@ language, what service it configured, who can act, which systems participate,
what happens when they fail, how a decision can be reviewed, and where the
evidence remains—and the product can prove that explanation at runtime.
## Snapshot appendix: 2026-07-20
## Dated Context
This appendix records volatile facts that informed this revision. It is not a
second source of truth and should be refreshed or removed when a later roadmap
review uses a new release/backlog snapshot.
### Composition and release snapshot
The cross-repository contract scan found 43 module manifest contracts, 29
provided interface names, 16 requirements, and no contract error across 65
scanned repositories. That is meaningful composition evidence, but the release
metadata trailed the integrated code: Core, Policy, Poll, and Scheduling
declared `0.1.9` while the whole-product release requirements remained on
module tag `v0.1.8`; the root self-hosted `.env.example` and release smoke
composition did not yet exercise all installed release modules. Other
development compositions already included some of those modules. This was a
release/composition gap, not evidence that the underlying slices did not exist.
### Backlog snapshot
The Gitea audit found 206 open issues across 36 of 66 catalogued repositories
and 362 closed issues. Campaign had 51 open issues and Core 44; together they
held 46% of current work. This reflected substantial completed kernel,
security, and platform work and a deliberate concentration on the first usable
vertical, but also risked crowding out production evidence and the shared
process spine.
The issue workflow needed a reconciliation pass before another delivery
program could be inferred from labels: 119 open issues remained in triage, 116
had no milestone, and several recently pushed Calendar, Scheduling, Poll,
Campaign, and Files slices still described themselves as local or awaiting
integration. Conversely, 30 repositories had no open issue; for many
later-wave modules this meant no implementation program had been opened, not
that the capability was complete.
[Poll #2](https://git.add-ideas.de/GovOPlaN/govoplan-poll/issues/2) was a clear
tracker-drift example: its configurable transition engine, agreed transition
matrix/history, idempotent keyed retries, re-decision audit, archive/unarchive,
and preservation behavior were implemented and pushed while the issue still
reported `needs-info`.
Issue anchors that informed the bridge from the baseline into this roadmap:
- [Meta #10](https://git.add-ideas.de/GovOPlaN/govoplan/issues/10) for the
capability/infrastructure assessment and its target proof;
- [Meta #11](https://git.add-ideas.de/GovOPlaN/govoplan/issues/11) for the
universal interface and focused-view direction;
- [Core #225](https://git.add-ideas.de/GovOPlaN/govoplan-core/issues/225) for
guided, safe configuration;
- [Core #29](https://git.add-ideas.de/GovOPlaN/govoplan-core/issues/29) for the
backup/restore production gate;
- [Core #263](https://git.add-ideas.de/GovOPlaN/govoplan-core/issues/263) and
[Campaign #63](https://git.add-ideas.de/GovOPlaN/govoplan-campaign/issues/63),
[#62](https://git.add-ideas.de/GovOPlaN/govoplan-campaign/issues/62),
[#65](https://git.add-ideas.de/GovOPlaN/govoplan-campaign/issues/65), and
[#69](https://git.add-ideas.de/GovOPlaN/govoplan-campaign/issues/69) for the
reference interface/delivery vocabulary and behavior;
- [Poll #1](https://git.add-ideas.de/GovOPlaN/govoplan-poll/issues/1) for the
database-enforced respondent invariant exposed by Scheduling;
- [Connectors #6](https://git.add-ideas.de/GovOPlaN/govoplan-connectors/issues/6)
for the governed connector configuration/simulation foundation;
- [Meta #9](https://git.add-ideas.de/GovOPlaN/govoplan/issues/9) for the first
permit-to-payment reference process; and
- [Meta #12](https://git.add-ideas.de/GovOPlaN/govoplan/issues/12) for the
deliberately deferred, consumer-independent export-control story.
Live Gitea issue state remains canonical. These dated facts explain the roadmap
sequence only.
The volatile release and backlog appendix that originally accompanied this
roadmap has been removed so the durable direction cannot become a competing
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
[Strategy Status](../STRATEGY_STATUS.md), and live work state belongs in Gitea.
+11
View File
@@ -0,0 +1,11 @@
# GovOPlaN developer meta-package
`govoplan` is an optional convenience package for local development and
composition tests. The default dependency set matches the reviewed runtime
release roots; `govoplan[full]` adds every packageable module present in the
workspace at generation time.
This package is not a production deployment artifact. Production installations
consume the signed runtime distribution manifest and digest-pinned OCI images.
The package does not enable modules, apply migrations, choose infrastructure,
or replace installation and recovery evidence.
+97
View File
@@ -0,0 +1,97 @@
[build-system]
requires = ["setuptools>=69", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "govoplan"
version = "0.1.43"
description = "Developer convenience package for a versioned GovOPlaN composition"
readme = "README.md"
requires-python = ">=3.12"
license = { text = "AGPL-3.0-or-later" }
dependencies = [
"govoplan-core[server]==0.1.43",
"govoplan-tenancy==0.1.21",
"govoplan-organizations==0.1.20",
"govoplan-identity==0.1.20",
"govoplan-idm==0.1.24",
"govoplan-access==0.1.24",
"govoplan-admin==0.1.22",
"govoplan-policy==0.1.22",
"govoplan-audit==0.1.20",
"govoplan-dashboard==0.1.20",
"govoplan-files==0.1.24",
"govoplan-mail==0.1.26",
"govoplan-campaign==0.1.27",
"govoplan-calendar==0.1.22",
"govoplan-docs==0.1.22",
"govoplan-ops==0.1.21",
]
[project.optional-dependencies]
full = [
"govoplan-addresses==0.1.21",
"govoplan-approvals==0.1.20",
"govoplan-assets==0.1.20",
"govoplan-booking==0.1.20",
"govoplan-cases==0.1.22",
"govoplan-certificates==0.1.20",
"govoplan-committee==0.1.20",
"govoplan-connectors==0.1.25",
"govoplan-consultation==0.1.20",
"govoplan-contracts==0.1.20",
"govoplan-dataflow==0.1.23",
"govoplan-datasources==0.1.24",
"govoplan-decisions==0.1.19",
"govoplan-dist-lists==0.1.20",
"govoplan-dms==0.1.20",
"govoplan-encryption==0.1.19",
"govoplan-erp==0.1.20",
"govoplan-evaluation==0.1.20",
"govoplan-facilities==0.1.20",
"govoplan-fit-connect==0.1.20",
"govoplan-forms==0.1.22",
"govoplan-forms-runtime==0.1.20",
"govoplan-grants==0.1.20",
"govoplan-helpdesk==0.1.21",
"govoplan-identity-trust==0.1.20",
"govoplan-inspections==0.1.20",
"govoplan-learning==0.1.20",
"govoplan-mandates==0.1.19",
"govoplan-notifications==0.1.19",
"govoplan-parties==0.1.19",
"govoplan-payments==0.1.21",
"govoplan-permits==0.1.20",
"govoplan-poll==0.1.20",
"govoplan-portal==0.1.21",
"govoplan-postbox==0.1.22",
"govoplan-procurement==0.1.20",
"govoplan-projects==0.1.19",
"govoplan-quick-access==0.1.20",
"govoplan-records==0.1.22",
"govoplan-reporting==0.1.20",
"govoplan-resources==0.1.20",
"govoplan-rest==0.1.19",
"govoplan-risk-compliance==0.1.20",
"govoplan-scheduling==0.1.21",
"govoplan-search==0.1.19",
"govoplan-services==0.1.19",
"govoplan-soap==0.1.19",
"govoplan-tasks==0.1.21",
"govoplan-templates==0.1.21",
"govoplan-tickets==0.1.22",
"govoplan-transparency==0.1.20",
"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]
Repository = "https://git.add-ideas.de/GovOPlaN/govoplan"
Documentation = "https://govoplan.add-ideas.de"
[tool.setuptools.packages.find]
where = ["src"]
@@ -0,0 +1,12 @@
"""Metadata helpers for the optional GovOPlaN developer composition."""
from importlib.metadata import PackageNotFoundError, version
try:
__version__ = version("govoplan")
except PackageNotFoundError: # pragma: no cover - source checkout only
__version__ = "0+unknown"
__all__ = ["__version__"]
@@ -28,5 +28,7 @@ The artifact remains a `product` package. Promotion to `reference` requires:
access; and
- version-pinned user and administrator documentation.
Optional Notifications, Portal, Reporting, and Workflow Engine integrations do
not change the package boundary when absent.
Optional Notifications, Portal, Reporting, Tasks, and Workflow Engine
integrations do not change the package boundary when absent. When Tasks is
present, acknowledgement, reconciliation, and operator intervention remain
owned by their source modules and are projected into the common work inbox.
@@ -21,6 +21,7 @@
{"module_id": "notifications"},
{"module_id": "portal"},
{"module_id": "reporting"},
{"module_id": "tasks"},
{"module_id": "workflow_engine"}
],
"evidence": [
@@ -12,7 +12,7 @@ review.
1. Register a typed datasource with source authority, purpose, classification,
owner, freshness, and correction policy.
2. Acquire or upload an immutable source state.
3. execute a versioned flow and retain intermediate materializations and
3. Execute a versioned flow and retain intermediate materializations and
provenance;
4. publish a report or decision input against exact source and flow revisions;
5. link obligation, governed object, risk, control, evidence, finding,
@@ -37,5 +37,28 @@ The artifact remains a `product` package. Promotion to `reference` requires:
aggregate disclosure; and
- version-pinned user and administrator documentation.
Optional Connectors, Files, Notifications, and Workflow Engine integrations
must remain capability-based and absence-safe.
Optional Connectors, Files, Notifications, Tasks, and Workflow Engine
integrations must remain capability-based and absence-safe. Tasks may present
review and recovery handoffs, but Dataflow and Risk Compliance remain the
authoritative owners of run and screening state.
## Executable evidence
- `tools/checks/check-datasource-composition.py` composes Connector snapshots,
governed Datasources, queued Dataflow execution, frozen publication,
idempotent replay, and recovery evidence.
- `govoplan-dataflow/fixtures/golden/monthly-reconciliation` pins synthetic
monthly inputs, stable reconciliation hashes, reviewed decisions, expected
output, source fingerprints, and output hashes.
- `tools/checks/check-sanctions-screening-composition.py` composes an immutable
Connector acquisition, idempotent Risk Compliance import and screening,
independent disposition, a cleared gate, changed-source invalidation, and
the rescreening queue through the registered versioned capabilities.
- `govoplan-dataflow/fixtures/golden/sanctions-screening` independently proves
the deterministic normalization and matching graph with exact expected
output.
These checks use synthetic data and run without network access. They prove the
module contracts and durable state transitions; they do not replace the
deployment, security, privacy, accessibility, and operator evidence still
listed above.
@@ -21,6 +21,7 @@
{"module_id": "connectors"},
{"module_id": "files"},
{"module_id": "notifications"},
{"module_id": "tasks"},
{"module_id": "workflow_engine"}
],
"evidence": [
@@ -28,7 +29,17 @@
"kind": "documentation",
"reference": "packages/product/governed-data-assurance/README.md",
"summary": "Defines the package boundary, provenance chain, and reference-readiness gates."
},
{
"kind": "target_test",
"reference": "tools/checks/check-datasource-composition.py",
"summary": "Proves governed Connector acquisition, Datasource registration, queued Dataflow execution, frozen publication, idempotency, and recovery evidence."
},
{
"kind": "target_test",
"reference": "tools/checks/check-sanctions-screening-composition.py",
"summary": "Proves immutable sanctions acquisition, import, screening replay, independent review, freshness gates, and rescreening across module capabilities."
}
],
"tags": ["datasources", "dataflow", "reporting", "assurance"]
"tags": ["datasources", "dataflow", "reporting", "sanctions", "assurance"]
}
+75 -5
View File
@@ -9,6 +9,34 @@ resolution -> approval/deliberation -> formal Decision -> observed delivery
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
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
```
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
provider-owned revisions for the parts they own. Portal, Cases, and Committee
consume capabilities for cross-module semantics only. The package does not
@@ -24,19 +60,53 @@ grant cross-module table access and can omit optional presentation, work,
deliberation, delivery, or records modules while retaining explicit references
to externally performed steps.
When Records is present, Forms Runtime, Cases, and Decisions expose exact,
digest-bound source snapshots for explicit filing. The source module rechecks
current access, Records chooses the destination and preserves chronology, and
the filed reference never becomes an editable copy. When Search is present,
the same three owners contribute rebuildable metadata-only projections. Form
values, evidence payloads, Decision reasoning, operative results, and
conditions are excluded; every candidate is authorized again before it is
shown.
When Tasks is present, explicit work and source-owned Workflow handoffs appear
in one resumable inbox with typed account, group, role, function, or assignment
responsibility. Workflow Engine retains process state and completion commands;
Tasks retains only explicit tasks and the aggregation surface.
## Security And Recovery
Every provider is tenant-bound. Missing or conflicting authority fails closed.
Protected Decision content has a separate permission. Writes are replay-safe
and OCC-guarded. Database restore is the semantic-state recovery unit; file and
communication effects remain governed by their owning providers and are linked
through requested/observed effect, evidence, and audit references.
through requested/observed effect, evidence, and audit references. Search is a
derived recovery unit and can be rebuilt from authoritative module state.
The executable fixture in
`tests/test_institutional_governance_journey.py` proves SQL-backed Service,
Case, Party, Mandate, Committee meeting/agendum/vote/minute, and Decision state.
`tests/test_institutional_service_journey.py` separately proves exact Portal
Form launch, persisted submission provenance, and idempotent replay.
Target-environment accessibility, security, operator, privacy,
delivery-provider, and recovery evidence are still required before this product
package may claim `reference_ready` maturity.
Form launch, persisted submission provenance, idempotent replay, resumable
assisted intake with enforced read-back evidence, and a durable Workflow handoff
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,
and Decision revision filing. Target-environment browser accessibility,
production identity and delivery, a named archive profile, and recovery evidence
are still required before this product package may claim `reference_ready`
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,35 +8,366 @@
"category": "institutional-governance",
"license": "AGPL-3.0-or-later",
"required_modules": [
{"module_id": "access"},
{"module_id": "audit"},
{"module_id": "cases"},
{"module_id": "decisions"},
{"module_id": "forms"},
{"module_id": "forms_runtime"},
{"module_id": "mandates"},
{"module_id": "parties"},
{"module_id": "payments"},
{"module_id": "policy"},
{"module_id": "portal"},
{"module_id": "services"}
{"module_id": "services"},
{"module_id": "tasks"},
{"module_id": "workflow_engine"}
],
"required_capabilities": [
"access.configuration",
"cases.party_context",
"cases.service_intake",
"decisions.registry",
"forms.configuration",
"forms.definitions",
"mandates.resolver",
"parties.resolver",
"payments.requests",
"portal.service_directory",
"services.availability",
"services.definitions"
"services.definitions",
"workflow.configuration"
],
"optional_modules": [
{"module_id": "approvals"},
{"module_id": "committee"},
{"module_id": "files"},
{"module_id": "forms"},
{"module_id": "forms_runtime"},
{"module_id": "postbox"},
{"module_id": "records"},
{"module_id": "tasks"},
{"module_id": "workflow_engine"}
{"module_id": "search"}
],
"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": [
{
@@ -44,6 +375,11 @@
"reference": "packages/product/service-to-decision/README.md",
"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",
"reference": "tests/test_institutional_governance_journey.py",
+1
View File
@@ -58,6 +58,7 @@
{"name": "govoplan-postbox", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-postbox.git", "path": "govoplan-postbox"},
{"name": "govoplan-procurement", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-procurement.git", "path": "govoplan-procurement"},
{"name": "govoplan-projects", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-projects.git", "path": "govoplan-projects"},
{"name": "govoplan-quick-access", "category": "module", "subtype": "platform", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-quick-access.git", "path": "govoplan-quick-access"},
{"name": "govoplan-records", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-records.git", "path": "govoplan-records"},
{"name": "govoplan-reporting", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-reporting.git", "path": "govoplan-reporting"},
{"name": "govoplan-resources", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-resources.git", "path": "govoplan-resources"},
+3
View File
@@ -30,11 +30,14 @@
-e ../govoplan-parties
-e ../govoplan-mandates
-e ../govoplan-decisions
-e ../govoplan-payments
-e ../govoplan-connectors
-e ../govoplan-datasources
-e ../govoplan-dataflow
-e ../govoplan-workflow-engine
-e ../govoplan-workflow
-e ../govoplan-tasks
-e ../govoplan-quick-access
-e ../govoplan-views
-e ../govoplan-voting
-e ../govoplan-search
+15 -15
View File
@@ -1,18 +1,18 @@
# Whole-product release install from immutable, independently versioned module tags.
# Only add a module after its referenced tag has been published.
../govoplan-core[server]
govoplan-tenancy @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-tenancy.git@v0.1.8
govoplan-organizations @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-organizations.git@v0.1.8
govoplan-identity @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-identity.git@v0.1.8
govoplan-idm @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-idm.git@v0.1.8
govoplan-access @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-access.git@v0.1.8
govoplan-admin @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-admin.git@v0.1.8
govoplan-policy @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-policy.git@v0.1.8
govoplan-audit @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-audit.git@v0.1.8
govoplan-dashboard @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-dashboard.git@v0.1.8
govoplan-files @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-files.git@v0.1.8
govoplan-mail @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-mail.git@v0.1.10
govoplan-campaign @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-campaign.git@v0.1.11
govoplan-calendar @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-calendar.git@v0.1.8
govoplan-docs @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-docs.git@v0.1.8
govoplan-ops @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-ops.git@v0.1.8
govoplan-tenancy @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-tenancy.git@v0.1.21
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.20
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.24
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.22
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.20
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.26
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.22
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.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."
]
}
}
+120
View File
@@ -0,0 +1,120 @@
from __future__ import annotations
import base64
import json
from pathlib import Path
import stat
import subprocess
import sys
import tempfile
import unittest
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
from jsonschema import Draft202012Validator, FormatChecker
META_ROOT = Path(__file__).resolve().parents[1]
GENERATOR = META_ROOT / "tools" / "assessments" / "generate-authority-keypair.py"
class AssessmentAuthorityKeypairTests(unittest.TestCase):
def test_generates_schema_valid_scoped_proof_authority(self) -> None:
with tempfile.TemporaryDirectory() as temp_dir:
output_dir = Path(temp_dir)
output_dir.chmod(0o700)
private_path = output_dir / "target.pem"
keyring_path = output_dir / "target.json"
result = subprocess.run(
(
sys.executable,
str(GENERATOR),
"--purpose",
"proof",
"--key-id",
"authority:target-2026",
"--scope",
"target_environment",
"--scope",
"operations",
"--private-key",
str(private_path),
"--keyring",
str(keyring_path),
),
check=False,
capture_output=True,
text=True,
)
self.assertEqual(0, result.returncode, result.stderr)
self.assertEqual(0o600, stat.S_IMODE(private_path.stat().st_mode))
self.assertEqual(0o600, stat.S_IMODE(keyring_path.stat().st_mode))
keyring = json.loads(keyring_path.read_text(encoding="utf-8"))
schema = json.loads(
(
META_ROOT
/ "docs"
/ "capability-fit-proof-authority-keyring.schema.json"
).read_text(encoding="utf-8")
)
errors = tuple(
Draft202012Validator(
schema, format_checker=FormatChecker()
).iter_errors(keyring)
)
self.assertEqual((), errors)
self.assertEqual(
["target_environment", "operations"],
keyring["keys"][0]["allowed_scopes"],
)
private_key = serialization.load_pem_private_key(
private_path.read_bytes(), password=None
)
self.assertIsInstance(private_key, Ed25519PrivateKey)
public_key = base64.b64encode(
private_key.public_key().public_bytes(
encoding=serialization.Encoding.Raw,
format=serialization.PublicFormat.Raw,
)
).decode("ascii")
self.assertEqual(public_key, keyring["keys"][0]["public_key"])
def test_installer_authority_uses_fixed_scope_and_refuses_overwrite(self) -> None:
with tempfile.TemporaryDirectory() as temp_dir:
output_dir = Path(temp_dir)
output_dir.chmod(0o700)
private_path = output_dir / "installer.pem"
keyring_path = output_dir / "installer.json"
command = (
sys.executable,
str(GENERATOR),
"--purpose",
"installer",
"--key-id",
"authority:installer-2026",
"--private-key",
str(private_path),
"--keyring",
str(keyring_path),
)
first = subprocess.run(
command, check=False, capture_output=True, text=True
)
second = subprocess.run(
command, check=False, capture_output=True, text=True
)
self.assertEqual(0, first.returncode, first.stderr)
self.assertNotEqual(0, second.returncode)
keyring = json.loads(keyring_path.read_text(encoding="utf-8"))
self.assertEqual(
["installed_release_origin"],
keyring["keys"][0]["allowed_scopes"],
)
if __name__ == "__main__":
unittest.main()
+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
import unittest
from sqlalchemy import create_engine
from sqlalchemy.orm import Session
from govoplan_core.core.configuration_packages import (
ConfigurationApplyResult,
ConfigurationExportResult,
ConfigurationPackageManifest,
ConfigurationPlanItem,
ConfigurationPreflightContext,
ConfigurationPreflightResult,
ConfigurationProviderDescription,
configuration_package_claim_issues,
dry_run_configuration_package,
)
@@ -54,10 +62,19 @@ class ConfigurationPackageArtifactTests(unittest.TestCase):
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(
manifest,
(),
providers,
ConfigurationPreflightContext(
supplied_data=supplied_data,
installed_modules={
item.module_id: item.version or "workspace"
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.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__":
unittest.main()
+477 -2
View File
@@ -1,8 +1,10 @@
from __future__ import annotations
from contextlib import redirect_stderr, redirect_stdout
from datetime import UTC, datetime, timedelta
import io
import json
import os
from pathlib import Path
import stat
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
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
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):
def test_kubernetes_evidence_requires_two_node_spread_and_safe_runtime(
self,
@@ -158,6 +200,81 @@ class DeploymentInstallerTests(unittest.TestCase):
evidence["snapshot"]["ready_node_names"],
)
def test_kubernetes_api_loss_uses_a_non_json_mutation_command(self) -> None:
initial_pods = [
_kubernetes_test_pod("api-a", "api", "node-a"),
_kubernetes_test_pod("api-b", "api", "node-b"),
_kubernetes_test_pod("web-a", "web", "node-a"),
_kubernetes_test_pod("web-b", "web", "node-b"),
]
replacement_pods = [
_kubernetes_test_pod("api-b", "api", "node-b"),
_kubernetes_test_pod("api-c", "api", "node-a"),
]
deleted = False
actions: list[tuple[str, ...]] = []
def run(arguments):
if "nodes" in arguments:
return {
"items": [
{
"metadata": {"name": name},
"spec": {},
"status": {
"conditions": [
{"type": "Ready", "status": "True"}
]
},
}
for name in ("node-a", "node-b")
]
}
if "deployments" in arguments:
return {
"items": [
_kubernetes_test_deployment("api", 2),
_kubernetes_test_deployment("web", 2),
]
}
return {"items": replacement_pods if deleted else initial_pods}
def act(arguments):
nonlocal deleted
actions.append(tuple(arguments))
deleted = True
evidence = collect_kubernetes_evidence(
installation_id="govoplan-cluster",
namespace="govoplan",
ops_url="https://govoplan.example.test/api/v1/ops/status",
api_key="not-retained",
exercise_api_pod_loss=True,
command_runner=run,
action_runner=act,
json_fetcher=lambda _url, _key: {
"readiness": {"ready": True},
"runtime_cluster": {
"composition": {"skewed": False},
"software_versions": {"skewed": False},
"queues": {"missing": []},
},
"checks": [
{
"id": "database_capacity",
"state": "ok",
"detail": "Within budget",
}
],
},
)
self.assertEqual("passed", evidence["api_pod_loss"]["state"])
self.assertEqual(1, len(actions))
self.assertIn("delete", actions[0])
self.assertNotIn("-o", actions[0])
self.assertNotIn("not-retained", json.dumps(evidence))
def test_pre_migration_failure_restores_checksum_verified_applied_bundle(
self,
) -> None:
@@ -339,6 +456,27 @@ class DeploymentInstallerTests(unittest.TestCase):
self.assertNotIn("db-secret", rendered)
self.assertNotIn("redis-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("StatefulSet", kinds)
self.assertEqual(3, deployments["govoplan-cluster-api"]["spec"]["replicas"])
@@ -347,6 +485,10 @@ class DeploymentInstallerTests(unittest.TestCase):
"template"
]["spec"]["containers"][0]["command"]
self.assertIn("govoplan_core.commands.fenced_run", scheduler_command)
self.assertEqual(
["--schedule", "/tmp/celerybeat-schedule"],
scheduler_command[-2:],
)
api_init_command = deployments["govoplan-cluster-api"]["spec"]["template"][
"spec"
]["initContainers"][0]["command"]
@@ -381,10 +523,32 @@ class DeploymentInstallerTests(unittest.TestCase):
"containers"
][0]["readinessProbe"]["httpGet"]["httpHeaders"],
)
api_pod_spec = deployments["govoplan-cluster-api"]["spec"]["template"][
"spec"
]
self.assertEqual(30, api_pod_spec["terminationGracePeriodSeconds"])
self.assertEqual(
["/bin/sh", "-c", "sleep 10"],
api_pod_spec["containers"][0]["lifecycle"]["preStop"]["exec"][
"command"
],
)
self.assertNotIn(
"lifecycle",
deployments["govoplan-cluster-worker"]["spec"]["template"]["spec"][
"containers"
][0],
)
worker_command = deployments["govoplan-cluster-worker"]["spec"]["template"][
"spec"
]["containers"][0]["command"]
self.assertIn("--concurrency", worker_command)
for deployment in deployments.values():
spread = deployment["spec"]["template"]["spec"][
"topologySpreadConstraints"
][0]
self.assertEqual("DoNotSchedule", spread["whenUnsatisfiable"])
self.assertEqual(["pod-template-hash"], spread["matchLabelKeys"])
self.assertEqual(
"62",
manifest["metadata"]["annotations"][
@@ -392,6 +556,99 @@ class DeploymentInstallerTests(unittest.TestCase):
],
)
def test_kubernetes_export_mounts_an_optional_s3_ca_on_backend_roles(
self,
) -> None:
spec = default_spec(
installation_id="govoplan-cluster",
postgres_mode="external",
redis_mode="external",
storage_mode="s3",
api_replicas=2,
web_replicas=2,
worker_replicas=2,
api_image="registry.example.test/govoplan-api@sha256:" + "a" * 64,
web_image="registry.example.test/govoplan-web@sha256:" + "b" * 64,
)
environment = initial_secrets(
spec,
supplied={
"DATABASE_URL": "postgresql+psycopg://user:secret@postgres.example.test/govoplan",
"GOVOPLAN_DATABASE_URL_PGTOOLS": "postgresql://user:secret@postgres.example.test/govoplan",
"REDIS_URL": "rediss://: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",
"GOVOPLAN_DB_CONNECTION_LIMIT": "100",
},
)
manifest = render_kubernetes(
spec,
environment,
s3_ca_secret_name="govoplan-s3-ca",
backup_required=False,
)
backend_pods = [
item["spec"]["template"]["spec"]
for item in manifest["items"]
if item["kind"] in {"Deployment", "Job"}
and item["metadata"]["labels"].get("app.kubernetes.io/component")
in {"api", "worker", "scheduler", "migration"}
]
web = next(
item
for item in manifest["items"]
if item["kind"] == "Deployment"
and item["metadata"]["labels"].get("app.kubernetes.io/component")
== "web"
)
self.assertTrue(backend_pods)
for pod in backend_pods:
self.assertIn(
{
"name": "s3-ca",
"secret": {
"secretName": "govoplan-s3-ca",
"items": [{"key": "ca.crt", "path": "s3-ca.crt"}],
},
},
pod["volumes"],
)
for container in [*pod.get("initContainers", []), *pod["containers"]]:
self.assertIn(
{
"name": "AWS_CA_BUNDLE",
"value": "/etc/govoplan/trust/s3-ca.crt",
},
container["env"],
)
self.assertIn(
{
"name": "s3-ca",
"mountPath": "/etc/govoplan/trust",
"readOnly": True,
},
container["volumeMounts"],
)
self.assertNotIn(
"s3-ca",
{
volume["name"]
for volume in web["spec"]["template"]["spec"]["volumes"]
},
)
with self.assertRaisesRegex(ValueError, "S3 CA secret"):
render_kubernetes(
spec,
environment,
s3_ca_secret_name="INVALID_NAME",
backup_required=False,
)
def test_kubernetes_export_splits_worker_queues_and_rejects_capacity_overrun(
self,
) -> None:
@@ -529,6 +786,10 @@ class DeploymentInstallerTests(unittest.TestCase):
compose["services"]["load-balancer"]["ports"],
)
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"]["web"]["scale"])
@@ -754,6 +1015,85 @@ class DeploymentInstallerTests(unittest.TestCase):
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:
spec = default_spec(
storage_mode="garage",
@@ -883,10 +1223,14 @@ class DeploymentInstallerTests(unittest.TestCase):
with tempfile.TemporaryDirectory(prefix="govoplan-deploy-test-") as directory:
paths = bundle_paths(Path(directory))
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)
write_env(paths.env, first_environment)
first_plan = build_plan(first_spec, paths, include_host_checks=False)
first_capabilities = infrastructure_capability_document(
first_spec,
first_environment,
)
atomic_write(
paths.receipt,
canonical_json(
@@ -897,12 +1241,17 @@ class DeploymentInstallerTests(unittest.TestCase):
first_plan.desired_environment_fingerprint
),
"services": list(render_compose(first_spec)["services"]),
"infrastructure_capabilities": first_capabilities,
}
),
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(
paths.env,
reconcile_runtime_environment(second_spec, first_environment),
@@ -918,6 +1267,79 @@ class DeploymentInstallerTests(unittest.TestCase):
{"redis", "worker", "scheduler", "test-mail"},
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:
with tempfile.TemporaryDirectory(prefix="govoplan-deploy-test-") as directory:
@@ -1027,6 +1449,54 @@ class DeploymentInstallerTests(unittest.TestCase):
)[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:
with tempfile.TemporaryDirectory(prefix="govoplan-deploy-test-") as directory:
root = Path(directory) / "installation"
@@ -1286,6 +1756,11 @@ class DeploymentInstallerTests(unittest.TestCase):
receipt["listen"],
)
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:
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 datetime import UTC, datetime, timedelta
import json
from pathlib import Path
import unittest
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)
JOURNEY = json.loads(
(Path(__file__).parent / "fixtures/resident_parking_permit_journey.json").read_text(
encoding="utf-8"
)
)
def _reference(
@@ -177,31 +184,31 @@ class InstitutionalGovernanceJourneyTests(unittest.TestCase):
service = ServiceDefinition(
reference=_reference(
"service",
"permit-service",
JOURNEY["service"]["object_id"],
owner="services",
version="5",
version=JOURNEY["service"]["version"],
),
key="permit.apply",
key=JOURNEY["service"]["key"],
temporal=TemporalRevision(
revision="5",
revision=JOURNEY["service"]["version"],
valid_from=NOW - timedelta(days=1),
valid_to=NOW + timedelta(days=30),
recorded_at=NOW - timedelta(days=2),
change_reason="Service published.",
),
title="Apply for a permit",
title=JOURNEY["title"],
audience=("resident",),
legal_bases=(legal_basis,),
required_evidence_types=("application", "identity"),
channels=("portal", "postbox"),
required_evidence_types=tuple(JOURNEY["service"]["required_evidence_types"]),
channels=tuple(JOURNEY["service"]["channels"]) + ("postbox",),
responsible_organization_ref=organization,
responsible_function_ref=function,
mandate_ref=mandate_ref,
jurisdiction_refs=(jurisdiction,),
bindings=(
ServiceBinding("case", "permit-application"),
ServiceBinding("workflow", "workflow:permit-review"),
ServiceBinding("result", "decision:permit"),
ServiceBinding("case", JOURNEY["case"]["type_key"]),
ServiceBinding("workflow", "workflow:resident-parking-permit-review"),
ServiceBinding("result", f"decision:{JOURNEY['decision']['type']}"),
),
remedy_refs=("review:administrative-court",),
publication_state="published",
@@ -230,37 +237,40 @@ class InstitutionalGovernanceJourneyTests(unittest.TestCase):
upsert_case_status(
session,
principal,
status_key="intake",
status_key=JOURNEY["case"]["initial_status"],
label="Intake",
)
upsert_case_status(
session,
principal,
status_key="decided",
status_key=JOURNEY["case"]["decided_status"],
label="Decided",
category="decided",
)
upsert_case_type(
session,
principal,
type_key="permit-application",
label="Permit application",
initial_status_key="intake",
allowed_status_keys=("intake", "decided"),
type_key=JOURNEY["case"]["type_key"],
label=JOURNEY["title"],
initial_status_key=JOURNEY["case"]["initial_status"],
allowed_status_keys=(
JOURNEY["case"]["initial_status"],
JOURNEY["case"]["decided_status"],
),
)
case_record = create_case_from_intake(
session,
principal,
plan=intake,
case_number="PERMIT-2026-0001",
title="Permit application",
status_key=None,
case_number=JOURNEY["case"]["number"],
title=JOURNEY["title"],
status_key=JOURNEY["case"]["initial_status"],
opened_at=NOW,
recorded_at=NOW,
change_reason="Portal application received.",
idempotency_key="journey-case-create",
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")
@@ -333,7 +343,7 @@ class InstitutionalGovernanceJourneyTests(unittest.TestCase):
change_reason="Permit authority delegated.",
),
task_types=("committee.formal_decision",),
authority_types=("permit",),
authority_types=(JOURNEY["decision"]["type"],),
organization_unit_refs=(organization,),
function_refs=(function,),
jurisdiction_refs=(jurisdiction,),
@@ -380,7 +390,7 @@ class InstitutionalGovernanceJourneyTests(unittest.TestCase):
object_id="item-1",
revision=1,
state="deliberating",
title="Permit application",
title=JOURNEY["title"],
parent_id=meeting.object_id,
recorded_at=NOW,
change_reason="Agenda item entered deliberation.",
@@ -398,7 +408,7 @@ class InstitutionalGovernanceJourneyTests(unittest.TestCase):
object_id="vote-1",
revision=1,
state="closed",
title="Vote on permit application",
title=f"Vote on {JOURNEY['title'].lower()}",
parent_id=agenda.object_id,
recorded_at=NOW,
change_reason="Vote result accepted.",
@@ -445,7 +455,7 @@ class InstitutionalGovernanceJourneyTests(unittest.TestCase):
effective_at=NOW,
meeting_ref="meeting-1",
agenda_item_ref="item-1",
decision_type="permit",
decision_type=JOURNEY["decision"]["type"],
subject_refs=(case_record.reference,),
organization_unit_ref=organization,
function_ref=function,
@@ -461,8 +471,8 @@ class InstitutionalGovernanceJourneyTests(unittest.TestCase):
),
fact_evidence=(application_evidence, address_evidence),
legal_bases=(legal_basis,),
operative_result="Permit granted.",
reasoning="The application satisfies the effective rule.",
operative_result=JOURNEY["decision"]["operative_result"],
reasoning=JOURNEY["decision"]["reasoning"],
case_ref=case_record.reference,
jurisdiction_refs=(jurisdiction,),
party_refs=(applicant, representative),
@@ -475,7 +485,7 @@ class InstitutionalGovernanceJourneyTests(unittest.TestCase):
classification="restricted",
purposes=("permit-decision", "party-delivery"),
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",
),
),
@@ -568,7 +578,7 @@ class InstitutionalGovernanceJourneyTests(unittest.TestCase):
get_case(session, principal, case_id="case-1"),
)
self.assertEqual(
"decided",
JOURNEY["case"]["decided_status"],
get_workspace_object(
session,
principal,
@@ -583,7 +593,7 @@ class InstitutionalGovernanceJourneyTests(unittest.TestCase):
self.assertEqual("confirmed", reconstruction["observed_effects"][0]["state"])
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("The application satisfies the effective rule.", reconstruction["reasoning"])
self.assertEqual(JOURNEY["decision"]["reasoning"], reconstruction["reasoning"])
self.assertEqual("review:administrative-court", reconstruction["review_refs"][0])
+631 -32
View File
@@ -2,14 +2,21 @@ from __future__ import annotations
from dataclasses import dataclass
from datetime import UTC, datetime, timedelta
import json
from pathlib import Path
from types import SimpleNamespace
from urllib.parse import parse_qs, urlparse
import unittest
from sqlalchemy import create_engine
from sqlalchemy.orm import Session
from sqlalchemy.orm import Session, sessionmaker
from govoplan_core.auth import ApiPrincipal
from govoplan_core.core.access import PrincipalRef
from govoplan_core.core.institutional import (
CAPABILITY_FORM_DEFINITIONS,
CAPABILITY_SERVICE_DEFINITIONS,
EvidenceReference,
FormDefinition,
FormFieldDefinition,
InstitutionalReference,
@@ -18,6 +25,22 @@ from govoplan_core.core.institutional import (
TemporalRevision,
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 (
DistributedLease,
RuntimeIdentity,
bind_process_runtime_identity,
)
from govoplan_core.core.tasks import (
RegisteredWorkItemProvider,
WorkItemProviderRegistration,
WorkItemQuery,
)
from govoplan_core.core.recovery import RecoveryCheckpoint, RecoveryOperation
from govoplan_cases.backend.service_intake import (
CAPABILITY_CASES_SERVICE_INTAKE,
CaseServiceIntake,
@@ -28,18 +51,66 @@ from govoplan_forms.backend.service import (
record_form_definition,
)
from govoplan_forms_runtime.backend.db.models import (
FormAssistedConfirmation,
FormInstanceEvent,
FormInstanceIdentity,
FormInstanceRevision,
FormIntakeProfile,
FormIntakeSession,
FormStatusAccessGrant,
FormStatusAccessPolicy,
FormStatusAccessToken,
)
from govoplan_forms_runtime.backend.intake import FormIntakeService
from govoplan_forms_runtime.backend.service import (
FormRuntimeError,
FormRuntimeService,
FormsServiceLauncher,
)
from govoplan_forms_runtime.backend.status_access import FormStatusAccessService
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_workflow_engine.backend.db.models import (
WorkflowDefinition,
WorkflowDefinitionRevision,
WorkflowInstance,
WorkflowInstanceEvent,
WorkflowInstanceStep,
WorkflowTrigger,
WorkflowTriggerDelivery,
WorkflowWaitState,
)
from govoplan_workflow_engine.backend.instance_service import (
resolve_step,
start_instance,
)
from govoplan_workflow_engine.backend.schemas import (
WorkflowDefinitionCreateRequest,
WorkflowEdge,
WorkflowGraph,
WorkflowInstanceStartRequest,
WorkflowNode,
WorkflowStepActionRequest,
)
from govoplan_workflow_engine.backend.service import (
activate_definition,
create_definition,
)
from govoplan_workflow_engine.backend.work_items import WorkflowWorkItemProvider
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:
@@ -47,24 +118,24 @@ def _service() -> ServiceDefinition:
reference=InstitutionalReference(
kind="service",
owner_module="portal",
object_id="permit",
object_id=JOURNEY["service"]["object_id"],
tenant_id="tenant-1",
version="5",
version=JOURNEY["service"]["version"],
),
key="permit.apply",
key=JOURNEY["service"]["key"],
temporal=TemporalRevision(
revision="5",
revision=JOURNEY["service"]["version"],
valid_from=NOW - timedelta(days=1),
valid_to=NOW + timedelta(days=1),
recorded_at=NOW - timedelta(days=2),
),
title="Apply for a permit",
audience=("resident",),
required_evidence_types=("application",),
title=JOURNEY["title"],
audience=(JOURNEY["service"]["audience"],),
required_evidence_types=tuple(JOURNEY["service"]["required_evidence_types"]),
bindings=(
ServiceBinding("capability", CAPABILITY_CASES_SERVICE_INTAKE),
ServiceBinding("case", "permit-application"),
ServiceBinding("workflow", "workflow:permit-review"),
ServiceBinding("case", JOURNEY["case"]["type_key"]),
ServiceBinding("workflow", "workflow:resident-parking-permit-review"),
),
publication_state="published",
)
@@ -74,10 +145,14 @@ class _Provider:
def __init__(self, definition: ServiceDefinition) -> None:
self.definition = definition
def get_service_definition(self, session, principal, *, reference, effective_at=None):
def get_service_definition(
self, session, principal, *, reference, effective_at=None
):
return self.definition
def list_service_definitions(self, session, principal, *, tenant_id, query="", limit=100):
def list_service_definitions(
self, session, principal, *, tenant_id, query="", limit=100
):
return (self.definition,)
@@ -111,15 +186,76 @@ class _Principal:
class _FormRegistry(_Registry):
def __init__(self, definition: ServiceDefinition) -> None:
super().__init__(definition)
self.capabilities[CAPABILITY_FORM_DEFINITIONS] = (
SqlFormDefinitionProvider()
)
self.capabilities[service_launch_capability("form")] = (
FormsServiceLauncher(self)
self.capabilities[CAPABILITY_FORM_DEFINITIONS] = SqlFormDefinitionProvider()
self.capabilities[service_launch_capability("form")] = FormsServiceLauncher(
self
)
self.notifications = _NotificationProvider()
self.capabilities[CAPABILITY_NOTIFICATIONS_DISPATCH] = self.notifications
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:
def __init__(self) -> None:
self.provider = WorkflowWorkItemProvider(registry=self)
self.registered = RegisteredWorkItemProvider(
module_id="workflow_engine",
registration=WorkItemProviderRegistration(
id="workflow_engine.handoffs",
factory=lambda _context: self.provider,
order=20,
),
)
def has_capability(self, _name: str) -> bool:
return False
def capability(self, name: str) -> object:
raise KeyError(name)
def work_item_providers(self):
return ((self.registered, self.provider),)
def _workflow_principal() -> ApiPrincipal:
return ApiPrincipal(
principal=PrincipalRef(
account_id="account-1",
membership_id="membership-1",
tenant_id="tenant-1",
scopes=frozenset(
{
"tasks:item:read",
"workflow:definition:read",
"workflow:instance:read",
"workflow:instance:start",
"workflow:instance:transition",
}
),
),
account=SimpleNamespace(id="account-1"),
user=SimpleNamespace(id="membership-1"),
)
class InstitutionalServiceJourneyTests(unittest.TestCase):
@@ -143,8 +279,19 @@ class InstitutionalServiceJourneyTests(unittest.TestCase):
self.assertTrue(entries[0].available)
self.assertIs(definition, entries[0].definition)
self.assertEqual(definition.reference, plan.service_ref)
self.assertEqual("5", plan.context.service_ref.version)
self.assertEqual("workflow:permit-review", plan.workflow_refs[0])
self.assertEqual(JOURNEY["service"]["version"], plan.context.service_ref.version)
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:
engine = create_engine("sqlite+pysqlite:///:memory:")
@@ -165,19 +312,19 @@ class InstitutionalServiceJourneyTests(unittest.TestCase):
reference=InstitutionalReference(
kind="form",
owner_module="forms",
object_id="permit-application",
object_id=JOURNEY["form"]["object_id"],
tenant_id="tenant-1",
version="3",
),
key="permit-application",
key=JOURNEY["form"]["object_id"],
temporal=TemporalRevision(
revision="3",
valid_from=NOW - timedelta(days=1),
valid_to=NOW + timedelta(days=1),
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=(
FormFieldDefinition(
key="applicant_name",
@@ -185,6 +332,25 @@ class InstitutionalServiceJourneyTests(unittest.TestCase):
required=True,
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",
allow_drafts=True,
@@ -199,18 +365,18 @@ class InstitutionalServiceJourneyTests(unittest.TestCase):
reference=InstitutionalReference(
kind="service",
owner_module="services",
object_id="permit",
object_id=JOURNEY["service"]["object_id"],
tenant_id="tenant-1",
version="6",
version=JOURNEY["service"]["version"],
),
key="permit.apply",
key=JOURNEY["service"]["key"],
temporal=TemporalRevision(
revision="6",
revision=JOURNEY["service"]["version"],
valid_from=NOW - timedelta(days=1),
valid_to=NOW + timedelta(days=1),
recorded_at=NOW - timedelta(days=2),
),
title="Apply for a permit",
title=JOURNEY["title"],
audience=("public",),
bindings=(binding,),
publication_state="published",
@@ -224,7 +390,7 @@ class InstitutionalServiceJourneyTests(unittest.TestCase):
reference=service.reference,
requested_at=NOW,
idempotency_key="portal-form-launch-1",
parameters={"applicant_name": "Ada Lovelace"},
parameters=JOURNEY["form"]["fields"],
)
replay = directory.launch_service(
session,
@@ -232,7 +398,7 @@ class InstitutionalServiceJourneyTests(unittest.TestCase):
reference=service.reference,
requested_at=NOW,
idempotency_key="portal-form-launch-1",
parameters={"applicant_name": "Ada Lovelace"},
parameters=JOURNEY["form"]["fields"],
)
instance = FormRuntimeService(registry).get_instance(
session,
@@ -261,7 +427,440 @@ class InstitutionalServiceJourneyTests(unittest.TestCase):
)
self.assertEqual(NOW, instance.definition_ref.valid_at)
self.assertEqual(service.reference, instance.service_ref)
self.assertEqual("Ada Lovelace", instance.values["applicant_name"])
self.assertEqual(JOURNEY["form"]["fields"], instance.values)
finally:
session.close()
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(
self,
) -> None:
engine = create_engine("sqlite+pysqlite:///:memory:")
tables = (
DistributedLease.__table__,
RecoveryOperation.__table__,
RecoveryCheckpoint.__table__,
WorkflowDefinition.__table__,
WorkflowDefinitionRevision.__table__,
WorkflowInstance.__table__,
WorkflowInstanceStep.__table__,
WorkflowInstanceEvent.__table__,
WorkflowTrigger.__table__,
WorkflowTriggerDelivery.__table__,
WorkflowWaitState.__table__,
)
for table in tables:
table.create(engine)
sessions = sessionmaker(bind=engine)
registry = _WorkflowTaskRegistry()
principal = _workflow_principal()
bind_process_runtime_identity(
RuntimeIdentity(
installation_id="service-journey",
node_id="journey-node",
incarnation="journey-run",
role="web",
software_version="test",
composition_hash="c" * 64,
)
)
try:
with sessions() as session:
definition = create_definition(
session,
tenant_id="tenant-1",
actor_id="account-1",
payload=WorkflowDefinitionCreateRequest(
name=JOURNEY["workflow"]["definition_name"],
graph=WorkflowGraph(
nodes=[
WorkflowNode(
id="start",
type="workflow.start.manual",
),
WorkflowNode(
id="review",
type="workflow.activity",
config={
"title": JOURNEY["workflow"]["work_item_title"],
"instructions": JOURNEY["workflow"]["instructions"],
"assignee": "account:account-1",
"due_after": "2d",
},
),
WorkflowNode(
id="done",
type="workflow.end.completed",
),
],
edges=[
WorkflowEdge(
id="start-review",
source="start",
target="review",
),
WorkflowEdge(
id="review-done",
source="review",
target="done",
),
],
),
execution_mode="guided",
),
)
activate_definition(
session,
tenant_id="tenant-1",
definition_id=definition.id,
actor_id="account-1",
)
instance, replayed = start_instance(
session,
tenant_id="tenant-1",
definition_id=definition.id,
actor_id="account-1",
principal=principal,
registry=registry,
payload=WorkflowInstanceStartRequest(
idempotency_key="permit-decision-1",
input={"case_id": "case-1"},
correlation_id="case-1",
),
)
self.assertFalse(replayed)
session.commit()
instance_id = instance.id
step_id = instance.current_step_id
with sessions() as reopened:
work = aggregate_work_items(
registry,
reopened,
principal,
query=WorkItemQuery(tenant_id="tenant-1"),
)
self.assertEqual(1, work.total)
self.assertEqual(step_id, work.items[0].id)
self.assertEqual(
JOURNEY["workflow"]["work_item_title"],
work.items[0].title,
)
self.assertTrue(work.items[0].action_url.startswith("/workflow?"))
self.assertIn(f"run={instance_id}", work.items[0].action_url)
resolve_step(
reopened,
tenant_id="tenant-1",
instance_id=instance_id,
step_id=step_id,
actor_id="account-1",
principal=principal,
registry=registry,
payload=WorkflowStepActionRequest(action="complete"),
)
reopened.commit()
with sessions() as verified:
self.assertEqual(
0,
aggregate_work_items(
registry,
verified,
principal,
query=WorkItemQuery(tenant_id="tenant-1"),
).total,
)
finally:
bind_process_runtime_identity(None)
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()
+273
View File
@@ -0,0 +1,273 @@
from __future__ import annotations
from contextlib import redirect_stdout
from dataclasses import replace
import io
import json
from pathlib import Path
import shutil
import stat
import subprocess
import sys
import tempfile
import unittest
from unittest.mock import MagicMock
META_ROOT = Path(__file__).resolve().parents[1]
LAB_TOOLS = META_ROOT / "tools" / "lab"
if str(LAB_TOOLS) not in sys.path:
sys.path.insert(0, str(LAB_TOOLS))
from govoplan_lab.cli import main # noqa: E402
from govoplan_lab.config import LabConfigError, load_config # noqa: E402
from govoplan_lab.lifecycle import ( # noqa: E402
CommandRunner,
LabOperationError,
_assert_domain_owned,
_domain_description,
_ensure_certificates,
_render_kubectl_wrapper,
destroy,
)
from govoplan_lab.render import ( # noqa: E402
render_k3s_config,
render_registry_config,
render_state_compose,
write_private,
)
REHEARSAL_CONFIG = LAB_TOOLS / "govoplan-lab.example.toml"
ACCEPTANCE_CONFIG = LAB_TOOLS / "govoplan-lab.acceptance.example.toml"
class KubernetesLabTests(unittest.TestCase):
def test_example_inventories_describe_their_evidence_boundary(self) -> None:
rehearsal = load_config(REHEARSAL_CONFIG)
acceptance = load_config(ACCEPTANCE_CONFIG)
self.assertEqual("rehearsal", rehearsal.mode)
self.assertFalse(rehearsal.evidence_capable)
self.assertEqual(2, len(rehearsal.workers))
self.assertEqual("acceptance", acceptance.mode)
self.assertTrue(acceptance.evidence_capable)
self.assertEqual(3, len({node.hypervisor for node in acceptance.nodes}))
self.assertEqual(3, len({node.failure_domain for node in acceptance.nodes}))
def test_acceptance_inventory_rejects_collapsed_worker_failure_domains(self) -> None:
source = ACCEPTANCE_CONFIG.read_text(encoding="utf-8")
collapsed = source.replace(
'hypervisor = "lab-admin@hypervisor-b.example.org"',
'hypervisor = "lab-admin@hypervisor-a.example.org"',
).replace('failure_domain = "rack-b"', 'failure_domain = "rack-a"')
with tempfile.TemporaryDirectory(prefix="govoplan-lab-config-") as directory:
path = Path(directory) / "lab.toml"
path.write_text(collapsed, encoding="utf-8")
with self.assertRaisesRegex(LabConfigError, "acceptance mode"):
load_config(path)
def test_create_without_apply_is_a_non_mutating_preview(self) -> None:
output = io.StringIO()
with redirect_stdout(output):
exit_code = main(["--config", str(REHEARSAL_CONFIG), "create"])
self.assertEqual(0, exit_code)
self.assertIn("Dry run: create", output.getvalue())
self.assertIn("Re-run with --apply", output.getvalue())
def test_local_hypervisor_uses_system_libvirt_without_sudo(self) -> None:
config = load_config(REHEARSAL_CONFIG)
runner = CommandRunner(config)
runner.run = MagicMock(
return_value=subprocess.CompletedProcess([], 0, stdout=b"", stderr=b"")
)
runner.hypervisor(config.nodes[0], ["virsh", "dominfo", "test-domain"])
runner.run.assert_called_once_with(
[
"virsh",
"--connect",
"qemu:///system",
"dominfo",
"test-domain",
],
capture=False,
check=True,
timeout=None,
)
def test_local_hypervisor_file_operations_do_not_use_sudo(self) -> None:
config = load_config(REHEARSAL_CONFIG)
runner = CommandRunner(config)
runner.run = MagicMock(
return_value=subprocess.CompletedProcess([], 0, stdout=b"", stderr=b"")
)
runner.hypervisor(config.nodes[0], ["install", "-d", "/tmp/lab"])
runner.run.assert_called_once_with(
["install", "-d", "/tmp/lab"],
capture=False,
check=True,
timeout=None,
)
def test_kubectl_wrapper_quotes_remote_arguments(self) -> None:
wrapper = _render_kubectl_wrapper(
["ssh", "-i", "/tmp/lab key", "govoplan@example.test"]
)
self.assertIn("shlex.join(_REMOTE)", wrapper)
self.assertIn('["sudo", "--", "k3s", "kubectl", *sys.argv[1:]]', wrapper)
self.assertNotIn('kubectl \"$@\"', wrapper)
def test_destroy_requires_the_exact_lab_name(self) -> None:
config = load_config(REHEARSAL_CONFIG)
with self.assertRaisesRegex(LabOperationError, "--confirm"):
destroy(
config,
apply=True,
confirmation="wrong-lab",
purge_local_state=False,
)
def test_enroll_admin_without_apply_is_a_non_mutating_preview(self) -> None:
output = io.StringIO()
with redirect_stdout(output):
exit_code = main(
[
"--config",
str(REHEARSAL_CONFIG),
"enroll-admin",
"--email",
"owner@example.test",
]
)
self.assertEqual(0, exit_code)
self.assertIn("Dry run: enroll-admin", output.getvalue())
self.assertIn("owner@example.test", output.getvalue())
def test_domain_ownership_requires_marker_and_expected_disks(self) -> None:
config = load_config(REHEARSAL_CONFIG)
node = config.nodes[0]
node_directory = f"{config.vm_image_directory}/{config.name}/{node.name}"
runner = MagicMock()
runner.hypervisor.side_effect = [
subprocess.CompletedProcess(
[],
0,
stdout=(_domain_description(config, node) + "\n").encode(),
stderr=b"",
),
subprocess.CompletedProcess(
[],
0,
stdout=(
f"file disk vda {node_directory}/root.qcow2\n"
f"file cdrom sda {node_directory}/seed.img\n"
).encode(),
stderr=b"",
),
]
_assert_domain_owned(config, runner, node)
runner.hypervisor.side_effect = [
subprocess.CompletedProcess(
[],
0,
stdout=b"unrelated domain\n",
stderr=b"",
)
]
with self.assertRaisesRegex(LabOperationError, "ownership marker"):
_assert_domain_owned(config, runner, node)
def test_state_compose_uses_only_supplied_pinned_images(self) -> None:
names = ("postgres", "redis", "garage", "managed_ingress", "test_mail")
images = {
name: f"registry.example.test/{name}@sha256:{index:064x}"
for index, name in enumerate(names, start=1)
}
compose = json.loads(render_state_compose(images))
self.assertEqual(images["postgres"], compose["services"]["postgres"]["image"])
self.assertEqual(images["redis"], compose["services"]["redis"]["image"])
self.assertEqual(images["garage"], compose["services"]["garage"]["image"])
self.assertEqual(
images["managed_ingress"], compose["services"]["s3-tls"]["image"]
)
self.assertEqual(
images["test_mail"], compose["services"]["test-mail"]["image"]
)
def test_k3s_workers_join_the_primary_control_and_receive_failure_labels(
self,
) -> None:
config = load_config(ACCEPTANCE_CONFIG)
worker = config.workers[0]
rendered = render_k3s_config(config, worker, cluster_token="test-token")
self.assertIn(f'server: "https://{config.primary_control.address}:6443"', rendered)
self.assertIn(
f'topology.govoplan.add-ideas.de/failure-domain={worker.failure_domain}',
rendered,
)
self.assertNotIn("cluster-init", rendered)
def test_registry_credentials_are_all_or_nothing(self) -> None:
self.assertEqual("", render_registry_config("", ""))
with self.assertRaisesRegex(ValueError, "supplied together"):
render_registry_config("publisher", "")
rendered = render_registry_config("publisher", "secret")
self.assertIn('"git.add-ideas.de"', rendered)
self.assertIn("publisher", rendered)
def test_private_writer_enforces_owner_only_permissions(self) -> None:
with tempfile.TemporaryDirectory(prefix="govoplan-lab-private-") as directory:
path = Path(directory) / "nested" / "secret.txt"
write_private(path, "secret\n")
self.assertEqual(0o600, stat.S_IMODE(path.stat().st_mode))
@unittest.skipUnless(shutil.which("openssl"), "openssl is required")
def test_generated_lab_ca_passes_strict_chain_validation(self) -> None:
with tempfile.TemporaryDirectory(prefix="govoplan-lab-pki-") as directory:
config = replace(
load_config(REHEARSAL_CONFIG),
state_directory=Path(directory),
)
_ensure_certificates(config, CommandRunner(config))
ca_certificate = config.state_directory / "pki" / "ca.crt"
server_certificate = config.state_directory / "pki" / "server.crt"
result = subprocess.run(
[
"openssl",
"verify",
"-x509_strict",
"-CAfile",
str(ca_certificate),
str(server_certificate),
],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
check=False,
)
self.assertEqual(
0,
result.returncode,
(result.stdout + result.stderr).decode(errors="replace"),
)
if __name__ == "__main__":
unittest.main()
+113
View File
@@ -0,0 +1,113 @@
from __future__ import annotations
import json
from pathlib import Path
import shutil
import subprocess
import tempfile
import unittest
META_ROOT = Path(__file__).resolve().parents[1]
class ModulePackageWorkflowTests(unittest.TestCase):
def test_template_enforces_tag_version_hash_and_registry_contract(self) -> None:
workflow = (
META_ROOT / "tools/repo/templates/module-package-release.yml"
).read_text(encoding="utf-8")
self.assertIn("GITEA_REPOSITORY: ${{ gitea.repository }}", workflow)
self.assertNotIn("tag_protections", workflow)
self.assertNotIn("secrets.GITEA_TOKEN", workflow)
self.assertIn("git merge-base --is-ancestor", workflow)
self.assertIn("does not match", workflow)
self.assertIn("package-artifacts.json", workflow)
self.assertIn("api/packages/GovOPlaN/pypi", workflow)
self.assertIn("api/packages/GovOPlaN/npm", workflow)
self.assertIn('npm publish "./${webui_packages[0]}"', workflow)
self.assertIn("Check immutable registry state", workflow)
self.assertIn('files[0].get("sha256") != expected_sha256', workflow)
self.assertIn('if [[ "$PUBLISH_PYPI" == 1 ]]', workflow)
self.assertIn('[[ "$PUBLISH_NPM" == 1 ]]', workflow)
self.assertIn("GOVOPLAN_PACKAGE_TOKEN", workflow)
self.assertIn("must resolve to an exact registry version", workflow)
self.assertIn("git\\\\.add-ideas\\\\.de/(?:GovOPlaN|add-ideas)", workflow)
self.assertIn("release package identity does not match", workflow)
self.assertNotIn("Generic", workflow)
@unittest.skipUnless(shutil.which("node"), "Node.js is required")
def test_webui_publication_normalizes_internal_git_dependencies(self) -> None:
workflow = (
META_ROOT / "tools/repo/templates/module-package-release.yml"
).read_text(encoding="utf-8")
marker = " node <<'NODE'\n"
script = workflow.split(marker, 1)[1].split("\n NODE", 1)[0]
with tempfile.TemporaryDirectory() as temporary:
root = Path(temporary)
package_dir = root / ".package-webui"
package_dir.mkdir()
package_path = package_dir / "package.json"
package_path.write_text(
json.dumps(
{
"name": "@govoplan/core-webui",
"version": "0.1.14",
"private": True,
"dependencies": {
"@govoplan/access-webui": (
"git+ssh://git@git.add-ideas.de/GovOPlaN/"
"govoplan-access.git#v0.1.11"
),
"@govoplan/admin-webui": (
"git+ssh://git@git.add-ideas.de/add-ideas/"
"govoplan-admin.git#v0.1.8"
)
},
}
),
encoding="utf-8",
)
subprocess.run(
["node"],
input=script,
cwd=root,
check=True,
text=True,
capture_output=True,
)
package = json.loads(package_path.read_text(encoding="utf-8"))
self.assertNotIn("private", package)
self.assertEqual(
"0.1.11", package["dependencies"]["@govoplan/access-webui"]
)
self.assertEqual(
"0.1.8", package["dependencies"]["@govoplan/admin-webui"]
)
def test_sync_script_only_targets_packageable_govoplan_repositories(self) -> None:
namespace: dict[str, object] = {
"__file__": str(META_ROOT / "tools/repo/sync-module-package-workflows.py"),
"__name__": "test_sync_module_package_workflows",
}
script = (META_ROOT / "tools/repo/sync-module-package-workflows.py").read_text(
encoding="utf-8"
)
exec(compile(script, str(namespace["__file__"]), "exec"), namespace)
with tempfile.TemporaryDirectory() as temporary:
parent = Path(temporary)
package_repositories = namespace["package_repositories"]
# The production inventory is authoritative, so a temporary parent
# only exposes matching paths that are present in that inventory.
known = parent / "govoplan-core"
known.mkdir()
(known / "pyproject.toml").write_text("[project]\n", encoding="utf-8")
self.assertEqual(package_repositories(parent), (known,))
if __name__ == "__main__":
unittest.main()
+228
View File
@@ -0,0 +1,228 @@
from __future__ import annotations
from io import BytesIO
import importlib.util
import json
from pathlib import Path
import sys
import tarfile
import tempfile
import tomllib
import unittest
import zipfile
ROOT = Path(__file__).resolve().parents[1]
def _load(name: str, path: Path):
spec = importlib.util.spec_from_file_location(name, path)
assert spec is not None and spec.loader is not None
module = importlib.util.module_from_spec(spec)
sys.modules[name] = module
spec.loader.exec_module(module)
return module
PACKAGE_SET = _load(
"generate_release_package_set",
ROOT / "tools/release/generate-release-package-set.py",
)
ARTIFACTS = _load(
"resolve_package_artifacts",
ROOT / "tools/release/resolve-package-artifacts.py",
)
class PackageRegistryReleaseTests(unittest.TestCase):
def test_current_release_sources_form_a_hash_bound_package_set(self) -> None:
core_version = tomllib.loads(
(ROOT.parent / "govoplan-core/pyproject.toml").read_text(encoding="utf-8")
)["project"]["version"]
payload = PACKAGE_SET.generate_package_set(
core_version=core_version,
requirements=ROOT / "requirements-release.txt",
workspace=ROOT.parent,
)
self.assertEqual("1", payload["schema_version"])
self.assertEqual("base", payload["profile"])
self.assertEqual("govoplan-core", payload["python"][0]["name"])
self.assertIn(
"@govoplan/core-webui",
{item["name"] for item in payload["webui"]},
)
unsigned = dict(payload)
digest = unsigned.pop("package_set_sha256")
self.assertEqual(ARTIFACTS._canonical_sha256(unsigned), digest)
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(
ROOT / "packages/govoplan-meta/pyproject.toml",
core_version=core_version,
)
by_name = {item["name"]: item for item in selected}
self.assertIn("govoplan-core", by_name)
self.assertIn("govoplan-records", by_name)
self.assertEqual("0.1.21", by_name["govoplan-tasks"]["version"])
payload = PACKAGE_SET.generate_package_set(
core_version=core_version,
requirements=ROOT / "requirements-release.txt",
workspace=ROOT.parent,
profile="full",
meta_package=ROOT / "packages/govoplan-meta/pyproject.toml",
)
self.assertEqual("full", payload["profile"])
self.assertEqual(len(selected), len(payload["python"]))
self.assertIn(
"@govoplan/records-webui",
{item["name"] for item in payload["webui"]},
)
def test_python_registry_artifact_url_is_immutable_and_credential_free(self) -> None:
url = ARTIFACTS._python_artifact_url(
"https://git.add-ideas.de/api/packages/GovOPlaN/pypi/simple",
package={"name": "govoplan-files", "version": "0.1.18"},
filename="govoplan_files-0.1.18-py3-none-any.whl",
)
self.assertEqual(
"https://git.add-ideas.de/api/packages/GovOPlaN/pypi/files/govoplan-files/0.1.18/govoplan_files-0.1.18-py3-none-any.whl",
url,
)
def test_wheel_and_webui_artifacts_are_verified_by_embedded_identity(self) -> None:
with tempfile.TemporaryDirectory(prefix="govoplan-package-artifacts-") as value:
root = Path(value)
wheels = root / "wheels"
webui = root / "webui"
wheels.mkdir()
webui.mkdir()
wheel = wheels / "govoplan_demo-1.2.3-py3-none-any.whl"
with zipfile.ZipFile(wheel, "w") as archive:
archive.writestr(
"govoplan_demo-1.2.3.dist-info/METADATA",
"Metadata-Version: 2.1\nName: govoplan-demo\nVersion: 1.2.3\n",
)
package_json = json.dumps(
{"name": "@govoplan/demo-webui", "version": "1.2.3"}
).encode("utf-8")
npm = webui / "govoplan-demo-webui-1.2.3.tgz"
with tarfile.open(npm, "w:gz") as archive:
member = tarfile.TarInfo("package/package.json")
member.size = len(package_json)
archive.addfile(member, BytesIO(package_json))
source = {
"version": "1.2.3",
"repository": "govoplan-demo",
"tag": "v1.2.3",
"commit": "1" * 40,
}
python_rows = ARTIFACTS._verify_wheels(
({"name": "govoplan-demo", "extras": ["server"], **source},), wheels
)
webui_rows = ARTIFACTS._verify_webui(
({"name": "@govoplan/demo-webui", **source},), webui
)
self.assertEqual("govoplan-demo", python_rows[0]["name"])
self.assertEqual(["server"], python_rows[0]["extras"])
self.assertEqual("@govoplan/demo-webui", webui_rows[0]["name"])
self.assertTrue(str(webui_rows[0]["integrity"]).startswith("sha512-"))
def test_package_set_rejects_argument_shaped_package_names(self) -> None:
payload = {
"schema_version": "1",
"release_version": "1.2.3",
"registries": {
"python": "https://packages.example.test/pypi/simple",
"npm": "https://packages.example.test/npm/",
},
"python": [
{
"name": "--index-url",
"version": "1.2.3",
"repository": "govoplan-demo",
"extras": [],
"tag": "v1.2.3",
"commit": "1" * 40,
}
],
"webui": [
{
"name": "@govoplan/demo-webui",
"version": "1.2.3",
"repository": "govoplan-demo",
"tag": "v1.2.3",
"commit": "1" * 40,
}
],
}
payload["package_set_sha256"] = ARTIFACTS._canonical_sha256(payload)
with tempfile.TemporaryDirectory() as value:
path = Path(value) / "packages.json"
path.write_text(json.dumps(payload), encoding="utf-8")
with self.assertRaisesRegex(
ARTIFACTS.PackageArtifactError, "invalid identity"
):
ARTIFACTS._load_package_set(path)
def test_runtime_workflow_consumes_registry_artifacts_and_publishes_lock(self) -> None:
workflow = (ROOT / ".gitea/workflows/runtime-distribution.yml").read_text(
encoding="utf-8"
)
self.assertIn("resolve-package-artifacts.py", workflow)
self.assertIn("--profile full", workflow)
self.assertIn('git show "v$VERSION:requirements-release.txt"', workflow)
self.assertIn('git show "v$VERSION:packages/govoplan-meta/pyproject.toml"', workflow)
self.assertIn("--meta-package runtime-output/govoplan-meta.source.toml", workflow)
self.assertIn("GOVOPLAN_WEBUI_INSTALL_ALL_PACKAGES=true", workflow)
self.assertIn("package-artifacts.lock.json", workflow)
self.assertIn(
"--package-lock runtime-output/package-artifacts.lock.json",
workflow,
)
self.assertIn('PYTHON="$PWD/.runtime-build/bin/python"', workflow)
self.assertNotIn(
"pip wheel --no-deps --wheel-dir runtime-output/local-wheels",
workflow,
)
def test_developer_meta_package_matches_workspace_versions(self) -> None:
script = _load(
"generate_developer_meta_package",
ROOT / "tools/release/generate-developer-meta-package.py",
)
expected = script.render(
workspace=ROOT.parent,
requirements=ROOT / "requirements-release.txt",
)
actual = (ROOT / "packages/govoplan-meta/pyproject.toml").read_text(
encoding="utf-8"
)
self.assertEqual(expected, actual)
def test_meta_package_workflow_supports_hash_safe_tag_retry(self) -> None:
workflow = (
ROOT / ".gitea/workflows/publish-developer-meta-package.yml"
).read_text(encoding="utf-8")
self.assertIn("workflow_dispatch:", workflow)
self.assertIn("TRIGGER_TAG: ${{ gitea.ref_name }}", workflow)
self.assertNotIn("GITEA_REF_NAME", workflow)
self.assertIn('refs/tags/{tag}^{{commit}}', workflow)
self.assertIn("already exists with a different SHA-256", workflow)
self.assertIn("PUBLISH_PYPI", workflow)
if __name__ == "__main__":
unittest.main()
+44
View File
@@ -0,0 +1,44 @@
from __future__ import annotations
import importlib.util
from pathlib import Path
import sys
import unittest
META_ROOT = Path(__file__).resolve().parents[1]
TOOLS_ROOT = META_ROOT / "tools" / "gitea"
if str(TOOLS_ROOT) not in sys.path:
sys.path.insert(0, str(TOOLS_ROOT))
SCRIPT = TOOLS_ROOT / "gitea-dispatch-package-set.py"
SPEC = importlib.util.spec_from_file_location("gitea_dispatch_package_set", SCRIPT)
assert SPEC is not None and SPEC.loader is not None
MODULE = importlib.util.module_from_spec(SPEC)
sys.modules[SPEC.name] = MODULE
SPEC.loader.exec_module(MODULE)
class PackageSetDispatchTests(unittest.TestCase):
def test_meta_package_resolves_to_exact_tagged_repository_targets(self) -> None:
targets = MODULE.package_targets()
self.assertEqual(73, len(targets))
self.assertEqual(73, len({target.distribution for target in targets}))
by_name = {target.distribution: target for target in targets}
self.assertEqual("v0.1.38", by_name["govoplan-core"].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-access"].has_webui)
self.assertEqual(
"@govoplan/access-webui",
by_name["govoplan-access"].webui_package,
)
if __name__ == "__main__":
unittest.main()
+199
View File
@@ -141,6 +141,97 @@ class PlatformInterfaceInventoryTests(unittest.TestCase):
self.assertEqual(1, result["summary"]["stale_endpoint_declarations"])
self.assertIsNone(result["api"]["backend_endpoints"][0]["surface"])
def test_endpoint_only_strict_mode_does_not_fail_on_translation_debt(
self,
) -> None:
result = {
"translation_health": {"missing_catalog_entries": ["missing.key"]},
"api": {
"unclassified_endpoints": [],
"stale_endpoint_declarations": [],
},
}
self.assertEqual(
[],
inventory._strict_failures(
result,
check_translations=False,
check_endpoints=True,
),
)
self.assertEqual(
["used translation keys are missing from generated catalogs"],
inventory._strict_failures(
result,
check_translations=True,
check_endpoints=True,
),
)
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:
tree = ast.parse(
"""
@@ -171,6 +262,114 @@ def read_item(item_id: str):
},
)
def test_source_declarations_normalize_stable_control_and_contribution_ids(
self,
) -> None:
webui = {
"fields": [
{
"repository": "govoplan-example",
"file": "webui/src/Example.tsx",
"line": 12,
"column": 3,
"id": "govoplan-example.field.example.name.abc123",
"idSource": "source_anchor",
"explicitId": None,
"context": "Example",
"helpId": "govoplan-example.field.example.name.abc123.help",
"helpDynamic": False,
}
],
"actions": [],
"contributions": [
{
"repository": "govoplan-example",
"file": "webui/src/module.ts",
"line": 20,
"column": 5,
"kind": "frontend_route",
"id": "/examples/:exampleId",
"path": "/examples/:exampleId",
}
],
"translationCatalog": {"en": {}, "de": {}},
}
manifests = [{"repository": "govoplan-example", "id": "examples"}]
declarations = inventory._source_interface_declarations(webui, manifests)
keys = {item["key"] for item in declarations}
self.assertIn("field:examples.field.example.name.abc123", keys)
self.assertIn(
"help:examples.field.example.name.abc123.help",
keys,
)
self.assertIn(
"frontend_route:examples.route.examples.exampleid",
keys,
)
def test_declaration_health_rejects_duplicate_and_undeclared_source_ids(
self,
) -> None:
declaration = {
"key": "frontend_route:example.route.unlisted",
"id": "example.route.unlisted",
"module_id": "example",
"kind": "frontend_route",
"origin": "webui_contribution",
}
manifests = [
{
"id": "example",
"repository": "govoplan-example",
"interface_catalog": {"declarations": []},
}
]
health = inventory._declaration_health(
[declaration, dict(declaration)],
manifests,
)
self.assertEqual(1, len(health["duplicate_ids"]))
self.assertEqual(1, len(health["undeclared_source_surfaces"]))
def test_runtime_snapshot_comparison_accepts_an_installed_subset(self) -> None:
manifests = [
{
"id": "one",
"interface_catalog": {
"contract_version": "1",
"module_id": "one",
"module_version": "1.0.0",
"digest": "sha256:one",
},
},
{
"id": "two",
"interface_catalog": {
"contract_version": "1",
"module_id": "two",
"module_version": "1.0.0",
"digest": "sha256:two",
},
},
]
snapshot = {
"contract_version": "1",
"modules": [dict(manifests[1]["interface_catalog"])],
}
comparison = inventory._compare_runtime_snapshot(snapshot, manifests)
self.assertEqual(["two"], comparison["matched_modules"])
self.assertEqual([], comparison["mismatches"])
snapshot["modules"][0]["digest"] = "sha256:changed"
comparison = inventory._compare_runtime_snapshot(snapshot, manifests)
self.assertEqual("digest_mismatch", comparison["mismatches"][0]["reason"])
if __name__ == "__main__":
unittest.main()
+47 -5
View File
@@ -1,10 +1,11 @@
from __future__ import annotations
import sys
import tomllib
import unittest
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 (
ExternalProviderDeclaration,
ModuleArchitectureDeclaration,
@@ -26,6 +27,13 @@ from govoplan_release.catalog_entry_synthesis import ( # noqa: E402
from govoplan_release.selective_catalog import apply_repo_updates # noqa: E402
def repository_version(name: str) -> str:
payload = tomllib.loads(
(META_ROOT.parent / name / "pyproject.toml").read_text(encoding="utf-8")
)
return str(payload["project"]["version"])
class ReleaseCatalogEntrySynthesisTests(unittest.TestCase):
def test_catalog_entry_preserves_architecture_and_provider_declarations(
self,
@@ -65,7 +73,7 @@ class ReleaseCatalogEntrySynthesisTests(unittest.TestCase):
),
ModuleMaturityEvidence(
kind="documentation",
reference="docs/INSTITUTIONAL_GOVERNANCE_TARGET_ARCHITECTURE.md",
reference="docs/architecture/INSTITUTIONAL_GOVERNANCE_TARGET_ARCHITECTURE.md",
summary="Defines the provider declaration contract.",
),
),
@@ -80,6 +88,18 @@ class ReleaseCatalogEntrySynthesisTests(unittest.TestCase):
id="example",
name="Example",
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,
external_providers=(provider,),
),
@@ -92,12 +112,31 @@ class ReleaseCatalogEntrySynthesisTests(unittest.TestCase):
)
self.assertEqual("vertical_slice", entry["architecture"]["maturity"])
self.assertEqual(
"contract_only",
entry["information_governance"]["dimensions"]["retention"][
"adoption"
],
)
self.assertEqual(
"external_mirror",
entry["external_providers"][0]["objects"][0][
"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:
payload: dict[str, object] = {
@@ -116,9 +155,12 @@ class ReleaseCatalogEntrySynthesisTests(unittest.TestCase):
changes = apply_repo_updates(
payload,
repo_versions={
"govoplan-addresses": "0.1.9",
"govoplan-poll": "0.1.11",
"govoplan-scheduling": "0.1.11",
name: repository_version(name)
for name in (
"govoplan-addresses",
"govoplan-poll",
"govoplan-scheduling",
)
},
repo_contracts={},
repository_base="git+ssh://git@git.add-ideas.de/GovOPlaN",
+64 -2
View File
@@ -29,25 +29,87 @@ class ReleaseEntrypointGateTests(unittest.TestCase):
workflow = script[confirm:]
source_gate = workflow.index("run_version_alignment_gate source")
baseline = workflow.index("record_migration_release_baseline")
first_commit = workflow.index('run git -C "$repo" commit')
lock_generation = workflow.index("generate_release_lock")
full_gate = workflow.index("run_version_alignment_gate", source_gate + 1)
first_push = workflow.index('run git -C "$repo" push')
self.assertLess(baseline, source_gate)
self.assertLess(source_gate, first_commit)
self.assertLess(first_commit, lock_generation)
self.assertLess(lock_generation, full_gate)
self.assertLess(full_gate, first_push)
self.assertLess(manifest_gate, confirm)
def test_source_catalog_generator_enforces_explicit_repo_versions(self) -> None:
def test_lockstep_release_pushes_meta_package_after_core(self) -> None:
script = (META_ROOT / "tools" / "release" / "push-release-tag.sh").read_text()
module_push = script.index('for repo in "${MODULE_REPOS[@]}"; do\n run git -C "$repo" push')
core_push = script.index('run git -C "$ROOT" push', module_push)
support_push = script.index('for repo in "${SUPPORT_REPOS[@]}"; do\n run git -C "$repo" push', core_push)
self.assertLess(module_push, core_push)
self.assertLess(core_push, support_push)
def test_default_migration_preflight_accepts_new_release_heads(self) -> None:
script = (META_ROOT / "tools" / "release" / "push-release-tag.sh").read_text()
audit_function = script[
script.index("run_migration_release_audit()") :
script.index("record_migration_release_baseline()")
]
self.assertNotIn("--strict-if-baseline", audit_function)
self.assertIn('command+=("--strict")', audit_function)
def test_source_gate_does_not_require_tags_before_they_are_created(self) -> None:
script = (META_ROOT / "tools" / "release" / "push-release-tag.sh").read_text()
gate = script[
script.index("run_version_alignment_gate()") :
script.index("run_manifest_shape_gate()")
]
self.assertIn('command+=(--source-metadata-only)', gate)
self.assertIn('else\n command+=(--release-composition)', gate)
def test_version_updater_targets_canonical_runtime_declarations(self) -> None:
script = (META_ROOT / "tools" / "release" / "push-release-tag.sh").read_text()
self.assertIn("^manifest\\s*=\\s*ModuleManifest", script)
self.assertIn("could not update module version declaration", script)
self.assertIn("update_package_init_versions", script)
self.assertIn("synchronize-webui-package-metadata.py", script)
self.assertIn('"peerDependenciesMeta",', script)
self.assertLess(
script.index('synchronize-webui-package-metadata.py" --repo "$repo"'),
script.index('synchronize_lockfile_root "$package_path"', script.index('synchronize-webui-package-metadata.py" --repo "$repo"')),
)
self.assertNotIn("could not update ModuleManifest.version", script)
def test_release_lock_refreshes_candidate_govoplan_metadata(self) -> None:
script = (META_ROOT / "tools" / "release" / "generate-release-lock.sh").read_text()
self.assertEqual(2, script.count('"npm_config_cache=$TMP_DIR/npm-cache"'))
self.assertNotIn(
'cp "$WEBUI/package-lock.release.json" "$TMP_DIR/package-lock.json"',
script,
)
self.assertIn('cp "$WEBUI/package.release.json" "$TMP_DIR/package.json"', script)
def test_catalog_generator_validates_registry_package_set_before_writing(self) -> None:
script = (META_ROOT / "tools" / "release" / "generate-release-catalog.py").read_text()
gate = script.index("selected_repository_version_issues(")
gate = script.index("_validate_release_inputs(package_set, package_lock")
write = script.index("output.write_text(")
self.assertLess(gate, write)
def test_full_catalog_publication_synchronizes_browsable_module_directory(self) -> None:
publisher = (META_ROOT / "tools" / "release" / "publish-release-catalog.sh").read_text()
self.assertIn('--module-directory-output "$WEB_ROOT/public/catalogs/v1"', publisher)
self.assertIn('git -C "$WEB_ROOT" add -A', publisher)
self.assertIn('"$MODULE_DIRECTORY_PATH"', publisher)
def test_candidate_publication_uses_existing_keyring_as_trust_anchor(self) -> None:
publisher = (META_ROOT / "tools" / "release" / "govoplan_release" / "publisher.py").read_text()
+75
View File
@@ -1,7 +1,9 @@
from __future__ import annotations
import json
from pathlib import Path
import sys
import tempfile
import unittest
from unittest import mock
@@ -14,6 +16,7 @@ if str(RELEASE_TOOLS_ROOT) not in sys.path:
from govoplan_release.module_directory import ( # noqa: E402
module_directory_payloads,
safe_path_part,
write_module_directory,
)
@@ -77,6 +80,78 @@ class ReleaseModuleDirectoryTests(unittest.TestCase):
catalog_payload={}, keyring_payload={}, channel="../stable"
)
def test_prune_removes_stale_json_but_keeps_unrelated_assets(self) -> None:
catalog = {
"generated_at": "2026-08-06T12:00:00Z",
"sequence": 8,
"modules": [
{
"module_id": "files",
"name": "Files",
"version": "1.2.3",
"python_package": "govoplan-files",
"source": {
"repository": "govoplan-files",
"tag": "v1.2.3",
"commit": "a" * 40,
},
"artifact_integrity": {
"python": {"sha256": "b" * 64},
},
}
],
}
with tempfile.TemporaryDirectory() as value:
output_root = Path(value)
stale = output_root / "modules" / "legacy" / "0.1.0" / "manifest.json"
stale.parent.mkdir(parents=True)
stale.write_text("{}\n", encoding="utf-8")
unrelated = output_root / "modules" / "README.txt"
unrelated.write_text("keep\n", encoding="utf-8")
written = write_module_directory(
catalog_payload=catalog,
keyring_payload={},
output_root=output_root,
channel="stable",
prune=True,
)
self.assertFalse(stale.exists())
self.assertTrue(unrelated.exists())
self.assertEqual(3, len(written))
manifest = json.loads(
(output_root / "modules" / "files" / "1.2.3" / "manifest.json").read_text()
)
self.assertEqual("govoplan-files", manifest["module"]["repo"])
self.assertEqual("v1.2.3", manifest["module"]["python_tag"])
self.assertEqual("b" * 64, manifest["module"]["artifact_integrity"]["python"]["sha256"])
def test_writer_refuses_nested_symlink_targets(self) -> None:
catalog = {
"modules": [{"module_id": "files", "version": "1.2.3"}],
}
with tempfile.TemporaryDirectory() as value:
root = Path(value)
output_root = root / "public"
external = root / "external"
(output_root / "modules").mkdir(parents=True)
external.mkdir()
(output_root / "modules" / "files").symlink_to(
external,
target_is_directory=True,
)
with self.assertRaisesRegex(ValueError, "symlinks"):
write_module_directory(
catalog_payload=catalog,
keyring_payload={},
output_root=output_root,
channel="stable",
)
self.assertEqual([], list(external.iterdir()))
if __name__ == "__main__":
unittest.main()
+8
View File
@@ -93,6 +93,14 @@ class RuntimeDistributionTests(unittest.TestCase):
with self.assertRaisesRegex(DistributionError, "active trusted key"):
verify_manifest(unknown, self.keyring, now=self.now)
with self.assertRaisesRegex(DistributionError, "expected 'candidate'"):
verify_manifest(
self._manifest(),
self.keyring,
expected_channel="candidate",
now=self.now,
)
def test_offline_image_index_is_complete_and_digest_bound(self) -> None:
with tempfile.TemporaryDirectory(prefix="govoplan-offline-images-") as value:
root = Path(value)
+62 -2
View File
@@ -1,6 +1,7 @@
from __future__ import annotations
import argparse
import hashlib
import importlib.util
import json
import os
@@ -120,6 +121,22 @@ class RuntimeDistributionBuildTests(unittest.TestCase):
):
self.assertIn(temporary_path, nginx)
def test_web_runtime_resolves_the_configured_api_service_at_startup(self) -> None:
nginx = (ROOT / "tools/release/runtime/nginx.conf").read_text(
encoding="utf-8"
)
dockerfile = (ROOT / "tools/release/runtime/Dockerfile.web").read_text(
encoding="utf-8"
)
entrypoint = (ROOT / "tools/release/runtime/web-entrypoint.sh").read_text(
encoding="utf-8"
)
self.assertIn("proxy_pass ${GOVOPLAN_API_UPSTREAM};", nginx)
self.assertIn("nginx.conf.template", dockerfile)
self.assertIn("govoplan-web-entrypoint", dockerfile)
self.assertIn("envsubst '${GOVOPLAN_API_UPSTREAM}'", entrypoint)
def test_workflow_verifies_portable_bootstrap_artifacts_before_execution(
self,
) -> None:
@@ -162,7 +179,7 @@ class RuntimeDistributionBuildTests(unittest.TestCase):
)
self.assertNotIn(".platforms[\\\"linux/amd64\\\"]", workflow)
def test_workflow_binds_the_release_tag_to_the_workflow_commit(self) -> None:
def test_workflow_binds_distribution_to_the_peeled_release_tag(self) -> None:
workflow = (ROOT / ".gitea/workflows/runtime-distribution.yml").read_text(
encoding="utf-8"
)
@@ -170,7 +187,21 @@ class RuntimeDistributionBuildTests(unittest.TestCase):
encoding="utf-8"
)
self.assertIn("SOURCE_COMMIT: ${{ gitea.sha }}", workflow)
self.assertIn(
'git fetch --force --no-tags origin "refs/tags/v$VERSION:refs/tags/v$VERSION"',
workflow,
)
self.assertIn(
'git rev-parse "v$VERSION^{commit}" > runtime-output/release-source-commit',
workflow,
)
self.assertEqual(
2,
workflow.count(
'SOURCE_COMMIT="$(cat runtime-output/release-source-commit)"'
),
)
self.assertNotIn("SOURCE_COMMIT: ${{ gitea.sha }}", workflow)
self.assertIn('--target-commit "$SOURCE_COMMIT"', workflow)
self.assertIn('"target_commitish": target_commit', publisher)
self.assertIn("self._resolve_commit(tag) != target_commit", publisher)
@@ -298,12 +329,37 @@ class RuntimeDistributionBuildTests(unittest.TestCase):
(root / "web.json").write_text(json.dumps(web_metadata))
deployer = root / "govoplan-deploy.pyz"
deployer.write_bytes(b"zipapp")
package_lock = root / "package-artifacts.lock.json"
package_lock_value = {
"schema_version": "1",
"release_version": "1.2.3",
"python": [
{
"name": "govoplan-core",
"version": "1.2.3",
"sha256": "8" * 64,
}
],
"webui": [],
}
package_lock_value["lock_sha256"] = hashlib.sha256(
json.dumps(
package_lock_value,
sort_keys=True,
separators=(",", ":"),
).encode("utf-8")
).hexdigest()
package_lock.write_text(
json.dumps(package_lock_value) + "\n",
encoding="utf-8",
)
args = argparse.Namespace(
composition=root / "composition.json",
api_metadata=root / "api.json",
web_metadata=root / "web.json",
deployer=deployer,
deployer_url="https://downloads.example/govoplan-deploy.pyz",
package_lock=package_lock,
artifact_base_url="https://downloads.example/runtime/v1.2.3",
source_commit="f" * 40,
version="1.2.3",
@@ -327,6 +383,10 @@ class RuntimeDistributionBuildTests(unittest.TestCase):
)
self.assertTrue((root / "evidence/api-sbom.cdx.json").is_file())
self.assertTrue((root / "evidence/web-provenance.json").is_file())
self.assertEqual(
hashlib.sha256(package_lock.read_bytes()).hexdigest(),
descriptor["package_lock"]["sha256"],
)
def test_rejects_incomplete_oci_index(self) -> None:
with self.assertRaisesRegex(ValueError, "linux/amd64 and linux/arm64"):
+6
View File
@@ -75,6 +75,12 @@ class RuntimeImageContextTests(unittest.TestCase):
).read_text()
)
self.assertEqual(composition, published)
self.assertEqual(
(
SCRIPT.parent / "runtime" / "web-entrypoint.sh"
).read_bytes(),
(root / "context" / "web-entrypoint.sh").read_bytes(),
)
def test_rejects_missing_required_module(self) -> None:
with tempfile.TemporaryDirectory(prefix="govoplan-runtime-context-") as value:
+71
View File
@@ -0,0 +1,71 @@
from __future__ import annotations
import json
from pathlib import Path
import subprocess
import sys
import tempfile
import unittest
META_ROOT = Path(__file__).resolve().parents[1]
SCRIPT = META_ROOT / "tools" / "release" / "synchronize-webui-package-metadata.py"
class SynchronizeWebuiPackageMetadataTests(unittest.TestCase):
def test_copies_peer_contract_without_changing_publish_paths(self) -> None:
with tempfile.TemporaryDirectory() as directory:
repo = Path(directory)
(repo / "webui").mkdir()
(repo / "package.json").write_text(
json.dumps(
{
"name": "@govoplan/example-webui",
"exports": {".": "./webui/src/index.ts"},
"peerDependencies": {"vite": "^6"},
}
)
)
(repo / "webui" / "package.json").write_text(
json.dumps(
{
"name": "@govoplan/example-webui",
"peerDependencies": {"vite": "^7"},
"peerDependenciesMeta": {"vite": {"optional": True}},
}
)
)
subprocess.run(
[sys.executable, str(SCRIPT), "--repo", str(repo)],
check=True,
capture_output=True,
text=True,
)
package = json.loads((repo / "package.json").read_text())
self.assertEqual({"vite": "^7"}, package["peerDependencies"])
self.assertEqual({"vite": {"optional": True}}, package["peerDependenciesMeta"])
self.assertEqual({".": "./webui/src/index.ts"}, package["exports"])
def test_leaves_distinct_root_and_webui_packages_separate(self) -> None:
with tempfile.TemporaryDirectory() as directory:
repo = Path(directory)
(repo / "webui").mkdir()
(repo / "package.json").write_text(json.dumps({"name": "@govoplan/one"}))
(repo / "webui" / "package.json").write_text(json.dumps({"name": "@govoplan/two"}))
subprocess.run(
[sys.executable, str(SCRIPT), "--repo", str(repo)],
check=True,
capture_output=True,
text=True,
)
root = json.loads((repo / "package.json").read_text())
self.assertEqual("@govoplan/one", root["name"])
self.assertNotIn("peerDependencies", root)
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,194 @@
#!/usr/bin/env python3
"""Generate an independently held Ed25519 assessment-authority keypair."""
from __future__ import annotations
import argparse
import base64
from datetime import UTC, datetime, timedelta
import json
import os
from pathlib import Path
import re
import stat
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
KEY_ID_PATTERN = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:-]{0,159}$")
PROOF_SCOPES = (
"target_environment",
"external_providers",
"accessibility",
"privacy",
"security",
"operations",
"recovery",
"production_approval",
)
PURPOSES = {
"proof": (
"govoplan.capability-fit-proof-authorities",
"./capability-fit-proof-authority-keyring.schema.json",
),
"installer": (
"govoplan.installer-receipt-authorities",
"./installer-receipt-authority-keyring.schema.json",
),
}
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--purpose", choices=tuple(PURPOSES), required=True)
parser.add_argument("--key-id", required=True)
parser.add_argument(
"--scope",
action="append",
choices=PROOF_SCOPES,
default=[],
help="Authorized proof scope; repeat as needed. Not used for installer keys.",
)
parser.add_argument("--private-key", type=Path, required=True)
parser.add_argument("--keyring", type=Path, required=True)
parser.add_argument(
"--valid-days",
type=int,
default=365,
help="Validity from generation time (default: 365 days).",
)
parser.add_argument(
"--status",
choices=("active", "next"),
default="active",
)
args = parser.parse_args(argv)
if not KEY_ID_PATTERN.fullmatch(args.key_id):
parser.error("--key-id must be a valid opaque identifier")
if args.valid_days < 1 or args.valid_days > 3660:
parser.error("--valid-days must be between 1 and 3660")
scopes = _resolve_scopes(parser, purpose=args.purpose, scopes=args.scope)
private_path = args.private_key.expanduser().resolve()
keyring_path = args.keyring.expanduser().resolve()
_require_fresh_output(parser, private_path, label="private key")
_require_fresh_output(parser, keyring_path, label="keyring")
_require_private_directory(parser, private_path.parent)
_require_output_directory(parser, keyring_path.parent)
private_key = Ed25519PrivateKey.generate()
private_bytes = private_key.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.PKCS8,
encryption_algorithm=serialization.NoEncryption(),
)
public_bytes = private_key.public_key().public_bytes(
encoding=serialization.Encoding.Raw,
format=serialization.PublicFormat.Raw,
)
public_base64 = base64.b64encode(public_bytes).decode("ascii")
now = datetime.now(UTC).replace(microsecond=0)
not_after = now + timedelta(days=args.valid_days)
purpose, schema = PURPOSES[args.purpose]
keyring = {
"$schema": schema,
"schema_version": "0.1.0",
"purpose": purpose,
"keys": [
{
"key_id": args.key_id,
"status": args.status,
"public_key": public_base64,
"allowed_scopes": scopes,
"not_before": _rfc3339(now),
"not_after": _rfc3339(not_after),
}
],
}
_write_new_private_file(private_path, private_bytes)
try:
_write_new_private_file(
keyring_path,
(json.dumps(keyring, indent=2, sort_keys=True) + "\n").encode("utf-8"),
)
except BaseException:
private_path.unlink(missing_ok=True)
keyring_path.unlink(missing_ok=True)
raise
print(f"private_key={private_path}")
print(f"keyring={keyring_path}")
print(f"key_id={args.key_id}")
print(f"allowed_scopes={','.join(scopes)}")
return 0
def _resolve_scopes(
parser: argparse.ArgumentParser, *, purpose: str, scopes: list[str]
) -> list[str]:
if purpose == "installer":
if scopes:
parser.error("installer authorities do not accept --scope")
return ["installed_release_origin"]
unique = list(dict.fromkeys(scopes))
if not unique:
parser.error("proof authorities require at least one --scope")
return unique
def _require_fresh_output(
parser: argparse.ArgumentParser, path: Path, *, label: str
) -> None:
if path.exists() or path.is_symlink():
parser.error(f"{label.capitalize()} output already exists: {path}")
def _require_private_directory(
parser: argparse.ArgumentParser, directory: Path
) -> None:
_require_output_directory(parser, directory)
mode = stat.S_IMODE(directory.stat().st_mode)
if mode & (stat.S_IRWXG | stat.S_IRWXO):
parser.error(
"Private-key parent directory must not be accessible by group or others"
)
def _require_output_directory(
parser: argparse.ArgumentParser, directory: Path
) -> None:
try:
metadata = directory.lstat()
except OSError as exc:
parser.error(f"Output parent directory is unavailable: {directory}")
raise AssertionError from exc
if stat.S_ISLNK(metadata.st_mode) or not stat.S_ISDIR(metadata.st_mode):
parser.error(f"Output parent must be a real directory: {directory}")
def _write_new_private_file(path: Path, payload: bytes) -> None:
flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL
if hasattr(os, "O_NOFOLLOW"):
flags |= os.O_NOFOLLOW
descriptor = os.open(path, flags, 0o600)
try:
with os.fdopen(descriptor, "wb", closefd=False) as handle:
handle.write(payload)
handle.flush()
os.fsync(handle.fileno())
metadata = os.fstat(descriptor)
if not stat.S_ISREG(metadata.st_mode) or stat.S_IMODE(metadata.st_mode) != 0o600:
raise OSError("Authority output could not be secured")
finally:
os.close(descriptor)
def _rfc3339(value: datetime) -> str:
return value.isoformat().replace("+00:00", "Z")
if __name__ == "__main__":
raise SystemExit(main())
@@ -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",
)
@@ -15,6 +15,7 @@ from govoplan_core.core.dataflows import (
dataflow_run_lifecycle,
)
from govoplan_core.core.automation import AutomationPrincipalResolution
from govoplan_core.core.change_sequence import ChangeSequenceEntry
from govoplan_core.core.access import (
CAPABILITY_AUTH_AUTOMATION_PRINCIPAL_PROVIDER,
)
@@ -79,6 +80,7 @@ def main() -> int:
Base.metadata.create_all(
engine,
tables=[
ChangeSequenceEntry.__table__,
DistributedLease.__table__,
RecoveryOperation.__table__,
RecoveryCheckpoint.__table__,
+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())
+30
View File
@@ -38,10 +38,17 @@ cd "$ROOT"
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
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"
"$PYTHON" tools/inventory/platform-interface-inventory.py --strict-declarations --strict-endpoints
"$PYTHON" tools/repo/sync-module-package-workflows.py --check
"$PYTHON" tools/release/generate-developer-meta-package.py --check
"$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_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_institutional_governance_journey
"$PYTHON" -m unittest tests.test_institutional_service_journey
@@ -85,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")'
"$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 discover -s /mnt/DATA/git/govoplan-connectors/tests
"$PYTHON" -m unittest discover -s /mnt/DATA/git/govoplan-datasources/tests
@@ -92,9 +102,11 @@ PY
"$PYTHON" -m unittest discover -s /mnt/DATA/git/govoplan-workflow-engine/tests
"$PYTHON" -m unittest discover -s /mnt/DATA/git/govoplan-workflow/tests
"$PYTHON" -m unittest discover -s /mnt/DATA/git/govoplan-views/tests
"$PYTHON" -m unittest discover -s /mnt/DATA/git/govoplan-quick-access/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-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-runtime/tests
"$PYTHON" -m unittest discover -s /mnt/DATA/git/govoplan-cases/tests
@@ -103,15 +115,24 @@ PY
"$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-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" "$META_ROOT/tools/checks/check-datasource-composition.py"
"$PYTHON" "$META_ROOT/tools/checks/check-sanctions-screening-composition.py"
"$PYTHON" -m unittest discover -s /mnt/DATA/git/govoplan-mail/tests
"$PYTHON" -m unittest tests.test_api_smoke.ApiSmokeTests.test_mailbox_message_listing_reports_total_count
cd "$ROOT/webui"
"$NPM" run test:layout-primitives
"$NPM" run test:mail-components
"$NPM" run test:module-capabilities
"$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
"$NPM" run test:structure
@@ -125,6 +146,9 @@ cd /mnt/DATA/git/govoplan-workflow/webui
cd /mnt/DATA/git/govoplan-dashboard/webui
"$NPM" run test:dashboard-layout
cd /mnt/DATA/git/govoplan-approvals/webui
"$NPM" run test:workspace-layout
cd /mnt/DATA/git/govoplan-postbox/webui
"$NPM" run test:ui-structure
@@ -134,3 +158,9 @@ cd /mnt/DATA/git/govoplan-mail/webui
cd /mnt/DATA/git/govoplan-campaign/webui
"$NPM" run test:policy-ui
"$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
+171
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_]*)*"
)
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:
@@ -113,6 +177,42 @@ def main() -> int:
)
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]
if manifest.architecture is None:
if args.require_architecture:
@@ -128,6 +228,13 @@ def main() -> int:
manifest=manifest,
)
)
errors.extend(
_information_governance_evidence_errors(
repository_name=repository_name,
repository_root=repository_root,
manifest=manifest,
)
)
manifests.append(manifest)
@@ -135,6 +242,23 @@ def main() -> int:
print("\n".join(errors), file=sys.stderr)
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()
try:
for manifest in manifests:
@@ -153,6 +277,20 @@ def main() -> int:
f"Architecture declaration coverage: {declared}/{len(manifests)} modules "
f"({(declared / len(manifests) * 100):.1f}%)."
)
governance_counts: dict[str, int] = {}
for manifest in manifests:
for dimension in manifest.information_governance.dimensions.values():
governance_counts[dimension.adoption] = (
governance_counts.get(dimension.adoption, 0) + 1
)
print(
"Information-governance adoption: "
+ ", ".join(
f"{status}={count}"
for status, count in sorted(governance_counts.items())
)
+ "."
)
return 0
@@ -201,6 +339,39 @@ def _architecture_evidence_errors(
return errors
def _information_governance_evidence_errors(
*,
repository_name: str,
repository_root: Path,
manifest: object,
) -> list[str]:
declaration = getattr(manifest, "information_governance", None)
if declaration is None:
return [
f"{repository_name}: module has no information-governance declaration"
]
errors: list[str] = []
for dimension_name, dimension in declaration.dimensions.items():
for reference in dimension.evidence:
if not _looks_like_repository_reference(reference):
continue
candidate = (repository_root / reference).resolve()
try:
candidate.relative_to(repository_root.resolve())
except ValueError:
errors.append(
f"{repository_name}: {dimension_name} evidence escapes the "
f"repository: {reference!r}"
)
continue
if not candidate.exists():
errors.append(
f"{repository_name}: {dimension_name} evidence does not exist: "
f"{reference!r}"
)
return errors
def _looks_like_repository_reference(reference: str) -> bool:
normalized = reference.strip()
if not normalized or "://" in normalized:
+2
View File
@@ -26,6 +26,8 @@ cd "$ROOT"
"$PYTHON" "$META_ROOT/tools/checks/check_dependency_boundaries.py"
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
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
@@ -182,6 +182,11 @@ run_step "Validate installed module manifests and registry"
"$PYTHON" "$META_ROOT/tools/checks/release_integration.py" artifacts \
--requirements "$META_ROOT/requirements-release.txt"
run_step "Validate platform interface and endpoint declarations"
"$PYTHON" "$META_ROOT/tools/inventory/platform-interface-inventory.py" \
--strict-declarations \
--strict-endpoints
run_step "Generate release dependency provenance"
"$PYTHON" "$META_ROOT/tools/release/generate-release-sbom.py" \
--python "$PYTHON" \
@@ -257,6 +262,8 @@ cd "$WORK_ROOT/govoplan-campaign/webui"
"$NPM" run test:policy-ui
"$NPM" run test:template-preview
"$NPM" run test:import-utils
"$NPM" run test:campaign-collaboration
"$NPM" run test:campaign-work
echo
echo "Release integration check passed."
@@ -0,0 +1,306 @@
#!/usr/bin/env python3
"""Prove the governed Connectors -> Risk Compliance sanctions journey."""
from __future__ import annotations
from types import SimpleNamespace
from sqlalchemy import create_engine
from sqlalchemy.orm import Session
from govoplan_connectors.backend.db.models import (
ConnectorSanctionsAcquisitionRun,
ConnectorSanctionsSnapshot,
)
from govoplan_connectors.backend.sanctions_sources import (
SANCTIONS_READ_SCOPE as CONNECTOR_SANCTIONS_READ_SCOPE,
SANCTIONS_REFRESH_SCOPE,
SYNTHETIC_PROVIDER_ID,
)
from govoplan_core.auth import ApiPrincipal
from govoplan_core.core.access import PrincipalRef
from govoplan_core.core.modules import ModuleContext
from govoplan_core.core.recovery import RecoveryCheckpoint, RecoveryOperation
from govoplan_core.core.runtime_coordination import (
DistributedLease,
RuntimeIdentity,
bind_process_runtime_identity,
)
from govoplan_core.core.sanctions import (
SanctionsScreeningFreshnessRequest,
SanctionsScreeningPolicy,
SanctionsScreeningRequest,
SanctionsScreeningSubject,
sanctions_screening_provider,
sanctions_snapshot_provider,
)
from govoplan_core.db.base import Base
from govoplan_core.server.registry import build_platform_registry
from govoplan_risk_compliance.backend.db.models import (
RiskAssuranceEdge,
RiskAssuranceNode,
RiskSanctionsAddress,
RiskSanctionsAlias,
RiskSanctionsDate,
RiskSanctionsEntry,
RiskSanctionsIdentifier,
RiskSanctionsListSnapshot,
RiskScreeningCandidate,
RiskScreeningDisposition,
RiskScreeningException,
RiskScreeningRun,
RiskScreeningSubjectSnapshot,
)
from govoplan_risk_compliance.backend.permissions import (
SANCTIONS_ADMIN_SCOPE,
SANCTIONS_READ_SCOPE,
SANCTIONS_REVIEW_SCOPE,
SANCTIONS_SCREEN_SCOPE,
)
from govoplan_risk_compliance.backend.review import (
DispositionInput,
record_disposition,
)
from govoplan_risk_compliance.backend.sanctions_catalog import (
import_connector_snapshot,
)
from govoplan_risk_compliance.backend.screening import (
get_screening_run,
list_rescreening_requirements,
)
TABLES = (
DistributedLease.__table__,
RecoveryOperation.__table__,
RecoveryCheckpoint.__table__,
ConnectorSanctionsAcquisitionRun.__table__,
ConnectorSanctionsSnapshot.__table__,
RiskAssuranceNode.__table__,
RiskAssuranceEdge.__table__,
RiskSanctionsListSnapshot.__table__,
RiskSanctionsEntry.__table__,
RiskSanctionsAlias.__table__,
RiskSanctionsIdentifier.__table__,
RiskSanctionsDate.__table__,
RiskSanctionsAddress.__table__,
RiskScreeningSubjectSnapshot.__table__,
RiskScreeningRun.__table__,
RiskScreeningCandidate.__table__,
RiskScreeningDisposition.__table__,
RiskScreeningException.__table__,
)
def main() -> int:
registry = build_platform_registry(("connectors", "risk_compliance"))
registry.configure_capability_context(
ModuleContext(registry=registry, settings=object())
)
snapshot_provider = sanctions_snapshot_provider(registry)
screening_provider = sanctions_screening_provider(registry)
refresh_source = getattr(snapshot_provider, "refresh_source", None)
if snapshot_provider is None or not callable(refresh_source):
raise RuntimeError("Connectors sanctions acquisition is unavailable.")
if screening_provider is None:
raise RuntimeError("Risk Compliance sanctions screening is unavailable.")
engine = create_engine("sqlite:///:memory:")
Base.metadata.create_all(engine, tables=TABLES)
bind_process_runtime_identity(
RuntimeIdentity(
installation_id="sanctions-composition-check",
node_id="sanctions-worker",
incarnation="sanctions-worker-incarnation",
role="worker",
software_version="test",
composition_hash="d" * 64,
)
)
try:
with Session(engine) as session:
operator = _principal("operator-1", operational=True)
reviewer = _principal("reviewer-1", operational=False)
acquired = refresh_source(
session,
operator,
provider_id=SYNTHETIC_PROVIDER_ID,
idempotency_key="synthetic-sanctions-2026-08-01",
)
replay = refresh_source(
session,
operator,
provider_id=SYNTHETIC_PROVIDER_ID,
idempotency_key="synthetic-sanctions-2026-08-01",
)
if acquired.status != "succeeded" or acquired.snapshot is None:
raise RuntimeError(f"Synthetic acquisition failed: {acquired!r}")
if (
replay.run_id != acquired.run_id
or replay.snapshot is None
or replay.snapshot.ref != acquired.snapshot.ref
or replay.snapshot.sha256 != acquired.snapshot.sha256
):
raise RuntimeError("Acquisition idempotency did not replay exact evidence.")
imported, created = import_connector_snapshot(
session,
operator,
registry=registry,
connector_snapshot_ref=acquired.snapshot.ref,
)
imported_replay, replay_created = import_connector_snapshot(
session,
operator,
registry=registry,
connector_snapshot_ref=acquired.snapshot.ref,
)
if not created or replay_created or imported_replay.id != imported.id:
raise RuntimeError("Risk Compliance snapshot import is not idempotent.")
subject = SanctionsScreeningSubject(
subject_type="person",
primary_name="Alex Example",
subject_ref="party:fixture-person-1",
)
policy = SanctionsScreeningPolicy(failure_policy="block")
request = SanctionsScreeningRequest(
list_snapshot_id=imported.id,
idempotency_key="fixture-party-screening-1",
subject=subject,
policy=policy,
)
screened = screening_provider.request_screening(
session,
operator,
request,
)
screened_replay = screening_provider.request_screening(
session,
operator,
request,
)
if not screened.created or screened_replay.created:
raise RuntimeError("Screening request idempotency is not stable.")
if screened.evidence.ref != screened_replay.evidence.ref:
raise RuntimeError("Screening replay returned different evidence.")
if screened.evidence.outcome != "potential" or screened.evidence.candidate_count != 1:
raise RuntimeError(f"Synthetic match was not reviewable: {screened.evidence!r}")
run = get_screening_run(
session,
operator,
run_id=screened.evidence.run_id,
)
candidate, disposition = record_disposition(
session,
reviewer,
candidate_id=run.candidates[0].id,
disposition=DispositionInput(
decision="false_positive",
reason="Independent fixture evidence excludes the screened party.",
evidence_refs=(acquired.snapshot.raw_evidence_ref,),
),
)
if candidate.review_status != "false_positive":
raise RuntimeError("Independent review did not resolve the candidate.")
if disposition.separation_status != "independent":
raise RuntimeError("Reviewer separation evidence was not retained.")
cleared = screening_provider.check_freshness(
session,
operator,
SanctionsScreeningFreshnessRequest(
evidence_ref=screened.evidence.ref,
current_subject=subject,
expected_list_snapshot_id=imported.id,
policy=policy,
),
)
if not cleared.fresh or cleared.gate_decision != "allow":
raise RuntimeError(f"Reviewed evidence did not clear the gate: {cleared!r}")
# Acquisition and review are separate durable commands in production.
session.commit()
session.expire_all()
refreshed = refresh_source(
session,
operator,
provider_id=SYNTHETIC_PROVIDER_ID,
idempotency_key="synthetic-sanctions-2026-08-02",
)
if refreshed.snapshot is None or refreshed.snapshot.ref == acquired.snapshot.ref:
raise RuntimeError("A new acquisition did not create new immutable evidence.")
current, current_created = import_connector_snapshot(
session,
operator,
registry=registry,
connector_snapshot_ref=refreshed.snapshot.ref,
)
if not current_created:
raise RuntimeError("The refreshed list state was not imported separately.")
stale = screening_provider.check_freshness(
session,
operator,
SanctionsScreeningFreshnessRequest(
evidence_ref=screened.evidence.ref,
current_subject=subject,
expected_list_snapshot_id=current.id,
policy=policy,
),
)
if stale.fresh or stale.gate_decision != "block":
raise RuntimeError(f"Changed source evidence did not close the gate: {stale!r}")
if "source_snapshot_changed" not in stale.reasons:
raise RuntimeError("Source change provenance was not reported.")
requirements = list_rescreening_requirements(session, operator)
if screened.evidence.run_id not in {item.run.id for item in requirements}:
raise RuntimeError("The stale screening is absent from the rescreening queue.")
session.commit()
if session.query(RecoveryOperation).count() != 2:
raise RuntimeError("Connector acquisition recovery evidence is incomplete.")
if session.query(RiskScreeningDisposition).count() != 1:
raise RuntimeError("Disposition evidence was duplicated or lost.")
finally:
bind_process_runtime_identity(None)
engine.dispose()
print(
"Connectors -> immutable sanctions snapshot -> Risk Compliance review "
"and rescreening composition passed."
)
return 0
def _principal(account_id: str, *, operational: bool) -> ApiPrincipal:
scopes = {
SANCTIONS_READ_SCOPE,
SANCTIONS_REVIEW_SCOPE,
}
if operational:
scopes.update(
{
CONNECTOR_SANCTIONS_READ_SCOPE,
SANCTIONS_REFRESH_SCOPE,
SANCTIONS_ADMIN_SCOPE,
SANCTIONS_SCREEN_SCOPE,
}
)
return ApiPrincipal(
principal=PrincipalRef(
account_id=account_id,
membership_id=f"membership-{account_id}",
tenant_id="tenant-1",
scopes=frozenset(scopes),
),
account=SimpleNamespace(id=account_id),
user=SimpleNamespace(id=f"membership-{account_id}"),
)
if __name__ == "__main__":
raise SystemExit(main())
@@ -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())

Some files were not shown because too many files have changed in this diff Show More