diff --git a/apps/web/e2e/returned-practice-db-closed-loop.spec.ts b/apps/web/e2e/returned-practice-db-closed-loop.spec.ts index 9daa834..fe45266 100644 --- a/apps/web/e2e/returned-practice-db-closed-loop.spec.ts +++ b/apps/web/e2e/returned-practice-db-closed-loop.spec.ts @@ -238,7 +238,7 @@ test.describe("returned-practice browser-only DB closed loop", () => { const firstButton = card.getByRole("button", { name: isDesktop(testInfo) ? "이번 회기를 독립 관찰로 반영" - : "이번 회기를 독립 관찰로 반영", + : "반영 상태 다시 확인", }); const postUrl = `/api/practice/${encodeURIComponent( diff --git a/apps/web/e2e/session-review.spec.ts b/apps/web/e2e/session-review.spec.ts index 2b4e9ce..2eaf827 100644 --- a/apps/web/e2e/session-review.spec.ts +++ b/apps/web/e2e/session-review.spec.ts @@ -625,6 +625,18 @@ test.describe("session review", () => { }; response.summary = "평가 AI는 [NAME]가 [ORG]에서 보인 조심스러운 반응을 분석했습니다."; + response.goodMoments[0] = { + ...response.goodMoments[0], + title: "[NAME]가 보인 강점", + body: "[ORG] 장면에서 내담자 [NAME]을 기다렸습니다.", + }; + response.growthPoints[0] = { + ...response.growthPoints[0], + title: "[NAME] 반응을 서두르지 않기", + body: "[ORG] 장면에서 내담자 [NAME]을 한 번 더 확인합니다.", + }; + response.nextLine = "[NAME]의 말을 [ORG] 맥락에서 다시 확인할까요?"; + response.clientFeedback = "[NAME] 이야기를 [ORG]에서 안전하게 다뤘어요."; if (!response.caseWorksheet) { throw new Error("filled review fixture must include a worksheet"); } @@ -661,8 +673,25 @@ test.describe("session review", () => { ); await expect(summary).not.toContainText("[NAME]"); await expect(summary).not.toContainText("[ORG]"); + await openReviewTab(page, "피드백"); + const feedback = page.locator("#sr-panel-feedback"); + await expect(feedback).toContainText("익명 내담자가 보인 강점"); + await expect(feedback).toContainText("소속 기관 장면에서 내담자 반응을 기다렸습니다."); + await expect(feedback).toContainText( + "익명 내담자의 말을 소속 기관 맥락에서 다시 확인할까요?", + ); + await expect(feedback).not.toContainText("[NAME]"); + await expect(feedback).not.toContainText("[ORG]"); await openReviewTab(page, "워크시트"); - await expect(page.locator(".sr-ws-evidence").filter({ hasText: "[NAME]" })).toBeVisible(); + const worksheetPanel = page.locator("#sr-panel-worksheet"); + const worksheetEvidence = worksheetPanel.locator(".sr-ws-evidence").filter({ + hasText: "저는 익명 내담자이고 소속 기관에서 상담받고 있어요.", + }); + await expect(worksheetEvidence).toContainText( + "저는 익명 내담자이고 소속 기관에서 상담받고 있어요.", + ); + await expect(worksheetPanel).not.toContainText("[NAME]"); + await expect(worksheetPanel).not.toContainText("[ORG]"); }); test("stores learner pre/post pilot evidence without effect claims", async ({ page }) => { diff --git a/apps/web/src/lib/piiDisplay.ts b/apps/web/src/lib/piiDisplay.ts index 95a3f3e..484447a 100644 --- a/apps/web/src/lib/piiDisplay.ts +++ b/apps/web/src/lib/piiDisplay.ts @@ -54,6 +54,15 @@ export function displayPiiSafeText(text: string) { /\[NAME\]는 건지\s+잘 \[NAME\]는데요/g, "되는 건지 잘 모르겠는데요", ); + // 평가 문장에 이미 역할 명사가 있으면 이름 토큰을 다시 "익명 내담자"로 + // 풀지 않는다. 특히 "내담자 [NAME]을 받을 여백"은 저장 마스킹 뒤 + // 사람에게 "내담자 반응을 받을 여백"으로 보여야 한다. + next = next + .replace(/내담자\s+\[NAME\](?:을|를)/g, "내담자 반응을") + .replace(/내담자\s+\[NAME\](?:이|가)/g, "내담자가") + .replace(/내담자\s+\[NAME\](?:은|는)/g, "내담자는") + .replace(/내담자\s+\[NAME\]의/g, "내담자의") + .replace(/내담자\s+\[NAME\]/g, "내담자"); for (const [placeholder, label] of Object.entries(DISPLAY_PLACEHOLDERS)) { next = replacePlaceholderWithNaturalParticle(next, placeholder, label); } diff --git a/apps/web/src/pages/SessionReview.tsx b/apps/web/src/pages/SessionReview.tsx index 38d384f..8d5df73 100644 --- a/apps/web/src/pages/SessionReview.tsx +++ b/apps/web/src/pages/SessionReview.tsx @@ -67,6 +67,7 @@ import { REVIEW_READY_POLL_INTERVAL_MS, REVIEW_READY_POLL_LIMIT, displayEvaluationRetryError, + displayGeneratedReviewText, displayReviewSummary, displayTranscriptText, noteAuthorLabel, @@ -367,15 +368,15 @@ function SupervisorCallout({ note }: { note: ReviewNote }) { >
발화 근거 -) : null}{note.quote}
+{displayGeneratedReviewText(note.quote)}
- “{data.clientFeedback}” + “{displayGeneratedReviewText(data.clientFeedback)}”
현재: G8·공개 앱은 정상이다. 주기 실회기 핵심 SSE→review→G4/G5 0→1→1은 통과했지만 회귀 fixture 오염으로 전체 receipt는 FAILED다. 다음: 격리 보정 후 clean single run 전체 GREEN → G7 물리 마이크·human pack·checker exit 0 순서로 닫는다.
현재: G8·공개 앱 정상. 주기 핵심 폐루프 2회 GREEN, 전체 receipt는 FAILED다. 다음: mobile locator 보정의 clean single run → G7 mic·human pack·checker exit 0.
G8 clean-head NAS-origin은 candidate 112/112·실제 origin 112/112로 DONE이다. 공개 API는 detached-clean b34623f3…·tree 32cc85c7…에서 OpenAPI 126, /admin/voice-runtime, local_whisper/small + melotts/melotts-korean, WS queue 4를 제공하며 fresh provenance receipt 9f8d1941…a21bf가 passed다. 공개 Web은 Pages entry index-CGZK4CHD.js와 review chunk SessionReview-D4ScnJ5q.js에서 literal [NAME] 0·시간 만료·모바일 44px/자동 스크롤 보정을 노출한다. 주기 실회기 5차의 전체 receipt 실패 뒤 회귀 fixture를 분리했으며 다음 clean single run 전에는 자동 승격하지 않는다. 마이크를 열지 않은 G7 rehearsal은 인증 WSS·runtime·Windows topology 세 leg 7-sample로 통과했다. 다음 외부 gate는 실제 mic·동시 high-water·human evidence다.
G8 clean-head NAS-origin은 candidate 112/112·실제 origin 112/112로 DONE이다. 공개 API는 detached-clean b34623f3…·tree 32cc85c7…에서 OpenAPI 126, /admin/voice-runtime, local_whisper/small + melotts/melotts-korean, WS queue 4를 제공하며 fresh provenance receipt 9f8d1941…a21bf가 passed다. 공개 Web은 Pages entry index-BrpoQ5nP.js와 review chunk SessionReview-E03q-dGb.js에서 literal [NAME] 0·시간 만료·모바일 44px/자동 스크롤 보정을 노출한다. 주기 실회기 6차는 서로 다른 zero-state fixture와 핵심 폐루프를 통과했지만 desktop이 적재한 성공 상태를 mobile G4 테스트가 최초 버튼으로 다시 찾는 locator 결함을 fail-closed했다. 다음 clean single run 전에는 자동 승격하지 않는다. 마이크를 열지 않은 G7 rehearsal은 인증 WSS·runtime·Windows topology 세 leg 7-sample로 통과했다. 다음 외부 gate는 실제 mic·동시 high-water·human evidence다.
2026-08-07 공개 DB는 recovered named volume으로 전환했고 원본 container·dump·volume을 보존했다. 2026-08-09 public DB incident 뒤 fresh custom dump f1fd569c…5f64를 만들고, 누락된 forward migration 14만 단일 트랜잭션으로 적용해 runtime schema 8/8을 복구했다. 공개 API·cloudflared는 clean release b34623f3…로 fresh 교체했고 public health 3/3·db/engine true·OpenAPI 126·local voice exact를 통과했다. 로그온·5분 watchdog 두 task도 같은 root/commit/tree/script SHA에 pin해 명시 실행 결과 0을 확인했다. 공개 runtime receipt는 9f8d1941…a21bf, 무마이크 rehearsal evidence는 voice 61af2e98…·runtime c6e8670e…·topology 89f1cb86…이며 UUID/email literal 0이다. G7은 배포가 아니라 실제 mic·동시 high-water·human evidence만 남은 external GATE다.
194차 적용(2026-08-10): 주기 runner의 최초 세 full run은 모두 전용 clean stack에서 fail-closed했다. 1차는 성공 SSE의 body를 다시 읽는 CDP 오류, 2차는 UI 2턴만 보고 durable 저장 전에 종료해 리뷰가 기록 없음, 3차는 서버가 의도적으로 닫지 않는 SSE에 response.finished()를 기다려 20분 상한에 도달한 하네스 결함이었다. 세 번 모두 fixture review-ready·stack health는 통과했고 공개 8001·실 DB 55432·engine 9099·NAS 접촉 0, container/volume/network/listener/temp 잔여 0이었다. 현재 소스는 자동 retry 0과 GET /sessions/{id} durable turn 2개 poll만 완료 신호로 사용한다. 같은 단계 반복 원칙에 따라 이번 턴에는 더 실행하지 않으며, 다음 clean single run의 첫 GREEN receipt 전까지 주기 실회기 검증을 완료로 표기하지 않는다.
195차 적용(2026-08-10): clean HEAD 13d1c39a…·tree beb93801…의 4차 수동 full run은 앞선 SSE hang을 제거해 6.3분에 종료했고, 실제 동일 학습자 SSE·review ready·G4 첫 POST와 원장 count 0→1까지 통과했다. 이후 full page reload에서 이미 저장된 현재 회기 episode를 카드가 hydrate하지 않아 원장 반영됨 대신 학습자만 실행으로 되돌아가는 제품 정직성 결함을 fail-closed했다. receipt periodic-learner-e2e-20260810-003150.json은 공개·NAS·active DB 접촉 0과 container/volume/network/listener/temp 잔여 0을 증명한다. UI는 현재 session+prescription의 persisted episode를 read model에서 복원해 현재 원장 상태와 재확인 행동을 표시하도록 수정했고, 실제 reload를 포함한 자기주도 route fixture desktop/mobile 6/6·typecheck를 통과했다. full runner는 재시도하지 않았으므로 첫 GREEN receipt와 자동 주기 승격은 여전히 미완료다.
196차 적용(2026-08-10): clean HEAD 9a7dd98c…·tree 1dce20c5…의 5차 단일 full run은 실제 동일 학습자 브라우저 단계에서 SSE·review ready·G4 0→1→1·G5 0→1→1을 78초에 모두 통과해 핵심 폐루프 첫 GREEN을 만들었다. 다만 같은 fixture를 이어서 사용한 returned-practice desktop/mobile 회귀가 이미 적재된 G4/G5를 초기 0으로 오인해 2/4 실패했고 전체 receipt periodic-learner-e2e-20260810-010038.json은 정직하게 FAILED다. 공개·NAS·active DB 접촉 0, container/volume/network/listener/temp 잔여 0은 유지했다. runner는 핵심 폐루프와 후속 회귀 전에 각각 새 학습자·회기 fixture를 준비하도록 분리했고 unit 12/12·Ruff·compile을 통과했다. 실패 화면에서 리뷰 요약 literal [NAME]/[ORG]과 익명 내담자가고 조사 결함도 발견해 표시 전용 자연어 치환과 복합 조사 우선순위를 보강했고 session-review desktop/mobile 2/2·typecheck·build를 통과했다. 전체 GREEN receipt 전에는 자동 full runner를 등록하지 않는다.
197차 적용(2026-08-10): clean HEAD 4cd35488…·tree 001a7c74…의 6차 full run은 핵심 폐루프를 두 번째로 통과했고, 별도 returned-practice fixture도 초기 G4/G5 0을 증명했다. desktop G4/G5 2건은 GREEN이었지만 desktop이 만든 성공 상태를 mobile G4가 다시 읽을 때 테스트가 반영 상태 다시 확인 대신 최초 버튼을 찾아 5분 timeout, mobile G5는 serial 중단됐다. receipt periodic-learner-e2e-20260810-012909.json은 FAILED이며 public/NAS/active DB 접촉 0·exact cleanup GREEN이다. mobile 성공-state locator를 교정했고 실패 화면에서 추가로 확인한 강점·개선점·다음 문장·내담자 반응·워크시트 근거의 literal [NAME]/[ORG]도 저장/API는 유지한 채 표시층에서 자연어화했다. focused session-review desktop/mobile 2/2·typecheck·live spec 4건 collection을 통과했지만 full runner는 재실행하지 않았다.
docker build -f apps/api/Dockerfile . / docker run ... python -c "import app.main" / docker build -f apps/web/Dockerfile apps/webnode_modules; API image excludes local .env files.python scripts\check-deploy-preflight.py --skip-db --env-file infra\.env.example --allow-placeholder-secrets / DB mode with local DATABASE_URLdata/kb source pack, env template keys, and DB readiness (current_user=vignette). DB mode can additionally check app-role DSN with --require-app-role and now verifies session read-model columns including app.turns voice metadata columns(audio_ref/silence_ms/speech_rate/barge_in/provider_events) plus worksheet review columns.docker compose -p vignette-packaging-smoke -f infra/docker-compose.yml up -d --build + proxy /api/healthhttp://localhost:18080/api/health 200 with db:true, engine:true, engine_mode:"claude_cli". Smoke volumes/network removed after run.http://100.116.83.60:8088 / vignette-e2e 매시간 heartbeat / 배포 증거 / 기계 판독 증거61a41d1f…6af·tree 87dec55d…3b77의 archive 4d15d055…119d4d를 전용 Compose 프로젝트·포트·네트워크·named volume에 승격했다. candidate 112/112와 실제 NAS-origin 112/112는 통과했지만 영수증은 실 API/DB 22건과 route fixture 90건을 분리한다. 자동화는 공개/NAS 화면과 health를 읽기 전용으로 매시간 보여준다. material milestone의 동일 학습자 SSE→review→G4/G5 actual 폐루프는 clean HEAD/tree·전용 engine/DB/API/Web·보호 포트 거부·sentinel·exact cleanup runner가 소유한다. 1~3차는 하네스 결함, 4차는 reload ledger hydration 제품 결함을 fail-closed했다. 5차 핵심 동일 학습자 브라우저는 SSE·review·G4/G5 0→1→1을 처음 통과했지만 같은 fixture를 재사용한 후속 회귀 2/4 실패로 전체 receipt는 FAILED다. cleanup 5/5는 GREEN이다. 두 브라우저 게이트를 서로 다른 zero-state fixture로 분리했고 unit 12/12를 통과했지만 다음 clean single run의 전체 GREEN 전에는 자동 실행을 등록하지 않는다. release gate·NAS preflight·배포 SHA 변경 때만 프리뷰를 갱신한다. G7 외부 mic/provider/human 증거는 별도다.http://100.116.83.60:8088 / vignette-e2e 매시간 heartbeat / 배포 증거 / 기계 판독 증거61a41d1f…6af·tree 87dec55d…3b77의 archive 4d15d055…119d4d를 전용 Compose 프로젝트·포트·네트워크·named volume에 승격했다. candidate 112/112와 실제 NAS-origin 112/112는 통과했지만 영수증은 실 API/DB 22건과 route fixture 90건을 분리한다. 자동화는 공개/NAS 화면과 health를 읽기 전용으로 매시간 보여준다. material milestone의 동일 학습자 SSE→review→G4/G5 actual 폐루프는 clean HEAD/tree·전용 engine/DB/API/Web·보호 포트 거부·sentinel·exact cleanup runner가 소유한다. 1~3차는 하네스, 4차는 reload ledger hydration 제품 결함을 fail-closed했다. 5·6차 핵심 동일 학습자 브라우저는 SSE·review·G4/G5 0→1→1을 연속 통과했다. 5차는 fixture 공유, 6차는 mobile 성공-state locator 결함으로 후속 회귀가 중단되어 전체 receipt는 여전히 FAILED다. cleanup 6/6은 GREEN이고 fixture·locator는 수정했지만 다음 clean single run의 전체 GREEN 전에는 자동 실행을 등록하지 않는다. release gate·NAS preflight·배포 SHA 변경 때만 프리뷰를 갱신한다. G7 외부 mic/provider/human 증거는 별도다.scripts/run-g7-external-proof-window.py · 준비 문서b34623f3…와 fresh receipt 9f8d1941…로 완료했다. Cloudflare가 Python 기본 User-Agent를 403으로 거부하는 경계도 브라우저 호환 header와 회귀 4/4로 고정했다. 30초 rehearsal은 세 leg 7 samples를 모두 통과했으며 마이크를 열지 않았다. 남은 선행 조건: 사용자의 물리 장치 선택과 명시 동의, 독립 human voice-gain pack이다.run-g7-external-proof-window.py → check-g7-external-proof.pyexit 0·gate_closed=true에 결속되고, browser Origin과 API transport host는 별도 allowlist로 검증된다. 세 artifact 교집합은 ≥3000s + 120초 margin이고 Windows proof는 detached-clean HEAD/tree·tool SHA·psutil==6.1.1을 pin한다. clean-source 배포와 무마이크 rehearsal은 완료했다. 남은 것은 물리 마이크 3,120초, 동시 runtime/topology, 독립 human pack을 합친 4-artifact 실증뿐이다.vignette-preview-20260807 · run-outcome-os-release-agent.py4d15d055…119d4d, source HEAD/tree 61a41d1f…6af/87dec55d…3b77, API/Web d5021950…e4b1/9796c092…4b36 exact running. candidate 112/112와 actual NAS-origin 112/112, health ok·db/engine true·auth 401·OpenAPI 126을 통과했다. fresh dump 36ec8748…24db8 1,097,100 bytes, TOC 1,738 / TABLE DATA 129와 previous active 6030a677…c611을 보존했다. release agent의 DB restore 비소유·image/Compose/active-state rollback 경계는 유지한다.