From 7cece90e339992238cdfc86b96a029b4ceb34776 Mon Sep 17 00:00:00 2001 From: Yun Chan Date: Sun, 9 Aug 2026 21:04:29 +0900 Subject: [PATCH] =?UTF-8?q?PII=20=ED=91=9C=EC=8B=9C=20=ED=86=A0=ED=81=B0?= =?UTF-8?q?=20=EB=88=84=EC=B6=9C=20=EB=B3=B4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/api/app/routes/sessions.py | 2 + apps/api/app/routes/voice.py | 3 +- apps/api/app/services/evaluator.py | 4 +- apps/api/app/services/guardrail.py | 15 +- apps/api/app/test_orchestrator_masking.py | 3 +- apps/api/app/test_session_turn_persistence.py | 27 ++- apps/api/app/test_voice_ws.py | 6 +- apps/api/app/turn_runtime.py | 5 +- apps/web/e2e/dev-dashboard.spec.ts | 4 +- apps/web/e2e/full-sweep-session.spec.ts | 87 ++++++++- apps/web/src/lib/piiDisplay.ts | 30 +++ apps/web/src/pages/Session.tsx | 3 +- apps/web/src/pages/session-review/model.ts | 18 +- docs/HANDOFF.md | 171 ++++++++---------- docs/TODO.md | 40 ++-- docs/dev_dashboard.html | 25 +-- docs/guides/testing.md | 6 +- docs/ops/backlog-2026-06-26.md | 28 +-- scripts/check-dev-dashboard-ssot.py | 4 +- 19 files changed, 284 insertions(+), 197 deletions(-) create mode 100644 apps/web/src/lib/piiDisplay.ts diff --git a/apps/api/app/routes/sessions.py b/apps/api/app/routes/sessions.py index 30f58ce..1d8da2c 100644 --- a/apps/api/app/routes/sessions.py +++ b/apps/api/app/routes/sessions.py @@ -247,6 +247,8 @@ async def _retrieve_live_coach_grounding( ) -> list[live_coach.LiveCoachGrounding]: """라이브 코치용 평가 근거 회수. 미가용 시 빈 리스트로 진행한다.""" learner_masked = guardrail.mask_pii(learner_text).text_masked + # LiveCoachRequest.client_reply는 브라우저가 보내는 제어 입력이므로 합성 출력이 + # 아니다. 일반 입력용 PII 게이트를 유지해 문맥 없는 실제 이름도 차단한다. client_masked = guardrail.mask_pii(client_reply or "").text_masked query = " ".join( part for part in [stage, theory_mode, learner_masked, client_masked] if part diff --git a/apps/api/app/routes/voice.py b/apps/api/app/routes/voice.py index ea7a229..5a2f172 100644 --- a/apps/api/app/routes/voice.py +++ b/apps/api/app/routes/voice.py @@ -41,6 +41,7 @@ from ..persona_repository import ( from ..runtime_policy import require_runtime_fallback_allowed from ..services import ( evaluator, + guardrail, multimodal_alliance, multimodal_alliance_store, orchestrator, @@ -1512,7 +1513,7 @@ def _client_turn_text_for_speech(sess: InProcSession, turn_seq: int) -> str | No match = visible_clients[index] if 0 <= index < len(visible_clients) else None if match is not None: text = (match.text_masked or match.text).strip() - return text or None + return guardrail.humanize_pii_placeholders(text) or None return None diff --git a/apps/api/app/services/evaluator.py b/apps/api/app/services/evaluator.py index 242f218..e4949a1 100644 --- a/apps/api/app/services/evaluator.py +++ b/apps/api/app/services/evaluator.py @@ -520,7 +520,9 @@ def build_fast_messages(ctx: "TurnContext", client_reply: str) -> list[EngineMes """fast-loop 평가 프롬프트(L0 역할 + 후보 라벨 + 이번 턴 맥락).""" st = ctx.state_after or ctx.state_before theory = _theory_mode(ctx) - client_reply_masked = guardrail.mask_pii(client_reply).text_masked + client_reply_masked = guardrail.mask_synthetic_generated_pii( + client_reply + ).text_masked recent = ( "\n".join( f"{speaker_ko_label(t.get('speaker'))}: {t.get('text', '')}" diff --git a/apps/api/app/services/guardrail.py b/apps/api/app/services/guardrail.py index 286eb2b..c1d1e31 100644 --- a/apps/api/app/services/guardrail.py +++ b/apps/api/app/services/guardrail.py @@ -49,7 +49,7 @@ _KOREAN_CONTEXTLESS_NAME = rf"[{_KOREAN_SURNAME_CHARS}][가-힣]{{2,3}}" # 인명 마지막 글자로 사실상 쓰이지 않는 용언 활용/명사화 꼬리 글자. 문맥 단서가 # 없는 인명 패턴이 "유의점은·방치되는·마무리했을·연결감과·표현함을·어지러움은" # 같은 일반 단어를 이름으로 오탐해 평가 문장을 훼손하는 것을 막는다(정밀도 가드). -_KOREAN_NAME_TAIL_GUARD = "(? str: if particle is None: return value + + last = value[-1] if value else "" + codepoint = ord(last) - 0xAC00 if "가" <= last <= "힣" else -1 + has_batchim = codepoint >= 0 and codepoint % 28 != 0 if particle in ("은", "는"): - return value + "은" + return value + ("은" if has_batchim else "는") if particle in ("이", "가"): - return value + "이" + return value + ("이" if has_batchim else "가") if particle in ("을", "를"): - return value + "을" + return value + ("을" if has_batchim else "를") if particle in ("와", "과"): - return value + "과" + return value + ("과" if has_batchim else "와") return value + particle @@ -621,6 +625,7 @@ __all__ = [ "PiiEntitySpan", "set_ko_pii_recognizer", "mask_pii", + "mask_synthetic_generated_pii", "CrisisKind", "CrisisResult", "classify_crisis", diff --git a/apps/api/app/test_orchestrator_masking.py b/apps/api/app/test_orchestrator_masking.py index 770c90a..198860e 100644 --- a/apps/api/app/test_orchestrator_masking.py +++ b/apps/api/app/test_orchestrator_masking.py @@ -221,7 +221,8 @@ class OrchestratorMaskingGateTest(unittest.IsolatedAsyncioTestCase): text = ( "유의점은 세 가지다. 정서 신호가 방치되는 것을 피하고, 회기를 " "마무리했을 때 연결감과 작업동맹을 지키며, 감정을 표현함을 존중한다. " - "어지러움은 신체 신호로 다룬다." + "어지러움은 신체 신호로 다룬다. 뭘 할 수 있게 되는 건지 잘 " + "모르겠는데요." ) masked = guardrail.mask_pii(text) diff --git a/apps/api/app/test_session_turn_persistence.py b/apps/api/app/test_session_turn_persistence.py index 41f00e8..762c2f1 100644 --- a/apps/api/app/test_session_turn_persistence.py +++ b/apps/api/app/test_session_turn_persistence.py @@ -376,7 +376,10 @@ class SessionTurnPersistenceTest(unittest.IsolatedAsyncioTestCase): turn_seq=ctx.state_after.turn_seq, stage=ctx.state_after.stage.value, effective_openness=ctx.state_after.effective_openness, - client_reply="저는 김서연 씨고 한신대학교 상담심리학과 학생이에요.", + client_reply=( + "저는 김서연 씨고 한신대학교 상담심리학과 학생이에요. " + "여기서 뭘 해야 하는지 잘 모르겠는데요." + ), safety_flagged=False, state_after=ctx.state_after, llm_provider="claude_cli", @@ -395,19 +398,24 @@ class SessionTurnPersistenceTest(unittest.IsolatedAsyncioTestCase): self.assertEqual( response.client_reply, - "저는 김서연 씨고 한신대학교 상담심리학과 학생이에요.", + "저는 김서연 씨고 한신대학교 상담심리학과 학생이에요. " + "여기서 뭘 해야 하는지 잘 모르겠는데요.", ) self.assertEqual(len(sess.turns), 2) learner_turn, client_turn = sess.turns self.assertIsNone(learner_turn.llm_provider) self.assertEqual( - client_turn.text, "저는 김서연 씨고 한신대학교 상담심리학과 학생이에요." + client_turn.text, + "저는 김서연 씨고 한신대학교 상담심리학과 학생이에요. " + "여기서 뭘 해야 하는지 잘 모르겠는데요.", ) self.assertNotIn("김서연", client_turn.text_masked) self.assertNotIn("한신대학교", client_turn.text_masked) self.assertNotIn("상담심리학과", client_turn.text_masked) self.assertIn("[NAME]", client_turn.text_masked) self.assertIn("[ORG]", client_turn.text_masked) + self.assertIn("잘 모르겠는데요", client_turn.text_masked) + self.assertNotIn("[NAME]는데요", client_turn.text_masked) self.assertEqual(client_turn.llm_provider, "claude_cli") self.assertEqual(client_turn.model, "gateway-default") self.assertEqual(client_turn.tokens_in, 17) @@ -991,6 +999,7 @@ class SessionTurnPersistenceTest(unittest.IsolatedAsyncioTestCase): latency_ms=12, query_text="정리 humanistic 자살 위험 최근성 보호요인", ) + retrieve_grounding = AsyncMock(return_value=retrieval) with ( patch.object( @@ -999,13 +1008,16 @@ class SessionTurnPersistenceTest(unittest.IsolatedAsyncioTestCase): patch.object( sessions.rag, "retrieve_eval_grounding", - AsyncMock(return_value=retrieval), + retrieve_grounding, ), patch.object(sessions.rag, "log_retrieval", AsyncMock()), ): grounding = await sessions._retrieve_live_coach_grounding( learner_text="자살 위험 최근성과 보호요인을 확인했어요.", - client_reply="그래도 오늘은 친구에게 연락할 수 있어요.", + client_reply=( + "박민수에게 연락할지 잘 모르겠는데요. 그래도 오늘은 " + "친구에게 연락할 수 있어요." + ), stage="정리", theory_mode="humanistic", ) @@ -1017,6 +1029,11 @@ class SessionTurnPersistenceTest(unittest.IsolatedAsyncioTestCase): self.assertEqual(source.source_type, "official_guideline_summary") self.assertEqual(source.version, "2026-06-15") self.assertEqual(source.citation, "허가된 공식 지침 요약") + query = retrieve_grounding.await_args.kwargs["query"] + self.assertNotIn("박민수", query) + self.assertIn("[NAME]에게", query) + self.assertIn("잘 모르겠는데요", query) + self.assertNotIn("[NAME]는데요", query) async def test_live_coach_persistence_failure_is_not_swallowed(self) -> None: principal = _principal() diff --git a/apps/api/app/test_voice_ws.py b/apps/api/app/test_voice_ws.py index d3c9564..9ca7b57 100644 --- a/apps/api/app/test_voice_ws.py +++ b/apps/api/app/test_voice_ws.py @@ -240,7 +240,9 @@ class VoiceWebSocketContractTest(unittest.IsolatedAsyncioTestCase): speaker="client", stage="초기", text="raw client reply", - text_masked="마스킹된 내담자 응답", + text_masked=( + "[NAME]은 [ORG]에서 상담받고 있고 [PHONE]는 [DATE]와 달라요." + ), ), TurnRecord( turn_seq=3, @@ -255,7 +257,7 @@ class VoiceWebSocketContractTest(unittest.IsolatedAsyncioTestCase): self.assertEqual( voice_routes._client_turn_text_for_speech(session, 2), - "마스킹된 내담자 응답", + "그 이름은 그 기관에서 상담받고 있고 연락처는 그 날짜와 달라요.", ) self.assertIsNone(voice_routes._client_turn_text_for_speech(session, 3)) self.assertIsNone(voice_routes._client_turn_text_for_speech(session, 99)) diff --git a/apps/api/app/turn_runtime.py b/apps/api/app/turn_runtime.py index d353a5a..911e3f9 100644 --- a/apps/api/app/turn_runtime.py +++ b/apps/api/app/turn_runtime.py @@ -122,7 +122,10 @@ async def record_completed_turn( context=f"{context_prefix} turn append", ) if result.client_reply: - client_mask = guardrail.mask_pii(result.client_reply) + # result.client_reply는 출력 가드레일을 통과한 합성 내담자 발화다. + # 입력용의 광범위한 성씨 휴리스틱을 다시 적용하면 "모르겠는데요" 같은 + # 일반 용언이 [NAME]으로 오탐되어 저장·재개 화면에 누출된다. + client_mask = guardrail.mask_synthetic_generated_pii(result.client_reply) await append_completed_turn( sess, TurnRecord( diff --git a/apps/web/e2e/dev-dashboard.spec.ts b/apps/web/e2e/dev-dashboard.spec.ts index 79a3b25..e58186b 100644 --- a/apps/web/e2e/dev-dashboard.spec.ts +++ b/apps/web/e2e/dev-dashboard.spec.ts @@ -145,8 +145,8 @@ async function readMetrics(page: Page): Promise { function expectDerivedMetrics(metrics: DashboardMetrics) { expect(metrics.total).toBeGreaterThan(0); - expect(metrics.done).toBe(32); - expect(metrics.doing).toBe(3); + expect(metrics.done).toBe(33); + expect(metrics.doing).toBe(2); expect(metrics.planned).toBe(0); expect(metrics.total).toBe(metrics.done + metrics.doing + metrics.planned); expect(metrics.ownerBoard).toBe( diff --git a/apps/web/e2e/full-sweep-session.spec.ts b/apps/web/e2e/full-sweep-session.spec.ts index 6162b13..48e078d 100644 --- a/apps/web/e2e/full-sweep-session.spec.ts +++ b/apps/web/e2e/full-sweep-session.spec.ts @@ -192,6 +192,24 @@ async function routeSessionFixtureApi(page: Page, options: SessionFixtureOptions }); }); + await page.route( + `**/api/sessions/${fixtureSessionId}/multimodal-alliance/consent`, + async (route) => { + const consentBody = route.request().postDataJSON() as Record; + await route.fulfill({ + status: 201, + contentType: "application/json", + body: JSON.stringify({ + submission_id: consentBody.submission_id, + consent_snapshot_id: "00000000-0000-4000-8000-000000000778", + consent_status: "granted", + deletion_request_id: null, + idempotent_replay: false, + }), + }); + }, + ); + await page.route(`**/api/sessions/${fixtureSessionId}/alliance-pulses`, async (route) => { await route.fulfill({ status: 200, @@ -319,6 +337,14 @@ async function routeSessionFixtureApi(page: Page, options: SessionFixtureOptions return { liveCoachRequests, consentRequests }; } +async function acceptVoiceInputConsent(page: Page) { + const consentDialog = page.getByRole("dialog", { name: "음성 입력을 사용하기 전에" }); + await expect(consentDialog).toBeVisible(); + await expect(consentDialog).toContainText("원음은 보존하지 않습니다"); + await consentDialog.getByRole("button", { name: "동의하고 마이크 켜기" }).click(); + await expect(consentDialog).toHaveCount(0); +} + /** * 음성 마이크·WebSocket mock — session-mvp.spec.ts 패턴. * audio_end 수신 시 transcript→reply→state:speaking 을 흘려 @@ -615,6 +641,56 @@ test.describe("full sweep — counseling session", () => { await expect(compose).toHaveAttribute("placeholder", "종료된 회기입니다."); }); + test("naturalizes privacy placeholders when a persisted session is resumed", async ({ + page, + }) => { + const startedAt = new Date(Date.now() - 60_000).toISOString(); + await routeSessionFixtureApi(page, { + detailOverrides: { + started_at: startedAt, + turns: [ + { + speaker: "learner", + text: + "[NAME] 이야기와 [PHONE]·[EMAIL]·[RRN]·[NUMID]·[DATE]·[MONEY]·[ADDR]·[ADDRESS]를 확인할까요?", + turn_seq: 1, + created_at: new Date(Date.now() - 50_000).toISOString(), + }, + { + speaker: "client", + text: "여기서 뭘 할 수 있게 [NAME]는 건지 잘 [NAME]는데요. [ORG]에서 오라고 했어요.", + turn_seq: 2, + created_at: new Date(Date.now() - 40_000).toISOString(), + }, + ], + }, + }); + + await page.goto(`/learn/session/${fixtureSessionId}`); + + const transcript = page.locator(".sx-transcript"); + await expect(transcript.locator(".sx-utt")).toHaveCount(2); + for (const token of [ + "[NAME]", + "[ORG]", + "[PHONE]", + "[EMAIL]", + "[RRN]", + "[NUMID]", + "[DATE]", + "[MONEY]", + "[ADDR]", + "[ADDRESS]", + ]) { + await expect(transcript).not.toContainText(token); + } + await expect(transcript).toContainText("익명 내담자 이야기"); + await expect(transcript).toContainText( + "뭘 할 수 있게 되는 건지 잘 모르겠는데요", + ); + await expect(transcript).toContainText("소속 기관에서 오라고 했어요"); + }); + // checklist: session-transcript-autoscroll test("releases autoscroll when scrolling up, jumps back with the latest button, and follows new turns", async ({ page, @@ -966,6 +1042,7 @@ test.describe("full sweep — counseling session", () => { const micButton = page.getByRole("button", { name: "마이크 켜기" }); await expect(micButton).toBeEnabled(); await micButton.click(); + await acceptVoiceInputConsent(page); await page.getByRole("button", { name: "발화 보내기" }).click(); // reply + state:speaking → 재생 중에만 '건너뛰기'가 노출된다. @@ -1018,11 +1095,14 @@ test.describe("full sweep — counseling session", () => { }); // checklist: session-keyboard-shortcuts - test("toggles pause with P and mic with Space only outside text inputs", async ({ page }) => { + test("toggles pause with P and mic with Alt+M only outside text inputs", async ({ page }) => { await routeSessionFixtureApi(page); await installVoiceSpeakingFixture(page, voiceTranscript, voiceClientReply); await startFixtureSession(page); + await page.evaluate(() => { + if (document.activeElement instanceof HTMLElement) document.activeElement.blur(); + }); // P — 일시정지 토글. await page.keyboard.press("p"); @@ -1037,9 +1117,10 @@ test.describe("full sweep — counseling session", () => { await expect(compose).toHaveValue("p"); await expect(page.getByRole("button", { name: "일시정지" })).toBeVisible(); - // 포커스를 입력창 밖으로 옮긴 뒤 Space — 마이크 시작. + // 포커스를 입력창 밖으로 옮긴 뒤 Alt+M — 마이크 시작. await compose.blur(); - await page.keyboard.press("Space"); + await page.keyboard.press("Alt+m"); + await acceptVoiceInputConsent(page); await expect(page.getByRole("button", { name: "발화 보내기" })).toBeVisible(); }); }); diff --git a/apps/web/src/lib/piiDisplay.ts b/apps/web/src/lib/piiDisplay.ts new file mode 100644 index 0000000..47b1f32 --- /dev/null +++ b/apps/web/src/lib/piiDisplay.ts @@ -0,0 +1,30 @@ +const DISPLAY_PLACEHOLDERS: Record = { + "[NAME]": "익명 내담자", + "[ORG]": "소속 기관", + "[PHONE]": "연락처", + "[EMAIL]": "이메일", + "[RRN]": "주민등록번호", + "[NUMID]": "식별번호", + "[DATE]": "날짜", + "[MONEY]": "금액", + "[ADDR]": "주소", + "[ADDRESS]": "주소", +}; + +/** + * 저장/API의 privacy-proof 토큰은 유지하고, 사람이 읽는 일반 대화 표면에서만 + * 토큰을 안전한 설명으로 낮춘다. 근거 인용·내보내기에는 사용하지 않는다. + */ +export function displayPiiSafeText(text: string) { + // 2026-08-09 이전 저장본에서 광범위한 성씨 휴리스틱이 평범한 용언을 + // NAME으로 오탐한 두 실관측 문형. 원문은 복원할 수 없으므로 표시층에서만 + // 결정적으로 복구하고 저장/API privacy proof는 그대로 둔다. + let next = text.replace( + /\[NAME\]는 건지\s+잘 \[NAME\]는데요/g, + "되는 건지 잘 모르겠는데요", + ); + for (const [placeholder, label] of Object.entries(DISPLAY_PLACEHOLDERS)) { + next = next.split(placeholder).join(label); + } + return next; +} diff --git a/apps/web/src/pages/Session.tsx b/apps/web/src/pages/Session.tsx index 3cfad72..5943937 100644 --- a/apps/web/src/pages/Session.tsx +++ b/apps/web/src/pages/Session.tsx @@ -43,6 +43,7 @@ import { } from "../lib/api"; import { useAuth } from "../lib/auth"; import { formatElapsed, formatTimecode, clamp01 } from "../lib/format"; +import { displayPiiSafeText } from "../lib/piiDisplay"; import { parseVoicePracticeContext, voicePracticeSearch, @@ -1001,7 +1002,7 @@ export default function Session() { (detail.turns ?? []).map((turn) => ({ id: nextId(), speaker: turn.speaker === "client" ? "client" : "learner", - text: turn.text, + text: displayPiiSafeText(turn.text), turnSeq: turn.turn_seq, at: secondsBetween(detail.started_at, turn.created_at), })), diff --git a/apps/web/src/pages/session-review/model.ts b/apps/web/src/pages/session-review/model.ts index d02e28b..f0ee0e6 100644 --- a/apps/web/src/pages/session-review/model.ts +++ b/apps/web/src/pages/session-review/model.ts @@ -4,6 +4,7 @@ import type { UserPrepostMeasureItem, UserPrepostMeasuresResponse, } from "../../lib/api"; +import { displayPiiSafeText } from "../../lib/piiDisplay"; export type PrepostMeasureName = UserPrepostMeasureItem["measure_name"]; export type PrepostTimepoint = UserPrepostMeasureItem["timepoint"]; @@ -132,23 +133,8 @@ export function displayEvaluationRetryError(message: string) { return "AI 평가 재시도를 완료하지 못했습니다. 잠시 뒤 다시 실행해 주세요."; } -const REVIEW_DISPLAY_PLACEHOLDERS: Record = { - "[NAME]": "익명 내담자", - "[ORG]": "소속 기관", - "[PHONE]": "연락처", - "[EMAIL]": "이메일", - "[RRN]": "주민등록번호", - "[ADDRESS]": "주소", -}; - export function displayTranscriptText(text: string) { - let next = text; - for (const [placeholder, label] of Object.entries( - REVIEW_DISPLAY_PLACEHOLDERS, - )) { - next = next.split(placeholder).join(label); - } - return next; + return displayPiiSafeText(text); } export function shouldPollReviewReady(data: SessionReviewResponse) { diff --git a/docs/HANDOFF.md b/docs/HANDOFF.md index 62d021f..416ffea 100644 --- a/docs/HANDOFF.md +++ b/docs/HANDOFF.md @@ -1,15 +1,14 @@ # Vignette G0~G8 완료 추진 핸드오프 -> Updated: 2026-08-09 KST (다른 에이전트 실행용 최종 인계 · G7 외부-proof P0 코드 완료 · current-source fixture 분할 GREEN/clean HEAD 재실행) +> Updated: 2026-08-09 KST (G8 clean-head NAS-origin 종료 · 남은 핵심은 G7 external proof) > Workspace: `D:\workspace\vignette` -> 이 문서는 다음 에이전트가 **G0~G8 아홉 목표 중 남은 G7 외부 게이트와 G8 NAS-origin 재검증을 닫기 위한** 실행 기준이다. +> 이 문서는 다음 에이전트가 **G0~G8 아홉 목표 중 마지막 G7 외부 게이트를 닫기 위한** 실행 기준이다. ## 0. 현재 상태 한 줄 요약 -**G0~G6의 내부 구현·증거는 종료됐다. 단, 최종 clean source 전체 회귀는 다시 실행해야 한다. G8 실제 rollback -receipt는 완료됐지만 current source clean-head 승격은 아직 0회다. -두 execute는 모두 NAS mutation 전에 안전하게 중단됐다. naive `started_at`의 9시간 오차 P0는 UTC/KST 계약과 -실DB 브라우저 회귀로 수정됐고, 이제 새 clean commit 결속과 NAS-origin 전체 E2E가 남았다. G7 공개 런처의 +**G0~G6의 내부 구현·증거와 G8 clean-head NAS-origin runtime은 종료됐다. G8은 source HEAD +`61a41d1f…6af`·tree `87dec55d…3b77`·archive `4d15d055…119d4d`로 candidate 112/112와 actual NAS-origin +112/112를 모두 통과했다. 앞선 fail-closed·verified rollback 이력과 previous `6030a677…c611`은 보존했다. G7 공개 런처의 local sidecar lifecycle·provider/model readiness·Uvicorn queue, fresh PID/source provenance, 3,120초 runner와 3,000초 공통 시간창·canonical exit 계약은 소스에서 완료됐지만, current source 공개 배포와 외부 4-artifact 게이트는 남아 있다.** @@ -23,28 +22,24 @@ local sidecar lifecycle·provider/model readiness·Uvicorn queue, fresh PID/sour | G5 Calibration & Transfer | DONE | actual transfer execution, independent/model-run/instrument/evidence provenance, DB/browser idempotency | | G6 Supervision & Research | DONE | safety event metadata-only projection, safety priority 1 > deterioration 2, raw detail 0 | | G7 Multimodal Alliance | **internal DONE · external GATE** | consent/withdrawal, synthetic soak 59/59, G7 runner/checker/topology 86/86, launcher/sidecar 80/80, API voice 71/71 | -| G8 Continuous Improvement | **receipt DONE · runtime REVALIDATION** | 실제 image rollback 2회 + executed receipt 2건. timestamp 수정 실DB GREEN, 새 clean commit·NAS-origin E2E 미완료 | +| G8 Continuous Improvement | **DONE · CLEAN-HEAD NAS VERIFIED** | 실제 image rollback 2회 + executed receipt 2건, source/archive 결속, candidate 112/112, actual NAS-origin 112/112 | -대시보드 status count는 **32 done / 3 doing / 0 planned**다. +대시보드 status count는 **33 done / 2 doing / 0 planned**다. -Git 기준선은 branch `master`, HEAD `94c681d450d2f3b4df55c036e333cd817db3f7fb`이고 `origin/master`보다 2 commit -앞서 있다. 공유 worktree에는 -문서/대시보드/G7·G8 런북, 완결된 clean-head 릴리스 에이전트·테스트, UTC/KST session timestamp 수정과 -browser 회귀가 tracked dirty로 남아 있다. Git 작업은 stage/commit/push하지 않았다. +G8 배포 source 기준선은 branch `master`, HEAD `61a41d1f08239b5f9e99cfae42a206be250416af`·tree +`87dec55daf0a22ca5c2e489cd803a2f731e63b77`이며 2회 동일 archive SHA는 +`4d15d055d74f33f1fa2ff470afbf900d565ab238b19c79df34ca7ba240119d4d`다. 공유 worktree의 후속 변경은 이 +배포 source에 포함되지 않으므로 G8 완료 증거와 섞지 않는다. ### 이 문서가 인계된 정확한 중단점 -- 사용자가 **현재 구현을 잠시 멈추고 다른 에이전트로 넘길 핸드오프를 만들라**고 요청했다. 핸드오프 정리 중 - public/NAS/DB/마이크/프로세스의 수동 mutation은 0회다. 5분 Scheduled Task는 한 번씩 자동 실행됐으나 아래 - source-pin gate에서 안전하게 exit 1로 중단됐다. +- G8 clean-head execute는 격리 NAS preview만 변경했고 공개 API/DB·물리 마이크는 건드리지 않았다. source HEAD/tree와 + archive 2회 결정성, fresh dump, exact images, candidate/actual-origin 결과를 모두 결속했다. - active goal은 계속 `active`다. 목표는 “문서 작성”이 아니라 **G0~G8 아홉 목표를 실제 runtime과 E2E로 - 모두 닫는 것**이다. G7 canonical checker exit 0과 G8 current clean source의 NAS-origin 0-failure가 없으면 - `goal complete`를 호출하지 않는다. -- 마지막 완료 작업은 G7 runner/checker/topology·fresh public launcher provenance P0, session timestamp UTC/KST 계약, - API full 921/921, gateway 58/58, Web API types·typecheck·build, 자기주도 route fixture desktop/mobile 6/6이다. - fixture desktop/mobile은 마지막 단일 - 120/120 뒤 dashboard spec/HTML이 다시 바뀌었고 그 변경의 dashboard 10/10을 별도로 확인했으므로, - **현재 트리는 불변 110 + 최신 dashboard 10의 분할 GREEN**이다. 새 clean HEAD에서 exact 120을 다시 실행한다. + 모두 닫는 것**이다. G8은 종료됐고 G7 canonical checker exit 0이 없으면 `goal complete`를 호출하지 않는다. +- 마지막 완료 작업은 G8 source HEAD `61a41d1f…6af`·tree `87dec55d…3b77`·archive `4d15d055…119d4d`의 + candidate 112/112와 actual NAS-origin 112/112다. API/Web `d5021950…e4b1`/`9796c092…4b36`, health + ok·db/engine true·auth 401·OpenAPI 126, fresh dump `36ec8748…24db8`을 확인했다. - G7 Windows topology mode, 3,000초 공통 시간창, canonical checker-bound exit와 공개 런처의 local Whisper/MeloTTS lifecycle·exact readiness·Uvicorn queue·fresh API/cloudflared provenance 계약은 코드와 회귀가 끝났지만 public launcher 실행과 capture는 수행하지 않았다. @@ -61,8 +56,8 @@ browser 회귀가 tracked dirty로 남아 있다. Git 작업은 stage/commit/pus `status=ok, db=true, engine=true`다. clean commit 뒤 두 task를 동일한 detached stable release root로 재등록하고 action pin·watchdog result 0·로그온/reboot smoke를 확인하기 전에는 public runtime 자동복구를 DONE으로 부르지 않는다. -- 다음 에이전트의 첫 mutation은 **사용자 승인 아래 새 clean commit 생성**이어야 한다. 그 clean source만 G8 NAS - preview 또는 G7 public runtime의 배포 입력으로 쓰고, 두 환경을 동시에 변경하지 말며 mutation owner는 한 명만 둔다. +- 다음 mutation 대상은 G7 public runtime이다. G8 NAS는 새 material milestone이 생기기 전까지 읽기 전용 기준선으로 + 두고, public/DB mutation owner는 한 명만 둔다. ### 완료 조건 판정 (2026-08-09 재검증) @@ -70,9 +65,9 @@ browser 회귀가 tracked dirty로 남아 있다. Git 작업은 stage/commit/pus | # | 조건 | 판정 | 근거 | |---|---|---|---| -| 1 | G0~G8 각각의 코드 계약과 실제 runtime 증거 | **부분 충족** | G0~G6 충족. G8 receipt는 충족했지만 NAS-origin 회기 재검증이 남았고 G7 외부 GATE가 열려 있다 | +| 1 | G0~G8 각각의 코드 계약과 실제 runtime 증거 | **부분 충족** | G0~G6과 G8 충족. G7 외부 GATE만 열려 있다 | | 2 | 학생 홈→회기→리뷰→처방→재연습→전이 폐루프가 실제 API/DB/browser 통과 | **충족** | 일회용 클론에서 `returned-practice-db-closed-loop` desktop 2/2 + mobile 2/2, route mock 0 | -| 3 | current source 증거와 대시보드·TODO·backlog 일치 | **충족** | SSOT checker FAIL 0, unit 5/5, dashboard E2E 10/10, count 32/3/0이 카드와 일치 | +| 3 | current source 증거와 대시보드·TODO·backlog 일치 | **충족** | SSOT checker FAIL 0, dashboard E2E 10/10, count 33/2/0이 카드와 일치 | | 4 | G7을 가짜·합성 증거로 DONE 처리하지 않음 | **충족** | `check-g7-external-proof.py` exit 1 유지, G7 카드는 `GATE` | 재검증 수치: API **921 passed**, gateway **58**, executor **28**, probe **11**, helper launcher **37**, @@ -89,8 +84,8 @@ live health 테스트는 React StrictMode 중복 조회의 첫 응답을 고정 재검증됐다. 따라서 current tree의 정직한 판정은 **110 불변 + 10 최신 분할 GREEN**이며, 새 clean commit에서 같은 120건을 한 번 재실행해 archive/source 결속 뒤의 결과를 고정한다. -**따라서 `update_goal complete`를 호출하지 않는다.** 조건 1이 G7과 G8 NAS-origin 재검증 때문에 미충족이고, -조건 4가 G7을 합성 증거로 메우는 것을 금지한다. G8 receipt 자체는 종료됐다(§3). +**따라서 `update_goal complete`를 호출하지 않는다.** 조건 1이 G7 external proof 때문에 미충족이고, +조건 4가 G7을 합성 증거로 메우는 것을 금지한다. G8은 clean-head NAS-origin까지 종료됐다(§3·§5). ### G0~G8 목표별 인수 기준 @@ -104,7 +99,7 @@ live health 테스트는 React StrictMode 중복 조회의 첫 응답을 고정 | G5 | 자기평가 잠금, calibration 감소, unseen transfer 유지, subgroup drift | actual transfer execution과 independent/model-run/instrument/evidence provenance | G4와 같은 disposable DB에서 actual POST·read reload·1→1 멱등 재확인 | | G6 | 3-click queue, 원장 drilldown, 버전 재현, 역할/RLS/audit | safety metadata-only, safety priority 1 > deterioration 2, raw detail 0 | current HEAD producer/store/API와 supervisor UI 회귀 재실행 | | G7 | 시간 정렬, **물리 마이크+공개 WSS 장시간 E2E**, text-only 대비 음성 이득, 동의/삭제 정책 | 내부 계약·synthetic soak만 완료. canonical checker는 의도적으로 exit 1 | current 공개 배포→인증 WSS→명시 동의 mic 50분→동시 runtime/topology→독립 human pack→checker exit 0 | -| G8 | source→draft→red-team→benchmark→catalog 재현, 누수/PII/무근거 0, 자동 calibration+rollback, incident→test/backlog 환류 | agentic worker·human gate·실제 rollback/restore receipt 2건, timestamp focused 실DB/browser GREEN | 사용자 승인 아래 새 clean commit→NAS 배포→**실제 NAS origin** 전체 회기 E2E GREEN→SSOT 동기화 | +| G8 | source→draft→red-team→benchmark→catalog 재현, 누수/PII/무근거 0, 자동 calibration+rollback, incident→test/backlog 환류 | agentic worker·human gate·실제 rollback/restore receipt 2건, clean-head candidate/NAS-origin 112/112 | **종료.** 새 material milestone 전까지 NAS 기준선 보존 | ## 1. 작업 시작 전 반드시 읽을 파일 @@ -126,13 +121,11 @@ live health 테스트는 React StrictMode 중복 조회의 첫 응답을 고정 - Windows 11 + PowerShell 환경이다. 실행 전 OS·셸·도구 경로를 다시 확인한다. - 명령은 PowerShell 5.1 호환 문법을 기본으로 작성한다. NAS 원격 스크립트는 **UTF-8 base64 전달 방식**을 쓴다. (`$()`가 PowerShell에서 먼저 확장되는 것을 막는다.) -- 이 핸드오프 작성 시 branch/HEAD는 `master` / `94c681d450d2f3b4df55c036e333cd817db3f7fb`이고 include 후보는 - 52개다: UTC/KST timestamp, G7 voice/topology/public launcher, pinned `psutil`, G8 clean-head controller, public task source pin, - 학생 boot recovery, current fixture 안정화, dashboard/TODO/backlog/HANDOFF·런북 동기화다. 이 묶음들은 아직 - commit에 들어 있지 않다. +- G8 배포 source는 `master` / `61a41d1f08239b5f9e99cfae42a206be250416af`이며 공유 worktree의 후속 변경은 + 이 clean release와 분리한다. stage/commit 시 `git add -A`를 쓰지 않고 명시적 경로만 다룬다. - clean-head 구현은 unit 30/30, G7 runner/checker/topology는 86/86, G7 public launcher/sidecar는 80/80, public task source pin은 provenance 11개를 포함한 combined 22/22, timestamp·voice 계약을 포함한 API는 - 921/921와 KST 실DB browser 1/1, current fixture는 110 불변 + 최신 dashboard 10 분할 GREEN, + 921/921와 KST 실DB browser 1/1, G8 candidate/NAS-origin은 각각 112/112, SSOT는 FAIL 0와 dashboard E2E 10/10을 통과했다. 다음 에이전트는 최초 diff review 뒤 동일한 focused gate를 다시 실행하고, 서로 다른 lane의 파일을 누락한 채 clean commit을 만들지 않는다. @@ -148,7 +141,7 @@ live health 테스트는 React StrictMode 중복 조회의 첫 응답을 고정 리스너를 정리할 수 있으므로 공개 런타임이 떠 있는 동안 실행하지 않는다. - 비밀값, 이메일, cookie, 사용자 UUID가 포함된 raw response, `.env` 내용은 로그·증거·채팅에 출력하지 않는다. -## 3. G8 receipt — 종료됨 · NAS-origin runtime 재검증은 열림 +## 3. G8 receipt + clean-head NAS-origin — 종료됨 ### Gate6 계약 정정 (성립 불가능했던 이전 계약을 코드로 교체) @@ -216,29 +209,27 @@ UI가 active DB container에 직접 stop을 보내 같은 증상이 재발했다 공개 DB에는 current dirty source를 배포하지 않는다. -## 5. NAS 배포 기준선 — 현재 HEAD가 아니라 2026-08-07 릴리스 +## 5. NAS 배포 기준선 — 2026-08-09 clean-head DONE -- **2026-08-09 17:07 KST read-only snapshot:** URL `http://100.116.83.60:8088` · project - `vignette-preview-20260807` · remote root `/volume1/docker/vignette-preview-20260807` · active release SHA - `6030a677af7e87cbfabc422b553d108d53414fd3c446548734a13b036d35c611`. -- API `sha256:52e0e816…8b2d` / Web `sha256:6fdbb646…f215` exact image가 실행 중이고 previous - `79ec731f…0450` / `c530213f…62f28`도 보존돼 있다. Compose `api/web/db/proxy` 4개는 모두 running, - restart policy는 `unless-stopped`다. -- DB volume `vignette-preview-20260807_pgdata` · pre-proof dump `vignette-preview-g8-preproof-20260807T110610Z.dump` - SHA256 `92ed4736…1570f`, 960,970 bytes · pre-clean-head dump - `vignette-preview-pre-clean-head-20260809T051611Z.dump` SHA256 `726982…5d41c`, 1,015,222 bytes. -- health 3/3(`db=true`, `engine=true`), auth 401, OpenAPI 126 + G1~G8 route, root/JS/CSS 200이다. -- helper container 0, NAS listener 8018/18149 0, Windows tunnel listener 18018 0이다. -- **P1 권한 관측:** remote release root와 Compose 파일이 mode `0777`이다. 다음 execute 전에 owner/mode를 - 최소권한으로 좁히고 release agent preflight가 이를 fail-closed로 검증해야 한다. 현재 상태를 안전한 배포 신뢰 - 경계로 간주하지 않는다. -- localhost candidate browser E2E 108/108(634.578s), postdeploy SSE→DB review PASS -- **G8 rollback/restore 이후 재검증:** 실제 브라우저 SSE→DB review 재실행 PASS(35.0s) +- URL `http://100.116.83.60:8088` · project `vignette-preview-20260807` · remote root + `/volume1/docker/vignette-preview-20260807`. +- source HEAD `61a41d1f08239b5f9e99cfae42a206be250416af` · tree + `87dec55daf0a22ca5c2e489cd803a2f731e63b77` · 2회 동일 active archive + `4d15d055d74f33f1fa2ff470afbf900d565ab238b19c79df34ca7ba240119d4d`. +- API `sha256:d502195021beafe869c555f3ec80269426062ef4586dc15a3e049f9c8660e4b1` / Web + `sha256:9796c0925f6a984cb846b61a4f13fda4d403bda7b3a2b6232b728cebbcdc4b36` exact image가 실행 중이다. +- candidate session E2E **112/112**, 실제 NAS 평문 origin browser review **112/112**(11 specs, + desktop/mobile/single-run), health `status=ok·db=true·engine=true`, auth 401, OpenAPI 126. +- fresh custom dump `vignette-preview-pre-clean-head-retry-61a41d1f-20260809T112038Z.dump`, SHA256 + `36ec8748cf28c520d9489d108dee9d4a154e6260f6996b4da3e0fe3b8a624db8`, 1,097,100 bytes, + TOC 1,738 / TABLE DATA 129를 보존했다. +- previous active `6030a677af7e87cbfabc422b553d108d53414fd3c446548734a13b036d35c611`과 이전 images, + named DB volume을 삭제하지 않았다. 배포 중 첫 108 gate가 103/5로 fail-closed된 이력(G5 fixture drift, StrictMode retry fixture, `shell.css` manifest 누락)은 지우지 않는다. -### ⚠ 배포된 SHA는 비-secure origin 결함을 갖고 있다 (수정은 소스에만 있음) +### 과거 비-secure origin 결함 — 수정·배포 검증 완료 배포 후 NAS 프리뷰(`http://100.116.83.60:8088`, 평문 HTTP·비-localhost)에 전체 회기 스펙을 돌려보니 24건이 실패했고, 원인은 단 하나였다. 페이지 스냅샷의 실제 예외는 `crypto.randomUUID is not a function`이다. @@ -250,14 +241,13 @@ UI가 active DB container에 직접 stop을 보내 같은 증상이 재발했다 - 수정: `apps/web/src/lib/uuid.ts`의 `randomUuid()`로 통일. fallback도 `crypto.getRandomValues`를 우선 사용해 idempotency key의 예측 불가능성을 유지하고, Web Crypto가 아예 없을 때만 `Math.random`으로 내려간다. - 회귀: `apps/web/e2e/insecure-context-uuid.spec.ts` **6/6** (직접 호출 0건 검사 포함), typecheck·build 통과. -- **아직 NAS에 배포하지 않았다.** 배포된 SHA `6030a677…c611`은 여전히 결함 빌드다. 다음 배포 때 - release agent로 승격하고, 승격 뒤에는 NAS origin에서 회기 스펙을 다시 돌려 24건이 사라지는지 확인한다. +- UUID와 portable SHA-256 수정은 archive `4d15d055…119d4d`로 NAS에 배포했고 actual-origin 112/112를 통과했다. - 교훈: candidate gate를 localhost에서만 돌리면 secure-context 전용 API 결함을 못 잡는다. 배포 대상과 같은 scheme/host 형태에서 최소 한 번은 회기 리뷰 라우트를 열어봐야 한다. -## 6. 다음 실행 순서 — G8 runtime 재검증 후 G7 외부 종료 +## 6. 다음 실행 순서 — G8 종료, G7 외부 종료 -### 6.1 G8: clean-head 릴리스 경로부터 완성 +### 6.1 G8: clean-head 릴리스 경로 — DONE `scripts/run-outcome-os-release-agent.py`의 명시적 clean-head 경로는 구현됐다. 기존 patch mode를 기본값으로 보존하며, tracked-clean source worktree의 exact HEAD/tree/archive만 candidate로 사용한다. 수동 복사나 NAS 직접 @@ -275,18 +265,16 @@ UI가 active DB container에 직접 stop을 보내 같은 증상이 재발했다 production candidate/NAS-origin은 실제 배포 번들에서 동작하는 11개 회기 스펙만 실행한다. 7. unit/fake runner가 tracked dirty, archive SHA drift, evidence binding, patch-mode 무회귀를 검증해야 한다. -검증: release-agent unit 30/30, source-only insecure-context gate 12/12, Ruff, py_compile, `git diff --check` PASS. 공유 worktree에는 -인계 문서와 controller 변경이 있으므로 이 경로 자체가 의도대로 tracked-dirty를 차단한다. 실제 배포는 새 commit을 -가리키는 별도 detached clean worktree를 만들고 `--source-repo-root `로 지정한 뒤 -dry-run→execute 순서로 수행한다. +검증: release-agent unit 30/30, source-only insecure-context gate 12/12, Ruff, py_compile, `git diff --check` PASS. +최종 실행은 detached clean HEAD `61a41d1f…6af`에서 candidate 112/112와 actual NAS-origin 112/112를 통과했다. +이하 preflight와 실패 기록은 최종 성공에 이르기까지의 역사 증거이며 다시 실행할 지시가 아니다. -#### clean commit preflight — include 52 / exclude 6 +#### 역사 기록: 첫 clean commit preflight — include 52 / exclude 6 2026-08-09 최종 경로 목록 SHA-256은 ordinal 정렬된 UTF-8 LF 경로를 줄바꿈으로 연결하고 마지막 개행 없이 계산해 include `4d94694ccf9def6945272c61f673cac0eaa9fb59f4f9b6c165d661d819f891a1`, exclude -`1ff8c26732772df6b5857bf84de01a53d7da9c3b7d52df08938e77f3a527f022`다. 아래 include 52개만 -사용자 승인 후 **명시적으로** stage한다. 그 직전 `git status --porcelain=v1 -uall`로 목록·해시를 다시 계산하고, -새 경로가 하나라도 있으면 멈춘다. `git add -A`는 금지한다. +`1ff8c26732772df6b5857bf84de01a53d7da9c3b7d52df08938e77f3a527f022`였다. 아래 목록은 첫 clean commit 범위를 +감사하기 위해 보존한 이력이며 현재 worktree staging 목록이 아니다. ```text apps/api/app/config.py @@ -410,15 +398,15 @@ apps/web/test-results/g7-voice-consent-mobile.png - **테스트 강제 클릭이나 모달 닫기 우회는 사용하지 않았다.** 첫 clean source `5221f79e…1c69`에는 제품 수정과 controller 변경이 포함됐고, production entry 차단 회귀 2건의 테스트 수정만 후속 clean commit으로 다시 고정한다. -### 6.2 G8: NAS preview 배포와 실제 origin E2E +### 6.2 G8: NAS preview 배포와 실제 origin E2E — DONE -clean-head release mode가 GREEN이면 전용 NAS preview만 대상으로 백업→dry-run→배포→검증한다. 공개 DB·공개 API는 -이 단계에서 건드리지 않는다. 성공 조건은 다음 전부다. +전용 NAS preview만 대상으로 백업→dry-run→배포→검증했고 공개 DB·공개 API는 건드리지 않았다. 아래 성공 조건을 +전부 충족했다. -직전 새 backup은 -`/volume1/docker/vignette-preview-20260807/backups/vignette-preview-pre-clean-head-20260809T051611Z.dump`, -SHA-256 `72698232d132a8f848d81ff9d8602430e7e623c7646cc236cb39f978aab5d41c`, -1,015,222 bytes, TOC 1,752다. 하지만 release agent 자체에는 `pg_dump`/`pg_restore` 실행 계약이 없다. +최종 fresh backup은 +`/volume1/docker/vignette-preview-20260807/backups/vignette-preview-pre-clean-head-retry-61a41d1f-20260809T112038Z.dump`, +SHA-256 `36ec8748cf28c520d9489d108dee9d4a154e6260f6996b4da3e0fe3b8a624db8`, +1,097,100 bytes, TOC 1,738 / TABLE DATA 129다. release agent 자체에는 `pg_dump`/`pg_restore` 실행 계약이 없다. 따라서 **매 execute 직전** 별도 승인된 백업 단계에서 fresh custom dump를 만들고 SHA-256·byte size·TOC parse/count와 current DB identity를 증거에 결속한 뒤에만 승격한다. 기존 dump 존재만으로 이 단계를 생략하지 않는다. @@ -426,12 +414,13 @@ release agent의 자동 rollback 범위는 exact API/Web image, Compose 적용 DB migration이나 데이터 변경은 자동 복구하지 않는다. migration/data restore는 별도 owner 승인·대상 DB identity·dump hash를 갖춘 복원 절차로만 수행한다. 이미지 rollback 성공을 DB rollback 성공으로 기록하면 안 된다. -- 새로운 active SHA와 exact API/Web image ID, predeploy dump SHA, previous image 보존. -- health 3/3, db/engine true, auth 401, OpenAPI와 G0~G8 route 존재. -- `http://100.116.83.60:8088` **실제 평문 NAS origin**에서 §8의 회기 E2E 묶음 0 failure. +- active archive `4d15d055…119d4d`, exact API/Web `d5021950…e4b1`/`9796c092…4b36`, fresh dump SHA, + previous `6030a677…c611` 보존. +- health ok, db/engine true, auth 401, OpenAPI 126. +- `http://100.116.83.60:8088` **실제 평문 NAS origin**에서 11-spec browser review 112/112. - `crypto.randomUUID` 예외 0, error boundary 0, SSE→DB review PASS. - 학생 홈→회기→리뷰→G4/G5 반환 연습의 desktop/mobile·DB read reload·멱등·UUID 비노출 유지. -- 실패 시 자동 rollback과 previous image 복구를 검증하고, 성공 전에는 G8 카드 전체를 DONE으로 바꾸지 않는다. +- 앞선 실패 시 자동 rollback과 previous image 복구를 검증했고 최종 성공 뒤 G8 카드를 DONE으로 바꿨다. ### 6.3 G7: 외부 종료 Gate @@ -694,7 +683,7 @@ node.exe .\node_modules\@playwright\test\cli.js test e2e/session-layout.spec.ts --project=chromium-desktop --project=chromium-mobile --project=chromium-single-run --workers=1 --reporter=line ``` -전체 Playwright inventory는 620 tests / 44 files다. 실행 환경/API/DB를 정확히 맞추지 않고 fixture failure를 +전체 Playwright inventory는 622 tests / 44 files다. 실행 환경/API/DB를 정확히 맞추지 않고 fixture failure를 제품 failure로 오인하지 않는다. 같은 blocker가 두 번 반복되면 전체 재시도 대신 원인·증거·수정 계획을 먼저 보고한다. ## 9. 핵심 변경 파일 @@ -738,9 +727,9 @@ node.exe .\node_modules\@playwright\test\cli.js test e2e/session-layout.spec.ts 대체 불가능한 동의·인증 세션·실제 참가자/평가자 승인만 요청한다. 동시 작업은 최대 세 하위 lane으로 나누되, NAS/public/DB mutation owner는 항상 한 에이전트만 둔다. -1. **G8 release lane:** clean-head release mode 구현·unit·dry-run·NAS preview 배포·NAS-origin E2E. 이 lane만 NAS mutation. -2. **G7 proof lane:** current public route/config 감사, local provider readiness, `--rehearse`, 네 artifact 수집 준비. - 실제 마이크는 사용자 명시 동의 전 0회, public mutation은 G8 lane과 시간 겹치지 않게 단일 owner에게 인계. +1. **G7 proof lane:** current public route/config 감사, local provider readiness, `--rehearse`, 네 artifact 수집 준비. + 실제 마이크는 사용자 명시 동의 전 0회, public mutation은 단일 owner에게 인계. +2. **G8/NAS monitor lane:** 완료된 archive·images·health·자동화 drift를 읽기 전용으로 감시. 새 material milestone 전 mutation 0. 3. **E2E/SSOT lane:** local fixture, disposable DB browser, visual/accessibility, dashboard/TODO/backlog 정합을 읽기 전용으로 감사. 루트 에이전트는 각 lane의 증거 SHA와 실패 원인을 합쳐 게이트를 판정한다. 같은 blocker가 두 번 반복되면 세 번째 @@ -752,17 +741,13 @@ NAS/public/DB mutation owner는 항상 한 에이전트만 둔다. D:\workspace\vignette의 docs/HANDOFF.md를 인수인계 SSOT로 읽고, AGENTS.md → docs/README.md → docs/dev_dashboard.html → docs/TODO.md → docs/ops/backlog-2026-06-26.md 순서로 현재 상태를 재확인해. -현재 master는 HEAD 94c681d…7fb이고 origin/master보다 2 commit 앞서며, timestamp·voice provider/model, -G7 topology·public launcher·G8 clean-head·SSOT가 -아직 tracked dirty다. 기존 D:\workspace\vignette-clean-head-94c681d와 g8-focus-head-94c681d.tar는 이 최신 변경을 -포함하지 않으므로 배포 입력으로 사용하지 마. 먼저 git status/diff와 각 lane의 focused test를 재확인해. +G8 배포 source는 HEAD `61a41d1f…6af`·tree `87dec55d…3b77`·archive `4d15d055…119d4d`다. 격리 NAS의 +API/Web `d5021950…e4b1`/`9796c092…4b36`, candidate 112/112, actual NAS-origin 112/112, health·auth 401·OpenAPI +126을 통과했고 fresh dump `36ec8748…24db8`과 previous `6030a677…c611`을 보존했다. G8은 DONE이며 새 material +milestone 전에는 NAS를 mutation하지 마. 현재 공유 worktree의 후속 dirty 변경은 이 배포 source와 분리해서 다뤄. -최종 목표는 G0~G8 아홉 목표를 코드-only가 아니라 실제 runtime 증거로 모두 닫는 것이다. G0~G6은 현재 내부 DONE -증거를 보존하되 새 clean HEAD 전체 회귀로 재확인해. G8 rollback receipt와 clean-head controller는 구현됐지만 -2026-08-09 execute 두 번은 모두 NAS mutation 전에 fail-closed됐다. docs/HANDOFF.md §6.1의 naive -session timestamp 버그는 UTC/KST 계약과 focused disposable DB/API/browser E2E로 GREEN이다. 테스트 강제 클릭 우회는 -없다. 사용자 승인을 받아 이 변경을 새 clean commit으로 만들고 HEAD/tree/archive를 다시 결속해 격리 NAS preview에 -배포한 다음 실제 NAS HTTP origin의 전체 회기 E2E를 0 failure로 통과시켜. G7은 내부 구현만 DONE이다. current source 공개 배포, +최종 목표는 G0~G8 아홉 목표를 코드-only가 아니라 실제 runtime 증거로 모두 닫는 것이다. G0~G6과 G8은 DONE이다. +G7은 내부 구현만 DONE이다. current source 공개 배포, local_whisper/melotts ready, authenticated public WSS, 사용자의 명시 동의를 받은 물리 마이크 50분 soak, 같은 시간창의 runtime/topology high-water, 독립 human voice-gain pack까지 모아 canonical checker exit 0을 만들어야 한다. G7 runner exit의 checker exit 0/gate_closed 결속, browser Origin과 API/WSS host 분리, 3,120초 capture와 @@ -779,12 +764,6 @@ worktree와 legacy 인자를 가리키며, 5분 watchdog은 최신 실행에서 현재 health ok지만 자동복구는 의도적으로 fail-closed다. clean commit 뒤 동일 stable release root로 두 task를 재등록하고 action pin·watchdog result 0·로그온/reboot smoke를 증명한 뒤에만 runtime DONE으로 바꿔. -NAS 17:07 기준선은 active 6030a677…c611, API/Web 52e0…8b2d/6fdb…f215, previous images 보존, -Compose 4개 running+unless-stopped, volume vignette-preview-20260807_pgdata, health/routes/assets GREEN, helper/listener 0이다. -release agent는 DB dump/restore를 지원하지 않으므로 execute 직전 fresh custom dump SHA/size/TOC와 DB identity를 -별도로 결속해. 자동 rollback은 image/Compose/active-state뿐이고 migration/data restore는 별도 owner 승인이야. -remote release root/Compose mode 0777 P1도 최소권한 preflight로 닫기 전에는 execute하지 마. - 학생 자기주도 폐루프는 홈 추천→회기→리뷰→G4 처방/G5 전이→별도 재연습→completed-session observer→ actual attempt/execution→read-model reload→before/after·멱등까지 desktop/mobile 실제 DB/browser로 검증해. 최종 검증은 API full, gateway full, schema/provenance, Web API types/typecheck/build, fixture E2E, disposable DB E2E, diff --git a/docs/TODO.md b/docs/TODO.md index 21a235d..b7acd53 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -44,9 +44,9 @@ `RuptureRepairCard.tsx`는 렌더 시점(`useRef`)에 호출해 화면 전체가 죽었다. 릴리스 게이트는 localhost 후보 스택(secure context)에서만 돌아 이 경로를 밟은 적이 없다. `src/lib/uuid.ts`의 `randomUuid()`로 통일했고 fallback도 `crypto.getRandomValues`를 우선 사용해 idempotency key의 - 예측 불가능성을 유지한다. 회귀는 `e2e/insecure-context-uuid.spec.ts` 6/6으로 고정했다(직접 호출 - 0건 검사 포함). **주의: 이 수정은 아직 NAS 프리뷰에 배포되지 않았다** — 배포된 SHA - `6030a677…c611`은 여전히 결함이 있는 빌드다. + 예측 불가능성을 유지한다. 문장 fingerprint의 `crypto.subtle.digest` 의존도 portable SHA-256 fallback으로 + 교체했다. source-only UUID/SHA 회귀 12/12와 secure/insecure exact digest 회귀를 고정했고, 최종 archive + `4d15d055…119d4d`의 실제 NAS 평문 origin browser review 112/112로 배포 검증까지 닫았다. - [x] **워치독 engine readiness 사각지대** — 2026-08-07 22:0x KST 소유자 승인으로 9099 재기동과 판정 기준 교정을 모두 마쳤다. 복구 실측: 9099 `/ready?force=true` 200 `ok:true, detail:OK`, API 8001과 공개 `api-vignette.chanpaca.net` 모두 `status:ok, engine:true, engine_detail:OK`. @@ -132,18 +132,18 @@ fail-closed 경계(동의 없음·pack 없음·3,120초 미만·host/Origin/scheme 불일치)는 CLI로 실증했다. 상세: `ops/outcome-os-g7-external-proof-readiness-2026-08-07.md`. -> **격리 NAS current baseline / G8 `runtime REVALIDATION`:** 2026-08-09 17:07 KST 프리뷰 -> `http://100.116.83.60:8088`, project `vignette-preview-20260807`은 active SHA `6030a677…c611`, exact -> API/Web `52e0…8b2d`/`6fdb…f215`로 실행 중이며 previous `79ec…4450`/`c530…2f28`도 보존한다. +> **격리 NAS current baseline / G8 `DONE`:** 2026-08-09 clean-head 프리뷰 +> `http://100.116.83.60:8088`, project `vignette-preview-20260807`은 source HEAD `61a41d1f…6af`·tree +> `87dec55d…3b77`의 active archive `4d15d055…119d4d`, exact API/Web `d5021950…e4b1`/ +> `9796c092…4b36`로 실행 중이며 previous active `6030a677…c611`과 이전 images도 보존한다. > `api/web/db/proxy` 4개는 running + `unless-stopped`, volume은 `vignette-preview-20260807_pgdata`다. -> health 3/3·db/engine true·auth 401·OpenAPI 126 + G1~G8 routes·root/JS/CSS 200, helper/listener는 모두 0이다. -> 보존 dump는 `92ed…1570f` 960,970 bytes와 `726982…5d41c` 1,015,222 bytes다. release agent는 DB +> candidate session E2E 112/112와 실제 NAS-origin browser review 112/112(11 specs, desktop/mobile/single-run), +> health ok·db/engine true·auth 401·OpenAPI 126을 통과했다. fresh dump `36ec8748…24db8`은 1,097,100 bytes, +> TOC 1,738 / TABLE DATA 129다. release agent는 DB > dump/restore를 수행하지 않으므로 매 execute 직전 fresh custom dump SHA/size/TOC와 DB identity를 별도 결속한다. > 자동 rollback은 image/Compose/active-state만 복구하며 적용 migration/data restore는 별도 owner 승인이 필요하다. -> remote release root/Compose mode `0777`은 P1 권한 위험이므로 execute 전 최소권한·preflight를 닫는다. localhost -> candidate 108/108과 postdeploy SSE→DB review는 과거 증거이고, 같은 SHA의 평문 origin은 UUID 결함 24건 실패라 -> current source의 완료 증거가 아니다. `randomUuid()` 수정 배포와 NAS-origin 전체 회기·SSE→DB·G4/G5 -> returned-practice 0 failure 전에는 G8을 DONE으로 바꾸지 않는다. 매일 04:30 KST `vignette-e2e` 자동화는 ACTIVE다. +> 과거 `6030a677…c611`의 localhost 108/108·평문 origin UUID 24건 실패와 후속 verified rollback은 이력으로 보존한다. +> 매일 04:30 KST `vignette-e2e` 자동화는 ACTIVE다. > 증거: [배포 증거](./ops/nas-preview-deployment-evidence-2026-08-07.md), [기계 판독 증거](./ops/evidence/nas-preview-current-deploy-2026-08-07.json), [브라우저 증거](./ops/evidence/nas-preview-live-turn-2026-08-07.png). 이는 G7 물리 마이크·독립 사람 평가 증거를 대체하지 않는다. - [ ] **claude_cli ↔ Anthropic API live 동일성** — provider 라우팅·모델 탐색·설정 저장 경로는 구현 완료. 남은 게이트는 연구팀/기관 `ANTHROPIC_API_KEY`를 게이트웨이 호스트에 주입한 뒤 같은 프롬프트의 live 응답·계량·오류 표면화를 비교하는 것이다. @@ -497,7 +497,7 @@ 증거 binding, source-only insecure-context 12/12 gate(UUID 6 + SHA-256 6), commit 전 NAS-origin 11-spec rollback gate를 unit 30/30·Ruff·py_compile로 고정했다. 첫 execute는 archive CRLF를 candidate에서 정규화하도록 fail-closed했고, 두 번째 execute는 API/types/build 뒤 109/5에서 fail-closed해 둘 다 NAS mutation 0이다. -- [ ] **G8 NAS-origin 재검증** — `randomUuid()`와 portable SHA-256 수정, insecure-context 12/12 회귀를 포함한 current HEAD를 +- [x] **G8 NAS-origin 재검증** — `randomUuid()`와 portable SHA-256 수정, insecure-context 12/12 회귀를 포함한 current HEAD를 위 clean-head mode로 격리 NAS preview에 배포한다. `started_at`의 9시간 오차 P0는 UTC `+00:00`, KST 달력 날짜, KST browser focused 실DB E2E로 수정했고 API 전체 921 passed와 코칭 이력 1/1을 통과했다. 시간종료 모달 강제 닫기 우회는 없다. 첫 clean commit `5221f79e` execute는 API/types/build와 candidate DB-backed 회기 후반까지 @@ -505,11 +505,11 @@ mutation 없이 중단됐다. source와 production preview에서 수정 회귀 2/2씩 통과했다. 이 테스트 수정의 새 clean commit `21461ab3` candidate 112/112 뒤 실제 NAS-origin G4 2건이 `crypto.subtle` 부재로 실패했고, release agent는 이전 image·health·auth·OpenAPI를 검증해 rollback했다. 문장 원문을 보내지 않는 portable SHA-256 fallback은 - secure localhost 2/2와 Tailnet insecure origin 2/2에서 동일 표준 digest를 통과했다. 이 수정의 새 clean - commit·HEAD/tree/archive를 결속해 실제 - `http://100.116.83.60:8088` origin에서 전체 회기 - E2E 0 failure·SSE→DB review·student returned-practice desktop/mobile을 다시 검증하기 전에는 G8 전체를 DONE으로 - 올리지 않는다. + secure localhost 2/2와 Tailnet insecure origin 2/2에서 동일 표준 digest를 통과했다. 최종 clean source HEAD + `61a41d1f…6af`·tree `87dec55d…3b77`·2회 동일 archive `4d15d055…119d4d`를 NAS에 승격했고 candidate + 112/112와 실제 `http://100.116.83.60:8088` origin browser review 112/112(11 specs, desktop/mobile/single-run), + health ok·db/engine true·auth 401·OpenAPI 126을 통과했다. API/Web는 `d5021950…e4b1`/`9796c092…4b36`, fresh dump는 + `36ec8748…24db8` 1,097,100 bytes·TOC 1,738/TABLE DATA 129이며 previous `6030a677…c611`을 보존했다. ### I-DAG. 첫 에이전트 실행팩 AOS-001~012 @@ -531,7 +531,7 @@ ## 지금 임계경로 (한 줄) 납품(공개 데모)의 핵심 블로커는 **A(공개 `/turn` proof + 배포 secrets)**. 그 다음이 **B(한신대 거버넌스)**와 **C(Phase 3 파일럿)** — C는 2026-07-13 회의로 **7월 말~8월 초 파일럿 일정 확정**, 그 전제로 **H의 P1(회기 종료 시간 기반)**이 -새 구현 게이트다. **I(Outcome & Alliance OS)는 현재 감사 기준 G0~G6이 internal DONE이다. -G8 receipt-bound 실제 NAS rollback은 완료됐고 UUID 수정 배포·NAS-origin 회기 재검증이 남았다. G7은 내부 구현 DONE과 외부 proof GATE를 분리하며, +새 구현 게이트다. **I(Outcome & Alliance OS)는 현재 감사 기준 G0~G6과 G8이 DONE이다. +G8은 receipt-bound 실제 rollback과 current clean source NAS-origin 112/112를 완료했다. G7은 내부 구현 DONE과 외부 proof GATE를 분리하며, current source 공개 배포·`local_whisper`/`melotts` ready·auth public WSS·명시 동의 mic·50분 soak·독립 라벨 benchmark·운영 resource high-water가 남았다.** D~G는 품질·확장 작업으로 게이트가 아니다. 상세 상태·검증 증거는 SSOT 대시보드가 소유한다. diff --git a/docs/dev_dashboard.html b/docs/dev_dashboard.html index 2ae89ee..224158c 100644 --- a/docs/dev_dashboard.html +++ b/docs/dev_dashboard.html @@ -290,15 +290,15 @@
project pulse -

