정식 승격 상태판과 증거를 동기화

This commit is contained in:
Yun Chan 2026-08-30 00:06:45 +09:00
parent 34aff65cb0
commit be08c0b573
16 changed files with 1160 additions and 63 deletions

View file

@ -237,11 +237,25 @@ RBAC×AIView로 차단된다. 이 모듈은 평가 신호만 산출한다.
cache key·prompt·completion 없이 enabled/entries/hits/misses/stores/evictions/requests/hit_rate와
일별 `daily_cost` bucket(day, turns, tokens, cost)을 관리자 관측값으로 반환한다. 비용은
공급자가 반환한 저장값을 우선하되 Claude CLI는 SDK 추정치(`provider_estimate`)로 명시하고,
저장 비용이 없는 Agy/Gemini·Codex·Claude API는 `app.services.llm_pricing`의 버전 고정 공식 참조단가(`reference_rate`)로
입력·캐시 입력·출력 토큰을 환산한다. 기존 DB에 비용 0으로 저장된 행도 조회 시 같은 단가로
보정하며, 단가가 없는 모델은 0달러로 위장하지 않고 `unavailable`로 표시한다.
응답은 `recorded_cost_usd`, `estimated_cost_usd`, provider/model별 `cost_basis`,
`rate_label`, `rate_source_url`을 함께 반환하고 예산 상태는 두 비용을 합친 유효 비용을 사용한다.
신규 Agy/Gemini·Codex·Claude API 호출은 `app.services.llm_pricing`의 호출 시작일 기준 공식
참조단가(`reference_rate`)로 입력·캐시 입력·출력 토큰을 환산한다. Gemini 3.6/3.7 Flash는
유효기간이 있는 단가 구간을 사용하고, 기존 DB의 비용 0 행은 일별 합산 전에 요청별로 다시
가격화해 200K 같은 요청 단위 경계를 보존한다. 과거 행은 캐시 입력 수가 따로 남아 있지 않아
전체 입력 단가로 계산한 보수적 상한(`reference_upper_bound`, UI `참조 상한`)으로 표시한다.
호출별 확정 산정액과 미산정액이 섞이면 `partial``$…+`로 표시한다. 상한 추정과 미산정이
함께 있으면 방향을 단정할 수 없으므로 `partial_upper_bound``미산정`으로 표시하며, 단가가
전혀 없는 모델도 0달러로 위장하지 않고 `unavailable`로 표시한다. 양수로 저장된 과거 참조
비용은 현재 단가 라벨을 다시 붙이지 않고 `호출 시점에 저장된 참조단가 추정값`으로만 설명한다.
응답은 전체·일별·provider/model·예산에 `cost_basis`를 전파하고 `recorded_cost_usd`,
`estimated_cost_usd`, `rate_label`, `rate_source_url`을 함께 반환한다. 부분 산정·상한 상태에서는
호출당 비용·1천 토큰당 비용·비용 비중을 확정값처럼 계산하지 않으며 예산도 `indeterminate`
사용할 수 있다. DB 조회는 동일한 day/provider/model/token/cost 시그니처만 묶어 요청별 단가
경계를 보존하고, migration 18의 client-turn `created_at` partial index로 기간 조회를 제한한다.
이 인덱스는 release agent가 `CREATE INDEX CONCURRENTLY`로 트랜잭션 밖에서 적용해 턴 쓰기를
막지 않으며, 다른 schema migration은 기존 단일 트랜잭션 적용을 유지한다.
제어된 음성 E2E가 남긴 정확한 `(e2e, fake-client, input=1, output=1, cost=0)` 조합은 비용 원장·예산에서만 제외하며,
다른 `e2e` provider/model 조합이나 원본 DB 행을 포괄 삭제하지 않는다. 음성 성공 E2E의
전용 disposable PostgreSQL 격리는 후속 항목이며, 공유 DB를 직접 삭제하는 cleanup은 사용하지 않는다.
`EVALUATOR_SEMANTIC_CACHE_ENABLED`, `EVALUATOR_SEMANTIC_CACHE_TTL_SECONDS`,
`EVALUATOR_SEMANTIC_CACHE_MAX_ENTRIES`로 제한하며, 원문 prompt/completion은 캐시에 저장하지 않는다.
cache hit은 `engine.generate()`와 metadata-only `audit.llm_call_log` 기록을 건너뛰고,
@ -1053,10 +1067,29 @@ DB 레벨 이중강제(`04_audit_eval_rls.sql` §5, `app/db.py` `acquire()`):
렌더링하지 않고, 빈 상태 카드 대신 가입 직후 사용자 정보를 입력하는 단순 폼만 보여준다.
이 화면은 이메일을 다시 받지 않고 닉네임, 자기소개,
선택 아바타 이미지, 이름, 소속, 학과, 학년/직위, 연락처, 주소/수령지와 서비스 이용약관·개인정보
처리방침 초안 동의를 저장한다. 아바타 파일은 `POST /users/me/avatar`가 MIME/시그니처/3MB 제한을
확인한 뒤 `USER_UPLOAD_DIR/profile-avatars`에 저장하고 URL만 `app_user.avatar_url`에 보관한다.
처리방침 초안 동의를 저장한다. 아바타 파일은 `POST /users/me/avatar`가 MIME/시그니처/3MB 제한에 더해
Pillow full decode와 확장자-format 일치를 확인한 뒤 `USER_UPLOAD_DIR/profile-avatars`에 저장하고 URL만
`app_user.avatar_url`에 보관한다. 시그니처만 닮고 실제 decode가 실패하는 payload는 저장 전에 거부한다.
학습자 `POST /sessions`와 dev 음성 persona 시작은 온보딩 완료 후에만 허용하며, 온보딩 저장 시
learner `consent_at`도 함께 세팅한다.
- public avatar storage는 `USER_UPLOAD_DIR/profile-avatars``/uploads/profile-avatars`에 mount한다. 같은
`USER_UPLOAD_DIR``multimodal-audio`는 private adapter 전용이며 static mount와 public migration에서 제외한다.
public runtime은 source/upload root 밖 private migration receipt(SHA/root binding)와 현재 DB의 모든 managed avatar
참조 파일(regular/no-reparse/readable)을 기동마다 검증한다. 초기 8개 migration snapshot과 현재 set은 동일할 필요가
없지만, non-empty initialization 뒤 current set이 0이 되려면 별도 reset receipt가 필요하다.
- upload manifest v3는 object count뿐 아니라 전체 byte 합계·inventory SHA-256·객체별 `decode_valid`와 preserved/current
decode 정상/실패 count를 결속한다. immutable public cache에는 decode-valid bytes만 올리고, preserved이지만 decode-invalid인
legacy payload는 forensic 보존하되 static handler가 404로 차단해 프론트 fallback을 사용하게 한다. 경로 존재·PNG signature만으로
정상 이미지를 선언하지 않는다. manifest 이후 생성된 UUID형 업로드도 DB URL 기록·현재 참조 검증·GET/HEAD마다 용량 제한과
Pillow full decode·확장자-format 일치를 다시 확인하고, 검증한 동일 bytes를 응답해 검사 뒤 재오픈 경쟁을 막는다. 이후 손상된
신규 파일은 current invalid count와 health fallback에 반영되고 public 404가 된다. 2026-08-29 감사 기준은 union 93 objects·
52,973 bytes·`9d703126…e6aa`, decode 정상 3/
실패 90, 현 DB 참조 정상 2/실패 6/missing 0이다. 이 계약은 후보 코드이며 public API 장애 복구·전체 E2E·정식 승격 전에는
deployed runtime의 동작으로 간주하지 않는다.
- 프로필 업로드는 새 random target을 create-new 방식으로 완전히 flush한 뒤 publish하고 기존 DB 참조 파일을 선삭제하지
않는다. upload 뒤 PATCH가 실패해도 기존 URL의 파일은 남으며, unreferenced object 정리는 별도 manifest-aware GC가
소유한다. fresh storage cutover는 API write-lease freeze가 active/valid/drained임을 증명하고 tunnel-first로 외부 write를
닫은 뒤 진행한다.
- 관리자/교수자 권한은 온보딩 화면에서 신청받지 않는다. 서버는 `AUTH_ADMIN_EMAILS` /
`AUTH_TEACHER_EMAILS` allowlist와 관리자 사용자 관리 경로로만 역할을 부여한다.
- 로컬/Tailnet dev는 dev-login을 사용한다. 공개 `OAUTH_REDIRECT_URI`가 로컬/Tailnet 세션이 아니라 public API

