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 산출물은 커밋에서 제외했다.
This commit is contained in:
parent
93dd8f82d7
commit
16e791e044
390 changed files with 243188 additions and 499 deletions
486
scripts/smoke-multimodal-alliance-ledger.py
Normal file
486
scripts/smoke-multimodal-alliance-ledger.py
Normal file
|
|
@ -0,0 +1,486 @@
|
|||
#!/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())
|
||||
Loading…
Add table
Add a link
Reference in a new issue