Connect lifecycle revisions to protected human review

Assistant: codex
Assistant-Model: gpt-6-astra
Assistant-Session: 01a10780-6d41-7952-9718-dfbc1f41236f
This commit is contained in:
tegwick 2026-10-04 20:33:24 +02:00
parent 0e79d69e27
commit f3b71b1521
16 changed files with 649 additions and 23 deletions

View file

@ -98,7 +98,13 @@ class LifecycleWorkspace:
except LifecycleError as error:
if error.status != 403:
raise
return 200, self._page(memo, events, session, previous), []
outcome = None
try:
outcome = self.service.outcomes(caller, memo.id, memo.version)
except LifecycleError as error:
if error.status != 403:
raise
return 200, self._page(memo, events, session, previous, outcome), []
if method == "POST" and action in {"revise", "comment", "publish", "source", "evaluate"}:
if params:
raise ValueError()
@ -131,13 +137,31 @@ class LifecycleWorkspace:
raise ValueError()
uuid.UUID(form["key"])
def _page(self, memo, events, session, previous=None):
def _page(self, memo, events, session, previous=None, outcome=None):
base = "/decisions/" + memo.id
revision = hidden("version", memo.version)
content = (f'<p><a href="/decisions">Decision workspace</a></p><h1>{text(memo.question)}</h1>'
f'<p>Revision {memo.version} · {text(memo.binding.target.label)}</p>'
f'<section class="card"><h2>Context and recommendation</h2><p class="brief">{text(memo.brief)}</p></section>'
'<p>Human acceptance is not available for this draft. Publishing invites discussion; it does not record acceptance.</p>')
'<p>Publishing invites discussion; it does not record acceptance.</p>')
if not outcome or not outcome['review'] or not outcome['review'].get('path'):
content += '<p>Human acceptance is not available for this draft.</p>'
if outcome and outcome["review"]:
review = outcome["review"]
content += '<section class="card"><h2>Human review</h2>'
if review.get("path"):
content += f'<p><a href="{text(review["path"])}">Open protected human review</a></p>'
elif review["stale"]:
content += '<p>This review belongs to an earlier revision and cannot receive a new answer.</p>'
else:
content += '<p>Review handoff: ' + text(review["intake_state"].replace("_", " ")) + '</p>'
for item in outcome["dispositions"]:
answer = item["disposition"]
content += f'<p>{text(answer["actor"]["sub"])}: {text(answer["verb"])}'
if item["submission_state"]:
content += ' · approval entry ' + text(item["submission_state"])
content += '</p>'
content += '</section>'
if previous is not None:
content += f'<details class="card"><summary>Compare with revision {previous.version}</summary>'
for field, label in (("question", "Question"), ("brief", "Context and recommendation")):
@ -153,7 +177,7 @@ class LifecycleWorkspace:
content += '</ul></section>'
content += '<section class="card"><h2>Discussion and history</h2>'
for event in events:
body = {key: value for key, value in event["body"].items() if key != "policy_observation_id"}
body = {key: value for key, value in event["body"].items() if key not in {"policy_observation_id", "envelope"}}
content += (f'<article class="record"><h3>{text(event["kind"])} · revision {event["version"]}</h3>'
f'<p>{text(event["actor"]["sub"])} ({text(event["actor"]["kind"])}) · {text(event["at"])}</p>')
if event["kind"] == "source":