View file

@ -91,13 +91,29 @@ prod API 8001, web preview 5174, engine gateway 9099, cloudflared tunnel을 검
worktree를 만들고, 그 root 내부 registrar에 `-StableSourceRoot`를 명시해 두 task를 승격한다.
$releaseRoot = 'D:\workspace\vignette-public-runtime-<commit>'
$userUploadDir = Join-Path $env:LOCALAPPDATA 'Vignette\public-runtime\uploads'
$uploadStateDir = Join-Path $env:LOCALAPPDATA 'Vignette\public-runtime\private-state'
$uploadManifestSha = '<initializer가 출력한 64자리 sha256>'
$uploadManifestPath = Join-Path $uploadStateDir "public-avatar-upload-$uploadManifestSha.json"
$uploadFreezePath = Join-Path $uploadStateDir 'avatar-cutover.freeze.json'
$bootRegistrar = Join-Path $releaseRoot 'scripts\register-boot-task.ps1'
& powershell.exe -NoProfile -ExecutionPolicy Bypass -File $bootRegistrar -StableSourceRoot $releaseRoot
& powershell.exe -NoProfile -ExecutionPolicy Bypass -File $bootRegistrar `
-StableSourceRoot $releaseRoot -UserUploadDir $userUploadDir `
-UserUploadManifestPath $uploadManifestPath `
-ExpectedUserUploadManifestSha256 $uploadManifestSha `
-UserUploadWriteFreezePath $uploadFreezePath
$watchdogInstaller = Join-Path $releaseRoot 'scripts\install-public-runtime-task.ps1'
& powershell.exe -NoProfile -ExecutionPolicy Bypass -File $watchdogInstaller -StableSourceRoot $releaseRoot -IntervalMinutes 5
& powershell.exe -NoProfile -ExecutionPolicy Bypass -File $watchdogInstaller `
-StableSourceRoot $releaseRoot -UserUploadDir $userUploadDir `
-UserUploadManifestPath $uploadManifestPath `
-ExpectedUserUploadManifestSha256 $uploadManifestSha `
-UserUploadWriteFreezePath $uploadFreezePath -IntervalMinutes 5
- `VignettePublicRuntime`은 로그온 Docker/DB 복구, `VignettePublicRuntimeWatchdog`은 로그온+5분 반복 health/recovery다.
- 두 task action은 release root와 Git commit/tree, boot 또는 watchdog SHA-256, start script SHA-256을 고정한다.
- 두 task action은 release root와 Git commit/tree, boot 또는 watchdog SHA-256, start script SHA-256,
source 밖의 절대 `USER_UPLOAD_DIR`, private migration manifest SHA, source/upload root 밖의 freeze path를 고정한다.
consumer는 빈 upload root를 만들지 않으며 watchdog `-CheckOnly`는 정확한 listener PID의 cwd/env와 현재 DB가 참조하는
모든 profile avatar의 존재·regular/no-reparse/readability를 다시 증명한다.
등록 뒤 action marker와 working directory를 읽어 검증하기 전에는 `RunNow`를 호출하지 않는다.
- secret은 task 인자에 넣지 않는다. API secret은 stable release의 `apps/api/.env`, cloudflared/Claude CLI credential은
사용자 profile에 둔다. `.env` 내용을 console이나 evidence에 출력하지 않는다.
@ -368,8 +384,13 @@ C:\Users\encep\AppData\Local\Programs\Python\Python311\python.exe scripts\mainta
- 신규 사용자 또는 온보딩 미완료 사용자는 로그인 직후 `/onboarding`에서 닉네임, 자기소개,
선택 아바타 이미지, 이름, 소속, 학과, 학년/직위, 전화번호, 주소/수령지와 약관·개인정보
동의를 저장해야 역할 홈으로 이동한다. 학습자 회기 시작은 온보딩 완료와 동의가 모두 있어야 한다.
- 프로필 아바타 업로드는 API 작업 디렉터리 기준 `USER_UPLOAD_DIR`(기본 `uploads`) 아래
`profile-avatars/`에 저장되고, `/uploads/profile-avatars/...` URL로 서빙된다.
- 프로필 아바타 업로드는 `USER_UPLOAD_DIR` 아래 `profile-avatars/`에 저장되고,
`/uploads/profile-avatars/...` URL로 서빙된다. 로컬 dev에서 변수를 생략하면 API 작업 디렉터리 기준
`uploads`를 쓰지만, Windows public runtime은 release root 밖의 절대
`%LOCALAPPDATA%\Vignette\public-runtime\uploads`를 task와 API process environment에 명시한다. API는 전체 root가
아니라 `profile-avatars`만 static mount하므로 private `multimodal-audio``/uploads`로 노출되지 않는다. public
runtime의 manifest/freeze는 upload root 밖 private state directory에 두며, 정확한 초기화·cutover 순서는
`docs/ops/public-runtime-watchdog.md`를 따른다.
> 주의(이메일 도메인): 이 제한은 dev-login·SAML 조직 정책에만 적용된다. dev-login은
> 이메일 도메인이 `AUTH_ALLOWED_EMAIL_DOMAINS`에 있어야 한다. 예: `learner@hs.ac.kr`.

