런타임 계약과 학습자 흐름 보강

This commit is contained in:
Yun Chan 2026-06-29 08:12:14 +09:00
parent f456b8997a
commit 206018b088
56 changed files with 4306 additions and 1008 deletions

View file

@ -26,11 +26,14 @@ The first implementation boundary is the engine gateway contract:
- SSE events: `token`, `done`, `error`
- request/response models owned by `apps/api/app/contracts/engine_gateway.py`
- gateway SSE line parsing owned by `EngineGatewaySseLineDecoder` behind `EngineClient.stream_packets()`
- default-routing sentinel `gateway-default` normalized by `normalize_engine_gateway_model()`
- structured response fallback owned by `structured_payload_from_response()`
- current-turn prompt split owned by the Python gateway `GatewayPromptParts(system_prompt, user_payload)` boundary
Both the FastAPI engine client and the current Python engine gateway import this same contract. The API orchestrator consumes decoded `EngineGatewaySsePacket` values from `EngineClient.stream_packets()` instead of hand-parsing `event:`/`data:` lines. The current Python gateway also returns `/v1/generate` through `GenerateResponse.model_dump()` rather than a hand-mirrored dict literal. A future Node.js gateway must preserve these shapes.
Both the FastAPI engine client and the current Python engine gateway import this same contract. The API orchestrator consumes decoded `EngineGatewaySsePacket` values from `EngineClient.stream_packets()` instead of hand-parsing `event:`/`data:` lines. The current Python gateway also returns `/v1/generate` through `GenerateResponse.model_dump()` rather than a hand-mirrored dict literal, and its `provider/model` response metadata is owned by one helper path. `GatewayPromptParts` names the current-turn prompt boundary: non-empty system messages are joined for `--system-prompt`, the final user message is sent as the turn payload, and missing-user generate/stream requests fail with 400 before session resolution. Evaluator, live-coach, and persona generation call `structured_payload_from_response()` directly, so a future Node.js gateway must preserve the structured field and legacy text-JSON fallback behavior.
The cross-runtime golden fixture is `apps/api/engine_gateway/golden/engine_gateway_contract.v1.json`, backed by `apps/api/engine_gateway/golden/engine_gateway_schema.v1.json`.
The fixture pins a representative `/v1/generate` request/response, stream `token/done/error` frames, decoded packet payloads, and the provider `[DONE]` compatibility line. The schema artifact pins the same request/response/event shapes without requiring Python imports. The Python contract test validates that the schema artifact matches the current Pydantic contract and that the fixture passes both JSON Schema and the runtime SSE decoder. `scripts/check-engine-gateway-contract.mjs` reads the same artifacts from Node.js, decodes the SSE frames, ignores provider `[DONE]` compatibility lines, and proves packet-level equivalence without importing Python code. A future Node.js gateway should keep this runner green and then add live endpoint conformance on top of it.
The fixture pins a representative `/v1/generate` request/response, stream `token/done/error` frames, decoded packet payloads, the `gateway-default` default-routing sentinel, and the provider `[DONE]` compatibility line. The schema artifact pins the same request/response/event shapes without requiring Python imports. The Python contract test validates that the schema artifact matches the current Pydantic contract and that the fixture passes both JSON Schema and the runtime SSE decoder. `scripts/check-engine-gateway-contract.mjs` reads the same artifacts from Node.js, decodes the SSE frames, ignores provider `[DONE]` compatibility lines, verifies that `gateway-default` appears only as the request routing sentinel and not as a resolved response model, and proves packet-level equivalence without importing Python code. A future Node.js gateway should keep this runner green and then add live endpoint conformance on top of it.
For `/v1/stream`, gateway-to-API SSE frames are:
@ -47,6 +50,8 @@ Provider pass-through sentinels such as `data: [DONE]` are ignored compatibility
- Browser-facing `/sessions/{id}/stream` SSE stays a separate app contract; the engine gateway stream uses JSON token payloads.
- Browser-facing session list/dashboard/detail/review/share response DTOs stay stable across the Python route and any future Node read API.
- Browser-facing persona catalog/review/draft/source/evidence response DTOs stay stable across the Python route and any future Node read API.
- Persona draft generation keeps the same structured schema, prompt bundle id/version/hash, output extraction, and default coercion semantics.
- Session evaluation persistence keeps the same `status/source/scope/stage/payload/error` write packet semantics.
- RBAC, RLS, audit logging, and PII masking stay fail-closed.
- OpenAPI generated frontend contracts stay current.
- Existing ports and local/prod startup paths stay compatible until a service is deliberately replaced.
@ -61,28 +66,34 @@ This preserves the current FastAPI delivery path while making a future Node.js r
## Third Boundary
The third internal boundary is the browser-facing persona read model.
The third internal boundary is the browser-facing persona read model plus persona draft generation contract.
`apps/api/app/persona_read_model.py` now owns persona catalog, review queue, draft payload/detail, source document, generation evidence, and draft generation response DTOs plus deterministic mappers. `apps/api/app/routes/personas.py` keeps route decorators, auth and teacher/admin gates, repository calls, RAG source registration, LLM draft generation, and HTTP error mapping.
`apps/api/app/persona_read_model.py` now owns persona catalog, review queue, draft payload/detail, source document, generation evidence, and draft generation response DTOs plus deterministic mappers. `apps/api/app/persona_generation_contract.py` owns the draft structured schema, prompt bundle id/version/hash, `GenerateResponse` payload extraction, and generated draft default/coercion rules. `apps/api/app/routes/personas.py` keeps route decorators, auth and teacher/admin gates, repository calls, RAG source registration, engine invocation, provenance assembly, and HTTP error mapping.
This keeps `/personas` fail-closed semantics stable: normal catalog entries remain `source:"database", degraded:false`, seed fallback remains degraded, and OpenAPI schema names such as `PersonaSummary`, `PersonaReviewSummary`, `PersonaDraftPayload`, and `PersonaDraftDetail` stay unchanged.
## Fourth Boundary
The fourth internal boundary is the session evaluation write packet.
`apps/api/app/session_persistence.py` now exposes `SessionEvaluationWrite` for `app.session_evaluation` writes. `routes/sessions.py` and `routes/eval.py` construct the named packet instead of passing an anonymous keyword bag, while the SQL columns, fallback cache record, status/source/scope/stage semantics, and saved payload shape stay unchanged.
## Evidence
- `python -B -m py_compile app/contracts/engine_gateway.py app/engine_client.py app/services/orchestrator.py engine_gateway/gateway.py engine_gateway/test_gateway_model.py app/test_orchestrator_masking.py app/test_session_turn_persistence.py`
- `py -3.11 -X utf8 -B -m py_compile app/contracts/engine_gateway.py app/engine_client.py app/services/orchestrator.py engine_gateway/gateway.py engine_gateway/test_gateway_model.py app/test_orchestrator_masking.py app/test_session_turn_persistence.py app/persona_generation_contract.py app/persona_read_model.py app/routes/personas.py app/session_persistence.py app/routes/sessions.py app/routes/eval.py app/services/evaluator.py app/services/live_coach.py`
- `node scripts/check-engine-gateway-contract.mjs --json`
- `python -B -m pytest -p no:cacheprovider engine_gateway/test_gateway_model.py -q` — 19 passed
- `python -B -m pytest -p no:cacheprovider engine_gateway/test_gateway_model.py app/test_orchestrator_masking.py app/test_session_turn_persistence.py -q` — 50 passed
- `py -3.11 -X utf8 -B -m pytest -p no:cacheprovider engine_gateway/test_gateway_model.py -q` — 27 passed
- `py -3.11 -X utf8 -B -m pytest -p no:cacheprovider engine_gateway/test_gateway_model.py app/test_evaluation_persistence.py app/test_evaluator_model_routing.py app/test_session_turn_persistence.py app/test_live_coach_sources.py app/test_persona_generation_contract.py app/test_persona_review.py -q` — 120 passed
- `apps/api/engine_gateway/golden/engine_gateway_contract.v1.json` and `apps/api/engine_gateway/golden/engine_gateway_schema.v1.json` validated by `engine_gateway/test_gateway_model.py`
- `python -B -m py_compile app/session_read_model.py app/routes/sessions.py`
- `python -B -m pytest -p no:cacheprovider app/test_session_turn_persistence.py app/test_evaluation_persistence.py app/test_session_share.py app/test_learner_dashboard.py app/test_rbac_idor.py app/test_teacher_dashboard.py -q` — 48 passed
- `python -B -m py_compile app/persona_read_model.py app/routes/personas.py`
- `python -B -m pytest -p no:cacheprovider app/test_persona_review.py -q` — 33 passed
- `python -B -m pytest -p no:cacheprovider app/test_persona_review.py app/test_session_turn_persistence.py -q` — 55 passed
- `py -3.11 -X utf8 -B -m py_compile app/persona_generation_contract.py app/persona_read_model.py app/routes/personas.py app/test_persona_generation_contract.py app/test_persona_review.py`
- `py -3.11 -X utf8 -B -m pytest -p no:cacheprovider app/test_persona_generation_contract.py app/test_persona_review.py -q` — 39 passed
- `npm run check:api-types`
- `npm run typecheck`
## Next Refactor Candidates
1. Add live Node.js gateway endpoint conformance once a Node gateway implementation exists; the artifact runner is now in place.
2. Leave `admin.py` mapper cleanup for later because RBAC, audit, and RLS blast radius is larger.
2. Extract a `TurnEvaluationPayload` adapter after pinning the current dict shape in tests.
3. Leave `admin.py` mapper cleanup for later because RBAC, audit, and RLS blast radius is larger.

File diff suppressed because one or more lines are too long

View file

@ -141,7 +141,9 @@ vignette/
| L6 | 직전 K턴 맥락(히스토리) + 이번 발화(L5) | ❌ | 상담자=user, 내담자(자기)=assistant 매핑 |
메시지 순서: `system(L0+L1, cache)``system(L2, cache)``system(L3)``system(L4)`
assistant/user 히스토리(L6) → `user(이번 마스킹 발화, L5)`. 게이트웨이는 마지막 user를 stdin으로 주입한다.
assistant/user 히스토리(L6) → `user(이번 마스킹 발화, L5)`. 현재 Python gateway의
`_split_messages()` 경계는 system 묶음과 마지막 user payload만 소비한다. L6의 system 외
history를 실제 프롬프트에 직렬화하는 변경은 별도 프롬프트 동작 패치로 다룬다.
**안전 불변식**(`L0_SAFETY`, docstring R4/R5/M6):
@ -215,7 +217,7 @@ RBAC×AIView로 차단된다. 이 모듈은 평가 신호만 산출한다.
supervisor rationale/critique + alternative_utterances. `_deep_schema()`.
- 정답 라벨 enum은 `app/taxonomy.py`가 단일 원천(SoT). LLM 출력은 enum으로 안전 파싱(미지값 폐기,
`_parse_technique`/`_parse_client_state`). 게이트웨이 structured 우선, 없으면 text에서 JSON 추출
(`_structured_payload`, 코드펜스 관용).
(`structured_payload_from_response()`, 코드펜스 관용).
- 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와
@ -426,13 +428,19 @@ GET {ENGINE_URL}/ready|/health — readiness/liveness
- 단일 프로세스(uvicorn) 가정의 단순 dict. 멀티워커에선 DB가 SoR이므로 무방.
- 영속/폴백 분기는 `app/runtime_policy.py`(`runtime_fallback_allowed` / `require_runtime_fallback_allowed`)가
게이트. 라우트는 `session_persistence`(DB) → 실패 시 store(in-proc) 순으로 시도한다.
- 회기 평가 저장은 `SessionEvaluationWrite``status/source/scope/stage/payload/error` write packet을
소유한다. `routes/sessions.py``routes/eval.py`는 같은 named packet을 만들어 저장하고, DB SQL과
fallback cache record는 `session_persistence.py`가 유지한다.
### 2.12 페르소나 카탈로그 — `app/persona_repository.py`
DB `app.persona_card`가 승인 페르소나의 SoR. 이 모듈이 in-proc `PersonaCard`와 DB 행을 잇는 경계다.
브라우저-facing persona catalog/review/draft/source/evidence DTO와 deterministic mapper는
`app/persona_read_model.py`가 소유한다. `routes/personas.py`는 route/auth, teacher/admin gate,
DB repository 호출, RAG source 등록, LLM draft generation, HTTP error mapping을 유지한다.
`app/persona_read_model.py`가 소유한다. draft generation structured schema, prompt bundle
id/version/hash, `GenerateResponse` payload extraction, generated draft default/coercion은
`app/persona_generation_contract.py`가 소유한다. `routes/personas.py`는 route/auth,
teacher/admin gate, DB repository 호출, RAG source 등록, engine invocation, provenance assembly,
HTTP error mapping을 유지한다.
- `load_file_personas()` / `built_in_personas()` — in-code P1~P3과 저장소 `data/personas/P4~P7.json`을 deterministic catalog로 합친다.
- `materialize_seed_personas()` — built-in P1~P7을 초기 승인 카탈로그로 누락분만 insert(admin 롤). `ON CONFLICT DO NOTHING`이므로 교수 편집본을 덮어쓰거나 `archived` 보관본을 재승인하지 않는다.
@ -476,7 +484,8 @@ cd apps/api && uvicorn engine_gateway.gateway:app --host 0.0.0.0 --port 9099
- **상주 풀** `/session`, `/session/{sid}/turn`, `/session/{sid}`(DELETE) — 회기 단위 프로세스.
- **stateless 어댑터**(engine_client 계약과 정합):
- `POST /v1/generate``EngineMessage[]``_split_messages()`로 (system_prompt, last_user) 분리
- `POST /v1/generate``EngineMessage[]``_split_messages()`
`GatewayPromptParts(system_prompt, user_payload)` 분리
`--system-prompt`로 주입, 마지막 user를 stdin content로. structured_schema는
`_inject_schema()`로 system에 JSON 준수 지시 주입(claude -p는 response_format 미지원이라 차선).
- `POST /v1/stream``turn_stream()`이 assistant 텍스트 델타를 즉시 yield → SSE
@ -488,9 +497,10 @@ cd apps/api && uvicorn engine_gateway.gateway:app --host 0.0.0.0 --port 9099
provider 라우팅 모드는 `ENGINE_MODE`(`app/config.py`)로 선택: `claude_api`(기본, Anthropic Messages API
직결) / `claude_cli`(로컬 상주 풀) / `openai`(폴백/평가 보조) / `solar`(국내, PII 민감구간 inference_geo:kr).
> 포트 주의: 게이트웨이 docstring 예시는 `:9099`이고, `app/config.py``engine_url` 기본값은
> compose 서비스명 기반 `http://engine:8100`이다. 로컬에서는 `ENGINE_URL`을 게이트웨이 실제 포트
> (예: `http://127.0.0.1:9099`)로 맞춰 주입한다.
> 포트 주의: 게이트웨이 docstring 예시는 `:9099`이고, `app/config.py`의 bare Settings fallback은
> legacy compose 서비스명 기반 `http://engine:8100`이다. 현재 지원 compose/local runtime은
> `ENGINE_URL`을 실제 host gateway 포트(예: `http://host.docker.internal:9099` 또는
> `http://127.0.0.1:9099`)로 override한다.
---

View file

@ -55,6 +55,21 @@ powershell -NoProfile -ExecutionPolicy Bypass -File scripts\start-tailscale-runt
- dev-login은 `AUTH_DEV_LOGIN_EXTRA_ORIGINS`에 Tailnet origin이 들어간 경우에만 dev 환경에서 열린다. prod에서는 열리지 않는다.
- Tailnet/로컬 dev에서는 Google OAuth를 사용하지 않는다. OAuth redirect URI가 공개 API callback으로 고정된 동안에는 콜백이 로컬/Tailnet 세션이 아니라 공개 API 세션으로 돌아가므로, 로그인 화면은 Google 버튼을 비활성화하고 직접 `/api/auth/login?provider=google`을 열어도 `local_oauth_unavailable` 안내로 되돌린다.
### 0.2 Public runtime watchdog
공개 API 복구 스크립트는 `docs/ops/public-runtime-watchdog.md`가 runbook이다. 로컬 개발 서버와 별개로
prod API 8001, web preview 5174, engine gateway 9099, cloudflared tunnel을 검사한다.
```powershell
powershell -NoProfile -ExecutionPolicy Bypass -File scripts\watch-public-runtime.ps1 -CheckOnly
powershell -NoProfile -ExecutionPolicy Bypass -File scripts\install-public-runtime-task.ps1 -RunNow
```
- Scheduled Task는 현재 Windows 사용자 기준 `AtLogOn` + 반복 watchdog이다. 사용자 로그인 전 headless boot service가 아니다.
- secret은 task 인자에 넣지 않는다. API secret은 `apps/api/.env`, cloudflared/Claude CLI credential은 사용자 profile에 둔다.
- 아직 DNS가 없는 future host는 기본 검사에 넣지 않는다. `api-vnet.18ka.net`처럼 실제로 열린 뒤에만 `-AdditionalPublicHealthUrls`로 명시 추가한다.
- 완료 판정은 parser/check-only가 아니라 실제 재부팅 또는 로그오프/로그온 뒤 `Get-ScheduledTaskInfo`, watchdog 로그의 `restart verified`, public health, 인증된 public `/turn` smoke까지 한 세트로 남겨야 한다.
### 수동 기동(대안)
DB·엔진 없이도 UI/로그인/페르소나/세션 생성까지는 동작한다(턴 생성만 불가).
@ -73,7 +88,7 @@ DB·엔진 없이도 UI/로그인/페르소나/세션 생성까지는 동작한
## 1. 사전 준비
- **Python 3.11** (운영 스크립트가 Python 3.11 기준). 가상환경 권장.
- **Node.js**(최신 LTS) + npm — `apps/web`.
- **Node.js 22 + npm** — CI와 같은 기준. newer LTS는 별도 재검증 전까지 기준선으로 쓰지 않는다.
- (선택) **Docker Desktop**`infra/docker-compose.yml` 전체 스택을 띄울 때만.
- (선택) **`claude` CLI** — `ENGINE_MODE=claude_cli`로 실제 턴 생성을 할 때. 설치 후 로그인되어 있어야 한다.
@ -163,7 +178,27 @@ cd D:\workspace\vignette
py -3.11 scripts\materialize-persona-seeds.py --json
```
### 2.4 DB 없이 degraded 기동 (정상 동작)
### 2.4 M2 session digest worker 실행
세션 종료 시 저장되는 fallback digest를 LLM 후보로 압축해 볼 때는 명시 session id runner를 쓴다.
기본은 dry-run이며, DB row를 바꾸려면 `--apply`를 반드시 붙인다. runner는 DB에서 작업을 읽은 뒤
connection을 놓고 engine을 호출하고, accepted 결과만 짧은 DB acquire로 적용한다.
```powershell
cd D:\workspace\vignette
py -3.11 scripts\run-session-digest-worker.py --session-id <session_uuid> --json
# accepted 후보를 실제 session_summary/case_profile에 반영할 때만
py -3.11 scripts\run-session-digest-worker.py --session-id <session_uuid> --apply --json
```
- 출력은 기본적으로 metadata-only다. digest 본문은 민감할 수 있으므로 `--show-digest`를 명시할 때만 출력한다.
- 입력은 client-visible `text_masked` transcript와 open thread만 사용한다. raw `text`, evaluator-only turn, CCD, `end_state`는 압축 prompt에 넣지 않는다.
- API 서버는 `SESSION_DIGEST_WORKER_ENABLED=true`일 때만 세션 종료 뒤 같은 worker를 background task로 실행한다. 기본값은 false다.
- scheduler도 DB load/apply 구간만 connection을 잡고, engine 호출은 DB transaction 밖에서 수행한다.
- 장시간 provider 운영, 임상 골든셋 품질평가, 재압축 정책은 별도 gate다.
### 2.5 DB 없이 degraded 기동 (정상 동작)
DB 연결이 안 되어도 dev에서는 그대로 기동한다. `main.py` lifespan이 풀 초기화 예외를 잡고
`store` 인메모리 폴백으로 degraded 기동하며, 다음 경고를 남긴다.
@ -432,8 +467,8 @@ RAG 임베딩/리랭커 의존성은 기본 이미지에 설치하지 않는다.
```powershell
# 백엔드 (apps/api)
cd apps\api
python -m pytest app/ -q # 현재 178 pass
python -m pytest engine_gateway\ -q # 현재 11 pass
python -m pytest app/ -q # 백엔드 기준선 178 pass
python -m pytest engine_gateway\ -q # 현재 27 pass
# 웹 (apps/web)
cd apps\web

