Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a5866ada72 | ||
|
|
4cc21e2674 | ||
|
|
c9bd2bfab8 |
@@ -11,6 +11,11 @@ publication, remedy, and review references.
|
|||||||
The ordinary read projection withholds protected reasoning and operative
|
The ordinary read projection withholds protected reasoning and operative
|
||||||
content. A separate sensitive-read permission is required to disclose it.
|
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
|
## Lifecycle
|
||||||
|
|
||||||
Writes use the shared Decision transition matrix and optimistic concurrency.
|
Writes use the shared Decision transition matrix and optimistic concurrency.
|
||||||
|
|||||||
+2
-2
@@ -4,12 +4,12 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "govoplan-decisions"
|
name = "govoplan-decisions"
|
||||||
version = "0.1.15"
|
version = "0.1.17"
|
||||||
description = "Formal institutional decision lifecycle and reconstruction for GovOPlaN."
|
description = "Formal institutional decision lifecycle and reconstruction 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.17"]
|
||||||
|
|
||||||
[tool.setuptools.packages.find]
|
[tool.setuptools.packages.find]
|
||||||
where = ["src"]
|
where = ["src"]
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
"""GovOPlaN Decisions module."""
|
"""GovOPlaN Decisions module."""
|
||||||
|
|
||||||
__version__ = "0.1.15"
|
__version__ = "0.1.17"
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ from govoplan_decisions.backend.service import SqlDecisionRegistry
|
|||||||
|
|
||||||
MODULE_ID = "decisions"
|
MODULE_ID = "decisions"
|
||||||
MODULE_NAME = "Decisions"
|
MODULE_NAME = "Decisions"
|
||||||
MODULE_VERSION = "0.1.15"
|
MODULE_VERSION = "0.1.17"
|
||||||
READ_SCOPE = "decisions:decision:read"
|
READ_SCOPE = "decisions:decision:read"
|
||||||
SENSITIVE_READ_SCOPE = "decisions:decision:read_sensitive"
|
SENSITIVE_READ_SCOPE = "decisions:decision:read_sensitive"
|
||||||
WRITE_SCOPE = "decisions:decision:write"
|
WRITE_SCOPE = "decisions:decision:write"
|
||||||
@@ -68,7 +68,7 @@ manifest = ModuleManifest(
|
|||||||
id="decisions.formal-outcome",
|
id="decisions.formal-outcome",
|
||||||
title="Formal institutional Decisions",
|
title="Formal institutional Decisions",
|
||||||
summary="Reconstruct authority, evidence, reasoning, outcome, effects, and review history.",
|
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",
|
layer="configured",
|
||||||
documentation_types=("admin", "user"),
|
documentation_types=("admin", "user"),
|
||||||
audience=("user", "operator", "module_admin", "auditor"),
|
audience=("user", "operator", "module_admin", "auditor"),
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ from govoplan_core.core.institutional import (
|
|||||||
TemporalRevision,
|
TemporalRevision,
|
||||||
revise_formal_decision,
|
revise_formal_decision,
|
||||||
)
|
)
|
||||||
|
from govoplan_core.db.temporal import apply_temporal_revision_filter
|
||||||
from govoplan_decisions.backend.db.models import FormalDecisionRevision
|
from govoplan_decisions.backend.db.models import FormalDecisionRevision
|
||||||
|
|
||||||
|
|
||||||
@@ -37,7 +38,7 @@ class SqlDecisionRegistry:
|
|||||||
FormalDecisionRevision.decision_id == reference.object_id,
|
FormalDecisionRevision.decision_id == reference.object_id,
|
||||||
)
|
)
|
||||||
if reference.version is None:
|
if reference.version is None:
|
||||||
query = query.filter(FormalDecisionRevision.superseded_at.is_(None))
|
query = apply_temporal_revision_filter(query, FormalDecisionRevision)
|
||||||
else:
|
else:
|
||||||
query = query.filter(FormalDecisionRevision.revision == reference.version)
|
query = query.filter(FormalDecisionRevision.revision == reference.version)
|
||||||
row = query.order_by(FormalDecisionRevision.recorded_at.desc()).first()
|
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.")
|
raise DecisionStoreError("Decision list limit must be between 1 and 200.")
|
||||||
query = session.query(FormalDecisionRevision).filter(
|
query = session.query(FormalDecisionRevision).filter(
|
||||||
FormalDecisionRevision.tenant_id == tenant_id,
|
FormalDecisionRevision.tenant_id == tenant_id,
|
||||||
FormalDecisionRevision.superseded_at.is_(None),
|
|
||||||
)
|
)
|
||||||
|
query = apply_temporal_revision_filter(query, FormalDecisionRevision)
|
||||||
if state:
|
if state:
|
||||||
query = query.filter(FormalDecisionRevision.state == state)
|
query = query.filter(FormalDecisionRevision.state == state)
|
||||||
if decision_type:
|
if decision_type:
|
||||||
|
|||||||
+66
-1
@@ -17,8 +17,17 @@ from govoplan_core.core.institutional import (
|
|||||||
TemporalRevision,
|
TemporalRevision,
|
||||||
revise_formal_decision,
|
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.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)
|
NOW = datetime(2026, 8, 1, 12, 0, tzinfo=UTC)
|
||||||
@@ -96,6 +105,62 @@ class DecisionTests(unittest.TestCase):
|
|||||||
with self.assertRaisesRegex(Exception, "same-tenant"):
|
with self.assertRaisesRegex(Exception, "same-tenant"):
|
||||||
registry.get_decision(self.session, Principal("tenant-2"), reference=first.reference)
|
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__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
Reference in New Issue
Block a user