View file

@ -72,7 +72,7 @@
| **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 파이프라인 2차 구현 | `scripts/export-recursive-dataset.py``app.services.dataset_export`로 masked-text JSONL dry-run, PII scan, kappa/ICC 계산, approved export 게이트를 구현했다. exporter는 consent가 남아 있고 client-visible인 `text_masked` 턴만 고르며, supervisor comment raw text를 JSONL에 넣지 않는다. `scripts/check-phase3-artifacts.py`는 approved와 dry-run dataset JSONL의 required keys, row count, privacy, PII shape를 검사해 `{}` 한 줄 같은 false-positive를 막는다. 기본은 `technical_dry_run`이며 실제 승인 export·골든셋 승격은 데이터 steward/legal review와 IAA 통과가 필요하다. | 파일럿 evidence에서 reviewer disposition, steward/legal 승인, gold annotation 라운드 적재, withdrawal/consent roster 대조, `min_completed_sessions` 정책 반영 후 `approved_for_recursive_learning_seed` 승격 검증. |
| **X2** | AI API 비용 관측·예산 경고·평가 저비용 라우팅·evaluator cache 관측·일별 비용 추이·모델별 비용 검증 리포트 3차 완료 | 턴별 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을 산출한다. 3차에서는 Claude CLI SDK 비용 추정값을 우선하고, 비용이 없는 Agy/Gemini·Codex·Claude API는 버전 고정 공식 참조단가로 입력·캐시 입력·출력 토큰을 환산한다. Claude 토큰은 `modelUsage` 전체 agent tree와 cache read/create 입력을 합산한다. 과거 미수집 442건은 비용 역산 없이 로컬 Claude JSONL의 정규화 응답 SHA-256·생성 시각이 유일하게 일치한 169건만 실제 usage로 백필했고, 불일치 273건은 `미계량`으로 남겼다. 기존 DB의 0달러 행도 조회 시 보정하고, 단가 미등록 모델은 `미산정`으로 명시한다. DB 미가용 dev는 runtime store fallback, prod는 fail-closed다. | 자동 차단·한도 enforcement 정책, 공식 단가 변경 시 rate card 갱신·회귀. |
| **X2** | AI API 비용 관측·예산 경고·평가 저비용 라우팅·evaluator cache 관측·일별 비용 추이·모델별 비용 검증 리포트 4차 완료 | 턴별 provider/model/tokens/cost 저장 경로와 `GET /admin/usage`, 관리자 비용 대시보드를 연결했다. `ADMIN_USAGE_BUDGET_USD` 기준 예산 상태(ok/warn/exceeded/indeterminate)도 응답/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별 token share와 계량 coverage를 산출하며, cost share/cost-per-unit은 비용 확실성이 보장될 때만 산출한다. Claude CLI SDK 추정값을 우선하고 Agy/Gemini·Codex·Claude API는 호출 시작일 기준 공식 참조단가를 쓴다. 4차에서는 Gemini 3.6/3.7 Flash의 출시일·2026-08-13 프로모션·2027-01-01 표준 단가를 유효기간으로 분리하고, 과거 0달러 행을 동일 계량 시그니처별로 묶어 요청별 가격화해 입력 200K 단가 경계를 보존한다. 캐시 토큰이 별도 보존되지 않은 과거 보정액은 `reference_upper_bound`, 확정 산정/미산정 혼합은 `partial`, 상한/미산정 혼합은 `partial_upper_bound`, 완전 미산정은 `unavailable`로 구분한다. 전체·일별·예산에도 같은 `cost_basis`를 전파한다. 정확한 `(e2e, fake-client, input=1, output=1, cost=0)` 테스트 조합만 보고서에서 제외한다. migration 18은 client turn 기간 조회용 partial index를 `CREATE INDEX CONCURRENTLY`로 트랜잭션 밖에서 추가한다. Claude 토큰은 `modelUsage` 전체 agent tree와 cache read/create 입력을 합산한다. 과거 미수집 442건은 비용 역산 없이 로컬 Claude JSONL의 정규화 응답 SHA-256·생성 시각이 유일하게 일치한 169건만 실제 usage로 백필했고, 불일치 273건은 `미계량`으로 남겼다. DB 미가용 dev는 runtime store fallback, prod는 fail-closed다. | 자동 차단·한도 enforcement 정책, 공식 단가 변경 자동 감시, `priced_at`/`rate_id`/캐시 입력 토큰의 영속 provenance, 음성 E2E 전용 disposable DB 격리. |
| **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 비용'을 운영 리스크로 명시.

