Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ac7a964a69 | ||
|
|
6144fba6ce |
@@ -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
@@ -4,12 +4,12 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "govoplan-mandates"
|
name = "govoplan-mandates"
|
||||||
version = "0.1.15"
|
version = "0.1.16"
|
||||||
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.16"]
|
||||||
|
|
||||||
[tool.setuptools.packages.find]
|
[tool.setuptools.packages.find]
|
||||||
where = ["src"]
|
where = ["src"]
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
"""GovOPlaN institutional mandates module."""
|
"""GovOPlaN institutional mandates module."""
|
||||||
|
|
||||||
__version__ = "0.1.15"
|
__version__ = "0.1.16"
|
||||||
|
|||||||
@@ -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.16"
|
||||||
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"),
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
Reference in New Issue
Block a user