"""Decision outcome read for a submitter's own memos (INFD-WP-0010-T04, founder decision 2026-11-01). State Hub's dispatch worker (STATE-WP-0096) pulls the human's answer to a memo it posed and carries it to the hub; informed-decision holds no hub credential. This module builds the ``informed-decision.decision-outcome`` v1 record (state-hub ``docs/decision-services-alignment-answer.md`` section 5.2) from evidence already recorded here. Read-only. Value-free: identifiers, digests, the verb, the deciding subject and its assurance. No packet content, brief, note, reasons, display name or token ever leaves through this record. It is a projection of evidence; nothing may execute on the strength of it. """ from datetime import datetime, timezone import json from .evidence import EventClass from .store import EvidenceUnavailable DISPOSITION = EventClass.DISPOSITION.value KIND = "informed-decision.decision-outcome" VERSION = 1 TERMINAL = ("accept", "decline") MAX_RECORD = 8 * 1024 class OutcomeError(Exception): def __init__(self, status, code): self.status, self.code = status, code super().__init__(code) def _assurance(db, disposition_id): """The assurance the PDP evaluated for this act, from its recorded policy observation.""" row = db.execute("SELECT content FROM evidence WHERE class=? " "AND json_extract(envelope,'$.data.disposition_id')=? ORDER BY rowid LIMIT 1", (DISPOSITION, disposition_id)).fetchone() policy_id = json.loads(row["content"]).get("policy_observation_id") if row else None observation = db.execute("SELECT request,outcome FROM policy_observations WHERE id=?", (policy_id,)).fetchone() if policy_id else None if observation is None or observation["outcome"] != "allow": return None attributes = json.loads(observation["request"]).get("subject", {}).get("attributes", {}) assurance = attributes.get("assurance") if (not isinstance(assurance, dict) or not isinstance(assurance.get("level"), str) or type(assurance.get("at")) is not int or assurance["at"] <= 0): return None auth_time = datetime.fromtimestamp(assurance["at"], timezone.utc).isoformat(timespec="seconds") return {"aal": assurance["level"], "auth_time": auth_time.replace("+00:00", "Z")} def outcome_for(store, submitter_id, memo_id, *, issuer): """(status, body) for one memo. 404 hides memos this submitter did not pose.""" with store._connection() as db: intake = db.execute("SELECT metadata FROM intake_ledger WHERE submitter=? AND memo_id=? AND state='ready'", (submitter_id, memo_id)).fetchone() if intake is None: raise OutcomeError(404, "memo_not_found") metadata = json.loads(intake["metadata"]) serves = metadata.get("serves") if metadata.get("profile") == "present_existing" or not serves or serves.get("resolves") != "on-answer": raise OutcomeError(409, "memo_does_not_resolve") # The first terminal human answer on any revision is the answer (resolves: on-answer). rows = db.execute("SELECT d.id,d.body FROM dispositions d JOIN presentations p ON p.id=d.presentation_id " "WHERE p.memo_id=? ORDER BY d.rowid", (memo_id,)).fetchall() answer = None for row in rows: body = json.loads(row["body"]) if body["verb"] in TERMINAL and body["actor"]["kind"] == "person": answer = body break if answer is None: return 202, {"kind": KIND, "version": VERSION, "memo_id": memo_id, "status": "pending"} presentation = json.loads(db.execute("SELECT body FROM presentations WHERE id=?", (answer["presentation_id"],)).fetchone()["body"]) assurance = _assurance(db, answer["id"]) if assurance is None: raise OutcomeError(409, "assurance_unrecorded") record = {"kind": KIND, "version": VERSION, "status": "decided", "hub_decision_id": serves["hub_decision_id"], "memo_id": memo_id, "memo_revision": answer["memo_version"], "verb": answer["verb"], "subject": {"issuer": issuer, "sub": answer["actor"]["sub"]}, "assurance": assurance, "decided_at": answer["at"], "view_hash": presentation["view_hash"], "presentation_id": answer["presentation_id"], "approval_id": answer.get("approval_id")} if answer["verb"] == "accept": submission = db.execute("SELECT state,approved_at FROM submissions WHERE disposition_id=?", (answer["id"],)).fetchone() if submission is None or submission["state"] in ("prepared", "in_flight"): # The human accepted; the approval entry is not yet confirmed. Not an answer yet. return 202, {"kind": KIND, "version": VERSION, "memo_id": memo_id, "status": "pending"} if submission["state"] != "confirmed": raise OutcomeError(409, "approval_entry_unresolved") # approval-engine entries carry no id; (approval_id, subject, approved_at) is the correlation. record["approval_entry"] = {"approved_at": submission["approved_at"]} audit = db.execute("SELECT o.receiver_reference FROM evidence e JOIN outbox o ON o.id=e.id " "WHERE e.class=? AND json_extract(e.envelope,'$.data.disposition_id')=? " "AND o.state='delivered' ORDER BY e.rowid LIMIT 1", (DISPOSITION, answer["id"])).fetchone() if audit is not None and audit["receiver_reference"]: record["audit_ref"] = audit["receiver_reference"] if len(json.dumps(record).encode()) > MAX_RECORD: raise EvidenceUnavailable("outcome record exceeds its bound") return 200, record