Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a10780-6d41-7952-9718-dfbc1f41236f
133 lines
7.6 KiB
Python
133 lines
7.6 KiB
Python
"""Durable revision-to-intake handoff; never an approval state machine."""
|
|
from dataclasses import replace
|
|
import json
|
|
import uuid
|
|
|
|
from .intake import KIND, MEMO_KEYS, SubmitterGrant, validate_submission
|
|
from .memo import Principal, BindingLevel, StepKind
|
|
from .review import UI_RELEASE
|
|
from .records import dumps, memo_from
|
|
from .store import Conflict, EvidenceUnavailable
|
|
|
|
|
|
def supports_review(memo):
|
|
"""Structural compatibility only; publication and authorization remain required."""
|
|
return (memo.locale == 'en' and memo.binding_level is BindingLevel.ORGANIZATIONAL
|
|
and memo.step_kind is StepKind.APPROVE and memo.ui_release == UI_RELEASE)
|
|
|
|
|
|
def handoff(repository, tenant, memo_id, version):
|
|
if type(version) is not int or version < 1:
|
|
raise ValueError('positive revision required')
|
|
with repository.store._connection() as db:
|
|
repository._record(db, tenant, memo_id)
|
|
repository.store._memo(db, memo_id, version)
|
|
row = db.execute("SELECT id,body FROM lifecycle_events WHERE tenant=? AND memo_id=? "
|
|
"AND version=? AND kind='review_requested'", (tenant, memo_id, version)).fetchone()
|
|
if row is None:
|
|
raise EvidenceUnavailable('review request unavailable')
|
|
body = json.loads(row['body'])
|
|
return {'event_id': row['id'], **{k: body[k] for k in ('review_memo_id', 'envelope')}}
|
|
|
|
|
|
def review_request(db, review_id):
|
|
row = db.execute("SELECT * FROM lifecycle_events WHERE kind='review_requested' "
|
|
"AND json_extract(body,'$.review_memo_id')=?", (review_id,)).fetchone()
|
|
return row
|
|
|
|
|
|
def require_current(db, review_id):
|
|
row = review_request(db, review_id)
|
|
if row is not None:
|
|
current = db.execute('SELECT MAX(version) FROM memos WHERE id=?', (row['memo_id'],)).fetchone()[0]
|
|
if current != row['version']:
|
|
raise Conflict('review source revision changed')
|
|
return row
|
|
|
|
|
|
def check_intake(db, envelope, submitter):
|
|
row = require_current(db, envelope['memo']['id'])
|
|
if row is not None:
|
|
body = json.loads(row['body'])
|
|
if row['tenant'] != 'tenant:platform' or body['submitter'] != submitter or body['envelope'] != envelope:
|
|
raise Conflict('review handoff does not match the retained request')
|
|
|
|
|
|
def prepare(repository, context, memo_id, version, options, *, key, guard):
|
|
required = {'submitter', 'memo_class', 'review_group', 'approval_intent'}
|
|
if not isinstance(options, dict) or set(options) != required:
|
|
raise ValueError('exact review routing required')
|
|
if context.tenant != 'tenant:platform' or type(version) is not int or version < 1:
|
|
raise ValueError('platform revision required for intake')
|
|
if not isinstance(options['submitter'], str) or not options['submitter'].strip() or len(options['submitter']) > 200:
|
|
raise ValueError('invalid submitter')
|
|
scope, digest = repository._request(context, 'request_review', key,
|
|
{'memo_id': memo_id, 'version': version, 'options': options})
|
|
with repository.store._transaction() as db:
|
|
policy_id = guard(db)
|
|
repository._record(db, context.tenant, memo_id)
|
|
replay = repository._replay(db, scope, digest)
|
|
if replay:
|
|
body = json.loads(db.execute('SELECT body FROM lifecycle_events WHERE id=?', (replay,)).fetchone()[0])
|
|
return {'event_id': replay, **{k: body[k] for k in ('review_memo_id', 'envelope')}}
|
|
memo = repository.store._memo(db, memo_id)
|
|
if memo.version != version:
|
|
raise Conflict('review requires the current revision')
|
|
if not supports_review(memo):
|
|
raise ValueError('unsupported human review profile')
|
|
if repository._policy_context(db, context.tenant, memo_id, version)['published_audience'] is None:
|
|
raise Conflict('publish this revision before requesting review')
|
|
if db.execute("SELECT 1 FROM lifecycle_events WHERE memo_id=? AND version=? AND kind='review_requested'",
|
|
(memo_id, version)).fetchone():
|
|
raise Conflict('this revision already has a review request')
|
|
review_id = 'review-' + str(uuid.uuid4())
|
|
draft = replace(memo, id=review_id, version=1,
|
|
binding=replace(memo.binding, principal=Principal('pending-human-session', 'person', 'To be assigned')))
|
|
projection = {k: v for k, v in json.loads(dumps(draft)).items() if k in MEMO_KEYS}
|
|
# Never silently discard content the narrower intake projection cannot carry.
|
|
if dumps(memo_from(projection)) != dumps(draft):
|
|
raise ValueError('memo requires a broader intake projection')
|
|
packet = []
|
|
for item in draft.packet:
|
|
document = db.execute('SELECT media_type,content FROM documents WHERE digest=?', (item.hash,)).fetchone()
|
|
if document is None:
|
|
raise EvidenceUnavailable('packet unavailable')
|
|
packet.append({'item_id': item.item_id, 'media_type': document['media_type'],
|
|
'content': bytes(document['content']).decode('utf-8')})
|
|
envelope = {'kind': KIND, 'version': 1, 'idempotency_key': review_id,
|
|
'memo_class': options['memo_class'], 'review_group': options['review_group'],
|
|
'approval_intent': options['approval_intent'], 'memo': projection, 'packet': packet}
|
|
# Shape validation only. Actual submitter admission is always rechecked by intake.
|
|
validate_submission(envelope, submitter_id=options['submitter'], grant=SubmitterGrant(
|
|
options['submitter'], frozenset({options['memo_class']}), frozenset({options['review_group']})))
|
|
if len(dumps(envelope).encode()) > 256 * 1024:
|
|
raise ValueError('review envelope too large')
|
|
body = {'review_memo_id': review_id, 'submitter': options['submitter'], 'envelope': envelope}
|
|
event_id = repository._append(db, scope, digest, memo, 'review_requested', body, policy_id)
|
|
return {'event_id': event_id, 'review_memo_id': review_id, 'envelope': envelope}
|
|
|
|
|
|
def outcomes(repository, tenant, memo_id, version):
|
|
if type(version) is not int or version < 1:
|
|
raise ValueError('positive revision required')
|
|
with repository.store._connection() as db:
|
|
repository._record(db, tenant, memo_id)
|
|
repository.store._memo(db, memo_id, version)
|
|
row = db.execute("SELECT body FROM lifecycle_events WHERE tenant=? AND memo_id=? AND version=? "
|
|
"AND kind='review_requested'", (tenant, memo_id, version)).fetchone()
|
|
if row is None:
|
|
return {'review': None, 'dispositions': []}
|
|
body = json.loads(row['body'])
|
|
review_id = body['review_memo_id']
|
|
ledger = db.execute("SELECT state FROM intake_ledger WHERE memo_id=?", (review_id,)).fetchone()
|
|
result = {'review': {'memo_id': review_id, 'intake_state': ledger['state'] if ledger else 'not_submitted'},
|
|
'dispositions': []}
|
|
result['review']['stale'] = repository.store._memo(db, memo_id).version != version
|
|
if ledger and ledger['state'] == 'ready' and not result['review']['stale']:
|
|
result['review']['path'] = '/review?memo_id=' + review_id
|
|
for item in db.execute('SELECT d.body,s.state,s.approved_at FROM dispositions d '
|
|
'JOIN presentations p ON p.id=d.presentation_id '
|
|
'LEFT JOIN submissions s ON s.disposition_id=d.id WHERE p.memo_id=? ORDER BY d.rowid', (review_id,)):
|
|
result['dispositions'].append({'disposition': json.loads(item['body']),
|
|
'submission_state': item['state'], 'approved_at': item['approved_at']})
|
|
return result
|