지금 위치: G0~G6 internal DONE.
G8 runtime 재검증G7 external proof를 닫는 중.

-

현재: 18:02 KST Docker Desktop UI의 직접 DB stop을 같은 container·recovered named volume 재시작으로 복구했고 public DB·engine은 다시 정상이다. rollback receipt 2건도 보존했다. current fixture는 불변 110 + 최신 dashboard 10 분할 GREEN이고 자기주도 desktop/mobile 6/6을 재확인했다. 다음: clean commit에서 exact 120 재실행 → NAS-origin 0 failure → G7 공개 음성·물리 마이크·독립 사람 증거 순서로 진행한다.

+

지금 위치: G0~G6 · G8 DONE.
G7 external proof를 닫는 중.

+

현재: G8 clean-head는 source HEAD 61a41d1f…·tree 87dec55d…·2회 결정적 archive 4d15d055…119d4d로 격리 NAS에 승격됐다. candidate 112/112와 실제 NAS 평문 origin browser review 112/112가 모두 GREEN이고 health·db·engine, auth 401, OpenAPI 126도 통과했다. 다음: G7 current source 공개 배포 → 인증 WSS → 명시 동의 물리 마이크·운영 high-water·독립 사람 증거 순서로 진행한다.

운영 복구·watchdog·배포 상세 보기 -