View file

@ -60,7 +60,7 @@
| ID | 갭 | 현재상태 | 권고 | 근거 |
|---|---|---|---|---|
| **H1** | 계약 평가 KPI(자기효능감·기술숙련도·수련만족도 사전사후) 수집·입력·CSV/report 계산 2차 | `app.learner_prepost_measure`, 학습자 본인용 `GET/PUT /users/me/prepost-measures`, `SessionReview`의 파일럿 증거 원장 카드가 3척도 pre/post 1~5 aggregate evidence를 저장·조회한다. `app.services.phase3_kpi_export``scripts/export-phase3-kpi.py`는 원장 row를 Phase 3 evidence root의 `02-measures/prepost_measures.csv``02-measures/kpi_report.json` scaffold로 산출한다. participant id는 가명화하고, 3척도 paired normalized mean pre/post/delta, complete/missing pair를 계산한다. 2차에서는 `phase3_kpi_contract.py`가 KPI metric 이름·필수키·`computed_prepost`/`design_pending` status 값을 소유해 exporter/checker/test의 drift를 줄이고, 계산 가능한 pre/post evidence와 평가설계 전 미계산 KPI를 report 안에서 구분한다. | 공식 문항 확정, 실험/통제군 배정, 추이 시각화, 통계검정 종류/alpha/결측 처리, 실제 20명 evidence와 steward/legal/IAA 검수. 현재 API/UI/export는 공식 효과성·성적·수료 판정이 아니라 파일럿 evidence 계산이다. (κ/ICC·환각률은 doc4 미명시 → 평가설계 확정.) | doc4(20명 실험/통제군·단회기 50분·3척도 pre-post) |
| **H2** | 턴별 fast-loop + 라이브 코칭 1차 가동·학습자 리뷰 2열 UI 1차 완료·골든셋 잔여 | `make_eval_hook`이 submit/voice 생성 경로에 주입되고, stream은 `_evaluate_stream_turn`으로 fast-loop 평가를 붙인다. 결과는 `feedback_scores`, `alternative_utterance` 등 정규화 테이블에 적재·hydrate된다. 추가로 `app/services/live_coach.py`, `POST/GET /sessions/{id}/live-coach`, `POST /kb/live-coach/source-packs/sync`, `app.live_coach_events`, `data/kb/live_coaching_workbook_0615.json`, `data/kb/live_coaching_sources/*.json`을 연결해 워크북·DSM·공식 지침 요약 기반 코칭 아바타 말풍선·근거 모달·발화별 이력 오버레이와 RAG 증분 색인을 제공한다. `app.services.source_pack_sync`가 repo source pack의 active `content_hash`를 비교하고 변경 시 document version을 최신+1로 올린다. live turn은 프로세스 로컬 source pack snapshot을 재사용하지만, 관리자 sync/CLI는 `refresh=True`로 캐시를 비운 뒤 repo 파일을 다시 읽어 stale `content_hash` 비교를 막는다. 학습자 `SessionReview` 데스크톱은 좌측 축어록 타임라인, 우측 요약·감정·흐름·루브릭·강점·개선점·pre/post·워크시트·피드백 작업열의 2열 구조로 재배치했다. | 원천 축어록 few-shot 골든셋 적재, 임상팀 확정 루브릭과 source pack 임상 검수 상태 운영정책 보강. | doc2·doc5(골드 포맷) |
| **H2** | 턴별 fast-loop + 라이브 코칭 1차 가동·학습자 리뷰 3열 workbench 완료·골든셋 잔여 | `make_eval_hook`이 submit/voice 생성 경로에 주입되고, stream은 `_evaluate_stream_turn`으로 fast-loop 평가를 붙인다. 결과는 `feedback_scores`, `alternative_utterance` 등 정규화 테이블에 적재·hydrate된다. 추가로 `app/services/live_coach.py`, `POST/GET /sessions/{id}/live-coach`, `POST /kb/live-coach/source-packs/sync`, `app.live_coach_events`, `data/kb/live_coaching_workbook_0615.json`, `data/kb/live_coaching_sources/*.json`을 연결해 워크북·DSM·공식 지침 요약 기반 코칭 아바타 말풍선·근거 모달·발화별 이력 오버레이와 RAG 증분 색인을 제공한다. `app.services.source_pack_sync`가 repo source pack의 active `content_hash`를 비교하고 변경 시 document version을 최신+1로 올린다. live turn은 프로세스 로컬 source pack snapshot을 재사용하지만, 관리자 sync/CLI는 `refresh=True`로 캐시를 비운 뒤 repo 파일을 다시 읽어 stale `content_hash` 비교를 막는다. 학습자 `SessionReview` 데스크톱은 요약/흐름, 축어록, 평가 rail의 3열 workbench와 하단 워크시트로 재배치했고, empty review는 2열 이하로 유지한다. 워크시트/pre-post/교수자 메모 입력과 발화 이동 버튼은 name/autocomplete/aria-label 및 공유 focus token으로 접근성 회귀 표면을 줄였다. | 원천 축어록 few-shot 골든셋 적재, 임상팀 확정 루브릭과 source pack 임상 검수 상태 운영정책 보강. | doc2·doc5(골드 포맷) |
| **H3** | 임상팀 콘텐츠 입력 경로(페르소나 저작 CRUD) 2차 구현·원문 격리 정책 1차·항목형 목록 저작/프롬프트 검토 UI 완료·임상 검수 잔여 | draft 생성·조회·편집·검수요청 API와 교수 콘솔 JSON 초안 패널은 연결됐다. `persona_repository.py`는 in-code `SEED_PERSONAS`(P1~P3)와 `data/personas/P4.json`~`P7.json``PersonaCard`로 합쳐 `materialize_seed_personas()`와 seed fallback catalog에 포함한다. `scripts/materialize-persona-seeds.py`는 같은 seed manifest를 dry-run 기본으로 보고하고, `--apply`에서만 DB pool을 초기화한 뒤 기존 idempotent DB materializer를 호출한다. `scripts/sync-persona-sources.py`는 DB-backed dry-run/apply runner로 repo-managed source pack의 `content_hash`/document version을 비교한다. RAG 기반 draft 생성은 source/chunk evidence와 함께 `persona-draft-rag@2026-06-28.1` prompt bundle id/version/hash를 engine metadata 및 draft `source_provenance`에 남긴다. `POST /personas/sources`는 raw 원문 hash-only 증거를 `kb.raw_source_artifact`에 따로 기록하고, sanitized 파생본만 evaluator-only RAG chunk로 색인한다. `rag.index_document()``sensitivity=3` 또는 raw marker chunk를 DB 접근 전에 차단한다. `app/persona_read_model.py`는 catalog/review/draft/source/evidence DTO와 mapper를 route에서 분리해 OpenAPI schema 이름을 유지한다. PersonaStudio는 자동사고, 회기 시나리오, 말투 filler/verbal tic/nonverbal cue, 역린·금기 응답·금기어를 행 추가/삭제 UI로 편집하고, 저장 직전 빈 항목을 제거하되 기존 배열 schema를 유지한다. 프롬프트 탭은 raw JSON textarea 대신 L1 카드·인적 범주·임상 배경·말투·수치 파라미터·역린·회기 시나리오·추가 계약 섹션으로 같은 draft 데이터를 검토하게 한다. | 루브릭·이론 콘텐츠 외부화, P4~P7 포함 임상팀 최종 검수/서면 evidence 확보, 암호화 blob/vault 기반 원문 실저장. | doc3(R&R)·doc4(페르소나=전문가 산출물) |
| **H4** | PII 마스킹 한국어 로컬 휴리스틱 + optional ko recognizer adapter + 15-case fixture/schema 평가 harness + 온보딩·동의 게이트 잔여 | Presidio `language='en'` 고정 한계를 보완하기 위해 정규식 폴백에 한국어 날짜·금액·행정구역 주소와 함께 이름/성명 라벨, 성씨+이름+조사/호칭, 대학교·학과·병원·센터 등 기관 suffix 기반 로컬 휴리스틱 마스킹을 추가했다. 66차에서는 `제 이름은 김서연입니다`, `보호자 이름은 박민수입니다`, `저는 최하늘입니다` 같은 자연 발화형 이름 라벨·자기소개 패턴을 추가하고 `이름은 중요하지 않다` negative control로 오탐을 막았다. Presidio가 설치돼도 한국어 누락을 막기 위해 fallback을 후단에 한 번 더 태운다. 70차에서는 `guardrail.mask_pii()` 내부에 선택형 한국어 PII recognizer adapter 경계를 추가했다. adapter는 import-time hard dependency가 아니며 명시 등록 전에는 비활성이고, 실패해도 기존 regex fallback이 마지막 안전망으로 유지된다. fake adapter 테스트는 regex가 못 잡는 별명/기관 span을 `[NAME]`/`[ORG]`로 마스킹하고 같은 문장의 전화번호는 후단 regex가 `[PHONE]`으로 처리하는지, adapter 실패 시에도 fallback이 유지되는지 검증한다. 상담 생성(generate/stream), fast evaluator prompt, client turn `text_masked`에서 한국어 NAME/ORG raw 값이 남지 않도록 회귀화했다. `app.services.pii_masking_eval`, `data/privacy/pii-masking-ko-fixtures.json`, `scripts/evaluate-pii-masking.py`로 합성 fixture 15케이스를 NAME/ORG/PHONE/EMAIL/RRN/NUMID/DATE/MONEY/ADDR/negative-control 범위에서 entity recall·forbidden substring removal·unexpected entity violation으로 평가한다. `data/privacy/pii-masking-eval-input.schema.json``data/privacy/pii-masking-eval-report.schema.json`은 source/category/severity metadata와 summary-only `technical_dry_run` 리포트 계약을 고정하며, 기본 CLI JSON은 `masked_text`/`forbidden_remaining` 원문 증거를 제외한다. `소속`/`안내`/`이름` NAME 오탐도 stopword로 보정했다. 외부 LLM 호출은 상담 생성(generate/stream)·fast/deep 평가 직후 `audit.llm_call_log`에 provider/model/token/cost/inference_geo/latency만 적재하도록 연결했고, prompt/completion 본문은 저장하지 않는다. 로컬 dev-login 실제 `/turn` smoke에서 `audit.llm_call_log` 3행 증가를 확인했다. `app_user`에 이름·소속·학과·학년/직위·연락처·주소/수령지·닉네임·자기소개·아바타 URL·약관/개인정보 동의 버전 필드를 추가했고, 로그인 직후 `/onboarding` 완료 전에는 역할 홈과 learner 회기 시작을 막는다. 아바타 이미지는 `/users/me/avatar`에서 MIME/시그니처/3MB 제한 후 파일 저장소에 두고 URL만 보관한다. 온보딩 저장 시 learner `consent_at`도 함께 세팅하며 auth E2E에서 신규 계정 온보딩→아바타 업로드→학습자 홈 이동을 검증했다. | 실제 ko recognizer 모델/provider 선정, 운영 말뭉치 기반 오탐/미탐 평가, 미성년/guardian 및 법무 검토가 필요한 최종 서명 동의서·개인정보 처리방침·약관 evidence 확보. 공개 Google OAuth 실제 `/turn` proof는 별도 운영 게이트. | doc1/2/5(실명·날짜·미성년·자살시도 다수)·doc4(IRB·개인정보) |
@ -69,11 +69,11 @@
| ID | 갭 | 현재상태 | 권고 |
|---|---|---|---|
| **M1** | 비언어/준언어 임상 이벤트 캡처·태깅 4차 진행 | 1차에서 `audio_ref`/`silence_ms`/`speech_rate`/`barge_in`을 learner voice turn에 저장하고 리뷰 `nonverbal` 칩으로 파생했다. 2차에서는 `app.turns.provider_events JSONB``TurnRecord.provider_events`를 추가해 WebSocket control/STT provider 이벤트를 allowlist·size limit 후 보존한다. 3차에서는 저장 전 sanitizer에서 내부 taxonomy `event_type`/`category`를 붙인다. 4차에서는 인증된 회기 리뷰 API가 `sigh`/`cry`/`laugh`/`breath`, prosody, background noise 계열만 한글 label/detail 칩으로 파생 노출한다. raw transcript/text payload, provider/source/raw type, 공개 공유 카드 노출은 제외한다. | 실제 provider 기반 한숨·울음·억양 감지 연결, 장시간 마이크/WSS 실측, 리뷰 칩을 역량 지표로 해석할지에 대한 정책. |
| **M2** | 다회기 종단 케이스 아크·교차회기 사례개념화 8차 구동 | `(persona_id, learner_id)` 안정 `case_profile` upsert, 원자적 `session_no`, voice/REST/SSE recall cache 주입을 연결했다. 세션 종료 시 마스킹 축어록 기반 fallback `session_summary.digest`, `case_profile.case_digest`, `rapport_trajectory`, `alliance_level`을 갱신하고, 다음 회기 seed recall은 `case_digest`·직전 `session_summary`·client-visible non-contradicted `pinned_fact`를 함께 조립한다. 3차에서는 마스킹된 client-visible 발화에서 `[NAME]`/`[ORG]` identity와 명시적 상담 약속만 보수적으로 `pinned_fact`에 upsert한다. 4차에서는 pinned fact 삽입 또는 값 변경 시 `pinned_fact_history`에 append-only 이력을 남긴다. 5차에서는 명시적 상담 약속 철회/부정만 기존 non-locked `agreement:counseling` fact를 `contradicted`로 격리하고 history reason `contradiction`을 남긴다. 6차에서는 세션 종료 저장 성공 뒤 마스킹된 client-visible 내담자 발화만 `app.turn_embedding`에 BGE-M3 dense/sparse로 idempotent 색인한다. 7차에서는 REST submit/SSE stream의 턴 컨텍스트 조립 경계를 `_prepare_turn_context()`로 묶고, DB seed recall과 pinned fact가 다음 턴 EngineMessage L2/L4에 raw 이름 마스킹 상태로 주입되는 route-level 회귀를 추가했다. 8차에서는 `SessionDigestInput`/`SessionDigestResult`/`SessionSummaryWrite`로 종료 digest 입력·fallback 결과·DB write 인자 경계를 명시해 future LLM worker가 raw text, evaluator-only turn, 평가 payload, CCD, deterministic carry를 압축 prompt에 우회 주입하지 못하게 했다. `digest_pending`은 CompressionJob 생성 여부를 알리는 비동기 압축 필요 신호로 유지한다. 같은 값 재확인은 history를 늘리지 않고, `locked` fact는 건드리지 않는다. 관계갈등·위기·임상 추론은 자동 pinning/모순 처리에서 제외한다. | 관계·임상 fact 승격 기준, LLM digest worker 실행/품질평가/재압축, 접수면접→다회기 연속성·자기개념 진화 실증. |
| **M2** | 다회기 종단 케이스 아크·교차회기 사례개념화 11차 구동 | `(persona_id, learner_id)` 안정 `case_profile` upsert, 원자적 `session_no`, voice/REST/SSE recall cache 주입을 연결했다. 세션 종료 시 마스킹 축어록 기반 fallback `session_summary.digest`, `case_profile.case_digest`, `rapport_trajectory`, `alliance_level`을 갱신하고, 다음 회기 seed recall은 `case_digest`·직전 `session_summary`·client-visible non-contradicted `pinned_fact`를 함께 조립한다. 3차에서는 마스킹된 client-visible 발화에서 `[NAME]`/`[ORG]` identity와 명시적 상담 약속만 보수적으로 `pinned_fact`에 upsert한다. 4차에서는 pinned fact 삽입 또는 값 변경 시 `pinned_fact_history`에 append-only 이력을 남긴다. 5차에서는 명시적 상담 약속 철회/부정만 기존 non-locked `agreement:counseling` fact를 `contradicted`로 격리하고 history reason `contradiction`을 남긴다. 6차에서는 세션 종료 저장 성공 뒤 마스킹된 client-visible 내담자 발화만 `app.turn_embedding`에 BGE-M3 dense/sparse로 idempotent 색인한다. 7차에서는 REST submit/SSE stream의 턴 컨텍스트 조립 경계를 `_prepare_turn_context()`로 묶고, DB seed recall과 pinned fact가 다음 턴 EngineMessage L2/L4에 raw 이름 마스킹 상태로 주입되는 route-level 회귀를 추가했다. 8차에서는 `SessionDigestInput`/`SessionDigestResult`/`SessionSummaryWrite`로 종료 digest 입력·fallback 결과·DB write 인자 경계를 명시해 LLM worker 후보가 raw text, evaluator-only turn, 평가 payload, CCD, deterministic carry를 압축 prompt에 우회 주입하지 못하게 했다. 9차에서는 `DigestQualityAssessment`/`SessionDigestWorkerOutcome`로 local quality harness를 추가해 빈/짧은 digest, raw forbidden substring, 내부 평가·CCD·상태 marker, 잘못된 `S{session_no}:` prefix를 fallback 유지 대상으로 판정한다. 10차에서는 `session_digest_worker.py``CompressionJob`을 Node-compatible `GenerateRequest`/`EngineMessage`로 변환하고, 주입형 engine/audit 호출 뒤 quality gate 통과 결과만 `session_summary.digest/compressed_by/token_count``case_profile.case_digest`에 적용하는 one-shot 경계를 소유한다. 11차에서는 `scripts/run-session-digest-worker.py` dry-run/apply runner와 `SESSION_DIGEST_WORKER_ENABLED=false` 기본값의 세션 종료 background scheduler 골격을 붙였다. scheduler는 DB load/apply 때만 connection을 잡고 engine 호출은 transaction 밖에서 수행하며, `compressed_by IS NULL` loader/apply CAS로 재실행 race를 막는다. DB loader는 persisted fallback summary와 client-visible `text_masked` transcript만 재구성하며 raw `text`, evaluator-only turn, CCD, end_state는 prompt에 넣지 않는다. `digest_pending`은 CompressionJob 생성 여부를 알리는 비동기 압축 필요 신호로 유지한다. 같은 값 재확인은 history를 늘리지 않고, `locked` fact는 건드리지 않는다. 관계갈등·위기·임상 추론은 자동 pinning/모순 처리에서 제외한다. | 관계·임상 fact 승격 기준, 실 provider 장시간 운영, 임상 골든셋 품질평가, 재압축, 접수면접→다회기 연속성·자기개념 진화 실증. |
| **M3** | SSO claim 매핑·식별자 안정성 1차 완료·운영 IdP 감사 미연결 | Google/SAML/dev-login이 `AUTH_EMAIL_COHORT_MAP`·`AUTH_DOMAIN_COHORT_MAP` 및 SAML cohort claim을 `cohort_ids`로 전달하고, `app_user.external_id`는 provider subject(`google:`/`saml:`/`dev:`) 기반으로 저장한다. 운영 SAML 서명검증, 기관 claim schema/test tenant, deprovisioning audit은 아직 없다. | 한신 IdP 확정 후 SAML 서명검증, claim→role/cohort/institution_user_id 매핑 표 실연동, role변경/삭제 audit, deprovisioning evidence. |
| **X1** | 재귀학습·데이터셋 export 파이프라인 1차 구현 | `scripts/export-recursive-dataset.py``app.services.dataset_export`로 masked-text JSONL dry-run, PII scan, kappa/ICC 계산, approved export 게이트를 구현했다. 기본은 `technical_dry_run`이며 실제 승인 export·골든셋 승격은 데이터 steward/legal review와 IAA 통과가 필요하다. | 파일럿 evidence에서 reviewer disposition, steward/legal 승인, gold annotation 라운드 적재 후 `approved_for_recursive_learning_seed` 승격 검증. |
| **X2** | AI API 비용 관측·예산 경고·평가 저비용 라우팅·evaluator cache 관측·일별 비용 추이·모델별 비용 검증 리포트 2차 완료 | 턴별 provider/model/tokens/cost 저장 경로와 `GET /admin/usage`, 관리자 비용 대시보드를 연결했다. `ADMIN_USAGE_BUDGET_USD` 기준 예산 상태(ok/warn/exceeded)도 응답/UI에 표시한다. `EVALUATOR_FAST_MODEL`/`EVALUATOR_DEEP_MODEL` 설정 시 fast/deep 평가 호출만 해당 모델 override로 gateway에 전달하고, 비워두면 기존 gateway default 라우팅을 유지한다. fast/deep evaluator structured 결과는 canonical request SHA-256 기반 인메모리 semantic cache로 재사용하며, 원문 prompt·completion은 저장하지 않고 성공 파싱 결과만 TTL/entry 제한 안에서 캐시한다. `/admin/usage`와 관리자 비용 카드가 cache enabled/entries/hits/misses/stores/evictions/requests/hit_rate와 일별 `daily_cost` 추이를 노출한다. `app.services.usage_report``scripts/report-ai-usage.py`는 같은 usage JSON에서 provider/model별 cost share, token share, cost/turn, cost/1k tokens, metered coverage, budget/cache warning을 산출한다. DB 미가용 dev는 runtime store fallback, prod는 fail-closed다. | 자동 차단·한도 enforcement 정책. |
| **L1** | 기술스택 신청서-구현 불일치 및 단기일정 산출물 압박 | doc4 신청서 스택(Spring Boot 3/Node.js·TimescaleDB) vs 실제 FastAPI/Python 불일치, 20주 단기일정·9월 저작권 등재 압박. 소유자 결정으로 장기 교체 대상은 Node.js 우선, 현재 FastAPI 전면 재작성은 보류했다. 내부 전환 증거로 engine gateway 공유 계약, `EngineClient.stream_packets()` decode 경계, schema-backed golden fixture(`engine_gateway_contract.v1.json`/`engine_gateway_schema.v1.json`), Python import 없는 `scripts/check-engine-gateway-contract.mjs` Node.js conformance runner, 브라우저-facing 세션 read-model 분리(`app/session_read_model.py`), 페르소나 DTO/mapper 분리(`app/persona_read_model.py`), stage 라벨/phase-key 정규화 SSOT(`app/stage_contract.py`)까지 고정했다. | 신청서/저작권 등재 문서에 FastAPI 유지 사유와 계약 우선 Node 전환 계획을 반영하는 외부 거버넌스 증거. 내부 후보였던 H3 항목형 목록 저작 UI와 프롬프트 미리보기 de-JSON은 10차에서 완료. |
| **L1** | 기술스택 신청서-구현 불일치 및 단기일정 산출물 압박 | doc4 신청서 스택(Spring Boot 3/Node.js·TimescaleDB) vs 실제 FastAPI/Python 불일치, 20주 단기일정·9월 저작권 등재 압박. 소유자 결정으로 장기 교체 대상은 Node.js 우선, 현재 FastAPI 전면 재작성은 보류했다. 내부 전환 증거로 engine gateway 공유 계약, `EngineClient.stream_packets()` decode 경계, schema-backed golden fixture(`engine_gateway_contract.v1.json`/`engine_gateway_schema.v1.json`), Python import 없는 `scripts/check-engine-gateway-contract.mjs` Node.js conformance runner, `gateway-default` default-routing sentinel 정규화, `structured_payload_from_response()` 기반 structured/legacy JSON response parser, 브라우저-facing 세션 read-model 분리(`app/session_read_model.py`), 페르소나 DTO/mapper 분리(`app/persona_read_model.py`), 페르소나 draft generation 계약(`app/persona_generation_contract.py`), session evaluation write packet(`SessionEvaluationWrite`), stage 라벨/phase-key 정규화 SSOT(`app/stage_contract.py`)까지 고정했다. | 신청서/저작권 등재 문서에 FastAPI 유지 사유와 계약 우선 Node 전환 계획을 반영하는 외부 거버넌스 증거. 내부 후보였던 H3 항목형 목록 저작 UI와 프롬프트 미리보기 de-JSON은 10차에서 완료. |
> X2 근거: doc3 회의록이 'AI API 비용'을 운영 리스크로 명시.
@ -88,9 +88,10 @@
- **C1 4차 완료**: `caseWorksheet` 응답 구조, 리뷰 화면 편집 UI, `app.case_worksheet` 저장/재조회 경로, 외부 루브릭 schema/loader/validation scaffold, 교수자 수동 워크시트 검수 상태 저장/표시. 워크시트 템플릿 key source는 `CASE_WORKSHEET_SECTION_SPECS`/`case_worksheet_template_item_keys()`로 명시했다. 후속은 임상팀 확정 루브릭 콘텐츠, AI 채점 보정, 승인 후 잠금·재제출 정책.
- **C2 1차 완료**: 위기 신호는 엔진 전 중단, 109 안내, `safety_events` 적재, 교수자 알림 큐, `ideation_observed` 전달까지 배선했다. 후속은 임상 스크립트·서약 문안·감점 루브릭.
- **C3 2차 완료**: `theory_mode`가 세션·평가·생성 프롬프트까지 흐르고, 학습자는 세션 시작 전 기존 3개 모드 중 하나를 명시 선택해 `POST /sessions`로 보낸다. 후속은 임상팀 CBT 체인·이론부합 루브릭.
- **H2 1차+라이브 코칭+리뷰 2열 UI 완료**: `make_eval_hook`과 stream 평가가 턴 파이프라인에 붙고 정규화 테이블로 적재·복원된다. 대안발화도 `app.alternative_utterance`로 정규화한다. 라이브 코칭은 0615 워크북·DSM·공식 지침 요약/RAG 근거로 코칭 아바타 말풍선·근거 모달·발화별 이력 오버레이까지 연결했다. `POST /kb/live-coach/source-packs/sync`는 공용 source pack sync service를 통해 evaluator 전용 RAG에 증분 색인하고, hash 변경 시 document version을 최신+1로 올린다. 관리자 sync/CLI는 source pack manifest 생성 전 process-local cache를 refresh해 현재 repo JSON 기준으로 비교한다. 학습자 `SessionReview` 데스크톱은 좌측 축어록 타임라인과 우측 작업열의 2열 구조로 재배치했고, 교수자/모바일 레이아웃은 기존 규칙을 유지한다. 후속은 골든셋·임상팀 루브릭·source pack 임상 검수 상태 운영.
- **H2 1차+라이브 코칭+리뷰 3열 workbench 완료**: `make_eval_hook`과 stream 평가가 턴 파이프라인에 붙고 정규화 테이블로 적재·복원된다. 대안발화도 `app.alternative_utterance`로 정규화한다. 라이브 코칭은 0615 워크북·DSM·공식 지침 요약/RAG 근거로 코칭 아바타 말풍선·근거 모달·발화별 이력 오버레이까지 연결했다. `POST /kb/live-coach/source-packs/sync`는 공용 source pack sync service를 통해 evaluator 전용 RAG에 증분 색인하고, hash 변경 시 document version을 최신+1로 올린다. 관리자 sync/CLI는 source pack manifest 생성 전 process-local cache를 refresh해 현재 repo JSON 기준으로 비교한다. 학습자 `SessionReview` 데스크톱은 요약/흐름, 축어록, 평가 rail의 3열 workbench와 하단 워크시트로 재배치했고, 교수자/모바일 레이아웃은 기존 규칙을 유지한다. 후속은 골든셋·임상팀 루브릭·source pack 임상 검수 상태 운영.
- **H3 10차 완료**: 페르소나 저작 CRUD(draft→review), P4~P7 저장소 JSON 로드, RAG source 기반 draft generation, prompt bundle id/version/hash provenance와 seed/version materializer runner가 붙었다. seed runner는 dry-run/JSON manifest를 제공하고, `--apply`에서만 DB pool을 초기화해 기존 materializer를 호출한다. repo-managed source pack runner는 DB-backed dry-run/apply를 제공하며 active `content_hash`가 바뀐 문서만 최신 version+1로 sync한다. `kb.raw_source_artifact` hash-only 레코드와 `rag.index_document()` raw/sensitivity=3 fail-closed guard를 추가해 raw 원문이 `kb.chunk`/embedding/FTS로 들어가지 않게 했고, `app/persona_read_model.py`로 persona DTO/mapper 경계를 분리했다. PersonaStudio의 자동사고·회기 시나리오·말투 목록·역린/금기 목록은 행 추가/삭제 UI로 바꾸고 기존 배열 payload 계약을 유지한다. 이번 패스에서 프롬프트 미리보기 raw JSON textarea를 라벨형 검토 섹션으로 교체했다. 후속은 루브릭·이론 콘텐츠 외부화, 임상팀 최종 검수 evidence, 암호화 blob/vault 기반 원문 실저장.
- **H4(부분)·X1·X2**: 한국어 날짜/금액/주소 및 이름/기관 로컬 휴리스틱 마스킹, 15-case 합성 fixture 평가 harness와 input/report schema(summary-only `technical_dry_run` report), 자연 발화형 이름 라벨·자기소개 마스킹 보강, optional ko recognizer adapter 인터페이스와 fake/failure 회귀, 외부 LLM 호출 metadata-only `audit.llm_call_log` 적재 경로, 학습자 동의 수락/철회/회기 시작 하드게이트 골격은 완료했다. 실제 ko recognizer 모델/provider 선정, 운영 말뭉치 기반 평가, guardian/legal 서명 evidence는 후속이다. dry-run JSONL export·PII scan·IAA 계산 1차도 완료했고 `ds.*` write는 `--write-dataset` 명시 시에만 수행한다. X2 비용 관측·예산 경고, evaluator fast/deep 모델 override, evaluator semantic cache, 운영 hit-rate 관측, 일별 비용 추이, 모델별 비용 검증 리포트는 완료했고 자동 차단·한도 enforcement 정책은 후속. M1은 provider_events 보존 슬롯, 내부 taxonomy, 인증 리뷰용 제한 파생 칩까지 완료했고, M2는 보수적 identity/agreement pinned_fact 자동 실적재, append-only history, 명시적 상담 약속 철회 contradiction, episodic embedding writer, 다음 턴 EngineMessage 주입 회귀까지 완료했다. L1은 Node.js conformance runner와 session/persona read-model 분리까지 완료했다. 실제 provider 기반 한숨·울음 감지는 후속.
- **H3 11차 / L1 계약 보강 완료**: `app/persona_generation_contract.py`가 페르소나 draft structured schema, prompt bundle id/version/hash, `GenerateResponse` payload extraction, generated draft defaults/coercion을 소유한다. `routes/personas.py`는 auth, RAG evidence, engine invocation, provenance assembly, HTTP error mapping을 유지한다. 이어서 evaluator/live-coach의 로컬 structured payload alias를 제거하고 `structured_payload_from_response()`를 직접 호출하게 했으며, `SessionEvaluationWrite``app.session_evaluation` 저장 packet을 소유한다. 최신 L1 검증은 persona contract+review 39 passed, gateway contract 27 passed, backend focused 120 passed, Node conformance OK, `npm run check:api-types`, `npm run typecheck`.
- **H4(부분)·X1·X2**: 한국어 날짜/금액/주소 및 이름/기관 로컬 휴리스틱 마스킹, 15-case 합성 fixture 평가 harness와 input/report schema(summary-only `technical_dry_run` report), 자연 발화형 이름 라벨·자기소개 마스킹 보강, optional ko recognizer adapter 인터페이스와 fake/failure 회귀, 외부 LLM 호출 metadata-only `audit.llm_call_log` 적재 경로, 학습자 동의 수락/철회/회기 시작 하드게이트 골격은 완료했다. 실제 ko recognizer 모델/provider 선정, 운영 말뭉치 기반 평가, guardian/legal 서명 evidence는 후속이다. dry-run JSONL export·PII scan·IAA 계산 1차도 완료했고 `ds.*` write는 `--write-dataset` 명시 시에만 수행한다. X2 비용 관측·예산 경고, evaluator fast/deep 모델 override, evaluator semantic cache, 운영 hit-rate 관측, 일별 비용 추이, 모델별 비용 검증 리포트는 완료했고 자동 차단·한도 enforcement 정책은 후속. M1은 provider_events 보존 슬롯, 내부 taxonomy, 인증 리뷰용 제한 파생 칩까지 완료했고, M2는 보수적 identity/agreement pinned_fact 자동 실적재, append-only history, 명시적 상담 약속 철회 contradiction, episodic embedding writer, 다음 턴 EngineMessage 주입 회귀, digest contract/local quality harness, one-shot worker/runner/default-off scheduler/CAS 경계까지 완료했다. L1은 Node.js conformance runner, `gateway-default` default-routing sentinel 정규화, session/persona read-model 분리까지 완료했다. 실제 provider 기반 한숨·울음 감지는 후속.
### B. 소유자 결정 / 외부(임상팀·기관) 의존
@ -135,8 +136,8 @@ rg -n "materialize_seed_personas|init_pool|close_pool|--apply|--json" scripts/ma
```powershell
# 백엔드 (작업 디렉터리 apps/api)
python -m pytest app/ -q # 현재 118 pass
python -m pytest engine_gateway/ -q # 현재 11 pass
python -m pytest app/ -q # 백엔드 기준선 178 pass
python -m pytest engine_gateway/ -q # 게이트웨이 현재 27 pass
# 프론트 (작업 디렉터리 apps/web)
npm run typecheck
@ -154,13 +155,17 @@ npm run e2e # Playwright — web+api+DB 스택 필요
- OCR 잔재·오탈자 존재(doc1·doc5). 의미는 시각 보정했으나 일부 표현 불확실.
- **doc4 κ/ICC·환각률은 신청서 본문 미명시** — 계약 확정 지표로 단정 금지.
- doc4/doc3 행정 불일치(참여교수 1명 vs 2명, 서식 연도 '2025' 오기, 연구책임자 표기 불일치, 트웬티온스 성명 공란).
- '현재상태'는 grep/코드 사실 기반 적대적 비평 인용에서 시작했으나, **C1·C2·C3·H1 pre/post 원장·KPI export·metric status contract·H2·H3·M1 provider_events 보존/taxonomy/인증 리뷰 파생 칩·M2 case memory/pinned_fact/history/명시철회 contradiction/episodic embedding writer/다음턴 주입회귀/digest contract-only·M3, H4 로컬 마스킹/optional ko adapter/fixture 평가/감사/온보딩 경로, X1 dry-run export, X2 비용 관측·evaluator routing/cache/hit-rate 관측·모델별 비용 리포트, L1 engine gateway contract/golden/schema/Node conformance runner/session/persona read-model 분리는 구현 후 재검증 완료** 기준이다. H1은 파일럿 evidence 계산과 report 계약까지이며, 공식 효과성 판정·통계해석·20명 evidence는 완료로 보지 않는다. M2는 fallback digest`digest_pending` 응답 계약까지이며, LLM digest worker 실행·품질평가·재압축은 완료로 보지 않는다. 다만 H4의 실제 ko recognizer 모델/provider 선정과 운영 corpus 평가, guardian/legal evidence, 공개 OAuth `/turn` proof, M1 실제 provider 기반 한숨·울음 감지는 여전히 게이트로 남아 있다. L1 외부 거버넌스는 FastAPI 유지 사유와 Node 전환 계획의 제출/등재 증거가 남아 있다.
- '현재상태'는 grep/코드 사실 기반 적대적 비평 인용에서 시작했으나, **C1·C2·C3·H1 pre/post 원장·KPI export·metric status contract·H2·H3·M1 provider_events 보존/taxonomy/인증 리뷰 파생 칩·M2 case memory/pinned_fact/history/명시철회 contradiction/episodic embedding writer/다음턴 주입회귀/digest contract/local quality harness/one-shot worker+runner+default-off scheduler boundary·M3, H4 로컬 마스킹/optional ko adapter/fixture 평가/감사/온보딩 경로, X1 dry-run export, X2 비용 관측·evaluator routing/cache/hit-rate 관측·모델별 비용 리포트, L1 engine gateway contract/golden/schema/Node conformance runner/default-routing sentinel/structured payload parser/session read-model/persona read-model/persona generation contract/session evaluation write packet 분리는 구현 후 재검증 완료** 기준이다. H1은 파일럿 evidence 계산과 report 계약까지이며, 공식 효과성 판정·통계해석·20명 evidence는 완료로 보지 않는다. M2는 fallback digest, `digest_pending` 응답 계약, LLM 후보 local quality harness, accepted-only one-shot worker/runner/default-off scheduler 경계까지이며, 실 provider 장시간 운영·임상 골든셋 품질평가·재압축은 완료로 보지 않는다. 다만 H4의 실제 ko recognizer 모델/provider 선정과 운영 corpus 평가, guardian/legal evidence, 공개 OAuth `/turn` proof, M1 실제 provider 기반 한숨·울음 감지는 여전히 게이트로 남아 있다. L1 외부 거버넌스는 FastAPI 유지 사유와 Node 전환 계획의 제출/등재 증거가 남아 있다.
---
### 변경 이력
- 2026-06-28: M2 8차 digest contract-only 경계와 focused 74 passed 검증 기준 반영.
- 2026-06-29: L1 `gateway-default` default-routing sentinel, `structured_payload_from_response()`, persona generation contract, `SessionEvaluationWrite`와 gateway contract 검증 기준 반영.
- 2026-06-29: `scripts/check-dev-dashboard-ssot.py` dashboard SSOT drift gate와 M2 30/87 검증 수치 guard 반영.
- 2026-06-28: M2 8차 digest contract 경계와 focused 74 passed 검증 기준 반영.
- 2026-06-29: M2 9차 local digest quality harness와 `test_session_memory.py` 20 passed 검증 기준 반영.
- 2026-06-29: M2 11차 one-shot digest worker runner/default-off scheduler/CAS boundary와 focused 30 passed 및 주변 회귀 87 passed 검증 기준 반영.
- 2026-06-28: H1 2차 KPI metric status contract와 focused 10 passed 검증 기준 반영.
- 2026-06-28: H4 optional ko recognizer adapter 경계와 focused 47 passed 검증 기준 반영.
- 2026-06-26: 초판. `docs/ops/source-docs-gap-analysis-2026-06-26.md`와 SSOT 대시보드 "원천문서 갭 분석" 섹션을 요약·인덱스화.

View file

@ -15,8 +15,8 @@ Vignette 저장소의 모든 검증 수단(백엔드 단위 테스트, 웹 타
| 검증 | 작업 디렉터리 | 명령 | DB | API(8000) | 웹(5173) | 엔진GW(9099) | 브라우저 | 현재 통과 |
|---|---|---|---|---|---|---|---|---|
| 백엔드 단위 테스트 | `apps/api` | `python -m pytest app/ -q` | 불필요 | 불필요 | 불필요 | 불필요 | 불필요 | 178 pass |
| 엔진 게이트웨이 테스트 | `apps/api` | `python -m pytest engine_gateway/ -q` | 불필요 | 불필요 | 불필요 | 불필요 | 불필요 | 11 pass |
| 백엔드 단위 테스트 | `apps/api` | `python -m pytest app/ -q` | 불필요 | 불필요 | 불필요 | 불필요 | 불필요 | 기준선 178 pass |
| 엔진 게이트웨이 테스트 | `apps/api` | `python -m pytest engine_gateway/ -q` | 불필요 | 불필요 | 불필요 | 불필요 | 불필요 | 현재 27 pass |
| API 타입 생성 체크 | `apps/web` | `npm run check:api-types` | 불필요 | 불필요 | 불필요 | 불필요 | 불필요 | pass |
| 웹 타입체크 | `apps/web` | `npm run typecheck` | 불필요 | 불필요 | 불필요 | 불필요 | 불필요 | pass |
| 웹 빌드 | `apps/web` | `npm run build` | 불필요 | 불필요 | 불필요 | 불필요 | 불필요 | pass |
@ -42,15 +42,15 @@ Vignette 저장소의 모든 검증 수단(백엔드 단위 테스트, 웹 타
```sh
# apps/api
python -m pytest app/ -q # 앱 단위 테스트 (현재 178 pass)
python -m pytest engine_gateway/ -q # 게이트웨이 단위 테스트 (현재 11 pass)
python -m pytest app/ -q # 앱 단위 테스트 기준선 178 pass
python -m pytest engine_gateway/ -q # 게이트웨이 단위 테스트 현재 27 pass
```
수집만 빠르게 확인하려면:
```sh
python -m pytest app/ --collect-only -q # "178 tests collected"
python -m pytest engine_gateway/ --collect-only -q # → "11 tests collected"
python -m pytest app/ --collect-only -q # 기준선: "178 tests collected"
python -m pytest engine_gateway/ --collect-only -q # 현재: "27 tests collected"
```
> 참고: 실행 중 `PendingDeprecationWarning: Please use 'import python_multipart'`
@ -72,6 +72,8 @@ python -m pytest engine_gateway/ --collect-only -q # → "11 tests collected"
| `app/test_persona_review.py` | 페르소나 리뷰 워크플로 |
| `app/test_voice_service.py` | 음성 캐스케이드 서비스(STT/TTS) |
| `app/test_voice_ws.py` | 음성 WebSocket 경계 |
| `app/test_session_digest_worker.py` | M2 session digest worker 요청 계약, accepted-only 적용, raw text 차단, 재실행 방지 |
| `scripts/check-dev-dashboard-ssot.py --json` | `docs/dev_dashboard.html` 상태 카운트·M2 검증 수치·DONE/GATE stale 문구 guard |
### 1.4 `engine_gateway/` 테스트
@ -262,18 +264,19 @@ VITE_API_BASE=http://127.0.0.1:8000 npm run e2e # 프록시 대신 API
- **`@single-run` 직렬 시나리오**: 17 tests (DB 영속화·세션 MVP·음성 성공경로 등)
- `e2e/voice-success.spec.ts`는 직접 `/voice/ws` 캐스케이드와 Session 마이크 UI를 함께 검증하며,
브라우저 `<audio>.play()`가 차단된 조건에서도 Web Audio buffer source 재생이 시작되는지 확인한다.
- 2026-06-27 최종 로컬 풀스택 검증: `PLAYWRIGHT_PORT=5174 npm run e2e` **113 passed**.
- 2026-06-27/28 기준선: `PLAYWRIGHT_PORT=5174 npm run e2e` **113 passed**.
레이아웃·시각 회귀 게이트(핵심 합격선):
| 게이트 | 스펙 | 구성 | 개수 |
|---|---|---|---|
| 세션 레이아웃 | `e2e/session-layout.spec.ts` | 4 테스트 × (desktop+mobile) | **8 / 8** |
| 시각 레이아웃 게이트 | `e2e/layout-visual-gate.spec.ts` | `@single-run`, 7개 화면 × 7개 폭 검사 | **7 / 7** |
| 레이아웃 포커스(재설계 화면) | `session-layout`·`session-review`·`admin`·`learner`·`settings`·`teacher`, `@single-run` 제외 | desktop+mobile 병렬 | **58** |
| 시각 레이아웃 게이트 | `e2e/layout-visual-gate.spec.ts` | `@single-run`, 9개 화면 × 7개 폭 검사 + 다크 테마 assertion | **9 / 9** |
| 레이아웃 포커스(재설계 화면) | `session-layout`·`session-review`·`admin`·`learner`·`settings`·`teacher`, `@single-run` 제외 | desktop+mobile 병렬 | **54** |
> `layout-visual-gate`는 7개 폭(390/720/861/900/1024/1280/1440)에서 가로 오버플로·잘린
> 컨트롤을 검사하고 전체 페이지 스크린샷을 `node_modules/.tmp/layout-gate/`에 남긴다.
> `layout-visual-gate`는 7개 폭(390/720/861/900/1024/1280/1440)에서 9개 핵심 화면의 가로
> 오버플로·잘린 컨트롤·다크 테마 적용을 검사하고 전체 페이지 스크린샷을
> `node_modules/.tmp/layout-gate/`에 남긴다.
> `session-layout`은 회기 전/활성 화면이 뷰포트를 벗어나지 않는지, 우측 패널이 코어 영역을
> 침범하지 않는지, 시작 후 실제 `session_id` URL에서 새로고침해도 활성 회기 상세가 유지되는지,
> 스트림 실패 시 미저장 전사가 남지 않는지를 검증한다.

View file

@ -4,6 +4,8 @@
대시보드(`docs/dev_dashboard.html`)의 상태와 일치한다. 운영 원칙상 가짜 증거로 DONE 표기하지 않으므로,
실증/외부 의존/소유자 결정이 필요한 항목은 여기서 추적한다.
최신 동기화(2026-06-29): M2 다회기 케이스 아크는 `TurnMemory` 값 객체로 턴 메모리 전달 경계를 줄였고, `DigestQualityAssessment`/`SessionDigestWorkerOutcome`로 LLM digest 후보 local quality harness를 추가했다. 이번 라운드에서는 `session_digest_worker.py``CompressionJob`→Node-compatible `GenerateRequest` 변환, 주입형 engine/audit 호출, accepted-only `session_summary`/`case_profile` 적용 계획과 one-shot DB loader 경계를 소유한다. `scripts/run-session-digest-worker.py`는 metadata-only dry-run/apply runner를 제공하고, API는 `SESSION_DIGEST_WORKER_ENABLED=false` 기본값에서 opt-in일 때만 세션 종료 뒤 background worker를 예약한다. loader/apply는 `compressed_by IS NULL` CAS로 이미 압축된 세션 재실행 race를 막고, engine 호출은 DB transaction 밖에서 수행한다. loader는 persisted fallback summary와 client-visible `text_masked` transcript만 재구성하며 raw `text`, evaluator-only turn, CCD, end_state를 압축 prompt에 넣지 않는다. `scripts/check-dev-dashboard-ssot.py`는 대시보드 카드 상태와 M2 30/87 검증 수치, stale DONE/GATE 문구를 guard한다. `recall_summary`/`pinned_facts`/`recent_turns`/`kb_behavior_cues``TurnContext.memory`에 보관되고, `prepare_turn(memory=...)`에서 마스킹된 뒤 `build_turn_messages(memory=...)`로 전달된다. auth managed-user upsert 입력도 `ManagedUserUpsertInput`으로 단일화했지만, 이는 내부 호출 경계 정리이며 외부 API·온보딩 정책·권한 모델 변경은 아니다. Python engine gateway의 `provider/model` 응답 메타 규칙은 helper로 모았고, `GatewayPromptParts`가 current-turn `system_prompt`/`user_payload` 분해 경계를 소유한다. Node conformance runner는 `gateway-default`가 request default-routing sentinel이며 response/done model은 resolved provider model이어야 함을 검증한다. 검증은 `py_compile`, runner `--help`, dashboard SSOT checker PASS, M2 digest worker + memory focused 30 passed, M2 주변 회귀 87 passed, M2 local harness 20 passed, M2/voice/evaluation focused 115 passed, auth/admin/session focused 103 passed, gateway contract 27 passed, backend focused 120 passed, Node conformance OK, `npm run check:api-types`. 단, 실 provider 장시간 운영·임상 골든셋 품질평가·재압축은 계속 후속 GATE다.
분류: **B1 비차단 폴리시** · **B2 환경 제약(증거 생산 불가)** · **B3 소유자 결정** · **B4 외부 거버넌스**
> **B0. 원천문서 갭 분석 (2026-06-26 추가)** — 한신대 산학협력 원천문서 5종 정독으로 도출한 "부족한 부분"(critical 3 / high 4 / medium+ 6)은 **SSOT 대시보드** `docs/dev_dashboard.html` "원천문서 갭 분석" 섹션과 상세 `docs/ops/source-docs-gap-analysis-2026-06-26.md`에서 추적한다. C1 사례개념화 산출물은 저장형 워크시트, 외부 루브릭 scaffold, 교수자 수동 검수 상태 저장까지 4차 구조를 만들었고, C2 위기개입 프로토콜은 1차 구조, C3 이론모드는 2차 명시 선택 UI까지 만들었다. 콘텐츠 정의는 임상팀(구훈정·어유경) 소유라 코드는 구조를 선제 구축하되 임상 문안과 평가기준은 외부 정의로 받는다.
@ -21,26 +23,26 @@
- [x] **회기 아카이브 저장/복원 API** — (2026-06-28 처리) `/learn/history``보관됨`을 실제 학습자별 보기 상태로 연결했다. 종료 회기는 `POST /sessions/{id}/archive`/`restore`로 보관·복원하고, `app.session_archive_state`는 삭제가 아니라 `archived_at`/`updated_at`만 저장한다. `LearnerSessionSummary.archived`, `SessionArchiveResponse`, `LearnerDashboardOverview.archived_sessions`를 OpenAPI에 고정했고, 보관된 회기는 리뷰 대기 행동 큐에서 빠진다. 회기·턴·리뷰·공유 링크·연구/감사 증거는 삭제하지 않는다. **검증: `python -B -m pytest app/ -q` 178 passed, `python -B -m pytest engine_gateway/ -q` 11 passed, `npm run check:api-types`, `npm run typecheck`, `npm run build`, `npx playwright test e2e/learner.spec.ts --project=chromium-desktop --workers=1` 6 passed.**
- [x] **OpenAI TTS voice preset DB map 연결** — (2026-06-28 처리) `app.persona_voice_map`의 OpenAI row를 `/voice/ws` TTS voice 선택에 연결했다. 명시 query preset은 DB map보다 우선하고, 세션 바인딩은 `app.sessions.persona_id/persona_version`으로 voice map을 찾으며, dev persona 생성은 catalog persona id/version map을 사용한다. seed materializer는 기본 OpenAI voice map을 `ON CONFLICT DO NOTHING`으로 생성하고, dev 런타임 스키마 보강은 기존 DB의 `persona_voice_map` 누락도 복구한다. OpenAI가 아닌 provider row는 기존 persona-code fallback으로 안전하게 흡수한다. **검증: `pytest app/test_runtime_policy.py app/test_persona_review.py app/test_voice_service.py app/test_voice_ws.py -q` 75 passed, `pytest app/ -q` 178 passed, `pytest engine_gateway/ -q` 11 passed.**
- [x] **빈상태 컬럼 높이 여백** — (2026-06-28 처리) session-review 0건 상태는 1280px 이상에서 3컬럼 masonry를 쓰지 않고 빈 리뷰 전용 2컬럼 순차 레이아웃으로 전환한다. 가짜 기록·장식 콘텐츠를 넣지 않고, `EMPTY_REVIEW_SESSION_ID` fixture와 `session-review-empty` visual gate로 390/720/861/900/1024/1280/1440 전 폭을 검증한다. session-prestart와 learner-home은 기존 gate 대상에 남겨 함께 무회귀 확인했다. **검증: `npm run typecheck`, `npm run build`, `npx playwright test e2e/layout-visual-gate.spec.ts --project=chromium-single-run --workers=1` 9 passed, `npx playwright test e2e/session-layout.spec.ts e2e/session-review.spec.ts --project=chromium-desktop --workers=1` 6 passed.**
- [ ] **다크 UI v2 시안 실제 적용 라운드** — (2026-06-28 PLAN) `03-session-responsive-v2-dark-unified.png`에서 확인한 단일 dark surface 방향을 실제 코드 UI로 옮긴다. 우선순위는 P1 라이브 상담 세션(`Session.tsx`/`session.css`) → P2 회기 리뷰(`SessionReview.tsx`/`session-review.css`) → P3 학습자 홈(`LearnerHome.tsx`/`learner-home.css`) 순서다. 생성 PNG는 앱에 직접 넣지 않고 레이아웃·밀도·톤 기준으로만 사용한다. 실행 계획과 검증 게이트는 `docs/ops/dark-ui-refresh-implementation-plan-2026-06-28.md`에 고정했다. **완료 조건: 각 화면의 실제 Playwright 캡처가 v2 방향과 맞고, `typecheck`, `build`, 관련 focused E2E, `layout-visual-gate` 통과. DONE 표기는 새 캡처와 테스트 증거 이후에만 가능.**
- [x] **다크 UI v2 시안 실제 적용 라운드** — (2026-06-29 처리) `02/03/04-*-v2-dark-unified.png` 시안을 실제 React/CSS 화면으로 반영했다. P1 라이브 상담 세션은 내담자 무대 발화, 좌측 세션 진행, 우측 라이브 신호/안전 점검을 다크 훈련 화면으로 재구성했다. P2 회기 리뷰는 learner filled 상태를 3열 workbench(요약/축어록/평가 rail + 하단 워크시트)로 바꾸고 empty review는 sparse third column 없이 유지했다. P3 학습자 홈은 대시보드에서 연습 대상 레일을 제거하고, 진행 회기/리뷰 대기/최근 평가/라포 흐름을 상단 CTA 바로 올렸으며, 오늘 이어갈 회기·AI 코치·다음 연습 추천·최근 피드백을 하나의 `lh-work-cluster` 작업 묶음으로 재배치했다. 오른쪽 레일은 최근 기록, 리뷰 대기, 반복 대상만 담당한다. 생성 PNG는 앱에 직접 넣지 않고 레이아웃·밀도·톤 기준으로만 사용했다. 새 실제 렌더 캡처: `docs/design-verification/full-pages/02-learner-home-{desktop,tablet,mobile}.png`, `03-session-active-{desktop,tablet,mobile}.png`, `04-session-review-{desktop,tablet,mobile}.png`, `04-session-review-empty-{desktop,tablet,mobile}.png`. 실행 기록은 `docs/ops/dark-ui-refresh-implementation-plan-2026-06-28.md`에 고정했다. **최신 검증: `npm run typecheck`, `npm run build`, `npx playwright test e2e/layout-visual-gate.spec.ts --project=chromium-single-run --workers=1` 9 passed.**
- [x] **운영 티켓·헬스 샘플 이력 저장 모델 1차**`/admin` 운영 홈과 `/admin/tickets``app.admin_health_event`·`app.support_ticket` 실제 DB 모델에 연결됐다. `/users/support-tickets`는 인증 사용자의 문제·불만·장애 접수를 저장하고, `/admin/tickets`는 미해결 큐·우선순위·상태 변경을 처리한다. `/admin/uptime`은 상시 SLA가 아니라 관리자 콘솔이 관측한 헬스 샘플 정상률/중단 이력을 반환한다. 가짜 티켓 수·가짜 다운타임 숫자는 계속 금지. **검증: `python -m pytest app/test_admin_ops.py -q` 3 passed, `npm run generate:api-types`, `npm run typecheck`.**
- [x] **사용자별 운영 티켓 조회 UI** — (2026-06-28 처리) 학습자 Settings에 `지원 요청` 섹션을 추가해 본인이 접수한 티켓의 상태·카테고리·우선순위·담당 그룹·해결 메모를 읽기 전용으로 확인한다. `GET /users/support-tickets``reporter_id = current user` 기준으로만 조회하고, 본문과 신고자 이메일/이름은 응답·감사 검증에서 제외한다. **검증: `python -B -m pytest app/test_user_support_tickets.py app/test_admin_ops.py app/test_runtime_policy.py -q` 29 passed, `npm run generate:api-types`, `npm run check:api-types`, `npm run typecheck`, `npm run build`, `npx playwright test e2e/settings.spec.ts --project=chromium-desktop --workers=1` 7 passed, `npx playwright test e2e/admin.spec.ts --project=chromium-desktop --workers=1 --grep "operation tickets"` 1 passed.**
- [x] **상시 synthetic monitor/cron 샘플러 1차** — (2026-06-28 처리) `/admin/health`와 같은 헬스 계산을 `record_admin_health_sample()`로 공통화하고, 브라우저 세션 없이 `scripts/record-admin-health-sample.py``app.admin_health_event`에 서비스별 샘플을 append한다. Windows 예약 실행은 `scripts/install-health-sampler-task.ps1``VignetteAdminHealthSampler` Scheduled Task로 설치한다. 이 값은 SLA가 아니라 synthetic sampler가 관측한 최근 샘플 이력이다. **검증: `python -B -m py_compile apps/api/app/routes/admin.py apps/api/app/test_admin_ops.py scripts/record-admin-health-sample.py`, `python scripts/record-admin-health-sample.py --help`, `powershell -NoProfile -ExecutionPolicy Bypass -File scripts\install-health-sampler-task.ps1 -PrintOnly -IntervalMinutes 7`, `python -B -m pytest app/test_admin_ops.py app/test_runtime_policy.py app/test_user_support_tickets.py -q` 31 passed, 로컬 one-shot `recorded_count=5`, `status=ok`, `engine_mode=claude_cli`.**
- [x] **헬스 이벤트 retention/rollup 실행 경로 1차** — (2026-06-28 처리) raw `app.admin_health_event`와 일별 `app.admin_health_daily_rollup`을 분리했다. `scripts/maintain-admin-health-events.py``--rollup-days``--retention-days`를 명시해야 하며 기본은 dry-run이다. `--apply`는 rollup을 먼저 upsert한 뒤 retention window 밖 raw 샘플만 삭제하고, non-dev apply는 `--allow-non-dev-apply` 없이는 차단한다. `/admin/uptime` 요약은 raw 이벤트와 daily rollup을 중복 없이 합산하고, 상세 `events` 목록은 최근 raw 샘플만 유지한다. **검증: `python -B -m py_compile apps/api/app/db.py apps/api/app/auth_sessions.py apps/api/app/routes/admin.py apps/api/app/services/admin_health_maintenance.py scripts/maintain-admin-health-events.py`, `python scripts/maintain-admin-health-events.py --help`, `python -B -m pytest app/test_admin_health_maintenance.py app/test_admin_ops.py app/test_runtime_policy.py app/test_user_support_tickets.py -q` 36 passed, 로컬 dry-run `rollup_event_count=0`, `prunable_event_count=0`.**
- [ ] **운영 티켓·가용성 정책 결정** — 1차 DB 큐, 사용자별 조회 UI, synthetic sampler, retention/rollup 실행 경로는 연결됐지만 raw/rollup 보존 기간의 운영 기본값, 담당 그룹 자동 배정, 우선순위 escalation은 아직 운영 정책 결정이 필요하다. `/admin/uptime`과 sampler 값은 계속 SLA가 아니라 관측 샘플 이력이다.
- [x] **운영 티켓 필터·카테고리 큐·수동 처리 감사 1차** — (2026-06-28 처리) `/admin/tickets`가 status/category/priority/assigned_group/source_path/stale/search 필터를 서버 query로 처리하고, 운영 콘솔은 검색·상태·카테고리·우선순위·담당 그룹·정체 필터와 카테고리 quick queue를 제공한다. 티켓 접수와 관리자 수동 상태/우선순위/담당그룹/해결메모 변경은 `audit.audit_log`에 metadata-only(`support_ticket_create`, `support_ticket_update`)로 남기며, 제목/본문 전문은 감사 로그에 복제하지 않는다. **검증: `pytest app/test_admin_ops.py app/test_runtime_policy.py -q` 28 passed, `npm run generate:api-types`, `npm run check:api-types`, `npm run typecheck`.**
- [x] **운영 티켓 중복 저장·수동 연결 1차** — (2026-06-28 처리) `app.support_ticket`에 결정론적 `fingerprint``parent_ticket_id`를 추가했다. 사용자 티켓 생성은 fingerprint를 저장하지만 자동 병합·자동 담당그룹·자동 우선순위 변경은 하지 않는다. `/admin/tickets`는 같은 fingerprint의 중복 후보 수와 oldest parent 후보를 반환하고, 관리자는 기존 `PATCH /admin/tickets/{id}`로 parent 연결/해제를 수동 처리한다. self-parent와 parent cycle은 서버가 거부하며, 변경 감사는 `support_ticket_update` metadata-only로 남기고 subject/body 전문은 복제하지 않는다. Settings의 사용자별 티켓 조회에는 fingerprint/parent를 노출하지 않는다. **검증: `python -B -m py_compile app/auth_sessions.py app/routes/admin.py app/routes/users.py app/services/support_tickets.py`, `python -B -m pytest app/test_admin_health_maintenance.py app/test_admin_ops.py app/test_runtime_policy.py app/test_user_support_tickets.py -q` 38 passed, `npm run generate:api-types`, `npm run check:api-types`, `npm run typecheck`, `npm run build`, `npx playwright test e2e/admin.spec.ts --project=chromium-desktop --workers=1 --grep "operation tickets|duplicate"` 2 passed.**
- [ ] **운영 티켓 자동 분류·처리 후속** — Claude Recipe headless 자동 수정 후보, 관리자 승인 후 이슈 등록·PR/작업 스레드 생성, 자동 담당 그룹 배정, 우선순위 escalation, 처리 결과 audit trail 확장은 아직 설계/승인 필요. 자동 수정은 운영자 승인 전까지 실행하지 않는다.
- [ ] **운영 티켓 자동 분류·처리 후속** — Claude Recipe headless 자동 수정 후보, 관리자 승인 후 이슈 등록·PR/작업 스레드 생성, 처리 결과 audit trail 확장은 아직 설계/승인 필요. 담당 그룹 자동 배정·우선순위 escalation·raw/rollup 보존기간 같은 운영 정책은 B3 결정 항목에서 먼저 닫아야 한다. 자동 수정은 운영자 승인 전까지 실행하지 않는다.
- [x] **learner-home 로딩 스켈레톤 밀도(390)** — (2026-06-26 처리) '연습 대상' 로딩 스켈레톤을 빈 div 단일 셰이머에서 실제 카드 구조(마크 박스 + 이름/메타/요약 2줄 플레이스홀더)를 모사하도록 밀도 보강. `LearnerHome.tsx` (`.lh-skel__box`/`.lh-skel__line*`), reduced-motion 분기 동반 갱신. **검증: `npm run typecheck` PASS + `vite build` PASS.**
검증 기준: 변경 후 `npm run typecheck`, `e2e/layout-visual-gate.spec.ts`(현재 9/9), 레이아웃 포커스 E2E(54), `e2e/session-layout.spec.ts`(8/8) 무회귀. 이 후속 라운드는 빈상태 레이아웃(`typecheck`, `build`, layout gate 9/9, session-layout+session-review desktop 6/6), 사용자별 티켓 조회 UI(backend 29, settings 7, admin ticket 1), synthetic health sampler(backend 31, one-shot recorded_count 5), health retention/rollup(backend 36, dry-run OK), 티켓 중복 저장·수동 연결(backend 38, API types/typecheck/build OK, admin tickets E2E 2), M2 route-level seed recall 주입(py_compile, M2 focused 68)을 확인했다.
검증 기준: 변경 후 `npm run typecheck`, `e2e/layout-visual-gate.spec.ts`(현재 9/9), 레이아웃 포커스 E2E(54), `e2e/session-layout.spec.ts`(8/8) 무회귀. 이 후속 라운드는 빈상태 레이아웃(`typecheck`, `build`, layout gate 9/9, session-layout+session-review desktop 6/6), 사용자별 티켓 조회 UI(backend 29, settings 7, admin ticket 1), synthetic health sampler(backend 31, one-shot recorded_count 5), health retention/rollup(backend 36, dry-run OK), 티켓 중복 저장·수동 연결(backend 38, API types/typecheck/build OK, admin tickets E2E 2), M2 route-level seed recall 주입(py_compile, M2 focused 68)을 확인했다. 최신 다크 UI v2 라운드는 `learner.spec.ts`+`session-review.spec.ts` 9 passed, `session-layout.spec.ts` 8 passed, `layout-visual-gate.spec.ts` 9 passed로 별도 기록한다.
---
## B2. 환경 제약 — 이 워크스테이션에서 증거 생산 불가
- [ ] **`vnet.18ka.net` 공개 live 전환** — 코드/스크립트는 `vnet.18ka.net`·`api-vnet.18ka.net`을 허용하도록 갱신 완료. Tailnet 주소 `https://alpaca-home.taile93291.ts.net`은 dev-login까지 검증됐고, public OAuth redirect가 로컬/Tailnet 세션으로 붙지 않는 혼선을 막기 위해 Tailnet Google 버튼과 직접 Google 시작 URL은 `local_oauth_unavailable`로 차단한다. 공개 vnet은 현재 (1) `vnet.18ka.net`/`api-vnet.18ka.net` DNS 부재, (2) cloudflared 현재 인증 컨텍스트가 `18ka.net` zone이 아니라 `chanpaca.net` zone에 route dns를 생성, (3) Google Console redirect URI `https://api-vnet.18ka.net/auth/callback` 등록 필요 때문에 live 완료 아님. 현재 prod API 8001 자체는 `db:true`, `engine:true`로 건강하다. 상세: `docs/ops/tailscale-vnet-runtime-2026-06-27.md`.
- [ ] **실배포 `infra/.env` owner-secret fill-in + real preflight** — compose 템플릿과 더미 env smoke는 통과했지만 실제 배포지는 소유자 비밀값이 필요하다. 필수값: `APP_DB_PASSWORD`, `OAUTH_GOOGLE_CLIENT_ID`, `OAUTH_GOOGLE_CLIENT_SECRET`, `OPENAI_API_KEY`, `SESSION_SECRET`, production-safe engine/voice flags. 현재 로컬 stray 값(`ENGINE_MODE=claude_p`, prod sample TTS flag 등)은 운영값으로 복사하면 안 된다. 실제 secret 주입 후 `scripts/check-deploy-preflight.py` DB 포함 모드와 배포지 health를 별도 증거로 닫는다.
- [ ] **공개 Google OAuth 실제 `/turn` proof** — 로그인 가능한 계정으로 `storageState` 캡처 후 `E2E_PUBLIC_AUTH=1` + `chromium-public-auth` 1회 통과 필요. (소유자 지시로 이번엔 보류.)
- [ ] **음성 캐스케이드 live** — (2026-06-28 갱신) 로컬 API에서 실제 OpenAI STT/TTS 경로는 live WS smoke로 통과했다(P2 세션, MP3 발화 → transcript 52자 → reply 105자 → TTS binary 66 chunks/266,496 bytes). Session UI는 Web Audio unlock 재생으로 브라우저 autoplay 차단을 회피하도록 보강했고 synthetic mic E2E도 통과했다. 남은 범위는 물리 마이크, 공개 WSS, 50분 양방향 장시간 실측이다.
- [ ] **음성 캐스케이드 live** — (2026-06-29 갱신) 로컬 API에서 실제 OpenAI STT/TTS 경로는 live WS smoke로 통과했다(P2 세션, MP3 발화 → transcript 52자 → reply 105자 → TTS binary 66 chunks/266,496 bytes). Session UI는 Web Audio unlock 재생과 AudioWorklet-first PCM16 capture로 보강했고, backend는 `format=pcm` raw chunk를 STT 업로드 직전 WAV로 정규화한다. `/voice/ws`는 provider-neutral `stt_result` control을 받아 EOT ready/pending을 route에서 판단한다. synthetic mic E2E는 AudioWorklet 경로로 통과했다. 남은 범위는 실제 Deepgram interim/final WSS, 물리 마이크, 공개 WSS, 50분 양방향 장시간 실측이다.
- [ ] **claude_cli ↔ Messages API 폴백 동일성**`ANTHROPIC_API_KEY`가 있어야 Messages API 경로를 돌려 동일성 검증 가능. claude_cli 경로는 게이트웨이 probe로 live 실측 완료.
- [x] **저항엔진 openness 곡선 DB 실증** — (2026-06-28 처리) `scripts/smoke-resistance-openness-db.py`로 실제 API 경로(dev-login→onboarding→P1 세션 2개→각 5턴)를 실행하고 DB `app.session_state`/`app.turns`/`app.turn_client_state`를 직접 조회했다. 공감 5턴은 API curve `0.0→0.0→0.095→0.133→0.171`, DB final `stage=탐색`, `effective_openness=0.17100003361701965`; 조언점프 5턴은 API curve 전부 `0.0`, DB final `stage=라포`, `effective_openness=0.0`, `resistance=0.9580000042915344`. 증거: `docs/ops/resistance-openness-db-smoke-2026-06-28.json`, `docs/ops/resistance-openness-db-smoke-2026-06-28.md`. **검증: `python -B -m py_compile scripts/smoke-resistance-openness-db.py`, `pytest app/test_state_machine_resistance.py app/test_evaluation_persistence.py -q` 9 passed, live smoke PASS.**
- [ ] **재부팅 후 watchdog smoke**`watch-public-runtime.ps1` + Scheduled Task가 재부팅 후 엔진/API/터널을 복구하고 public `/turn`이 통과하는지 실측. 재부팅 불가로 미실행(parser/ check-only 경로는 확인됨). 2026-06-27 점검에서 아직 DNS가 없는 `api-vnet.18ka.net`은 기본 watchdog 검사에서 제외했고, 개통 후 `-AdditionalPublicHealthUrls`로 명시 추가하도록 runbook을 보강했다.
@ -52,13 +54,16 @@
> 결정되면 대시보드 `결정 필요` 탭과 관련 Phase 항목을 갱신한다.
- [ ] **운영 티켓·가용성 정책 결정** — 1차 DB 큐, 사용자별 조회 UI, synthetic sampler, retention/rollup 실행 경로는 연결됐지만 raw/rollup 보존 기간의 운영 기본값, 담당 그룹 자동 배정, 우선순위 escalation은 아직 운영 정책 결정이 필요하다. `/admin/uptime`과 sampler 값은 계속 SLA가 아니라 관측 샘플 이력이다.
- [ ] **평가설계 (H1: 실험/통제군·3척도·κ/ICC)** — doc4가 20명 실험/통제군·단회기 50분·3척도 pre-post를 명시하지만 κ/ICC·환각률 목표, 통계검정 종류, alpha, 결측 처리, 공식 효과성 판정 기준은 미확정이다. 영향: 평가 타당도·Phase 3 측정력·파일럿 evidence 해석.
- [ ] **s2s 2차 PoC 채택 여부** — 캐스케이드만 / s2s 2차 PoC 유지. 영향: 음성 아키텍처·전송 비용·로그 보존. 기준표 `docs/decisions/voice-s2s-poc.md`.
- [ ] **서비스용 페르소나 음성 모델·권리 정책** — (2026-06-27) P1 서연 음성 아트 PoC는 Higgs v3 무참조 synthetic seed 방식으로 생성(`docs/voice-art/p1-seoyeon-higgs-v3-20260627/`). 실존 인물/성우 reference 사용 금지 확정. `VIGNETTE_VOICE_POC_SAMPLE_TTS=true` 개발 플래그로 `/voice/ws` P1 sample TTS provider에 연결했지만, 서비스 배포 전 선택 필요: Higgs 상업 라이선스 확보 / 동의 받은 reference voice만 사용 / 배포 가능한 대체 TTS provider 채택.
- [ ] **fast-loop 외부 API 허용** — 로컬 전용 / 외부 Haiku·Solar 폴백 허용. 영향: 데이터주권·레이턴시·장애 대응.
- [ ] **재귀학습 fine-tuning 범위** — few-shot 자동갱신만 / fine-tuning 포함. 영향: 동의서·데이터셋 품질·운영 리스크.
- [ ] **자유연습 기본값** — 기본 ON(+피드백 권장 배지+교수자 토글) / 교수자 승인 후 ON. 영향: 학습자 자율성·평가 품질·안전 정책.
- [x] **자유연습 기본값** — 결정됨: 기본 ON(+피드백 권장 배지+교수자 토글). 미동의·미성년 개인정보 gate만 차단한다. 근거: `docs/redteam/MASTERPLAN_REVISIONS.md` F-10 승인.
- [ ] **추가 축어록 수급 규모·일정** — 목표 N건/일정 확정. 영향: 평가 타당도·페르소나 다양성·Phase 3 측정력.
- [x] **백엔드 언어 방향** — (2026-06-28 결정) Node.js를 장기 교체 대상과 신규 분리 서비스 우선 스택으로 삼되, 현재 FastAPI 전면 재작성은 납품 일정 리스크라 보류한다. 전환은 계약 우선 strangler 방식으로 진행한다. 1차 경계로 `app/contracts/engine_gateway.py`를 추가해 FastAPI client와 현재 Python gateway가 `/v1/generate`, `/v1/stream`, SSE `token/done/error` 계약을 공유한다. 이어서 raw gateway SSE line 해석을 `EngineClient.stream_packets()`로 올려 API orchestrator가 `EngineGatewaySsePacket`만 처리하게 했고, Python gateway `/v1/generate` 응답은 `GenerateResponse.model_dump()`로 고정해 hand-mirrored dict drift를 줄였다. 2차로 `apps/api/engine_gateway/golden/engine_gateway_contract.v1.json``engine_gateway_schema.v1.json`을 추가하고, 3차로 `scripts/check-engine-gateway-contract.mjs` Node.js conformance runner를 붙여 Node gateway가 Python import 없이 request/response/SSE packet conformance를 검증할 수 있게 했다. 4차로 `app/session_read_model.py`를 추가해 세션 목록·대시보드·상세·리뷰·공유 payload의 브라우저-facing DTO와 deterministic builder를 `routes/sessions.py`에서 분리했다. 5차로 `app/persona_read_model.py`를 추가해 페르소나 catalog/review/draft/source/evidence DTO와 deterministic mapper를 `routes/personas.py`에서 분리했다. 각 route는 auth/RLS DB read/persistence, teacher/admin gate, RAG/LLM side effect, session lifecycle을 계속 소유한다. 결정 기록: `docs/decisions/backend-node-transition.md`. **검증: `node scripts/check-engine-gateway-contract.mjs --json`, `python -B -m pytest -p no:cacheprovider engine_gateway/test_gateway_model.py -q` 19 passed, `python -B -m pytest -p no:cacheprovider engine_gateway/test_gateway_model.py app/test_orchestrator_masking.py app/test_session_turn_persistence.py -q` 50 passed, `python -B -m py_compile app/session_read_model.py app/routes/sessions.py`, session read-model focused 48 passed, `python -B -m py_compile app/persona_read_model.py app/routes/personas.py`, persona read-model focused 33 passed, persona/session-start 55 passed, `npm run check:api-types`, `npm run typecheck`.**
- [x] **백엔드 언어 방향** — (2026-06-28 결정, 2026-06-29 보강) Node.js를 장기 교체 대상과 신규 분리 서비스 우선 스택으로 삼되, 현재 FastAPI 전면 재작성은 납품 일정 리스크라 보류한다. 전환은 계약 우선 strangler 방식으로 진행한다. 고정된 내부 경계는 `app/contracts/engine_gateway.py`의 gateway request/response/SSE 계약과 `gateway-default` sentinel, `structured_payload_from_response()`, `scripts/check-engine-gateway-contract.mjs` schema-driven Node conformance runner, `GatewayPromptParts`, `app/session_read_model.py`, `app/persona_read_model.py`, `app/persona_generation_contract.py`, `SessionEvaluationWrite.from_result()/from_error()`다. FastAPI route는 auth/RLS DB read/persistence, teacher/admin gate, RAG/LLM side effect, session lifecycle, provenance/error mapping을 계속 소유한다. 2026-06-29 후속으로 Python gateway의 `provider/model` 응답 메타 규칙을 helper로 모으고, Node runner가 `gateway-default` request sentinel과 resolved response model 분리를 검증하며, current-turn prompt split과 missing-user 400 경계를 테스트로 고정했다. 결정 기록: `docs/decisions/backend-node-transition.md`. **검증: `node scripts/check-engine-gateway-contract.mjs --json`, gateway contract 27 passed, persona contract+review 39 passed, backend focused 120 passed, `npm run check:api-types`, `npm run typecheck`.**
- [x] **Node 전환 경계 보강 — 페르소나 생성 계약 + 평가 저장 packet** — (2026-06-29 처리) `app/persona_generation_contract.py`가 페르소나 draft structured schema, prompt bundle id/version/hash, `GenerateResponse` payload extraction, generated draft coercion/default fallback을 소유한다. `SessionEvaluationWrite.from_result()/from_error()``app.session_evaluation` 저장 packet(`status/source/scope/stage/payload/error`) 생성을 소유한다. Node conformance runner는 stream event 목록과 payload 검증을 `engine_gateway_schema.v1.json``StreamPacket.oneOf`, payload `$ref`, `x-engine-gateway-sse-events`에서 읽는다. `routes/personas.py`는 auth, source 등록·조회, RAG evidence, engine 호출, provenance 조립, HTTP error mapping을 유지하고, `routes/sessions.py`/`routes/eval.py`는 factory로 named packet을 만들어 저장한다. **검증: `py -3.11 -X utf8 -B -m py_compile app\persona_generation_contract.py app\routes\personas.py app\persona_read_model.py app\test_persona_generation_contract.py app\test_persona_review.py engine_gateway\test_gateway_model.py app\session_persistence.py app\routes\sessions.py app\routes\eval.py app\services\evaluator.py app\services\live_coach.py`, `py -3.11 -X utf8 -B -m pytest -p no:cacheprovider app\test_persona_generation_contract.py app\test_persona_review.py -q` 39 passed, backend focused 120 passed, `node scripts\check-engine-gateway-contract.mjs --json`, `npm run check:api-types`, `npm run typecheck`.**
---
@ -80,17 +85,19 @@
- 레이아웃 시각 수용: 기본 게이트 7/7 + 적대적 재검수 7/7 accept에 더해 빈 회기리뷰 전용 gate를 추가한 현재 9/9 — DONE.
- live 운영 증거: engine config 운영값, 상주 엔진풀 probe(TTFT/cost/세션재사용), 상주 엔진풀 RSS 실측, Postgres RLS/audit smoke 5 checks PASS, turn cost telemetry(app.turns 13행) — DONE.
- P1 서연 음성 아트 PoC: Higgs v3 무참조 synthetic seed + 5개 정서/속도 변주(mp3/wav) 생성. 실존 reference voice 미사용. `/voice/ws`에는 dev-only sample TTS provider로 연결했고 live text_turn smoke에서 `p1-sample-poc` binary 20 chunks/78,573 bytes 수신 확인. 이 provider는 TTS override만 담당한다(마이크/STT는 OpenAI 키 필요). 프로덕션 탑재는 라이선스/권리 결정 전 금지.
- Session.tsx 음성 guard/view-model P2 리팩터 + capture/EOT 계약 보강: `isVoiceStatusBusy()``sessionVoiceStatusView()`가 마이크 busy/disabled, aria label, transcript/response/status label, 텍스트 입력 차단 계산을 소유한다. Session mic capture는 `AudioWorklet`-first PCM16 + `MediaRecorder` fallback이고, `/voice/ws``stt_result` control로 EOT ready/pending을 route에서 판단한다. 검증은 web typecheck/build, `session-mvp`, `voice-success` synthetic UI, `session-layout`, `test_voice_ws.py/test_voice_service.py` 범위만 인정한다. 실제 Deepgram WSS, 물리 마이크·공개 WSS·50분 장시간 실측은 B2 음성 캐스케이드 live 항목에 계속 남긴다.
- 부수: RLS smoke 스크립트 cleanup RLS 컨텍스트 버그 수정, `vignette_app`(NOBYPASSRLS) dev 역할 프로비저닝.
- 평가 SSOP 2차: `TurnRecord.evaluation``app.feedback_scores`/`turn_technique`/`turn_client_state`/`supervisor_comment`/`alternative_utterance`로 정규화 적재·복원하고, submit/stream/voice 저장 경로를 `app.turn_runtime`으로 공통화했다. 원시 평가 테이블은 evaluator/admin RLS로 제한하고 서버가 evaluator 컨텍스트로 적재한다. `app.turns` insert는 `RETURNING id` 확인으로 silent success 제거. **검증: `pytest app/test_evaluation_persistence.py -q` 6 passed, 관련 회귀 45 passed, `pytest app/` 119 passed, `pytest engine_gateway/` 7 passed.**
- 라이브 코칭 1차 + 이력 UI/RAG sync + 학습자 리뷰 2열 UI: `app/services/live_coach.py`, `data/kb/live_coaching_workbook_0615.json`, `data/kb/live_coaching_sources/*.json`, `POST /sessions/{id}/live-coach`, `GET /sessions/{id}/live-coach`, `POST /kb/live-coach/source-packs/sync`, `app.live_coach_events`를 추가해 0615 사례개념화 워크북·DSM·공식 지침 요약 기반 코칭을 구조화했다. 세션 UI는 코칭 모드에서 AI 코치 아바타 말풍선, 근거 모달, 학습자 발화 우측 `C` 마커, 채팅 위 스크롤 이력 오버레이를 제공한다. 저장 payload는 PII 마스킹 excerpt와 코칭 구조화 JSON으로 제한한다. 공식 자료는 허가된 source pack의 version/citation/summary로만 코칭에 넣고 장문 원문·공식 문항 재현은 하지 않는다. 같은 source pack은 `app.services.source_pack_sync`로 evaluator 전용 RAG에 증분 색인하며, active `content_hash`가 바뀌면 `kb.document.version`을 최신+1로 올려 UNIQUE 충돌을 피한다. 2026-06-28 refactor-governance P4에서는 live turn의 프로세스 로컬 source pack snapshot은 유지하되 관리자 sync/CLI가 `refresh=True`로 캐시를 비우고 repo 파일을 다시 읽게 해 stale `content_hash` 비교를 막았다. 학습자 `SessionReview` 데스크톱은 좌측 축어록 타임라인과 우측 요약·감정·흐름·루브릭·강점·개선점·pre/post·워크시트·피드백 작업열의 2열 구조로 재배치했고, 교수자/모바일 레이아웃은 기존 규칙을 유지한다. **검증: `python -m compileall app -q`, 과거 `pytest app/test_live_coach_sources.py app/test_orchestrator_masking.py app/test_session_turn_persistence.py -q` 27 passed, source sync `py -3.11 -X utf8 -m pytest -p no:cacheprovider app/test_live_coach_sources.py -q` 9 passed, H2 related `py -3.11 -X utf8 -m pytest -p no:cacheprovider app/test_live_coach_sources.py app/test_orchestrator_masking.py app/test_session_turn_persistence.py -q` 44 passed, `py -3.11 -X utf8 scripts\sync-persona-sources.py --help`, `npm run generate:api-types`, `npm run check:api-types`, `npm run typecheck`, `npm run build`, `npm run e2e -- e2e/layout-visual-gate.spec.ts e2e/session-layout.spec.ts` 15 passed. 2열 UI 검증: `npm run typecheck`, `npx playwright test e2e/session-review.spec.ts --project=chromium-desktop --workers=1` 3 passed, `npx playwright test e2e/layout-visual-gate.spec.ts --project=chromium-single-run --workers=1` 9 passed, `session-review__1440-desktop.png` 직접 확인. 로컬 DB smoke: sync 4 sources/15 chunks embedded, 재호출 skipped_unchanged 4, `official_suicide_risk_guidelines` source_id 필터 검색 성공.**
- 라이브 코칭 1차 + 이력 UI/RAG sync + 학습자 리뷰 3열 workbench: `app/services/live_coach.py`, `data/kb/live_coaching_workbook_0615.json`, `data/kb/live_coaching_sources/*.json`, `POST /sessions/{id}/live-coach`, `GET /sessions/{id}/live-coach`, `POST /kb/live-coach/source-packs/sync`, `app.live_coach_events`를 추가해 0615 사례개념화 워크북·DSM·공식 지침 요약 기반 코칭을 구조화했다. 세션 UI는 코칭 모드에서 AI 코치 아바타 말풍선, 근거 모달, 학습자 발화 우측 `C` 마커, 채팅 위 스크롤 이력 오버레이를 제공한다. 저장 payload는 PII 마스킹 excerpt와 코칭 구조화 JSON으로 제한한다. 공식 자료는 허가된 source pack의 version/citation/summary로만 코칭에 넣고 장문 원문·공식 문항 재현은 하지 않는다. 같은 source pack은 `app.services.source_pack_sync`로 evaluator 전용 RAG에 증분 색인하며, active `content_hash`가 바뀌면 `kb.document.version`을 최신+1로 올려 UNIQUE 충돌을 피한다. 2026-06-28 refactor-governance P4에서는 live turn의 프로세스 로컬 source pack snapshot은 유지하되 관리자 sync/CLI가 `refresh=True`로 캐시를 비우고 repo 파일을 다시 읽게 해 stale `content_hash` 비교를 막았다. 학습자 `SessionReview` 데스크톱은 요약/흐름, 축어록, 평가 rail의 3열 workbench와 하단 워크시트로 재배치했고, empty review는 2열 이하로 유지한다. 2026-06-29에는 워크시트/pre-post/교수자 메모 입력에 name/autocomplete/aria-label과 공유 focus token을 보강하고, 발화 이동 버튼 aria-label을 추가했다. **검증: `python -m compileall app -q`, 과거 `pytest app/test_live_coach_sources.py app/test_orchestrator_masking.py app/test_session_turn_persistence.py -q` 27 passed, source sync `py -3.11 -X utf8 -m pytest -p no:cacheprovider app/test_live_coach_sources.py -q` 9 passed, H2 related `py -3.11 -X utf8 -m pytest -p no:cacheprovider app/test_live_coach_sources.py app/test_orchestrator_masking.py app/test_session_turn_persistence.py -q` 44 passed, `py -3.11 -X utf8 scripts\sync-persona-sources.py --help`, `npm run generate:api-types`, `npm run check:api-types`, `npm run typecheck`, `npm run build`, `npm run e2e -- e2e/layout-visual-gate.spec.ts e2e/session-layout.spec.ts` 15 passed. 3열 workbench 검증: `npm run typecheck`, `npm run build`, `npx playwright test e2e/session-review.spec.ts --project=chromium-desktop --workers=1` 3 passed, `npx playwright test e2e/layout-visual-gate.spec.ts --project=chromium-single-run --workers=1` 9 passed, `04-session-review-desktop.png` 직접 확인. 로컬 DB smoke: sync 4 sources/15 chunks embedded, 재호출 skipped_unchanged 4, `official_suicide_risk_guidelines` source_id 필터 검색 성공.**
- 계약 SSOT 7차: FastAPI OpenAPI를 `apps/web/src/lib/api.gen.ts`로 생성하는 `npm run generate:api-types`와 stale 체크용 `npm run check:api-types`를 유지한다. `TurnResponse`/`SessionEndResponse`에 이어 `MeResponse`, `AuthConfigResponse`, `ConsentResponse`, `PersonaSummary`, `PersonaReviewSummary`, admin/user/engine DTO, review leaf DTO, `CrisisResourceResponse`, teacher safety/growth leaf DTO, `LearnerSessionsResponse`, `LearnerDashboardResponse`, `SessionReviewResponse`, worksheet aggregate, `TeacherDashboardResponse`, `TeacherSessionSummary`, `TeacherLearnerGrowth`, `SessionStartResponse`, `SessionDetailResponse`, `SessionDetailTurn`를 생성 타입 alias로 전환했다. 세션 응답 stage는 `StageLabel` enum으로 OpenAPI에 고정했고, 69차에서는 `ReviewPhaseSegment.key/label`, `SessionReviewResponse.reachedPhase`, `EvaluationSummary.stage`, 평가 트리거 응답 stage, `TeacherSessionSummary.stage`, `TeacherGrowthPoint.stage`도 union/null 계약으로 축소했다. 71차에서는 공개 공유 응답 `PublicSessionShareResponse.reachedPhase`와 live-coach 이력 `LiveCoachEvent.stage``StageLabel | null` 계약으로 축소했다. 72차에서는 `app/stage_contract.py``StageLabel`, `ReviewPhaseKey`, legacy stage code 정규화, unknown→null 판단, 리뷰 phase key 매핑을 소유하게 해 `turn_runtime.py`, `session_read_model.py`, `routes/eval.py`의 중복 stage 규칙을 제거했다. generated optional/default 차이는 UI form state, notification default helper, 배열 렌더링 fallback, live-coach `단계 미상` 표시로 흡수한다. `.github/workflows/api-contract.yml`은 PR/master push에서 `npm run check:api-types` drift를 막는다. **검증: `py -3.11 -X utf8 -m pytest -p no:cacheprovider app/test_evaluation_persistence.py app/test_evaluator_model_routing.py app/test_teacher_dashboard.py app/test_rbac_idor.py app/test_session_turn_persistence.py -q` 50 passed, `py -3.11 -X utf8 -m pytest -p no:cacheprovider app/test_session_share.py app/test_live_coach_sources.py app/test_session_turn_persistence.py app/test_orchestrator_masking.py -q` 43 passed, `py -3.11 -X utf8 -m pytest -p no:cacheprovider app/test_stage_contract.py app/test_session_share.py app/test_session_turn_persistence.py app/test_evaluation_persistence.py app/test_teacher_dashboard.py -q` 42 passed, `py -3.11 -X utf8 -m py_compile apps/api/app/stage_contract.py apps/api/app/turn_runtime.py apps/api/app/session_read_model.py apps/api/app/routes/eval.py apps/api/app/routes/sessions.py apps/api/app/routes/share.py apps/api/app/services/live_coach.py`, `npm run check:api-types`, `npm run typecheck`, `npm run build`, 과거 `learner.spec.ts + session-review.spec.ts + teacher.spec.ts + layout-visual-gate.spec.ts` 32 passed.**
- 위기게이트 109 1차: 실제 자해·자살 신호는 LLM/엔진 호출 전 중단하고 `crisis_resource(109)``conversation_stopped`를 REST/SSE/voice 응답에 싣는다. `app.safety_events` detail 적재와 교수자 대시보드 안전 알림 큐까지 연결했다. 안전 이벤트 DB insert payload도 회귀화했다. 임상 문안, 실시간 push/메일, 지역 자원 확장은 후속. **검증: `pytest app/test_session_turn_persistence.py app/test_voice_ws.py -q` 20 passed, `pytest app/test_teacher_dashboard.py app/test_session_turn_persistence.py app/test_voice_ws.py -q` 21 passed.**
- 교수자 성장 추적/리뷰 상세 진입 1차: `teacher_dashboard`가 정규화된 턴 평가를 기반으로 학습자별 적절성·라포·기술 사용 추이를 집계하고, 교수 콘솔에 최근 회기/항목별 성장 카드와 안전 알림 큐를 함께 표시한다. 종료 회기 리뷰 대기 행과 최근 회기 행은 `/teach/session/:sessionId/review`로 이어지고, 교수자는 회기 리뷰와 사례개념화 워크시트를 읽기 전용으로 검토한다. 학습자 워크시트 저장은 계속 learner 전용이다. **검증: `pytest app/test_teacher_dashboard.py app/test_session_turn_persistence.py app/test_voice_ws.py -q` 21 passed, `pytest app/test_rbac_idor.py app/test_teacher_dashboard.py -q` 9 passed, `npm run check:api-types`, `npm run typecheck`, `npm run build`, `npx playwright test e2e/teacher.spec.ts --project=chromium-single-run --workers=1` 4 passed.**
- 교수자 검토 상태 1차: `app.session_review_status`에 교수자 회기 검토 상태를 저장하고, teacher dashboard 응답에 `review_status/review_note/reviewed_at`를 포함한다. `PUT /teacher/sessions/{session_id}/review-status`로 교수자 메모 저장과 검토 완료 처리를 수행하며, 완료된 회기는 pending queue에서 제외한다. 교수자/관리자의 `/sessions/{id}/review` 읽기 허용은 유지하고, 학습자 워크시트 저장은 learner 전용으로 계속 제한한다. 교수 콘솔 검토 큐는 숨김 처리하지 않고 상단 triage로 노출하며, 학습자 리뷰 화면에는 교수자 전용 grid 빈칸을 예약하지 않는다. **검증: `pytest app/test_teacher_dashboard.py app/test_rbac_idor.py app/test_learner_dashboard.py -q` 12 passed, `npm run check:api-types`, `npm run typecheck`, `npm run build`, `teacher.spec.ts` 4 passed, `session-review.spec.ts` 2 passed, `layout-visual-gate.spec.ts` 7 passed.**
- 학습자 개인화 대시보드 1차: 교수자 성장 집계와 중복되던 점수/라포/기법 계산을 `app.services.session_metrics`로 공용화하고, `GET /sessions/dashboard`가 본인 세션 기반 `overview/growth/persona_progress/achievements/recent_feedback`를 반환한다. 학습자 홈은 누적 회기, 리뷰 대기, 최근 평가, 라포 흐름, 페르소나별 진행, 최근 피드백, 마지막 세션 리캡을 실제 서버 데이터로 표시한다. 성취는 공식 등급/수료가 아니라 실제 연습 milestone만 사용한다. **검증: `pytest app/test_learner_dashboard.py app/test_teacher_dashboard.py app/test_rbac_idor.py -q` 12 passed, `npm run check:api-types`, `npm run typecheck`, `npm run build`, `learner.spec.ts` 6 passed, `session-review.spec.ts` 2 passed, `teacher.spec.ts` 4 passed, `layout-visual-gate.spec.ts` 7 passed, `session-layout.spec.ts` 4 passed.**
- 학습자 개인화 대시보드 1차: 교수자 성장 집계와 중복되던 점수/라포/기법 계산을 `app.services.session_metrics`로 공용화하고, `GET /sessions/dashboard`가 본인 세션 기반 `overview/growth/persona_progress/achievements/recent_feedback`를 반환한다. 학습자 홈은 누적 회기, 리뷰 대기, 최근 평가, 라포 흐름, 페르소나별 진행, 최근 피드백, 마지막 세션 리캡을 실제 서버 데이터로 표시한다. 성취는 공식 등급/수료가 아니라 실제 연습 milestone만 사용한다. 2026-06-29 refactor-governance 패스에서 `session_metrics.turn_technique_label()`이 DB 재수화 계약의 `label_ko`를 우선하고 기존 `label/name/id/code` fallback을 유지하도록 고정했다. **검증: `py -3.11 -X utf8 -B -m pytest -p no:cacheprovider app/test_evaluation_persistence.py app/test_evaluator_model_routing.py app/test_session_turn_persistence.py app/test_rbac_idor.py app/test_teacher_dashboard.py app/test_learner_dashboard.py -q` 59 passed, 기존 `npm run check:api-types`, `npm run typecheck`, `npm run build`, `learner.spec.ts` 6 passed, `session-review.spec.ts` 2 passed, `teacher.spec.ts` 4 passed, `layout-visual-gate.spec.ts` 7 passed, `session-layout.spec.ts` 4 passed.**
- 페르소나 스튜디오/RAG 저작 10차: 교수 콘솔의 JSON 패널을 분리해 teacher/admin 전용 `/teach/personas` 3열 스튜디오(좌: 저작 흐름·카탈로그·검수 큐, 중앙: RAG 자료 등록·항목형 편집 탭, 우: 검증·RAG 근거·검수 결정)로 옮겼다. 2026-06-27 UI 정리에서 중복 네이티브 파일 입력을 커스텀 SSOT 첨부 드롭존으로 교체하고, 저작 스테퍼·탭별 작성 가이드·검증 dot 정렬을 보강했다. 2026-06-28 항목형 목록 UI 정리에서 자동사고, 회기 시나리오, 말투 filler/verbal tic/nonverbal cue, 역린·금기 응답·금기어를 행 추가/삭제 UI로 전환했고, 저장 payload는 기존 배열 schema를 유지하며 빈 항목만 저장 직전에 제거한다. 프롬프트 탭은 raw JSON textarea 대신 L1 카드·인적 범주·임상 배경·말투·수치 파라미터·역린·회기 시나리오·추가 계약 섹션으로 같은 draft 데이터를 라벨형 검토 UI에 표시한다. 레이아웃 근거와 생성 시안은 `docs/ops/layout-research-2026-06-27/persona-dashboard-layout-guideline.md`에 묶었다. `POST /personas/sources`는 첨부/붙여넣기 자료를 PII 마스킹 후 raw 원문 hash-only 증거를 `kb.raw_source_artifact`에 따로 기록하고, sanitized 파생본만 `kb.source/document/chunk`에 evaluator 전용(`visible_to=['evaluator']`, `sensitivity=2`) 근거 문서로 등록한다. `rag.index_document()``sensitivity=3` 또는 raw marker chunk를 DB 접근 전에 차단해 raw 원문이 `kb.chunk`/embedding/FTS에 들어가지 않게 한다. `POST /personas/drafts/generate``source_id` 기반 RAG 검색 결과만 생성 프롬프트에 넘긴다. 생성 응답과 draft `source_provenance`에는 source id, doc/content hash, chunk id, prompt bundle id/version/hash(`persona-draft-rag@2026-06-28.1`)를 남긴다. P1~P7 시스템 페르소나는 DB 저작 카탈로그의 초기 부트스트랩으로 승격했고, `materialize_seed_personas()`는 누락분만 insert해서 교수 편집본이나 `archived` 보관본을 덮어쓰지 않는다. `scripts/materialize-persona-seeds.py`는 같은 seed/version manifest를 dry-run 기본으로 보고하며, `--apply`일 때만 DB pool을 초기화한 뒤 기존 idempotent materializer를 호출한다. `scripts/sync-persona-sources.py``app.services.source_pack_sync`는 repo-managed source pack의 active `content_hash`를 DB에서 비교하고, 변경 시 `kb.document.version`을 최신+1로 색인한다. 교수자는 공개 목록에서 승인본을 다음 버전 draft로 복제해 수정하고, 불필요한 페르소나는 기존 회기 FK 보존을 위해 같은 code family 전체를 `archived`로 보관 처리한다. `app/persona_read_model.py`는 catalog/review/draft/source/evidence DTO와 mapper를 route에서 분리해 schema 이름과 `source/degraded` fail-closed 경계를 유지한다. 암호화 blob/vault 기반 원문 실저장, 임상팀 최종 검수/서면 evidence, 루브릭·이론 콘텐츠 외부화는 후속. **검증: `python -B -m py_compile app/services/rag.py app/routes/personas.py app/routes/kb.py app/test_persona_review.py app/test_live_coach_sources.py`, `python -B -m pytest -p no:cacheprovider app/test_persona_review.py app/test_live_coach_sources.py -q` 38 passed, `python -B -m py_compile app/persona_read_model.py app/routes/personas.py`, `python -B -m pytest -p no:cacheprovider app/test_persona_review.py -q` 33 passed, `python -B -m pytest -p no:cacheprovider app/test_persona_review.py app/test_session_turn_persistence.py -q` 55 passed, `npm run check:api-types`, `npm run typecheck`, `npm run build`, `py -3.11 scripts\materialize-persona-seeds.py --json` dry-run `manifest_count=7`, `py -3.11 scripts\sync-persona-sources.py --help`, `npx playwright test e2e/teacher.spec.ts --project=chromium-single-run --workers=1` 8 passed, 프롬프트 탭 포함 `layout-visual-gate.spec.ts` 9 passed, `session-layout.spec.ts` desktop/mobile 8 passed.**
- 페르소나 생성 계약 11차 + 평가 저장 packet: `app/persona_generation_contract.py`가 draft structured schema, prompt bundle, legacy/structured `GenerateResponse` payload extraction, generated draft defaults/coercion을 소유한다. 기존 `routes/personas.py`는 teacher/admin gate, RAG evidence 수집, engine request, provenance 조립, DB/HTTP error mapping을 계속 담당한다. `SessionEvaluationWrite.from_result()/from_error()`는 session evaluation 저장 packet 생성을 소유해 route의 long keyword bag을 제거했다. Node conformance runner는 `StreamPacket.oneOf`, payload `$ref`, `gateway-default` request sentinel과 resolved response model 분리를 읽어 event/payload/default-routing 검증 중복을 줄였고, `GatewayPromptParts`는 current-turn prompt split 경계를 명명했다. **검증: persona contract+review 39 passed, backend focused 120 passed, Node conformance OK, gateway contract 27 passed, `npm run check:api-types`, `npm run typecheck`.**
- M1 비언어 이벤트 4차: voice learner turn에 이미 저장하던 `audio_ref`/`silence_ms`/`speech_rate`/`barge_in`을 리뷰 API `nonverbal` 이벤트로 노출하고, 회기 리뷰 축어록에 침묵·발화 속도·끼어듦·음성 입력 칩을 표시했다. 2차에서는 `app.turns.provider_events JSONB``TurnRecord.provider_events`를 추가해 WebSocket control/STT provider 이벤트를 allowlist·size limit 후 보존했고, 3차에서는 저장 전 sanitizer에서 내부 taxonomy `event_type`/`category`를 붙인다. 4차에서는 인증된 회기 리뷰에 한숨·울음·웃음·호흡·운율·배경소음 계열만 한글 label/detail 칩으로 파생 노출한다. raw transcript/text/provider/source/raw type은 응답에서 제외하고, 공개 공유 카드에는 축어록과 provider raw를 싣지 않는다. 실제 provider 기반 한숨·울음 감지, 역량 지표화 정책, live 마이크/WSS 장시간 실측은 후속. **검증: `python -B -m py_compile app/routes/sessions.py app/test_session_turn_persistence.py`, `python -B -m pytest -p no:cacheprovider app/test_voice_ws.py app/test_session_turn_persistence.py app/test_voice_service.py -q` 45 passed, `npm run check:api-types`, `npm run typecheck`, `npm run build`, `npx playwright test e2e/session-review.spec.ts --project=chromium-desktop --workers=1` 3 passed.**
- M2 다회기 케이스 아크 8차: DB 세션 생성 시 `(persona_id, learner_id)` 기준 `case_profile`을 upsert하고, `session_no`를 트랜잭션 안에서 원자 증가시키며, `InProcSession.case_id`가 매회 새 `runtime_case_id`가 아니라 안정 `case_id`를 가리키게 수정했다. 시작/submit/stream/voice 경로는 case recall cache를 사용한다. 세션 종료 시 마스킹 축어록 기반 fallback `session_summary.digest``case_profile.case_digest`, `rapport_trajectory`, `alliance_level`을 갱신하고, 다음 회기 seed recall에서 `case_digest`·직전 `session_summary`·client-visible non-contradicted `pinned_fact`를 함께 조립한다. 3차에서는 마스킹된 client-visible 발화에서 `[NAME]`/`[ORG]` identity와 명시적 상담 약속만 보수적으로 `pinned_fact`에 upsert했고, 4차에서는 삽입 또는 값 변경 시 `pinned_fact_history`에 append-only 이력을 남긴다. 5차에서는 명시적 상담 약속 철회/부정만 기존 non-locked `agreement:counseling` fact를 `contradicted`로 격리하고 history reason `contradiction`을 남긴다. 6차에서는 세션 종료 저장 성공 뒤 마스킹된 client-visible 내담자 발화만 `app.turn_embedding`에 BGE-M3 dense/sparse로 `ON CONFLICT (turn_id) DO NOTHING` 색인한다. 7차에서는 submit/stream 공통 `_prepare_turn_context()` 경계를 만들고 DB seed recall·pinned fact가 다음 턴 EngineMessage L2/L4에 raw 이름 마스킹 상태로 주입되는 route-level 회귀를 추가했다. 8차에서는 `SessionDigestInput`/`SessionDigestResult`/`SessionSummaryWrite`로 종료 digest 입력·fallback 결과·DB write 인자 경계를 명시했다. future LLM digest worker 입력은 client-visible masked turn과 open thread로 제한하고, 종료 상태 수치·평가 payload·CCD·raw text는 압축 prompt에 넣지 않는다. `/sessions/{id}/end``digest_pending`은 CompressionJob 생성 여부를 알리는 비동기 압축 필요 신호로 유지한다. 새 contradicted fact는 임의 생성하지 않고, 같은 값 재확인은 history를 늘리지 않으며, `locked` fact는 건드리지 않는다. learner-owned `case_profile` 기준 RLS insert/update를 추가했고, 관계갈등·위기·임상 추론은 자동 pinning/모순 처리에서 제외했다. 관계·임상 fact 승격 기준과 LLM digest worker 실행/품질평가/재압축은 후속. **검증: `py -3.11 -X utf8 -m py_compile apps\api\app\services\memory.py apps\api\app\routes\sessions.py apps\api\app\session_persistence.py apps\api\app\test_session_memory.py`, `py -3.11 -X utf8 -m pytest -p no:cacheprovider app/test_session_memory.py -q` 17 passed, `py -3.11 -X utf8 -m pytest -p no:cacheprovider app/test_session_memory.py app/test_session_turn_persistence.py app/test_orchestrator_masking.py app/test_runtime_policy.py -q` 74 passed, `npm run check:api-types`, `npm run typecheck`.**
- M2 다회기 케이스 아크 11차: DB 세션 생성 시 `(persona_id, learner_id)` 기준 `case_profile`을 upsert하고, `session_no`를 트랜잭션 안에서 원자 증가시키며, `InProcSession.case_id`가 매회 새 `runtime_case_id`가 아니라 안정 `case_id`를 가리키게 수정했다. 시작/submit/stream/voice 경로는 case recall cache를 사용한다. 세션 종료 시 마스킹 축어록 기반 fallback `session_summary.digest``case_profile.case_digest`, `rapport_trajectory`, `alliance_level`을 갱신하고, 다음 회기 seed recall에서 `case_digest`·직전 `session_summary`·client-visible non-contradicted `pinned_fact`를 함께 조립한다. 3차에서는 마스킹된 client-visible 발화에서 `[NAME]`/`[ORG]` identity와 명시적 상담 약속만 보수적으로 `pinned_fact`에 upsert했고, 4차에서는 삽입 또는 값 변경 시 `pinned_fact_history`에 append-only 이력을 남긴다. 5차에서는 명시적 상담 약속 철회/부정만 기존 non-locked `agreement:counseling` fact를 `contradicted`로 격리하고 history reason `contradiction`을 남긴다. 6차에서는 세션 종료 저장 성공 뒤 마스킹된 client-visible 내담자 발화만 `app.turn_embedding`에 BGE-M3 dense/sparse로 `ON CONFLICT (turn_id) DO NOTHING` 색인한다. 7차에서는 submit/stream 공통 `_prepare_turn_context()` 경계를 만들고 DB seed recall·pinned fact가 다음 턴 EngineMessage L2/L4에 raw 이름 마스킹 상태로 주입되는 route-level 회귀를 추가했다. 8차에서는 `SessionDigestInput`/`SessionDigestResult`/`SessionSummaryWrite`로 종료 digest 입력·fallback 결과·DB write 인자 경계를 명시했다. 9차에서는 `DigestQualityAssessment`/`SessionDigestWorkerOutcome`로 LLM digest 후보 local quality harness를 추가해 빈/짧은 digest, raw forbidden substring, 내부 평가·CCD·상태 marker, 잘못된 `S{session_no}:` prefix를 fallback 유지 대상으로 판정한다. 10차에서는 `session_digest_worker.py``CompressionJob``GenerateRequest` 변환, 주입형 engine/audit 호출, accepted-only `session_summary.digest/compressed_by/token_count``case_profile.case_digest` 적용 계획, one-shot DB loader를 소유한다. 11차에서는 `scripts/run-session-digest-worker.py` dry-run/apply runner와 `SESSION_DIGEST_WORKER_ENABLED=false` 기본값의 세션 종료 background scheduler 골격을 추가하고, `compressed_by IS NULL` loader/apply CAS로 이미 압축된 세션 재실행 race를 막는다. LLM digest worker 입력 후보는 client-visible `text_masked` turn과 open thread로 제한하고, 종료 상태 수치·평가 payload·CCD·raw text는 압축 prompt에 넣지 않는다. `/sessions/{id}/end``digest_pending`은 CompressionJob 생성 여부를 알리는 비동기 압축 필요 신호로 유지한다. 새 contradicted fact는 임의 생성하지 않고, 같은 값 재확인은 history를 늘리지 않으며, `locked` fact는 건드리지 않는다. learner-owned `case_profile` 기준 RLS insert/update를 추가했고, 관계갈등·위기·임상 추론은 자동 pinning/모순 처리에서 제외했다. 관계·임상 fact 승격 기준, 실 provider 장시간 운영, 임상 골든셋 품질평가, 재압축은 후속. **검증: `py -3.11 -X utf8 -B -m pytest -p no:cacheprovider app/test_session_digest_worker.py app/test_session_memory.py -q` 30 passed, `py -3.11 -X utf8 -B -m pytest -p no:cacheprovider app/test_session_digest_worker.py app/test_session_memory.py app/test_session_turn_persistence.py app/test_orchestrator_masking.py app/test_runtime_policy.py -q` 87 passed, `py -3.11 -X utf8 -B -m py_compile app/services/session_digest_worker.py app/services/memory.py app/test_session_digest_worker.py app/test_session_memory.py`, 기존 `py -3.11 -X utf8 -B -m pytest -p no:cacheprovider app/test_session_memory.py -q` 20 passed, 과거 `py -3.11 -X utf8 -m pytest -p no:cacheprovider app/test_session_memory.py app/test_session_turn_persistence.py app/test_orchestrator_masking.py app/test_runtime_policy.py -q` 74 passed, `npm run check:api-types`, `npm run typecheck`.**
- C1 사례개념화 워크시트 4차: `SessionReviewResponse.caseWorksheet`와 리뷰 화면 카드가 탐색 11항목·호소 5영역·인지삼제/감정·보호/방해·생물심리사회 목표 초안을 축어록 근거 기반으로 제공한다. 학습자가 편집한 저장본은 `PUT /sessions/{id}/review/worksheet``app.case_worksheet`에 저장되고, 이후 `GET /review``saved_by_learner` 저장본을 자동 초안보다 우선 반환한다. 2026-06-28에는 `data/rubrics/case-worksheet-rubric.json`, schema, loader/validator service, `scripts/check-case-worksheet-rubric.py`를 추가해 임상팀 확정 루브릭을 외부 JSON으로 받을 scaffold를 만들었다. 현재는 `status=scaffold_only`, `scoring_enabled=false`이며 5개 section/28개 item key와 sha256 provenance만 검증한다. 64차 refactor-governance 패스에서 워크시트 템플릿 key source를 `CASE_WORKSHEET_SECTION_SPECS`/`case_worksheet_template_item_keys()`로 명시해 CLI/test가 더미 턴 없이 같은 생성 spec을 검증하게 했다. 73차에서는 기존 `app.session_review_status``worksheet_status`/`worksheet_note`/`worksheet_reviewed_at`을 추가하고, `PUT /teacher/sessions/{id}/review-status`와 교수자 `SessionReview` 카드에서 승인·수정요청·반려 수동 판정을 저장/표시한다. 임상팀 확정 루브릭 콘텐츠, AI 추출/채점 적용·calibration, 승인 후 잠금·재제출 정책, 재귀학습 데이터셋 approved 연계는 후속 gate다. **검증: `py -3.11 -X utf8 -m pytest -p no:cacheprovider app/test_session_turn_persistence.py app/test_teacher_dashboard.py app/test_rbac_idor.py app/test_case_worksheet_rubric.py -q` 40 passed, `py -3.11 -X utf8 scripts/check-case-worksheet-rubric.py --json` PASS, `py -3.11 -X utf8 -m py_compile` focused files, `npm run generate:api-types`, `npm run check:api-types`, `npm run typecheck`, `npm run build`, `npx playwright test e2e/session-review.spec.ts --project=chromium-desktop --workers=1` 3 passed, 로컬 API smoke 세션 생성→저장→`GET /review` `saved_by_learner:local smoke saved worksheet`.**
- C3 이론모드 2차: `theory_mode``TurnContext`/sessions/voice/evaluator뿐 아니라 `build_turn_messages`의 엔진 메시지까지 전달된다. 프론트는 `persona.theory_target` 기준 기본값을 유지하되, 세션 시작 전 `humanistic`/`cbt`/`integrative` segmented control로 학습자가 명시 선택하고 `POST /sessions``theory_mode`로 보낸다. CBT 체인·이론부합 루브릭은 후속. **검증: `python -B -m pytest -p no:cacheprovider app/test_orchestrator_masking.py app/test_session_turn_persistence.py -q` 31 passed, `npm run typecheck`, `npm run build`, `npx playwright test e2e/session-layout.spec.ts --project=chromium-desktop --workers=1` 4 passed, `npx playwright test e2e/session-layout.spec.ts --project=chromium-mobile --workers=1` 4 passed, `npx playwright test e2e/session-mvp.spec.ts --project=chromium-single-run --workers=1` 1 passed, `npx playwright test e2e/layout-visual-gate.spec.ts --project=chromium-single-run --workers=1` 9 passed.**
- M3 인증 claim 1차: Google/SAML/dev-login이 `AUTH_EMAIL_COHORT_MAP`·`AUTH_DOMAIN_COHORT_MAP` 및 SAML cohort claim을 `cohort_ids`로 전달하고, DB `app_user.external_id`는 provider subject(`google:`/`saml:`/`dev:`) 기반으로 저장한다. 운영 SAML 서명검증, 기관 claim schema/test tenant, deprovisioning audit은 후속. **검증: `pytest app/test_auth_providers.py -q` 30 passed, `pytest app/ -q` 178 passed.**

