feat: declare governed external provider state
This commit is contained in:
@@ -16,6 +16,7 @@ from govoplan_core.core.calendar import (
|
||||
)
|
||||
from govoplan_core.core.module_guards import drop_table_retirement_provider, persistent_table_uninstall_guard
|
||||
from govoplan_core.core.modules import (
|
||||
DocumentationTopic,
|
||||
FrontendModule,
|
||||
FrontendRoute,
|
||||
MigrationSpec,
|
||||
@@ -26,10 +27,225 @@ from govoplan_core.core.modules import (
|
||||
PermissionDefinition,
|
||||
RoleTemplate,
|
||||
)
|
||||
from govoplan_core.core.provider_governance import (
|
||||
ExternalProviderDeclaration,
|
||||
ExternalProviderStateProviderRegistration,
|
||||
ModuleArchitectureDeclaration,
|
||||
ModuleArchitectureDocumentation,
|
||||
ModuleMaturityEvidence,
|
||||
ProviderBehaviorDeclaration,
|
||||
ProviderObjectDeclaration,
|
||||
)
|
||||
from govoplan_core.core.views import ViewSurface
|
||||
from govoplan_core.db.base import Base
|
||||
|
||||
|
||||
CALDAV_PROVIDER_ID = "calendar.caldav_sync"
|
||||
ICS_PROVIDER_ID = "calendar.ics_subscription"
|
||||
GRAPH_PROVIDER_ID = "calendar.microsoft_graph"
|
||||
EWS_PROVIDER_ID = "calendar.exchange_ews"
|
||||
|
||||
CALENDAR_ARCHITECTURE = ModuleArchitectureDeclaration(
|
||||
layer="communication_participation",
|
||||
kind="domain",
|
||||
maturity="vertical_slice",
|
||||
evidence=(
|
||||
ModuleMaturityEvidence(
|
||||
kind="test",
|
||||
reference="tests/test_caldav.py",
|
||||
summary="Exercises CalDAV discovery, pull, push, deletion, and reconciliation behavior.",
|
||||
),
|
||||
ModuleMaturityEvidence(
|
||||
kind="test",
|
||||
reference="tests/test_outbox.py",
|
||||
summary="Exercises durable calendar effects, retries, terminal states, and cleanup.",
|
||||
),
|
||||
ModuleMaturityEvidence(
|
||||
kind="documentation",
|
||||
reference="docs/CALENDAR_INTEGRATION_CONCEPT.md",
|
||||
summary="Documents Calendar ownership and optional integration boundaries.",
|
||||
),
|
||||
),
|
||||
known_limits=(
|
||||
"Provider target-environment and recovery-drill evidence is not yet packaged as a reference package.",
|
||||
"Microsoft Graph and EWS adapters do not yet have the same write/reconciliation coverage as CalDAV.",
|
||||
),
|
||||
supported_authority_modes=(
|
||||
"native_authoritative",
|
||||
"external_authoritative",
|
||||
"external_mirror",
|
||||
"governed_sync",
|
||||
"linked_reference",
|
||||
),
|
||||
owned_concepts=(
|
||||
"calendar collections",
|
||||
"VEVENT lifecycle",
|
||||
"recurrence and availability",
|
||||
"calendar synchronization state",
|
||||
),
|
||||
non_owned_concepts=(
|
||||
"meeting polls",
|
||||
"mail delivery",
|
||||
"task lifecycle",
|
||||
"external identity and credential secrets",
|
||||
),
|
||||
target_tested_providers=(CALDAV_PROVIDER_ID,),
|
||||
documentation=ModuleArchitectureDocumentation(
|
||||
migration=("src/govoplan_calendar/backend/migrations/versions",),
|
||||
upgrade=("docs/CALENDAR_INTEGRATION_CONCEPT.md",),
|
||||
recovery=("docs/CALENDAR_INTEGRATION_CONCEPT.md",),
|
||||
security=("tests/test_caldav_security.py", "tests/test_http_security.py"),
|
||||
operations=("docs/CALENDAR_INTEGRATION_CONCEPT.md",),
|
||||
),
|
||||
)
|
||||
|
||||
def _read_only_calendar_provider(
|
||||
*,
|
||||
provider_id: str,
|
||||
label: str,
|
||||
source_name: str,
|
||||
) -> ExternalProviderDeclaration:
|
||||
return ExternalProviderDeclaration(
|
||||
id=provider_id,
|
||||
module_id="calendar",
|
||||
label=label,
|
||||
maturity="read",
|
||||
operations=("read", "preview"),
|
||||
objects=(
|
||||
ProviderObjectDeclaration(
|
||||
object_type="calendar_collection",
|
||||
field_groups=("identity", "display", "sync_state"),
|
||||
authority_modes=("external_authoritative", "external_mirror"),
|
||||
default_authority_mode="external_mirror",
|
||||
),
|
||||
ProviderObjectDeclaration(
|
||||
object_type="calendar_event",
|
||||
field_groups=(
|
||||
"identity",
|
||||
"schedule",
|
||||
"recurrence",
|
||||
"participants",
|
||||
"content",
|
||||
),
|
||||
authority_modes=("external_authoritative", "external_mirror"),
|
||||
default_authority_mode="external_mirror",
|
||||
),
|
||||
),
|
||||
behavior=ProviderBehaviorDeclaration(
|
||||
revision_tokens=f"{source_name} object identity, remote revision, and content fingerprint are retained.",
|
||||
concurrency="The source is inbound-only; refresh compares remote identity and revision before replacing the derived projection.",
|
||||
freshness="Last attempt, last success, and source status are retained.",
|
||||
health="Authentication, transport, parsing, and source failures are recorded without exposing credentials.",
|
||||
max_read_items=5000,
|
||||
evidence=f"Imported events retain the {source_name} source, remote identity, revision, and normalized content fingerprint.",
|
||||
audit_event_types=("calendar.sync.requested", "calendar.sync.completed"),
|
||||
correction="A later successful refresh creates the corrected local projection while source authority remains external.",
|
||||
reconciliation="Re-read the bounded source and compare remote identity, revision, and normalized event content.",
|
||||
outage="The last local projection remains available with stale or unknown freshness.",
|
||||
classifications=("internal", "confidential", "restricted"),
|
||||
purposes=("calendar subscription", "availability", "meeting coordination"),
|
||||
retention="Calendar projection, sync evidence, and credential retention are independent policies.",
|
||||
secret_handling="Authentication material remains in Calendar credential records or deployment-owned secret references.",
|
||||
),
|
||||
documentation_topic_ids=("calendar.external-sources-and-sync",),
|
||||
)
|
||||
|
||||
|
||||
CALENDAR_EXTERNAL_PROVIDERS = (
|
||||
ExternalProviderDeclaration(
|
||||
id=CALDAV_PROVIDER_ID,
|
||||
module_id="calendar",
|
||||
label="CalDAV calendar synchronization",
|
||||
maturity="synchronize",
|
||||
operations=(
|
||||
"discover",
|
||||
"read",
|
||||
"write",
|
||||
"delete",
|
||||
"synchronize",
|
||||
"preview",
|
||||
),
|
||||
objects=(
|
||||
ProviderObjectDeclaration(
|
||||
object_type="calendar_collection",
|
||||
field_groups=("identity", "display", "sync_state"),
|
||||
authority_modes=(
|
||||
"native_authoritative",
|
||||
"external_authoritative",
|
||||
"external_mirror",
|
||||
"governed_sync",
|
||||
),
|
||||
default_authority_mode="governed_sync",
|
||||
),
|
||||
ProviderObjectDeclaration(
|
||||
object_type="calendar_event",
|
||||
field_groups=(
|
||||
"identity",
|
||||
"schedule",
|
||||
"recurrence",
|
||||
"participants",
|
||||
"content",
|
||||
),
|
||||
authority_modes=(
|
||||
"native_authoritative",
|
||||
"external_authoritative",
|
||||
"external_mirror",
|
||||
"governed_sync",
|
||||
),
|
||||
default_authority_mode="governed_sync",
|
||||
),
|
||||
),
|
||||
behavior=ProviderBehaviorDeclaration(
|
||||
revision_tokens="CalDAV sync tokens and per-resource ETags are retained.",
|
||||
concurrency="Conditional requests reject stale ETags; conflicts remain explicit.",
|
||||
freshness="Last attempt, last success, sync token, and pending outbox work are visible.",
|
||||
health="Discovery, authentication, transport, collection, and reconciliation failures are recorded.",
|
||||
max_read_items=5000,
|
||||
idempotency="Stable VEVENT UID, source id, and durable operation keys suppress duplicate effects.",
|
||||
retry="Only classified transient failures receive bounded backoff retries.",
|
||||
timeout_seconds=30,
|
||||
conflicts="Stale revisions and concurrent remote changes enter conflict/reconciliation state.",
|
||||
outcome_unknown="A timed-out remote write is outcome-unknown and is not blindly repeated.",
|
||||
outcome_unknown_supported=True,
|
||||
evidence="Operation id, request intent, ETag/sync token, response classification, and reconciliation result are retained.",
|
||||
audit_event_types=(
|
||||
"calendar.sync.requested",
|
||||
"calendar.sync.completed",
|
||||
"calendar.sync.conflict",
|
||||
"calendar.sync.reconciled",
|
||||
),
|
||||
correction="A later conditional update or tombstone corrects external state after reconciliation.",
|
||||
rollback="Remote effects are not treated as transactionally rollback-safe.",
|
||||
compensation="A compensating event update/delete may be queued after the remote state is known.",
|
||||
reconciliation="Read the resource by href/UID, compare ETag and content, then classify applied, retryable, conflict, or manual.",
|
||||
outage="Local calendars remain usable with stale markers; pending writes stay durable and bounded.",
|
||||
classifications=("internal", "confidential", "restricted"),
|
||||
purposes=("calendar collaboration", "availability", "meeting coordination"),
|
||||
retention="Calendar event, outbox terminal-state, audit, and credential retention are independent policies.",
|
||||
secret_handling="Authentication material is stored through Calendar credential records or external secret references and is never emitted in provider metadata.",
|
||||
),
|
||||
capability_names=(CAPABILITY_CALENDAR_OUTBOX,),
|
||||
interface_names=("calendar.outbox",),
|
||||
documentation_topic_ids=("calendar.external-sources-and-sync",),
|
||||
),
|
||||
_read_only_calendar_provider(
|
||||
provider_id=ICS_PROVIDER_ID,
|
||||
label="ICS and webcal subscription",
|
||||
source_name="ICS/webcal",
|
||||
),
|
||||
_read_only_calendar_provider(
|
||||
provider_id=GRAPH_PROVIDER_ID,
|
||||
label="Microsoft Graph calendar projection",
|
||||
source_name="Microsoft Graph",
|
||||
),
|
||||
_read_only_calendar_provider(
|
||||
provider_id=EWS_PROVIDER_ID,
|
||||
label="Exchange Web Services calendar projection",
|
||||
source_name="Exchange Web Services",
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
_calendar_table_retirement_provider = drop_table_retirement_provider(
|
||||
calendar_models.CalendarCollection,
|
||||
calendar_models.CalendarEvent,
|
||||
@@ -185,6 +401,30 @@ def _calendar_outbox_provider(context: ModuleContext) -> object:
|
||||
)
|
||||
|
||||
|
||||
def _caldav_provider_states(context):
|
||||
from govoplan_calendar.backend.provider_state import caldav_provider_states
|
||||
|
||||
return caldav_provider_states(context)
|
||||
|
||||
|
||||
def _ics_provider_states(context):
|
||||
from govoplan_calendar.backend.provider_state import ics_provider_states
|
||||
|
||||
return ics_provider_states(context)
|
||||
|
||||
|
||||
def _graph_provider_states(context):
|
||||
from govoplan_calendar.backend.provider_state import graph_provider_states
|
||||
|
||||
return graph_provider_states(context)
|
||||
|
||||
|
||||
def _ews_provider_states(context):
|
||||
from govoplan_calendar.backend.provider_state import ews_provider_states
|
||||
|
||||
return ews_provider_states(context)
|
||||
|
||||
|
||||
manifest = ModuleManifest(
|
||||
id="calendar",
|
||||
name="Calendar",
|
||||
@@ -200,6 +440,52 @@ manifest = ModuleManifest(
|
||||
route_factory=_calendar_router,
|
||||
role_templates=ROLE_TEMPLATES,
|
||||
tenant_summary_providers=(_tenant_summary,),
|
||||
architecture=CALENDAR_ARCHITECTURE,
|
||||
external_providers=CALENDAR_EXTERNAL_PROVIDERS,
|
||||
external_provider_state_providers=(
|
||||
ExternalProviderStateProviderRegistration(
|
||||
module_id="calendar",
|
||||
provider_id=CALDAV_PROVIDER_ID,
|
||||
provider=_caldav_provider_states,
|
||||
),
|
||||
ExternalProviderStateProviderRegistration(
|
||||
module_id="calendar",
|
||||
provider_id=ICS_PROVIDER_ID,
|
||||
provider=_ics_provider_states,
|
||||
),
|
||||
ExternalProviderStateProviderRegistration(
|
||||
module_id="calendar",
|
||||
provider_id=GRAPH_PROVIDER_ID,
|
||||
provider=_graph_provider_states,
|
||||
),
|
||||
ExternalProviderStateProviderRegistration(
|
||||
module_id="calendar",
|
||||
provider_id=EWS_PROVIDER_ID,
|
||||
provider=_ews_provider_states,
|
||||
),
|
||||
),
|
||||
documentation=(
|
||||
DocumentationTopic(
|
||||
id="calendar.manage-calendars-and-events",
|
||||
title="Use calendars and events",
|
||||
summary="Create calendar collections and work with all-day or timed events in continuous, month, week, workweek, and day views.",
|
||||
body="Calendar remembers the selected view and preferences. Events can be created, edited, moved, resized, repeated, imported, exported, or deleted when the current account has the corresponding permission. All-day events use dates rather than local clock times; timed events retain their timezone-aware start and end values.",
|
||||
documentation_types=("user",),
|
||||
audience=("user", "calendar_manager"),
|
||||
related_modules=("scheduling", "notifications"),
|
||||
metadata={"kind": "reference"},
|
||||
),
|
||||
DocumentationTopic(
|
||||
id="calendar.external-sources-and-sync",
|
||||
title="Connect and synchronize external calendars",
|
||||
summary="Calendar supports local collections, two-way CalDAV, and read-only ICS/webcal, Microsoft Graph, and Exchange Web Services sources.",
|
||||
body="Each external source keeps its URL, synchronization direction, status, and credential reference with the Calendar collection. Manual or scheduled synchronization records bounded outcomes. CalDAV writes use conditional requests and durable outbox state; conflicts and unknown outcomes require synchronization or explicit reconciliation instead of blind repetition. Removing an external source removes the connection, while deleting a local calendar deletes its owned events after confirmation or transfer.",
|
||||
documentation_types=("admin", "user"),
|
||||
audience=("user", "calendar_manager", "operator"),
|
||||
related_modules=("connectors", "audit", "ops"),
|
||||
metadata={"kind": "reference"},
|
||||
),
|
||||
),
|
||||
capability_factories={
|
||||
CAPABILITY_CALENDAR_OUTBOX: _calendar_outbox_provider,
|
||||
CAPABILITY_CALENDAR_SCHEDULING: _calendar_scheduling_provider,
|
||||
|
||||
Reference in New Issue
Block a user