2026-08-07 18:26 KST 공개 DB를 vignette_recovered_prod_20260807 named volume으로 전환했고 원본 container·dump·volume은 삭제하지 않았다. 기존 auth session은 복사하지 않아 사용자는 Google 재로그인이 필요하다. 2026-08-09 18:02:55 KST Docker Desktop UI가 active DB container에 직접 stop을 보내 public db=false가 재발했지만 같은 container ID와 recovered volume만 18:05 KST 재시작했다. 재기동 뒤 owner read-only 집계는 users 356·sessions 193·turns 726, Google users 16·Google-owned sessions 31이고 public health는 다시 status=ok·db=true·engine=true다. public task source pin 코드는 완료됐다. watchdog·로그온 boot는 detached clean release와 exact Git commit/tree·watchdog|boot/start SHA가 없으면 health·Docker·process mutation 전에 실패하고 hidden VBS도 pinned task만 trigger한다(provenance 11개 포함 combined 22/22, PS5.1·WSH PASS). 다만 실제 두 Scheduled Task action은 아직 shared worktree라 watchdog은 필수 pin 인자 부재로 LastTaskResult=1이며 자동복구가 의도적으로 fail-closed다. clean commit 뒤 두 task를 동일 stable release root로 재등록하고 action pin·result 0·로그온/reboot smoke를 확인해야 한다. G8 NAS SHA 6030a677…c611은 localhost candidate와 SSE→DB review는 통과했지만 평문 origin UUID 결함 빌드이므로 current clean source 재배포가 필요하다. G7 source는 runner/checker/topology 86/86, launcher/sidecar 80/80, 통합 166/166, API voice 71/71이지만 public은 아직 OpenAI provider·Git 없는 냉동 release라 external GATE다.

