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
257
scripts/smoke-multimodal-consent-concurrency.py
Normal file
257
scripts/smoke-multimodal-consent-concurrency.py
Normal file
|
|
@ -0,0 +1,257 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Actual-Postgres proof for G7 consent-withdrawal serialization.
|
||||
|
||||
The probe creates a unique synthetic learner/session, grants derived-feature
|
||||
processing, holds the shared consent transaction lock while committing a
|
||||
withdrawal, and proves a concurrent evaluator timeline write remains blocked
|
||||
until that withdrawal commits and then fails closed. No microphone, provider,
|
||||
login, or external service is used. The append-only fixture is intentionally
|
||||
left in the expendable development database as audit evidence.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
from uuid import UUID, uuid4
|
||||
|
||||
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 # noqa: E402
|
||||
from app.contracts.multimodal_alliance import AlignedVoiceTimeline # noqa: E402
|
||||
from app.deps import Principal, Role # noqa: E402
|
||||
from app.services import multimodal_alliance_store as store # noqa: E402
|
||||
|
||||
|
||||
def learner_principal(learner_id: UUID) -> Principal:
|
||||
return Principal(
|
||||
user_id=str(learner_id),
|
||||
role=Role.LEARNER,
|
||||
cohort_ids=["g7-consent-concurrency"],
|
||||
consent_at=1.0,
|
||||
profile_completed_at=1.0,
|
||||
)
|
||||
|
||||
|
||||
async def seed_fixture(learner_id: UUID, session_id: UUID) -> None:
|
||||
pool = db.get_pool()
|
||||
async with pool.acquire() as conn:
|
||||
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,'G7 Consent Concurrency','learner',
|
||||
'g7-consent-concurrency',now(),now())
|
||||
""",
|
||||
learner_id,
|
||||
f"g7-consent-concurrency:{learner_id}",
|
||||
f"{learner_id}@g7-consent-concurrency.invalid",
|
||||
)
|
||||
async with db.acquire(
|
||||
role="learner",
|
||||
user_id=str(learner_id),
|
||||
cohort_ids=["g7-consent-concurrency"],
|
||||
) as conn:
|
||||
await conn.execute(
|
||||
"""
|
||||
INSERT INTO app.sessions (id, learner_id, theory_mode)
|
||||
VALUES ($1,$2,'integrative')
|
||||
""",
|
||||
session_id,
|
||||
learner_id,
|
||||
)
|
||||
|
||||
|
||||
async def persist_timeline(session_id: UUID, submission_id: UUID) -> dict:
|
||||
async with db.acquire(ai_view="evaluator", ai_context=True) as conn:
|
||||
return await store.append_timeline(
|
||||
conn=conn,
|
||||
session_id=session_id,
|
||||
submission_id=submission_id,
|
||||
timeline=AlignedVoiceTimeline(
|
||||
audio_duration_ms=1000,
|
||||
words=(),
|
||||
events=(),
|
||||
),
|
||||
audio_asset=None,
|
||||
)
|
||||
|
||||
|
||||
async def append_withdrawal_inside_lock(
|
||||
*,
|
||||
conn,
|
||||
learner_id: UUID,
|
||||
session_id: UUID,
|
||||
submission_id: UUID,
|
||||
) -> UUID:
|
||||
latest = await store._latest_consent(conn, session_id)
|
||||
if latest is None or latest["consent_status"] != "granted":
|
||||
raise AssertionError("concurrency probe requires a current granted consent")
|
||||
consent_snapshot_id = uuid4()
|
||||
await store._insert_request(
|
||||
conn,
|
||||
submission_id=submission_id,
|
||||
content_hash=store._canonical_hash(
|
||||
{
|
||||
"probe": "concurrent-withdrawal",
|
||||
"session_id": str(session_id),
|
||||
}
|
||||
),
|
||||
request_kind="consent",
|
||||
session_id=session_id,
|
||||
learner_id=learner_id,
|
||||
result_id=consent_snapshot_id,
|
||||
created_by_role="learner",
|
||||
)
|
||||
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,'withdrawn',FALSE,FALSE,TRUE,NULL,
|
||||
'g7-concurrency-v1','concurrency_probe',$4)
|
||||
""",
|
||||
consent_snapshot_id,
|
||||
submission_id,
|
||||
session_id,
|
||||
learner_id,
|
||||
int(latest["sequence_no"]) + 1,
|
||||
)
|
||||
deletion_submission_id, deletion_request_id = store._withdrawal_ids(submission_id)
|
||||
await store._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",
|
||||
)
|
||||
return consent_snapshot_id
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
learner_id = uuid4()
|
||||
session_id = uuid4()
|
||||
grant_submission_id = uuid4()
|
||||
withdrawal_submission_id = uuid4()
|
||||
timeline_submission_id = uuid4()
|
||||
await db.init_pool()
|
||||
timeline_task: asyncio.Task[dict] | None = None
|
||||
try:
|
||||
await seed_fixture(learner_id, session_id)
|
||||
learner = learner_principal(learner_id)
|
||||
await store.append_consent_snapshot(
|
||||
principal=learner,
|
||||
session_id=session_id,
|
||||
submission_id=grant_submission_id,
|
||||
consent_status="granted",
|
||||
retain_audio=False,
|
||||
retain_derived_features=True,
|
||||
transcript_retained=True,
|
||||
retention_days=30,
|
||||
policy_version="g7-concurrency-v1",
|
||||
reason_code=None,
|
||||
)
|
||||
|
||||
blocked_while_withdrawal_uncommitted = False
|
||||
started_at = time.monotonic()
|
||||
async with db.acquire(
|
||||
role="learner",
|
||||
user_id=str(learner_id),
|
||||
cohort_ids=["g7-consent-concurrency"],
|
||||
) as withdrawal_conn:
|
||||
await store._lock_consent_processing(withdrawal_conn, session_id)
|
||||
timeline_task = asyncio.create_task(
|
||||
persist_timeline(session_id, timeline_submission_id)
|
||||
)
|
||||
try:
|
||||
await asyncio.wait_for(asyncio.shield(timeline_task), timeout=0.25)
|
||||
except TimeoutError:
|
||||
blocked_while_withdrawal_uncommitted = True
|
||||
else:
|
||||
raise AssertionError(
|
||||
"timeline write escaped the shared consent transaction lock"
|
||||
)
|
||||
withdrawal_snapshot_id = await append_withdrawal_inside_lock(
|
||||
conn=withdrawal_conn,
|
||||
learner_id=learner_id,
|
||||
session_id=session_id,
|
||||
submission_id=withdrawal_submission_id,
|
||||
)
|
||||
|
||||
write_blocked_after_commit = False
|
||||
try:
|
||||
await asyncio.wait_for(timeline_task, timeout=5.0)
|
||||
except store.MultimodalConsentRequiredError:
|
||||
write_blocked_after_commit = True
|
||||
if not write_blocked_after_commit:
|
||||
raise AssertionError("timeline write was not rejected after withdrawal commit")
|
||||
|
||||
async with db.acquire(ai_view="evaluator", ai_context=True) as conn:
|
||||
timeline_rows = await conn.fetchval(
|
||||
"""
|
||||
SELECT count(*) FROM app.multimodal_audio_timeline
|
||||
WHERE submission_id = $1
|
||||
""",
|
||||
timeline_submission_id,
|
||||
)
|
||||
latest = await store._latest_consent(conn, session_id)
|
||||
if timeline_rows != 0:
|
||||
raise AssertionError("a post-withdrawal timeline row was persisted")
|
||||
if latest is None or latest["consent_snapshot_id"] != withdrawal_snapshot_id:
|
||||
raise AssertionError("withdrawal is not the latest consent snapshot")
|
||||
|
||||
voice_processing_blocked = False
|
||||
try:
|
||||
await store.assert_voice_processing_allowed(
|
||||
principal=learner,
|
||||
session_id=str(session_id),
|
||||
)
|
||||
except store.MultimodalConsentWithdrawnError:
|
||||
voice_processing_blocked = True
|
||||
if not voice_processing_blocked:
|
||||
raise AssertionError("withdrawal did not block subsequent voice processing")
|
||||
|
||||
print(
|
||||
json.dumps(
|
||||
{
|
||||
"schema_version": "vignette.g7-consent-concurrency-proof.v1",
|
||||
"status": "passed",
|
||||
"actual_postgres": True,
|
||||
"synthetic_fixture": True,
|
||||
"session_id": str(session_id),
|
||||
"blocked_while_withdrawal_uncommitted": (
|
||||
blocked_while_withdrawal_uncommitted
|
||||
),
|
||||
"write_blocked_after_withdrawal_commit": (
|
||||
write_blocked_after_commit
|
||||
),
|
||||
"timeline_rows_persisted": timeline_rows,
|
||||
"subsequent_voice_processing_blocked": voice_processing_blocked,
|
||||
"elapsed_ms": round((time.monotonic() - started_at) * 1000),
|
||||
"microphone_used": False,
|
||||
"external_provider_used": False,
|
||||
},
|
||||
ensure_ascii=False,
|
||||
indent=2,
|
||||
)
|
||||
)
|
||||
finally:
|
||||
if timeline_task is not None and not timeline_task.done():
|
||||
timeline_task.cancel()
|
||||
await db.close_pool()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
Loading…
Add table
Add a link
Reference in a new issue