View file

@ -15,12 +15,12 @@ Vignette 저장소의 모든 검증 수단(백엔드 단위 테스트, 웹 타
| 검증 | 작업 디렉터리 | 명령 | DB | API(8000) | 웹(5173) | 엔진GW(9099) | 브라우저 | 현재 통과 |
|---|---|---|---|---|---|---|---|---|
| 백엔드 단위 테스트 | `apps/api` | `python -m pytest app/ -q` | 불필요 | 불필요 | 불필요 | 불필요 | 불필요 | 2026-08-28 전체 실행 1002 passed |
| 백엔드 단위 테스트 | `apps/api` | `python -m pytest app/ -q` | 불필요 | 불필요 | 불필요 | 불필요 | 불필요 | 2026-08-29 전체 실행 1074 passed / 1 skipped |
| 엔진 게이트웨이 테스트 | `apps/api` | `python -m pytest engine_gateway/ -q` | 불필요 | 불필요 | 불필요 | 불필요 | 불필요 | 2026-08-28 전체 실행 68 passed |
| API 타입 생성 체크 | `apps/web` | `npm run check:api-types` | 불필요 | 불필요 | 불필요 | 불필요 | 불필요 | pass |
| 웹 타입체크 | `apps/web` | `npm run typecheck` | 불필요 | 불필요 | 불필요 | 불필요 | 불필요 | pass |
| 웹 빌드 | `apps/web` | `npm run build` | 불필요 | 불필요 | 불필요 | 불필요 | 불필요 | pass |
| Playwright E2E(전체) | `apps/web` | `npm run e2e` | **필요(+시드)** | **필요** | 자동기동 | 일부만 | **필요** | 현재 수집 1138 tests / 62 files · 현 작업트리 전체 GREEN 미검증 |
| Playwright E2E(전체) | `apps/web` | `npm run e2e` | **필요(+시드)** | **필요** | 자동기동 | 일부만 | **필요** | 현재 수집 1158 tests / 64 files · 현 작업트리 전체 GREEN 미검증 |
핵심 원칙: **단위 테스트(pytest)와 타입체크/빌드는 외부 서비스 없이 단독 실행된다.**
**E2E만 풀스택(DB+API+웹+브라우저)을 요구한다.** 아래 각 절에서 근거와 절차를 설명한다.
@ -42,7 +42,7 @@ Vignette 저장소의 모든 검증 수단(백엔드 단위 테스트, 웹 타
```sh
# apps/api
python -m pytest app/ -q # 앱 단위 테스트: 2026-08-28 전체 실행 1002 passed
python -m pytest app/ -q # 앱 단위 테스트: 2026-08-29 전체 실행 1074 passed / 1 skipped
python -m pytest engine_gateway/ -q # 게이트웨이 단위 테스트: 2026-08-28 전체 실행 68 passed
```
@ -80,12 +80,12 @@ python -m pytest engine_gateway/ --collect-only -q
#### 개선관리 워크북 focused 증거 (2026-08-27~28)
아래는 전체 1002/68과 별도로 해당 계약을 좁혀 실행한 확정 증거다. 같은 묶음의 테스트가 여러 요구사항
아래는 전체 API 1074 passed/1 skipped·gateway 68 passed와 별도로 해당 계약을 좁혀 실행한 확정 증거다. 같은 묶음의 테스트가 여러 요구사항
경계를 함께 검증할 수 있으므로 숫자를 요구사항별 전체 합계로 더하지 않는다.
| 항목 | focused 명령/파일 | 확인 결과 |
|---|---|---|
| C-001 기술 사전검증 | `scripts/check-clinical-crisis-review.py`, `run-clinical-crisis-technical-observations.py`, checker/client reply/source pack/state machine/session focused pytest | 90 passed + Ruff PASS(Starlette 제3자 경고 1건) — P1 생성·스트림 실제 경로 6/6, v2 사례 6건과 런타임 패키지 8개가 HEAD·작업트리 SHA에 일치한다. DB direct load·baseline·carry-over·observed 값을 `1..3`으로 정규화하고 과상한 출력은 재생성하며, 수련생 실제 위기는 엔진 전 중단·109, 내담자 수단 상세는 차단한다. 6시트 개선관리 완료본은 수식 오류 0, 10 완료/1 검토/총 11, canonical gate `pending-valid`다. 사례 30필드·청소년 6답변·자격 포함 승인 10필드·서명 증거가 비어 있어 `review_complete=false`; 외부 임상 승인을 대체하지 않음 |
| C-001 기술 사전검증 | `scripts/check-clinical-crisis-review.py`, `run-clinical-crisis-technical-observations.py`, checker/client reply/source pack/state machine/session focused pytest | 90 passed + Ruff PASS(Starlette 제3자 경고 1건) — P1 생성·스트림 실제 경로 6/6, v2 사례 6건과 런타임 패키지 8개가 HEAD·작업트리 SHA에 일치한다. DB direct load·baseline·carry-over·observed 값을 `1..3`으로 정규화하고 과상한 출력은 재생성하며, 수련생 실제 위기는 엔진 전 중단·109, 내담자 수단 상세는 차단한다. 6시트 개선관리 완료본은 수식 38개·수식 오류 0, 9 완료/2 검토/총 11, canonical gate `pending-valid`다. REQ-001은 기존 Gmail 인증 세션·관리자 권한·복구 데이터 가시성 수락으로 완료했고, REQ-008 실제 Windows 재부팅 smoke와 C-001 사례 30필드·청소년 6답변·자격 포함 승인 10필드·서명 증거만 남아 있으며 외부 임상 승인을 대체하지 않음 |
| C-002 | `app/test_first_session_checklist.py` | 3 passed — 첫 회기 라포 반영·한 초점 열린 질문, evidence turn, 이후 회기 not-applicable |
| C-003 | `app/test_learner_dashboard.py` | 11 passed — 종료 회기 4건 전에는 insufficient, 이후 dominant share 0.75 경계 |
| REQ-001 | `app/test_auth_providers.py`, `app/test_access_logging.py`, OAuth UI focused E2E, `e2e/admin.spec.ts`/`e2e/uc-admin-console.spec.ts` | auth 41 + access-log redaction 6 pytest, OAuth UI desktop/mobile 4, route-mock browser 2, 실제 DB/browser 1 passed — 모든 provider-verified Google 이메일을 도메인·사전등록 없이 learner·approved로 허용하고 suspended는 보존. callback query는 Uvicorn access log에서 제거. 관리자 사전등록 create는 pending 고정이며 승인 전 `/personas` 403·`/learn→/pending`, 별도 PATCH 승인 뒤 같은 세션 `/personas` 200, `learner_feedback_enabled=false` 영속 재조회, 비활성화·활성 세션 0 |
@ -99,10 +99,12 @@ python -m pytest engine_gateway/ --collect-only -q
| REQ-008 | `app/test_engine_health_contract.py`와 session stream 회귀 | health contract 1 passed; incomplete EOF는 live/session 50 묶음에 포함 |
| 학습자 성장 패널 헤더 | `uc-learner-home-dashboard.spec.ts --grep "성장 지표 라포 헤더"` + `check:design-ssot` + `check:cosmetic-filter-safety` + typecheck | chromium desktop/mobile 2 passed — 선택된 라포 문구의 부모는 plain `div`, 배경 transparent·border/radius/padding 0이며 주변 panel 구조는 유지 |
| 교수자 검토·요약 카드 간격 | `full-sweep-professor.spec.ts --grep "renders six KPI cards"` + `layout-visual-gate.spec.ts` + `session-layout.spec.ts` | 기존 0px에서 회귀 게이트 RED를 확인하고 `--sp-3`=12px로 교정했다. focused 1 passed, 7폭 교수자 gate 1 passed, 전체 layout visual 15 passed, session 무회귀 8 passed. Pages `0c60261e…` 승격 뒤 실제 `/teach`는 computed/실측 12px, overflow 0, console warning/error 0이다. |
| REQ-001 공개 운영 | `https://vignette.chanpaca.net/login` + Google account chooser/callback | `allowed_email_domains=[]`이며 기존 일반 Google 계정 callback→`/admin` 성공으로 가입 범위는 확인했다. Pages production `ef48c0ae…` 승격 뒤 custom domain은 단일 `Google 계정으로 로그인` CTA 1개·구 계정 선택 버튼 0·dev login 0·console warning/error 0이고 격리 Playwright OAuth 시작 검증 **1 passed**다. 실제 Gmail callback과 복구 계정의 `/admin`·기존 데이터 확인 전에는 검토를 유지한다. |
| Google 계정 데이터 복구 | migration 19, alias/auth focused, 전체 API·gateway, 백업 복제 DB, 활성 DB·공개 API | Gmail의 빈 중복 사용자와 canonical 관리자 계정을 이메일 자동병합 없이 명시적 Google subject 별칭 2개로 연결했다. 정식 계정 21회기·64턴·평가 6건과 Gmail 원본 설정 1행을 보존하고 source-only 알림 키를 canonical 설정에 무손실 병합했다. Gmail 옛 세션 1개 철회, 중복 계정 suspended, 감사로그 1건, 사용자 1175·회기 469·평가 65와 고아 0을 확인했다. API+gateway **1111 passed/1 skipped**, release agent **31 passed**, 백업 복제 DB 트랜잭션·cleanup 및 런타임 DB 역할 alias SELECT를 통과했다. 실제 Gmail OAuth callback은 사용자 계정 선택 후 최종 확인한다. |
| REQ-001 공개 운영 | `https://vignette.chanpaca.net/login`, `/settings`, `/learn/history`, `/admin/continuous-improvement` | 2026-08-28 증거에서 `allowed_email_domains=[]`, Pages `0c60261e…`의 단일 Google CTA·공개 Playwright 2 passed, 기존 인증 세션의 Gmail 로그인 이메일·관리자 콘솔·온보딩 비전환·복구 이력 20건/리뷰 필요 6건을 확인했고 소유자가 데이터 가시성을 수락해 REQ-001을 완료했다. 2026-08-29 public API 530은 별도 현재 장애이며, 이 과거 수락 증거를 current health GREEN으로 재사용하지 않는다. |
| Google 계정 데이터 복구 | migration 19, alias/auth focused, 전체 API·gateway, 백업 복제 DB, 활성 DB·공개 API·기존 인증 브라우저 세션 | Gmail의 빈 중복 사용자와 canonical 관리자 계정을 이메일 자동병합 없이 명시적 Google subject 별칭 2개로 연결했다. 정식 계정 21회기·64턴·평가 6건과 Gmail 원본 설정 1행을 보존하고 source-only 알림 키를 canonical 설정에 무손실 병합했다. Gmail 옛 세션 1개 철회, 중복 계정 suspended, 감사로그 1건, 사용자 1175·회기 469·평가 65와 고아 0을 확인했다. API+gateway **1111 passed/1 skipped**, release agent **31 passed**, 백업 복제 DB 트랜잭션·cleanup 및 런타임 DB 역할 alias SELECT를 통과했다. 후속 기존 인증 브라우저 세션에서 Gmail 로그인 이메일·관리자 권한·복구 데이터 가시성을 확인했다. |
| REQ-005·007·008 공개 운영 | P20 회기 `03dddadd-adf3-4922-acbf-31002470da52` | 생성→자기점검 원장 잠금→실제 학습자/AI 내담자 2턴→종료→AI 평가 완료→P20 1회 기록 영속→다음 회기 버튼 재노출 |
| 운영 배포·복구 | live API clean `dba9b75a…`·tree `14cd4607…`, Pages `ef48c0ae-374e-41fc-b6fd-03f69b26e946`; boot/watchdog 동일 pin | fresh API+cloudflared provenance receipt SHA `186529be…f7a4`, 실제 API cwd가 새 detached root이며 local/public prod·db·engine·voice, OpenAPI 129, auth 401이 정상이다. boot/watchdog 실제 실행은 `LastTaskResult=0`·failcount 0이고 custom domain Pages entry SHA는 clean build와 일치한다. 배포별 preview origin은 정적 자산만 일치하고 연결 진단을 표시하므로 runtime GREEN이 아니며, 실제 Windows 재부팅 smoke는 미실행이다. |
| 아바타 decode/fallback 후보 | `app/test_upload_storage_contract.py`, `scripts/test_initialize_public_runtime_upload_root.py`, `initialize-public-runtime-upload-root.py probe` | source union 실측은 93 objects·52,973 bytes·inventory SHA-256 `9d703126…e6aa`, decode 정상 3/실패 90이며 현 DB 참조는 정상 2/실패 6/missing 0이다. 후보는 manifest v3 decode 상태·합계·digest를 fail-closed로 결속하고 invalid static 응답을 404/fallback으로 보낸다. focused 단위/probe 증거와 전체 API/Web/PowerShell/E2E·정식 배포 증거를 분리한다. |
| G8 사람 게이트 현재 상태 | `e2e/continuous-improvement-admin.spec.ts`, `e2e/continuous-improvement-live.spec.ts`, Codex 내장 브라우저 | typecheck와 route-fixture desktop/mobile **10/10** 통과, 3.5초/8초 watchdog overlay 0이다. 격리 local stateful in-app browser에서도 8.5초 뒤 overlay 0·heading 정상, content `keep_quarantine` 뒤 effect 0, 증거 없는 release 승인 disabled→`reject` 뒤 effect 0, 증거 4종 promote 승인 뒤 lifecycle effect 정확히 1을 확인했다. 이 local proof는 GREEN이지만 실DB append-only 결정과 public proof는 아직 남아 있다. |
| 운영 배포·복구 | deployed API/task baseline `dba9b75a…`·tree `14cd4607…`, Pages `0c60261e-cb37-482d-ba42-d91586194c48` | 2026-08-29 최신 read-only 상태는 local 8001 연결 거부·public API 530·Docker daemon 부재다. 정적 public Web 200과 2026-08-28 green receipt는 현재 API/DB GREEN 증거가 아니다. avatar 후보는 미배포이며, 전체 통합 GREEN·후보 SHA·사용자 승인 전에는 runtime/task/DB/upload-root를 바꾸지 않는다. 실제 Windows 재부팅 smoke도 미실행이다. |
로컬 Playwright는 기존 포트의 서버를 기본 재사용하지 않는다. 공개 preview나 다른 checkout의 stale bundle을
현 작업트리 증거로 오인하지 않도록 충돌 시 fail-closed하며, 동일 dev server를 의도적으로 공유할 때만
@ -307,7 +309,7 @@ VITE_API_BASE=http://127.0.0.1:8000 npm run e2e # 프록시 대신 API
### 3.6 실측 테스트 개수 (현재)
2026-08-28 `npx playwright test --list` 기준 **현재 수집 1138 tests / 62 files**다
2026-08-29 `npx playwright test --list` 기준 **현재 수집 1158 tests / 64 files**다
(유스케이스 16테마 `uc-*.spec.ts` 239 시나리오 포함).
이 숫자는 수집량이지 통과량이 아니다. 현 작업트리 전체 1109개 완주는 아직 증거가 없으며,
과거 전체 GREEN 기록과 이번 focused/release gate 결과를 구분해 적는다.
@ -553,7 +555,9 @@ hourly heartbeat는 최신 GREEN이 6시간 이상 오래됐거나 material mile
- 2026-07-15 보태니컬 글래스 UI·SSOT 검증: `npm run check:design-ssot` + `npm run typecheck` + `npm run build` 통과, `npx playwright test e2e/layout-visual-gate.spec.ts --project=chromium-single-run --reporter=line` 시각 게이트 **15/15**, `npx playwright test e2e/auth-visual.spec.ts --project=chromium-single-run --reporter=line` **1/1**, `npx playwright test e2e/session-layout.spec.ts e2e/learner.spec.ts e2e/session-review.spec.ts --project=chromium-desktop --project=chromium-mobile` **46/46**, 로그인→온보딩 focused desktop/mobile **2/2**. 공통 AppShell GNB, Theme store, Surface variant의 소유권과 전 폭 대시보드/리뷰 탭, 로그인·온보딩 라이트/다크 390/1280px, 고해상도 보태니컬 자산을 함께 고정한다. 라이트 테마도 패널당 복수 굴절 그라데이션, backdrop blur, 헤어라인, 그림자를 computed style로 단언하며 모바일 셸이 보태니컬 배경을 제거하지 않는지 검사한다. 관리자 사용자 표는 semantic table, 정렬 헤더, 1440px 최소 폭과 표 전용 가로 스크롤을 desktop/mobile에서 검증한다.
- 2026-07-31 활성 세션 보태니컬 워크스페이스 검증: `full-sweep-session.spec.ts`의 1536×1024 계약이 좌 326px·우 357px 레일, 1408px 상단/본문, 1468px 하단 제어바, 좌우/스테이지 보태니컬 WebP 연결과 라이트 테마를 실측한다. 세션 전수 desktop/mobile **32 passed / 2 skipped**, `session-layout` **8/8**, 7폭 `layout-visual-gate` **15/15**, `check:design-ssot`·typecheck·build를 통과했다. 1366×768 이하는 스테이지보다 자막이 작아지지 않게 별도 압축 계약을 적용한다.
- 2026-07-31 관리자 Provider·모델별 비용 원장 검증: `python -X utf8 -m pytest -p no:cacheprovider app/test_llm_pricing.py app/test_admin_ops.py app/test_usage_report.py engine_gateway/test_provider_registry.py engine_gateway/test_gateway_model.py -q` **66 passed**. Claude CLI SDK 비용 추정값과 Agy/Gemini·Codex·Claude API 공식 참조단가 추정을 분리하고, 기존 DB의 0달러 Agy 행을 조회 시 재산정하며, 단가 미등록 모델은 `미산정`으로 남기는 계약을 고정했다. `npm run generate:api-types`·`check:api-types`·typecheck·build를 통과했고, 관리자 AI desktop/mobile focused E2E **2 passed**와 7폭 focused layout visual gate **1 passed**에서 Gemini 원장 `$0.06`·`참조단가` 표시와 레이아웃 containment를 확인했다. 공개 API 프로세스를 재시작하고 Cloudflare Pages production `11b3e11f`에 배포했다. 인증된 공개 `/admin/usage?window_days=30``source=database`, `durable=true`, `gemini-3.6-flash-high` 5호출·입력 35,703·출력 1,129·참조단가 `$0.062022`를 반환했고, 검증용 인증 세션은 즉시 삭제했다. 커스텀 도메인의 `AdminAi-BfxMUlzj.js``AdminAi-XHkda_d4.css`는 올바른 JavaScript/CSS MIME으로 200을 반환한다.
- 2026-08-28 Gemini 3.7 비용 미산정 회귀 검증: screenshot과 같은 `gemini-3.7-flash-high` 21회·입력 115,950·출력 4,407의 기존 0달러 원장을 2026 프로모션 기준 `≤$0.103489`(`reference_upper_bound`)로 보정한다. Gemini 3.6/3.7 출시·프로모션·2027 경계, 요청별 200K 단가 구간, 일부 미산정 `$…+`, 상한+미산정 `partial_upper_bound``미산정`, 저장된 과거 비용의 라벨 불변, 정확한 `(e2e, fake-client, input=1, output=1, cost=0)` 제외를 회귀화했다. 전체·일별·예산에 비용 확실성을 전파하고 불확실한 평균·비중은 숨긴다. 동일 계량 시그니처 SQL 집계와 migration 18의 client-turn 기간 인덱스를 고정했으며, release agent는 이 파일만 `CREATE INDEX CONCURRENTLY`로 트랜잭션 밖에서 적용한다. focused **105 passed / 1 skipped**, `app engine_gateway` 전체 **1102 passed / 1 skipped**, 새 빈 PostgreSQL에서 온라인 마이그레이션+통합 **1 passed**, release agent **31 passed**, API type generation/check·typecheck·build, 관리자 AI desktop/mobile **6 passed**를 확인했다. 통합 컨테이너와 테스트 DB는 `--rm`으로 정리했으며, 공개 배포·운영 DB 데이터 삭제는 하지 않았다.
- 2026-08-29 Google 계정 데이터 복구 검증: migration 19가 owner-managed `auth_identity_alias`와 세션의 별도 `login_email`을 추가한다. Gmail 중복 계정은 학습 데이터 0건이지만 기본 설정 1행이 있어 최초 가드가 rollback했고, 전체 app_user FK를 전수 스캔한 뒤 canonical 설정과 겹치는 값이 동일함을 확인했다. 개정 트랜잭션은 source-only `account_approval` 알림 키를 canonical 설정에 합치고 원본 설정 행은 보존한다. 백업 복제 DB에서 alias 2·세션 21·턴 64·평가 6·활성 source session 0·고아 0으로 통과 후 복제 DB를 삭제했고, 활성 DB도 사용자 1175·회기 469·평가 65·고아 0을 유지했다. 공개 API fresh provenance는 commit `dba9b75a…`·tree `14cd4607…`, health DB/engine true, voice exact, OpenAPI 129, auth 401이며 boot/watchdog 실제 실행 결과는 0/0이다. Pages production `ef48c0ae…`의 custom domain은 clean build entry·login chunk SHA와 일치하고 단일 Google CTA E2E 1건을 통과했다. 실제 Gmail callback은 브라우저 계정 선택을 기다린다.
- 2026-08-29 REQ-001 완료·개선관리 재생성 검증: 기존 실서비스 인증 세션의 `/settings`에서 `yunchan8804@gmail.com`, 관리자 메뉴와 `/admin/continuous-improvement` 접근, 온보딩 비전환을 확인했고 `/learn/history`는 회기 20건·리뷰 필요 6건을 표시했다. 소유자가 데이터 가시성을 수락했으며, 최신 Pages `0c60261e…`의 단일 Google CTA·무제한 도메인 계약·공개 Playwright 2 passed와 결합해 REQ-001을 완료로 승격했다. 완료본은 artifact-tool import→export→reimport로 6시트·11개 고유 요구·38수식·수식 오류 0·완료 9/검토 2를 확인했다. C-001은 패키지 8/8·기술 사례 6/6·canonical checker 정상의 `pending-valid`지만 외부 입력 46칸을 공란으로 보존했고, REQ-008은 실제 Windows 재부팅 smoke 전까지 검토다. 완료본 SHA는 `c832547f…d8bd`, sidecar SHA는 `8c9618f0…a610`이다.
- 2026-07-31 Claude CLI 토큰 원장 focused 검증: result `modelUsage`의 전체 agent tree를 합산하고 최상위 `usage` 폴백, cache read/create 입력 포함, generate·SSE done 전파를 회귀화했다. 전체 backend **436 passed**, gateway **45 passed**, API type generation/check·typecheck·build, 관리자 AI desktop/mobile **2 passed**. 신규 live Opus generate는 입력 **31,918**·출력 **4**·비용 추정 `$0.124862`, Haiku SSE done은 입력 **30,450**·출력 **159**·비용 추정 `$0.061685`를 반환했다. 운영 DB 30일 원장의 과거 Claude 행은 역산하지 않고 `tokens=미계량`, `token_unmetered_turns=194`, `cost_basis=provider_estimate`로 분리됨을 임시 인증 세션으로 확인하고 세션을 즉시 폐기했다.
- 2026-07-31 Claude CLI 과거 토큰 백필 검증: 로컬 Claude JSONL **3,908개**의 assistant usage record **3,848개**를 읽되 본문을 출력하지 않고, 정규화 응답 SHA-256과 DB 생성 시각 창(-30초~+180초)이 모두 일치하며 후보가 정확히 1개인 턴만 복구했다. 운영 DB의 과거 0/0 Claude 턴 **442건****169건**을 실제 입력 **5,179,999**·출력 **46,910** 토큰으로 갱신했고, 불일치 **273건**은 계속 `미계량`, 모호한 후보는 **0건**이었다. apply는 `--expected-matches 169` 가드와 행별 `UPDATE 1` 확인을 통과했고, 적용 후 dry-run 재실행에서 추가 exact match **0건**을 확인했다. 전체 backend **439 passed**, gateway **45 passed**이며, 공개 관리자 브라우저 **1 passed**에서 Claude 행의 실제 토큰과 부분 계량 `125/183회` 표시를 확인하고 임시 세션을 폐기했다.