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:
Yun Chan 2026-08-08 01:30:53 +09:00
parent 93dd8f82d7
commit 16e791e044
390 changed files with 243188 additions and 499 deletions

View file

@ -57,10 +57,10 @@ vignette/
│ apps/api app.main:app (uvicorn :8000) │
│ routes/ → services/ → engine_client │
└───┬───────────────┬───────────────┬───────┘
│ HTTP(ENGINE_URL)│ asyncpg 풀 │ httpx(OpenAI)
│ HTTP(ENGINE_URL)│ asyncpg 풀 │ HTTPS/WSS voice providers
▼ ▼ ▼
engine_gateway Postgres16 OpenAI
(:9099 등 host) + pgvector STT/TTS
engine_gateway Postgres16 Deepgram/OpenAI/Higgs
(:9099 등 host) + pgvector streaming STT/TTS
claude -p 상주풀 (app/audit/kb/ds) (voice 캐스케이드)
▼ subprocess
@ -72,6 +72,7 @@ vignette/
`fetch` 스트림으로 직접 파싱한다(`openSessionStream`).
- 백엔드는 엔진 게이트웨이를 `ENGINE_URL`로 HTTP 호출만 한다(`app/engine_client.py`).
게이트웨이가 provider 라우팅·캐싱·상주 프로세스 풀을 흡수한다.
`ENGINE_GATEWAY_SHARED_SECRET`이 설정되면 모든 호출에 `X-Vignette-Engine-Token`을 붙인다.
---
@ -228,7 +229,13 @@ RBAC×AIView로 차단된다. 이 모듈은 평가 신호만 산출한다.
- evaluator structured 결과는 `evaluate_turn()`/`evaluate_session()` 경계에서 canonical
`GenerateRequest` SHA-256 키의 인메모리 semantic cache로 재사용할 수 있다. `/admin/usage`
cache key·prompt·completion 없이 enabled/entries/hits/misses/stores/evictions/requests/hit_rate와
일별 `daily_cost` bucket(day, turns, tokens, cost)을 관리자 관측값으로 반환한다.
일별 `daily_cost` bucket(day, turns, tokens, cost)을 관리자 관측값으로 반환한다. 비용은
공급자가 반환한 저장값을 우선하되 Claude CLI는 SDK 추정치(`provider_estimate`)로 명시하고,
저장 비용이 없는 Agy/Gemini·Codex·Claude API는 `app.services.llm_pricing`의 버전 고정 공식 참조단가(`reference_rate`)로
입력·캐시 입력·출력 토큰을 환산한다. 기존 DB에 비용 0으로 저장된 행도 조회 시 같은 단가로
보정하며, 단가가 없는 모델은 0달러로 위장하지 않고 `unavailable`로 표시한다.
응답은 `recorded_cost_usd`, `estimated_cost_usd`, provider/model별 `cost_basis`,
`rate_label`, `rate_source_url`을 함께 반환하고 예산 상태는 두 비용을 합친 유효 비용을 사용한다.
`EVALUATOR_SEMANTIC_CACHE_ENABLED`, `EVALUATOR_SEMANTIC_CACHE_TTL_SECONDS`,
`EVALUATOR_SEMANTIC_CACHE_MAX_ENTRIES`로 제한하며, 원문 prompt/completion은 캐시에 저장하지 않는다.
cache hit은 `engine.generate()`와 metadata-only `audit.llm_call_log` 기록을 건너뛰고,
@ -305,7 +312,12 @@ RBAC×AIView로 차단된다. 이 모듈은 평가 신호만 산출한다.
STT/TTS 어댑터(순수 변환 + voice preset 매핑). 상담 로직은 라우트가 조립한다.
- STT: `/audio/transcriptions` (gpt-4o-transcribe → 404 시 whisper-1 폴백), 언어 힌트 `ko`.
- STT provider는 `VIGNETTE_VOICE_STT_PROVIDER=openai|deepgram`이다. OpenAI 경로는 batch
`/audio/transcriptions`(gpt-4o-transcribe → 404 시 whisper-1 폴백), 언어 힌트 `ko`를 사용한다.
Deepgram 경로는 `/v1/listen` WebSocket 한 세션을 발화 중 유지하며 `interim_results`, `Results`,
`speech_final`, word start/end와 allowlisted provider event를 순차 반영한다. 기본 model은 `nova-3`, 언어는
`ko`이며 endpointing/utterance end/keepalive/finalize timeout/MIP opt-out을 env로 조정한다.
Deepgram 선택 상태에서 key가 없고 OpenAI key가 있으면 `openai-batch-fallback`으로 명시 전환한다.
- TTS: `/audio/speech` (gpt-4o-mini-tts → tts-1 폴백). 음성 선택 우선순위는 명시 query preset →
`app.persona_voice_map`의 OpenAI row(`base_params.preset/openai_voice/rate/instructions`) →
페르소나 code 기본 preset(`PRESET_TO_OPENAI_VOICE`, P1=coral / P2=ash / P3=shimmer)이다.
@ -322,7 +334,9 @@ STT/TTS 어댑터(순수 변환 + voice preset 매핑). 상담 로직은 라우
- 텍스트 턴의 AI 내담자 응답은 인증된 `POST /voice/speech``session_id`/`turn_seq`로 소유 회기를 다시
로드하고, 이미 저장된 client-visible 내담자 응답만 선택 provider의 오디오로 합성한다. 브라우저가 임의
문장을 보내는 TTS 프록시가 아니며, 마이크 WebSocket과 같은 voice map/TTS 어댑터를 공유한다.
- 키 없으면 명확히 degraded(`is_available()=False`, `VoiceUnavailable`). 라우트가 503/WS close로 변환.
- 사용 가능한 STT/TTS credential이 없으면 명확히 degraded(`VoiceUnavailable`)하고 라우트가 503/WS close로
변환한다. fallback은 `ready.stt_provider/model`에 실제 provider/model을 노출하므로 expected-provider
public preflight를 통과할 수 없다.
`/voice/ws` WebSocket 캐스케이드(`routes/voice.py`):
```
@ -332,6 +346,10 @@ server: ready → state(listening) → state(thinking) → transcript → reply
```
- 인증: REST와 동일한 서버측 세션 쿠키를 WebSocket 쿠키에서 복원(`_principal_from_websocket`),
learner만 허용.
- `ready`는 실제 `stt_provider`, `stt_model`, `tts_provider`, `tts_model`과 batch fallback 가능 여부를 반환한다.
public preflight는 마이크 없이 이 네 값을 배포 기대값과 정확히 비교한다.
- Deepgram streaming 중에는 동의 원장을 1초마다 재검사한다. 철회·미동의가 확인되면 streaming session을
abort하고 추가 오디오 provider 전송과 후속 파생 저장을 fail-closed한다.
- 턴 실행은 동일하게 `orchestrator.prepare_turn``run_turn_generate`를 거치고, 응답 생성 *후에만*
발화를 영속화한다(실패한 AI 턴이 학습자 단독 축어록을 남기지 않도록).
- `audio_end.provider_events`와 STT provider 이벤트는 allowlist·size limit 후
@ -341,6 +359,8 @@ server: ready → state(listening) → state(thinking) → transcript → reply
provider/source/raw type/text/transcript는 응답하지 않고, 공개 공유 카드는 축어록과 provider raw를 싣지 않는다.
- 텍스트 입력 경로도 턴 저장 완료 뒤 `/voice/speech`를 호출해 AI 내담자 음성을 재생한다. 새 텍스트 발화를
보내면 이전 합성/재생 요청을 무효화해 겹쳐 재생하지 않는다.
- 네트워크/TTS가 끊기면 Session은 작성 중 텍스트를 보존하고 텍스트 계속하기와 키보드 가능한 음성 재연결을
제공한다. 이 recovery는 실패한 provider를 성공으로 위장하지 않는다.
### 2.9 세션 라우트 — `app/routes/sessions.py` (실제 데이터 흐름)
@ -562,6 +582,12 @@ HTTP error mapping을 유지한다.
Codex CLI, Agy CLI의 모델 탐색·실행 차이를 흡수한다. Claude CLI는 회기당 1 `EngineSession` 프로세스를
상주시켜 페르소나 system 프롬프트와 prompt caching을 유지한다. 나머지 공급자는 격리된 stateless 호출로 실행한다.
선택 보안 경계인 `ENGINE_GATEWAY_SHARED_SECRET`을 설정한 인스턴스는 순수 liveness인 `/health`
무인증으로 열고, 실제 생성을 수행하는 `/ready`와 capability·session·generate·stream을 포함한 나머지 모든 HTTP 경로에서
`X-Vignette-Engine-Token`을 constant-time 비교한다. 값은 32자 이상 비-placeholder여야 하며 API와
gateway 양쪽에 동일하게 주입한다. 미설정 인스턴스는 기존 localhost 9099 개발 호환을 유지한다.
인증은 미들웨어 경계에 있어 secret이나 인증 헤더 값이 OpenAPI·애플리케이션 로그에 포함되지 않는다.
실행:
```
cd apps/api && uvicorn engine_gateway.gateway:app --host 0.0.0.0 --port 9099
@ -576,7 +602,7 @@ cd apps/api && uvicorn engine_gateway.gateway:app --host 0.0.0.0 --port 9099
`--system-prompt`로 주입, 마지막 user를 stdin content로. structured_schema는
`_inject_schema()`로 system에 JSON 준수 지시 주입(claude -p는 response_format 미지원이라 차선).
- `POST /v1/stream``turn_stream()`이 assistant 텍스트 델타를 즉시 yield → SSE
`event: token` / `event: done`(provider/model/cost 메타) / `event: error` 프레이밍.
`event: token` / `event: done`(provider/model/token/cost 메타) / `event: error` 프레이밍.
- `session_id`가 살아있으면 풀 재사용, 아니면 1회성(ephemeral) 세션 생성 후 close(`_resolve_session`).
- **공급자 capability** `GET /v1/capabilities` — Codex app-server `model/list`, `agy models`, Anthropic
`GET /v1/models` 또는 Claude CLI 공식 alias를 공통 `EngineModelOption`으로 정규화한다. 60초 캐시와
@ -588,6 +614,16 @@ provider 라우팅 모드는 `ENGINE_MODE`(`app/config.py`) 또는 DB의 관리
실행 가능 공급자는 `claude_cli`, `claude_api`, `codex_cli`, `agy_cli`다. Codex 기본은
`gpt-5.6-terra` / Medium, Agy 기본은 `gemini-3.6-flash-high` / High다. `openai``solar`
기존 계약값을 보존하지만 실행 어댑터가 없어 capability에서 unavailable로 fail-closed한다.
각 어댑터는 사용량을 입력·캐시 입력·출력 토큰으로 정규화한다. Claude CLI는 result의
`modelUsage`/`model_usage`를 우선해 전체 agent tree를 합산하며, 원장의 입력 토큰은
비캐시 입력 + cache read + cache creation 합계다. 모델별 사용량이 없을 때만 최상위
`usage`로 폴백한다. `total_cost_usd`는 Claude SDK의 호출별 클라이언트 추정값이지 청구서
실비가 아니므로 `provider_estimate`로 표시한다. Agy/Gemini·Codex·Claude API는
`app.services.llm_pricing.estimate_reference_cost()`로 공식 참조단가를 적용한다. 과거
토큰 미수집 행은 비용에서 역산하지 않는다. `scripts/backfill-claude-token-usage.py`는 로컬
Claude JSONL의 실제 assistant usage와 DB 응답을 정규화 본문 SHA-256·생성 시각 창으로 대조하고,
후보가 정확히 하나인 행만 `--expected-matches` 가드 아래 복구한다. 일치하지 않거나 모호한 행은
`token_unmetered_turns`로 남긴다.
`AdminEngineConfigResponse`는 provider·URL·model에 `reasoning_effort`를 더해 DB에 저장한다. PATCH는
제안된 게이트웨이에서 capability를 강제 재조회한 뒤 실제 목록에 없는 모델·추론 강도, 미인증 공급자,
@ -678,7 +714,7 @@ React 19 + Vite. 라우팅은 `apps/web/src/App.tsx`(react-router-dom).
## 5. 데이터베이스 스키마 개요
DB는 PostgreSQL 16 + pgvector(단일 SoR). 초기화 SQL은 `infra/db/init/`에 순서대로 적용된다
(`01_extensions → 02_schema → 03_kb → 04_audit_eval_rls → 05_runtime_auth → 06_session_evaluation`).
(`01_extensions → 02_schema → 03_kb → 04_audit_eval_rls → 05_runtime_auth → 06_session_evaluation → 07_measurement_foundation`).
스키마는 **4분할**: `app` / `kb` / `audit` / `ds`.
### 5.1 app 스키마 (`infra/db/init/02_schema.sql`)
@ -714,6 +750,99 @@ DB는 PostgreSQL 16 + pgvector(단일 SoR). 초기화 SQL은 `infra/db/init/`에
이벤트와 수신자별 delivery로 분리해 저장한다. `idempotency_key`가 중복 메일을 막고, delivery는
`queued/sending/sent/failed/skipped` 상태와 시도 횟수, provider message id, 마지막 오류만 저장한다.
메일 본문 HTML이나 회기 축어록은 DB에 복제하지 않는다. RLS는 관리자 전체 처리만 허용한다.
- **Outcome & Alliance 측정 원장** `app.measurement_instrument` / `app.measurement_event` — 척도·훈련지표를
`(instrument_id, instrument_version)`으로 고정하고 측정값을 append-only event로 남긴다. 정정은 UPDATE가
아니라 `supersedes_id`를 가진 새 event이며, `source_kind``perspective`의 호환성, scale, 상태,
`visible_to[]`, 모델 provenance를 DB CHECK/RLS로 강제한다. 기존 `rapport_credit``alliance_level`
Working Alliance가 아닌 `simulation_progress` 신호로만 적응한다.
- **Alliance Pulse 실행 원장** `app.alliance_pulse` / `app.self_assessment` /
`audit.alliance_pulse_status_event` — 수련생의 goal/task/bond 자기평가를 먼저 잠근 뒤 가상내담자와 독립
관찰자를 서로 다른 엔진 세션으로 실행한다. `awaiting_agents` 동안은 자기보고만 읽을 수 있고 모든 agent
실행·측정 event 저장이 끝난 뒤 `ready`로 단방향 전이하면서 reveal한다. 실패는 무점수 `error/degraded`
남기며 정상 점수로 보정하지 않는다. 교수자 평정은 `human_rated/supervisor_human` 새 event 3개를 추가하고
재평정은 직전 event를 `supersedes_id`로 연결한다. prompt/schema/input evidence hash와 시도별 실패 이력은
`audit.model_run`에 보존한다.
- **종단 성과 궤적** `ds.synthetic_outcome_arc` / `app.outcome_trajectory_revision` /
`app.outcome_trajectory_observation` — 같은 case의 1~5회기에서 고통 부담·일상 기능·학습 참여를 독립 축으로
읽고, 기대분포는 `synthetic_educational`·`clinical_claim_allowed=false`로 고정한다. 누락·오류는 무점수로
보존하고 revision은 source fingerprint와 supersession을 가진 append-only snapshot이다. 학습자 체크인은
submission ID와 축별 measurement ID로 멱등하며, 관계 기억은 `app.relationship_memory_event`
역할별 `app.relationship_memory_projection`으로 분리한다. safety signal은 outcome 상태에 합산하지 않는다.
- **파열·수선 원장** `app.rupture_episode` / `app.rupture_observation_event` /
`app.rupture_reconciliation_revision` / `app.rupture_safety_reference` — G3의 onset→recognized/repair_attempted→
missed/partial/resolved 전이를 episode별 sequence로 append한다. fast warning과 deep reconciliation은 별도
revision으로 대조하고 사람 정정은 supersession event로 남긴다. 내부 evaluator write API는
`VIGNETTE_RUPTURE_INTERNAL_TOKEN`이 없거나 32자 미만이면 503으로 닫히며, 잘못된 토큰은 evaluator DB
context를 열기 전에 거부한다. safety는 resolution에 영향을 주는 점수 없이 동일 회기 FK만 보존한다.
`rupture_scenario_director`는 case/session/turn seed에서 4~6턴 간격으로 9종을 순환하되 client prompt에는
조건부 행동 cue만 주입한다. taxonomy·scenario UUID·selector provenance는 engine metadata에만 남고,
safety escalation이나 내부 marker 누출 가능성이 있으면 생성·SSE를 fail-closed한다. 텍스트·SSE·음성은
모두 durable 상담자/내담자 UUID pair와 structured fast evaluation이 저장된 뒤 같은 detector를 실행한다.
- **의도적 수련 원장** `app.practice_coaching_card` / `app.practice_prescription` /
`app.practice_attempt_episode` / `app.practice_attempt` / `app.practice_competency_snapshot` /
`app.practice_curriculum_decision` / `app.practice_teacher_correction` — G4 coaching card를 관찰 가능한
원자 행동과 5종 실행 prescription으로 변환하고, 약점·망각 위험·선행 역량으로 다음 과제를 선택한다.
익숙한 장면의 반복 성공과 학습자 자기 성공 주장은 mastery 근거가 아니며, `unseen_transfer` 성공 전에는
`mastery_allowed=false`를 강제한다. 내부 처방 write는 `VIGNETTE_PRACTICE_INTERNAL_TOKEN`을 DB acquire 전에
검증하고, 학습자 시도와 교수자 교정은 역할·cohort RLS와 append-only supersession으로 분리한다.
- **자기보정·전이 원장** `app.calibration_prediction_revision` / `app.calibration_prediction_lock` /
`app.calibration_performance_observation` / `app.calibration_assessment` /
`app.calibration_meta_prescription` / `app.calibration_transfer_suite` / `app.calibration_transfer_trial` /
`app.calibration_subgroup_drift` — 외부평가 reveal 전 학습자 성공확률·확신도 revision을 보존하고, 명시적
lock 뒤 revision을 trigger로 거부한다. runtime/evaluator 수행 관측은 독립 perspective로 저장하며 역량별
error·bounded interval·과신/과소신과 baseline→recent 변화를 계산한다. transfer는 context·관계스타일·난이도·
표현의 coverage와 phrase/scenario family novelty를 요구하고 암기 문구를 차단한다. 합성 subgroup drift는
실제 인구집단 주장이 아니며 표본 부족을 별도 상태로 둔다. 내부 관측 write는
`VIGNETTE_CALIBRATION_TRANSFER_INTERNAL_TOKEN`을 DB acquire 전에 검증하고 역할·cohort RLS를 유지한다.
- **감독·연구 OS 원장** `app.supervision_attention_snapshot` / `app.supervision_attention_item` /
`app.supervision_evidence_pointer` / `app.supervision_teacher_ai_disagreement` /
`app.supervision_calibration_dataset_row` / `audit.supervision_teacher_event` /
`app.supervision_curriculum_gap_snapshot` / `app.supervision_evaluation_batch` /
`app.supervision_drift_report` / `app.supervision_phase3_manifest` — G6 위험·정체·미해결 관계 사건을
총점 대신 원장 UUID pointer 최대 3개로 queue화하고, 교수자 정정은 raw transcript 없는 disagreement/dataset/audit
append로 보존한다. baseline/candidate model·prompt·instrument와 subgroup metric, Phase 3 네 도메인 artifact
hash/provenance를 재현 read model로 제공한다. supervisor/research AI view는 물리 분리하며 내부 write는
`VIGNETTE_SUPERVISION_RESEARCH_INTERNAL_TOKEN`을 DB acquire 전에 검증하고 사람 조회는 teacher/admin·cohort
RLS로 제한한다.
- **멀티모달 동맹 원장** `app.multimodal_consent_snapshot` / `app.multimodal_audio_asset` /
`app.multimodal_audio_timeline` / `app.multimodal_word_timestamp` / `app.multimodal_voice_event` /
`app.multimodal_axis_measurement` / `app.multimodal_fusion_decision` /
`app.multimodal_deletion_request` / `audit.multimodal_deletion_tombstone` — G7 음성 입력을 STT word와
silence/overlap/interruption/prosody가 공유하는 단일 밀리초 시계로 정렬한다. text와 voice 축 측정은 별도
provenance로 저장하고 synthetic benchmark에서 유의한 추가 이득이 있을 때만 fusion한다. 비언어 이벤트는
관찰 가능한 interaction signal만 허용하며 감정·진단 추론을 계약에서 거부한다. streaming STT word 원문은
저장하지 않고 deployment `SESSION_SECRET`으로 `session_id:submission_id:casefold(word)`를 HMAC-SHA256한
pseudonym과 시간만 저장한다. learner 동의가 없거나 철회되면 최초 처리 전과 열린 stream 중 1초 간격으로
voice ingestion을 fail-closed하고, raw audio는 learner/admin만 보며 teacher는 metadata-only read model을 사용한다.
삭제 tombstone 뒤에는 raw audio와 derived voice feature를 read model에서도 제거한다. 내부 write는
`VIGNETTE_MULTIMODAL_ALLIANCE_INTERNAL_TOKEN`을 DB acquire 전에 검증한다.
- **G7 voice runtime·external evidence boundary** — Session의 첫 음성 입력은 처리 설명과 명시 동의 뒤
`app.multimodal_consent_snapshot`에 원음 미보존·전사/파생 특징 30일 동의를 먼저 append한다. write 실패 전에는
브라우저 `getUserMedia``/voice/ws`를 시작하지 않으며, 회기 전환·unmount·pause는 capture generation을
무효화하고 뒤늦은 MediaStream track을 종료한다. 관리자 전용 `GET /admin/voice-runtime`은 single-worker
RSS/peak RSS·CPU·thread/FD, WebSocket/provider session, route audio buffer, streaming event queue,
overflow/fallback/error high-water만 반환한다. session ID·축어록·원음·provider payload는 포함하지 않는다.
production Docker는 Uvicorn worker 1개와 `--ws-max-queue 4`를 사용한다. 외부 종료는 public WSS/physical mic,
process-local runtime, exact Linux topology, independent human-held-out voice-gain 네 artifact가 같은 public host와
겹치는 50분 시간창을 가져야 한다. Cloudflare 내부 큐를 직접 측정했다고 주장하지 않고 public runner의
TLS/CF-Ray/latency/gap/disconnect를 edge 경계로 쓴다. canonical validator는
`scripts/check-g7-external-proof.py`이며 synthetic/preflight/short probe는 종료 증거가 아니다.
- **지속 개선 원장** `app.ci_agentic_job` / `app.ci_content_pipeline` / `app.ci_content_qualification` /
`app.ci_model_change_gate` / `app.ci_release_gate` / `app.ci_gate_artifact` /
`audit.ci_human_approval_event` / `audit.ci_lifecycle_event` / `app.ci_operational_incident` /
`app.ci_regression_dag_node` — G8 source pack에서 생성된
사례·균열·연습·benchmark 초안을 독립 red-team과 품질 gate로 통과시킨 뒤, baseline·threshold·provenance·
rollback 네 종류 증거와 관리자 사유가 모두 있을 때만 append-only 승인 effect를 기록한다. 자동 publish 버튼은
없고 pending human approval을 권한 경계로 유지한다. 모델·릴리스 monitor와 rollback, 운영 incident→재현 테스트→
backlog DAG를 같은 read model에 연결하되 raw transcript·PII·임상 주장·합산 총점을 노출하지 않는다. 내부 write는
`VIGNETTE_CONTINUOUS_IMPROVEMENT_INTERNAL_TOKEN`을 DB acquire 전에 검증한다. opt-in scheduled producer는
repo-approved synthetic source를 immutable job으로 등록하고 lease/재시도 상태를 durable하게 소유한다.
source usage·고정 classification·본문 hash·PII를 model 호출 전에 다시 검사하고, model/structured 실패는
`retry_wait`, 안전 gate 실패는 pipeline 미저장 `rejected`, 전 단계 통과만 `pending_human_approval`로 남긴다.
API lifespan은 schema contract가 준비됐을 때만 producer를 시작하며 producer는 사람 승인·catalog 승격을
수행할 권한이 없다. 별도 opt-in drift trigger는 G6 합성 drift 원장의 canonical 전체·subgroup 임계값과
근거 행을 다시 대조한 뒤 metadata-only incident·4-node DAG·`scheduled_incident` job을 같은 research
트랜잭션에서 멱등 생성하며, 자동 승인이나 catalog 쓰기 경로를 호출하지 않는다.
- **운영 콘솔** `app.admin_health_event` / `app.admin_health_daily_rollup` — 관리자 `/admin/health`
조회 시점 또는 `scripts/record-admin-health-sample.py` synthetic sampler 실행 시점의 서비스별 원시
헬스 샘플은 `app.admin_health_event`에 남긴다. `scripts/maintain-admin-health-events.py`는 명시
@ -746,6 +875,9 @@ DB는 PostgreSQL 16 + pgvector(단일 SoR). 초기화 SQL은 `infra/db/init/`에
- 종단: `app.learner_profile`(dim_ewma/dim_slope/persistent_gaps — 회기 거듭하며 나아지는지).
- 감사(append-only): `audit.persona_drift_log`(임베딩 일관성/CCD 누출), `audit.audit_log`(인간 열람 추적),
`audit.llm_call_log`(비용·inference_geo·latency metadata-only; prompt/completion 본문 미저장).
- 측정 실행 감사(append-only): `audit.model_run`은 provider/model뿐 아니라 prompt bundle id/version/hash,
structured schema version, input evidence hash를 남긴다. `model_inferred`·`agent_reported` 측정은 이 실행을
참조하지 않으면 저장할 수 없다.
### 5.3 ds 스키마 — 재귀학습 파이프라인 (`infra/db/init/04_audit_eval_rls.sql` §4)
@ -753,11 +885,16 @@ AI자동(1R) → 인간검수(2R) → IAA 게이트(κ≥0.6, ICC≥0.75) →
`ds.dataset``ds.dataset_item``ds.annotation_round`(ai_auto/human_review) → `ds.annotation`
`ds.export_manifest`(iaa_kappa/iaa_icc/jsonl_path).
G0 측정 benchmark는 운영 회기와 분리된 `ds.benchmark_case` / `ds.benchmark_observation`에 둔다. 첫 pack은
goal/task mismatch, empathic miss, withdrawal, confrontation, successful/failed repair,
warm-but-directionless의 8개 버전 고정 장면이며 기대 방향과 근거 turn index, 금지 주장을 함께 저장한다.
### 5.4 정보비대칭 2-레이어 + RLS
DB 레벨 이중강제(`04_audit_eval_rls.sql` §5, `app/db.py` `acquire()`):
- **레이어1 (AI 정보비대칭)**: `app.ai_context='1'` + `app.current_ai_view`(client/counselor/evaluator)
- **레이어1 (AI 정보비대칭)**: `app.ai_context='1'` + `app.current_ai_view`(client/counselor/evaluator;
measurement 원장은 supervisor/research까지 확장)
+ `app.current_sens_max``turns`/`kb.chunk`/`pinned_fact``visible_to[]`/`sensitivity` WHERE 강제.
AIView별 sensitivity 상한 기본값: client=1, counselor=0, evaluator=2(`db._default_sensitivity_max`).
- **레이어2 (인간 RBAC×cohort)**: `app.current_role` + `app.current_uid` + `app.current_cohort`
@ -865,7 +1002,8 @@ DB 레벨 이중강제(`04_audit_eval_rls.sql` §5, `app/db.py` `acquire()`):
|---|---|---|
| DB(Postgres) | `/health` `db:false`, status `degraded` | `app/main.py` lifespan이 dev에서 예외 흡수, `store.py` in-proc로 1턴 동작 |
| 엔진 게이트웨이 | `/health` `engine:false` | 턴 생성 실패(503/SSE error). UI/로그인/페르소나/세션생성(in-memory)은 동작 |
| OpenAI 키 미설정 | `/voice/health` `degraded` | 음성 비활성, WS는 degraded 통지 후 정상 close |
| 선택 STT credential 미설정 | `/voice/health` `degraded` 또는 `ready`의 batch fallback 메타 | Deepgram 선택+key 없음은 OpenAI key가 있을 때만 `openai-batch-fallback`; usable credential이 없으면 WS degraded close |
| 운영 TTS provider/model 불일치 | health/ready expected exact match 실패 | 운영은 OpenAI API `gpt-4o-mini-tts`와 AI 생성 음성 고지를 유지하고, Higgs·sample provider는 dev-only로 차단 |
| Presidio 미설치 | — | 정규식 PII 마스킹 폴백 |
| 승인 페르소나 조회 실패 | persona `degraded=true` | `ALLOW_SEED_PERSONA_FALLBACK=true`면 시드 카드로 폴백 |