View file

@ -108,6 +108,47 @@
1. `P1 라이브 상담 세션`만 먼저 구현한다.
2. 현재 `03-session-active-*` 캡처와 v2 이미지를 나란히 비교해 CSS delta를 최소화한다.
3. `Session.tsx` 구조 변경은 피하고, 가능한 한 `session.css`에서 surface/density/responsive만 조정한다.
3. 1차 CSS 보정만으로 v2 근접도가 부족하면 `Session.tsx` 구조도 실제 기능 데이터 범위 안에서 조정한다.
4. 검증 통과 후 새 full-page 캡처를 남기고 SSOT를 DONE/VERIFY 상태로 갱신한다.
5. 그 다음 `P2 회기 리뷰`, `P3 학습자 홈` 순서로 반복한다.
## 5. P1 실행 기록
2026-06-29 1차 적용:
- `Session.tsx` stage에 내담자 이름/현재 발화 영역을 추가하고, 좌측에는 실제 경과·권장 시간·저장 발화 수 기반 `세션 진행` 패널을 추가했다.
- 우측 rail은 `라이브 신호`를 파형/상태 행/최근 흐름으로 재구성하고, `안전 점검`은 정상 상태에서도 보이는 체크 패널로 바꿨다.
- `session.css` active 화면 전용 dark token을 추가해 beige/off-white surface 혼입을 막고, stage/transcript/input/controlbar의 표면 명도와 focus-visible 상태를 v2 방향으로 정리했다.
- 갱신 캡처: `docs/design-verification/full-pages/03-session-active-desktop.png`, `03-session-active-tablet.png`, `03-session-active-mobile.png`.
- 검증: `npm run typecheck`, `npm run build`, `npx playwright test e2e/session-layout.spec.ts --project=chromium-desktop --project=chromium-mobile --workers=1` 8 passed, `npx playwright test e2e/layout-visual-gate.spec.ts --project=chromium-single-run --workers=1` 9 passed.
## 6. P2 실행 기록
2026-06-29 2차 적용:
- `session-review.css`의 learner filled review를 2열에서 3열 workbench로 바꿔, 왼쪽 요약/차트/흐름, 중앙 축어록, 오른쪽 평가/인사이트, 하단 워크시트 구조로 재배치했다.
- `layout-visual-gate.spec.ts`의 기존 2열 가정을 3열 workbench 검증으로 갱신하고, empty review는 계속 sparse third column 없이 2열 이하로 접히게 유지했다.
- 4차 QA에서 dark surface 계층을 overview/worksheet/rubric/prepost별로 다시 분리하고, chip/evidence/timestamp 버튼의 `focus-visible` 상태를 보강했다.
- 갱신 캡처: `docs/design-verification/full-pages/04-session-review-desktop.png`, `04-session-review-tablet.png`, `04-session-review-mobile.png`.
- empty 상태 증거 캡처: `04-session-review-empty-desktop.png`, `04-session-review-empty-tablet.png`, `04-session-review-empty-mobile.png`.
- 검증: `npm run typecheck`, `npm run build`, `npx playwright test e2e/session-review.spec.ts --project=chromium-desktop --workers=1` 3 passed, `npx playwright test e2e/layout-visual-gate.spec.ts --project=chromium-single-run --workers=1` 9 passed.
## 7. P3 실행 기록
2026-06-29 3차 적용:
- `LearnerHome.tsx` dashboard view를 재구성해 대시보드의 연습 대상 레일을 제거했다. 내담자 선택은 `/learn/practice`의 역할로 분리하고, `/learn`은 학습 상태와 오늘 이어갈 회기 판단에 집중한다.
- 진행 회기/리뷰 대기/최근 평가/라포 흐름은 상단 CTA 바 `lh-dashboard-status`로 올렸다. 핵심 지표는 오늘 회기 카드의 부속 정보가 아니라 대시보드 진입 직후 확인하는 상태 요약이다.
- 오늘 이어갈 회기, AI 코치, 다음 연습 추천, 최근 피드백은 `lh-work-cluster` 안에서 같은 작업 묶음으로 보이게 했다. 데스크톱에서는 회기와 코치가 같은 row에 놓이고, 추천/피드백은 그 아래 row로 묶인다.
- 오른쪽 레일은 최근 기록, 리뷰 대기, 반복 대상만 남겨 학습 이력과 후속 행동을 담당하게 했다.
- visual gate의 learner-home 준비 조건은 상단 지표 CTA, 작업 클러스터, 실제 최근 기록 row가 보이는 상태로 강화했다.
- 4차 QA에서 `학습 현황` ARIA label strict 충돌을 제거하고, persona name/summary clamp와 주요 버튼의 `focus-visible` 상태를 보강했다.
- 갱신 캡처: `docs/design-verification/full-pages/02-learner-home-desktop.png`, `02-learner-home-tablet.png`, `02-learner-home-mobile.png`.
- 검증: `npm run typecheck`, `npm run build`, `npx playwright test e2e/layout-visual-gate.spec.ts --project=chromium-single-run --workers=1` 9 passed.
## 8. 완료 상태
- P1/P2/P3 모두 실제 React/CSS 화면에 적용했고, 생성 PNG는 레이아웃·밀도·톤 기준으로만 사용했다.
- 이번 라운드는 API response shape를 바꾸지 않았으므로 `npm run check:api-types`는 추가하지 않았다.
- `layout-visual-gate.spec.ts`는 4차 QA부터 각 캡처가 dark UI surface인지 `html[data-theme="dark"]`로 단언한다.
- 전체 DONE 근거는 최신 full-page 캡처와 `typecheck/build/layout-gate` 검증이다.