+

2026-08-07 18:26 KST 공개 DB를 vignette_recovered_prod_20260807 named volume으로 전환했고 원본 container·dump·volume은 삭제하지 않았다. 기존 auth session은 복사하지 않아 사용자는 Google 재로그인이 필요하다. 2026-08-09 18:02:55 KST Docker Desktop UI가 active DB container에 직접 stop을 보내 public db=false가 재발했지만 같은 container ID와 recovered volume만 18:05 KST 재시작했다. 재기동 뒤 owner read-only 집계는 users 356·sessions 193·turns 726, Google users 16·Google-owned sessions 31이고 public health는 다시 status=ok·db=true·engine=true다. public task source pin 코드는 완료됐다. watchdog·로그온 boot는 detached clean release와 exact Git commit/tree·watchdog|boot/start SHA가 없으면 health·Docker·process mutation 전에 실패하고 hidden VBS도 pinned task만 trigger한다(provenance 11개 포함 combined 22/22, PS5.1·WSH PASS). 다만 실제 두 Scheduled Task action은 아직 shared worktree라 watchdog은 필수 pin 인자 부재로 LastTaskResult=1이며 자동복구가 의도적으로 fail-closed다. clean commit 뒤 두 task를 동일 stable release root로 재등록하고 action pin·result 0·로그온/reboot smoke를 확인해야 한다. G8 NAS는 archive 4d15d055…119d4d, API/Web d5021950…e4b1/9796c092…4b36으로 current clean source 승격과 actual-origin 112/112를 완료했다. 이전 6030a677…c611과 이미지는 복구 기준으로 보존했다. G7 source는 runner/checker/topology 86/86, launcher/sidecar 80/80, 통합 166/166, API voice 71/71이지만 public은 아직 OpenAI provider·Git 없는 냉동 release라 external GATE다.

