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