From 5d4535f7b598a7a6f22b2a605acf91f3313a88e7 Mon Sep 17 00:00:00 2001 From: Albrecht Degering Date: Thu, 20 Aug 2026 12:58:53 +0200 Subject: [PATCH] feat: generate evidence-based fit assessments --- docs/README.md | 10 +- ...TUTIONAL_GOVERNANCE_TARGET_ARCHITECTURE.md | 2 +- docs/capability-fit-current.json | 280 ++++++++- docs/capability-fit.schema.json | 67 ++- ...BILITY_AND_INFRASTRUCTURE_FIT.generated.md | 323 ++++++++++ .../CAPABILITY_AND_INFRASTRUCTURE_FIT.md | 8 +- .../CONNECTED_GOVERNANCE_PLATFORM_ROADMAP.md | 2 +- tests/test_capability_fit_generation.py | 124 ++++ .../generate-capability-fit-report.py | 126 ++++ .../govoplan_assessment/report_generator.py | 551 ++++++++++++++++++ tools/checks/check-focused.sh | 2 + tools/checks/check-module-matrix.sh | 2 + 12 files changed, 1487 insertions(+), 10 deletions(-) create mode 100644 docs/evidence/snapshots/CAPABILITY_AND_INFRASTRUCTURE_FIT.generated.md create mode 100644 tests/test_capability_fit_generation.py create mode 100644 tools/assessments/generate-capability-fit-report.py create mode 100644 tools/assessments/govoplan_assessment/report_generator.py diff --git a/docs/README.md b/docs/README.md index 1693010..f3778b3 100644 --- a/docs/README.md +++ b/docs/README.md @@ -82,7 +82,8 @@ 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. -- [Campaign capability and infrastructure fit, 2026-07-22](evidence/snapshots/CAPABILITY_AND_INFRASTRUCTURE_FIT.md) +- [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) @@ -93,6 +94,13 @@ evidence inputs, and project configuration. Their paths are intentionally stable because tools and published schema identifiers consume them; they are not additional reading-list entries. +Regenerate and verify the human fit report from its JSON input with: + +```sh +./.venv/bin/python tools/assessments/generate-capability-fit-report.py +./.venv/bin/python tools/assessments/generate-capability-fit-report.py --check +``` + ## Maintenance Rules 1. Gitea issues are the only live source for work state, priority, and owner. diff --git a/docs/architecture/INSTITUTIONAL_GOVERNANCE_TARGET_ARCHITECTURE.md b/docs/architecture/INSTITUTIONAL_GOVERNANCE_TARGET_ARCHITECTURE.md index 2524982..ef6c81d 100644 --- a/docs/architecture/INSTITUTIONAL_GOVERNANCE_TARGET_ARCHITECTURE.md +++ b/docs/architecture/INSTITUTIONAL_GOVERNANCE_TARGET_ARCHITECTURE.md @@ -23,7 +23,7 @@ Read this together with: - [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](../evidence/snapshots/CAPABILITY_AND_INFRASTRUCTURE_FIT.md) +- [Generated Capability and Infrastructure Fit](../evidence/snapshots/CAPABILITY_AND_INFRASTRUCTURE_FIT.generated.md) - [Core Module Architecture](../../../govoplan-core/docs/MODULE_ARCHITECTURE.md) - [Core 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) diff --git a/docs/capability-fit-current.json b/docs/capability-fit-current.json index 69c5975..4af4f99 100644 --- a/docs/capability-fit-current.json +++ b/docs/capability-fit-current.json @@ -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.", diff --git a/docs/capability-fit.schema.json b/docs/capability-fit.schema.json index 111e153..ff34c24 100644 --- a/docs/capability-fit.schema.json +++ b/docs/capability-fit.schema.json @@ -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" }, diff --git a/docs/evidence/snapshots/CAPABILITY_AND_INFRASTRUCTURE_FIT.generated.md b/docs/evidence/snapshots/CAPABILITY_AND_INFRASTRUCTURE_FIT.generated.md new file mode 100644 index 0000000..1dabb1e --- /dev/null +++ b/docs/evidence/snapshots/CAPABILITY_AND_INFRASTRUCTURE_FIT.generated.md @@ -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**
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.
**Proof:** Run contract, migration, API and WebUI module-permutation gates on the installed release. | +| **access.local**
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.
**Proof:** Exercise joiner, role change, suspension and protected-owner recovery. | +| **campaign.journey**
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.
**Proof:** Run the complete journey with safe data and the target-like mail service. | +| **files.managed_attachments**
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.
**Proof:** Back up and restore files together with database references. | +| **mail.smtp_imap**
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.
**Proof:** Prove SMTP acceptance, IMAP append, throttling and outcome reconciliation. | +| **addresses.recipient_sources**
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.
**Proof:** Build a Campaign from a source list and verify immutable recipient provenance. | +| **audit.local**
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.
**Proof:** Exercise privileged-event review, retention and any required external export. | +| **identity.federation**
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.
**Proof:** Run provider metadata, login/provisioning, deprovisioning and failure tests. | +| **compliance.export_control**
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.
**Proof:** Validate list ingestion, versioning, matching, false-positive review and audit evidence. | +| **workflow**
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.
**Proof:** Reassess in a later Workflow-focused composition. | + +## Infrastructure matrix + +| Requirement | Status | Evidence | Conditions and gaps | Recommendation and proof | +| --- | --- | --- | --- | --- | +| **runtime.web_api**
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.
**Proof:** Deploy the built artifacts and run health/module-route checks. | +| **runtime.worker**
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.
**Proof:** Interrupt and restart a worker while preserving job/reconciliation safety. | +| **runtime.scheduler**
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.
**Proof:** Prove missed-schedule recovery and single-leader behavior. | +| **data.postgresql**
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.
**Proof:** Run migrations and restore a target-like database. | +| **queue.redis**
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.
**Proof:** Exercise broker interruption and worker recovery. | +| **storage.local**
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.
**Proof:** Restore files and verify all database references. | +| **storage.object**
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.
**Proof:** Upload, retrieve, version, back up and restore representative objects. | +| **edge.proxy_tls**
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.
**Proof:** Run external TLS/header/cookie/CORS and upload-limit tests. | +| **security.secret_store**
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.
**Proof:** Rotate a non-production credential and recover from a protected backup. | +| **identity.access**
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.
**Proof:** Exercise login, role change, account suspension, protected bootstrap, and break-glass recovery in the target. | +| **connectors.mail**
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.
**Proof:** Exercise target-like SMTP acceptance, IMAP append, throttling, outage, retry, and reconciliation through the approved network path. | +| **operations.monitoring**
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.
**Proof:** Trigger each readiness/delivery failure and verify an actionable alert. | +| **operations.audit**
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.
**Proof:** Exercise privileged-event review, retention, export failure/retry, and target SIEM or archive ingestion. | +| **operations.backup_restore**
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.
**Proof:** Restore the whole service into an isolated environment and measure it. | +| **operations.disaster_recovery**
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.
**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**
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**
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**
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. diff --git a/docs/evidence/snapshots/CAPABILITY_AND_INFRASTRUCTURE_FIT.md b/docs/evidence/snapshots/CAPABILITY_AND_INFRASTRUCTURE_FIT.md index 0379628..90ec962 100644 --- a/docs/evidence/snapshots/CAPABILITY_AND_INFRASTRUCTURE_FIT.md +++ b/docs/evidence/snapshots/CAPABILITY_AND_INFRASTRUCTURE_FIT.md @@ -1,4 +1,10 @@ -# 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 diff --git a/docs/strategy/reference/CONNECTED_GOVERNANCE_PLATFORM_ROADMAP.md b/docs/strategy/reference/CONNECTED_GOVERNANCE_PLATFORM_ROADMAP.md index 7da1f05..60322d5 100644 --- a/docs/strategy/reference/CONNECTED_GOVERNANCE_PLATFORM_ROADMAP.md +++ b/docs/strategy/reference/CONNECTED_GOVERNANCE_PLATFORM_ROADMAP.md @@ -21,7 +21,7 @@ Read it together with: - 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 [pinned Campaign capability and infrastructure fit assessment](../../evidence/snapshots/CAPABILITY_AND_INFRASTRUCTURE_FIT.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) diff --git a/tests/test_capability_fit_generation.py b/tests/test_capability_fit_generation.py new file mode 100644 index 0000000..fa77b55 --- /dev/null +++ b/tests/test_capability_fit_generation.py @@ -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() diff --git a/tools/assessments/generate-capability-fit-report.py b/tools/assessments/generate-capability-fit-report.py new file mode 100644 index 0000000..789aa8a --- /dev/null +++ b/tools/assessments/generate-capability-fit-report.py @@ -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()) diff --git a/tools/assessments/govoplan_assessment/report_generator.py b/tools/assessments/govoplan_assessment/report_generator.py new file mode 100644 index 0000000..4bc483c --- /dev/null +++ b/tools/assessments/govoplan_assessment/report_generator.py @@ -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( + "| **{}**
{} | {} | {} | {} | {} |".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( + "| **{}**
{} | `{}` | {} | {} | {}
**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", +) diff --git a/tools/checks/check-focused.sh b/tools/checks/check-focused.sh index a8c5e54..70a3281 100644 --- a/tools/checks/check-focused.sh +++ b/tools/checks/check-focused.sh @@ -46,6 +46,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 diff --git a/tools/checks/check-module-matrix.sh b/tools/checks/check-module-matrix.sh index e150baf..f2dda0c 100644 --- a/tools/checks/check-module-matrix.sh +++ b/tools/checks/check-module-matrix.sh @@ -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