vignette/apps/api/app/services/multimodal_alliance_store.py
Yun Chan 16e791e044 G0~G8 성과·동맹 측정 OS 작업 일괄 고정
8월 7일까지 워킹트리에만 남아 있던 미커밋 작업을 커밋한다. 여러 사본
폴더(worktree·clone)에 흩어져 있던 중간 스냅샷을 정리하기 전에 원본을
git 이력으로 고정하는 것이 목적이다.

- contracts/routes/services: measurement, outcome_trajectory, rupture_repair,
  deliberate_practice, calibration_transfer, supervision_research,
  multimodal_alliance, continuous_improvement 계열 신규 모듈과 테스트
- infra/db/init: 07~16 마이그레이션(측정 기반~calibration transfer 실행)
- apps/web: 세션 리뷰 카드·관리 화면·E2E 스펙 추가
- docs/ops: G0~G8 라이브 통합·배포·롤백 증거 문서와 evidence JSON/PNG
- scripts: smoke·ledger·릴리스 에이전트·NAS 프리뷰 운영 스크립트

engine.public 로그 .bak과 apps/web/test-results 산출물은 커밋에서 제외했다.
2026-08-08 01:30:53 +09:00

1269 lines
45 KiB
Python

"""Postgres persistence for the G7 consent-bound multimodal ledger."""
from __future__ import annotations
import hashlib
import json
from collections.abc import Mapping, Sequence
from datetime import UTC, datetime, timedelta
from typing import Any
from uuid import NAMESPACE_URL, UUID, uuid4, uuid5
import asyncpg
from .. import db
from ..config import settings
from ..contracts.multimodal_alliance import (
AlignedVoiceTimeline,
FusionCalibration,
ModalityAxisMeasurement,
)
from ..deps import Principal, Role
from .multimodal_alliance import build_calibrated_axis_read_model
class MultimodalAllianceError(RuntimeError):
pass
class MultimodalAllianceNotFoundError(MultimodalAllianceError):
pass
class MultimodalAllianceConflictError(MultimodalAllianceError):
pass
class MultimodalAllianceStateError(MultimodalAllianceError):
pass
class MultimodalConsentRequiredError(MultimodalAllianceStateError):
pass
class MultimodalConsentWithdrawnError(MultimodalAllianceStateError):
pass
def _value(row: Any, key: str, default: Any = None) -> Any:
if row is None:
return default
if isinstance(row, Mapping):
return row.get(key, default)
try:
return row[key]
except (KeyError, TypeError):
return getattr(row, key, default)
def _canonical_hash(payload: object) -> str:
encoded = json.dumps(
payload,
ensure_ascii=False,
sort_keys=True,
separators=(",", ":"),
default=str,
).encode("utf-8")
return hashlib.sha256(encoded).hexdigest()
async def _visible_session(conn: asyncpg.Connection, session_id: UUID) -> Any:
row = await conn.fetchrow(
"SELECT id, learner_id FROM app.sessions WHERE id = $1",
session_id,
)
if row is None:
raise MultimodalAllianceNotFoundError("session not found or not visible")
return row
async def _latest_consent(conn: asyncpg.Connection, session_id: UUID) -> Any:
return await conn.fetchrow(
"""
SELECT consent_snapshot_id, learner_id, sequence_no, consent_status,
retain_audio, retain_derived_features, transcript_retained,
retention_days, policy_version, reason_code, created_at
FROM app.multimodal_consent_snapshot
WHERE session_id = $1
ORDER BY sequence_no DESC
LIMIT 1
""",
session_id,
)
async def _lock_consent_processing(
conn: asyncpg.Connection, session_id: UUID
) -> None:
"""Serialize consent mutation with all consent-bound persistence.
The database triggers still enforce the latest snapshot at INSERT time.
This shared transaction lock additionally gives concurrent withdrawal and
evaluator persistence one unambiguous order, so a writer that starts after
withdrawal commits cannot persist derived voice data from a stale grant.
"""
await conn.execute(
"SELECT pg_advisory_xact_lock(hashtextextended($1::text, 0))",
f"multimodal-consent:{session_id}",
)
async def _existing_request(
conn: asyncpg.Connection,
*,
submission_id: UUID,
request_kind: str,
content_hash: str,
session_id: UUID,
learner_id: UUID,
) -> UUID | None:
row = await conn.fetchrow(
"""
SELECT request_kind, content_hash, session_id, learner_id, result_id
FROM app.multimodal_ingestion_request
WHERE submission_id = $1
""",
submission_id,
)
if row is None:
return None
if (
_value(row, "request_kind") != request_kind
or _value(row, "content_hash") != content_hash
or UUID(str(_value(row, "session_id"))) != session_id
or UUID(str(_value(row, "learner_id"))) != learner_id
):
raise MultimodalAllianceConflictError(
"submission_id was already used with different multimodal content"
)
return UUID(str(_value(row, "result_id")))
async def _insert_request(
conn: asyncpg.Connection,
*,
submission_id: UUID,
content_hash: str,
request_kind: str,
session_id: UUID,
learner_id: UUID,
result_id: UUID,
created_by_role: str,
) -> None:
await conn.execute(
"""
INSERT INTO app.multimodal_ingestion_request (
submission_id, content_hash, request_kind, session_id, learner_id,
result_id, created_by_role
) VALUES ($1,$2,$3,$4,$5,$6,$7)
""",
submission_id,
content_hash,
request_kind,
session_id,
learner_id,
result_id,
created_by_role,
)
def _withdrawal_ids(submission_id: UUID) -> tuple[UUID, UUID]:
deletion_submission_id = uuid5(
NAMESPACE_URL, f"vignette:g7:withdrawal-submission:{submission_id}"
)
deletion_request_id = uuid5(
NAMESPACE_URL, f"vignette:g7:withdrawal-request:{submission_id}"
)
return deletion_submission_id, deletion_request_id
async def _insert_deletion_request(
conn: asyncpg.Connection,
*,
submission_id: UUID,
deletion_request_id: UUID,
session_id: UUID,
learner_id: UUID,
scopes: Sequence[str],
request_reason: str,
requested_by_uid: UUID,
created_by_role: str,
) -> tuple[UUID, bool]:
unique_scopes = tuple(sorted(set(scopes)))
if not unique_scopes or not set(unique_scopes) <= {
"audio",
"derived_features",
"transcript",
}:
raise MultimodalAllianceStateError("unsupported deletion scope")
payload = {
"session_id": str(session_id),
"learner_id": str(learner_id),
"scopes": unique_scopes,
"request_reason": request_reason,
"requested_by_uid": str(requested_by_uid),
}
content_hash = _canonical_hash(payload)
existing = await _existing_request(
conn,
submission_id=submission_id,
request_kind="deletion_request",
content_hash=content_hash,
session_id=session_id,
learner_id=learner_id,
)
if existing is not None:
return existing, True
await _insert_request(
conn,
submission_id=submission_id,
content_hash=content_hash,
request_kind="deletion_request",
session_id=session_id,
learner_id=learner_id,
result_id=deletion_request_id,
created_by_role=created_by_role,
)
await conn.execute(
"""
INSERT INTO app.multimodal_deletion_request (
deletion_request_id, submission_id, session_id, learner_id,
scopes, request_reason, requested_by_uid
) VALUES ($1,$2,$3,$4,$5::text[],$6,$7)
""",
deletion_request_id,
submission_id,
session_id,
learner_id,
list(unique_scopes),
request_reason,
requested_by_uid,
)
return deletion_request_id, False
async def append_consent_snapshot(
*,
principal: Principal,
session_id: UUID,
submission_id: UUID,
consent_status: str,
retain_audio: bool,
retain_derived_features: bool,
transcript_retained: bool,
retention_days: int | None,
policy_version: str,
reason_code: str | None,
) -> dict[str, Any]:
if principal.role != Role.LEARNER:
raise MultimodalAllianceStateError(
"only the learner may change multimodal consent"
)
if consent_status not in {"granted", "withdrawn", "not_granted"}:
raise MultimodalAllianceStateError("unsupported consent status")
if not transcript_retained:
raise MultimodalAllianceStateError(
"G7 deletes audio and derived features only; masked transcript remains retained"
)
if consent_status == "granted":
if not retain_derived_features or retention_days is None:
raise MultimodalAllianceStateError(
"granted multimodal consent requires derived retention and expiry"
)
elif retain_audio or retain_derived_features or retention_days is not None:
raise MultimodalAllianceStateError(
"ungranted consent must not retain audio or derived features"
)
learner_id = UUID(principal.user_id)
payload = {
"session_id": str(session_id),
"consent_status": consent_status,
"retain_audio": retain_audio,
"retain_derived_features": retain_derived_features,
"transcript_retained": transcript_retained,
"retention_days": retention_days,
"policy_version": policy_version.strip(),
"reason_code": reason_code.strip() if reason_code else None,
}
content_hash = _canonical_hash(payload)
consent_snapshot_id = uuid4()
deletion_request_id: UUID | None = None
async with db.acquire(
role=principal.role.value,
user_id=principal.user_id,
cohort_ids=principal.cohort_ids,
) as conn:
await _lock_consent_processing(conn, session_id)
session = await _visible_session(conn, session_id)
if UUID(str(_value(session, "learner_id"))) != learner_id:
raise MultimodalAllianceNotFoundError("session not found or not visible")
existing = await _existing_request(
conn,
submission_id=submission_id,
request_kind="consent",
content_hash=content_hash,
session_id=session_id,
learner_id=learner_id,
)
if existing is not None:
deletion = await conn.fetchrow(
"""
SELECT deletion_request_id FROM app.multimodal_deletion_request
WHERE session_id = $1 AND request_reason = 'consent_withdrawal'
ORDER BY requested_at DESC LIMIT 1
""",
session_id,
)
return {
"submission_id": submission_id,
"consent_snapshot_id": existing,
"consent_status": consent_status,
"deletion_request_id": (
UUID(str(_value(deletion, "deletion_request_id")))
if deletion is not None
else None
),
"idempotent_replay": True,
}
latest = await _latest_consent(conn, session_id)
if latest is not None and _value(latest, "consent_status") == "withdrawn":
raise MultimodalAllianceConflictError(
"withdrawn multimodal consent is terminal for this session"
)
sequence_no = int(_value(latest, "sequence_no", 0)) + 1
await _insert_request(
conn,
submission_id=submission_id,
content_hash=content_hash,
request_kind="consent",
session_id=session_id,
learner_id=learner_id,
result_id=consent_snapshot_id,
created_by_role="learner",
)
try:
await conn.execute(
"""
INSERT INTO app.multimodal_consent_snapshot (
consent_snapshot_id, submission_id, session_id, learner_id,
sequence_no, consent_status, retain_audio,
retain_derived_features, transcript_retained, retention_days,
policy_version, reason_code, created_by_uid
) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13)
""",
consent_snapshot_id,
submission_id,
session_id,
learner_id,
sequence_no,
consent_status,
retain_audio,
retain_derived_features,
transcript_retained,
retention_days,
payload["policy_version"],
payload["reason_code"],
learner_id,
)
if consent_status == "withdrawn":
deletion_submission_id, deletion_request_id = _withdrawal_ids(
submission_id
)
await _insert_deletion_request(
conn,
submission_id=deletion_submission_id,
deletion_request_id=deletion_request_id,
session_id=session_id,
learner_id=learner_id,
scopes=("audio", "derived_features"),
request_reason="consent_withdrawal",
requested_by_uid=learner_id,
created_by_role="learner",
)
except (asyncpg.CheckViolationError, asyncpg.ForeignKeyViolationError) as exc:
raise MultimodalAllianceStateError(
"multimodal consent violated ownership or retention invariants"
) from exc
except asyncpg.UniqueViolationError as exc:
raise MultimodalAllianceConflictError(
"multimodal consent submission already exists"
) from exc
return {
"submission_id": submission_id,
"consent_snapshot_id": consent_snapshot_id,
"consent_status": consent_status,
"deletion_request_id": deletion_request_id,
"idempotent_replay": False,
}
async def assert_voice_processing_allowed(
*, principal: Principal, session_id: str
) -> dict[str, Any] | None:
"""Fail closed for enrolled sessions after refusal/withdrawal.
A database without the G7 migration is treated as a legacy, non-enrolled
deployment so the pre-existing voice cascade remains available. Any other
database error propagates before STT is invoked.
"""
try:
session_uuid = UUID(session_id)
except ValueError:
return None
try:
async with db.acquire(
role=principal.role.value,
user_id=principal.user_id,
cohort_ids=principal.cohort_ids,
) as conn:
await _visible_session(conn, session_uuid)
consent = await _latest_consent(conn, session_uuid)
except (
RuntimeError,
asyncpg.UndefinedTableError,
MultimodalAllianceNotFoundError,
) as exc:
if settings.environment == "dev":
return None
raise MultimodalAllianceStateError(
"multimodal consent state is unavailable; voice processing is blocked"
) from exc
if consent is None:
return None
status = str(_value(consent, "consent_status"))
if status == "withdrawn":
raise MultimodalConsentWithdrawnError(
"multimodal consent was withdrawn; new voice processing is blocked"
)
if status != "granted":
raise MultimodalConsentRequiredError(
"multimodal voice processing consent was not granted"
)
return {
"consent_snapshot_id": UUID(str(_value(consent, "consent_snapshot_id"))),
"retain_audio": bool(_value(consent, "retain_audio")),
"retain_derived_features": bool(_value(consent, "retain_derived_features")),
"retention_days": int(_value(consent, "retention_days")),
}
async def append_runtime_timeline(
*,
session_id: str,
submission_id: UUID,
timeline: AlignedVoiceTimeline,
audio_asset: Mapping[str, Any] | None,
) -> dict[str, Any] | None:
"""Append a consent-bound runtime STT timeline using evaluator RLS.
Legacy development sessions may run without the G7 migration. Staging and
production never reinterpret a missing schema/session as permission.
"""
try:
session_uuid = UUID(session_id)
except ValueError as exc:
if settings.environment == "dev":
return None
raise MultimodalAllianceStateError(
"multimodal timeline session identity is invalid"
) from exc
try:
async with db.acquire(ai_view="evaluator", ai_context=True) as conn:
await _visible_session(conn, session_uuid)
consent = await _latest_consent(conn, session_uuid)
if consent is None:
return None
consent_status = str(_value(consent, "consent_status"))
if consent_status == "withdrawn":
raise MultimodalConsentWithdrawnError(
"multimodal consent was withdrawn; timeline persistence is blocked"
)
if consent_status != "granted":
raise MultimodalConsentRequiredError(
"multimodal voice processing consent was not granted"
)
if not bool(_value(consent, "retain_derived_features")):
return {
"submission_id": submission_id,
"retention": "derived_features_disabled",
}
retained_audio_asset = (
audio_asset if bool(_value(consent, "retain_audio")) else None
)
if retained_audio_asset is None and bool(_value(consent, "retain_audio")):
raise MultimodalAllianceStateError(
"consented audio retention requires runtime audio metadata"
)
return await append_timeline(
conn=conn,
session_id=session_uuid,
submission_id=submission_id,
timeline=timeline,
audio_asset=retained_audio_asset,
)
except MultimodalAllianceNotFoundError as exc:
if settings.environment == "dev":
return None
raise MultimodalAllianceStateError(
"multimodal timeline session is unavailable"
) from exc
except MultimodalAllianceError:
raise
except (RuntimeError, asyncpg.UndefinedTableError) as exc:
if settings.environment == "dev":
return None
raise MultimodalAllianceStateError(
"multimodal timeline persistence is unavailable"
) from exc
async def append_timeline(
*,
conn: asyncpg.Connection,
session_id: UUID,
submission_id: UUID,
timeline: AlignedVoiceTimeline,
audio_asset: Mapping[str, Any] | None,
) -> dict[str, Any]:
await _lock_consent_processing(conn, session_id)
await conn.execute(
"SELECT pg_advisory_xact_lock(hashtextextended($1::text, 0))",
f"multimodal-timeline:{session_id}",
)
session = await _visible_session(conn, session_id)
learner_id = UUID(str(_value(session, "learner_id")))
consent = await _latest_consent(conn, session_id)
if consent is None or _value(consent, "consent_status") != "granted":
raise MultimodalConsentRequiredError(
"latest granted consent is required before voice processing"
)
if not bool(_value(consent, "retain_derived_features")):
raise MultimodalConsentRequiredError(
"derived feature processing was not granted"
)
retain_audio = bool(_value(consent, "retain_audio"))
if retain_audio != (audio_asset is not None):
raise MultimodalAllianceStateError(
"audio metadata must exactly follow the consent retention snapshot"
)
payload = {
"session_id": str(session_id),
"timeline": timeline.model_dump(mode="json"),
"audio_asset": dict(audio_asset) if audio_asset is not None else None,
"consent_snapshot_id": str(_value(consent, "consent_snapshot_id")),
}
content_hash = _canonical_hash(payload)
existing = await _existing_request(
conn,
submission_id=submission_id,
request_kind="timeline",
content_hash=content_hash,
session_id=session_id,
learner_id=learner_id,
)
if existing is not None:
return {
"submission_id": submission_id,
"timeline_id": existing,
"idempotent_replay": True,
}
timeline_id = uuid4()
audio_asset_id = uuid4() if audio_asset is not None else None
await _insert_request(
conn,
submission_id=submission_id,
content_hash=content_hash,
request_kind="timeline",
session_id=session_id,
learner_id=learner_id,
result_id=timeline_id,
created_by_role="evaluator",
)
try:
if audio_asset is not None:
retention_days = int(_value(consent, "retention_days"))
retained_until = datetime.now(UTC) + timedelta(days=retention_days)
await conn.execute(
"""
INSERT INTO app.multimodal_audio_asset (
audio_asset_id, timeline_submission_id, session_id, learner_id,
consent_snapshot_id, audio_ref, audio_sha256, media_type,
byte_size, duration_ms, retained_until
) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11)
""",
audio_asset_id,
submission_id,
session_id,
learner_id,
_value(consent, "consent_snapshot_id"),
str(audio_asset["audio_ref"]),
str(audio_asset["audio_sha256"]),
str(audio_asset["media_type"]),
int(audio_asset["byte_size"]),
timeline.audio_duration_ms,
retained_until,
)
await conn.execute(
"""
INSERT INTO app.multimodal_audio_timeline (
timeline_id, submission_id, session_id, learner_id,
consent_snapshot_id, audio_asset_id, audio_duration_ms,
word_count, event_count
) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9)
""",
timeline_id,
submission_id,
session_id,
learner_id,
_value(consent, "consent_snapshot_id"),
audio_asset_id,
timeline.audio_duration_ms,
len(timeline.words),
len(timeline.events),
)
for word in timeline.words:
await conn.execute(
"""
INSERT INTO app.multimodal_word_timestamp (
word_timestamp_id, timeline_id, session_id, learner_id,
word_index, start_ms, end_ms, speaker, token_hash
) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9)
""",
uuid4(),
timeline_id,
session_id,
learner_id,
word.word_index,
word.start_ms,
word.end_ms,
word.speaker,
word.token_hash,
)
for event in timeline.events:
await conn.execute(
"""
INSERT INTO app.multimodal_voice_event (
voice_event_record_id, timeline_id, session_id, learner_id,
event_id, event_type, start_ms, end_ms, actor,
observed_feature, uncertainty, source
) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12)
""",
uuid4(),
timeline_id,
session_id,
learner_id,
event.event_id,
event.event_type,
event.start_ms,
event.end_ms,
event.actor,
event.observed_feature,
event.uncertainty,
event.source,
)
await conn.execute(
"SET CONSTRAINTS app.trg_multimodal_timeline_counts, "
"app.trg_multimodal_word_counts, app.trg_multimodal_event_counts "
"IMMEDIATE"
)
except (asyncpg.CheckViolationError, asyncpg.ForeignKeyViolationError) as exc:
raise MultimodalAllianceStateError(
"multimodal timeline violated consent, clock, or observation invariants"
) from exc
except asyncpg.UniqueViolationError as exc:
raise MultimodalAllianceConflictError(
"multimodal timeline submission already exists"
) from exc
return {
"submission_id": submission_id,
"timeline_id": timeline_id,
"audio_asset_id": audio_asset_id,
"idempotent_replay": False,
}
def _measurement_values(
measurement: ModalityAxisMeasurement,
provenance: Mapping[str, str],
) -> tuple[Any, ...]:
return (
measurement.measurement_id,
measurement.axis,
measurement.modality,
measurement.status,
measurement.value,
measurement.confidence,
measurement.uncertainty,
list(measurement.evidence_refs),
measurement.model_run_id,
provenance["instrument_id"],
provenance["instrument_version"],
provenance["model_name"],
provenance["prompt_version"],
f"model_inferred_{measurement.modality}",
measurement.error_code,
)
async def append_measurement_fusion(
*,
conn: asyncpg.Connection,
session_id: UUID,
submission_id: UUID,
text: ModalityAxisMeasurement,
voice: ModalityAxisMeasurement,
calibration: FusionCalibration,
text_provenance: Mapping[str, str],
voice_provenance: Mapping[str, str],
) -> dict[str, Any]:
if text.modality != "text" or voice.modality != "voice":
raise MultimodalAllianceStateError(
"text and voice measurements must keep independent modality provenance"
)
if text.status != "ready":
raise MultimodalAllianceStateError(
"a ready text measurement is required as the fail-safe baseline"
)
await _lock_consent_processing(conn, session_id)
session = await _visible_session(conn, session_id)
learner_id = UUID(str(_value(session, "learner_id")))
consent = await _latest_consent(conn, session_id)
consent_status = _value(consent, "consent_status") if consent is not None else None
if voice.status == "ready" and consent_status != "granted":
raise MultimodalConsentRequiredError(
"ready voice measurement requires current granted consent"
)
if consent_status == "withdrawn" and voice.status != "missing":
raise MultimodalConsentWithdrawnError(
"withdrawal permits only an explicit missing voice measurement"
)
result = build_calibrated_axis_read_model(
text=text,
voice=voice,
calibration=calibration,
)
payload = {
"session_id": str(session_id),
"text": text.model_dump(mode="json"),
"voice": voice.model_dump(mode="json"),
"calibration": calibration.model_dump(mode="json"),
"text_provenance": dict(text_provenance),
"voice_provenance": dict(voice_provenance),
}
content_hash = _canonical_hash(payload)
await conn.execute(
"SELECT pg_advisory_xact_lock(hashtextextended($1::text, 0))",
f"multimodal-measurement:{submission_id}",
)
existing = await _existing_request(
conn,
submission_id=submission_id,
request_kind="measurement_fusion",
content_hash=content_hash,
session_id=session_id,
learner_id=learner_id,
)
if existing is not None:
return {
"submission_id": submission_id,
"fusion_record_id": existing,
"result": result.model_dump(mode="json"),
"idempotent_replay": True,
}
fusion_record_id = uuid4()
await _insert_request(
conn,
submission_id=submission_id,
content_hash=content_hash,
request_kind="measurement_fusion",
session_id=session_id,
learner_id=learner_id,
result_id=fusion_record_id,
created_by_role="evaluator",
)
try:
for measurement, provenance in (
(text, text_provenance),
(voice, voice_provenance),
):
await conn.execute(
"""
INSERT INTO app.multimodal_axis_measurement (
measurement_record_id, submission_id, session_id, learner_id,
measurement_id, axis, modality, status, value, confidence,
uncertainty, evidence_refs, model_run_id, instrument_id,
instrument_version, model_name, prompt_version, source_kind,
error_code
) VALUES (
$1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12::text[],$13,$14,$15,
$16,$17,$18,$19
)
""",
uuid4(),
submission_id,
session_id,
learner_id,
*_measurement_values(measurement, provenance),
)
await conn.execute(
"""
INSERT INTO app.multimodal_fusion_decision (
fusion_record_id, submission_id, session_id, learner_id, axis,
status, value, uncertainty, modalities_used, measurement_ids,
fusion_applied, calibration_id, benchmark_version, text_weight,
voice_weight, text_only_accuracy, fused_accuracy,
minimum_incremental_gain, incremental_gain, counterevidence
) VALUES (
$1,$2,$3,$4,$5,$6,$7,$8,$9::text[],$10::text[],$11,$12,$13,$14,
$15,$16,$17,$18,$19,$20::text[]
)
""",
fusion_record_id,
submission_id,
session_id,
learner_id,
result.axis,
result.status,
result.value,
result.uncertainty,
list(result.modalities_used),
list(result.measurement_ids),
result.fusion_applied,
result.fusion_calibration_id,
calibration.benchmark_version,
calibration.text_weight,
calibration.voice_weight,
calibration.text_only_accuracy,
calibration.fused_accuracy,
calibration.minimum_incremental_gain,
calibration.incremental_gain,
list(result.counterevidence),
)
except (asyncpg.CheckViolationError, asyncpg.ForeignKeyViolationError) as exc:
raise MultimodalAllianceStateError(
"multimodal measurement violated provenance or fusion invariants"
) from exc
except asyncpg.UniqueViolationError as exc:
raise MultimodalAllianceConflictError(
"multimodal measurement submission already exists"
) from exc
return {
"submission_id": submission_id,
"fusion_record_id": fusion_record_id,
"result": result.model_dump(mode="json"),
"idempotent_replay": False,
}
async def append_deletion_request(
*,
principal: Principal,
session_id: UUID,
submission_id: UUID,
scopes: Sequence[str],
request_reason: str = "learner_request",
) -> dict[str, Any]:
if principal.role not in {Role.LEARNER, Role.ADMIN}:
raise MultimodalAllianceStateError(
"multimodal deletion request requires learner or admin role"
)
learner_actor = UUID(principal.user_id)
deletion_request_id = uuid4()
async with db.acquire(
role=principal.role.value,
user_id=principal.user_id,
cohort_ids=principal.cohort_ids,
) as conn:
session = await _visible_session(conn, session_id)
learner_id = UUID(str(_value(session, "learner_id")))
if principal.role == Role.LEARNER and learner_id != learner_actor:
raise MultimodalAllianceNotFoundError("session not found or not visible")
await conn.execute(
"SELECT pg_advisory_xact_lock(hashtextextended($1::text, 0))",
f"multimodal-deletion:{session_id}",
)
deletion_request_id, replay = await _insert_deletion_request(
conn,
submission_id=submission_id,
deletion_request_id=deletion_request_id,
session_id=session_id,
learner_id=learner_id,
scopes=scopes,
request_reason=request_reason,
requested_by_uid=learner_actor,
created_by_role=principal.role.value,
)
return {
"submission_id": submission_id,
"deletion_request_id": deletion_request_id,
"idempotent_replay": replay,
}
async def request_expired_retention_deletions(
*,
conn: asyncpg.Connection,
as_of: datetime,
limit: int = 100,
) -> list[dict[str, Any]]:
"""Materialize deterministic deletion work for expired raw audio assets."""
rows = await conn.fetch(
"""
SELECT a.audio_asset_id, a.session_id, a.learner_id
FROM app.multimodal_audio_asset a
WHERE a.retained_until <= $1
AND NOT EXISTS (
SELECT 1 FROM audit.multimodal_deletion_tombstone t
WHERE t.session_id = a.session_id AND t.scope = 'audio'
)
ORDER BY a.retained_until, a.audio_asset_id
LIMIT $2
""",
as_of,
max(1, min(int(limit), 100)),
)
results: list[dict[str, Any]] = []
for row in rows:
audio_asset_id = UUID(str(_value(row, "audio_asset_id")))
session_id = UUID(str(_value(row, "session_id")))
learner_id = UUID(str(_value(row, "learner_id")))
submission_id = uuid5(
NAMESPACE_URL,
f"vignette:g7:retention-submission:{audio_asset_id}",
)
deletion_request_id = uuid5(
NAMESPACE_URL,
f"vignette:g7:retention-request:{audio_asset_id}",
)
request_id, replay = await _insert_deletion_request(
conn,
submission_id=submission_id,
deletion_request_id=deletion_request_id,
session_id=session_id,
learner_id=learner_id,
scopes=("audio",),
request_reason="retention_expired",
requested_by_uid=learner_id,
created_by_role="evaluator",
)
results.append(
{
"audio_asset_id": audio_asset_id,
"submission_id": submission_id,
"deletion_request_id": request_id,
"idempotent_replay": replay,
}
)
return results
async def complete_deletion(
*,
conn: asyncpg.Connection,
deletion_request_id: UUID,
submission_id: UUID,
tombstones: Sequence[Mapping[str, Any]],
actor_uid: UUID | None,
actor_kind: str,
) -> dict[str, Any]:
request = await conn.fetchrow(
"""
SELECT deletion_request_id, session_id, learner_id, scopes
FROM app.multimodal_deletion_request
WHERE deletion_request_id = $1
""",
deletion_request_id,
)
if request is None:
raise MultimodalAllianceNotFoundError(
"multimodal deletion request not found or not visible"
)
requested_scopes = set(_value(request, "scopes", ()))
ordered_tombstones = tuple(sorted(tombstones, key=lambda item: str(item["scope"])))
supplied_scopes = {str(item["scope"]) for item in ordered_tombstones}
if requested_scopes != supplied_scopes or len(supplied_scopes) != len(
ordered_tombstones
):
raise MultimodalAllianceStateError(
"deletion completion must prove every requested scope exactly once"
)
session_id = UUID(str(_value(request, "session_id")))
learner_id = UUID(str(_value(request, "learner_id")))
payload = {
"deletion_request_id": str(deletion_request_id),
"tombstones": [dict(item) for item in ordered_tombstones],
"actor_uid": str(actor_uid) if actor_uid else None,
"actor_kind": actor_kind,
}
content_hash = _canonical_hash(payload)
await conn.execute(
"SELECT pg_advisory_xact_lock(hashtextextended($1::text, 0))",
f"multimodal-deletion-complete:{deletion_request_id}",
)
existing = await _existing_request(
conn,
submission_id=submission_id,
request_kind="deletion_completion",
content_hash=content_hash,
session_id=session_id,
learner_id=learner_id,
)
if existing is not None:
existing_rows = await conn.fetch(
"""
SELECT tombstone_id
FROM audit.multimodal_deletion_tombstone
WHERE completion_submission_id = $1
ORDER BY scope
""",
submission_id,
)
return {
"submission_id": submission_id,
"deletion_request_id": deletion_request_id,
"tombstone_ids": [
UUID(str(_value(row, "tombstone_id"))) for row in existing_rows
]
or [existing],
"idempotent_replay": True,
}
tombstone_ids = [uuid4() for _ in ordered_tombstones]
await _insert_request(
conn,
submission_id=submission_id,
content_hash=content_hash,
request_kind="deletion_completion",
session_id=session_id,
learner_id=learner_id,
result_id=tombstone_ids[0],
created_by_role="evaluator" if actor_kind == "retention_worker" else "admin",
)
try:
for tombstone_id, item in zip(tombstone_ids, ordered_tombstones, strict=True):
await conn.execute(
"""
INSERT INTO audit.multimodal_deletion_tombstone (
tombstone_id, completion_submission_id, deletion_request_id,
session_id, learner_id, scope, target_ref_hash,
deletion_proof, actor_uid, actor_kind, deleted_at
) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11)
""",
tombstone_id,
submission_id,
deletion_request_id,
session_id,
learner_id,
item["scope"],
item["target_ref_hash"],
item["deletion_proof"],
actor_uid,
actor_kind,
item["deleted_at"],
)
except (asyncpg.CheckViolationError, asyncpg.ForeignKeyViolationError) as exc:
raise MultimodalAllianceStateError(
"multimodal deletion proof violated audit invariants"
) from exc
except asyncpg.UniqueViolationError as exc:
raise MultimodalAllianceConflictError(
"multimodal deletion scope was already completed"
) from exc
return {
"submission_id": submission_id,
"deletion_request_id": deletion_request_id,
"tombstone_ids": tombstone_ids,
"idempotent_replay": False,
}
async def read_session_metadata(
*, principal: Principal, session_id: UUID
) -> dict[str, Any]:
async with db.acquire(
role=principal.role.value,
user_id=principal.user_id,
cohort_ids=principal.cohort_ids,
) as conn:
session = await _visible_session(conn, session_id)
learner_id = UUID(str(_value(session, "learner_id")))
consent_rows = await conn.fetch(
"""
SELECT consent_snapshot_id, sequence_no, consent_status, retain_audio,
retain_derived_features, transcript_retained, retention_days,
policy_version, reason_code, created_at
FROM app.multimodal_consent_snapshot
WHERE session_id = $1 ORDER BY sequence_no
""",
session_id,
)
timeline_rows = await conn.fetch(
"""
SELECT timeline_id, audio_duration_ms, clock_version, word_count,
event_count, created_at, derived_features_available
FROM app.multimodal_session_metadata_v
WHERE session_id = $1 ORDER BY created_at
""",
session_id,
)
derived_available = any(
bool(_value(row, "derived_features_available")) for row in timeline_rows
)
derived_deleted = bool(timeline_rows) and not derived_available
words: Sequence[Any] = ()
events: Sequence[Any] = ()
if derived_available:
words = await conn.fetch(
"""
SELECT timeline_id, word_index, start_ms, end_ms, speaker, token_hash
FROM app.multimodal_word_timestamp
WHERE session_id = $1 ORDER BY timeline_id, word_index
""",
session_id,
)
events = await conn.fetch(
"""
SELECT timeline_id, event_id, event_type, start_ms, end_ms, actor,
observed_feature, uncertainty, source, claim_scope,
clinical_claim_allowed
FROM app.multimodal_voice_event
WHERE session_id = $1 ORDER BY timeline_id, start_ms, event_id
""",
session_id,
)
measurements = await conn.fetch(
"""
SELECT measurement_id, axis, modality, status, value, confidence,
uncertainty, evidence_refs, model_run_id, instrument_id,
instrument_version, model_name, prompt_version, source_kind,
error_code, created_at
FROM app.multimodal_axis_measurement
WHERE session_id = $1
AND ($2::boolean = FALSE OR modality = 'text')
ORDER BY created_at, axis, modality
""",
session_id,
derived_deleted,
)
fusions = await conn.fetch(
"""
SELECT fusion_record_id, axis, status, value, uncertainty,
modalities_used, measurement_ids, fusion_applied,
calibration_id, benchmark_version, incremental_gain,
counterevidence, created_at
FROM app.multimodal_fusion_decision
WHERE session_id = $1
AND ($2::boolean = FALSE OR NOT ('voice' = ANY(modalities_used)))
ORDER BY created_at, axis
""",
session_id,
derived_deleted,
)
deletions = await conn.fetch(
"""
SELECT r.deletion_request_id, r.scopes, r.request_reason, r.requested_at,
COALESCE(array_agg(t.scope ORDER BY t.scope)
FILTER (WHERE t.scope IS NOT NULL), '{}') AS completed_scopes
FROM app.multimodal_deletion_request r
LEFT JOIN audit.multimodal_deletion_tombstone t
ON t.deletion_request_id = r.deletion_request_id
WHERE r.session_id = $1
GROUP BY r.deletion_request_id, r.scopes, r.request_reason, r.requested_at
ORDER BY r.requested_at
""",
session_id,
)
return {
"session_id": session_id,
"learner_id": learner_id,
"clinical_claim_allowed": False,
"consent_snapshots": [dict(row) for row in consent_rows],
"timelines": [dict(row) for row in timeline_rows],
"word_timestamps": [dict(row) for row in words],
"voice_events": [dict(row) for row in events],
"measurements": [dict(row) for row in measurements],
"fusion_decisions": [dict(row) for row in fusions],
"deletion_requests": [dict(row) for row in deletions],
}
async def read_raw_audio_access(
*, principal: Principal, session_id: UUID
) -> list[dict[str, Any]]:
if principal.role not in {Role.LEARNER, Role.ADMIN}:
raise MultimodalAllianceStateError(
"raw audio access is limited to the learner and administrator"
)
async with db.acquire(
role=principal.role.value,
user_id=principal.user_id,
cohort_ids=principal.cohort_ids,
) as conn:
await _visible_session(conn, session_id)
rows = await conn.fetch(
"""
SELECT audio_asset_id, session_id, learner_id, audio_ref, audio_sha256,
media_type, byte_size, duration_ms, retained_until, created_at
FROM app.multimodal_raw_audio_access_v
WHERE session_id = $1 ORDER BY created_at
""",
session_id,
)
return [dict(row) for row in rows]
async def read_raw_audio_asset(
*, principal: Principal, session_id: UUID, audio_asset_id: UUID
) -> dict[str, Any]:
"""Return one consent-valid raw-audio storage handle behind learner/admin RLS."""
if principal.role not in {Role.LEARNER, Role.ADMIN}:
raise MultimodalAllianceStateError(
"raw audio access is limited to the learner and administrator"
)
async with db.acquire(
role=principal.role.value,
user_id=principal.user_id,
cohort_ids=principal.cohort_ids,
) as conn:
await _visible_session(conn, session_id)
row = await conn.fetchrow(
"""
SELECT audio_asset_id, session_id, learner_id, audio_ref, audio_sha256,
media_type, byte_size, duration_ms, retained_until, created_at
FROM app.multimodal_raw_audio_access_v
WHERE session_id = $1 AND audio_asset_id = $2
""",
session_id,
audio_asset_id,
)
if row is None:
raise MultimodalAllianceNotFoundError(
"raw audio asset not found, expired, or deleted"
)
return dict(row)
__all__ = [
"MultimodalAllianceConflictError",
"MultimodalAllianceNotFoundError",
"MultimodalAllianceStateError",
"MultimodalConsentRequiredError",
"MultimodalConsentWithdrawnError",
"append_consent_snapshot",
"append_deletion_request",
"append_measurement_fusion",
"append_runtime_timeline",
"append_timeline",
"assert_voice_processing_allowed",
"complete_deletion",
"read_raw_audio_access",
"read_raw_audio_asset",
"read_session_metadata",
"request_expired_retention_deletions",
]