From c7bea69e26932074505e979b4060f69d6db84100 Mon Sep 17 00:00:00 2001 From: Albrecht Degering Date: Tue, 14 Jul 2026 08:33:28 +0200 Subject: [PATCH] initial commit --- .gitea/ISSUE_TEMPLATE/bug_report.md | 35 ++++++++ .gitea/ISSUE_TEMPLATE/config.yaml | 1 + .gitea/ISSUE_TEMPLATE/docs_workflow.md | 27 ++++++ .gitea/ISSUE_TEMPLATE/feature_request.md | 32 +++++++ .gitea/ISSUE_TEMPLATE/task.md | 28 ++++++ .gitea/ISSUE_TEMPLATE/tech_debt.md | 25 ++++++ .gitea/PULL_REQUEST_TEMPLATE.md | 15 ++++ .gitignore | 13 +++ README.md | 69 +++++++++++++++ docs/DISTRIBUTION_LISTS_ARCHITECTURE.md | 98 +++++++++++++++++++++ docs/IMPLEMENTATION_PLAN.md | 75 ++++++++++++++++ pyproject.toml | 23 +++++ src/govoplan_dist_lists/__init__.py | 5 ++ src/govoplan_dist_lists/backend/__init__.py | 1 + src/govoplan_dist_lists/backend/manifest.py | 95 ++++++++++++++++++++ src/govoplan_dist_lists/py.typed | 1 + tests/test_manifest.py | 25 ++++++ 17 files changed, 568 insertions(+) create mode 100644 .gitea/ISSUE_TEMPLATE/bug_report.md create mode 100644 .gitea/ISSUE_TEMPLATE/config.yaml create mode 100644 .gitea/ISSUE_TEMPLATE/docs_workflow.md create mode 100644 .gitea/ISSUE_TEMPLATE/feature_request.md create mode 100644 .gitea/ISSUE_TEMPLATE/task.md create mode 100644 .gitea/ISSUE_TEMPLATE/tech_debt.md create mode 100644 .gitea/PULL_REQUEST_TEMPLATE.md create mode 100644 .gitignore create mode 100644 README.md create mode 100644 docs/DISTRIBUTION_LISTS_ARCHITECTURE.md create mode 100644 docs/IMPLEMENTATION_PLAN.md create mode 100644 pyproject.toml create mode 100644 src/govoplan_dist_lists/__init__.py create mode 100644 src/govoplan_dist_lists/backend/__init__.py create mode 100644 src/govoplan_dist_lists/backend/manifest.py create mode 100644 src/govoplan_dist_lists/py.typed create mode 100644 tests/test_manifest.py diff --git a/.gitea/ISSUE_TEMPLATE/bug_report.md b/.gitea/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..1d0c50f --- /dev/null +++ b/.gitea/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,35 @@ +--- +name: "Bug" +about: "Report a reproducible defect, regression, or incorrect behavior" +title: "[Bug] " +labels: + - type/bug + - status/triage + - module/dist-lists +--- + +## Scope + +- Repository: +- Area/module: +- Affected version or commit: + +## Behavior + +Expected: + +Actual: + +## Reproduction + +1. +2. +3. + +## Evidence + +Logs, screenshots, traces, or failing test output: + +## Verification Target + +Command or workflow that should pass when fixed: diff --git a/.gitea/ISSUE_TEMPLATE/config.yaml b/.gitea/ISSUE_TEMPLATE/config.yaml new file mode 100644 index 0000000..3ba13e0 --- /dev/null +++ b/.gitea/ISSUE_TEMPLATE/config.yaml @@ -0,0 +1 @@ +blank_issues_enabled: false diff --git a/.gitea/ISSUE_TEMPLATE/docs_workflow.md b/.gitea/ISSUE_TEMPLATE/docs_workflow.md new file mode 100644 index 0000000..5985cd9 --- /dev/null +++ b/.gitea/ISSUE_TEMPLATE/docs_workflow.md @@ -0,0 +1,27 @@ +--- +name: "Docs / workflow" +about: "Request documentation, process, or developer workflow changes" +title: "[Docs] " +labels: + - type/docs + - status/triage + - module/dist-lists + - area/docs +--- + +## Scope + +- Repository: +- Document or workflow: + +## Current State + +What is missing, unclear, duplicated, or stale? + +## Desired State + +What should the docs or workflow make clear? + +## Verification Target + +How should this be checked? diff --git a/.gitea/ISSUE_TEMPLATE/feature_request.md b/.gitea/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..cac7d1a --- /dev/null +++ b/.gitea/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,32 @@ +--- +name: "Feature" +about: "Propose new user-visible behavior or platform capability" +title: "[Feature] " +labels: + - type/feature + - status/triage + - module/dist-lists +--- + +## Problem + +What user, operator, or developer problem should this solve? + +## Proposed Capability + +What should exist when this is done? + +## Ownership + +- Owning repository: +- Related module repositories: +- Extension point or integration boundary: + +## Acceptance Criteria + +- [ ] +- [ ] + +## Verification Target + +Command, scenario, or UI flow that should prove completion: diff --git a/.gitea/ISSUE_TEMPLATE/task.md b/.gitea/ISSUE_TEMPLATE/task.md new file mode 100644 index 0000000..581bd87 --- /dev/null +++ b/.gitea/ISSUE_TEMPLATE/task.md @@ -0,0 +1,28 @@ +--- +name: "Task" +about: "Track implementation, maintenance, or migration work" +title: "[Task] " +labels: + - type/task + - status/triage + - module/dist-lists +--- + +## Objective + +What needs to be completed? + +## Scope + +- Owning repository: +- In-scope: +- Out-of-scope: + +## Checklist + +- [ ] +- [ ] + +## Verification Target + +Command or manual check: diff --git a/.gitea/ISSUE_TEMPLATE/tech_debt.md b/.gitea/ISSUE_TEMPLATE/tech_debt.md new file mode 100644 index 0000000..c4d1850 --- /dev/null +++ b/.gitea/ISSUE_TEMPLATE/tech_debt.md @@ -0,0 +1,25 @@ +--- +name: "Tech debt" +about: "Track cleanup, refactoring, risk reduction, or deferred engineering work" +title: "[Debt] " +labels: + - type/debt + - status/triage + - module/dist-lists +--- + +## Current Cost + +What does this make harder, riskier, slower, or more fragile? + +## Desired Shape + +What should the code, tests, or architecture look like afterwards? + +## Constraints + +What behavior, compatibility, or module boundary must be preserved? + +## Verification Target + +Focused checks that should pass: diff --git a/.gitea/PULL_REQUEST_TEMPLATE.md b/.gitea/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..1984736 --- /dev/null +++ b/.gitea/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,15 @@ +## Issue + +Closes # + +## Summary + +- + +## Verification + +- + +## Notes + +Follow-up issues: diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3ecde23 --- /dev/null +++ b/.gitignore @@ -0,0 +1,13 @@ +.venv/ +__pycache__/ +*.py[cod] +*.egg-info/ +.pytest_cache/ +.ruff_cache/ +dist/ +build/ +node_modules/ +coverage/ +.env +.env.* +!.env.example diff --git a/README.md b/README.md new file mode 100644 index 0000000..850d894 --- /dev/null +++ b/README.md @@ -0,0 +1,69 @@ +# GovOPlaN Distribution Lists + + +**Repository type:** module (domain). + + +`govoplan-dist-lists` will own reusable operational distribution lists +(`Verteiler`) for GovOPlaN. Distribution lists are broader than address lists: +they can resolve contacts, raw addresses, identities, users, groups, +organization units, functions, roles, and later nested lists into immutable +recipient snapshots for campaigns, postbox messages, notifications, scheduling, +polls, consultations, approvals, workflow, and case operations. + +The module is intentionally initialized as a documentation and manifest seed +first. Classical address lists remain in `govoplan-addresses`; this module will +take over the mixed operational recipient model once the address-owned list +primitive is stable. + +## Boundary + +Distribution Lists owns: + +- reusable mixed recipient definitions +- German-administration `Verteiler` semantics +- expansion of mixed entries into concrete delivery/recipient targets +- snapshot DTOs with source revision and provenance +- stale-source detection for expanded entries +- eligibility and policy explanations for disabled recipient entries +- audit evidence for who was resolved at execution time + +Distribution Lists does not own: + +- contacts, vCard/CardDAV address books, or address-only lists; those belong in + `govoplan-addresses` +- mail transport, queues, or mailbox access; those belong in `govoplan-mail` +- campaign versions, message rendering, or delivery evidence; those belong in + `govoplan-campaign` +- workflow/Umlauf execution state, ordering, deadlines, escalation, or task + state; those belong in `govoplan-workflow` and `govoplan-tasks` +- global identity, organization, tenancy, access, or policy engines + +## Key Distinction + +```text +AddressBook -> contacts and contact points +AddressList -> address-domain grouping of contacts/contact methods +DistributionList -> operational Verteiler with mixed recipient entry types +Umlauf -> workflow execution over recipients, actors, tasks, and deadlines +CampaignVersion/PostboxMessage/etc. -> immutable snapshot of expanded recipients +``` + +## Development Install + +```bash +cd /mnt/DATA/git/govoplan +./.venv/bin/python -m pip install -e ../govoplan-dist-lists +``` + +Focused manifest verification: + +```bash +cd /mnt/DATA/git/govoplan-dist-lists +PYTHONPATH=src:/mnt/DATA/git/govoplan-core/src /mnt/DATA/git/govoplan/.venv/bin/python -m unittest discover -s tests +``` + +## Design Documents + +- [Distribution lists architecture](docs/DISTRIBUTION_LISTS_ARCHITECTURE.md) +- [Implementation plan](docs/IMPLEMENTATION_PLAN.md) diff --git a/docs/DISTRIBUTION_LISTS_ARCHITECTURE.md b/docs/DISTRIBUTION_LISTS_ARCHITECTURE.md new file mode 100644 index 0000000..ba327f5 --- /dev/null +++ b/docs/DISTRIBUTION_LISTS_ARCHITECTURE.md @@ -0,0 +1,98 @@ +# GovOPlaN Distribution Lists Architecture + +## Decision + +`govoplan-dist-lists` owns reusable operational distribution lists +(`Verteiler`). This module is deliberately separate from `govoplan-addresses`: +address lists are plain address-domain groupings, while distribution lists are +cross-module recipient definitions that may resolve through address books, +identity, organizations, groups, functions, roles, raw addresses, and other +future provider capabilities. + +## Vocabulary + +- **Address book:** stores contacts and contact points such as email and postal + addresses. +- **Address list:** an address-owned list of contacts/contact methods from + address books. +- **Distribution list / Verteiler:** a reusable operational recipient + definition with mixed entry types. +- **Umlauf:** an execution workflow over recipients or actors with order, + state, deadlines, decisions, and escalation. Distribution lists define who; + workflow/tasks define what happens. +- **Snapshot:** immutable expansion evidence stored by the consuming module at + send, publish, decision, notification, or execution time. + +## Ownership + +Distribution Lists owns: + +- distribution-list definitions +- mixed recipient entries +- provider-neutral recipient entry DTOs +- expansion plans and expansion results +- immutable expansion snapshots and stale-source warnings +- nested-list handling, cycle detection, and expansion limits +- source provenance and traceability +- policy/read-only explanation payloads for unavailable entries + +It does not own: + +- contact storage or vCard/CardDAV sync +- identity or organization master data +- delivery transports +- campaign versioning +- workflow task execution + +## Entry Types + +Initial supported entry types should be provider-neutral: + +- `address_contact` +- `address_email` +- `raw_email` +- `raw_postal_address` +- `identity_principal` +- `organization_unit` +- `group` +- `function` +- `role` +- `distribution_list` + +Each entry should carry a stable source reference, display label, optional +delivery-channel hints, provenance, and a policy/readiness state. + +## Capability Direction + +The first capabilities should be: + +- `distLists.source`: list visible distribution lists as reusable recipient + sources. +- `distLists.expand`: expand a distribution list into immutable recipient rows. +- `distLists.writer`: explain whether a caller can create or change a list. + +Consumers must resolve those capabilities through core and must not import +distribution-list ORM or service internals. + +## Consumers + +Likely consumers: + +- Campaign: campaign recipient sources and send snapshots. +- Mail: ad-hoc recipient picking and reusable send groups. +- Postbox: recipient routing and publish snapshots. +- Notifications: notification target groups. +- Scheduling: invitation target groups. +- Poll/Evaluation/Consultation: invited participants and response audiences. +- Workflow/Tasks: Umlauf participant lists and escalation targets. +- Cases/Permits: case routing and responsible-function targeting. + +## Relationship To Address Lists + +`govoplan-addresses` should implement classical address lists first. A +distribution list may later include an address list as an entry, but the +address module remains the owner of contact records and address-only grouping. + +The distribution-list module owns mixed operational routing. This prevents +campaign-specific recipient logic from becoming the platform's de facto +Verteiler implementation. diff --git a/docs/IMPLEMENTATION_PLAN.md b/docs/IMPLEMENTATION_PLAN.md new file mode 100644 index 0000000..1e8052b --- /dev/null +++ b/docs/IMPLEMENTATION_PLAN.md @@ -0,0 +1,75 @@ +# GovOPlaN Distribution Lists Implementation Plan + +## Milestone 0: Module Seed + +Goal: establish repository identity, module boundary, and installable manifest. + +Tasks: + +- [x] initialize repository type and package metadata +- [x] document boundary between address lists, distribution lists, and Umlauf +- [x] add a no-op module manifest with durable documentation topics +- [ ] create Gitea issues for the implementation milestones + +## Milestone 1: Contract And DTOs + +Goal: define mixed recipient entries without implementing every provider. + +Tasks: + +- define distribution-list, entry, expansion-plan, and expansion-result DTOs +- define source reference format for address, identity, organization, group, + function, role, raw, and nested-list entries +- define policy/read-only explanation payloads +- define snapshot evidence shape +- add cycle detection and expansion-limit semantics + +## Milestone 2: Local Distribution Lists + +Goal: implement local, manually maintained distribution lists. + +Tasks: + +- add database tables and migrations +- add CRUD routes and permissions +- add local raw email/postal entries +- add references to address contacts and address lists via capabilities +- expose `distLists.source` and `distLists.expand` +- add tests for expansion and snapshots + +## Milestone 3: Provider Integrations + +Goal: allow distribution lists to resolve through installed modules. + +Tasks: + +- integrate with `addresses.lookup` and address-list source capability +- integrate with identity/principal resolution +- integrate with organizations/groups/functions once those modules expose + provider capabilities +- preserve module independence when optional providers are absent + +## Milestone 4: UI + +Goal: make distribution lists practical for administrators and operators. + +Tasks: + +- add list management UI +- add mixed entry editor with provider-specific search +- show disabled/read-only entries with hover explanations +- show expansion preview and stale-source warnings +- expose picker components to campaign, mail, postbox, notifications, + scheduling, poll, and workflow consumers + +## Milestone 5: Umlauf Integration + +Goal: support administrative circulation workflows without confusing list +definition with execution. + +Tasks: + +- let workflow/tasks consume distribution-list expansion snapshots +- support ordered participant sequences where workflow owns state +- add escalation and substitution hooks through workflow, not distribution lists +- preserve immutable recipient evidence for every execution diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..3640820 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,23 @@ +[build-system] +requires = ["setuptools>=69", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "govoplan-dist-lists" +version = "0.1.8" +description = "GovOPlaN operational distribution lists and Verteiler module seed." +readme = "README.md" +requires-python = ">=3.12" +authors = [{ name = "GovOPlaN" }] +dependencies = [ + "govoplan-core>=0.1.8", +] + +[tool.setuptools.packages.find] +where = ["src"] + +[tool.setuptools.package-data] +govoplan_dist_lists = ["py.typed"] + +[project.entry-points."govoplan.modules"] +dist_lists = "govoplan_dist_lists.backend.manifest:get_manifest" diff --git a/src/govoplan_dist_lists/__init__.py b/src/govoplan_dist_lists/__init__.py new file mode 100644 index 0000000..e026dea --- /dev/null +++ b/src/govoplan_dist_lists/__init__.py @@ -0,0 +1,5 @@ +"""GovOPlaN distribution lists module.""" + +__all__ = ["__version__"] + +__version__ = "0.1.8" diff --git a/src/govoplan_dist_lists/backend/__init__.py b/src/govoplan_dist_lists/backend/__init__.py new file mode 100644 index 0000000..b99fdf8 --- /dev/null +++ b/src/govoplan_dist_lists/backend/__init__.py @@ -0,0 +1 @@ +"""Backend integration for the distribution lists module.""" diff --git a/src/govoplan_dist_lists/backend/manifest.py b/src/govoplan_dist_lists/backend/manifest.py new file mode 100644 index 0000000..51ef300 --- /dev/null +++ b/src/govoplan_dist_lists/backend/manifest.py @@ -0,0 +1,95 @@ +from __future__ import annotations + +from govoplan_core.core.access import CAPABILITY_AUTH_PERMISSION_EVALUATOR, CAPABILITY_AUTH_PRINCIPAL_RESOLVER +from govoplan_core.core.modules import DocumentationTopic, ModuleInterfaceProvider, ModuleManifest, PermissionDefinition, RoleTemplate + +MODULE_ID = "dist_lists" +MODULE_NAME = "Distribution Lists" +MODULE_VERSION = "0.1.8" + +READ_SCOPE = "dist_lists:list:read" +WRITE_SCOPE = "dist_lists:list:write" +ADMIN_SCOPE = "dist_lists:list:admin" + + +def _permission(scope: str, label: str, description: str) -> PermissionDefinition: + module_id, resource, action = scope.split(":", 2) + return PermissionDefinition( + scope=scope, + label=label, + description=description, + category=MODULE_NAME, + level="tenant", + module_id=module_id, + resource=resource, + action=action, + ) + + +PERMISSIONS = ( + _permission(READ_SCOPE, "View distribution lists", "Read operational distribution lists and expansion previews."), + _permission(WRITE_SCOPE, "Manage distribution lists", "Create and edit operational distribution lists."), + _permission(ADMIN_SCOPE, "Administer distribution lists", "Configure distribution-list policies and provider integrations."), +) + +ROLE_TEMPLATES = ( + RoleTemplate( + slug="distribution_list_manager", + name="Distribution list manager", + description="Create, manage, preview, and reuse operational distribution lists.", + permissions=(READ_SCOPE, WRITE_SCOPE), + ), +) + +DOCUMENTATION = ( + DocumentationTopic( + id="dist-lists.boundary", + title="Distribution list boundary", + summary="Distribution lists model operational Verteiler separately from address-book lists and workflow Umlauf execution.", + body=( + "Distribution Lists owns reusable mixed recipient definitions and expansion snapshots. " + "Address lists remain in the addresses module and represent address-domain groupings only. " + "Workflow and Tasks own Umlauf execution state, ordering, deadlines, escalation, and completion." + ), + layer="available", + documentation_types=("admin", "user"), + audience=("operator", "module_admin", "product_owner"), + related_modules=("addresses", "campaigns", "mail", "postbox", "notifications", "scheduling", "poll", "workflow", "tasks"), + metadata={"seed": True}, + ), +) + + +manifest = ModuleManifest( + id=MODULE_ID, + name=MODULE_NAME, + version=MODULE_VERSION, + dependencies=(), + optional_dependencies=( + "addresses", + "access", + "identity", + "organizations", + "campaigns", + "mail", + "postbox", + "notifications", + "scheduling", + "poll", + "workflow", + "tasks", + ), + optional_capabilities=(CAPABILITY_AUTH_PRINCIPAL_RESOLVER, CAPABILITY_AUTH_PERMISSION_EVALUATOR), + provides_interfaces=( + ModuleInterfaceProvider(name="distLists.source", version=MODULE_VERSION), + ModuleInterfaceProvider(name="distLists.expand", version=MODULE_VERSION), + ModuleInterfaceProvider(name="distLists.writer", version=MODULE_VERSION), + ), + permissions=PERMISSIONS, + role_templates=ROLE_TEMPLATES, + documentation=DOCUMENTATION, +) + + +def get_manifest() -> ModuleManifest: + return manifest diff --git a/src/govoplan_dist_lists/py.typed b/src/govoplan_dist_lists/py.typed new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/govoplan_dist_lists/py.typed @@ -0,0 +1 @@ + diff --git a/tests/test_manifest.py b/tests/test_manifest.py new file mode 100644 index 0000000..7e99ac7 --- /dev/null +++ b/tests/test_manifest.py @@ -0,0 +1,25 @@ +from __future__ import annotations + +import unittest + +from govoplan_core.core.modules import ModuleManifest +from govoplan_dist_lists.backend.manifest import get_manifest + + +class DistributionListsManifestTests(unittest.TestCase): + def test_manifest_contract(self) -> None: + manifest = get_manifest() + + self.assertIsInstance(manifest, ModuleManifest) + self.assertEqual(manifest.id, "dist_lists") + self.assertFalse(manifest.dependencies) + self.assertIn("addresses", manifest.optional_dependencies) + self.assertIn("auth.principalResolver", manifest.optional_capabilities) + self.assertIn("distLists.expand", {interface.name for interface in manifest.provides_interfaces}) + self.assertIn("dist_lists:list:read", {permission.scope for permission in manifest.permissions}) + self.assertIsNone(manifest.route_factory) + self.assertIsNone(manifest.migration_spec) + + +if __name__ == "__main__": + unittest.main()