STATE-WP-0101-T02/T03: decision classes, evidence refusals, write-back route
T02: additive migration d5e6f7a8b9c0 (decision_class, memo_id,
approval_id, resolution_source, resolution_evidence, evidence_refs,
CHECKs, partial unique memo_id); typed evidence_refs; 409
evidence_required for text resolve, resolving PATCH fields and resolved
create on C1/C2; raise-only class; C3 refused as reserved.
T03: POST /decisions/{id}/resolve-evidence (link/resolve), projected
token verified by TokenReview with audience
state-hub-decision-writeback and an exact subject allowlist; outcome
record v1 validation, 8 KiB bound, secret scan; idempotent on
(decision_id, memo_id). Off by default (DECISION_WRITEBACK_ENABLED).
Tests use a fake reviewer; full suite 1286 passed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 5502@bnt-lap001
Assistant-Session: 68b3b02f-5d0f-4e71-8421-796f62e50196
This commit is contained in:
parent
b86bd17cef
commit
0b473c1bda
8 changed files with 1225 additions and 39 deletions
|
|
@ -2,8 +2,8 @@ import enum
|
|||
import uuid
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import CheckConstraint, DateTime, Enum, ForeignKey, String, Text
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
from sqlalchemy import CheckConstraint, DateTime, Enum, ForeignKey, Index, String, Text, text
|
||||
from sqlalchemy.dialects.postgresql import JSONB, UUID
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from api.models.base import Base, TimestampMixin, new_uuid
|
||||
|
|
@ -21,6 +21,41 @@ class DecisionStatus(str, enum.Enum):
|
|||
superseded = "superseded"
|
||||
|
||||
|
||||
class DecisionClass(str, enum.Enum):
|
||||
"""Gate-house decision classes (GH-DEC-2026-026). NULL on a row means legacy (as C0)."""
|
||||
|
||||
C0 = "C0"
|
||||
C1 = "C1"
|
||||
C2 = "C2"
|
||||
C3 = "C3" # reserved by GH-DEC-2026-026; stored values allowed, the API refuses it
|
||||
|
||||
|
||||
class ResolutionSource(str, enum.Enum):
|
||||
text = "text"
|
||||
informed_decision = "informed_decision"
|
||||
|
||||
|
||||
# Classes whose resolution needs informed-decision evidence (STATE-WP-0101, design section 3.1).
|
||||
# C3 is reserved and refused by the API; the database treats it fail-closed like C2.
|
||||
EVIDENCE_CLASSES = frozenset({DecisionClass.C1, DecisionClass.C2, DecisionClass.C3})
|
||||
APPROVAL_CLASSES = frozenset({DecisionClass.C2, DecisionClass.C3})
|
||||
CLASS_RANK = {None: 0, DecisionClass.C0: 0, DecisionClass.C1: 1, DecisionClass.C2: 2, DecisionClass.C3: 3}
|
||||
|
||||
# Shared with the Alembic migration (STATE-WP-0101-T02); keep the two in step.
|
||||
CK_DECISION_CLASS = "decision_class IS NULL OR decision_class IN ('C0','C1','C2','C3')"
|
||||
CK_RESOLUTION_SOURCE = (
|
||||
"resolution_source IS NULL OR resolution_source IN ('text','informed_decision')"
|
||||
)
|
||||
CK_CLASS_EVIDENCE = (
|
||||
"NOT (status = 'resolved' AND decision_class IN ('C1','C2','C3')) OR "
|
||||
"(resolution_source = 'informed_decision' AND memo_id IS NOT NULL "
|
||||
"AND resolution_evidence IS NOT NULL)"
|
||||
)
|
||||
CK_CLASS_APPROVAL = (
|
||||
"NOT (status = 'resolved' AND decision_class IN ('C2','C3')) OR approval_id IS NOT NULL"
|
||||
)
|
||||
|
||||
|
||||
class Decision(Base, TimestampMixin):
|
||||
__tablename__ = "decisions"
|
||||
__table_args__ = (
|
||||
|
|
@ -28,6 +63,16 @@ class Decision(Base, TimestampMixin):
|
|||
"topic_id IS NOT NULL OR workplan_id IS NOT NULL",
|
||||
name="ck_decisions_topic_or_workplan",
|
||||
),
|
||||
CheckConstraint(CK_DECISION_CLASS, name="ck_decisions_decision_class"),
|
||||
CheckConstraint(CK_RESOLUTION_SOURCE, name="ck_decisions_resolution_source"),
|
||||
CheckConstraint(CK_CLASS_EVIDENCE, name="ck_decisions_class_evidence"),
|
||||
CheckConstraint(CK_CLASS_APPROVAL, name="ck_decisions_class_approval"),
|
||||
Index(
|
||||
"uq_decisions_memo_id",
|
||||
"memo_id",
|
||||
unique=True,
|
||||
postgresql_where=text("memo_id IS NOT NULL"),
|
||||
),
|
||||
)
|
||||
|
||||
id: Mapped[uuid.UUID] = mapped_column(
|
||||
|
|
@ -58,6 +103,15 @@ class Decision(Base, TimestampMixin):
|
|||
superseded_by: Mapped[uuid.UUID | None] = mapped_column(
|
||||
UUID(as_uuid=True), ForeignKey("decisions.id", ondelete="SET NULL"), nullable=True
|
||||
)
|
||||
# STATE-WP-0101: decision class and informed-decision evidence links.
|
||||
decision_class: Mapped[str | None] = mapped_column(String(2), nullable=True, index=True)
|
||||
memo_id: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
approval_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), nullable=True)
|
||||
resolution_source: Mapped[str | None] = mapped_column(String(32), nullable=True)
|
||||
resolution_evidence: Mapped[dict | None] = mapped_column(JSONB, nullable=True)
|
||||
evidence_refs: Mapped[list] = mapped_column(
|
||||
JSONB, nullable=False, default=list, server_default=text("'[]'::jsonb")
|
||||
)
|
||||
|
||||
topic: Mapped["Topic | None"] = relationship("Topic", back_populates="decisions") # noqa: F821
|
||||
workplan: Mapped["Workplan | None"] = relationship("Workplan", back_populates="decisions") # noqa: F821
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue