4 Commits
Author SHA1 Message Date
zemion 8e3a144bc2 Release v0.1.18
Module Package Release / publish-packages (push) Successful in 10s
2026-08-05 21:07:45 +02:00
zemion 47b87284b1 Release v0.1.17
Module Package Release / publish-packages (push) Successful in 11s
2026-08-05 20:34:04 +02:00
zemion ac7a964a69 Release v0.1.16
Module Package Release / publish-packages (push) Successful in 12s
2026-08-05 19:52:11 +02:00
zemion 6144fba6ce feat: honor temporal read context 2026-08-05 00:03:32 +02:00
6 changed files with 57 additions and 20 deletions
+5
View File
@@ -21,6 +21,11 @@ Consumers must freeze the exact mandate reference and evidence used for a
consequential action. A later mandate correction does not rewrite historical consequential action. A later mandate correction does not rewrite historical
decisions or effects. decisions or effects.
Catalogue reads follow the platform temporal-data context, independently
selecting valid time and the system's recorded-state cutoff. A resolver's
explicit effective instant takes precedence. Competence and authorization for
a new action are never recovered merely by browsing historical data.
## Revision And Recovery ## Revision And Recovery
Creation and revision use optimistic concurrency. Every new revision has a new Creation and revision use optimistic concurrency. Every new revision has a new
+2 -2
View File
@@ -4,12 +4,12 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "govoplan-mandates" name = "govoplan-mandates"
version = "0.1.15" version = "0.1.18"
description = "Effective institutional mandate, jurisdiction, and authority lifecycle for GovOPlaN." description = "Effective institutional mandate, jurisdiction, and authority lifecycle for GovOPlaN."
readme = "README.md" readme = "README.md"
requires-python = ">=3.12" requires-python = ">=3.12"
authors = [{ name = "GovOPlaN" }] authors = [{ name = "GovOPlaN" }]
dependencies = ["govoplan-core>=0.1.15"] dependencies = ["govoplan-core>=0.1.18"]
[tool.setuptools.packages.find] [tool.setuptools.packages.find]
where = ["src"] where = ["src"]
+1 -1
View File
@@ -1,3 +1,3 @@
"""GovOPlaN institutional mandates module.""" """GovOPlaN institutional mandates module."""
__version__ = "0.1.15" __version__ = "0.1.18"
+3 -2
View File
@@ -26,7 +26,7 @@ from govoplan_mandates.backend.service import SqlMandateResolver
MODULE_ID = "mandates" MODULE_ID = "mandates"
MODULE_NAME = "Mandates" MODULE_NAME = "Mandates"
MODULE_VERSION = "0.1.15" MODULE_VERSION = "0.1.18"
READ_SCOPE = "mandates:definition:read" READ_SCOPE = "mandates:definition:read"
WRITE_SCOPE = "mandates:definition:write" WRITE_SCOPE = "mandates:definition:write"
ADMIN_SCOPE = "mandates:definition:admin" ADMIN_SCOPE = "mandates:definition:admin"
@@ -117,7 +117,8 @@ manifest = ModuleManifest(
summary="Define and resolve effective authority, jurisdiction, legal basis, and evidence.", summary="Define and resolve effective authority, jurisdiction, legal basis, and evidence.",
body=( body=(
"Mandates stores immutable revisions and resolves the one effective authority for a task. " "Mandates stores immutable revisions and resolves the one effective authority for a task. "
"Conflicting or missing authority fails closed. Consequential consumers retain the exact revision and evidence." "Conflicting or missing authority fails closed. Consequential consumers retain the exact revision and evidence. "
"Catalogue reads follow the titlebar valid-time and recorded-time selection; explicit resolution times take precedence and current authorization is unchanged."
), ),
layer="configured", layer="configured",
documentation_types=("admin", "user"), documentation_types=("admin", "user"),
+16 -15
View File
@@ -3,7 +3,6 @@ from __future__ import annotations
from datetime import UTC, datetime from datetime import UTC, datetime
from typing import Any, Mapping from typing import Any, Mapping
from sqlalchemy import or_
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from govoplan_core.core.institutional import ( from govoplan_core.core.institutional import (
@@ -15,6 +14,8 @@ from govoplan_core.core.institutional import (
resolve_mandate_candidates, resolve_mandate_candidates,
revise_mandate_definition, revise_mandate_definition,
) )
from govoplan_core.core.temporal import TemporalDataContext
from govoplan_core.db.temporal import apply_temporal_revision_filter
from govoplan_mandates.backend.db.models import MandateRevision from govoplan_mandates.backend.db.models import MandateRevision
@@ -120,7 +121,7 @@ def get_mandate(
if revision is not None: if revision is not None:
query = query.filter(MandateRevision.revision == revision) query = query.filter(MandateRevision.revision == revision)
else: else:
query = query.filter(MandateRevision.superseded_at.is_(None)) query = apply_temporal_revision_filter(query, MandateRevision)
row = query.order_by(MandateRevision.recorded_at.desc()).first() row = query.order_by(MandateRevision.recorded_at.desc()).first()
return _definition_from_row(row) if row is not None else None return _definition_from_row(row) if row is not None else None
@@ -137,8 +138,8 @@ def list_mandates(
raise MandateStoreError("Mandate list limit must be between 1 and 200.") raise MandateStoreError("Mandate list limit must be between 1 and 200.")
query = session.query(MandateRevision).filter( query = session.query(MandateRevision).filter(
MandateRevision.tenant_id == tenant_id, MandateRevision.tenant_id == tenant_id,
MandateRevision.superseded_at.is_(None),
) )
query = apply_temporal_revision_filter(query, MandateRevision)
if status: if status:
query = query.filter(MandateRevision.status == status) query = query.filter(MandateRevision.status == status)
rows = query.order_by( rows = query.order_by(
@@ -162,19 +163,19 @@ class SqlMandateResolver:
"Mandate resolution cannot cross tenants." "Mandate resolution cannot cross tenants."
) )
typed_session = _session(session) typed_session = _session(session)
query = typed_session.query(MandateRevision).filter(
MandateRevision.tenant_id == tenant_id,
)
query = apply_temporal_revision_filter(
query,
MandateRevision,
context=TemporalDataContext(
validity_mode="at",
valid_at=request.effective_at,
),
)
rows = ( rows = (
typed_session.query(MandateRevision) query
.filter(
MandateRevision.tenant_id == tenant_id,
or_(
MandateRevision.valid_from.is_(None),
MandateRevision.valid_from <= request.effective_at,
),
or_(
MandateRevision.valid_to.is_(None),
MandateRevision.valid_to > request.effective_at,
),
)
.order_by( .order_by(
MandateRevision.mandate_id.asc(), MandateRevision.mandate_id.asc(),
MandateRevision.recorded_at.desc(), MandateRevision.recorded_at.desc(),
+30
View File
@@ -13,6 +13,11 @@ from govoplan_core.core.institutional import (
MandateResolutionRequest, MandateResolutionRequest,
TemporalRevision, TemporalRevision,
) )
from govoplan_core.core.temporal import (
TemporalDataContext,
bind_temporal_data_context,
reset_temporal_data_context,
)
from govoplan_mandates.backend.db.models import MandateRevision from govoplan_mandates.backend.db.models import MandateRevision
from govoplan_mandates.backend.service import ( from govoplan_mandates.backend.service import (
MandateStoreError, MandateStoreError,
@@ -124,6 +129,31 @@ class MandateTests(unittest.TestCase):
) )
) )
def test_read_context_can_reconstruct_recorded_state(self) -> None:
record_mandate(self.session, self.principal, definition=definition())
record_mandate(
self.session,
self.principal,
definition=definition(revision="2", status="suspended"),
expected_revision="1",
)
token = bind_temporal_data_context(
TemporalDataContext(
validity_mode="at",
valid_at=NOW + timedelta(hours=1),
recorded_at=NOW + timedelta(seconds=30),
)
)
try:
result = get_mandate(
self.session,
self.principal,
mandate_id="committee-permit",
)
self.assertEqual("1", result.temporal.revision if result else None)
finally:
reset_temporal_data_context(token)
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()