STATE-WP-0101-T02/T03: decision classes, evidence refusals, write-back route
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 2s
Build and Publish Multi-Context Image / build-and-push (push) Successful in 1m3s

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:
tegwick 2026-11-01 04:01:48 +01:00
parent b86bd17cef
commit 0b473c1bda
8 changed files with 1225 additions and 39 deletions

View file

@ -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