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 산출물은 커밋에서 제외했다.
486 lines
18 KiB
Python
486 lines
18 KiB
Python
#!/usr/bin/env python3
|
|
"""Real-Postgres G7 smoke using synthetic WAV metadata and the store boundary.
|
|
|
|
Run only against an expendable database. The script creates fixture users/session
|
|
and intentionally leaves them for the caller to discard with the database.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
# smoke 러너는 앱 패키지를 불러오기 전에 apps/api를 sys.path에 추가해야 한다.
|
|
# ruff: noqa: E402
|
|
|
|
import asyncio
|
|
import hashlib
|
|
import io
|
|
import math
|
|
import struct
|
|
import sys
|
|
import wave
|
|
from datetime import UTC, datetime, timedelta
|
|
from pathlib import Path
|
|
from uuid import UUID
|
|
|
|
API_ROOT = Path(__file__).resolve().parents[1] / "apps" / "api"
|
|
if str(API_ROOT) not in sys.path:
|
|
sys.path.insert(0, str(API_ROOT))
|
|
|
|
from app import db
|
|
from app.contracts.multimodal_alliance import (
|
|
FusionCalibration,
|
|
ModalityAxisMeasurement,
|
|
)
|
|
from app.deps import Principal, Role
|
|
from app.services import multimodal_alliance_store
|
|
from app.services.multimodal_alliance import align_voice_timeline
|
|
|
|
|
|
LEARNER_ID = UUID("77000000-0000-4000-8000-000000000001")
|
|
TEACHER_ID = UUID("77000000-0000-4000-8000-000000000002")
|
|
OUTSIDER_ID = UUID("77000000-0000-4000-8000-000000000003")
|
|
ADMIN_ID = UUID("77000000-0000-4000-8000-000000000004")
|
|
SESSION_ID = UUID("77000000-0000-4000-8000-000000000010")
|
|
CONSENT_SUBMISSION_ID = UUID("77000000-0000-4000-8000-000000000011")
|
|
TIMELINE_SUBMISSION_ID = UUID("77000000-0000-4000-8000-000000000012")
|
|
TEXT_ONLY_SUBMISSION_ID = UUID("77000000-0000-4000-8000-000000000013")
|
|
FUSION_SUBMISSION_ID = UUID("77000000-0000-4000-8000-000000000014")
|
|
WITHDRAW_SUBMISSION_ID = UUID("77000000-0000-4000-8000-000000000015")
|
|
COMPLETION_SUBMISSION_ID = UUID("77000000-0000-4000-8000-000000000016")
|
|
|
|
|
|
def principal(user_id: UUID, role: Role, cohort: str) -> Principal:
|
|
return Principal(
|
|
user_id=str(user_id),
|
|
role=role,
|
|
cohort_ids=[cohort],
|
|
consent_at=1.0,
|
|
profile_completed_at=1.0,
|
|
)
|
|
|
|
|
|
def synthetic_wav() -> bytes:
|
|
sample_rate = 16_000
|
|
duration_ms = 1200
|
|
frames = sample_rate * duration_ms // 1000
|
|
output = io.BytesIO()
|
|
with wave.open(output, "wb") as writer:
|
|
writer.setnchannels(1)
|
|
writer.setsampwidth(2)
|
|
writer.setframerate(sample_rate)
|
|
payload = bytearray()
|
|
for index in range(frames):
|
|
value = int(3200 * math.sin(2 * math.pi * 220 * index / sample_rate))
|
|
payload.extend(struct.pack("<h", value))
|
|
writer.writeframes(bytes(payload))
|
|
return output.getvalue()
|
|
|
|
|
|
def timeline():
|
|
return align_voice_timeline(
|
|
audio_duration_ms=1200,
|
|
words=(
|
|
{
|
|
"word_index": 0,
|
|
"start_ms": 40,
|
|
"end_ms": 220,
|
|
"speaker": "learner",
|
|
"token_hash": "a" * 64,
|
|
},
|
|
{
|
|
"word_index": 1,
|
|
"start_ms": 500,
|
|
"end_ms": 710,
|
|
"speaker": "client",
|
|
"token_hash": "b" * 64,
|
|
},
|
|
),
|
|
events=(
|
|
{
|
|
"event_id": "oas-g7-event-smoke-silence",
|
|
"event_type": "silence",
|
|
"start_ms": 220,
|
|
"end_ms": 500,
|
|
"actor": "both",
|
|
"observed_feature": "280ms turn transition silence",
|
|
"uncertainty": 0.05,
|
|
"source": "stt_word_timestamps",
|
|
},
|
|
{
|
|
"event_id": "oas-g7-event-smoke-overlap",
|
|
"event_type": "overlap",
|
|
"start_ms": 710,
|
|
"end_ms": 770,
|
|
"actor": "both",
|
|
"observed_feature": "60ms simultaneous speech segment",
|
|
"uncertainty": 0.1,
|
|
"source": "observed_audio_runtime",
|
|
},
|
|
{
|
|
"event_id": "oas-g7-event-smoke-interruption",
|
|
"event_type": "interruption",
|
|
"start_ms": 770,
|
|
"end_ms": 840,
|
|
"actor": "learner",
|
|
"observed_feature": "learner segment began before client segment ended",
|
|
"uncertainty": 0.12,
|
|
"source": "observed_audio_runtime",
|
|
},
|
|
{
|
|
"event_id": "oas-g7-event-smoke-prosody",
|
|
"event_type": "prosody",
|
|
"start_ms": 840,
|
|
"end_ms": 1080,
|
|
"actor": "learner",
|
|
"observed_feature": "median intensity decreased by 3dB",
|
|
"uncertainty": 0.2,
|
|
"source": "observed_audio_runtime",
|
|
},
|
|
),
|
|
)
|
|
|
|
|
|
async def seed_fixture() -> None:
|
|
pool = db.get_pool()
|
|
async with pool.acquire() as conn:
|
|
for user_id, role, cohort in (
|
|
(LEARNER_ID, "learner", "g7-cohort"),
|
|
(TEACHER_ID, "instructor", "g7-cohort"),
|
|
(OUTSIDER_ID, "instructor", "other-cohort"),
|
|
(ADMIN_ID, "admin", "admin-cohort"),
|
|
):
|
|
await conn.execute(
|
|
"""
|
|
INSERT INTO app.app_user (
|
|
user_id, external_id, email, display_name, role, cohort,
|
|
consent_at, profile_completed_at
|
|
) VALUES ($1,$2,$3,$4,$5,$6,now(),now())
|
|
ON CONFLICT (user_id) DO NOTHING
|
|
""",
|
|
user_id,
|
|
f"g7-smoke:{user_id}",
|
|
f"{user_id}@g7-smoke.invalid",
|
|
"G7 Smoke",
|
|
role,
|
|
cohort,
|
|
)
|
|
async with db.acquire(
|
|
role="learner",
|
|
user_id=str(LEARNER_ID),
|
|
cohort_ids=["g7-cohort"],
|
|
) as conn:
|
|
await conn.execute(
|
|
"""
|
|
INSERT INTO app.sessions (id, learner_id, theory_mode)
|
|
VALUES ($1,$2,'integrative')
|
|
ON CONFLICT (id) DO NOTHING
|
|
""",
|
|
SESSION_ID,
|
|
LEARNER_ID,
|
|
)
|
|
|
|
|
|
def measurement(
|
|
*,
|
|
measurement_id: str,
|
|
modality: str,
|
|
value: float,
|
|
evidence_ref: str,
|
|
model_run_id: UUID,
|
|
) -> ModalityAxisMeasurement:
|
|
return ModalityAxisMeasurement(
|
|
measurement_id=measurement_id,
|
|
axis="bond",
|
|
modality=modality,
|
|
status="ready",
|
|
value=value,
|
|
confidence=0.82,
|
|
uncertainty=0.18,
|
|
evidence_refs=(evidence_ref,),
|
|
model_run_id=model_run_id,
|
|
)
|
|
|
|
|
|
async def main() -> None:
|
|
await db.init_pool()
|
|
try:
|
|
await seed_fixture()
|
|
learner = principal(LEARNER_ID, Role.LEARNER, "g7-cohort")
|
|
teacher = principal(TEACHER_ID, Role.TEACHER, "g7-cohort")
|
|
outsider = principal(OUTSIDER_ID, Role.TEACHER, "other-cohort")
|
|
admin = principal(ADMIN_ID, Role.ADMIN, "admin-cohort")
|
|
|
|
consent = await multimodal_alliance_store.append_consent_snapshot(
|
|
principal=learner,
|
|
session_id=SESSION_ID,
|
|
submission_id=CONSENT_SUBMISSION_ID,
|
|
consent_status="granted",
|
|
retain_audio=True,
|
|
retain_derived_features=True,
|
|
transcript_retained=True,
|
|
retention_days=30,
|
|
policy_version="g7-smoke-v1",
|
|
reason_code=None,
|
|
)
|
|
replay = await multimodal_alliance_store.append_consent_snapshot(
|
|
principal=learner,
|
|
session_id=SESSION_ID,
|
|
submission_id=CONSENT_SUBMISSION_ID,
|
|
consent_status="granted",
|
|
retain_audio=True,
|
|
retain_derived_features=True,
|
|
transcript_retained=True,
|
|
retention_days=30,
|
|
policy_version="g7-smoke-v1",
|
|
reason_code=None,
|
|
)
|
|
assert replay["idempotent_replay"]
|
|
assert consent["consent_snapshot_id"] == replay["consent_snapshot_id"]
|
|
|
|
audio = synthetic_wav()
|
|
aligned = timeline()
|
|
audio_asset = {
|
|
"audio_ref": "g7-smoke://synthetic/one.wav",
|
|
"audio_sha256": hashlib.sha256(audio).hexdigest(),
|
|
"media_type": "audio/wav",
|
|
"byte_size": len(audio),
|
|
}
|
|
async with db.acquire(ai_view="evaluator", ai_context=True) as conn:
|
|
first = await multimodal_alliance_store.append_timeline(
|
|
conn=conn,
|
|
session_id=SESSION_ID,
|
|
submission_id=TIMELINE_SUBMISSION_ID,
|
|
timeline=aligned,
|
|
audio_asset=audio_asset,
|
|
)
|
|
async with db.acquire(ai_view="evaluator", ai_context=True) as conn:
|
|
second = await multimodal_alliance_store.append_timeline(
|
|
conn=conn,
|
|
session_id=SESSION_ID,
|
|
submission_id=TIMELINE_SUBMISSION_ID,
|
|
timeline=aligned,
|
|
audio_asset=audio_asset,
|
|
)
|
|
assert second["idempotent_replay"]
|
|
assert first["timeline_id"] == second["timeline_id"]
|
|
changed = aligned.model_copy(update={"audio_duration_ms": 1300})
|
|
try:
|
|
async with db.acquire(ai_view="evaluator", ai_context=True) as conn:
|
|
await multimodal_alliance_store.append_timeline(
|
|
conn=conn,
|
|
session_id=SESSION_ID,
|
|
submission_id=TIMELINE_SUBMISSION_ID,
|
|
timeline=changed,
|
|
audio_asset=audio_asset,
|
|
)
|
|
except multimodal_alliance_store.MultimodalAllianceConflictError:
|
|
pass
|
|
else:
|
|
raise AssertionError("changed idempotency payload was not rejected")
|
|
|
|
provenance = {
|
|
"instrument_id": "alliance-axis-observer",
|
|
"instrument_version": "g7-smoke-v1",
|
|
"model_name": "synthetic-observer",
|
|
"prompt_version": "g7-smoke-v1",
|
|
}
|
|
text = measurement(
|
|
measurement_id="oas-g7-measurement-smoke-text-no-gain",
|
|
modality="text",
|
|
value=0.62,
|
|
evidence_ref="turn:learner:1",
|
|
model_run_id=UUID("77000000-0000-4000-8000-000000000020"),
|
|
)
|
|
voice = measurement(
|
|
measurement_id="oas-g7-measurement-smoke-voice-no-gain",
|
|
modality="voice",
|
|
value=0.7,
|
|
evidence_ref="timeline:oas-g7-event-smoke-prosody",
|
|
model_run_id=UUID("77000000-0000-4000-8000-000000000021"),
|
|
)
|
|
no_gain = FusionCalibration(
|
|
calibration_id="oas-g7-fusion-smoke-no-gain",
|
|
axis="bond",
|
|
text_weight=0.75,
|
|
voice_weight=0.25,
|
|
text_only_accuracy=0.81,
|
|
fused_accuracy=0.815,
|
|
benchmark_version="g7-benchmark-smoke-v1",
|
|
minimum_incremental_gain=0.01,
|
|
)
|
|
async with db.acquire(ai_view="evaluator", ai_context=True) as conn:
|
|
text_only = await multimodal_alliance_store.append_measurement_fusion(
|
|
conn=conn,
|
|
session_id=SESSION_ID,
|
|
submission_id=TEXT_ONLY_SUBMISSION_ID,
|
|
text=text,
|
|
voice=voice,
|
|
calibration=no_gain,
|
|
text_provenance=provenance,
|
|
voice_provenance=provenance,
|
|
)
|
|
assert text_only["result"]["modalities_used"] == ["text"]
|
|
assert not text_only["result"]["fusion_applied"]
|
|
|
|
text_gain = text.model_copy(
|
|
update={"measurement_id": "oas-g7-measurement-smoke-text-gain"}
|
|
)
|
|
voice_gain = voice.model_copy(
|
|
update={"measurement_id": "oas-g7-measurement-smoke-voice-gain"}
|
|
)
|
|
gain = no_gain.model_copy(
|
|
update={
|
|
"calibration_id": "oas-g7-fusion-smoke-gain",
|
|
"fused_accuracy": 0.84,
|
|
}
|
|
)
|
|
async with db.acquire(ai_view="evaluator", ai_context=True) as conn:
|
|
fused = await multimodal_alliance_store.append_measurement_fusion(
|
|
conn=conn,
|
|
session_id=SESSION_ID,
|
|
submission_id=FUSION_SUBMISSION_ID,
|
|
text=text_gain,
|
|
voice=voice_gain,
|
|
calibration=gain,
|
|
text_provenance=provenance,
|
|
voice_provenance=provenance,
|
|
)
|
|
assert fused["result"]["fusion_applied"]
|
|
assert set(fused["result"]["modalities_used"]) == {"text", "voice"}
|
|
|
|
learner_view = await multimodal_alliance_store.read_session_metadata(
|
|
principal=learner, session_id=SESSION_ID
|
|
)
|
|
teacher_view = await multimodal_alliance_store.read_session_metadata(
|
|
principal=teacher, session_id=SESSION_ID
|
|
)
|
|
assert len(learner_view["voice_events"]) == 4
|
|
assert len(teacher_view["voice_events"]) == 4
|
|
assert await multimodal_alliance_store.read_raw_audio_access(
|
|
principal=learner, session_id=SESSION_ID
|
|
)
|
|
try:
|
|
await multimodal_alliance_store.read_raw_audio_access(
|
|
principal=teacher, session_id=SESSION_ID
|
|
)
|
|
except multimodal_alliance_store.MultimodalAllianceStateError:
|
|
pass
|
|
else:
|
|
raise AssertionError("teacher unexpectedly received raw audio access")
|
|
try:
|
|
await multimodal_alliance_store.read_session_metadata(
|
|
principal=outsider, session_id=SESSION_ID
|
|
)
|
|
except multimodal_alliance_store.MultimodalAllianceNotFoundError:
|
|
pass
|
|
else:
|
|
raise AssertionError("out-of-cohort teacher unexpectedly saw G7 metadata")
|
|
assert await multimodal_alliance_store.read_raw_audio_access(
|
|
principal=admin, session_id=SESSION_ID
|
|
)
|
|
|
|
async with db.acquire(ai_view="evaluator", ai_context=True) as conn:
|
|
expiry_work = (
|
|
await multimodal_alliance_store.request_expired_retention_deletions(
|
|
conn=conn,
|
|
as_of=datetime.now(UTC) + timedelta(days=31),
|
|
limit=10,
|
|
)
|
|
)
|
|
assert len(expiry_work) == 1
|
|
assert not expiry_work[0]["idempotent_replay"]
|
|
async with db.acquire(ai_view="evaluator", ai_context=True) as conn:
|
|
expiry_replay = (
|
|
await multimodal_alliance_store.request_expired_retention_deletions(
|
|
conn=conn,
|
|
as_of=datetime.now(UTC) + timedelta(days=31),
|
|
limit=10,
|
|
)
|
|
)
|
|
assert expiry_replay[0]["idempotent_replay"]
|
|
assert (
|
|
expiry_work[0]["deletion_request_id"]
|
|
== expiry_replay[0]["deletion_request_id"]
|
|
)
|
|
|
|
pool = db.get_pool()
|
|
async with pool.acquire() as conn:
|
|
async with conn.transaction():
|
|
await conn.execute("SET LOCAL ROLE vignette")
|
|
public_count = await conn.fetchval(
|
|
"SELECT count(*) FROM app.multimodal_session_metadata_v"
|
|
)
|
|
assert public_count == 0
|
|
|
|
withdrawal = await multimodal_alliance_store.append_consent_snapshot(
|
|
principal=learner,
|
|
session_id=SESSION_ID,
|
|
submission_id=WITHDRAW_SUBMISSION_ID,
|
|
consent_status="withdrawn",
|
|
retain_audio=False,
|
|
retain_derived_features=False,
|
|
transcript_retained=True,
|
|
retention_days=None,
|
|
policy_version="g7-smoke-v1",
|
|
reason_code="learner_withdrawal",
|
|
)
|
|
assert withdrawal["deletion_request_id"] is not None
|
|
try:
|
|
await multimodal_alliance_store.assert_voice_processing_allowed(
|
|
principal=learner,
|
|
session_id=str(SESSION_ID),
|
|
)
|
|
except multimodal_alliance_store.MultimodalConsentWithdrawnError:
|
|
pass
|
|
else:
|
|
raise AssertionError("withdrawal did not block new voice processing")
|
|
|
|
target_hash = hashlib.sha256(
|
|
audio_asset["audio_ref"].encode("utf-8")
|
|
).hexdigest()
|
|
async with db.acquire(ai_view="evaluator", ai_context=True) as conn:
|
|
completed = await multimodal_alliance_store.complete_deletion(
|
|
conn=conn,
|
|
deletion_request_id=withdrawal["deletion_request_id"],
|
|
submission_id=COMPLETION_SUBMISSION_ID,
|
|
tombstones=(
|
|
{
|
|
"scope": "audio",
|
|
"target_ref_hash": target_hash,
|
|
"deletion_proof": "synthetic object-store delete acknowledged",
|
|
"deleted_at": datetime.now(UTC),
|
|
},
|
|
{
|
|
"scope": "derived_features",
|
|
"target_ref_hash": hashlib.sha256(
|
|
str(first["timeline_id"]).encode("utf-8")
|
|
).hexdigest(),
|
|
"deletion_proof": "derived feature access tombstoned",
|
|
"deleted_at": datetime.now(UTC),
|
|
},
|
|
),
|
|
actor_uid=None,
|
|
actor_kind="retention_worker",
|
|
)
|
|
assert len(completed["tombstone_ids"]) == 2
|
|
assert not await multimodal_alliance_store.read_raw_audio_access(
|
|
principal=learner, session_id=SESSION_ID
|
|
)
|
|
after = await multimodal_alliance_store.read_session_metadata(
|
|
principal=learner, session_id=SESSION_ID
|
|
)
|
|
assert not after["word_timestamps"] and not after["voice_events"]
|
|
assert all(item["modality"] == "text" for item in after["measurements"])
|
|
assert all(
|
|
"voice" not in item["modalities_used"] for item in after["fusion_decisions"]
|
|
)
|
|
|
|
print(
|
|
"G7 real DB smoke passed: consent, clock alignment, idempotency, "
|
|
"fusion gate, RLS, retention sweep, withdrawal, and tombstones"
|
|
)
|
|
finally:
|
|
await db.close_pool()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
asyncio.run(main())
|