- Fixture E2E 110 + dashboard 10 분할 GREEN · clean HEAD 120/120 대기 + G8 candidate/NAS-origin E2E 112/112 · 112/112 현재 전체 회귀 · gateway 58 · backend 921 Outcome clean E2E 126 · visual 15/15 Alliance G1 baseline 1.2 9/9 · 내부 DONE @@ -657,6 +657,7 @@

189차 적용(2026-08-09): G7 외부 실행 전 P0를 소스에서 닫았다. production runner exit 0은 canonical checker 0·gate_closed=true에 결속하고, browser Origin allowlist를 API/WSS/admin/topology transport host·scheme과 분리했다. capture는 최소 3,120초·ceil+terminal sample이며 checker는 voice/runtime/topology 공통 교집합 3,000초를 강제한다. Windows 증거는 detached-clean commit/tree, runner/collector/checker SHA, psutil==6.1.1을 매 sample 전후 검증한다. fresh public launcher는 mutation 전에 source·Python·cloudflared·config SHA를 고정하고, legacy API와 exact-config cloudflared를 bounded 교체해 새 PID/start/exe·command SHA/실제 cwd의 raw command-line 없는 receipt를 남긴다. receipt destination은 runtime mutation 전에 create/flush/atomic-replace 권한을 검사하고 기존 receipt를 보존한 채 원자 게시한다. 검증: G7 runner/checker/topology 86/86, launcher/sidecar 80/80, 통합 166/166, API 921, gateway 58, Web api-types·typecheck·build, 자기주도 desktop/mobile 6/6, Ruff·compile·PS5.1 parser·diff-check PASS. 실제 public 실행·마이크·사람 평가는 하지 않았으므로 G7은 external GATE다.

