Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3766e26377 | ||
|
|
6c2b36af0f | ||
|
|
3f75ca8e48 | ||
|
|
a886a9b3de | ||
|
|
fe83290d56 | ||
|
|
c50f699399 | ||
|
|
59b45a0829 | ||
|
|
861abcc573 | ||
|
|
5e995fed88 | ||
|
|
41ca242004 | ||
|
|
85caa8d337 | ||
|
|
64640327ae | ||
|
|
cc7c2a91ee | ||
|
|
7c92565d9d | ||
|
|
23bfe5e2f8 | ||
|
|
cf2f7f6890 | ||
|
|
23b601bc0d | ||
|
|
99c52c2153 | ||
|
|
ca68d98806 | ||
|
|
79c4cb067a | ||
|
|
cd498dc1d8 | ||
|
|
e07b3487e3 | ||
|
|
72279de2c0 | ||
|
|
88543ab115 | ||
|
|
81fe0f4680 | ||
|
|
fe784cc562 | ||
|
|
9fe7ad2cb4 | ||
|
|
f1eebd849c | ||
|
|
4eb90079d5 | ||
|
|
628714804b | ||
|
|
ff9fa37a88 | ||
|
|
4c7552f0dd | ||
|
|
a20e02291d | ||
|
|
b6452c6f53 | ||
|
|
75103d49af | ||
|
|
a60b8b0752 | ||
|
|
7f2f896a0f | ||
|
|
60e04a324c | ||
|
|
6a74e53a1c | ||
|
|
6517b6ac27 | ||
|
|
26a66814b4 | ||
|
|
d08f9f0f2d | ||
|
|
47c90400af | ||
|
|
5d4535f7b5 | ||
|
|
83ccb7f198 | ||
|
|
f407419d25 | ||
|
|
e88dceb639 | ||
|
|
ef8fd45457 | ||
|
|
d0ff2f1510 | ||
|
|
f8b06887d2 | ||
|
|
69519a92b4 | ||
|
|
e2f505eeab | ||
|
|
a1b80eda27 | ||
|
|
5bb8028147 | ||
|
|
5efb0eea6f | ||
|
|
5e9234d4b6 | ||
|
|
b76581a89a | ||
|
|
bec62f38d1 | ||
|
|
c66e1b768d | ||
|
|
209a43592f | ||
|
|
50b81c9ca7 | ||
|
|
612a44bc8e | ||
|
|
dce725636d | ||
|
|
78811f7f6e | ||
|
|
09046e6e62 | ||
|
|
f3cfd1bccc | ||
|
|
241db623c7 | ||
|
|
b269791c48 | ||
|
|
69ba1037bf | ||
|
|
8bdf7b5f7e | ||
|
|
d6fdd7ddf5 | ||
|
|
7b0ab31adf | ||
|
|
389df7c3d5 | ||
|
|
3b9ae901dd |
+4
-2
@@ -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
|
||||
|
||||
@@ -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 \
|
||||
|
||||
@@ -123,7 +123,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 +170,19 @@ Create and validate a private, declarative installation bundle:
|
||||
```
|
||||
|
||||
The current executable slice and remaining production gates are documented in
|
||||
[Installation and Deployment Architecture](docs/INSTALLATION_AND_DEPLOYMENT_ARCHITECTURE.md).
|
||||
[Installation and Deployment Architecture](docs/operations/INSTALLATION_AND_DEPLOYMENT_ARCHITECTURE.md).
|
||||
The canonical distinction between local source development, split source
|
||||
integration, immutable single-host rehearsal, one-host production and
|
||||
multi-host Kubernetes production is in
|
||||
[Deployment Profiles](docs/operations/DEPLOYMENT_PROFILES.md).
|
||||
Same-host replica balancing and the multi-host promotion boundary are documented
|
||||
in [Scaling and Multi-Host Deployment](docs/SCALING_AND_MULTI_HOST_DEPLOYMENT.md).
|
||||
in [Scaling and Multi-Host Deployment](docs/operations/SCALING_AND_MULTI_HOST_DEPLOYMENT.md).
|
||||
Create, update, pause, resume, verify and remove a local or multi-hypervisor K3s
|
||||
VM target with the guarded lifecycle documented in
|
||||
[Kubernetes VM Test Lab](docs/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 +195,14 @@ such as `~/.config/gitea/gitea.env` and be passed with `--env-file`.
|
||||
|
||||
## Structure
|
||||
|
||||
The repository categories are documented in
|
||||
`docs/REPOSITORY_STRUCTURE.md`. The machine-readable list lives in
|
||||
`repositories.json`; the clickable human-readable index is
|
||||
`docs/REPOSITORY_INDEX.md`.
|
||||
Start with the [documentation map](docs/README.md). It separates stable
|
||||
strategy, architecture, operations, project reference, pinned evidence, and
|
||||
historical records and identifies the canonical source for each question.
|
||||
|
||||
Meta ownership and module install/contract boundaries are documented in
|
||||
`docs/META_REPO_SCAN.md` and `docs/MODULE_CONTRACTS_AND_INSTALLS.md`.
|
||||
Frontend layout principles for module pages are documented in
|
||||
`docs/FRONTEND_LAYOUT_PRINCIPLES.md`.
|
||||
The provider-neutral datasource boundary and reusable Dataflow/Workflow graph
|
||||
contract are documented in
|
||||
`docs/DATASOURCE_AND_DEFINITION_GRAPH_ARCHITECTURE.md`.
|
||||
The cross-product destination, stakeholder visions, configuration archetypes,
|
||||
connected outcome stories, and capability horizons are documented in
|
||||
the [Connected Governance Platform Roadmap](docs/CONNECTED_GOVERNANCE_PLATFORM_ROADMAP.md).
|
||||
The reconciled institutional semantics, source-authority modes, module layers,
|
||||
candidate Mandates/Services/Parties/Decisions boundaries, and migration
|
||||
sequence are documented in the
|
||||
[Institutional Governance Target Architecture](docs/INSTITUTIONAL_GOVERNANCE_TARGET_ARCHITECTURE.md).
|
||||
The selected Campaign-to-Postbox-to-data-to-collaboration implementation path,
|
||||
including stage gates and shared documentation expectations, is in the
|
||||
[Reference Journey Program](docs/REFERENCE_JOURNEY_PROGRAM.md).
|
||||
The administrator journey from Core-only bootstrap through online module
|
||||
installation, scale-out, and reversible environment promotion is defined in
|
||||
[System Administrator Lifecycle User Story](docs/SYSTEM_ADMINISTRATOR_LIFECYCLE_USER_STORY.md).
|
||||
The corresponding host deployment compiler, managed/external component choices,
|
||||
reconfiguration semantics, and safe Web update boundary are defined in
|
||||
[Installation and Deployment Architecture](docs/INSTALLATION_AND_DEPLOYMENT_ARCHITECTURE.md).
|
||||
The concrete replica, worker-node, load-balancer, and shared-state topology is
|
||||
defined in [Scaling and Multi-Host Deployment](docs/SCALING_AND_MULTI_HOST_DEPLOYMENT.md).
|
||||
Durable deployment journals, Core recovery evidence, and the distinction
|
||||
between pre-migration configuration restore and post-migration forward recovery
|
||||
are defined in
|
||||
[Recovery and Rollback Guarantees](docs/RECOVERY_AND_ROLLBACK_GUARANTEES.md).
|
||||
The first Campaign-centric capability and infrastructure fit assessment is in
|
||||
`docs/CAPABILITY_AND_INFRASTRUCTURE_FIT.md`. Its rerun tooling can collect and
|
||||
verify a bounded installed composition; target, provider and production claims
|
||||
remain separate, expiring attestations signed by independently scoped proof
|
||||
authorities. The operational issuance, target-run, recovery-measurement, key
|
||||
custody, and promotion-gate procedure is in
|
||||
[Target Maturity Evidence Runbook](docs/TARGET_MATURITY_EVIDENCE_RUNBOOK.md).
|
||||
The machine-readable repository list lives in `repositories.json`; the
|
||||
clickable directory is the
|
||||
[Repository Index](docs/project/REPOSITORY_INDEX.md), and ownership boundaries
|
||||
are in [Repository Structure](docs/project/REPOSITORY_STRUCTURE.md).
|
||||
|
||||
# GovOPlaN Docker
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
# GovOPlaN Frontend Layout Principles
|
||||
|
||||
GovOPlaN modules should choose their page layout by the kind of work the user is
|
||||
doing, not by the repository that owns the feature.
|
||||
|
||||
These concise layout choices are one canonical input to the broader
|
||||
[`INTERFACE_PATTERN_LANGUAGE.md`](INTERFACE_PATTERN_LANGUAGE.md). The current
|
||||
route and rollout evidence lives in
|
||||
[`INTERFACE_SURFACE_INVENTORY.md`](INTERFACE_SURFACE_INVENTORY.md).
|
||||
|
||||
## Structured Data Directories
|
||||
|
||||
Use a full-available-space workspace for structured data directories: files,
|
||||
addresses, calendars, records, mailboxes, document stores, and similar domains
|
||||
where the primary task is browsing, selecting, filtering, inspecting, and acting
|
||||
on related objects.
|
||||
|
||||
Principles:
|
||||
|
||||
- The module route should use the full available content area.
|
||||
- Do not add a separate page heading row above the main workspace.
|
||||
- Prefer persistent navigation panes, such as tree panels, source panels, folder
|
||||
panels, calendar list panels, or mailbox folder panels.
|
||||
- Keep collection navigation and collection-level actions close to the relevant
|
||||
pane header.
|
||||
- In a list-detail workspace such as Scheduling, keep related lists stacked in
|
||||
the left pane and use the remaining main pane for view/create/edit. A single
|
||||
Add action stays in the relevant list-pane header and opens the common main
|
||||
editor; it does not create an additional menu or launcher.
|
||||
- Use bounded widths for navigation/list panes and let the main detail/content
|
||||
pane take the remaining space.
|
||||
- Keep filtering controls inside the pane they affect.
|
||||
- Use overlays, toasts, or floating alerts for transient messages so the
|
||||
workspace height does not change.
|
||||
|
||||
This pattern is appropriate when the user is working inside one coherent data
|
||||
domain and needs spatial continuity.
|
||||
|
||||
## Workflow And Configuration Surfaces
|
||||
|
||||
Use the standard heading/menu/card visual language for workflow structures,
|
||||
settings, administration, dashboards, and pages that collect essentially
|
||||
unrelated areas.
|
||||
|
||||
Principles:
|
||||
|
||||
- A page heading and subnavigation are appropriate when the page explains a
|
||||
task, workflow stage, or administrative area.
|
||||
- Cards are appropriate for repeated independent panels, settings groups,
|
||||
summaries, and dashboard widgets.
|
||||
- Collapsible panels and segmented controls are appropriate when a dense
|
||||
configuration area needs controlled disclosure.
|
||||
- A collapsible card whose sole content is a table gives that table the full
|
||||
available card body; avoid nested cards, duplicate padding, inner max-widths,
|
||||
and nested scrolling.
|
||||
- Avoid forcing workflow/configuration pages into a file-explorer style unless
|
||||
the primary interaction is genuinely directory browsing.
|
||||
|
||||
This pattern is appropriate when the user is comparing or configuring separate
|
||||
concerns rather than navigating one structured object space.
|
||||
|
||||
## Shared Components
|
||||
|
||||
Reusable layout components belong in `govoplan-core` WebUI. Modules may consume
|
||||
shared components from core, but must not import another module's private UI
|
||||
components directly.
|
||||
|
||||
When a module-specific component becomes generally useful, promote it to core
|
||||
with a parameterized API before reusing it elsewhere.
|
||||
|
||||
Non-self-explanatory fields use Core `FieldLabel`; documented omissions must
|
||||
name their accessible-label source. Explicit Discard and dirty navigation use
|
||||
the same Core unsaved-changes dialog. Table action sets retain unavailable row
|
||||
actions as disabled controls and reserve empty-state slots so Add remains
|
||||
aligned. Use central feedback/dialog components; `window.alert` is not an
|
||||
authorized product surface unless a product-owner-approved exception is first
|
||||
recorded in the Core decision ledger.
|
||||
+108
-56
@@ -1,75 +1,127 @@
|
||||
# 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) |
|
||||
| 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.
|
||||
|
||||
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
|
||||
```
|
||||
|
||||
+10
@@ -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.
|
||||
|
||||
+10
-10
@@ -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
|
||||
|
||||
+162
-14
@@ -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
|
||||
+45
-2
@@ -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,23 @@ 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 first versioned `product_surfaces` slice is now implemented in Core. It
|
||||
binds a stable product identity and entry path to one or more owner routes,
|
||||
View surfaces, presentations, capabilities, search sources, help contexts and
|
||||
documentation topics. It also carries standard unavailable/degraded
|
||||
explanations and migration aliases. Mail and Postbox contribute the first
|
||||
shared identity, `communication.messages`: `/messages` and the migration alias
|
||||
`/inbox` select the first currently authorized, View-visible owner while the
|
||||
underlying `/mail` and `/postbox` deep links, custody and permissions remain
|
||||
unchanged. Alias resolution emits a bounded client telemetry event before the
|
||||
redirect.
|
||||
|
||||
Core's `ProductAvailabilityState` is the shared presentation primitive for
|
||||
authorization, Policy, configuration, disabled, missing-capability, offline and
|
||||
provider-degraded states. Product language is primary; exact module,
|
||||
capability, provider and correlation provenance is available only in an
|
||||
expandable technical section.
|
||||
|
||||
## Navigation Model
|
||||
|
||||
The default shell should prioritize:
|
||||
@@ -80,6 +99,22 @@ 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 remains available as **All
|
||||
available tools**. Its ability to scroll is useful and is not itself the
|
||||
product defect. The defect is requiring people to infer a task or outcome from
|
||||
repository topology.
|
||||
|
||||
Task-local Work, Calendar, Messages and Files tools may be contributed to the
|
||||
optional `govoplan-quick-access` rail. Messages composes Mail, Postbox and
|
||||
future governed chat presentation without merging their channel semantics or
|
||||
state.
|
||||
|
||||
A module route remains a valid deep link. A product area may combine links and
|
||||
owner-rendered surfaces from several modules. When a required contribution is
|
||||
absent, the area explains the missing outcome rather than rendering a broken
|
||||
@@ -106,11 +141,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 +160,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,232 @@
|
||||
# Quick Access And Product Areas
|
||||
|
||||
## Purpose
|
||||
|
||||
GovOPlaN presents institutional work without requiring ordinary users to
|
||||
understand the installed package graph. Two complementary projections provide
|
||||
that experience:
|
||||
|
||||
- **product areas** group destinations, objects, work and actions by the
|
||||
outcome a person recognizes;
|
||||
- **Quick Access** keeps a small set of task-local tools available without
|
||||
leaving the current page, case, record or Workflow context.
|
||||
|
||||
Technical modules remain the implementation, release and provenance boundary.
|
||||
Product areas and Quick Access are presentation contracts over those owners;
|
||||
they do not copy domain state or bypass authorization.
|
||||
|
||||
Implementation is tracked by Core #283 and #285, GovOPlaN's product-experience
|
||||
umbrella, Views, Policy and `govoplan-quick-access`.
|
||||
|
||||
The repository and product name is `govoplan-quick-access`, with module id
|
||||
`quick_access`. `govoplan-qar` was rejected because the abbreviation hides the
|
||||
purpose in package catalogues, diagnostics, permissions and operations.
|
||||
|
||||
## Implementation Status
|
||||
|
||||
The first production-shaped slice is implemented:
|
||||
|
||||
- Core validates and publishes versioned `product_areas` and
|
||||
`quick_access_tools` manifest contracts;
|
||||
- `govoplan-quick-access` derives its live catalogue from installed modules,
|
||||
persists optimistic-concurrency-protected system, tenant and user profiles,
|
||||
and resolves blocked, forced, ordered and stale preferences;
|
||||
- the shell hosts the optional right rail and one composed drawer with keyboard
|
||||
dismissal, focus return, responsive mobile behavior and full-page fallbacks;
|
||||
- Tasks, Calendar, Mail, Postbox and Files contribute the first owner-rendered
|
||||
tools; Mail and Postbox remain separate sections inside Messages;
|
||||
- immutable View revisions now carry grouped/flat navigation, product-area
|
||||
order and optional labels. Scoped Views therefore configure product
|
||||
presentation for system, tenant, group, user and Workflow contexts;
|
||||
- the expanded left rail groups classified destinations while retaining
|
||||
Dashboard and every authorized unclassified destination under More tools.
|
||||
|
||||
The baseline classification is now manifest-declared for every ordinary
|
||||
user-facing module and enforced by the workspace manifest check. A separately
|
||||
versioned launch-context contract carries bounded active-object, acting,
|
||||
temporal, View and return references into full-page Quick Access fallbacks;
|
||||
Cases publishes the first active-object reference. The remaining rollout is to
|
||||
add useful bounded tools and active-object publishers only where a maintained
|
||||
journey benefits, and to extend browser evidence to a pinned reference
|
||||
composition. Authorized global and technical routes remain visible through
|
||||
their dedicated shell entry or **All available tools**.
|
||||
|
||||
## Quick Access Boundary
|
||||
|
||||
Core owns a versioned contribution contract. Feature modules may register a
|
||||
tool when they have a useful bounded surface. They do not import Quick Access.
|
||||
`govoplan-quick-access` owns configuration, effective resolution, ordering,
|
||||
the right-side rail and its drawer. Views may narrow tools for the current
|
||||
task. Policy may constrain availability and customization. Access and each
|
||||
owner's backend remain authoritative.
|
||||
|
||||
The initial categories are:
|
||||
|
||||
| Category | Typical contributions |
|
||||
| --- | --- |
|
||||
| Work | Explicit Tasks, Workflow handoffs, approvals, deadlines and exceptions |
|
||||
| Calendar | Today/upcoming agenda, event creation and scheduling launch |
|
||||
| Messages | Mail, function-bound Postbox messages and future governed chat providers |
|
||||
| Files | Contextual/recent files, attachment selection and upload |
|
||||
|
||||
Messages is one shell category but not one data model. Mail, Postbox and future
|
||||
chat providers retain their channel semantics, custody, policy, audit and
|
||||
delivery behavior. The drawer identifies the channel where that distinction
|
||||
matters.
|
||||
|
||||
## Contribution Contract
|
||||
|
||||
A Quick Access contribution declares:
|
||||
|
||||
- contract version 1, a stable id, category and human label;
|
||||
- icon, order and optional badge/summary provider;
|
||||
- required permissions and optional dependencies;
|
||||
- global or active-object availability, accepted context-reference kinds and
|
||||
produced result-reference kinds;
|
||||
- an owner-rendered bounded WebUI surface and full-page fallback route;
|
||||
- View surface, help context and availability explanation;
|
||||
- whether the contribution supports preview, create, select or resume.
|
||||
|
||||
The shell passes only bounded references: tenant, acting context, temporal
|
||||
read context, active task/Workflow, current institutional object, selected
|
||||
resources and a safe return location. The owner reauthorizes every read and
|
||||
effect. Credentials, protected content and permission decisions are never
|
||||
embedded in launch context.
|
||||
|
||||
Launch-context version 2 identifies reference contract version 1 and carries
|
||||
the exact resolved View revision plus optional recommended and focused tool
|
||||
ids. Recommendations affect order and emphasis only. Focus narrows the rail
|
||||
only when at least one focused contribution survives module enablement,
|
||||
configuration, context compatibility and authorization; otherwise the normal
|
||||
effective rail remains available. Workflow gets the same behavior by resolving
|
||||
the exact View revision instead of acquiring separate presentation authority.
|
||||
|
||||
An owner-rendered tool explicitly returns result contract version 1 as either
|
||||
`completed` with an action and typed owner reference, or `cancelled` with a
|
||||
reason. The shell correlates the result with the source and tool, rejects
|
||||
cross-tenant or undeclared reference kinds, and does not interpret closing the
|
||||
drawer as completion. Owner modules validate, persist, recover and audit their
|
||||
own effects. The overlay leaves the host route mounted, so unsaved host-page
|
||||
state is preserved; the full-page route remains the bounded-work fallback.
|
||||
|
||||
## Effective Configuration
|
||||
|
||||
The effective rail is resolved from:
|
||||
|
||||
1. installed and enabled modules and their registered contributions;
|
||||
2. system availability, forced entries and ordering defaults;
|
||||
3. tenant availability, forced entries and ordering defaults;
|
||||
4. group and user View/Policy ceilings where configured;
|
||||
5. the user's enabled categories, entries and ordering;
|
||||
6. the active View and optional Workflow-step narrowing overlay;
|
||||
7. current authorization and contribution availability.
|
||||
|
||||
Lower scopes may narrow or reorder allowed entries but cannot enable a tool
|
||||
blocked above them. A forced entry cannot be removed below its source. User
|
||||
configuration stores stable contribution ids; unavailable or retired ids are
|
||||
retained as explained stale preferences without rendering broken controls.
|
||||
|
||||
Configuration screens derive their available choices from the live registry.
|
||||
Installing or enabling a contributing module adds its permitted choices;
|
||||
disabling it removes the runtime tool while preserving harmless preferences.
|
||||
If Quick Access is absent, contributors behave exactly as before.
|
||||
|
||||
## Interaction Model
|
||||
|
||||
Desktop uses a narrow right-side rail with at most four initial category
|
||||
buttons and an overflow when an administrator or user adds more categories.
|
||||
Selecting a category opens one fixed, owner-neutral drawer. Contributions are
|
||||
shown inside that drawer as tabs, sections or commands according to the
|
||||
category contract. The default drawer overlays content so DataGrid and fixed
|
||||
workspace layouts do not resize unexpectedly; a later explicit pinned mode may
|
||||
reserve layout width on sufficiently wide screens.
|
||||
|
||||
The drawer preserves host-page state, has a deterministic focus return, closes
|
||||
with Escape, supports keyboard traversal, and provides explicit completion,
|
||||
cancellation and full-page actions. Mobile and narrow layouts use the same
|
||||
category/configuration semantics in a bottom sheet or compact menu.
|
||||
|
||||
## Product Areas
|
||||
|
||||
Product areas are stable configurable identities, not repositories. The
|
||||
recommended baseline is:
|
||||
|
||||
- Work;
|
||||
- Services and Cases;
|
||||
- Records and Documents;
|
||||
- Communication;
|
||||
- Meetings and Decisions;
|
||||
- Data and Assurance;
|
||||
- People and Responsibility.
|
||||
|
||||
Modules contribute routes, objects, actions, widgets, work sources and help to
|
||||
one or more areas. Product packages and administrators may define sensible
|
||||
system and tenant defaults. Views select, order, rename or narrow allowed
|
||||
areas, and users may personalize them within Policy ceilings. An empty area is
|
||||
omitted. An area with one destination may open it directly. A multi-destination
|
||||
area provides a useful work/recent/action surface rather than another menu.
|
||||
|
||||
Familiar product nouns such as Calendar, Mail or Files may remain directly
|
||||
pinned. The objective is not to hide every module name; it is to prevent
|
||||
repository topology from determining a person's workflow.
|
||||
|
||||
The initial module classification is deliberately outcome-oriented:
|
||||
|
||||
| Product area | Contributing user-facing modules |
|
||||
| --- | --- |
|
||||
| Work | Approvals, Projects, Tasks, Workflow |
|
||||
| Services and Cases | Cases, Forms, Forms Runtime, Portal |
|
||||
| Records and Documents | Files, Records, Templates |
|
||||
| Communication | Campaigns, Distribution Lists, Mail, Notifications, Postbox |
|
||||
| Meetings and Decisions | Calendar, Committee, Scheduling, Voting |
|
||||
| Data and Assurance | Dataflow, Datasources, Reporting, Risk Compliance |
|
||||
| People and Responsibility | Address Book, IDM, Organizations |
|
||||
|
||||
Dashboard, Search, Documentation and Quick Access remain global shell
|
||||
affordances. Access, Administration, Audit, Encryption, Identity Trust,
|
||||
Operations, Policy, Tenancy and Views remain administrative or platform
|
||||
surfaces available through their dedicated entry point or **All available
|
||||
tools**. The manifest-shape check enforces both this explicit exception set and
|
||||
the shared label, icon, description and ordering of every canonical area.
|
||||
|
||||
## Full Access And Provenance
|
||||
|
||||
The existing permission-derived module rail remains available as **All
|
||||
available tools** for power users and deliberate escape from a focused View.
|
||||
It contains only currently authorized destinations. Technical module,
|
||||
capability, provider and package provenance remains visible in administration,
|
||||
diagnostics, evidence and expandable details.
|
||||
|
||||
Search, deep links and help distinguish three states:
|
||||
|
||||
- available in the active View;
|
||||
- authorized but outside the active View, with a temporary escape or View
|
||||
switch;
|
||||
- unavailable because of authorization, Policy, configuration or a missing
|
||||
capability, with an actionable explanation.
|
||||
|
||||
## Delivery Order
|
||||
|
||||
1. Define Core product-area and Quick Access contracts and validation.
|
||||
2. Implement `govoplan-quick-access` configuration, effective resolution and
|
||||
shell capability.
|
||||
3. Contribute Work, Calendar, Messages and Files bounded surfaces.
|
||||
4. Add configurable product-area defaults through Views and product packages.
|
||||
5. Migrate navigation, breadcrumbs, search, errors, documentation, dashboard
|
||||
and administration toward product terminology.
|
||||
6. Prove keyboard, focus, responsive, optional-module and reference-journey
|
||||
behavior before making it the ordinary-user default.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- A user can configure allowed Quick Access categories and ordering without
|
||||
gaining authority.
|
||||
- System and tenant administrators can make entries available, forced or
|
||||
unavailable with provenance.
|
||||
- Mail, Postbox and another future channel can share Messages presentation
|
||||
while retaining independent state and channel semantics.
|
||||
- A reference journey can use a bounded tool and return without losing host
|
||||
state or Workflow context.
|
||||
- Product areas remain useful under sparse and rich permission sets and under
|
||||
optional-module permutations.
|
||||
- All available tools and technical provenance remain deliberately reachable.
|
||||
@@ -7,6 +7,11 @@ responsibility, or workflow step. A View can reduce the visible modules,
|
||||
navigation entries, routes, page sections, and commands to the interface
|
||||
needed for the current job.
|
||||
|
||||
Views also project configurable product areas and Quick Access contributions.
|
||||
They may select, order, rename or hide permitted presentation identities but
|
||||
do not move ownership or merge Mail, Postbox, Files, Calendar, Tasks or other
|
||||
domain state.
|
||||
|
||||
Views are optional. If `govoplan-views` is not installed or enabled, the normal
|
||||
permission-derived interface remains unchanged.
|
||||
|
||||
@@ -119,6 +124,12 @@ Implemented in the initial Views slice:
|
||||
prevention
|
||||
- surface declarations for every currently installed module that contributes a
|
||||
WebUI, including finer-grained shared administration and settings surfaces
|
||||
- immutable presentation settings for grouped or flat navigation, product-area
|
||||
order and product-area labels; the shell resolves these settings through the
|
||||
same system, tenant, group, user and Workflow-aware View projection
|
||||
- live product-area identities from module manifests, with authorized
|
||||
unclassified destinations retained under More tools during incremental
|
||||
adoption
|
||||
|
||||
Still intentionally separate:
|
||||
|
||||
@@ -127,6 +138,11 @@ Still intentionally separate:
|
||||
- read-only and layout-replacement projections beyond the version `1`
|
||||
visible/hidden contract
|
||||
|
||||
Quick Access ordering and availability remain owned by
|
||||
`govoplan-quick-access`; Views only narrow its declared surfaces for the active
|
||||
task. Neither contract permits arbitrary layout or styling. See
|
||||
`docs/architecture/QUICK_ACCESS_AND_PRODUCT_AREAS.md` in the meta repository.
|
||||
|
||||
## Gitea Work Packages
|
||||
|
||||
- `govoplan#17`: task-focused Views user story
|
||||
+5
-1
@@ -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
|
||||
+12
-3
@@ -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.
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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.
|
||||
+21
-15
@@ -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: 48
|
||||
- Reviewed no-store rationales: 24
|
||||
- Unexplained coverage gaps: 0
|
||||
|
||||
| Module | Repository | Persistence | Coverage | Rationale |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `access` | `govoplan-access` | Migration-owned | Provider | Provider `privacy.dsar.access` is registered and documented. |
|
||||
| `addresses` | `govoplan-addresses` | Migration-owned | Provider | Provider `privacy.dsar.addresses` is registered and documented. |
|
||||
| `admin` | `govoplan-admin` | Migration-owned | Provider | Provider `privacy.dsar.admin` is registered and documented. |
|
||||
| `approvals` | `govoplan-approvals` | Migration-owned | Provider | Provider `privacy.dsar.approvals` is registered and documented. |
|
||||
| `assets` | `govoplan-assets` | No module migration | Reviewed no-store rationale | Contract-only module: asset persistence and lifecycle APIs are not implemented; reassess before adding a migration-owned store. |
|
||||
| `audit` | `govoplan-audit` | Migration-owned | Provider | Provider `privacy.dsar.audit` is registered and documented. |
|
||||
| `booking` | `govoplan-booking` | No module migration | Reviewed no-store rationale | Contract-only module: booking persistence and reservation workflows are not implemented; reassess before adding a migration-owned store. |
|
||||
| `calendar` | `govoplan-calendar` | Migration-owned | Provider | Provider `privacy.dsar.calendar` is registered and documented. |
|
||||
| `campaigns` | `govoplan-campaign` | Migration-owned | Provider | Provider `privacy.dsar.campaigns` is registered and documented. |
|
||||
| `cases` | `govoplan-cases` | Migration-owned | Provider | Provider `privacy.dsar.cases` is registered and documented. |
|
||||
| `certificates` | `govoplan-certificates` | No module migration | Reviewed no-store rationale | Contract-only module: certificate issuance and revocation persistence are not implemented; reassess before adding a migration-owned store. |
|
||||
| `committee` | `govoplan-committee` | Migration-owned | Provider | Provider `privacy.dsar.committee` is registered and documented. |
|
||||
| `connectors` | `govoplan-connectors` | Migration-owned | Provider | Provider `privacy.dsar.connectors` is registered and documented. |
|
||||
| `consultation` | `govoplan-consultation` | No module migration | Reviewed no-store rationale | Contract-only module: consultation submissions and evaluation persistence are not implemented; reassess before adding a migration-owned store. |
|
||||
| `contracts` | `govoplan-contracts` | No module migration | Reviewed no-store rationale | Contract-only module: contract, amendment, and obligation persistence are not implemented; reassess before adding a migration-owned store. |
|
||||
| `dashboard` | `govoplan-dashboard` | Migration-owned | Provider | Provider `privacy.dsar.dashboard` is registered and documented. |
|
||||
| `dataflow` | `govoplan-dataflow` | Migration-owned | Provider | Provider `privacy.dsar.dataflow` is registered and documented. |
|
||||
| `datasources` | `govoplan-datasources` | Migration-owned | Provider | Provider `privacy.dsar.datasources` is registered and documented. |
|
||||
| `decisions` | `govoplan-decisions` | Migration-owned | Provider | Provider `privacy.dsar.decisions` is registered and documented. |
|
||||
| `dist_lists` | `govoplan-dist-lists` | Migration-owned | Provider | Provider `privacy.dsar.dist_lists` is registered and documented. |
|
||||
| `dms` | `govoplan-dms` | No module migration | Reviewed no-store rationale | Stateless integration-preview module: DMS retains no document, person, credential, or provider-response store; Files and Records remain the subject-data owners. Reassess before persisting a target binding, plan, receipt, or diagnostic. |
|
||||
| `docs` | `govoplan-docs` | Migration-owned | Provider | Provider `privacy.dsar.docs` is registered and documented. |
|
||||
| `encryption` | `govoplan-encryption` | Migration-owned | Provider | Provider `privacy.dsar.encryption` is registered and documented. |
|
||||
| `erp` | `govoplan-erp` | No module migration | Reviewed no-store rationale | Stateless integration-contract module: ERP retains no invoice, payable, plan, booking observation, provider response, or credential store; Procurement, Payments, Ledger, Files, and Audit remain the subject-data owners. Reassess before persisting a target binding, plan, receipt, reconciliation decision, or diagnostic. |
|
||||
| `evaluation` | `govoplan-evaluation` | No module migration | Reviewed no-store rationale | Contract-only module: evaluation runs, responses, and scores are not persisted; reassess before adding a migration-owned store. |
|
||||
| `facilities` | `govoplan-facilities` | No module migration | Reviewed no-store rationale | Contract-only module: facility and maintenance persistence are not implemented; reassess before adding a migration-owned store. |
|
||||
| `files` | `govoplan-files` | Migration-owned | Provider | Provider `privacy.dsar.files` is registered and documented. |
|
||||
| `fit_connect` | `govoplan-fit-connect` | No module migration | Reviewed no-store rationale | Stateless transport-contract module: FIT-Connect retains no submission, attachment, receipt, acknowledgement plan, key, provider response, or diagnostic store; the owning Service, Forms, Cases, Files, and Audit workflows remain responsible for subject data. Reassess before persisting any ingress or event-log evidence. |
|
||||
| `forms` | `govoplan-forms` | Migration-owned | Provider | Provider `privacy.dsar.forms` is registered and documented. |
|
||||
| `forms_runtime` | `govoplan-forms-runtime` | Migration-owned | Provider | Provider `privacy.dsar.forms_runtime` is registered and documented. |
|
||||
| `grants` | `govoplan-grants` | No module migration | Reviewed no-store rationale | Contract-only module: grant applications, awards, and monitoring are not persisted; reassess before adding a migration-owned store. |
|
||||
| `helpdesk` | `govoplan-helpdesk` | Migration-owned | Provider | Provider `privacy.dsar.helpdesk` is registered and documented. |
|
||||
| `identity` | `govoplan-identity` | Migration-owned | Provider | Provider `privacy.dsar.identity` is registered and documented. |
|
||||
| `identity_trust` | `govoplan-identity-trust` | Migration-owned | Provider | Provider `privacy.dsar.identity_trust` is registered and documented. |
|
||||
| `idm` | `govoplan-idm` | Migration-owned | Provider | Provider `privacy.dsar.idm` is registered and documented. |
|
||||
| `inspections` | `govoplan-inspections` | No module migration | Reviewed no-store rationale | Contract-only module: inspections, findings, and measures are not persisted; reassess before adding a migration-owned store. |
|
||||
| `learning` | `govoplan-learning` | No module migration | Reviewed no-store rationale | Contract-only module: learning offers, enrollment, and completion are not persisted; reassess before adding a migration-owned store. |
|
||||
| `mail` | `govoplan-mail` | Migration-owned | Provider | Provider `privacy.dsar.mail` is registered and documented. |
|
||||
| `mandates` | `govoplan-mandates` | Migration-owned | Provider | Provider `privacy.dsar.mandates` is registered and documented. |
|
||||
| `notifications` | `govoplan-notifications` | Migration-owned | Provider | Provider `privacy.dsar.notifications` is registered and documented. |
|
||||
| `ops` | `govoplan-ops` | No module migration | Reviewed no-store rationale | Projection-only module: Ops reads bounded platform and provider status; durable recovery evidence remains owned by Core and domain modules. |
|
||||
| `organizations` | `govoplan-organizations` | Migration-owned | Provider | Provider `privacy.dsar.organizations` is registered and documented. |
|
||||
| `parties` | `govoplan-parties` | Migration-owned | Provider | Provider `privacy.dsar.parties` is registered and documented. |
|
||||
| `payments` | `govoplan-payments` | Migration-owned | Provider | Provider `privacy.dsar.payments` is registered and documented. |
|
||||
| `permits` | `govoplan-permits` | No module migration | Reviewed no-store rationale | Contract-only module: permit applications, assessments, and decisions are not persisted; reassess before adding a migration-owned store. |
|
||||
| `policy` | `govoplan-policy` | Migration-owned | Provider | Provider `privacy.dsar.policy` is registered and documented. |
|
||||
| `poll` | `govoplan-poll` | Migration-owned | Provider | Provider `privacy.dsar.poll` is registered and documented. |
|
||||
| `portal` | `govoplan-portal` | No module migration | Reviewed no-store rationale | Projection-only module: Portal stores no applicant records; Services, Forms Runtime, Cases, and Postbox own and export authoritative subject data. |
|
||||
| `postbox` | `govoplan-postbox` | Migration-owned | Provider | Provider `privacy.dsar.postbox` is registered and documented. |
|
||||
| `procurement` | `govoplan-procurement` | No module migration | Reviewed no-store rationale | Contract-only module: procurement procedures, tenders, and awards are not persisted; reassess before adding a migration-owned store. |
|
||||
| `projects` | `govoplan-projects` | Migration-owned | Provider | Provider `privacy.dsar.projects` is registered and documented. |
|
||||
| `quick_access` | `govoplan-quick-access` | Migration-owned | Provider | Provider `privacy.dsar.quick_access` is registered and documented. |
|
||||
| `records` | `govoplan-records` | Migration-owned | Provider | Provider `privacy.dsar.records` is registered and documented. |
|
||||
| `reporting` | `govoplan-reporting` | Migration-owned | Provider | Provider `privacy.dsar.reporting` is registered and documented. |
|
||||
| `resources` | `govoplan-resources` | No module migration | Reviewed no-store rationale | Contract-only module: resource catalog and allocation persistence are not implemented; reassess before adding a migration-owned store. |
|
||||
| `rest` | `govoplan-rest` | No module migration | Reviewed no-store rationale | Transport-only module: REST binds explicitly published functions and owns no domain or subject-data store. |
|
||||
| `risk_compliance` | `govoplan-risk-compliance` | Migration-owned | Provider | Provider `privacy.dsar.risk_compliance` is registered and documented. |
|
||||
| `scheduling` | `govoplan-scheduling` | Migration-owned | Provider | Provider `privacy.dsar.scheduling` is registered and documented. |
|
||||
| `search` | `govoplan-search` | Migration-owned | Provider | Provider `privacy.dsar.search` is registered and documented. |
|
||||
| `services` | `govoplan-services` | Migration-owned | Provider | Provider `privacy.dsar.services` is registered and documented. |
|
||||
| `soap` | `govoplan-soap` | No module migration | Reviewed no-store rationale | Transport-only module: SOAP binds explicitly published operations and owns no domain or subject-data store. |
|
||||
| `tasks` | `govoplan-tasks` | Migration-owned | Provider | Provider `privacy.dsar.tasks` is registered and documented. |
|
||||
| `templates` | `govoplan-templates` | Migration-owned | Provider | Provider `privacy.dsar.templates` is registered and documented. |
|
||||
| `tenancy` | `govoplan-tenancy` | No module migration | Reviewed no-store rationale | Orchestration module: tenant lifecycle and settings use Core-owned storage; Access covers account and membership subject data. |
|
||||
| `tickets` | `govoplan-tickets` | Migration-owned | Provider | Provider `privacy.dsar.tickets` is registered and documented. |
|
||||
| `transparency` | `govoplan-transparency` | No module migration | Reviewed no-store rationale | Contract-only module: requests, disclosure reviews, and publications are not persisted; reassess before adding a migration-owned store. |
|
||||
| `views` | `govoplan-views` | Migration-owned | Provider | Provider `privacy.dsar.views` is registered and documented. |
|
||||
| `voting` | `govoplan-voting` | Migration-owned | Provider | Provider `privacy.dsar.voting` is registered and documented. |
|
||||
| `wiki` | `govoplan-wiki` | Migration-owned | Provider | Provider `privacy.dsar.wiki` is registered and documented. |
|
||||
| `workflow` | `govoplan-workflow` | No module migration | Reviewed no-store rationale | Presentation-only module: Workflow edits and projects Workflow Engine state; Workflow Engine owns persistence and DSAR coverage. |
|
||||
| `workflow_engine` | `govoplan-workflow-engine` | Migration-owned | Provider | Provider `privacy.dsar.workflow_engine` is registered and documented. |
|
||||
| `xrechnung` | `govoplan-xrechnung` | No module migration | Reviewed no-store rationale | Stateless validation-contract module: XRechnung persists no invoice, report, diagnostic, or handoff; the invoking Files, Procurement, or Payments workflow remains the subject-data owner. Reassess before adding a validation store. |
|
||||
|
||||
Provider search, export minimization, retention, and erasure behavior remains
|
||||
documented and tested by each owning module. This matrix verifies adoption and
|
||||
ownership coverage; Core continues to test disabled providers, partial failure,
|
||||
retry, authorization evidence, and horizontally coordinated execution.
|
||||
+13
-6
@@ -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,
|
||||
@@ -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",
|
||||
|
||||
+2
-2
@@ -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
|
||||
@@ -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`.
|
||||
+34
-5
@@ -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,7 @@ 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 |
|
||||
| `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 +111,17 @@ 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.
|
||||
|
||||
Build the same dependency-free tool as one downloadable artifact:
|
||||
|
||||
@@ -195,9 +210,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.
|
||||
|
||||
@@ -404,16 +419,24 @@ 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.
|
||||
- Replacing or removing a capability adds a review action that names the prior
|
||||
and desired state/source plus declared module consumers. This does not claim
|
||||
that the deployer can inspect module-owned database configuration; the
|
||||
operator must review that inventory 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 +449,12 @@ 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.
|
||||
|
||||
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
|
||||
@@ -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
|
||||
|
||||
@@ -136,31 +136,38 @@ 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/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 SHA-256 values and npm integrity
|
||||
values. Credentials are accepted only through environment variables and are
|
||||
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 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 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, 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.
|
||||
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
|
||||
@@ -170,6 +177,65 @@ The source refs remain in the module catalog for source provenance and release
|
||||
planning. Production installation consumes the signed runtime images rather
|
||||
than invoking `pip`, `npm`, or Git on the target host.
|
||||
|
||||
## Public module directory
|
||||
|
||||
`tools/release/publish-release-catalog.sh` resolves the selected package set and
|
||||
registry lock before it creates a catalog. Catalog entries are synthesized from
|
||||
the exact tagged module manifests, never from a hand-maintained module list or
|
||||
the current workspace. Each entry binds its Python wheel and optional WebUI
|
||||
tarball to the registry URL, filename, size, SHA-256, package identity, source
|
||||
tag, and source commit before the complete catalog is signed.
|
||||
|
||||
The same publication transaction regenerates and prunes the browsable static
|
||||
directory under `public/catalogs/v1/modules/`. It writes a global
|
||||
`modules/index.json`, one `<module>/index.json`, and one
|
||||
`<module>/<version>/manifest.json` for every entry in the signed channel.
|
||||
These files are derived from that exact signed payload and keyring; stale JSON
|
||||
from an older partial catalog is removed while unrelated static assets are left
|
||||
untouched. The signed channel remains the trust anchor, while the module
|
||||
directory provides stable discovery URLs for browsers and external tooling.
|
||||
|
||||
Official GovOPlaN modules are open-source directory entries and do not require
|
||||
license entitlements. The generic `license_features` contract remains available
|
||||
for third-party package directories, support/configuration packages, or future
|
||||
deployment-specific presets. A catalog entry is gated only when that entry
|
||||
explicitly declares such features.
|
||||
|
||||
Core carries the public stable catalog URL and its independently pinned trust
|
||||
anchor. In the absence of an operator-configured catalog, Admin discovers the
|
||||
official directory automatically. Selecting an entry creates a reviewed
|
||||
install/update plan; the trusted installer downloads the exact signed artifacts
|
||||
into a private digest cache, verifies size and hash, and installs only from that
|
||||
cache. A saved plan is rejected if any package ref, artifact identity, catalog
|
||||
channel, sequence, or signing-key identity differs from the currently validated
|
||||
catalog.
|
||||
|
||||
The Admin directory can be searched by module, package, repository, or tag and
|
||||
filtered by available, installed, update, and blocked/withdrawn states. It
|
||||
shows the source revision, artifact digest, release notes, and configuration
|
||||
requirements. Missing dependency/interface providers and unsupported update
|
||||
windows are surfaced before an operator adds the entry to a plan; installer
|
||||
preflight remains authoritative.
|
||||
|
||||
Catalog entries also carry the permission definitions declared by the tagged
|
||||
module manifest. Admin groups and exposes their scopes before an install or
|
||||
update is planned. This is disclosure only: installing a module does not grant
|
||||
its permissions to an account, role, group, tenant, or service account.
|
||||
|
||||
Package lifecycle and availability are intentionally separate:
|
||||
|
||||
- install, update, and uninstall change the instance-wide package composition;
|
||||
- enable and disable change the active instance runtime graph;
|
||||
- tenant module entitlements define unavailable, available, and forced modules;
|
||||
- group/user presentation is governed through Views and Policy; and
|
||||
- enabling a capability module does not opt data into that capability.
|
||||
|
||||
Single-process or single-host installations may execute a supervised package
|
||||
plan locally. Shared-state and Kubernetes profiles reject node-local package
|
||||
mutation: operators compose and roll out a new signed full-profile runtime image
|
||||
instead. This prevents replicas from drifting while retaining the same Admin
|
||||
catalog and preflight experience.
|
||||
|
||||
## Developer meta-package
|
||||
|
||||
`packages/govoplan-meta` builds the optional `govoplan` package. Its default
|
||||
@@ -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
|
||||
|
||||
+5
-1
@@ -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.
|
||||
@@ -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.
|
||||
|
||||
@@ -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?
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
+23
-13
@@ -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
|
||||
+32
-28
@@ -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 3–5; collaborative documents combine
|
||||
the evidence spine, service packages, and records assurance across Horizons
|
||||
2–4. 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.
|
||||
@@ -4,82 +4,89 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "govoplan"
|
||||
version = "0.1.18"
|
||||
version = "0.1.40"
|
||||
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.18",
|
||||
"govoplan-tenancy==0.1.18",
|
||||
"govoplan-organizations==0.1.18",
|
||||
"govoplan-identity==0.1.18",
|
||||
"govoplan-idm==0.1.18",
|
||||
"govoplan-access==0.1.18",
|
||||
"govoplan-admin==0.1.18",
|
||||
"govoplan-policy==0.1.18",
|
||||
"govoplan-audit==0.1.18",
|
||||
"govoplan-dashboard==0.1.18",
|
||||
"govoplan-files==0.1.18",
|
||||
"govoplan-mail==0.1.18",
|
||||
"govoplan-campaign==0.1.18",
|
||||
"govoplan-calendar==0.1.18",
|
||||
"govoplan-docs==0.1.18",
|
||||
"govoplan-ops==0.1.18",
|
||||
"govoplan-core[server]==0.1.40",
|
||||
"govoplan-tenancy==0.1.20",
|
||||
"govoplan-organizations==0.1.20",
|
||||
"govoplan-identity==0.1.20",
|
||||
"govoplan-idm==0.1.24",
|
||||
"govoplan-access==0.1.23",
|
||||
"govoplan-admin==0.1.22",
|
||||
"govoplan-policy==0.1.22",
|
||||
"govoplan-audit==0.1.20",
|
||||
"govoplan-dashboard==0.1.20",
|
||||
"govoplan-files==0.1.23",
|
||||
"govoplan-mail==0.1.25",
|
||||
"govoplan-campaign==0.1.27",
|
||||
"govoplan-calendar==0.1.22",
|
||||
"govoplan-docs==0.1.22",
|
||||
"govoplan-ops==0.1.20",
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
full = [
|
||||
"govoplan-addresses==0.1.18",
|
||||
"govoplan-approvals==0.1.18",
|
||||
"govoplan-assets==0.1.18",
|
||||
"govoplan-booking==0.1.18",
|
||||
"govoplan-cases==0.1.18",
|
||||
"govoplan-certificates==0.1.18",
|
||||
"govoplan-committee==0.1.18",
|
||||
"govoplan-connectors==0.1.18",
|
||||
"govoplan-consultation==0.1.18",
|
||||
"govoplan-contracts==0.1.18",
|
||||
"govoplan-dataflow==0.1.18",
|
||||
"govoplan-datasources==0.1.18",
|
||||
"govoplan-decisions==0.1.18",
|
||||
"govoplan-dist-lists==0.1.18",
|
||||
"govoplan-encryption==0.1.18",
|
||||
"govoplan-evaluation==0.1.18",
|
||||
"govoplan-facilities==0.1.18",
|
||||
"govoplan-forms==0.1.18",
|
||||
"govoplan-forms-runtime==0.1.18",
|
||||
"govoplan-grants==0.1.18",
|
||||
"govoplan-helpdesk==0.1.18",
|
||||
"govoplan-identity-trust==0.1.18",
|
||||
"govoplan-inspections==0.1.18",
|
||||
"govoplan-learning==0.1.18",
|
||||
"govoplan-mandates==0.1.18",
|
||||
"govoplan-notifications==0.1.18",
|
||||
"govoplan-parties==0.1.18",
|
||||
"govoplan-permits==0.1.18",
|
||||
"govoplan-poll==0.1.18",
|
||||
"govoplan-portal==0.1.18",
|
||||
"govoplan-postbox==0.1.18",
|
||||
"govoplan-procurement==0.1.18",
|
||||
"govoplan-projects==0.1.18",
|
||||
"govoplan-records==0.1.18",
|
||||
"govoplan-reporting==0.1.18",
|
||||
"govoplan-resources==0.1.18",
|
||||
"govoplan-rest==0.1.18",
|
||||
"govoplan-risk-compliance==0.1.18",
|
||||
"govoplan-scheduling==0.1.18",
|
||||
"govoplan-search==0.1.18",
|
||||
"govoplan-services==0.1.18",
|
||||
"govoplan-soap==0.1.18",
|
||||
"govoplan-templates==0.1.18",
|
||||
"govoplan-tickets==0.1.18",
|
||||
"govoplan-transparency==0.1.18",
|
||||
"govoplan-views==0.1.18",
|
||||
"govoplan-voting==0.1.18",
|
||||
"govoplan-wiki==0.1.18",
|
||||
"govoplan-workflow==0.1.18",
|
||||
"govoplan-workflow-engine==0.1.18",
|
||||
"govoplan-addresses==0.1.21",
|
||||
"govoplan-approvals==0.1.20",
|
||||
"govoplan-assets==0.1.20",
|
||||
"govoplan-booking==0.1.20",
|
||||
"govoplan-cases==0.1.22",
|
||||
"govoplan-certificates==0.1.20",
|
||||
"govoplan-committee==0.1.20",
|
||||
"govoplan-connectors==0.1.25",
|
||||
"govoplan-consultation==0.1.20",
|
||||
"govoplan-contracts==0.1.20",
|
||||
"govoplan-dataflow==0.1.23",
|
||||
"govoplan-datasources==0.1.24",
|
||||
"govoplan-decisions==0.1.19",
|
||||
"govoplan-dist-lists==0.1.20",
|
||||
"govoplan-dms==0.1.20",
|
||||
"govoplan-encryption==0.1.19",
|
||||
"govoplan-erp==0.1.20",
|
||||
"govoplan-evaluation==0.1.20",
|
||||
"govoplan-facilities==0.1.20",
|
||||
"govoplan-fit-connect==0.1.20",
|
||||
"govoplan-forms==0.1.22",
|
||||
"govoplan-forms-runtime==0.1.19",
|
||||
"govoplan-grants==0.1.20",
|
||||
"govoplan-helpdesk==0.1.21",
|
||||
"govoplan-identity-trust==0.1.20",
|
||||
"govoplan-inspections==0.1.20",
|
||||
"govoplan-learning==0.1.20",
|
||||
"govoplan-mandates==0.1.19",
|
||||
"govoplan-notifications==0.1.19",
|
||||
"govoplan-parties==0.1.19",
|
||||
"govoplan-payments==0.1.21",
|
||||
"govoplan-permits==0.1.20",
|
||||
"govoplan-poll==0.1.20",
|
||||
"govoplan-portal==0.1.21",
|
||||
"govoplan-postbox==0.1.22",
|
||||
"govoplan-procurement==0.1.20",
|
||||
"govoplan-projects==0.1.19",
|
||||
"govoplan-quick-access==0.1.20",
|
||||
"govoplan-records==0.1.22",
|
||||
"govoplan-reporting==0.1.20",
|
||||
"govoplan-resources==0.1.20",
|
||||
"govoplan-rest==0.1.19",
|
||||
"govoplan-risk-compliance==0.1.20",
|
||||
"govoplan-scheduling==0.1.21",
|
||||
"govoplan-search==0.1.19",
|
||||
"govoplan-services==0.1.19",
|
||||
"govoplan-soap==0.1.19",
|
||||
"govoplan-tasks==0.1.21",
|
||||
"govoplan-templates==0.1.21",
|
||||
"govoplan-tickets==0.1.22",
|
||||
"govoplan-transparency==0.1.20",
|
||||
"govoplan-views==0.1.21",
|
||||
"govoplan-voting==0.1.20",
|
||||
"govoplan-wiki==0.1.22",
|
||||
"govoplan-workflow==0.1.22",
|
||||
"govoplan-workflow-engine==0.1.21",
|
||||
"govoplan-xrechnung==0.1.21",
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
@@ -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,47 @@ 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.
|
||||
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",
|
||||
|
||||
@@ -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"},
|
||||
|
||||
@@ -30,11 +30,14 @@
|
||||
-e ../govoplan-parties
|
||||
-e ../govoplan-mandates
|
||||
-e ../govoplan-decisions
|
||||
-e ../govoplan-payments
|
||||
-e ../govoplan-connectors
|
||||
-e ../govoplan-datasources
|
||||
-e ../govoplan-dataflow
|
||||
-e ../govoplan-workflow-engine
|
||||
-e ../govoplan-workflow
|
||||
-e ../govoplan-tasks
|
||||
-e ../govoplan-quick-access
|
||||
-e ../govoplan-views
|
||||
-e ../govoplan-voting
|
||||
-e ../govoplan-search
|
||||
|
||||
+15
-15
@@ -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.18
|
||||
govoplan-organizations @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-organizations.git@v0.1.18
|
||||
govoplan-identity @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-identity.git@v0.1.18
|
||||
govoplan-idm @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-idm.git@v0.1.18
|
||||
govoplan-access @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-access.git@v0.1.18
|
||||
govoplan-admin @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-admin.git@v0.1.18
|
||||
govoplan-policy @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-policy.git@v0.1.18
|
||||
govoplan-audit @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-audit.git@v0.1.18
|
||||
govoplan-dashboard @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-dashboard.git@v0.1.18
|
||||
govoplan-files @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-files.git@v0.1.18
|
||||
govoplan-mail @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-mail.git@v0.1.18
|
||||
govoplan-campaign @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-campaign.git@v0.1.18
|
||||
govoplan-calendar @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-calendar.git@v0.1.18
|
||||
govoplan-docs @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-docs.git@v0.1.18
|
||||
govoplan-ops @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-ops.git@v0.1.18
|
||||
govoplan-tenancy @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-tenancy.git@v0.1.20
|
||||
govoplan-organizations @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-organizations.git@v0.1.20
|
||||
govoplan-identity @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-identity.git@v0.1.20
|
||||
govoplan-idm @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-idm.git@v0.1.24
|
||||
govoplan-access @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-access.git@v0.1.23
|
||||
govoplan-admin @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-admin.git@v0.1.22
|
||||
govoplan-policy @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-policy.git@v0.1.22
|
||||
govoplan-audit @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-audit.git@v0.1.20
|
||||
govoplan-dashboard @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-dashboard.git@v0.1.20
|
||||
govoplan-files @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-files.git@v0.1.23
|
||||
govoplan-mail @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-mail.git@v0.1.25
|
||||
govoplan-campaign @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-campaign.git@v0.1.27
|
||||
govoplan-calendar @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-calendar.git@v0.1.22
|
||||
govoplan-docs @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-docs.git@v0.1.22
|
||||
govoplan-ops @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-ops.git@v0.1.20
|
||||
|
||||
+101
@@ -0,0 +1,101 @@
|
||||
{
|
||||
"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 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": [
|
||||
"Complete the digital journey with keyboard and screen reader at desktop and mobile widths.",
|
||||
"Complete the assisted operator journey with keyboard and screen reader 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."
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
|
||||
@@ -33,6 +33,10 @@ 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,
|
||||
)
|
||||
from govoplan_deploy.cluster_evidence import ( # noqa: E402
|
||||
collect_kubernetes_evidence,
|
||||
)
|
||||
@@ -414,6 +418,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 +485,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"]
|
||||
@@ -707,6 +748,10 @@ class DeploymentInstallerTests(unittest.TestCase):
|
||||
compose["services"]["load-balancer"]["ports"],
|
||||
)
|
||||
self.assertNotIn("ports", compose["services"]["web"])
|
||||
self.assertIn(
|
||||
"./infrastructure-capabilities.json:/etc/govoplan/deployment/infrastructure-capabilities.json:ro",
|
||||
compose["services"]["api"]["volumes"],
|
||||
)
|
||||
self.assertEqual(1, compose["services"]["api"]["scale"])
|
||||
self.assertEqual(1, compose["services"]["web"]["scale"])
|
||||
|
||||
@@ -932,6 +977,63 @@ 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_replica_counts_drive_compose_and_load_balancer_discovery(self) -> None:
|
||||
spec = default_spec(
|
||||
storage_mode="garage",
|
||||
@@ -1061,10 +1163,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 +1181,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 +1207,20 @@ 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
|
||||
)
|
||||
)
|
||||
|
||||
def test_secret_change_is_planned_without_exposing_secret_values(self) -> None:
|
||||
with tempfile.TemporaryDirectory(prefix="govoplan-deploy-test-") as directory:
|
||||
@@ -1464,6 +1589,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"):
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
DOCS = ROOT / "docs"
|
||||
MARKDOWN_LINK = re.compile(
|
||||
r"!?\[[^\n]*?\]\((?P<destination><[^>]+>|[^)\s]+)"
|
||||
)
|
||||
|
||||
|
||||
class DocumentationStructureTests(unittest.TestCase):
|
||||
def test_documentation_root_has_one_human_entry_point(self) -> None:
|
||||
self.assertEqual(
|
||||
[path.name for path in sorted(DOCS.glob("*.md"))],
|
||||
["README.md"],
|
||||
)
|
||||
|
||||
def test_documentation_front_doors_exist(self) -> None:
|
||||
expected = (
|
||||
DOCS / "strategy" / "PLATFORM_CORE_IDEAS.md",
|
||||
DOCS / "strategy" / "ROADMAP.md",
|
||||
DOCS / "strategy" / "STRATEGY_STATUS.md",
|
||||
DOCS / "strategy" / "REFERENCE_JOURNEY_PROGRAM.md",
|
||||
)
|
||||
self.assertFalse([path for path in expected if not path.is_file()])
|
||||
|
||||
def test_local_markdown_links_resolve(self) -> None:
|
||||
broken: list[str] = []
|
||||
sources = [ROOT / "README.md", *sorted(DOCS.rglob("*.md"))]
|
||||
for source in sources:
|
||||
for line_number, line in enumerate(
|
||||
source.read_text(encoding="utf-8").splitlines(),
|
||||
start=1,
|
||||
):
|
||||
for match in MARKDOWN_LINK.finditer(line):
|
||||
destination = match.group("destination")
|
||||
if destination.startswith("<") and destination.endswith(">"):
|
||||
destination = destination[1:-1]
|
||||
path_text = destination.split("#", 1)[0]
|
||||
if (
|
||||
not path_text
|
||||
or path_text.startswith(("/", "mailto:", "data:"))
|
||||
or "://" in path_text
|
||||
):
|
||||
continue
|
||||
target = (source.parent / path_text).resolve()
|
||||
if not target.is_relative_to(ROOT):
|
||||
continue
|
||||
if not target.exists():
|
||||
broken.append(
|
||||
f"{source.relative_to(ROOT)}:{line_number}: {destination}"
|
||||
)
|
||||
self.assertEqual(broken, [])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -2,6 +2,8 @@ from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, replace
|
||||
from datetime import UTC, datetime, timedelta
|
||||
import json
|
||||
from pathlib import Path
|
||||
import unittest
|
||||
|
||||
from sqlalchemy import create_engine
|
||||
@@ -66,6 +68,11 @@ from govoplan_services.backend.service import SqlServiceDefinitionProvider, reco
|
||||
|
||||
|
||||
NOW = datetime(2026, 8, 1, 10, 0, tzinfo=UTC)
|
||||
JOURNEY = json.loads(
|
||||
(Path(__file__).parent / "fixtures/resident_parking_permit_journey.json").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def _reference(
|
||||
@@ -177,31 +184,31 @@ class InstitutionalGovernanceJourneyTests(unittest.TestCase):
|
||||
service = ServiceDefinition(
|
||||
reference=_reference(
|
||||
"service",
|
||||
"permit-service",
|
||||
JOURNEY["service"]["object_id"],
|
||||
owner="services",
|
||||
version="5",
|
||||
version=JOURNEY["service"]["version"],
|
||||
),
|
||||
key="permit.apply",
|
||||
key=JOURNEY["service"]["key"],
|
||||
temporal=TemporalRevision(
|
||||
revision="5",
|
||||
revision=JOURNEY["service"]["version"],
|
||||
valid_from=NOW - timedelta(days=1),
|
||||
valid_to=NOW + timedelta(days=30),
|
||||
recorded_at=NOW - timedelta(days=2),
|
||||
change_reason="Service published.",
|
||||
),
|
||||
title="Apply for a permit",
|
||||
title=JOURNEY["title"],
|
||||
audience=("resident",),
|
||||
legal_bases=(legal_basis,),
|
||||
required_evidence_types=("application", "identity"),
|
||||
channels=("portal", "postbox"),
|
||||
required_evidence_types=tuple(JOURNEY["service"]["required_evidence_types"]),
|
||||
channels=tuple(JOURNEY["service"]["channels"]) + ("postbox",),
|
||||
responsible_organization_ref=organization,
|
||||
responsible_function_ref=function,
|
||||
mandate_ref=mandate_ref,
|
||||
jurisdiction_refs=(jurisdiction,),
|
||||
bindings=(
|
||||
ServiceBinding("case", "permit-application"),
|
||||
ServiceBinding("workflow", "workflow:permit-review"),
|
||||
ServiceBinding("result", "decision:permit"),
|
||||
ServiceBinding("case", JOURNEY["case"]["type_key"]),
|
||||
ServiceBinding("workflow", "workflow:resident-parking-permit-review"),
|
||||
ServiceBinding("result", f"decision:{JOURNEY['decision']['type']}"),
|
||||
),
|
||||
remedy_refs=("review:administrative-court",),
|
||||
publication_state="published",
|
||||
@@ -230,37 +237,40 @@ class InstitutionalGovernanceJourneyTests(unittest.TestCase):
|
||||
upsert_case_status(
|
||||
session,
|
||||
principal,
|
||||
status_key="intake",
|
||||
status_key=JOURNEY["case"]["initial_status"],
|
||||
label="Intake",
|
||||
)
|
||||
upsert_case_status(
|
||||
session,
|
||||
principal,
|
||||
status_key="decided",
|
||||
status_key=JOURNEY["case"]["decided_status"],
|
||||
label="Decided",
|
||||
category="decided",
|
||||
)
|
||||
upsert_case_type(
|
||||
session,
|
||||
principal,
|
||||
type_key="permit-application",
|
||||
label="Permit application",
|
||||
initial_status_key="intake",
|
||||
allowed_status_keys=("intake", "decided"),
|
||||
type_key=JOURNEY["case"]["type_key"],
|
||||
label=JOURNEY["title"],
|
||||
initial_status_key=JOURNEY["case"]["initial_status"],
|
||||
allowed_status_keys=(
|
||||
JOURNEY["case"]["initial_status"],
|
||||
JOURNEY["case"]["decided_status"],
|
||||
),
|
||||
)
|
||||
case_record = create_case_from_intake(
|
||||
session,
|
||||
principal,
|
||||
plan=intake,
|
||||
case_number="PERMIT-2026-0001",
|
||||
title="Permit application",
|
||||
status_key=None,
|
||||
case_number=JOURNEY["case"]["number"],
|
||||
title=JOURNEY["title"],
|
||||
status_key=JOURNEY["case"]["initial_status"],
|
||||
opened_at=NOW,
|
||||
recorded_at=NOW,
|
||||
change_reason="Portal application received.",
|
||||
idempotency_key="journey-case-create",
|
||||
evidence_refs=(application_evidence,),
|
||||
deadline_at=NOW + timedelta(days=30),
|
||||
deadline_at=NOW + timedelta(days=JOURNEY["case"]["deadline_days"]),
|
||||
)
|
||||
|
||||
applicant = _reference("party", "applicant", owner="parties")
|
||||
@@ -333,7 +343,7 @@ class InstitutionalGovernanceJourneyTests(unittest.TestCase):
|
||||
change_reason="Permit authority delegated.",
|
||||
),
|
||||
task_types=("committee.formal_decision",),
|
||||
authority_types=("permit",),
|
||||
authority_types=(JOURNEY["decision"]["type"],),
|
||||
organization_unit_refs=(organization,),
|
||||
function_refs=(function,),
|
||||
jurisdiction_refs=(jurisdiction,),
|
||||
@@ -380,7 +390,7 @@ class InstitutionalGovernanceJourneyTests(unittest.TestCase):
|
||||
object_id="item-1",
|
||||
revision=1,
|
||||
state="deliberating",
|
||||
title="Permit application",
|
||||
title=JOURNEY["title"],
|
||||
parent_id=meeting.object_id,
|
||||
recorded_at=NOW,
|
||||
change_reason="Agenda item entered deliberation.",
|
||||
@@ -398,7 +408,7 @@ class InstitutionalGovernanceJourneyTests(unittest.TestCase):
|
||||
object_id="vote-1",
|
||||
revision=1,
|
||||
state="closed",
|
||||
title="Vote on permit application",
|
||||
title=f"Vote on {JOURNEY['title'].lower()}",
|
||||
parent_id=agenda.object_id,
|
||||
recorded_at=NOW,
|
||||
change_reason="Vote result accepted.",
|
||||
@@ -445,7 +455,7 @@ class InstitutionalGovernanceJourneyTests(unittest.TestCase):
|
||||
effective_at=NOW,
|
||||
meeting_ref="meeting-1",
|
||||
agenda_item_ref="item-1",
|
||||
decision_type="permit",
|
||||
decision_type=JOURNEY["decision"]["type"],
|
||||
subject_refs=(case_record.reference,),
|
||||
organization_unit_ref=organization,
|
||||
function_ref=function,
|
||||
@@ -461,8 +471,8 @@ class InstitutionalGovernanceJourneyTests(unittest.TestCase):
|
||||
),
|
||||
fact_evidence=(application_evidence, address_evidence),
|
||||
legal_bases=(legal_basis,),
|
||||
operative_result="Permit granted.",
|
||||
reasoning="The application satisfies the effective rule.",
|
||||
operative_result=JOURNEY["decision"]["operative_result"],
|
||||
reasoning=JOURNEY["decision"]["reasoning"],
|
||||
case_ref=case_record.reference,
|
||||
jurisdiction_refs=(jurisdiction,),
|
||||
party_refs=(applicant, representative),
|
||||
@@ -475,7 +485,7 @@ class InstitutionalGovernanceJourneyTests(unittest.TestCase):
|
||||
classification="restricted",
|
||||
purposes=("permit-decision", "party-delivery"),
|
||||
legal_basis_refs=("permit-law:3@2026-01",),
|
||||
retention_policy_ref="records:permit",
|
||||
retention_policy_ref=JOURNEY["records"]["retention_policy_ref"],
|
||||
disclosure_state="partly_disclosable",
|
||||
),
|
||||
),
|
||||
@@ -568,7 +578,7 @@ class InstitutionalGovernanceJourneyTests(unittest.TestCase):
|
||||
get_case(session, principal, case_id="case-1"),
|
||||
)
|
||||
self.assertEqual(
|
||||
"decided",
|
||||
JOURNEY["case"]["decided_status"],
|
||||
get_workspace_object(
|
||||
session,
|
||||
principal,
|
||||
@@ -583,7 +593,7 @@ class InstitutionalGovernanceJourneyTests(unittest.TestCase):
|
||||
self.assertEqual("confirmed", reconstruction["observed_effects"][0]["state"])
|
||||
self.assertEqual("audit:delivery-1", reconstruction["observed_effects"][0]["audit_event_refs"][0])
|
||||
self.assertEqual("application-1", reconstruction["fact_evidence"][0]["evidence_id"])
|
||||
self.assertEqual("The application satisfies the effective rule.", reconstruction["reasoning"])
|
||||
self.assertEqual(JOURNEY["decision"]["reasoning"], reconstruction["reasoning"])
|
||||
self.assertEqual("review:administrative-court", reconstruction["review_refs"][0])
|
||||
|
||||
|
||||
|
||||
@@ -2,14 +2,21 @@ from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from datetime import UTC, datetime, timedelta
|
||||
import json
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
from urllib.parse import parse_qs, urlparse
|
||||
import unittest
|
||||
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy.orm import Session, sessionmaker
|
||||
|
||||
from govoplan_core.auth import ApiPrincipal
|
||||
from govoplan_core.core.access import PrincipalRef
|
||||
from govoplan_core.core.institutional import (
|
||||
CAPABILITY_FORM_DEFINITIONS,
|
||||
CAPABILITY_SERVICE_DEFINITIONS,
|
||||
EvidenceReference,
|
||||
FormDefinition,
|
||||
FormFieldDefinition,
|
||||
InstitutionalReference,
|
||||
@@ -18,6 +25,22 @@ from govoplan_core.core.institutional import (
|
||||
TemporalRevision,
|
||||
service_launch_capability,
|
||||
)
|
||||
from govoplan_core.core.notifications import CAPABILITY_NOTIFICATIONS_DISPATCH
|
||||
from govoplan_core.core.payments import (
|
||||
ManualPaymentReconciliationCommand,
|
||||
PaymentRequestCommand,
|
||||
)
|
||||
from govoplan_core.core.runtime_coordination import (
|
||||
DistributedLease,
|
||||
RuntimeIdentity,
|
||||
bind_process_runtime_identity,
|
||||
)
|
||||
from govoplan_core.core.tasks import (
|
||||
RegisteredWorkItemProvider,
|
||||
WorkItemProviderRegistration,
|
||||
WorkItemQuery,
|
||||
)
|
||||
from govoplan_core.core.recovery import RecoveryCheckpoint, RecoveryOperation
|
||||
from govoplan_cases.backend.service_intake import (
|
||||
CAPABILITY_CASES_SERVICE_INTAKE,
|
||||
CaseServiceIntake,
|
||||
@@ -28,18 +51,66 @@ from govoplan_forms.backend.service import (
|
||||
record_form_definition,
|
||||
)
|
||||
from govoplan_forms_runtime.backend.db.models import (
|
||||
FormAssistedConfirmation,
|
||||
FormInstanceEvent,
|
||||
FormInstanceIdentity,
|
||||
FormInstanceRevision,
|
||||
FormIntakeProfile,
|
||||
FormIntakeSession,
|
||||
FormStatusAccessGrant,
|
||||
FormStatusAccessPolicy,
|
||||
FormStatusAccessToken,
|
||||
)
|
||||
from govoplan_forms_runtime.backend.intake import FormIntakeService
|
||||
from govoplan_forms_runtime.backend.service import (
|
||||
FormRuntimeError,
|
||||
FormRuntimeService,
|
||||
FormsServiceLauncher,
|
||||
)
|
||||
from govoplan_forms_runtime.backend.status_access import FormStatusAccessService
|
||||
from govoplan_portal.backend.service_directory import PortalServiceDirectory
|
||||
from govoplan_payments.backend.db.models import (
|
||||
PaymentEvent,
|
||||
PaymentObligation,
|
||||
PaymentReconciliation,
|
||||
)
|
||||
from govoplan_payments.backend.service import SqlPaymentRequestProvider
|
||||
from govoplan_tasks.backend.aggregation import aggregate_work_items
|
||||
from govoplan_workflow_engine.backend.db.models import (
|
||||
WorkflowDefinition,
|
||||
WorkflowDefinitionRevision,
|
||||
WorkflowInstance,
|
||||
WorkflowInstanceEvent,
|
||||
WorkflowInstanceStep,
|
||||
WorkflowTrigger,
|
||||
WorkflowTriggerDelivery,
|
||||
WorkflowWaitState,
|
||||
)
|
||||
from govoplan_workflow_engine.backend.instance_service import (
|
||||
resolve_step,
|
||||
start_instance,
|
||||
)
|
||||
from govoplan_workflow_engine.backend.schemas import (
|
||||
WorkflowDefinitionCreateRequest,
|
||||
WorkflowEdge,
|
||||
WorkflowGraph,
|
||||
WorkflowInstanceStartRequest,
|
||||
WorkflowNode,
|
||||
WorkflowStepActionRequest,
|
||||
)
|
||||
from govoplan_workflow_engine.backend.service import (
|
||||
activate_definition,
|
||||
create_definition,
|
||||
)
|
||||
from govoplan_workflow_engine.backend.work_items import WorkflowWorkItemProvider
|
||||
|
||||
|
||||
NOW = datetime(2026, 8, 1, 10, 0, tzinfo=UTC)
|
||||
JOURNEY = json.loads(
|
||||
(Path(__file__).parent / "fixtures/resident_parking_permit_journey.json").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def _service() -> ServiceDefinition:
|
||||
@@ -47,24 +118,24 @@ def _service() -> ServiceDefinition:
|
||||
reference=InstitutionalReference(
|
||||
kind="service",
|
||||
owner_module="portal",
|
||||
object_id="permit",
|
||||
object_id=JOURNEY["service"]["object_id"],
|
||||
tenant_id="tenant-1",
|
||||
version="5",
|
||||
version=JOURNEY["service"]["version"],
|
||||
),
|
||||
key="permit.apply",
|
||||
key=JOURNEY["service"]["key"],
|
||||
temporal=TemporalRevision(
|
||||
revision="5",
|
||||
revision=JOURNEY["service"]["version"],
|
||||
valid_from=NOW - timedelta(days=1),
|
||||
valid_to=NOW + timedelta(days=1),
|
||||
recorded_at=NOW - timedelta(days=2),
|
||||
),
|
||||
title="Apply for a permit",
|
||||
audience=("resident",),
|
||||
required_evidence_types=("application",),
|
||||
title=JOURNEY["title"],
|
||||
audience=(JOURNEY["service"]["audience"],),
|
||||
required_evidence_types=tuple(JOURNEY["service"]["required_evidence_types"]),
|
||||
bindings=(
|
||||
ServiceBinding("capability", CAPABILITY_CASES_SERVICE_INTAKE),
|
||||
ServiceBinding("case", "permit-application"),
|
||||
ServiceBinding("workflow", "workflow:permit-review"),
|
||||
ServiceBinding("case", JOURNEY["case"]["type_key"]),
|
||||
ServiceBinding("workflow", "workflow:resident-parking-permit-review"),
|
||||
),
|
||||
publication_state="published",
|
||||
)
|
||||
@@ -74,10 +145,14 @@ class _Provider:
|
||||
def __init__(self, definition: ServiceDefinition) -> None:
|
||||
self.definition = definition
|
||||
|
||||
def get_service_definition(self, session, principal, *, reference, effective_at=None):
|
||||
def get_service_definition(
|
||||
self, session, principal, *, reference, effective_at=None
|
||||
):
|
||||
return self.definition
|
||||
|
||||
def list_service_definitions(self, session, principal, *, tenant_id, query="", limit=100):
|
||||
def list_service_definitions(
|
||||
self, session, principal, *, tenant_id, query="", limit=100
|
||||
):
|
||||
return (self.definition,)
|
||||
|
||||
|
||||
@@ -111,15 +186,76 @@ class _Principal:
|
||||
class _FormRegistry(_Registry):
|
||||
def __init__(self, definition: ServiceDefinition) -> None:
|
||||
super().__init__(definition)
|
||||
self.capabilities[CAPABILITY_FORM_DEFINITIONS] = (
|
||||
SqlFormDefinitionProvider()
|
||||
)
|
||||
self.capabilities[service_launch_capability("form")] = (
|
||||
FormsServiceLauncher(self)
|
||||
self.capabilities[CAPABILITY_FORM_DEFINITIONS] = SqlFormDefinitionProvider()
|
||||
self.capabilities[service_launch_capability("form")] = FormsServiceLauncher(
|
||||
self
|
||||
)
|
||||
self.notifications = _NotificationProvider()
|
||||
self.capabilities[CAPABILITY_NOTIFICATIONS_DISPATCH] = self.notifications
|
||||
|
||||
def has(self, module_id: str) -> bool:
|
||||
return module_id in {"portal", "forms", "forms_runtime"}
|
||||
return module_id in {"portal", "forms", "forms_runtime", "notifications"}
|
||||
|
||||
|
||||
class _NotificationProvider:
|
||||
def __init__(self) -> None:
|
||||
self.requests: list[object] = []
|
||||
|
||||
def tenant_id_for_notification(self, session, *, notification_id):
|
||||
return "tenant-1"
|
||||
|
||||
def enqueue_notification(self, session, request, *, enqueue_delivery=True):
|
||||
self.requests.append(request)
|
||||
return {"id": f"notification-{len(self.requests)}"}
|
||||
|
||||
def deliver_notification(self, session, *, notification_id):
|
||||
return {"id": notification_id}
|
||||
|
||||
def deliver_pending(self, session, *, tenant_id=None, limit=50):
|
||||
return {"delivered": 0}
|
||||
|
||||
|
||||
class _WorkflowTaskRegistry:
|
||||
def __init__(self) -> None:
|
||||
self.provider = WorkflowWorkItemProvider(registry=self)
|
||||
self.registered = RegisteredWorkItemProvider(
|
||||
module_id="workflow_engine",
|
||||
registration=WorkItemProviderRegistration(
|
||||
id="workflow_engine.handoffs",
|
||||
factory=lambda _context: self.provider,
|
||||
order=20,
|
||||
),
|
||||
)
|
||||
|
||||
def has_capability(self, _name: str) -> bool:
|
||||
return False
|
||||
|
||||
def capability(self, name: str) -> object:
|
||||
raise KeyError(name)
|
||||
|
||||
def work_item_providers(self):
|
||||
return ((self.registered, self.provider),)
|
||||
|
||||
|
||||
def _workflow_principal() -> ApiPrincipal:
|
||||
return ApiPrincipal(
|
||||
principal=PrincipalRef(
|
||||
account_id="account-1",
|
||||
membership_id="membership-1",
|
||||
tenant_id="tenant-1",
|
||||
scopes=frozenset(
|
||||
{
|
||||
"tasks:item:read",
|
||||
"workflow:definition:read",
|
||||
"workflow:instance:read",
|
||||
"workflow:instance:start",
|
||||
"workflow:instance:transition",
|
||||
}
|
||||
),
|
||||
),
|
||||
account=SimpleNamespace(id="account-1"),
|
||||
user=SimpleNamespace(id="membership-1"),
|
||||
)
|
||||
|
||||
|
||||
class InstitutionalServiceJourneyTests(unittest.TestCase):
|
||||
@@ -143,8 +279,16 @@ class InstitutionalServiceJourneyTests(unittest.TestCase):
|
||||
self.assertTrue(entries[0].available)
|
||||
self.assertIs(definition, entries[0].definition)
|
||||
self.assertEqual(definition.reference, plan.service_ref)
|
||||
self.assertEqual("5", plan.context.service_ref.version)
|
||||
self.assertEqual("workflow:permit-review", plan.workflow_refs[0])
|
||||
self.assertEqual(JOURNEY["service"]["version"], plan.context.service_ref.version)
|
||||
self.assertEqual("workflow:resident-parking-permit-review", plan.workflow_refs[0])
|
||||
|
||||
def test_reference_fixture_names_remaining_manual_target_evidence(self) -> None:
|
||||
self.assertEqual("Anwohnerparkausweis", JOURNEY["title_de"])
|
||||
self.assertEqual("de-DE", JOURNEY["locale"])
|
||||
self.assertEqual("email_link", JOURNEY["status_access"]["mode"])
|
||||
self.assertEqual("manual", JOURNEY["payment"]["mode"])
|
||||
self.assertEqual(8, len(JOURNEY["acceptance"]["automated"]))
|
||||
self.assertEqual(6, len(JOURNEY["acceptance"]["manual_or_target"]))
|
||||
|
||||
def test_portal_launches_exact_form_revision_and_persists_submission(self) -> None:
|
||||
engine = create_engine("sqlite+pysqlite:///:memory:")
|
||||
@@ -165,19 +309,19 @@ class InstitutionalServiceJourneyTests(unittest.TestCase):
|
||||
reference=InstitutionalReference(
|
||||
kind="form",
|
||||
owner_module="forms",
|
||||
object_id="permit-application",
|
||||
object_id=JOURNEY["form"]["object_id"],
|
||||
tenant_id="tenant-1",
|
||||
version="3",
|
||||
),
|
||||
key="permit-application",
|
||||
key=JOURNEY["form"]["object_id"],
|
||||
temporal=TemporalRevision(
|
||||
revision="3",
|
||||
valid_from=NOW - timedelta(days=1),
|
||||
valid_to=NOW + timedelta(days=1),
|
||||
recorded_at=NOW - timedelta(days=2),
|
||||
change_reason="Publish the permit application.",
|
||||
change_reason="Publish the resident parking permit application.",
|
||||
),
|
||||
title="Permit application",
|
||||
title=JOURNEY["title"],
|
||||
fields=(
|
||||
FormFieldDefinition(
|
||||
key="applicant_name",
|
||||
@@ -185,6 +329,25 @@ class InstitutionalServiceJourneyTests(unittest.TestCase):
|
||||
required=True,
|
||||
constraints={"min_length": 2},
|
||||
),
|
||||
FormFieldDefinition(
|
||||
key="applicant_email",
|
||||
label="Applicant email",
|
||||
value_type="email",
|
||||
required=True,
|
||||
constraints={"min_length": 5},
|
||||
),
|
||||
FormFieldDefinition(
|
||||
key="residence_address",
|
||||
label="Primary residence address",
|
||||
required=True,
|
||||
constraints={"min_length": 5},
|
||||
),
|
||||
FormFieldDefinition(
|
||||
key="licence_plate",
|
||||
label="Vehicle licence plate",
|
||||
required=True,
|
||||
constraints={"min_length": 3},
|
||||
),
|
||||
),
|
||||
publication_state="published",
|
||||
allow_drafts=True,
|
||||
@@ -199,18 +362,18 @@ class InstitutionalServiceJourneyTests(unittest.TestCase):
|
||||
reference=InstitutionalReference(
|
||||
kind="service",
|
||||
owner_module="services",
|
||||
object_id="permit",
|
||||
object_id=JOURNEY["service"]["object_id"],
|
||||
tenant_id="tenant-1",
|
||||
version="6",
|
||||
version=JOURNEY["service"]["version"],
|
||||
),
|
||||
key="permit.apply",
|
||||
key=JOURNEY["service"]["key"],
|
||||
temporal=TemporalRevision(
|
||||
revision="6",
|
||||
revision=JOURNEY["service"]["version"],
|
||||
valid_from=NOW - timedelta(days=1),
|
||||
valid_to=NOW + timedelta(days=1),
|
||||
recorded_at=NOW - timedelta(days=2),
|
||||
),
|
||||
title="Apply for a permit",
|
||||
title=JOURNEY["title"],
|
||||
audience=("public",),
|
||||
bindings=(binding,),
|
||||
publication_state="published",
|
||||
@@ -224,7 +387,7 @@ class InstitutionalServiceJourneyTests(unittest.TestCase):
|
||||
reference=service.reference,
|
||||
requested_at=NOW,
|
||||
idempotency_key="portal-form-launch-1",
|
||||
parameters={"applicant_name": "Ada Lovelace"},
|
||||
parameters=JOURNEY["form"]["fields"],
|
||||
)
|
||||
replay = directory.launch_service(
|
||||
session,
|
||||
@@ -232,7 +395,7 @@ class InstitutionalServiceJourneyTests(unittest.TestCase):
|
||||
reference=service.reference,
|
||||
requested_at=NOW,
|
||||
idempotency_key="portal-form-launch-1",
|
||||
parameters={"applicant_name": "Ada Lovelace"},
|
||||
parameters=JOURNEY["form"]["fields"],
|
||||
)
|
||||
instance = FormRuntimeService(registry).get_instance(
|
||||
session,
|
||||
@@ -261,7 +424,440 @@ class InstitutionalServiceJourneyTests(unittest.TestCase):
|
||||
)
|
||||
self.assertEqual(NOW, instance.definition_ref.valid_at)
|
||||
self.assertEqual(service.reference, instance.service_ref)
|
||||
self.assertEqual("Ada Lovelace", instance.values["applicant_name"])
|
||||
self.assertEqual(JOURNEY["form"]["fields"], instance.values)
|
||||
finally:
|
||||
session.close()
|
||||
engine.dispose()
|
||||
|
||||
def test_assisted_intake_reuses_exact_form_and_persists_readback_provenance(
|
||||
self,
|
||||
) -> None:
|
||||
engine = create_engine("sqlite+pysqlite:///:memory:")
|
||||
for table in (
|
||||
FormDefinitionRevision.__table__,
|
||||
FormInstanceIdentity.__table__,
|
||||
FormInstanceRevision.__table__,
|
||||
FormInstanceEvent.__table__,
|
||||
FormIntakeProfile.__table__,
|
||||
FormIntakeSession.__table__,
|
||||
FormAssistedConfirmation.__table__,
|
||||
FormStatusAccessPolicy.__table__,
|
||||
FormStatusAccessGrant.__table__,
|
||||
FormStatusAccessToken.__table__,
|
||||
):
|
||||
table.create(engine)
|
||||
sessions = sessionmaker(bind=engine)
|
||||
principal = _Principal()
|
||||
assisted = JOURNEY["assisted_intake"]
|
||||
try:
|
||||
with sessions() as session:
|
||||
form = record_form_definition(
|
||||
session,
|
||||
principal,
|
||||
definition=FormDefinition(
|
||||
reference=InstitutionalReference(
|
||||
kind="form",
|
||||
owner_module="forms",
|
||||
object_id=JOURNEY["form"]["object_id"],
|
||||
tenant_id="tenant-1",
|
||||
version=JOURNEY["form"]["version"],
|
||||
),
|
||||
key=JOURNEY["form"]["object_id"],
|
||||
temporal=TemporalRevision(
|
||||
revision=JOURNEY["form"]["version"],
|
||||
recorded_at=NOW - timedelta(days=2),
|
||||
change_reason="Publish the resident parking permit application.",
|
||||
),
|
||||
title=JOURNEY["title"],
|
||||
fields=tuple(
|
||||
FormFieldDefinition(
|
||||
key=key,
|
||||
label=key.replace("_", " ").title(),
|
||||
value_type=(
|
||||
"email" if key == "applicant_email" else "text"
|
||||
),
|
||||
required=True,
|
||||
constraints={"min_length": 2},
|
||||
)
|
||||
for key in JOURNEY["form"]["fields"]
|
||||
),
|
||||
publication_state="published",
|
||||
allow_drafts=True,
|
||||
handoff_kinds=("case",),
|
||||
),
|
||||
)
|
||||
registry = _FormRegistry(_service())
|
||||
status_access = JOURNEY["status_access"]
|
||||
FormStatusAccessService(registry).upsert_policy(
|
||||
session,
|
||||
principal,
|
||||
definition_ref=form.reference,
|
||||
mode=status_access["mode"],
|
||||
enabled=True,
|
||||
email_field_key=status_access["email_field_key"],
|
||||
token_ttl_seconds=status_access["token_ttl_seconds"],
|
||||
request_limit_per_hour=status_access[
|
||||
"request_limit_per_hour"
|
||||
],
|
||||
recorded_at=NOW,
|
||||
)
|
||||
intake = FormIntakeService(registry)
|
||||
profile = intake.create_profile(
|
||||
session,
|
||||
principal,
|
||||
definition_ref=form.reference,
|
||||
mode="assisted",
|
||||
custodian_ref=assisted["responsible_function_ref"],
|
||||
recorded_at=NOW,
|
||||
)
|
||||
started = intake.start_assisted(
|
||||
session,
|
||||
principal,
|
||||
profile_id=profile.profile_id,
|
||||
values=JOURNEY["form"]["fields"],
|
||||
channel=assisted["channel"],
|
||||
affected_party_ref=assisted["affected_party_ref"],
|
||||
represented_party_ref=assisted["represented_party_ref"],
|
||||
authority_basis=assisted["authority_basis"],
|
||||
purpose=assisted["purpose"],
|
||||
legal_basis_ref=assisted["legal_basis_ref"],
|
||||
consent_basis=assisted["consent_basis"],
|
||||
notice_given=assisted["notice_given"],
|
||||
responsible_function_ref=assisted["responsible_function_ref"],
|
||||
language=assisted["language"],
|
||||
accessibility_needs=assisted["accessibility_needs"],
|
||||
field_sources={
|
||||
key: {
|
||||
"source": "person_statement",
|
||||
"confidence": "stated",
|
||||
"declared_by_ref": assisted["affected_party_ref"],
|
||||
}
|
||||
for key in JOURNEY["form"]["fields"]
|
||||
},
|
||||
idempotency_key="resident-permit-assisted-start",
|
||||
recorded_at=NOW + timedelta(minutes=1),
|
||||
)
|
||||
self.assertEqual(form.reference, started.instance.definition_ref)
|
||||
self.assertEqual(JOURNEY["form"]["fields"], started.instance.values)
|
||||
self.assertEqual(
|
||||
assisted["purpose"], started.instance.metadata["intake"]["purpose"]
|
||||
)
|
||||
instance_id = started.instance.instance_id
|
||||
session.commit()
|
||||
|
||||
with sessions() as resumed:
|
||||
runtime = FormRuntimeService(registry)
|
||||
current = runtime.get_instance(
|
||||
resumed,
|
||||
principal,
|
||||
instance_id=instance_id,
|
||||
)
|
||||
self.assertIsNotNone(current)
|
||||
with self.assertRaisesRegex(FormRuntimeError, "read-back confirmation"):
|
||||
runtime.submit_instance(
|
||||
resumed,
|
||||
principal,
|
||||
instance_id=instance_id,
|
||||
expected_revision=current.revision,
|
||||
values=current.values,
|
||||
attachment_refs=(),
|
||||
signature_refs=(),
|
||||
idempotency_key="resident-permit-assisted-unconfirmed",
|
||||
recorded_at=NOW + timedelta(minutes=2),
|
||||
)
|
||||
confirmation = FormIntakeService(registry).record_assisted_confirmation(
|
||||
resumed,
|
||||
principal,
|
||||
instance_id=instance_id,
|
||||
expected_revision=current.revision,
|
||||
values=current.values,
|
||||
attachment_refs=(),
|
||||
signature_refs=(),
|
||||
outcome=assisted["confirmation_outcome"],
|
||||
method=assisted["confirmation_method"],
|
||||
confirmed_by_ref=assisted["affected_party_ref"],
|
||||
confirmed_at=NOW + timedelta(minutes=3),
|
||||
idempotency_key="resident-permit-assisted-readback",
|
||||
field_sources={
|
||||
key: {
|
||||
"source": "person_statement",
|
||||
"confidence": "stated",
|
||||
"declared_by_ref": assisted["affected_party_ref"],
|
||||
}
|
||||
for key in JOURNEY["form"]["fields"]
|
||||
},
|
||||
)
|
||||
submitted = runtime.submit_instance(
|
||||
resumed,
|
||||
principal,
|
||||
instance_id=instance_id,
|
||||
expected_revision=current.revision,
|
||||
values=current.values,
|
||||
attachment_refs=(),
|
||||
signature_refs=(),
|
||||
idempotency_key="resident-permit-assisted-submit",
|
||||
recorded_at=NOW + timedelta(minutes=4),
|
||||
)
|
||||
resumed.commit()
|
||||
self.assertEqual("submitted", submitted.status)
|
||||
self.assertEqual(current.revision, confirmation.instance_revision)
|
||||
self.assertEqual(assisted["affected_party_ref"], confirmation.confirmed_by_ref)
|
||||
status_service = FormStatusAccessService(registry)
|
||||
access = status_service.access_summary_for_instance(
|
||||
resumed,
|
||||
tenant_id="tenant-1",
|
||||
instance_id=instance_id,
|
||||
)
|
||||
self.assertIsNotNone(access)
|
||||
tracking_id = str(access["tracking_id"])
|
||||
challenge = status_service.public_access_challenge(
|
||||
resumed,
|
||||
tracking_id=tracking_id,
|
||||
)
|
||||
self.assertEqual("email_link", challenge["mode"])
|
||||
self.assertFalse(
|
||||
status_service.request_email_link(
|
||||
resumed,
|
||||
tracking_id=tracking_id,
|
||||
email="wrong@example.test",
|
||||
requested_at=NOW + timedelta(minutes=5),
|
||||
)
|
||||
)
|
||||
self.assertTrue(
|
||||
status_service.request_email_link(
|
||||
resumed,
|
||||
tracking_id=tracking_id,
|
||||
email=JOURNEY["form"]["fields"]["applicant_email"],
|
||||
requested_at=NOW + timedelta(minutes=6),
|
||||
)
|
||||
)
|
||||
notification = registry.notifications.requests[-1]
|
||||
query = parse_qs(urlparse(notification.action_url).query)
|
||||
projection = status_service.get_public_projection(
|
||||
resumed,
|
||||
tracking_id=tracking_id,
|
||||
token=query["token"][0],
|
||||
observed_at=NOW + timedelta(minutes=7),
|
||||
)
|
||||
self.assertEqual("submitted", projection["status"])
|
||||
self.assertEqual(JOURNEY["title"], projection["title"])
|
||||
self.assertEqual(
|
||||
["submitted"],
|
||||
[item["status"] for item in projection["timeline"]],
|
||||
)
|
||||
self.assertNotIn("values", projection)
|
||||
finally:
|
||||
engine.dispose()
|
||||
|
||||
def test_workflow_handoff_survives_session_reopen_and_projects_into_tasks(
|
||||
self,
|
||||
) -> None:
|
||||
engine = create_engine("sqlite+pysqlite:///:memory:")
|
||||
tables = (
|
||||
DistributedLease.__table__,
|
||||
RecoveryOperation.__table__,
|
||||
RecoveryCheckpoint.__table__,
|
||||
WorkflowDefinition.__table__,
|
||||
WorkflowDefinitionRevision.__table__,
|
||||
WorkflowInstance.__table__,
|
||||
WorkflowInstanceStep.__table__,
|
||||
WorkflowInstanceEvent.__table__,
|
||||
WorkflowTrigger.__table__,
|
||||
WorkflowTriggerDelivery.__table__,
|
||||
WorkflowWaitState.__table__,
|
||||
)
|
||||
for table in tables:
|
||||
table.create(engine)
|
||||
sessions = sessionmaker(bind=engine)
|
||||
registry = _WorkflowTaskRegistry()
|
||||
principal = _workflow_principal()
|
||||
bind_process_runtime_identity(
|
||||
RuntimeIdentity(
|
||||
installation_id="service-journey",
|
||||
node_id="journey-node",
|
||||
incarnation="journey-run",
|
||||
role="web",
|
||||
software_version="test",
|
||||
composition_hash="c" * 64,
|
||||
)
|
||||
)
|
||||
try:
|
||||
with sessions() as session:
|
||||
definition = create_definition(
|
||||
session,
|
||||
tenant_id="tenant-1",
|
||||
actor_id="account-1",
|
||||
payload=WorkflowDefinitionCreateRequest(
|
||||
name=JOURNEY["workflow"]["definition_name"],
|
||||
graph=WorkflowGraph(
|
||||
nodes=[
|
||||
WorkflowNode(
|
||||
id="start",
|
||||
type="workflow.start.manual",
|
||||
),
|
||||
WorkflowNode(
|
||||
id="review",
|
||||
type="workflow.activity",
|
||||
config={
|
||||
"title": JOURNEY["workflow"]["work_item_title"],
|
||||
"instructions": JOURNEY["workflow"]["instructions"],
|
||||
"assignee": "account:account-1",
|
||||
"due_after": "2d",
|
||||
},
|
||||
),
|
||||
WorkflowNode(
|
||||
id="done",
|
||||
type="workflow.end.completed",
|
||||
),
|
||||
],
|
||||
edges=[
|
||||
WorkflowEdge(
|
||||
id="start-review",
|
||||
source="start",
|
||||
target="review",
|
||||
),
|
||||
WorkflowEdge(
|
||||
id="review-done",
|
||||
source="review",
|
||||
target="done",
|
||||
),
|
||||
],
|
||||
),
|
||||
execution_mode="guided",
|
||||
),
|
||||
)
|
||||
activate_definition(
|
||||
session,
|
||||
tenant_id="tenant-1",
|
||||
definition_id=definition.id,
|
||||
actor_id="account-1",
|
||||
)
|
||||
instance, replayed = start_instance(
|
||||
session,
|
||||
tenant_id="tenant-1",
|
||||
definition_id=definition.id,
|
||||
actor_id="account-1",
|
||||
principal=principal,
|
||||
registry=registry,
|
||||
payload=WorkflowInstanceStartRequest(
|
||||
idempotency_key="permit-decision-1",
|
||||
input={"case_id": "case-1"},
|
||||
correlation_id="case-1",
|
||||
),
|
||||
)
|
||||
self.assertFalse(replayed)
|
||||
session.commit()
|
||||
instance_id = instance.id
|
||||
step_id = instance.current_step_id
|
||||
|
||||
with sessions() as reopened:
|
||||
work = aggregate_work_items(
|
||||
registry,
|
||||
reopened,
|
||||
principal,
|
||||
query=WorkItemQuery(tenant_id="tenant-1"),
|
||||
)
|
||||
self.assertEqual(1, work.total)
|
||||
self.assertEqual(step_id, work.items[0].id)
|
||||
self.assertEqual(
|
||||
JOURNEY["workflow"]["work_item_title"],
|
||||
work.items[0].title,
|
||||
)
|
||||
self.assertTrue(work.items[0].action_url.startswith("/workflow?"))
|
||||
self.assertIn(f"run={instance_id}", work.items[0].action_url)
|
||||
|
||||
resolve_step(
|
||||
reopened,
|
||||
tenant_id="tenant-1",
|
||||
instance_id=instance_id,
|
||||
step_id=step_id,
|
||||
actor_id="account-1",
|
||||
principal=principal,
|
||||
registry=registry,
|
||||
payload=WorkflowStepActionRequest(action="complete"),
|
||||
)
|
||||
reopened.commit()
|
||||
|
||||
with sessions() as verified:
|
||||
self.assertEqual(
|
||||
0,
|
||||
aggregate_work_items(
|
||||
registry,
|
||||
verified,
|
||||
principal,
|
||||
query=WorkItemQuery(tenant_id="tenant-1"),
|
||||
).total,
|
||||
)
|
||||
finally:
|
||||
bind_process_runtime_identity(None)
|
||||
engine.dispose()
|
||||
|
||||
def test_case_bound_payment_handoff_is_replay_safe_and_evidence_bound(
|
||||
self,
|
||||
) -> None:
|
||||
engine = create_engine("sqlite+pysqlite:///:memory:")
|
||||
for table in (
|
||||
PaymentObligation.__table__,
|
||||
PaymentReconciliation.__table__,
|
||||
PaymentEvent.__table__,
|
||||
):
|
||||
table.create(engine)
|
||||
session = Session(engine)
|
||||
provider = SqlPaymentRequestProvider()
|
||||
payment = JOURNEY["payment"]
|
||||
try:
|
||||
command = PaymentRequestCommand(
|
||||
tenant_id="tenant-1",
|
||||
source_module="cases",
|
||||
source_resource_type="case",
|
||||
source_resource_id="case-1",
|
||||
amount_minor=payment["amount_minor"],
|
||||
currency=payment["currency"],
|
||||
subject=payment["subject"],
|
||||
idempotency_key="resident-permit-case-1-fee",
|
||||
requested_at=NOW + timedelta(days=1),
|
||||
requested_by_ref="workflow:resident-parking-permit-review",
|
||||
due_at=NOW + timedelta(days=1 + payment["due_days"]),
|
||||
context_refs={
|
||||
"case": "case-1",
|
||||
"workflow": "workflow:resident-parking-permit-review",
|
||||
},
|
||||
)
|
||||
requested = provider.request_payment(session, command)
|
||||
replay = provider.request_payment(session, command)
|
||||
self.assertEqual(requested["payment_id"], replay["payment_id"])
|
||||
self.assertTrue(replay["replayed"])
|
||||
self.assertEqual("case-1", requested["source"]["resource_id"])
|
||||
|
||||
paid = provider.reconcile_manual_payment(
|
||||
session,
|
||||
ManualPaymentReconciliationCommand(
|
||||
tenant_id="tenant-1",
|
||||
payment_id=str(requested["payment_id"]),
|
||||
amount_minor=payment["amount_minor"],
|
||||
currency=payment["currency"],
|
||||
transaction_reference="BANK-RPP-2026-0001",
|
||||
evidence_ref=EvidenceReference(
|
||||
kind="document",
|
||||
owner_module=payment["evidence_owner"],
|
||||
evidence_id="file-payment-rpp-1",
|
||||
tenant_id="tenant-1",
|
||||
version="1",
|
||||
checksum="b" * 64,
|
||||
),
|
||||
idempotency_key="resident-permit-bank-receipt-1",
|
||||
received_at=NOW + timedelta(days=2),
|
||||
recorded_at=NOW + timedelta(days=2, minutes=5),
|
||||
recorded_by_ref="account:payment-officer-1",
|
||||
),
|
||||
)
|
||||
session.commit()
|
||||
self.assertEqual("paid", paid["status"])
|
||||
self.assertEqual(
|
||||
"BANK-RPP-2026-0001",
|
||||
paid["reconciliation"]["transaction_reference"],
|
||||
)
|
||||
self.assertEqual(2, len(paid["events"]))
|
||||
finally:
|
||||
session.close()
|
||||
engine.dispose()
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from contextlib import redirect_stdout
|
||||
from dataclasses import replace
|
||||
import io
|
||||
import json
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import stat
|
||||
import subprocess
|
||||
import sys
|
||||
@@ -24,6 +26,7 @@ from govoplan_lab.lifecycle import ( # noqa: E402
|
||||
LabOperationError,
|
||||
_assert_domain_owned,
|
||||
_domain_description,
|
||||
_ensure_certificates,
|
||||
_render_kubectl_wrapper,
|
||||
destroy,
|
||||
)
|
||||
@@ -234,6 +237,37 @@ class KubernetesLabTests(unittest.TestCase):
|
||||
|
||||
self.assertEqual(0o600, stat.S_IMODE(path.stat().st_mode))
|
||||
|
||||
@unittest.skipUnless(shutil.which("openssl"), "openssl is required")
|
||||
def test_generated_lab_ca_passes_strict_chain_validation(self) -> None:
|
||||
with tempfile.TemporaryDirectory(prefix="govoplan-lab-pki-") as directory:
|
||||
config = replace(
|
||||
load_config(REHEARSAL_CONFIG),
|
||||
state_directory=Path(directory),
|
||||
)
|
||||
_ensure_certificates(config, CommandRunner(config))
|
||||
ca_certificate = config.state_directory / "pki" / "ca.crt"
|
||||
server_certificate = config.state_directory / "pki" / "server.crt"
|
||||
|
||||
result = subprocess.run(
|
||||
[
|
||||
"openssl",
|
||||
"verify",
|
||||
"-x509_strict",
|
||||
"-CAfile",
|
||||
str(ca_certificate),
|
||||
str(server_certificate),
|
||||
],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
check=False,
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
0,
|
||||
result.returncode,
|
||||
(result.stdout + result.stderr).decode(errors="replace"),
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -46,6 +46,7 @@ class PackageRegistryReleaseTests(unittest.TestCase):
|
||||
)
|
||||
|
||||
self.assertEqual("1", payload["schema_version"])
|
||||
self.assertEqual("base", payload["profile"])
|
||||
self.assertEqual("govoplan-core", payload["python"][0]["name"])
|
||||
self.assertIn(
|
||||
"@govoplan/core-webui",
|
||||
@@ -55,6 +56,48 @@ class PackageRegistryReleaseTests(unittest.TestCase):
|
||||
digest = unsigned.pop("package_set_sha256")
|
||||
self.assertEqual(ARTIFACTS._canonical_sha256(unsigned), digest)
|
||||
|
||||
def test_full_profile_is_derived_from_the_developer_meta_package(self) -> None:
|
||||
core_version = tomllib.loads(
|
||||
(ROOT.parent / "govoplan-core/pyproject.toml").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
)["project"]["version"]
|
||||
selected = PACKAGE_SET.parse_meta_package(
|
||||
ROOT / "packages/govoplan-meta/pyproject.toml",
|
||||
core_version=core_version,
|
||||
)
|
||||
|
||||
by_name = {item["name"]: item for item in selected}
|
||||
self.assertIn("govoplan-core", by_name)
|
||||
self.assertIn("govoplan-records", by_name)
|
||||
self.assertEqual("0.1.21", by_name["govoplan-tasks"]["version"])
|
||||
|
||||
payload = PACKAGE_SET.generate_package_set(
|
||||
core_version=core_version,
|
||||
requirements=ROOT / "requirements-release.txt",
|
||||
workspace=ROOT.parent,
|
||||
profile="full",
|
||||
meta_package=ROOT / "packages/govoplan-meta/pyproject.toml",
|
||||
)
|
||||
self.assertEqual("full", payload["profile"])
|
||||
self.assertEqual(len(selected), len(payload["python"]))
|
||||
self.assertIn(
|
||||
"@govoplan/records-webui",
|
||||
{item["name"] for item in payload["webui"]},
|
||||
)
|
||||
|
||||
def test_python_registry_artifact_url_is_immutable_and_credential_free(self) -> None:
|
||||
url = ARTIFACTS._python_artifact_url(
|
||||
"https://git.add-ideas.de/api/packages/GovOPlaN/pypi/simple",
|
||||
package={"name": "govoplan-files", "version": "0.1.18"},
|
||||
filename="govoplan_files-0.1.18-py3-none-any.whl",
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
"https://git.add-ideas.de/api/packages/GovOPlaN/pypi/files/govoplan-files/0.1.18/govoplan_files-0.1.18-py3-none-any.whl",
|
||||
url,
|
||||
)
|
||||
|
||||
def test_wheel_and_webui_artifacts_are_verified_by_embedded_identity(self) -> None:
|
||||
with tempfile.TemporaryDirectory(prefix="govoplan-package-artifacts-") as value:
|
||||
root = Path(value)
|
||||
@@ -138,6 +181,11 @@ class PackageRegistryReleaseTests(unittest.TestCase):
|
||||
)
|
||||
|
||||
self.assertIn("resolve-package-artifacts.py", workflow)
|
||||
self.assertIn("--profile full", workflow)
|
||||
self.assertIn('git show "v$VERSION:requirements-release.txt"', workflow)
|
||||
self.assertIn('git show "v$VERSION:packages/govoplan-meta/pyproject.toml"', workflow)
|
||||
self.assertIn("--meta-package runtime-output/govoplan-meta.source.toml", workflow)
|
||||
self.assertIn("GOVOPLAN_WEBUI_INSTALL_ALL_PACKAGES=true", workflow)
|
||||
self.assertIn("package-artifacts.lock.json", workflow)
|
||||
self.assertIn(
|
||||
"--package-lock runtime-output/package-artifacts.lock.json",
|
||||
|
||||
@@ -22,11 +22,16 @@ class PackageSetDispatchTests(unittest.TestCase):
|
||||
def test_meta_package_resolves_to_exact_tagged_repository_targets(self) -> None:
|
||||
targets = MODULE.package_targets()
|
||||
|
||||
self.assertEqual(66, len(targets))
|
||||
self.assertEqual(66, len({target.distribution for target in targets}))
|
||||
self.assertEqual(73, len(targets))
|
||||
self.assertEqual(73, len({target.distribution for target in targets}))
|
||||
by_name = {target.distribution: target for target in targets}
|
||||
self.assertEqual("v0.1.14", by_name["govoplan-core"].tag)
|
||||
self.assertEqual("v0.1.8", by_name["govoplan-access"].tag)
|
||||
self.assertEqual("v0.1.38", by_name["govoplan-core"].tag)
|
||||
self.assertEqual("v0.1.22", by_name["govoplan-access"].tag)
|
||||
self.assertEqual("v0.1.20", by_name["govoplan-dms"].tag)
|
||||
self.assertEqual("v0.1.20", by_name["govoplan-erp"].tag)
|
||||
self.assertEqual("v0.1.20", by_name["govoplan-fit-connect"].tag)
|
||||
self.assertEqual("v0.1.23", by_name["govoplan-idm"].tag)
|
||||
self.assertEqual("v0.1.21", by_name["govoplan-xrechnung"].tag)
|
||||
self.assertTrue(by_name["govoplan-core"].tag_exists)
|
||||
self.assertTrue(by_name["govoplan-access"].has_webui)
|
||||
self.assertEqual(
|
||||
|
||||
@@ -169,6 +169,69 @@ class PlatformInterfaceInventoryTests(unittest.TestCase):
|
||||
),
|
||||
)
|
||||
|
||||
def test_high_risk_help_baseline_is_validated(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
path = Path(directory) / "help-baseline.json"
|
||||
path.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"schema_version": 1,
|
||||
"maximum_missing_exact_help": 3,
|
||||
}
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
3,
|
||||
inventory._load_high_risk_help_baseline(path)[
|
||||
"maximum_missing_exact_help"
|
||||
],
|
||||
)
|
||||
path.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"schema_version": 1,
|
||||
"maximum_missing_exact_help": -1,
|
||||
}
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
with self.assertRaisesRegex(ValueError, "non-negative integer"):
|
||||
inventory._load_high_risk_help_baseline(path)
|
||||
|
||||
def test_declaration_strict_mode_rejects_high_risk_help_regression(
|
||||
self,
|
||||
) -> None:
|
||||
result = {
|
||||
"translation_health": {"missing_catalog_entries": []},
|
||||
"api": {
|
||||
"unclassified_endpoints": [],
|
||||
"stale_endpoint_declarations": [],
|
||||
},
|
||||
"declaration_health": {},
|
||||
"help_health": {
|
||||
"invalid_risk_annotations": [],
|
||||
"unresolved_exact_high_risk_help": [],
|
||||
"high_risk_help_without_german": [],
|
||||
"missing_exact_high_risk_help": [{"id": "example.delete"}],
|
||||
"baseline_maximum_missing": 0,
|
||||
"baseline_regression": True,
|
||||
},
|
||||
}
|
||||
|
||||
self.assertEqual(
|
||||
[
|
||||
"1 high-risk controls lack exact F1 help; baseline permits at most 0"
|
||||
],
|
||||
inventory._strict_failures(
|
||||
result,
|
||||
check_translations=False,
|
||||
check_endpoints=False,
|
||||
check_declarations=True,
|
||||
),
|
||||
)
|
||||
|
||||
def test_fastapi_route_scanner_includes_router_prefix(self) -> None:
|
||||
tree = ast.parse(
|
||||
"""
|
||||
|
||||
@@ -5,7 +5,7 @@ import tomllib
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from govoplan_core.core.modules import ModuleManifest
|
||||
from govoplan_core.core.modules import ModuleManifest, PermissionDefinition
|
||||
from govoplan_core.core.provider_governance import (
|
||||
ExternalProviderDeclaration,
|
||||
ModuleArchitectureDeclaration,
|
||||
@@ -73,7 +73,7 @@ class ReleaseCatalogEntrySynthesisTests(unittest.TestCase):
|
||||
),
|
||||
ModuleMaturityEvidence(
|
||||
kind="documentation",
|
||||
reference="docs/INSTITUTIONAL_GOVERNANCE_TARGET_ARCHITECTURE.md",
|
||||
reference="docs/architecture/INSTITUTIONAL_GOVERNANCE_TARGET_ARCHITECTURE.md",
|
||||
summary="Defines the provider declaration contract.",
|
||||
),
|
||||
),
|
||||
@@ -88,6 +88,18 @@ class ReleaseCatalogEntrySynthesisTests(unittest.TestCase):
|
||||
id="example",
|
||||
name="Example",
|
||||
version="1.2.3",
|
||||
permissions=(
|
||||
PermissionDefinition(
|
||||
scope="example:records:read",
|
||||
label="Read records",
|
||||
description="Read example records.",
|
||||
category="Records",
|
||||
level="tenant",
|
||||
module_id="example",
|
||||
resource="records",
|
||||
action="read",
|
||||
),
|
||||
),
|
||||
architecture=architecture,
|
||||
external_providers=(provider,),
|
||||
),
|
||||
@@ -112,6 +124,19 @@ class ReleaseCatalogEntrySynthesisTests(unittest.TestCase):
|
||||
"default_authority_mode"
|
||||
],
|
||||
)
|
||||
self.assertEqual(
|
||||
{
|
||||
"scope": "example:records:read",
|
||||
"label": "Read records",
|
||||
"description": "Read example records.",
|
||||
"category": "Records",
|
||||
"level": "tenant",
|
||||
"resource": "records",
|
||||
"action": "read",
|
||||
"deprecated": False,
|
||||
},
|
||||
entry["permissions"][0],
|
||||
)
|
||||
|
||||
def test_selective_update_synthesizes_initial_entries_from_package_manifests(self) -> None:
|
||||
payload: dict[str, object] = {
|
||||
|
||||
@@ -95,14 +95,21 @@ class ReleaseEntrypointGateTests(unittest.TestCase):
|
||||
)
|
||||
self.assertIn('cp "$WEBUI/package.release.json" "$TMP_DIR/package.json"', script)
|
||||
|
||||
def test_source_catalog_generator_enforces_explicit_repo_versions(self) -> None:
|
||||
def test_catalog_generator_validates_registry_package_set_before_writing(self) -> None:
|
||||
script = (META_ROOT / "tools" / "release" / "generate-release-catalog.py").read_text()
|
||||
|
||||
gate = script.index("selected_repository_version_issues(")
|
||||
gate = script.index("_validate_release_inputs(package_set, package_lock")
|
||||
write = script.index("output.write_text(")
|
||||
|
||||
self.assertLess(gate, write)
|
||||
|
||||
def test_full_catalog_publication_synchronizes_browsable_module_directory(self) -> None:
|
||||
publisher = (META_ROOT / "tools" / "release" / "publish-release-catalog.sh").read_text()
|
||||
|
||||
self.assertIn('--module-directory-output "$WEB_ROOT/public/catalogs/v1"', publisher)
|
||||
self.assertIn('git -C "$WEB_ROOT" add -A', publisher)
|
||||
self.assertIn('"$MODULE_DIRECTORY_PATH"', publisher)
|
||||
|
||||
def test_candidate_publication_uses_existing_keyring_as_trust_anchor(self) -> None:
|
||||
publisher = (META_ROOT / "tools" / "release" / "govoplan_release" / "publisher.py").read_text()
|
||||
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from unittest import mock
|
||||
|
||||
@@ -14,6 +16,7 @@ if str(RELEASE_TOOLS_ROOT) not in sys.path:
|
||||
from govoplan_release.module_directory import ( # noqa: E402
|
||||
module_directory_payloads,
|
||||
safe_path_part,
|
||||
write_module_directory,
|
||||
)
|
||||
|
||||
|
||||
@@ -77,6 +80,78 @@ class ReleaseModuleDirectoryTests(unittest.TestCase):
|
||||
catalog_payload={}, keyring_payload={}, channel="../stable"
|
||||
)
|
||||
|
||||
def test_prune_removes_stale_json_but_keeps_unrelated_assets(self) -> None:
|
||||
catalog = {
|
||||
"generated_at": "2026-08-06T12:00:00Z",
|
||||
"sequence": 8,
|
||||
"modules": [
|
||||
{
|
||||
"module_id": "files",
|
||||
"name": "Files",
|
||||
"version": "1.2.3",
|
||||
"python_package": "govoplan-files",
|
||||
"source": {
|
||||
"repository": "govoplan-files",
|
||||
"tag": "v1.2.3",
|
||||
"commit": "a" * 40,
|
||||
},
|
||||
"artifact_integrity": {
|
||||
"python": {"sha256": "b" * 64},
|
||||
},
|
||||
}
|
||||
],
|
||||
}
|
||||
with tempfile.TemporaryDirectory() as value:
|
||||
output_root = Path(value)
|
||||
stale = output_root / "modules" / "legacy" / "0.1.0" / "manifest.json"
|
||||
stale.parent.mkdir(parents=True)
|
||||
stale.write_text("{}\n", encoding="utf-8")
|
||||
unrelated = output_root / "modules" / "README.txt"
|
||||
unrelated.write_text("keep\n", encoding="utf-8")
|
||||
|
||||
written = write_module_directory(
|
||||
catalog_payload=catalog,
|
||||
keyring_payload={},
|
||||
output_root=output_root,
|
||||
channel="stable",
|
||||
prune=True,
|
||||
)
|
||||
|
||||
self.assertFalse(stale.exists())
|
||||
self.assertTrue(unrelated.exists())
|
||||
self.assertEqual(3, len(written))
|
||||
manifest = json.loads(
|
||||
(output_root / "modules" / "files" / "1.2.3" / "manifest.json").read_text()
|
||||
)
|
||||
self.assertEqual("govoplan-files", manifest["module"]["repo"])
|
||||
self.assertEqual("v1.2.3", manifest["module"]["python_tag"])
|
||||
self.assertEqual("b" * 64, manifest["module"]["artifact_integrity"]["python"]["sha256"])
|
||||
|
||||
def test_writer_refuses_nested_symlink_targets(self) -> None:
|
||||
catalog = {
|
||||
"modules": [{"module_id": "files", "version": "1.2.3"}],
|
||||
}
|
||||
with tempfile.TemporaryDirectory() as value:
|
||||
root = Path(value)
|
||||
output_root = root / "public"
|
||||
external = root / "external"
|
||||
(output_root / "modules").mkdir(parents=True)
|
||||
external.mkdir()
|
||||
(output_root / "modules" / "files").symlink_to(
|
||||
external,
|
||||
target_is_directory=True,
|
||||
)
|
||||
|
||||
with self.assertRaisesRegex(ValueError, "symlinks"):
|
||||
write_module_directory(
|
||||
catalog_payload=catalog,
|
||||
keyring_payload={},
|
||||
output_root=output_root,
|
||||
channel="stable",
|
||||
)
|
||||
|
||||
self.assertEqual([], list(external.iterdir()))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Generate the human capability-fit report from its machine-readable input."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import os
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
|
||||
META_ROOT = Path(__file__).resolve().parents[2]
|
||||
ASSESSMENT_TOOLS_ROOT = META_ROOT / "tools" / "assessments"
|
||||
RELEASE_TOOLS_ROOT = META_ROOT / "tools" / "release"
|
||||
for tools_root in (ASSESSMENT_TOOLS_ROOT, RELEASE_TOOLS_ROOT):
|
||||
if str(tools_root) not in sys.path:
|
||||
sys.path.insert(0, str(tools_root))
|
||||
|
||||
from govoplan_assessment.report_generator import ( # noqa: E402
|
||||
AssessmentGenerationError,
|
||||
load_bounded_json,
|
||||
render_assessment_markdown,
|
||||
validate_report_input,
|
||||
)
|
||||
|
||||
|
||||
DEFAULT_ASSESSMENT = META_ROOT / "docs" / "capability-fit-current.json"
|
||||
DEFAULT_SCHEMA = META_ROOT / "docs" / "capability-fit.schema.json"
|
||||
DEFAULT_OUTPUT = (
|
||||
META_ROOT
|
||||
/ "docs"
|
||||
/ "evidence"
|
||||
/ "snapshots"
|
||||
/ "CAPABILITY_AND_INFRASTRUCTURE_FIT.generated.md"
|
||||
)
|
||||
MAX_OUTPUT_BYTES = 16 * 1024 * 1024
|
||||
|
||||
|
||||
def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Render a deterministic human report from one capability-fit JSON input."
|
||||
)
|
||||
parser.add_argument("--assessment", type=Path, default=DEFAULT_ASSESSMENT)
|
||||
parser.add_argument("--schema", type=Path, default=DEFAULT_SCHEMA)
|
||||
parser.add_argument("--output", type=Path, default=DEFAULT_OUTPUT)
|
||||
parser.add_argument(
|
||||
"--check",
|
||||
action="store_true",
|
||||
help="Fail when the output is missing or differs instead of writing it.",
|
||||
)
|
||||
return parser.parse_args(argv)
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
args = parse_args(argv)
|
||||
try:
|
||||
assessment = load_bounded_json(args.assessment, label="assessment")
|
||||
schema = load_bounded_json(args.schema, label="assessment schema")
|
||||
validate_report_input(assessment=assessment, schema=schema)
|
||||
rendered = render_assessment_markdown(assessment)
|
||||
encoded = rendered.encode("utf-8")
|
||||
if len(encoded) > MAX_OUTPUT_BYTES:
|
||||
raise AssessmentGenerationError(
|
||||
f"Generated report exceeds the {MAX_OUTPUT_BYTES}-byte output limit"
|
||||
)
|
||||
if args.check:
|
||||
try:
|
||||
current = args.output.read_bytes()
|
||||
except OSError:
|
||||
current = None
|
||||
if current != encoded:
|
||||
print(
|
||||
f"Capability-fit report is stale: {args.output}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 2
|
||||
print(f"Capability-fit report is current: {args.output}")
|
||||
return 0
|
||||
_atomic_write(args.output, encoded)
|
||||
print(f"Generated capability-fit report: {args.output}")
|
||||
return 0
|
||||
except AssessmentGenerationError as exc:
|
||||
print(str(exc), file=sys.stderr)
|
||||
return 1
|
||||
|
||||
|
||||
def _atomic_write(path: Path, content: bytes) -> None:
|
||||
if not path.parent.is_dir():
|
||||
raise AssessmentGenerationError(
|
||||
f"Output parent directory does not exist: {path.parent}"
|
||||
)
|
||||
if path.is_symlink():
|
||||
raise AssessmentGenerationError("Output path must not be a symbolic link")
|
||||
descriptor = -1
|
||||
temporary_name = ""
|
||||
try:
|
||||
descriptor, temporary_name = tempfile.mkstemp(
|
||||
prefix=".govoplan-fit-report-",
|
||||
suffix=".tmp",
|
||||
dir=path.parent,
|
||||
)
|
||||
os.fchmod(descriptor, 0o644)
|
||||
with os.fdopen(descriptor, "wb", closefd=True) as handle:
|
||||
descriptor = -1
|
||||
handle.write(content)
|
||||
handle.flush()
|
||||
os.fsync(handle.fileno())
|
||||
os.replace(temporary_name, path)
|
||||
temporary_name = ""
|
||||
except OSError as exc:
|
||||
raise AssessmentGenerationError(
|
||||
f"Could not write generated report atomically: {exc}"
|
||||
) from exc
|
||||
finally:
|
||||
if descriptor >= 0:
|
||||
os.close(descriptor)
|
||||
if temporary_name:
|
||||
try:
|
||||
os.unlink(temporary_name)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,551 @@
|
||||
"""Deterministically render one validated capability-fit assessment as Markdown."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any, Iterable, Mapping, Sequence
|
||||
|
||||
from jsonschema import Draft202012Validator, FormatChecker
|
||||
from jsonschema.exceptions import SchemaError
|
||||
|
||||
|
||||
MAX_ASSESSMENT_BYTES = 16 * 1024 * 1024
|
||||
STATUS_DEFINITIONS = (
|
||||
(
|
||||
"verified",
|
||||
"Implemented and directly exercised by evidence appropriate to the stated scope.",
|
||||
),
|
||||
(
|
||||
"available_unconfigured",
|
||||
"Implemented with supporting evidence, but not configured and exercised in the target.",
|
||||
),
|
||||
(
|
||||
"partial",
|
||||
"A useful subset exists, but a material part of the requirement is missing or unproved.",
|
||||
),
|
||||
(
|
||||
"scaffold",
|
||||
"Contracts or structure exist, but the end-to-end capability is not usable.",
|
||||
),
|
||||
(
|
||||
"external_system",
|
||||
"The deployment or another system must supply the capability.",
|
||||
),
|
||||
(
|
||||
"planned",
|
||||
"Only a concept, backlog item, or design direction exists.",
|
||||
),
|
||||
(
|
||||
"not_fit",
|
||||
"Evidence shows that the assessed composition cannot meet the requirement.",
|
||||
),
|
||||
(
|
||||
"not_assessed",
|
||||
"The requirement or target environment is not sufficiently known.",
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class AssessmentGenerationError(ValueError):
|
||||
"""The assessment cannot be safely validated or rendered."""
|
||||
|
||||
|
||||
def load_bounded_json(path: Path, *, label: str) -> dict[str, Any]:
|
||||
try:
|
||||
size = path.stat().st_size
|
||||
except OSError as exc:
|
||||
raise AssessmentGenerationError(f"Could not inspect {label}: {exc}") from exc
|
||||
if size > MAX_ASSESSMENT_BYTES:
|
||||
raise AssessmentGenerationError(
|
||||
f"{label} exceeds the {MAX_ASSESSMENT_BYTES}-byte input limit"
|
||||
)
|
||||
try:
|
||||
payload = json.loads(
|
||||
path.read_text(encoding="utf-8"),
|
||||
object_pairs_hook=_unique_object,
|
||||
)
|
||||
except (OSError, UnicodeError, json.JSONDecodeError) as exc:
|
||||
raise AssessmentGenerationError(f"Could not read {label}: {exc}") from exc
|
||||
if not isinstance(payload, dict):
|
||||
raise AssessmentGenerationError(f"{label} must contain one JSON object")
|
||||
return payload
|
||||
|
||||
|
||||
def validate_report_input(
|
||||
*,
|
||||
assessment: Mapping[str, Any],
|
||||
schema: Mapping[str, Any],
|
||||
) -> None:
|
||||
try:
|
||||
Draft202012Validator.check_schema(schema)
|
||||
except SchemaError as exc:
|
||||
raise AssessmentGenerationError(
|
||||
f"Assessment schema is invalid: {exc.message}"
|
||||
) from exc
|
||||
errors = sorted(
|
||||
Draft202012Validator(
|
||||
schema,
|
||||
format_checker=FormatChecker(),
|
||||
).iter_errors(assessment),
|
||||
key=lambda item: tuple(str(part) for part in item.absolute_path),
|
||||
)
|
||||
if errors:
|
||||
details = "; ".join(
|
||||
f"{_json_path(error.absolute_path)}: {error.message}"
|
||||
for error in errors[:20]
|
||||
)
|
||||
raise AssessmentGenerationError(f"Assessment does not match schema: {details}")
|
||||
_validate_references(assessment)
|
||||
_reject_sensitive_keys(assessment)
|
||||
|
||||
|
||||
def render_assessment_markdown(assessment: Mapping[str, Any]) -> str:
|
||||
"""Return stable Markdown derived only from a validated assessment object."""
|
||||
|
||||
assessment_hash = hashlib.sha256(
|
||||
json.dumps(
|
||||
assessment,
|
||||
sort_keys=True,
|
||||
separators=(",", ":"),
|
||||
ensure_ascii=True,
|
||||
).encode("utf-8")
|
||||
).hexdigest()
|
||||
scope = _mapping(assessment["scope"])
|
||||
release = _mapping(assessment["release"])
|
||||
profile = _mapping(assessment["deployment_profile"])
|
||||
lines = [
|
||||
"# GovOPlaN Capability and IT-Infrastructure Fit Assessment",
|
||||
"",
|
||||
"> Generated from [`capability-fit-current.json`](../../capability-fit-current.json).",
|
||||
"> Edit and validate the machine-readable assessment, then regenerate this file;",
|
||||
"> do not maintain conclusions independently in Markdown.",
|
||||
"",
|
||||
"This is an evidence-based fit assessment, not a production approval or",
|
||||
"security certification. Repository or manifest existence alone never counts",
|
||||
"as an implemented capability. Unknown target requirements remain explicitly",
|
||||
"`not_assessed`.",
|
||||
"",
|
||||
"## Assessment record",
|
||||
"",
|
||||
"| Field | Value |",
|
||||
"| --- | --- |",
|
||||
f"| Assessment ID | `{_cell(assessment['assessment_id'])}` |",
|
||||
f"| Schema version | `govoplan.fit-assessment/{_cell(assessment['schema_version'])}` |",
|
||||
f"| Assessed on | {_cell(assessment['assessed_at'])} |",
|
||||
f"| Scope | {_cell(scope['title'])} |",
|
||||
f"| Release | `{_cell(release['ref'])}` ({_cell(release['kind'])}) |",
|
||||
f"| Meta commit | `{_cell(release['meta_commit'])}` |",
|
||||
f"| Deployment profile | `{_cell(profile['id'])}` · `{_cell(profile['status'])}` |",
|
||||
f"| Configuration packages | {_inline_list(release['configuration_packages'], code=True)} |",
|
||||
f"| Canonical input SHA-256 | `{assessment_hash}` |",
|
||||
"",
|
||||
"## Controlled status vocabulary",
|
||||
"",
|
||||
"| Status | Meaning |",
|
||||
"| --- | --- |",
|
||||
]
|
||||
lines.extend(
|
||||
f"| `{status}` | {_cell(description)} |"
|
||||
for status, description in STATUS_DEFINITIONS
|
||||
)
|
||||
lines.extend(
|
||||
[
|
||||
"",
|
||||
"## Scope and reference journeys",
|
||||
"",
|
||||
"Reference journeys:",
|
||||
"",
|
||||
*_bullets(scope["reference_journeys"]),
|
||||
"",
|
||||
"Explicitly postponed:",
|
||||
"",
|
||||
*_bullets(scope["postponed"]),
|
||||
"",
|
||||
"## Facts",
|
||||
"",
|
||||
*_bullets(assessment["facts"]),
|
||||
"",
|
||||
"## Decisions",
|
||||
"",
|
||||
*_bullets(assessment["decisions"]),
|
||||
"",
|
||||
"## Assumptions",
|
||||
"",
|
||||
*_bullets(assessment["assumptions"]),
|
||||
"",
|
||||
"## Unresolved decisions",
|
||||
"",
|
||||
*_bullets(assessment["open_questions"]),
|
||||
"",
|
||||
"## Pinned release and composition",
|
||||
"",
|
||||
f"Release reproducible: **{'yes' if release['reproducible'] else 'no'}**.",
|
||||
"",
|
||||
]
|
||||
)
|
||||
lines.extend(_notes(release.get("notes", [])))
|
||||
lines.extend(
|
||||
[
|
||||
"",
|
||||
"| Module | Repository and commit | Manifest version | Enabled | Role |",
|
||||
"| --- | --- | --- | --- | --- |",
|
||||
]
|
||||
)
|
||||
for module_value in assessment["composition"]:
|
||||
module = _mapping(module_value)
|
||||
lines.append(
|
||||
"| `{}` | `{}` @ `{}` | `{}` | {} | {} |".format(
|
||||
_cell(module["module_id"]),
|
||||
_cell(module["repository"]),
|
||||
_cell(module["commit"]),
|
||||
_cell(module["manifest_version"]),
|
||||
"yes" if module["enabled"] else "no",
|
||||
_cell(module["role"]),
|
||||
)
|
||||
)
|
||||
lines.extend(
|
||||
[
|
||||
"",
|
||||
"## Deployment profile",
|
||||
"",
|
||||
f"Status: `{_cell(profile['status'])}`",
|
||||
"",
|
||||
_text(profile["description"]),
|
||||
"",
|
||||
"Evidence:",
|
||||
"",
|
||||
*_bullets(_evidence_labels(profile["evidence"])),
|
||||
"",
|
||||
"## Recommended scenarios",
|
||||
"",
|
||||
]
|
||||
)
|
||||
for scenario_value in assessment["scenarios"]:
|
||||
scenario = _mapping(scenario_value)
|
||||
lines.extend(
|
||||
[
|
||||
f"### {_text(scenario['label'])}",
|
||||
"",
|
||||
f"Status: `{_cell(scenario['status'])}`",
|
||||
"",
|
||||
_text(scenario["recommendation"]),
|
||||
"",
|
||||
f"Composition: {_inline_list(scenario['composition'], code=True)}.",
|
||||
"",
|
||||
"Topology:",
|
||||
"",
|
||||
*_bullets(scenario["topology"]),
|
||||
"",
|
||||
"Conditions:",
|
||||
"",
|
||||
*_bullets(scenario["conditions"]),
|
||||
"",
|
||||
]
|
||||
)
|
||||
functional_context = _mapping(assessment["functional_context"])
|
||||
lines.extend(
|
||||
[
|
||||
"## Functional matrix context",
|
||||
"",
|
||||
"### Required modules",
|
||||
"",
|
||||
*_bullets(functional_context["required_modules"]),
|
||||
"",
|
||||
"### Optional modules",
|
||||
"",
|
||||
*_bullets(functional_context["optional_modules"]),
|
||||
"",
|
||||
"### External systems and connectors",
|
||||
"",
|
||||
*_bullets(functional_context["external_systems"]),
|
||||
"",
|
||||
"### Missing contracts",
|
||||
"",
|
||||
*_bullets(functional_context["missing_contracts"]),
|
||||
"",
|
||||
"### Policy decisions",
|
||||
"",
|
||||
*_bullets(functional_context["policy_decisions"]),
|
||||
"",
|
||||
"### Manual workarounds",
|
||||
"",
|
||||
*_bullets(functional_context["manual_workarounds"]),
|
||||
"",
|
||||
"### Blockers",
|
||||
"",
|
||||
*_bullets(functional_context["blockers"]),
|
||||
"",
|
||||
]
|
||||
)
|
||||
lines.extend(
|
||||
[
|
||||
"## Assessment questionnaire",
|
||||
"",
|
||||
"Every required area remains visible even when its target answer is unknown.",
|
||||
"",
|
||||
"| Area | Question | State | Answer | Evidence |",
|
||||
"| --- | --- | --- | --- | --- |",
|
||||
]
|
||||
)
|
||||
questionnaire = _mapping(assessment["questionnaire"])
|
||||
for area, answers in questionnaire.items():
|
||||
for answer_value in _sequence(answers):
|
||||
answer = _mapping(answer_value)
|
||||
raw_answer = answer["answer"]
|
||||
answer_text = (
|
||||
_inline_list(raw_answer)
|
||||
if isinstance(raw_answer, list)
|
||||
else _text(raw_answer) if raw_answer is not None else "—"
|
||||
)
|
||||
lines.append(
|
||||
"| {} | {} | `{}` | {} | {} |".format(
|
||||
_cell(area.replace("_", " ").title()),
|
||||
_cell(answer["question"]),
|
||||
_cell(answer["state"]),
|
||||
_cell(answer_text),
|
||||
_cell("; ".join(_evidence_labels(answer["evidence"])) or "—"),
|
||||
)
|
||||
)
|
||||
lines.extend(_assessed_matrix("Functional capability matrix", assessment["capabilities"]))
|
||||
lines.extend(_assessed_matrix("Infrastructure matrix", assessment["infrastructure"]))
|
||||
lines.extend(
|
||||
[
|
||||
"## Data flows and trust boundaries",
|
||||
"",
|
||||
"| Flow | From → to | Data | Trust boundary | Controls |",
|
||||
"| --- | --- | --- | --- | --- |",
|
||||
]
|
||||
)
|
||||
for flow_value in assessment["data_flows"]:
|
||||
flow = _mapping(flow_value)
|
||||
lines.append(
|
||||
"| `{}` | {} → {} | {} | {} | {} |".format(
|
||||
_cell(flow["id"]),
|
||||
_cell(flow["from"]),
|
||||
_cell(flow["to"]),
|
||||
_cell(_inline_list(flow["data"])),
|
||||
_cell(flow["trust_boundary"]),
|
||||
_cell(_inline_list(flow["controls"])),
|
||||
)
|
||||
)
|
||||
lines.extend(
|
||||
[
|
||||
"",
|
||||
"## Risks and residual risks",
|
||||
"",
|
||||
"| Risk | Impact | Treatment | Owner | Residual risk |",
|
||||
"| --- | --- | --- | --- | --- |",
|
||||
]
|
||||
)
|
||||
for risk_value in assessment["risks"]:
|
||||
risk = _mapping(risk_value)
|
||||
lines.append(
|
||||
"| **{}**<br>{} | {} | {} | {} | {} |".format(
|
||||
_cell(risk["id"]),
|
||||
_cell(risk["statement"]),
|
||||
_cell(risk["impact"]),
|
||||
_cell(risk["treatment"]),
|
||||
_cell(risk["owner"] or "unassigned"),
|
||||
_cell(risk["residual_risk"]),
|
||||
)
|
||||
)
|
||||
lines.extend(
|
||||
[
|
||||
"",
|
||||
"## Recommendations",
|
||||
"",
|
||||
*_bullets(assessment["recommendations"]),
|
||||
"",
|
||||
"## Proof-of-concept and promotion checks",
|
||||
"",
|
||||
*_numbered(assessment["proof_checks"]),
|
||||
"",
|
||||
"## Generation contract",
|
||||
"",
|
||||
"This report is deterministic output from the schema-validated JSON companion.",
|
||||
"The generator rejects duplicate JSON keys, schema drift, secret-bearing field",
|
||||
"names, stale checked-in output, and oversized inputs. A new assessment or",
|
||||
"release changes the canonical input hash and requires review of the affected",
|
||||
"evidence and conclusions through the release-aware reassessment tool.",
|
||||
"",
|
||||
]
|
||||
)
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def _assessed_matrix(title: str, values: object) -> list[str]:
|
||||
lines = [
|
||||
"",
|
||||
f"## {title}",
|
||||
"",
|
||||
"| Requirement | Status | Evidence | Conditions and gaps | Recommendation and proof |",
|
||||
"| --- | --- | --- | --- | --- |",
|
||||
]
|
||||
for item_value in _sequence(values):
|
||||
item = _mapping(item_value)
|
||||
conditions = [f"Condition: {value}" for value in item["conditions"]]
|
||||
gaps = [f"Gap: {value}" for value in item["gaps"]]
|
||||
risks = [f"Risk: {value}" for value in item["risks"]]
|
||||
lines.append(
|
||||
"| **{}**<br>{} | `{}` | {} | {} | {}<br>**Proof:** {} |".format(
|
||||
_cell(item["id"]),
|
||||
_cell(item["requirement"]),
|
||||
_cell(item["status"]),
|
||||
_cell("; ".join(_evidence_labels(item["evidence"])) or "Explicit absence of evidence"),
|
||||
_cell("; ".join([*conditions, *gaps, *risks]) or "—"),
|
||||
_cell(item["recommendation"] or "—"),
|
||||
_cell(item["proof_check"] or "—"),
|
||||
)
|
||||
)
|
||||
return lines
|
||||
|
||||
|
||||
def _evidence_labels(values: object) -> list[str]:
|
||||
labels: list[str] = []
|
||||
for value in _sequence(values):
|
||||
item = _mapping(value)
|
||||
label = f"{item['kind']}/{item['scope']}: {item['locator']}"
|
||||
if item.get("note"):
|
||||
label += f" ({item['note']})"
|
||||
labels.append(label)
|
||||
return labels
|
||||
|
||||
|
||||
def _unique_object(pairs: list[tuple[str, Any]]) -> dict[str, Any]:
|
||||
result: dict[str, Any] = {}
|
||||
for key, value in pairs:
|
||||
if key in result:
|
||||
raise AssessmentGenerationError(f"Duplicate JSON key: {key!r}")
|
||||
result[key] = value
|
||||
return result
|
||||
|
||||
|
||||
def _reject_sensitive_keys(value: object, path: tuple[str, ...] = ()) -> None:
|
||||
forbidden = {
|
||||
"access_token",
|
||||
"api_key",
|
||||
"credential_value",
|
||||
"password",
|
||||
"private_key",
|
||||
"refresh_token",
|
||||
"secret",
|
||||
}
|
||||
if isinstance(value, Mapping):
|
||||
for key, nested in value.items():
|
||||
normalized = str(key).strip().casefold()
|
||||
if normalized in forbidden:
|
||||
raise AssessmentGenerationError(
|
||||
f"Assessment contains forbidden sensitive field {_json_path((*path, str(key)))}"
|
||||
)
|
||||
_reject_sensitive_keys(nested, (*path, str(key)))
|
||||
elif isinstance(value, Sequence) and not isinstance(value, (str, bytes)):
|
||||
for index, nested in enumerate(value):
|
||||
_reject_sensitive_keys(nested, (*path, str(index)))
|
||||
|
||||
|
||||
def _validate_references(assessment: Mapping[str, Any]) -> None:
|
||||
composition = [_mapping(item) for item in _sequence(assessment["composition"])]
|
||||
module_ids = [str(item["module_id"]) for item in composition]
|
||||
if len(module_ids) != len(set(module_ids)):
|
||||
raise AssessmentGenerationError("Composition contains duplicate module IDs")
|
||||
modules = {str(item["module_id"]): item for item in composition}
|
||||
context = _mapping(assessment["functional_context"])
|
||||
required = {str(item) for item in _sequence(context["required_modules"])}
|
||||
optional = {str(item) for item in _sequence(context["optional_modules"])}
|
||||
unknown_context = (required | optional) - set(modules)
|
||||
if unknown_context:
|
||||
raise AssessmentGenerationError(
|
||||
"Functional context references unknown modules: "
|
||||
+ ", ".join(sorted(unknown_context))
|
||||
)
|
||||
if required & optional:
|
||||
raise AssessmentGenerationError(
|
||||
"Functional context cannot mark a module both required and optional"
|
||||
)
|
||||
for scenario_value in _sequence(assessment["scenarios"]):
|
||||
scenario = _mapping(scenario_value)
|
||||
referenced = {str(item) for item in _sequence(scenario["composition"])}
|
||||
unknown = referenced - set(modules)
|
||||
if unknown:
|
||||
raise AssessmentGenerationError(
|
||||
f"Scenario {scenario['id']!r} references unknown modules: "
|
||||
+ ", ".join(sorted(unknown))
|
||||
)
|
||||
disabled = sorted(
|
||||
module_id
|
||||
for module_id in referenced
|
||||
if not bool(modules[module_id]["enabled"])
|
||||
)
|
||||
if disabled:
|
||||
raise AssessmentGenerationError(
|
||||
f"Scenario {scenario['id']!r} references disabled modules: "
|
||||
+ ", ".join(disabled)
|
||||
)
|
||||
for collection in ("capabilities", "infrastructure", "data_flows", "risks"):
|
||||
identifiers = [
|
||||
str(_mapping(item)["id"])
|
||||
for item in _sequence(assessment[collection])
|
||||
]
|
||||
if len(identifiers) != len(set(identifiers)):
|
||||
raise AssessmentGenerationError(
|
||||
f"Assessment contains duplicate {collection} IDs"
|
||||
)
|
||||
|
||||
|
||||
def _mapping(value: object) -> Mapping[str, Any]:
|
||||
if not isinstance(value, Mapping):
|
||||
raise AssessmentGenerationError("Validated assessment contains a non-object value")
|
||||
return value
|
||||
|
||||
|
||||
def _sequence(value: object) -> Sequence[Any]:
|
||||
if not isinstance(value, Sequence) or isinstance(value, (str, bytes)):
|
||||
raise AssessmentGenerationError("Validated assessment contains a non-list value")
|
||||
return value
|
||||
|
||||
|
||||
def _text(value: object) -> str:
|
||||
return str(value).strip()
|
||||
|
||||
|
||||
def _cell(value: object) -> str:
|
||||
return _text(value).replace("|", "\\|").replace("\r", " ").replace("\n", " ")
|
||||
|
||||
|
||||
def _inline_list(values: object, *, code: bool = False) -> str:
|
||||
items = [_text(item) for item in _sequence(values)]
|
||||
if not items:
|
||||
return "none"
|
||||
if code:
|
||||
return ", ".join(f"`{_cell(item)}`" for item in items)
|
||||
return "; ".join(items)
|
||||
|
||||
|
||||
def _bullets(values: object) -> list[str]:
|
||||
items = [_text(item) for item in _sequence(values)]
|
||||
return [f"- {item}" for item in items] or ["- None recorded."]
|
||||
|
||||
|
||||
def _numbered(values: object) -> list[str]:
|
||||
return [f"{index}. {_text(item)}" for index, item in enumerate(_sequence(values), 1)]
|
||||
|
||||
|
||||
def _notes(values: object) -> list[str]:
|
||||
items = _bullets(values)
|
||||
return ["Release notes:", "", *items]
|
||||
|
||||
|
||||
def _json_path(parts: Iterable[object]) -> str:
|
||||
suffix = "".join(f"[{part}]" if str(part).isdigit() else f".{part}" for part in parts)
|
||||
return f"${suffix}"
|
||||
|
||||
|
||||
__all__ = (
|
||||
"AssessmentGenerationError",
|
||||
"MAX_ASSESSMENT_BYTES",
|
||||
"load_bounded_json",
|
||||
"render_assessment_markdown",
|
||||
"validate_report_input",
|
||||
)
|
||||
@@ -0,0 +1,265 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Require DSAR coverage or a reviewed no-store rationale for every module."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import importlib
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
META_ROOT = Path(__file__).resolve().parents[2]
|
||||
EXEMPTIONS_PATH = Path(__file__).with_name("dsar-coverage-exemptions.json")
|
||||
REPORT_PATH = (
|
||||
META_ROOT
|
||||
/ "docs"
|
||||
/ "evidence"
|
||||
/ "snapshots"
|
||||
/ "DSAR_PROVIDER_COVERAGE.generated.md"
|
||||
)
|
||||
MODULE_NAME_PATTERN = re.compile(r"[A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)*")
|
||||
REQUIRED_DOCUMENTATION_TYPES = frozenset({"admin"})
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class CoverageRow:
|
||||
module_id: str
|
||||
repository: str
|
||||
migration_owned: bool
|
||||
capability: str | None
|
||||
rationale: str
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument(
|
||||
"--workspace-root",
|
||||
type=Path,
|
||||
default=None,
|
||||
help="Directory containing GovOPlaN repositories.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--render",
|
||||
action="store_true",
|
||||
help="Print the current matrix instead of comparing the checked-in report.",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
catalog = json.loads((META_ROOT / "repositories.json").read_text(encoding="utf-8"))
|
||||
workspace_root = (args.workspace_root or Path(catalog["default_parent"])).resolve()
|
||||
exemptions = _exemptions()
|
||||
manifests, load_errors = _load_manifests(
|
||||
workspace_root=workspace_root,
|
||||
repositories=tuple(catalog["repositories"]),
|
||||
)
|
||||
|
||||
errors = list(load_errors)
|
||||
rows: list[CoverageRow] = []
|
||||
manifest_ids = {manifest.id for _, manifest in manifests}
|
||||
stale_exemptions = sorted(set(exemptions) - manifest_ids)
|
||||
if stale_exemptions:
|
||||
errors.append(
|
||||
"DSAR coverage exemptions reference unknown modules: "
|
||||
+ ", ".join(stale_exemptions)
|
||||
)
|
||||
|
||||
for repository, manifest in manifests:
|
||||
expected = f"privacy.dsar.{manifest.id}"
|
||||
provided = {
|
||||
item.name
|
||||
for item in manifest.provides_interfaces
|
||||
if item.name.startswith("privacy.dsar.")
|
||||
}
|
||||
factories = {
|
||||
name
|
||||
for name in manifest.capability_factories
|
||||
if name.startswith("privacy.dsar.")
|
||||
}
|
||||
migration_owned = manifest.migration_spec is not None
|
||||
rationale = exemptions.get(manifest.id)
|
||||
|
||||
if provided != factories:
|
||||
errors.append(
|
||||
f"{repository}: DSAR interface/factory mismatch: "
|
||||
f"interfaces={sorted(provided)!r}, factories={sorted(factories)!r}"
|
||||
)
|
||||
if provided and provided != {expected}:
|
||||
errors.append(
|
||||
f"{repository}: expected only {expected!r}, found {sorted(provided)!r}"
|
||||
)
|
||||
|
||||
capability = (
|
||||
expected if expected in provided and expected in factories else None
|
||||
)
|
||||
if migration_owned and capability is None:
|
||||
errors.append(
|
||||
f"{repository}: migration-owning module {manifest.id!r} must provide "
|
||||
f"and register {expected!r}"
|
||||
)
|
||||
if migration_owned and rationale is not None:
|
||||
errors.append(
|
||||
f"{repository}: migration-owning module {manifest.id!r} cannot use a "
|
||||
"no-store DSAR exemption"
|
||||
)
|
||||
if not migration_owned and capability is None and rationale is None:
|
||||
errors.append(
|
||||
f"{repository}: module {manifest.id!r} needs a DSAR provider or an "
|
||||
"explicit reviewed no-store rationale"
|
||||
)
|
||||
if capability is not None and rationale is not None:
|
||||
errors.append(
|
||||
f"{repository}: module {manifest.id!r} has both DSAR coverage and a "
|
||||
"stale exemption"
|
||||
)
|
||||
if capability is not None:
|
||||
if capability not in manifest.capability_documentation:
|
||||
errors.append(
|
||||
f"{repository}: {capability!r} lacks capability documentation"
|
||||
)
|
||||
matching_topics = tuple(
|
||||
topic
|
||||
for topic in manifest.documentation
|
||||
if "data-subject-request" in topic.id
|
||||
)
|
||||
if not matching_topics or not any(
|
||||
REQUIRED_DOCUMENTATION_TYPES.issubset(topic.documentation_types)
|
||||
for topic in matching_topics
|
||||
):
|
||||
errors.append(
|
||||
f"{repository}: DSAR coverage needs a static administrator "
|
||||
"data-subject-requests DocumentationTopic"
|
||||
)
|
||||
|
||||
rows.append(
|
||||
CoverageRow(
|
||||
module_id=manifest.id,
|
||||
repository=repository,
|
||||
migration_owned=migration_owned,
|
||||
capability=capability,
|
||||
rationale=(
|
||||
f"Provider `{capability}` is registered and documented."
|
||||
if capability
|
||||
else rationale or "MISSING"
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
report = _report(rows)
|
||||
if args.render:
|
||||
print(report, end="")
|
||||
elif not REPORT_PATH.is_file():
|
||||
errors.append(f"DSAR coverage report is missing: {REPORT_PATH}")
|
||||
elif REPORT_PATH.read_text(encoding="utf-8") != report:
|
||||
errors.append(
|
||||
"DSAR coverage report is stale; review changes and replace it with "
|
||||
"the output of tools/checks/check-dsar-coverage.py --render"
|
||||
)
|
||||
|
||||
if errors:
|
||||
print("\n".join(errors), file=sys.stderr)
|
||||
return 1
|
||||
|
||||
provider_count = sum(row.capability is not None for row in rows)
|
||||
print(
|
||||
"DSAR coverage check passed: "
|
||||
f"{provider_count} providers, {len(rows) - provider_count} reviewed "
|
||||
f"no-store rationales, {len(rows)} active modules."
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
def _exemptions() -> dict[str, str]:
|
||||
values = json.loads(EXEMPTIONS_PATH.read_text(encoding="utf-8"))
|
||||
if not isinstance(values, dict) or any(
|
||||
not isinstance(key, str) or not isinstance(value, str) or not value.strip()
|
||||
for key, value in values.items()
|
||||
):
|
||||
raise ValueError("DSAR coverage exemptions must be non-empty string mappings.")
|
||||
return {key: value.strip() for key, value in values.items()}
|
||||
|
||||
|
||||
def _load_manifests(*, workspace_root: Path, repositories: tuple[dict, ...]):
|
||||
sources: list[Path] = []
|
||||
candidates: list[tuple[str, Path, Path]] = []
|
||||
for repository in repositories:
|
||||
source = workspace_root / repository["path"] / "src"
|
||||
if not source.is_dir():
|
||||
continue
|
||||
sources.append(source)
|
||||
candidates.extend(
|
||||
(repository["name"], source, path)
|
||||
for path in sorted(source.glob("*/backend/manifest.py"))
|
||||
)
|
||||
core_source = workspace_root / "govoplan-core" / "src"
|
||||
sys.path[:0] = [
|
||||
str(core_source),
|
||||
*(str(source) for source in sources if source != core_source),
|
||||
]
|
||||
|
||||
manifests = []
|
||||
errors = []
|
||||
for repository, source, path in candidates:
|
||||
module_name = ".".join(path.relative_to(source).with_suffix("").parts)
|
||||
if MODULE_NAME_PATTERN.fullmatch(module_name) is None:
|
||||
errors.append(f"{repository}: unsafe manifest module name {module_name!r}")
|
||||
continue
|
||||
try:
|
||||
module = importlib.import_module(module_name)
|
||||
manifests.append((repository, module.get_manifest()))
|
||||
except Exception as exc: # pragma: no cover - emitted as check evidence
|
||||
errors.append(f"{repository}: could not load {module_name}: {exc}")
|
||||
return manifests, errors
|
||||
|
||||
|
||||
def _report(rows: list[CoverageRow]) -> str:
|
||||
ordered = sorted(rows, key=lambda row: row.module_id)
|
||||
providers = sum(row.capability is not None for row in ordered)
|
||||
lines = [
|
||||
"# DSAR Provider Coverage",
|
||||
"",
|
||||
"This generated matrix is enforced by `tools/checks/check-dsar-coverage.py`.",
|
||||
"A migration-owning module must register and document its canonical DSAR provider.",
|
||||
"Every other active module requires a reviewed explanation of why it owns no",
|
||||
"persistent subject-data store. Adding a migration invalidates that explanation.",
|
||||
"",
|
||||
f"- Active modules: {len(ordered)}",
|
||||
f"- Registered and documented DSAR providers: {providers}",
|
||||
f"- Reviewed no-store rationales: {len(ordered) - providers}",
|
||||
"- Unexplained coverage gaps: 0",
|
||||
"",
|
||||
"| Module | Repository | Persistence | Coverage | Rationale |",
|
||||
"| --- | --- | --- | --- | --- |",
|
||||
]
|
||||
for row in ordered:
|
||||
lines.append(
|
||||
"| "
|
||||
+ " | ".join(
|
||||
(
|
||||
f"`{row.module_id}`",
|
||||
f"`{row.repository}`",
|
||||
"Migration-owned" if row.migration_owned else "No module migration",
|
||||
"Provider" if row.capability else "Reviewed no-store rationale",
|
||||
row.rationale.replace("|", "\\|"),
|
||||
)
|
||||
)
|
||||
+ " |"
|
||||
)
|
||||
lines.extend(
|
||||
(
|
||||
"",
|
||||
"Provider search, export minimization, retention, and erasure behavior remains",
|
||||
"documented and tested by each owning module. This matrix verifies adoption and",
|
||||
"ownership coverage; Core continues to test disabled providers, partial failure,",
|
||||
"retry, authorization evidence, and horizontally coordinated execution.",
|
||||
"",
|
||||
)
|
||||
)
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -38,6 +38,7 @@ cd "$ROOT"
|
||||
GOVOPLAN_CORE_ROOT="$ROOT" PYTHON="$PYTHON" CHECK_TESTCLIENT_DEPRECATIONS=1 bash "$META_ROOT/tools/checks/check-dependency-hygiene.sh"
|
||||
"$PYTHON" "$META_ROOT/tools/checks/check-contracts.py" --no-impact
|
||||
PYTHONDONTWRITEBYTECODE=1 "$PYTHON" "$META_ROOT/tools/checks/check-manifest-shapes.py" --require-architecture
|
||||
PYTHONDONTWRITEBYTECODE=1 "$PYTHON" "$META_ROOT/tools/checks/check-dsar-coverage.py"
|
||||
|
||||
cd "$META_ROOT"
|
||||
"$PYTHON" tools/inventory/platform-interface-inventory.py --strict-declarations --strict-endpoints
|
||||
@@ -46,6 +47,8 @@ cd "$META_ROOT"
|
||||
"$PYTHON" -m unittest tests.test_module_package_workflows tests.test_package_registry_release
|
||||
"$PYTHON" -m unittest tests.test_deployment_installer
|
||||
"$PYTHON" -m unittest tests.test_capability_fit_evidence
|
||||
"$PYTHON" -m unittest tests.test_capability_fit_generation tests.test_capability_fit_review
|
||||
"$PYTHON" tools/assessments/generate-capability-fit-report.py --check
|
||||
"$PYTHON" -m unittest tests.test_configuration_package_artifacts
|
||||
"$PYTHON" -m unittest tests.test_institutional_governance_journey
|
||||
"$PYTHON" -m unittest tests.test_institutional_service_journey
|
||||
@@ -89,6 +92,9 @@ PY
|
||||
|
||||
"$PYTHON" -c 'import govoplan_core.db.bootstrap; import govoplan_access.backend.admin.service; import govoplan_addresses.backend.manifest; import govoplan_files.backend.router; import govoplan_mail.backend.sending.imap; print("targeted backend imports passed")'
|
||||
"$META_ROOT/tools/checks/check_dependency_boundaries.py"
|
||||
"$PYTHON" "$META_ROOT/tools/checks/check-shared-webui-layouts.py"
|
||||
"$PYTHON" "$META_ROOT/tools/checks/check-shared-webui-primitives.py"
|
||||
"$PYTHON" "$META_ROOT/tools/checks/check-shared-webui-foundations.py"
|
||||
"$PYTHON" -m unittest tests.test_module_system
|
||||
"$PYTHON" -m unittest discover -s /mnt/DATA/git/govoplan-connectors/tests
|
||||
"$PYTHON" -m unittest discover -s /mnt/DATA/git/govoplan-datasources/tests
|
||||
@@ -96,9 +102,11 @@ PY
|
||||
"$PYTHON" -m unittest discover -s /mnt/DATA/git/govoplan-workflow-engine/tests
|
||||
"$PYTHON" -m unittest discover -s /mnt/DATA/git/govoplan-workflow/tests
|
||||
"$PYTHON" -m unittest discover -s /mnt/DATA/git/govoplan-views/tests
|
||||
"$PYTHON" -m unittest discover -s /mnt/DATA/git/govoplan-quick-access/tests
|
||||
"$PYTHON" -m unittest discover -s /mnt/DATA/git/govoplan-dashboard/tests
|
||||
"$PYTHON" -m unittest discover -s /mnt/DATA/git/govoplan-postbox/tests
|
||||
"$PYTHON" -m unittest discover -s /mnt/DATA/git/govoplan-portal/tests
|
||||
"$PYTHON" -m unittest discover -s /mnt/DATA/git/govoplan-payments/tests
|
||||
"$PYTHON" -m unittest discover -s /mnt/DATA/git/govoplan-forms/tests
|
||||
"$PYTHON" -m unittest discover -s /mnt/DATA/git/govoplan-forms-runtime/tests
|
||||
"$PYTHON" -m unittest discover -s /mnt/DATA/git/govoplan-cases/tests
|
||||
@@ -107,15 +115,24 @@ PY
|
||||
"$PYTHON" -m unittest discover -s /mnt/DATA/git/govoplan-approvals/tests
|
||||
"$PYTHON" -m unittest discover -s /mnt/DATA/git/govoplan-identity-trust/tests
|
||||
"$PYTHON" -m unittest discover -s /mnt/DATA/git/govoplan-encryption/tests
|
||||
"$PYTHON" -m pytest -q /mnt/DATA/git/govoplan-wiki/tests
|
||||
"$PYTHON" -m pytest -q /mnt/DATA/git/govoplan-campaign/tests/test_approval_gate.py
|
||||
"$PYTHON" "$META_ROOT/tools/checks/check-datasource-composition.py"
|
||||
"$PYTHON" "$META_ROOT/tools/checks/check-sanctions-screening-composition.py"
|
||||
"$PYTHON" -m unittest discover -s /mnt/DATA/git/govoplan-mail/tests
|
||||
"$PYTHON" -m unittest tests.test_api_smoke.ApiSmokeTests.test_mailbox_message_listing_reports_total_count
|
||||
|
||||
cd "$ROOT/webui"
|
||||
"$NPM" run test:layout-primitives
|
||||
"$NPM" run test:mail-components
|
||||
"$NPM" run test:module-capabilities
|
||||
"$NPM" run test:module-permutations
|
||||
"$NPM" run test:conformance
|
||||
|
||||
"$WEBUI_BIN/tsc" -p /mnt/DATA/git/govoplan-payments/webui/tsconfig.json
|
||||
|
||||
cd /mnt/DATA/git/govoplan-payments/webui
|
||||
"$NPM" run test:interface-pattern
|
||||
|
||||
cd /mnt/DATA/git/govoplan-dataflow/webui
|
||||
"$NPM" run test:structure
|
||||
@@ -129,6 +146,9 @@ cd /mnt/DATA/git/govoplan-workflow/webui
|
||||
cd /mnt/DATA/git/govoplan-dashboard/webui
|
||||
"$NPM" run test:dashboard-layout
|
||||
|
||||
cd /mnt/DATA/git/govoplan-approvals/webui
|
||||
"$NPM" run test:workspace-layout
|
||||
|
||||
cd /mnt/DATA/git/govoplan-postbox/webui
|
||||
"$NPM" run test:ui-structure
|
||||
|
||||
@@ -138,3 +158,9 @@ cd /mnt/DATA/git/govoplan-mail/webui
|
||||
cd /mnt/DATA/git/govoplan-campaign/webui
|
||||
"$NPM" run test:policy-ui
|
||||
"$NPM" run test:template-preview
|
||||
"$NPM" run test:accessibility-contract
|
||||
"$NPM" run test:campaign-collaboration
|
||||
"$NPM" run test:campaign-work
|
||||
|
||||
cd /mnt/DATA/git/govoplan-wiki/webui
|
||||
"$NPM" run test:interface-pattern
|
||||
|
||||
@@ -17,6 +17,70 @@ MODULE_NAME_PATTERN = re.compile(
|
||||
r"[A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)*"
|
||||
)
|
||||
REQUIRED_DOCUMENTATION_TYPES = frozenset({"admin", "user"})
|
||||
CANONICAL_PRODUCT_AREAS = {
|
||||
"work": (
|
||||
"i18n:govoplan-core.product_area.work",
|
||||
"list-checks",
|
||||
"i18n:govoplan-core.product_area.work_description",
|
||||
10,
|
||||
),
|
||||
"services-cases": (
|
||||
"i18n:govoplan-core.product_area.services_cases",
|
||||
"landmark",
|
||||
"i18n:govoplan-core.product_area.services_cases_description",
|
||||
20,
|
||||
),
|
||||
"records-documents": (
|
||||
"i18n:govoplan-core.product_area.records_documents",
|
||||
"folder",
|
||||
"i18n:govoplan-core.product_area.records_documents_description",
|
||||
30,
|
||||
),
|
||||
"communication": (
|
||||
"i18n:govoplan-core.product_area.communication",
|
||||
"mail",
|
||||
"i18n:govoplan-core.product_area.communication_description",
|
||||
40,
|
||||
),
|
||||
"meetings-decisions": (
|
||||
"i18n:govoplan-core.product_area.meetings_decisions",
|
||||
"calendar",
|
||||
"i18n:govoplan-core.product_area.meetings_decisions_description",
|
||||
50,
|
||||
),
|
||||
"data-assurance": (
|
||||
"i18n:govoplan-core.product_area.data_assurance",
|
||||
"database-zap",
|
||||
"i18n:govoplan-core.product_area.data_assurance_description",
|
||||
60,
|
||||
),
|
||||
"people-responsibility": (
|
||||
"i18n:govoplan-core.product_area.people_responsibility",
|
||||
"users",
|
||||
"i18n:govoplan-core.product_area.people_responsibility_description",
|
||||
70,
|
||||
),
|
||||
}
|
||||
# These surfaces are intentionally global, administrative, security-policy, or
|
||||
# shell infrastructure. They remain discoverable through their dedicated shell
|
||||
# affordance or through "All available tools" instead of a business area.
|
||||
PRODUCT_AREA_EXEMPT_MODULES = frozenset(
|
||||
{
|
||||
"access",
|
||||
"admin",
|
||||
"audit",
|
||||
"dashboard",
|
||||
"docs",
|
||||
"encryption",
|
||||
"identity_trust",
|
||||
"ops",
|
||||
"policy",
|
||||
"quick_access",
|
||||
"search",
|
||||
"tenancy",
|
||||
"views",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
@@ -113,6 +177,42 @@ def main() -> int:
|
||||
)
|
||||
continue
|
||||
|
||||
frontend = manifest.frontend
|
||||
has_user_facing_surface = frontend is not None and bool(
|
||||
frontend.routes
|
||||
or frontend.public_routes
|
||||
or frontend.nav_items
|
||||
or frontend.settings_routes
|
||||
)
|
||||
if (
|
||||
has_user_facing_surface
|
||||
and not frontend.product_areas
|
||||
and manifest.id not in PRODUCT_AREA_EXEMPT_MODULES
|
||||
):
|
||||
errors.append(
|
||||
f"{repository_name}: user-facing module {manifest.id!r} has no "
|
||||
"ProductAreaContribution and is not an explicit global/technical exemption"
|
||||
)
|
||||
if frontend is not None:
|
||||
for contribution in frontend.product_areas:
|
||||
expected = CANONICAL_PRODUCT_AREAS.get(contribution.id)
|
||||
actual = (
|
||||
contribution.label,
|
||||
contribution.icon,
|
||||
contribution.description,
|
||||
contribution.order,
|
||||
)
|
||||
if expected is None:
|
||||
errors.append(
|
||||
f"{repository_name}: module {manifest.id!r} uses unknown product "
|
||||
f"area {contribution.id!r}"
|
||||
)
|
||||
elif actual != expected:
|
||||
errors.append(
|
||||
f"{repository_name}: module {manifest.id!r} redefines canonical "
|
||||
f"product area {contribution.id!r}; expected {expected!r}, found {actual!r}"
|
||||
)
|
||||
|
||||
repository_root = manifest_path.parents[3]
|
||||
if manifest.architecture is None:
|
||||
if args.require_architecture:
|
||||
@@ -142,6 +242,23 @@ def main() -> int:
|
||||
print("\n".join(errors), file=sys.stderr)
|
||||
return 1
|
||||
|
||||
contributed_product_areas = {
|
||||
contribution.id
|
||||
for manifest in manifests
|
||||
if manifest.frontend is not None
|
||||
for contribution in manifest.frontend.product_areas
|
||||
}
|
||||
missing_product_areas = sorted(
|
||||
set(CANONICAL_PRODUCT_AREAS) - contributed_product_areas
|
||||
)
|
||||
if missing_product_areas:
|
||||
print(
|
||||
"Canonical product areas have no contributing module: "
|
||||
+ ", ".join(missing_product_areas),
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 1
|
||||
|
||||
registry = PlatformRegistry()
|
||||
try:
|
||||
for manifest in manifests:
|
||||
|
||||
@@ -26,6 +26,8 @@ cd "$ROOT"
|
||||
"$PYTHON" "$META_ROOT/tools/checks/check_dependency_boundaries.py"
|
||||
|
||||
cd "$META_ROOT"
|
||||
"$PYTHON" -m unittest tests.test_capability_fit_generation
|
||||
"$PYTHON" tools/assessments/generate-capability-fit-report.py --check
|
||||
"$PYTHON" -m unittest tests.test_configuration_package_artifacts
|
||||
PYTHONPATH="$META_ROOT/../govoplan-portal/src:$META_ROOT/../govoplan-forms/src:$META_ROOT/../govoplan-forms-runtime/src:$META_ROOT/../govoplan-cases/src:$ROOT/src${PYTHONPATH:+:$PYTHONPATH}" \
|
||||
"$PYTHON" -m unittest tests.test_institutional_service_journey
|
||||
|
||||
@@ -262,6 +262,8 @@ cd "$WORK_ROOT/govoplan-campaign/webui"
|
||||
"$NPM" run test:policy-ui
|
||||
"$NPM" run test:template-preview
|
||||
"$NPM" run test:import-utils
|
||||
"$NPM" run test:campaign-collaboration
|
||||
"$NPM" run test:campaign-work
|
||||
|
||||
echo
|
||||
echo "Release integration check passed."
|
||||
|
||||
@@ -0,0 +1,306 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Prove the governed Connectors -> Risk Compliance sanctions journey."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from types import SimpleNamespace
|
||||
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from govoplan_connectors.backend.db.models import (
|
||||
ConnectorSanctionsAcquisitionRun,
|
||||
ConnectorSanctionsSnapshot,
|
||||
)
|
||||
from govoplan_connectors.backend.sanctions_sources import (
|
||||
SANCTIONS_READ_SCOPE as CONNECTOR_SANCTIONS_READ_SCOPE,
|
||||
SANCTIONS_REFRESH_SCOPE,
|
||||
SYNTHETIC_PROVIDER_ID,
|
||||
)
|
||||
from govoplan_core.auth import ApiPrincipal
|
||||
from govoplan_core.core.access import PrincipalRef
|
||||
from govoplan_core.core.modules import ModuleContext
|
||||
from govoplan_core.core.recovery import RecoveryCheckpoint, RecoveryOperation
|
||||
from govoplan_core.core.runtime_coordination import (
|
||||
DistributedLease,
|
||||
RuntimeIdentity,
|
||||
bind_process_runtime_identity,
|
||||
)
|
||||
from govoplan_core.core.sanctions import (
|
||||
SanctionsScreeningFreshnessRequest,
|
||||
SanctionsScreeningPolicy,
|
||||
SanctionsScreeningRequest,
|
||||
SanctionsScreeningSubject,
|
||||
sanctions_screening_provider,
|
||||
sanctions_snapshot_provider,
|
||||
)
|
||||
from govoplan_core.db.base import Base
|
||||
from govoplan_core.server.registry import build_platform_registry
|
||||
from govoplan_risk_compliance.backend.db.models import (
|
||||
RiskAssuranceEdge,
|
||||
RiskAssuranceNode,
|
||||
RiskSanctionsAddress,
|
||||
RiskSanctionsAlias,
|
||||
RiskSanctionsDate,
|
||||
RiskSanctionsEntry,
|
||||
RiskSanctionsIdentifier,
|
||||
RiskSanctionsListSnapshot,
|
||||
RiskScreeningCandidate,
|
||||
RiskScreeningDisposition,
|
||||
RiskScreeningException,
|
||||
RiskScreeningRun,
|
||||
RiskScreeningSubjectSnapshot,
|
||||
)
|
||||
from govoplan_risk_compliance.backend.permissions import (
|
||||
SANCTIONS_ADMIN_SCOPE,
|
||||
SANCTIONS_READ_SCOPE,
|
||||
SANCTIONS_REVIEW_SCOPE,
|
||||
SANCTIONS_SCREEN_SCOPE,
|
||||
)
|
||||
from govoplan_risk_compliance.backend.review import (
|
||||
DispositionInput,
|
||||
record_disposition,
|
||||
)
|
||||
from govoplan_risk_compliance.backend.sanctions_catalog import (
|
||||
import_connector_snapshot,
|
||||
)
|
||||
from govoplan_risk_compliance.backend.screening import (
|
||||
get_screening_run,
|
||||
list_rescreening_requirements,
|
||||
)
|
||||
|
||||
|
||||
TABLES = (
|
||||
DistributedLease.__table__,
|
||||
RecoveryOperation.__table__,
|
||||
RecoveryCheckpoint.__table__,
|
||||
ConnectorSanctionsAcquisitionRun.__table__,
|
||||
ConnectorSanctionsSnapshot.__table__,
|
||||
RiskAssuranceNode.__table__,
|
||||
RiskAssuranceEdge.__table__,
|
||||
RiskSanctionsListSnapshot.__table__,
|
||||
RiskSanctionsEntry.__table__,
|
||||
RiskSanctionsAlias.__table__,
|
||||
RiskSanctionsIdentifier.__table__,
|
||||
RiskSanctionsDate.__table__,
|
||||
RiskSanctionsAddress.__table__,
|
||||
RiskScreeningSubjectSnapshot.__table__,
|
||||
RiskScreeningRun.__table__,
|
||||
RiskScreeningCandidate.__table__,
|
||||
RiskScreeningDisposition.__table__,
|
||||
RiskScreeningException.__table__,
|
||||
)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
registry = build_platform_registry(("connectors", "risk_compliance"))
|
||||
registry.configure_capability_context(
|
||||
ModuleContext(registry=registry, settings=object())
|
||||
)
|
||||
snapshot_provider = sanctions_snapshot_provider(registry)
|
||||
screening_provider = sanctions_screening_provider(registry)
|
||||
refresh_source = getattr(snapshot_provider, "refresh_source", None)
|
||||
if snapshot_provider is None or not callable(refresh_source):
|
||||
raise RuntimeError("Connectors sanctions acquisition is unavailable.")
|
||||
if screening_provider is None:
|
||||
raise RuntimeError("Risk Compliance sanctions screening is unavailable.")
|
||||
|
||||
engine = create_engine("sqlite:///:memory:")
|
||||
Base.metadata.create_all(engine, tables=TABLES)
|
||||
bind_process_runtime_identity(
|
||||
RuntimeIdentity(
|
||||
installation_id="sanctions-composition-check",
|
||||
node_id="sanctions-worker",
|
||||
incarnation="sanctions-worker-incarnation",
|
||||
role="worker",
|
||||
software_version="test",
|
||||
composition_hash="d" * 64,
|
||||
)
|
||||
)
|
||||
try:
|
||||
with Session(engine) as session:
|
||||
operator = _principal("operator-1", operational=True)
|
||||
reviewer = _principal("reviewer-1", operational=False)
|
||||
|
||||
acquired = refresh_source(
|
||||
session,
|
||||
operator,
|
||||
provider_id=SYNTHETIC_PROVIDER_ID,
|
||||
idempotency_key="synthetic-sanctions-2026-08-01",
|
||||
)
|
||||
replay = refresh_source(
|
||||
session,
|
||||
operator,
|
||||
provider_id=SYNTHETIC_PROVIDER_ID,
|
||||
idempotency_key="synthetic-sanctions-2026-08-01",
|
||||
)
|
||||
if acquired.status != "succeeded" or acquired.snapshot is None:
|
||||
raise RuntimeError(f"Synthetic acquisition failed: {acquired!r}")
|
||||
if (
|
||||
replay.run_id != acquired.run_id
|
||||
or replay.snapshot is None
|
||||
or replay.snapshot.ref != acquired.snapshot.ref
|
||||
or replay.snapshot.sha256 != acquired.snapshot.sha256
|
||||
):
|
||||
raise RuntimeError("Acquisition idempotency did not replay exact evidence.")
|
||||
|
||||
imported, created = import_connector_snapshot(
|
||||
session,
|
||||
operator,
|
||||
registry=registry,
|
||||
connector_snapshot_ref=acquired.snapshot.ref,
|
||||
)
|
||||
imported_replay, replay_created = import_connector_snapshot(
|
||||
session,
|
||||
operator,
|
||||
registry=registry,
|
||||
connector_snapshot_ref=acquired.snapshot.ref,
|
||||
)
|
||||
if not created or replay_created or imported_replay.id != imported.id:
|
||||
raise RuntimeError("Risk Compliance snapshot import is not idempotent.")
|
||||
|
||||
subject = SanctionsScreeningSubject(
|
||||
subject_type="person",
|
||||
primary_name="Alex Example",
|
||||
subject_ref="party:fixture-person-1",
|
||||
)
|
||||
policy = SanctionsScreeningPolicy(failure_policy="block")
|
||||
request = SanctionsScreeningRequest(
|
||||
list_snapshot_id=imported.id,
|
||||
idempotency_key="fixture-party-screening-1",
|
||||
subject=subject,
|
||||
policy=policy,
|
||||
)
|
||||
screened = screening_provider.request_screening(
|
||||
session,
|
||||
operator,
|
||||
request,
|
||||
)
|
||||
screened_replay = screening_provider.request_screening(
|
||||
session,
|
||||
operator,
|
||||
request,
|
||||
)
|
||||
if not screened.created or screened_replay.created:
|
||||
raise RuntimeError("Screening request idempotency is not stable.")
|
||||
if screened.evidence.ref != screened_replay.evidence.ref:
|
||||
raise RuntimeError("Screening replay returned different evidence.")
|
||||
if screened.evidence.outcome != "potential" or screened.evidence.candidate_count != 1:
|
||||
raise RuntimeError(f"Synthetic match was not reviewable: {screened.evidence!r}")
|
||||
|
||||
run = get_screening_run(
|
||||
session,
|
||||
operator,
|
||||
run_id=screened.evidence.run_id,
|
||||
)
|
||||
candidate, disposition = record_disposition(
|
||||
session,
|
||||
reviewer,
|
||||
candidate_id=run.candidates[0].id,
|
||||
disposition=DispositionInput(
|
||||
decision="false_positive",
|
||||
reason="Independent fixture evidence excludes the screened party.",
|
||||
evidence_refs=(acquired.snapshot.raw_evidence_ref,),
|
||||
),
|
||||
)
|
||||
if candidate.review_status != "false_positive":
|
||||
raise RuntimeError("Independent review did not resolve the candidate.")
|
||||
if disposition.separation_status != "independent":
|
||||
raise RuntimeError("Reviewer separation evidence was not retained.")
|
||||
|
||||
cleared = screening_provider.check_freshness(
|
||||
session,
|
||||
operator,
|
||||
SanctionsScreeningFreshnessRequest(
|
||||
evidence_ref=screened.evidence.ref,
|
||||
current_subject=subject,
|
||||
expected_list_snapshot_id=imported.id,
|
||||
policy=policy,
|
||||
),
|
||||
)
|
||||
if not cleared.fresh or cleared.gate_decision != "allow":
|
||||
raise RuntimeError(f"Reviewed evidence did not clear the gate: {cleared!r}")
|
||||
|
||||
# Acquisition and review are separate durable commands in production.
|
||||
session.commit()
|
||||
session.expire_all()
|
||||
|
||||
refreshed = refresh_source(
|
||||
session,
|
||||
operator,
|
||||
provider_id=SYNTHETIC_PROVIDER_ID,
|
||||
idempotency_key="synthetic-sanctions-2026-08-02",
|
||||
)
|
||||
if refreshed.snapshot is None or refreshed.snapshot.ref == acquired.snapshot.ref:
|
||||
raise RuntimeError("A new acquisition did not create new immutable evidence.")
|
||||
current, current_created = import_connector_snapshot(
|
||||
session,
|
||||
operator,
|
||||
registry=registry,
|
||||
connector_snapshot_ref=refreshed.snapshot.ref,
|
||||
)
|
||||
if not current_created:
|
||||
raise RuntimeError("The refreshed list state was not imported separately.")
|
||||
|
||||
stale = screening_provider.check_freshness(
|
||||
session,
|
||||
operator,
|
||||
SanctionsScreeningFreshnessRequest(
|
||||
evidence_ref=screened.evidence.ref,
|
||||
current_subject=subject,
|
||||
expected_list_snapshot_id=current.id,
|
||||
policy=policy,
|
||||
),
|
||||
)
|
||||
if stale.fresh or stale.gate_decision != "block":
|
||||
raise RuntimeError(f"Changed source evidence did not close the gate: {stale!r}")
|
||||
if "source_snapshot_changed" not in stale.reasons:
|
||||
raise RuntimeError("Source change provenance was not reported.")
|
||||
requirements = list_rescreening_requirements(session, operator)
|
||||
if screened.evidence.run_id not in {item.run.id for item in requirements}:
|
||||
raise RuntimeError("The stale screening is absent from the rescreening queue.")
|
||||
|
||||
session.commit()
|
||||
if session.query(RecoveryOperation).count() != 2:
|
||||
raise RuntimeError("Connector acquisition recovery evidence is incomplete.")
|
||||
if session.query(RiskScreeningDisposition).count() != 1:
|
||||
raise RuntimeError("Disposition evidence was duplicated or lost.")
|
||||
finally:
|
||||
bind_process_runtime_identity(None)
|
||||
engine.dispose()
|
||||
|
||||
print(
|
||||
"Connectors -> immutable sanctions snapshot -> Risk Compliance review "
|
||||
"and rescreening composition passed."
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
def _principal(account_id: str, *, operational: bool) -> ApiPrincipal:
|
||||
scopes = {
|
||||
SANCTIONS_READ_SCOPE,
|
||||
SANCTIONS_REVIEW_SCOPE,
|
||||
}
|
||||
if operational:
|
||||
scopes.update(
|
||||
{
|
||||
CONNECTOR_SANCTIONS_READ_SCOPE,
|
||||
SANCTIONS_REFRESH_SCOPE,
|
||||
SANCTIONS_ADMIN_SCOPE,
|
||||
SANCTIONS_SCREEN_SCOPE,
|
||||
}
|
||||
)
|
||||
return ApiPrincipal(
|
||||
principal=PrincipalRef(
|
||||
account_id=account_id,
|
||||
membership_id=f"membership-{account_id}",
|
||||
tenant_id="tenant-1",
|
||||
scopes=frozenset(scopes),
|
||||
),
|
||||
account=SimpleNamespace(id=account_id),
|
||||
user=SimpleNamespace(id=f"membership-{account_id}"),
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,118 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Enforce Core ownership of WebUI visual foundations."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pathlib
|
||||
import re
|
||||
import sys
|
||||
|
||||
|
||||
META_ROOT = pathlib.Path(__file__).resolve().parents[2]
|
||||
REPOS_ROOT = META_ROOT.parent
|
||||
TOKENS_PATH = REPOS_ROOT / "govoplan-core/webui/src/styles/tokens.css"
|
||||
|
||||
RAW_HEX_COLOR = re.compile(r"#[0-9a-fA-F]{3,8}\b")
|
||||
RAW_COLOR_FUNCTION = re.compile(r"\b(?:rgb|rgba|hsl|hsla)\((?!\s*var\()", re.IGNORECASE)
|
||||
RADIUS_DECLARATION = re.compile(r"border-radius\s*:\s*([^;}]+)")
|
||||
MEDIA_MAX_WIDTH = re.compile(r"@media[^\n{]*\(max-width\s*:\s*(\d+)px\)")
|
||||
|
||||
RESPONSIVE_BANDS = {560, 600, 680, 760, 900, 1100, 1280}
|
||||
REQUIRED_TOKENS = {
|
||||
"--radius-hairline",
|
||||
"--radius-tight",
|
||||
"--radius-xs",
|
||||
"--radius-sm",
|
||||
"--radius-compact",
|
||||
"--radius-md",
|
||||
"--radius-lg",
|
||||
"--radius-xl",
|
||||
"--radius-round",
|
||||
"--radius-pill",
|
||||
"--shadow-drawer-side",
|
||||
"--shadow-drawer-bottom",
|
||||
"--action-primary-bg",
|
||||
"--action-primary-border",
|
||||
"--action-primary-text",
|
||||
"--action-danger-bg",
|
||||
"--action-danger-text",
|
||||
"--badge-accent-text",
|
||||
"--data-category-blue",
|
||||
"--data-category-green",
|
||||
"--data-category-amber",
|
||||
"--data-category-purple",
|
||||
"--data-category-rose",
|
||||
*(f"--data-series-{index}" for index in range(1, 9)),
|
||||
}
|
||||
|
||||
|
||||
def line_number(source: str, offset: int) -> int:
|
||||
return source.count("\n", 0, offset) + 1
|
||||
|
||||
|
||||
def css_files() -> list[pathlib.Path]:
|
||||
files: list[pathlib.Path] = []
|
||||
for repository in sorted(REPOS_ROOT.glob("govoplan-*")):
|
||||
styles = repository / "webui/src"
|
||||
if styles.is_dir():
|
||||
files.extend(sorted(styles.rglob("*.css")))
|
||||
return files
|
||||
|
||||
|
||||
def display_path(path: pathlib.Path) -> str:
|
||||
return str(path.relative_to(REPOS_ROOT))
|
||||
|
||||
|
||||
def main() -> int:
|
||||
errors: list[str] = []
|
||||
tokens = TOKENS_PATH.read_text(encoding="utf-8")
|
||||
for token in sorted(REQUIRED_TOKENS):
|
||||
if f"{token}:" not in tokens:
|
||||
errors.append(f"{display_path(TOKENS_PATH)}: missing required foundation token {token}")
|
||||
|
||||
files = css_files()
|
||||
for path in files:
|
||||
source = path.read_text(encoding="utf-8")
|
||||
owns_literals = path == TOKENS_PATH
|
||||
|
||||
if not owns_literals:
|
||||
for pattern, label in (
|
||||
(RAW_HEX_COLOR, "raw color"),
|
||||
(RAW_COLOR_FUNCTION, "raw color function"),
|
||||
):
|
||||
for match in pattern.finditer(source):
|
||||
errors.append(
|
||||
f"{display_path(path)}:{line_number(source, match.start())}: "
|
||||
f"{label} must use a Core theme token"
|
||||
)
|
||||
|
||||
for match in RADIUS_DECLARATION.finditer(source):
|
||||
value = match.group(1).strip()
|
||||
if "var(" not in value and value not in {"0", "inherit", "initial", "unset"}:
|
||||
errors.append(
|
||||
f"{display_path(path)}:{line_number(source, match.start())}: "
|
||||
f"border radius {value!r} must use a Core radius token"
|
||||
)
|
||||
|
||||
for match in MEDIA_MAX_WIDTH.finditer(source):
|
||||
width = int(match.group(1))
|
||||
if width not in RESPONSIVE_BANDS:
|
||||
errors.append(
|
||||
f"{display_path(path)}:{line_number(source, match.start())}: "
|
||||
f"{width}px is not a shared responsive band; use one of "
|
||||
f"{', '.join(f'{value}px' for value in sorted(RESPONSIVE_BANDS))}"
|
||||
)
|
||||
|
||||
if errors:
|
||||
print("\n".join(errors))
|
||||
return 1
|
||||
|
||||
print(
|
||||
"Shared WebUI foundation contract passed for "
|
||||
f"{len(files)} stylesheets and {len(RESPONSIVE_BANDS)} responsive bands."
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,208 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Keep raw module page frames from growing while shared layouts are adopted."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pathlib
|
||||
import re
|
||||
import sys
|
||||
|
||||
|
||||
META_ROOT = pathlib.Path(__file__).resolve().parents[2]
|
||||
REPOS_ROOT = META_ROOT.parent
|
||||
BASELINE_PATH = pathlib.Path(__file__).with_name("shared-webui-layout-baseline.txt")
|
||||
WORKSPACE_BASELINE_PATH = pathlib.Path(__file__).with_name(
|
||||
"shared-webui-workspace-baseline.txt"
|
||||
)
|
||||
RAW_PAGE_FRAME = 'className="content-pad workspace-data-page'
|
||||
RAW_WORKSPACE = re.compile(r'<div\s+className="workspace(?:\s|\")')
|
||||
PAGE_LAYOUT_USAGE = re.compile(r"<PageLayout\b")
|
||||
SEMANTIC_PAGE_LAYOUT_USAGE = re.compile(
|
||||
r"<PageLayout\s+(?:\n\s*)?archetype="
|
||||
)
|
||||
LOCAL_PAGE_LAYOUT = re.compile(r"\b(?:function|class|const)\s+PageLayout\b")
|
||||
LOCAL_WORKSPACE_LAYOUT = re.compile(
|
||||
r"\b(?:function|class|const)\s+WorkspaceLayout\b"
|
||||
)
|
||||
CENTRAL_LAYOUT = pathlib.Path("govoplan-core/webui/src/components/PageLayout.tsx")
|
||||
CENTRAL_WORKSPACE_LAYOUT = pathlib.Path(
|
||||
"govoplan-core/webui/src/components/WorkspaceLayout.tsx"
|
||||
)
|
||||
CENTRAL_ACTION_BARS = {
|
||||
pathlib.Path("govoplan-core/webui/src/components/PageActionBar.tsx"),
|
||||
pathlib.Path("govoplan-core/webui/src/components/WorkspaceActionBar.tsx"),
|
||||
}
|
||||
SEMANTIC_ACTION_USAGE = re.compile(r"<(?:Page|Workspace)ActionBar\b")
|
||||
EDITOR_ACTION_USAGE = re.compile(
|
||||
r"<(?:Page|Workspace)ActionBar\b[\s\S]{0,1200}?variant=\"editor\""
|
||||
)
|
||||
PANEL_HEADER_ACTION_TOOLBAR = re.compile(
|
||||
r"<ActionToolbar\b[^>]*\bsurface=\"panel-header\""
|
||||
)
|
||||
UNSAVED_GUARD_MARKERS = (
|
||||
"useUnsavedDraftGuard",
|
||||
"useCampaignDraftEditor",
|
||||
"useRegisterUnsavedChanges",
|
||||
"semantic-editor-guard:",
|
||||
)
|
||||
|
||||
|
||||
def baseline_paths(path: pathlib.Path) -> set[pathlib.Path]:
|
||||
return {
|
||||
pathlib.Path(line.strip())
|
||||
for line in path.read_text(encoding="utf-8").splitlines()
|
||||
if line.strip() and not line.lstrip().startswith("#")
|
||||
}
|
||||
|
||||
|
||||
def source_paths() -> list[pathlib.Path]:
|
||||
paths: list[pathlib.Path] = []
|
||||
for repository in sorted(REPOS_ROOT.glob("govoplan*")):
|
||||
source_root = repository / "webui" / "src"
|
||||
if source_root.is_dir():
|
||||
paths.extend(sorted(source_root.rglob("*.tsx")))
|
||||
return paths
|
||||
|
||||
|
||||
def relative(path: pathlib.Path) -> pathlib.Path:
|
||||
return path.relative_to(REPOS_ROOT)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
sources = source_paths()
|
||||
source_text = {relative(path): path.read_text(encoding="utf-8") for path in sources}
|
||||
page_consumers = {
|
||||
path for path, text in source_text.items()
|
||||
if path != CENTRAL_LAYOUT and PAGE_LAYOUT_USAGE.search(text)
|
||||
}
|
||||
workspace_consumers = {
|
||||
path for path, text in source_text.items()
|
||||
if path != CENTRAL_WORKSPACE_LAYOUT and "<WorkspaceLayout" in text
|
||||
}
|
||||
action_consumers = {
|
||||
path for path, text in source_text.items()
|
||||
if path not in CENTRAL_ACTION_BARS and SEMANTIC_ACTION_USAGE.search(text)
|
||||
}
|
||||
editor_consumers = {
|
||||
path for path, text in source_text.items()
|
||||
if path not in CENTRAL_ACTION_BARS and EDITOR_ACTION_USAGE.search(text)
|
||||
}
|
||||
raw_frames = {path for path, text in source_text.items() if RAW_PAGE_FRAME in text}
|
||||
baseline = baseline_paths(BASELINE_PATH)
|
||||
available_baseline = {
|
||||
path for path in baseline if (REPOS_ROOT / path.parts[0]).is_dir()
|
||||
}
|
||||
errors: list[str] = []
|
||||
|
||||
unexpected = sorted(raw_frames - available_baseline)
|
||||
if unexpected:
|
||||
errors.append("New raw page frames must use @govoplan/core-webui PageLayout:")
|
||||
errors.extend(f"- {path}" for path in unexpected)
|
||||
|
||||
resolved = sorted(available_baseline - raw_frames)
|
||||
if resolved:
|
||||
errors.append("Remove migrated page frames from the shared-layout baseline:")
|
||||
errors.extend(f"- {path}" for path in resolved)
|
||||
|
||||
raw_workspaces = {
|
||||
path for path, text in source_text.items() if RAW_WORKSPACE.search(text)
|
||||
}
|
||||
workspace_baseline = baseline_paths(WORKSPACE_BASELINE_PATH)
|
||||
available_workspace_baseline = {
|
||||
path
|
||||
for path in workspace_baseline
|
||||
if (REPOS_ROOT / path.parts[0]).is_dir()
|
||||
}
|
||||
unexpected_workspaces = sorted(raw_workspaces - available_workspace_baseline)
|
||||
if unexpected_workspaces:
|
||||
errors.append("New raw workspaces must use @govoplan/core-webui WorkspaceLayout:")
|
||||
errors.extend(f"- {path}" for path in unexpected_workspaces)
|
||||
|
||||
resolved_workspaces = sorted(available_workspace_baseline - raw_workspaces)
|
||||
if resolved_workspaces:
|
||||
errors.append("Remove migrated workspaces from the shared-workspace baseline:")
|
||||
errors.extend(f"- {path}" for path in resolved_workspaces)
|
||||
|
||||
for path, text in source_text.items():
|
||||
if path != CENTRAL_LAYOUT and LOCAL_PAGE_LAYOUT.search(text):
|
||||
errors.append(f"Module-local PageLayout definition is not allowed: {path}")
|
||||
if path != CENTRAL_WORKSPACE_LAYOUT and LOCAL_WORKSPACE_LAYOUT.search(text):
|
||||
errors.append(f"Module-local WorkspaceLayout definition is not allowed: {path}")
|
||||
|
||||
page_layout_count = len(PAGE_LAYOUT_USAGE.findall(text))
|
||||
semantic_layout_count = len(SEMANTIC_PAGE_LAYOUT_USAGE.findall(text))
|
||||
if page_layout_count and semantic_layout_count != page_layout_count:
|
||||
errors.append(
|
||||
"Every PageLayout must declare its semantic archetype immediately "
|
||||
f"after the component name: {path} ({semantic_layout_count}/{page_layout_count})"
|
||||
)
|
||||
if (
|
||||
page_layout_count
|
||||
and "actions=" in text
|
||||
and path != pathlib.Path("govoplan-core/webui/src/components/admin/AdminPageLayout.tsx")
|
||||
and "<PageActionBar" not in text
|
||||
and "semantic-page-actions: delegated" not in text
|
||||
):
|
||||
errors.append(
|
||||
f"Headed page actions must use the semantic PageActionBar: {path}"
|
||||
)
|
||||
if "<PageActionBar" in text and "consequentialActions=" in text:
|
||||
errors.append(
|
||||
f"Ambiguous consequential action slots are forbidden; use destructiveActions: {path}"
|
||||
)
|
||||
|
||||
if (
|
||||
path not in CENTRAL_ACTION_BARS
|
||||
and PANEL_HEADER_ACTION_TOOLBAR.search(text)
|
||||
):
|
||||
errors.append(
|
||||
"Panel-header actions must use WorkspaceActionBar so their ordering, "
|
||||
f"state, and destructive separation remain semantic: {path}"
|
||||
)
|
||||
|
||||
if "<WorkspaceFrame" in text and not SEMANTIC_ACTION_USAGE.search(text):
|
||||
errors.append(
|
||||
f"WorkspaceFrame routes must declare a semantic page or pane action bar: {path}"
|
||||
)
|
||||
|
||||
for path in editor_consumers:
|
||||
text = source_text[path]
|
||||
for required in ('variant="editor"', "state=", "discardAction=", "saveAction="):
|
||||
if required not in text:
|
||||
errors.append(f"Editor page is missing {required}: {path}")
|
||||
if not any(guard in text for guard in UNSAVED_GUARD_MARKERS):
|
||||
errors.append(f"Editor page is missing an unsaved-change guard: {path}")
|
||||
|
||||
for path, text in source_text.items():
|
||||
if "destructiveActions=" in text and 'variant="danger"' not in text:
|
||||
errors.append(f"Destructive page actions must contain a danger action: {path}")
|
||||
|
||||
core_index = REPOS_ROOT / "govoplan-core/webui/src/index.ts"
|
||||
if core_index.exists() and "PageLayout, PageHeader" not in core_index.read_text(encoding="utf-8"):
|
||||
errors.append("Core must export PageLayout and PageHeader from @govoplan/core-webui.")
|
||||
if core_index.exists() and "WorkspaceLayout" not in core_index.read_text(encoding="utf-8"):
|
||||
errors.append("Core must export WorkspaceLayout from @govoplan/core-webui.")
|
||||
if core_index.exists() and "PageActionBar" not in core_index.read_text(encoding="utf-8"):
|
||||
errors.append("Core must export PageActionBar from @govoplan/core-webui.")
|
||||
if core_index.exists() and "WorkspaceActionBar" not in core_index.read_text(encoding="utf-8"):
|
||||
errors.append("Core must export WorkspaceActionBar from @govoplan/core-webui.")
|
||||
|
||||
if errors:
|
||||
print("\n".join(errors), file=sys.stderr)
|
||||
return 1
|
||||
|
||||
print(
|
||||
"Shared WebUI layout contract passed: "
|
||||
f"{len(page_consumers)} discovered page consumers, "
|
||||
f"{sum(len(PAGE_LAYOUT_USAGE.findall(text)) for text in source_text.values())} semantic pages, "
|
||||
f"{len(action_consumers)} semantic action consumers, "
|
||||
f"{len(editor_consumers)} guarded editor consumers, "
|
||||
f"{len(raw_frames)} registered legacy page-frame files; "
|
||||
f"{len(workspace_consumers)} discovered workspace consumers, "
|
||||
f"{len(raw_workspaces)} registered legacy workspace files."
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,447 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Enforce Core ownership of repeated WebUI layout and dialog anatomy."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pathlib
|
||||
import re
|
||||
import sys
|
||||
|
||||
|
||||
META_ROOT = pathlib.Path(__file__).resolve().parents[2]
|
||||
REPOS_ROOT = META_ROOT.parent
|
||||
CORE_INDEX = pathlib.Path("govoplan-core/webui/src/index.ts")
|
||||
DIALOG_WIDTH_EXCEPTIONS = META_ROOT / "tools/checks/shared-webui-dialog-width-exceptions.txt"
|
||||
|
||||
RAW_ELEMENT = re.compile(
|
||||
r'<(?P<tag>div|span|header|section|form)\b(?P<attrs>[^>]*?)'
|
||||
r'\bclassName="(?P<classes>[^"]+)"',
|
||||
re.DOTALL,
|
||||
)
|
||||
LEGACY_LAYOUT_TOKENS = {
|
||||
"form-grid",
|
||||
"admin-form-grid",
|
||||
"dashboard-grid",
|
||||
"settings-grid",
|
||||
"admin-dialog",
|
||||
"admin-dialog-wide",
|
||||
"admin-details-grid",
|
||||
"detail-list",
|
||||
"metric-grid",
|
||||
}
|
||||
CENTRAL_COMPONENTS = {
|
||||
"PageActionBar": pathlib.Path(
|
||||
"govoplan-core/webui/src/components/PageActionBar.tsx"
|
||||
),
|
||||
"ActionToolbar": pathlib.Path(
|
||||
"govoplan-core/webui/src/components/ActionToolbar.tsx"
|
||||
),
|
||||
"ToolbarGroup": pathlib.Path(
|
||||
"govoplan-core/webui/src/components/ActionToolbar.tsx"
|
||||
),
|
||||
"ToolbarSpacer": pathlib.Path(
|
||||
"govoplan-core/webui/src/components/ActionToolbar.tsx"
|
||||
),
|
||||
"ContentGrid": pathlib.Path(
|
||||
"govoplan-core/webui/src/components/ContentGrid.tsx"
|
||||
),
|
||||
"ContentSection": pathlib.Path(
|
||||
"govoplan-core/webui/src/components/ContentSection.tsx"
|
||||
),
|
||||
"FormGrid": pathlib.Path("govoplan-core/webui/src/components/ContentGrid.tsx"),
|
||||
"FormLayout": pathlib.Path(
|
||||
"govoplan-core/webui/src/components/ContentGrid.tsx"
|
||||
),
|
||||
"GridItem": pathlib.Path("govoplan-core/webui/src/components/ContentGrid.tsx"),
|
||||
"FormSection": pathlib.Path(
|
||||
"govoplan-core/webui/src/components/FormSection.tsx"
|
||||
),
|
||||
"DialogActions": pathlib.Path(
|
||||
"govoplan-core/webui/src/components/DialogAnatomy.tsx"
|
||||
),
|
||||
"DialogForm": pathlib.Path(
|
||||
"govoplan-core/webui/src/components/DialogAnatomy.tsx"
|
||||
),
|
||||
"DialogSection": pathlib.Path(
|
||||
"govoplan-core/webui/src/components/DialogAnatomy.tsx"
|
||||
),
|
||||
"DescriptionList": pathlib.Path(
|
||||
"govoplan-core/webui/src/components/DescriptionList.tsx"
|
||||
),
|
||||
"DescriptionItem": pathlib.Path(
|
||||
"govoplan-core/webui/src/components/DescriptionList.tsx"
|
||||
),
|
||||
"MetricGrid": pathlib.Path(
|
||||
"govoplan-core/webui/src/components/MetricGrid.tsx"
|
||||
),
|
||||
"MetricCard": pathlib.Path(
|
||||
"govoplan-core/webui/src/components/MetricCard.tsx"
|
||||
),
|
||||
"FilterBar": pathlib.Path(
|
||||
"govoplan-core/webui/src/components/FilterBar.tsx"
|
||||
),
|
||||
"StatePanel": pathlib.Path(
|
||||
"govoplan-core/webui/src/components/StatePanel.tsx"
|
||||
),
|
||||
"CountBadge": pathlib.Path(
|
||||
"govoplan-core/webui/src/components/CountBadge.tsx"
|
||||
),
|
||||
"SelectionList": pathlib.Path(
|
||||
"govoplan-core/webui/src/components/SelectionList.tsx"
|
||||
),
|
||||
"SelectionListItem": pathlib.Path(
|
||||
"govoplan-core/webui/src/components/SelectionList.tsx"
|
||||
),
|
||||
"SelectionListItemContent": pathlib.Path(
|
||||
"govoplan-core/webui/src/components/SelectionList.tsx"
|
||||
),
|
||||
"WorkspaceLayout": pathlib.Path(
|
||||
"govoplan-core/webui/src/components/WorkspaceLayout.tsx"
|
||||
),
|
||||
"WorkspaceFrame": pathlib.Path(
|
||||
"govoplan-core/webui/src/components/WorkspaceFrame.tsx"
|
||||
),
|
||||
"DefinitionPalette": pathlib.Path(
|
||||
"govoplan-core/webui/src/components/DefinitionPalette.tsx"
|
||||
),
|
||||
"DefinitionPaletteGroup": pathlib.Path(
|
||||
"govoplan-core/webui/src/components/DefinitionPalette.tsx"
|
||||
),
|
||||
"DefinitionPaletteItem": pathlib.Path(
|
||||
"govoplan-core/webui/src/components/DefinitionPalette.tsx"
|
||||
),
|
||||
"DefinitionNodeIcon": pathlib.Path(
|
||||
"govoplan-core/webui/src/components/DefinitionNodeIcon.tsx"
|
||||
),
|
||||
"FloatingStatus": pathlib.Path(
|
||||
"govoplan-core/webui/src/components/FloatingStatus.tsx"
|
||||
),
|
||||
}
|
||||
REQUIRED_CONSUMERS = {
|
||||
"PageActionBar": (
|
||||
pathlib.Path("govoplan-payments/webui/src/features/payments/PaymentsPage.tsx"),
|
||||
),
|
||||
"ActionToolbar": (
|
||||
pathlib.Path("govoplan-core/webui/src/components/WysiwygEditor.tsx"),
|
||||
pathlib.Path("govoplan-calendar/webui/src/features/calendar/CalendarPage.tsx"),
|
||||
pathlib.Path("govoplan-files/webui/src/features/files/FilesPage.tsx"),
|
||||
pathlib.Path("govoplan-templates/webui/src/features/templates/TemplatesPage.tsx"),
|
||||
),
|
||||
"ContentGrid": (
|
||||
pathlib.Path("govoplan-core/webui/src/features/settings/SettingsPage.tsx"),
|
||||
pathlib.Path("govoplan-campaign/webui/src/features/campaigns/GlobalSettingsPage.tsx"),
|
||||
pathlib.Path("govoplan-notifications/webui/src/features/notifications/NotificationSettingsPanel.tsx"),
|
||||
),
|
||||
"ContentSection": (
|
||||
pathlib.Path("govoplan-datasources/webui/src/features/datasources/DatasourcesPage.tsx"),
|
||||
pathlib.Path("govoplan-dist-lists/webui/src/features/distributionLists/DistributionListsPage.tsx"),
|
||||
pathlib.Path("govoplan-templates/webui/src/features/templates/TemplatesPage.tsx"),
|
||||
),
|
||||
"FormGrid": (
|
||||
pathlib.Path("govoplan-core/webui/src/components/mail/MailServerSettingsPanel.tsx"),
|
||||
pathlib.Path("govoplan-calendar/webui/src/features/calendar/CalendarEventDialog.tsx"),
|
||||
pathlib.Path("govoplan-forms/webui/src/features/forms/FormDefinitionDialog.tsx"),
|
||||
pathlib.Path("govoplan-postbox/webui/src/features/postbox/PostboxAdminPanel.tsx"),
|
||||
),
|
||||
"FormSection": (
|
||||
pathlib.Path("govoplan-addresses/webui/src/features/addressbook/AddressBookPage.tsx"),
|
||||
pathlib.Path("govoplan-quick-access/webui/src/features/settings/QuickAccessSettingsPanel.tsx"),
|
||||
),
|
||||
"DialogForm": (
|
||||
pathlib.Path("govoplan-addresses/webui/src/features/addressbook/AddressBookPage.tsx"),
|
||||
pathlib.Path("govoplan-calendar/webui/src/features/calendar/CalendarEventDialog.tsx"),
|
||||
pathlib.Path("govoplan-records/webui/src/features/records/RecordsPage.tsx"),
|
||||
),
|
||||
"DialogSection": (
|
||||
pathlib.Path("govoplan-datasources/webui/src/features/datasources/DatasourcesPage.tsx"),
|
||||
pathlib.Path("govoplan-files/webui/src/features/files/components/FileShareDialog.tsx"),
|
||||
pathlib.Path("govoplan-templates/webui/src/features/templates/TemplatesPage.tsx"),
|
||||
),
|
||||
"DescriptionList": (
|
||||
pathlib.Path("govoplan-access/webui/src/features/admin/UsersPanel.tsx"),
|
||||
pathlib.Path("govoplan-campaign/webui/src/features/campaigns/CampaignReportPage.tsx"),
|
||||
pathlib.Path("govoplan-docs/webui/src/features/docs/DocsPage.tsx"),
|
||||
pathlib.Path("govoplan-policy/webui/src/features/policy/ViewPoliciesPanel.tsx"),
|
||||
),
|
||||
"MetricGrid": (
|
||||
pathlib.Path("govoplan-core/webui/src/features/dashboard/DashboardPage.tsx"),
|
||||
pathlib.Path("govoplan-admin/webui/src/features/admin/ModuleManagementPanel.tsx"),
|
||||
pathlib.Path("govoplan-campaign/webui/src/features/operator/OperatorQueuePage.tsx"),
|
||||
pathlib.Path("govoplan-notifications/webui/src/features/notifications/NotificationSummaryWidget.tsx"),
|
||||
),
|
||||
"MetricCard": (
|
||||
pathlib.Path("govoplan-admin/webui/src/features/admin/AdminOverviewPanel.tsx"),
|
||||
pathlib.Path("govoplan-approvals/webui/src/features/approvals/ApprovalsPage.tsx"),
|
||||
pathlib.Path("govoplan-campaign/webui/src/features/campaigns/ReviewSendPage.tsx"),
|
||||
pathlib.Path("govoplan-voting/webui/src/features/voting/VotingPage.tsx"),
|
||||
),
|
||||
"FilterBar": (
|
||||
pathlib.Path("govoplan-cases/webui/src/features/cases/CasesPage.tsx"),
|
||||
pathlib.Path("govoplan-dataflow/webui/src/features/dataflow/DataflowPage.tsx"),
|
||||
pathlib.Path("govoplan-records/webui/src/features/records/RecordsPage.tsx"),
|
||||
pathlib.Path("govoplan-tasks/webui/src/features/tasks/TasksPage.tsx"),
|
||||
),
|
||||
"StatePanel": (
|
||||
pathlib.Path("govoplan-committee/webui/src/features/committee/CommitteePage.tsx"),
|
||||
pathlib.Path("govoplan-notifications/webui/src/features/notifications/NotificationCenterPage.tsx"),
|
||||
pathlib.Path("govoplan-postbox/webui/src/features/postbox/PostboxPage.tsx"),
|
||||
pathlib.Path("govoplan-risk-compliance/webui/src/features/riskCompliance/RiskCompliancePage.tsx"),
|
||||
),
|
||||
"CountBadge": (
|
||||
pathlib.Path("govoplan-core/webui/src/layout/Titlebar.tsx"),
|
||||
pathlib.Path("govoplan-mail/webui/src/features/mail/MailboxPage.tsx"),
|
||||
pathlib.Path("govoplan-search/webui/src/features/search/SearchPage.tsx"),
|
||||
),
|
||||
"SelectionListItemContent": (
|
||||
pathlib.Path("govoplan-approvals/webui/src/features/approvals/ApprovalsPage.tsx"),
|
||||
pathlib.Path("govoplan-datasources/webui/src/features/datasources/DatasourcesPage.tsx"),
|
||||
pathlib.Path("govoplan-voting/webui/src/features/voting/VotingPage.tsx"),
|
||||
),
|
||||
"WorkspaceLayout": (
|
||||
pathlib.Path("govoplan-dataflow/webui/src/features/dataflow/DataflowPage.tsx"),
|
||||
pathlib.Path("govoplan-notifications/webui/src/features/notifications/NotificationCenterPage.tsx"),
|
||||
pathlib.Path("govoplan-workflow/webui/src/features/workflow/WorkflowPage.tsx"),
|
||||
),
|
||||
"WorkspaceFrame": (
|
||||
pathlib.Path("govoplan-cases/webui/src/features/cases/CasesPage.tsx"),
|
||||
pathlib.Path("govoplan-portal/webui/src/features/portal/PortalPage.tsx"),
|
||||
pathlib.Path("govoplan-records/webui/src/features/records/RecordsPage.tsx"),
|
||||
),
|
||||
"DefinitionPalette": (
|
||||
pathlib.Path("govoplan-dataflow/webui/src/features/dataflow/DataflowPage.tsx"),
|
||||
pathlib.Path("govoplan-workflow/webui/src/features/workflow/WorkflowPage.tsx"),
|
||||
),
|
||||
"DefinitionNodeIcon": (
|
||||
pathlib.Path("govoplan-dataflow/webui/src/features/dataflow/DataflowNode.tsx"),
|
||||
pathlib.Path("govoplan-workflow/webui/src/features/workflow/WorkflowNode.tsx"),
|
||||
),
|
||||
"FloatingStatus": (
|
||||
pathlib.Path("govoplan-dataflow/webui/src/features/dataflow/DataflowPage.tsx"),
|
||||
pathlib.Path("govoplan-workflow/webui/src/features/workflow/WorkflowPage.tsx"),
|
||||
),
|
||||
}
|
||||
LEGACY_CSS_SELECTOR = re.compile(
|
||||
r"(?<![-\w])\.(?:admin-details-grid|detail-list|metric-grid)(?![-\w])"
|
||||
)
|
||||
RETIRED_LOCAL_CSS_CLASSES = {
|
||||
"admin-assignment-grid",
|
||||
"approval-metrics",
|
||||
"dataflow-node-count",
|
||||
"dataflow-shell",
|
||||
"datasources-metrics",
|
||||
"datasources-shell",
|
||||
"dist-lists-metrics",
|
||||
"dist-lists-shell",
|
||||
"notifications-count",
|
||||
"notifications-empty-state",
|
||||
"notifications-shell",
|
||||
"review-flow-execution-summary",
|
||||
"review-flow-fact-grid",
|
||||
"risk-metrics",
|
||||
"search-filter-count",
|
||||
"tasks-empty-detail",
|
||||
"tasks-shell",
|
||||
"templates-shell",
|
||||
"voting-metrics",
|
||||
"voting-shell",
|
||||
"workflow-shell",
|
||||
"dataflow-palette-items",
|
||||
"workflow-palette-items",
|
||||
"dataflow-working-indicator",
|
||||
"workflow-working-indicator",
|
||||
"datasources-detail-section",
|
||||
"dist-lists-section",
|
||||
"templates-section",
|
||||
}
|
||||
LOCAL_METRIC_HELPER = re.compile(r"\b(?:function\s+Metric\b|const\s+Metric\s*=)")
|
||||
CSS_BLOCK = re.compile(r"([^{}]+)\{([^{}]*)\}")
|
||||
CSS_COMMENT = re.compile(r"/\*.*?\*/", re.DOTALL)
|
||||
DIALOG_CLASS = re.compile(r"\.([A-Za-z0-9_-]*(?:dialog|modal)[A-Za-z0-9_-]*)", re.IGNORECASE)
|
||||
DIALOG_WIDTH = re.compile(r"(?:^|;)\s*(?:width|max-width)\s*:", re.MULTILINE)
|
||||
DIALOG_WIDTH_VALUE = re.compile(
|
||||
r"(?:^|;)\s*(?:width|max-width)\s*:\s*([^;]+)", re.MULTILINE
|
||||
)
|
||||
STANDARD_DIALOG_WIDTH = re.compile(r"\b(?:460|560|680|1040|1440)px\b")
|
||||
DIALOG_INTERNAL_SUFFIXES = (
|
||||
"-actions",
|
||||
"-body",
|
||||
"-close",
|
||||
"-content",
|
||||
"-field",
|
||||
"-fields",
|
||||
"-footer",
|
||||
"-form",
|
||||
"-header",
|
||||
"-title",
|
||||
)
|
||||
|
||||
|
||||
def source_paths() -> list[pathlib.Path]:
|
||||
paths: list[pathlib.Path] = []
|
||||
for repository in sorted(REPOS_ROOT.glob("govoplan*")):
|
||||
source_root = repository / "webui" / "src"
|
||||
if source_root.is_dir():
|
||||
paths.extend(sorted(source_root.rglob("*.tsx")))
|
||||
return paths
|
||||
|
||||
|
||||
def css_paths() -> list[pathlib.Path]:
|
||||
paths: list[pathlib.Path] = []
|
||||
for repository in sorted(REPOS_ROOT.glob("govoplan*")):
|
||||
source_root = repository / "webui" / "src"
|
||||
if source_root.is_dir():
|
||||
paths.extend(sorted(source_root.rglob("*.css")))
|
||||
return paths
|
||||
|
||||
|
||||
def relative(path: pathlib.Path) -> pathlib.Path:
|
||||
return path.relative_to(REPOS_ROOT)
|
||||
|
||||
|
||||
def raw_reason(classes: str) -> str | None:
|
||||
tokens = classes.split()
|
||||
legacy = sorted(set(tokens) & LEGACY_LAYOUT_TOKENS)
|
||||
if legacy:
|
||||
return f"legacy shared layout class {', '.join(legacy)}"
|
||||
toolbars = [
|
||||
token
|
||||
for token in tokens
|
||||
if token == "admin-toolbar-row" or token.endswith("-toolbar")
|
||||
]
|
||||
if toolbars:
|
||||
return f"raw toolbar class {', '.join(toolbars)}"
|
||||
dialog_forms = [token for token in tokens if token.endswith("dialog-form")]
|
||||
if dialog_forms:
|
||||
return f"raw dialog form class {', '.join(dialog_forms)}"
|
||||
return None
|
||||
|
||||
|
||||
def normalized_css_selector(selector: str) -> str:
|
||||
return " ".join(selector.split())
|
||||
|
||||
|
||||
def dialog_width_exceptions(styles: dict[pathlib.Path, str]) -> dict[str, str]:
|
||||
exceptions: dict[str, str] = {}
|
||||
central_dialog_styles = pathlib.Path("govoplan-core/webui/src/styles/dialogs.css")
|
||||
for path, content in styles.items():
|
||||
if path == central_dialog_styles:
|
||||
continue
|
||||
without_comments = CSS_COMMENT.sub("", content)
|
||||
for match in CSS_BLOCK.finditer(without_comments):
|
||||
selector = normalized_css_selector(match.group(1))
|
||||
declarations = match.group(2)
|
||||
if not DIALOG_WIDTH.search(declarations):
|
||||
continue
|
||||
dialog_classes = DIALOG_CLASS.findall(selector)
|
||||
if not dialog_classes:
|
||||
continue
|
||||
if all(name.lower().endswith(DIALOG_INTERNAL_SUFFIXES) for name in dialog_classes):
|
||||
continue
|
||||
signature = f"{path}|{selector}"
|
||||
exceptions[signature] = declarations
|
||||
return exceptions
|
||||
|
||||
|
||||
def exception_baseline() -> set[str]:
|
||||
if not DIALOG_WIDTH_EXCEPTIONS.exists():
|
||||
return set()
|
||||
return {
|
||||
line.strip()
|
||||
for line in DIALOG_WIDTH_EXCEPTIONS.read_text(encoding="utf-8").splitlines()
|
||||
if line.strip() and not line.lstrip().startswith("#")
|
||||
}
|
||||
|
||||
|
||||
def main() -> int:
|
||||
sources = source_paths()
|
||||
source_text = {relative(path): path.read_text(encoding="utf-8") for path in sources}
|
||||
styles = css_paths()
|
||||
style_text = {relative(path): path.read_text(encoding="utf-8") for path in styles}
|
||||
errors: list[str] = []
|
||||
|
||||
for path, content in source_text.items():
|
||||
for match in RAW_ELEMENT.finditer(content):
|
||||
reason = raw_reason(match.group("classes"))
|
||||
if reason is None:
|
||||
continue
|
||||
line = content.count("\n", 0, match.start()) + 1
|
||||
errors.append(
|
||||
f"Raw {match.group('tag')} repeats shared anatomy ({reason}): {path}:{line}"
|
||||
)
|
||||
|
||||
for path, content in style_text.items():
|
||||
uncommented = CSS_COMMENT.sub("", content)
|
||||
legacy_match = LEGACY_CSS_SELECTOR.search(uncommented)
|
||||
if legacy_match:
|
||||
line = content.count("\n", 0, legacy_match.start()) + 1
|
||||
errors.append(f"Legacy shared layout selector is not allowed: {path}:{line}")
|
||||
for class_name in sorted(RETIRED_LOCAL_CSS_CLASSES):
|
||||
retired = re.search(rf"(?<![-\w])\.{re.escape(class_name)}(?![-\w])\s*(?:,|\{{)", uncommented)
|
||||
if retired:
|
||||
line = content.count("\n", 0, retired.start()) + 1
|
||||
errors.append(f"Retired module-local shared anatomy selector is not allowed: {path}:{line} ({class_name})")
|
||||
|
||||
dialog_exceptions = dialog_width_exceptions(style_text)
|
||||
baseline = exception_baseline()
|
||||
for signature in sorted(set(dialog_exceptions) - baseline):
|
||||
errors.append(f"Unreviewed local dialog width; use Dialog size or register a justified exception: {signature}")
|
||||
for signature in sorted(baseline - set(dialog_exceptions)):
|
||||
errors.append(f"Stale dialog width exception can be removed: {signature}")
|
||||
for signature, declarations in sorted(dialog_exceptions.items()):
|
||||
width_values = " ".join(DIALOG_WIDTH_VALUE.findall(declarations))
|
||||
standard = STANDARD_DIALOG_WIDTH.search(width_values)
|
||||
if standard:
|
||||
errors.append(f"Local dialog width duplicates Core size {standard.group(0)}: {signature}")
|
||||
|
||||
for name, owner in CENTRAL_COMPONENTS.items():
|
||||
definition = re.compile(
|
||||
rf"\b(?:function|class)\s+{name}\b|\bconst\s+{name}\s*="
|
||||
)
|
||||
for path, content in source_text.items():
|
||||
if path != owner and definition.search(content):
|
||||
errors.append(f"Module-local {name} definition is not allowed: {path}")
|
||||
|
||||
for path, content in source_text.items():
|
||||
if LOCAL_METRIC_HELPER.search(content):
|
||||
errors.append(f"Module-local Metric helper is not allowed; compose MetricCard directly: {path}")
|
||||
|
||||
usage_counts: dict[str, int] = {}
|
||||
for name in CENTRAL_COMPONENTS:
|
||||
usage = re.compile(rf"<{name}\b")
|
||||
usage_counts[name] = sum(bool(usage.search(content)) for content in source_text.values())
|
||||
|
||||
for name, consumers in REQUIRED_CONSUMERS.items():
|
||||
for path in consumers:
|
||||
absolute = REPOS_ROOT / path
|
||||
if not absolute.exists():
|
||||
continue
|
||||
if f"<{name}" not in absolute.read_text(encoding="utf-8"):
|
||||
errors.append(f"Required shared {name} consumer regressed: {path}")
|
||||
|
||||
core_index_path = REPOS_ROOT / CORE_INDEX
|
||||
if core_index_path.exists():
|
||||
core_index = core_index_path.read_text(encoding="utf-8")
|
||||
for name in CENTRAL_COMPONENTS:
|
||||
if not re.search(rf"\b{name}\b", core_index):
|
||||
errors.append(f"Core must export {name} from @govoplan/core-webui.")
|
||||
|
||||
dialog_path = REPOS_ROOT / "govoplan-core/webui/src/components/Dialog.tsx"
|
||||
if dialog_path.exists():
|
||||
dialog_text = dialog_path.read_text(encoding="utf-8")
|
||||
if "<DialogActions" not in dialog_text:
|
||||
errors.append("Every Core Dialog footer must compose DialogActions.")
|
||||
|
||||
if errors:
|
||||
print("\n".join(errors), file=sys.stderr)
|
||||
return 1
|
||||
|
||||
migrated = ", ".join(
|
||||
f"{name}={usage_counts[name]} files"
|
||||
for name in ("PageActionBar", "ActionToolbar", "WorkspaceFrame", "WorkspaceLayout", "FilterBar", "StatePanel", "SelectionList", "CountBadge", "DefinitionPalette", "DefinitionNodeIcon", "FloatingStatus", "ContentSection", "ContentGrid", "FormGrid", "FormSection", "DialogForm", "DialogSection", "MetricGrid", "MetricCard", "DescriptionList")
|
||||
)
|
||||
print(f"Shared WebUI primitive contract passed: {migrated}; {len(dialog_exceptions)} reviewed dialog width exceptions; no raw legacy anatomy.")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"assets": "Contract-only module: asset persistence and lifecycle APIs are not implemented; reassess before adding a migration-owned store.",
|
||||
"booking": "Contract-only module: booking persistence and reservation workflows are not implemented; reassess before adding a migration-owned store.",
|
||||
"certificates": "Contract-only module: certificate issuance and revocation persistence are not implemented; reassess before adding a migration-owned store.",
|
||||
"consultation": "Contract-only module: consultation submissions and evaluation persistence are not implemented; reassess before adding a migration-owned store.",
|
||||
"contracts": "Contract-only module: contract, amendment, and obligation persistence are not implemented; reassess before adding a migration-owned store.",
|
||||
"dms": "Stateless integration-preview module: DMS retains no document, person, credential, or provider-response store; Files and Records remain the subject-data owners. Reassess before persisting a target binding, plan, receipt, or diagnostic.",
|
||||
"erp": "Stateless integration-contract module: ERP retains no invoice, payable, plan, booking observation, provider response, or credential store; Procurement, Payments, Ledger, Files, and Audit remain the subject-data owners. Reassess before persisting a target binding, plan, receipt, reconciliation decision, or diagnostic.",
|
||||
"evaluation": "Contract-only module: evaluation runs, responses, and scores are not persisted; reassess before adding a migration-owned store.",
|
||||
"facilities": "Contract-only module: facility and maintenance persistence are not implemented; reassess before adding a migration-owned store.",
|
||||
"fit_connect": "Stateless transport-contract module: FIT-Connect retains no submission, attachment, receipt, acknowledgement plan, key, provider response, or diagnostic store; the owning Service, Forms, Cases, Files, and Audit workflows remain responsible for subject data. Reassess before persisting any ingress or event-log evidence.",
|
||||
"grants": "Contract-only module: grant applications, awards, and monitoring are not persisted; reassess before adding a migration-owned store.",
|
||||
"inspections": "Contract-only module: inspections, findings, and measures are not persisted; reassess before adding a migration-owned store.",
|
||||
"learning": "Contract-only module: learning offers, enrollment, and completion are not persisted; reassess before adding a migration-owned store.",
|
||||
"ops": "Projection-only module: Ops reads bounded platform and provider status; durable recovery evidence remains owned by Core and domain modules.",
|
||||
"permits": "Contract-only module: permit applications, assessments, and decisions are not persisted; reassess before adding a migration-owned store.",
|
||||
"portal": "Projection-only module: Portal stores no applicant records; Services, Forms Runtime, Cases, and Postbox own and export authoritative subject data.",
|
||||
"procurement": "Contract-only module: procurement procedures, tenders, and awards are not persisted; reassess before adding a migration-owned store.",
|
||||
"resources": "Contract-only module: resource catalog and allocation persistence are not implemented; reassess before adding a migration-owned store.",
|
||||
"rest": "Transport-only module: REST binds explicitly published functions and owns no domain or subject-data store.",
|
||||
"soap": "Transport-only module: SOAP binds explicitly published operations and owns no domain or subject-data store.",
|
||||
"tenancy": "Orchestration module: tenant lifecycle and settings use Core-owned storage; Access covers account and membership subject data.",
|
||||
"transparency": "Contract-only module: requests, disclosure reviews, and publications are not persisted; reassess before adding a migration-owned store.",
|
||||
"workflow": "Presentation-only module: Workflow edits and projects Workflow Engine state; Workflow Engine owns persistence and DSAR coverage.",
|
||||
"xrechnung": "Stateless validation-contract module: XRechnung persists no invoice, report, diagnostic, or handoff; the invoking Files, Procurement, or Payments workflow remains the subject-data owner. Reassess before adding a validation store."
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
# Reviewed module-specific Dialog and modal width selectors.
|
||||
#
|
||||
# Standard Core widths (460/560/680/1040/1440px) are never valid here.
|
||||
# Remove an entry when its consumer moves to Dialog size; additions require
|
||||
# an explicit design review and an inventory explanation.
|
||||
govoplan-addresses/webui/src/styles/addresses.css|.address-governance-dialog .dialog-panel
|
||||
govoplan-addresses/webui/src/styles/addresses.css|.address-import-dialog
|
||||
govoplan-addresses/webui/src/styles/addresses.css|.address-sync-dialog
|
||||
govoplan-addresses/webui/src/styles/addresses.css|.dialog-panel.address-contact-dialog
|
||||
govoplan-addresses/webui/src/styles/addresses.css|.dialog-panel.address-member-dialog
|
||||
govoplan-addresses/webui/src/styles/addresses.css|.dialog-panel.address-quality-dialog, .address-quality-dialog .dialog-panel
|
||||
govoplan-approvals/webui/src/styles/approvals.css|.approval-request-dialog, .approval-template-dialog
|
||||
govoplan-approvals/webui/src/styles/approvals.css|.approval-template-history-dialog
|
||||
govoplan-calendar/webui/src/styles/calendar.css|.calendar-delete-dialog
|
||||
govoplan-calendar/webui/src/styles/calendar.css|.calendar-migration-dialog
|
||||
govoplan-calendar/webui/src/styles/calendar.css|.calendar-outbox-dialog
|
||||
govoplan-calendar/webui/src/styles/calendar.css|.calendar-vevent-dialog
|
||||
govoplan-campaign/webui/src/styles/campaign-workspace.css|.attachment-linking-detail-modal
|
||||
govoplan-campaign/webui/src/styles/campaign-workspace.css|.attachment-rules-modal
|
||||
govoplan-campaign/webui/src/styles/campaign-workspace.css|.campaign-content-library-dialog
|
||||
govoplan-campaign/webui/src/styles/campaign-workspace.css|.campaign-copy-dialog
|
||||
govoplan-campaign/webui/src/styles/campaign-workspace.css|.campaign-schedule-dialog
|
||||
govoplan-campaign/webui/src/styles/campaign-workspace.css|.dialog-panel-wide
|
||||
govoplan-campaign/webui/src/styles/campaign-workspace.css|.message-preview-modal
|
||||
govoplan-campaign/webui/src/styles/campaign-workspace.css|.recipient-address-editor-modal
|
||||
govoplan-campaign/webui/src/styles/campaign-workspace.css|.recipient-import-modal
|
||||
govoplan-campaign/webui/src/styles/campaign-workspace.css|.template-action-dialog
|
||||
govoplan-campaign/webui/src/styles/campaign-workspace.css|.template-expression-dialog
|
||||
govoplan-campaign/webui/src/styles/campaign-workspace.css|.template-preview-modal
|
||||
govoplan-cases/webui/src/styles/cases.css|.case-decision-dialog
|
||||
govoplan-cases/webui/src/styles/cases.css|.case-share-dialog
|
||||
govoplan-committee/webui/src/styles/committee.css|.committee-ballot-dialog
|
||||
govoplan-committee/webui/src/styles/committee.css|.committee-record-dialog
|
||||
govoplan-core/webui/src/styles/components.css|.dialog-panel.wysiwyg-editor-dialog
|
||||
govoplan-core/webui/src/styles/components.css|.guided-config-dialog
|
||||
govoplan-core/webui/src/styles/components.css|.password-generator-dialog
|
||||
govoplan-core/webui/src/styles/layout.css|.concurrency-conflict-dialog
|
||||
govoplan-dashboard/webui/src/styles/dashboard.css|.dashboard-widget-config-dialog
|
||||
govoplan-dataflow/webui/src/styles/dataflow.css|.dataflow-decision-dialog
|
||||
govoplan-dataflow/webui/src/styles/dataflow.css|.dataflow-definition-dialog
|
||||
govoplan-dataflow/webui/src/styles/dataflow.css|.dataflow-run-dialog
|
||||
govoplan-dataflow/webui/src/styles/dataflow.css|.dataflow-source-dialog
|
||||
govoplan-dataflow/webui/src/styles/dataflow.css|.dataflow-triggers-dialog
|
||||
govoplan-datasources/webui/src/styles/datasources.css|.datasources-add-dialog
|
||||
govoplan-datasources/webui/src/styles/datasources.css|.datasources-governance-dialog
|
||||
govoplan-dist-lists/webui/src/styles/dist-lists.css|.dist-lists-entry-dialog
|
||||
govoplan-dist-lists/webui/src/styles/dist-lists.css|.dist-lists-explanation-dialog
|
||||
govoplan-files/webui/src/styles/file-manager.css|.file-dialog
|
||||
govoplan-files/webui/src/styles/file-manager.css|.file-dialog:has(.archive-preview)
|
||||
govoplan-files/webui/src/styles/file-manager.css|.file-dialog:has(.connector-sync-grid)
|
||||
govoplan-files/webui/src/styles/file-manager.css|.file-dialog:has(.file-share-dialog-content)
|
||||
govoplan-files/webui/src/styles/file-manager.css|.managed-file-chooser-dialog
|
||||
govoplan-forms-runtime/webui/src/styles/forms-runtime.css|.form-intake-dialog
|
||||
govoplan-forms/webui/src/styles/forms.css|.form-definition-dialog
|
||||
govoplan-organizations/webui/src/styles/organizations.css|.organization-upgrade-dialog
|
||||
govoplan-postbox/webui/src/styles/postbox.css|.postbox-dialog
|
||||
govoplan-postbox/webui/src/styles/postbox.css|.postbox-message-dialog
|
||||
govoplan-postbox/webui/src/styles/postbox.css|.postbox-template-dialog
|
||||
govoplan-projects/webui/src/styles/projects.css|.project-editor-dialog
|
||||
govoplan-records/webui/src/styles/records.css|.records-dialog
|
||||
govoplan-reporting/webui/src/styles/reporting.css|.reporting-drill-dialog
|
||||
govoplan-views/webui/src/styles/views.css|.views-assignment-dialog
|
||||
govoplan-workflow/webui/src/styles/workflow.css|.workflow-definition-dialog
|
||||
govoplan-workflow/webui/src/styles/workflow.css|.workflow-runs-dialog
|
||||
govoplan-workflow/webui/src/styles/workflow.css|.workflow-standard-comparison-dialog
|
||||
@@ -0,0 +1,2 @@
|
||||
# Raw page-frame exceptions. Keep this file empty: module pages use the Core
|
||||
# PageLayout contract and new exceptions are rejected by the layout checker.
|
||||
@@ -0,0 +1,2 @@
|
||||
# Raw workspace exceptions. Keep this file empty: module workspaces use the Core
|
||||
# WorkspaceLayout contract and new exceptions are rejected by the layout checker.
|
||||
@@ -26,6 +26,7 @@ CADDY_CONFIG_FILENAME = "Caddyfile"
|
||||
EXISTING_PROXY_FILENAME = "existing-proxy.json"
|
||||
PLAN_FILENAME = "plan.json"
|
||||
RECEIPT_FILENAME = "receipt.json"
|
||||
CAPABILITIES_FILENAME = "infrastructure-capabilities.json"
|
||||
MANIFEST_FILENAME = "distribution-manifest.json"
|
||||
KEYRING_FILENAME = "distribution-keyring.json"
|
||||
BACKUP_EVIDENCE_FILENAME = "backup-evidence.json"
|
||||
@@ -88,6 +89,7 @@ RUNTIME_ENV_KEYS = (
|
||||
"DEV_BOOTSTRAP_ENABLED",
|
||||
"GOVOPLAN_ALLOW_PROCESS_LOCAL_LOGIN_THROTTLE",
|
||||
"GOVOPLAN_DEPLOYMENT_SPEC_PATH",
|
||||
"GOVOPLAN_DEPLOYMENT_CAPABILITIES_PATH",
|
||||
"FILE_STORAGE_BACKEND",
|
||||
"FILE_STORAGE_LOCAL_ROOT",
|
||||
"FILE_STORAGE_S3_ENDPOINT_URL",
|
||||
@@ -113,6 +115,7 @@ class BundlePaths:
|
||||
existing_proxy: Path
|
||||
plan: Path
|
||||
receipt: Path
|
||||
capabilities: Path
|
||||
manifest: Path
|
||||
keyring: Path
|
||||
backup_evidence: Path
|
||||
@@ -137,6 +140,7 @@ def bundle_paths(root: Path) -> BundlePaths:
|
||||
existing_proxy=resolved / EXISTING_PROXY_FILENAME,
|
||||
plan=resolved / PLAN_FILENAME,
|
||||
receipt=resolved / RECEIPT_FILENAME,
|
||||
capabilities=resolved / CAPABILITIES_FILENAME,
|
||||
manifest=resolved / MANIFEST_FILENAME,
|
||||
keyring=resolved / KEYRING_FILENAME,
|
||||
backup_evidence=resolved / BACKUP_EVIDENCE_FILENAME,
|
||||
@@ -296,6 +300,7 @@ def reconcile_runtime_environment(
|
||||
"DEV_AUTO_MIGRATE_ENABLED": "false",
|
||||
"DEV_BOOTSTRAP_ENABLED": "false",
|
||||
"GOVOPLAN_DEPLOYMENT_SPEC_PATH": "/etc/govoplan/deployment/installation.json",
|
||||
"GOVOPLAN_DEPLOYMENT_CAPABILITIES_PATH": "/etc/govoplan/deployment/infrastructure-capabilities.json",
|
||||
}
|
||||
)
|
||||
if redis.mode == "disabled":
|
||||
@@ -370,7 +375,10 @@ def render_compose(spec: InstallationSpec) -> dict[str, object]:
|
||||
deployment_mount = (
|
||||
f"./{SPEC_FILENAME}:/etc/govoplan/deployment/installation.json:ro"
|
||||
)
|
||||
data_mounts = [deployment_mount]
|
||||
capabilities_mount = (
|
||||
f"./{CAPABILITIES_FILENAME}:/etc/govoplan/deployment/infrastructure-capabilities.json:ro"
|
||||
)
|
||||
data_mounts = [deployment_mount, capabilities_mount]
|
||||
if spec.components.storage.mode == "local":
|
||||
data_mounts.append("files-data:/var/lib/govoplan/files")
|
||||
|
||||
|
||||
@@ -0,0 +1,489 @@
|
||||
"""Non-secret infrastructure capability projection and change impact."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import asdict, dataclass
|
||||
from typing import Mapping
|
||||
from urllib.parse import urlsplit
|
||||
|
||||
from .model import InstallationSpec
|
||||
|
||||
|
||||
CAPABILITY_DOCUMENT_SCHEMA_VERSION = 1
|
||||
CAPABILITY_STATES = frozenset(
|
||||
{
|
||||
"configured",
|
||||
"available_unconfigured",
|
||||
"externally_supplied",
|
||||
"unavailable",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class InfrastructureCapability:
|
||||
id: str
|
||||
label: str
|
||||
state: str
|
||||
source: str
|
||||
detail: str
|
||||
endpoint: Mapping[str, object]
|
||||
secret_refs: tuple[str, ...]
|
||||
dependent_modules: tuple[str, ...]
|
||||
|
||||
def to_dict(self) -> dict[str, object]:
|
||||
value = asdict(self)
|
||||
value["endpoint"] = dict(self.endpoint)
|
||||
value["secret_refs"] = list(self.secret_refs)
|
||||
value["dependent_modules"] = list(self.dependent_modules)
|
||||
return value
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class CapabilityChangeImpact:
|
||||
capability_id: str
|
||||
action: str
|
||||
previous_state: str
|
||||
desired_state: str
|
||||
previous_source: str
|
||||
desired_source: str
|
||||
dependent_modules: tuple[str, ...]
|
||||
detail: str
|
||||
required_action: str
|
||||
|
||||
def to_dict(self) -> dict[str, object]:
|
||||
value = asdict(self)
|
||||
value["dependent_modules"] = list(self.dependent_modules)
|
||||
return value
|
||||
|
||||
|
||||
def infrastructure_capability_document(
|
||||
spec: InstallationSpec,
|
||||
environment: Mapping[str, str],
|
||||
) -> dict[str, object]:
|
||||
"""Project installer choices without copying credentials or secret URLs."""
|
||||
|
||||
capabilities = tuple(
|
||||
sorted(
|
||||
(
|
||||
_postgres_capability(spec, environment),
|
||||
_redis_capability(spec, environment),
|
||||
_mail_capability(spec),
|
||||
_storage_capability(spec, environment),
|
||||
_load_balancer_capability(spec),
|
||||
_ingress_capability(spec),
|
||||
),
|
||||
key=lambda item: item.id,
|
||||
)
|
||||
)
|
||||
tasks = _post_install_tasks(spec, capabilities)
|
||||
return {
|
||||
"schema_version": CAPABILITY_DOCUMENT_SCHEMA_VERSION,
|
||||
"installation_id": spec.installation_id,
|
||||
"profile": spec.profile,
|
||||
"capabilities": [item.to_dict() for item in capabilities],
|
||||
"post_install_tasks": tasks,
|
||||
}
|
||||
|
||||
|
||||
def capability_change_impacts(
|
||||
previous_document: object,
|
||||
desired_document: Mapping[str, object],
|
||||
) -> tuple[CapabilityChangeImpact, ...]:
|
||||
previous = _capability_map(previous_document)
|
||||
desired = _capability_map(desired_document)
|
||||
if not previous:
|
||||
return ()
|
||||
impacts: list[CapabilityChangeImpact] = []
|
||||
for capability_id in sorted(set(previous) | set(desired)):
|
||||
before = previous.get(capability_id)
|
||||
after = desired.get(capability_id)
|
||||
if before is None or after is None:
|
||||
continue
|
||||
previous_state = str(before.get("state") or "unavailable")
|
||||
desired_state = str(after.get("state") or "unavailable")
|
||||
previous_source = str(before.get("source") or "unknown")
|
||||
desired_source = str(after.get("source") or "unknown")
|
||||
previous_endpoint = _endpoint_signature(before.get("endpoint"))
|
||||
desired_endpoint = _endpoint_signature(after.get("endpoint"))
|
||||
previous_secret_refs = tuple(
|
||||
sorted(_string_items(before.get("secret_refs")))
|
||||
)
|
||||
desired_secret_refs = tuple(
|
||||
sorted(_string_items(after.get("secret_refs")))
|
||||
)
|
||||
if (
|
||||
previous_state == desired_state
|
||||
and previous_source == desired_source
|
||||
and previous_endpoint == desired_endpoint
|
||||
and previous_secret_refs == desired_secret_refs
|
||||
):
|
||||
continue
|
||||
action = (
|
||||
"remove"
|
||||
if previous_state != "unavailable" and desired_state == "unavailable"
|
||||
else "replace"
|
||||
if previous_source != desired_source
|
||||
else "reconfigure"
|
||||
)
|
||||
dependents = tuple(
|
||||
sorted(
|
||||
{
|
||||
*(_string_items(before.get("dependent_modules"))),
|
||||
*(_string_items(after.get("dependent_modules"))),
|
||||
}
|
||||
)
|
||||
)
|
||||
dependent_label = ", ".join(dependents) or "no declared module consumers"
|
||||
binding_change = _binding_change_label(
|
||||
previous_endpoint,
|
||||
desired_endpoint,
|
||||
previous_secret_refs,
|
||||
desired_secret_refs,
|
||||
)
|
||||
impacts.append(
|
||||
CapabilityChangeImpact(
|
||||
capability_id=capability_id,
|
||||
action=action,
|
||||
previous_state=previous_state,
|
||||
desired_state=desired_state,
|
||||
previous_source=previous_source,
|
||||
desired_source=desired_source,
|
||||
dependent_modules=dependents,
|
||||
detail=(
|
||||
f"{capability_id} changes from {previous_state}/{previous_source} "
|
||||
f"to {desired_state}/{desired_source}{binding_change}; "
|
||||
f"declared consumers: {dependent_label}."
|
||||
),
|
||||
required_action=(
|
||||
"Review module-owned configuration and data migration or recovery evidence before apply."
|
||||
),
|
||||
)
|
||||
)
|
||||
return tuple(impacts)
|
||||
|
||||
|
||||
def _postgres_capability(
|
||||
spec: InstallationSpec,
|
||||
environment: Mapping[str, str],
|
||||
) -> InfrastructureCapability:
|
||||
managed = spec.components.postgres.mode == "managed"
|
||||
endpoint = (
|
||||
{"scheme": "postgresql", "host": "postgres", "port": 5432}
|
||||
if managed
|
||||
else _redacted_endpoint(environment.get("DATABASE_URL", ""), default_port=5432)
|
||||
)
|
||||
return InfrastructureCapability(
|
||||
id="database.postgresql",
|
||||
label="PostgreSQL database",
|
||||
state="configured" if managed else "externally_supplied",
|
||||
source="installer-managed" if managed else "operator-supplied",
|
||||
detail=(
|
||||
"The installer manages the database service."
|
||||
if managed
|
||||
else "The deployment binds an externally operated PostgreSQL service."
|
||||
),
|
||||
endpoint=endpoint,
|
||||
secret_refs=("env:POSTGRES_PASSWORD",) if managed else ("env:DATABASE_URL",),
|
||||
dependent_modules=("core", *tuple(sorted(spec.enabled_modules))),
|
||||
)
|
||||
|
||||
|
||||
def _redis_capability(
|
||||
spec: InstallationSpec,
|
||||
environment: Mapping[str, str],
|
||||
) -> InfrastructureCapability:
|
||||
mode = spec.components.redis.mode
|
||||
consumers = _enabled_consumers(
|
||||
spec,
|
||||
{
|
||||
"campaigns",
|
||||
"dataflow",
|
||||
"files",
|
||||
"mail",
|
||||
"notifications",
|
||||
"scheduling",
|
||||
"workflow_engine",
|
||||
},
|
||||
include_core=True,
|
||||
)
|
||||
if mode == "disabled":
|
||||
return InfrastructureCapability(
|
||||
id="coordination.redis",
|
||||
label="Redis coordination and queues",
|
||||
state="unavailable",
|
||||
source="disabled",
|
||||
detail="Distributed queues and coordination are disabled.",
|
||||
endpoint={},
|
||||
secret_refs=(),
|
||||
dependent_modules=consumers,
|
||||
)
|
||||
managed = mode == "managed"
|
||||
endpoint = (
|
||||
{"scheme": "redis", "host": "redis", "port": 6379}
|
||||
if managed
|
||||
else _redacted_endpoint(environment.get("REDIS_URL", ""), default_port=6379)
|
||||
)
|
||||
return InfrastructureCapability(
|
||||
id="coordination.redis",
|
||||
label="Redis coordination and queues",
|
||||
state="configured" if managed else "externally_supplied",
|
||||
source="installer-managed" if managed else "operator-supplied",
|
||||
detail=(
|
||||
"The installer manages the Redis service."
|
||||
if managed
|
||||
else "The deployment binds an externally operated Redis service."
|
||||
),
|
||||
endpoint=endpoint,
|
||||
secret_refs=("env:REDIS_PASSWORD",) if managed else ("env:REDIS_URL",),
|
||||
dependent_modules=consumers,
|
||||
)
|
||||
|
||||
|
||||
def _mail_capability(spec: InstallationSpec) -> InfrastructureCapability:
|
||||
mode = spec.components.mail.mode
|
||||
consumers = _enabled_consumers(
|
||||
spec,
|
||||
{"campaigns", "mail", "notifications"},
|
||||
)
|
||||
if mode == "disabled":
|
||||
return InfrastructureCapability(
|
||||
id="mail.smtp",
|
||||
label="SMTP delivery",
|
||||
state="unavailable",
|
||||
source="disabled",
|
||||
detail="No SMTP infrastructure was selected.",
|
||||
endpoint={},
|
||||
secret_refs=(),
|
||||
dependent_modules=consumers,
|
||||
)
|
||||
if mode == "test-mail":
|
||||
return InfrastructureCapability(
|
||||
id="mail.smtp",
|
||||
label="SMTP delivery",
|
||||
state="available_unconfigured",
|
||||
source="installer-managed-test",
|
||||
detail="GreenMail is reachable, but Mail still owns profile and credential configuration.",
|
||||
endpoint={"scheme": "smtp", "host": "test-mail", "port": 3025},
|
||||
secret_refs=(),
|
||||
dependent_modules=consumers,
|
||||
)
|
||||
return InfrastructureCapability(
|
||||
id="mail.smtp",
|
||||
label="SMTP delivery",
|
||||
state="available_unconfigured",
|
||||
source="operator-supplied",
|
||||
detail="An external relay was selected; Mail still needs a reviewed server and credential binding.",
|
||||
endpoint={},
|
||||
secret_refs=(),
|
||||
dependent_modules=consumers,
|
||||
)
|
||||
|
||||
|
||||
def _storage_capability(
|
||||
spec: InstallationSpec,
|
||||
environment: Mapping[str, str],
|
||||
) -> InfrastructureCapability:
|
||||
mode = spec.components.storage.mode
|
||||
consumers = _enabled_consumers(
|
||||
spec,
|
||||
{"campaigns", "files", "records", "templates"},
|
||||
)
|
||||
if mode == "local":
|
||||
return InfrastructureCapability(
|
||||
id="files.storage",
|
||||
label="Managed file content storage",
|
||||
state="configured",
|
||||
source="host-local",
|
||||
detail="Files use the installer-managed local persistent volume.",
|
||||
endpoint={"kind": "filesystem", "reference": "volume:files-data"},
|
||||
secret_refs=(),
|
||||
dependent_modules=consumers,
|
||||
)
|
||||
if mode == "garage":
|
||||
return InfrastructureCapability(
|
||||
id="files.storage",
|
||||
label="Managed file content storage",
|
||||
state="configured",
|
||||
source="installer-managed-garage",
|
||||
detail="Files use the installer-managed single-node Garage service.",
|
||||
endpoint={"scheme": "http", "host": "garage", "port": 3900},
|
||||
secret_refs=(
|
||||
"env:FILE_STORAGE_S3_ACCESS_KEY_ID",
|
||||
"env:FILE_STORAGE_S3_SECRET_ACCESS_KEY",
|
||||
"env:GARAGE_RPC_SECRET",
|
||||
),
|
||||
dependent_modules=consumers,
|
||||
)
|
||||
return InfrastructureCapability(
|
||||
id="files.storage",
|
||||
label="Managed file content storage",
|
||||
state="externally_supplied",
|
||||
source="operator-supplied-s3",
|
||||
detail="Files use an externally operated S3-compatible service.",
|
||||
endpoint=_redacted_endpoint(
|
||||
environment.get("FILE_STORAGE_S3_ENDPOINT_URL", ""),
|
||||
default_port=443,
|
||||
),
|
||||
secret_refs=(
|
||||
"env:FILE_STORAGE_S3_ACCESS_KEY_ID",
|
||||
"env:FILE_STORAGE_S3_SECRET_ACCESS_KEY",
|
||||
),
|
||||
dependent_modules=consumers,
|
||||
)
|
||||
|
||||
|
||||
def _load_balancer_capability(spec: InstallationSpec) -> InfrastructureCapability:
|
||||
return InfrastructureCapability(
|
||||
id="runtime.load_balancing",
|
||||
label="Application load balancing",
|
||||
state="configured",
|
||||
source="installer-managed",
|
||||
detail=(
|
||||
f"HAProxy balances {spec.replicas.web} WebUI and {spec.replicas.api} API replica(s)."
|
||||
),
|
||||
endpoint={"scheme": "http", "host": "load-balancer", "port": 8080},
|
||||
secret_refs=(),
|
||||
dependent_modules=("core", "ops"),
|
||||
)
|
||||
|
||||
|
||||
def _ingress_capability(spec: InstallationSpec) -> InfrastructureCapability:
|
||||
mode = spec.ingress.mode
|
||||
endpoint = _redacted_endpoint(spec.public_url, default_port=443)
|
||||
if mode == "unconfigured":
|
||||
state = "unavailable"
|
||||
source = "unconfigured"
|
||||
detail = "No supported public ingress boundary is configured."
|
||||
elif mode == "existing-proxy":
|
||||
state = "externally_supplied"
|
||||
source = "operator-supplied-proxy"
|
||||
detail = "An externally operated reverse proxy provides public ingress."
|
||||
else:
|
||||
state = "configured"
|
||||
source = "installer-managed" if mode == "managed" else "host-local"
|
||||
detail = "The installer has a bounded public ingress configuration."
|
||||
return InfrastructureCapability(
|
||||
id="network.ingress",
|
||||
label="Public HTTP ingress",
|
||||
state=state,
|
||||
source=source,
|
||||
detail=detail,
|
||||
endpoint=endpoint,
|
||||
secret_refs=(),
|
||||
dependent_modules=("core", "ops"),
|
||||
)
|
||||
|
||||
|
||||
def _post_install_tasks(
|
||||
spec: InstallationSpec,
|
||||
capabilities: tuple[InfrastructureCapability, ...],
|
||||
) -> list[dict[str, object]]:
|
||||
by_id = {item.id: item for item in capabilities}
|
||||
tasks: list[dict[str, object]] = []
|
||||
mail = by_id["mail.smtp"]
|
||||
if mail.state == "available_unconfigured" and "mail" in spec.enabled_modules:
|
||||
tasks.append(
|
||||
{
|
||||
"id": "mail.smtp-profile",
|
||||
"resume_key": f"{spec.installation_id}:mail.smtp-profile:v1",
|
||||
"capability_id": mail.id,
|
||||
"state": "pending",
|
||||
"owner_module": "mail",
|
||||
"summary": "Create or select a Mail SMTP server and credential envelope.",
|
||||
"required_inputs": [
|
||||
"server endpoint",
|
||||
"transport security policy",
|
||||
"credential envelope reference when authentication is required",
|
||||
],
|
||||
"secret_boundary": "credential-envelope-reference-only",
|
||||
}
|
||||
)
|
||||
ingress = by_id["network.ingress"]
|
||||
if ingress.state == "unavailable":
|
||||
tasks.append(
|
||||
{
|
||||
"id": "network.configure-ingress",
|
||||
"resume_key": f"{spec.installation_id}:network.configure-ingress:v1",
|
||||
"capability_id": ingress.id,
|
||||
"state": "pending",
|
||||
"owner_module": "ops",
|
||||
"summary": "Select managed ingress or bind an existing reverse proxy.",
|
||||
"required_inputs": ["public URL", "TLS and proxy trust boundary"],
|
||||
"secret_boundary": "no-secret-material",
|
||||
}
|
||||
)
|
||||
return tasks
|
||||
|
||||
|
||||
def _enabled_consumers(
|
||||
spec: InstallationSpec,
|
||||
candidates: set[str],
|
||||
*,
|
||||
include_core: bool = False,
|
||||
) -> tuple[str, ...]:
|
||||
consumers = candidates.intersection(spec.enabled_modules)
|
||||
if include_core:
|
||||
consumers.add("core")
|
||||
return tuple(sorted(consumers))
|
||||
|
||||
|
||||
def _redacted_endpoint(value: str, *, default_port: int) -> dict[str, object]:
|
||||
try:
|
||||
parsed = urlsplit(value)
|
||||
host = parsed.hostname
|
||||
port = parsed.port or default_port
|
||||
except ValueError:
|
||||
return {"reference": "unresolved"}
|
||||
if not parsed.scheme or not host:
|
||||
return {"reference": "unresolved"}
|
||||
return {"scheme": parsed.scheme, "host": host, "port": port}
|
||||
|
||||
|
||||
def _capability_map(value: object) -> dict[str, Mapping[str, object]]:
|
||||
if not isinstance(value, Mapping):
|
||||
return {}
|
||||
raw_items = value.get("capabilities")
|
||||
if not isinstance(raw_items, list):
|
||||
return {}
|
||||
result: dict[str, Mapping[str, object]] = {}
|
||||
for item in raw_items:
|
||||
if not isinstance(item, Mapping):
|
||||
continue
|
||||
capability_id = str(item.get("id") or "").strip()
|
||||
state = str(item.get("state") or "").strip()
|
||||
if capability_id and state in CAPABILITY_STATES:
|
||||
result[capability_id] = item
|
||||
return result
|
||||
|
||||
|
||||
def _string_items(value: object) -> tuple[str, ...]:
|
||||
if not isinstance(value, list):
|
||||
return ()
|
||||
return tuple(str(item).strip() for item in value if str(item).strip())
|
||||
|
||||
|
||||
def _endpoint_signature(value: object) -> tuple[tuple[str, str], ...]:
|
||||
if not isinstance(value, Mapping):
|
||||
return ()
|
||||
return tuple(
|
||||
sorted(
|
||||
(str(key), str(raw))
|
||||
for key, raw in value.items()
|
||||
if isinstance(key, str) and isinstance(raw, (str, int, bool))
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def _binding_change_label(
|
||||
previous_endpoint: tuple[tuple[str, str], ...],
|
||||
desired_endpoint: tuple[tuple[str, str], ...],
|
||||
previous_secret_refs: tuple[str, ...],
|
||||
desired_secret_refs: tuple[str, ...],
|
||||
) -> str:
|
||||
changes: list[str] = []
|
||||
if previous_endpoint != desired_endpoint:
|
||||
changes.append("endpoint binding")
|
||||
if previous_secret_refs != desired_secret_refs:
|
||||
changes.append("secret-reference binding")
|
||||
return f" with changed {' and '.join(changes)}" if changes else ""
|
||||
@@ -45,6 +45,7 @@ from .bundle import (
|
||||
service_names,
|
||||
write_env,
|
||||
)
|
||||
from .capabilities import infrastructure_capability_document
|
||||
from .cluster_evidence import collect_kubernetes_evidence
|
||||
from .distribution import (
|
||||
MAX_KEYRING_BYTES,
|
||||
@@ -1287,6 +1288,10 @@ def _deployment_receipt(
|
||||
"agent": "cli",
|
||||
"web_updates": False,
|
||||
},
|
||||
"infrastructure_capabilities": infrastructure_capability_document(
|
||||
spec,
|
||||
secrets,
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
@@ -1464,6 +1469,11 @@ def _write_bundle(
|
||||
runtime_environment.update(_backup_runtime_environment(spec, paths))
|
||||
atomic_write(paths.spec, canonical_json(spec.to_dict()), mode=0o600)
|
||||
write_env(paths.env, runtime_environment)
|
||||
atomic_write(
|
||||
paths.capabilities,
|
||||
canonical_json(infrastructure_capability_document(spec, runtime_environment)),
|
||||
mode=0o644,
|
||||
)
|
||||
atomic_write(paths.compose, canonical_json(render_compose(spec)), mode=0o600)
|
||||
atomic_write(
|
||||
paths.load_balancer_config,
|
||||
|
||||
@@ -11,6 +11,7 @@ from typing import Any, Mapping
|
||||
from urllib.parse import urlsplit
|
||||
|
||||
from .bundle import BACKUP_RUNTIME_ENV_KEYS
|
||||
from .capabilities import infrastructure_capability_document
|
||||
from .model import InstallationSpec, image_is_digest_pinned
|
||||
|
||||
|
||||
@@ -96,6 +97,7 @@ def render_kubernetes(
|
||||
public_host = urlsplit(spec.public_url).hostname or "localhost"
|
||||
labels = {"app.kubernetes.io/name": "govoplan", "app.kubernetes.io/instance": name}
|
||||
config_name = f"{name}-runtime"
|
||||
capabilities_config_name = f"{name}-infrastructure-capabilities"
|
||||
service_account = f"{name}-runtime"
|
||||
config = {
|
||||
key: str(environment[key])
|
||||
@@ -142,6 +144,22 @@ def render_kubernetes(
|
||||
"metadata": {"name": config_name, "namespace": namespace, "labels": labels},
|
||||
"data": dict(sorted(config.items())),
|
||||
},
|
||||
{
|
||||
"apiVersion": "v1",
|
||||
"kind": "ConfigMap",
|
||||
"metadata": {
|
||||
"name": capabilities_config_name,
|
||||
"namespace": namespace,
|
||||
"labels": labels,
|
||||
},
|
||||
"data": {
|
||||
"infrastructure-capabilities.json": json.dumps(
|
||||
infrastructure_capability_document(spec, environment),
|
||||
sort_keys=True,
|
||||
separators=(",", ":"),
|
||||
)
|
||||
},
|
||||
},
|
||||
_deployment(
|
||||
name=f"{name}-api",
|
||||
namespace=namespace,
|
||||
@@ -161,6 +179,7 @@ def render_kubernetes(
|
||||
"--proxy-headers",
|
||||
),
|
||||
config_name=config_name,
|
||||
capabilities_config_name=capabilities_config_name,
|
||||
secret_name=secret_name,
|
||||
s3_ca_secret_name=s3_ca_secret_name,
|
||||
service_account=service_account,
|
||||
@@ -168,6 +187,7 @@ def render_kubernetes(
|
||||
readiness_path="/health/ready",
|
||||
liveness_path="/health",
|
||||
probe_host=public_host,
|
||||
graceful_shutdown_seconds=10,
|
||||
extra_environment=_role_database_environment(environment, "API"),
|
||||
),
|
||||
_service(
|
||||
@@ -186,6 +206,7 @@ def render_kubernetes(
|
||||
image=spec.release.web_image,
|
||||
command=(),
|
||||
config_name=None,
|
||||
capabilities_config_name=None,
|
||||
secret_name=None,
|
||||
s3_ca_secret_name=None,
|
||||
service_account=service_account,
|
||||
@@ -246,6 +267,7 @@ def render_kubernetes(
|
||||
"INFO",
|
||||
),
|
||||
config_name=config_name,
|
||||
capabilities_config_name=capabilities_config_name,
|
||||
secret_name=secret_name,
|
||||
s3_ca_secret_name=s3_ca_secret_name,
|
||||
service_account=service_account,
|
||||
@@ -302,6 +324,7 @@ def render_kubernetes(
|
||||
"/tmp/celerybeat-schedule",
|
||||
),
|
||||
config_name=config_name,
|
||||
capabilities_config_name=capabilities_config_name,
|
||||
secret_name=secret_name,
|
||||
s3_ca_secret_name=s3_ca_secret_name,
|
||||
service_account=service_account,
|
||||
@@ -651,6 +674,7 @@ def _deployment(
|
||||
image: str,
|
||||
command: tuple[str, ...],
|
||||
config_name: str | None,
|
||||
capabilities_config_name: str | None,
|
||||
secret_name: str | None,
|
||||
s3_ca_secret_name: str | None,
|
||||
service_account: str,
|
||||
@@ -658,6 +682,7 @@ def _deployment(
|
||||
readiness_path: str | None = None,
|
||||
liveness_path: str | None = None,
|
||||
probe_host: str | None = None,
|
||||
graceful_shutdown_seconds: int = 0,
|
||||
extra_environment: Mapping[str, str] | None = None,
|
||||
selector_labels: Mapping[str, str] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
@@ -680,6 +705,13 @@ def _deployment(
|
||||
)
|
||||
if secret_name:
|
||||
environment.extend(_secret_environment(secret_name))
|
||||
if capabilities_config_name:
|
||||
environment.append(
|
||||
{
|
||||
"name": "GOVOPLAN_DEPLOYMENT_CAPABILITIES_PATH",
|
||||
"value": "/etc/govoplan/deployment/infrastructure-capabilities.json",
|
||||
}
|
||||
)
|
||||
if s3_ca_secret_name:
|
||||
environment.append(
|
||||
{"name": "AWS_CA_BUNDLE", "value": "/etc/govoplan/trust/s3-ca.crt"}
|
||||
@@ -715,6 +747,18 @@ def _deployment(
|
||||
container_port or 8000,
|
||||
host=probe_host,
|
||||
)
|
||||
if graceful_shutdown_seconds:
|
||||
container["lifecycle"] = {
|
||||
"preStop": {
|
||||
"exec": {
|
||||
"command": [
|
||||
"/bin/sh",
|
||||
"-c",
|
||||
f"sleep {graceful_shutdown_seconds}",
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
pod_spec: dict[str, Any] = {
|
||||
"serviceAccountName": service_account,
|
||||
"automountServiceAccountToken": False,
|
||||
@@ -734,10 +778,38 @@ def _deployment(
|
||||
}
|
||||
],
|
||||
}
|
||||
if graceful_shutdown_seconds:
|
||||
pod_spec["terminationGracePeriodSeconds"] = max(
|
||||
30,
|
||||
graceful_shutdown_seconds + 20,
|
||||
)
|
||||
container["volumeMounts"] = [{"name": "tmp", "mountPath": "/tmp"}]
|
||||
if s3_ca_secret_name:
|
||||
pod_spec["volumes"].append(_s3_ca_volume(s3_ca_secret_name))
|
||||
container["volumeMounts"].append(_s3_ca_volume_mount())
|
||||
if capabilities_config_name:
|
||||
pod_spec["volumes"].append(
|
||||
{
|
||||
"name": "deployment-capabilities",
|
||||
"configMap": {
|
||||
"name": capabilities_config_name,
|
||||
"items": [
|
||||
{
|
||||
"key": "infrastructure-capabilities.json",
|
||||
"path": "infrastructure-capabilities.json",
|
||||
}
|
||||
],
|
||||
},
|
||||
}
|
||||
)
|
||||
container["volumeMounts"].append(
|
||||
{
|
||||
"name": "deployment-capabilities",
|
||||
"mountPath": "/etc/govoplan/deployment/infrastructure-capabilities.json",
|
||||
"subPath": "infrastructure-capabilities.json",
|
||||
"readOnly": True,
|
||||
}
|
||||
)
|
||||
if config_name:
|
||||
pod_spec["containers"][0]["envFrom"] = [{"configMapRef": {"name": config_name}}]
|
||||
if config_name and secret_name:
|
||||
|
||||
@@ -54,7 +54,9 @@ FULL_MODULES = (
|
||||
"dataflow",
|
||||
"workflow_engine",
|
||||
"workflow",
|
||||
"tasks",
|
||||
"views",
|
||||
"quick_access",
|
||||
"search",
|
||||
"risk_compliance",
|
||||
"postbox",
|
||||
|
||||
@@ -35,6 +35,11 @@ from .bundle import (
|
||||
render_existing_proxy_contract,
|
||||
service_names,
|
||||
)
|
||||
from .capabilities import (
|
||||
CapabilityChangeImpact,
|
||||
capability_change_impacts,
|
||||
infrastructure_capability_document,
|
||||
)
|
||||
from .distribution import (
|
||||
MAX_KEYRING_BYTES,
|
||||
MAX_MANIFEST_BYTES,
|
||||
@@ -83,6 +88,8 @@ class DeploymentPlan:
|
||||
desired_environment_fingerprint: str
|
||||
actions: tuple[PlanAction, ...]
|
||||
checks: tuple[Check, ...]
|
||||
infrastructure_capabilities: Mapping[str, object]
|
||||
capability_impacts: tuple[CapabilityChangeImpact, ...]
|
||||
|
||||
@property
|
||||
def blocked(self) -> bool:
|
||||
@@ -97,6 +104,8 @@ class DeploymentPlan:
|
||||
"blocked": self.blocked,
|
||||
"actions": [action.to_dict() for action in self.actions],
|
||||
"checks": [check.to_dict() for check in self.checks],
|
||||
"infrastructure_capabilities": dict(self.infrastructure_capabilities),
|
||||
"capability_impacts": [item.to_dict() for item in self.capability_impacts],
|
||||
}
|
||||
|
||||
|
||||
@@ -122,6 +131,14 @@ def build_plan(
|
||||
spec_digest = digest_json(spec.to_dict())
|
||||
compose_digest = digest_json(compose)
|
||||
environment_digest = environment_fingerprint(read_env(paths.env))
|
||||
infrastructure_capabilities = infrastructure_capability_document(
|
||||
spec,
|
||||
read_env(paths.env),
|
||||
)
|
||||
capability_impacts = capability_change_impacts(
|
||||
previous.get("infrastructure_capabilities"),
|
||||
infrastructure_capabilities,
|
||||
)
|
||||
|
||||
actions: list[PlanAction] = []
|
||||
if not previous:
|
||||
@@ -166,12 +183,21 @@ def build_plan(
|
||||
"Remove the service container; retained volumes are not deleted.",
|
||||
)
|
||||
)
|
||||
for impact in capability_impacts:
|
||||
actions.append(
|
||||
PlanAction(
|
||||
"review",
|
||||
f"capability:{impact.capability_id}",
|
||||
impact.detail,
|
||||
)
|
||||
)
|
||||
if (
|
||||
previous
|
||||
and previous_spec_digest == spec_digest
|
||||
and previous_compose_digest == compose_digest
|
||||
and previous_environment_fingerprint == environment_digest
|
||||
and previous_services == desired_services
|
||||
and not capability_impacts
|
||||
):
|
||||
actions.append(
|
||||
PlanAction(
|
||||
@@ -180,6 +206,15 @@ def build_plan(
|
||||
)
|
||||
|
||||
checks = list(static_checks(spec, paths))
|
||||
checks.extend(
|
||||
Check(
|
||||
id=f"capability.change.{impact.capability_id}",
|
||||
level="warning",
|
||||
message=impact.detail,
|
||||
action=impact.required_action,
|
||||
)
|
||||
for impact in capability_impacts
|
||||
)
|
||||
if include_host_checks:
|
||||
checks.extend(host_checks(spec, paths, command_runner=command_runner))
|
||||
return DeploymentPlan(
|
||||
@@ -189,6 +224,8 @@ def build_plan(
|
||||
desired_environment_fingerprint=environment_digest,
|
||||
actions=tuple(actions),
|
||||
checks=tuple(checks),
|
||||
infrastructure_capabilities=infrastructure_capabilities,
|
||||
capability_impacts=capability_impacts,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ _BUNDLE_FILES = (
|
||||
"backup-keyring.json",
|
||||
"backup-verification.json",
|
||||
"receipt.json",
|
||||
"infrastructure-capabilities.json",
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -247,7 +247,7 @@ def is_excluded_repo_file(path: pathlib.Path) -> bool:
|
||||
return True
|
||||
if "testing_plan" in name or "test_plan" in name:
|
||||
return True
|
||||
if text.endswith("/docs/GITEA_ISSUES.md"):
|
||||
if text.endswith(("/docs/GITEA_ISSUES.md", "/docs/project/GITEA_ISSUES.md")):
|
||||
return True
|
||||
if text.endswith("/docs/GOVOPLAN_MASTER_ROADMAP.md"):
|
||||
return True
|
||||
|
||||
@@ -34,6 +34,7 @@ DEFAULT_EXCLUDES = (
|
||||
"!**/.cache/**",
|
||||
"!.gitea/**",
|
||||
"!docs/GITEA_ISSUES.md",
|
||||
"!docs/project/GITEA_ISSUES.md",
|
||||
"!tools/gitea/gitea-todo-import.py",
|
||||
"!tools/gitea/gitea-sync-labels.py",
|
||||
"!tools/gitea/gitea-codex-note.py",
|
||||
|
||||
@@ -280,6 +280,13 @@
|
||||
"rationale": "The module WebUI constructs this endpoint through a mounted router prefix, generic action, or provider path.",
|
||||
"repository": "govoplan-audit"
|
||||
},
|
||||
{
|
||||
"category": "intentionally_headless",
|
||||
"method": "GET",
|
||||
"path": "/admin/audit/evidence-bundles/{}",
|
||||
"rationale": "Evidence export clients can poll the persisted request/result lifecycle before downloading; the current synchronous administration action downloads ready bundles directly.",
|
||||
"repository": "govoplan-audit"
|
||||
},
|
||||
{
|
||||
"category": "ui_reachable",
|
||||
"method": "GET",
|
||||
@@ -357,6 +364,27 @@
|
||||
"rationale": "The local bootstrap handoff reads only minimum first-run readiness before a normal authenticated shell exists.",
|
||||
"repository": "govoplan-core"
|
||||
},
|
||||
{
|
||||
"category": "ui_reachable",
|
||||
"method": "POST",
|
||||
"path": "/admin/privacy/data-subject-requests/{}/erasure-plan",
|
||||
"rationale": "The Data-subject requests administration panel builds this revision-guarded action URL from the selected request identifier.",
|
||||
"repository": "govoplan-core"
|
||||
},
|
||||
{
|
||||
"category": "ui_reachable",
|
||||
"method": "POST",
|
||||
"path": "/admin/privacy/data-subject-requests/{}/execute",
|
||||
"rationale": "The Data-subject requests administration panel builds this explicitly confirmed action URL from the selected request identifier.",
|
||||
"repository": "govoplan-core"
|
||||
},
|
||||
{
|
||||
"category": "ui_reachable",
|
||||
"method": "POST",
|
||||
"path": "/admin/privacy/data-subject-requests/{}/search",
|
||||
"rationale": "The Data-subject requests administration panel builds this provider-search action URL from the selected request identifier.",
|
||||
"repository": "govoplan-core"
|
||||
},
|
||||
{
|
||||
"category": "public_integration",
|
||||
"method": "GET",
|
||||
@@ -448,6 +476,188 @@
|
||||
"rationale": "This capability-first module intentionally exposes a headless API for other modules and integrations.",
|
||||
"repository": "govoplan-connectors"
|
||||
},
|
||||
{
|
||||
"category": "ui_reachable",
|
||||
"method": "GET",
|
||||
"path": "/connectors/governed/configurations",
|
||||
"rationale": "The Connector governance administration page lists tenant configurations through this endpoint.",
|
||||
"repository": "govoplan-connectors"
|
||||
},
|
||||
{
|
||||
"category": "ui_reachable",
|
||||
"method": "POST",
|
||||
"path": "/connectors/governed/configurations",
|
||||
"rationale": "The Connector governance administration page creates tenant configurations through this endpoint.",
|
||||
"repository": "govoplan-connectors"
|
||||
},
|
||||
{
|
||||
"category": "ui_reachable",
|
||||
"method": "PUT",
|
||||
"path": "/connectors/governed/configurations/{}",
|
||||
"rationale": "The Connector governance administration page constructs this revision-guarded URL from the selected configuration identifier.",
|
||||
"repository": "govoplan-connectors"
|
||||
},
|
||||
{
|
||||
"category": "ui_reachable",
|
||||
"method": "POST",
|
||||
"path": "/connectors/governed/configurations/{}/dry-runs",
|
||||
"rationale": "The Connector governance administration page constructs this bounded dry-run URL from the selected configuration identifier.",
|
||||
"repository": "govoplan-connectors"
|
||||
},
|
||||
{
|
||||
"category": "ui_reachable",
|
||||
"method": "POST",
|
||||
"path": "/connectors/governed/configurations/{}/simulations",
|
||||
"rationale": "The Connector governance administration page constructs this bounded simulation URL from the selected configuration identifier.",
|
||||
"repository": "govoplan-connectors"
|
||||
},
|
||||
{
|
||||
"category": "ui_reachable",
|
||||
"method": "GET",
|
||||
"path": "/connectors/governed/definitions",
|
||||
"rationale": "The Connector governance administration page lists governed connector definitions through this endpoint.",
|
||||
"repository": "govoplan-connectors"
|
||||
},
|
||||
{
|
||||
"category": "ui_reachable",
|
||||
"method": "POST",
|
||||
"path": "/connectors/governed/definitions",
|
||||
"rationale": "The Connector governance administration page publishes immutable connector-definition revisions through this endpoint.",
|
||||
"repository": "govoplan-connectors"
|
||||
},
|
||||
{
|
||||
"category": "ui_reachable",
|
||||
"method": "GET",
|
||||
"path": "/connectors/governed/runs",
|
||||
"rationale": "The Connector governance administration page lists dry-run and simulation evidence through this endpoint.",
|
||||
"repository": "govoplan-connectors"
|
||||
},
|
||||
{
|
||||
"category": "ui_reachable",
|
||||
"method": "POST",
|
||||
"path": "/connectors/governed/runs/{}/review",
|
||||
"rationale": "The Connector governance administration page constructs this review-decision URL from the selected run identifier.",
|
||||
"repository": "govoplan-connectors"
|
||||
},
|
||||
{
|
||||
"category": "ui_reachable",
|
||||
"method": "GET",
|
||||
"path": "/connectors/knowledge/profiles",
|
||||
"rationale": "The External knowledge administration page lists MediaWiki and BlueSpice profiles through this endpoint.",
|
||||
"repository": "govoplan-connectors"
|
||||
},
|
||||
{
|
||||
"category": "ui_reachable",
|
||||
"method": "POST",
|
||||
"path": "/connectors/knowledge/profiles",
|
||||
"rationale": "The External knowledge administration page creates a mapped, ACL-governed knowledge profile through this endpoint.",
|
||||
"repository": "govoplan-connectors"
|
||||
},
|
||||
{
|
||||
"category": "ui_reachable",
|
||||
"method": "PUT",
|
||||
"path": "/connectors/knowledge/profiles/{}",
|
||||
"rationale": "The External knowledge administration page constructs this revision-guarded profile URL from the selected profile identifier.",
|
||||
"repository": "govoplan-connectors"
|
||||
},
|
||||
{
|
||||
"category": "ui_reachable",
|
||||
"method": "POST",
|
||||
"path": "/connectors/knowledge/profiles/{}/discover",
|
||||
"rationale": "The External knowledge administration page discovers product, version, capabilities, namespaces, and diagnostics through this endpoint.",
|
||||
"repository": "govoplan-connectors"
|
||||
},
|
||||
{
|
||||
"category": "ui_reachable",
|
||||
"method": "POST",
|
||||
"path": "/connectors/knowledge/profiles/{}/migration-dry-runs",
|
||||
"rationale": "The External knowledge administration page runs a bounded, non-writing native-Wiki migration preview through this endpoint.",
|
||||
"repository": "govoplan-connectors"
|
||||
},
|
||||
{
|
||||
"category": "ui_reachable",
|
||||
"method": "GET",
|
||||
"path": "/connectors/knowledge/profiles/{}/objects",
|
||||
"rationale": "The External knowledge administration page lists synchronized, identity-stable knowledge snapshots through this endpoint.",
|
||||
"repository": "govoplan-connectors"
|
||||
},
|
||||
{
|
||||
"category": "ui_reachable",
|
||||
"method": "POST",
|
||||
"path": "/connectors/knowledge/profiles/{}/pages/{}/publish",
|
||||
"rationale": "The External knowledge administration page constructs this revision-checked external publication URL from the selected profile and page identifiers.",
|
||||
"repository": "govoplan-connectors"
|
||||
},
|
||||
{
|
||||
"category": "ui_reachable",
|
||||
"method": "POST",
|
||||
"path": "/connectors/knowledge/profiles/{}/sync",
|
||||
"rationale": "The External knowledge administration page runs keyed full backfills and recent-change deltas through this endpoint.",
|
||||
"repository": "govoplan-connectors"
|
||||
},
|
||||
{
|
||||
"category": "ui_reachable",
|
||||
"method": "GET",
|
||||
"path": "/connectors/knowledge/runs",
|
||||
"rationale": "The External knowledge administration page lists synchronization, migration-preview, and publication evidence through this endpoint.",
|
||||
"repository": "govoplan-connectors"
|
||||
},
|
||||
{
|
||||
"category": "ui_reachable",
|
||||
"method": "GET",
|
||||
"path": "/connectors/service-desk/profiles",
|
||||
"rationale": "The External service desk administration page lists Znuny/OTRS profiles through this endpoint.",
|
||||
"repository": "govoplan-connectors"
|
||||
},
|
||||
{
|
||||
"category": "ui_reachable",
|
||||
"method": "POST",
|
||||
"path": "/connectors/service-desk/profiles",
|
||||
"rationale": "The External service desk administration page creates route-, queue-, field-, authority-, and ACL-governed profiles through this endpoint.",
|
||||
"repository": "govoplan-connectors"
|
||||
},
|
||||
{
|
||||
"category": "ui_reachable",
|
||||
"method": "PUT",
|
||||
"path": "/connectors/service-desk/profiles/{}",
|
||||
"rationale": "The External service desk administration page constructs this revision-guarded profile URL from the selected profile identifier.",
|
||||
"repository": "govoplan-connectors"
|
||||
},
|
||||
{
|
||||
"category": "ui_reachable",
|
||||
"method": "POST",
|
||||
"path": "/connectors/service-desk/profiles/{}/discover",
|
||||
"rationale": "The External service desk administration page discovers product, version, maturity, capabilities, route health, and diagnostics through this endpoint.",
|
||||
"repository": "govoplan-connectors"
|
||||
},
|
||||
{
|
||||
"category": "ui_reachable",
|
||||
"method": "GET",
|
||||
"path": "/connectors/service-desk/profiles/{}/objects",
|
||||
"rationale": "The External service desk administration page lists synchronized identity-stable, ACL-governed external ticket projections through this endpoint.",
|
||||
"repository": "govoplan-connectors"
|
||||
},
|
||||
{
|
||||
"category": "ui_reachable",
|
||||
"method": "POST",
|
||||
"path": "/connectors/service-desk/profiles/{}/sync",
|
||||
"rationale": "The External service desk administration page runs keyed bounded full and delta synchronization through this endpoint.",
|
||||
"repository": "govoplan-connectors"
|
||||
},
|
||||
{
|
||||
"category": "ui_reachable",
|
||||
"method": "POST",
|
||||
"path": "/connectors/service-desk/profiles/{}/tickets/{}/update",
|
||||
"rationale": "The External service desk ticket list opens a separated revision-checked governed update dialog and constructs this URL from stable profile and provider ticket identifiers.",
|
||||
"repository": "govoplan-connectors"
|
||||
},
|
||||
{
|
||||
"category": "ui_reachable",
|
||||
"method": "GET",
|
||||
"path": "/connectors/service-desk/runs",
|
||||
"rationale": "The External service desk administration page lists synchronization and external-mutation recovery evidence through this endpoint.",
|
||||
"repository": "govoplan-connectors"
|
||||
},
|
||||
{
|
||||
"category": "intentionally_headless",
|
||||
"method": "GET",
|
||||
@@ -483,6 +693,13 @@
|
||||
"rationale": "This capability-first module intentionally exposes a headless API for other modules and integrations.",
|
||||
"repository": "govoplan-connectors"
|
||||
},
|
||||
{
|
||||
"category": "intentionally_headless",
|
||||
"method": "POST",
|
||||
"path": "/connectors/tabular-sources/files",
|
||||
"rationale": "This capability-first module intentionally exposes a headless API for other modules and integrations.",
|
||||
"repository": "govoplan-connectors"
|
||||
},
|
||||
{
|
||||
"category": "intentionally_headless",
|
||||
"method": "POST",
|
||||
@@ -490,6 +707,13 @@
|
||||
"rationale": "This capability-first module intentionally exposes a headless API for other modules and integrations.",
|
||||
"repository": "govoplan-connectors"
|
||||
},
|
||||
{
|
||||
"category": "intentionally_headless",
|
||||
"method": "POST",
|
||||
"path": "/connectors/tabular-sources/sql",
|
||||
"rationale": "This capability-first module intentionally exposes a headless API for other modules and integrations.",
|
||||
"repository": "govoplan-connectors"
|
||||
},
|
||||
{
|
||||
"category": "intentionally_headless",
|
||||
"method": "DELETE",
|
||||
@@ -504,6 +728,13 @@
|
||||
"rationale": "This capability-first module intentionally exposes a headless API for other modules and integrations.",
|
||||
"repository": "govoplan-connectors"
|
||||
},
|
||||
{
|
||||
"category": "intentionally_headless",
|
||||
"method": "POST",
|
||||
"path": "/connectors/tabular-sources/{}/refresh",
|
||||
"rationale": "This capability-first module intentionally exposes a headless API for other modules and integrations.",
|
||||
"repository": "govoplan-connectors"
|
||||
},
|
||||
{
|
||||
"category": "public_integration",
|
||||
"method": "GET",
|
||||
@@ -672,6 +903,90 @@
|
||||
"rationale": "The module WebUI constructs this endpoint through a mounted router prefix, generic action, or provider path.",
|
||||
"repository": "govoplan-docs"
|
||||
},
|
||||
{
|
||||
"category": "ui_reachable",
|
||||
"method": "GET",
|
||||
"path": "/semantic/entries",
|
||||
"rationale": "The Docs semantic authoring UI calls this mounted sub-router through the /docs prefix, which static endpoint matching cannot compose.",
|
||||
"repository": "govoplan-docs"
|
||||
},
|
||||
{
|
||||
"category": "ui_reachable",
|
||||
"method": "POST",
|
||||
"path": "/semantic/entries",
|
||||
"rationale": "The Docs semantic authoring UI calls this mounted sub-router through the /docs prefix, which static endpoint matching cannot compose.",
|
||||
"repository": "govoplan-docs"
|
||||
},
|
||||
{
|
||||
"category": "ui_reachable",
|
||||
"method": "GET",
|
||||
"path": "/semantic/entries/{}",
|
||||
"rationale": "The Docs semantic authoring UI constructs the entry identifier dynamically on the mounted semantic sub-router.",
|
||||
"repository": "govoplan-docs"
|
||||
},
|
||||
{
|
||||
"category": "ui_reachable",
|
||||
"method": "PUT",
|
||||
"path": "/semantic/entries/{}",
|
||||
"rationale": "The Docs semantic authoring UI constructs the entry identifier dynamically on the mounted semantic sub-router.",
|
||||
"repository": "govoplan-docs"
|
||||
},
|
||||
{
|
||||
"category": "ui_reachable",
|
||||
"method": "GET",
|
||||
"path": "/semantic/entries/{}/history",
|
||||
"rationale": "The Docs semantic authoring UI constructs immutable history paths dynamically on the mounted semantic sub-router.",
|
||||
"repository": "govoplan-docs"
|
||||
},
|
||||
{
|
||||
"category": "ui_reachable",
|
||||
"method": "POST",
|
||||
"path": "/semantic/entries/{}/publish",
|
||||
"rationale": "The Docs semantic authoring UI constructs lifecycle action paths dynamically on the mounted semantic sub-router.",
|
||||
"repository": "govoplan-docs"
|
||||
},
|
||||
{
|
||||
"category": "ui_reachable",
|
||||
"method": "POST",
|
||||
"path": "/semantic/entries/{}/retire",
|
||||
"rationale": "The Docs semantic authoring UI constructs lifecycle action paths dynamically on the mounted semantic sub-router.",
|
||||
"repository": "govoplan-docs"
|
||||
},
|
||||
{
|
||||
"category": "ui_reachable",
|
||||
"method": "POST",
|
||||
"path": "/semantic/entries/{}/supersede",
|
||||
"rationale": "The Docs semantic authoring UI constructs lifecycle action paths dynamically on the mounted semantic sub-router.",
|
||||
"repository": "govoplan-docs"
|
||||
},
|
||||
{
|
||||
"category": "ui_reachable",
|
||||
"method": "GET",
|
||||
"path": "/semantic/export",
|
||||
"rationale": "The Docs semantic authoring UI calls tenant export on the mounted semantic sub-router.",
|
||||
"repository": "govoplan-docs"
|
||||
},
|
||||
{
|
||||
"category": "ui_reachable",
|
||||
"method": "GET",
|
||||
"path": "/semantic/policy",
|
||||
"rationale": "The Docs semantic authoring UI reads publication policy through the mounted semantic sub-router.",
|
||||
"repository": "govoplan-docs"
|
||||
},
|
||||
{
|
||||
"category": "ui_reachable",
|
||||
"method": "PUT",
|
||||
"path": "/semantic/policy",
|
||||
"rationale": "The Docs semantic authoring UI saves publication policy through the mounted semantic sub-router.",
|
||||
"repository": "govoplan-docs"
|
||||
},
|
||||
{
|
||||
"category": "ui_reachable",
|
||||
"method": "GET",
|
||||
"path": "/semantic/subjects",
|
||||
"rationale": "The Docs semantic authoring UI discovers authorized module subjects through the mounted semantic sub-router.",
|
||||
"repository": "govoplan-docs"
|
||||
},
|
||||
{
|
||||
"category": "ui_reachable",
|
||||
"method": "GET",
|
||||
@@ -798,6 +1113,20 @@
|
||||
"rationale": "The vault action group rotates the current key with policy, assurance, reason, and revision evidence.",
|
||||
"repository": "govoplan-encryption"
|
||||
},
|
||||
{
|
||||
"category": "public_integration",
|
||||
"method": "POST",
|
||||
"path": "/files/form-evidence/upload",
|
||||
"rationale": "A short-lived purpose-bound bearer grant lets the public Forms Runtime surface stream one attachment directly to Files without granting general Files access.",
|
||||
"repository": "govoplan-files"
|
||||
},
|
||||
{
|
||||
"category": "ui_reachable",
|
||||
"method": "GET",
|
||||
"path": "/files/{}/versions/{}/download",
|
||||
"rationale": "Postbox resolves immutable Files evidence references to this exact-version download URL; Files performs the final tenant and access check.",
|
||||
"repository": "govoplan-files"
|
||||
},
|
||||
{
|
||||
"category": "ui_reachable",
|
||||
"method": "POST",
|
||||
@@ -889,6 +1218,20 @@
|
||||
"rationale": "Retained compatibility endpoint; current module surfaces use the replacement contract.",
|
||||
"repository": "govoplan-identity"
|
||||
},
|
||||
{
|
||||
"category": "ui_reachable",
|
||||
"method": "POST",
|
||||
"path": "/identity/identities/{}/activate",
|
||||
"rationale": "The Identity administration workspace constructs this explicit lifecycle-action URL from the selected identity identifier.",
|
||||
"repository": "govoplan-identity"
|
||||
},
|
||||
{
|
||||
"category": "ui_reachable",
|
||||
"method": "POST",
|
||||
"path": "/identity/identities/{}/deactivate",
|
||||
"rationale": "The Identity administration workspace constructs this explicit lifecycle-action URL from the selected identity identifier.",
|
||||
"repository": "govoplan-identity"
|
||||
},
|
||||
{
|
||||
"category": "intentionally_headless",
|
||||
"method": "POST",
|
||||
@@ -1111,6 +1454,34 @@
|
||||
"rationale": "This capability-first module intentionally exposes a headless API for other modules and integrations.",
|
||||
"repository": "govoplan-parties"
|
||||
},
|
||||
{
|
||||
"category": "ui_reachable",
|
||||
"method": "GET",
|
||||
"path": "/payments/requests",
|
||||
"rationale": "The Payments workspace lists obligations, preserves stale data on refresh failure, and offers explicit retry.",
|
||||
"repository": "govoplan-payments"
|
||||
},
|
||||
{
|
||||
"category": "ui_reachable",
|
||||
"method": "POST",
|
||||
"path": "/payments/requests",
|
||||
"rationale": "The guided Payments request dialog creates fixed, source-bound obligations with an explicit replay key.",
|
||||
"repository": "govoplan-payments"
|
||||
},
|
||||
{
|
||||
"category": "intentionally_headless",
|
||||
"method": "GET",
|
||||
"path": "/payments/requests/{}",
|
||||
"rationale": "The operator list response already carries the complete detail projection; the single-object endpoint remains available to capability consumers without duplicating a detail fetch in the workspace.",
|
||||
"repository": "govoplan-payments"
|
||||
},
|
||||
{
|
||||
"category": "ui_reachable",
|
||||
"method": "POST",
|
||||
"path": "/payments/requests/{}/manual-reconciliations",
|
||||
"rationale": "The separate manual reconciliation dialog fixes amount and currency and captures transaction and immutable evidence references.",
|
||||
"repository": "govoplan-payments"
|
||||
},
|
||||
{
|
||||
"category": "intentionally_headless",
|
||||
"method": "DELETE",
|
||||
@@ -1314,6 +1685,13 @@
|
||||
"rationale": "Postbox delivery, access-decision, and routing-preview APIs are consumed by Campaign and other module capabilities.",
|
||||
"repository": "govoplan-postbox"
|
||||
},
|
||||
{
|
||||
"category": "public_integration",
|
||||
"method": "POST",
|
||||
"path": "/postbox/admin/postboxes/{}/protection-transitions/{}/transform",
|
||||
"rationale": "An approved external encryption client uses this governed endpoint to submit per-message E2EE or plaintext transforms with digest and authority evidence; the ordinary server-rendered WebUI never handles E2EE private keys.",
|
||||
"repository": "govoplan-postbox"
|
||||
},
|
||||
{
|
||||
"category": "intentionally_headless",
|
||||
"method": "POST",
|
||||
@@ -1335,6 +1713,83 @@
|
||||
"rationale": "The module WebUI constructs this endpoint through a mounted router prefix, generic action, or provider path.",
|
||||
"repository": "govoplan-projects"
|
||||
},
|
||||
{
|
||||
"category": "ui_reachable",
|
||||
"method": "POST",
|
||||
"path": "/records/{}/appraise",
|
||||
"rationale": "The Records lifecycle panel invokes the record appraisal action through a dynamically constructed record path.",
|
||||
"repository": "govoplan-records"
|
||||
},
|
||||
{
|
||||
"category": "ui_reachable",
|
||||
"method": "POST",
|
||||
"path": "/records/{}/close",
|
||||
"rationale": "The Records lifecycle panel invokes the record closure action through a dynamically constructed record path.",
|
||||
"repository": "govoplan-records"
|
||||
},
|
||||
{
|
||||
"category": "ui_reachable",
|
||||
"method": "POST",
|
||||
"path": "/records/{}/dispositions",
|
||||
"rationale": "The Records lifecycle panel proposes a governed disposition through a dynamically constructed record path.",
|
||||
"repository": "govoplan-records"
|
||||
},
|
||||
{
|
||||
"category": "ui_reachable",
|
||||
"method": "POST",
|
||||
"path": "/records/{}/dispositions/{}/finalize",
|
||||
"rationale": "The Records lifecycle panel finalizes an approved disposition through a dynamically constructed record path.",
|
||||
"repository": "govoplan-records"
|
||||
},
|
||||
{
|
||||
"category": "ui_reachable",
|
||||
"method": "POST",
|
||||
"path": "/records/{}/dispositions/{}/withdraw",
|
||||
"rationale": "The Records lifecycle panel withdraws a pending disposition through a dynamically constructed record path.",
|
||||
"repository": "govoplan-records"
|
||||
},
|
||||
{
|
||||
"category": "ui_reachable",
|
||||
"method": "POST",
|
||||
"path": "/records/{}/holds",
|
||||
"rationale": "The Records lifecycle panel applies a hold through a dynamically constructed record path.",
|
||||
"repository": "govoplan-records"
|
||||
},
|
||||
{
|
||||
"category": "ui_reachable",
|
||||
"method": "POST",
|
||||
"path": "/records/{}/holds/{}/release",
|
||||
"rationale": "The Records lifecycle panel releases a hold through a dynamically constructed record path.",
|
||||
"repository": "govoplan-records"
|
||||
},
|
||||
{
|
||||
"category": "ui_reachable",
|
||||
"method": "POST",
|
||||
"path": "/records/{}/reopen",
|
||||
"rationale": "The Records lifecycle panel invokes the record reopen action through a dynamically constructed record path.",
|
||||
"repository": "govoplan-records"
|
||||
},
|
||||
{
|
||||
"category": "ui_reachable",
|
||||
"method": "POST",
|
||||
"path": "/records/{}/transfer-packages",
|
||||
"rationale": "The Records lifecycle panel prepares an archive-neutral transfer package through a dynamically constructed record path.",
|
||||
"repository": "govoplan-records"
|
||||
},
|
||||
{
|
||||
"category": "ui_reachable",
|
||||
"method": "POST",
|
||||
"path": "/records/{}/transfer-packages/{}/dispatch",
|
||||
"rationale": "The Records lifecycle panel runs an explicitly simulated transfer through a dynamically constructed record path.",
|
||||
"repository": "govoplan-records"
|
||||
},
|
||||
{
|
||||
"category": "ui_reachable",
|
||||
"method": "POST",
|
||||
"path": "/records/{}/volumes",
|
||||
"rationale": "The Records filing dialog creates explicit record volumes through a dynamically constructed record path.",
|
||||
"repository": "govoplan-records"
|
||||
},
|
||||
{
|
||||
"category": "missing_ui",
|
||||
"method": "GET",
|
||||
@@ -1673,68 +2128,67 @@
|
||||
"repository": "govoplan-campaign"
|
||||
},
|
||||
{
|
||||
"category": "missing_ui",
|
||||
"category": "ui_reachable",
|
||||
"method": "GET",
|
||||
"path": "/relationships",
|
||||
"rationale": "IDM documents typed relationships but its current administration page exposes function assignments only.",
|
||||
"repository": "govoplan-idm",
|
||||
"tracking_issue": "https://git.add-ideas.de/GovOPlaN/govoplan-idm/issues/11"
|
||||
"rationale": "The IDM relationship administration grid lists effective-dated identity relationships and their lifecycle state.",
|
||||
"repository": "govoplan-idm"
|
||||
},
|
||||
{
|
||||
"category": "missing_ui",
|
||||
"category": "ui_reachable",
|
||||
"method": "POST",
|
||||
"path": "/relationships",
|
||||
"rationale": "IDM documents typed relationships but its current administration page exposes function assignments only.",
|
||||
"repository": "govoplan-idm",
|
||||
"tracking_issue": "https://git.add-ideas.de/GovOPlaN/govoplan-idm/issues/11"
|
||||
"rationale": "Authorized IDM administrators create relationships through the searchable relationship editor.",
|
||||
"repository": "govoplan-idm"
|
||||
},
|
||||
{
|
||||
"category": "missing_ui",
|
||||
"category": "ui_reachable",
|
||||
"method": "PATCH",
|
||||
"path": "/relationships/{}",
|
||||
"rationale": "IDM documents typed relationships but its current administration page exposes function assignments only.",
|
||||
"repository": "govoplan-idm",
|
||||
"tracking_issue": "https://git.add-ideas.de/GovOPlaN/govoplan-idm/issues/11"
|
||||
"rationale": "Authorized IDM administrators update a loaded relationship with its optimistic revision.",
|
||||
"repository": "govoplan-idm"
|
||||
},
|
||||
{
|
||||
"category": "missing_ui",
|
||||
"category": "ui_reachable",
|
||||
"method": "POST",
|
||||
"path": "/relationships/{}/revoke",
|
||||
"rationale": "IDM documents typed relationships but its current administration page exposes function assignments only.",
|
||||
"repository": "govoplan-idm",
|
||||
"tracking_issue": "https://git.add-ideas.de/GovOPlaN/govoplan-idm/issues/11"
|
||||
"rationale": "The IDM relationship grid exposes a separated destructive action with mandatory reason and confirmation.",
|
||||
"repository": "govoplan-idm"
|
||||
},
|
||||
{
|
||||
"category": "missing_ui",
|
||||
"category": "ui_reachable",
|
||||
"method": "GET",
|
||||
"path": "/typed-groups",
|
||||
"rationale": "IDM documents typed groups but its current administration page exposes function assignments only.",
|
||||
"repository": "govoplan-idm",
|
||||
"tracking_issue": "https://git.add-ideas.de/GovOPlaN/govoplan-idm/issues/11"
|
||||
"rationale": "The IDM typed-group administration grid lists active and optionally inactive groups.",
|
||||
"repository": "govoplan-idm"
|
||||
},
|
||||
{
|
||||
"category": "missing_ui",
|
||||
"category": "ui_reachable",
|
||||
"method": "POST",
|
||||
"path": "/typed-groups",
|
||||
"rationale": "IDM documents typed groups but its current administration page exposes function assignments only.",
|
||||
"repository": "govoplan-idm",
|
||||
"tracking_issue": "https://git.add-ideas.de/GovOPlaN/govoplan-idm/issues/11"
|
||||
"rationale": "Authorized IDM administrators create typed groups through the group editor.",
|
||||
"repository": "govoplan-idm"
|
||||
},
|
||||
{
|
||||
"category": "missing_ui",
|
||||
"category": "ui_reachable",
|
||||
"method": "PATCH",
|
||||
"path": "/typed-groups/{}",
|
||||
"rationale": "IDM documents typed groups but its current administration page exposes function assignments only.",
|
||||
"repository": "govoplan-idm",
|
||||
"tracking_issue": "https://git.add-ideas.de/GovOPlaN/govoplan-idm/issues/11"
|
||||
"rationale": "Authorized IDM administrators edit group metadata, lifecycle state, and provenance with optimistic revision checks.",
|
||||
"repository": "govoplan-idm"
|
||||
},
|
||||
{
|
||||
"category": "missing_ui",
|
||||
"category": "ui_reachable",
|
||||
"method": "GET",
|
||||
"path": "/typed-groups/{}/memberships",
|
||||
"rationale": "IDM lacks the typed-group membership explanation surface for this existing API.",
|
||||
"repository": "govoplan-idm",
|
||||
"tracking_issue": "https://git.add-ideas.de/GovOPlaN/govoplan-idm/issues/11"
|
||||
"rationale": "The IDM membership inspector resolves a group at a selected time and shows included and excluded decisions.",
|
||||
"repository": "govoplan-idm"
|
||||
},
|
||||
{
|
||||
"category": "intentionally_headless",
|
||||
"method": "GET",
|
||||
"path": "/tasks/{}",
|
||||
"rationale": "Task command clients retrieve one explicit task and its strong revision token; the Work UI already receives the same projection through the aggregated list.",
|
||||
"repository": "govoplan-tasks"
|
||||
}
|
||||
],
|
||||
"schema_version": 1
|
||||
|
||||
@@ -56,11 +56,35 @@ const labelAttributes = new Set([
|
||||
"title"
|
||||
]);
|
||||
const helpAttributes = new Set([
|
||||
"data-help-context-id",
|
||||
"description",
|
||||
"help",
|
||||
"helpContextId",
|
||||
"helperText",
|
||||
"helpText"
|
||||
]);
|
||||
const exactHelpAttributes = new Set([
|
||||
"data-help-context-id",
|
||||
"helpContextId"
|
||||
]);
|
||||
const helpRiskAttributes = new Set([
|
||||
"data-help-risk",
|
||||
"helpRisk"
|
||||
]);
|
||||
const reviewedHelpRiskAttributes = new Set([
|
||||
"data-help-risk-reviewed",
|
||||
"helpRiskReviewed"
|
||||
]);
|
||||
const supportedHelpRisks = new Set([
|
||||
"authority",
|
||||
"credential",
|
||||
"disclosure",
|
||||
"encryption",
|
||||
"external-effect",
|
||||
"irreversible",
|
||||
"policy",
|
||||
"retention"
|
||||
]);
|
||||
const actionComponentPattern = /(?:Action|Button|Link)$/;
|
||||
const contributionTypes = new Map([
|
||||
["AdminSectionsUiCapability", "admin_section"],
|
||||
@@ -198,20 +222,43 @@ function inspectSource(repository, sourceRoot, sourcePath) {
|
||||
const parentAttributes = parentFormField
|
||||
? jsxAttributes(parentFormField)
|
||||
: new Map();
|
||||
const scopedAncestorAttributes = nearestScopedHelpAttributes(node);
|
||||
const label =
|
||||
attributes.get("label") ??
|
||||
attributes.get("aria-label") ??
|
||||
parentAttributes.get("label") ??
|
||||
null;
|
||||
const help = firstAttribute(attributes, helpAttributes) ??
|
||||
firstAttribute(parentAttributes, helpAttributes);
|
||||
firstAttribute(parentAttributes, helpAttributes) ??
|
||||
firstAttribute(scopedAncestorAttributes, helpAttributes);
|
||||
const hasHelp = hasAnyAttribute(attributes, helpAttributes) ||
|
||||
hasAnyAttribute(parentAttributes, helpAttributes);
|
||||
hasAnyAttribute(parentAttributes, helpAttributes) ||
|
||||
hasAnyAttribute(scopedAncestorAttributes, helpAttributes);
|
||||
const hasExactHelp = hasAnyAttribute(attributes, exactHelpAttributes) ||
|
||||
hasAnyAttribute(parentAttributes, exactHelpAttributes) ||
|
||||
hasAnyAttribute(scopedAncestorAttributes, exactHelpAttributes);
|
||||
const helpContextId = firstAttribute(attributes, exactHelpAttributes) ??
|
||||
firstAttribute(parentAttributes, exactHelpAttributes) ??
|
||||
firstAttribute(scopedAncestorAttributes, exactHelpAttributes);
|
||||
const explicitId = firstAttribute(
|
||||
attributes,
|
||||
new Set(["interfaceId", "data-interface-id", "id", "name", "field"])
|
||||
);
|
||||
const context = nearestNamedContext(node);
|
||||
const risk = helpRiskFor({
|
||||
component,
|
||||
context,
|
||||
file: relativeFile,
|
||||
label,
|
||||
explicitId,
|
||||
name: attributes.get("name") ?? attributes.get("id") ?? attributes.get("field") ?? null,
|
||||
explicitRisk: firstAttribute(attributes, helpRiskAttributes) ??
|
||||
firstAttribute(parentAttributes, helpRiskAttributes) ??
|
||||
firstAttribute(scopedAncestorAttributes, helpRiskAttributes)
|
||||
});
|
||||
const riskReviewed = firstAttribute(attributes, reviewedHelpRiskAttributes) ??
|
||||
firstAttribute(parentAttributes, reviewedHelpRiskAttributes) ??
|
||||
firstAttribute(scopedAncestorAttributes, reviewedHelpRiskAttributes);
|
||||
const stableId = sourceIdentity(
|
||||
"field",
|
||||
node,
|
||||
@@ -235,7 +282,14 @@ function inspectSource(repository, sourceRoot, sourcePath) {
|
||||
help: help ?? null,
|
||||
helpId: hasHelp ? `${stableId}.help` : null,
|
||||
helpDynamic: hasHelp && help === null,
|
||||
helpCandidate: !hasHelp
|
||||
helpCandidate: !hasHelp,
|
||||
helpExact: hasExactHelp,
|
||||
helpContextId,
|
||||
helpContextDynamic: hasExactHelp && helpContextId === null,
|
||||
helpRisk: risk.value,
|
||||
helpRiskSource: risk.source,
|
||||
helpRiskReviewed: riskReviewed,
|
||||
highRiskHelpMissing: risk.value !== null && !hasExactHelp && riskReviewed !== "standard"
|
||||
});
|
||||
|
||||
}
|
||||
@@ -259,6 +313,19 @@ function inspectSource(repository, sourceRoot, sourcePath) {
|
||||
new Set(["interfaceId", "data-interface-id", "id", "name"])
|
||||
);
|
||||
const context = nearestNamedContext(node);
|
||||
const hasHelp = hasAnyAttribute(attributes, helpAttributes);
|
||||
const hasExactHelp = hasAnyAttribute(attributes, exactHelpAttributes);
|
||||
const helpContextId = firstAttribute(attributes, exactHelpAttributes);
|
||||
const risk = helpRiskFor({
|
||||
component,
|
||||
context,
|
||||
file: relativeFile,
|
||||
label,
|
||||
explicitId,
|
||||
name: attributes.get("name") ?? attributes.get("id") ?? null,
|
||||
explicitRisk: firstAttribute(attributes, helpRiskAttributes)
|
||||
});
|
||||
const riskReviewed = firstAttribute(attributes, reviewedHelpRiskAttributes);
|
||||
result.actions.push({
|
||||
...locate(node),
|
||||
id: sourceIdentity(
|
||||
@@ -271,7 +338,15 @@ function inspectSource(repository, sourceRoot, sourcePath) {
|
||||
idSource: explicitId === null ? "source_anchor" : "explicit",
|
||||
context,
|
||||
component,
|
||||
label
|
||||
label,
|
||||
helpExact: hasExactHelp,
|
||||
helpContextId,
|
||||
helpContextDynamic: hasExactHelp && helpContextId === null,
|
||||
helpDynamic: hasHelp && firstAttribute(attributes, helpAttributes) === null,
|
||||
helpRisk: risk.value,
|
||||
helpRiskSource: risk.source,
|
||||
helpRiskReviewed: riskReviewed,
|
||||
highRiskHelpMissing: risk.value !== null && !hasExactHelp && riskReviewed !== "standard"
|
||||
});
|
||||
}
|
||||
|
||||
@@ -352,6 +427,26 @@ function inspectSource(repository, sourceRoot, sourcePath) {
|
||||
return null;
|
||||
}
|
||||
|
||||
function nearestScopedHelpAttributes(node) {
|
||||
let current = node.parent;
|
||||
while (current) {
|
||||
if (ts.isJsxElement(current)) {
|
||||
const attributes = jsxAttributes(current.openingElement);
|
||||
if (attributes.get("data-help-scope") === "field") return attributes;
|
||||
}
|
||||
if (
|
||||
ts.isFunctionDeclaration(current) ||
|
||||
ts.isMethodDeclaration(current) ||
|
||||
ts.isArrowFunction(current) ||
|
||||
ts.isFunctionExpression(current)
|
||||
) {
|
||||
return new Map();
|
||||
}
|
||||
current = current.parent;
|
||||
}
|
||||
return new Map();
|
||||
}
|
||||
|
||||
function jsxAttributes(node) {
|
||||
const mapped = new Map();
|
||||
for (const attribute of node.attributes.properties) {
|
||||
@@ -577,6 +672,34 @@ function hasAnyAttribute(attributes, names) {
|
||||
return false;
|
||||
}
|
||||
|
||||
function helpRiskFor({ component, context, file, label, explicitId, name, explicitRisk }) {
|
||||
if (typeof explicitRisk === "string") {
|
||||
return supportedHelpRisks.has(explicitRisk)
|
||||
? { value: explicitRisk, source: "explicit" }
|
||||
: { value: null, source: "invalid_explicit" };
|
||||
}
|
||||
const value = [component, context, file, label, explicitId, name]
|
||||
.filter((item) => typeof item === "string")
|
||||
.join(" ")
|
||||
.toLowerCase()
|
||||
.replace(/^i18n:/g, "")
|
||||
.replace(/[._-]+/g, " ");
|
||||
const patterns = [
|
||||
["irreversible", /\b(delete|destroy|erase|purge|dispose|disposition|revoke|withdraw|shred)\b/],
|
||||
["credential", /\b(credential|password|secret|token|api key|private key)\b/],
|
||||
["retention", /\b(retention|legal hold|archive lifecycle)\b/],
|
||||
["encryption", /\b(encrypt|encryption|decrypt|decryption|signing key|signature key)\b/],
|
||||
["disclosure", /\b(disclose|disclosure|publish|share externally|public export)\b/],
|
||||
["external-effect", /\b(send|deliver|transfer|refund|payment execution|webhook execution)\b/],
|
||||
["authority", /\b(grant permission|role assignment|approve|reject|formal decision|mandate)\b/],
|
||||
["policy", /\b(policy apply|policy override|enforcement mode)\b/]
|
||||
];
|
||||
for (const [risk, pattern] of patterns) {
|
||||
if (pattern.test(value)) return { value: risk, source: "inferred" };
|
||||
}
|
||||
return { value: null, source: null };
|
||||
}
|
||||
|
||||
function slug(value) {
|
||||
const normalized = value
|
||||
.toLowerCase()
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user