View file

@ -1,6 +1,7 @@
# 리팩토링 + RAG 활성화 패치 (워크플로우 산출, 2026-06-26)
> 적용 순서·리스크는 아래 'Sequence' 참고. 일부 anchor는 이후 voice.py 수정(speakable_text)으로 stale일 수 있어 적용 전 현재 코드와 대조 필수.
> 현재 상태(2026-06-29): 이 문서는 적용 전 충돌 분석 기록이다. `TurnMemory`는 현재 `persona.py`에 정의되어 `TurnContext.memory`, `prepare_turn(memory=...)`, `build_turn_messages(memory=...)`, REST/SSE/voice/reevaluate 경로에 반영됐다. `ManagedUserUpsertInput`은 auth managed-user create/reactivate 입력 경계로 반영됐고, `ManagedUserMemoryInput`/`ManagedUserPatch`와 역할을 분리한다. `digest_pending`과 LLM digest worker 실행/품질평가/재압축은 여전히 별도 GATE다.
## 적용 순서/리스크 (Sequence)
@ -15,7 +16,7 @@
| 코드 | 패치 | 성격 | risk |
|---|---|---|---|
| **P1** | 데드코드 3종 제거 (LogHook / gateway·client tier / 서버 RMS 힌트) | 순수 삭제 | medium |
| **P2** | 파라미터 객체화 3종 (`OpennessParams` / `TurnMemory` / `ManagedUserInput`) | 시그니처 리팩토링 | medium |
| **P2** | 파라미터 객체화 3종 (`OpennessParams` / `TurnMemory` / `ManagedUserUpsertInput`) | 시그니처 리팩토링 | medium |
| **P3** | `turn_runtime.py` 추출 (REST/WS 턴 처리 공용화) | 코드 이동·래퍼 | medium |
| **P4** | `routes/sessions.py` RAG 배선 (회상·KB 행동단서) | 기능 추가 | medium |
@ -111,7 +112,7 @@ python -c "from app.services import persona, orchestrator, state_machine; from a
python -m pytest app/test_state_machine_resistance.py app/test_orchestrator_masking.py app/test_session_turn_persistence.py app/test_rbac_idor.py -q
python -m pytest app -q && python -m pytest engine_gateway -q
```
- 중점: 순환참조 부재(`persona→state_machine` 단방향, `orchestrator→persona(TurnMemory)`, `eval→orchestrator(TurnMemory)`), PII 마스킹 불변(`memory=TurnMemory`로 받아도 recall/pinned/recent 마스킹 유지), 저항 곡선 불변, `ctx.memory.recent_turns`가 evaluator-only 비공개 발화 제외, auth/admin 경로(`ManagedUserInput`) 회귀.
- 중점: 순환참조 부재(`persona→state_machine` 단방향, `orchestrator→persona(TurnMemory)`, `eval→orchestrator(TurnMemory)`), PII 마스킹 불변(`memory=TurnMemory`로 받아도 recall/pinned/recent 마스킹 유지), 저항 곡선 불변, `ctx.memory.recent_turns`가 evaluator-only 비공개 발화 제외, auth/admin 경로(`ManagedUserUpsertInput`) 회귀.
- 잔존 참조 점검: `ctx.recall_summary`/`ctx.pinned_facts`/`ctx.recent_turns`/`ctx.kb_behavior_cues` 가 evaluator.py·eval.py·테스트에 남아있지 않은지(AttributeError 차단).
### P3 후
@ -156,15 +157,15 @@ python -m pytest app -q && python -m pytest engine_gateway -q
### 파라미터 객체화 3종 리팩토링 (모두 읽기전용 분석 → 적용 가능한 정확 패치 스펙):
(1) state_machine.init_state(base_resistance/unlock_rate/decay_floor/ideation_baseline) → frozen dataclass OpennessParams 단일 인자 + PersonaCard.openness_params() 팩토리.
(2) orchestrator.prepare_turn / persona.build_turn_messages 의 recall_summary/pinned_facts/recent_turns/kb_behavior_cues → 공유 TurnMemory dataclass (persona.py 정의, orchestrator 재노출). TurnContext 도 memory:TurnMemory 단일 필드로.
(3) auth_sessions.upsert_managed_user / _memory_upsert_managed_user 의 7키워드(email/display_name/role/cohort_ids/user_id/affiliation/reactivate) → pydantic ManagedUserInput. (risk=medium)
(3) auth_sessions.upsert_managed_user 의 create/reactivate 키워드 묶음(email/display_name/role/admin_access/cohort_ids/user_id/external_id/affiliation/account_status/reactivate) → dataclass ManagedUserUpsertInput. `_memory_upsert_managed_user`는 DB→메모리 fallback/store 동기화용 `ManagedUserMemoryInput`, update 계열은 `ManagedUserPatch`로 분리 유지. (risk=medium)
- 근거: 세 리팩토링 모두 "동작 보존 + 인자 묶음"으로 부수효과가 없다. (1) OpennessParams 는 init_state 의 4개 키워드를 frozen dataclass 로 그대로 옮기며 계산식(compute_effective_openness/carry-over 로직)은 한 글자도 바꾸지 않았다. PersonaCard.openness_params() 는 기존 base_resistance()/unlock_rate()/decay_floor()/ideation_baseline() 접근자를 그대로 호출하는 얇은 팩토리라 값이 동일하다. persona→state_machine 은 단방향 import(state_machine 은 어떤 앱 모듈도 import 하지 않음)라 순환참조가 없다. (2) TurnMemory 는 prepare_turn 입력 4종 + TurnContext 저장 + build_turn_messages 입력을 하나로 통일해 "마스킹 1회 → 동일 객체 공유" 흐름을 명확히 한다. 마스킹은 prepare_turn 본문에서 그대로 수행되므로 PII 불변식이 유지된다. TurnContext 필드 축소로 ctx.recent_turns 를 읽던 evaluator.py·eval.py·test_rbac_idor.py 3곳을 ctx.memory.recent_turns 로 동시 갱신해 누락이 없다. (3) ManagedUserInput(pydantic) 는 upsert/_memory_upsert 가 공유하던 동일한 7키워드 중복을 제거하고, fallback 경로는 `_memory_upsert_managed_user(inp)` 로 단순화돼 email 재정규화가 함수 내부에서 멱등 처리된다. 모든 호출부(create_session, admin.create_user, 내부 3곳)를 ManagedUserInput 생성으로 교체했고 외부 엔드포인트/공개 함수(create_session) 시그니처는 불변이라 라우트 회귀가 없다. 가치: 4파라미터 init_state 5호출부, 4파라미터 prepare_turn 7호출부, 7키워드 upsert 5호출부의 시그니처 표류 위험을 타입 객체 하나로 수렴시켜 향후 파라미터 추가 시 호출부 일괄 누락 버그를 구조적으로 차단한다. medium 위험인 이유는 프로덕션 라우트(sessions/voice/admin)·평가기·인증 경로를 동시에 건드리지만, 모두 기계적 치환이고 단위테스트 77개가 경로를 커버한다.
- 근거: 세 리팩토링 모두 "동작 보존 + 인자 묶음"으로 부수효과가 없다. (1) OpennessParams 는 init_state 의 4개 키워드를 frozen dataclass 로 그대로 옮기며 계산식(compute_effective_openness/carry-over 로직)은 한 글자도 바꾸지 않았다. PersonaCard.openness_params() 는 기존 base_resistance()/unlock_rate()/decay_floor()/ideation_baseline() 접근자를 그대로 호출하는 얇은 팩토리라 값이 동일하다. persona→state_machine 은 단방향 import(state_machine 은 어떤 앱 모듈도 import 하지 않음)라 순환참조가 없다. (2) TurnMemory 는 prepare_turn 입력 4종 + TurnContext 저장 + build_turn_messages 입력을 하나로 통일해 "마스킹 1회 → 동일 객체 공유" 흐름을 명확히 한다. 마스킹은 prepare_turn 본문에서 그대로 수행되므로 PII 불변식이 유지된다. TurnContext 필드 축소로 ctx.recent_turns 를 읽던 evaluator.py·eval.py·test_rbac_idor.py 3곳을 ctx.memory.recent_turns 로 동시 갱신해 누락이 없다. (3) ManagedUserUpsertInput(dataclass) 은 upsert_managed_user 의 create/reactivate 입력 묶음을 제거한다. DB→메모리 fallback/store 동기화는 `ManagedUserMemoryInput`, `/users/me`·온보딩·관리자 수정은 `ManagedUserPatch`가 계속 소유하므로 경계가 섞이지 않는다. 모든 호출부(create_session, admin.create_user, 직접 auth 회귀 호출)를 ManagedUserUpsertInput 생성으로 교체했고 외부 엔드포인트/공개 함수(create_session) 시그니처는 불변이라 라우트 회귀가 없다. 가치: 4파라미터 init_state 5호출부, 4파라미터 prepare_turn 7호출부, upsert keyword bag 3호출부의 시그니처 표류 위험을 타입 객체 하나로 수렴시켜 향후 파라미터 추가 시 호출부 일괄 누락 버그를 구조적으로 차단한다. medium 위험인 이유는 프로덕션 라우트(sessions/voice/admin)·평가기·인증 경로를 동시에 건드리지만, 모두 기계적 치환이고 focused tests가 경로를 커버한다.
- 검증: 작업 디렉터리 D:\workspace\vignette\apps\api 기준.
1) 임포트/순환참조 sanity: `python -c "from app.services import persona, orchestrator, state_machine; from app import auth_sessions; from app.routes import admin; from app.routes import eval as _e"` (persona→state_machine 단방향 import, orchestrator→persona(TurnMemory), eval→orchestrator(TurnMemory) 가 깨지지 않는지 확인).
2) 핵심 회귀 4종: `python -m pytest app/test_state_machine_resistance.py app/test_orchestrator_masking.py app/test_session_turn_persistence.py app/test_rbac_idor.py -q` — 특히 (a) test_orchestrator_masking 의 PII 마스킹(prepare_turn 이 memory=TurnMemory 로 받아도 recall/pinned/recent 가 여전히 마스킹되는지), (b) test_state_machine_resistance 의 공감/조언 곡선 불변, (c) test_rbac_idor 의 ctx.memory.recent_turns 가 evaluator-only 비공개 발화를 제외하는지.
3) 전체 백엔드 단위테스트 77개: `python -m pytest app -q` (auth/admin/login 경로 — create_session→upsert_managed_user(ManagedUserInput), admin create_user 엔드포인트 포함 회귀 확인).
3) auth/admin/login focused: `python -m pytest app/test_auth_providers.py app/test_admin_ops.py app/test_rbac_idor.py app/test_runtime_policy.py -q` (create_session→upsert_managed_user(ManagedUserUpsertInput), admin create_user 엔드포인트 포함 회귀 확인).
4) 게이트웨이 영향 없음 확인: `python -m pytest engine_gateway -q` (7개).
모두 green 이어야 하며, 시그니처 변경분(init_state 단일 인자, prepare_turn/build_turn_messages memory 인자, upsert/_memory_upsert 의 ManagedUserInput)이 호출부와 정합하는지 import 에러/AttributeError(ctx.recall_summary 등 잔존 참조) 부재로 검증.
모두 green 이어야 하며, 시그니처 변경분(init_state 단일 인자, prepare_turn/build_turn_messages memory 인자, upsert_managed_user 의 ManagedUserUpsertInput)이 호출부와 정합하는지 import 에러/AttributeError(ctx.recall_summary 등 잔존 참조) 부재로 검증.
- affected_callers: ['init_state — apps/api/app/session_persistence.py:101', 'init_state — apps/api/app/routes/sessions.py:712', 'init_state — apps/api/app/routes/voice.py:498', 'init_state — apps/api/app/test_orchestrator_masking.py:32', 'init_state — apps/api/app/test_state_machine_resistance.py:29', 'prepare_turn — apps/api/app/routes/sessions.py:903', 'prepare_turn — apps/api/app/routes/sessions.py:979', 'prepare_turn — apps/api/app/routes/voice.py:293', 'prepare_turn — apps/api/app/test_orchestrator_masking.py:41', 'prepare_turn — apps/api/app/test_orchestrator_masking.py:132', 'prepare_turn — apps/api/app/test_session_turn_persistence.py:178', 'prepare_turn — apps/api/app/test_session_turn_persistence.py:213', 'build_turn_messages — apps/api/app/services/orchestrator.py:146 (유일 호출부)', 'TurnContext.memory(생성) — apps/api/app/services/orchestrator.py:115', 'TurnContext.memory(생성) — apps/api/app/routes/eval.py:176 (+ import 171)', 'TurnContext.memory(읽기 ctx.recent_turns) — apps/api/app/services/evaluator.py:381', 'TurnContext.memory(읽기 ctx.recent_turns) — apps/api/app/test_rbac_idor.py:285', 'upsert_managed_user — apps/api/app/auth_sessions.py:675 (create_session)', 'upsert_managed_user — apps/api/app/routes/admin.py:493 (+ import 12-18)', '_memory_upsert_managed_user — apps/api/app/auth_sessions.py:452 (upsert DB 성공)', '_memory_upsert_managed_user — apps/api/app/auth_sessions.py:466 (upsert fallback)', '_memory_upsert_managed_user — apps/api/app/auth_sessions.py:548 (update_managed_user)', 'PersonaCard.openness_params 신규 — apps/api/app/services/persona.py (P1/P2/P3 시드는 변경 불필요)']
- edits: 44건