feat: implement governed project portfolio

This commit is contained in:
2026-08-01 17:48:38 +02:00
parent df8c3907c5
commit 8bfcf2d6b4
23 changed files with 4577 additions and 20 deletions
+257 -10
View File
@@ -1,16 +1,46 @@
from __future__ import annotations
from pathlib import Path
from sqlalchemy import func
from govoplan_core.core.access import (
CAPABILITY_AUTH_PERMISSION_EVALUATOR,
CAPABILITY_AUTH_PRINCIPAL_RESOLVER,
)
from govoplan_core.core.module_guards import (
drop_table_retirement_provider,
persistent_table_uninstall_guard,
)
from govoplan_core.core.modules import (
CapabilityDocumentation,
DocumentationLink,
DocumentationTopic,
FrontendModule,
FrontendRoute,
MigrationSpec,
ModuleContext,
ModuleInterfaceProvider,
ModuleManifest,
NavItem,
PermissionDefinition,
RoleTemplate,
)
from govoplan_core.core.provider_governance import (
ModuleArchitectureDeclaration,
ModuleArchitectureDocumentation,
ModuleMaturityEvidence,
)
from govoplan_core.core.search import SearchSourceProviderRegistration
from govoplan_core.core.views import ViewSurface
from govoplan_core.db.base import Base
from govoplan_projects.backend.acl import ProjectScopeAclProvider
from govoplan_projects.backend.db import models as project_models
from govoplan_projects.backend.search_source import create_projects_search_source
from govoplan_projects.backend.service import (
CAPABILITY_PROJECTS_REGISTRY,
SqlProjectRegistry,
)
MODULE_ID = "projects"
@@ -30,8 +60,97 @@ OPTIONAL_DEPENDENCIES = (
"connectors",
"search",
"notifications",
"reporting",
"risk_compliance",
)
ARCHITECTURE = ModuleArchitectureDeclaration(
layer="domain_capability",
kind="domain",
maturity="vertical_slice",
evidence=(
ModuleMaturityEvidence(
kind="documentation",
reference="docs/PROJECTS_DOMAIN_BOUNDARY.md",
summary=(
"Defines portfolio, project, outcome, governance, and integration "
"ownership."
),
),
ModuleMaturityEvidence(
kind="test",
reference="tests/test_project_service.py",
summary=(
"Proves immutable revisions, OCC, idempotency, tenant isolation, "
"restricted memberships, state transitions, and search rechecks."
),
),
),
known_limits=(
"OpenProject transport and synchronization remain connector-owned provider work.",
"Task execution remains in Tasks or Tickets; Projects links work without duplicating its lifecycle.",
"The first WebUI edits core planning fields; advanced outcome, capacity, and benefit structures remain available through the governed API.",
),
supported_authority_modes=(
"native_authoritative",
"external_authoritative",
"external_mirror",
"governed_sync",
"governance_overlay",
"linked_reference",
),
owned_concepts=(
"portfolio and project identity",
"project goals and intended outcomes",
"project dependencies and capacity assumptions",
"project benefit and change-impact review",
),
non_owned_concepts=(
"actionable task and ticket lifecycle",
"risk and control lifecycle",
"report calculation and presentation",
"external project-system transport",
),
documentation=ModuleArchitectureDocumentation(
operations=("docs/PROJECTS_DOMAIN_BOUNDARY.md",),
),
)
def _router(context: ModuleContext):
del context
from govoplan_projects.backend.router import router
return router
def _registry(context: ModuleContext) -> SqlProjectRegistry:
del context
return SqlProjectRegistry()
def _tenant_summary(session, tenant_id: str) -> dict[str, int]:
counts = {
kind: count
for kind, count in (
session.query(
project_models.ProjectObjectRevision.object_kind,
func.count(),
)
.filter(
project_models.ProjectObjectRevision.tenant_id == tenant_id,
project_models.ProjectObjectRevision.superseded_at.is_(None),
)
.group_by(project_models.ProjectObjectRevision.object_kind)
.all()
)
}
return {
"portfolios": int(counts.get("portfolio", 0)),
"projects": int(counts.get("project", 0)),
"milestones": int(counts.get("milestone", 0)),
}
def _permission(
scope: str,
@@ -52,9 +171,15 @@ def _permission(
PERMISSIONS = (
_permission(READ_SCOPE, "View projects", "Read accessible projects and portfolios."),
_permission(WRITE_SCOPE, "Manage projects", "Create and update projects and milestones."),
_permission(ADMIN_SCOPE, "Administer projects", "Configure project types and policies."),
_permission(
READ_SCOPE, "View projects", "Read accessible projects and portfolios."
),
_permission(
WRITE_SCOPE, "Manage projects", "Create and update projects and milestones."
),
_permission(
ADMIN_SCOPE, "Administer projects", "Configure project types and policies."
),
)
ROLE_TEMPLATES = (
@@ -77,17 +202,19 @@ DOCUMENTATION = (
id="projects.module-boundary",
title="Projects module boundary",
summary=(
"Portfolios, projects, goals, milestones, participants, status, "
"work structure, and project-level references."
"Portfolios, projects, versioned goals and outcomes, milestones, "
"dependencies, capacity, benefits, participants, status, and references."
),
body=(
"Projects owns native project context. Tasks and Tickets own "
"actionable work, Cases owns formal procedures, and Connectors "
"owns OpenProject synchronization."
"owns OpenProject synchronization. Reporting owns measured indicators; "
"Risk Compliance owns risks and controls; Projects links those facts to "
"planning, change impact, and benefit review."
),
layer="available",
documentation_types=("admin",),
audience=("operator", "module_admin", "product_owner"),
documentation_types=("admin", "user"),
audience=("user", "operator", "module_admin", "product_owner"),
related_modules=OPTIONAL_DEPENDENCIES,
links=(
DocumentationLink(
@@ -102,13 +229,17 @@ DOCUMENTATION = (
"portfolio",
"project",
"milestone",
"versioned goal and intended outcome",
"dependency and capacity assumption",
"benefit review",
"project participant",
"project resource link",
"external project reference",
],
"first_slice": (
"Implement project identity, status, milestones, participants, "
"resource links, and OpenProject reference mapping."
"Implement project and portfolio identity, status, milestones, "
"participants, outcome/benefit intent, dependency/resource links, "
"and OpenProject reference mapping."
),
},
),
@@ -126,7 +257,123 @@ manifest = ModuleManifest(
),
permissions=PERMISSIONS,
role_templates=ROLE_TEMPLATES,
route_factory=_router,
nav_items=(
NavItem(
path="/projects",
label="Projects",
icon="folder-kanban",
required_any=(READ_SCOPE,),
order=36,
),
),
frontend=FrontendModule(
module_id=MODULE_ID,
package_name="@govoplan/projects-webui",
routes=(
FrontendRoute(
path="/projects",
component="ProjectsPage",
required_any=(READ_SCOPE,),
order=36,
),
),
nav_items=(
NavItem(
path="/projects",
label="Projects",
icon="folder-kanban",
required_any=(READ_SCOPE,),
order=36,
),
),
view_surfaces=(
ViewSurface(
id="projects.navigation",
module_id=MODULE_ID,
kind="navigation",
label="Projects navigation",
order=10,
),
ViewSurface(
id="projects.workspace",
module_id=MODULE_ID,
kind="route",
label="Projects workspace",
order=20,
),
ViewSurface(
id="projects.portfolios",
module_id=MODULE_ID,
kind="section",
label="Portfolio planning",
parent_id="projects.workspace",
order=30,
),
ViewSurface(
id="projects.outcomes",
module_id=MODULE_ID,
kind="section",
label="Outcomes and benefits",
parent_id="projects.workspace",
order=40,
),
),
),
provides_interfaces=(
ModuleInterfaceProvider(name="projects.registry", version="0.1.0"),
),
capability_factories={CAPABILITY_PROJECTS_REGISTRY: _registry},
capability_documentation={
CAPABILITY_PROJECTS_REGISTRY: CapabilityDocumentation(
label="Projects registry",
summary=(
"Persists portfolios, projects, milestones, planning intent, "
"membership grants, revisions, and lifecycle events."
),
contract_version="0.1.0",
),
},
search_sources=(
SearchSourceProviderRegistration(
id="projects.objects",
factory=create_projects_search_source,
),
),
migration_spec=MigrationSpec(
module_id=MODULE_ID,
metadata=Base.metadata,
script_location=str(Path(__file__).with_name("migrations") / "versions"),
retirement_supported=True,
retirement_provider=drop_table_retirement_provider(
project_models.ProjectObjectEvent,
project_models.ProjectMembershipGrant,
project_models.ProjectObjectRevision,
project_models.ProjectObjectIdentity,
label="Projects",
),
retirement_notes=(
"Destructive retirement requires a database snapshot and removes "
"Projects identities, immutable revisions, memberships, and events."
),
),
uninstall_guard_providers=(
persistent_table_uninstall_guard(
project_models.ProjectObjectIdentity,
project_models.ProjectObjectRevision,
project_models.ProjectMembershipGrant,
project_models.ProjectObjectEvent,
label="Projects",
),
),
resource_acl_providers=(
ProjectScopeAclProvider("portfolio"),
ProjectScopeAclProvider("project"),
ProjectScopeAclProvider("milestone"),
),
tenant_summary_providers=(_tenant_summary,),
documentation=DOCUMENTATION,
architecture=ARCHITECTURE,
)