Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4cc21e2674 | ||
|
|
c9bd2bfab8 |
@@ -11,6 +11,11 @@ publication, remedy, and review references.
|
||||
The ordinary read projection withholds protected reasoning and operative
|
||||
content. A separate sensitive-read permission is required to disclose it.
|
||||
|
||||
Unversioned list and detail reads follow the platform temporal-data context.
|
||||
Valid time determines when the outcome applied; recorded time reconstructs
|
||||
which revision the system knew then. Exact revision references bypass that
|
||||
projection. Permissions and mutation targets always use current state.
|
||||
|
||||
## Lifecycle
|
||||
|
||||
Writes use the shared Decision transition matrix and optimistic concurrency.
|
||||
|
||||
+2
-2
@@ -4,12 +4,12 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "govoplan-decisions"
|
||||
version = "0.1.15"
|
||||
version = "0.1.16"
|
||||
description = "Formal institutional decision lifecycle and reconstruction for GovOPlaN."
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.12"
|
||||
authors = [{ name = "GovOPlaN" }]
|
||||
dependencies = ["govoplan-core>=0.1.15"]
|
||||
dependencies = ["govoplan-core>=0.1.16"]
|
||||
|
||||
[tool.setuptools.packages.find]
|
||||
where = ["src"]
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
"""GovOPlaN Decisions module."""
|
||||
|
||||
__version__ = "0.1.15"
|
||||
__version__ = "0.1.16"
|
||||
|
||||
@@ -13,7 +13,7 @@ from govoplan_decisions.backend.service import SqlDecisionRegistry
|
||||
|
||||
MODULE_ID = "decisions"
|
||||
MODULE_NAME = "Decisions"
|
||||
MODULE_VERSION = "0.1.15"
|
||||
MODULE_VERSION = "0.1.16"
|
||||
READ_SCOPE = "decisions:decision:read"
|
||||
SENSITIVE_READ_SCOPE = "decisions:decision:read_sensitive"
|
||||
WRITE_SCOPE = "decisions:decision:write"
|
||||
@@ -68,7 +68,7 @@ manifest = ModuleManifest(
|
||||
id="decisions.formal-outcome",
|
||||
title="Formal institutional Decisions",
|
||||
summary="Reconstruct authority, evidence, reasoning, outcome, effects, and review history.",
|
||||
body="Decisions preserves immutable formal outcomes. Corrections and revocations create linked revisions; requested and observed effects remain distinct for reconciliation.",
|
||||
body="Decisions preserves immutable formal outcomes. Corrections and revocations create linked revisions; requested and observed effects remain distinct for reconciliation. List and unversioned detail reads follow the titlebar valid-time and recorded-time selection; exact revision references remain exact and current authorization is unchanged.",
|
||||
layer="configured",
|
||||
documentation_types=("admin", "user"),
|
||||
audience=("user", "operator", "module_admin", "auditor"),
|
||||
|
||||
@@ -12,6 +12,7 @@ from govoplan_core.core.institutional import (
|
||||
TemporalRevision,
|
||||
revise_formal_decision,
|
||||
)
|
||||
from govoplan_core.db.temporal import apply_temporal_revision_filter
|
||||
from govoplan_decisions.backend.db.models import FormalDecisionRevision
|
||||
|
||||
|
||||
@@ -37,7 +38,7 @@ class SqlDecisionRegistry:
|
||||
FormalDecisionRevision.decision_id == reference.object_id,
|
||||
)
|
||||
if reference.version is None:
|
||||
query = query.filter(FormalDecisionRevision.superseded_at.is_(None))
|
||||
query = apply_temporal_revision_filter(query, FormalDecisionRevision)
|
||||
else:
|
||||
query = query.filter(FormalDecisionRevision.revision == reference.version)
|
||||
row = query.order_by(FormalDecisionRevision.recorded_at.desc()).first()
|
||||
@@ -142,8 +143,8 @@ def list_decisions(
|
||||
raise DecisionStoreError("Decision list limit must be between 1 and 200.")
|
||||
query = session.query(FormalDecisionRevision).filter(
|
||||
FormalDecisionRevision.tenant_id == tenant_id,
|
||||
FormalDecisionRevision.superseded_at.is_(None),
|
||||
)
|
||||
query = apply_temporal_revision_filter(query, FormalDecisionRevision)
|
||||
if state:
|
||||
query = query.filter(FormalDecisionRevision.state == state)
|
||||
if decision_type:
|
||||
|
||||
+66
-1
@@ -17,8 +17,17 @@ from govoplan_core.core.institutional import (
|
||||
TemporalRevision,
|
||||
revise_formal_decision,
|
||||
)
|
||||
from govoplan_core.core.temporal import (
|
||||
TemporalDataContext,
|
||||
bind_temporal_data_context,
|
||||
reset_temporal_data_context,
|
||||
)
|
||||
from govoplan_decisions.backend.db.models import FormalDecisionRevision
|
||||
from govoplan_decisions.backend.service import DecisionStoreError, SqlDecisionRegistry
|
||||
from govoplan_decisions.backend.service import (
|
||||
DecisionStoreError,
|
||||
SqlDecisionRegistry,
|
||||
list_decisions,
|
||||
)
|
||||
|
||||
|
||||
NOW = datetime(2026, 8, 1, 12, 0, tzinfo=UTC)
|
||||
@@ -96,6 +105,62 @@ class DecisionTests(unittest.TestCase):
|
||||
with self.assertRaisesRegex(Exception, "same-tenant"):
|
||||
registry.get_decision(self.session, Principal("tenant-2"), reference=first.reference)
|
||||
|
||||
def test_temporal_context_selects_valid_and_recorded_state(self) -> None:
|
||||
registry = SqlDecisionRegistry()
|
||||
first = registry.record_decision(self.session, self.principal, decision=decision())
|
||||
temporal = TemporalRevision(
|
||||
revision="2",
|
||||
valid_from=NOW,
|
||||
recorded_at=NOW + timedelta(minutes=1),
|
||||
change_reason="Effect confirmed.",
|
||||
)
|
||||
revised = revise_formal_decision(
|
||||
first,
|
||||
expected_revision="1",
|
||||
temporal=temporal,
|
||||
state="effective",
|
||||
)
|
||||
registry.record_decision(
|
||||
self.session,
|
||||
self.principal,
|
||||
decision=revised,
|
||||
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:
|
||||
historical = registry.get_decision(
|
||||
self.session,
|
||||
self.principal,
|
||||
reference=reference("decision", "decision-1", "decisions", None),
|
||||
)
|
||||
self.assertEqual("1", historical.temporal.revision if historical else None)
|
||||
finally:
|
||||
reset_temporal_data_context(token)
|
||||
|
||||
token = bind_temporal_data_context(
|
||||
TemporalDataContext(
|
||||
validity_mode="at",
|
||||
valid_at=NOW - timedelta(days=1),
|
||||
)
|
||||
)
|
||||
try:
|
||||
self.assertEqual((), list_decisions(self.session, self.principal))
|
||||
finally:
|
||||
reset_temporal_data_context(token)
|
||||
|
||||
token = bind_temporal_data_context(TemporalDataContext(validity_mode="all"))
|
||||
try:
|
||||
self.assertEqual(1, len(list_decisions(self.session, self.principal)))
|
||||
finally:
|
||||
reset_temporal_data_context(token)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
Reference in New Issue
Block a user