190차 적용(2026-08-09): exact include 52개를 clean commit 5221f79e…1c69으로 고정하고 tree e4f15001…308b, 2회 동일 archive 1109bf86…0f4b를 결속했다. fresh NAS dump 6f4b95a7…b529f(1,015,222 bytes·TOC 1,752/TABLE DATA 129) 뒤 execute는 API 921·types·typecheck·build·insecure-context 6/6·candidate DB 회기 후반까지 통과했지만 110/112에서 승격 전에 fail-closed했다. 실패 2건은 Vite entry /src/main.tsx만 차단하던 boot 진단 테스트가 production hashed entry /assets/index-*.js를 차단하지 못한 환경 계약 누락이었다. 두 entry를 함께 차단하도록 고쳐 Vite source 2/2와 production preview 2/2를 통과했다. NAS mutation·rollback·DB migration은 0이며, 테스트 수정의 새 clean commit을 재결속한 한 번의 execute가 남았다.

191차 적용(2026-08-09): 후속 clean commit 21461ab3…6fde·tree 5c5f12a8…524e·archive 20d49694…fa1a는 candidate 112/112를 통과하고 NAS 새 API/Web d5021950…/376a3aa8…를 올렸다. 실제 NAS-origin postdeploy G4 desktop/mobile 2건이 110/112에서 실패하자 active-state commit 전에 이전 52e0e816…/6fdbb646…로 자동 rollback했고 health·auth 401·OpenAPI 126·G0~G8 route·image ID를 재검증해 rollback verified로 종료했다. exact-image 평문 origin에서 문장 fingerprint의 crypto.subtle.digest 의존이 요청 전 예외를 내는 원인을 확인했다. 원문 외부 전송 없이 portable SHA-256 fallback을 추가하고 표준 digest를 payload에 exact 고정해 secure localhost 2/2와 Tailnet insecure origin 2/2를 통과했다. 새 clean commit의 전체 candidate+NAS-origin 0 failure 전까지 G8은 runtime REVALIDATION이다.

+

192차 적용(2026-08-09): G8 clean-head 최종 승격을 완료했다. source HEAD 61a41d1f…6af·tree 87dec55d…3b77에서 2회 동일하게 만든 archive 4d15d055d74f…119d4d를 active release로 고정했고 candidate session E2E 112/112와 실제 http://100.116.83.60:8088 평문 NAS-origin browser review 112/112(11 specs, desktop/mobile/single-run)를 모두 통과했다. 실행 이미지는 API d5021950…e4b1·Web 9796c092…4b36이며 health는 status=ok·db=true·engine=true, 비인증 auth 401, OpenAPI 126을 재확인했다. execute 직전 fresh custom dump 36ec8748…24db8(1,097,100 bytes, TOC 1,738 / TABLE DATA 129)를 보존했고 이전 active 6030a677…c611과 이전 이미지는 rollback 기준으로 남겼다. 앞선 fail-closed·verified rollback 이력은 삭제하지 않는다. 이 증거로 G8을 DONE으로 승격하며 남은 Outcome OS gate는 G7 external proof다.

래스터만 사용이미지 생성 도구 산출물은 PNG 기반 시안이다. SVG·벡터·와이어프레임·로고 시트로 해석하지 않는다.
기능 우선메인 라우트의 실제 액션과 정보 구조를 먼저 반영한다. 장식은 기능을 가리지 않는 수준에서만 쓴다.
@@ -913,9 +914,9 @@
내부 구현 DONE

consent→단일 clock→독립 text/voice/fusion→철회·tombstone, streaming interim/final·word timestamp·provider event, HMAC word pseudonym, 열린 stream의 1초 동의 재검사·abort, 텍스트 보존·음성 재연결 UX를 연결했다. 운영 기본 provider는 노트북 상주 local_whisper/melotts이고 외부 Deepgram/OpenAI adapter는 fallback으로 보존한다. 격리 synthetic PCM soak는 59/59 연결 종료·성공 발화 55·철회 차단 2·미동의 차단 1·overflow 거부/복구 1/1·10MiB 선초과 거부를 통과했다. Windows host topology는 detached-clean commit/tree·도구 SHA·psutil version과 PID/start/exe·command SHA/cwd·process/TCP high-water를 fail-closed로 수집하고, 3,120초 capture의 공통 3,000초를 canonical exit와 결속한다. current source의 public launcher도 sidecar-before-API lifecycle, local_whisper/small/cpu-int8·melotts/melotts-korean exact readiness, provider env, Uvicorn --ws websockets --ws-max-queue 4, fresh API/cloudflared provenance receipt를 연결했다. runner/checker/topology 86/86·launcher/sidecar 80/80·통합 166/166·API voice 71/71을 통과했다.

외부 종료 GATE

새 launcher는 아직 public에 실행하지 않았다. 현재 public API는 Git metadata가 없는 냉동 release cwd와 OpenAI voice provider로 실행돼 새 Windows provenance gate를 통과하지 못한다. 검증된 clean commit 기반 runtime으로 정렬하고 /admin/voice-runtime과 local provider/model ready를 실제 공개 경로에서 확인한 뒤 authenticated public WSS, 실행 직전 명시 동의 물리 마이크 50분 양방향 soak, 같은 public host·공통 3,000초 시간창의 worker/Uvicorn queue와 process/TCP high-water, 30명/50회기/150축 독립 라벨 held-out voice-gain을 수집해야 한다. 네 artifact가 scripts/check-g7-external-proof.py exit 0을 만들기 전에는 메인 상태를 DONE으로 바꾸지 않는다.

-
- -
agentic worker + 운영 환류 + human gate

승인 source의 실제 hash·PII를 engine 호출 전에 검사하고 structured generator 1개, 결과를 공유하지 않는 red-team 2개, variant generator와 variant별 독립 judge 3개 이상을 실행한다. 정답 누수·PII·unknown source claim·open finding·안전/보상 해킹·모델/structured 실패는 store 호출 전에 차단한다. repo-approved source는 기본 비활성 scheduler가 durable lease/retry job으로 관리한다. 실제 claude_cli/claude-opus-4-8 7 calls와 dev PostgreSQL reviewer 2·variant 3·pass rate 1.0을 거쳐 synthetic payload를 pending_human_approval로 저장했다. G6 canonical drift trigger는 metadata-only incident→DAG→job을 원자 enqueue하고, 실제 pending 후보의 관리자 검수는 catalog 0→1·approval 1과 역할 403·멱등/409·민감정보 비노출을 통과했다. 별도 누수 후보는 422·저장 0이다. 상세: ops/outcome-os-g8-agentic-worker-evidence-2026-08-07.md.

NAS-origin runtime 재검증

SHA 6030a677…c611의 API/Web 이미지는 health 3/3·db/engine true, OpenAPI 126·auth 401·실제 SSE→DB-backed review를 통과했다. 그러나 108/108은 localhost candidate 결과였고 같은 NAS 평문 origin에서는 crypto.randomUUID 예외로 24건이 실패했다. 소스는 UUID fallback에 더해 문장 fingerprint의 portable SHA-256 fallback을 추가했고 source-only Vite 12/12·typecheck·build를 통과했다. clean-head controller도 HEAD/tree/archive 2회 결정성·archive-only candidate·patch 무회귀와 commit 전 actual-origin 11-spec rollback gate를 unit 30/30로 고정했다. 첫 current clean commit 5221f79e는 production entry 차단 테스트 2건으로 110/112에서 승격 전 중단됐고, 후속 21461ab3는 candidate 112/112 뒤 실제 NAS-origin G4 2건에서 실패해 이전 이미지로 verified rollback됐다. entry 회귀와 secure/insecure SHA 회귀는 모두 GREEN이다. 새 clean commit을 결속하고 동일 NAS-origin 회기 묶음이 GREEN이 되기 전에는 G8 전체를 DONE으로 올리지 않는다.

실제 rollback receipt DONE

runtime+release_gate 전용 executor는 승인·idempotency·artifact·Compose/env/resolved-policy SHA와 exact API/Web image를 일회성 manifest에 결속하고 HMAC journal·crash recovery·previous-image 복구를 고정한다. 2026-08-07 격리 NAS에서 별도 loopback control-plane API(127.0.0.1:8018)와 별도 executor(127.0.0.1:18149)로 실제 image rollback 2회를 실행했다. rollback-old는 receipt nas-g8-723eeef2…로 previous 이미지 79ec…4450/c530…2f28를, restore-current는 receipt nas-g8-2738846c…로 current 이미지 52e0…8b2d/6fdb…f215를 활성화했다. release gate·approval 각각 2회 멱등, lifecycle rollback/executed 2건, 승인 authorize_rollback 2건, silent auto-promotion 0, HMAC journal 6-record 체인 검증, health 3/3·OpenAPI 126·auth 401·Web 200을 확인했다. helper는 image 상속 compose label을 baseline으로 두고 project/service를 격리 override하는 계약으로 검증했다(launch-nas-preview-g8-helpers.py, 37/37). 상세: 런북 · 기계 판독 증거.

+
+ +
agentic worker + 운영 환류 + human gate

승인 source의 실제 hash·PII를 engine 호출 전에 검사하고 structured generator 1개, 결과를 공유하지 않는 red-team 2개, variant generator와 variant별 독립 judge 3개 이상을 실행한다. 정답 누수·PII·unknown source claim·open finding·안전/보상 해킹·모델/structured 실패는 store 호출 전에 차단한다. repo-approved source는 기본 비활성 scheduler가 durable lease/retry job으로 관리한다. 실제 claude_cli/claude-opus-4-8 7 calls와 dev PostgreSQL reviewer 2·variant 3·pass rate 1.0을 거쳐 synthetic payload를 pending_human_approval로 저장했다. G6 canonical drift trigger는 metadata-only incident→DAG→job을 원자 enqueue하고, 실제 pending 후보의 관리자 검수는 catalog 0→1·approval 1과 역할 403·멱등/409·민감정보 비노출을 통과했다. 별도 누수 후보는 422·저장 0이다. 상세: ops/outcome-os-g8-agentic-worker-evidence-2026-08-07.md.

clean-head NAS-origin 종료 증거

source HEAD 61a41d1f…6af·tree 87dec55d…3b77·2회 동일 archive 4d15d055…119d4d를 active release에 결속했다. candidate session E2E 112/112와 실제 평문 NAS-origin browser review 112/112(11 specs, desktop/mobile/single-run)가 GREEN이며 API/Web d5021950…e4b1/9796c092…4b36, health ok·db/engine true, auth 401, OpenAPI 126을 확인했다. fresh dump 36ec8748…24db8(1,097,100 bytes, TOC 1,738 / TABLE DATA 129)와 previous 6030a677…c611을 보존했다.

실제 rollback receipt DONE

runtime+release_gate 전용 executor는 승인·idempotency·artifact·Compose/env/resolved-policy SHA와 exact API/Web image를 일회성 manifest에 결속하고 HMAC journal·crash recovery·previous-image 복구를 고정한다. 2026-08-07 격리 NAS에서 별도 loopback control-plane API(127.0.0.1:8018)와 별도 executor(127.0.0.1:18149)로 실제 image rollback 2회를 실행했다. rollback-old는 receipt nas-g8-723eeef2…로 previous 이미지 79ec…4450/c530…2f28를, restore-current는 receipt nas-g8-2738846c…로 current 이미지 52e0…8b2d/6fdb…f215를 활성화했다. release gate·approval 각각 2회 멱등, lifecycle rollback/executed 2건, 승인 authorize_rollback 2건, silent auto-promotion 0, HMAC journal 6-record 체인 검증을 확인했다. 상세: 런북 · 기계 판독 증거.

@@ -939,7 +940,7 @@
Phase 2aplanned

3역할 / OAuth / 페르소나

학습자, 교수자, 관리자 권한 분리와 페르소나 확장.

  • DoD: 타인 session 직접 접근 403
  • 범위: OAuth BFF, RBAC, 교수 검수 루프
  • 주의: visible_to와 인간 권한은 별도 레이어
Phase 2bplanned

음성 캐스케이드

STT → 엔진 → TTS 왕복, WSS/터널 안정성, barge-in 로깅.

  • DoD: 50분 WSS 끊김 실측
  • 범위: local_whisper STT + MeloTTS 기본, Deepgram/OpenAI fallback
  • 리스크: 실시간 레이턴시는 실측 전 완료 표기 금지
Phase 3planned

교육용 파일럿 / 효과성

수련생 파일럿, 사전사후 효과 측정, 재귀학습 데이터셋 산출.

  • DoD: 20명 완주 + KPI 충족
  • 측정: SUS, 자기효능감, κ/ICC, 환각률
  • 정정: IRB 게이트가 아니라 데이터 활용동의/개인정보 게이트
-
Outcome OSG7 external · G8 runtime

성과·동맹·수련 폐루프

측정 → 이상 탐지 → 원인 가설 → 재연습 → 전이 검증 → 숙련도 갱신.

  • 상태: G0~G6 internal DONE · G8 실제 NAS rollback receipt DONE/runtime 재검증 중 · G7 외부 voice/resource/human 대기
  • 완료 DAG: AOS-001~012
  • 운영: NAS SHA 6030a677…c611은 UUID 수정 전 빌드 · G8 receipt 2건 완료 · 수정 배포/NAS-origin E2E와 G7 canonical four-artifact checker exit 0 필요
+
Outcome OSG7 external

성과·동맹·수련 폐루프

측정 → 이상 탐지 → 원인 가설 → 재연습 → 전이 검증 → 숙련도 갱신.

  • 상태: G0~G6 internal DONE · G8 clean-head NAS-origin DONE · G7 외부 voice/resource/human 대기
  • 완료 DAG: AOS-001~012
  • 운영: NAS archive 4d15d055…119d4d · candidate/NAS-origin 112/112 · G7 canonical four-artifact checker exit 0 필요
