91 Commits
Author SHA1 Message Date
zemion 2ffdb23f69 feat(devkit): add resumable workspace automation and UI review tooling
Dependency Audit / dependency-audit (push) Successful in 1m45s
Deployment Installer / deployment-installer (push) Successful in 6s
Security Audit / security-audit (push) Successful in 11m30s
Verified with the coordinated workspace changes by devkit full run
2026-09-08T225814-186389-0000-3e3ed7cd (all seven phases passed).
This shared UI pass does not mark the individual module reviews complete.
2026-09-09 02:03:17 +02:00
zemion 14b19fbead chore(release): coordinate integrity and performance source updates
Dependency Audit / dependency-audit (push) Successful in 2m9s
Deployment Installer / deployment-installer (push) Successful in 8s
Security Audit / security-audit (push) Successful in 14m36s
Developer Meta-package Release / publish-package (push) Successful in 10s
Release v0.1.46. Coordinated integrity review: GovOPlaN/govoplan-core#298.
2026-09-08 12:53:09 +02:00
zemion 845dcbafdb chore(release): coordinate integrity and performance source updates
Release v0.1.46. Coordinated integrity review: GovOPlaN/govoplan-core#298.
2026-09-08 12:36:36 +02:00
zemion 58d320d9b3 Harden source release preparation and record verified security follow-up
Dependency Audit / dependency-audit (push) Successful in 1m51s
Deployment Installer / deployment-installer (push) Successful in 8s
Security Audit / security-audit (push) Successful in 12m32s
2026-09-08 08:04:12 +02:00
zemion 9554657bb5 fix(release): preserve failed installer retry status
Dependency Audit / dependency-audit (push) Successful in 1m43s
Deployment Installer / deployment-installer (push) Successful in 7s
Security Audit / security-audit (push) Successful in 11m21s
Cover all retry call sites with isolated regressions in focused checks and installer CI. Add EN/DE operating guidance and record the unreleased Xrechnung and installer audit follow-ups without changing immutable release artifacts.

Refs #54
2026-09-08 05:36:10 +02:00
zemion 88b685ff5e test(release): follow relocated operations runbook
Developer Meta-package Release / publish-package (push) Successful in 9s
Dependency Audit / dependency-audit (push) Successful in 1m41s
Deployment Installer / deployment-installer (push) Successful in 6s
Security Audit / security-audit (push) Successful in 14m12s
2026-09-08 03:02:31 +02:00
zemion be57a1823a fix(release): support validated per-run SSH address family 2026-09-08 02:50:59 +02:00
zemion 6bcb75f577 feat(release): build verified full-registry catalog candidates 2026-09-08 02:26:19 +02:00
zemion 32fe4b7238 chore(release): document hardening and verify complete source package composition 2026-09-08 02:06:35 +02:00
zemion 6a8f53b87d fix(release): prepare aligned 0.1.45 composition and guarded candidate sequencing 2026-09-08 01:52:08 +02:00
zemion 1cec4ee1d8 chore: compose governed Cases access
Dependency Audit / dependency-audit (push) Successful in 1m43s
Deployment Installer / deployment-installer (push) Successful in 6s
Security Audit / security-audit (push) Failing after 12m23s
2026-08-24 20:26:44 +02:00
zemion 29d03aa2ca chore: compose purpose-bound Records access
Dependency Audit / dependency-audit (push) Successful in 1m44s
Deployment Installer / deployment-installer (push) Successful in 6s
Security Audit / security-audit (push) Failing after 12m45s
2026-08-24 19:28:37 +02:00
zemion 6fb928d6cf release: publish stable product destinations
Dependency Audit / dependency-audit (push) Successful in 1m43s
Deployment Installer / deployment-installer (push) Successful in 6s
Security Audit / security-audit (push) Failing after 12m26s
Developer Meta-package Release / publish-package (push) Successful in 9s
2026-08-24 18:30:01 +02:00
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
223 changed files with 34060 additions and 1358 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
@@ -23,6 +23,9 @@ jobs:
- name: Test declarative deployment bundle
working-directory: govoplan
run: python -m unittest -v tests.test_deployment_installer
- name: Test WebUI installer retry failures
working-directory: govoplan
run: python -m unittest -v tests.test_webui_release_dependency_retries
- name: Build single-file deployer artifact
working-directory: govoplan
run: |
@@ -101,6 +101,8 @@ jobs:
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"
@@ -119,6 +121,9 @@ jobs:
.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 \
@@ -130,6 +135,7 @@ jobs:
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 \
+1
View File
@@ -9,6 +9,7 @@ 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/
+7
View File
@@ -15,6 +15,13 @@ platform behavior remains owned by the corresponding module repository.
## Working Rules
- Start repeated workflows with `./devkit commands`; use `context --changed`,
`check --profile quick --changed --dry-run`, and `review MODULE` instead of
reconstructing repository/check inventories. See `docs/operations/DEVKIT.md`.
- Prefer compact check receipts (`status`, `summary`, `logs`) to repeated full
log dumps. `check --profile full` retains the required focused gate; targeted
profiles or cached results do not waive required verification or approvals.
- Treat Gitea issues as the canonical backlog and state log.
- Preserve optional module boundaries and use Core contracts or capabilities for integrations.
- Prefer targeted checks before full workspace scans.
+27 -45
View File
@@ -19,6 +19,17 @@ installed modules/connectors discovered by core.
## Common Commands
For repeated development and review workflows, start with the unified command
suite. [Developer command guide](docs/operations/DEVKIT.md) documents profiles,
evidence, safe Git/release operations and reuse in other projects.
```sh
./devkit commands
./devkit context --changed
./devkit check --profile quick --changed --dry-run
./devkit review campaign
```
Create the whole-product development virtualenv in this meta repository:
```sh
@@ -123,7 +134,7 @@ Synchronize module package workflows and inspect the registry release contract:
Package publication, exact artifact locking, and the optional `govoplan`
developer meta-package are documented in
[Package Registry Releases](docs/PACKAGE_REGISTRY_RELEASES.md).
[Package Registry Releases](docs/operations/PACKAGE_REGISTRY_RELEASES.md).
For reproducible release artifacts, set `SOURCE_DATE_EPOCH` to the release
commit timestamp (or pass an explicit timezone-qualified `--timestamp`):
@@ -170,15 +181,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/KUBERNETES_TEST_LAB.md).
[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
@@ -191,47 +206,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:
Executable
+12
View File
@@ -0,0 +1,12 @@
#!/bin/sh
# One stable entry point; no environment activation, installation or server startup.
set -eu
DEVKIT_ROOT=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
if [ -n "${PYTHON:-}" ]; then
DEVKIT_PYTHON=$(command -v "$PYTHON") || { echo "devkit: configured PYTHON is unavailable" >&2; exit 127; }
elif [ -x "$DEVKIT_ROOT/.venv/bin/python" ]; then
DEVKIT_PYTHON="$DEVKIT_ROOT/.venv/bin/python"
else
DEVKIT_PYTHON=$(command -v python3)
fi
exec "$DEVKIT_PYTHON" "$DEVKIT_ROOT/tools/devkit/devkit.py" "$@"
-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.
-197
View File
@@ -1,197 +0,0 @@
# 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` translates the reviewed Git
source refs in `requirements-release.txt` into an exact registry package set.
It resolves each version tag to its commit and verifies the package metadata in
that tag.
`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 SHA-256 values and npm integrity
values. 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 wheelhouse directly and
installs module WebUI tarballs only after matching them 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, then binds both the signed distribution source and the Gitea
release assets to that exact commit. A post-tag workflow repair can therefore
retry publication without 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.
## 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.
+111 -56
View File
@@ -1,75 +1,130 @@
# GovOPlaN Documentation Map
# GovOPlaN Documentation
This directory contains cross-repository product, architecture, release, and
operational documentation. The map below defines which document answers which
question. A document not listed as the current status source must not present
volatile repository, issue, release, or maturity counts as current facts.
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
| Question | Canonical source |
| Document | Role |
| --- | --- |
| What are the stable ideas and boundaries of the platform? | [Platform Core Ideas](PLATFORM_CORE_IDEAS.md) |
| What product outcomes should GovOPlaN pursue? | [Connected Governance Platform Roadmap](CONNECTED_GOVERNANCE_PLATFORM_ROADMAP.md) |
| Which institutional concepts and owners form the target architecture? | [Institutional Governance Target Architecture](INSTITUTIONAL_GOVERNANCE_TARGET_ARCHITECTURE.md) |
| Which end-to-end proofs should guide implementation? | [Reference Journey Program](REFERENCE_JOURNEY_PROGRAM.md) |
| What is the reconciled state now? | [Strategy Status](STRATEGY_STATUS.md) |
| [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 |
The dated [Strategic Review](STRATEGIC_REVIEW_2026-08-05.md) explains why the
current reset and sequencing were chosen. It is an assessment record, not a
second live status page.
## Product Architecture
## Architecture
| Topic | Canonical source |
| --- | --- |
| Product-facing experience and hiding technical module boundaries | [Product Experience and Module Boundaries](PRODUCT_EXPERIENCE_AND_MODULE_BOUNDARIES.md) |
| Federation between autonomous installations | [Federated GovOPlaN Architecture](FEDERATED_GOVOPLAN_ARCHITECTURE.md) |
| Institutional digital twin and continuous assurance | [Institutional Digital Twin](INSTITUTIONAL_DIGITAL_TWIN.md) |
| Assisted and non-digital channels | [Assisted and Non-Digital Channels](ASSISTED_AND_NON_DIGITAL_CHANNELS.md) |
| Cross-module temporal, purpose, retention, and institutional-context adoption | `govoplan-core/docs/INFORMATION_GOVERNANCE_ADOPTION.md` |
| eAkte and digital-record ownership | `govoplan-records/docs/EAKTE_ARCHITECTURE.md` |
| Data source, definition, and transformation graph | [Datasource and Definition Graph Architecture](DATASOURCE_AND_DEFINITION_GRAPH_ARCHITECTURE.md) |
| Focused task views | [Views Architecture](VIEWS_ARCHITECTURE.md) |
| Shared interface patterns | [Interface Pattern Language](INTERFACE_PATTERN_LANGUAGE.md) |
| 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) |
## Runtime And Delivery
Module-specific architecture remains in the owning repository. In particular,
information-governance adoption is in
`govoplan-core/docs/INFORMATION_GOVERNANCE_ADOPTION.md`, and the eAkte model is
in `govoplan-records/docs/EAKTE_ARCHITECTURE.md`.
- [Module Contracts and Installs](MODULE_CONTRACTS_AND_INSTALLS.md)
- [Platform Control Plane](PLATFORM_CONTROL_PLANE.md)
- [Installation and Deployment Architecture](INSTALLATION_AND_DEPLOYMENT_ARCHITECTURE.md)
- [Kubernetes VM Test Lab](KUBERNETES_TEST_LAB.md)
- [Scaling and Multi-Host Deployment](SCALING_AND_MULTI_HOST_DEPLOYMENT.md)
- [Recovery and Rollback Guarantees](RECOVERY_AND_ROLLBACK_GUARANTEES.md)
- [Recovery Ledger Adoption](RECOVERY_LEDGER_ADOPTION.md)
- [Package Registry Releases](PACKAGE_REGISTRY_RELEASES.md)
## Operations
## Evidence And Snapshots
| Need | Source |
| --- | --- |
| Installation model and managed components | [Installation and Deployment Architecture](operations/INSTALLATION_AND_DEPLOYMENT_ARCHITECTURE.md) |
| Supported operating modes | [Deployment Profiles](operations/DEPLOYMENT_PROFILES.md) |
| Horizontal scaling and multi-host topology | [Scaling and Multi-Host Deployment](operations/SCALING_AND_MULTI_HOST_DEPLOYMENT.md) |
| Local Kubernetes evidence target | [Kubernetes VM Test Lab](operations/KUBERNETES_TEST_LAB.md) |
| Recovery guarantees and state machine | [Recovery and Rollback Guarantees](operations/RECOVERY_AND_ROLLBACK_GUARANTEES.md) |
| Recovery-ledger rollout | [Recovery Ledger Adoption](operations/RECOVERY_LEDGER_ADOPTION.md) |
| Backup evidence contract | [Backup and Restore Evidence](operations/BACKUP_AND_RESTORE_EVIDENCE.md) |
| Target handoff and independent evidence | [Production Target Handoff](operations/PRODUCTION_TARGET_HANDOFF.md) |
| Evidence collection and promotion | [Target Maturity Evidence Runbook](operations/TARGET_MATURITY_EVIDENCE_RUNBOOK.md) |
| Package publication and consumption | [Package Registry Releases](operations/PACKAGE_REGISTRY_RELEASES.md) |
| Release-console operation | [Release Console](operations/RELEASE_CONSOLE.md) |
| Repeated development, verification and evidence commands | [Developer Command Suite](operations/DEVKIT.md) |
| Module compatibility and install behavior | [Module Contracts and Installs](operations/MODULE_CONTRACTS_AND_INSTALLS.md) |
| Security-audit toolchain | [Security Audit](operations/SECURITY_AUDIT.md) |
These documents are intentionally dated or pinned. They may remain useful even
after the product changes, but they do not override `STRATEGY_STATUS.md`.
## Project Reference
- [Capability and Infrastructure Fit Assessment](CAPABILITY_AND_INFRASTRUCTURE_FIT.md), pinned to the 2026-07-22 Campaign composition
- [Strategic Review 2026-08-05](STRATEGIC_REVIEW_2026-08-05.md)
- [Backup and Restore Evidence](BACKUP_AND_RESTORE_EVIDENCE.md)
- [Production Target Handoff](PRODUCTION_TARGET_HANDOFF.md)
- [Target Maturity Evidence Runbook](TARGET_MATURITY_EVIDENCE_RUNBOOK.md)
- [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.
- [UI Review Program](project/UI_REVIEW_PROGRAM.md) defines the principle-led,
per-module review process and links the canonical Gitea review inventory.
Machine-readable schemas and evidence files belong beside the document that
defines them. Generated inventories belong in `audit-reports/` and should not
be edited manually.
## 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 work-state source.
2. `STRATEGY_STATUS.md` is the only prose reconciliation of current portfolio
state. Refresh it from manifests, inventories, tests, and Gitea; do not copy
its counts into durable architecture pages.
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 status and issues for implementation depth.
4. Dated assessments retain their original composition and conclusion. Add a
snapshot notice rather than silently updating their claims.
5. Module-specific behavior and user/admin documentation remain in the owning
repository. Meta documentation defines cross-module outcomes and contracts.
6. A new strategy document must replace, narrow, or link an existing source;
it must not introduce a parallel roadmap.
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
```
@@ -72,6 +72,16 @@ An assisted session is a resumable work item, not a privileged bypass. It:
8. creates follow-up tasks when original documents, signatures, translation,
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.
@@ -12,21 +12,21 @@ The source concepts describe GovOPlaN as an operational governance platform for
public institutions. This document is the canonical repository version of that
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_STATUS.md), and Gitea issues remain the
lives in [Strategy Status](../strategy/STRATEGY_STATUS.md), and Gitea issues remain the
source of truth for delivery state.
Read this together with:
- [Connected Governance Platform Roadmap](CONNECTED_GOVERNANCE_PLATFORM_ROADMAP.md)
- [Platform Core Ideas](PLATFORM_CORE_IDEAS.md)
- [Strategy Status](STRATEGY_STATUS.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
@@ -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
@@ -15,6 +15,8 @@ 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
@@ -70,6 +72,37 @@ Each WebUI module should be able to announce:
The contract references surfaces. It does not permit Core or a product package
to import their implementation.
The versioned `product_surfaces` slice is 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. Tasks, Calendar and Files contribute the corresponding single-owner
identities:
| Product identity | Stable destination | Compatible owner route |
| --- | --- | --- |
| Work | `/work` | `/tasks` |
| Calendar | `/agenda` | `/calendar` |
| Messages | `/messages` (`/inbox` alias) | `/mail`, `/postbox` |
| Files | `/documents` | `/files` |
Core replaces those owner entries in the ordinary rail with the stable product
destinations. A collapsed **All available tools** catalogue retains every
authorized technical owner route independently of View focus; unauthorized
entries are never disclosed. The original deep links remain valid, and all
contributing owner paths keep the corresponding product entry active. 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:
@@ -80,6 +113,23 @@ The default shell should prioritize:
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 is available as the collapsed
**All available tools** escape. It is deliberately independent of the active
View while still enforcing authorization. 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
@@ -90,6 +140,12 @@ 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.
Core browser conformance exercises the German Anwohnerparkausweis reference
context with Work, Calendar, Messages and Files entries, verifies that package
owner labels are absent from the primary rail, expands the technical catalogue,
and runs WCAG 2 A/AA checks over the result. Unit permutations cover two-owner,
one-owner, unauthorized-owner and focused-View compositions.
## Error And Provenance Language
Normal errors answer:
@@ -106,11 +162,17 @@ 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
- classify every route, navigation item, widget, setting, search object, and
- continue classifying every route, navigation item, widget, setting, search object, and
help context by product area and object type;
- add product aliases without removing existing deep links;
- extend the implemented product-surface aliases without removing existing deep links;
- flag raw module IDs in ordinary-user labels and errors.
### Slice 2: work-first shell
@@ -119,6 +181,8 @@ hiding operational truth.
- 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
@@ -0,0 +1,241 @@
# 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;
- Core promotes Work (`/work`), Calendar (`/agenda`), Messages (`/messages`)
and Files (`/documents`) into stable primary destinations and collapses the
compatible owner routes under **All available tools**;
- **All available tools** is permission-derived but independent of the active
View, providing a deliberate escape without granting access or discarding
the original `/tasks`, `/calendar`, `/mail`, `/postbox` and `/files` links.
The baseline classification and the four initial stable destinations are now
manifest-declared. The area classification covers every ordinary user-facing
module and is 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. The pinned German Anwohnerparkausweis browser composition
verifies stable product labels, technical escape, keyboard access and WCAG
conformance. 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 or Files remain direct product
destinations. The objective is not to hide every implementation name from
administrators; 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
@@ -1,5 +1,8 @@
# 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
@@ -9,8 +12,8 @@ need is convergence: fewer simultaneous fronts, stronger cross-cutting
adoption, and end-to-end reference journeys that non-developers can complete.
This is a dated review. Current status belongs in
[Strategy Status](STRATEGY_STATUS.md); stable direction belongs in
[Platform Core Ideas](PLATFORM_CORE_IDEAS.md).
[Strategy Status](../../strategy/STRATEGY_STATUS.md); stable direction belongs in
[Platform Core Ideas](../../strategy/PLATFORM_CORE_IDEAS.md).
## What Is Already Strong
@@ -53,6 +56,13 @@ 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
@@ -123,4 +133,3 @@ 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,8 +1,14 @@
# GovOPlaN Capability and IT-Infrastructure Fit Assessment
# Supporting Narrative: 2026-07-22 Capability and Infrastructure Assessment
> **Canonical report:** The schema-validated human report is generated from the
> machine-readable input at
> [`CAPABILITY_AND_INFRASTRUCTURE_FIT.generated.md`](CAPABILITY_AND_INFRASTRUCTURE_FIT.generated.md).
> This file retains the original hand-authored evidence narrative and operator
> guidance; it is not maintained as a second set of conclusions.
> **Pinned historical evidence:** This document assesses the exact 2026-07-22
> Campaign composition below. It is intentionally not updated to describe later
> main-branch work. Use [Strategy Status](STRATEGY_STATUS.md) for the current
> main-branch work. Use [Strategy Status](../../strategy/STRATEGY_STATUS.md) for the current
> cross-product reconciliation and create a new dated fit assessment for a new
> target composition.
@@ -17,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)
and [Strategy Status](STRATEGY_STATUS.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
@@ -404,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.
@@ -487,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`.
@@ -511,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,
@@ -521,7 +527,7 @@ is authorized for the full proof interval; catalog and installer key reuse is
rejected. The command immediately verifies its own result and atomically writes
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 \
@@ -599,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
@@ -623,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.
@@ -104,7 +109,9 @@ semantics as authenticated navigation routes.
| `/scheduling/public/:requestId/:token` | Scheduling | Public signed token | Public participation | Scheduling #8 complete in `c17cbda` |
| `/search` | Search | `search:result:read` | Keyboard-first global/context overlay and full results fallback | Search pattern migration complete in [Search #4](https://git.add-ideas.de/GovOPlaN/govoplan-search/issues/4); durable evidence in `govoplan-search/docs/INTERFACE_PATTERN_MIGRATION.md` |
| `/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` |
| `/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
@@ -318,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,
+6
View File
@@ -371,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`.
+300
View File
@@ -0,0 +1,300 @@
# Developer command suite
`./devkit` is the maintained entry point for repeated development work. It uses
ordinary local programs, not an AI service. People, CI and coding agents use the
same commands and results. It composes existing GovOPlaN checks and release
services rather than defining parallel product rules.
## Start here
From the Meta checkout:
```sh
./devkit commands
./devkit doctor --repo core
./devkit context --changed
./devkit check --profile quick --repo campaign --dry-run
./devkit coverage --profile ui --repo campaign
./devkit check --profile quick --repo campaign
./devkit latest
./devkit review campaign
```
Use `--help` on each command. `--json` (or `--format json`) returns structured
results; the default summary intentionally omits full logs and source contents.
Global options work before or after the command. `--workspace-root` is the parent
directory of the registered repositories, not the Core checkout. Unknown
repository filters fail rather than silently widening/narrowing scope.
The launcher prefers Meta's `.venv/bin/python`, or the explicit `PYTHON`
executable, with `python3` as the no-venv fallback. Checks resolve `NODE` and
`NPM` from explicit environment settings or `PATH`; no username-specific Node
installation path is required. Doctor reports repairs but never installs
packages, changes configuration, kills an occupied port or starts a server.
## Command contracts
| Command | Purpose | Effects |
| --- | --- | --- |
| `context [--changed] [--repo NAME]` | Offline Git state, instruction/documentation paths and review links | Reads only; upstream counts are not freshly fetched |
| `doctor [--repo NAME] [--profile PROFILE]` | Interpreter, dependency, browser and test-resource preflight | Reads only; repair suggestions are not executed |
| `check --profile PROFILE` | Registered checks with explanations, bounded logs and a receipt | Runs trusted tests/builds; `--dry-run` only plans |
| `coverage --profile PROFILE [--repo NAME]` | Declared suite dispositions and explicit coverage limits | Reads only; not execution evidence |
| `runs [--limit 10] [--before RUN]`, `latest` | Discover recent runs without looking up state-directory paths | Reads only; malformed latest evidence is not replaced with an older pass |
| `status RUN`, `summary RUN` | Stage counts and failure log locations | Reads only |
| `logs RUN --stage STAGE --tail 40 [--final-only]` | Provisional live output or a hash-verified final stage log | Reads only; provisional output never proves a pass |
| `resume RUN` | Replan the previous check selection and reuse eligible successful stages | Runs remaining checks, creating a new receipt |
| `recover RUN [--apply]` | Recover an abandoned check-run record after acquiring its OS lock | No test replay; apply changes only the local record |
| `review MODULE` | Source inventory, principle revision, issue links, check plan and walkthrough checklist | Does not perform or complete a module review |
| `docs audit [--repo NAME] [--changed]` | Owning manifest/help/translation checks and visible-label gaps | Local checks only; never translates or edits content |
| `issues note …` | Preview and explicitly append deduplicated evidence | Remote comment creation only with `--apply` |
| `release …` | Existing durable release planning/execution/recovery | Explicit `--apply`, request IDs and existing step confirmations |
| `git …` | Frozen, explicit-path maintenance commit and exact branch push | Preview first; mutations require `--apply`; no stage-all, force or tags |
Git maintenance is a separate, selected-path workflow; see
[Git maintenance](DEVKIT_MAINTENANCE.md). Issue notes and module bundles are
documented in [Evidence and review commands](DEVKIT_EVIDENCE.md). The complete
release command syntax, limitations and examples are in
[Headless release operations](DEVKIT_RELEASE.md).
## Check profiles and their limits
- `quick`: existing manifest/interface invariants, import/help guards and selected
module source/structure tests. It does not compile component suites or run the
full browser matrix.
- `ui`: quick/source contracts plus the shared Core component batch, compiled
once in an isolated directory. This is not a full visual/usability review.
- `backend`: existing manifest/interface checks and selected repository backend
test suites. Shared backend-test state is serialized.
- `full`: the canonical `tools/checks/check-focused.sh`, including optional-module
build permutations and integrated browser conformance. Repository filters do
not reduce this required cross-module completion gate.
`--changed` includes uncommitted work, commits ahead of the locally cached
upstream and repositories with commits but no configured upstream.
Missing/unreadable repositories remain visible as errors. For the
GovOPlaN profiles, Core or Meta changes conservatively select every registered
consumer; other changes expand through declared interface consumers. This is
not complete semantic dependency analysis. Use the full gate for cross-module
completion as required by `AGENTS.md`.
The module catalog reuses declared direct-Node tests and established structural
script names. Arbitrary shell chains are not guessed or rewritten. Unsupported
declared scripts remain visible with a reason; only exact known Core component
aliases are covered by the shared component runner. Invalid package metadata is
an error, not an empty test inventory.
`coverage` inventories declared suites as `planned`, `covered_elsewhere`,
`excluded` or `unsupported`, naming their covering stage where applicable. The
same inventory accompanies check dry runs and saved receipts. **Full means the
canonical focused gate, not every test in every package.** Its inventory follows
explicit canonical commands conservatively; it does not infer arbitrary nested
shell commands or npm hooks. Add new deterministic checks to the owning
package/canonical gate and the catalog's tests as appropriate. Coverage planning
does not execute the scripts it inventories or assert that they passed. See
[Coverage and validation details](DEVKIT_COVERAGE.md) for examples and boundaries.
Independent checks can run with `--jobs 1..8` (default 2). Shared resources use
OS locks for the current user, including across runs with different evidence
directories. Browser/port locks are shared across that user's workspaces. Full runs
reserve the shared WebUI/backend/browser resources. Standalone Core component
commands use isolated temporary build directories; `npm run test:components --
page-layout documentation-help` compiles once for that batch. Managed temporary
output is cleaned; no user worktree is reset or restored.
## Evidence, interruption and safe reuse
A check announces its run ID and saves a `preparing` receipt before source and
environment fingerprinting. Progress on stderr shows elapsed time, stage counts
and active stages; it also distinguishes `checking` from final snapshot
verification (`finalizing`). `--json` keeps stdout as one final JSON result and
emits structured progress events on stderr; `--quiet` suppresses these events.
Use `latest`, `status RUN` and `logs RUN --stage STAGE` from another terminal
while a check runs. Counts are stages, not a guessed percentage of test effort.
Live snapshots are private, bounded, explicitly provisional and not accepted for
reuse or issue evidence. A completed stage log can be hash-verified before the
whole run finishes, but only the final run snapshot verifies the overall result.
`--final-only` rejects unfinished logs. Run history supports cursor pagination
(up to 100 rows per page), bounds directory scanning, and never deletes evidence.
An explicitly selected `--project` also filters run discovery by project file.
Default records are private below
`$XDG_STATE_HOME/govoplan/devkit/workspace-<identity>/runs/<run>/` (fallback
`~/.local/state`). `--state-dir` selects another base; workspace scoping still
applies. Status changes are atomically persisted with restrictive file modes;
symlinked evidence paths are rejected. A record contains source/environment/plan
fingerprints, stage reasons, status, exit code, timing and log identities.
Stages distinguish pending, running, passed, failed, timed out, blocked, skipped,
stale and interrupted. A skipped or unexecuted stage is never a pass. A source or
environment change during verification marks the overall result **stale**, even
when individual subprocesses returned zero. Such a result is not passing evidence.
`resume RUN` replans the current selection and compares each phase independently.
Matching **verified checkpoints** are reused; changed/new phases and phases
without a checkpoint run again. A checkpoint binds the exact command and working
directory, declared repository inputs, transitive data-dependency identities,
devkit implementation/schema, tool/dependency metadata and environment. Logs must
retain their recorded hashes; a missing or changed cached log fails closed.
The summary reports how many checkpoints were reused; JSON records `input_scope`,
`cache_key`, `checkpoint_verified`, `reused_from` and the reuse/rerun reason.
Each checkpoint is saved while its resource locks remain held, only after exit
zero, a verified final log, and matching before/after inputs and environment.
A phase that changes its own inputs is stale, even if another phase later restores
those bytes. Final verification rechecks all passed/reused phases against current
inputs before the new aggregate can pass. An interrupted, failed or stale run may
donate an independently verified matching checkpoint, but never becomes passing
evidence itself. Old receipts remain readable: legacy monolithic logs and receipts
without these checkpoints cannot be retroactively split or reused as verified phases.
After a hard crash, `recover RUN` shows the abandoned owner and guidance. Inspect the
recorded commands and manually stop any surviving test/build processes before
using `recover RUN --apply --confirm-processes-stopped`. The command verifies the
run lock is free before marking the record interrupted. A free parent lock alone
does **not** prove children stopped after a hard kill. Recovery does not replay
processes or infer that a partially completed operation succeeded.
### Canonical phases and narrower inputs
The full gate has seven explicit phases: `preflight`, `tooling`, `backend`,
`core-ui`, `module-builds`, `browser`, and `module-ui`. Devkit stage IDs are
`focused.<phase>`. The authoritative Bash bodies remain in
`tools/checks/check-focused.sh`; the bounded metadata in
`tools/checks/focused-phases.json` names their order and resources. The direct
shell command still runs every phase in the original fail-fast order. For inspection:
```sh
tools/checks/check-focused.sh --list-phases
./devkit check --profile full --jobs 1
./devkit resume RUN_ID --jobs 1
```
For example, a browser failure no longer forces successful backend/build phases
to run again if their inputs still match. Standalone `--phase browser` is available
for diagnostics, but is not a substitute for the complete gate or its receipt.
Scopes are whole repositories, not inferred file globs. A trusted check may declare
`"inputs": {"repos": ["canonical-repository-name"]}`; no declaration means the whole
registered workspace. Dry runs show that boundary. Native source-discovery checks
retain broad scopes where cross-module dependencies cannot safely be narrowed.
Changes outside an explicit scope do not invalidate it, but tool/environment
changes remain conservatively global. Scope declarations are a correctness
contract: include every source repository that the check reads, not only its cwd.
Native discovery ownership/presence is rechecked at phase boundaries, so a newly
appearing module or WebUI directory invalidates the in-flight environment identity.
Unregistered native sibling sources disable reuse with an explicit coverage note;
register them before relying on cached results. Ordinary generated files inside
existing source directories do not change this directory-shape identity.
Every snapshot rereads repository presence, HEAD, index/flags and tracked plus
non-ignored untracked membership, including assume-unchanged files. File hashes
use a bounded **in-process-only** memo with file identity/ctime/mtime/size checks
before and after an open file descriptor; no persistent mtime cache is trusted.
In-repository regular-file symlink targets are included; escaping or directory
symlinks fail closed. Ignored files, undeclared external data and service state
are not source evidence and need explicit verification.
Environment probes stream bounded regular-file contents and reject concurrent
replacement, growth and symlink retargeting. Stable venv executable and dependency
directory symlinks remain supported without changing the executable path; absent
optional metadata remains optional. Environment identities are not persistently
cached and remain deliberately global.
These checkpoints cache **verification results, not output artifacts**. The full
gate's phases were audited so later phases do not require an earlier phase's
retained build output. A custom setup/build that produces files consumed later
must declare `"reuse": "never"`; its consumers must use `deps`. Until output
manifests/restoration exist, do not assume a cached successful build recreates
deleted ignored artifacts.
Check output is drained without unbounded memory growth. Stored output retains
the beginning and actual final tail within an 8 MiB bound per stage, with a small
truncation marker and an omitted-byte count; output truncation does not change
the subprocess exit result. `logs` exposes at most 200 lines and 16 KiB. Live
snapshots retain at most 64 KiB after redaction. Incomplete live lines and cut
retention-boundary lines are withheld to avoid exposing fragments of secrets;
very long single-line output may therefore be absent from live views.
Known environment credentials and common authorization patterns are
redacted as display hygiene. **Never pass credentials as check arguments or print
them from tests.** Redaction is not a general secret-classification guarantee.
JSON command output is a redacted presentation; the receipt file at the reported
path is the canonical local record. Cancellation and deadlines terminate the
owned process group; a parent leaving running descendants does not pass.
During source/environment fingerprinting, cancellation is observed between
probes; the current bounded probe may finish first. Cancellation during final
verification is recorded as interrupted, never as a passing run.
Deliberately detached processes are outside that group: this is not a sandbox.
These local hash-bound records detect accidental changes; they are not signed
attestations, a security certification, proof of complete test coverage, or
permission to publish. External service state and undeclared dependencies still
need explicit verification. No automated result closes a Gitea issue or marks a
module reviewed. Detailed logs remain local unless deliberately shared. Old run
directories are not automatically deleted; review retention before removing any
evidence referenced by an issue.
## Reuse in another project
The runner, repository context, doctor and evidence primitives are usable with an
explicit local JSON project manifest. The example at
`tools/devkit/examples/project.json` registers ordinary Python tests and a Git
whitespace check; the format is described by
`tools/devkit/project.schema.json`.
```sh
/path/to/govoplan/devkit --workspace-root /path/to/project \
--project /path/to/project/devkit-project.json check --profile quick --dry-run
```
Repository paths and check working directories must remain inside the selected
workspace. Check arguments are arrays, not shell-evaluated strings. The supported
tool placeholders are `{python}`, `{node}`, `{npm}` and `{workspace}`. Check
dependencies form an acyclic graph; resources serialize incompatible tasks.
`deps` declares actual data dependencies: consumers rerun whenever a dependency
runs again. `after` declares only fail-fast execution ordering: a failed predecessor
skips the follower, but a successfully rerun independent predecessor does not
invalidate the follower's inputs. Both edge types are validated together for
cycles, missing IDs and overlap. `reuse` is `verified` by default or `never` for
setup/output-producing checks. Repository `inputs` names are canonical, unique,
nonempty and validated even for unselected checks.
The complete custom manifest is validated, including unselected checks/profiles:
unknown fields, misspellings, invalid bounds/types and unresolved dependencies
fail early. This prevents silently ignored resource or timeout declarations.
Portable `doctor` always requires its Python runtime, plus tools needed by the
selected checks and their dependencies and tools explicitly configured in the
manifest. Without `--profile`, all declared profiles are considered. Unused
Node/npm are marked `not_required` and do not block Python-only projects. Declare
indirect tool dependencies explicitly: arbitrary script contents are not
analyzed. Native GovOPlaN retains its Python/Node/npm requirements.
Only use project manifests and scripts you trust: running a registered test is
ordinary code execution, not a sandbox. Platform-specific release and Docs
commands reject generic project manifests; put another project's checks in its
own profiles rather than pretending its release policy is GovOPlaN's.
The initial runtime targets POSIX environments with Python 3.11+ and OS advisory
locks; it does not claim Windows support. No globally installed service or Codex
plugin is needed. Keep this implementation versioned and reuse it; avoid copying
diverging helper implementations into each project.
Devkit is not a complete build system: it does not infer semantic dependencies,
restore output artifacts, sandbox checks, or share signed remote caches. Repository
scopes intentionally stop short of file/glob narrowing; environment identities
remain global. These are explicit boundaries, not claims of exhaustive coverage.
## Development and conformance
```sh
./.venv/bin/python -m pytest -q tests/test_devkit_*.py
./.venv/bin/python -m pytest -q tests/test_focused_phases.py
node --test tests/test-devkit-display-labels.mjs
./.venv/bin/python -m unittest tests.test_documentation_structure
```
The focused gate includes the Python command-suite regression tests. Fixture
tests use temporary repositories/processes and mocked issue/release transports;
they do not commit user work, send mail, publish artifacts or change live issues.
Documentation is part of each command change: update `--help`, this guide and the
corresponding targeted contract tests together.
+72
View File
@@ -0,0 +1,72 @@
# Check configuration and suite coverage
`devkit coverage` explains what a selected plan intends to run. It does not run
tests, start servers, or report successful verification:
```sh
./devkit coverage --profile quick --repo campaign
./devkit coverage --profile ui --repo portal --json
./devkit coverage --profile full --json
./devkit --project /path/to/project.json coverage --profile quick
```
The inventory accounts for declared `test` and `test:*` scripts in each
registered repository's root and `webui/package.json`, discovered UI structural
checks, and explicitly configured project checks. It is a suite inventory, not
an enumeration of every test function or recursive dependency. Unselected suites
remain visible; every row has a disposition and reason:
| Disposition | Meaning |
| --- | --- |
| `planned` | A selected stage directly invokes the suite. |
| `covered_elsewhere` | An exact equivalent invocation or an explicitly selected shared batch owns it; the covering stage is named. |
| `excluded` | The suite is outside the actual selected plan. |
| `unsupported` | Scoped discovery cannot safely interpret or locate the command; no command is guessed or executed. |
`full` means the existing canonical `check-focused.sh` gate. It does **not**
mean every package script or every component suite. Coverage reads the current
script's explicit npm/Node commands inside its seven registered marked phase
bodies, without executing shell code or inferring arbitrary functions, nested
scripts, npm hooks, branches or here-document contents. Each covered suite names
the `focused.<phase>` that owns it. The
current gate names four of the sixteen Core component suites: layout primitives,
page layout, DataGrid actions/sizing, and Mail components. The other twelve stay
explicitly excluded from that gate's component batch. `ui` runs the shared
sixteen-suite batch once; `quick` does not compile component tests.
Only Core's exact known component aliases receive that shared-batch treatment.
A different repository using the same filename, an unknown alias, or a compound
command containing the runner is not silently credited or launched. Ordinary
scoped source discovery accepts bounded direct `node`/`node --test` commands
targeting regular package-owned `.mjs` files in `scripts` or `tests`; shell
chains, extra flags, missing targets and symlinked targets are not rewritten.
An explicit invocation already present in the canonical full gate remains part
of that gate, even when the narrower discovery profile does not support it.
Coverage rows retain a command hash and, for supported commands, redacted argv.
Unsupported shell bodies are not copied into the inventory. This is display
hygiene, not permission to put secrets into project commands. Coverage is
attached to check plans/receipts; it never turns an excluded suite into a pass.
## Portable-project validation
The runtime validates custom project metadata against the published
[`project.schema.json`](../../tools/devkit/project.schema.json) using a small
dependency-free validator. All checks and profiles are validated before
selection, including checks the chosen profile does not execute. Unknown nested
fields such as `resource` or `timout_seconds`, malformed types, oversized values,
duplicate references, missing dependencies, unknown profiles and dependency
cycles fail with controlled errors. Repository aliases and resolved paths must
be unambiguous and confined to the workspace. Check `cwd` defaults to `.` when
omitted; configured paths cannot escape the workspace.
Package JSON is bounded to 1 MiB and 512 scripts; duplicate JSON keys, excessive
nesting and malformed script objects fail before planning. Command strings are
bounded to 8,192 characters. This validates configuration, not the safety of its
code: run only project manifests and test scripts you trust.
Checks may declare repository-scoped `inputs`, true data `deps`, order-only
`after`, and `reuse: "never"` for setup/output-producing work. Unknown repositories,
empty/duplicate scopes, overlapping edge types and cycles fail before execution.
Missing inputs deliberately fall back to all registered repositories. See
[checkpoint and input contracts](DEVKIT.md#canonical-phases-and-narrower-inputs).
+171
View File
@@ -0,0 +1,171 @@
# Devkit review bundles and issue evidence
Gitea remains the canonical backlog and review state log. These commands assemble
guidance and append evidence; they do not create a second progress tracker, close
issues, rewrite issue bodies, tick checkboxes, or certify a module review.
## Prepare a module review
```sh
./devkit review campaign
./devkit review campaigns --json --output /tmp/campaign-review.json
./devkit review campaign --evidence RUN_ID
```
Repository names, repository aliases and scope IDs from the
[issue discovery inventory](../project/ui-review-issue-inventory.json) are accepted.
`review bundle campaign` is an equivalent spelling. A bundle contains the current
source inventory and fingerprint, canonical module/central issue links, the
Core principle revision and content hash, applicable **planned** check stages,
and a manual checklist. No checks or application servers are started. The
optional output file is a local snapshot, not an authoritative state record.
The filename inventory is a starting point, not proof that all runtime surfaces
were found. It does not import application manifests or optional modules. Review
routes, panes, dialogs, settings, widgets, public forms, contributed/headless
interfaces, missing-module behavior, permissions, English/German and narrow/wide
layouts manually. Missing source, no UI files or no planned checks is never an
automatic pass or N/A. Record exceptions and remaining work in the module issue.
Apply [UI-01 and UI-02, and the other Core principles](../../../govoplan-core/docs/UI_DESIGN_PRINCIPLES.md):
books belong beside meaningful visible text; normal screens display compact
data, with intentional scoped dialog editing. A bulk editing mode needs a
documented reason. Record new principles and their back-propagation to previously
reviewed modules through the [central review program](../project/UI_REVIEW_PROGRAM.md).
An explicit portable `--project project.json` does not inherit GovOPlaN issue
links or principles. Its optional configuration is:
```json
{
"review": {
"issue_inventory": "meta/docs/review-issues.json",
"principles": "core/docs/UI_DESIGN_PRINCIPLES.md"
}
}
```
Paths are relative to `--workspace-root` and cannot escape it or traverse
symlinks. The inventory uses schema version 1, `issues[]` with `scope_id`,
`repository`, `number`, `url`, and optional `name`/`kind`; an optional `epic`
contains `repository`, `number`, `url`. Snapshot issue-state fields are not
reported as current state. Custom checks come from the project's selected
profile (`--profile ui` by default).
## Preview and append evidence
```sh
./devkit issues note --root govoplan-campaign --issue 103 \
--evidence RUN_ID --summary 'Targeted checks passed.' \
--next 'Manual module review and remaining fixes are still required.' --json
./devkit issues note --root govoplan-campaign --issue 103 \
--evidence RUN_ID --summary 'Targeted checks passed.' \
--next 'Manual module review and remaining fixes are still required.' \
--env-file /private/gitea.env --apply
```
`--root` is resolved relative to the selected **workspace root**, not the shell's
current directory. In the normal siblings workspace, use `--root govoplan-campaign`
or an absolute checkout path; do not use a path escaping the workspace.
The default is a completely offline dry run; it does not load credentials or
contact Gitea. `--apply` explicitly authorizes comment creation. Tokens are read
from `GITEA_TOKEN` or the explicit `--env-file` (environment wins); there is no
implicit `.env` search. The credential file is parsed, never sourced, and is not
printed or uploaded. Git's selected remote (default `origin`) determines the
exact target: ambient `GITEA_OWNER`, `GITEA_REPO` and `GITEA_URL` cannot retarget
the request. Credential-bearing HTTP Git remote URLs are rejected.
Use repeatable `--summary`/`--next`, `--body-file` for Markdown, or `--note-file`
with structured JSON `{ "summary": ["..."], "next": ["..."], "body": "..." }`.
Do not put secrets in notes, paths, check arguments or logs. Display redaction is
best-effort protection, not permission to include confidential data.
For a bounded multi-issue operation, replace `--root`/`--issue` with
`--target-plan targets.json`:
```json
{
"schema_version": 1,
"targets": [
{
"root": "govoplan-campaign",
"issue": 103,
"url": "https://git.add-ideas.de/GovOPlaN/govoplan-campaign/issues/103"
}
]
}
```
Targets must be unique, inside the workspace and bound exactly to their Git
remote/issue URLs, all on one exact Gitea base URL. Optional `issue_id` binds
Gitea's immutable numeric ID as well. Every target, complete comment history,
marker collision and local journal binding is checked before the first POST.
Comments are then posted serially and read back against the exact issue. A
closed issue stays closed; existing bodies, comments and checklists are untouched.
Each note has a stable hidden marker binding its issue, `--key` purpose
(`verification` by default), and evidence run ID. Identical already-published
notes are read back instead of reposted; changed content with the same marker
is a collision, not an update. Use a distinct intentional `--key` for a separate
follow-up. Without a receipt, the note's structured content determines identity.
A lost POST response is **not** retried automatically. The publisher scans all
comment pages and reads back any matching comment. If it cannot confirm the
result, it records `uncertain`, stops later posts, and exits nonzero. The next
invocation reconciles again without replaying the POST. Only after inspecting
the issue and local result, use `--apply --retry-uncertain` to permit one new
attempt when the marker is still absent. Local journals and OS-released locks
live in the workspace-scoped devkit state directory; do not delete them to bypass
uncertainty. These are retry receipts, not issue state. Independent machines or
different state directories cannot share a lock: coordinate publishers, because
Gitea comment creation does not provide an atomic idempotency key.
## What a receipt does and does not prove
`--evidence RUN_ID` reads the workspace-scoped runner receipt and checks local
integrity. `--evidence /path/to/receipt.json` accepts bounded validated external
receipt metadata, explicitly labeled `external-unverified`. A self-consistent
digest is not a signature or independent verification. Foreign-workspace,
malformed and inconsistent successful receipts are rejected. Receipt-provided
commands are never run; log files are not opened or uploaded, only referenced.
Successful receipts must explicitly record a verified source snapshot; an
external file's flag is still a reported claim, not an independent attestation.
An aggregate containing a skipped stage cannot claim success. New versioned
receipts additionally require independent verified checkpoints for passed phases.
Failed/interrupted/stale aggregates remain labeled as such even when they contain
reusable successful phases; reuse creates a new run and preserves the original.
Stage coverage limitations are validated and retained in the note, including
checks omitted by scoped profiles. Compact review output shows the first eight
limitations and points to the complete list in its JSON bundle. A passing stage
never implies that omitted compiler, browser, permutation or manual checks ran.
Documentation audit limitations are saved with their stage receipts, so later
review bundles and issue notes retain the same scope as the original command.
Managed inventory collectors use the explicit selected workspace throughout;
an incomplete checkout never borrows sources or missing module imports from a
fuller default workspace. Missing required Core/parser dependencies fail the
audit. Legacy direct inventory commands without `--workspace-root` retain their
discovery behavior; use the explicit option for reproducible evidence.
The note distinguishes `matches-current`, `historical-source-differs`,
`different-project-not-compared` and unavailable comparisons. Matching source
is a comparison at bundle/note preparation time, not a live test run or proof
that a person completed review. If the source or environment has changed, run
the appropriate checks again and keep historical evidence labeled honestly.
Versioned repository-input receipts compare the recorded repository scope, not a
legacy whole-workspace hash. Partial scopes remain explicitly labeled in evidence
notes. This comparison reads source only; receipt commands are never executed.
Legacy receipts retain their original comparison format. A matching source scope
does not prove that ignored build outputs, the environment or live services match.
Fixtures cover dry-run isolation, exact target bindings, capped/repeated
pagination, collisions, append-only idempotency, uncertain POST reconciliation,
secret-safe errors, receipt validation, portable projects and optional-module
review boundaries:
```sh
python3 -m pytest -q tests/test_devkit_issues.py tests/test_devkit_review.py
```
+97
View File
@@ -0,0 +1,97 @@
# Selected-path Git maintenance
The optional `git` namespace of `tools/devkit/devkit.py` is a deliberately
narrow maintenance workflow. It is not a replacement for normal Git, the
release executor, required human review, or Gitea's canonical issue state.
From the Meta repository, preview and then save a plan for explicit files:
```sh
python3 tools/devkit/devkit.py git plan --repo core --path webui/src/example.ts --message "Fix the reviewed example"
python3 tools/devkit/devkit.py git plan --repo core --path webui/src/example.ts --message "Fix the reviewed example" --apply
```
Use the saved `git-…` plan ID for the remaining commands. Each mutation needs
its own `--apply`; an unqualified command only previews the operation:
```sh
python3 tools/devkit/devkit.py git commit git-PLAN_ID
python3 tools/devkit/devkit.py git commit git-PLAN_ID --apply
python3 tools/devkit/devkit.py git push git-PLAN_ID
python3 tools/devkit/devkit.py git push git-PLAN_ID --apply
python3 tools/devkit/devkit.py git status git-PLAN_ID
```
The immutable local plan binds the registered repository, HEAD, branch, origin
fetch/push URL hashes, Git configuration, complete index identity, explicit
selected file paths, working-tree hashes, expected Git blobs, and commit message.
Raw remote URLs and file contents are not stored. A changed input blocks the
operation; inspect it and create another plan instead of weakening the check.
Receipts are private local records with integrity checks, not signed approval
or a security attestation.
Commit captures each selected file once, verifies the captured bytes and Git
blob against the plan, and builds an isolated index/tree from the frozen HEAD
plus only those blobs. Git's normal identity rules create the commit object;
an atomic branch compare-and-swap publishes it only if the planned parent is
still current. Active hooks/signing remain refused rather than bypassed.
An editor racing with this operation cannot substitute newer working-tree
bytes: those edits remain uncommitted, and the helper never overwrites files.
The real index is protected by Git's standard index lock. Only selected entries
are updated from a prepared private copy; unrelated staging and index flags are
preserved. If the index changes independently, its new data is not overwritten
and the receipt requires reconciliation. The resulting parent, message,
changed paths, blobs/modes and unrelated index entries are verified.
Git refs and index files are separately atomic, not one filesystem transaction.
Independent writers that ignore the standard index lock can still change refs
during publication. The helper rechecks the exact recorded candidate before
index recovery and before recording success; it refuses an uncertain result
instead of adopting a newer HEAD or claiming that competing work was its own.
Directories, noncanonical paths, implicit globs, staging-all, amend,
force-push and extra-tag publication are not supported. A selected file with
different staged and working-tree changes is refused; decide explicitly which
version to commit using normal Git first.
Push requires the recorded commit to remain current, an unchanged origin and
one standard-transport push destination. It requests a normal, non-forced push
of that commit to the recorded branch and checks the remote branch afterward.
The push preview does not contact a remote. A normal push may run the remote's
usual CI or server-side hooks; this is an external effect of the separately
authorized push, not of planning or committing.
Local active hooks (including index-change hooks), filter/encoding attributes,
signing, external fsmonitor/SSH configuration, Git environment overrides,
in-progress merge/rebase/cherry-pick operations, submodules, symlinks and
assume-unchanged/skip-worktree entries, split indexes and replacement/graft
history are deliberately
unsupported. The helper
refuses these cases instead of disabling hooks, signatures or filters. An
unused globally installed filter definition alone does not block maintenance;
active attributes are checked across tracked files as well as selected files.
Custom remote receive-pack/upload-pack/helper commands, recursive submodule
pushes and partial-clone lazy fetch are also refused. Of inherited `GIT_*`
variables, only `GIT_OPTIONAL_LOCKS`, `GIT_TERMINAL_PROMPT` and `GIT_PAGER` are
allowed (every command explicitly passes `--no-pager`, so the pager is inert);
namespace, identity, alternate-index/object-directory and unknown overrides
are not silently removed. Git subprocesses have bounded input/output and
deadlines; cancellation terminates their owned process group, including
transport helpers. Network failures can still leave a remote effect uncertain,
which is why receipts require explicit reconciliation rather than blind retry.
An interruption or uncertain failure is recorded before any retry. Use
`git reconcile git-PLAN_ID` to preview, then add `--apply` to verify an
already-existing result. Reconciliation never makes a commit or push. It may
finish the selected real-index update after an interrupted commit publication,
but only when the original index fingerprint still matches; independent staged
work is preserved and requires normal Git resolution. For an
uncertain push it may read the frozen remote, but only with `--apply`. If no
planned commit is current, the helper reports that state and requires a fresh
plan after inspection. A forcibly killed process may leave its private scratch
index or an owned Git index lock; the helper never guesses that an existing
lock is safe to delete. Blob/commit preparation can leave unreferenced Git
objects for ordinary Git garbage collection. It never automatically resets,
restores, deletes, or rolls back user work.
The maintenance tests use disposable local repositories and local bare remotes.
They never commit or push the user's workspace repositories.
+121
View File
@@ -0,0 +1,121 @@
# Headless durable releases
The devkit `release` commands use the **same release application, request
validation, run store and bounded executors** as the
[release console](RELEASE_CONSOLE.md). They invoke its ASGI application inside
the current process, with an ephemeral internal token. No listening socket,
background server, browser session or externally supplied console URL is used.
The development environment's FastAPI/HTTPX dependencies load only when a
release command runs; ordinary devkit help does not import them.
This is a GovOPlaN-specific provider. Repository and origin authority still comes
from the release service's registered catalog. `--project` is explicitly rejected
for release commands; a portable project configuration must not silently override
that authority. A different workspace path does not authorize arbitrary release
repositories, remotes, commands, signing policies or source bindings.
The default compact output includes the overall status, selected repository
states and versions, source-preflight readiness, bounded gate findings and the
recommended next action. Run inspection also shows step-state counts and the
first outstanding steps. These are projections of the service response, not new
readiness checks. Use `--json` for the complete existing plan or receipt; compact
output deliberately omits executor commands, arguments and source bindings.
## Commands and boundaries
| Command | Purpose | Effect boundary |
| --- | --- | --- |
| `release status` | Current release dashboard | Offline by default; explicit remote/catalog flags enable their checks |
| `release plan` | Selective repository/version plan | Inspection only; does not create a run |
| `release list` | Bounded run history, with `--limit` and `--cursor` | Existing workspace-scoped store |
| `release show RUN_ID` | Exact verified run, steps, receipts and required confirmations | No release execution |
| `release create` | Freeze a selected repository/version plan | Preview by default; `--apply` persists a run |
| `release preview RUN_ID STEP_ID` | Frozen step and its current constraints | No step claim; catalog publication additionally uses the existing validated-candidate preview |
| `release execute RUN_ID STEP_ID` | One available durable step | Preview by default; `--apply`, explicit request ID and the step's exact confirmation are required |
| `release resume RUN_ID` | Mark persisted running attempts interrupted | Requires `--apply`; never assumes an effect succeeded |
| `release retry RUN_ID STEP_ID` | Prepare an eligible failed/read-only-interrupted step | Requires `--apply`; does not execute it |
| `release reconcile RUN_ID STEP_ID` | Record a proven uncertain-write outcome | Requires `--apply`, `--confirm RECONCILE` and an explicit outcome |
Create and transition commands require caller-chosen `--request-id` values.
Retain the same ID for an uncertain replay of the **same** command; changing
inputs under an existing ID fails closed. No command automatically retries a
mutation. These commands perform one explicit transition, not an implicit
"release everything" loop.
Read commands do not commit, tag, push, publish or apply database migrations.
The existing private run store can initialize its lock directory or upgrade a
legacy record while inspecting it; this does not advance release steps.
`--include-migrations` requests the existing audits, never migration application.
`--online`, `--remote-tags` and `--public-catalog` are explicit network-check
choices. A generic step preview describes frozen intent; it is not a claim that
live preflight, remote identity or release acceptance has passed.
## Example: review, freeze and execute one step
Run the devkit through the workspace `./devkit` entrypoint. These examples put
global options before `release`:
```sh
./devkit --workspace-root /path/to/workspace --format json release plan \
--repo-version govoplan-files=0.1.9
./devkit --workspace-root /path/to/workspace release create \
--repo-version govoplan-files=0.1.9 --request-id files-release-create-0001
./devkit --workspace-root /path/to/workspace release create \
--repo-version govoplan-files=0.1.9 --request-id files-release-create-0001 --apply
./devkit --workspace-root /path/to/workspace release show RUN_ID
./devkit --workspace-root /path/to/workspace release preview RUN_ID STEP_ID
./devkit --workspace-root /path/to/workspace release execute RUN_ID STEP_ID \
--request-id files-release-step-0001 --confirm REQUIRED_CONFIRMATION --apply
```
`RUN_ID`, `STEP_ID` and `REQUIRED_CONFIRMATION` stand for the actual values
returned by the frozen run. For read-only preflight/alignment/install-verification
steps, omit `--confirm` when the service reports an empty confirmation. Metadata,
commit, release-lock, source-tag, source-push, candidate-generation and catalog
publication steps keep the existing `UPDATE`, `COMMIT`, `LOCK`, `TAG`, `PUBLISH`,
`GENERATE` and `PUSH` confirmations respectively. Prerequisite ordering is enforced
by the same store; a later step cannot be forced through this adapter.
Version selection supports repeated `--repo-version REPO=VERSION`, or repeated
`--repo REPO` with `--target-version VERSION`. Every created run needs explicit
versions for all selected repositories. Conflicting version assignments are
rejected. Planning is selective; repositories are never silently selected merely
because their worktrees are dirty.
## Candidate publication and recovery
Use the frozen run's `catalog:selective-generator` and
`catalog:validate-sign-publish` steps, not the disabled legacy mutation endpoints.
Generation accepts repeated `--signing-key KEY_ID=PRIVATE_KEY_FILE`; these are
operator-owned key-file references, never inline key material. Signing arguments
are not emitted in devkit JSON or persisted by the adapter. Publication consumes
the candidate receipt created by that run, not an arbitrary candidate directory.
The existing source/runtime trust, immutable tag/remote identity, private
candidate checks and exact commit-delta checks remain authoritative.
After an interrupted write, inspect the run and the actual local/remote effect.
Use `resume --apply` if an attempt was left running. Then use `reconcile` with
`effect_absent`, `effect_succeeded` or `unresolved`, `--confirm RECONCILE`, a new
reconciliation request ID and `--apply`. Successful reconciliation still performs
the service's independent receipt checks. An unresolved effect must not be
retried or papered over by generating a new request ID. A failed/read-only
attempt may use `retry --apply` and a new explicit execute attempt only when
the existing lifecycle makes that safe.
## State and limitations
Every successful response exposes `state_location` and `candidate_location`.
Without `--state-dir`, the adapter uses the console's existing default private,
workspace-fingerprinted state directory. With `--state-dir PATH`, it uses
`PATH/release-console/workspace-<fingerprint>/release-runs`. Workspaces cannot
read or resume one another's runs. Existing ownership, symlink, permission,
retention and record-integrity checks are unchanged.
Release preparation can commit only the recognized, receipt-bound metadata
changes its executors produced. This command does **not** stage arbitrary source
changes, use `git add -A`, force-push, retarget tags or enable the disabled generic
prepare/sync/push endpoints. Normal selected-path maintenance requires its own
explicit reviewed-change workflow; it is not silently folded into release.
+138
View File
@@ -0,0 +1,138 @@
# Full registry candidates / Vollständige Registry-Kandidaten
## Operator workflow (EN)
The canonical `release-catalog.py full-registry` command takes `--package-set`,
`--package-lock`, `--wheelhouse`, `--webui-packages`, `--output-dir`, and a
configured `--catalog-signing-key`. Generate the package set with
`generate-release-package-set.py --profile full` and download its exact artifacts
with `resolve-package-artifacts.py`; do not substitute locally rebuilt wheels.
The candidate compares the package set with the exact developer meta-package
pins, checks archive bytes and package metadata against the lock, and synthesizes
every entry from its immutable tagged manifest. Native package publication and
its CI authority remain trusted: this verifies the published artifact identity,
not independent reproducible-build equivalence to source.
Pass `--selected-repository` once for each newly released repository, including
Core when it changes. These selected units must have clean, version-aligned
named branches whose HEAD equals the annotated local and remote release tag.
Other full-profile packages retain their exact older annotated tags; a later
workflow-only commit on `main` does not relabel those package contents or force
a version bump. Every source fetch/push endpoint must match the registered
origin, and all entries bind their source commit and annotated tag object.
Git replacement objects, caller Git configuration, and executable-path
redirection cannot substitute another tagged manifest tree.
The fixed existing website catalog and keyring are authenticated before signing.
An older catalog without a signed keyring hash can be migrated only through this
complete rebuild, only when its signature verifies and its entire keyring
exactly matches the configured known signers. No old entries or artifact hashes
are reused. The new catalog signs the exact unchanged website keyring hash;
key rotation remains a separate reviewed operation. Selective candidates still
reject unpinned base keyrings. New candidate directories are private and
exclusive: a retry must choose a new directory, not overwrite a reviewed one.
Run these commands on the trusted host, with an operator-private source workspace
and artifact directory. `RELEASE_PYTHON` must select its private environment and
`RELEASE_NPM` an absolute npm executable with a trusted sibling Node 22 binary.
In Flatpak, execute host commands through `flatpak-spawn --host`; sandbox and
host UID mappings are not interchangeable. Do not weaken trust gates or change
system-wide permissions.
```sh
# These paths identify previously prepared private operator resources.
RELEASE_WORKSPACE=/path/to/private/workspace
RELEASE_PYTHON="$RELEASE_WORKSPACE/govoplan/.host-venv/bin/python"
RELEASE_NPM=/path/to/private/node22/bin/npm
ARTIFACT_ROOT=/path/to/private/artifacts
RELEASE_CANDIDATE=/path/to/private/new-candidate
RELEASE_VERSION=0.1.45
RELEASE_TOOLS="$RELEASE_WORKSPACE/govoplan/tools/release"
umask 077
"$RELEASE_PYTHON" "$RELEASE_TOOLS/generate-release-package-set.py" \
--version "$RELEASE_VERSION" --profile full \
--workspace "$RELEASE_WORKSPACE" --output "$ARTIFACT_ROOT/packages.json"
PATH="$(dirname "$RELEASE_NPM"):/usr/bin:/bin" \
"$RELEASE_PYTHON" "$RELEASE_TOOLS/resolve-package-artifacts.py" \
--package-set "$ARTIFACT_ROOT/packages.json" \
--wheelhouse "$ARTIFACT_ROOT/wheels" \
--webui-packages "$ARTIFACT_ROOT/webui" \
--lock-output "$ARTIFACT_ROOT/artifacts.lock.json" \
--python "$RELEASE_PYTHON" --npm "$RELEASE_NPM"
# Repeat --selected-repository for EVERY newly released unit, not just Core.
"$RELEASE_PYTHON" "$RELEASE_TOOLS/release-catalog.py" full-registry \
--workspace-root "$RELEASE_WORKSPACE" \
--package-set "$ARTIFACT_ROOT/packages.json" \
--package-lock "$ARTIFACT_ROOT/artifacts.lock.json" \
--wheelhouse "$ARTIFACT_ROOT/wheels" --webui-packages "$ARTIFACT_ROOT/webui" \
--output-dir "$RELEASE_CANDIDATE" --selected-repository govoplan-core \
--catalog-signing-key known-key=/path/to/private/known-key.pem --json
"$RELEASE_PYTHON" "$RELEASE_TOOLS/release-catalog.py" publish-candidate \
--workspace-root "$RELEASE_WORKSPACE" --candidate-dir "$RELEASE_CANDIDATE" \
--channel stable --npm "$RELEASE_NPM" --build-web \
--commit --tag --push --tag-name "catalog-v$RELEASE_VERSION" --json
```
The last command is a strict non-mutating preview because `--apply` is absent.
Review its output, then repeat it with `--apply` to publish. The website's locked
build dependencies must already be installed before `--build-web`. The publisher
sanitizes the build and Git environments and pushes the verified immutable
website commit/tag. Source tag publication and registry package availability
must be complete before candidate generation.
Source tags, registry packages, and a signed module catalog do not imply that a
new runtime distribution exists. While runtime images are held, leave the Meta
Gitea runtime Release held too: a normal source-only Release can replace Gitea's
`releases/latest` discovery result despite having no deployment assets. The
deployer still requires an explicit signed manifest, digest, and trusted
keyring; it does not deploy a tag or module catalog directly.
## Betriebsablauf (DE)
`release-catalog.py full-registry` übernimmt den vollständigen Paketbestand,
die Registry-Sperrdatei, das Wheel-Verzeichnis, die WebUI-Archive und den
konfigurierten Signaturschlüssel. Zuerst mit
`generate-release-package-set.py --profile full` die exakten Meta-Paketversionen
ermitteln und mit `resolve-package-artifacts.py` die veröffentlichten Artefakte
herunterladen. Lokal neu gebaute Wheels sind kein Ersatz. Der Kandidat prüft
Paketidentitäten, Dateigrößen und Hashes und erzeugt alle Einträge aus den
unveränderlichen getaggten Manifesten. Die Registry und ihre veröffentlichende
CI bleiben eine Vertrauensgrundlage; dies ist kein unabhängiger Nachweis eines
reproduzierbaren Builds aus dem Quellcode.
Jedes neu veröffentlichte Repository wird mit `--selected-repository`
angegeben. Nur diese Auswahl muss mit dem sauberen, versionsgleichen HEAD eines
benannten Branches und dem annotierten lokalen und entfernten Tag übereinstimmen.
Unveränderte Pakete behalten ihren ursprünglichen Tag, auch wenn auf `main`
bereits eine spätere Workflow-Korrektur liegt. Alle Quelladressen müssen dem
registrierten Ursprung entsprechen; Commit und annotiertes Tag-Objekt werden
für jeden Eintrag gebunden. Git-Ersetzungsobjekte oder fremde Git-Konfiguration
können dabei keinen anderen Manifestbaum unterschieben.
Vor dem Signieren werden der bestehende Website-Katalog und sein Schlüsselbund
geprüft. Ein alter Katalog ohne signierten Schlüsselbund-Hash darf ausschließlich
durch diesen vollständigen Neuaufbau migriert werden: Seine Signatur muss gültig
sein und der gesamte Schlüsselbund exakt den konfigurierten bekannten Signierern
entsprechen. Alte Einträge oder Artefakt-Hashes werden nicht übernommen. Der neue
Katalog bindet den unveränderten Schlüsselbund-Hash; ein Schlüsselwechsel bleibt
ein eigener geprüfter Vorgang. Selektive Kandidaten verlangen weiterhin einen
bereits gebundenen Schlüsselbund. Kandidaten werden nur in neuen privaten
Verzeichnissen erzeugt und niemals überschrieben.
Das obige Befehlsbeispiel wird auf dem vertrauenswürdigen Host ausgeführt. Dafür
die private Python-Umgebung und einen absoluten `--npm`-Pfad zu Node 22 verwenden;
unter Flatpak die Host-Werkzeuge über `flatpak-spawn --host` aufrufen. Die
gesperrten Website-Build-Abhängigkeiten vorher installieren. Keine
Vertrauensprüfung umgehen und keine globalen Rechte ändern. Die Artefaktordner
müssen privat und bei der Auflösung leer sein. Vor der Kandidatenerzeugung
müssen Quell-Tags und Registry-Pakete vollständig veröffentlicht sein.
Die Veröffentlichung zunächst mit `publish-candidate --commit --tag --push
--build-web` ohne `--apply` prüfen und erst nach Prüfung mit `--apply` ausführen.
Solange Laufzeit-Images zurückgestellt sind, bleibt auch das Meta-Gitea-Runtime-
Release zurückgestellt: Ein reines Quellcode-Release könnte sonst als neuestes
Release erscheinen. Eine Installation benötigt weiterhin ein signiertes
Laufzeitmanifest, dessen Digest und einen explizit vertrauenswürdigen Schlüsselbund.
@@ -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.
@@ -335,6 +361,15 @@ and WebUI API proxy traffic across API replicas. The WebUI and API services do
not publish host ports. HAProxy has no Docker socket and discovers only the
bounded replica slots rendered into `load-balancer.cfg`.
New installation specifications default to HAProxy `3.2.23-alpine`, pinned to
registry index `sha256:6343ce34a132a5dceaa24767d739df2bd519f8f7c1079ae39e4821334e8eb42e`.
This patch remains on HAProxy 3.2 LTS and Alpine 3.24.1. Loading an existing
specification preserves its explicit image; it does not perform an upgrade.
The [runtime remediation evidence](../security/RUNTIME_IMAGE_REMEDIATION_2026-09-08.md)
records both architecture scans and the pending binary/configuration, runtime,
inventory and final-image checks. The source default is not a release approval:
runtime publication remains held in Meta #52.
Replica counts are desired state:
```sh
@@ -404,16 +439,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.
@@ -426,6 +475,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
@@ -0,0 +1,91 @@
# Integrity and performance source release — September 2026
Coordinated tracking: [Core #298](https://git.add-ideas.de/GovOPlaN/govoplan-core/issues/298).
This source publication advances only the affected packages; it is not a signed
catalog publication, runtime-image release, or remote production rollout.
## Package set
| Package | Version |
| --- | --- |
| Core / developer meta-package | 0.1.46 |
| Addresses | 0.1.23 |
| Calendar | 0.1.24 |
| Campaign | 0.1.29 |
| Cases | 0.1.25 |
| Committee | 0.1.22 |
| Connectors | 0.1.27 |
| Dataflow | 0.1.25 |
| Datasources | 0.1.26 |
| Files | 0.1.27 |
| Forms Runtime | 0.1.22 |
| IDM | 0.1.26 |
| Mail | 0.1.28 |
| Reporting | 0.1.22 |
| Tickets | 0.1.23 |
Consumers of new Core helpers require Core 0.1.46 or later. Dataflow and
Datasources also require the matching Core WebUI contract. The release manifests,
immutable Git lock, and developer package describe this coordinated composition.
Unchanged packages retain their independent versions.
## Database and data integrity
The reviewed additive heads are Connectors `d2a4c6e8f0b1`, Datasources
`e2b8d4a0f6c3`, Files `a2b3c4d5e701`, and Mail `b5d6e7f8091a`.
Back up the target deployment and rehearse its normal upgrade before rollout.
Never downgrade away retained CSV originals without a separate recovery plan.
Files preserves historical duplicate copies; Mail leaves legacy maildrop identity
unset rather than guessing an account. A schema upgrade does not authorize POP3
retrieval/reconciliation, provider deletion, or message resending.
Development startup may automatically apply pending migrations after a watched
source change. Therefore, inspect actual Alembic heads and schema before assuming
a live development database is still at its pre-change state. A backup taken
after such an upgrade is a current-state recovery copy, not a pre-upgrade backup.
On 8 September, the local PostgreSQL development database already contained all
four heads. Schema and constraint inspection passed; recomputing the Files
identity backfill checked 7,385 rows with zero mismatches. A private current-state
database backup was created. Per-instance backup paths and row contents are not
published in this repository. The implementation's initial receipt claiming no
live migration occurred was incorrect: watched development-server restarts had
applied the migration files automatically. Restoring the backup into an isolated
PostgreSQL 16 cluster and running the normal upgrade preserved all 281 public
tables, 142,287 rows, and migration heads exactly. Both Datasources PostgreSQL
concurrency regressions passed. The test-only cluster was then stopped.
Release preparation additionally corrected Alembic's ConfigParser handling of
percent-escaped connection URLs, preserving the exact database URL. The static
migration auditor now recognizes the existing reviewed development-wrapper
aliases and peer filenames without executing migration code; historical
migrations are unchanged.
## Verification and boundaries
The implementation passed the required focused workspace gate, including 63
frontend build configurations and 230 browser conformance tests. Owning-module
regressions cover exact import/rollback evidence, authorization-before-pagination,
bounded recurrence and response processing, collision-safe attachment naming,
and stale asynchronous UI completion. English/German feature documentation stays
in each owning module; Core documents shared integrity contracts.
Mock-provider tests and local work-count measurements do not establish production
throughput or provider race behavior. Real S3/SMB/Seafile and POP3 acceptance,
representative load testing, and deployment authentication checks remain separate
operational validation. Source tags trigger package workflows; a pushed source
tag alone does not prove a registry artifact or signed catalog is published.
## Deutsch
Dieses koordinierte Quellrelease veröffentlicht nur die betroffenen Pakete.
Produktions-Images, signierter Modulkatalog und entfernte Produktivinstanzen
werden dadurch nicht ausgerollt. Die vier Migrationen bewahren bestehende Daten;
historische POP3-Zuordnungen werden nicht geraten. POP3 ist ein eigener
Import-Arbeitsablauf innerhalb von **Mail**, kein separat installierbares Modul.
Der Entwicklungsserver kann Migrationen beim automatischen Neustart nach einer
Quelländerung bereits anwenden. Tatsächliche Schema-Stände prüfen; eine danach
erstellte Sicherung enthält den aktuellen Stand und ist keine Sicherung vor dem
Upgrade. Externe Transportaktionen, erneutes Versenden und Löschungen werden
durch die Migration oder Quellveröffentlichung nicht ausgelöst.
@@ -187,7 +187,9 @@ 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.
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`:
@@ -206,7 +208,8 @@ sudo update-ca-certificates
```
Review mappings before adding them to `/etc/hosts`; the lifecycle does not edit
the workstation's trust or resolver configuration.
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
@@ -0,0 +1,465 @@
# 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.
Every module referenced by Core's Git-based `package.release.json` must expose
its WebUI identity at the repository root, including matching peer requirements
and `webui/`-prefixed entry exports (also CSS subpaths). npm resolves Git
dependencies from the repository root, while the native-package workflow packs
`webui/`; success in one path does not verify the other. Run
`python tools/checks/check-webui-package-facades.py` after changing either
manifest or the release composition. The focused gate also runs this check.
Adding or correcting a facade in an already published repository requires a
new patch tag; never repair an existing immutable tag in place.
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
For an operator-reviewed full publication, use
`tools/release/release-catalog.py full-registry` followed by the same tool's
`publish-candidate` command. Resolve the package set and registry lock first;
the older direct-write shell wrapper is not the strict candidate publication
path. See [Full registry candidates / Vollständige Registry-Kandidaten](FULL_REGISTRY_CANDIDATES.md)
for the private host runtime, exact artifact checks, and legacy keyring transition.
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
```
The direct generator is a development synchronization tool, not a receipt-gated
release executor. For release preparation, use the guarded out-of-run stage below.
`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.
### Shared source-tag contract and Meta composition
The shared version collector names Meta's real
`packages/govoplan-meta/pyproject.toml` separately from root `pyproject.toml`.
Only the registered `govoplan` system/meta repository with nested project name
`govoplan` receives this contract. Missing, unknown, or misidentified metadata
does not become a versionless exception. Version alignment compares the complete
nested file with the canonical operator-tool generator output: its version must
match Core, and dependencies and `full` composition must match the reviewed
requirements and workspace package versions. Validation never executes a
generator from a selected checkout. The shared trusted manifest checker can
load reviewed application manifests; these checks are not a code sandbox.
Meta's complete generated file is recognized by shared version-mutation discovery,
but the generic durable version executor deliberately cannot write it. A durable
run freezes the release console's own Meta checkout as trusted runtime code;
changing it in place would invalidate that run. The planner therefore places Meta
after Core and exposes only non-executable support preparation/publication steps,
not misleading automatic Meta version, commit, tag, or push actions. A missing or
different Core target produces an actionable preparation prerequisite.
Prepare Core and the intended module inputs first, commit their reviewed state,
then stop active durable runs for the target workspace. Use trusted operator tools
against a separate registered, private source checkout, never the running operator
Meta directory. Preview outside any selected source checkout, for example:
```sh
python tools/release/prepare-developer-meta-package.py \
--workspace /private/release-workspace --target-version X.Y.Z \
> /private/operator/meta-preview.json
python tools/release/prepare-developer-meta-package.py \
--workspace /private/release-workspace --target-version X.Y.Z \
--receipt /private/operator/meta-preview.json --apply --confirm-out-of-run
```
The explicit confirmation attests that no durable run is active for that target
workspace; the helper does not discover or stop other processes. Preview/apply
requires registered clean main sources, matching origins and live-main ancestry,
Core already aligned at the target, the exact nested identity, and no existing or
unverifiable target Meta tag. Frozen receipts cover source HEADs/filesystem
identities, release requirements, every discovered registered full-composition
pyproject, the trusted generator snapshot, and the resulting full-file hash.
Inputs are limited to 128 selected files, 2 MiB per file and 16 MiB aggregate.
The canonical generator renders copied bounded data in a temporary directory;
no generator from the selected checkout executes. Changed receipts block before
the file effect. Apply writes only `packages/govoplan-meta/pyproject.toml`, then
rechecks the other sources and exact output. A write or post-check failure that
may have changed the file reports `needs-reconciliation` and leaves that bounded
delta for explicit review; it never retries, rolls back, commits or publishes.
Review the complete generated composition and manually commit the resulting file.
Complete matching Core publication before guarded Meta source tagging/publication,
then start a fresh durable run from reviewed, clean, published operator tooling.
Hot self-updating durable Meta release execution remains explicitly unsupported;
this out-of-run preparation is the existing developer-meta support contract.
For every `tag_repositories` batch, strict checks apply to every selected
repository before any tag creation, fetch, or push: registered checkout and
origin/push URL, a clean `main` tracking `origin/main`, live remote-main ancestry,
exact frozen HEADs, and immutable annotated local/remote tag objects. Git metadata
must remain inside the operator's trusted workspace. Missing local knowledge of
live remote main is a blocker; fetch and review it separately. Unknown repositories
and non-registered remote aliases fail closed. If selected, Meta runs last.
For Meta only, the matching annotated Core release must exist before its effect; Core may be
an earlier selected repository, or an already tagged dependency. Publication
requires that Core's exact tag and main commit are already remote.
Non-Meta selections do not acquire Meta's composition or Core-tag prerequisite.
Local module-candidate tags still work before Core's final release lock or tag.
The existing Core WebUI bundle gate still applies to module publication and
batches selecting Core: relevant Core release-package and release-lock inputs
must be operator-owned regular files, at most 16 MiB each, and their identities
and content hashes are frozen before preflight and rechecked before every effect.
When Core is unselected, this does not require its checkout to be clean or tagged;
reviewed pending composition inputs retain their previous meaning. Backend-only
selections never read irrelevant Core WebUI files.
Before even read-only Git commands, source ancestry must be owned by root or the
current operator and must not be group/world writable. A sticky shared ancestor
such as `/tmp` is permitted only above an owned, protected child; the workspace
and checkouts receive no writable-directory exception. The current operator must
own source inputs and actual Git/worktree/common metadata, which must be regular
files/directories, non-symlinked, and non-writable by other users. Metadata walks
are bounded to 500,000 entries and 128 levels, and tracked inputs to 100,000 paths
and 16 MiB of listing text. Read-only Git targets, object alternates/grafts and
hidden/sparse/unmerged index entries are blocked. Frozen receipts include actual
checkout/Git directory paths, devices, inodes, owners and modes, so replacing Git
metadata with the same HEAD is still detected. All selected version/composition
inputs must be tracked, including root and WebUI package/lock metadata, discovered
module manifests and package initializers, and Meta's nested package and release
requirements; ignored working files cannot supply declarations absent from a tag.
No chmod, ownership repair or
global Git trust change is performed. A shared writable workspace must first be
recreated or reviewed in the operator's protected release area by an explicitly
authorized preparation workflow.
Preview is read-only. Local-tag mode creates only pinned annotated tags (or
retrieves an identical published annotation); it does not publish main or tags.
Publish mode atomically pushes the frozen main commit and annotation object,
without force, retagging, fallback, or automatic retry. The complete source
receipt is rechecked before every effect and afterward; remote main and the
exact annotated tag must both match, not merely the Git exit status. Changes
after preflight stop the remaining batch. Atomicity is per repository, not
across repositories: earlier successful publications and a newly created local
tag can remain after a later failure. Inspect reported receipts and obtain a new
review before retrying; do not move immutable tags.
Whole-batch revalidation deliberately repeats source and live-remote checks around
each repository effect; the number of checks can grow quadratically with batch
size. Plan release time accordingly rather than bypassing trust checks. The
shared internal preflight is read-only and exposes no legacy mutation path.
The fixture suite covers Meta and non-Meta preview/local-tag/
publication using temporary local bare remotes, including stale compositions,
unsafe origins, divergent branches, damaged tag identity, changed receipts and
false publication success. This is local tooling evidence, not a real release
publication or production permission check.
Deutsch: Die gemeinsamen Helfer erkennen ausschließlich das registrierte
Meta-Repository mit dem echten Paket `packages/govoplan-meta/pyproject.toml`
(Projektname `govoplan`). Version und vollständige Zusammensetzung müssen dem
kanonischen Generator, Core und den geprüften Anforderungen entsprechen; der
Generator stammt niemals aus dem ausgewählten Checkout. Der gemeinsame
Manifestprüfer kann geprüften Anwendungscode laden und ist keine Sandbox.
Die gemeinsame Änderungsplanung erkennt die vollständig generierte Paketdatei,
aber der dauerhafte Versionsausführer darf Meta nicht selbst verändern: sein
eingefrorener Lauf bindet den Meta-Checkout als vertrauenswürdigen Programmstand.
Meta erscheint deshalb nach Core ausschließlich mit nicht automatisch ausführbaren
Vorbereitungs-/Veröffentlichungsschritten. Zuerst Core und Modulquellen vorbereiten
und geprüft committen; bei abweichender Core-Zielversion nennt der Plan diese
Voraussetzung ausdrücklich. Aktive dauerhafte Läufe des Ziel-Workspaces beenden.
Mit `prepare-developer-meta-package.py` zunächst eine Vorschau außerhalb der
Quell-Checkouts speichern, dann deren JSON über `--receipt` zusammen mit `--apply`
und `--confirm-out-of-run` bestätigen. Das Ziel muss ein separater registrierter
privater Checkout sein, niemals das laufende Operator-Meta. Die Bestätigung ist
eine Betreibererklärung; der Helfer sucht oder beendet keine fremden Prozesse.
Quell-HEADs, Pfadidentitäten, Anforderungen, alle registrierten vollständigen
Paket-Eingaben, der vertrauenswürdige Generator und der vollständige Ausgabehash
werden eingefroren. Es gelten höchstens 128 Quelldateien, 2 MiB je Datei und
16 MiB insgesamt. Core muss bereits vollständig zur Zielversion passen; vorhandene
oder nicht verifizierbare Meta-Zieltags sperren die Vorbereitung. Geänderte
Nachweise stoppen vor dem Schreiben. Ausschließlich die verschachtelte Paketdatei
wird vollständig generiert und danach geprüft; ein Fehler nach dem Schreiben
meldet `needs-reconciliation` und erfordert die manuelle Prüfung dieser begrenzten
Änderung, ohne automatisches Zurücksetzen. Kein automatischer
Commit, Push oder Wiederholungsversuch findet statt. Zusammensetzung prüfen,
manuell committen, Core zuerst veröffentlichen, dann die geschützte Meta-Tag-Route
verwenden und einen neuen dauerhaften Lauf starten. Eine Selbstaktualisierung
des aktiven dauerhaften Meta-Laufs bleibt ausdrücklich nicht unterstützt.
Für jeden Tag-Stapel, auch ohne Meta, gelten
Vertrauens-, Origin-, saubere Main- und Live-Abstammungsprüfungen für die gesamte
Auswahl vor jeder Änderung. Unbekannte Repositories und nicht registrierte
Remote-Aliase sind gesperrt. Nur bei ausgewähltem Meta gelten zusätzlich dessen
Zusammensetzungsprüfung und der passende annotierte Core-Tag als Voraussetzung;
Meta folgt zuletzt. Für Metas Veröffentlichung müssen Core-Tag und Main-Commit
bereits auf dem Remote vorliegen. Lokale Modul-Kandidatentags bleiben vor Cores
abschließendem Release-Lock und Tag möglich. Die vorhandene Core-WebUI-Bundleprüfung
bleibt bei Modulveröffentlichung und Core-Auswahl erhalten. Relevante Core-Paket-
und Lockdateien müssen eigene reguläre Dateien mit höchstens je 16 MiB sein;
Identität und Inhaltshash werden eingefroren und vor jeder Aktion erneut geprüft.
Nicht ausgewähltes Core benötigt dafür weder einen sauberen Checkout noch einen
Tag. Reine Backend-Auswahlen lesen keine irrelevanten Core-WebUI-Dateien.
Vor Git-Aufrufen werden Eigentümer, Schreibrechte, sichere
Pfadabstammung und echte Git-/Worktree-Metadaten geprüft; veränderbare gemeinsame
Verzeichnisse, fremde Eigentümer, Alternates, Grafts und versteckte Indexeinträge
sind gesperrt. Ein Sticky-Bit-Vorfahr wie `/tmp` ist nur oberhalb eines eigenen
geschützten Unterverzeichnisses zulässig. Es erfolgen weder Rechtereparaturen
noch globale Git-Vertrauensänderungen. Ausgewählte Versions- und Zusammensetzungs-
dateien müssen versioniert sein: Paket-/Lockdateien, Modulmanifeste und
Paketinitialisierer sowie Metas verschachteltes Paket und Release-Anforderungen.
Ignorierte Arbeitsdateien dürfen keine vom Tag abweichenden Angaben liefern.
Die Vorschau schreibt nichts, lokale Tags veröffentlichen nichts,
und die Veröffentlichung überträgt Main und den exakten annotierten Tag atomar
je Repository. Unmittelbar vor und nach den Aktionen werden die eingefrorenen
Quellnachweise erneut geprüft, einschließlich entferntem Main und Tag-Objekt.
Bei Änderungen oder Fehlern stoppt der Rest des Stapels ohne automatischen
Wiederholungsversuch. Frühere Veröffentlichungen und neu erzeugte lokale Tags
können bestehen bleiben: vor einem neuen Versuch Nachweise prüfen und erneut
freigeben, niemals unveränderliche Tags verschieben. Die vollständigen Quell- und
Live-Remote-Prüfungen werden um jede Aktion wiederholt; bei großen Stapeln kann
deren Anzahl quadratisch wachsen. Diese konservativen Prüfkosten gehören zur
Release-Planung. Der interne Vorprüfer ist ausschließlich lesend und besitzt
keinen alten Änderungspfad. Tests für Auswahlen mit und ohne Meta verwenden
nur temporäre lokale Remotes und ersetzen keine echte Veröffentlichungsprüfung.
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.
@@ -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
@@ -58,6 +58,45 @@ checkouts remain usable for read-only planning, but every durable executor
fails closed there; clone the registered origins into a private workspace
before releasing.
For a host with a confirmed IPv6 connection timeout, set
`GOVOPLAN_RELEASE_SSH_ADDRESS_FAMILY=inet` only for the release-tool invocation.
When unset, the original SSH command is preserved, including the trusted
operator's per-host `AddressFamily` configuration (normally `any`). Explicit
values accepted by the shared source/tag Git helper are exactly `any`, `inet`
(IPv4 only), and `inet6` (IPv6 only). Empty, misspelled, whitespace-padded, or
injected values fail before Git starts. The selector only adds the corresponding
fixed SSH `AddressFamily` option: it does not change DNS, host-key verification,
the registered remote, authentication, `BatchMode=yes`, or `ConnectTimeout=8`.
Arbitrary `GIT_SSH_COMMAND` overrides remain ignored. For example, start a
single local console invocation with:
```sh
GOVOPLAN_RELEASE_SSH_ADDRESS_FAMILY=inet \
./.venv/bin/python tools/release/release-console.py
```
The same process-scoped setting applies to canonical source/tag readbacks and
registry-candidate source verification. Under Flatpak, pass it explicitly to
the host invocation with `flatpak-spawn --host /usr/bin/env
GOVOPLAN_RELEASE_SSH_ADDRESS_FAMILY=inet ...`. It is not a global SSH setting
and does not affect the website publisher's separate transport sanitizer, npm,
or HTTP downloads. An IPv4-only setting cannot reach IPv6-only hosts; omit it
or use `any` when the diagnosed restriction no longer applies.
Deutsch: Bei einem bestätigten IPv6-Verbindungs-Timeout kann für genau einen
Release-Werkzeugaufruf `GOVOPLAN_RELEASE_SSH_ADDRESS_FAMILY=inet` gesetzt werden.
Ohne diese Variable bleibt der bisherige SSH-Befehl einschließlich der
vertrauenswürdigen Host-Konfiguration unverändert (normalerweise `any`).
Explizit zulässig sind ausschließlich `any`, `inet` (nur IPv4) und `inet6`
(nur IPv6). Andere oder leere Werte werden vor dem Git-Aufruf abgewiesen.
DNS, Hostschlüsselprüfung, registrierte Quelladresse, Authentifizierung und
Zeitlimit bleiben unverändert; frei vorgegebene SSH-Befehle bleiben gesperrt.
Unter Flatpak die Variable ausdrücklich an den Host-Aufruf übergeben. Die
Auswahl gilt für den gemeinsamen Git-Helfer der Quell-/Tag-Prüfungen, nicht
für den separaten Website-Publisher, npm oder HTTP-Downloads. Sie ändert keine
globale Konfiguration. Nach Behebung des Netzwerkproblems die Variable
weglassen oder auf `any` setzen; IPv4-only erreicht keine IPv6-only-Ziele.
The runtime itself is part of the authority boundary. Durable run creation
verifies the meta checkout, release/check tooling, repository registry, Python
environment, loaded `govoplan_core` and cryptography packages, and Git/SSH
@@ -221,6 +260,9 @@ Repository capabilities are frozen into each plan unit (`python-package`,
`core-release-bundle`, and the universal `git-source`) and determine which
steps appear. Internally aligned version changes are rendered deterministically
from recognized TOML, JSON, lockfile, manifest, and package declarations.
The manifest may use a literal version or a top-level literal `MODULE_VERSION`;
the latter is updated without rewriting independently versioned interfaces.
Computed or missing version declarations fail before any metadata is written.
Pre-existing dirty worktrees remain visible but have no commit executor; the
console never absorbs unrelated operator changes.
@@ -232,6 +274,17 @@ runs a receipt-bound alignment gate before exposing any atomic branch/tag push.
A failed step stops later steps while preserving prior receipts for explicit
retry or reconciliation.
Local module candidate creation deliberately does not require those candidates
to be resolved already in Core's release lock: their annotated tags are inputs
to the next lock-generation step. The internal tag helper applies this ordering
only when no Core repository is selected and remote publication is disabled.
Module version/lock consistency, manifest validity, clean/non-behind worktrees,
and local/remote tag immutability checks still apply. Core candidate tagging
continues to validate its own complete bundle, and every remote-publication
preview and execution requires the selected modules to match Core's release
input and resolved lock. A local candidate is therefore not publication
approval; a stale Core lock blocks publication without changing remote refs.
The browser likewise retains the request identifier for an uncertain
resume/retry/reconciliation response and replays it after reload. A successful
replay selects the returned run state. Transport and server failures retain the
@@ -301,8 +354,17 @@ such as `0.2.0` or `0.2.0-alpha1`, but requires the first three version numbers
to move forward.
Plain repository pushes are separate from catalog publication. `Preview Push`
shows the selected repository push commands. `Push Selected` requires `PUSH` in
the repository push confirmation field.
shows the selected repository push commands, but generic push, sync and prepare
mutation endpoints are disabled: they require a separate durable, receipt-bound
maintenance workflow and cannot be enabled by typing a confirmation. Source
release branch/tag publication uses the durable release-run steps described
above; do not route ordinary dirty worktrees through the legacy all-repository
stage/commit/tag helper.
The [headless devkit release commands](DEVKIT_RELEASE.md) invoke this same
application in-process without starting a server. They expose selective planning,
bounded status/history, create/show/preview/execute and explicit recovery while
retaining the same request IDs, confirmations, source bindings and receipts.
The source release panel retains `Preview Tag + Publish` as a non-mutating
inspection. Its legacy `Create Tags` and `Publish Tags` controls stay visible
@@ -503,6 +565,15 @@ tree and requires byte-for-byte equality with those validated objects. Tags and
remote branch updates then reference that exact commit SHA rather than the
mutable worktree `HEAD`.
For a full registry-backed release, first build a fresh private candidate using
`release-catalog.py full-registry`. Pass `--selected-repository` for newly
released HEAD-bound units, not every unchanged package in the full profile.
The command independently checks all full-profile registry bytes and annotated
tag provenance, then feeds this same strict `publish-candidate` transaction.
It does not create Gitea runtime Releases or dispatch image builds. See
[Full registry candidates / Vollständige Registry-Kandidaten](FULL_REGISTRY_CANDIDATES.md)
for the complete EN/DE workflow and the narrowly scoped legacy keyring transition.
Published channels are expected below the public catalog base URL:
- `https://govoplan.add-ideas.de/catalogs/v1/channels/stable.json`
@@ -515,8 +586,9 @@ updated catalog, and keep the published keyring healthy.
When a selected module exposes a WebUI package, its requested version must also
match Core's `webui/package.release.json` input and the resolved
`package-lock.release.json` entry. The source-tag preflight, selective plan, and
catalog-candidate writer all enforce this composition boundary. Pins for modules
`package-lock.release.json` entry. The source-publication preflight, selective
plan, and catalog-candidate writer all enforce this composition boundary;
module-only local candidate tags use the staged order described above. Pins for modules
that are not part of the selective release remain unchanged.
Release integration also enforces repository and composition version alignment
@@ -270,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.
@@ -0,0 +1,91 @@
# WebUI release dependency installer retries
## English
This operational note covers
[`install-webui-release-dependencies.sh`](../../tools/release/install-webui-release-dependencies.sh)
and the exit-status repair tracked in
[Meta #54](https://git.add-ideas.de/GovOPlaN/govoplan/issues/54).
It applies to release administrators using the legacy runtime WebUI installer;
there are no new application settings, permissions, or end-user workflows.
Each retried npm install or Git clone has at most three attempts. The installer
waits 10 seconds after the first failure and 20 seconds after the second, and
continues immediately after success. If all attempts fail, it exits with the
last command's nonzero status. Its `set -e` execution stops before subsequent
installation stages; callers using `set -e` also stop before subsequent work.
Previously, the retry helper could report success after three failures because
it captured the status of a completed `if` statement instead of the command.
On exhaustion, inspect the npm or Git error and correct the reported cause
before rerunning the installation. The temporary dependency workspace is
removed on exit. Earlier changes to `package.json`, removal of `package-lock.json`,
cache cleaning, and completed dependency installations are not rolled back;
prepare a fresh disposable release workspace when a clean retry is required.
The repair preserves the existing retry count, backoff, cache behavior, and
peer-resolution flags. It does not lift the runtime publication hold tracked in
[Meta #52](https://git.add-ideas.de/GovOPlaN/govoplan/issues/52).
Review the historical `--legacy-peer-deps` workaround separately before lifting
that hold. Strict disposable Git-release and signed catalog verification do not
use this installer; strict release verification must not bypass peer checks.
See [Package Registry Releases](PACKAGE_REGISTRY_RELEASES.md) for release context.
Run the isolated regression suite from the meta repository:
```sh
python3 -m unittest -v tests.test_webui_release_dependency_retries
```
The suite executes the actual Bash installer and a caller using `set -e`, with
local npm, Git, Node, and sleep stubs. It covers success on attempts one, two, and
three, final failure status, backoff, and termination at each retry call site.
It performs no network access, real waiting, or changes to the real npm cache.
It checks shell control flow, not package resolution or runtime publication.
## Deutsch
Dieser Betriebshinweis beschreibt
[`install-webui-release-dependencies.sh`](../../tools/release/install-webui-release-dependencies.sh)
und die unter [Meta #54](https://git.add-ideas.de/GovOPlaN/govoplan/issues/54)
erfasste Korrektur des Rückgabestatus. Er richtet sich an Release-Administratoren,
die den bisherigen WebUI-Installer für Laufzeit-Releases verwenden. Neue
Anwendungseinstellungen, Berechtigungen oder Endanwenderabläufe entstehen nicht.
Jede wiederholte npm-Installation und jeder Git-Klon erhält höchstens drei
Versuche. Nach dem ersten Fehlschlag wartet der Installer 10 Sekunden, nach dem
zweiten 20 Sekunden; nach einem Erfolg fährt er sofort fort. Scheitern alle
Versuche, endet er mit dem letzten von null verschiedenen Rückgabestatus.
Durch `set -e` werden nachfolgende Installationsschritte nicht ausgeführt;
auch aufrufende Skripte mit `set -e` brechen vor ihren nächsten Schritten ab.
Bisher konnte die Hilfsfunktion nach drei Fehlschlägen Erfolg melden, weil sie
den Status der abgeschlossenen `if`-Anweisung statt des Befehls übernahm.
Prüfen Sie nach dem Abbruch die npm- oder Git-Fehlermeldung und beheben Sie deren
Ursache vor einem erneuten Installationslauf. Das temporäre Verzeichnis für
Abhängigkeiten wird beim Beenden entfernt. Vorherige Änderungen an `package.json`,
das Entfernen von `package-lock.json`, die Cache-Bereinigung und abgeschlossene
Installationen werden nicht zurückgerollt. Bereiten Sie bei Bedarf einen neuen
temporären Release-Arbeitsbereich für einen sauberen Wiederholungslauf vor.
Die Korrektur erhält Anzahl und Wartezeiten der Versuche, Cache-Verhalten und
Optionen zur Peer-Auflösung. Die Sperre für Laufzeitveröffentlichungen aus
[Meta #52](https://git.add-ideas.de/GovOPlaN/govoplan/issues/52) bleibt bestehen.
Der bisherige Einsatz von `--legacy-peer-deps` muss vor ihrer Aufhebung gesondert
geprüft werden. Die strenge Git-Release-Prüfung in einem temporären Arbeitsbereich
und die Prüfung signierter Kataloge verwenden diesen Installer nicht; die strenge
Release-Prüfung darf Peer-Prüfungen nicht umgehen. Weitere Zusammenhänge erläutert
[Package Registry Releases](PACKAGE_REGISTRY_RELEASES.md).
Führen Sie die isolierten Regressionstests im Meta-Repository aus:
```sh
python3 -m unittest -v tests.test_webui_release_dependency_retries
```
Die Tests führen den tatsächlichen Bash-Installer und ein aufrufendes Skript mit
`set -e` aus. Lokale Testprogramme ersetzen npm, Git, Node und sleep. Geprüft werden
Erfolge im ersten, zweiten und dritten Versuch, der letzte Fehlerstatus,
Warteintervalle und der Abbruch an jeder Aufrufstelle. Es gibt keine
Netzwerkzugriffe, echten Wartezeiten oder Änderungen am tatsächlichen npm-Cache.
Die Tests prüfen den Shell-Ablauf, nicht die Paketauflösung oder Veröffentlichung.
@@ -79,7 +79,7 @@ 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/PACKAGE_REGISTRY_RELEASES.md`. Git tags remain source provenance; package
`docs/operations/PACKAGE_REGISTRY_RELEASES.md`. Git tags remain source provenance; package
registries are reusable artifact transport; the signed runtime manifest and
digest-pinned images remain production authority.
+164
View File
@@ -0,0 +1,164 @@
# Product-wide UI review program
The [central Gitea epic](https://git.add-ideas.de/GovOPlaN/govoplan/issues/56)
coordinates the review; its linked module issues are the canonical backlog,
progress and evidence log. The [issue inventory](ui-review-issue-inventory.json)
is a discovery/link snapshot, **not a second progress tracker**. All reviews were
initialized pending. Implementing a shared component or moving a help icon does
not by itself complete a module review.
The visual and interaction rules live in Core's
[UI design principles](https://git.add-ideas.de/GovOPlaN/govoplan-core/src/branch/main/docs/UI_DESIGN_PRINCIPLES.md).
This document defines how to apply and verify those principles across repositories;
it does not fork their definitions. The usual [Gitea issue contract](GITEA_ISSUES.md)
still applies. At the user's request, this cross-product program is coordinated in
Meta; shared primitives and their implementation remain owned by Core.
## Scope and initial inventory
The September 2026 source/catalog baseline contains **77 review tracks**:
- **73 implemented scopes:** Core/shared shell and 72 manifest-backed modules.
- **4 registered placeholders:** Appointments, Ledger, XÖV and XTA/OSCI. They have
README-only repositories at initialization. Their separate readiness tracks
remain pending for future interfaces; they are not counted as implemented or
reviewed modules.
Meta and the public website are not business-module review scopes. Module IDs
come from actual manifests, not repository-name guesses: for example,
`govoplan-campaign` owns `campaigns` and `govoplan-dist-lists` owns `dist_lists`.
The inventory is derived from `repositories.json`, trusted module manifests and
the owning repository's source files. Changes to the catalog must be reconciled
with the central epic rather than silently dropping a module.
A module with no standalone WebUI is still in scope. Review its configuration
and administrator experience, contributed operator/public surfaces, widgets,
documentation, permissions, errors and interactions exposed through other
modules. A missing standalone page is not evidence of conformance. A reviewer
may mark a particular principle not applicable only with a concrete explanation
and source/runtime evidence. A placeholder likewise needs an explicit future-UI
gate, not a fabricated page inventory or an automatic green review.
## Work order and ownership
Prioritize user-visible defects, usability and consistent interactions before
broader features. Start with Core/shared-shell contracts and Campaign's complete
workflow, then Files, Mail, Templates, Notifications, Docs/Help, Dashboard, Quick
Access and Views. The remaining queue is ordered by observed defects, shared
dependencies and risk. This is a starting order, not a claim that those reviews
have started or finished; the central issue records the current focus.
For Campaign, use a compact, read-only campaign settings overview with explicit,
scoped edit dialogs. Large recipient or attachment grids may use the UI-02 bulk
editing exception: clearly entered edit mode, explicit Save and Cancel, dirty
navigation protection, and no silent loss or accidental save. This direction
must be implemented and verified in the Campaign review, not marked complete by
the program bootstrap.
Fix a repeated problem in the owning shared primitive or semantic contract first,
then migrate consumers and verify representative actual-module fixtures. Keep
module business behavior in its own repository and preserve optional module
boundaries. Do not replace concrete user evidence with a generic fixture alone.
Parallel reviewers may own different modules, but they must coordinate shared
Core files and browser fixtures instead of racing the same resources.
## Per-module review workflow
1. **Inventory.** Verify and extend the issue's source-derived seed: navigation,
pages, nested routes, dialogs, labels/forms, tables/trees, admin/system/tenant/
user settings, public/operator interfaces, widgets and optional-module
contributions. Follow actual module entrypoints and runtime contributions;
filename heuristics and manifest routes are a starting point, not an exhaustive
runtime audit. Note roles, permission boundaries and installed optional modules.
2. **Observe.** Exercise realistic narrow and wide viewports, German and English,
keyboard navigation and focus, empty/loading/error/success states, clean/dirty
edit modes, reload, navigation away and return, and restoration of personal
preferences. Include realistic row counts, long labels, horizontal overflow,
pagination and two-way column resizing with fixed columns between text fields.
3. **Record.** Keep a findings/TODO/done ledger in the module issue. Every finding
names the surface, reproduction, expected principle, impact, owner and linked
implementation/evidence. A source inventory, planned fix or green generic
test is not a completed finding. State product decisions and manual checks
explicitly with enough context to resolve them.
4. **Implement.** Prefer reusable shared primitives and action/page contracts.
Keep display mode readable and editing intentional. Preserve authorization,
server validation, save/cancel behavior and data integrity. Never send mail,
delete records, change live configuration or trigger other irreversible effects
merely to obtain UI evidence; use authorized fixtures or a safe test context.
5. **Verify.** Run proportionate unit, structure and actual-module browser tests.
Record commands, results, source revision and safe runtime evidence. Update the
owning module's manifest-driven EN/DE documentation for behavior changes; run
`tools/checks/check-manifest-shapes.py` and relevant cross-module checks.
6. **Conclude honestly.** Complete the principle matrix, evidence and manual
checks. Link unresolved follow-ups and blockers; do not close a supposedly
complete review while required work is still pending. Closing an issue requires
explicit reviewed scope and the applicable design-principle revision.
## Principle ledger and back-propagation
Use the stable IDs from Core in each issue. The initial revision covers UI-01
heading/label help placement; UI-02 display-first/scoped editing; UI-03 actions;
UI-04 table/card geometry; UI-05 loading/error/progress; UI-06 tree interaction;
UI-07 accessibility and German; UI-08 data integrity; and UI-09 propagation of
revised principles. Core remains the authoritative definition.
Each module maintains this matrix, initially entirely pending:
| Principle/revision | Applicable surfaces / justified N/A | Applied / remaining work | Evidence | Exception / owner / follow-up |
| --- | --- | --- | --- | --- |
| UI-01 … UI-09 | Pending inventory | Pending review | Not yet recorded | None approved |
Whenever a principle is added or changed:
1. Update Core's rule with its rationale, stable ID and revision/change reference.
2. List **all already-reviewed modules**, not just the current implementation's
consumers. Check whether the changed rule applies to each.
3. Record unchanged conformance with evidence, or reopen the review / create a
linked follow-up with owner, exact affected surfaces and required checks.
4. Update the central epic's propagation ledger. Previously reviewed modules with
outstanding applicable work are **follow-up required**, not silently green.
5. Resolve the propagation sweep only after every affected module has current
evidence or an explicit, owned and justified exception. Exceptions do not
silently change the shared rule.
Suggested central ledger:
| Principle change | Already-reviewed modules checked | Conformant evidence | Reopened / follow-up required | Exceptions / owner | Sweep state |
| --- | --- | --- | --- | --- | --- |
| Initial UI-01UI-09 baseline | None reviewed at initialization | None | All module reviews pending | None approved | Pending |
## Safe inventory/bootstrap automation
`tools/gitea/gitea-ui-review-program.py` derives the current scopes from the
catalog and source manifests, inventories routes and UI source entrypoints,
then deduplicates issues by a stable hidden marker and normalized exact title.
It includes closed issues in duplicate checks and never reopens, closes or
overwrites existing module issues. Ambiguous or unmanaged duplicates stop the
operation for review. Existing issue bodies, labels and review progress are
preserved. A failed POST is not automatically retried; a subsequent run checks
the marker again before deciding whether another create is needed.
Dry-run is the default. An authenticated dry-run performs only Gitea reads:
```sh
.venv/bin/python tools/gitea/gitea-ui-review-program.py \
--env-file /home/zemion/.config/gitea/gitea.env --epic 56
```
Creating missing review issues requires explicit `--apply`. Initial population
of the central epic's module-link block additionally requires
`--initialize-links`; it is accepted only for the untouched bootstrap placeholder.
Later runs must not rewrite checkboxes or human-maintained progress. Existing
complete links are verified read-only. The token is loaded through the shared
Gitea helpers and is never emitted in reports. A scoped `--ipv4` option works
around host-specific IPv6 connectivity while retaining HTTPS certificate and
hostname verification.
The tool emits a JSON result to stdout with source counts and issue links. The
checked-in inventory is a reviewed initialization snapshot of that result;
refreshing links does not authorize replacing live review status with the
snapshot. Test the automation offline with:
```sh
.venv/bin/python -m pytest tests/test_ui_review_program.py -q
```
+866
View File
@@ -0,0 +1,866 @@
{
"schema_version": 1,
"snapshot_purpose": "Issue discovery links; live Gitea issues own review state and evidence.",
"epic": {
"repository": "govoplan",
"number": 56,
"url": "https://git.add-ideas.de/GovOPlaN/govoplan/issues/56"
},
"scope_count": 77,
"manifest_modules": 72,
"implemented_scopes": 73,
"catalogued_placeholders": 4,
"created": 77,
"missing": 0,
"issues": [
{
"scope_id": "core",
"name": "Core / shared shell",
"repository": "govoplan-core",
"kind": "core",
"ui_source_count": 111,
"number": 301,
"url": "https://git.add-ideas.de/GovOPlaN/govoplan-core/issues/301",
"state_at_verification": "open",
"operation": "created"
},
{
"scope_id": "access",
"name": "Access",
"repository": "govoplan-access",
"kind": "manifest",
"ui_source_count": 18,
"number": 23,
"url": "https://git.add-ideas.de/GovOPlaN/govoplan-access/issues/23",
"state_at_verification": "open",
"operation": "created"
},
{
"scope_id": "addresses",
"name": "Addresses",
"repository": "govoplan-addresses",
"kind": "manifest",
"ui_source_count": 1,
"number": 26,
"url": "https://git.add-ideas.de/GovOPlaN/govoplan-addresses/issues/26",
"state_at_verification": "open",
"operation": "created"
},
{
"scope_id": "admin",
"name": "Admin",
"repository": "govoplan-admin",
"kind": "manifest",
"ui_source_count": 9,
"number": 11,
"url": "https://git.add-ideas.de/GovOPlaN/govoplan-admin/issues/11",
"state_at_verification": "open",
"operation": "created"
},
{
"scope_id": "approvals",
"name": "Approvals",
"repository": "govoplan-approvals",
"kind": "manifest",
"ui_source_count": 4,
"number": 5,
"url": "https://git.add-ideas.de/GovOPlaN/govoplan-approvals/issues/5",
"state_at_verification": "open",
"operation": "created"
},
{
"scope_id": "assets",
"name": "Assets",
"repository": "govoplan-assets",
"kind": "manifest",
"ui_source_count": 0,
"number": 1,
"url": "https://git.add-ideas.de/GovOPlaN/govoplan-assets/issues/1",
"state_at_verification": "open",
"operation": "created"
},
{
"scope_id": "audit",
"name": "Audit",
"repository": "govoplan-audit",
"kind": "manifest",
"ui_source_count": 1,
"number": 10,
"url": "https://git.add-ideas.de/GovOPlaN/govoplan-audit/issues/10",
"state_at_verification": "open",
"operation": "created"
},
{
"scope_id": "booking",
"name": "Booking",
"repository": "govoplan-booking",
"kind": "manifest",
"ui_source_count": 0,
"number": 1,
"url": "https://git.add-ideas.de/GovOPlaN/govoplan-booking/issues/1",
"state_at_verification": "open",
"operation": "created"
},
{
"scope_id": "calendar",
"name": "Calendar",
"repository": "govoplan-calendar",
"kind": "manifest",
"ui_source_count": 10,
"number": 26,
"url": "https://git.add-ideas.de/GovOPlaN/govoplan-calendar/issues/26",
"state_at_verification": "open",
"operation": "created"
},
{
"scope_id": "campaigns",
"name": "Campaigns",
"repository": "govoplan-campaign",
"kind": "manifest",
"ui_source_count": 60,
"number": 103,
"url": "https://git.add-ideas.de/GovOPlaN/govoplan-campaign/issues/103",
"state_at_verification": "open",
"operation": "created"
},
{
"scope_id": "cases",
"name": "Cases",
"repository": "govoplan-cases",
"kind": "manifest",
"ui_source_count": 4,
"number": 8,
"url": "https://git.add-ideas.de/GovOPlaN/govoplan-cases/issues/8",
"state_at_verification": "open",
"operation": "created"
},
{
"scope_id": "certificates",
"name": "Certificates",
"repository": "govoplan-certificates",
"kind": "manifest",
"ui_source_count": 0,
"number": 1,
"url": "https://git.add-ideas.de/GovOPlaN/govoplan-certificates/issues/1",
"state_at_verification": "open",
"operation": "created"
},
{
"scope_id": "committee",
"name": "Committee",
"repository": "govoplan-committee",
"kind": "manifest",
"ui_source_count": 3,
"number": 5,
"url": "https://git.add-ideas.de/GovOPlaN/govoplan-committee/issues/5",
"state_at_verification": "open",
"operation": "created"
},
{
"scope_id": "connectors",
"name": "Connectors",
"repository": "govoplan-connectors",
"kind": "manifest",
"ui_source_count": 3,
"number": 20,
"url": "https://git.add-ideas.de/GovOPlaN/govoplan-connectors/issues/20",
"state_at_verification": "open",
"operation": "created"
},
{
"scope_id": "consultation",
"name": "Consultation",
"repository": "govoplan-consultation",
"kind": "manifest",
"ui_source_count": 0,
"number": 1,
"url": "https://git.add-ideas.de/GovOPlaN/govoplan-consultation/issues/1",
"state_at_verification": "open",
"operation": "created"
},
{
"scope_id": "contracts",
"name": "Contracts",
"repository": "govoplan-contracts",
"kind": "manifest",
"ui_source_count": 0,
"number": 1,
"url": "https://git.add-ideas.de/GovOPlaN/govoplan-contracts/issues/1",
"state_at_verification": "open",
"operation": "created"
},
{
"scope_id": "dashboard",
"name": "Dashboard",
"repository": "govoplan-dashboard",
"kind": "manifest",
"ui_source_count": 5,
"number": 6,
"url": "https://git.add-ideas.de/GovOPlaN/govoplan-dashboard/issues/6",
"state_at_verification": "open",
"operation": "created"
},
{
"scope_id": "dataflow",
"name": "Dataflow",
"repository": "govoplan-dataflow",
"kind": "manifest",
"ui_source_count": 5,
"number": 24,
"url": "https://git.add-ideas.de/GovOPlaN/govoplan-dataflow/issues/24",
"state_at_verification": "open",
"operation": "created"
},
{
"scope_id": "datasources",
"name": "Datasources",
"repository": "govoplan-datasources",
"kind": "manifest",
"ui_source_count": 1,
"number": 10,
"url": "https://git.add-ideas.de/GovOPlaN/govoplan-datasources/issues/10",
"state_at_verification": "open",
"operation": "created"
},
{
"scope_id": "decisions",
"name": "Decisions",
"repository": "govoplan-decisions",
"kind": "manifest",
"ui_source_count": 0,
"number": 2,
"url": "https://git.add-ideas.de/GovOPlaN/govoplan-decisions/issues/2",
"state_at_verification": "open",
"operation": "created"
},
{
"scope_id": "dist_lists",
"name": "Distribution Lists",
"repository": "govoplan-dist-lists",
"kind": "manifest",
"ui_source_count": 2,
"number": 10,
"url": "https://git.add-ideas.de/GovOPlaN/govoplan-dist-lists/issues/10",
"state_at_verification": "open",
"operation": "created"
},
{
"scope_id": "dms",
"name": "DMS",
"repository": "govoplan-dms",
"kind": "manifest",
"ui_source_count": 0,
"number": 3,
"url": "https://git.add-ideas.de/GovOPlaN/govoplan-dms/issues/3",
"state_at_verification": "open",
"operation": "created"
},
{
"scope_id": "docs",
"name": "Docs",
"repository": "govoplan-docs",
"kind": "manifest",
"ui_source_count": 2,
"number": 23,
"url": "https://git.add-ideas.de/GovOPlaN/govoplan-docs/issues/23",
"state_at_verification": "open",
"operation": "created"
},
{
"scope_id": "encryption",
"name": "Encryption",
"repository": "govoplan-encryption",
"kind": "manifest",
"ui_source_count": 1,
"number": 7,
"url": "https://git.add-ideas.de/GovOPlaN/govoplan-encryption/issues/7",
"state_at_verification": "open",
"operation": "created"
},
{
"scope_id": "erp",
"name": "ERP",
"repository": "govoplan-erp",
"kind": "manifest",
"ui_source_count": 0,
"number": 2,
"url": "https://git.add-ideas.de/GovOPlaN/govoplan-erp/issues/2",
"state_at_verification": "open",
"operation": "created"
},
{
"scope_id": "evaluation",
"name": "Evaluation",
"repository": "govoplan-evaluation",
"kind": "manifest",
"ui_source_count": 0,
"number": 1,
"url": "https://git.add-ideas.de/GovOPlaN/govoplan-evaluation/issues/1",
"state_at_verification": "open",
"operation": "created"
},
{
"scope_id": "facilities",
"name": "Facilities",
"repository": "govoplan-facilities",
"kind": "manifest",
"ui_source_count": 0,
"number": 1,
"url": "https://git.add-ideas.de/GovOPlaN/govoplan-facilities/issues/1",
"state_at_verification": "open",
"operation": "created"
},
{
"scope_id": "files",
"name": "Files",
"repository": "govoplan-files",
"kind": "manifest",
"ui_source_count": 8,
"number": 48,
"url": "https://git.add-ideas.de/GovOPlaN/govoplan-files/issues/48",
"state_at_verification": "open",
"operation": "created"
},
{
"scope_id": "fit_connect",
"name": "FIT-Connect",
"repository": "govoplan-fit-connect",
"kind": "manifest",
"ui_source_count": 0,
"number": 2,
"url": "https://git.add-ideas.de/GovOPlaN/govoplan-fit-connect/issues/2",
"state_at_verification": "open",
"operation": "created"
},
{
"scope_id": "forms",
"name": "Forms",
"repository": "govoplan-forms",
"kind": "manifest",
"ui_source_count": 2,
"number": 7,
"url": "https://git.add-ideas.de/GovOPlaN/govoplan-forms/issues/7",
"state_at_verification": "open",
"operation": "created"
},
{
"scope_id": "forms_runtime",
"name": "Forms Runtime",
"repository": "govoplan-forms-runtime",
"kind": "manifest",
"ui_source_count": 6,
"number": 7,
"url": "https://git.add-ideas.de/GovOPlaN/govoplan-forms-runtime/issues/7",
"state_at_verification": "open",
"operation": "created"
},
{
"scope_id": "grants",
"name": "Grants",
"repository": "govoplan-grants",
"kind": "manifest",
"ui_source_count": 0,
"number": 1,
"url": "https://git.add-ideas.de/GovOPlaN/govoplan-grants/issues/1",
"state_at_verification": "open",
"operation": "created"
},
{
"scope_id": "helpdesk",
"name": "Helpdesk",
"repository": "govoplan-helpdesk",
"kind": "manifest",
"ui_source_count": 1,
"number": 1,
"url": "https://git.add-ideas.de/GovOPlaN/govoplan-helpdesk/issues/1",
"state_at_verification": "open",
"operation": "created"
},
{
"scope_id": "identity",
"name": "Identity",
"repository": "govoplan-identity",
"kind": "manifest",
"ui_source_count": 1,
"number": 5,
"url": "https://git.add-ideas.de/GovOPlaN/govoplan-identity/issues/5",
"state_at_verification": "open",
"operation": "created"
},
{
"scope_id": "identity_trust",
"name": "Identity Trust",
"repository": "govoplan-identity-trust",
"kind": "manifest",
"ui_source_count": 1,
"number": 4,
"url": "https://git.add-ideas.de/GovOPlaN/govoplan-identity-trust/issues/4",
"state_at_verification": "open",
"operation": "created"
},
{
"scope_id": "idm",
"name": "IDM",
"repository": "govoplan-idm",
"kind": "manifest",
"ui_source_count": 3,
"number": 14,
"url": "https://git.add-ideas.de/GovOPlaN/govoplan-idm/issues/14",
"state_at_verification": "open",
"operation": "created"
},
{
"scope_id": "inspections",
"name": "Inspections",
"repository": "govoplan-inspections",
"kind": "manifest",
"ui_source_count": 0,
"number": 1,
"url": "https://git.add-ideas.de/GovOPlaN/govoplan-inspections/issues/1",
"state_at_verification": "open",
"operation": "created"
},
{
"scope_id": "learning",
"name": "Learning",
"repository": "govoplan-learning",
"kind": "manifest",
"ui_source_count": 0,
"number": 1,
"url": "https://git.add-ideas.de/GovOPlaN/govoplan-learning/issues/1",
"state_at_verification": "open",
"operation": "created"
},
{
"scope_id": "mail",
"name": "Mail",
"repository": "govoplan-mail",
"kind": "manifest",
"ui_source_count": 6,
"number": 25,
"url": "https://git.add-ideas.de/GovOPlaN/govoplan-mail/issues/25",
"state_at_verification": "open",
"operation": "created"
},
{
"scope_id": "mandates",
"name": "Mandates",
"repository": "govoplan-mandates",
"kind": "manifest",
"ui_source_count": 0,
"number": 2,
"url": "https://git.add-ideas.de/GovOPlaN/govoplan-mandates/issues/2",
"state_at_verification": "open",
"operation": "created"
},
{
"scope_id": "notifications",
"name": "Notifications",
"repository": "govoplan-notifications",
"kind": "manifest",
"ui_source_count": 3,
"number": 7,
"url": "https://git.add-ideas.de/GovOPlaN/govoplan-notifications/issues/7",
"state_at_verification": "open",
"operation": "created"
},
{
"scope_id": "ops",
"name": "Ops",
"repository": "govoplan-ops",
"kind": "manifest",
"ui_source_count": 2,
"number": 5,
"url": "https://git.add-ideas.de/GovOPlaN/govoplan-ops/issues/5",
"state_at_verification": "open",
"operation": "created"
},
{
"scope_id": "organizations",
"name": "Organizations",
"repository": "govoplan-organizations",
"kind": "manifest",
"ui_source_count": 4,
"number": 9,
"url": "https://git.add-ideas.de/GovOPlaN/govoplan-organizations/issues/9",
"state_at_verification": "open",
"operation": "created"
},
{
"scope_id": "parties",
"name": "Parties",
"repository": "govoplan-parties",
"kind": "manifest",
"ui_source_count": 0,
"number": 2,
"url": "https://git.add-ideas.de/GovOPlaN/govoplan-parties/issues/2",
"state_at_verification": "open",
"operation": "created"
},
{
"scope_id": "payments",
"name": "Payments",
"repository": "govoplan-payments",
"kind": "manifest",
"ui_source_count": 3,
"number": 3,
"url": "https://git.add-ideas.de/GovOPlaN/govoplan-payments/issues/3",
"state_at_verification": "open",
"operation": "created"
},
{
"scope_id": "permits",
"name": "Permits",
"repository": "govoplan-permits",
"kind": "manifest",
"ui_source_count": 0,
"number": 1,
"url": "https://git.add-ideas.de/GovOPlaN/govoplan-permits/issues/1",
"state_at_verification": "open",
"operation": "created"
},
{
"scope_id": "policy",
"name": "Policy",
"repository": "govoplan-policy",
"kind": "manifest",
"ui_source_count": 3,
"number": 14,
"url": "https://git.add-ideas.de/GovOPlaN/govoplan-policy/issues/14",
"state_at_verification": "open",
"operation": "created"
},
{
"scope_id": "poll",
"name": "Poll",
"repository": "govoplan-poll",
"kind": "manifest",
"ui_source_count": 0,
"number": 5,
"url": "https://git.add-ideas.de/GovOPlaN/govoplan-poll/issues/5",
"state_at_verification": "open",
"operation": "created"
},
{
"scope_id": "portal",
"name": "Portal",
"repository": "govoplan-portal",
"kind": "manifest",
"ui_source_count": 2,
"number": 4,
"url": "https://git.add-ideas.de/GovOPlaN/govoplan-portal/issues/4",
"state_at_verification": "open",
"operation": "created"
},
{
"scope_id": "postbox",
"name": "Postbox",
"repository": "govoplan-postbox",
"kind": "manifest",
"ui_source_count": 4,
"number": 29,
"url": "https://git.add-ideas.de/GovOPlaN/govoplan-postbox/issues/29",
"state_at_verification": "open",
"operation": "created"
},
{
"scope_id": "procurement",
"name": "Procurement",
"repository": "govoplan-procurement",
"kind": "manifest",
"ui_source_count": 0,
"number": 1,
"url": "https://git.add-ideas.de/GovOPlaN/govoplan-procurement/issues/1",
"state_at_verification": "open",
"operation": "created"
},
{
"scope_id": "projects",
"name": "Projects",
"repository": "govoplan-projects",
"kind": "manifest",
"ui_source_count": 1,
"number": 4,
"url": "https://git.add-ideas.de/GovOPlaN/govoplan-projects/issues/4",
"state_at_verification": "open",
"operation": "created"
},
{
"scope_id": "quick_access",
"name": "Quick Access",
"repository": "govoplan-quick-access",
"kind": "manifest",
"ui_source_count": 2,
"number": 3,
"url": "https://git.add-ideas.de/GovOPlaN/govoplan-quick-access/issues/3",
"state_at_verification": "open",
"operation": "created"
},
{
"scope_id": "records",
"name": "Records",
"repository": "govoplan-records",
"kind": "manifest",
"ui_source_count": 4,
"number": 10,
"url": "https://git.add-ideas.de/GovOPlaN/govoplan-records/issues/10",
"state_at_verification": "open",
"operation": "created"
},
{
"scope_id": "reporting",
"name": "Reporting",
"repository": "govoplan-reporting",
"kind": "manifest",
"ui_source_count": 3,
"number": 11,
"url": "https://git.add-ideas.de/GovOPlaN/govoplan-reporting/issues/11",
"state_at_verification": "open",
"operation": "created"
},
{
"scope_id": "resources",
"name": "Resources",
"repository": "govoplan-resources",
"kind": "manifest",
"ui_source_count": 0,
"number": 1,
"url": "https://git.add-ideas.de/GovOPlaN/govoplan-resources/issues/1",
"state_at_verification": "open",
"operation": "created"
},
{
"scope_id": "rest",
"name": "REST Connector",
"repository": "govoplan-rest",
"kind": "manifest",
"ui_source_count": 0,
"number": 1,
"url": "https://git.add-ideas.de/GovOPlaN/govoplan-rest/issues/1",
"state_at_verification": "open",
"operation": "created"
},
{
"scope_id": "risk_compliance",
"name": "Risk Compliance",
"repository": "govoplan-risk-compliance",
"kind": "manifest",
"ui_source_count": 1,
"number": 10,
"url": "https://git.add-ideas.de/GovOPlaN/govoplan-risk-compliance/issues/10",
"state_at_verification": "open",
"operation": "created"
},
{
"scope_id": "scheduling",
"name": "Scheduling",
"repository": "govoplan-scheduling",
"kind": "manifest",
"ui_source_count": 4,
"number": 10,
"url": "https://git.add-ideas.de/GovOPlaN/govoplan-scheduling/issues/10",
"state_at_verification": "open",
"operation": "created"
},
{
"scope_id": "search",
"name": "Search",
"repository": "govoplan-search",
"kind": "manifest",
"ui_source_count": 4,
"number": 6,
"url": "https://git.add-ideas.de/GovOPlaN/govoplan-search/issues/6",
"state_at_verification": "open",
"operation": "created"
},
{
"scope_id": "services",
"name": "Services",
"repository": "govoplan-services",
"kind": "manifest",
"ui_source_count": 0,
"number": 2,
"url": "https://git.add-ideas.de/GovOPlaN/govoplan-services/issues/2",
"state_at_verification": "open",
"operation": "created"
},
{
"scope_id": "soap",
"name": "SOAP Connector",
"repository": "govoplan-soap",
"kind": "manifest",
"ui_source_count": 0,
"number": 1,
"url": "https://git.add-ideas.de/GovOPlaN/govoplan-soap/issues/1",
"state_at_verification": "open",
"operation": "created"
},
{
"scope_id": "tasks",
"name": "Tasks",
"repository": "govoplan-tasks",
"kind": "manifest",
"ui_source_count": 3,
"number": 5,
"url": "https://git.add-ideas.de/GovOPlaN/govoplan-tasks/issues/5",
"state_at_verification": "open",
"operation": "created"
},
{
"scope_id": "templates",
"name": "Templates",
"repository": "govoplan-templates",
"kind": "manifest",
"ui_source_count": 1,
"number": 8,
"url": "https://git.add-ideas.de/GovOPlaN/govoplan-templates/issues/8",
"state_at_verification": "open",
"operation": "created"
},
{
"scope_id": "tenancy",
"name": "Tenancy",
"repository": "govoplan-tenancy",
"kind": "manifest",
"ui_source_count": 2,
"number": 7,
"url": "https://git.add-ideas.de/GovOPlaN/govoplan-tenancy/issues/7",
"state_at_verification": "open",
"operation": "created"
},
{
"scope_id": "tickets",
"name": "Tickets",
"repository": "govoplan-tickets",
"kind": "manifest",
"ui_source_count": 1,
"number": 3,
"url": "https://git.add-ideas.de/GovOPlaN/govoplan-tickets/issues/3",
"state_at_verification": "open",
"operation": "created"
},
{
"scope_id": "transparency",
"name": "Transparency",
"repository": "govoplan-transparency",
"kind": "manifest",
"ui_source_count": 0,
"number": 1,
"url": "https://git.add-ideas.de/GovOPlaN/govoplan-transparency/issues/1",
"state_at_verification": "open",
"operation": "created"
},
{
"scope_id": "views",
"name": "Views",
"repository": "govoplan-views",
"kind": "manifest",
"ui_source_count": 3,
"number": 6,
"url": "https://git.add-ideas.de/GovOPlaN/govoplan-views/issues/6",
"state_at_verification": "open",
"operation": "created"
},
{
"scope_id": "voting",
"name": "Voting",
"repository": "govoplan-voting",
"kind": "manifest",
"ui_source_count": 2,
"number": 8,
"url": "https://git.add-ideas.de/GovOPlaN/govoplan-voting/issues/8",
"state_at_verification": "open",
"operation": "created"
},
{
"scope_id": "wiki",
"name": "Wiki",
"repository": "govoplan-wiki",
"kind": "manifest",
"ui_source_count": 1,
"number": 2,
"url": "https://git.add-ideas.de/GovOPlaN/govoplan-wiki/issues/2",
"state_at_verification": "open",
"operation": "created"
},
{
"scope_id": "workflow",
"name": "Workflow",
"repository": "govoplan-workflow",
"kind": "manifest",
"ui_source_count": 6,
"number": 17,
"url": "https://git.add-ideas.de/GovOPlaN/govoplan-workflow/issues/17",
"state_at_verification": "open",
"operation": "created"
},
{
"scope_id": "workflow_engine",
"name": "Workflow Engine",
"repository": "govoplan-workflow-engine",
"kind": "manifest",
"ui_source_count": 0,
"number": 4,
"url": "https://git.add-ideas.de/GovOPlaN/govoplan-workflow-engine/issues/4",
"state_at_verification": "open",
"operation": "created"
},
{
"scope_id": "xrechnung",
"name": "XRechnung",
"repository": "govoplan-xrechnung",
"kind": "manifest",
"ui_source_count": 0,
"number": 3,
"url": "https://git.add-ideas.de/GovOPlaN/govoplan-xrechnung/issues/3",
"state_at_verification": "open",
"operation": "created"
},
{
"scope_id": "catalog:govoplan-appointments",
"name": "Appointments",
"repository": "govoplan-appointments",
"kind": "placeholder",
"ui_source_count": 0,
"number": 2,
"url": "https://git.add-ideas.de/GovOPlaN/govoplan-appointments/issues/2",
"state_at_verification": "open",
"operation": "created"
},
{
"scope_id": "catalog:govoplan-ledger",
"name": "Ledger",
"repository": "govoplan-ledger",
"kind": "placeholder",
"ui_source_count": 0,
"number": 1,
"url": "https://git.add-ideas.de/GovOPlaN/govoplan-ledger/issues/1",
"state_at_verification": "open",
"operation": "created"
},
{
"scope_id": "catalog:govoplan-xoev",
"name": "Xoev",
"repository": "govoplan-xoev",
"kind": "placeholder",
"ui_source_count": 0,
"number": 2,
"url": "https://git.add-ideas.de/GovOPlaN/govoplan-xoev/issues/2",
"state_at_verification": "open",
"operation": "created"
},
{
"scope_id": "catalog:govoplan-xta-osci",
"name": "Xta Osci",
"repository": "govoplan-xta-osci",
"kind": "placeholder",
"ui_source_count": 0,
"number": 1,
"url": "https://git.add-ideas.de/GovOPlaN/govoplan-xta-osci/issues/1",
"state_at_verification": "open",
"operation": "created"
}
],
"snapshot_date": "2026-09-08",
"initial_review_status": "pending for every scope; this snapshot does not track subsequent issue progress"
}
+80
View File
@@ -0,0 +1,80 @@
# GovOPlaN 0.1.45 — usability, reliability and security hardening
Release coordination: [GovOPlaN #51](https://git.add-ideas.de/GovOPlaN/govoplan/issues/51).
The exact independently versioned composition is recorded in
`packages/govoplan-meta/pyproject.toml`; unchanged modules retain their versions.
This source release does not by itself establish a deployed or independently
approved production environment. Package, signed catalog and runtime publication
results are recorded separately in the coordination issue.
## Runtime publication hold
The [runtime image audit](../security/RUNTIME_IMAGE_AUDIT_2026-09-08.md) completed
eleven registry-only amd64 scans, but found unresolved vulnerabilities and
inventory gaps. Runtime publication remains held separately from this source
release. Patch-only image updates are insufficient; maintained minor-line
changes, narrowly evidenced finding decisions, arm64/final-layer scans and
deployment checks remain necessary. No audited candidate was automatically
adopted and no image was executed during those scans.
The remaining gates are tracked in
[GovOPlaN #52](https://git.add-ideas.de/GovOPlaN/govoplan/issues/52).
## Included changes
- Shared page/action placement, reusable navigation grouping/editing, table and
dialog sizing, field alignment, multi-select filters and predictable tree
selection. Files, Mail, Search, Notifications and domain pages use the same
contracts, with browser regression coverage.
- Campaign draft saving and independent Mail/ZIP-policy repair, persistent and
bulk message review, clearer delivery eligibility, bounded configurable
synchronous delivery, guarded workerless recovery, lightweight SMTP/IMAP
progress, reused IMAP connections and recipient-complete reporting.
- Files archive staging/reuse, unpacking previously uploaded archives, numeric
progress and bounded traversal. Optional native archive acceleration retains
the same validation rules; portable fallbacks remain available.
- Mail credential references and IMAP folder-name decoding; help topics can be
found by area and tags without expanding every occurrence of the same topic.
- Authentication provenance/scope and browser-cache hardening, patched rich-text
dependencies, spreadsheet/archive/template/Dataflow resource limits, batched
Docs/Notifications queries and safe Reporting bind names. See the
[security/performance review](../security/SECURITY_PERFORMANCE_REVIEW_2026-09-08.md)
for measurements, test evidence and remaining limitations.
- A deterministic governance-journey clock fixture, fresh-process Campaign
import coverage, and a new Cases patch aligning its root npm facade with its
Python/WebUI package. Historical published tags are not rewritten.
- Git-root WebUI package facades are aligned with their owning packages, with
a cross-composition parity check. Tasks is included in default module
discovery; it remains subject to enabled modules and normal permissions.
## Upgrade and verification
Back up the database and file storage before upgrading. Apply the complete
selected migration graph before starting the new API/workers. This release
includes additive repair migrations `c58a2d7e9f10` (Core ownership history) and
`d8f1b4e7a0c3` (Access external-function mappings), plus Campaign delivery-state
migrations. Existing business evidence is retained; a schema downgrade is not
a substitute for a reviewed backup/restore plan. Restart API and worker
processes together after upgrading their matching packages.
Updated UI consumers require Core 0.1.45 where they use its new shared contracts.
Tenant keys that previously relied on unintended system permissions/wildcards
must be corrected; the release does not preserve that unsafe behavior. Extremely
sparse spreadsheets, oversized generated output and excessive archive paths
can now fail early with a diagnostic.
For archive staging across multiple hosts, provide shared POSIX storage with
working locks or sticky routing. Background delivery still needs configured
workers; increasing the synchronous limit does not create a worker or guarantee
delivery after a process failure. An unknown SMTP outcome must be reconciled,
not automatically resent.
After deployment, manually verify login/logout and least-privilege API keys,
Campaign Settings and independent Mail/ZIP saves, archive upload/unpack,
recipient-complete reports, and SMTP/IMAP progress with an explicitly approved
test mailbox. No release verification sends real campaign mail automatically.
Hard process isolation, forced-password-change/recovery enforcement, bounded
Xrechnung subprocess output and large-history pagination remain separate open
issues. This release is not a claim that all security or performance debt is
resolved. Production-image scans and multi-host evidence must refer to the
actual signed runtime being deployed.
@@ -0,0 +1,125 @@
# Runtime image candidate audit — 8 September 2026
Release coordination: [GovOPlaN #51](https://git.add-ideas.de/GovOPlaN/govoplan/issues/51).
Canonical remediation: [GovOPlaN #52](https://git.add-ideas.de/GovOPlaN/govoplan/issues/52).
This follow-up to the [source security/performance review](SECURITY_PERFORMANCE_REVIEW_2026-09-08.md)
records registry-only scans of nine proposed runtime dependencies and two
same-minor patch candidates. **Runtime publication is held:** patch-only updates
do not resolve the baseline. Source/package publication is a separate outcome.
No images were executed, rebuilt, selected for CI, or published by this audit.
## Method and reproducible evidence
Official Trivy **0.74.0** was installed only in a private local task directory,
without sudo or Docker access. Its Linux-64bit release archive matched both the
official checksums file and GitHub release asset metadata:
- Archive SHA256: `2ae6fe3ee734b7fdf11335663e18c75ea12dccc76062f09f164a3b0f8be4371a`.
- Checksums-file SHA256: `bc701c3c3ee8b9acbea2c23257e41381e3854888f51281616a6ba5dc96963821`.
- Vulnerability database schema 2, updated `2026-09-07T19:06:01.154199452Z`,
downloaded from `mirror.gcr.io/aquasec/trivy-db:2`.
- Scan flags: `--image-src remote --platform linux/amd64 --scanners vuln
--format json --no-progress --timeout 8m --max-image-size 2GB --exit-code 0`.
Findings were counted from validated JSON; exit zero did not mean clean.
- Existing Docker credentials were not read; no private keys or secrets were
used. Checksums over official HTTPS metadata were verified, not independent
Sigstore signatures. See the [official release](https://github.com/aquasecurity/trivy/releases/tag/v0.74.0)
and [registry-only scan documentation](https://trivy.dev/docs/latest/target/container_image/).
Raw evidence is retained locally, not committed:
`/home/zemion/.cache/govoplan-trivy-remote.yKZgjDOg/scan/`.
It contains eleven `reports/*-amd64.json` reports/logs, scanner scripts,
`patch-candidate-inspection.json`, exact successor registry indices, and
`evidence-checksums.json`. Summary SHA256 values:
- `summary.json`: `f2785a731d637452ab9c0b1f5399772c0f8828a63ca83d5fa7496abdad1c757a`.
- `patch-summary.json`: `b3fc6273fcdad98864040ccdf3477ecf379afd46e9f94444b1f2910f48c1d85b`.
All eleven executions succeeded without timeout/rate-limit failure. Initial
summary fields distinguish `scan_execution_complete: true` from
`coverage_complete: false`: Garage has no detectable package inventory.
Checksums preserve evidence identity, not indefinite storage availability.
## Exact requested pins and results
All references below use `docker.io/`. Counts are package-vulnerability records,
not distinct CVEs or confirmed exploitable application defects. A vulnerability
can appear against several installed packages. Unfixed/unknown records remain.
| Image tag | Exact index SHA256 | Critical / High / Medium / Low / Unknown | Fixable C/H |
| --- | --- | --- | ---: |
| `library/python:3.12-slim-bookworm` | `782412e85d0f0984994c290652577d4018aff08145c85b262bb63dc0c7522254` | 5 / 55 / 102 / 103 / 5 | 0 |
| `library/postgres:16-alpine` | `cf78e76683b9ca8c5733cbbdce6c9262b45b6767934dd0a95e671f9a0fc20685` | 1 / 30 / 28 / 14 / 1 | 31 |
| `library/redis:7-alpine` | `ff02b58f971e7d7d156a1267e283fcbbeee91773b6aa36c49dac28ecfe28eadf` | 0 / 0 / 0 / 0 / 0 | 0 |
| `nginxinc/nginx-unprivileged:1.29-alpine` | `0c79d56aee561a1d81c63f00eee5fb5fe29279560cdc55e91425133104c7fbe6` | 0 / 33 / 74 / 37 / 20 | 33 |
| `library/haproxy:3.2.21-alpine` | `66e25cc9a8332635f4e897f7f4b1e5622c25f09f0ee23cddc6ce9bdb3a24772a` | 0 / 2 / 6 / 12 / 0 | 2 |
| `library/caddy:2.10.2-alpine` | `4c6e91c6ed0e2fa03efd5b44747b625fec79bc9cd06ac5235a779726618e530d` | 7 / 75 / 67 / 37 / 4 | 82 |
| `dxflrs/garage:v2.3.0` | `866bd13ed2038ba7e7190e840482bc27234c4afaf77be8cfa439ae088c1e4690` | **Unknown: no inventory** | — |
| `greenmail/standalone:2.1.9` | `3ac5a83dd6727cf95e4d50e18907fb8ee7bbf5f67e8534714dee2fb1b5b2e1d4` | 0 / 0 / 116 / 35 / 0 | 0 |
| `tonistiigi/binfmt:qemu-v10.2.3-68` | `400a4873b838d1b89194d982c45e5fb3cda4593fbfd7e08a02e76b03b21166f0` | 0 / 9 / 2 / 1 / 1 | 9 |
## Patch-only options and limits
Complete publisher tag listings were inspected for nginx 1.29, Caddy 2.10,
HAProxy 3.2, GreenMail 2.1 and binfmt qemu10.2. Two newer candidates were
scanned; their registry index bytes matched both registry and publisher digests,
and contained amd64 and arm64 manifests:
- `library/haproxy:3.2.23-alpine@sha256:6343ce34a132a5dceaa24767d739df2bd519f8f7c1079ae39e4821334e8eb42e`:
same Alpine 3.24.1, 24 detected OS packages, zero reported findings. This is
a useful candidate, not a completed compatibility test or application audit.
- `greenmail/standalone:2.1.13@sha256:3df66b7edd01c8a301343ca5e3601d8674760d4708655573560c24745e624fb2`:
upstream changes Ubuntu 22.04 to Debian 13.6; **3 C / 80 H / 98 M / 85 L /
5 unknown**, 30 fixable C/H records. Not selected as a no-base-change update.
- nginx's newest matching Alpine patch is already 1.29.8 at the scanned pin;
Caddy 2.10 remains 2.10.2; binfmt qemu10.2 remains 10.2.3-68. No newer matching
publisher images were found. The current [official Caddy image catalogue](https://raw.githubusercontent.com/docker-library/official-images/master/library/caddy)
uses 2.11.4; switching minor lines requires new scans and compatibility checks.
Priority remediation: Caddy's own seven HIGH records require fixes through
2.11.4, with additional bundled Go/library fixes that must be re-scanned;
nginx's packages include curl/libcurl fixes through 8.22.0-r0, OpenSSL 3.5.8-r0,
c-ares 1.34.8-r0, expat 2.8.1-r0 and libuuid 2.41.6-r1. PostgreSQL's OS records
require OpenSSL 3.5.8-r0 and libuuid 2.42.3-r1; its CRITICAL plus 21 HIGH Go
records concern the **gosu helper**, not PostgreSQL server code. binfmt's nine
HIGH records concern its Go 1.26.4 build, with fixes through 1.26.6. Package
presence does not establish vulnerable-symbol reachability. No unscanned tag
is claimed to meet every fix requirement.
## Python triage and coverage caveats
Python image metadata identifies CPython 3.12.14, but Trivy inventories only
Debian packages and pip, **not CPython/stdlib**. All 60 C/H records concern
Debian packages: 21 CVEs, 50 `affected` records, 9 `fix_deferred`, 1
`will_not_fix`, without a recorded fixed Bookworm version. Five util-linux CVEs
repeat across eight binary packages. These remain installed; they are not all
removed build dependencies. Pip 25.0.1 separately has five MEDIUM/one LOW
records, with fixes through 26.2.0; it is install tooling, and the API image uses
an offline `--no-index` wheelhouse rather than an arbitrary package index.
Narrow triage examples, **not blanket exemptions**:
- Debian states [CVE-2023-45853](https://security-tracker.debian.org/tracker/CVE-2023-45853)
does not affect the built Bookworm zlib binaries because vulnerable minizip
code is not included. Other bundled minizip implementations are separate.
- [CVE-2026-8376](https://security-tracker.debian.org/tracker/CVE-2026-8376)
explicitly requires 32-bit Perl; this scan targets amd64.
- [CVE-2025-7458](https://security-tracker.debian.org/tracker/CVE-2025-7458)
requires crafted arbitrary SQLite SQL; the managed runtime uses PostgreSQL,
but alternate SQLite use must be reviewed.
- Perl's regex and Archive::Tar records need exact binary/module applicability
checks; vendor-deferred status alone is not a finding dismissal.
Only amd64 was scanned. arm64, newly built GovOPlaN API/Web layers and optional
dependency combinations remain unverified. Garage has no inventory; Redis,
HAProxy and PostgreSQL source-built executables, CPython and QEMU static
binaries need supplemental SBOM/source coverage. Zero detected OS findings is
not zero application vulnerabilities. Trivy also lacks Alpine 3.24 EOL metadata
and nginx CVE-2026-80256 detail; unknowns are retained. There were no runtime,
exploitability, secret, misconfiguration, malware or signature-policy checks.
Before lifting the runtime hold: approve and test maintained image-line changes
where necessary, fix or narrowly disposition findings with evidence, close
inventory gaps, scan both architectures and final runtime layers, then run
deployment/ingress smoke checks. Do not silently change base OS, use unpinned
`latest`, rebuild third-party images, or accept all HIGH/CRITICAL findings.
@@ -0,0 +1,139 @@
# Runtime image remediation follow-up — 8 September 2026
Canonical tracking: [Meta #52](https://git.add-ideas.de/GovOPlaN/govoplan/issues/52)
and [website #9](https://git.add-ideas.de/add-ideas/addideas-govoplan-website/issues/9).
This addendum supplements the [original audit](RUNTIME_IMAGE_AUDIT_2026-09-08.md);
it does not replace that historical baseline or lift either publication or
deployment gate. The immutable 0.1.45 release and `catalog-v0.1.45` are unchanged.
## Source change and candidate decisions
New installer specifications now use
`haproxy:3.2.23-alpine@sha256:6343ce34a132a5dceaa24767d739df2bd519f8f7c1079ae39e4821334e8eb42e`.
This is a patch update from 3.2.21 within the supported
[3.2 LTS branch](https://www.haproxy.org/), keeping Alpine 3.24.1.
The [publisher's exact build source](https://github.com/docker-library/haproxy/blob/7a5c202cde713867a737033dca56e7a211a8b8df/3.2/alpine/Dockerfile)
and scanned image configuration retain the non-root `haproxy` user,
`/usr/local/etc/haproxy/haproxy.cfg`, entrypoint and graceful-stop signal.
The [upstream changelog](https://www.haproxy.org/download/3.2/src/CHANGELOG)
includes HTTP parsing, TLS and memory-safety fixes in 3.2.22/3.2.23.
Existing specifications retain their explicit image, including an older pin;
this source change does not update a running installation.
| Candidate | OS | C / H / M / L / Unknown, per architecture | Disposition |
| --- | --- | --- | --- |
| HAProxy `3.2.23-alpine` | Alpine 3.24.1 | 0 / 0 / 0 / 0 / 0 | Installer source default updated; binary/runtime checks pending |
| nginx-unprivileged `1.30.4-alpine` | Alpine 3.24.1 | 0 / 0 / 0 / 0 / 0 | Candidate only; compatibility and website OS upgrade review pending |
| Caddy `2.11.4-alpine` | Alpine 3.23.5 | 1 / 38 / 41 / 12 / 23 | Not selected; all 39 C/H records have recorded fixes |
| Node `24-alpine` (24.20.0) | Alpine 3.24.1 | 0 / 6 / 11 / 12 / 0 | Not selected; major change and six fixable HIGH records |
Each row was scanned separately for **linux/amd64 and linux/arm64**, with the
same counts on both. Counts are package-vulnerability records, not distinct
CVEs or proven exploits. The [machine-readable evidence](runtime-image-candidates-2026-09-08.json)
contains exact index, platform-manifest, config and report digests, inventory
counts, scanner bounds and decisions. It is audit data, not an accepted release
manifest or an installer input.
nginx's candidate reference is
`docker.io/nginxinc/nginx-unprivileged:1.30.4-alpine@sha256:442753882674b49ae2c1de83ed67896131c0777f56df5005e356e62bc3f7e7ce`.
It inventories 70 Alpine packages including nginx/NJS, uses UID 101 and exposes
8080. The [upstream stable release](https://nginx.org/en/download.html) and
[security advisories](https://nginx.org/en/security_advisories.html) include the
1.30.4 fixes. The publisher retains its
[unprivileged port and temporary-path contract](https://github.com/nginx/docker-nginx-unprivileged).
For the website, this changes nginx 1.27.5 to 1.30.4, NJS 0.8.10 to 1.0.1 and
Alpine 3.21.3 to 3.24.1. These changes are explicit review items; the website
Dockerfile has not been changed. The GovOPlaN Web image still requires an
explicit verified `NGINX_IMAGE` build argument.
Caddy's candidate reference is
`docker.io/library/caddy:2.11.4-alpine@sha256:5f5c8640aae01df9654968d946d8f1a56c497f1dd5c5cda4cf95ab7c14d58648`.
Although this is the current
[official image line](https://raw.githubusercontent.com/docker-library/official-images/master/library/caddy),
its inventory still includes Go 1.26.3, `x/crypto` 0.52.0, `x/net` 0.55.0,
`x/text` 0.37.0 and gRPC 1.81.0. Recorded fixes include Go 1.26.6,
`x/crypto` 0.55.0, `x/net` 0.56.0, `x/text` 0.39.0 and gRPC 1.83.1; Alpine
findings also remain in c-ares, curl/libcurl and OpenSSL. The CRITICAL
`CVE-2026-56854` concerns `x/crypto/ssh` source-address enforcement. A module
record alone does not establish that this binary exposes that SSH path; exact
binary symbol/reachability analysis is still required for a disposition.
The [official Node image catalogue](https://raw.githubusercontent.com/docker-library/official-images/master/library/node)
still maps Node 22 Alpine to 22.23.2 and the previously scanned digest. Node 24's
candidate is
`docker.io/library/node:24-alpine@sha256:e67514e5d0f6c46656005e1b693b2ec9d52e80b641307de684d4a015ba7a4eaf`.
Its HIGH records remain in two OpenSSL packages and npm dependencies
`brace-expansion`, `ip-address` and `tar`; fixing the earlier critical tar
record alone is insufficient. The website builder stays on Node 22 pending
a reviewed build-tool remedy and a final builder scan.
## Method, verification and retained evidence
The existing Trivy 0.74.0 executable was rehashed against the previously verified
archive member: `d89bcc6510a267f11b773398cbf1be5520ce39f9e8b6633178c4487f05b7d791`.
The same schema-2 vulnerability database was used, updated
`2026-09-07T19:06:01.154199452Z`. No tool installation or database refresh occurred.
Index bytes matched both the registry digest header and Docker Hub publisher
metadata; both platform-manifest byte hashes matched the index. All eight
registry-only scans completed successfully with validated JSON, `--list-all-pkgs`,
`--scanners vuln`, an eight-minute/2GB image bound, an empty Docker configuration
and no inherited credentials. Exit zero means execution succeeded. Private
temporary paths and in-memory artifact cache isolated this follow-up from the
earlier scanner's artifact cache; its vulnerability database was read only.
Raw reports, logs, manifests, publisher metadata and the scanner script are in
`/home/zemion/.cache/govoplan-runtime-remediation.qfDSWHJh/`:
- `summary.json` SHA-256: `0d44390408ab35270e4430516f77bf11aa7877334eff2ef19e11e9a863fe5c56`.
- `frozen-images.json` SHA-256: `d0cb156f4a88998531ec55ab950067a3f1350ded648f07650c463af101dad467`.
- `scan_successors.py` SHA-256: `f64c69e06efc2ad7b5a657a25aa73f9ff586e3685737d525456bcecbe3ab5f07`.
Local retention is not permanent artifact hosting; preserve this evidence with
the eventual reviewed release. The JSON evidence records compressed registry
layer sizes; these are not expanded filesystem limits or final GovOPlaN sizes.
Installer regression checks cover the new generated image pin, legacy
specification fallback, preserved explicit images, generated topology and
configuration: `python -I -m unittest discover -s tests -p
test_deployment_installer.py` ran 45 tests successfully with one skip because
Core was not importable in that isolated test environment. The skipped Core
startup-configuration integration was subsequently rerun in the shared development
environment with Core available: all 45 installer tests passed with no skips,
including generated-environment startup validation. This is configuration
validation, not execution of the candidate image.
Both repositories passed `git diff --check`; the audit JSON and all eight
report hashes were checked against the retained evidence.
**Docker, Podman and HAProxy executables are unavailable on
this host**, so no image or HAProxy configuration was executed and no daemon was
installed. Publisher metadata and installer tests support the scoped source
patch; they do not establish binary or deployed compatibility.
## Gates that remain open
- Validate `haproxy -c` on generated local, existing-proxy and managed-ingress
configurations using the exact pinned image and target architectures. Run
bounded isolated checks without live mounts, secrets, privilege or external
network access. Then verify DNS discovery, readiness, forwarded headers,
replica routing and graceful termination in the intended runtime.
- Test the nginx candidate with both the website configuration and GovOPlaN
WebUI entrypoint/proxy configuration, including UID 101, writable temporary
paths, health paths, cache headers and static catalog bytes. Approve the
website nginx/NJS/Alpine version changes before changing its Dockerfile.
- Resolve Caddy, Node build-tool and all unchanged baseline dependencies with
updated publisher images or narrow reviewed applicability evidence. No
severity-wide exceptions or custom third-party rebuilds were introduced.
- Close the original source-built/static inventory gaps. HAProxy's 24-package
OS inventory still omits the source-built HAProxy executable. Node's npm
inventory still omits the Node executable/stdlib. Garage, CPython, Redis,
PostgreSQL and QEMU gaps are unchanged. Alpine 3.24 EOL metadata is still
missing from this scanner; zero findings is not complete coverage.
- Scan **final built** API/Web/website layers and the selected managed
dependencies on both architectures, then perform migration, worker,
readiness and ingress smoke checks. Record failure and unknown states.
Secrets, misconfiguration and image signature policy need separate checks.
- Obtain the website deployment host/operator and rebuild/restart authority,
preserving the exact immutable catalog/keyring/module-directory bytes and
verifying fresh public responses after an authorized rollout.
No images were built, executed, published or deployed; no running service,
release tag, signed manifest, CI image input or live infrastructure was changed.
@@ -0,0 +1,219 @@
# Security and performance follow-up — 8 September 2026
This follows the [original review](SECURITY_PERFORMANCE_REVIEW_2026-09-08.md)
and its post-release issue reconciliation. It describes new source work after
the frozen 0.1.45 release; it does not change published tags, packages, signed
catalogs or deployed images. Gitea remains the canonical state log.
## Implemented source slices
- [Core #297](https://git.add-ideas.de/GovOPlaN/govoplan-core/issues/297):
a shared disposable-process runner enforces wall/CPU/address-space/input/output
limits, bounded stderr, process-group cleanup and non-queuing per-process
admission. A private binary codec bounds decoding before allocating a full
object graph and preserves explicitly supported data types without pickle.
Read the owning Core `docs/BOUNDED_PROCESS_CONTRACT.md` before adding callers.
- Connectors XLSX parsing, Templates rendering, Files ZIP/TAR inspection and
extraction, and Dataflow reference previews/development execution now use
that boundary. Existing authorization, sessions, provider credentials,
idempotency and persistence remain in the parent. No unprotected inline
fallback is used. Each module contributes static EN/DE user/admin limits and
operational consequences through its manifest.
- Files snapshots authorized sources inside shared admission, validates private
staged members, and acknowledges each persisted member before decoding the
next. Numeric progress remains available. The acknowledgement is event-driven,
not a fixed sleep per member. Reads allocate by validated actual file size,
not by the configured ceiling. Failures reap children, clear private staging
and retain the existing transaction/blob cleanup and explicit retry behavior.
- Dataflow's normal reference preview formerly bypassed the backend wrapper;
it now enters the worker too. Nested source configurations cannot collide
merely because subflows reuse node IDs. Combined reference-source data is
checked before creating further columnar copies, while individual providers
retain their own authorized-read bounds. Staging/production still require
DuckDB; this change does not replace that separate backend.
- [Access #22](https://git.add-ideas.de/GovOPlaN/govoplan-access/issues/22):
current-password change, session/CSRF rotation, cross-tenant session and human
API-key revocation, and optional administrator-assisted recovery. Recovery
codes are hashed, single-use, expire after 15 minutes, require a current local
System owner and explicit identity verification, and recheck current account,
membership, tenant and issuer authority at redemption. A password change also
invalidates outstanding codes issued by that account for other people. Audit
evidence and validation/error responses do not contain passwords or codes.
External-provider and service-account rules remain separate.
- The Access UI provides first-login/required change, self-service change,
policy-aware sign-in help, public code redemption and eligible owner issuance.
Core consumes an optional lazy auth-action capability rather than importing
Access internals. The required-action gate fails closed if its UI is missing.
- [Workflow Engine #3](https://git.add-ideas.de/GovOPlaN/govoplan-workflow-engine/issues/3):
full-history lists batch pinned revisions, while new summary and bounded
step/event endpoints preserve authorization and explicit pagination. Existing
full-history responses are not silently truncated. Exact inbox total semantics
are retained and their counting cost is documented.
- [Meta #55](https://git.add-ideas.de/GovOPlaN/govoplan/issues/55):
shared version/planning helpers recognize the existing nested developer
package, not invented root metadata. All tag batches enforce trusted private
source ownership, registered origins and clean main/upstream state. Meta
batches additionally require exact composition and matching Core evidence.
Whole-batch preflight,
frozen source receipts, annotated immutable tags, object-pinned atomic
publication and post-effect remote checks are covered with temporary local
repositories. Hidden Git index flags, unsafe ancestry, alternates and changed
Git-directory identities are rejected. Selected version/composition metadata
must be tracked, so ignored files cannot describe bytes absent from a tag.
Applicable unselected Core WebUI inputs have bounded, frozen read receipts;
backend-only releases do not read them. The existing local module-candidate
exception remains intact. The weaker legacy mutation path was removed.
Canonical whole-package preview and receipt-bound apply now cover Meta's
version preparation too. Core must already match the target. Preparation
requires a separate trusted checkout, explicit out-of-run confirmation and
unchanged source/tooling receipts; it cannot rewrite the running operator.
Plans place Meta after Core and explain the manual preparation/publication
steps instead of promising a durable self-update. Ambiguous partial writes
require reconciliation, without automatic rollback or retry.
- [Runtime-image follow-up](RUNTIME_IMAGE_REMEDIATION_2026-09-08.md): eight new
registry-only scans cover four exact candidates on amd64 and arm64. New
installer specifications select the patched same-line HAProxy digest;
existing specifications retain their explicit image. Other candidates and
unresolved inventory/deployment gates remain visible, not blanket-approved.
## Verification record
Targeted checks include actual child execution, catastrophic regex CPU,
aggregate memory exhaustion, TAR extension metadata, noisy output, malformed
transport/staging data, Unicode allocation limits, cancellation/callback
failures, descendant cleanup, rollback and explicit retries. Local mixed-owner
composition tests completed nine real children, rejected six overlapping
requests as busy, observed at most one unreaped child and recovered all slots.
This is local admission evidence, not a target deployment load certification.
Workflow fixtures serialize 40 different pinned revisions with five SQL reads;
summary lists use one query for 40 ordinary rows. Exact inbox totals for
40/400/4,000 candidates used one query, with measured local costs approximately
0.011/0.057/0.492 seconds. These are fixture measurements, not production SLOs.
The broader Core API smoke suite exposed three stale campaign assertions.
All three failures were reproduced against the unchanged private frozen 0.1.45
sources. Updated fixtures verify recipient-summary projection, detailed payload
separation and explicit fenced recovery of a confirmed stopped runtime; observing
SENDING alone must not make a claim recoverable. All 76 smoke tests then passed.
No production Campaign behavior was changed to satisfy these tests.
The final release-tool suite passed 279 tests and 68 subtests, including
temporary local remotes and adversarial source/tag/receipt changes. Rechecking
the whole batch before effects is deliberately conservative: its repeated
filesystem/Git/remote work grows quadratically with batch size. It is not a
new unattended publication path or permission to execute unreviewed source.
Strict interface inventory now reports no unclassified endpoints and exact
contextual help for all 133 high-risk controls. Seventeen password browser cases
include actual F1 help from the restricted screen, empty workspace scopes,
EN/DE layouts, Unicode boundaries and no credential values in help URLs.
The initial production bundle remains within the unchanged limits (512,036
raw bytes and 162,415 gzip bytes; 1,713 gzip bytes below its ceiling), with
46 optional descriptors and no eager optional-module imports.
The focused checker now includes the new Core process, mixed-owner admission,
Access password, Templates and Files worker tests, the repaired campaign smoke
cases, and browser-side auth/password transport contracts. The full focused run
passed, including 63 production module/build permutations and all 230 browser
cases. Its two opt-in Datasources PostgreSQL cases were skipped in that run
and subsequently passed against the isolated real database described below.
The final Meta preparation gate was added after that full run and verified
with the owning release-tool suite and the focused release-gate command.
Manifest validation passed for all 72 modules. The full focused log is
`/mnt/DATA/tmp/govoplan-security-followup-20260908-focused.log`.
The first follow-up quick audit captured an unchanged 79-repository snapshot
in `/mnt/DATA/tmp/govoplan-security-followup-quick-20260908-7s8Sgh/`.
All four required scanners completed, with zero missing/execution reports;
all 168 report checksums and 163 machine-readable reports were validated.
Gitleaks found no secrets in all 79 histories and 79 worktrees. Local Semgrep
rules reported zero findings. Production Bandit reported 65 low and four medium
warnings, and production Ruff retained 54 warnings. The two added Bandit
warnings identify the new Core subprocess import and invocation: trusted
server-owned arguments, no shell, and the documented resource/process boundary
were reviewed; warnings remain visible. This is report-only evidence, not a
warning-free audit or a penetration test. A final snapshot follows the
cross-module declaration/contextual-help corrections and release-tool checks.
That final audit completed on 8 September, 05:59:4606:02:18 UTC, in
`/mnt/DATA/tmp/govoplan-security-final-quick-20260908-vAwQIh/`. All 79 start/end
source fingerprints were identical; all four scanners completed, all 168
registered report checksums matched, and all 163 JSON/SARIF reports parsed.
There were no missing reports or scanner execution errors. Semgrep and both
Gitleaks scopes again reported zero findings. Production counts were unchanged
from the first follow-up: Bandit 65 low/four medium and Ruff 54. Test-only
counts were Bandit 140 low/34 medium and Ruff 136. A separate frozen scan of
all ten changed Meta release/deployment Python files reported seven low Bandit
and four Ruff S603 warnings, with no execution errors. Its four argv-only
subprocess sites were reviewed; the preparation additions introduced no new
warnings. No findings were hidden or severity-wide exceptions added.
The audit manifest SHA-256 is
`a997b786239cd11443cb665d5f9041a968cc38f9d49171e68bb868bf2bd73310`;
its report-checksum list SHA-256 is
`dc590ca5b0a4e445019a05536d410226088d67b40d61cd7657bdef4a4eae56d8`.
The audit includes the eight committed feature/website source changes and the
final uncommitted Meta source. Only this evidence document was updated after
the source freeze ended; the final Meta commit and remote publication are
recorded in the linked Gitea issues, not inferred from local audit completion.
Fresh dependency audits are retained in
`/mnt/DATA/tmp/govoplan-dependency-final-20260908-d24LEK/`: all four full npm
lockfile audits (Core WebUI, Mail root/WebUI and website) report zero known
vulnerabilities. Installed Python auditing covers 137 distributions with zero
known vulnerabilities; 51 local GovOPlaN distributions lack PyPI advisory
coverage. Core's 46 linked packages are likewise not claimed covered by public
registry advisories. All 12 dependency-file hashes and the installed inventory
were unchanged. No packages were installed or automatically fixed.
Managed PostgreSQL 16.15 fixtures used private Unix sockets, synthetic roles
and databases, no TCP listener, per-case schemas and bounded SQL/lock waits.
Both previously skipped Datasources races passed. Twenty-one existing Access
password HTTP tests and four additional races passed on PostgreSQL: single-use
redemption, stale-session/password replacement, competing issuance, and issuer
password revocation during redemption. Four release/development migration checks
also passed for Access and Workflow, including credential preservation and
idempotent indexes. The four races are now owning opt-in Access regressions;
see `govoplan-access/docs/PASSWORD_RECOVERY_POSTGRES_TESTS.md`. These local
database checks do not certify a deployment, fleet load or external recovery
handover. With both explicit PostgreSQL test URLs enabled, the full Access suite
passed 122 tests and 18 subtests, and the full Datasources suite passed 57 tests,
without skips. Access retained 12 existing SQLite datetime-adapter warnings in
its separate SQLite migration cases. Both temporary PostgreSQL fixtures were
stopped and independently verified: no server process, private socket,
generated schema or synthetic cluster remains. Scripts, logs and shutdown
receipts are retained under `/home/zemion/.cache/govoplan-pg-security-20260908.RAUitg/`
and `/home/zemion/.cache/govoplan-pg-promoted-20260908.JZcIKL/`.
## Adoption and remaining gates
1. `AUTH_LOCAL_PASSWORD_RECOVERY_ENABLED` remains **false** by default. The
existing flag is still advisory until an operator explicitly adopts and
enables the complete recovery policy. Confirm who verifies identity and how
the one-time code is handed over; automated email recovery is not enabled.
Test first-login, lost-password, code expiry and administrator availability
in the target environment before enforcement.
2. Access migration `e9a2c5f8b1d4` adds recovery evidence; Workflow migration
`9e6b3f8a2c7d` adds summary-pagination indexes. Use normal backed-up upgrade
procedures and account for index-build cost. No manual live migration or
server restart was performed during this work. The user's existing devserver
has automatic reload, so live schema state must not be assumed unchanged.
3. Release preparation must assign new source/package versions and require a
Core version containing the new worker/auth contracts in the affected module
metadata, including matching WebUI assets. The old immutable release must
not be relabelled or treated as containing these APIs.
4. Resource limits are not an arbitrary-code, filesystem or network sandbox.
Admission is per API/worker process, not fleet-wide. Validate Linux/cgroup
memory, disk quotas, process counts, cancellation and legitimate large-file
workloads on the intended runtime before increasing concurrency. Core #297
retains this target-evidence follow-up.
5. Meta #52 and website #9 retain runtime-image/publication/deployment holds.
Docker/Podman/HAProxy executables are unavailable here. Final built images,
binary/source inventories, ingress behavior, migration/readiness/worker
smoke checks and the website's target/operator authority remain outstanding.
Zero findings in a detected package inventory is not full image coverage.
No real messages, IMAP appends, password resets, provider operations or deployment
actions were used as test fixtures. Development tests use temporary databases,
private temporary files, mock transports and managed test-browser servers.
@@ -0,0 +1,207 @@
# Security and performance review — 8 September 2026
Coordinated status: [Core #296](https://git.add-ideas.de/GovOPlaN/govoplan-core/issues/296).
This records a workspace-wide automated scan, targeted manual boundary review,
and a verified implementation pass. It is not a penetration test, an exhaustive
line-by-line review, or a security certification. The audit was completed on
local, unpublished changes, preserving existing worktree changes. Subsequent
release preparation/publication is tracked in
[GovOPlaN #51](https://git.add-ideas.de/GovOPlaN/govoplan/issues/51) and the
[0.1.45 release notes](../releases/0.1.45.md).
## Implemented findings
| Area | Finding and change | Evidence / ownership |
| --- | --- | --- |
| Authentication — high | Preserve service-account provenance and current scope ceilings instead of recalculating them as ordinary membership permissions. Tenant API keys cannot retain canonical system permissions or unsafe wildcard grants. | Previously failing isolated regressions; [Access #21](https://git.add-ideas.de/GovOPlaN/govoplan-access/issues/21). |
| Authentication — medium | Warm-cache API keys must follow the same explicit-header credential rules as cold authentication. A session cookie cannot turn an API key into a session credential. | Regression covering source-dependent authentication. |
| Browser authority/cache — medium | Clear reusable data on auth changes and write settlement; fence late 200/304 writes and obsolete 401 side effects. Honor server no-store/no-cache and explicit fresh-read requests. Interactive login/logout remove retained automation keys that could shadow cookie-session identity. | 23 real-client regressions. Unchanged settings keep their object identity, preventing profile-fetch loops. Core `docs/API_CLIENT_CACHE_CONTRACT.md`; owning Access EN/DE session/field documentation. |
| Spreadsheet resource exhaustion | Validate actual XLSX coordinates before openpyxl traversal; ignore misleading declared dimensions; count blank row gaps toward the existing limits. | [Connectors #18](https://git.add-ideas.de/GovOPlaN/govoplan-connectors/issues/18), 13 tests and 2 subtests. |
| Template resource exhaustion | Enforce the existing 5 MiB output budget during substitution and item construction, including UTF-8, HTML escaping and separators. | [Templates #7](https://git.add-ideas.de/GovOPlaN/govoplan-templates/issues/7), full 20 tests; independent 3,000-case valid-output comparison. |
| Archive resource exhaustion | Inspect regular TAR member limits before traversing payloads. Limit archive paths to 4,096 UTF-8 bytes / 128 components and count derived directories against entry limits. | [Files #46](https://git.add-ideas.de/GovOPlaN/govoplan-files/issues/46), 55 archive and 15 documentation tests. Extension-header decoding still needs stronger isolation. |
| Dataflow resource exhaustion | Reject LPAD/RPAD target lengths above the existing 1,000,000-byte preview budget before fill evaluation/allocation. Preserve final serialized-byte checks. | [Dataflow #22](https://git.add-ideas.de/GovOPlaN/govoplan-dataflow/issues/22), full 104 tests and 39 subtests; 7 new guard tests independently rerun. |
| Docs performance / defense in depth | Batch revision reads per request, avoid loading pending draft bodies for readers, and validate tenant/entry/publication consistency while retaining owner/audience checks. | [Docs #22](https://git.add-ideas.de/GovOPlaN/govoplan-docs/issues/22), full 39 tests. |
| Notifications performance / defense in depth | Batch delivery-attempt loading while preserving recipient checks and rejecting inconsistent attempt references, including already-loaded relationships. | [Notifications #6](https://git.add-ideas.de/GovOPlaN/govoplan-notifications/issues/6), full 23 tests. |
| Session-list performance | Apply active/expiry predicates and the existing 100-row cap in SQL, before loading session history. | Query-shape regression in Access. |
| Reporting correctness | Use structural bind-name suffixes for recursive calculated measures, preserving valid dotted/hyphenated public keys and parameter uniqueness. | [Reporting #10](https://git.add-ideas.de/GovOPlaN/govoplan-reporting/issues/10), full 29 tests. |
| Audit hygiene | Redact Gitleaks logs and machine reports on current, history and legacy scanner paths. | 13 audit-wrapper tests enforce the flag. |
All changed module workflows/limits have owning EN/DE DocumentationTopic updates.
Independent review found no concrete regression in the backend changes.
## Measured performance changes
These are SQL-query counts in isolated 40-item fixtures, not production latency
or throughput claims. Authorization is still evaluated for each request.
| Projection | Before | After |
| --- | ---: | ---: |
| Docs reader entries | 41 SELECTs | 2 SELECTs |
| Docs editor entries | 81 SELECTs | 2 SELECTs |
| Notification list with attempts | 41 SELECTs | 2 SELECTs |
The Docs 401-entry batching regression uses 3 SELECTs. Resource guards reject
oversized work before the formerly expensive allocation/traversal. This does
not make every legitimate upload or campaign faster. Honoring no-cache can
increase server validation requests; ETags still avoid retransmitting unchanged
bodies. That authorization/freshness trade-off is deliberate.
The original audit snapshot measured 517,380 initial JavaScript bytes and
164,119 gzip bytes. Release preparation's pure-defaults split reduces this to
516,730 initial bytes and 163,908 gzip bytes. Restoring the missing Tasks
descriptor then measures 516,987 initial / 163,976 gzip bytes with all 46 module
descriptors lazy, within the unchanged 524,288 / 164,128 caps. The gzip margin is still small; future
startup work should reduce eager dependencies, not raise the cap automatically.
The full 209-case browser suite passed before the split, followed by 13 focused
browser checks after it. Radon recorded 238 rank-D-or-higher entries; complexity
is a review-priority signal, not a performance measurement.
## Dependency remediation
Core's full npm audit went from 30 affected package entries to zero. Most initial
entries were transitive effects of the same Tiptap advisory, not 30 independent
application exploits. The website went from two affected entries to zero; both
Mail lockfiles also report zero.
- Tiptap packages are aligned at 3.31.3, with direct minimum ranges raised to
3.30.4 in both development and release manifests, with a parity regression.
Added an actual installed-library prototype-attribute regression for
the [maintainer's security advisory](https://github.com/ueberdosis/tiptap/security/advisories/GHSA-cp6q-959q-f8rh).
- Core now resolves xmldom 0.9.12, browserslist 4.28.9 and nanoid 3.3.18.
The website's affected browserslist/nanoid dependencies are patched too.
- Development/audit requirements now require pip >=26.2; the local development
environment uses 26.2.1. The installed audit originally flagged
[CVE-2026-13346](https://github.com/advisories/GHSA-qwm4-qh6w-59xr), requiring an
attacker-controlled package index. This is an installation-tool vulnerability,
not evidence of an exposed application endpoint.
The final installed Python audit enumerated 188 distributions: 137 were
auditable with zero known vulnerabilities, and 51 local distributions were not
available in PyPI. Those skips are covered by source review, not by a claim of
dependency-advisory coverage. Production images and every optional dependency
combination were not independently resolved or scanned.
## Scan coverage and limitations
Evidence directory:
`/mnt/DATA/tmp/govoplan-security-performance-20260908-gsk8jn/`.
The final `final-quick/manifest.json` captures 79 repositories, tool versions,
start/end repository fingerprints, report checksums, 168 report artifacts and
163 validated JSON/SARIF reports. It records an unchanged workspace, complete
coverage for its four required scanners, no execution errors and no missing
reports. It ran in report-only mode: exit zero does **not** mean zero warnings.
- Final production Bandit: 447,008 Python lines; 67 warnings (63 low, 4 medium),
no high findings. Ruff security rules: 54 warnings. SQL-construction warnings
were reviewed against identifier/operator validation and bound values in
DuckDB/Reporting; no injection fix was warranted there. XML import warnings
were checked: feed/BPMN input parsing uses defusedxml; stdlib imports support
types/output construction. Operator-owned fenced-run argv is not a public
arbitrary-command endpoint. Xrechnung output buffering remains a follow-up.
Assertions and error-swallowing markers remain review/maintenance warnings,
not proof that all such code is harmless.
- Final local Semgrep rules: no findings. The broader OWASP-rule pass applied
272 rules to 4,169 tracked targets. Its seven warnings recommended weakening
owner-only 0700 permissions; they were rejected as false positives. One
Calendar rule timeout was rerun with a 60-second budget: zero findings/errors.
Bash and conformance TypeScript checks passed despite two scanner-specific
parser limitations. Ignored/dependency/generated paths are not a complete
line-by-line source audit.
- Gitleaks: 79 Git histories plus 79 worktrees, 158 redacted reports, zero
detected secrets. This does not establish that deployed credentials are safe
or that formerly exposed credentials have been rotated.
- Tool versions included Semgrep 1.176.1, Bandit 1.9.4, Ruff 0.15.21 and
Gitleaks 8.30.1. The downloaded Gitleaks binary archive matched the official
release SHA-256 before execution.
- The containerized full-toolbox path could not access Docker's daemon. Its
full-mode Trivy/misconfiguration and additional OSV scans were **not** run.
A subsequent [registry-only runtime image audit](RUNTIME_IMAGE_AUDIT_2026-09-08.md)
successfully scanned nine pinned candidates and two same-minor successors
for amd64 without Docker. It found unresolved vulnerabilities and inventory
gaps; runtime publication is held. This does not complete full-toolbox,
arm64, final-runtime-image or deployment coverage.
No live application probes, database changes, file operations, mail sends,
IMAP appends, imports, notification delivery, deployments, commits or pushes
were performed. Browser tests used isolated mocked fixtures. Package installs,
builds and temporary audit-tool installation were local development operations.
## Verification and remaining work
- 209/209 browser conformance tests pass; production Core/website builds,
conformance TypeScript, 24 Core client/dependency regressions, 4 real-client
Files reload checks, and 72/72 manifest checks pass.
- Access's full 91-test suite passed before the final documentation-only update;
the final documentation suite passed all 4 tests. Other module counts appear
above. The new authentication/resource tests include demonstrated pre-fix
failures rather than only structural assertions.
- The original focused workspace run stopped at the institutional
governance/Portal fixture (`tests/test_institutional_governance_journey.py:223`,
`IndexError`). Release preparation fixes its mixed clocks using the existing
temporal context, retaining validity-boundary exclusions; 7 journey tests and
ambient-year checks pass. Tracked in
[Meta #50](https://git.add-ideas.de/GovOPlaN/govoplan/issues/50).
- Campaign's apparent host-path issue was ruled out by existing tracked
API/build/snapshot guards and 11 passing tests under normal initialization.
Release preparation fixes the standalone import cycle through a deferred
resolver import without changing validation rules. Fresh-process coverage,
all 11 path tests and Campaign's full 611-test suite pass.
Next coordinated work:
1. [Hard resource isolation — Core #297](https://git.add-ideas.de/GovOPlaN/govoplan-core/issues/297):
regex CPU, aggregate allocation, TAR extension metadata, bounded workers and
cancellation, followed by production-like concurrent load tests.
2. [Forced password change/recovery — Access #22](https://git.add-ideas.de/GovOPlaN/govoplan-access/issues/22):
the current flag is advisory only. Do not enable enforcement without a usable
local-password/recovery flow and external-provider rules.
3. [Bound subprocess output — Xrechnung #2](https://git.add-ideas.de/GovOPlaN/govoplan-xrechnung/issues/2):
enforce the existing 2 MiB limit while draining stdout/stderr, not afterwards.
4. [Workflow revision batching/history projection — Workflow Engine #3](https://git.add-ideas.de/GovOPlaN/govoplan-workflow-engine/issues/3):
batch evidence lookups; separately define explicit history pagination and
authorized-total semantics. Docs/notification history volumes also remain.
5. Resolve the [runtime image audit](RUNTIME_IMAGE_AUDIT_2026-09-08.md) findings
tracked in [Meta #52](https://git.add-ideas.de/GovOPlaN/govoplan/issues/52)
and coverage gaps before lifting its publication hold; complete deployment
audits, review exposed development credentials and worker quotas, and
benchmark realistic tenant sizes/concurrency. The sanctions
transport's fixed HTTPS/redirect allowlist is not a demonstrated arbitrary-URL
issue, but migration to Core's pinned egress transport remains desirable.
Operational compatibility: tenant keys relying on accidental system/wildcard
permissions must be corrected rather than weakening the guard. Extreme sparse
spreadsheets, overly deep/long archive paths and oversized padding intermediates
can now fail early with diagnostics. No stored documents or configurations were
deleted or silently migrated.
## Post-release follow-up — 2026-09-08
The findings and scanner counts above describe the original audit snapshot.
The following source fixes are subsequent to the frozen `0.1.45` composition;
they do not change its immutable tags or published package bytes.
- [Xrechnung #2](https://git.add-ideas.de/GovOPlaN/govoplan-xrechnung/issues/2)
now enforces the existing shared 2 MiB stdout/stderr limit during execution
and kills/reaps the direct validator on overflow, timeout or cancellation.
Report reads are bounded to 16 MiB plus one probe byte before interpretation.
The 30-test module suite passes; noisy-child and report-read regressions were
also demonstrated to fail against the previous source. Owning EN/DE static
documentation is updated. POSIX pipe capture is required; disk quotas,
descendant isolation and process-level CPU/memory limits remain separate work.
- [Meta #54](https://git.add-ideas.de/GovOPlaN/govoplan/issues/54) now preserves
the last command's failure status after exhausted installer retries. Twelve
isolated stage/scenario combinations cover every retry call site, success,
backoff and caller termination under `set -e`. The test is included in the
focused checks and installer CI. See the bilingual
[installer retry note](../operations/WEBUI_RELEASE_DEPENDENCY_RETRIES.md).
These are unreleased follow-up source changes, not a new runtime release or
deployment. The runtime-image hold under Meta #52 remains in force; the
historical peer-dependency workaround still needs its separate review.
Further implementation and adoption gates are tracked in the
[security follow-up](SECURITY_FOLLOWUP_2026-09-08.md), including disposable
parsing/execution workers, opt-in password recovery, workflow read projections
and the newer runtime-image evidence. The original scanner counts above remain
historical and are not silently replaced by later test results.
@@ -0,0 +1,305 @@
{
"schema_version": 1,
"purpose": "Audit evidence only; not a release manifest or active installer configuration.",
"observed_at": "2026-09-08T03:56:47.666808+00:00",
"runtime_publication_held": true,
"website_deployment_held": true,
"scan_execution_complete": true,
"coverage_complete": false,
"scanner": {
"name": "Trivy",
"version": "0.74.0",
"binary_sha256": "d89bcc6510a267f11b773398cbf1be5520ce39f9e8b6633178c4487f05b7d791",
"database_metadata": {
"Version": 2,
"NextUpdate": "2026-09-08T19:06:01.154199291Z",
"UpdatedAt": "2026-09-07T19:06:01.154199452Z",
"DownloadedAt": "2026-09-07T23:30:53.198039224Z"
},
"source": "remote",
"scanners": [
"vuln"
],
"list_all_packages": true,
"images_executed": false,
"existing_docker_credentials_used": false,
"timeout": "8m",
"maximum_image_size": "2GB"
},
"evidence": {
"private_directory": "/home/zemion/.cache/govoplan-runtime-remediation.qfDSWHJh",
"summary_sha256": "0d44390408ab35270e4430516f77bf11aa7877334eff2ef19e11e9a863fe5c56",
"frozen_images_sha256": "d0cb156f4a88998531ec55ab950067a3f1350ded648f07650c463af101dad467",
"scanner_script_sha256": "f64c69e06efc2ad7b5a657a25aa73f9ff586e3685737d525456bcecbe3ab5f07"
},
"candidates": [
{
"name": "haproxy",
"image": "docker.io/library/haproxy:3.2.23-alpine@sha256:6343ce34a132a5dceaa24767d739df2bd519f8f7c1079ae39e4821334e8eb42e",
"disposition": "source_default_updated_binary_runtime_verification_pending",
"platforms": [
{
"platform": "linux/amd64",
"manifest_digest": "sha256:0666a2c2f41d341084ed2da85392b48cdcd766adfa28231f31305724ed5c6ea5",
"config_digest": "sha256:9621d75e50a8f26d3738ae3cdbf15e98c6aa5cd48e6baca0699492de502156f5",
"compressed_layer_bytes": 20516844,
"scan_exit_code": 0,
"os": {
"Family": "alpine",
"Name": "3.24.1"
},
"inventory": [
{
"type": "alpine",
"packages": 24
}
],
"counts": {
"CRITICAL": 0,
"HIGH": 0,
"MEDIUM": 0,
"LOW": 0,
"UNKNOWN": 0
},
"fixable_high_critical": 0,
"unique_cves": 0,
"report_sha256": "40cfc3db74e29f09723f38698660ccdd50ac935c8d6e1e75c6e0555b3e9361fb",
"log_sha256": "46881f695780f89c037d5b0b4dc0ded9ea4e459077c7658d80b786efc5754084"
},
{
"platform": "linux/arm64",
"manifest_digest": "sha256:cd20b9dc6b4713956a2a043997001a1948167d345e7c8d5bd5ff2e667166651f",
"config_digest": "sha256:19796bff8905d4a46c9463c576203b20cac8984d41b7b01ea9cbff55e8422c34",
"compressed_layer_bytes": 20970772,
"scan_exit_code": 0,
"os": {
"Family": "alpine",
"Name": "3.24.1"
},
"inventory": [
{
"type": "alpine",
"packages": 24
}
],
"counts": {
"CRITICAL": 0,
"HIGH": 0,
"MEDIUM": 0,
"LOW": 0,
"UNKNOWN": 0
},
"fixable_high_critical": 0,
"unique_cves": 0,
"report_sha256": "0e1326c58abf358d592fa55c94333228ce1094edf4e489fcc4ece6e32d3320f6",
"log_sha256": "397c2fbf1044cf50733d68b4b9cc4eb68211d96f1f302d5e9f8af0d11fb0de1c"
}
]
},
{
"name": "nginx-stable",
"image": "docker.io/nginxinc/nginx-unprivileged:1.30.4-alpine@sha256:442753882674b49ae2c1de83ed67896131c0777f56df5005e356e62bc3f7e7ce",
"disposition": "candidate_pending_compatibility",
"platforms": [
{
"platform": "linux/amd64",
"manifest_digest": "sha256:b8c179cd3c2ae222a873dd59fbae240fadc03836cae5198afc9e9c19919c3880",
"config_digest": "sha256:8b5953dae38d27a76bca22373bb920fd6ce8d9d7da21578d2926e678002de8a0",
"compressed_layer_bytes": 25526590,
"scan_exit_code": 0,
"os": {
"Family": "alpine",
"Name": "3.24.1"
},
"inventory": [
{
"type": "alpine",
"packages": 70
}
],
"counts": {
"CRITICAL": 0,
"HIGH": 0,
"MEDIUM": 0,
"LOW": 0,
"UNKNOWN": 0
},
"fixable_high_critical": 0,
"unique_cves": 0,
"report_sha256": "3ad164dae3cdf891b12e41451b8a8e65a5e1688824a7c01d848e1274363e6bf9",
"log_sha256": "0f99ff3d9b4396de48655bf8299df30c14ba0c579f480d37baa7d2f6a4c11f1d"
},
{
"platform": "linux/arm64",
"manifest_digest": "sha256:b6742a0cbd749add25346658991c3da06a8e38796949df120fed78db8c512576",
"config_digest": "sha256:4d8b10f5d2ff99e7aa5f161930d8a4693a86a26f288ec8c0d4cd47f2ef5af179",
"compressed_layer_bytes": 25892370,
"scan_exit_code": 0,
"os": {
"Family": "alpine",
"Name": "3.24.1"
},
"inventory": [
{
"type": "alpine",
"packages": 70
}
],
"counts": {
"CRITICAL": 0,
"HIGH": 0,
"MEDIUM": 0,
"LOW": 0,
"UNKNOWN": 0
},
"fixable_high_critical": 0,
"unique_cves": 0,
"report_sha256": "506fdeb97525ed8e4d9ae538c42bab7aa2217f662a7135f0f12d16d20410c7a6",
"log_sha256": "c41ef9ea091d00f18c7a097404672591bee85e7477ae17d8f5ca771d8e42b2bb"
}
]
},
{
"name": "caddy",
"image": "docker.io/library/caddy:2.11.4-alpine@sha256:5f5c8640aae01df9654968d946d8f1a56c497f1dd5c5cda4cf95ab7c14d58648",
"disposition": "not_selected_remaining_fixable_findings",
"platforms": [
{
"platform": "linux/amd64",
"manifest_digest": "sha256:98eb57d882ccd5213d1688764db10c1ca2c58a1ca3a6717a3411ad798f7a423a",
"config_digest": "sha256:af555904a0961945f16bb323a501457b13a4f7e9bde969b145b97da80b38ecbe",
"compressed_layer_bytes": 23907283,
"scan_exit_code": 0,
"os": {
"Family": "alpine",
"Name": "3.23.5"
},
"inventory": [
{
"type": "alpine",
"packages": 32
},
{
"type": "gobinary",
"packages": 146
}
],
"counts": {
"CRITICAL": 1,
"HIGH": 38,
"MEDIUM": 41,
"LOW": 12,
"UNKNOWN": 23
},
"fixable_high_critical": 39,
"unique_cves": 68,
"report_sha256": "e483352a1d5b9b97950dcf92e4dfcf4600f5b09306af3d0640b10ca229a07779",
"log_sha256": "9cd599d6dd8c101b421fdeb57036cec1f69f815d765a8bf1f850ec60061036f4"
},
{
"platform": "linux/arm64",
"manifest_digest": "sha256:1172d4213087d3fc30bafc7ff2c2896180eb0c41ff7f75f315568fb36cabdcba",
"config_digest": "sha256:6b08c1b9858ca9a7d99c1da13c3695081e0e604c6cf214ca26a7ce0e2c4fd9b4",
"compressed_layer_bytes": 22722712,
"scan_exit_code": 0,
"os": {
"Family": "alpine",
"Name": "3.23.5"
},
"inventory": [
{
"type": "alpine",
"packages": 32
},
{
"type": "gobinary",
"packages": 146
}
],
"counts": {
"CRITICAL": 1,
"HIGH": 38,
"MEDIUM": 41,
"LOW": 12,
"UNKNOWN": 23
},
"fixable_high_critical": 39,
"unique_cves": 68,
"report_sha256": "20e04d820e3b27d57575ea177e523e23109fd83344cdf57e184a4d2fad27e803",
"log_sha256": "a1d9c37aa7948c137db64040d95e5930c5859a8acd71ac5bc41ff168e2b270c5"
}
]
},
{
"name": "node-lts",
"image": "docker.io/library/node:24-alpine@sha256:e67514e5d0f6c46656005e1b693b2ec9d52e80b641307de684d4a015ba7a4eaf",
"disposition": "not_selected_remaining_fixable_findings",
"platforms": [
{
"platform": "linux/amd64",
"manifest_digest": "sha256:4caaaf42195bcd6f6f3559a413b20cb8f8ad089e231ee874cf7701643966689f",
"config_digest": "sha256:ee289c69ed1ac50a5a042112ea97f132800e2dd53e832da27784f00e45b3289c",
"compressed_layer_bytes": 58486244,
"scan_exit_code": 0,
"os": {
"Family": "alpine",
"Name": "3.24.1"
},
"inventory": [
{
"type": "alpine",
"packages": 18
},
{
"type": "node-pkg",
"packages": 146
}
],
"counts": {
"CRITICAL": 0,
"HIGH": 6,
"MEDIUM": 11,
"LOW": 12,
"UNKNOWN": 0
},
"fixable_high_critical": 6,
"unique_cves": 19,
"report_sha256": "c927d995dde700c92027f6328dc6c273f0f1445cd8154301480757cb962e02b2",
"log_sha256": "c7dc1900cbe39c9f91e228b2770bcbd394ec2914d2b3879478295fc7f8f77ab3"
},
{
"platform": "linux/arm64",
"manifest_digest": "sha256:d3724e44ee368606d753e0027eb8d2a94fc1f275e5d9e4620178a12edb655f5f",
"config_digest": "sha256:722cc1507731edf58a4c0bc3e29553c44ce5774d0849242c1b237abc79926a0a",
"compressed_layer_bytes": 58935654,
"scan_exit_code": 0,
"os": {
"Family": "alpine",
"Name": "3.24.1"
},
"inventory": [
{
"type": "alpine",
"packages": 18
},
{
"type": "node-pkg",
"packages": 146
}
],
"counts": {
"CRITICAL": 0,
"HIGH": 6,
"MEDIUM": 11,
"LOW": 12,
"UNKNOWN": 0
},
"fixable_high_critical": 6,
"unique_cves": 19,
"report_sha256": "e5f7799761f23cefc36929381b4186eeb3afb46a2a559c789d12a7eea5dc30d0",
"log_sha256": "fd24671f0da4d3b20dc8bcc2718531e6f6e19155d49bed15958965e21dd10813"
}
]
}
]
}
@@ -61,6 +61,12 @@ 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
@@ -69,6 +75,12 @@ 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,
@@ -84,6 +96,11 @@ 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
@@ -106,6 +123,10 @@ deployment tiers.
| 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:
@@ -131,6 +152,11 @@ 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.
@@ -170,4 +196,3 @@ institutional journey and can answer:
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,7 +11,7 @@ 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
@@ -39,6 +39,49 @@ 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
@@ -113,6 +156,17 @@ journey needs and supplies contracts shared by all five stages.
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
+100
View File
@@ -0,0 +1,100 @@
# GovOPlaN Roadmap
## Purpose
GovOPlaN should become the connective, governance-aware operating layer of an
institution: people complete services and work without learning the module
graph, while the institution can explain authority, policy, source data,
effects, evidence, and recovery.
This is the concise product roadmap. It states durable outcomes and sequence,
not release dates or issue state. Use [Strategy Status](STRATEGY_STATUS.md) for
the current reconciliation and Gitea issues for active work. The
[detailed connected-platform vision](reference/CONNECTED_GOVERNANCE_PLATFORM_ROADMAP.md)
retains stakeholder perspectives, configuration archetypes, and the complete
outcome-story catalogue.
## Product Promise
GovOPlaN will:
1. model institutional context, responsibility, authority, and time;
2. turn incoming information into owned, reviewable human and machine work;
3. connect native and external systems without obscuring the source of truth;
4. preserve decisions, effects, records, corrections, and recovery evidence;
5. support digital, assisted, paper, message, calendar, and system channels as
paths through the same governed work; and
6. package successful configurations so institutions can adopt them without
code forks or loss of local autonomy.
It will not replace every specialist system, copy all data into one master
database, infer authority from membership, or claim production maturity from
repository breadth.
## Outcome Horizons
| Horizon | Outcome | Completion evidence |
| --- | --- | --- |
| Trustworthy baseline | A pinned composition can be installed, upgraded, operated, explained, and recovered. | Signed artifacts, clean install/upgrade, provider failure tests, restore drill, coherent UI, and target evidence |
| Connected work | Intake becomes accountable work with context, assignment, review, communication, and evidence. | One digital and assisted service reaches a decision and eAkte without losing responsibility or state |
| Reusable products | Complete service, communication, and data outcomes ship as governed configuration packages. | Two materially different deployments adapt packages without code forks |
| Institutional assurance | Records, transparency, privacy, risk, regulated review, and reporting connect to real operations. | A consequential decision can be reconstructed, corrected, retained, and disclosed under policy |
| Federated ecosystem | Autonomous installations exchange signed data and configuration across explicit trust boundaries. | Paired-instance exchange, reconciliation, supported deployment profiles, and independent evidence |
## Current Sequence
The sequence is outcome-led. Shared foundation work enters when one of these
proofs needs it.
1. **Enforce the platform quality contract.** German is the reference locale;
help, accessibility, temporal browsing, purpose-aware access, retention,
institutional context, optional-module combinations, and recovery behavior
become measurable release gates.
2. **Complete governed communication.** Prove recipient selection, Campaign,
Files, Mail, function-bound Postbox delivery, acknowledgement, uncertain
outcomes, correction, filing, and recovery against a named target.
3. **Complete the monthly-data and sanctions journey.** Acquire immutable
source snapshots, validate and reconcile data interactively, preserve
lineage and review, publish reports and files, and deliver accepted results.
4. **Complete inclusive service to decision.** Accept digital or assisted
input, establish actor and purpose, persist human handoffs, decide, notify,
and reconstruct the exact eAkte under current authorization.
5. **Complete discovery and external coexistence.** Finish native PostgreSQL
search coverage, prove reauthorization and reindexing, then prove one
external product connector and one paired GovOPlaN federation exchange.
6. **Prove production operation.** Complete multi-host, provider, restore,
accessibility, volume, key-custody, and independently signed target
evidence before raising maturity claims.
## Continuous Foundation
Every journey applies the same boundaries:
- modules cooperate through versioned Core contracts and typed references;
- permissions, policy, institutional context, purpose, and current authority
are evaluated before presenting or acting on data;
- requested actions, durable intent, observed effects, unknown outcomes,
retries, reconciliation, and correction remain distinct;
- Workflow Engine coordinates stable module-owned actions and human handoffs;
it does not become a second owner of domain state;
- Files owns managed bytes, Records owns institutional filing and retention,
and source systems retain explicitly declared authority;
- focused views and product areas reduce interface complexity without granting
access or hiding material consequences;
- configuration packages include terminology, forms, policies, workflows,
views, reports, providers, documentation, migration, and evidence; and
- maturity advances from scaffold to vertical slice, reference-ready,
supported, and LTS only with evidence appropriate to each claim.
## Decision Rule
A roadmap item should answer all of the following before implementation:
1. Which real journey and actor outcome does it improve?
2. Which module or external system owns each object and source of truth?
3. Which institutional, temporal, purpose, and policy context applies?
4. Which effects, evidence, retention, failure, and recovery states result?
5. Which package and target evidence will prove the outcome?
If those answers are missing, retain the idea in the Product Input Register or
Gitea discovery work rather than opening an unbounded implementation program.
@@ -4,9 +4,10 @@
| Field | Value |
| --- | --- |
| Reconciled on | 2026-08-05 |
| 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 [Connected Governance Platform Roadmap](CONNECTED_GOVERNANCE_PLATFORM_ROADMAP.md) |
| Stable direction | [Platform Core Ideas](PLATFORM_CORE_IDEAS.md) and [Roadmap](ROADMAP.md) |
| Collected product input | [Product Input Register](PRODUCT_INPUT_REGISTER.md) |
| Delivery source | Gitea issues |
This is the only prose source for current cross-product status. It is a
@@ -15,11 +16,14 @@ evidence remain authoritative for specific maturity claims.
## Portfolio Snapshot
- 65 source module manifests were loadable and architecture-declared.
- 47 modules declared `vertical_slice`; 18 declared `scaffold`.
- 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 live portfolio had 133 open issues, including 39 priority-P1 items.
- 117 open issues had no milestone, so issue labels do not yet express a
- 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
@@ -30,19 +34,21 @@ document.
## Interface And Contract Evidence
The 2026-08-05 source inventory found:
The 2026-08-17 source inventory found:
- 1,247 UI fields and 1,220 UI actions;
- 7,929 stable interface declarations with no duplicate IDs;
- 39 frontend routes and 872 backend endpoints;
- 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,247 fields with a resolvable F1 context; 1,087 remain candidates for
richer field-specific content beyond page/module fallback;
- 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;
- 260 module information-governance dimensions classified as `contract_only`.
- 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.
@@ -67,24 +73,32 @@ prevent a reference-ready claim.
Services, Forms, Forms Runtime, Cases, Parties, Mandates, Approvals, Committee,
Voting, Decisions, Portal, Postbox, and Audit have an executable service-to-
decision fixture. Browser-complete assisted intake, production identity,
records, delivery, and target evidence remain.
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 journeys still need real connectors, complete interactive
reconciliation, publication/export, and guided handoff evidence.
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 incomplete | Users still navigate modules and remember unfinished work | Task/work inbox, resumable guided journey, deadlines and handoffs |
| Records/eAkte shallow | Institutional memory and disposition remain fragmented | Native record lifecycle plus external DMS/archive overlay |
| 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 |
@@ -97,9 +111,14 @@ reconciliation, publication/export, and guided handoff evidence.
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 one digital and assisted service-to-decision journey with an eAkte.
5. Add native PostgreSQL search coverage for the objects used by those
journeys; keep OpenSearch optional.
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.
@@ -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
@@ -160,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.
@@ -194,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,30 +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 strategy status](STRATEGY_STATUS.md)
- the [pinned Campaign 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
@@ -42,14 +44,15 @@ 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 strategic review](STRATEGIC_REVIEW_2026-08-05.md) to understand
- Use the [dated strategic review](../../archive/2026-08/STRATEGIC_REVIEW_2026-08-05.md) to understand
why the current convergence and reference-journey order was chosen.
### 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 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 |
@@ -110,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,
@@ -266,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.
@@ -950,7 +953,7 @@ first analytical product prove Horizons 2 and 3; governed BI adds assurance and
ecosystem capabilities across Horizons 35; collaborative documents combine
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
@@ -976,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.
@@ -1219,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.
@@ -1356,6 +1360,6 @@ evidence remains—and the product can prove that explanation at runtime.
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](STRATEGIC_REVIEW_2026-08-05.md)
status source. The [Strategic Review 2026-08-05](../../archive/2026-08/STRATEGIC_REVIEW_2026-08-05.md)
retains the dated assessment and reasoning. Current reconciliation belongs in
[Strategy Status](STRATEGY_STATUS.md), and live work state belongs in Gitea.
[Strategy Status](../STRATEGY_STATUS.md), and live work state belongs in Gitea.
+74 -67
View File
@@ -4,82 +4,89 @@ build-backend = "setuptools.build_meta"
[project]
name = "govoplan"
version = "0.1.17"
version = "0.1.46"
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.17",
"govoplan-tenancy==0.1.17",
"govoplan-organizations==0.1.17",
"govoplan-identity==0.1.17",
"govoplan-idm==0.1.17",
"govoplan-access==0.1.17",
"govoplan-admin==0.1.17",
"govoplan-policy==0.1.17",
"govoplan-audit==0.1.17",
"govoplan-dashboard==0.1.17",
"govoplan-files==0.1.17",
"govoplan-mail==0.1.17",
"govoplan-campaign==0.1.17",
"govoplan-calendar==0.1.17",
"govoplan-docs==0.1.17",
"govoplan-ops==0.1.17",
"govoplan-core[server]==0.1.46",
"govoplan-tenancy==0.1.22",
"govoplan-organizations==0.1.21",
"govoplan-identity==0.1.21",
"govoplan-idm==0.1.26",
"govoplan-access==0.1.25",
"govoplan-admin==0.1.23",
"govoplan-policy==0.1.23",
"govoplan-audit==0.1.20",
"govoplan-dashboard==0.1.20",
"govoplan-files==0.1.27",
"govoplan-mail==0.1.28",
"govoplan-campaign==0.1.29",
"govoplan-calendar==0.1.24",
"govoplan-docs==0.1.23",
"govoplan-ops==0.1.22",
]
[project.optional-dependencies]
full = [
"govoplan-addresses==0.1.17",
"govoplan-approvals==0.1.17",
"govoplan-assets==0.1.17",
"govoplan-booking==0.1.17",
"govoplan-cases==0.1.17",
"govoplan-certificates==0.1.17",
"govoplan-committee==0.1.17",
"govoplan-connectors==0.1.17",
"govoplan-consultation==0.1.17",
"govoplan-contracts==0.1.17",
"govoplan-dataflow==0.1.17",
"govoplan-datasources==0.1.17",
"govoplan-decisions==0.1.17",
"govoplan-dist-lists==0.1.17",
"govoplan-encryption==0.1.17",
"govoplan-evaluation==0.1.17",
"govoplan-facilities==0.1.17",
"govoplan-forms==0.1.17",
"govoplan-forms-runtime==0.1.17",
"govoplan-grants==0.1.17",
"govoplan-helpdesk==0.1.17",
"govoplan-identity-trust==0.1.17",
"govoplan-inspections==0.1.17",
"govoplan-learning==0.1.17",
"govoplan-mandates==0.1.17",
"govoplan-notifications==0.1.17",
"govoplan-parties==0.1.17",
"govoplan-permits==0.1.17",
"govoplan-poll==0.1.17",
"govoplan-portal==0.1.17",
"govoplan-postbox==0.1.17",
"govoplan-procurement==0.1.17",
"govoplan-projects==0.1.17",
"govoplan-records==0.1.17",
"govoplan-reporting==0.1.17",
"govoplan-resources==0.1.17",
"govoplan-rest==0.1.17",
"govoplan-risk-compliance==0.1.17",
"govoplan-scheduling==0.1.17",
"govoplan-search==0.1.17",
"govoplan-services==0.1.17",
"govoplan-soap==0.1.17",
"govoplan-templates==0.1.17",
"govoplan-tickets==0.1.17",
"govoplan-transparency==0.1.17",
"govoplan-views==0.1.17",
"govoplan-voting==0.1.17",
"govoplan-wiki==0.1.17",
"govoplan-workflow==0.1.17",
"govoplan-workflow-engine==0.1.17",
"govoplan-addresses==0.1.23",
"govoplan-approvals==0.1.21",
"govoplan-assets==0.1.20",
"govoplan-booking==0.1.20",
"govoplan-cases==0.1.25",
"govoplan-certificates==0.1.20",
"govoplan-committee==0.1.22",
"govoplan-connectors==0.1.27",
"govoplan-consultation==0.1.20",
"govoplan-contracts==0.1.20",
"govoplan-dataflow==0.1.25",
"govoplan-datasources==0.1.26",
"govoplan-decisions==0.1.19",
"govoplan-dist-lists==0.1.21",
"govoplan-dms==0.1.20",
"govoplan-encryption==0.1.20",
"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.23",
"govoplan-forms-runtime==0.1.22",
"govoplan-grants==0.1.20",
"govoplan-helpdesk==0.1.21",
"govoplan-identity-trust==0.1.21",
"govoplan-inspections==0.1.20",
"govoplan-learning==0.1.20",
"govoplan-mandates==0.1.19",
"govoplan-notifications==0.1.20",
"govoplan-parties==0.1.19",
"govoplan-payments==0.1.22",
"govoplan-permits==0.1.20",
"govoplan-poll==0.1.20",
"govoplan-portal==0.1.22",
"govoplan-postbox==0.1.23",
"govoplan-procurement==0.1.20",
"govoplan-projects==0.1.20",
"govoplan-quick-access==0.1.21",
"govoplan-records==0.1.24",
"govoplan-reporting==0.1.22",
"govoplan-resources==0.1.20",
"govoplan-rest==0.1.19",
"govoplan-risk-compliance==0.1.21",
"govoplan-scheduling==0.1.22",
"govoplan-search==0.1.20",
"govoplan-services==0.1.19",
"govoplan-soap==0.1.19",
"govoplan-tasks==0.1.23",
"govoplan-templates==0.1.22",
"govoplan-tickets==0.1.23",
"govoplan-transparency==0.1.20",
"govoplan-views==0.1.22",
"govoplan-voting==0.1.21",
"govoplan-wiki==0.1.22",
"govoplan-workflow==0.1.23",
"govoplan-workflow-engine==0.1.21",
"govoplan-xrechnung==0.1.21",
]
[project.urls]
@@ -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"},
+1 -1
View File
@@ -2,7 +2,7 @@ bandit>=1.8,<2
click>=8.3.3
filelock>=3.20.3
idna>=3.15
pip>=26.1.2
pip>=26.2
pip-audit>=2.9,<3
python-multipart>=0.0.31
radon>=6,<7
+4 -1
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
@@ -53,7 +56,7 @@ httpx2>=2.5,<3
filelock>=3.20.3
idna>=3.15
jsonschema>=4,<5
pip>=26.1.2
pip>=26.2
pip-audit>=2.9,<3
pytest>=9.0.3,<10
pygments>=2.20,<3
+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.17
govoplan-organizations @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-organizations.git@v0.1.17
govoplan-identity @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-identity.git@v0.1.17
govoplan-idm @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-idm.git@v0.1.17
govoplan-access @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-access.git@v0.1.17
govoplan-admin @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-admin.git@v0.1.17
govoplan-policy @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-policy.git@v0.1.17
govoplan-audit @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-audit.git@v0.1.17
govoplan-dashboard @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-dashboard.git@v0.1.17
govoplan-files @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-files.git@v0.1.17
govoplan-mail @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-mail.git@v0.1.17
govoplan-campaign @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-campaign.git@v0.1.17
govoplan-calendar @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-calendar.git@v0.1.17
govoplan-docs @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-docs.git@v0.1.17
govoplan-ops @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-ops.git@v0.1.17
govoplan-tenancy @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-tenancy.git@v0.1.22
govoplan-organizations @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-organizations.git@v0.1.21
govoplan-identity @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-identity.git@v0.1.21
govoplan-idm @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-idm.git@v0.1.26
govoplan-access @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-access.git@v0.1.25
govoplan-admin @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-admin.git@v0.1.23
govoplan-policy @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-policy.git@v0.1.23
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.27
govoplan-mail @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-mail.git@v0.1.28
govoplan-campaign @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-campaign.git@v0.1.29
govoplan-calendar @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-calendar.git@v0.1.24
govoplan-docs @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-docs.git@v0.1.23
govoplan-ops @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-ops.git@v0.1.22
+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."
]
}
}
+55
View File
@@ -0,0 +1,55 @@
import assert from "node:assert/strict";
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
import { createRequire } from "node:module";
import { tmpdir } from "node:os";
import { dirname, join, resolve } from "node:path";
import { test } from "node:test";
import { auditRepository } from "../tools/devkit/audit-display-labels.mjs";
const require = createRequire(resolve(import.meta.dirname, "../../govoplan-core/webui/package.json"));
const ts = require("typescript");
function fixture(fn) {
const root = mkdtempSync(join(tmpdir(), "govoplan-label-audit-"));
const write = (relative, content) => { const target = join(root, relative); mkdirSync(dirname(target), { recursive: true }); writeFileSync(target, content); };
try {
write("core/webui/src/i18n/generatedTranslations.ts", 'export const generatedTranslations = { en: { Shared: "Shared" }, de: { Shared: "Gemeinsam" } };');
return fn(root, write);
} finally { rmSync(root, { recursive: true, force: true }); }
}
test("plain labels resolve owning registered catalogs, aliases, constants and Core defaults", () => fixture((root, write) => {
write("module/webui/src/i18n/generatedTranslations.ts", 'const en = { Projects: "Projects" }; const de = { Projects: "Projekte" }; export const generatedTranslations = { en, de };');
write("module/webui/src/module.ts", 'import { generatedTranslations as words } from "./i18n/generatedTranslations"; export const exampleModule: PlatformWebModule = { translations: words };');
write("module/webui/src/Page.tsx", 'import { PageLayout as Layout, PageTitle } from "@govoplan/core-webui"; const title = "Projects"; export const page = <><Layout title={title} /><PageTitle>Shared</PageTitle></>;');
const result = auditRepository(ts, join(root, "module"), join(root, "core"));
assert.equal(result.registration, "registered");
assert.equal(result.labels.length, 2);
assert.deepEqual(result.findings, []);
}));
test("a catalog file without module registration cannot mask untranslated titles", () => fixture((root, write) => {
write("module/webui/src/i18n/generatedTranslations.ts", 'export const generatedTranslations = { en: { Projects: "Projects" }, de: { Projects: "Projekte" } };');
write("module/webui/src/module.ts", 'export const exampleModule = { id: "example" };');
write("module/webui/src/Page.tsx", 'import * as Core from "@govoplan/core-webui"; export const page = <Core.PageLayout title="Projects" />;');
const result = auditRepository(ts, join(root, "module"), join(root, "core"));
assert(result.findings.some((item) => item.code === "catalog-not-registered"));
assert.deepEqual(result.labels[0].missing_locales, ["en", "de"]);
}));
test("only known static display slots fail; markers and runtime data remain separate", () => fixture((root, write) => {
write("module/webui/src/Page.tsx", 'import { PageLayout } from "@govoplan/core-webui"; export const page = <><PageLayout title="Untranslated" /><PageLayout title={campaign.name} /><PageLayout title="i18n:known.key" /><CustomThing title="Not a known slot" /></>;');
const result = auditRepository(ts, join(root, "module"), join(root, "core"));
assert.equal(result.findings.length, 1);
assert.equal(result.findings[0].text, "Untranslated");
assert.equal(result.review.length, 1);
assert.equal(result.review[0].code, "dynamic-display-slot");
}));
test("dynamic translation registration is reported for review, not falsely missing", () => fixture((root, write) => {
write("module/webui/src/module.ts", 'export const exampleModule: PlatformWebModule = { translations: configuredCatalog() };');
write("module/webui/src/Page.tsx", 'export const page = <PageTitle>Example</PageTitle>;');
const result = auditRepository(ts, join(root, "module"), join(root, "core"));
assert.equal(result.registration, "dynamic");
assert.deepEqual(result.findings, []);
assert(result.review.some((item) => item.code === "dynamic-catalog-review"));
}));
+64
View File
@@ -0,0 +1,64 @@
import assert from "node:assert/strict";
import { test } from "node:test";
import { findDetachedDocumentation } from "../tools/checks/check-heading-help.mjs";
import "./test-heading-translations.mjs";
const inspect = (source) => findDetachedDocumentation([{ path: "/fixture/Page.tsx", source }]).findings;
const book = '<DocumentationHelpLink reference={topic} />';
test("heading and text contracts accept contextual help", () => {
for (const component of ["PageHeader", "PageLayout", "AdminPageLayout", "Card", "Dialog", "PageActionBar", "WorkspaceActionBar"]) {
assert.equal(inspect(`const Page=()=> <${component} title="Topic" titleHelp={${book}} />`).length, 0, component);
}
assert.equal(inspect(`const Page=()=> <PageTitle titleHelp={${book}}>Topic</PageTitle>`).length, 0);
assert.equal(inspect(`const Page=()=> <TextWithHelp help={${book}}>Existing label</TextWithHelp>`).length, 0);
});
test("detached action and body links are rejected", () => {
for (const source of [`<PageActionBar helpAction={${book}} />`, `<Card title="Topic" actions={${book}} />`, `<div>${book}</div>`, `<Card title={${book}} />`]) {
assert.equal(inspect(`const Page=()=> ${source}`).length, 1, source);
}
});
test("aliases, conditional help and simple local references remain checked", () => {
assert.equal(inspect(`import {DocumentationHelpLink as Book, Card as Box} from '@govoplan/core-webui'; const Page=()=> <Box title="Topic" titleHelp={<Book reference={topic}/>} />`).length, 0);
assert.equal(inspect(`import {DocumentationHelpLink as Book} from '@govoplan/core-webui'; const Page=()=> <div><Book reference={topic}/></div>`).length, 1);
assert.equal(inspect(`const help=${book}; const Page=()=> <Card title="Topic" titleHelp={enabled ? help : null}/>`).length, 0);
assert.equal(inspect(`const help=${book}; const Page=()=> <><Card title="Topic" titleHelp={help}/><PageActionBar helpAction={help}/></>`).length, 1);
assert.equal(inspect(`const help=${book}; const again=help; const Page=()=> <Card title="Topic" titleHelp={again}/>`).length, 0);
});
test("empty anchors, unknown contracts and nested interactive elements fail", () => {
for (const source of [`<Card titleHelp={${book}}/>`, `<TextWithHelp help={${book}}/>`, `<TextWithHelp help={${book}}> </TextWithHelp>`, `<Unknown title="Topic" titleHelp={${book}}/>`, `<Card title="Topic" titleHelp={<button>${book}</button>}/>`]) {
assert.equal(inspect(`const Page=()=> ${source}`).length, 1, source);
}
});
test("namespace and explicit component default imports cannot bypass placement", () => {
for (const source of [
`import * as UI from '@govoplan/core-webui'; const Page=()=> <div><UI.DocumentationHelpLink reference={topic}/></div>`,
`import Book from './components/help/DocumentationHelpLink'; const Page=()=> <div><Book reference={topic}/></div>`,
]) assert.equal(inspect(source).length, 1, source);
assert.equal(inspect(`import * as UI from '@govoplan/core-webui'; const Page=()=> <UI.Card title="Topic" titleHelp={<UI.DocumentationHelpLink reference={topic}/>}/>`).length, 0);
assert.equal(inspect(`import Book from './components/help/DocumentationHelpLink'; import Label from './components/help/TextWithHelp'; const Page=()=> <Label help={<Book reference={topic}/>}>Topic</Label>`).length, 0);
assert.equal(findDetachedDocumentation([{ path: "/fixture/Page.tsx", source: `import Book from './business/Book'; const Page=()=> <div><Book/></div>` }]).links, 0);
});
test("outer interactive containers and statically absent text are rejected", () => {
for (const source of [
`<button><TextWithHelp help={${book}}>Topic</TextWithHelp></button>`,
`<a href="/other"><Card title="Topic" titleHelp={${book}}/></a>`,
`<Card title="" titleHelp={${book}}/>`,
`<Card title={false} titleHelp={${book}}/>`,
`<PageTitle titleHelp={${book}}>{null}</PageTitle>`,
`<TextWithHelp help={${book}}>{/* Topic */}</TextWithHelp>`,
`<TextWithHelp help={${book}}>{undefined}</TextWithHelp>`,
`<TextWithHelp help={${book}}><span hidden>Topic</span></TextWithHelp>`,
`<TextWithHelp hidden help={${book}}>Topic</TextWithHelp>`,
]) assert.equal(inspect(`const Page=()=> ${source}`).length, 1, source);
assert.equal(inspect(`const help=${book}; const Page=()=> <Button><TextWithHelp help={help}>Topic</TextWithHelp></Button>`).length, 1);
assert.equal(inspect(`const title=null; const Page=()=> <Card title={title} titleHelp={${book}}/>`).length, 1);
assert.equal(inspect(`const Page=()=> <Card title={translateText(title)} titleHelp={${book}}/>`).length, 0);
assert.equal(inspect(`const Page=()=> <TextWithHelp help={${book}}><TranslatedTitle/></TextWithHelp>`).length, 0);
assert.equal(inspect(`let title=""; title=translateText(key); const Page=()=> <Card title={title} titleHelp={${book}}/>`).length, 0);
});
+80
View File
@@ -0,0 +1,80 @@
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { createRequire } from "node:module";
import { resolve } from "node:path";
import { test } from "node:test";
const root = resolve(import.meta.dirname, "../..");
const require = createRequire(resolve(root, "govoplan-core/webui/package.json"));
const { buildSync } = require("esbuild");
const labels = {
approvals: { "Approval requests": "Genehmigungsanträge" },
forms: { "Form meaning": "Formularbedeutung" },
"forms-runtime": { "Form instance": "Formularinstanz", "Forms runtime": "Formularlaufzeit" },
helpdesk: { "Helpdesk profiles": "Helpdesk-Profile" },
portal: { "Service directory": "Leistungsverzeichnis" },
projects: { Projects: "Projekte" },
reporting: { Reporting: "Reporting" },
"risk-compliance": { "Risk Compliance": "Risiko und Compliance" },
scheduling: { Scheduling: "Terminplanung" },
tickets: { Tickets: "Tickets" },
voting: { Ballots: "Abstimmungen" },
wiki: { Wiki: "Wiki" },
workflow: { Workflows: "Workflows", Workflow: "Workflow" },
idm: {
"Direct changes to this governed function are": "Direkte Änderungen an dieser gesteuerten Funktion sind",
"emergency overrides": "Notfallübersteuerungen",
". Use a request or grant above for the normal process.": ". Verwenden Sie für den regulären Prozess einen Antrag oder eine Vergabe.",
},
};
// Render the actual Core heading/locale contract with one owning module at a
// time. The in-memory bundle creates no shared component-build artifacts.
const names = Object.keys(labels);
const imports = names.map((name, index) => `import { generatedTranslations as t${index} } from ${JSON.stringify(resolve(root, `govoplan-${name}/webui/src/i18n/generatedTranslations.ts`))};`).join("\n");
const { outputFiles } = buildSync({
stdin: {
contents: `${imports}
import { renderToStaticMarkup } from 'react-dom/server';
import { PlatformLanguageProvider } from ${JSON.stringify(resolve(root, "govoplan-core/webui/src/i18n/LanguageContext.tsx"))};
import PageTitle from ${JSON.stringify(resolve(root, "govoplan-core/webui/src/components/PageTitle.tsx"))};
import DocumentationHelpLink from ${JSON.stringify(resolve(root, "govoplan-core/webui/src/components/help/DocumentationHelpLink.tsx"))};
export const catalogs = [${names.map((_, index) => `t${index}`).join(",")}];
export function heading(index, language, label) {
return renderToStaticMarkup(<PlatformLanguageProvider preferredLanguageCode={language} moduleTranslations={[catalogs[index]]}>
<PageTitle titleHelp={<DocumentationHelpLink reference={{ contextId: "heading-test" }} />}>{label}</PageTitle>
</PlatformLanguageProvider>);
}`,
loader: "tsx",
resolveDir: resolve(root, "govoplan-core/webui"),
},
bundle: true,
write: false,
platform: "node",
format: "cjs",
jsx: "automatic",
external: ["react", "react-dom/server"],
logLevel: "silent",
});
const compiled = { exports: {} };
new Function("require", "module", "exports", outputFiles[0].text)(require, compiled, compiled.exports);
const { catalogs, heading } = compiled.exports;
test("new contextual labels render from their owning EN/DE catalogues", async (context) => {
for (const [index, name] of names.entries()) {
await context.test(name, () => {
const moduleSource = readFileSync(resolve(root, `govoplan-${name}/webui/src/module.ts`), "utf8");
assert.match(moduleSource, /import\s*\{\s*generatedTranslations\s*\}\s*from\s*["']\.\/i18n\/generatedTranslations["']/);
assert.match(moduleSource, /\btranslations(?:\s*:\s*generatedTranslations)?\s*,/);
for (const [english, german] of Object.entries(labels[name])) {
assert.equal(catalogs[index].en[english], english);
assert.equal(catalogs[index].de[english], german);
for (const [language, expected] of [["en", english], ["de", german]]) {
const markup = heading(index, language, english);
assert(markup.includes(`<h1>${expected}</h1>`), `${name}: ${language} contextual heading`);
assert(markup.includes(language === "de" ? "Benutzerdokumentation öffnen" : "Open user documentation"));
}
}
});
}
});
+15
View File
@@ -0,0 +1,15 @@
import assert from "node:assert/strict";
import { findTypeOnlyJsxImports } from "../tools/checks/check-jsx-value-imports.mjs";
const findings = (source) => findTypeOnlyJsxImports([{ path: "/fixture.tsx", source }]).map((item) => item.component);
assert.deepEqual(findings('import type { FormGrid, AuthInfo } from "@govoplan/core-webui"; const page = <Dialog><FormGrid /></Dialog>;'), ["FormGrid"]);
assert.deepEqual(findings('import { type FormGrid as Layout } from "ui"; const page = <Layout>Content</Layout>;'), ["Layout"]);
assert.deepEqual(findings('import type Layout from "ui"; const page = <Layout />;'), ["Layout"]);
assert.deepEqual(findings('import type * as ui from "ui"; const page = <ui.Layout />;'), ["ui.Layout"]);
assert.deepEqual(findings('import type { FormGrid } from "ui"; const page = <div title={<FormGrid />} />;'), ["FormGrid"]);
assert.deepEqual(findings('import { FormGrid, type AuthInfo } from "ui"; const page = <FormGrid />;'), []);
assert.deepEqual(findings('import type { FormGrid } from "ui"; function Page({ FormGrid }: Props) { return <FormGrid />; }'), []);
assert.deepEqual(findings('import type * as ui from "ui"; function Page(ui: RuntimeControls) { return <ui.Layout />; }'), []);
assert.deepEqual(findings('import type { Layout } from "ui"; const page: Layout = {};'), []);
assert.deepEqual(findings('import type { input } from "ui"; const page = <input />;'), []);
console.log("JSX runtime-import AST regression tests passed (10 cases).");
+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()
+322 -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,
@@ -414,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"])
@@ -460,6 +523,22 @@ 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"]
@@ -701,12 +780,21 @@ class DeploymentInstallerTests(unittest.TestCase):
self.assertIn("redis", compose["services"])
self.assertIn("worker", compose["services"])
self.assertIn("load-balancer", compose["services"])
self.assertEqual(
"haproxy:3.2.23-alpine@sha256:"
"6343ce34a132a5dceaa24767d739df2bd519f8f7c1079ae39e4821334e8eb42e",
compose["services"]["load-balancer"]["image"],
)
self.assertNotIn("test-mail", compose["services"])
self.assertEqual(
["127.0.0.1:8080:8080"],
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"])
@@ -932,6 +1020,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",
@@ -983,8 +1150,26 @@ class DeploymentInstallerTests(unittest.TestCase):
self.assertEqual(1, parsed.replicas.web)
self.assertEqual(1, parsed.replicas.worker)
self.assertEqual("managed", parsed.components.load_balancer.mode)
self.assertEqual(
default_spec().components.load_balancer.image,
parsed.components.load_balancer.image,
)
self.assertEqual("local", parsed.ingress.mode)
def test_load_balancer_patch_does_not_rewrite_an_existing_image(self) -> None:
for image in (
"haproxy:3.2.21-alpine",
"registry.example.test/haproxy@sha256:" + "a" * 64,
):
with self.subTest(image=image):
saved = default_spec(load_balancer_image=image).to_dict()
restored = parse_spec(json.loads(json.dumps(saved)))
compose = render_compose(restored)
self.assertEqual(image, restored.components.load_balancer.image)
self.assertEqual(image, compose["services"]["load-balancer"]["image"])
def test_compose_contains_no_secret_values(self) -> None:
spec = default_spec()
values = initial_secrets(spec)
@@ -1061,10 +1246,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(
@@ -1075,12 +1264,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),
@@ -1096,6 +1290,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:
@@ -1205,6 +1472,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"
@@ -1464,6 +1779,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"):
+438
View File
@@ -0,0 +1,438 @@
"""Audit the selected fixture workspace, never a fuller neighboring checkout."""
from argparse import Namespace
import importlib.util
import json
import os
from pathlib import Path
import shutil
import subprocess
import sys
from types import ModuleType, SimpleNamespace
import pytest
META_ROOT = Path(__file__).resolve().parents[1]
SCRIPT = META_ROOT / "tools/inventory/platform-interface-inventory.py"
WEBUI_SCRIPT = META_ROOT / "tools/inventory/extract-webui-structure.mjs"
SPEC = importlib.util.spec_from_file_location("audit_scope_inventory", SCRIPT)
assert SPEC is not None and SPEC.loader is not None
inventory = importlib.util.module_from_spec(SPEC)
SPEC.loader.exec_module(inventory)
sys.path.insert(0, str(META_ROOT / "tools/devkit"))
from govoplan_devkit import docs, issues, runner # noqa: E402
from govoplan_devkit.workspace import Project, Repository # noqa: E402
def write(path, content):
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(content, encoding="utf-8")
@pytest.fixture
def workspaces(tmp_path):
selected = tmp_path / "selected"
default = tmp_path / "fuller-default"
meta = tmp_path / "legacy-siblings/govoplan"
selected.mkdir()
meta.mkdir(parents=True)
catalog = {
"default_parent": str(default),
"repositories": [
{"name": "govoplan-core", "path": "govoplan-core"},
{"name": "govoplan-example", "path": "nested/example"},
{"name": "govoplan-optional", "path": "govoplan-optional"},
],
}
write(meta / "repositories.json", json.dumps(catalog))
write(
selected / "nested/example/webui/src/Page.tsx",
"export const page = <h1>SELECTED_ONLY</h1>;\n",
)
write(
default / "nested/example/webui/src/Page.tsx",
"export const page = <h1>DEFAULT_ONLY</h1>;\n",
)
write(
default / "govoplan-optional/webui/src/Page.tsx",
"export const page = <h1>DEFAULT_OPTIONAL</h1>;\n",
)
for item in catalog["repositories"]:
(default / item["path"] / "src").mkdir(parents=True)
return selected, default, meta, catalog
def test_explicit_partial_root_beats_fuller_legacy_discovery(workspaces, monkeypatch):
selected, default, meta, catalog = workspaces
monkeypatch.setattr(inventory, "META_ROOT", meta)
assert inventory._resolve_workspace_root(catalog) == default
assert inventory._resolve_workspace_root(catalog, selected) == selected
with pytest.raises(ValueError, match="existing directory"):
inventory._resolve_workspace_root(catalog, selected / "missing")
@pytest.mark.parametrize("relative", ["../fuller-default", "/absolute/path"])
def test_repository_paths_cannot_escape_explicit_root(workspaces, relative):
selected, _, _, _ = workspaces
with pytest.raises(ValueError, match="inside the selected workspace"):
inventory._validate_repository_roots(
{"repositories": [{"path": relative}]}, selected
)
def test_linked_repository_cannot_borrow_default_sources(workspaces):
selected, default, _, catalog = workspaces
(selected / "govoplan-core").symlink_to(
default / "govoplan-core", target_is_directory=True
)
with pytest.raises(ValueError, match="escapes the selected workspace"):
inventory._validate_repository_roots(catalog, selected)
def test_python_forwards_selected_root_and_configured_node(workspaces, monkeypatch):
selected, _, meta, _ = workspaces
monkeypatch.setattr(inventory, "META_ROOT", meta)
monkeypatch.setenv("NODE", "/selected/toolchain/node")
calls = []
def run(argv, **kwargs):
calls.append((argv, kwargs))
return SimpleNamespace(stdout=json.dumps({"workspaceRoot": str(selected)}))
monkeypatch.setattr(inventory.subprocess, "run", run)
assert inventory._extract_webui(selected)["workspaceRoot"] == str(selected)
argv, options = calls[0]
assert argv == [
"/selected/toolchain/node",
str(meta / "tools/inventory/extract-webui-structure.mjs"),
str(meta),
"--workspace-root",
str(selected),
]
assert options == {"check": True, "capture_output": True, "text": True}
def test_python_rejects_webui_result_from_another_root(workspaces, monkeypatch):
selected, default, _, _ = workspaces
monkeypatch.setattr(
inventory.subprocess,
"run",
lambda *a, **k: SimpleNamespace(
stdout=json.dumps({"workspaceRoot": str(default)})
),
)
with pytest.raises(ValueError, match="did not confirm"):
inventory._extract_webui(selected)
def test_main_forwards_same_root_to_every_collector(workspaces, monkeypatch):
selected, _, meta, catalog = workspaces
monkeypatch.setattr(inventory, "META_ROOT", meta)
roots = []
monkeypatch.setattr(
inventory, "_extract_webui", lambda root: roots.append(root) or {}
)
monkeypatch.setattr(
inventory,
"_extract_backend_endpoints",
lambda data, root: roots.append(root) or [],
)
monkeypatch.setattr(
inventory, "_extract_manifests", lambda data, root: roots.append(root) or []
)
monkeypatch.setattr(inventory, "_load_endpoint_declarations", lambda _: {})
monkeypatch.setattr(inventory, "_load_high_risk_help_baseline", lambda _: {})
monkeypatch.setattr(inventory, "_assemble_inventory", lambda **kwargs: {})
monkeypatch.setattr(inventory, "_render_markdown", lambda data: "fixture\n")
output = selected / "output"
monkeypatch.setattr(
sys,
"argv",
[str(SCRIPT), "--workspace-root", str(selected), "--output-dir", str(output)],
)
assert inventory.main() == 0
assert roots == [selected, selected, selected]
report = json.loads((output / "platform-interface-inventory.json").read_text())
assert report["workspace_root"] == str(selected)
assert report["workspace_selection"] == "explicit"
def install_fixture_compiler(root):
compiler = META_ROOT.parent / "govoplan-core/webui/node_modules/typescript"
if not compiler.is_dir() or not shutil.which("node"):
pytest.skip("Node and the installed Core TypeScript parser are required")
target = root / "govoplan-core/webui/node_modules/typescript"
target.parent.mkdir(parents=True, exist_ok=True)
# Parser dependencies may be shared; audited source checkouts may not.
target.symlink_to(compiler, target_is_directory=True)
def collect_webui(meta, selected=None):
argv = [shutil.which("node") or "node", str(WEBUI_SCRIPT), str(meta)]
if selected is not None:
argv += ["--workspace-root", str(selected)]
return subprocess.run(argv, capture_output=True, text=True, timeout=30)
def test_javascript_explicit_root_is_authoritative_and_legacy_cli_still_works(
workspaces,
):
selected, default, meta, _ = workspaces
install_fixture_compiler(selected)
install_fixture_compiler(default)
explicit = collect_webui(meta, selected)
assert explicit.returncode == 0, explicit.stderr
report = json.loads(explicit.stdout)
assert report["workspaceRoot"] == str(selected)
assert [item["value"] for item in report["visibleText"]] == ["SELECTED_ONLY"]
assert report["visibleText"][0]["file"] == "webui/src/Page.tsx"
legacy = collect_webui(meta)
assert legacy.returncode == 0, legacy.stderr
assert json.loads(legacy.stdout)["workspaceRoot"] == str(default)
assert {item["value"] for item in json.loads(legacy.stdout)["visibleText"]} == {
"DEFAULT_ONLY",
"DEFAULT_OPTIONAL",
}
def test_javascript_does_not_borrow_missing_core_dependencies(workspaces):
selected, default, meta, _ = workspaces
install_fixture_compiler(default)
result = collect_webui(meta, selected)
assert result.returncode != 0
assert (
str(selected / "govoplan-core/webui/node_modules/typescript") in result.stderr
)
assert not result.stdout
def test_javascript_rejects_source_root_linked_outside_workspace(workspaces):
selected, default, meta, _ = workspaces
install_fixture_compiler(selected)
linked = selected / "govoplan-optional/webui/src"
linked.parent.mkdir(parents=True)
linked.symlink_to(default / "govoplan-optional/webui/src", target_is_directory=True)
result = collect_webui(meta, selected)
assert result.returncode != 0
assert "source root escapes the selected workspace" in result.stderr
def test_backend_does_not_borrow_optional_endpoints(workspaces):
selected, default, _, catalog = workspaces
source = "from fastapi import APIRouter\nrouter = APIRouter()\n@router.get('/selected')\ndef endpoint(): pass\n"
write(selected / "nested/example/src/routes.py", source)
write(
default / "govoplan-optional/src/routes.py", source.replace("selected", "other")
)
endpoints = inventory._extract_backend_endpoints(catalog, selected)
assert [item["path"] for item in endpoints] == ["/selected"]
(selected / "nested/example/src/foreign.py").symlink_to(
default / "govoplan-optional/src/routes.py"
)
with pytest.raises(ValueError, match="Backend source path escapes"):
inventory._extract_backend_endpoints(catalog, selected)
def test_manifests_require_selected_core_sources(workspaces, monkeypatch):
selected, default, _, catalog = workspaces
write(
default / "govoplan-core/src/govoplan_core/core/platform_interfaces.py",
"raise AssertionError('foreign source imported')",
)
monkeypatch.syspath_prepend(str(default / "govoplan-core/src"))
with pytest.raises(ValueError, match="requires Core interface sources"):
inventory._extract_manifests(catalog, selected)
def test_cached_application_import_cannot_replace_missing_checkout(
workspaces, monkeypatch
):
selected, default, _, _ = workspaces
# Meta's own devkit/release packages may audit another workspace.
tooling = ModuleType("govoplan_release")
tooling.__file__ = str(default / "tools/release/govoplan_release/__init__.py")
modules = {"govoplan_release": tooling, "govoplan_devkit.docs": docs}
# Isolate the fixture from application packages collected by unrelated
# suites, without removing or replacing those real cached imports.
monkeypatch.setattr(inventory, "sys", SimpleNamespace(modules=modules))
inventory._assert_workspace_imports(selected)
application = ModuleType("govoplan_audit_fixture")
application.__file__ = str(
default / "govoplan-optional/src/govoplan_audit_fixture/__init__.py"
)
modules["govoplan_audit_fixture"] = application
with pytest.raises(ValueError, match="outside the selected inventory workspace"):
inventory._assert_workspace_imports(selected)
def test_partial_manifest_collection_uses_selected_sources_in_fresh_process(workspaces):
selected, default, _, catalog = workspaces
core = selected / "govoplan-core/src/govoplan_core"
write(core / "__init__.py", "")
write(core / "core/__init__.py", "")
write(
core / "core/platform_interfaces.py",
"def manifest_interface_catalog(manifest): return {'selected': True}\n",
)
write(
default / "govoplan-core/src/govoplan_core/__init__.py",
"raise AssertionError('foreign Core loaded')\n",
)
module = selected / "nested/example/src/govoplan_example"
write(module / "__init__.py", "")
write(module / "backend/__init__.py", "")
manifest = (
"from types import SimpleNamespace as S\n"
"def get_manifest():\n"
" return S(id='selected', name='Selected', version='1', dependencies=(), "
"optional_dependencies=(), required_capabilities=(), provides_interfaces=(), "
"capability_factories={}, permissions=(), documentation=(), architecture=None, "
"information_governance=S(to_dict=lambda: {}), frontend=None)\n"
)
write(module / "backend/manifest.py", manifest)
write(
default / "govoplan-optional/src/govoplan_optional/backend/manifest.py",
manifest.replace("selected", "foreign"),
)
code = (
"import importlib.util, json; from pathlib import Path; "
f"spec=importlib.util.spec_from_file_location('fixture', {str(SCRIPT)!r}); "
"module=importlib.util.module_from_spec(spec); spec.loader.exec_module(module); "
f"print(json.dumps(module._extract_manifests({catalog!r}, Path({str(selected)!r}))))"
)
env = {
**os.environ,
"PYTHONPATH": os.pathsep.join(
str(default / item["path"] / "src") for item in catalog["repositories"]
),
}
result = subprocess.run(
[sys.executable, "-c", code],
env=env,
capture_output=True,
text=True,
timeout=30,
)
assert result.returncode == 0, result.stderr
manifests = json.loads(result.stdout)
assert [manifest["id"] for manifest in manifests] == ["selected"]
assert manifests[0]["interface_catalog"] == {"selected": True}
def doc_plan(workspace, monkeypatch):
repos = tuple(
Repository(name, workspace / name)
for name in ("govoplan", "govoplan-core", "govoplan-example")
)
project = Project("fixture", repos, {})
monkeypatch.setattr(docs, "load_project", lambda *a: project)
args = Namespace(
workspace_root=workspace,
project=None,
state_dir=workspace.parent / "state",
repo=["govoplan-example"],
changed=False,
)
return docs.build_doc_stages(args)
def test_docs_plan_forwards_root_and_persists_all_limitations(workspaces, monkeypatch):
selected, _, _, _ = workspaces
stages = doc_plan(selected, monkeypatch)
by_id = {stage["id"]: stage for stage in stages}
for stage_id in (
"docs.manifests",
"docs.interface-inventory",
"docs.plain-display-labels",
):
argv = by_id[stage_id]["argv"]
assert argv[argv.index("--workspace-root") + 1] == str(selected)
assert by_id["docs.translation-structure"]["argv"][1] == str(
selected / "govoplan-core/webui/scripts/audit-i18n-structural.mjs"
)
assert issues.coverage_notes(stages) == docs.LIMITATIONS
assert by_id["docs.plain-display-labels"]["argv"][-2:] == [
"--repo",
"govoplan-example",
]
def test_docs_limitations_survive_real_receipt_and_issue_evidence(
workspaces, monkeypatch, tmp_path
):
selected, _, _, _ = workspaces
stages = doc_plan(selected, monkeypatch)
repo = selected / "example"
repo.mkdir()
subprocess.run(["git", "init", "-q", str(repo)], check=True, timeout=10)
write(repo / "source.txt", "fixture\n")
subprocess.run(
["git", "-C", str(repo), "add", "source.txt"], check=True, timeout=10
)
subprocess.run(
[
"git",
"-C",
str(repo),
"-c",
"user.name=Fixture",
"-c",
"user.email=fixture@example.invalid",
"commit",
"-qm",
"fixture",
],
check=True,
timeout=10,
)
project = tmp_path / "project.json"
write(
project,
json.dumps(
{
"schema_version": 1,
"name": "fixture",
"repositories": [{"name": "example", "path": "example"}],
"checks": [],
"profiles": {},
}
),
)
args = Namespace(
workspace_root=selected,
project=project,
state_dir=tmp_path / "state",
dry_run=False,
jobs=2,
profile="docs",
resume=None,
)
for stage in stages:
stage.update(
argv=[sys.executable, "-c", "print('fixture audit')"],
cwd=str(repo),
deps=[],
resources=[],
timeout_seconds=10,
)
monkeypatch.setattr(docs, "build_doc_stages", lambda _: stages)
monkeypatch.setattr(
runner, "environment_fingerprint", lambda *a, **k: "fixture-env"
)
result = docs.audit(args)
assert result["status"] == "passed", result
assert all(result["summary"].count(note) == 1 for note in docs.LIMITATIONS)
receipt = runner.read_receipt(selected, args.state_dir, result["run_id"])
assert issues.coverage_notes(receipt["stages"]) == docs.LIMITATIONS
evidence = issues.evidence_record(result["run_id"], args)
assert evidence["coverage_notes"] == docs.LIMITATIONS
assert evidence["source_state"] == "matches-current"
target = issues.NoteTarget(
repo, "https://gitea.example.invalid", "fixture", "example", 1
)
_, body = issues.render_note(
{"summary": [], "next": [], "body": ""}, evidence, target, "fixture"
)
assert all(note in body for note in docs.LIMITATIONS)
+88
View File
@@ -0,0 +1,88 @@
"""Signals during snapshot probes must not become passing run evidence."""
import os
import signal
import pytest
import test_devkit_runner as fixtures
from govoplan_devkit import runner
from govoplan_devkit.checkpoints import Checkpoints
example = fixtures.example
@pytest.mark.parametrize("interruption", [signal.SIGINT, signal.SIGTERM])
@pytest.mark.parametrize("probe", ["source", "environment"])
def test_interrupt_during_final_snapshot_is_not_a_passing_run(
example, monkeypatch, interruption, probe
):
args, repo = example
calls = {"source": 0, "environment": 0}
events, interruptions = [], []
args.on_progress = events.append
original = Checkpoints.source
def reached(kind):
calls[kind] += 1
if kind == probe and events and events[-1]["phase"] == "finalizing":
# Intermediate checkpoint probes are also real verification. Target
# finalization explicitly, while the runner owns signal handlers.
interruptions.append(kind)
os.kill(os.getpid(), interruption)
def source(self, *values, **kwargs):
reached("source")
return original(self, *values, **kwargs)
def environment(*_args, **_kwargs):
reached("environment")
return "fixture-environment"
monkeypatch.setattr(Checkpoints, "source", source)
monkeypatch.setattr(runner, "environment_fingerprint", environment)
result = runner.run_checks(args, [fixtures.stage(repo)])
assert interruptions == [probe]
assert calls[probe] >= 2
assert result["stages"][0]["status"] == "passed"
assert result["status"] == "interrupted"
assert result["_exit_code"] != 0
receipt = runner.read_receipt(args.workspace_root, args.state_dir, result["run_id"])
assert receipt["status"] == "interrupted"
@pytest.mark.parametrize("interruption", [signal.SIGINT, signal.SIGTERM])
@pytest.mark.parametrize("probe", ["source", "environment"])
def test_preparation_interrupt_stops_at_probe_boundary_without_running_checks(
example, monkeypatch, interruption, probe
):
args, repo = example
calls = []
original = Checkpoints.source
def reached(kind):
calls.append(kind)
if kind == probe:
os.kill(os.getpid(), interruption)
def source(self, *values, **kwargs):
reached("source")
return original(self, *values, **kwargs)
def environment(*_args, **_kwargs):
reached("environment")
return "fixture-environment"
def must_not_execute(*args, **kwargs):
pytest.fail("A cancelled preparation must not start a check")
monkeypatch.setattr(Checkpoints, "source", source)
monkeypatch.setattr(runner, "environment_fingerprint", environment)
monkeypatch.setattr(runner, "execute_stage", must_not_execute)
result = runner.run_checks(args, [fixtures.stage(repo)])
assert calls == (["source"] if probe == "source" else ["source", "environment"])
assert result["status"] == "interrupted"
assert result["snapshot_verified"] is False
assert result["_exit_code"] != 0
receipt = runner.read_receipt(args.workspace_root, args.state_dir, result["run_id"])
assert receipt["status"] == "interrupted"
+451
View File
@@ -0,0 +1,451 @@
"""Bounded planning fixtures: no application imports, compilers or servers."""
from __future__ import annotations
import argparse
import json
import shutil
from pathlib import Path
import sys
import tempfile
from types import SimpleNamespace
import unittest
from unittest.mock import patch
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "tools/devkit"))
from govoplan_devkit.catalog import (
_expanded_repositories,
build_stages,
module_ui_stages,
)
from govoplan_devkit.docs import build_doc_stages
from govoplan_devkit.workspace import Project, Repository
class CatalogTests(unittest.TestCase):
def setUp(self):
self.temp = tempfile.TemporaryDirectory(prefix="govoplan-devkit-catalog-")
self.addCleanup(self.temp.cleanup)
self.root = Path(self.temp.name)
self.core = Repository("govoplan-core", self.root / "govoplan-core", ("core",))
self.meta = Repository("govoplan", self.root / "govoplan", ("meta",))
self.module = Repository(
"govoplan-example", self.root / "govoplan-example", ("example",)
)
self.project = Project("fixture", (self.meta, self.core, self.module), {})
for repo in self.project.repositories:
repo.path.mkdir()
(self.meta.path / "tools/checks").mkdir(parents=True)
shutil.copyfile(
Path(__file__).resolve().parents[1] / "tools/checks/focused-phases.json",
self.meta.path / "tools/checks/focused-phases.json",
)
def write(self, relative, content):
path = self.root / relative
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(content, encoding="utf-8")
def plan(self, profile, repos=None, changed=False):
with patch("govoplan_devkit.workspace.load_project", return_value=self.project):
return build_stages(self.root, profile, repos or [], changed)
def test_ui_compiles_once_and_quick_does_not_compile(self):
self.write(
"govoplan-core/webui/package.json",
json.dumps(
{"scripts": {"test:components": "node scripts/run-component-tests.mjs"}}
),
)
self.write("govoplan-core/webui/scripts/run-component-tests.mjs", "// fixture")
self.assertFalse(
any(
"run-component-tests.mjs" in " ".join(item["argv"])
for item in self.plan("quick")
)
)
matches = [
item
for item in self.plan("ui")
if "run-component-tests.mjs" in " ".join(item["argv"])
]
self.assertEqual(len(matches), 1)
self.assertEqual(matches[0]["id"], "core.component-batch")
def test_module_script_metadata_is_bounded_and_deduplicated(self):
self.write(
"govoplan-example/webui/package.json",
json.dumps(
{
"scripts": {
"test:interface-pattern": "node scripts/test-interface-pattern-language.mjs",
"test:source": "node --test tests/source.test.mjs",
"test:dangerous-chain": "node tests/source.test.mjs && npm run dev",
"test:flags": "node --eval 'startServer()'",
}
}
),
)
self.write(
"govoplan-example/webui/scripts/test-interface-pattern-language.mjs",
"// source-only fixture",
)
self.write(
"govoplan-example/webui/tests/source.test.mjs", "// source-only fixture"
)
stages = module_ui_stages(self.module, reason="fixture")
self.assertEqual(len(stages), 2)
self.assertEqual(len({item["id"] for item in stages}), 2)
self.assertTrue(all(item["argv"][0] == "{node}" for item in stages))
self.assertFalse(any("&&" in item["argv"] for item in stages))
def test_full_is_never_narrowed_by_repo_filter(self):
result = self.plan("full", ["example"])
self.assertEqual(
[item["id"] for item in result],
[
"focused." + identity
for identity in (
"preflight",
"tooling",
"backend",
"core-ui",
"module-builds",
"browser",
"module-ui",
)
],
)
self.assertEqual(
[item["after"] for item in result],
[[], *[[item["id"]] for item in result[:-1]]],
)
self.assertTrue(all(item["deps"] == [] for item in result))
self.assertTrue(
all(
item["argv"]
== [
"bash",
str(self.meta.path / "tools/checks/check-focused.sh"),
"--phase",
item["id"].removeprefix("focused."),
]
for item in result
)
)
self.assertIn("webui:govoplan-example", result[-1]["resources"])
self.assertIn("backend:test-state", result[0]["resources"])
def test_changed_empty_is_not_full_verification(self):
with (
patch("govoplan_devkit.workspace.load_project", return_value=self.project),
patch("govoplan_devkit.workspace.selected_repositories", return_value=[]),
):
self.assertEqual(build_stages(self.root, "quick", [], True), [])
self.assertEqual(len(build_stages(self.root, "full", [], True)), 7)
def test_full_ui_scope_excludes_backend_only_but_includes_all_ui_owners(self):
backend = Repository(
"govoplan-backend-only", self.root / "govoplan-backend-only"
)
backend.path.mkdir()
(self.module.path / "webui").mkdir()
(self.core.path / "webui").mkdir()
project = Project("fixture", (*self.project.repositories, backend), {})
with patch("govoplan_devkit.workspace.load_project", return_value=project):
checks = build_stages(self.root, "full", ["example"], False)
for check in checks[:3]:
self.assertNotIn("inputs", check)
for check in checks[3:]:
self.assertEqual(
check["inputs"]["repos"],
["govoplan", "govoplan-core", "govoplan-example"],
)
(backend.path / "webui").mkdir()
with patch("govoplan_devkit.workspace.load_project", return_value=project):
replanned = build_stages(self.root, "full", [], False)
self.assertIn(backend.name, replanned[3]["inputs"]["repos"])
def test_full_missing_checkout_explicitly_falls_back_to_workspace_inputs(self):
missing = Repository("govoplan-missing", self.root / "govoplan-missing")
project = Project("fixture", (*self.project.repositories, missing), {})
with patch("govoplan_devkit.workspace.load_project", return_value=project):
checks = build_stages(self.root, "full", [], False)
self.assertTrue(all("inputs" not in check for check in checks))
self.assertTrue(
all(
any("workspace-wide" in note for note in check["coverage_notes"])
for check in checks[3:]
)
)
def test_full_requires_authoritative_phase_metadata(self):
(self.meta.path / "tools/checks/focused-phases.json").write_text("{}")
with self.assertRaisesRegex(ValueError, "phase metadata"):
self.plan("full")
def test_native_ui_guards_and_component_batch_share_safe_ui_owner_scope(self):
backend = Repository(
"govoplan-backend-only", self.root / "govoplan-backend-only"
)
backend.path.mkdir()
(self.module.path / "webui").mkdir()
(self.core.path / "webui").mkdir()
project = Project("fixture", (*self.project.repositories, backend), {})
with patch("govoplan_devkit.workspace.load_project", return_value=project):
checks = build_stages(self.root, "ui", ["example"], False)
for check in checks:
if check["id"] in {
"jsx-value-imports",
"heading-help",
"core.component-batch",
}:
self.assertEqual(
check["inputs"]["repos"],
["govoplan", "govoplan-core", "govoplan-example"],
)
else:
self.assertNotIn("inputs", check)
def test_unregistered_src_or_webui_disables_all_native_reuse(self):
for directory in ("src", "webui"):
with self.subTest(directory=directory):
unknown = self.root / "govoplan-unregistered" / directory
unknown.mkdir(parents=True)
try:
for profile in ("quick", "ui", "backend", "full"):
checks = self.plan(profile)
self.assertTrue(checks)
self.assertTrue(
all(check["reuse"] == "never" for check in checks)
)
self.assertTrue(
all(
any(
"Unregistered sibling" in note
for note in check["coverage_notes"]
)
for check in checks
)
)
finally:
unknown.rmdir()
unknown.parent.rmdir()
(self.root / "govoplan-unused-empty").mkdir()
self.assertTrue(all("reuse" not in check for check in self.plan("full")))
def test_unregistered_broken_source_symlink_does_not_allow_reuse(self):
unknown = self.root / "govoplan-unregistered"
unknown.mkdir()
(unknown / "src").symlink_to(self.root / "missing")
self.assertTrue(all(check["reuse"] == "never" for check in self.plan("full")))
def test_unknown_repo_is_not_silently_ignored(self):
with self.assertRaisesRegex(ValueError, "Unknown repository"):
self.plan("ui", ["not-a-repo"])
def test_changed_provider_selects_transitive_declared_consumers(self):
other = Repository("govoplan-other", self.root / "govoplan-other")
final = Repository("govoplan-final", self.root / "govoplan-final")
project = Project("fixture", (self.meta, self.module, other, final), {})
self.write("govoplan/tools/release/govoplan_release/contracts.py", "# fixture")
for name in ("example", "other", "final"):
self.write(f"govoplan-{name}/src/fixture/backend/manifest.py", "# fixture")
def contract(_path, repo_name):
gives = {
self.module.name: ["first"],
other.name: ["second"],
final.name: [],
}[repo_name]
needs = {
self.module.name: [],
other.name: ["first"],
final.name: ["second"],
}[repo_name]
return SimpleNamespace(
repo=repo_name,
provides_interfaces=[SimpleNamespace(name=value) for value in gives],
requires_interfaces=[SimpleNamespace(name=value) for value in needs],
)
with patch.dict(
sys.modules,
{
"govoplan_release.contracts": SimpleNamespace(
parse_manifest_contract=contract
)
},
):
selected, reason = _expanded_repositories(
project, [self.module], changed=True
)
self.assertEqual(
[repo.name for repo in selected], [self.module.name, other.name, final.name]
)
self.assertIn("declared interface consumers", reason)
def test_generic_dependency_closure_includes_filtered_prerequisites(self):
config = {
"schema_version": 1,
"name": "generic",
"repositories": [
{"name": "one", "path": "one"},
{"name": "two", "path": "two"},
],
"checks": [
{
"id": "compile",
"argv": ["{node}", "compile.mjs"],
"cwd": "one",
"repos": ["two"],
},
{
"id": "test",
"argv": ["{node}", "test.mjs"],
"cwd": "one",
"deps": ["compile"],
"repos": ["one"],
},
{
"id": "other",
"argv": ["{python}", "test.py"],
"cwd": "two",
"repos": ["two"],
},
],
"profiles": {"quick": ["test", "other"]},
}
self.write("project.json", json.dumps(config))
result = build_stages(
self.root, "quick", ["one"], False, self.root / "project.json"
)
self.assertEqual([item["id"] for item in result], ["compile", "test"])
self.assertEqual(result[1]["deps"], ["compile"])
self.assertEqual(result[1]["cwd"], str(self.root / "one"))
def test_generic_cycles_and_duplicate_ids_fail(self):
config = {
"schema_version": 1,
"name": "generic",
"repositories": [{"name": "one", "path": "one"}],
"checks": [
{"id": "one", "argv": ["test"], "deps": ["two"]},
{"id": "two", "argv": ["test"], "deps": ["one"]},
],
"profiles": {"quick": ["one"]},
}
self.write("project.json", json.dumps(config))
with self.assertRaisesRegex(ValueError, "Cyclic"):
build_stages(self.root, "quick", [], False, self.root / "project.json")
config["checks"][1]["id"] = "one"
self.write("project.json", json.dumps(config))
with self.assertRaisesRegex(ValueError, "Duplicate"):
build_stages(self.root, "quick", [], False, self.root / "project.json")
def test_generic_order_prerequisites_inputs_and_reuse_survive_planning(self):
config = {
"schema_version": 1,
"name": "generic",
"repositories": [
{"name": "one", "path": "one"},
{"name": "two", "path": "two"},
],
"checks": [
{
"id": "prepare",
"argv": ["prepare"],
"repos": ["two"],
"reuse": "never",
"inputs": {"repos": ["two"]},
},
{
"id": "test",
"argv": ["test"],
"repos": ["one"],
"after": ["prepare"],
"reuse": "verified",
"inputs": {"repos": ["one"]},
},
{"id": "broad", "argv": ["check"], "repos": ["one"]},
],
"profiles": {"quick": ["test", "broad"]},
}
self.write("project.json", json.dumps(config))
result = build_stages(
self.root, "quick", ["one"], False, self.root / "project.json"
)
self.assertEqual([item["id"] for item in result], ["prepare", "test", "broad"])
self.assertEqual(result[1]["after"], ["prepare"])
self.assertEqual(result[1]["deps"], [])
self.assertEqual(result[0]["reuse"], "never")
self.assertEqual(result[1]["reuse"], "verified")
self.assertEqual(result[0]["inputs"], {"repos": ["two"]})
self.assertEqual(result[1]["inputs"], {"repos": ["one"]})
self.assertNotIn("inputs", result[2])
def test_generic_original_types_are_validated_before_coercion(self):
for field, invalid in (
("argv", "false"),
("argv", None),
("resources", "shared"),
("cwd", None),
("cwd", "../outside"),
("title", 5),
("timeout_seconds", True),
):
with self.subTest(field=field, invalid=invalid):
config = {
"schema_version": 1,
"name": "generic",
"repositories": [{"name": "one", "path": "one"}],
"checks": [{"id": "test", "argv": ["test"], field: invalid}],
"profiles": {"quick": ["test"]},
}
self.write("project.json", json.dumps(config))
with self.assertRaises(ValueError):
build_stages(
self.root, "quick", [], False, self.root / "project.json"
)
def test_ui_coverage_notes_make_excluded_suites_explicit_and_discover_tests_folder(
self,
):
self.write(
"govoplan-example/webui/package.json",
json.dumps({"scripts": {"test:full-ui": "tsc && node tests/full-ui.js"}}),
)
self.write(
"govoplan-example/webui/tests/aggregate-report-structure.test.mjs",
"// fixture",
)
stages = self.plan("ui", ["example"])
self.assertTrue(
any("aggregate-report-structure" in item["id"] for item in stages)
)
self.assertTrue(
any("test:full-ui" in note for note in stages[0]["coverage_notes"])
)
def test_docs_reuses_existing_guards_and_only_narrows_plain_labels(self):
args = argparse.Namespace(
workspace_root=self.root,
state_dir=self.root / "state",
project=None,
repo=["example"],
changed=False,
)
with patch("govoplan_devkit.docs.load_project", return_value=self.project):
stages = build_doc_stages(args)
self.assertEqual(len(stages), 4)
self.assertIn("check-manifest-shapes.py", " ".join(stages[0]["argv"]))
self.assertIn("platform-interface-inventory.py", " ".join(stages[1]["argv"]))
self.assertIn("--strict-declarations", stages[1]["argv"])
self.assertEqual(stages[3]["argv"][-2:], ["--repo", "govoplan-example"])
self.assertFalse(
(self.root / "state").exists(), "Planning must not create artifacts"
)
if __name__ == "__main__":
unittest.main()
+158
View File
@@ -0,0 +1,158 @@
"""Public CLI contracts and an executable portable-project smoke fixture."""
import json
from pathlib import Path
import subprocess
import sys
from unittest.mock import patch
import pytest
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "tools/devkit"))
from govoplan_devkit import cli, doctor, runner
from govoplan_devkit.common import META_ROOT
@pytest.mark.parametrize(
"arguments",
[["--json", "commands"], ["commands", "--json"], ["commands", "--format", "json"]],
)
def test_global_output_flags_work_on_either_side_of_command(arguments, capsys):
assert cli.main(arguments) == 0
output = json.loads(capsys.readouterr().out)
names = {item["command"] for item in output["commands"]}
assert {"context", "check", "doctor", "docs", "issues", "release", "git"} <= names
@pytest.mark.parametrize(
"command",
[
"context",
"doctor",
"check",
"resume",
"recover",
"docs",
"review",
"issues",
"release",
"git",
],
)
def test_command_help_does_not_require_live_services(command, capsys):
with pytest.raises(SystemExit) as stopped:
cli.main([command, "--help"])
assert stopped.value.code == 0
assert "usage:" in capsys.readouterr().out
def test_portable_project_executes_registered_commands_and_reads_receipt(
tmp_path, capsys, monkeypatch
):
monkeypatch.setenv("XDG_STATE_HOME", str(tmp_path / "state"))
repo = tmp_path / "example"
repo.mkdir()
subprocess.run(["git", "init", "-q", str(repo)], check=True)
subprocess.run(
[
"git",
"-C",
str(repo),
"-c",
"user.name=Fixture",
"-c",
"user.email=fixture@example.invalid",
"commit",
"--allow-empty",
"-qm",
"fixture",
],
check=True,
)
project = tmp_path / "project.json"
project.write_text(
json.dumps(
{
"schema_version": 1,
"name": "Portable",
"repositories": [{"name": "example", "path": "example"}],
"checks": [
{
"id": "test",
"argv": ["{python}", "-c", "print('portable ok')"],
"cwd": "example",
}
],
"profiles": {"quick": ["test"]},
}
)
)
common = ["--workspace-root", str(tmp_path), "--project", str(project), "--json"]
with patch.object(runner, "environment_fingerprint", return_value="fixture"):
assert cli.main(common + ["check", "--profile", "quick"]) == 0
result = json.loads(capsys.readouterr().out)
assert result["status"] == "passed"
assert cli.main(["status", result["run_id"], *common]) == 0
status = json.loads(capsys.readouterr().out)
assert status["snapshot_verified"] is True
assert cli.main(["logs", result["run_id"], "--stage", "test", *common]) == 0
assert "portable ok" in capsys.readouterr().out
def test_portable_example_matches_published_schema():
import jsonschema
jsonschema.validate(
json.loads((META_ROOT / "tools/devkit/examples/project.json").read_text()),
json.loads((META_ROOT / "tools/devkit/project.schema.json").read_text()),
)
def test_malformed_project_is_a_controlled_json_error(tmp_path, capsys):
project = tmp_path / "bad.json"
project.write_text('{"schema_version":true,"repositories":[]}')
assert (
cli.main(
[
"context",
"--workspace-root",
str(tmp_path),
"--project",
str(project),
"--json",
]
)
== 2
)
assert json.loads(capsys.readouterr().out)["status"] == "error"
def test_doctor_is_read_only_and_preserves_dependency_warnings(tmp_path, capsys):
project = tmp_path / "project.json"
project.write_text(
json.dumps(
{"schema_version": 1, "repositories": [{"name": "example", "path": "."}]}
)
)
(tmp_path / "package.json").write_text("{}")
before = set(tmp_path.iterdir())
with (
patch.object(doctor, "tool_version", return_value="fixture"),
patch.object(doctor, "inspect_repository", return_value={"errors": []}),
):
assert (
cli.main(
[
"doctor",
"--workspace-root",
str(tmp_path),
"--project",
str(project),
"--json",
]
)
== 0
)
output = json.loads(capsys.readouterr().out)
assert any(item["status"] == "warning" for item in output["checks"])
assert set(tmp_path.iterdir()) == before
+339
View File
@@ -0,0 +1,339 @@
"""Coverage inventory is explicit intent, never execution or guessed completion."""
import json
from pathlib import Path
import re
import shutil
import sys
from unittest.mock import patch
import pytest
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / "tools/devkit"))
from govoplan_devkit.catalog import build_coverage, build_stages, module_ui_stages # noqa: E402
from govoplan_devkit.coverage import canonical_invocations # noqa: E402
from govoplan_devkit.package_tests import ( # noqa: E402
CORE_COMPONENT_SUITES,
declared_tests,
read_package,
)
from govoplan_devkit.workspace import Project, Repository # noqa: E402
@pytest.fixture
def fixture(tmp_path):
core = Repository("govoplan-core", tmp_path / "govoplan-core", ("core",))
meta = Repository("govoplan", tmp_path / "govoplan", ("meta",))
module = Repository("govoplan-example", tmp_path / "govoplan-example", ("example",))
for repo in (core, meta, module):
(repo.path / "webui/scripts").mkdir(parents=True)
(repo.path / "webui/tests").mkdir()
scripts = {
"test:components": "node scripts/run-component-tests.mjs",
**{
f"test:{name}": f"node scripts/run-component-tests.mjs {name}"
for name in CORE_COMPONENT_SUITES
},
}
(core.path / "webui/package.json").write_text(json.dumps({"scripts": scripts}))
(core.path / "webui/scripts/run-component-tests.mjs").write_text("// fixture")
(module.path / "webui/package.json").write_text(
json.dumps(
{
"scripts": {
"test:safe": "node --test tests/source.test.mjs",
"test:compound": "tsc && node test.js",
"test:bad-quote": "node 'broken",
}
}
)
)
(module.path / "webui/tests/source.test.mjs").write_text("// fixture")
script = meta.path / "tools/checks/check-focused.sh"
script.parent.mkdir(parents=True)
phase_metadata = ROOT / "tools/checks/focused-phases.json"
shutil.copyfile(phase_metadata, script.with_name("focused-phases.json"))
bodies = {
"core-ui": 'cd "$ROOT/webui"\n"$NPM" run test:components -- layout-primitives page-layout data-grid-actions mail-components\n',
"module-ui": 'cd "$WORKSPACE_ROOT/govoplan-example/webui"\n"$NPM" run test:compound\n',
}
script.write_text(
"\n".join(
f"focused_phase_{phase['id'].replace('-', '_')}() {{\n# devkit-phase: {phase['id']} begin\n"
+ bodies.get(phase["id"], 'cd "$ROOT"\n')
+ f"# devkit-phase: {phase['id']} end\n}}\n"
for phase in json.loads(phase_metadata.read_text())["phases"]
)
)
project = Project("Fixture", (meta, core, module), {})
with (
patch("govoplan_devkit.workspace.load_project", return_value=project),
patch("govoplan_devkit.coverage.load_project", return_value=project),
):
yield tmp_path, core, module, project
def rows(coverage, repo="govoplan-core"):
return {row["name"]: row for row in coverage["suites"] if row["repo"] == repo}
def test_core_aliases_are_explicitly_excluded_in_quick_and_covered_by_one_ui_stage(
fixture,
):
root, _, _, _ = fixture
quick = rows(build_coverage(root, "quick", [], False))
ui = rows(build_coverage(root, "ui", [], False))
for suite in CORE_COMPONENT_SUITES:
assert quick["test:" + suite]["disposition"] == "excluded"
assert "quick" in quick["test:" + suite]["reason"]
assert ui["test:" + suite]["disposition"] == "covered_elsewhere"
assert ui["test:" + suite]["covering_stage"] == "core.component-batch"
assert ui["test:components"]["covered_components"] == list(CORE_COMPONENT_SUITES)
def test_full_reports_only_four_of_sixteen_components_and_exact_shell_suite(fixture):
root, _, _, _ = fixture
result = build_coverage(root, "full", ["example"], False)
core = rows(result)
aliases = [core["test:" + suite] for suite in CORE_COMPONENT_SUITES]
assert sum(item["disposition"] == "covered_elsewhere" for item in aliases) == 4
assert sum(item["disposition"] == "excluded" for item in aliases) == 12
assert len(core["test:components"]["covered_components"]) == 4
assert "4/16" in core["test:components"]["reason"]
module = rows(result, "govoplan-example")
assert module["test:compound"]["disposition"] == "planned"
assert module["test:safe"]["disposition"] == "excluded"
assert result["stages"] == [
"focused." + phase["id"]
for phase in json.loads(
(ROOT / "tools/checks/focused-phases.json").read_text()
)["phases"]
]
assert core["test:components"]["covering_stage"] == "focused.core-ui"
assert module["test:compound"]["covering_stage"] == "focused.module-ui"
def test_prebuilt_plan_avoids_replanning(fixture):
root, _, _, _ = fixture
stages = build_stages(root, "quick", [], False)
with patch(
"govoplan_devkit.catalog.build_stages",
side_effect=AssertionError("must not replan"),
):
result = build_coverage(root, "quick", [], False, stages=stages)
assert rows(result, "govoplan-example")["test:safe"]["disposition"] == "planned"
assert sum(result["counts"].values()) == result["suite_count"]
assert all(item["reason"] for item in result["suites"])
def test_missing_or_spoofed_phase_stage_does_not_grant_component_coverage(fixture):
root, _, _, _ = fixture
stages = build_stages(root, "full", [], False)
without_core = [item for item in stages if item["id"] != "focused.core-ui"]
result = build_coverage(root, "full", [], False, stages=without_core)
assert rows(result)["test:components"]["covered_components"] == []
assert (
rows(result, "govoplan-example")["test:compound"]["covering_stage"]
== "focused.module-ui"
)
next(item for item in stages if item["id"] == "focused.core-ui")["argv"] = ["true"]
spoofed = build_coverage(root, "full", [], False, stages=stages)
assert rows(spoofed)["test:components"]["covered_components"] == []
@pytest.mark.parametrize("wrapper", ["heredoc", "function", "conditional"])
def test_lookalike_phase_wrappers_outside_top_level_do_not_grant_coverage(
fixture, wrapper
):
root, core, _, project = fixture
meta = next(repo.path for repo in project.repositories if repo.name == "govoplan")
path = meta / "tools/checks/check-focused.sh"
fake = 'focused_phase_core_ui() {\n# devkit-phase: core-ui begin\ncd "$ROOT/webui"\n"$NPM" run test:spoof\n# devkit-phase: core-ui end\n}\n'
prefix = {
"heredoc": "cat <<'BODY'\n" + fake + "BODY\n",
"function": "unused() {\n" + fake + "}\n",
"conditional": "if false; then\n" + fake + "fi\n",
}[wrapper]
path.write_text(prefix + path.read_text())
result = canonical_invocations(root, meta, core.path)
assert result["notes"] == []
assert [item["name"] for item in result["npm"]] == [
"test:components",
"test:compound",
]
@pytest.mark.parametrize("damage", ["missing", "duplicate", "bad-end"])
def test_invalid_marked_phase_bodies_do_not_infer_coverage(fixture, damage):
root, core, _, project = fixture
meta = next(repo.path for repo in project.repositories if repo.name == "govoplan")
path = meta / "tools/checks/check-focused.sh"
text = path.read_text()
if damage == "missing":
text = text.replace("focused_phase_core_ui()", "unregistered_core_ui()")
elif damage == "duplicate":
text += text
else:
text = text.replace(
"# devkit-phase: core-ui end", "# devkit-phase: another end"
)
path.write_text(text)
result = canonical_invocations(root, meta, core.path)
assert result["npm"] == [] and result["node"] == []
assert "no phase coverage inferred" in result["notes"][0]
@pytest.mark.parametrize(
"repo_name,name,command",
[
("govoplan-example", "test:components", "node scripts/run-component-tests.mjs"),
(
"govoplan-core",
"test:dialog-focus",
"node scripts/run-component-tests.mjs dialog-focus && npm run dev",
),
(
"govoplan-core",
"test:unrecognized",
"node scripts/run-component-tests.mjs dialog-focus",
),
],
)
def test_component_alias_exemption_is_exact_and_core_only(
tmp_path, repo_name, name, command
):
repo = Repository(repo_name, tmp_path / repo_name)
package = repo.path / "webui/package.json"
package.parent.mkdir(parents=True)
package.write_text(json.dumps({"scripts": {name: command}}))
declared = declared_tests(repo, package)
assert declared[0]["component_suite"] is None
assert declared[0]["_argv"] is None
assert module_ui_stages(repo, reason="fixture") == []
@pytest.mark.parametrize(
"body",
[
"[]",
"null",
'{"scripts":[]}',
'{"scripts":{"test:bad":42}}',
'{"scripts":{"test:one":"node a","test:one":"node b"}}',
"[" * 2000 + "]" * 2000,
],
)
def test_malformed_package_metadata_is_a_controlled_error(tmp_path, body):
package = tmp_path / "package.json"
package.write_text(body)
with pytest.raises(ValueError):
read_package(package)
def test_oversize_package_and_symlinked_test_are_not_discovered(tmp_path):
repo = Repository("example", tmp_path)
webui = tmp_path / "webui"
(webui / "tests").mkdir(parents=True)
package = webui / "package.json"
package.write_text(" " * (1024 * 1024 + 1))
with pytest.raises(ValueError):
read_package(package)
package.write_text(
json.dumps({"scripts": {"test:escape": "node tests/escape.mjs"}})
)
(webui / "tests/escape.mjs").symlink_to(tmp_path / "outside.mjs")
(tmp_path / "outside.mjs").write_text("// fixture")
assert module_ui_stages(repo, reason="fixture") == []
def test_unparseable_and_sensitive_commands_do_not_leak_into_coverage(fixture):
root, _, module, _ = fixture
package = module.path / "webui/package.json"
package.write_text(
json.dumps(
{
"scripts": {
"test:secret": "node tests/source.test.mjs --token unknown-private-value",
"test:broken": "node 'unknown-other-secret",
}
}
)
)
result = build_coverage(root, "quick", [], False)
encoded = json.dumps(result)
assert (
"unknown-private-value" not in encoded and "unknown-other-secret" not in encoded
)
assert "_argv" not in encoded and '"command"' not in encoded
assert all(
item["disposition"] == "unsupported"
for item in rows(result, "govoplan-example").values()
)
def test_custom_check_coverage_redacts_separate_token_and_includes_unselected(tmp_path):
project = tmp_path / "project.json"
project.write_text(
json.dumps(
{
"schema_version": 1,
"repositories": [{"name": "app", "path": "."}],
"checks": [
{
"id": "one",
"argv": ["check", "--token", "unknown-private-value"],
},
{"id": "two", "argv": ["true"]},
],
"profiles": {"quick": ["one"]},
}
)
)
result = build_coverage(tmp_path, "quick", [], False, project)
assert "unknown-private-value" not in json.dumps(result)
assert result["counts"]["planned"] == 1 and result["counts"]["excluded"] == 1
def test_canonical_parser_does_not_credit_comments_heredocs_conditionals_or_chains(
tmp_path,
):
meta, core = tmp_path / "govoplan", tmp_path / "govoplan-core"
path = meta / "tools/checks/check-focused.sh"
path.parent.mkdir(parents=True)
path.write_text(
'cd "$ROOT/webui"\n# "$NPM" run test:comment\n"$PYTHON" - <<\'PY\'\n"$NPM" run test:heredoc\nPY\nif false; then\n"$NPM" run test:conditional\nfi\n"$NPM" run test:compound && true\n"$NPM" run test:real\n'
)
result = canonical_invocations(tmp_path, meta, core)
assert [item["name"] for item in result["npm"]] == ["test:real"]
def test_real_canonical_gate_has_four_explicit_component_suites():
result = canonical_invocations(ROOT.parent, ROOT, ROOT.parent / "govoplan-core")
calls = [item for item in result["npm"] if item["name"] == "test:components"]
assert len(calls) == 1
assert calls[0]["args"] == [
"layout-primitives",
"page-layout",
"data-grid-actions",
"mail-components",
]
assert calls[0]["phase"] == "core-ui"
for name, phase in (
("test:module-permutations", "module-builds"),
("test:conformance", "browser"),
):
matching = [item for item in result["npm"] if item["name"] == name]
assert len(matching) == 1 and matching[0]["phase"] == phase
def test_known_core_component_aliases_match_the_owned_runner_registry():
runner = (
ROOT.parent / "govoplan-core/webui/scripts/run-component-tests.mjs"
).read_text()
block = runner.split("export const componentSuites = Object.freeze({", 1)[1].split(
"});", 1
)[0]
assert set(re.findall(r'^ "([a-z0-9-]+)":', block, re.M)) == set(
CORE_COMPONENT_SUITES
)
+164
View File
@@ -0,0 +1,164 @@
"""Portable preflight inference uses fixtures only and never installs or starts tools."""
from argparse import Namespace
from pathlib import Path
import sys
from unittest.mock import patch
import pytest
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "tools/devkit"))
from govoplan_devkit import doctor
from govoplan_devkit.workspace import Project, Repository
def project(tmp_path, checks, *, tools=None, profiles=None):
return Project(
"Fixture",
(Repository("app", tmp_path / "app"), Repository("other", tmp_path / "other")),
{
"checks": checks,
"profiles": profiles
if profiles is not None
else {"quick": [item["id"] for item in checks]},
"tools": tools or {},
},
)
def check(identity, executable, *, repos=None, deps=None):
return {
"id": identity,
"argv": [executable, "--version"],
"cwd": ".",
"repos": repos or [],
"deps": deps or [],
}
def diagnose(tmp_path, configured, *, profile=None, repos=None, versions=None):
args = Namespace(
workspace_root=tmp_path,
project=tmp_path / "not-read.json",
repo=repos or [],
profile=profile,
)
tools = {"python": sys.executable, "node": "fixture-node", "npm": "fixture-npm"}
version_map = versions or {
sys.executable: "Python 3",
"fixture-node": "unavailable",
"fixture-npm": "unavailable",
}
with (
patch.object(doctor, "load_project", return_value=configured),
patch.object(doctor, "resolve_tools", return_value=tools),
patch.object(
doctor,
"tool_version",
side_effect=lambda executable, _env: version_map[executable],
) as probe,
patch.object(doctor, "inspect_repository", return_value={"errors": []}),
):
result = doctor.diagnose(args)
return result, probe
def test_python_only_portable_project_does_not_probe_or_block_on_unused_node(tmp_path):
configured = project(tmp_path, [check("python-test", "{python}")])
result, probe = diagnose(tmp_path, configured)
assert result["_exit_code"] == 0
assert result["required_tools"] == ["python"]
assert probe.call_count == 1
assert {
item["id"] for item in result["checks"] if item["status"] == "not_required"
} == {"node", "npm"}
def test_selected_profile_excludes_other_profile_tool_requirements(tmp_path):
configured = project(
tmp_path,
[check("py", "{python}"), check("js", "{npm}")],
profiles={"quick": ["py"], "ui": ["js"]},
)
result, _ = diagnose(tmp_path, configured, profile="quick")
assert result["required_tools"] == ["python"]
result, _ = diagnose(tmp_path, configured)
assert result["required_tools"] == ["node", "npm", "python"]
assert result["_exit_code"] == 1
def test_selected_repository_includes_dependency_tool_requirements(tmp_path):
configured = project(
tmp_path,
[
check("build", "{npm}", repos=["other"]),
check("test", "{python}", repos=["app"], deps=["build"]),
],
)
result, _ = diagnose(tmp_path, configured, repos=["app"])
assert result["required_tools"] == ["node", "npm", "python"]
def test_unselected_repository_does_not_require_its_tool(tmp_path):
configured = project(
tmp_path,
[check("js", "{npm}", repos=["other"]), check("py", "{python}", repos=["app"])],
)
result, _ = diagnose(tmp_path, configured, repos=["app"])
assert result["required_tools"] == ["python"]
def test_explicit_tool_configuration_declares_indirect_script_dependency(tmp_path):
configured = project(tmp_path, [check("shell", "sh")], tools={"npm": "fixture-npm"})
result, _ = diagnose(tmp_path, configured)
assert result["required_tools"] == ["node", "npm", "python"]
assert result["_exit_code"] == 1
@pytest.mark.parametrize(
"executable,expected",
[
("/opt/node/bin/node", ["node", "python"]),
("npm", ["node", "npm", "python"]),
("npx", ["node", "npm", "python"]),
],
)
def test_direct_tool_names_are_inferred(tmp_path, executable, expected):
result, _ = diagnose(tmp_path, project(tmp_path, [check("test", executable)]))
assert result["required_tools"] == expected
def test_context_only_project_needs_no_node_and_makes_no_files(tmp_path):
before = set(tmp_path.iterdir())
result, _ = diagnose(tmp_path, project(tmp_path, [], profiles={}))
assert result["required_tools"] == ["python"]
assert set(tmp_path.iterdir()) == before
def test_missing_profile_is_not_silently_widened(tmp_path):
with pytest.raises(ValueError, match="does not declare profile"):
diagnose(tmp_path, project(tmp_path, []), profile="ui")
def test_native_govoplan_still_requires_all_three_tools(tmp_path):
configured = project(tmp_path, [])
args = Namespace(workspace_root=tmp_path, project=None, repo=[], profile=None)
with (
patch.object(doctor, "load_project", return_value=configured),
patch.object(
doctor,
"resolve_tools",
return_value={"python": sys.executable, "node": "node", "npm": "npm"},
),
patch.object(
doctor,
"tool_version",
side_effect=lambda executable, _env: (
"Python" if executable == sys.executable else "unavailable"
),
),
patch.object(doctor, "inspect_repository", return_value={"errors": []}),
):
result = doctor.diagnose(args)
assert result["required_tools"] == ["node", "npm", "python"]
assert result["_exit_code"] == 1
+378
View File
@@ -0,0 +1,378 @@
"""Environment probes read bounded stable files; all fixtures are local."""
import hashlib
import os
from pathlib import Path
import sys
from types import SimpleNamespace
from unittest.mock import patch
import pytest
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "tools/devkit"))
from govoplan_devkit import environment
from govoplan_devkit.common import digest
from govoplan_devkit.workspace import Project, Repository
def mutate_during_read(monkeypatch, callback):
original = hashlib.sha256
mutated = False
class MutatingHasher:
def __init__(self):
self.hasher = original()
def update(self, chunk):
nonlocal mutated
self.hasher.update(chunk)
if not mutated:
mutated = True
callback()
def hexdigest(self):
return self.hasher.hexdigest()
monkeypatch.setattr(environment.hashlib, "sha256", MutatingHasher)
@pytest.mark.parametrize("content", [b"", b"stable", b"x" * (1024 * 1024 + 3)])
def test_file_hash_keeps_existing_digest_and_exact_size_boundary(tmp_path, content):
path = tmp_path / "input"
path.write_bytes(content)
assert (
environment._environment_file_hash(path, len(content))
== hashlib.sha256(content).hexdigest()
)
def test_missing_optional_file_remains_optional_without_a_persistent_cache(tmp_path):
path = tmp_path / "input"
assert environment._environment_file_hash(path, 10) is None
path.write_bytes(b"first")
first = environment._environment_file_hash(path, 10)
path.write_bytes(b"other")
assert environment._environment_file_hash(path, 10) != first
@pytest.mark.parametrize("kind", ["fifo", "directory", "oversized", "dangling"])
def test_present_unsafe_input_is_rejected_without_blocking(tmp_path, kind):
path = tmp_path / "input"
if kind == "fifo":
os.mkfifo(path)
elif kind == "directory":
path.mkdir()
elif kind == "oversized":
path.write_bytes(b"too large")
else:
path.symlink_to(tmp_path / "missing")
with pytest.raises(ValueError, match="Environment input"):
environment._environment_file_hash(path, 2)
def test_stable_venv_executable_symlink_keeps_original_path_and_identity(tmp_path):
target = tmp_path / "real-python"
target.write_bytes(b"fixture binary")
executable = tmp_path / "venv" / "bin" / "python"
executable.parent.mkdir(parents=True)
executable.symlink_to(target)
repo = tmp_path / "repo"
metadata = repo / "node_modules" / ".package-lock.json"
metadata.parent.mkdir(parents=True)
metadata.write_bytes(b'{"fixture":true}')
project = Project("Fixture", (Repository("repo", repo),), {})
tools = {"python": str(executable)}
env = {"PATH": "fixture", "PWD": "ignored"}
distributions = b'[["fixture", "1"]]\n'
with (
patch.object(environment, "tool_version", return_value="fixture-version"),
patch.object(
environment,
"require_capture",
return_value=SimpleNamespace(returncode=0, stdout=distributions),
) as capture,
):
actual = environment.environment_fingerprint(tmp_path, project, tools, env)
assert actual == digest(
{
"environment": {"PATH": "fixture"},
"tools": {
"python": {
"path": str(executable),
"version": "fixture-version",
"sha256": hashlib.sha256(target.read_bytes()).hexdigest(),
}
},
"installed": {
str(metadata): hashlib.sha256(metadata.read_bytes()).hexdigest(),
"python_distributions": hashlib.sha256(distributions).hexdigest(),
},
}
)
assert capture.call_args.args[0][0] == str(executable)
def test_symlinked_package_directory_is_allowed_when_stable(tmp_path):
actual = tmp_path / "packages"
actual.mkdir()
(actual / ".package-lock.json").write_bytes(b"fixture")
link = tmp_path / "node_modules"
link.symlink_to(actual, target_is_directory=True)
assert environment._environment_file_hash(link / ".package-lock.json", 10)
def test_fifo_replacement_between_inspection_and_open_does_not_block(
tmp_path, monkeypatch
):
path = tmp_path / "input"
path.write_bytes(b"fixture")
real_open = os.open
def replace_before_open(target, flags):
path.unlink()
os.mkfifo(path)
assert flags & os.O_NONBLOCK
return real_open(target, flags)
monkeypatch.setattr(environment.os, "open", replace_before_open)
with pytest.raises(ValueError, match="bounded regular"):
environment._environment_file_hash(path, 10)
def test_growth_between_inspection_and_open_cannot_bypass_size_bound(
tmp_path, monkeypatch
):
path = tmp_path / "input"
path.write_bytes(b"a")
real_open = os.open
def grow_before_open(target, flags):
path.write_bytes(b"x" * 11)
return real_open(target, flags)
monkeypatch.setattr(environment.os, "open", grow_before_open)
with pytest.raises(ValueError, match="bounded regular"):
environment._environment_file_hash(path, 10)
def test_growth_during_read_cannot_bypass_size_bound(tmp_path, monkeypatch):
path = tmp_path / "input"
path.write_bytes(b"a")
mutate_during_read(monkeypatch, lambda: path.write_bytes(b"x" * 11))
with pytest.raises(ValueError, match="grew beyond"):
environment._environment_file_hash(path, 10)
def test_same_size_edit_with_restored_mtime_is_not_a_stable_identity(
tmp_path, monkeypatch
):
path = tmp_path / "input"
path.write_bytes(b"first")
metadata = path.stat()
def mutate():
path.write_bytes(b"other")
os.utime(path, ns=(metadata.st_atime_ns, metadata.st_mtime_ns))
mutate_during_read(monkeypatch, mutate)
with pytest.raises(ValueError, match="changed during"):
environment._environment_file_hash(path, 10)
def test_replacement_with_identical_bytes_is_not_a_stable_identity(
tmp_path, monkeypatch
):
path = tmp_path / "input"
path.write_bytes(b"fixture")
replacement = tmp_path / "replacement"
replacement.write_bytes(b"fixture")
mutate_during_read(monkeypatch, lambda: replacement.replace(path))
with pytest.raises(ValueError, match="changed during"):
environment._environment_file_hash(path, 10)
def test_symlink_retarget_with_identical_bytes_is_not_a_stable_identity(
tmp_path, monkeypatch
):
first, second, link = (tmp_path / name for name in ("first", "second", "python"))
first.write_bytes(b"fixture")
second.write_bytes(b"fixture")
link.symlink_to(first)
def retarget():
link.unlink()
link.symlink_to(second)
mutate_during_read(monkeypatch, retarget)
with pytest.raises(ValueError, match="changed during"):
environment._environment_file_hash(link, 10)
def test_source_disappearing_during_read_fails_closed(tmp_path, monkeypatch):
path = tmp_path / "input"
path.write_bytes(b"fixture")
mutate_during_read(monkeypatch, path.unlink)
with pytest.raises(ValueError, match="Environment input"):
environment._environment_file_hash(path, 10)
def test_parent_symlink_retarget_is_rejected_even_for_the_same_target_inode(
tmp_path, monkeypatch
):
first, second, link = (tmp_path / name for name in ("first", "second", "bin"))
first.mkdir()
second.mkdir()
(first / "python").write_bytes(b"fixture")
os.link(first / "python", second / "python")
link.symlink_to(first, target_is_directory=True)
def retarget():
link.unlink()
link.symlink_to(second, target_is_directory=True)
mutate_during_read(monkeypatch, retarget)
with pytest.raises(ValueError, match="changed during"):
environment._environment_file_hash(link / "python", 10)
def test_invalid_executable_is_rejected_before_any_version_process(tmp_path):
executable = tmp_path / "python"
os.mkfifo(executable)
project = Project("Fixture", (), {})
with patch.object(environment, "tool_version") as version:
with pytest.raises(ValueError, match="bounded regular"):
environment.environment_fingerprint(
tmp_path, project, {"python": str(executable)}, {}
)
version.assert_not_called()
def discovery_project(root, *, path="govoplan-backend"):
repo = root / path
(repo / "src").mkdir(parents=True)
return Project(
"GovOPlaN",
(Repository("govoplan-backend", repo),),
{"organization": "GovOPlaN"},
)
def test_native_shape_ignores_build_files_and_directory_timestamps(tmp_path):
project = discovery_project(tmp_path)
repo = project.repositories[0].path
(repo / "webui").mkdir()
before = environment._native_discovery_fingerprint(tmp_path, project)
for directory in (repo, repo / "src", repo / "webui"):
(directory / "temporary-build-output").write_text("changed")
(directory / "temporary-build-directory").mkdir()
assert environment._native_discovery_fingerprint(tmp_path, project) == before
(repo / "webui" / "temporary-build-output").unlink()
assert environment._native_discovery_fingerprint(tmp_path, project) == before
def test_native_shape_detects_unknown_sibling_and_source_addition_removal(tmp_path):
project = discovery_project(tmp_path)
baseline = environment._native_discovery_fingerprint(tmp_path, project)
sibling = tmp_path / "govoplan-unregistered"
sibling.mkdir()
empty = environment._native_discovery_fingerprint(tmp_path, project)
assert empty != baseline
for name in ("src", "webui"):
(sibling / name).mkdir()
assert environment._native_discovery_fingerprint(tmp_path, project) != empty
(sibling / name).rmdir()
assert environment._native_discovery_fingerprint(tmp_path, project) == empty
sibling.rmdir()
assert environment._native_discovery_fingerprint(tmp_path, project) == baseline
@pytest.mark.parametrize("repo_path", ["govoplan-backend", "nonstandard-layout"])
def test_native_shape_detects_registered_backend_gaining_webui(tmp_path, repo_path):
project = discovery_project(tmp_path, path=repo_path)
baseline = environment._native_discovery_fingerprint(tmp_path, project)
webui = project.repositories[0].path / "webui"
webui.mkdir()
assert environment._native_discovery_fingerprint(tmp_path, project) != baseline
webui.rmdir()
assert environment._native_discovery_fingerprint(tmp_path, project) == baseline
def test_native_shape_binds_registered_ownership_names(tmp_path):
project = discovery_project(tmp_path, path="nonstandard-layout")
renamed = Project(
project.name,
(Repository("govoplan-renamed", project.repositories[0].path),),
project.config,
)
assert environment._native_discovery_fingerprint(
tmp_path, project
) != environment._native_discovery_fingerprint(tmp_path, renamed)
def test_native_shape_detects_source_link_retarget_and_dangling_target(tmp_path):
project = discovery_project(tmp_path)
first, second = tmp_path / "first", tmp_path / "second"
first.mkdir()
second.mkdir()
link = project.repositories[0].path / "webui"
link.symlink_to(first, target_is_directory=True)
baseline = environment._native_discovery_fingerprint(tmp_path, project)
link.unlink()
link.symlink_to(second, target_is_directory=True)
changed = environment._native_discovery_fingerprint(tmp_path, project)
assert changed != baseline
second.rmdir()
assert environment._native_discovery_fingerprint(tmp_path, project) != changed
def test_native_shape_encodes_non_directory_source_entries(tmp_path):
project = discovery_project(tmp_path)
webui = project.repositories[0].path / "webui"
baseline = environment._native_discovery_fingerprint(tmp_path, project)
webui.write_text("not a directory")
file_shape = environment._native_discovery_fingerprint(tmp_path, project)
assert file_shape != baseline
webui.unlink()
webui.mkdir()
assert environment._native_discovery_fingerprint(tmp_path, project) != file_shape
def test_native_shape_audit_is_bounded_including_nonmatching_children(
tmp_path, monkeypatch
):
project = discovery_project(tmp_path)
(tmp_path / "unrelated-one").mkdir()
(tmp_path / "unrelated-two").mkdir()
monkeypatch.setattr(environment, "MAX_DISCOVERY_CHILDREN", 2)
with pytest.raises(ValueError, match="bounded ownership"):
environment._native_discovery_fingerprint(tmp_path, project)
def test_native_shape_resolution_loop_fails_closed(tmp_path):
project = discovery_project(tmp_path)
link = project.repositories[0].path / "webui"
link.symlink_to(link)
with pytest.raises(ValueError, match="discovery cannot be resolved"):
environment._native_discovery_fingerprint(tmp_path, project)
@pytest.mark.parametrize("portable", [False, True])
def test_only_native_environment_identity_binds_discovery_shape(tmp_path, portable):
project = discovery_project(tmp_path)
if portable:
project.config["schema_version"] = 1
executable = tmp_path / "fixture-python"
executable.write_text("not executed")
tools = {"python": str(executable)}
with (
patch.object(environment, "tool_version", return_value="fixture"),
patch.object(
environment,
"require_capture",
return_value=SimpleNamespace(returncode=0, stdout=b"[]"),
),
):
baseline = environment.environment_fingerprint(tmp_path, project, tools, {})
(tmp_path / "govoplan-new" / "src").mkdir(parents=True)
changed = environment.environment_fingerprint(tmp_path, project, tools, {})
assert (baseline == changed) is portable
+601
View File
@@ -0,0 +1,601 @@
"""Incremental checkpoints use isolated local repositories, never product or remote state."""
from argparse import Namespace
import argparse
import json
from pathlib import Path
import subprocess
import sys
import threading
import time
from unittest.mock import patch
import pytest
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "tools/devkit"))
from govoplan_devkit import runner
from govoplan_devkit.checkpoints import Checkpoints
from govoplan_devkit.common import atomic_json, read_json, resource_lock, state_root
@pytest.fixture
def workspace(tmp_path, monkeypatch):
monkeypatch.setenv("XDG_STATE_HOME", str(tmp_path / "xdg"))
root = tmp_path / "workspace"
repos = {}
for name in ("alpha", "beta"):
repo = root / name
repo.mkdir(parents=True)
subprocess.run(["git", "init", "-q", str(repo)], check=True)
(repo / "source.txt").write_text(name + " original\n")
subprocess.run(["git", "-C", str(repo), "add", "source.txt"], check=True)
subprocess.run(
[
"git",
"-C",
str(repo),
"-c",
"user.name=Fixture",
"-c",
"user.email=fixture@example.invalid",
"commit",
"-qm",
"fixture",
],
check=True,
)
repos[name] = repo
config = tmp_path / "project.json"
config.write_text(
json.dumps(
{
"schema_version": 1,
"name": "Incremental fixture",
"repositories": [{"name": name, "path": name} for name in repos],
"checks": [],
"profiles": {},
}
)
)
traces = tmp_path / "traces"
traces.mkdir()
args = Namespace(
workspace_root=root,
project=config,
state_dir=tmp_path / "state",
dry_run=False,
jobs=2,
profile="quick",
resume=None,
repo=[],
changed=False,
)
return args, repos, traces
def stage(workspace, identity, repo="alpha", *, inputs=True, body="", **extra):
_, repos, traces = workspace
counter = traces / identity
code = (
"from pathlib import Path; "
f"counter=Path({str(counter)!r}); "
"counter.write_text(str(int(counter.read_text())+1 if counter.exists() else 1)); "
f"print({identity!r},flush=True); " + body
)
result = {
"id": identity,
"title": identity,
"argv": [sys.executable, "-c", code],
"cwd": str(repos[repo]),
"timeout_seconds": 5,
**extra,
}
if inputs:
result["inputs"] = {"repos": [repo]}
return result
def run(workspace, stages, *, environment="e" * 64):
args, _, _ = workspace
with patch.object(runner, "environment_fingerprint", return_value=environment):
return runner.run_checks(args, stages)
def count(workspace, identity):
path = workspace[2] / identity
return int(path.read_text()) if path.exists() else 0
def stages_by_id(result):
return {item["id"]: item for item in result["stages"]}
def test_checkpoint_probes_and_durable_persistence_hold_stage_resource_lock(workspace):
args, _, _ = workspace
locks = state_root(args.workspace_root) / "resource-locks"
original_identity, original_write = Checkpoints.identity, runner.atomic_json
probes, persisted = [], []
def assert_held():
with pytest.raises(RuntimeError, match="busy"):
with resource_lock(locks, "checkpoint-fixture"):
pass
def identity(self, selected):
assert_held()
probes.append(selected["id"])
return original_identity(self, selected)
def write(path, payload):
original_write(path, payload)
if (
payload.get("phase") == "checking"
and payload["stages"][0].get("checkpoint_verified") is True
and not persisted
):
assert_held()
assert read_json(path)["stages"][0]["checkpoint_verified"] is True
persisted.append(path)
with (
patch.object(Checkpoints, "identity", identity),
patch.object(runner, "atomic_json", side_effect=write),
):
result = run(
workspace, [stage(workspace, "a", resources=["checkpoint-fixture"])]
)
assert result["status"] == "passed"
assert len(probes) >= 2 and len(persisted) == 1
def test_checkpoint_save_failure_never_releases_dependent_execution(workspace):
args, _, _ = workspace
original_write, original_command, original_wait = (
runner.atomic_json,
runner._execute_stage_command,
runner.wait,
)
scheduler_waiting = threading.Event()
checkpoint_stalled = threading.Event()
scheduler_rechecked = threading.Event()
release_failure = threading.Event()
failed_write_paths, results, errors = [], [], []
def command(selected, *values, **kwargs):
if selected["id"] == "producer":
# Let the scheduler finish its initial running-state save and enter
# its wait loop before the command publishes a passing checkpoint.
assert scheduler_waiting.wait(timeout=5)
return original_command(selected, *values, **kwargs)
def wait(*values, **kwargs):
scheduler_waiting.set()
result = original_wait(*values, **kwargs)
if checkpoint_stalled.is_set():
scheduler_rechecked.set()
return result
def write(path, payload):
producer = next(
(item for item in payload.get("stages", []) if item["id"] == "producer"),
None,
)
if (
producer
and producer.get("checkpoint_verified") is True
and not failed_write_paths
):
failed_write_paths.append(path)
checkpoint_stalled.set()
assert release_failure.wait(timeout=5)
raise OSError("fixture checkpoint persistence failed")
return original_write(path, payload)
def execute():
try:
results.append(
run(
workspace,
[
stage(workspace, "producer"),
stage(workspace, "dependent", "beta", deps=["producer"]),
],
)
)
except BaseException as exc:
errors.append(exc)
worker = threading.Thread(target=execute)
with (
patch.object(runner, "atomic_json", side_effect=write),
patch.object(runner, "_execute_stage_command", side_effect=command),
patch.object(runner, "wait", side_effect=wait),
):
worker.start()
try:
assert checkpoint_stalled.wait(timeout=5)
assert scheduler_rechecked.wait(timeout=5)
# Keep persistence blocked across a scheduling turn: the in-memory
# producer status must not grant authority to start a consumer.
time.sleep(0.15)
assert count(workspace, "dependent") == 0
durable = read_json(failed_write_paths[0])
assert (
stages_by_id(durable)["producer"].get("checkpoint_verified") is not True
)
finally:
release_failure.set()
worker.join(timeout=8)
assert not worker.is_alive() and not errors
assert len(results) == 1 and results[0]["status"] == "failed"
assert count(workspace, "producer") == 1 and count(workspace, "dependent") == 0
by_id = stages_by_id(results[0])
assert by_id["producer"]["status"] == "failed"
assert by_id["producer"]["checkpoint_verified"] is False
assert "fixture checkpoint persistence failed" in by_id["producer"]["error"]
assert by_id["dependent"]["status"] == "skipped"
persisted = runner.read_receipt(
args.workspace_root, args.state_dir, results[0]["run_id"]
)
assert stages_by_id(persisted)["producer"]["checkpoint_verified"] is False
def test_verified_checkpoint_reuses_unchanged_stage_after_other_repo_changes(workspace):
args, repos, _ = workspace
plan = [stage(workspace, "a"), stage(workspace, "b", "beta")]
first = run(workspace, plan)
old_bytes = Path(first["receipt_path"]).read_bytes()
(repos["beta"] / "source.txt").write_text("beta changed\n")
args.resume = first["run_id"]
second = run(workspace, plan)
by_id = stages_by_id(second)
assert second["status"] == "passed" and second["snapshot_verified"] is True
assert count(workspace, "a") == 1 and count(workspace, "b") == 2
assert by_id["a"]["reused_from"] == first["run_id"]
assert "reused_from" not in by_id["b"]
assert by_id["a"]["checkpoint_verified"] is True
assert by_id["a"]["checkpoint_version"] == 1
assert isinstance(by_id["a"]["cache_key"], str) and by_id["a"]["cache_key"]
assert second["source_fingerprint"] != first["source_fingerprint"]
assert Path(first["receipt_path"]).read_bytes() == old_bytes
def test_failed_post_execution_probe_preserves_actual_log_without_certifying_it(
workspace,
):
from govoplan_devkit.checkpoints import Checkpoints
identity = Checkpoints.identity
calls = 0
def probe(self, selected):
nonlocal calls
calls += 1
if calls == 2:
raise ValueError("fixture input became unreadable")
return identity(self, selected)
with patch.object(Checkpoints, "identity", probe):
result = run(workspace, [stage(workspace, "a")])
selected = result["stages"][0]
assert result["status"] == "stale"
assert selected["status"] == "stale" and selected["exit_code"] == 0
assert selected["checkpoint_verified"] is False
assert Path(selected["log_path"]).read_text() == "a\n"
assert "fixture input became unreadable" in selected["error"]
def test_changed_and_new_commands_run_without_discarding_unrelated_checkpoint(
workspace,
):
args, _, _ = workspace
first = run(workspace, [stage(workspace, "a"), stage(workspace, "b", "beta")])
args.resume = first["run_id"]
second = run(
workspace,
[
stage(workspace, "a"),
stage(workspace, "b", "beta", body="print('changed command')"),
stage(workspace, "new", "beta"),
],
)
assert second["status"] == "passed"
assert count(workspace, "a") == 1
assert count(workspace, "b") == 2
assert count(workspace, "new") == 1
assert first["plan_fingerprint"] != second["plan_fingerprint"]
def test_unrelated_profile_edit_does_not_change_existing_stage_execution_identity(
workspace,
):
args, _, _ = workspace
plan = [stage(workspace, "a")]
first = run(workspace, plan)
config = json.loads(args.project.read_text())
config["checks"] = [
{
"id": "extra",
"argv": [sys.executable, "-c", "print('extra')"],
"cwd": "beta",
"repos": ["beta"],
}
]
config["profiles"] = {"backend": ["extra"]}
args.project.write_text(json.dumps(config))
args.resume = first["run_id"]
second = run(workspace, plan)
assert second["status"] == "passed"
assert count(workspace, "a") == 1
def test_added_dependency_edge_invalidates_consumer_even_when_both_repo_bytes_match(
workspace,
):
args, _, _ = workspace
a, b = stage(workspace, "a"), stage(workspace, "b", "beta")
first = run(workspace, [a, b])
args.resume = first["run_id"]
second = run(workspace, [a, {**b, "deps": ["a"]}])
assert second["status"] == "passed"
assert count(workspace, "a") == 1 and count(workspace, "b") == 2
def test_failed_run_reuses_successful_phase_but_retries_failed_phase(workspace):
args, _, traces = workspace
ready = traces / "ready"
plan = [
stage(workspace, "a"),
stage(
workspace,
"b",
"beta",
after=["a"],
body=f"raise SystemExit(0 if Path({str(ready)!r}).exists() else 3)",
),
]
first = run(workspace, plan)
assert first["status"] == "failed"
assert stages_by_id(first)["a"]["checkpoint_verified"] is True
ready.touch()
args.resume = first["run_id"]
second = run(workspace, plan)
assert second["status"] == "passed"
assert count(workspace, "a") == 1 and count(workspace, "b") == 2
def test_changed_dependency_invalidates_transitive_consumers_but_not_independent_stage(
workspace,
):
args, repos, _ = workspace
plan = [
stage(workspace, "a"),
stage(workspace, "b", "beta", deps=["a"]),
stage(workspace, "c", "beta", deps=["b"]),
stage(workspace, "independent", "beta"),
]
first = run(workspace, plan)
(repos["alpha"] / "source.txt").write_text("alpha changed\n")
args.resume = first["run_id"]
second = run(workspace, plan)
assert second["status"] == "passed"
assert {
identity: count(workspace, identity)
for identity in ("a", "b", "c", "independent")
} == {"a": 2, "b": 2, "c": 2, "independent": 1}
def test_order_only_predecessor_change_does_not_invalidate_independent_stage(workspace):
args, repos, _ = workspace
plan = [stage(workspace, "a"), stage(workspace, "b", "beta", after=["a"])]
first = run(workspace, plan)
(repos["alpha"] / "source.txt").write_text("alpha changed\n")
args.resume = first["run_id"]
second = run(workspace, plan)
assert second["status"] == "passed"
assert count(workspace, "a") == 2 and count(workspace, "b") == 1
assert stages_by_id(second)["b"]["reused_from"] == first["run_id"]
def test_order_only_failure_prevents_new_downstream_execution(workspace):
plan = [
stage(workspace, "a", body="raise SystemExit(8)"),
stage(workspace, "b", "beta", after=["a"]),
]
result = run(workspace, plan)
assert result["status"] == "failed"
assert count(workspace, "a") == 1 and count(workspace, "b") == 0
assert stages_by_id(result)["b"]["status"] == "skipped"
def test_never_reused_stage_and_actual_consumers_rerun_but_order_only_stage_can_reuse(
workspace,
):
args, _, _ = workspace
plan = [
stage(workspace, "producer", reuse="never"),
stage(workspace, "consumer", "beta", deps=["producer"]),
stage(workspace, "independent", "beta", after=["producer"]),
]
first = run(workspace, plan)
args.resume = first["run_id"]
second = run(workspace, plan)
assert second["status"] == "passed"
assert count(workspace, "producer") == 2
assert count(workspace, "consumer") == 2
assert count(workspace, "independent") == 1
def test_unspecified_input_scope_stays_conservatively_workspace_wide(workspace):
args, repos, _ = workspace
plan = [stage(workspace, "broad", "beta", inputs=False)]
first = run(workspace, plan)
(repos["alpha"] / "source.txt").write_text("alpha changed\n")
args.resume = first["run_id"]
second = run(workspace, plan)
assert second["status"] == "passed"
assert count(workspace, "broad") == 2
def test_global_environment_change_invalidates_all_scoped_checkpoints(workspace):
args, _, _ = workspace
plan = [stage(workspace, "a"), stage(workspace, "b", "beta")]
first = run(workspace, plan)
args.resume = first["run_id"]
second = run(workspace, plan, environment="f" * 64)
assert second["status"] == "passed"
assert count(workspace, "a") == 2 and count(workspace, "b") == 2
assert second["environment_fingerprint"] != first["environment_fingerprint"]
def test_changed_declared_scope_prevents_same_id_reuse(workspace):
args, _, _ = workspace
selected = stage(workspace, "a")
first = run(workspace, [selected])
args.resume = first["run_id"]
selected = {**selected, "inputs": {"repos": ["alpha", "beta"]}}
second = run(workspace, [selected])
assert second["status"] == "passed" and count(workspace, "a") == 2
def test_input_mutation_during_passing_command_never_creates_reusable_checkpoint(
workspace,
):
args, repos, _ = workspace
plan = [
stage(
workspace,
"mutates",
body="Path('source.txt').write_text('mutated during stage\\n')",
)
]
first = run(workspace, plan)
assert first["status"] != "passed"
assert stages_by_id(first)["mutates"].get("checkpoint_verified") is not True
args.resume = first["run_id"]
# The same command is now stable against the already-mutated current bytes;
# its unverified first result still cannot be skipped.
second = run(workspace, plan)
assert count(workspace, "mutates") == 2
assert second["status"] == "passed"
assert (repos["alpha"] / "source.txt").read_text() == "mutated during stage\n"
def test_later_restoration_of_bytes_cannot_turn_invalid_phase_into_overall_pass(
workspace,
):
_, repos, _ = workspace
source = repos["alpha"] / "source.txt"
original = source.read_text()
plan = [
stage(
workspace,
"mutates",
body="Path('source.txt').write_text('temporary change\\n')",
),
stage(
workspace,
"restores",
"beta",
after=["mutates"],
body=f"Path({str(source)!r}).write_text({original!r})",
),
]
result = run(workspace, plan)
assert result["status"] != "passed"
assert stages_by_id(result)["mutates"].get("checkpoint_verified") is not True
def test_recovered_interruption_reuses_only_verified_durable_checkpoint(workspace):
args, _, _ = workspace
plan = [stage(workspace, "a"), stage(workspace, "b", "beta", after=["a"])]
first = run(workspace, plan)
receipt = read_json(Path(first["receipt_path"]))
receipt.update(
status="running", phase="checking", snapshot_verified=False, finished_at=None
)
unfinished = stages_by_id(receipt)["b"]
unfinished.update(status="running", exit_code=None, checkpoint_verified=False)
atomic_json(Path(first["receipt_path"]), runner._seal(receipt))
parser = argparse.ArgumentParser()
runner.register(parser.add_subparsers(dest="command", required=True))
recovered = parser.parse_args(
["recover", first["run_id"], "--apply", "--confirm-processes-stopped"],
namespace=Namespace(**vars(args)),
)
recovery = recovered.handler(recovered)
assert recovery["status"] == "interrupted"
args.resume = first["run_id"]
second = run(workspace, plan)
assert second["status"] == "passed"
assert count(workspace, "a") == 1 and count(workspace, "b") == 2
assert stages_by_id(second)["a"]["reused_from"] == first["run_id"]
@pytest.mark.parametrize("change", ["tampered", "missing"])
def test_cached_log_tamper_or_loss_is_not_accepted_as_verified_evidence(
workspace, change
):
args, _, _ = workspace
plan = [stage(workspace, "a")]
first = run(workspace, plan)
path = Path(first["stages"][0]["log_path"])
if change == "missing":
path.unlink()
else:
path.write_text("tampered evidence\n")
args.resume = first["run_id"]
result = run(workspace, plan)
assert result["status"] == "failed"
assert (
result["snapshot_verified"] is not True
or result["stages"][0]["status"] == "failed"
)
assert result["stages"][0].get("checkpoint_verified") is not True
assert "reused_from" not in result["stages"][0]
assert "error" in result["stages"][0]
assert count(workspace, "a") == 1
def test_stale_run_donates_only_checkpoints_matching_final_current_inputs(workspace):
args, repos, _ = workspace
source = repos["alpha"] / "source.txt"
plan = [
stage(workspace, "a"),
stage(
workspace,
"changes-alpha",
"beta",
after=["a"],
body=f"Path({str(source)!r}).write_text('new alpha bytes\\n')",
),
]
first = run(workspace, plan)
assert first["status"] != "passed"
args.resume = first["run_id"]
second = run(workspace, plan)
assert second["status"] == "passed"
assert count(workspace, "a") == 2
# This order-only phase did not consume alpha and its own scoped inputs are
# still identical; it may retain its independently verified checkpoint.
assert count(workspace, "changes-alpha") == 1
def test_legacy_checkpointless_receipt_does_not_gain_incremental_authority(workspace):
args, repos, _ = workspace
plan = [stage(workspace, "a")]
first = run(workspace, plan)
receipt = read_json(Path(first["receipt_path"]))
receipt.pop("fingerprint_version", None)
for item in receipt["stages"]:
for field in ("checkpoint_version", "checkpoint_verified", "cache_key"):
item.pop(field, None)
atomic_json(Path(first["receipt_path"]), runner._seal(receipt))
(repos["beta"] / "source.txt").write_text("unrelated changed source\n")
args.resume = first["run_id"]
try:
second = run(workspace, plan)
except ValueError:
return # Rejecting legacy incremental reuse is also safely fail-closed.
assert second["status"] == "passed"
assert count(workspace, "a") == 2
+393
View File
@@ -0,0 +1,393 @@
"""Repository input identities use disposable Git fixtures, never remote effects."""
from copy import deepcopy
import os
from pathlib import Path
import subprocess
import sys
import pytest
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "tools/devkit"))
from govoplan_devkit import inputs
from govoplan_devkit.inputs import InputSnapshotter, validate_input_declaration
from govoplan_devkit.workspace import Project, Repository, source_fingerprint
def git(repo, *arguments):
return subprocess.run(
["git", "-C", str(repo), *arguments], capture_output=True, check=True
).stdout
@pytest.fixture
def fixture(tmp_path, monkeypatch):
repositories = []
for name in ("alpha", "beta"):
path = tmp_path / name
path.mkdir()
git(path, "init", "-q")
(path / "source.txt").write_text("initial\n")
git(path, "add", "source.txt")
git(
path,
"-c",
"user.name=Fixture",
"-c",
"user.email=fixture@example.invalid",
"commit",
"-qm",
"fixture",
)
repositories.append(Repository(name, path, ("alias-" + name,)))
project = Project(
"Fixture",
tuple(repositories),
{"tools": {}, "profiles": {"quick": ["one"]}, "checks": []},
)
# Avoid incidental edits by other agents affecting these source-input tests.
# The actual tool-source hashing implementation is checked separately.
monkeypatch.setattr(
InputSnapshotter, "_tooling_identity", lambda *_: "fixture-devkit-source"
)
return tmp_path, project, InputSnapshotter(project, workspace_root=tmp_path)
def stage(identity="one", repos=None, **extra):
return {
"id": identity,
"argv": ["true"],
**({"inputs": {"repos": repos}} if repos is not None else {}),
**extra,
}
def fingerprint(snapshot, name="one"):
return snapshot["stages"][name]["fingerprint"]
def test_undeclared_inputs_remain_whole_workspace(fixture):
root, _, engine = fixture
result = engine.snapshot([stage()])
assert result["complete_workspace"] is True
assert result["observed_scope"]["repos"] == ["alpha", "beta"]
assert result["stages"]["one"]["scope"] == {
"version": 1,
"kind": "workspace",
"declared": False,
"repos": ["alpha", "beta"],
}
(root / "beta/source.txt").write_text("changed\n")
assert fingerprint(engine.snapshot([stage()])) != fingerprint(result)
def test_scoped_input_does_not_read_unrelated_repository_bytes(fixture, monkeypatch):
root, _, engine = fixture
original = inputs.os.open
def guarded(path, *args, **kwargs):
if Path(path).is_relative_to(root / "beta"):
raise AssertionError("Unrelated repository input was opened")
return original(path, *args, **kwargs)
monkeypatch.setattr(inputs.os, "open", guarded)
first = engine.snapshot([stage(repos=["alpha"])])
(root / "beta/source.txt").write_text("unrelated change")
second = engine.snapshot([stage(repos=["alpha"])])
assert fingerprint(first) == fingerprint(second)
assert second["scan_stats"]["repositories"] == 1
assert second["scan_stats"]["git_calls"] == 2
assert second["complete_workspace"] is False
def test_repository_union_scans_once_per_snapshot(fixture):
_, _, engine = fixture
result = engine.snapshot(
[
stage("one", ["alpha"]),
stage("two", ["alpha"]),
stage("three", ["beta", "alpha"]),
]
)
assert result["scan_stats"]["repositories"] == 2
assert result["scan_stats"]["git_calls"] == 4
assert result["stages"]["three"]["scope"]["repos"] == ["alpha", "beta"]
def test_session_rechecks_metadata_but_reuses_stable_file_content(fixture):
_, _, engine = fixture
plan = [stage(repos=["alpha"])]
before = engine.snapshot(plan)
after = engine.snapshot(plan)
assert fingerprint(before) == fingerprint(after)
assert before["scan_stats"]["bytes"] == len("initial\n")
assert after["scan_stats"]["bytes"] == 0
assert after["scan_stats"]["cache_hits"] == 1
assert after["scan_stats"]["git_calls"] == 2
def test_changed_bytes_with_restored_mtime_are_not_reused(fixture):
root, _, engine = fixture
plan = [stage(repos=["alpha"])]
before = engine.snapshot(plan)
path = root / "alpha/source.txt"
metadata = path.stat()
path.write_text("changed\n")
os.utime(path, ns=(metadata.st_atime_ns, metadata.st_mtime_ns))
after = engine.snapshot(plan)
assert fingerprint(after) != fingerprint(before)
assert after["scan_stats"]["bytes"] == len("changed\n")
def test_inode_replacement_with_same_size_and_mtime_is_not_reused(fixture):
root, _, engine = fixture
plan = [stage(repos=["alpha"])]
before = engine.snapshot(plan)
path = root / "alpha/source.txt"
metadata = path.stat()
replacement = root / "replacement.txt"
replacement.write_text("changed\n")
os.utime(replacement, ns=(metadata.st_atime_ns, metadata.st_mtime_ns))
replacement.replace(path)
after = engine.snapshot(plan)
assert fingerprint(before) != fingerprint(after)
assert after["scan_stats"]["cache_hits"] == 0
@pytest.mark.parametrize("flag", ["assume-unchanged", "skip-worktree"])
def test_hidden_index_flags_do_not_hide_worktree_changes(fixture, flag):
root, _, engine = fixture
git(root / "alpha", "update-index", "--" + flag, "source.txt")
before = engine.source_snapshot(["alpha"])
(root / "alpha/source.txt").write_text("hidden change\n")
after = engine.source_snapshot(["alpha"])
assert before["observed_source_fingerprint"] != after["observed_source_fingerprint"]
def test_index_head_and_new_deleted_files_are_bound(fixture):
root, _, engine = fixture
repo = root / "alpha"
identities = []
def record():
identities.append(
engine.source_snapshot(["alpha"])["observed_source_fingerprint"]
)
record()
(repo / "source.txt").write_text("changed\n")
record()
git(repo, "add", "source.txt")
record()
git(
repo,
"-c",
"user.name=Fixture",
"-c",
"user.email=fixture@example.invalid",
"commit",
"-qm",
"change",
)
record()
(repo / "new.txt").write_text("new")
record()
(repo / "source.txt").unlink()
record()
assert len(set(identities)) == len(identities)
def test_missing_registered_repository_becoming_present_invalidates(fixture):
root, project, _ = fixture
extra = Repository("missing", root / "missing")
extended = Project("Fixture", (*project.repositories, extra), project.config)
engine = InputSnapshotter(extended, workspace_root=root)
before = engine.source_snapshot(["missing"])
extra.path.mkdir()
git(extra.path, "init", "-q")
after = engine.source_snapshot(["missing"])
assert before["observed_source_fingerprint"] != after["observed_source_fingerprint"]
def test_unrelated_plan_config_edits_do_not_invalidate_scoped_stage(fixture):
root, project, engine = fixture
plan = [stage(repos=["alpha"])]
before = engine.snapshot(plan)
updated = deepcopy(project.config)
updated["profiles"]["full"] = ["unrelated"]
updated["checks"].append({"id": "unrelated", "argv": ["false"]})
other = InputSnapshotter(
Project("Changed label", project.repositories, updated), workspace_root=root
)
assert fingerprint(other.snapshot(plan)) == fingerprint(before)
def test_scope_command_tools_and_tooling_change_invalidate(fixture):
root, project, engine = fixture
first = engine.snapshot([stage(repos=["alpha"])])
assert fingerprint(engine.snapshot([stage(repos=["beta"])])) != fingerprint(first)
assert fingerprint(
engine.snapshot([stage(repos=["alpha"], argv=["false"])])
) != fingerprint(first)
assert fingerprint(
engine.snapshot([stage(repos=["alpha"])], tooling_fingerprint="new-env")
) != fingerprint(first)
changed = deepcopy(project.config)
changed["tools"] = {"node": "/another/node"}
other = InputSnapshotter(
Project("Fixture", project.repositories, changed), workspace_root=root
)
assert fingerprint(other.snapshot([stage(repos=["alpha"])])) != fingerprint(first)
def test_source_only_attestation_is_independent_of_plan_and_tooling(fixture):
_, _, engine = fixture
one = engine.snapshot([stage(repos=["alpha"])], tooling_fingerprint="env-one")
two = engine.snapshot(
[stage("different", ["alpha"], argv=["false"])], tooling_fingerprint="env-two"
)
source = engine.source_snapshot(["alpha"])
assert (
one["observed_source_fingerprint"]
== two["observed_source_fingerprint"]
== source["observed_source_fingerprint"]
)
assert (
one["stages"]["one"]["source_fingerprint"]
== source["observed_source_fingerprint"]
)
assert source["fingerprint_version"] == inputs.FINGERPRINT_VERSION
@pytest.mark.parametrize(
"value",
[
None,
{},
{"paths": ["src/**"]},
{"repos": []},
{"repos": "alpha"},
{"repos": ["unknown"]},
{"repos": ["alias-alpha"]},
{"repos": ["alpha", "alpha"]},
{"repos": [""]},
{"repos": ["alpha"], "extra": True},
],
)
def test_synthetic_stage_scopes_fail_closed(fixture, value):
_, _, engine = fixture
with pytest.raises(ValueError):
engine.snapshot([stage(inputs=value)])
def test_duplicate_stage_ids_and_escaped_repository_paths_fail(fixture):
root, project, engine = fixture
with pytest.raises(ValueError, match="Duplicate"):
engine.snapshot([stage(), stage()])
bad = Project("Bad", (Repository("outside", root.parent),), project.config)
with pytest.raises(ValueError, match="escapes"):
InputSnapshotter(bad, workspace_root=root)
def test_declaration_validation_is_pure_before_dry_run(monkeypatch):
monkeypatch.setattr(
inputs,
"git_bytes",
lambda *_a, **_k: pytest.fail("Planning validation ran Git"),
)
assert validate_input_declaration(
{"repos": ["beta", "alpha"]}, {"alpha", "beta"}
) == {"repos": ["alpha", "beta"]}
with pytest.raises(ValueError):
validate_input_declaration({"repos": ["unknown"]}, {"alpha", "beta"})
def test_runtime_records_are_not_hashed_as_execution_plans(fixture):
_, _, engine = fixture
with pytest.raises(ValueError, match="freshly planned"):
engine.snapshot([stage(repos=["alpha"], status="passed")])
def test_membership_count_is_bounded_before_entry_hashing(fixture, monkeypatch):
root, _, engine = fixture
(root / "alpha/new.txt").write_text("extra")
monkeypatch.setattr(inputs, "MAX_REPOSITORY_ENTRIES", 1)
with pytest.raises(ValueError, match="entry count"):
engine.source_snapshot(["alpha"])
def test_symlink_to_ignored_file_inside_repository_binds_target(fixture):
root, _, engine = fixture
repo = root / "alpha"
(repo / ".gitignore").write_text("ignored.txt\n")
(repo / "ignored.txt").write_text("first")
(repo / "linked.txt").symlink_to("ignored.txt")
before = engine.source_snapshot(["alpha"])
(repo / "ignored.txt").write_text("other")
assert (
engine.source_snapshot(["alpha"])["observed_source_fingerprint"]
!= before["observed_source_fingerprint"]
)
def test_dangling_symlink_target_creation_is_observed(fixture):
root, _, engine = fixture
repo = root / "alpha"
(repo / "linked.txt").symlink_to("future.txt")
before = engine.source_snapshot(["alpha"])
(repo / "future.txt").write_text("created")
assert (
engine.source_snapshot(["alpha"])["observed_source_fingerprint"]
!= before["observed_source_fingerprint"]
)
def test_cross_repository_and_directory_symlinks_are_not_silently_reused(fixture):
root, _, engine = fixture
link = root / "alpha/linked.txt"
link.symlink_to(root / "beta/source.txt")
with pytest.raises(ValueError, match="escapes"):
engine.source_snapshot(["alpha"])
link.unlink()
link.symlink_to(".")
with pytest.raises(ValueError, match="regular file"):
engine.source_snapshot(["alpha"])
def test_file_change_during_hash_is_rejected(fixture, monkeypatch):
root, _, engine = fixture
path = root / "alpha/source.txt"
original = inputs.os.fstat
changed = False
def mutate(descriptor):
nonlocal changed
metadata = original(descriptor)
if not changed:
changed = True
path.write_text("changed during read")
return metadata
monkeypatch.setattr(inputs.os, "fstat", mutate)
with pytest.raises(ValueError, match="changed"):
engine._file_hash(path, inputs._stats())
def test_legacy_whole_project_api_remains_independent(fixture):
_, project, engine = fixture
legacy = source_fingerprint(project)
engine.snapshot([stage(repos=["alpha"])])
assert source_fingerprint(project) == legacy
def test_real_tooling_inventory_is_bound_and_memoized(tmp_path):
repo = Repository("example", tmp_path)
project = Project("Fixture", (repo,), {"tools": {}})
engine = InputSnapshotter(project, workspace_root=tmp_path)
before_stats, after_stats = inputs._stats(), inputs._stats()
before = engine._tooling_identity(before_stats)
after = engine._tooling_identity(after_stats)
assert before == after
assert before_stats["tooling_files"] > 15 and before_stats["tooling_bytes"] > 0
assert after_stats["tooling_files"] == after_stats["tooling_cache_hits"]
assert after_stats["tooling_bytes"] == 0
+387
View File
@@ -0,0 +1,387 @@
import argparse
from copy import deepcopy
import json
from pathlib import Path
import subprocess
import sys
import pytest
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "tools/devkit"))
from govoplan_devkit import issues
from govoplan_devkit.common import atomic_json, digest, state_root
from govoplan_devkit.workspace import load_project, source_fingerprint
@pytest.fixture
def args(tmp_path, monkeypatch):
monkeypatch.delenv("GITEA_TOKEN", raising=False)
root = tmp_path / "repo"
root.mkdir()
subprocess.run(["git", "init", "-q", str(root)], check=True)
subprocess.run(["git", "-C", str(root), "remote", "add", "origin", "https://gitea.invalid/team/repo.git"], check=True)
project = tmp_path / "project.json"
project.write_text(json.dumps({"schema_version": 1, "name": "Fixture", "repositories": [{"name": "repo", "path": "repo"}]}))
env_file = tmp_path / "private.env"
env_file.write_text("GITEA_TOKEN=fixture-private-token\nGITEA_OWNER=wrong-owner\n")
return argparse.Namespace(workspace_root=tmp_path, state_dir=tmp_path / "state", project=project,
root=root, issue=7, target_plan=None, remote="origin", env_file=env_file, evidence=None,
key="verification", note_summary=["Verified scoped changes."], next_steps=["Manual review remains."],
body_file=None, note_file=None, apply=False, retry_uncertain=False)
class FakeClient:
def __init__(self, target):
self.target = target
self.calls = []
self.comments = []
self.issue_id = 700
self.post_mode = "normal"
self.bad_binding = False
self.repeat_pages = False
self.bad_issue = False
self.fail = False
self.page_size = 2 # A server may enforce a cap smaller than the requested limit.
def close(self):
pass
def request_json(self, method, path, body=None, query=None):
self.calls.append((method, path, deepcopy(body), deepcopy(query)))
if self.fail:
raise RuntimeError("Remote echoed fixture-private-token")
if method == "GET" and path == self.target.path:
return {"id": self.issue_id, "number": self.target.issue, "html_url": self.target.url + ("wrong" if self.bad_issue else ""),
"state": "closed", "body": "- [ ] Preserve this checklist"}
if method == "GET" and path == self.target.path + "/comments":
page = 1 if self.repeat_pages else query["page"]
return deepcopy(self.comments[(page - 1) * self.page_size:page * self.page_size])
if method == "GET" and "/issues/comments/" in path:
comment = deepcopy(next(item for item in self.comments if str(item["id"]) == path.rsplit("/", 1)[1]))
comment["html_url"] = self.target.url + ("-other" if self.bad_binding else "") + "#issuecomment-" + str(comment["id"])
return comment
if method == "POST" and path == self.target.path + "/comments":
if self.post_mode == "timeout-before-commit":
raise TimeoutError("fixture-private-token")
comment = {"id": 1000 + len(self.comments), "body": body["body"]}
self.comments.append(comment)
if self.post_mode == "timeout-after-commit":
raise TimeoutError("fixture-private-token")
return deepcopy(comment)
raise AssertionError((method, path))
@property
def posts(self):
return [call for call in self.calls if call[0] == "POST"]
@pytest.fixture
def client(args, monkeypatch):
client = FakeClient(issues.resolve_target(args.root, args.issue, args.workspace_root))
monkeypatch.setattr(issues, "make_client", lambda target, token: client)
return client
def test_default_dry_run_is_offline_and_ignores_ambient_target_overrides(args, monkeypatch):
monkeypatch.setenv("GITEA_OWNER", "wrong")
monkeypatch.setenv("GITEA_REPO", "wrong")
monkeypatch.setenv("GITEA_URL", "https://wrong.invalid")
monkeypatch.setattr(issues, "make_client", lambda *_: pytest.fail("No network in preview"))
args.env_file = args.workspace_root / "does-not-exist.env"
result = issues.handle_note(args)
assert result["targets"][0]["url"] == "https://gitea.invalid/team/repo/issues/7"
assert result["targets"][0]["status"] == "would-post"
assert not args.state_dir.exists()
def test_scoped_checkpoint_evidence_compares_only_recorded_sources(args, monkeypatch):
from govoplan_devkit import runner
other = args.workspace_root / "other"
other.mkdir()
subprocess.run(["git", "init", "-q", str(other)], check=True)
declaration = json.loads(args.project.read_text())
declaration["repositories"].append({"name": "other", "path": "other"})
args.project.write_text(json.dumps(declaration))
monkeypatch.setattr(runner, "environment_fingerprint", lambda *_: "fixture-env")
run_args = argparse.Namespace(**{**vars(args), "jobs": 1, "profile": "quick", "dry_run": False})
result = runner.run_checks(run_args, [{"id": "scoped", "argv": [sys.executable, "-c", "print('ok')"],
"cwd": str(args.root), "inputs": {"repos": ["repo"]}}])
assert result["status"] == "passed"
evidence = issues.evidence_record(result["run_id"], args)
assert evidence["source_state"] == "matches-current"
assert evidence["source_scope"]["repos"] == ["repo"]
assert any("recorded repository input scope" in note for note in evidence["coverage_notes"])
(other / "unrelated.txt").write_text("Outside the recorded scope")
assert issues.evidence_record(result["run_id"], args)["source_state"] == "matches-current"
(args.root / "changed.txt").write_text("Inside the recorded scope")
assert issues.evidence_record(result["run_id"], args)["source_state"] == "historical-source-differs"
def test_passing_aggregate_cannot_hide_skipped_checks(args):
payload = receipt(args)
payload["stages"].append({"id": "skipped", "status": "skipped", "exit_code": None})
with pytest.raises(ValueError, match="inconsistent"):
issues.validate_receipt(payload, args.workspace_root)
def test_apply_is_append_only_read_back_and_idempotent(args, client):
args.apply = True
result = issues.handle_note(args)
assert result["targets"][0]["status"] == "posted-verified"
assert len(client.posts) == 1
again = issues.handle_note(args)
assert again["targets"][0]["status"] == "existing-verified"
assert len(client.posts) == 1
assert {call[0] for call in client.calls} == {"GET", "POST"}
assert all(call[1].endswith("/comments") for call in client.posts)
def test_complete_pagination_continues_past_short_pages(args, client):
body = issues.handle_note(args)["targets"][0]["body"]
client.comments = [{"id": n, "body": "unrelated"} for n in range(1, 6)] + [{"id": 6, "body": body}]
args.apply = True
result = issues.handle_note(args)
assert result["targets"][0]["status"] == "existing-verified"
assert not client.posts
assert max(call[3]["page"] for call in client.calls if call[3]) == 4
@pytest.mark.parametrize("collision", ["different-body", "duplicate", "repeating-pagination"])
def test_collisions_and_incomplete_pagination_refuse_post(args, client, collision):
body = issues.handle_note(args)["targets"][0]["body"]
client.comments = [{"id": 1, "body": body}]
if collision == "different-body":
client.comments[0]["body"] += "changed"
elif collision == "duplicate":
client.comments.append({"id": 2, "body": body})
else:
client.repeat_pages = True
args.apply = True
assert issues.handle_note(args)["_exit_code"] == 2
assert not client.posts
def test_timeout_after_commit_reconciles_without_replaying_post(args, client):
args.apply = True
client.post_mode = "timeout-after-commit"
result = issues.handle_note(args)
assert result["targets"][0]["status"] == "reconciled-verified"
assert len(client.posts) == 1
assert "fixture-private-token" not in json.dumps(result)
def test_uncertain_post_requires_explicit_retry_after_reconciliation(args, client):
args.apply = True
client.post_mode = "timeout-before-commit"
assert issues.handle_note(args)["targets"][0]["status"] == "uncertain"
client.post_mode = "normal"
assert issues.handle_note(args)["targets"][0]["status"] == "uncertain-retry-required"
assert len(client.posts) == 1
args.retry_uncertain = True
assert issues.handle_note(args)["targets"][0]["status"] == "posted-verified"
assert len(client.posts) == 2
journals = list(state_root(args.workspace_root, args.state_dir).glob("issue-notes/*.json"))
assert journals and all("fixture-private-token" not in path.read_text() for path in journals)
def test_uncertain_readback_is_not_reported_verified_and_later_reconciles(args, client):
args.apply = True
client.bad_binding = True
assert issues.handle_note(args)["targets"][0]["status"] == "uncertain"
client.bad_binding = False
assert issues.handle_note(args)["targets"][0]["status"] == "existing-verified"
assert len(client.posts) == 1
def test_journal_binds_immutable_issue_id(args, client):
args.apply = True
assert issues.handle_note(args)["targets"][0]["status"] == "posted-verified"
client.issue_id += 1
client.comments.clear()
args.retry_uncertain = True
assert issues.handle_note(args)["_exit_code"] == 2
assert len(client.posts) == 1
def _plan(args, rows):
args.root = args.issue = None
args.target_plan = args.workspace_root / "targets.json"
args.target_plan.write_text(json.dumps({"schema_version": 1, "targets": rows}))
@pytest.mark.parametrize("change", ["wrong-url", "duplicate", "outside", "mixed-origin", "credentials"])
def test_target_plans_require_exact_unique_workspace_bindings(args, change):
row = {"root": "repo", "issue": 7, "url": "https://gitea.invalid/team/repo/issues/7"}
if change == "wrong-url":
row["url"] = "https://gitea.invalid/team/other/issues/7"
elif change == "outside":
row["root"] = "../other"
elif change == "credentials":
subprocess.run(["git", "-C", str(args.root), "remote", "set-url", "origin", "https://user:private@gitea.invalid/team/repo.git"], check=True)
rows = [row, deepcopy(row)] if change == "duplicate" else [row]
if change == "mixed-origin":
other = args.workspace_root / "other"
other.mkdir()
subprocess.run(["git", "init", "-q", str(other)], check=True)
subprocess.run(["git", "-C", str(other), "remote", "add", "origin", "http://gitea.invalid/team/other.git"], check=True)
rows.append({"root": "other", "issue": 8, "url": "http://gitea.invalid/team/other/issues/8"})
_plan(args, rows)
with pytest.raises(ValueError):
issues.handle_note(args)
def test_all_targets_preflight_before_first_post_and_posts_are_serial(args, client, monkeypatch):
first = client.target.record()
second = {**first, "issue": 8, "url": first["url"].rsplit("/", 1)[0] + "/8"}
_plan(args, [first, second])
second_client = FakeClient(issues.resolve_target(Path(first["root"]), 8, args.workspace_root))
second_client.bad_issue = True
monkeypatch.setattr(issues, "make_client", lambda target, token: client if target.issue == 7 else second_client)
args.apply = True
assert issues.handle_note(args)["_exit_code"] == 2
assert not client.posts and not second_client.posts
second_client.bad_issue = False
result = issues.handle_note(args)
assert [row["status"] for row in result["targets"]] == ["posted-verified", "posted-verified"]
assert len(client.posts) == len(second_client.posts) == 1
def test_credentials_and_response_errors_are_not_returned(args, client):
args.apply = True
client.fail = True
result = issues.handle_note(args)
assert result["_exit_code"] == 2
assert "fixture-private-token" not in json.dumps(result)
args.note_summary = ["fixture-private-token"]
with pytest.raises(ValueError, match="credential"):
issues.handle_note(args)
def test_marker_injection_and_symlink_inputs_are_rejected(args):
args.note_summary = [issues.MARKER_PREFIX + "fake -->"]
with pytest.raises(ValueError, match="reserved"):
issues.handle_note(args)
args.note_summary = ["safe"]
args.body_file = args.workspace_root / "alias.md"
args.body_file.symlink_to(args.env_file)
with pytest.raises(ValueError, match="symlink"):
issues.handle_note(args)
@pytest.mark.parametrize("remote", ["https://gitea.invalid/team/repo.git?token=secret", "https://gitea.invalid/team/repo.git#other", "https://gitea.invalid:bad/team/repo.git"])
def test_ambiguous_git_remote_is_not_silently_reinterpreted(args, remote):
subprocess.run(["git", "-C", str(args.root), "remote", "set-url", "origin", remote], check=True)
with pytest.raises(ValueError):
issues.handle_note(args)
def test_structured_inputs_are_merged_as_data_not_executed(args):
args.note_file = args.workspace_root / "note.json"
args.note_file.write_text(json.dumps({"summary": ["Recorded earlier"], "next": ["Still pending"], "body": "$(never-execute)"}))
args.body_file = args.workspace_root / "body.md"
args.body_file.write_text("`never-run-this-either`")
body = issues.handle_note(args)["targets"][0]["body"]
assert "Recorded earlier" in body and "Verified scoped changes" in body
assert "$(never-execute)" in body and "`never-run-this-either`" in body
def receipt(args):
return {"schema_version": 1, "run_id": "fixture-run", "workspace_root": str(args.workspace_root),
"project_file": str(args.project), "source_fingerprint": source_fingerprint(load_project(args.workspace_root, args.project)),
"status": "passed", "snapshot_verified": True, "generated_at": "2026-09-08T12:00:00Z", "finished_at": "2026-09-08T12:00:01Z",
"stages": [{"id": "fixture", "status": "passed", "exit_code": 0, "duration_seconds": 1,
"log_path": "/private/log-not-opened", "argv": ["never-execute-this"]}]}
def test_external_receipts_are_unverified_metadata_with_source_comparison(args):
path = args.workspace_root / "external.json"
path.write_text(json.dumps(receipt(args)))
args.evidence = str(path)
result = issues.handle_note(args)
evidence = result["evidence"]
assert evidence["origin"] == "external-unverified"
assert evidence["source_state"] == "matches-current"
assert "argv" not in evidence["stages"][0]
(args.root / "dirty.txt").write_text("changed")
assert issues.handle_note(args)["evidence"]["source_state"] == "historical-source-differs"
@pytest.mark.parametrize("mutation", ["foreign", "bad-fingerprint", "false-pass", "duplicate-stage", "unknown-status", "noninteger-exit", "invalid-state-shape", "invalid-stage-state-shape", "bool-schema"])
def test_invalid_receipt_cannot_supply_evidence(args, mutation):
payload = receipt(args)
if mutation == "foreign":
payload["workspace_root"] = str(args.workspace_root.parent)
elif mutation == "bad-fingerprint":
payload["source_fingerprint"] = "claimed-green"
elif mutation == "false-pass":
payload["stages"][0]["exit_code"] = 1
elif mutation == "duplicate-stage":
payload["stages"] *= 2
elif mutation == "unknown-status":
payload["status"] = "complete-review"
elif mutation == "invalid-state-shape":
payload["status"] = {}
elif mutation == "invalid-stage-state-shape":
payload["stages"][0]["status"] = []
elif mutation == "bool-schema":
payload["schema_version"] = True
else:
payload["stages"][0]["exit_code"] = False
with pytest.raises(ValueError):
issues.validate_receipt(payload, args.workspace_root)
def test_local_receipt_integrity_is_checked_but_not_an_attestation(args):
payload = receipt(args)
payload["integrity_sha256"] = digest(payload)
path = state_root(args.workspace_root, args.state_dir) / "runs/fixture-run/receipt.json"
atomic_json(path, payload)
args.evidence = "fixture-run"
result = issues.handle_note(args)
assert result["evidence"]["origin"] == "local-integrity-checked"
assert "not an independent attestation" in result["evidence"]["attestation"]
payload["status"] = "failed"
atomic_json(path, payload)
with pytest.raises(ValueError, match="integrity"):
issues.handle_note(args)
def test_timed_out_stage_is_reportable_without_claiming_success(args):
payload = receipt(args)
payload["status"] = "failed"
payload["stages"][0].update(status="timed_out", exit_code=-15)
assert issues.validate_receipt(payload, args.workspace_root)["status"] == "failed"
def test_scoped_coverage_limits_are_preserved_in_evidence_and_rendered_note(args, monkeypatch):
payload = receipt(args)
monkeypatch.setenv("FIXTURE_SECRET", "fixture-secret-value")
payload["stages"][0]["coverage_notes"] = ["Compiler/chained suite not run.", "Manual <script>not executed</script> fixture-secret-value"]
path = args.workspace_root / "coverage.json"
path.write_text(json.dumps(payload))
args.evidence = str(path)
result = issues.handle_note(args)
evidence = result["evidence"]
assert evidence["coverage_notes"] == evidence["stages"][0]["coverage_notes"]
assert "Compiler/chained suite not run." in result["targets"][0]["body"]
assert "omitted checks ran" in result["targets"][0]["body"]
assert "<script>" not in result["targets"][0]["body"]
assert "fixture-secret-value" not in json.dumps(result)
assert any("coverage limitation" in line for line in result["summary"])
@pytest.mark.parametrize("notes", ["not-a-list", [None], [""], ["x" * 4097], ["limit"] * 2049, [issues.MARKER_PREFIX + "injection"]])
def test_malformed_coverage_metadata_is_rejected(args, notes):
payload = receipt(args)
payload["stages"][0]["coverage_notes"] = notes
with pytest.raises(ValueError):
issues.validate_receipt(payload, args.workspace_root)
@pytest.mark.parametrize("snapshot_verified", [None, False, "true", 1])
def test_external_pass_requires_verified_snapshot_flag(args, snapshot_verified):
payload = receipt(args)
payload["snapshot_verified"] = snapshot_verified
with pytest.raises(ValueError, match="verified source snapshot"):
issues.validate_receipt(payload, args.workspace_root)

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