정정 기준: MASTERPLAN 원본의 IRB 신청/승인 게이트는 MASTERPLAN_REVISIONS 기준으로 제거한다. Phase 2는 2a(3역할/OAuth/페르소나)와 2b(음성 캐스케이드)로 분리하고, Phase 1 DoD에는 얇은 음성 왕복 1턴 PoC를 포함한다.
@@ -1067,7 +1068,7 @@ Web typechecknpm run typecheckPassed Design SSOT / auth visualnpm run check:design-ssot / npx playwright test e2e/auth-visual.spec.ts --project=chromium-single-run --reporter=line / npx playwright test e2e/layout-visual-gate.spec.ts --project=chromium-single-run --reporter=lineSSOT checker passed; login/onboarding light-dark desktop-mobile 1 passed; 14 core screens × 7 widths visual gate 14 passed. - Full Playwright E2E baselinenpm run e2e:parallel / npm run e2e:single-run / npm run e2e:list2026-08-09 현재 수집은 620 tests / 44 files다. 현 작업트리의 620개 전체 GREEN은 아직 검증 전이며, current 핵심 fixture는 불변 110 + 최신 dashboard 10의 분할 GREEN이다. 이전 단일 120/120과 2026-07-15의 fixture desktop/mobile 166/166 + DB/engine/provider 직렬 49/49 = 215/215는 범위가 다른 역사 기준선으로 보존한다. + Full Playwright E2E baselinenpm run e2e:parallel / npm run e2e:single-run / npm run e2e:list2026-08-09 현재 수집은 622 tests / 44 files다. 이 숫자는 수집량이며 현 작업트리 전체 GREEN과 동일하지 않다. G8 clean-head release gate는 candidate 112/112와 실제 NAS-origin 112/112를 통과했다. 이전 단일 120/120과 2026-07-15의 fixture desktop/mobile 166/166 + DB/engine/provider 직렬 49/49 = 215/215는 범위가 다른 역사 기준선으로 보존한다. Refactor governance P1~P8ruff check app / pytest -q app / pytest -q engine_gateway / npm run typecheck / npm run check:api-types / npm run check:design-ssot / npm run check:dead-code / npm run check:duplication / npm run build / npm audit --audit-level=high / full PlaywrightBackend 400 passed, gateway 29 passed, web gates/build/audit passed, vulnerabilities 0, production duplication 1 clone/15 lines/0.03%, Playwright 215/215 passed. 상세 근거는 ops/refactor-governance-2026-07-15.md. API typegen SSOTnpm run check:api-typesPassed; FastAPI OpenAPI → src/lib/api.gen.ts stale check Outcome & Alliance OS G0py -3.11 -X utf8 -m pytest -p no:cacheprovider apps/api/app/test_measurement_contract.py apps/api/app/test_runtime_schema_ssot.py -q / scripts/check-measurement-ledger.sql / measurement·API contract checks / web typecheck / DB-backed session-persistence focused E2E 3종G0 contract/schema 11 passed, 기존 backend 100 passed, auth 39 passed. Python→JSON Schema→TypeScript→PostgreSQL enum·필수필드 계약이 일치하고 8개 deterministic benchmark가 검증됐다. Live PostgreSQL에서 learner/client/evaluator 가시 행 1/1/2, 교차 누수 0, append-only guard 2를 확인했다. 학습자 턴→교수자 대시보드, 워크시트 검수, 종료 deep 평가→durable 리뷰 E2E는 각각 1 passed. G0/AOS-001~004 완료. @@ -1144,12 +1145,12 @@ Docker image smokedocker build -f apps/api/Dockerfile . / docker run ... python -c "import app.main" / docker build -f apps/web/Dockerfile apps/webAPI/Web image build and API import smoke pass after packaging cleanup. Web build uses npm lockfile and ignores host node_modules; API image excludes local .env files. Deploy preflightpython scripts\check-deploy-preflight.py --skip-db --env-file infra\.env.example --allow-placeholder-secrets / DB mode with local DATABASE_URLPassed: exact-pinned API requirements, live coaching data/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. Fresh compose smokedocker compose -p vignette-packaging-smoke -f infra/docker-compose.yml up -d --build + proxy /api/healthPassed with dummy production-safe env: API healthy, DB healthy, web/proxy up, http://localhost:18080/api/health 200 with db:true, engine:true, engine_mode:"claude_cli". Smoke volumes/network removed after run. - 격리 NAS 프리뷰 · 회기 E2E 자동화http://100.116.83.60:8088 / vignette-e2e 매일 04:30 KST / 배포 증거 / 기계 판독 증거SHA 6030a677…c611을 전용 Compose 프로젝트·포트·네트워크·named volume에 승격했다. localhost candidate browser E2E 108/108, health 3/3, OpenAPI 126·auth 401·G0~G8, postdeploy SSE→DB review를 통과했고 predeploy dump와 이전 exact images를 보존했다. 같은 NAS 평문 origin은 UUID 결함으로 24건 실패했으므로 현재 소스 수정 배포와 NAS-origin 재검증이 남았다. 자동화는 ACTIVE이며 material milestone+release gate+배포 SHA 변경 때만 프리뷰를 갱신한다. G7 외부 mic/provider/human 증거는 별도다. + 격리 NAS 프리뷰 · 회기 E2E 자동화http://100.116.83.60:8088 / vignette-e2e 매일 04:30 KST / 배포 증거 / 기계 판독 증거current clean source HEAD 61a41d1f…6af·tree 87dec55d…3b77의 archive 4d15d055…119d4d를 전용 Compose 프로젝트·포트·네트워크·named volume에 승격했다. candidate session E2E 112/112와 실제 NAS 평문 origin browser review 112/112, health ok·db/engine true, OpenAPI 126·auth 401을 통과했고 fresh dump와 이전 active/images를 보존했다. 자동화는 ACTIVE이며 material milestone+release gate·NAS preflight+배포 SHA 변경 때만 프리뷰를 갱신한다. G7 외부 mic/provider/human 증거는 별도다. G7 외부 증거 3-artifact 동시 시간창 오케스트레이터scripts/run-g7-external-proof-window.py · 준비 문서soak·runtime·topology 세 캡처를 같은 host의 공통 3,000초 시간창으로 실행하고 checker까지 잇는다. CLI는 동의·장치 없음, human pack 없음, 3,120초 미만, host 불일치를 전부 fail-closed하며 --rehearse는 마이크를 열지 않는다. 기존 Linux Compose gate를 보존한 채 Windows host Uvicorn+cloudflared topology mode를 추가해 detached-clean source/tool SHA와 PID/start/exe·command SHA/cwd·process/TCP high-water를 검증했고 runner/checker/topology 86/86을 통과했다. current source launcher도 local sidecar/provider/queue와 fresh PID provenance를 fail-closed로 연결했지만 아직 공개에 실행하지 않았다. 남은 선행 조건: 공개 런타임은 119 paths 구버전이고 /admin/voice-runtime이 없으며 /voice/health는 OpenAI STT/TTS, local sidecar 9882/9883은 내려가 있다. API는 Git 없는 냉동 release cwd라 provenance gate도 닫혀 있다. clean committed source runtime으로 정렬한 뒤에만 물리 마이크 50분독립 human voice-gain pack을 수집한다. G7 external GATE · 최종 감사 정정run-g7-external-proof-window.pycheck-g7-external-proof.py상태는 external GATE 유지. 물리 마이크 전 코드 P0 3건은 완료됐다. runner 프로세스 exit는 canonical checker exit 0·gate_closed=true에 결속되고, browser Origin vignette.chanpaca.net은 API/WSS/admin/topology host api-vignette.chanpaca.net과 분리된 HTTPS allowlist로 검증되며, 세 artifact 교집합은 ≥3000s + 120초 sampler margin으로 강제된다. Windows proof는 detached-clean HEAD/tree·runner/collector/checker script SHA·exact psutil==6.1.1을 pin한다. 공개 창에서는 legacy PID를 재사용하지 않고 API/cloudflared를 pinned executable/config와 stable release cwd에서 강제 재시작해 safe provenance receipt를 결속한다. 남은 것은 clean-source 실제 배포와 4-artifact 실증이다. - G8 runtime REVALIDATION · 17:07 NAS 기준선vignette-preview-20260807 · run-outcome-os-release-agent.py상태는 runtime REVALIDATION 유지. active SHA 6030a677…c611, API/Web 52e0…8b2d/6fdb…f215 exact running, previous 79ec…4450/c530…2f28 보존. api/web/db/proxy 4개 running + unless-stopped, volume vignette-preview-20260807_pgdata, health 3/3·db/engine true·auth 401·OpenAPI 126 + G1~G8 routes·root/JS/CSS 200, helper/listener 0이다. dump는 92ed…1570f 960,970 bytes와 726982…5d41c 1,015,222 bytes. release agent는 DB dump/restore를 지원하지 않으므로 매 execute 직전 fresh dump SHA/size/TOC·DB identity를 별도 결속한다. 자동 rollback은 image/Compose/active-state뿐이고 migration/data restore는 별도 owner 승인이다. remote release root/Compose mode 0777 P1은 최소권한 preflight로 닫는다. + G8 DONE · clean-head NAS 기준선vignette-preview-20260807 · run-outcome-os-release-agent.py상태는 DONE. active archive 4d15d055…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 경계는 유지한다. 노트북 로컬 음성 스택 — faster-whisper STT + MeloTTS TTSscripts/local-whisper-stt-server.py · scripts/start-local-whisper-stt.ps1 · voice_stt_provider=local_whisper2026-08-08 소유자 결정으로 음성 양쪽을 노트북 상주 로컬 모델로 정했다. TTS는 처음 Higgs로 잡았으나 연구/비상업 라이선스라 config.py가 운영에서 차단하고 있었고 그 가드를 푸는 건 법적 판단이라, 상업 사용이 허용된 MeloTTS Korean(MIT)으로 바꿔 가드 자체를 불필요하게 만들었다. Kokoro-82M은 Apache 2.0이지만 공식 VOICES.md한국어가 없어 탈락했다. local Whisper는 현재 호스트의 cuDNN 부재를 반영해 small/cpu-int8를 운영 기본으로 고정했고, API health도 MeloTTS model을 실제 provider와 맞춰 보고한다. 합성 시드 8.72초에서 interim 9·final 5·word timestamp 12개, 한 글자 차이 전사를 확인했고 MeloTTS CPU RTF는 0.27~0.28, 합성음→local STT 왕복은 완전 일치했다. current 통합은 launcher/sidecar 80/80·G7 전체 166/166·API voice 71/71·API 전체 921·gateway 58을 통과했다. Higgs 운영 사용은 라이선스 가드가 남아 dev 전용이다. - 비-secure origin 회기 리뷰 크래시 수정apps/web/src/lib/uuid.ts · apps/web/e2e/insecure-context-uuid.spec.ts격리 NAS 프리뷰(평문 HTTP·비-localhost)에서 회기 리뷰 라우트가 crypto.randomUUID is not a function으로 error boundary에 잡혔다. 이 API는 secure context 전용인데 제품 코드 18곳이 fallback 없이 호출했고 RuptureRepairCard는 렌더 시점 호출이라 화면 전체가 죽었다. 릴리스 게이트는 localhost 후보 스택에서만 돌아 미검출. randomUuid()로 통일하고 fallback도 getRandomValues 우선으로 예측 불가능성을 유지했다. 회귀 6/6(직접 호출 0건 검사 포함), typecheck·build 통과. NAS 프리뷰에는 아직 미배포이며 배포된 SHA 6030a677…c611은 여전히 결함 빌드다. + 비-secure origin 회기 리뷰 크래시 수정apps/web/src/lib/uuid.ts · apps/web/src/lib/sha256.ts · apps/web/e2e/insecure-context-uuid.spec.ts격리 NAS 프리뷰(평문 HTTP·비-localhost)에서 드러난 crypto.randomUUIDcrypto.subtle.digest secure-context 의존을 portable fallback으로 교체했다. source-only UUID/SHA 회귀 12/12와 secure/insecure exact digest 회귀를 통과했고, 최종 archive 4d15d055…119d4d의 actual NAS-origin 112/112로 배포 검증까지 닫았다. 공개 워치독 engine readiness 사각지대(해결)scripts/watch-public-runtime.ps1 · scripts/start-public-runtime.ps1 · apps/api/engine_gateway/gateway.py2026-08-07 22:0x KST 소유자 승인으로 9099를 재기동해 복구했다. 재기동 직후 /ready?force=true 200 ok:true·detail:OK, API 8001과 공개 api-vignette.chanpaca.net 모두 status:ok·engine:true·engine_detail:OK다. 원인은 16:06부터 상주한 9099 프로세스 하나였다(CLI·코드·cwd는 정상 — 동일 인자 직접 실행과 EngineSession 재현이 "OK"를 반환). 재발 방지 3건: (1) 워치독·기동 스크립트의 engine 판정을 /health(프로세스 liveness)에서 /ready(실제 생성)로 바꾸고 API 판정에 engine을 추가, shared secret 인스턴스용 토큰 헤더와 503 본문 로깅 포함. (2) 게이트웨이가 자식 claude -p의 stderr를 상시 드레인해(PIPE 미독 시 자식 블록도 차단) 실패 detail에 exit·stderr를 붙인다 — 실증: empty engine response (error: unknown option '--vignette-nonexistent-flag-xyz'). (3) 기동 시 게이트웨이 로그 회전, Stop-UvicornByPortName -like python* 추가(호출자 자기 자신 종료 방지). 검증: pytest engine_gateway 58/58, 워치독 -CheckOnly healthy 5/5, 장애 재현(9299·CLAUDE_BIN 부재)에서 /healthok:true로 통과하고 /ready는 503으로 검출되어 워치독이 unhealthy (1/3) + 503 본문을 남겼다. NAS용 9100(06:27 상주)은 실사용 세션 7개가 있어 미접촉이라 게이트웨이 stderr 개선은 다음 재기동 때 반영된다. G8 실제 receipt-bound image rollbackscripts/launch-nas-preview-g8-helpers.py / scripts/serve-nas-preview-rollback-executor.py / scripts/probe-nas-preview-g8-rollback.py / 런북 / 기계 판독 증거격리 NAS에서 별도 executor(18149)·control-plane(8018) helper로 실제 image rollback 2회를 실행했다. receipt nas-g8-723eeef2…로 previous 79ec…4450/c530…2f28, receipt nas-g8-2738846c…로 current 52e0…8b2d/6fdb…f215를 활성화했다. release gate·approval 각각 2회 멱등, audit.ci_lifecycle_event rollback/executed 2건과 receipt id 결속, silent auto-promotion 0, HMAC journal 6-record 체인 검증, health 3/3·OpenAPI 126·auth 401·Web 200, helper 0·listener 0 복귀. helper 격리 계약은 image 상속 label baseline 대비 검증이며 fake-runner 37/37로 고정했다. down/volume rm/prune 미실행, 공개 런타임 미접촉. Actual deployment envinfra/.env owner-secret fill-inRemaining external step: deployment target must provide real APP_DB_PASSWORD, OAuth client id/secret, OPENAI_API_KEY, SESSION_SECRET, and production-safe engine/voice flags. Current local stray values such as ENGINE_MODE=claude_cli and prod sample TTS must not be copied. diff --git a/docs/guides/testing.md b/docs/guides/testing.md index 12f137c..af2767b 100644 --- a/docs/guides/testing.md +++ b/docs/guides/testing.md @@ -20,7 +20,7 @@ Vignette 저장소의 모든 검증 수단(백엔드 단위 테스트, 웹 타 | 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` | **필요(+시드)** | **필요** | 자동기동 | 일부만 | **필요** | 현재 수집 620 tests / 44 files · 현 작업트리 전체 GREEN 미검증 | +| Playwright E2E(전체) | `apps/web` | `npm run e2e` | **필요(+시드)** | **필요** | 자동기동 | 일부만 | **필요** | 현재 수집 622 tests / 44 files · 현 작업트리 전체 GREEN 미검증 | 핵심 원칙: **단위 테스트(pytest)와 타입체크/빌드는 외부 서비스 없이 단독 실행된다.** **E2E만 풀스택(DB+API+웹+브라우저)을 요구한다.** 아래 각 절에서 근거와 절차를 설명한다. @@ -271,8 +271,8 @@ VITE_API_BASE=http://127.0.0.1:8000 npm run e2e # 프록시 대신 API ### 3.6 실측 테스트 개수 (현재) -2026-08-09 `npx playwright test --list` 기준 **현재 수집 620 tests / 44 files**다. -이 숫자는 수집량이지 통과량이 아니다. 현 작업트리 전체 620개 완주는 아직 증거가 없으며, +2026-08-09 `npx playwright test --list` 기준 **현재 수집 622 tests / 44 files**다. +이 숫자는 수집량이지 통과량이 아니다. 현 작업트리 전체 622개 완주는 아직 증거가 없으며, 과거 전체 GREEN 기록과 이번 focused/release gate 결과를 구분해 적는다. - **병렬 시나리오**: 166 tests (desktop 83 + mobile 83) diff --git a/docs/ops/backlog-2026-06-26.md b/docs/ops/backlog-2026-06-26.md index 8a56245..f6bf74e 100644 --- a/docs/ops/backlog-2026-06-26.md +++ b/docs/ops/backlog-2026-06-26.md @@ -13,7 +13,7 @@ > 선제 구축했고, 임상 문안·평가기준·골든셋 콘텐츠는 임상팀(구훈정·어유경) 외부 정의로 받는다. > > **Outcome & Alliance OS** — 2026-08-06 정식 전략 실행 트랙으로 승격했다. G0 Measurement Truth, G1 -> 현재 소스·실행 증거 재감사에서는 G0~G6이 internal DONE이다. G1 승격 prompt 1.2+read-skew/JSON 복구는 24/24 ready·방향 9/9·오류 0을 재확인했고, G0 census 29/29·위반 0, G4/G5 실제 API/DB/브라우저 폐루프, G6 safety metadata-only 최우선 runtime을 disposable clone에서 확인했다. G8의 실제 receipt-bound image rollback 2회(`nas-g8-723eeef2…`/`nas-g8-2738846c…`)는 executed lifecycle receipt로 완료됐지만, 같은 NAS SHA의 평문 origin 회기 E2E 24건 실패 때문에 UUID 수정 배포·NAS-origin 재검증 전까지 전체 상태는 runtime REVALIDATION이다. 과거 clean release/NAS의 route·asset·auth·browser 증거는 배포 존재 이력으로 보존하지만 현재 소스의 완료 증거로 재사용하지 않는다. G7 Multimodal Alliance는 내부 구현 DONE과 외부 proof GATE를 분리하며 current source 공개 배포, 운영 기본 `local_whisper`/`melotts` ready, authenticated public WSS, 명시 동의 물리 마이크·50분 soak, 독립 라벨 voice-gain benchmark와 운영 topology resource high-water를 추적한다. 외부 Deepgram/OpenAI adapter는 fallback으로 보존한다. G0~G8과 +> 현재 소스·실행 증거 재감사에서는 G0~G6과 G8이 DONE이다. G1 승격 prompt 1.2+read-skew/JSON 복구는 24/24 ready·방향 9/9·오류 0을 재확인했고, G0 census 29/29·위반 0, G4/G5 실제 API/DB/브라우저 폐루프, G6 safety metadata-only 최우선 runtime을 disposable clone에서 확인했다. G8은 실제 receipt-bound image rollback 2회(`nas-g8-723eeef2…`/`nas-g8-2738846c…`)에 더해 source HEAD `61a41d1f…6af`·tree `87dec55d…3b77`·archive `4d15d055…119d4d`의 candidate 112/112와 실제 NAS 평문 origin 112/112를 통과했다. 과거 `6030a677…c611`의 UUID 24건 실패와 후속 SHA 결함 rollback은 이력으로 보존하며 현재 완료 증거로 재사용하지 않는다. G7 Multimodal Alliance는 내부 구현 DONE과 외부 proof GATE를 분리하며 current source 공개 배포, 운영 기본 `local_whisper`/`melotts` ready, authenticated public WSS, 명시 동의 물리 마이크·50분 soak, 독립 라벨 voice-gain benchmark와 운영 topology resource high-water를 추적한다. 외부 Deepgram/OpenAI adapter는 fallback으로 보존한다. G0~G8과 > AOS-001~012는 `docs/TODO.md` I절에서 전건 추적하고, 상태는 SSOT 대시보드의 9개 계획 카드가 소유한다. > 이 얇은 백로그에는 그중 외부·환경 증거가 필요한 항목만 기존 B2/B4/Phase 3 게이트와 합쳐 유지한다. @@ -24,26 +24,6 @@ - [ ] **운영 티켓 자동 분류·처리 후속** — Claude Recipe headless 자동 수정 후보, 관리자 승인 후 이슈 등록·PR/작업 스레드 생성, 처리 결과 audit trail 확장은 아직 설계/승인 필요. 담당 그룹 자동 배정·우선순위 escalation·raw/rollup 보존기간 같은 운영 정책은 B3에서 이미 결정됨(수동 승인·미도입 고정). 자동 수정은 운영자 승인 전까지 실행하지 않는다. -- [ ] **G8 clean-head 릴리스와 NAS-origin 재검증** — explicit clean-head mode와 source-only insecure-context gate는 - unit 30/30·UUID 6/6·SHA-256 6/6으로 구현됐다. 2026-08-09 execute 두 번은 각각 archive CRLF와 session E2E에서 - fail-closed되어 NAS mutation 0이었다. timezone 없는 `started_at`의 약 9시간 오차는 UTC/KST 계약, API 전체 - 921 passed, KST focused DB/browser 1/1로 수정했다. 사용자 승인 아래 새 clean commit의 HEAD/tree/archive를 - 결속한 뒤 격리 NAS preview에 승격해 실제 평문 origin의 전체 회기 E2E·SSE→DB review·student returned-practice를 - 0 failure로 재검증한다. fixture-only desktop/mobile 묶음은 16:49 KST 단일 120/120 뒤 dashboard 변경분 10/10을 - 따로 재검증해 current tree는 불변 110 + 최신 dashboard 10의 분할 GREEN이며, clean HEAD에서 exact 120을 다시 - 실행한다. 어느 쪽도 NAS runtime 증거를 대신하지 않는다. release agent는 DB dump/restore를 지원하지 않으므로 매 execute 직전 fresh custom dump의 - SHA/size/TOC와 DB identity를 별도 증거로 결속한다. agent 자동 rollback은 image/Compose/active-state 범위뿐이며, - 적용 migration/data restore는 별도 owner 승인을 받는다. remote release root/Compose mode `0777` P1도 최소권한 - preflight로 닫는다. 이때만 G8 전체 DONE이 가능하다. - 첫 current clean commit `5221f79e`는 HEAD/tree/archive와 fresh NAS dump를 결속했지만 candidate E2E - 110/112에서 production hashed entry를 차단하지 못한 boot-diagnostic 테스트 desktop/mobile 2건으로 승격 전 - 중단됐다. NAS mutation 0이다. `/src/main.tsx`와 `/assets/index-*.js`를 함께 차단하는 수정은 Vite source 2/2, - production preview 2/2를 통과했으며 새 clean commit으로 한 번만 재실행한다. - 후속 `21461ab3` candidate는 112/112를 통과했지만 실제 NAS-origin deliberate-practice 2건이 - `crypto.subtle.digest` 부재로 실패했고 이전 이미지 rollback·health/auth/OpenAPI 검증은 완료됐다. 원문을 서버로 - 보내지 않는 portable SHA-256 fallback과 exact digest assertion은 secure localhost 2/2·Tailnet insecure 2/2를 - 통과했다. 새 clean commit을 결속한 전체 execute 0 failure가 여전히 종료조건이다. - > B1 완료 항목(학생 리뷰 1~5 척도 320px 반응형 재배치, 셸 구분선, 세션 종료 UX/다크테마, 아바타 SVG 리그 복귀와 래스터 비활성화, SEO/공유 카드, 레이아웃 정렬, 권한 위임, > 메일링, 아카이브 API, TTS voice map, 빈상태 레이아웃, 평가 실패 복구 UX, SSE 저장, live-coach 표면화, > 음성 메타, 위기 게이트 UI, 운영 티켓/헬스 샘플러 등)의 상세·검증 로그는 아카이브 full-history 참조. @@ -113,11 +93,7 @@ G7 통합 166/166을 통과했다. 이제 clean source 공개 배포·rehearse 전에는 물리 마이크를 열지 않는다. 격리 NAS 프리뷰 `http://100.116.83.60:8088`은 전용 Compose 프로젝트·포트·네트워크·볼륨에 배포했고, 실제 브라우저 회기와 review API 저장 축어록 2턴을 확인했다. 기존 프로젝트 중단·재생성 명령은 실행하지 않았다. 매일 04:30 KST `Vignette 회기 E2E 정기 검증` - (automation id `vignette-e2e`)은 ACTIVE이며 material milestone+release gate·NAS preflight+배포 SHA 변경 때만 프리뷰를 갱신한다. 2026-08-07 SHA `6030a677af7e87cbfabc422b553d108d53414fd3c446548734a13b036d35c611`은 API/Web 새 이미지, localhost candidate E2E 108/108, OpenAPI 126, auth 401, G0~G8, postdeploy browser SSE→DB review와 health 3/3을 통과했다. 같은 NAS 평문 origin에서는 UUID 결함으로 24건 실패했으므로 current source 배포 증거로 재사용하지 않는다. 실제 receipt-bound rollback은 같은 프리뷰에서 별도 helper로 두 번 실행해 종료했다([런북](./nas-preview-g8-rollback-proof-runbook.md), [기계 판독 증거](./evidence/nas-preview-g8-actual-rollback-2026-08-07.json)). 첫 예약 실행 이력은 대기 상태다. 증거: [배포 증거](./nas-preview-deployment-evidence-2026-08-07.md), - 2026-08-09 17:07 KST current baseline은 exact API/Web `52e0…8b2d`/`6fdb…f215`, previous - `79ec…4450`/`c530…2f28` 보존, `api/web/db/proxy` 4개 running + `unless-stopped`, DB volume - `vignette-preview-20260807_pgdata`, health 3/3·db/engine true·auth 401·OpenAPI 126 + G1~G8 routes·root/JS/CSS - 200, helper/listener 0이다. dump `92ed…1570f` 960,970 bytes와 `726982…5d41c` 1,015,222 bytes도 존재한다. + (automation id `vignette-e2e`)은 ACTIVE이며 material milestone+release gate·NAS preflight+배포 SHA 변경 때만 프리뷰를 갱신한다. 2026-08-07 SHA `6030a677af7e87cbfabc422b553d108d53414fd3c446548734a13b036d35c611`의 localhost 108/108과 평문 origin UUID 24건 실패는 역사 기준선으로 보존한다. 실제 receipt-bound rollback은 같은 프리뷰에서 별도 helper로 두 번 실행해 종료했다([런북](./nas-preview-g8-rollback-proof-runbook.md), [기계 판독 증거](./evidence/nas-preview-g8-actual-rollback-2026-08-07.json)). 현재는 source HEAD/tree `61a41d1f…6af`/`87dec55d…3b77`의 active archive `4d15d055…119d4d`, exact API/Web `d5021950…e4b1`/`9796c092…4b36`이 실행 중이다. candidate 112/112와 실제 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 `6030a677…c611`을 보존했다. 첫 예약 실행 이력은 대기 상태다. 증거: [배포 증거](./nas-preview-deployment-evidence-2026-08-07.md), [브라우저 증거](./evidence/nas-preview-live-turn-2026-08-07.png). 명시 동의 물리 마이크와 독립 human voice-gain 증거는 아직 없으므로 G7은 external GATE로 유지한다. - [ ] **claude_cli ↔ Anthropic API live 동일성** — provider 라우팅·Anthropic `/v1/models` 탐색·지원 추론 강도·관리자 fail-closed 저장 경로는 구현 완료. 남은 범위는 연구팀/기관 `ANTHROPIC_API_KEY`를 게이트웨이 호스트에 주입한 live 응답·계량·오류 표면화 비교다. Claude CLI·Codex CLI(Terra/Medium)·Agy CLI(Gemini 3.6 Flash/High)는 로컬 live probe를 통과했다. - [ ] **stable-source task 재등록 + 재부팅 후 watchdog smoke** — 사용자 승인 clean commit의 detached release diff --git a/scripts/check-dev-dashboard-ssot.py b/scripts/check-dev-dashboard-ssot.py index c49e0ee..09dfa72 100644 --- a/scripts/check-dev-dashboard-ssot.py +++ b/scripts/check-dev-dashboard-ssot.py @@ -23,8 +23,8 @@ DEFAULT_LOCAL_DEVELOPMENT = REPO_ROOT / "docs" / "guides" / "local-development.m DEFAULT_TODO = REPO_ROOT / "docs" / "TODO.md" EXPECTED_STATUS_COUNTS = { - "done": 32, - "doing": 3, + "done": 33, + "doing": 2, "planned": 0, } EXPECTED_OWNER_COLUMN_COUNTS = {