From 5221f79e3f4170c95b484c71fdf702928ead1c69 Mon Sep 17 00:00:00 2001 From: Yun Chan Date: Sun, 9 Aug 2026 18:22:03 +0900 Subject: [PATCH] =?UTF-8?q?G7=20=EC=A6=9D=EB=AA=85=EA=B3=BC=20G8=20clean-h?= =?UTF-8?q?ead=20=EC=8A=B9=EA=B2=A9=20=EC=A4=80=EB=B9=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/api/app/config.py | 2 +- apps/api/app/routes/voice.py | 10 +- apps/api/app/services/session_metrics.py | 4 +- apps/api/app/services/voice.py | 2 +- apps/api/app/session_read_model.py | 11 +- apps/api/app/test_session_read_model.py | 49 + apps/api/app/test_voice_service.py | 6 +- apps/api/app/test_voice_ws.py | 22 + apps/api/requirements.txt | 2 + apps/web/e2e/admin.spec.ts | 34 +- apps/web/e2e/dev-dashboard.spec.ts | 33 +- .../e2e/self-directed-learning-loop.spec.ts | 54 + apps/web/e2e/session-mvp.spec.ts | 30 + apps/web/e2e/session-persistence.spec.ts | 19 + apps/web/e2e/session-review-fixture.ts | 7 + apps/web/e2e/session-review.spec.ts | 11 +- apps/web/index.html | 80 +- docs/HANDOFF.md | 552 ++++++++++- docs/TODO.md | 92 +- docs/decisions/local-voice-stack.md | 12 + docs/dev_dashboard.html | 77 +- docs/guides/local-development.md | 27 +- docs/guides/testing.md | 20 +- docs/ops/backlog-2026-06-26.md | 62 +- ...an-head-material-milestone-2026-08-09.json | 33 + .../nas-preview-g8-rollback-proof-runbook.md | 2 +- ...-g7-external-proof-readiness-2026-08-07.md | 44 +- docs/ops/public-runtime-watchdog.md | 289 ++++-- scripts/boot-public-runtime.ps1 | 122 ++- scripts/capture-g7-topology-evidence.py | 926 +++++++++++++++++- scripts/check-dev-dashboard-ssot.py | 4 +- scripts/check-g7-external-proof.py | 575 ++++++++++- scripts/install-public-runtime-task.ps1 | 82 +- scripts/local-whisper-stt-server.py | 4 +- scripts/melotts-server.py | 1 + scripts/probe-public-voice-sidecars.py | 282 ++++++ scripts/register-boot-task.ps1 | 97 +- scripts/run-g7-external-proof-window.py | 288 +++++- scripts/run-outcome-os-release-agent.py | 333 ++++++- scripts/start-local-whisper-stt.ps1 | 4 +- scripts/start-public-runtime.ps1 | 796 ++++++++++++++- scripts/test_g7_external_proof.py | 276 ++++++ scripts/test_g7_topology_evidence.py | 360 +++++++ scripts/test_local_whisper_stt_server.py | 6 +- scripts/test_melotts_server.py | 2 + scripts/test_outcome_os_release_agent.py | 380 ++++++- ...test_public_runtime_watchdog_provenance.py | 408 ++++++++ scripts/test_public_voice_sidecar_probe.py | 144 +++ scripts/test_run_g7_external_proof_window.py | 227 ++++- scripts/test_start_public_runtime_contract.py | 331 +++++++ scripts/watch-public-runtime-hidden.vbs | 37 +- scripts/watch-public-runtime.ps1 | 111 ++- 52 files changed, 6876 insertions(+), 506 deletions(-) create mode 100644 apps/api/app/test_session_read_model.py create mode 100644 docs/ops/evidence/g8-clean-head-material-milestone-2026-08-09.json create mode 100644 scripts/probe-public-voice-sidecars.py create mode 100644 scripts/test_public_runtime_watchdog_provenance.py create mode 100644 scripts/test_public_voice_sidecar_probe.py create mode 100644 scripts/test_start_public_runtime_contract.py diff --git a/apps/api/app/config.py b/apps/api/app/config.py index bbc4051..be9673b 100644 --- a/apps/api/app/config.py +++ b/apps/api/app/config.py @@ -153,7 +153,7 @@ class Settings(BaseSettings): validation_alias="VIGNETTE_LOCAL_WHISPER_STT_URL", ) local_whisper_stt_model: str = Field( - default="large-v3", + default="small", validation_alias="VIGNETTE_LOCAL_WHISPER_STT_MODEL", ) local_whisper_stt_language: str = Field( diff --git a/apps/api/app/routes/voice.py b/apps/api/app/routes/voice.py index 430bc6e..ea7a229 100644 --- a/apps/api/app/routes/voice.py +++ b/apps/api/app/routes/voice.py @@ -206,9 +206,13 @@ async def voice_health() -> JSONResponse: "stt_model": voice_service.stt_model(), "stt_batch_fallback_available": voice_service.batch_stt_available(), "tts_model": ( - voice_svc.HIGGS_TTS_MODEL - if tts_provider == "higgs" - else voice_svc.TTS_MODEL + voice_svc.MELOTTS_TTS_MODEL + if tts_provider == "melotts" + else ( + voice_svc.HIGGS_TTS_MODEL + if tts_provider == "higgs" + else voice_svc.TTS_MODEL + ) ), "tts_provider": tts_provider, "limits": { diff --git a/apps/api/app/services/session_metrics.py b/apps/api/app/services/session_metrics.py index d457804..66b0316 100644 --- a/apps/api/app/services/session_metrics.py +++ b/apps/api/app/services/session_metrics.py @@ -3,7 +3,7 @@ from __future__ import annotations from dataclasses import dataclass, field -from datetime import datetime +from datetime import datetime, timezone from typing import Any, Callable from ..store import InProcSession @@ -44,7 +44,7 @@ class LearnerGrowthMetrics: def iso_datetime(ts: float | None) -> str | None: if ts is None: return None - return datetime.fromtimestamp(ts).isoformat(timespec="seconds") + return datetime.fromtimestamp(ts, tz=timezone.utc).isoformat(timespec="seconds") def session_activity_time(sess: InProcSession) -> float: diff --git a/apps/api/app/services/voice.py b/apps/api/app/services/voice.py index 7793111..e6df39c 100644 --- a/apps/api/app/services/voice.py +++ b/apps/api/app/services/voice.py @@ -998,7 +998,7 @@ class VoiceService: local_whisper_stt_model if local_whisper_stt_model is not None else settings.local_whisper_stt_model - ).strip() or "large-v3" + ).strip() or "small" self._local_whisper_stt_language = ( local_whisper_stt_language if local_whisper_stt_language is not None diff --git a/apps/api/app/session_read_model.py b/apps/api/app/session_read_model.py index 7d9caf1..fb37b48 100644 --- a/apps/api/app/session_read_model.py +++ b/apps/api/app/session_read_model.py @@ -10,7 +10,7 @@ from __future__ import annotations import re from collections import Counter from dataclasses import dataclass -from datetime import datetime +from datetime import datetime, timedelta, timezone from typing import Any, Literal, Optional, cast from pydantic import BaseModel, Field @@ -31,6 +31,7 @@ WorksheetItemSpec = tuple[str, str, list[str], WorksheetSpeaker | None] WorksheetSectionSpec = tuple[str, str, list[WorksheetItemSpec]] LEARNER_VISIBLE_AI_ROLE = "counselor" +KOREA_STANDARD_TIME = timezone(timedelta(hours=9), name="KST") MISSING_SESSION_EVALUATION_GRACE_SECONDS = 30.0 MISSING_SESSION_EVALUATION_ERROR = ( "회기말 평가가 제한 시간 이후에도 저장되지 않았습니다. AI 평가 재시도가 필요합니다." @@ -478,7 +479,11 @@ def stage_label(stage: object) -> StageLabel: def iso(ts: float | None) -> str | None: if ts is None: return None - return datetime.fromtimestamp(ts).isoformat(timespec="seconds") + return datetime.fromtimestamp(ts, tz=timezone.utc).isoformat(timespec="seconds") + + +def session_calendar_date(ts: float) -> str: + return datetime.fromtimestamp(ts, tz=KOREA_STANDARD_TIME).strftime("%Y-%m-%d") def learner_visible_turns(sess: InProcSession) -> list[TurnRecord]: @@ -1757,7 +1762,7 @@ def build_session_review(read_input: SessionReviewReadInput) -> SessionReviewRes initial=client_initial, persona=_persona_label(sess.persona_code, str(sess.persona.difficulty)), ), - date=datetime.fromtimestamp(sess.created_at).strftime("%Y-%m-%d"), + date=session_calendar_date(sess.created_at), durationLabel=_duration_label(duration_seconds), durationSeconds=duration_seconds, reachedPhase=reached_phase, diff --git a/apps/api/app/test_session_read_model.py b/apps/api/app/test_session_read_model.py new file mode 100644 index 0000000..85e7d38 --- /dev/null +++ b/apps/api/app/test_session_read_model.py @@ -0,0 +1,49 @@ +"""회기 읽기 모델의 시간 직렬화 계약 테스트.""" + +from __future__ import annotations + +import unittest +from datetime import datetime, timezone + +from .session_read_model import iso, session_calendar_date +from .services.session_metrics import iso_datetime + + +class SessionReadModelIsoTest(unittest.TestCase): + def test_none_remains_none(self) -> None: + self.assertIsNone(iso(None)) + + def test_epoch_is_serialized_as_utc_with_offset(self) -> None: + self.assertEqual(iso(0), "1970-01-01T00:00:00+00:00") + + def test_timestamp_round_trips_without_local_timezone_drift(self) -> None: + timestamp = 1_800_000_120.0 + + rendered = iso(timestamp) + + self.assertIsNotNone(rendered) + assert rendered is not None + parsed = datetime.fromisoformat(rendered) + self.assertEqual(parsed.tzinfo, timezone.utc) + self.assertEqual(parsed.timestamp(), timestamp) + + def test_serialized_timestamp_has_explicit_utc_suffix(self) -> None: + rendered = iso(1_800_000_120.0) + + self.assertIsNotNone(rendered) + assert rendered is not None + self.assertTrue(rendered.endswith("+00:00")) + + def test_dashboard_metrics_use_the_same_utc_contract(self) -> None: + timestamp = 1_800_000_120.0 + + self.assertEqual(iso_datetime(timestamp), iso(timestamp)) + + def test_review_calendar_date_uses_korea_standard_time(self) -> None: + utc_late_afternoon = datetime(2026, 8, 8, 15, 30, tzinfo=timezone.utc) + + self.assertEqual(session_calendar_date(utc_late_afternoon.timestamp()), "2026-08-09") + + +if __name__ == "__main__": + unittest.main() diff --git a/apps/api/app/test_voice_service.py b/apps/api/app/test_voice_service.py index 05042b4..daf7c3c 100644 --- a/apps/api/app/test_voice_service.py +++ b/apps/api/app/test_voice_service.py @@ -693,7 +693,7 @@ class LocalWhisperStreamingTest(unittest.IsolatedAsyncioTestCase): query = parse_qs(urlsplit(url).query) self.assertEqual(urlsplit(url).scheme, "ws") self.assertEqual(urlsplit(url).hostname, "127.0.0.1") - self.assertEqual(query["model"], ["large-v3"]) + self.assertEqual(query["model"], ["small"]) self.assertEqual(query["language"], ["ko"]) self.assertEqual(query["sample_rate"], ["16000"]) self.assertEqual(query["channels"], ["1"]) @@ -701,7 +701,7 @@ class LocalWhisperStreamingTest(unittest.IsolatedAsyncioTestCase): self.assertEqual(query["utterance_end_ms"], ["1200"]) self.assertEqual(result.text, "안녕하세요") - self.assertEqual(result.model, "large-v3") + self.assertEqual(result.model, "small") self.assertEqual(result.language, "ko") self.assertEqual([word.word for word in result.words], ["안녕하세요"]) self.assertAlmostEqual(result.words[0].start, 0.05, places=3) @@ -927,7 +927,7 @@ class LocalWhisperProviderSelectionTest(unittest.TestCase): service = VoiceService(api_key="", stt_provider="local_whisper") self.assertTrue(service.streaming_stt_enabled()) self.assertEqual(service.stt_provider(), "local_whisper") - self.assertEqual(service.stt_model(), "large-v3") + self.assertEqual(service.stt_model(), "small") def test_missing_sidecar_url_falls_back_to_batch(self) -> None: service = VoiceService( diff --git a/apps/api/app/test_voice_ws.py b/apps/api/app/test_voice_ws.py index bd60c05..d3c9564 100644 --- a/apps/api/app/test_voice_ws.py +++ b/apps/api/app/test_voice_ws.py @@ -16,6 +16,7 @@ from .persona_repository import PersonaVoiceMap from .routes import voice as voice_routes from .services import multimodal_alliance_store from .services.voice import ( + MELOTTS_TTS_MODEL, StreamingTranscriptEvent, TranscriptResult, TranscriptWord, @@ -150,6 +151,27 @@ class _FakeRouteStreamingSession: self.aborted = True +class VoiceHealthContractTest(unittest.IsolatedAsyncioTestCase): + async def test_melotts_health_reports_the_operated_model(self) -> None: + service = SimpleNamespace( + is_available=lambda: True, + stt_available=lambda: True, + tts_available=lambda: True, + tts_provider=lambda: "melotts", + stt_provider=lambda: "local_whisper", + stt_model=lambda: "small", + batch_stt_available=lambda: False, + ) + + with patch.object(voice_routes, "voice_service", service): + response = await voice_routes.voice_health() + + body = json.loads(bytes(response.body)) + self.assertEqual(body["status"], "ok") + self.assertEqual(body["tts_provider"], "melotts") + self.assertEqual(body["tts_model"], MELOTTS_TTS_MODEL) + + class VoiceWebSocketContractTest(unittest.IsolatedAsyncioTestCase): def _bind_result(self) -> tuple[str, VoicePreset, None, dict[str, object]]: return ( diff --git a/apps/api/requirements.txt b/apps/api/requirements.txt index 293853e..d7aae2d 100644 --- a/apps/api/requirements.txt +++ b/apps/api/requirements.txt @@ -9,6 +9,8 @@ python-multipart==0.0.18 httpx==0.28.1 websockets==14.2 sse-starlette==3.0.3 +# Windows G7 topology evidence pins and samples exact host-process provenance. +psutil==6.1.1 # P4(2026-07-13 회의): 자유 양식 엑셀 업로드 → 텍스트 변환 (personas/sources/upload) openpyxl==3.1.5 diff --git a/apps/web/e2e/admin.spec.ts b/apps/web/e2e/admin.spec.ts index 527cd8a..52f260c 100644 --- a/apps/web/e2e/admin.spec.ts +++ b/apps/web/e2e/admin.spec.ts @@ -590,6 +590,12 @@ function isOnline(seconds: number) { } async function openAdminAndReadHealth(page: Page) { + const userSnapshots: AdminUsersResponse[] = []; + const collectUsersResponse = async (response: Response) => { + if (!isAdminUsersResponse(response) || !response.ok()) return; + userSnapshots.push((await response.json()) as AdminUsersResponse); + }; + page.on("response", collectUsersResponse); const healthResponsePromise = page.waitForResponse(isAdminHealthResponse); const usageResponsePromise = page.waitForResponse(isAdminUsageResponse); const usersResponsePromise = page.waitForResponse(isAdminUsersResponse); @@ -613,10 +619,36 @@ async function openAdminAndReadHealth(page: Page) { const health = (await healthResponse.json()) as AdminHealthResponse; const usage = (await usageResponse.json()) as AdminUsageResponse; - const users = (await usersResponse.json()) as AdminUsersResponse; + const initialUsers = (await usersResponse.json()) as AdminUsersResponse; const uptime = (await uptimeResponse.json()) as AdminUptimeResponse; const tickets = (await ticketsResponse.json()) as AdminTicketsResponse; expect(health.services.length).toBeGreaterThan(0); + if (userSnapshots.length === 0) userSnapshots.push(initialUsers); + + let users = initialUsers; + await expect + .poll( + async () => { + const rendered = await page.locator(".vgops-kpi b").allTextContents(); + const matchingSnapshot = userSnapshots.find((snapshot) => { + const activeSessions = snapshot.users.reduce( + (sum, user) => sum + Math.max(0, user.active_sessions), + 0, + ); + const onlineUsers = snapshot.users.filter((user) => isOnline(user.last_seen_at)).length; + return ( + rendered[0] === countLabel(activeSessions) && + rendered[1] === countLabel(onlineUsers) && + rendered[2] === countLabel(snapshot.users.length) + ); + }); + if (matchingSnapshot) users = matchingSnapshot; + return Boolean(matchingSnapshot); + }, + { message: "관리자 KPI가 실제 /admin/users 응답 중 하나를 반영해야 한다" }, + ) + .toBe(true); + page.off("response", collectUsersResponse); return { health, usage, users, uptime, tickets }; } diff --git a/apps/web/e2e/dev-dashboard.spec.ts b/apps/web/e2e/dev-dashboard.spec.ts index 637404d..79a3b25 100644 --- a/apps/web/e2e/dev-dashboard.spec.ts +++ b/apps/web/e2e/dev-dashboard.spec.ts @@ -2,11 +2,12 @@ import http, { type Server } from "node:http"; import { promises as fs } from "node:fs"; import { AddressInfo } from "node:net"; import path from "node:path"; -import { pathToFileURL } from "node:url"; +import { fileURLToPath, pathToFileURL } from "node:url"; import { expect, test, type Page } from "@playwright/test"; import { expectNoHorizontalOverflow } from "./support"; -const repoRoot = path.resolve(process.cwd(), "..", ".."); +const specDir = path.dirname(fileURLToPath(import.meta.url)); +const repoRoot = path.resolve(specDir, "..", "..", ".."); const dashboardPath = path.join(repoRoot, "docs", "dev_dashboard.html"); interface StaticServer { @@ -144,8 +145,8 @@ async function readMetrics(page: Page): Promise { function expectDerivedMetrics(metrics: DashboardMetrics) { expect(metrics.total).toBeGreaterThan(0); - expect(metrics.done).toBe(33); - expect(metrics.doing).toBe(2); + expect(metrics.done).toBe(32); + expect(metrics.doing).toBe(3); expect(metrics.planned).toBe(0); expect(metrics.total).toBe(metrics.done + metrics.doing + metrics.planned); expect(metrics.ownerBoard).toBe( @@ -336,6 +337,30 @@ test.describe("dev dashboard static command center", () => { await page.setViewportSize(viewport); await openDashboard(page, `http://localhost:${staticServer.port}/docs/dev_dashboard.html`); await expectNoHorizontalOverflow(page); + + const pulseDetails = page.locator(".pulse-details"); + await expect(pulseDetails).toHaveJSProperty("open", false); + await pulseDetails.locator("summary").click(); + await expect(pulseDetails).toHaveJSProperty("open", true); + await expect(pulseDetails).toContainText("detached clean release와 exact Git commit/tree"); + await pulseDetails.locator("summary").click(); + await expect(pulseDetails).toHaveJSProperty("open", false); + + if (viewport.width <= 760) { + const controlHeights = await page + .locator(".owner-pill, .fbtn, .tab-button, .dg-link, .pulse-details summary") + .evaluateAll((controls) => + controls.map((control) => Math.ceil(control.getBoundingClientRect().height)), + ); + expect(controlHeights.length).toBeGreaterThan(0); + expect(controlHeights.every((height) => height >= 44)).toBe(true); + + const firstScreenPositions = await page.locator("#phase-rail, .kpis").evaluateAll((regions) => + regions.map((region) => Math.floor(region.getBoundingClientRect().top)), + ); + expect(firstScreenPositions).toHaveLength(2); + expect(firstScreenPositions.every((top) => top < viewport.height)).toBe(true); + } } }); }); diff --git a/apps/web/e2e/self-directed-learning-loop.spec.ts b/apps/web/e2e/self-directed-learning-loop.spec.ts index 62a7b9d..e9962b3 100644 --- a/apps/web/e2e/self-directed-learning-loop.spec.ts +++ b/apps/web/e2e/self-directed-learning-loop.spec.ts @@ -862,6 +862,60 @@ async function capture(page: Page, projectName: string, stage: string) { } test.describe("학습자 자기주도 전체 루프 — 실제 src UI / route fixture", () => { + test("3.5초 경계의 자기주도 경로 전환이 복구되면 부트 진단을 즉시 닫는다", async ({ + page, + }) => { + await routeSelfDirectedUiFixture(page); + await page.goto("/learn"); + await expect( + page.getByRole("heading", { name: "오늘 이어갈 회기를 먼저 봅니다." }), + ).toBeVisible(); + + // index.html의 첫 watchdog(3.5초)과 실제 자기주도 경로 전환을 겹친다. + // route가 잠깐 비어 보이는 동안에는 진단이 뜨되, React 콘텐츠 복구 후에는 + // 두 번째 watchdog(8초)을 기다리지 않고 바로 사라져야 한다. + await page.evaluate(async () => { + const remaining = Math.max(0, 3_300 - performance.now()); + await new Promise((resolve) => window.setTimeout(resolve, remaining)); + const root = document.getElementById("root"); + const action = document.querySelector( + "[data-learner-primary-action]", + ); + if (!root || !action) { + throw new Error("self-directed route transition fixture unavailable"); + } + root.style.visibility = "hidden"; + action.click(); + }); + + await expect(page).toHaveURL(/\/learn\/practice$/); + await expect(page.locator("#vignette-boot-diagnostic")).toBeVisible({ + timeout: 1_200, + }); + + await page.evaluate(() => { + document.getElementById("root")?.style.removeProperty("visibility"); + }); + await expect( + page.getByRole("heading", { name: "연습할 내담자를 선택합니다." }), + ).toBeVisible(); + await expect(page.locator("#vignette-boot-diagnostic")).toHaveCount(0, { + timeout: 1_000, + }); + }); + + test("React 부트가 실제로 비어 있으면 진단을 유지한다", async ({ page }) => { + await page.route("**/src/main.tsx", (route) => route.abort("failed")); + await page.goto("/learn", { waitUntil: "domcontentloaded" }); + + const diagnostic = page.locator("#vignette-boot-diagnostic"); + await expect(diagnostic).toBeVisible({ timeout: 4_500 }); + await expect(diagnostic).toContainText("React 화면이 정상 콘텐츠를 표시하지 못해서"); + await page.waitForTimeout(250); + await expect(diagnostic).toBeVisible(); + await expect(page.locator("#root")).toBeEmpty(); + }); + test("홈 추천과 목표 선택부터 회기·복기·처방 재연습 시작까지 한 흐름으로 보존한다", async ({ page, }, testInfo) => { diff --git a/apps/web/e2e/session-mvp.spec.ts b/apps/web/e2e/session-mvp.spec.ts index d643719..84f4fc4 100644 --- a/apps/web/e2e/session-mvp.spec.ts +++ b/apps/web/e2e/session-mvp.spec.ts @@ -432,6 +432,24 @@ async function routeMvpApi(page: Page, options: RouteMvpOptions = {}) { }); }); + await page.route( + `**/api/sessions/${sessionId}/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-000000000777", + consent_status: "granted", + deletion_request_id: null, + idempotent_replay: false, + }), + }); + }, + ); + return { sessionStartRequests, liveCoachRequests, @@ -444,6 +462,15 @@ async function routeMvpApi(page: Page, options: RouteMvpOptions = {}) { }; } +async function acceptVoiceInputConsent(page: Page) { + const consentDialog = page.getByRole("dialog", { name: "음성 입력을 사용하기 전에" }); + await expect(consentDialog).toBeVisible(); + await expect(consentDialog).toContainText("원음은 보존하지 않습니다"); + await expect(consentDialog).toContainText("동의하지 않아도 텍스트로 계속할 수 있습니다"); + await consentDialog.getByRole("button", { name: "동의하고 마이크 켜기" }).click(); + await expect(consentDialog).toHaveCount(0); +} + async function installVoicePersistenceFailureFixture(page: Page, transcriptText: string) { await page.addInitScript((text) => { const fixture = { @@ -1370,6 +1397,7 @@ test.describe("P1 MVP core loop", () => { const micButton = page.getByRole("button", { name: "마이크 켜기" }); await expect(micButton).toBeEnabled(); await micButton.click(); + await acceptVoiceInputConsent(page); await page.getByRole("button", { name: "발화 보내기" }).click(); const learnerBubble = page.locator(".sx-utt.is-learner").filter({ hasText: failedVoiceText }); @@ -1410,6 +1438,7 @@ test.describe("P1 MVP core loop", () => { await page.getByRole("button", { name: "이어가기" }).click(); await page.getByRole("button", { name: "마이크 켜기" }).click(); + await acceptVoiceInputConsent(page); await page.getByRole("button", { name: "발화 보내기" }).click(); const transcriptLog = page.getByRole("log", { name: "실시간 상담 축어록" }); @@ -1494,6 +1523,7 @@ test.describe("P1 MVP core loop", () => { const micButton = page.getByRole("button", { name: "마이크 켜기" }); await expect(micButton).toBeEnabled(); await micButton.click(); + await acceptVoiceInputConsent(page); await page.getByRole("button", { name: "발화 보내기" }).click(); const learnerBubble = page.locator(".sx-utt.is-learner").filter({ hasText: crisisText }); diff --git a/apps/web/e2e/session-persistence.spec.ts b/apps/web/e2e/session-persistence.spec.ts index cf37408..4268dc7 100644 --- a/apps/web/e2e/session-persistence.spec.ts +++ b/apps/web/e2e/session-persistence.spec.ts @@ -85,6 +85,8 @@ interface SessionDetailResponse { session_id: string; theory_mode: string; status: "active" | "ended"; + started_at: string; + ended_at?: string | null; turns?: Array<{ speaker: "learner" | "client"; text: string; @@ -548,6 +550,8 @@ async function openReviewTab(page: Page, label: "축어록" | "피드백" | "워 } test.describe("session persistence", () => { + test.use({ timezoneId: "Asia/Seoul" }); + test("persists selected CBT theory mode from session UI into DB-backed detail @single-run", async ({ page, }) => { @@ -922,6 +926,21 @@ test.describe("session persistence", () => { await expect(page.locator(".sx-utt.is-learner").filter({ hasText: learnerText })).toBeVisible({ timeout: 15_000, }); + const reloadedDetailResponse = await page.request.get(`/api/sessions/${sessionId}`); + await expectResponseOk(reloadedDetailResponse); + const reloadedDetail = (await reloadedDetailResponse.json()) as SessionDetailResponse; + expect(reloadedDetail.status).toBe("active"); + expect(reloadedDetail.ended_at).toBeNull(); + expect(reloadedDetail.started_at).toMatch(/(?:Z|[+-]\d{2}:\d{2})$/); + const sessionAgeMs = Date.now() - Date.parse(reloadedDetail.started_at); + expect(Number.isFinite(sessionAgeMs)).toBe(true); + expect(sessionAgeMs).toBeGreaterThanOrEqual(-5_000); + expect(sessionAgeMs).toBeLessThan(10 * 60 * 1_000); + await expect( + page.locator(".sx-session-progress__grid > span").first().locator("b"), + ).toHaveText(/^0\d:[0-5]\d$/); + await expect(page.locator(".sx-timebar.is-over")).toHaveCount(0); + await expect(page.getByRole("dialog", { name: "회기 시간이 끝났어요" })).toHaveCount(0); const coachMark = page.locator(".sx-utt.is-learner .sx-utt__coach-mark"); await expect(coachMark).toBeVisible({ timeout: 15_000, diff --git a/apps/web/e2e/session-review-fixture.ts b/apps/web/e2e/session-review-fixture.ts index f9eda2b..e2920d9 100644 --- a/apps/web/e2e/session-review-fixture.ts +++ b/apps/web/e2e/session-review-fixture.ts @@ -246,6 +246,13 @@ export async function routeFilledSessionReview( body: JSON.stringify(filledReviewResponse(sessionId)), }); }); + await page.route(`**/api/sessions/${sessionId}/alliance-pulses`, async (route) => { + await route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify({ items: [] }), + }); + }); } export function prepostMeasuresResponse( diff --git a/apps/web/e2e/session-review.spec.ts b/apps/web/e2e/session-review.spec.ts index 6d5c1af..70d6a87 100644 --- a/apps/web/e2e/session-review.spec.ts +++ b/apps/web/e2e/session-review.spec.ts @@ -156,6 +156,7 @@ async function expectReviewRegionsReachable(page: Page) { .evaluate((el) => window.getComputedStyle(el).gridTemplateAreas); expect(transcriptAreas).toContain("transcript"); + await page.locator(".sr-overview").scrollIntoViewIfNeeded(); await expect(page.locator(".sr-overview")).toBeInViewport(); await page.locator(".sr-card--transcript").scrollIntoViewIfNeeded(); await expect(page.locator(".sr-card--transcript")).toBeInViewport(); @@ -546,9 +547,12 @@ test.describe("session review", () => { }; }), ); - expect(filterMetrics.every((item) => item.height <= 34 && item.whiteSpace === "nowrap")).toBe( - true, - ); + expect(filterMetrics.every((item) => item.whiteSpace === "nowrap")).toBe(true); + if ((page.viewportSize()?.width ?? 0) <= 720) { + expect(filterMetrics.every((item) => item.height >= 44)).toBe(true); + } else { + expect(filterMetrics.every((item) => item.height <= 34)).toBe(true); + } await expectNoHorizontalOverflow(page); }); @@ -566,6 +570,7 @@ test.describe("session review", () => { expect(review.session_id).toBe(FILLED_REVIEW_SESSION_ID); expect(review.reviewReady).toBe(true); expect(review.turns).toHaveLength(6); + await expect(page.getByRole("button", { name: "다시 불러오기" })).toHaveCount(0); await expect(page).toHaveURL(new RegExp(`/learn/session/${FILLED_REVIEW_SESSION_ID}/review$`)); await expect(page.getByText("관계 신호가 안정적으로 회복된 회기")).toBeVisible(); diff --git a/apps/web/index.html b/apps/web/index.html index 6d465de..3800010 100644 --- a/apps/web/index.html +++ b/apps/web/index.html @@ -74,6 +74,8 @@ var errors = []; var loadedAt = Date.now(); var sentDiagnostics = {}; + var healthyCheckTimer = null; + var healthyObserver = null; function short(value) { return String(value || "").slice(0, 900); } @@ -116,14 +118,68 @@ style.opacity !== "0" ); } - function visibleCount() { + function visibleCount(scope) { + var root = scope || document; var nodes = Array.prototype.slice.call( - document.querySelectorAll( + root.querySelectorAll( "main,header,h1,h2,p,a,button,input,select,article,section,.vg-shell,.vg-route-error,.vgops-head,.vgops-diagnostic,.lg-root", ), ); return nodes.filter(isVisiblyRendered).length; } + function appHealth() { + var root = document.getElementById("root"); + var path = window.location.pathname; + var isAdmin = path === "/admin" || path.indexOf("/admin/") === 0; + var text = (root && root.innerText ? root.innerText : "").trim(); + var visible = root ? visibleCount(root) : 0; + var adminContent = root + ? root.querySelector( + "[data-vignette-admin-root],[data-vignette-admin-diagnostic],[data-testid='admin-ai-page']", + ) + : null; + var hasAdminContent = !isAdmin || isVisiblyRendered(adminContent); + return { + healthy: Boolean( + root && + root.childElementCount > 0 && + text.length > 24 && + visible > 0 && + hasAdminContent, + ), + visible: visible, + }; + } + function dismissDiagnosticIfHealthy() { + if (!appHealth().healthy) return false; + var panel = document.getElementById("vignette-boot-diagnostic"); + if (panel) panel.remove(); + return true; + } + function scheduleHealthyCheck() { + if (healthyCheckTimer !== null) window.clearTimeout(healthyCheckTimer); + healthyCheckTimer = window.setTimeout(function () { + healthyCheckTimer = null; + dismissDiagnosticIfHealthy(); + }, 60); + } + function observeAppHealth() { + if (healthyObserver || !window.MutationObserver) return; + var root = document.getElementById("root"); + if (!root) { + window.setTimeout(observeAppHealth, 100); + return; + } + healthyObserver = new MutationObserver(scheduleHealthyCheck); + healthyObserver.observe(root, { + attributes: true, + attributeFilter: ["aria-hidden", "class", "hidden", "style"], + characterData: true, + childList: true, + subtree: true, + }); + scheduleHealthyCheck(); + } function diagnosticApiBase() { var host = window.location.hostname; if (host === "vignette.chanpaca.net" || host.endsWith(".pages.dev")) { @@ -183,19 +239,16 @@ } function render(reason) { var path = window.location.pathname; - var isAdmin = path === "/admin" || path.indexOf("/admin/") === 0; var text = (document.body && document.body.innerText ? document.body.innerText : "").trim(); - var visible = visibleCount(); - var adminContent = document.querySelector( - "[data-vignette-admin-root],[data-vignette-admin-diagnostic],[data-testid='admin-ai-page']", - ); - var hasAdminContent = !isAdmin || isVisiblyRendered(adminContent); - if (reason === "watchdog" && text.length > 24 && visible > 0 && hasAdminContent) { - var old = document.getElementById("vignette-boot-diagnostic"); - if (old) old.remove(); + var health = appHealth(); + if (health.healthy && reason !== "manual") { + if (reason !== "watchdog") { + sendDiagnostic(reason, text.length, health.visible); + } + dismissDiagnosticIfHealthy(); return; } - sendDiagnostic(reason, text.length, visible); + sendDiagnostic(reason, text.length, health.visible); var panel = document.getElementById("vignette-boot-diagnostic"); if (!panel) { @@ -229,7 +282,7 @@ text.length + "", "
visibleNodes
" + - visible + + health.visible + "
", "
elapsed
" + (Date.now() - loadedAt) + @@ -260,6 +313,7 @@ render("manual"); }, }; + observeAppHealth(); window.setTimeout(function () { render("watchdog"); }, 3500); diff --git a/docs/HANDOFF.md b/docs/HANDOFF.md index 945786a..0f80267 100644 --- a/docs/HANDOFF.md +++ b/docs/HANDOFF.md @@ -1,13 +1,17 @@ -# Vignette G0~G8 완료 핸드오프 +# Vignette G0~G8 완료 추진 핸드오프 -> Updated: 2026-08-07 KST (G8 실제 rollback 종료 반영) +> Updated: 2026-08-09 KST (다른 에이전트 실행용 최종 인계 · G7 외부-proof P0 코드 완료 · current-source fixture 분할 GREEN/clean HEAD 재실행) > Workspace: `D:\workspace\vignette` -> 이 문서는 다음 에이전트가 **G0~G8 아홉 목표 중 마지막으로 남은 G7 외부 게이트를 닫기 위한** 실행 기준이다. +> 이 문서는 다음 에이전트가 **G0~G8 아홉 목표 중 남은 G7 외부 게이트와 G8 NAS-origin 재검증을 닫기 위한** 실행 기준이다. ## 0. 현재 상태 한 줄 요약 -**G0~G6·G8은 internal DONE + 실제 runtime 증거로 종료됐다. 남은 것은 G7 외부 4-artifact 게이트 하나이며, -이건 코드로 풀 수 없고 사용자가 제공해야 하는 운영 자원·동의가 있어야 진행된다.** +**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 공개 런처의 +local sidecar lifecycle·provider/model readiness·Uvicorn queue, fresh PID/source provenance, 3,120초 runner와 +3,000초 공통 시간창·canonical exit 계약은 소스에서 완료됐지만, current source 공개 배포와 외부 4-artifact 게이트는 남아 있다.** | Goal | 판정 | 완료 증거 | |---|---|---| @@ -18,28 +22,89 @@ | G4 Deliberate Practice | DONE | completed-session observer, actual API/DB attempts, teacher correction, returned-practice browser 폐루프 | | 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, public unauth WSS 1008, focused 119 PASS | -| G8 Continuous Improvement | **DONE** | 격리 NAS 실제 image rollback 2회 + executed receipt 2건 (아래 §3) | +| 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 미완료 | -대시보드 status count는 **33 done / 2 doing / 0 planned**다. +대시보드 status count는 **32 done / 3 doing / 0 planned**다. -### 완료 조건 판정 (2026-08-08 재검증) +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하지 않았다. + +### 이 문서가 인계된 정확한 중단점 + +- 사용자가 **현재 구현을 잠시 멈추고 다른 에이전트로 넘길 핸드오프를 만들라**고 요청했다. 핸드오프 정리 중 + public/NAS/DB/마이크/프로세스의 수동 mutation은 0회다. 5분 Scheduled Task는 한 번씩 자동 실행됐으나 아래 + source-pin gate에서 안전하게 exit 1로 중단됐다. +- 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을 다시 실행한다. +- 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는 수행하지 않았다. + 현재 public API가 Git metadata가 없는 냉동 release 디렉터리에서 실행되므로 provenance gate가 의도적으로 + 닫혀 있다. 이 검증을 약화해서 통과시키면 안 된다. +- **public task source-pin P0:** watchdog·로그온 boot는 이제 detached clean release만 허용하고 Git + commit/tree와 watchdog|boot/start script SHA를 health·Docker·process mutation 전에 검증한다. hidden VBS도 + shared source를 직접 실행하지 않고 구조적으로 pin된 task만 trigger한다. 회귀는 provenance 11개를 포함한 + watchdog/start/cutover combined 22/22, PowerShell 5.1 parser·WSH syntax PASS다. 그러나 **실제 두 Scheduled Task action은 + 아직 shared worktree를 가리키며 pin 인자가 없다.** 5분 watchdog은 pin 부재로 반복 실행마다 + `LastTaskResult=1`이고, 새 필수 pin 인자 부재 때문에 운영 프로세스를 건드리기 전에 의도대로 fail-closed했다. + 18:02:55 KST에는 Docker Desktop UI가 active DB container에 직접 stop을 보내 public `db=false`가 재발했다. + 같은 container ID와 recovered named volume만 18:05 KST 재시작했고, 공개 local/public health는 다시 + `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는 한 명만 둔다. + +### 완료 조건 판정 (2026-08-09 재검증) 원래 active goal의 완료 선언 조건 네 가지를 실행으로 대조한 결과다. | # | 조건 | 판정 | 근거 | |---|---|---|---| -| 1 | G0~G8 각각의 코드 계약과 실제 runtime 증거 | **부분 충족** | G0~G6·G8은 충족. **G7만 외부 GATE로 열려 있다** | +| 1 | G0~G8 각각의 코드 계약과 실제 runtime 증거 | **부분 충족** | G0~G6 충족. G8 receipt는 충족했지만 NAS-origin 회기 재검증이 남았고 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 33/2/0이 카드와 일치 | +| 3 | current source 증거와 대시보드·TODO·backlog 일치 | **충족** | SSOT checker FAIL 0, unit 5/5, dashboard E2E 10/10, count 32/3/0이 카드와 일치 | | 4 | G7을 가짜·합성 증거로 DONE 처리하지 않음 | **충족** | `check-g7-external-proof.py` exit 1 유지, G7 카드는 `GATE` | -재검증 수치: API **914 passed**, gateway **58**, executor **28**, probe **11**, helper launcher **37**, -release agent **21**, MeloTTS 사이드카 **16**, local whisper 사이드카 **37**, SSOT FAIL **0**, +재검증 수치: API **921 passed**, gateway **58**, executor **28**, probe **11**, helper launcher **37**, +release agent **30**, G7 runner/checker/topology 묶음 **86**, G7 public launcher/sidecar **80**, G7 통합 **166**, +API voice 통합 **71**, +MeloTTS 사이드카 **16**, local whisper 사이드카 **38**, +SSOT FAIL **0**, dashboard E2E **10/10**, web api-types·typecheck·build 통과, ruff clean. -**따라서 `update_goal complete`를 호출하지 않는다.** 조건 1이 G7 때문에 미충족이고, 조건 4가 그것을 -합성 증거로 메우는 것을 금지한다. G8 자체는 종료됐다(§3). +current-source fixture 묶음은 16:49 KST 단일 실행에서 **120/120 PASS(92.0s)**했다. dev dashboard file URL은 실행 cwd가 +아니라 spec의 `import.meta.url`로 repo root를 찾으며, session-review fixture는 alliance pulse까지 소유한다. 관리자 +live health 테스트는 React StrictMode 중복 조회의 첫 응답을 고정하지 않고 화면이 실제 반영한 성공 +`/admin/users` 스냅샷과 KPI를 결속한다. 그 뒤 dev-dashboard spec/HTML이 변경됐고 최신 dashboard 10/10만 따로 +재검증됐다. 따라서 current tree의 정직한 판정은 **110 불변 + 10 최신 분할 GREEN**이며, 새 clean commit에서 +같은 120건을 한 번 재실행해 archive/source 결속 뒤의 결과를 고정한다. + +**따라서 `update_goal complete`를 호출하지 않는다.** 조건 1이 G7과 G8 NAS-origin 재검증 때문에 미충족이고, +조건 4가 G7을 합성 증거로 메우는 것을 금지한다. G8 receipt 자체는 종료됐다(§3). + +### G0~G8 목표별 인수 기준 + +| Goal | 원 계획의 종료 조건 | 현재 인정 가능한 증거 | 다음 에이전트가 최종 종료 전에 할 일 | +|---|---|---|---| +| G0 | 100% provenance, evaluator/client 누수 0, Python/TS/DB schema conformance, 기존 회기 무회귀 | current producer census 29/29, 위반 0, migrations 14~16 2회 멱등 | current HEAD 전체 backend/schema 회귀와 최종 census를 다시 실행 | +| G1 | goal/task/bond 독립 저장, 축별 UI, gold 근거 span, 평가 실패 fail-closed | prediction 24/24 ready, 방향 9/9, error 0, recall 94.4%, precision 73.9%, pre/mid/post UI | current HEAD의 alliance desktop/mobile·DB-backed session 경로 재실행 | +| G2 | 5회기 연속성, 조기 경보 recall, false alert/uncertainty, synthetic 비임상 경계 | accuracy/recall 1.0, false alert 0, 실제 DB 10 sessions/30 measurements | current HEAD outcome trajectory API/UI 회귀 재실행 | +| G3 | rupture 유형 benchmark, missed/partial/resolved E2E, gaming/암기 방지 | macro-F1/status 1.0, gaming 0, 실제 DB/API missed→partial→resolved | text/SSE 회기 및 rupture UI desktop/mobile 재실행 | +| G4 | 모든 코칭 카드의 실행 재연습, 전후 근거 저장, unseen 전이 전 mastery 금지 | 실제 API/DB attempt·observer·teacher correction, browser route mock 0 | disposable DB에서 returned-practice desktop/mobile 4/4와 멱등 재확인 | +| 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 동기화 | ## 1. 작업 시작 전 반드시 읽을 파일 @@ -61,14 +126,29 @@ dashboard E2E **10/10**, web api-types·typecheck·build 통과, ruff clean. - Windows 11 + PowerShell 환경이다. 실행 전 OS·셸·도구 경로를 다시 확인한다. - 명령은 PowerShell 5.1 호환 문법을 기본으로 작성한다. NAS 원격 스크립트는 **UTF-8 base64 전달 방식**을 쓴다. (`$()`가 PowerShell에서 먼저 확장되는 것을 막는다.) -- worktree는 큰 규모로 dirty하다. `git reset --hard`, `git checkout --`, 대규모 자동 포맷, 임의 삭제 금지. +- 이 핸드오프 작성 시 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에 들어 있지 않다. +- 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, + SSOT는 FAIL 0와 dashboard E2E 10/10을 통과했다. 다음 에이전트는 최초 diff review 뒤 동일한 focused gate를 + 다시 실행하고, 서로 다른 lane의 파일을 + 누락한 채 clean commit을 만들지 않는다. +- 기존·watchdog 생성 엔진 로그 4개와 `apps/web/test-results/`의 PNG 2개는 commit에서 제외하고 보존한다. 신규 + `apps/api/app/test_session_read_model.py`와 + `docs/ops/evidence/g8-clean-head-material-milestone-2026-08-09.json`은 이번 목표 산출물이므로 clean source에 + 포함해야 한다. + 시작 즉시 `git status --short`로 다시 확인하고 `git reset --hard`, `git checkout --`, 대규모 자동 포맷, 임의 삭제는 금지한다. - Git commit/stage/push는 사용자가 요청하기 전에는 하지 않는다. - 공개 복구 DB, 원본 volume, pre-recovery container, dump, NAS named volume, 이전 API/Web 이미지를 삭제하지 않는다. -- **로컬 포트 8001(공개 API) · 5173(공개 Web) · 55432(공개 DB) · 9099/9100(엔진)은 공개 런타임이다.** - `scripts/dev-up.ps1`은 8000/5173 리스너를 정리하므로 공개 런타임이 떠 있는 동안 실행하지 않는다. +- **로컬 포트 8001(공개 API origin) · 55432(공개 DB) · 9099/9100(엔진)은 공개 런타임이다.** + 공개 Web은 Cloudflare Pages이고 5174는 `vnet.18ka.net`용 로컬 preview다. `scripts/dev-up.ps1`은 런타임 + 리스너를 정리할 수 있으므로 공개 런타임이 떠 있는 동안 실행하지 않는다. - 비밀값, 이메일, cookie, 사용자 UUID가 포함된 raw response, `.env` 내용은 로그·증거·채팅에 출력하지 않는다. -## 3. G8 — 종료됨 (2026-08-07 21:04~21:12 KST) +## 3. G8 receipt — 종료됨 · NAS-origin runtime 재검증은 열림 ### Gate6 계약 정정 (성립 불가능했던 이전 계약을 코드로 교체) @@ -104,31 +184,55 @@ dashboard E2E **10/10**, web api-types·typecheck·build 통과, ruff clean. ## 4. 공개 서비스 복구 상태 — 이미 완료, 다시 망가뜨리지 말 것 -공개 계정·회기 DB 복구는 2026-08-07 18:26 KST에 완료됐다. +공개 계정·회기 DB 복구는 2026-08-07 18:26 KST에 완료됐다. 2026-08-09 14:03 KST에는 Docker Desktop이 +중단돼 DB listener가 사라지면서 public health가 일시적으로 `db=false`가 됐고, 18:02:55 KST에는 Docker Desktop +UI가 active DB container에 직접 stop을 보내 같은 증상이 재발했다. 두 번 모두 보존된 동일 컨테이너·볼륨만 +재기동해 복구했다. 새 컨테이너·볼륨 생성이나 데이터 복원 덮어쓰기는 없었다. - Public Web `https://vignette.chanpaca.net` · Public API `https://api-vignette.chanpaca.net` (origin `127.0.0.1:8001`) - active DB container `vignette-dev-db`, volume `vignette_recovered_prod_20260807`, restart `unless-stopped` -- 복구 집계: users 84, sessions 30, turns 705, Google users 16, orphan sessions 0 +- cutover 기준 복구 집계: users 84, sessions 30, turns 705, Google users 16, orphan sessions 0 +- 2026-08-09 18:05 KST 재기동 후 owner read-only 집계: users 356, sessions 193, turns 726, Google users 16, + Google-owned sessions 31. 조회는 `BEGIN READ ONLY` 뒤 `ROLLBACK`했고 PII를 출력하지 않았다. - health `environment=prod`, `db=true`, `engine=true`, `engine_mode=claude_cli`, dev-login disabled, unauth `/auth/me` 401 -- 공개 OpenAPI 119 paths · watchdog `VignettePublicRuntimeWatchdog` 5분 주기 +- **2026-08-09 18:05 KST recovery snapshot:** local/public health는 `status=ok, db=true, engine=true`, Google OAuth + start는 302→`accounts.google.com`, 공개 OpenAPI는 119 paths다. `/voice/health`는 아직 OpenAI STT/TTS이고 + local Whisper 9882·MeloTTS 9883 listener는 0이다. +- DB 컨테이너는 `vignette_recovered_prod_20260807` named volume과 `unless-stopped`를 유지하고 local/public + health는 `status=ok, db=true, engine=true`다. 다만 watchdog·로그온 task action은 아직 shared worktree이며 + stable root/commit/tree pin 인자가 없다. 새 source-pin 코드가 5분 watchdog을 mutation 전에 차단해 반복 실행이 + `LastTaskResult=1`이다. clean commit의 detached release root로 두 task를 재등록하기 전까지 자동복구가 + 의도적으로 비활성인 상태다. 보안상 과거 `auth_session`은 복원하지 않았다. 사용자는 Google 재로그인이 필요하다. -보존물: pre-recovery container `vignette-dev-db-pre-recovery-20260807-182642`, -pre-cutover dump `…20260807-091057Z.dump` SHA256 `6b84d5c8…af1f`, -recovered dump `…20260807-092630Z.dump` SHA256 `d7bcc396…68ed`, -post-cutover dump `…20260807-095511Z.dump` SHA256 `660695c8…65b88`. +보존물: pre-recovery container `vignette-dev-db-pre-recovery-20260807-182642`과 원본/recovery Docker volumes. +과거 증거에는 pre-cutover dump `…20260807-091057Z.dump` SHA256 `6b84d5c8…af1f`, recovered dump +`…20260807-092630Z.dump` SHA256 `d7bcc396…68ed`, post-cutover dump `…20260807-095511Z.dump` SHA256 +`660695c8…65b88`이 기록돼 있으나, **인계 시점 로컬에는 `D:\workspace\vignette-backups` 디렉터리가 없고 +`D:\workspace\vignette-recovery`에도 이 dump들이 보이지 않는다.** 삭제로 단정하지 말고 off-host/NAS/다른 경로를 +확인하기 전 restore·cleanup을 금지한다. NAS의 pre-proof/pre-clean-head dump는 §5 기준으로 실제 존재와 hash를 +재확인했다. 공개 DB에는 current dirty source를 배포하지 않는다. -## 5. current source NAS 배포 — 완료 +## 5. NAS 배포 기준선 — 현재 HEAD가 아니라 2026-08-07 릴리스 -- 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` (previous: `79ec731f…0450` / `c530213f…62f28`) +- **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` -- candidate browser E2E 108/108(634.578s), postdeploy SSE→DB review PASS + 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) 배포 중 첫 108 gate가 103/5로 fail-closed된 이력(G5 fixture drift, StrictMode retry fixture, `shell.css` manifest 누락)은 @@ -151,29 +255,235 @@ post-cutover dump `…20260807-095511Z.dump` SHA256 `660695c8…65b88`. - 교훈: candidate gate를 localhost에서만 돌리면 secure-context 전용 API 결함을 못 잡는다. 배포 대상과 같은 scheme/host 형태에서 최소 한 번은 회기 리뷰 라우트를 열어봐야 한다. -## 6. **다음 에이전트의 유일한 남은 목표 — G7 외부 종료 Gate** +## 6. 다음 실행 순서 — G8 runtime 재검증 후 G7 외부 종료 -G7 내부 구현은 완료됐고 `scripts/check-g7-external-proof.py`는 현재 의도적으로 exit 1이다. +### 6.1 G8: clean-head 릴리스 경로부터 완성 + +`scripts/run-outcome-os-release-agent.py`의 명시적 clean-head 경로는 구현됐다. 기존 patch mode를 기본값으로 +보존하며, tracked-clean source worktree의 exact HEAD/tree/archive만 candidate로 사용한다. 수동 복사나 NAS 직접 +빌드는 계속 금지한다. + +구현된 **`--source-mode clean-head`** 계약은 다음과 같다. + +1. tracked dirty가 있으면 fail-closed. untracked 로그·테스트 결과는 archive에서 제외한다. +2. exact Git HEAD, tree, `git archive` SHA를 증거에 기록한다. +3. 같은 HEAD의 archive를 두 번 만들어 SHA가 동일한지 검증한다. +4. candidate는 그 archive만 풀어 만들고 patch/manifest를 암묵 적용하지 않는다. +5. 기존 preflight·API full·Web type/build·release E2E·DB snapshot·rollback 절차는 그대로 유지한다. +6. Vite source 전용 `e2e/insecure-context-uuid.spec.ts`는 별도 localhost gate(5198)로 6/6 실행하고, + 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 UUID gate 6/6, Ruff, py_compile, `git diff --check` PASS. 공유 worktree에는 +인계 문서와 controller 변경이 있으므로 이 경로 자체가 의도대로 tracked-dirty를 차단한다. 실제 배포는 새 commit을 +가리키는 별도 detached clean worktree를 만들고 `--source-repo-root `로 지정한 뒤 +dry-run→execute 순서로 수행한다. + +#### 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`는 금지한다. + +```text +apps/api/app/config.py +apps/api/app/routes/voice.py +apps/api/app/services/session_metrics.py +apps/api/app/services/voice.py +apps/api/app/session_read_model.py +apps/api/app/test_session_read_model.py +apps/api/app/test_voice_service.py +apps/api/app/test_voice_ws.py +apps/api/requirements.txt +apps/web/e2e/admin.spec.ts +apps/web/e2e/dev-dashboard.spec.ts +apps/web/e2e/self-directed-learning-loop.spec.ts +apps/web/e2e/session-mvp.spec.ts +apps/web/e2e/session-persistence.spec.ts +apps/web/e2e/session-review-fixture.ts +apps/web/e2e/session-review.spec.ts +apps/web/index.html +docs/HANDOFF.md +docs/TODO.md +docs/decisions/local-voice-stack.md +docs/dev_dashboard.html +docs/guides/local-development.md +docs/guides/testing.md +docs/ops/backlog-2026-06-26.md +docs/ops/evidence/g8-clean-head-material-milestone-2026-08-09.json +docs/ops/nas-preview-g8-rollback-proof-runbook.md +docs/ops/outcome-os-g7-external-proof-readiness-2026-08-07.md +docs/ops/public-runtime-watchdog.md +scripts/boot-public-runtime.ps1 +scripts/capture-g7-topology-evidence.py +scripts/check-dev-dashboard-ssot.py +scripts/check-g7-external-proof.py +scripts/install-public-runtime-task.ps1 +scripts/local-whisper-stt-server.py +scripts/melotts-server.py +scripts/probe-public-voice-sidecars.py +scripts/register-boot-task.ps1 +scripts/run-g7-external-proof-window.py +scripts/run-outcome-os-release-agent.py +scripts/start-local-whisper-stt.ps1 +scripts/start-public-runtime.ps1 +scripts/test_g7_external_proof.py +scripts/test_g7_topology_evidence.py +scripts/test_local_whisper_stt_server.py +scripts/test_melotts_server.py +scripts/test_outcome_os_release_agent.py +scripts/test_public_runtime_watchdog_provenance.py +scripts/test_public_voice_sidecar_probe.py +scripts/test_run_g7_external_proof_window.py +scripts/test_start_public_runtime_contract.py +scripts/watch-public-runtime-hidden.vbs +scripts/watch-public-runtime.ps1 +``` + +아래 6개와 ignored `infra/.env.nas-preview`는 절대 stage하지 않는다. NAS 실행 시 env는 +`--nas-env-file D:\workspace\vignette\infra\.env.nas-preview`로 외부 주입한다. + +```text +apps/api/engine.public.err.log.2026-08-07T2200.bak +apps/api/engine.public.err.log.20260809-160042.bak +apps/api/engine.public.out.log.2026-08-07T2200.bak +apps/api/engine.public.out.log.20260809-160042.bak +apps/web/test-results/g7-voice-consent-desktop.png +apps/web/test-results/g7-voice-consent-mobile.png +``` + +#### 2026-08-09 clean-head 실행 결과 — NAS mutation 0 + +- dry-run `D:\workspace\vignette-recovery\g8-clean-head-dry-run-20260809.json`: `ok=true`, + active `6030a677…c611`, desired archive `6b1b15bd…a3f7`, `deployment_mutated=false`. +- execute 1 `...\g8-clean-head-execute-20260809.json`: Windows Git archive의 CRLF 때문에 생성 API 타입 byte check가 + 실패했다. archive SHA 검증 후 candidate의 생성 타입/셸 스크립트만 LF로 정규화하도록 controller를 수정했다. + NAS mutation 0, rollback 불필요. +- execute 2 `...\g8-clean-head-execute-20260809-r2.json`: API/types/typecheck/build/candidate stack은 통과했고 + Playwright는 **109 passed / 5 failed**였다. source-only UUID 스펙 4건은 production build에서 + `/src/lib/uuid.ts`를 import할 수 없는 gate 배치 오류라 별도 Vite gate로 분리했다. 남은 1건은 아래 제품 버그다. + NAS mutation 0, rollback 불필요. 동일 전체 execute를 아직 다시 시도하지 않았다. + +#### P0 수정 완료 — 회기 timestamp UTC 전송과 KST 표시 날짜 + +- 최초 실패 스펙: `session-persistence.spec.ts`의 `persists AI tutor coaching history through reload @single-run`. +- 코칭 생성·DB 저장·reload 후 `C` 마커까지는 성공했지만, 수정 전 화면이 `9:03:59 / 543:59` 경과로 계산해 + `회기 시간이 종료됐어요` 모달을 띄웠고 그 모달이 코칭 마커 클릭을 가로막았다. +- 원인: `apps/api/app/session_read_model.py::iso()`가 + `datetime.fromtimestamp(ts).isoformat(timespec="seconds")`로 timezone 없는 문자열을 반환한다. + `apps/web/src/pages/Session.tsx::elapsedFromSession()`은 `Date.parse(detail.started_at)`로 이를 브라우저 로컬 KST로 + 해석한다. Linux API UTC와 KST 브라우저 사이에서 약 9시간 오차가 생긴다. +- 수정: `session_read_model.iso()`와 dashboard용 `session_metrics.iso_datetime()`은 UTC `+00:00`을 반환한다. + 회기 리뷰 달력 날짜는 서버 timezone이 아니라 고정 KST(+09:00)를 사용한다. +- backend 관련 65 passed, 후속 voice 계약을 포함한 API 전체 **921 passed**, Ruff·compile·web typecheck가 통과했다. +- 고유 Compose project/volume의 실제 DB/API/engine/browser에서 KST context로 focused 1/1 PASS했다. UTC suffix, + `status=active`, `ended_at=null`, 회기 나이 10분 미만, 화면 `00:00-09:59`, timebar/dialog 0, + coach mark 실제 클릭과 DB history/source-pack dialog를 모두 검증했다. 종료 뒤 container/volume/listener는 0이다. +- **테스트 강제 클릭이나 모달 닫기 우회는 사용하지 않았다.** 현재 clean source HEAD `94c681d…7fb`에는 이 수정과 + controller 변경이 없으므로 사용자에게 commit 승인을 받은 뒤 새 clean commit의 HEAD/tree/archive를 다시 산출한다. + +### 6.2 G8: NAS preview 배포와 실제 origin E2E + +clean-head release mode가 GREEN이면 전용 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` 실행 계약이 없다. +따라서 **매 execute 직전** 별도 승인된 백업 단계에서 fresh custom dump를 만들고 SHA-256·byte size·TOC parse/count와 +current DB identity를 증거에 결속한 뒤에만 승격한다. 기존 dump 존재만으로 이 단계를 생략하지 않는다. + +release agent의 자동 rollback 범위는 exact API/Web image, Compose 적용 상태, active-state 파일뿐이다. 이미 적용된 +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. +- `crypto.randomUUID` 예외 0, error boundary 0, SSE→DB review PASS. +- 학생 홈→회기→리뷰→G4/G5 반환 연습의 desktop/mobile·DB read reload·멱등·UUID 비노출 유지. +- 실패 시 자동 rollback과 previous image 복구를 검증하고, 성공 전에는 G8 카드 전체를 DONE으로 바꾸지 않는다. + +### 6.3 G7: 외부 종료 Gate + +G7 내부 소스 계약은 완료됐고 `scripts/check-g7-external-proof.py`는 현재 의도적으로 exit 1이다. 네 artifact를 **같은 public host, 겹치는 50분 시간창**으로 수집해야 한다. +먼저 닫아야 할 운영·코드 선행조건이 있다. + +- 공개 API는 여전히 119 paths 구버전이며 `/admin/voice-runtime`이 없고(404), `/voice/health`는 + `openai/gpt-4o-transcribe` + `openai/gpt-4o-mini-tts`다. +- local Whisper 9882와 MeloTTS 9883 listener는 아직 공개 호스트에 떠 있지 않다. 다만 current source의 + `start-public-runtime.ps1`는 두 sidecar를 API보다 먼저 시작하고 `local_whisper/small/cpu-int8`, + `melotts/melotts-korean` exact provider/model·WS/HTTP readiness를 fail-closed로 확인하며, 기존 비정상 리스너를 + 임의 종료·재사용하지 않는다. Uvicorn도 `--ws websockets --ws-max-queue 4`로 고정했다. source 검증은 + launcher/sidecar 80/80, API voice 71/71, PowerShell parse·Ruff·compile·diff-check PASS지만 공개 실행은 0회다. +- **외부 실행 전 코드 P0 3건은 완료:** (1) production 프로세스 exit 0은 canonical checker + `returncode == 0 && gate_closed == true`에 결속한다. (2) 실제 브라우저 Origin + `https://vignette.chanpaca.net`은 exact HTTPS allowlist로, API/WSS/admin/topology + `api-vignette.chanpaca.net`은 별도 transport host와 `wss`/`https` scheme으로 검증한다. (3) 실제 capture 기본·최소를 + 3,120초로 올리고 sampler를 ceil+terminal sample로 계산하며, checker가 voice/runtime/topology 공통 교집합 + **3,000초 이상**을 강제한다. focused runner 37/37과 통합 G7 166/166을 통과했다. +- 현재 공개 topology는 Windows host Uvicorn + cloudflared다. 명시적 `windows_host` topology mode는 구현 완료됐다. + API/cloudflared의 PID·start time·executable name/SHA256·command-line SHA256·Git SHA·cwd를 pin하고, 각 sample 전후 + identity와 Git HEAD drift를 검사한다. RSS/peak RSS/CPU/handles/threads, process TCP/established/listener, + API listen-port owner/conflict와 host TCP high-water도 수집한다. 기존 `linux_compose`의 api+caddy·fresh-container + 규칙은 그대로 유지한다. +- Windows topology focused 검증은 G7 runner/checker/topology 86/86, Ruff, `py_compile`, `git diff --check`를 통과했고 실제 + read-only adapter가 API 8001 owner=1/conflict=0, cloudflared established TCP>0를 읽었다. raw command line이나 + endpoint는 저장하지 않았다. +- **남은 provenance blocker는 배포/실증:** 현재 API cwd는 + `D:\workspace\vignette-outcome-g7-release-20260807\apps\api`이고 그 release 디렉터리에는 `.git`이 없다. 소스의 + `-RequireFreshPublicProvenance` 모드는 detached-clean commit/tree와 Python/cloudflared/config SHA를 mutation 전에 + 검증하고, legacy API와 exact-config cloudflared를 bounded stop한 뒤 pinned release cwd에서 새 PID로 교체한다. + PID/start/exe SHA/command SHA/실제 cwd를 재검증한 safe receipt에는 raw command line·config 내용을 남기지 않는다. + topology artifact도 같은 commit/tree, runner/collector/checker SHA와 `psutil==6.1.1`을 매 sample 전후 결속한다. + 이 코드는 atomic receipt contract 18/18을 포함한 G7 통합 166/166으로 GREEN이지만 **현재 public에는 아직 실행하지 않았다.** + clean commit 전 legacy cwd/Git pin을 약화하거나 기존 PID를 증거로 재사용하지 않는다. +- watchdog·로그온 boot의 detached-clean source pin 코드는 완료됐지만 실제 task action은 아직 legacy shared + source다. clean commit 뒤 `docs/ops/public-runtime-watchdog.md` 순서대로 같은 stable release root에 두 task를 + 재등록하고 commit/tree/script hash marker와 `LastTaskResult=0`을 확인한다. 그 전에는 current + `LastTaskResult=1`을 실패가 아니라 안전 차단으로 유지하고, 단일 mutation owner가 public 배포 시간창을 소유한다. +- 운영 Python에 `psutil`이 없으면 collector는 `command_unavailable:psutil`로 fail-closed한다. current + `apps/api/requirements.txt`와 명시적 Python 3.11 모두 `psutil==6.1.1`로 고정됐다. +- 현재 운영 STT 모델은 이 호스트에서 실측된 CPU int8 `small`로 고정한다. cuDNN 9가 설치되고 별도 성능·정확도 + gate를 통과하기 전까지 launcher·API runtime metadata·50분 runner/checker expected model을 모두 `small`로 유지한다. + ```powershell & $py -X utf8 -B scripts/check-g7-external-proof.py ` --voice-soak --runtime ` --topology --human-voice-gain ``` -1. `scripts/soak-public-voice-websocket.py` v4 — 운영 Deepgram/OpenAI TTS, authenticated public WSS, +1. `scripts/soak-public-voice-websocket.py` v4 — 운영 기본값 `local_whisper`/`melotts`의 ready metadata가 + 실제 provider/model과 정확히 일치하는 authenticated public WSS, **명시 동의 물리 마이크** 50분 양방향 `passed`. `--confirm-physical-capture` 없이는 장치 열거·캡처를 하지 않는다. 2. `scripts/capture-g7-runtime-evidence.py` — 같은 시간창의 관리자 endpoint worker/Uvicorn queue high-water. 3. `scripts/capture-g7-topology-evidence.py` — 같은 host·exact image의 50분 cgroup/proc/Docker/TCP high-water. -4. 독립 blind human voice-gain pack — 최소 30명 / 50회기 / 150 paired axis / blind evaluator 2인 / +4. 독립 blind human voice-gain pack — held-out 30명 외 calibration split 참가자 포함 총 최소 31명 / + held-out 50회기 / 150 paired axis / blind evaluator 2인 / ICC(A,1) ≥ 0.75 · κ ≥ 0.70 · gain ≥ 0.01 · participant-cluster bootstrap 10,000회 95% CI lower > 0. 준비 상세는 `docs/ops/outcome-os-g7-external-proof-readiness-2026-08-07.md`. +G7 Windows topology 회귀 명령: + +```powershell +Set-Location D:\workspace\vignette +$env:PYTHONPATH='D:\workspace\vignette\apps\api' +& $py -X utf8 -m unittest scripts/test_g7_external_proof.py scripts/test_g7_topology_evidence.py ` + scripts/test_g7_runtime_evidence.py scripts/test_g7_external_voice_soak.py ` + scripts/test_run_g7_external_proof_window.py apps/api/app/test_g7_voice_gain_evidence.py +& $ruff check scripts/capture-g7-topology-evidence.py scripts/check-g7-external-proof.py ` + scripts/run-g7-external-proof-window.py scripts/test_g7_topology_evidence.py ` + scripts/test_g7_external_proof.py scripts/test_run_g7_external_proof_window.py +``` + **사용자에게 받아야 하는 것 (코드로 대체 불가):** -- 운영 Deepgram key·quota 사용 권한 - authenticated learner/admin session 또는 안전한 `storageState` 생성 협조 - 물리 마이크 50분 실행에 대한 **명시적 동의** - 실제 참가자·독립 평가자 운영 승인 @@ -220,7 +530,7 @@ $py = 'C:\Users\encep\AppData\Local\Programs\Python\Python311\python.exe' $ruff = 'C:\Users\encep\AppData\Local\hermes\hermes-agent\venv\Scripts\ruff.exe' ``` -### Backend full — 기준 API **898 passed**, gateway **58 passed** +### Backend full — 기준 API **921 passed**, gateway **58 passed** ```powershell Set-Location D:\workspace\vignette\apps\api @@ -228,7 +538,7 @@ Set-Location D:\workspace\vignette\apps\api & $py -X utf8 -B -m pytest -p no:cacheprovider engine_gateway -q ``` -### G8 executor/probe/helper/release governance — 기준 28 / 11 / 37 / 21 +### G8 executor/probe/helper/release governance — 기준 28 / 11 / 37 / 30 ```powershell Set-Location D:\workspace\vignette @@ -248,6 +558,89 @@ npm run typecheck npm run build ``` +### 현재 HEAD 학생 UX·접근성 focused — 기준 **20/20** + +2026-08-09 읽기 전용 재감사에서 `check:api-types`·typecheck·build와 아래 fixture-only 묶음이 desktop/mobile +20/20을 통과했다: Alliance Pulse 8, 자기주도 폐루프 2, insecure-origin UUID 6, pre/mid 자기점검 2, +시간만료 경고·종료 모달 2. 390×844·320×568 overflow 0, tab 키보드, Enter CTA, 44px 조작부, +4.5:1 대비, raw UUID/theory key 비노출, 동의 전 `getUserMedia`/`enumerateDevices` 0도 확인했다. + +추가 회귀에서 새 음성 동의 modal을 실제로 수락한 뒤 위기 음성 종료까지 가는 desktop/mobile, 모바일 review +filter 44px 계약을 desktop/mobile로 검증해 4/4 통과했다. dashboard의 file-origin은 spec-relative repo root로 +고정했고, session-review fixture 소유권과 viewport assertion, admin live snapshot race를 교정했다. 16:49 단일 실행은 +**120/120 PASS(92.0s)**였지만, 그 뒤 dashboard 변경분은 dashboard 10/10으로만 따로 통과했다. 따라서 현 트리는 +110 불변 + 10 최신 분할 GREEN이고, 최종 clean source에서 아래 exact 묶음을 다시 단일 실행해 HEAD/tree/archive +결속 뒤 120/120을 요구한다. + +#### current-source fixture exact 120 — clean HEAD 재현 명령 + +정확한 수집 구성은 `dev-dashboard` 10 + `session-mvp` 24 + `session-review` 24 + `learner` 14 + +`session-layout` 8 + `admin` 40 = desktop/mobile 120이다. `127.0.0.1:8000`은 반드시 복구 dump에서 만든 +**disposable API/DB/engine 전용 스택**이어야 한다. 이 묶음은 dev-login·세션 쓰기를 포함할 수 있으므로 public +8001, 복구 원본 55432, candidate DB, NAS DB에 연결하지 않는다. + +```powershell +$ErrorActionPreference = 'Stop' +[Console]::OutputEncoding = [Text.UTF8Encoding]::new($false) +$OutputEncoding = [Text.UTF8Encoding]::new($false) +$cleanRoot = 'D:\workspace\vignette-clean-' +$cleanWeb = Join-Path $cleanRoot 'apps\web' +$tempRoot = Join-Path $env:TEMP 'vignette-clean-fixture-120' +$tempWeb = Join-Path $tempRoot 'apps\web' +$tempEvidence = Join-Path $tempRoot 'docs\ops\evidence' +if (Test-Path -LiteralPath $tempRoot) { throw "기존 TEMP와 충돌: $tempRoot" } +$pw = Join-Path $cleanWeb 'node_modules\.bin\playwright.cmd' +if (-not (Test-Path -LiteralPath $pw -PathType Leaf)) { + throw 'clean worktree에서 npm ci --ignore-scripts를 먼저 실행해 node_modules를 준비할 것' +} +$health = Invoke-RestMethod -Uri 'http://127.0.0.1:8000/health' -TimeoutSec 5 +if ($health.status -ne 'ok' -or -not $health.db -or -not $health.engine) { + throw 'disposable API8000의 db/engine ready가 아님' +} +New-Item -ItemType Directory -Path $tempWeb -Force | Out-Null +New-Item -ItemType Directory -Path $tempEvidence -Force | Out-Null +$config = Join-Path $cleanWeb 'playwright.config.ts' +$files = @( + 'e2e/dev-dashboard.spec.ts', + 'e2e/session-mvp.spec.ts', + 'e2e/session-review.spec.ts', + 'e2e/learner.spec.ts', + 'e2e/session-layout.spec.ts', + 'e2e/admin.spec.ts' +) +$env:PLAYWRIGHT_PORT = '15374' +$env:VITE_API_PROXY_TARGET = 'http://127.0.0.1:8000' +Remove-Item Env:PLAYWRIGHT_BASE_URL -ErrorAction SilentlyContinue +Remove-Item Env:PLAYWRIGHT_SKIP_WEB_SERVER -ErrorAction SilentlyContinue +$code = 99 +try { + Push-Location $tempWeb + try { + & $pw test @files ("--config={0}" -f $config) ` + --project=chromium-desktop --project=chromium-mobile ` + --workers=4 --reporter=line ("--output={0}" -f (Join-Path $tempRoot 'pw-results')) + $code = $LASTEXITCODE + } finally { + Pop-Location + } +} finally { + $listeners = @(Get-NetTCPConnection -State Listen -LocalPort 15374 -ErrorAction SilentlyContinue) + $listenerError = $null + if ($listeners.Count -ne 0) { $listenerError = "Playwright Vite listener 잔존: $($listeners.OwningProcess -join ',')" } + if (Test-Path -LiteralPath $tempRoot) { + $resolved = (Resolve-Path -LiteralPath $tempRoot).Path + if ($resolved -ne $tempRoot) { throw "unexpected TEMP target: $resolved" } + [IO.Directory]::Delete($tempRoot, $true) + } + if ($listenerError) { throw $listenerError } +} +if ($code -ne 0) { throw "clean fixture 120 failed: $code" } +``` + +과거 API-ready 없이 돌린 별도 Vite-only 혼합 실행의 `session-layout` 8건은 DB persona가 없어 제품 assertion 전에 +`seed_fallback/degraded`로 중단됐다. 위 exact 120 명령은 이 사각지대를 닫기 위해 disposable API의 db/engine health를 +선행 강제한다. 최종 완료 때는 `session-layout` 8/8과 returned-practice DB closed-loop 4/4를 모두 다시 통과시킨다. + ### Dashboard SSOT + E2E — 기준 SSOT PASS(FAIL 0), unit 5/5, dashboard 10/10 ```powershell @@ -275,7 +668,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는 610 tests / 44 files다. 실행 환경/API/DB를 정확히 맞추지 않고 fixture failure를 +전체 Playwright inventory는 614 tests / 44 files다. 실행 환경/API/DB를 정확히 맞추지 않고 fixture failure를 제품 failure로 오인하지 않는다. 같은 blocker가 두 번 반복되면 전체 재시도 대신 원인·증거·수정 계획을 먼저 보고한다. ## 9. 핵심 변경 파일 @@ -286,9 +679,18 @@ node.exe .\node_modules\@playwright\test\cli.js test e2e/session-layout.spec.ts - `scripts/run-outcome-os-release-agent.py` · `scripts/test_outcome_os_release_agent.py` - `scripts/check-dev-dashboard-ssot.py` · `scripts/test_dev_dashboard_ssot.py` - `scripts/check-g7-external-proof.py` · `scripts/soak-public-voice-websocket.py` · - `scripts/capture-g7-runtime-evidence.py` · `scripts/capture-g7-topology-evidence.py` + `scripts/capture-g7-runtime-evidence.py` · `scripts/capture-g7-topology-evidence.py` · + `scripts/run-g7-external-proof-window.py`와 대응 G7 테스트 3개 - `apps/web/src/lib/uuid.ts` · `apps/web/e2e/insecure-context-uuid.spec.ts` (신규, 비-secure origin 결함 수정) -- `apps/web/e2e/dev-dashboard.spec.ts` · `apps/web/e2e/returned-practice-db-closed-loop.spec.ts` · +- `apps/api/app/session_read_model.py` · `apps/api/app/services/session_metrics.py` · + `apps/api/app/test_session_read_model.py` · `apps/web/e2e/session-persistence.spec.ts` (UTC/KST P0와 실DB browser 회귀) +- `apps/web/index.html` · `apps/web/e2e/self-directed-learning-loop.spec.ts` (SPA route 전환 중 boot diagnostic 회복) +- `apps/web/e2e/admin.spec.ts` · `apps/web/e2e/dev-dashboard.spec.ts` · + `apps/web/e2e/session-review-fixture.ts` · `apps/web/e2e/session-review.spec.ts` +- `scripts/watch-public-runtime.ps1` · `scripts/boot-public-runtime.ps1` · + `scripts/install-public-runtime-task.ps1` · `scripts/register-boot-task.ps1` · + `scripts/watch-public-runtime-hidden.vbs` · `scripts/test_public_runtime_watchdog_provenance.py` +- `apps/web/e2e/returned-practice-db-closed-loop.spec.ts` · `apps/web/e2e/harness/prepare-returned-practice-db.py` - `docs/dev_dashboard.html` · `docs/TODO.md` · `docs/README.md` · `docs/ops/backlog-2026-06-26.md` - `docs/ops/nas-preview-g8-rollback-proof-runbook.md` · @@ -303,3 +705,69 @@ node.exe .\node_modules\@playwright\test\cli.js test e2e/session-layout.spec.ts - G7 물리 마이크·사람 평가를 무동의/합성 데이터로 대체하지 않는다. - current/previous images와 DB backup을 확인하기 전 destructive operation을 실행하지 않는다. - 성공보다 정직한 fail-closed 증거가 우선이다. + +## 11. 권장 멀티에이전트 실행 구조 + +사람 개발자 작업계획은 만들지 않는다. 구현·테스트·배포·증거 동기화는 에이전트가 수행하고, 사람에게는 코드로 +대체 불가능한 동의·인증 세션·실제 참가자/평가자 승인만 요청한다. 동시 작업은 최대 세 하위 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에게 인계. +3. **E2E/SSOT lane:** local fixture, disposable DB browser, visual/accessibility, dashboard/TODO/backlog 정합을 읽기 전용으로 감사. + +루트 에이전트는 각 lane의 증거 SHA와 실패 원인을 합쳐 게이트를 판정한다. 같은 blocker가 두 번 반복되면 세 번째 +재시도 전에 원인·증거·수정 계획을 사용자에게 보고한다. + +## 12. 새 에이전트에 그대로 줄 시작 프롬프트 + +```text +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를 재확인해. + +최종 목표는 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 공개 배포, +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와 +3,000초 공통 overlap, detached-clean HEAD/tree, runner/collector/checker SHA와 exact psutil pin은 소스에서 완료됐다. +API/cloudflared는 legacy PID를 재사용하지 말고 `-RequireFreshPublicProvenance`로 pinned release cwd에서 강제 +재시작해 새 PID/start/exe/command SHA/cwd safe receipt를 결속해. +G7 runner/checker/topology는 86/86, public launcher/sidecar 계약은 80/80, 통합은 166/166, API voice 통합은 71/71로 구현됐지만 +현재 public API의 Git 없는 냉동 release cwd는 provenance gate를 +통과하지 못한다. current committed repo 기반 launcher/runtime으로 정렬하거나 동등하게 강한 provenance를 구현하고, +cwd/Git pin이나 기존 Linux Compose gate를 약화하지 마. 합성 증거로 G7을 DONE 처리하지 마. +public task source-pin 코드는 구현됐다. watchdog·로그온 boot는 detached clean release와 exact Git commit/tree, +watchdog|boot/start script SHA가 없으면 mutation 전에 실패한다. 하지만 실제 두 Scheduled Task action은 아직 shared +worktree와 legacy 인자를 가리키며, 5분 watchdog은 최신 실행에서 pin 인자 부재로 `LastTaskResult=1`이다. public은 +현재 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, +NAS-origin E2E, visual/accessibility, auth/OpenAPI/assets/SSE→DB review를 포함해야 한다. + +독립 작업은 서브에이전트로 병렬화하되 NAS/public/DB mutation은 단일 owner만 수행해. 공개 DB 복구 volume과 +pre-recovery container/dumps, NAS named volume과 previous images는 삭제하지 마. 비밀/PII/raw UUID를 증거에 남기지 마. +과거 로컬 recovery dump 경로는 인계 시점에 보이지 않으므로 삭제로 단정하거나 그 파일을 전제로 복원하지 말고, +off-host/NAS 보관 위치와 새 backup 목적지를 먼저 확인해. +source-only PASS나 health-only를 DONE으로 부르지 말고, 대시보드·TODO·backlog·HANDOFF를 마지막 증거와 함께 동기화해. +모든 게이트가 실제로 닫히기 전에는 goal complete를 선언하지 마. +``` diff --git a/docs/TODO.md b/docs/TODO.md index 8810636..8806eb9 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -14,10 +14,18 @@ > 출처: `ops/backlog-2026-06-26.md` B2 · 대시보드 운영 탭 · `ops/tailscale-vnet-runtime-2026-06-27.md` · `ops/public-runtime-watchdog.md` · `HANDOFF.md` - [ ] **공개 DB 계정·회기 복구 안정화** — 2026-08-07 18:26 KST owner 승인으로 recovered named volume을 - 공개 `vignette-dev-db:55432`에 전환했다. public 기준 users 84/sessions 30/turns 705, + 공개 `vignette-dev-db:55432`에 전환했다. cutover 기준 users 84/sessions 30/turns 705, Google 계정 16/소유 회기 30, orphan 0, health·engine·OAuth·watchdog가 정상이다. 전환 직전 container와 dump, 원본 volume을 보존한다. 남은 것은 사용자가 Google 재로그인한 뒤 본인 과거 회기가 보이는지 확인하는 일과 자동 백업 운영화다. 기존 인증 세션은 복사하지 않아 재로그인이 필요하다. + 2026-08-09 Docker Desktop 중단과 18:02:55 KST Docker Desktop UI의 active DB 직접 stop으로 `db=false`가 + 재발했으나 동일 recovered container/volume만 재기동했고, 현재 owner read-only 집계는 users 356/sessions 193/ + turns 726, Google users 16/Google-owned sessions 31이며 public db/engine true다. watchdog·로그온 boot는 detached + clean release의 Git commit/tree와 watchdog|boot/start SHA가 없으면 mutation 전에 실패하도록 코드로 고정했다. + 실제 두 Scheduled Task action은 아직 shared worktree라 watchdog 반복 실행은 의도적으로 + `LastTaskResult=1`이다. 18:05 KST 복구 뒤 local/public health는 `status=ok·db=true·engine=true`, 공개 OpenAPI는 + 119 paths지만 `/voice/health`는 OpenAI STT/TTS이고 local voice sidecar listener 9882/9883은 0이다. + clean commit 뒤 동일 stable release root로 두 task를 재등록하기 전까지 자동복구는 DONE이 아니다. 상세: `ops/public-db-recovery-rehearsal-2026-08-07.md`. - [ ] **자동 DB 백업·정기 복원 리허설** — `scripts/backup-vignette-db.ps1`의 custom dump→TOC 검증→ SHA manifest 원자 게시와 현재 public-target 일회용 복원(129 tables/9,646 rows/orphan 0)을 통과했다. @@ -60,7 +68,9 @@ `CLAUDE_BIN` 부재로 기동)에서 `/health`는 `ok:true`로 통과하고 `/ready`는 503으로 검출되어 워치독이 `unhealthy (1/3)` + 503 본문을 남겼다. 남은 것: NAS용 9100은 실사용 세션 7개가 있어 미접촉이라 (2)는 다음 재기동 때 반영된다. -- [ ] **재부팅 후 watchdog smoke** — 실제 Windows 재부팅 후 엔진/API/터널 자동 복구 + public `/turn` 실측. +- [ ] **stable-source task 재등록 + 재부팅 후 watchdog smoke** — 사용자 승인 clean commit의 detached release root에 + watchdog·로그온 boot task를 함께 재등록하고 action의 commit/tree/script SHA pin, 5분 watchdog + `LastTaskResult=0`, 로그온 boot, 실제 Windows 재부팅 후 엔진/API/터널 자동 복구 + public `/turn`을 실측한다. DNS 개통 후 `api-vnet.18ka.net`을 `-AdditionalPublicHealthUrls`로 명시 추가. - [x] **노트북 로컬 음성 스택 STT 구현** — 2026-08-08 소유자 결정으로 음성 양쪽을 노트북 상주 로컬 모델로 쓴다(STT faster-whisper, TTS는 아래 항목대로 MeloTTS). 이전에는 코드에 로컬 STT 경로가 아예 없었고 @@ -69,7 +79,7 @@ `voice_stt_provider=local_whisper` 경로를 추가했다. 오디오는 호스트를 벗어나지 않고 외부 STT 키가 필요 없다. 실측: 저장소 합성 시드 음성 8.72초로 interim 9 · final 5 · word timestamp 12개 전부 present, 전사는 원문과 한 글자(`안녕하세요`→`안녕하세여`) 차이였다. 회귀 `scripts/test_local_whisper_stt_server.py` - 37/37, `apps/api/app/test_voice_service.py` 로컬 provider 케이스 포함 API 전체 908 passed. + 37/37, `apps/api/app/test_voice_service.py` 로컬 provider 케이스와 후속 계약을 포함한 current API 전체 921 passed. **발견 1:** 이 노트북은 CUDA 장치는 보이지만 cuDNN(`cudnn_ops64_9.dll`)이 없어 GPU 추론이 **네이티브 크래시**로 죽는다. 같은 프로세스 try/except로는 못 잡아서 디바이스 확인을 버릴 수 있는 자식 프로세스 (`--self-check`)로 분리하고 `auto`에서 CPU로 폴백하게 했다. cuDNN 9를 설치하면 GPU(float16)로 훨씬 @@ -84,19 +94,20 @@ 사전학습 다화자 모델이라 Higgs처럼 P1 프리셋 한정이 아니라 모든 페르소나에 적용된다. **실측:** CPU 정상 상태 RTF 0.27~0.28(실시간의 3.6배), `POST /tts` 200·WAV 350,566 bytes·3.61s, 빈 텍스트 422·미지 경로 404. **왕복 검증**으로 MeloTTS 합성음을 faster-whisper가 완전 일치 전사했다. - 회귀 사이드카 16/16, API 전체 914 passed, ruff clean. + 회귀 사이드카 16/16, 후속 계약을 포함한 current API 전체 921 passed, ruff clean. **검토 후 탈락:** Kokoro-82M은 Apache 2.0이지만 공식 `VOICES.md`에 **한국어가 없다**(검색 요약이 지원한다고 했으나 오답). Piper는 GPL, XTTS-v2·Fish Speech는 비상업이라 Higgs와 같은 문제다. 설치 함정 3가지는 `decisions/local-voice-stack.md`에 남겼다(setuptools<81, unidic download, eunjeon). 근거: [`decisions/local-voice-stack.md`](./decisions/local-voice-stack.md). -- [ ] **G7 외부 종료 게이트 — 남은 것은 사람 입력 둘뿐** — provider 의존은 해소됐다. 2026-08-08 로컬 스택 +- [ ] **G7 외부 종료 게이트 — 공개 runtime 선행 구현 + 사람 입력** — provider 의존 코드는 해소됐다. 2026-08-08 로컬 스택 (faster-whisper STT + MeloTTS TTS, 둘 다 MIT)으로 바꿔 외부 STT 키가 더 이상 필요 없고, 체커의 벤더 하드코딩도 운영 provider 허용목록으로 교정했다. 기계적인 부분도 `scripts/run-g7-external-proof-window.py`가 soak·runtime·topology 세 캡처를 **하나의 겹치는 시간창**에서 동시에 돌리고 checker까지 이어서 실행하도록 묶었다(회귀 20/20). - **남은 것은 코드로 만들 수 없는 둘이다.** + 사람에게서만 받을 수 있는 마지막 두 입력은 다음이다. (1) **명시 동의 하 물리 마이크 50분 양방향 soak** — 소유자가 실제로 50분 발화해야 한다. - (2) **독립 blind human voice-gain pack** — 참가자 30명 / 50회기 / 150 paired axis / 독립 평가자 2인 / + (2) **독립 blind human voice-gain pack** — held-out 30명 외 calibration split 참가자 포함 총 최소 31명 / + held-out 50회기 / 150 paired axis / 독립 평가자 2인 / ICC(A,1) ≥ 0.75 · κ ≥ 0.70 · gain ≥ 0.01 · participant-cluster bootstrap 10,000회 95% CI lower > 0. **선행 조건(2026-08-08 발견):** 공개 런타임은 119 paths 구버전이라 `/admin/voice-runtime`이 배포돼 있지 않다. 마이크와 사람을 다 준비해도 오늘 실행하면 50분을 버리고 artifact 2에서 실패한다. @@ -104,10 +115,36 @@ 오케스트레이터가 시작 전에 `admin_voice_runtime_not_deployed`로 막는다 — 실측 exit 2. 이 판정은 미인증 HTTP 상태가 아니라 OpenAPI `paths`로 한다. Cloudflare가 자동화 클라이언트에 모든 경로를 403(error 1010)으로 주기 때문에 상태 코드로는 배포 누락을 알 수 없다. - 실행 전 `--rehearse`로 배관을 먼저 검증한다. fail-closed 경계 4종(동의 없음·pack 없음·50분 미만·host - 불일치)은 CLI로 실증했다. 상세: `ops/outcome-os-g7-external-proof-readiness-2026-08-07.md`. + 2026-08-09 재감사에서 공개 `/voice/health`는 여전히 OpenAI STT/TTS이고 local Whisper 9882·MeloTTS + 9883 listener가 없음을 확인했다. current source에는 sidecar-before-API lifecycle, exact + `local_whisper/small/cpu-int8`·`melotts/melotts-korean` readiness, provider env와 Uvicorn + `--ws websockets --ws-max-queue 4`를 fail-closed로 연결했다(launcher/sidecar 80/80, API voice 71/71). + Windows host topology mode도 PID/start/exe·command SHA/cwd와 process/TCP high-water를 검증한다. + 하지만 이 source를 공개에 배포한 적은 없고, 현재 API의 Git 없는 냉동 release cwd는 provenance gate를 통과하지 + 못한다. current committed repo 기반으로 공개 runtime을 정렬한 뒤에만 50분 창을 연다. + **외부 실행 전 코드 P0 완료:** runner 종료 코드는 canonical checker `exit 0`/`gate_closed=true`에 결속됐고, + 브라우저 Origin(`vignette.chanpaca.net`)은 API/WSS/admin/topology host(`api-vignette.chanpaca.net`)와 분리된 HTTPS + allowlist로 검증한다. 실제 capture는 최소 3,120초·ceil+terminal sample이고 voice/runtime/topology 공통 overlap + `≥3000s`를 checker가 강제한다. Windows artifact는 detached-clean HEAD/tree, runner/collector/checker SHA, + exact `psutil==6.1.1`을 매 sample 전후 pin한다. fresh launcher는 legacy API와 exact-config cloudflared를 bounded + 교체하고 새 PID/start/exe/command SHA/실제 cwd의 raw command-line 없는 receipt를 만든다. G7 runner/checker/topology + 86/86, launcher/sidecar 80/80, 통합 166/166을 통과했다. 실제 공개 실행 전 `--rehearse`로 배관을 먼저 검증한다. + fail-closed 경계(동의 없음·pack 없음·3,120초 미만·host/Origin/scheme 불일치)는 CLI로 실증했다. 상세: + `ops/outcome-os-g7-external-proof-readiness-2026-08-07.md`. -> **완료된 격리 검증 경로:** NAS 프리뷰 `http://100.116.83.60:8088`을 전용 Compose 프로젝트·포트·네트워크·볼륨에 배포하고 실제 브라우저 회기와 review API 저장 축어록을 확인했다. current source exact SHA `6030a677af7e87cbfabc422b553d108d53414fd3c446548734a13b036d35c611`은 전체 browser E2E 108/108, postdeploy SSE→DB review, health 3/3, OpenAPI 126·auth 401·G0~G8 route를 통과했다. 기존 프로젝트를 대상으로 한 중단·재생성 명령은 실행하지 않았다. 매일 04:30 KST `Vignette 회기 E2E 정기 검증`(automation id `vignette-e2e`)은 ACTIVE다. 새 기능이 material milestone이고 release-only patch 결정성·manifest checker·canonical clean-index·NAS preflight를 통과하며 배포 SHA가 달라질 때만 같은 격리 프리뷰를 갱신한다. 첫 스케줄 실행 이력은 아직 증거에 포함하지 않았다. 증거: [배포 증거](./ops/nas-preview-deployment-evidence-2026-08-07.md), [current 기계 판독 증거](./ops/evidence/nas-preview-current-deploy-2026-08-07.json), [브라우저 증거](./ops/evidence/nas-preview-live-turn-2026-08-07.png). 이는 G8 실제 receipt-bound rollback이나 미승인 물리 마이크·부재한 Deepgram 운영 키를 대체하지 않는다. +> **격리 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`도 보존한다. +> `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 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다. +> 증거: [배포 증거](./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 응답·계량·오류 표면화를 비교하는 것이다. ## B. 외부 거버넌스 [외부] @@ -364,12 +401,13 @@ - [x] 텍스트·음성 평가 독립 산출 후 calibrated fusion 구현. - [x] 장면별 오디오 재생과 음성 재연습 구현. - [x] 비언어 특징의 과도한 임상 추론 차단 계약·테스트 구현. -- [x] Deepgram streaming STT adapter, interim/final·word timestamp·provider event 계약과 OpenAI batch fallback 구현. +- [x] Deepgram streaming STT adapter와 OpenAI batch fallback을 보존하고, 운영 기본 경로를 노트북 상주 + `local_whisper`/`melotts`로 전환해 interim/final·word timestamp·provider event 계약을 동일하게 구현. - [x] 저장 word token을 deployment `SESSION_SECRET` 기반 HMAC-SHA256 pseudonym으로 전환하고 원문 미보존을 고정. - [x] 열린 streaming 중 동의를 1초마다 재검사해 철회 시 provider 전송·후속 저장을 중단하는 fail-closed 계약 구현. - [x] 음성 단절/실패 뒤 작성 중 텍스트를 보존하고 텍스트 계속하기·키보드 가능한 음성 재연결 recovery UX 구현. -- [x] 운영 TTS를 OpenAI API `gpt-4o-mini-tts`로 고정하고 공식 Services Agreement의 앱 통합·출력 소유 근거와 - 최종 사용자 AI 생성 음성 고지를 제품·증거 계약에 반영. +- [x] 외부 OpenAI TTS adapter와 AI 생성 음성 고지를 보존하면서, 운영 기본 TTS를 상업 사용 가능한 + MeloTTS Korean(MIT) loopback 사이드카로 전환. - [x] authenticated WSS `ready`에 STT/TTS provider·model을 노출하고, 마이크 열거·캡처 없는 public preflight가 네 기대값과의 정확 일치를 검사하도록 구현. full soak만 명시적 `--confirm-physical-capture` 뒤 장치를 연다. - [x] synthetic gold target에 대해 text-only와 calibrated multimodal 출력을 직접 비교하는 1-MAE benchmark를 @@ -397,7 +435,7 @@ 격리 synthetic PCM soak는 59/59 연결 종료·성공 발화 55·철회 차단 2·미동의 차단 1·overflow 거부/복구 1/1, focused 119 passed를 통과했다. 공개 G7 route는 TLS 1.3 handshake·비인증 1008·Cloudflare edge와 마이크 열거/캡처 0을 확인했다. 증거: `ops/evidence/g7-public-wss-preflight-current-20260807.json`. - **남음:** 실제 Deepgram 운영 key·quota와 live interim/final, 기대값과 일치하는 authenticated public WSS ready, + **남음:** current source의 공개 배포와 `local_whisper`/`melotts` ready metadata 일치, authenticated public WSS ready, 사용자의 명시 동의를 받은 물리 마이크, 실행된 50분 양방향 soak, 독립 라벨 held-out/pilot의 실제 음성 이득, 실제 worker·Uvicorn·OS·Cloudflare edge를 같은 50분 시간창으로 묶은 RSS/CPU/queue high-water. preflight·synthetic·옵션 제공만으로 종료하지 않는다. @@ -416,7 +454,7 @@ semantic runtime/browser proof, 이미지 rollback, exact-SHA SSOT handoff를 fail-closed로 구현·독립 감사. - [x] 위 release agent로 변경 SHA `e9b482ccba616a610c1f4862366d44aabae20ee8c82d8ef7f2cf16069f2d6e99`를 실제 격리 NAS에 승격하고 postdeploy browser·DB review·rollback snapshot·exact-SHA SSOT handoff를 남겼다. -- [x] **G8 종료 GATE 재검증** — source→draft→red-team→benchmark→catalog 재현, 정답 누수·PII·무근거 +- [x] **G8 내부 구현·실제 rollback receipt GATE** — source→draft→red-team→benchmark→catalog 재현, 정답 누수·PII·무근거 임상 주장 0건, 모델 교체 자동 calibration/rollback, 운영 오류의 재현 테스트·backlog DAG 자동 환류. **현재 코드 증거:** caller가 완성 JSON을 넣는 기존 경계와 별도로 source hash·PII fail-closed→structured generator→서로 결과를 공유하지 않는 red-team 2개→variant generator→독립 judge 3개 이상→durable @@ -450,9 +488,22 @@ 공개 런타임 미접촉, 비밀 env 파기, helper 0·listener 0 복귀. 상세: [런북](./ops/nas-preview-g8-rollback-proof-runbook.md) · [기계 판독 증거](./ops/evidence/nas-preview-g8-actual-rollback-2026-08-07.json). -- [x] current source exact SHA `6030a677…c611`을 격리 NAS에 승격했다. release-only patch 3회 결정성, - 전체 browser E2E 108/108, postdeploy SSE→DB review 32.437s, health 3/3, OpenAPI 126·auth 401·G0~G8 - route, named DB volume 보존을 확인했다. 이는 runtime promotion 증거이며 실제 rollback receipt와 구분한다. +- [x] 2026-08-07 release-only SHA `6030a677…c611`을 격리 NAS에 승격했다. patch 3회 결정성, + localhost candidate browser E2E 108/108, postdeploy SSE→DB review 32.437s, health 3/3, + OpenAPI 126·auth 401·G0~G8 route, named DB volume 보존을 확인했다. 이는 당시 runtime promotion + 증거이며 현재 HEAD 배포 증거가 아니다. +- [x] **clean-head release mode** — 명시적 `--source-mode clean-head`와 별도 detached source root를 추가했다. + tracked-dirty 차단, exact HEAD/tree/archive SHA, 2회 결정성, archive-only candidate, patch-mode 무회귀, + 증거 binding, source-only insecure-context 6/6 gate, 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()` 수정과 insecure-origin 6/6 회귀를 포함한 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·HEAD/tree/archive를 결속해 실제 + `http://100.116.83.60:8088` origin에서 전체 회기 + E2E 0 failure·SSE→DB review·student returned-practice desktop/mobile을 다시 검증하기 전에는 G8 전체를 DONE으로 + 올리지 않는다. ### I-DAG. 첫 에이전트 실행팩 AOS-001~012 @@ -475,7 +526,6 @@ 납품(공개 데모)의 핵심 블로커는 **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 proof가 남았다. G7은 내부 구현 DONE과 외부 proof GATE를 분리하며, -Deepgram live·auth public WSS·명시 동의 mic· -50분 soak·독립 라벨 benchmark·운영 resource high-water가 남았다.** D~G는 +G8 receipt-bound 실제 NAS rollback은 완료됐고 UUID 수정 배포·NAS-origin 회기 재검증이 남았다. 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/decisions/local-voice-stack.md b/docs/decisions/local-voice-stack.md index 74f1c91..3c945b8 100644 --- a/docs/decisions/local-voice-stack.md +++ b/docs/decisions/local-voice-stack.md @@ -71,6 +71,18 @@ P1 프리셋 한정이었던 이유(권리 안전한 synthetic reference 를 P1 - `scripts/melotts-server.py` — loopback HTTP 사이드카(`/health`, `POST /tts` → WAV). `VIGNETTE_VOICE_TTS_PROVIDER=melotts` + `VIGNETTE_MELOTTS_TTS_URL` 설정. 실행: `scripts/start-melotts.ps1`. +- 공개 런처 `scripts/start-public-runtime.ps1`은 현재 CPU 운영값인 + `local_whisper/small/cpu-int8`(9882)과 `melotts/melotts-korean`(9883)을 직접 소유한다. + `scripts/probe-public-voice-sidecars.py`가 Whisper의 첫 WebSocket `ready` 프레임과 MeloTTS + `/health`의 provider/model/license/reference metadata를 정확히 검증한다. 기존 포트 리스너가 있어도 + 이 계약을 통과하지 않으면 임의 종료·재사용하지 않고 API 재시작 전에 fail-closed한다. +- 공개 API는 같은 provider/model을 `/voice/health`로 다시 증명해야 기존 프로세스를 유지한다. 새 Uvicorn은 + `--ws websockets --ws-max-queue 4`로 고정한다. 이 항목은 런처 소스 계약이며, 실제 공개 적용 완료 증거는 + authenticated WSS·동의 기반 50분 soak·동시간 topology/runtime 증거가 모두 통과한 뒤 G7 pack에 남긴다. +- G7 실제 증거 승격은 `-RequireFreshPublicProvenance`로 detached-clean commit/tree와 Python/cloudflared/config SHA를 + mutation 전에 검증한다. legacy API와 exact-config cloudflared를 bounded 교체한 뒤 새 PID/start/executable·command + SHA/실제 cwd의 safe receipt를 남기며 raw command line·config contents는 저장하지 않는다. runner는 최소 3,120초를 + 캡처하고 voice/runtime/topology 공통 3,000초를 canonical checker와 결속한다. ## 실측 증거 (2026-08-08) diff --git a/docs/dev_dashboard.html b/docs/dev_dashboard.html index 002df86..a7a53a6 100644 --- a/docs/dev_dashboard.html +++ b/docs/dev_dashboard.html @@ -46,10 +46,16 @@ section.block{margin-top:34px;scroll-margin-top:76px;} /* PULSE / hero */ .pulse{display:grid;grid-template-columns:minmax(0,1.05fr) minmax(330px,.95fr);gap:22px;align-items:stretch;margin-top:22px;} - .pulse-main{background:var(--surface);border:1px solid var(--hair);border-radius:16px;padding:22px 24px;box-shadow:0 1px 2px rgba(28,42,42,.05);} + .pulse-main{min-width:0;background:var(--surface);border:1px solid var(--hair);border-radius:16px;padding:22px 24px;box-shadow:0 1px 2px rgba(28,42,42,.05);} .pulse-main h1{margin-top:9px;} .pulse-state{margin:13px 0 0;color:var(--ink-2);font-size:13.5px;max-width:640px;} .pulse-state b{color:var(--ink);} + .pulse-details{max-width:640px;margin-top:12px;border-top:1px solid var(--hair);} + .pulse-details summary{display:flex;align-items:center;min-height:40px;color:var(--accent-deep);font-size:12.5px;font-weight:750;cursor:pointer;list-style:none;} + .pulse-details summary::-webkit-details-marker{display:none;} + .pulse-details summary:after{content:"+";margin-left:auto;color:var(--ink-3);font:700 14px/1 var(--mono);} + .pulse-details[open] summary:after{content:"−";} + .pulse-details .pulse-state{margin:0;padding:2px 0 4px;font-size:12.5px;line-height:1.6;} .phase-rail{display:grid;grid-template-columns:repeat(6,1fr);gap:7px;margin-top:18px;} .pr-seg{position:relative;} .pr-bar{height:7px;border-radius:999px;background:var(--paper-3);overflow:hidden;} @@ -78,7 +84,7 @@ .kpi-badge{position:absolute;right:13px;top:12px;display:none;align-items:center;gap:5px;font:800 10px/1 var(--mono);letter-spacing:.02em;color:#fff;background:var(--crit);padding:5px 8px;border-radius:7px;box-shadow:0 1px 3px rgba(168,53,44,.35);} .kpi.k-plan{border-color:color-mix(in srgb,var(--crit) 18%,var(--hair));} /* metric strip */ - .mstrip{display:flex;flex-wrap:wrap;gap:8px;margin-top:16px;} + .mstrip{display:flex;flex-wrap:wrap;min-width:0;max-width:100%;gap:8px;margin-top:16px;} .mchip{display:inline-flex;align-items:center;gap:7px;padding:6px 11px;border:1px solid var(--hair);border-radius:999px;background:var(--paper);font-size:11.5px;color:var(--ink-2);} .mchip b{font:700 12px/1 var(--mono);color:var(--ink);} .mchip.good b{color:var(--pos);} @@ -262,6 +268,9 @@ .task-row,.task-row.r3{grid-template-columns:1fr;} .task-row>div{border-bottom:1px solid var(--hair);} .task-row>div:last-child{border-bottom:0;} .brand small{display:none;} + .owner-pill,.fbtn,.tab-button,.dg-link,.pulse-details summary{min-height:44px;} + .mstrip{flex-wrap:nowrap;overflow-x:auto;padding-bottom:4px;scrollbar-width:thin;} + .mchip{flex:0 0 auto;} } @media (max-width:460px){ .kpis{grid-template-columns:1fr;} h1{font-size:22px;} .stamp{display:none;} } @media (prefers-reduced-motion:reduce){ *{transition:none!important;} } @@ -272,7 +281,7 @@
Vignette 커맨드센터팀장 리뷰 · dev status
내 차례 0 - 2026-08-07 KST + 2026-08-09 KST
@@ -281,12 +290,16 @@
project pulse -

지금 위치: Phase 0 제품 데모는 public /turn proof가 핵심 블로커.
Phase 1 MVP 코어 루프는 로컬 DB-backed 실증 완료.

-

긴급 운영 상태: 2026-08-07 18:26 KST 공개 DB를 복구 named volume으로 전환했다. public owner 집계 users 84·sessions 30·turns 705, Google 계정 16·소유 회기 30, orphan 0이며 health·engine·OAuth·로그인 화면·watchdog가 정상이다. 보안상 기존 auth session은 복사하지 않아 모든 사용자는 Google 재로그인이 필요하고, 전환 직전 container·dump와 원본 volume은 삭제하지 않았다. 개발 상태: G0 provenance census 29/29·위반 0과 G6 safety metadata-only 우선 runtime은 disposable clone에서 통과했다. G4·G5 실제 API/DB/browser 폐루프와 current exact SHA 6030a677…c611의 NAS 승격은 완료됐고 전체 browser E2E 108/108·postdeploy SSE→DB review·health 3/3·OpenAPI 126을 통과했다. G8은 격리 NAS에서 실제 image rollback 2회를 executed receipt(nas-g8-723eeef2…/nas-g8-2738846c…)로 실행해 DONE이며, G7은 Deepgram live·인증 WSS·명시 동의 물리 마이크 50분·독립 라벨 voice-gain과 resource high-water 전까지 외부 GATE다.

+

지금 위치: 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 공개 음성·물리 마이크·독립 사람 증거 순서로 진행한다.

+
+ 운영 복구·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다.

+
- Full E2E 현재 수집 604 · 전체 GREEN 대기 - 현재 전체 회귀 · gateway 58 · backend 898 + Fixture E2E 110 + dashboard 10 분할 GREEN · clean HEAD 120/120 대기 + 현재 전체 회귀 · gateway 58 · backend 921 Outcome clean E2E 126 · visual 15/15 Alliance G1 baseline 1.2 9/9 · 내부 DONE Tailnet 이전 확인 OK @@ -637,6 +650,11 @@

182차 적용(2026-08-08): 회기 전 자기점검(pre alliance gate) 화면을 데스크톱·모바일 전 폭에서 다시 짰다. 계측이 먼저였다. gate 상태에서 카드가 align-self:stretch + grid-template-rows: auto minmax(0,1fr) auto auto인데 base flex의 align-items:center가 살아 있어 3축 블록이 1fr 행 한가운데 떠 있었다. 1920×1080 기준 카드의 66.5%가 빈 공간이고 축 위·아래로 각각 277px의 죽은 여백이 생겼다. 척도 라벨은 0.54rem+nowrap+ellipsis라 901~1279px에서 최대 12개가 "전허 아…"로 잘렸고, ≤620px에서는 display:none으로 15개 전부 사라져 숫자 1~5만 남았다. 320×568에서는 @media (max-width:420px) and (max-height:620px)의 collapsed 전용 40px 고정 행이 gate에도 걸려 카드가 40px로 눌리고 헤더와 축이 겹쳐 화면이 사실상 깨져 있었다. 수정은 네 가지다. (1) gate 카드는 행 높이만큼 늘리지 않고 align-self:center+max-height:100%+내부 스크롤로 화면 중앙에 서게 했다. (2) 3열 격자를 버리고 축마다 질문 한 줄 + 전폭 5칸 척도 세로 스택으로 바꿔 어느 폭에서도 라벨을 자르거나 숨기지 않는다(word-break:keep-all+overflow-wrap:normal로 어절 경계만 줄바꿈). (3) 정보가 없던 3 AXES 모노 배지를 제출 버튼이 왜 잠겨 있는지 답하는 0/3 축 선택함 진행 표시(aria-live)로 교체했다. (4) 40px 행 규칙에 :not(.sx-alliance-gated)를 걸어 collapsed mid 전용으로 되돌렸다. 선택 상태 글자색도 반투명 --surface에서 --text-on-accent로 고쳤다(라이트 4.85:1 · 다크 6.36:1, AA). 15개 뷰포트 재계측 결과 빈 공간 비율 0.665→0.127(1920), 라벨 잘림 12→0, 숨김 15→0, 최소 탭 타깃 38×38→46×60, 문서 가로 오버플로 전 구간 0이고, 320×568에서는 카드 내부 스크롤로 제출까지 도달해 실제 잠금·발화 입력 활성까지 확인했다. 인라인 mid 카드는 ≥1080px에서만 3열로 눕혀 축어록을 밀지 않게 두었고 높이는 오히려 약 55px 줄었다. 무회귀: typecheck 통과, session-layout 8/8, alliance-pulse 8/8, pre/mid 잠금 E2E 2/2, layout-visual-gate 22/23(실패 1건 settings는 변경 전 baseline에서도 동일 실패로 확인된 기존 회귀). session-mvp 음성 4건 실패도 baseline 동일이라 이 변경과 무관하다.

183차 적용(2026-08-08): 182차 뒤 소유자 스크린샷에서 시간 만료 상태의 게이트 화면이 여전히 깨져 있어 네 개의 별개 결함을 계측으로 특정하고 닫았다. (1) .sx-timebar는 세션이 다크 stage 팔레트를 강제하던 시절의 잔재로, 배경이 어디에도 정의된 적 없는 --bg-panel의 하드코딩 다크 폴백으로 칠해지는데 글자만 테마 토큰을 따라가 라이트에서 제목 1.13:1·설명 1.76:1이었다. 의미색 토큰(--warn-*/--crit-*)으로 되돌려 13.88:1·6.94:1(다크 13.86·9.30)로 올렸다. (2) 같은 이유로 .sx-end-dialog__panel이 어두운 gradient 위 어두운 글자라 제목 2.68:1·본문 1.14:1로 사실상 보이지 않았다. 불투명 --bg-app 표면으로 바꿔 15.79:1·5.19:1(다크 15.18·6.52)이 됐다. (3) 보태니컬 장식 위로 본문을 올리는 .sx-page--active > *{position:relative}가 같은 특이도(0,1,0)로 뒤에 있어 종료 모달의 position:fixed를 덮어썼다. 오버레이가 화면을 덮는 대신 그리드 한 칸(1532×32px)으로 눌려 들어갔다. :not(.sx-end-dialog)로 제외해 fixed 1600×900 뷰포트 전체를 회복했다. (4) 게이트 행 정의 .sx-page--active.sx-alliance-gated(0,2,0)가 :has() 두 개짜리 규칙(0,3,0)보다 특이도가 낮아 시간 만료 바가 뜨는 순간 밀려났다. 숨긴 축어록·컨트롤바의 빈 트랙이 남아 화면 절반이 비었다(80px 59px 628px 0px 116px 5행). 게이트 규칙에도 같은 :has()를 붙여 3행(80px 59px 693px)으로 정정했다. 무회귀: typecheck 통과, session-layout+alliance-pulse+layout-visual-gate 31/31(182차에서 실패했던 settings도 통과), 시간만료 종료 다이얼로그 E2E 2/2, pre/mid 잠금 E2E 2/2, full-sweep-session+self-directed-learning-loop 30 pass. 남은 음성 4건(voice skip·Space mic)은 변경분 stash 후 baseline에서도 동일 실패라 이 변경과 무관한 기존 회귀다.

184차 적용(2026-08-08): 182·183차 변경이 로컬 워크트리에만 있어 소유자가 보던 공개 사이트에는 반영될 수 없던 상태를 배포로 닫았다. 소유자 스크린샷의 3 AXES는 이미 소스에서 사라진 문자열이었고, 접속 주소가 https://vignette.chanpaca.net이라 커밋 전 변경이 보일 수 없는 것이 원인이었다. 커밋 f0c2240f로 고정한 뒤 npm run build(Session 청크 Session-DnhnKvTy.js·Session-Xn1JLv-d.css), scripts\preserve-pages-assets.ps1로 직전 5개 production 배포의 지연 로드 자산 104개를 보존하고(진행 중 세션의 lazy chunk 404 방지), wrangler pages deploy dist --project-name vignette --branch main --commit-hash f0c2240f로 배포했다(deployment a12c195c, 신규 업로드 37 / 기존 666). 커스텀 도메인 검증은 index-BTkJUPLZ.js 200, Session JS/CSS 모두 올바른 MIME 200이며 신규 마커(축 선택함·is-gate·:not(.sx-end-dialog)) 각 1건, 구버전 마커(3 AXES·--bg-panel·척도 text-overflow) 전부 0건이다. 첫 조회에서 edge가 이전 index를 준 건 배포 직후 타이밍이었고 재조회에서 신규 index로 확정됐다. 공개 API·런타임은 건드리지 않았다.

+

185차 적용(2026-08-09): 177·181차의 G8 DONE 판정은 rollback receipt gate 완료로 범위를 정정했다. 108/108은 localhost candidate 결과였고 같은 NAS 평문 origin에서는 secure-context 전용 crypto.randomUUID 예외로 회기 E2E 24건이 실패했다. 소스는 randomUuid()로 통일해 insecure-origin 6/6·API type·typecheck·build를 통과했지만 NAS에는 아직 미배포다. 따라서 G8 카드는 BUILD · RECEIPT DONE · RUNTIME REVALIDATION으로 되돌리고, clean-head release mode 구현→수정 배포→실제 NAS-origin 전체 회기 E2E GREEN까지를 남긴다. G7은 운영 기본 local_whisper/melotts의 current 공개 배포·인증 WSS·명시 동의 mic 50분·동시 runtime/topology·독립 human pack 전까지 외부 GATE다.

+

186차 적용(2026-08-09): 공개 DB listener 소실 사고는 Docker Desktop 중단으로 좁혀졌고 recovered named volume을 보존한 기존 컨테이너만 재기동해 복구했다. public/local health는 db·engine true, current owner 집계는 users 171·sessions 93·turns 726이다. 당시 워치독 action의 삭제된 옛 workspace 드리프트를 shared current workspace·5분 주기로 교정하고 즉시 실행 0을 확인했다. 이 임시 조치는 후속 source-pin에서 안전하지 않은 입력으로 판정됐고, 현재 task는 detached-clean pin 인자 부재로 mutation 전에 fail-closed한다. G8 clean-head release mode는 tracked-dirty 차단·HEAD/tree/archive 2회 결정성·archive-only candidate·patch 무회귀·evidence binding과 commit 전 NAS-origin 11-spec rollback gate를 unit 30/30으로 고정했고, 새 NAS preview predeploy dump 72698232…5d41c(1,015,222 bytes·TOC 1,752)를 보존했다. G7 재감사에서는 공개 OpenAPI 119·/admin/voice-runtime 부재, OpenAI voice provider, local sidecar listener 0, Windows host topology와 Compose-only collector 불일치를 확인했으므로 사람 증거 전에 launcher·topology 선행 구현이 필요하다.

+

187차 적용(2026-08-09): G8 clean-head dry-run은 active 6030a677…c611→desired archive 6b1b15bd…a3f7를 mutation 0으로 결속했다. execute 1은 archive CRLF/API type byte-check에서, execute 2는 API·types·build 통과 뒤 session E2E 109 passed / 5 failed에서 모두 승격 전에 fail-closed되어 NAS active/image/DB는 바뀌지 않았다. source-only UUID 4건은 production 번들에서 /src/lib/uuid.ts import가 불가능한 gate 배치 오류라 별도 Vite gate 6/6으로 분리했다. 남은 실제 제품 blocker는 timezone 없는 started_at을 KST 브라우저가 약 9시간 경과로 오인해 시간종료 모달이 정상 코칭 이력을 가리는 문제다. backend UTC offset/Z 계약과 focused disposable DB/browser 회귀를 닫고 새 clean commit을 만든 뒤에만 NAS 배포를 재개한다.

+

188차 적용(2026-08-09): 회기 시간 P0를 우회 없이 수정했다. browser-facing started_at/ended_at과 learner/teacher dashboard timestamp는 UTC +00:00을 반환하고, 리뷰의 달력 날짜는 서버 locale 대신 KST(+09:00)를 사용한다. backend 관련 65 passed, 후속 voice 계약을 포함한 API 전체 921 passed, Ruff·compile·web typecheck를 통과했다. 고유 Compose DB/API/Web/engine과 Asia/Seoul browser에서 코칭 이력 focused 1/1은 active/null·UTC suffix·10분 미만 경과·timebar/dialog 0·reload 후 coach mark 실제 클릭과 source-pack dialog를 확인했다. 종료 뒤 container·volume·listener 0이다. 이제 새 clean commit 결속과 NAS-origin 전체 E2E가 남았다.

+

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다.

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

코드품질 · 로컬 개발 스택

리팩토링 연구 + 1-커맨드 dev 스택 (2026-06-26~27). code-quality-research-2026-06-26.md · local-development.md
- -
공개 전환

2026-08-07 18:26 KST owner 승인으로 vignette_recovered_prod_20260807vignette-dev-db:55432에 연결했다. public owner 집계 users 84·sessions 30·turns 705, Google 계정 16·소유 회기 30, orphan 0이다. local/public health는 prod·DB true·engine true·claude_cli, OpenAPI 119, Google OAuth 302, 공개 로그인 화면 렌더와 watchdog check-only를 통과했다.

보존·rollback

전환 직전 DB는 vignette-dev-db-pre-recovery-20260807-182642로 중지 보존했고 exact dump SHA는 6b84d5c8…8af1f다. 원본 volume, post-merge dump d24eaa75…2e32, 최종 recovered dump d7bcc396…68ed, 자동 rollback을 실증한 실패 후보 2개도 삭제하지 않았다.

사용자 확인

보안상 기존 auth_session은 복사하지 않아 모두 Google 재로그인이 필요하다. 계정·회기 원장은 복구돼 있다. 실제 사용자가 재로그인한 뒤 과거 소유 회기 목록이 보이는지 확인할 때까지 이 카드는 안정화 중으로 유지한다. 상세: ops/public-db-recovery-rehearsal-2026-08-07.md.

+ +
공개 전환

2026-08-07 18:26 KST owner 승인으로 vignette_recovered_prod_20260807vignette-dev-db:55432에 연결했다. cutover 집계 users 84·sessions 30·turns 705, Google 계정 16·소유 회기 30, orphan 0이다. 2026-08-09 Docker Desktop 중단 후 동일 컨테이너만 재기동했으며 현재 users 171·sessions 93·turns 726, local/public health prod·DB true·engine true·claude_cli, OpenAPI 119, Google OAuth 302다. source-pin 코드는 완료됐지만 실제 watchdog·로그온 task action은 shared worktree라 최신 watchdog은 pin 인자 부재로 mutation 전에 exit 1이다. clean commit의 detached stable release root로 두 task를 재등록하기 전까지 자동복구는 의도적으로 fail-closed다.

보존·rollback

전환 직전 DB는 vignette-dev-db-pre-recovery-20260807-182642로 중지 보존했고 exact dump SHA는 6b84d5c8…8af1f다. 원본 volume, post-merge dump d24eaa75…2e32, 최종 recovered dump d7bcc396…68ed, 자동 rollback을 실증한 실패 후보 2개도 삭제하지 않았다.

사용자 확인

보안상 기존 auth_session은 복사하지 않아 모두 Google 재로그인이 필요하다. 계정·회기 원장은 복구돼 있다. 실제 사용자가 재로그인한 뒤 과거 소유 회기 목록이 보이는지 확인할 때까지 이 카드는 안정화 중으로 유지한다. 상세: ops/public-db-recovery-rehearsal-2026-08-07.md.

@@ -866,8 +884,8 @@
구현

app.measurement_event·app.measurement_instrument·audit.model_run과 6종 provenance, Python→JSON Schema→TypeScript 계약, 기존 rapport/alliance/fast/deep/Phase 3 adapter, 혼합층 집계 차단, 8개 deterministic benchmark 장면을 구축했다. AOS-001~004 완료.

종료 증거

post-merge dump disposable clone에 migrations 14/15/16을 각각 두 번 원자 재적용했다. current source census는 producer 29/29, unknown/missing table·null instrument/source/perspective·orphan instrument/model-run/evidence·호환성 위반 0이다. 기존 계약/schema·live append-only/RLS 증거도 유지한다. 상세: ops/evidence/g0-provenance-census-scratch-post-g6-g8-20260807.json, ops/evidence/measurement-migration-rehearsal-2026-08-07.json.

- -
구현

회기 pulse, legacy alliance 격리, 자기평가 선잠금, 독립 모델 실행·재시도, 학습자 관점차 리뷰, 교수자 append-only 재평가를 구축했다. 실제 Session은 첫 발화 전 pre를 fail-closed로 잠그고 첫 왕복 뒤 흐름을 막지 않는 mid 30초 점검을 제공한다. AOS-005~010 완료.

종료 증거

focused backend 43 passed, live PostgreSQL reveal/RLS/audit/supersession smoke, desktop/mobile Alliance Pulse 6/6와 기존 회기 리뷰 12/12 통과. 동일 payload stable ID, 변경 payload 409, 타 학습자·교차 cohort 404와 pre/mid desktop·mobile 2/2·고정 캡처 4장을 추가 검증했다. 최신 source API/dev DB는 한 회기의 pre(degraded/insufficient_transcript) → mid(ready) → post(ready)를 실제 turn UUID·ended_at으로 닫고, awaiting 응답의 external-event read skew와 degraded terminal DB 제약을 교정했다. prompt 1.2.0 합성 gold 방향 정확도 9/9, 최종 실패 0, 근거 recall 88.9%. 상세: ops/evidence/alliance-pulse-pre-mid-post-live-2026-08-07.json, ops/outcome-os-g1-g4-g5-g7-self-directed-integration-evidence-2026-08-07.md.

+ +
구현

회기 pulse, legacy alliance 격리, 자기평가 선잠금, 독립 모델 실행·재시도, 학습자 관점차 리뷰, 교수자 append-only 재평가를 구축했다. 실제 Session은 첫 발화 전 pre를 fail-closed로 잠그고 첫 왕복 뒤 흐름을 막지 않는 mid 30초 점검을 제공한다. AOS-005~010 완료.

종료 증거

focused backend 43 passed, live PostgreSQL reveal/RLS/audit/supersession smoke, desktop/mobile Alliance Pulse 6/6와 기존 회기 리뷰 12/12 통과. 동일 payload stable ID, 변경 payload 409, 타 학습자·교차 cohort 404와 pre/mid desktop·mobile 2/2·고정 캡처 4장을 추가 검증했다. 최신 source API/dev DB는 한 회기의 pre(degraded/insufficient_transcript) → mid(ready) → post(ready)를 실제 turn UUID·ended_at으로 닫고, awaiting 응답의 external-event read skew와 degraded terminal DB 제약을 교정했다. prompt 1.2.0 read-skew 재검증은 prediction 24/24 ready·비교 대상 9/9·방향 9/9·오류 0, 근거 recall 94.4%·precision 73.9%다. 상세: ops/evidence/alliance-calibration-comparison-prompt-1.2.0-readskewfix-2026-08-07.json, ops/evidence/alliance-pulse-pre-mid-post-live-2026-08-07.json.

@@ -879,7 +897,7 @@
-
구현

원자적 practice prescription, 5개 연습 모드, competency graph·attempt evidence, 약점/망각 우선 선택, append-only PostgreSQL/API·RLS·내부 토큰, 학습자/교수자 리뷰 UI를 연결했다. ready 회기말 평가 저장 뒤 production session_learning_producer가 durable turn intent deviation을 실제 처방으로 변환한다. 학습자 CTA는 처방·원본 회기·성공 기준을 학습 홈→시작 전 브리핑→실제 새 세션 URL까지 보존한다.

종료 증거

실제 API/DB/엔진 smoke는 prescriptions 2·episodes 6·attempts 6·append-only correction 1·ready observer model-run 2를 통과했다. post-merge disposable clone의 실제 브라우저는 route mock 0으로 desktop 2/2·mobile 2/2, returned 카드 우선·G4 POST·read-model reload·before/after·멱등 retry를 확인했고 최종 episode/attempt/ready observer 각 1, 중복·UUID 노출·마이크 호출·overflow 0이다. 전체 API 현재 898 passed. 상세: ops/evidence/scratch-browser-returned-practice-closed-loop-evidence-20260807.json, ops/outcome-os-g4-g5-production-caller-evidence-2026-08-07.md.

+
구현

원자적 practice prescription, 5개 연습 모드, competency graph·attempt evidence, 약점/망각 우선 선택, append-only PostgreSQL/API·RLS·내부 토큰, 학습자/교수자 리뷰 UI를 연결했다. ready 회기말 평가 저장 뒤 production session_learning_producer가 durable turn intent deviation을 실제 처방으로 변환한다. 학습자 CTA는 처방·원본 회기·성공 기준을 학습 홈→시작 전 브리핑→실제 새 세션 URL까지 보존한다.

종료 증거

실제 API/DB/엔진 smoke는 prescriptions 2·episodes 6·attempts 6·append-only correction 1·ready observer model-run 2를 통과했다. post-merge disposable clone의 실제 브라우저는 route mock 0으로 desktop 2/2·mobile 2/2, returned 카드 우선·G4 POST·read-model reload·before/after·멱등 retry를 확인했고 최종 episode/attempt/ready observer 각 1, 중복·UUID 노출·마이크 호출·overflow 0이다. 전체 API 현재 921 passed. 상세: ops/evidence/scratch-browser-returned-practice-closed-loop-evidence-20260807.json, ops/outcome-os-g4-g5-production-caller-evidence-2026-08-07.md.

@@ -890,12 +908,12 @@
구현

append-only attention queue·원장 pointer, metadata-only 교수자/AI 불일치 dataset·audit, cohort gap, version/subgroup drift, 4-domain Phase 3 manifest와 역할 안전 교수자·연구 UX를 연결했다. scheduled producer가 G0~G5 실제 원장에서 attention/gap을 파생하고 repo-approved synthetic version comparison을 별도 research 트랜잭션으로 실행한다.

종료 증거

기존 6-signal queue·3-click·네 도메인 manifest·실제 API/DB/RLS/audit·version/subgroup 증거에 더해 post-merge disposable clone의 current source에서 safety event와 deterioration 경쟁 신호를 실제 derive했다. 결과는 safety_boundary(1) > deterioration(2), direct pointer 각 1, source detail canary 1·G6 projection canary 0, raw transcript/임상 주장 0이고 replay가 멱등이다. 상세: ops/evidence/g6-safety-priority-runtime-scratch-20260807.json, ops/outcome-os-g6-version-comparison-producer-evidence-2026-08-07.md.

- -
내부 구현 DONE

consent→단일 clock→독립 text/voice/fusion→철회·tombstone, streaming interim/final·word timestamp·provider event, HMAC word pseudonym, 열린 stream의 1초 동의 재검사·abort, 텍스트 보존·음성 재연결 UX를 연결했다. 격리 synthetic PCM soak는 59/59 연결 종료·성공 발화 55·철회 차단 2·미동의 차단 1·overflow 거부/복구 1/1·10MiB 선초과 거부를 통과했고, G7 focused 119 passed·Ruff·compile을 통과했다. 공개 WSS preflight는 TLS 1.3/Cloudflare 연결과 비인증 1008 차단, 마이크 열거·캡처 0을 확인했다. 증거: ops/evidence/g7-public-wss-preflight-current-20260807.json.

외부 종료 GATE

실제 Deepgram 운영 key·quota와 live interim/final, 기대값과 일치하는 authenticated public WSS ready, 실행 직전 명시 동의 물리 마이크 50분 양방향 soak, 같은 public host·겹치는 시간창의 worker/Uvicorn queue와 exact image·cgroup/proc/Docker/TCP high-water, 30명/50회기/150축 독립 라벨 held-out voice-gain이 필요하다. 네 artifact가 scripts/check-g7-external-proof.py exit 0을 만들기 전에는 메인 상태를 DONE으로 바꾸지 않는다. 현재 canonical checker exit 1은 이 외부 증거 부재를 정직하게 차단한 결과다.

+ +
내부 구현 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.

current NAS runtime

release-only SHA 6030a677…c611를 전용 NAS Compose 프로젝트에 승격했다. API/Web 이미지 52e0…8b2d/6fdb…f215, health 3/3·db/engine true, OpenAPI 126·auth 401·G0~G8, browser E2E 108/108과 실제 SSE→DB-backed review를 확인했다. named DB volume과 predeploy dump를 보존했다.

실제 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.

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건이 실패했다. 소스의 randomUuid() 수정은 source-only Vite 6/6·typecheck·build를 통과했다. clean-head controller도 HEAD/tree/archive 2회 결정성·archive-only candidate·patch 무회귀와 commit 전 actual-origin 11-spec rollback gate를 unit 30/30로 고정했고 새 predeploy dump 72698232…5d41c를 보존했다. 두 execute는 모두 승격 전 fail-closed되어 mutation 0이다. 후속으로 UTC timestamp·KST 날짜 계약을 구현해 API 전체 921 passed와 KST 실DB browser 코칭 이력 1/1을 통과했다. 사용자 승인 아래 새 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). 상세: 런북 · 기계 판독 증거.

@@ -917,9 +935,9 @@
Phase 0closing

기반 정렬 / 운영 안전장치

인프라, 인증 경계, fail-closed, 공개 배포, 회귀 테스트 안정화.

  • 상태: 대부분 완료, 공개 /turn proof만 남음
  • 검증: build + backend 178 기준선 + Playwright 113 기준선, Node/backend focused 120
  • 운영: prod API 8001, engine 9099, tunnel 1개
Phase 1next

MVP 코어 루프

1 페르소나 상담, 실제 턴 저장, 회기말 피드백, 저항 엔진 검증.

  • DoD: 로그인 → P1 상담 완주 → 피드백 수신
  • 추가: 음성 왕복 1턴 PoC 포함
  • 증거: cost/turn DB 적재 + openness 곡선 대조
Phase 2aplanned

3역할 / OAuth / 페르소나

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

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

음성 캐스케이드

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

  • DoD: 50분 WSS 끊김 실측
  • 범위: Deepgram STT, OpenAI TTS, voice preset
  • 리스크: 실시간 레이턴시는 실측 전 완료 표기 금지
+
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 proof

성과·동맹·수련 폐루프

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

  • 상태: G0~G6 internal DONE · G8 실제 NAS rollback receipt DONE · G7 외부 voice/resource/human 대기
  • 완료 DAG: AOS-001~012
  • 운영: current source NAS SHA 6030a677…c611 승격 완료 · G8 loopback control-plane rollback/restore receipt 2건 · G7 canonical four-artifact checker 준비
+
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 필요
정정 기준: MASTERPLAN 원본의 IRB 신청/승인 게이트는 MASTERPLAN_REVISIONS 기준으로 제거한다. Phase 2는 2a(3역할/OAuth/페르소나)와 2b(음성 캐스케이드)로 분리하고, Phase 1 DoD에는 얇은 음성 왕복 1턴 PoC를 포함한다.
@@ -1006,7 +1024,7 @@
DONE
OpenAI TTS voice preset 추상화

P1/P2/P3 persona voice preset, invalid preset fallback, speed clamp, instructions payload 계약을 회귀화했고, app.persona_voice_map을 live /voice/ws TTS 선택에 연결했다. 명시 preset은 DB map보다 우선하고, OpenAI가 아닌 provider row는 기존 fallback으로 안전하게 흡수한다.

산출물

resolve_voice, resolve_voice_from_map, build_tts_payload, get_session_voice_map, seed voice map row, dev runtime schema guard

검증

pytest app/test_runtime_policy.py app/test_persona_review.py app/test_voice_service.py app/test_voice_ws.py -q 75 passed; full API 178 passed + gateway 11 passed.

POC DONE
P1 서연 무참조 synthetic voice 샘플

Higgs v3 로컬 런타임에서 실존 reference voice 없이 synthetic seed를 먼저 만들고, 그 seed만 재참조해 우울·피로·불안·라포·회복감 5개 변주를 생성했다. 개발용 VIGNETTE_VOICE_POC_SAMPLE_TTS 플래그로 /voice/ws P1 sample TTS provider에도 연결했다.

산출물

docs/archive/voice-art/bimec-persona-voice-poc-2026-06-27.md, docs/voice-art/p1-seoyeon-higgs-v3-20260627/manifest.json, apps/api/app/services/voice.py

검증

mp3/wav 6개(seed+5) 생성, 24kHz, 7.72~11.68초. live /voice/ws text_turn smoke: provider p1-sample-poc, binary 20 chunks/78,573 bytes. 샘플 provider는 TTS override만 담당하고 마이크/STT는 OpenAI 키가 필요하다. 프로덕션 탑재는 모델 라이선스·권리정책 결정 전 금지.

LOCAL DONE
barge-in / paralinguistic logging

음성 learner turn에 audio_ref, silence_ms, speech_rate, barge_in을 nullable 메타로 저장하고, 회기 리뷰에서 침묵·발화 속도·끼어듦·음성 입력 비언어 칩으로 표시한다. 2차로 provider_events JSONB 보존 슬롯을 추가해 control/STT provider 이벤트를 allowlist·size limit 후 저장하고, 3차로 내부 taxonomy event_type/category를 붙인다. 4차로 인증된 회기 리뷰에는 한숨·울음·웃음·호흡·운율·배경소음 같은 정규화 이벤트만 label/detail 칩으로 제한 노출한다. 실제 Session 마이크 UI는 browser voice activity/trailing silence 메타를 audio_end에 싣고, readiness는 음성 메타 5개 컬럼 전체를 확인한다. raw transcript/text/provider/source/raw type은 응답에서 제외하고, 공개 공유 카드는 축어록과 provider raw를 계속 싣지 않는다.

산출물

app.turns.provider_events, TurnRecord.provider_events, voice route 이벤트 sanitizing/taxonomy, ReviewNonverbalEvent, SessionReview 칩 UI, browser voice metadata capture, voice metadata readiness guard

검증

python -X utf8 -m pytest -p no:cacheprovider app/test_runtime_policy.py app/test_session_turn_persistence.py app/test_voice_ws.py app/test_voice_service.py -q 77 passed; provider_events DB insert placeholder/args, WS raw key drop, STT+control event merge, internal taxonomy, client turn 미혼입, review 파생 칩 raw provider 미노출, 침묵 칩 중복 방지, 5-column readiness 검증. PLAYWRIGHT_PORT=5197 npx playwright test e2e/voice-success.spec.ts --project=chromium-single-run --workers=1 2 passed, PLAYWRIGHT_PORT=5199 npx playwright test e2e/session-persistence.spec.ts --project=chromium-single-run --workers=1 5 passed, npm run typecheck.

-
GATE
공개·실기기 음성 장시간 실측

Deepgram adapter, HMAC word pseudonym, 1초 동의 재검사, 공통 consent transaction lock, bounded queue/10MiB cap, recovery UX와 provider/model ready 계약은 내부 완료다. 남은 범위는 실제 Deepgram key·quota live, 기대 provider/model authenticated public WSS, 명시 동의 물리 마이크, 50분 양방향 soak, 독립 라벨 voice-gain benchmark, 실제 운영 topology resource high-water와 provider 기반 한숨·울음 표본이다. mic-free preflight와 synthetic E2E/label은 이를 대체하지 않는다.

산출물

expected provider/model public WSS ready log, Deepgram interim/final 표본, explicit-consent physical mic capture log, 50분 soak evidence, independently labeled voice-gain report, worker/proxy resource high-water, provider paralinguistic event sample

검증

실제 공개/실기기/외부 provider 환경에서만 완료 가능. 현재는 B2 음성 캐스케이드 live gate로 추적한다.

+
GATE
공개·실기기 음성 장시간 실측

provider-neutral streaming adapter, HMAC word pseudonym, 1초 동의 재검사, 공통 consent transaction lock, bounded queue/10MiB cap, recovery UX와 provider/model ready 계약은 내부 완료다. 남은 범위는 current committed source 기반 public launcher와 local_whisper/melotts exact ready, authenticated public WSS, 명시 동의 물리 마이크, 50분 양방향 soak, 독립 라벨 voice-gain benchmark, 실제 운영 topology resource high-water와 provider 기반 한숨·울음 표본이다. Deepgram/OpenAI는 fallback이며 mic-free preflight와 synthetic E2E/label은 외부 증거를 대체하지 않는다.

산출물

expected provider/model public WSS ready log, local Whisper interim/final 표본, explicit-consent physical mic capture log, 50분 soak evidence, independently labeled voice-gain report, worker/proxy resource high-water, provider paralinguistic event sample

검증

실제 공개/실기기/운영 provider 환경에서만 완료 가능. 현재는 B2 음성 캐스케이드 live gate로 추적한다.

DECIDED
s2s 2차 PoC 판단

소유자 결정(2026-06-30): s2s 2차 PoC 유지. 캐스케이드 1차를 운영 기준으로 두고 voice-s2s-poc.md 비교 PoC를 계속 연다. 최종 채택/폐기는 비교 리포트 이후 재판단한다. 채택 여부 자체는 확정됐고, 비교 PoC 구현·실측이 후속 build로 남는다.

산출물

docs/decisions/voice-s2s-poc.md

검증

transcript, safety, audit, latency, failure, privacy 기준 명시. 실제 비교 실측은 후속.

@@ -1047,7 +1065,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-07 현재 수집은 610 tests / 44 files다. 현 작업트리의 610개 전체 GREEN은 아직 검증 전이며, 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 현재 수집은 614 tests / 44 files다. 현 작업트리의 614개 전체 GREEN은 아직 검증 전이며, current 핵심 fixture는 불변 110 + 최신 dashboard 10의 분할 GREEN이다. 이전 단일 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 완료. @@ -1108,7 +1126,7 @@ Crisis safety gate UIPLAYWRIGHT_PORT=5193 npx playwright test e2e/session-mvp.spec.ts --project=chromium-single-run --workers=1 / PLAYWRIGHT_PORT=5259 npx playwright test e2e/session-mvp.spec.ts --project=chromium-desktop --workers=1 --grep "voice conversation stop"mock SSE safety + done.conversation_stopped와 mock voice reply.conversation_stopped에서 안전 자원 109와 입력 disabled가 유지되고, 빈 client reply를 내담자 응답 없음으로 덮지 않으며 live-coach를 호출하지 않는지 검증한다. 음성 위기 경로는 TTS 종료 이벤트 없이도 socket close를 확인한다. Persona/session review UI E2EPLAYWRIGHT_PORT=5215 npx playwright test e2e/teacher.spec.ts --project=chromium-single-run --workers=19 passed mixed suite; real server-row rendering과 route-fixture UI 흐름을 함께 포함한다. 교수자가 pending persona를 승인하고, pending/recent review detail로 진입하며, 별도 student analysis 메뉴·학습자 검색·행 펼침·full learner timeline drilldown을 검증한다. Voice UI synthetic E2Enpx playwright test e2e/voice-success.spec.ts --project=chromium-single-run / PLAYWRIGHT_PORT=5248 npx playwright test e2e/session-mvp.spec.ts --project=chromium-single-run --workers=1 --grep "pending voice transcript"voice-success 2 passed + pending transcript failure UI 1 passed; direct WS cascade, Session 텍스트 턴의 persisted reply /voice/speech→OpenAI speech→Web Audio 재생, Session mic button path with synthetic browser audio/Web Audio playback, and final transcript followed by persistence failure becoming a visible 저장 실패 turn instead of a silent successful transcript - G7 explicit consent · external proofpytest app/test_g7_voice_gain_evidence.py app/test_voice_runtime.py ... / pytest scripts/test_g7_external_proof.py scripts/test_g7_runtime_evidence.py scripts/test_g7_topology_evidence.py scripts/test_g7_external_voice_soak.py / voice-success.spec.tsAPI/G7 92 passed, evidence runner/checker·release gate 31 passed, voice UI 2/2. 서버 30일·원음 미보존 consent ledger 성공 전 getUserMedia/voice WS 0회, public v4 interim/final latency, worker/Uvicorn·Linux topology high-water와 independent human-held-out gate를 fail-closed로 고정했다. exact SHA a8c27b0a…1a0d NAS 승격과 clean candidate session E2E 24/24도 통과했다. 실제 마이크·Deepgram·human evidence는 미실행이라 G7 BUILD. + G7 explicit consent · external proofpytest app/test_g7_voice_gain_evidence.py app/test_voice_runtime.py ... / python -m unittest scripts/test_g7_external_proof.py scripts/test_g7_topology_evidence.py ... / voice-success.spec.ts내부 voice/G7 계약과 consent ledger, 동의 전 getUserMedia/voice WS 0회, public v4 interim/final latency, worker/Uvicorn 및 Linux/Windows topology, independent human-held-out gate를 fail-closed로 고정했다. runner/checker/topology는 86/86, public launcher/sidecar는 80/80, G7 통합은 166/166, API voice 통합은 71/71·Ruff·compile을 통과했다. 현재 public API는 Git 없는 냉동 release cwd라 provenance gate를 통과하지 못하고 local_whisper/melotts sidecar도 미기동이다. 실제 명시 동의 마이크·50분·동시 runtime/topology·human evidence가 없어 G7 external GATE다. Voice s2s decision memodocs/decisions/voice-s2s-poc.mdcriteria recorded; keep/drop decision remains owner DECIDE Hanshin data/SSO gatedocs/ops/hanshin-data-governance-gate.mdartifact created; written external evidence still required Hanshin feedback 1차 improvement packdocs/ops/hanshin-feedback-improvement-plan-2026-07-03.md / docs/redteam/hanshin-feedback-plan-redteam-2026-07-03.md / docs/redteam/hanshin-feedback-plan-counter-redteam-2026-07-03.md / backend+web implementation1차 implemented: P1 role mapping + client-only history injection, P2 quality retry/no-save gate + SSE full-response buffer, P3 review display humanization/raw error mapper/fast-deep labels/client feedback label, P4 supervisor scope panel. Verification: backend focused 84 passed, web typecheck and API type check passed, session-review focused 8 passed, layout visual gate 12 passed, session-layout 8 passed. Remaining follow-ups are stored fallback metadata, lower-latency partial stream policy, fast/deep reconciliation, resident namespace/prompt caching, and clinical rubric/CBT wording gates. @@ -1124,9 +1142,11 @@ 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 / 배포 증거 / 기계 판독 증거current SHA 6030a677…c611을 전용 Compose 프로젝트·포트·네트워크·named volume에 승격했다. browser E2E 108/108, health 3/3, OpenAPI 126·auth 401·G0~G8, postdeploy SSE→DB review를 통과했고 predeploy dump와 이전 exact images를 보존했다. 자동화는 ACTIVE이며 material milestone+release gate+배포 SHA 변경 때만 프리뷰를 갱신한다. G7 외부 mic/provider/human 증거는 별도다. - G7 외부 증거 3-artifact 동시 시간창 오케스트레이터scripts/run-g7-external-proof-window.py · 준비 문서soak·runtime·topology 세 캡처는 같은 host의 겹치는 시간창이어야 하는데 지금까지는 운영자가 세 명령을 손으로 맞춰야 했다. 한 명령으로 동시에 띄우고 checker까지 잇도록 묶었다. 게이트는 약화시키지 않았다 — CLI 실증 fail-closed 4종: 동의·장치 없음 physical_microphone_consent_required, human pack 없음 human_voice_gain_pack_required, 50분 미만 production_window_too_short, host 불일치 hosts_must_match, 전부 exit 2. --rehearse는 마이크를 열지 않고 배관만 확인하며 gate_closed는 항상 false다. 회귀 20/20. 선행 조건 발견: 공개 런타임은 119 paths 구버전이라 /admin/voice-runtime이 배포돼 있지 않아 마이크·사람을 준비해도 artifact 2 에서 실패한다. current source 를 공개에 먼저 승격해야 하며, 오케스트레이터가 시작 전 admin_voice_runtime_not_deployed로 막는다(실측 exit 2). 이 판정은 Cloudflare 가 모든 경로를 403(error 1010)으로 주기 때문에 미인증 상태 코드가 아니라 OpenAPI paths로 한다. 그 뒤 남는 것은 물리 마이크 50분독립 human voice-gain pack 둘뿐이다. - 노트북 로컬 음성 스택 — 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한국어가 없어 탈락했다. 이전에는 로컬 STT 경로가 코드에 없었고 interim/final 스트리밍이 Deepgram 전용이었다. Higgs와 같은 loopback 사이드카를 추가해 오디오가 호스트를 벗어나지 않고 외부 STT 키도 필요 없다. 실측: 저장소 합성 시드 8.72초에서 interim 9·final 5·word timestamp 12개 present, 전사는 원문과 한 글자 차이. 회귀 사이드카 37/37, API 전체 908 passed, gateway 58, G7 checker 23, ruff clean. 이 노트북은 CUDA는 보이지만 cuDNN 부재로 GPU 추론이 네이티브 크래시라 디바이스 확인을 자식 프로세스(--self-check)로 분리하고 CPU 폴백을 넣었다. cuDNN 9 설치는 소유자 판단으로 남겼다. MeloTTS는 CPU 정상 상태 RTF 0.27~0.28(실시간 3.6배), POST /tts 200·WAV 350,566 bytes·3.61s, 빈 텍스트 422·미지 경로 404이며, 합성음을 로컬 STT가 완전 일치 전사하는 왕복 검증을 통과했다(사이드카 16/16, API 914 passed). G7 체커의 deepgram 하드코딩도 운영 provider 허용목록으로 교정했고 선언/실제 불일치 차단은 유지한다. Higgs 운영 사용은 라이선스 가드가 남아 dev 전용이다. + 격리 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 증거는 별도다. + 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로 닫는다. + 노트북 로컬 음성 스택 — 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은 여전히 결함 빌드다. 공개 워치독 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 미실행, 공개 런타임 미접촉. @@ -1140,8 +1160,9 @@ Public OAuth start/auth/config + /auth/login?provider=google2026-06-30 복구 후 auth config 200, Google configured true, redirect URI https://api-vignette.chanpaca.net/auth/callback, dev-login disabled. Google login start는 GET 302와 __Host-vignette_oauth_state HttpOnly/Secure cookie를 반환한다. Authenticated public /turn proof는 owner storageState가 필요하다. Persona auth boundaryGET /personas2026-06-30 복구 후 public unauth /personas는 401 not authenticated를 반환한다. Public loginauth.spec.ts --grep public login1 passed - Public runtime scriptsstart/watch/boot-public-runtime*.ps1PowerShell 5.1 parser 오류 0. 관리자·인증 제어면(prod + db)과 엔진 readiness를 분리하고, API 코드 배포는 -ForceApiRestart -SkipEngineRestart -SkipWebRestart -SkipCloudflaredRestart로 API만 교체한다. 2026-07-30 OAuth upsert hotfix 뒤 API PID 40992, local/public status=ok, environment=prod, db=true, engine=true다. - Public API healthhttp://127.0.0.1:8001/health / https://api-vignette.chanpaca.net/health2026-07-30 최종 local/public 확인에서 environment=prod, status=ok, db=true, engine=true, engine_detail=OK다. 관리자·인증 제어면과 엔진 readiness를 분리했으며, 엔진 단독 장애는 API 재시작으로 전파되지 않는다. OAuth config는 200/configured true다. + Public runtime scripts · source pinstart/watch/boot-public-runtime*.ps1 · install-public-runtime-task.ps1 · register-boot-task.ps1PowerShell 5.1 parser 오류 0. detached-clean stable source, Git commit/tree, watchdog|boot/start script SHA를 runtime 검사·mutation 전에 검증하며 hidden VBS는 구조적으로 pin된 task만 trigger한다. 회귀는 provenance 11개를 포함한 combined 22/22·WSH syntax PASS. 실제 task migration은 아직 미완료: watchdog과 로그온 task action이 shared worktree/legacy 인자를 가리켜 최신 5분 watchdog은 mutation 전에 exit하고 LastTaskResult=1이다. 사용자 승인 clean commit 뒤 동일 stable release root로 두 task를 재등록하고 action pin·result 0·로그온/reboot smoke를 확인해야 한다. + Public API healthhttp://127.0.0.1:8001/health / https://api-vignette.chanpaca.net/health2026-08-09 핸드오프 read-only 확인에서 local/public 모두 status=ok, db=true, engine=true다. DB container는 recovered named volume과 unless-stopped를 유지한다. 이는 source-pin task migration이나 G7 current-source 배포 완료 증거가 아니다. + Public runtime 17:54 read-only snapshothealth + OpenAPI + voice + listeners + Scheduled Taskslocal/public health status=ok·db=true·engine=true, 공개 OpenAPI 119 paths, /admin/voice-runtime 없음, /voice/health openai/gpt-4o-transcribe + openai/gpt-4o-mini-tts, local Whisper 9882·MeloTTS 9883 listener 0. watchdog·로그온 task action은 여전히 legacy shared worktree이고 17:54 KST watchdog LastTaskResult=1이다. 이는 필수 pin 인자 부재로 mutation 전 fail-closed한 값이며 G7 current-source 배포 완료가 아니다. Public/local/Tailnet login recoveryhttps://vignette.chanpaca.net/login / https://api-vignette.chanpaca.net/health / https://alpaca-home.taile93291.ts.net/login2026-06-30 public web login HEAD 200, public API health 200, auth config 200, Google login start 302. Earlier Tailnet checks remain recorded separately; vnet DNS A records are still 0. Local 5175 loginPLAYWRIGHT_BASE_URL=http://127.0.0.1:5175 auth.spec.tsdesktop/mobile passed Learner/readiness E2Elearner.spec.ts + readiness.spec.ts desktop/mobile14 passed @@ -1181,7 +1202,7 @@ $env:E2E_PUBLIC_STORAGE_STATE=".\node_modules\.tmp\public-auth.json" powershell -NoProfile -ExecutionPolicy Bypass -File scripts\start-public-runtime.ps1
닫은 항목(live 증거): ① engine config 운영값(claude_cli / 9099 / gateway-default, durable, db). ② 상주 엔진풀 probe — 단일 session_id 2회 reused stream, TTFT 1667–4199ms, cost 누적 0.068→0.123. ③ 상주 엔진풀 RSS — gateway session 1개에서 claude -p child RSS 363.3MB, gateway RSS 16.2MB. ④ Postgres RLS/audit smoke 5 checks PASS. ⑤ turn cost telemetry — app.turns client_ai 13행에 provider/model/cost_usd 실적재(합 $1.22). ⑥ 레이아웃 시각 게이트 최신 9/9 + 2026-06-28 적대적 재검수 7/7 accept.
-
환경 제약으로 이 세션에서 못 닫는 항목(정직 표기): 공개 Google OAuth 실제 /turn(OAuth start는 2026-06-30 재확인 완료, 실제 계정 storageState 필요) · 실배포 infra/.env owner-secret fill-in(APP_DB_PASSWORD, OAUTH_GOOGLE_CLIENT_ID, OAUTH_GOOGLE_CLIENT_SECRET, OPENAI_API_KEY, SESSION_SECRET) · vnet.18ka.net/api-vnet.18ka.net 공개 런타임(DNS A record 0, Cloudflare 18ka.net zone 권한 불일치, Google OAuth redirect URI 등록 필요) · 한신대 데이터/SSO 거버넌스(owner/기관 written evidence 필요) · G7 실제 Deepgram 운영 key·quota live interim/final, 기대 provider/model authenticated public WSS ready, 명시 동의 물리 마이크와 50분 양방향 soak · claude_cli↔Messages API 폴백 동일성 · 실제 Windows 재부팅 후 watchdog smoke · Phase 3 파일럿 게이트. owner 결정 7건은 2026-06-30 전건 확정됨. 운영 원칙상 가짜 증거로 DONE 표기하지 않는다. 전체 추적: docs/ops/backlog-2026-06-26.md(B1 구현 후속 · B2 환경제약 · B3 소유자결정 · B4 거버넌스).
+
환경 제약으로 이 세션에서 못 닫는 항목(정직 표기): 공개 Google OAuth 실제 /turn(OAuth start는 2026-06-30 재확인 완료, 실제 계정 storageState 필요) · 실배포 infra/.env owner-secret fill-in(APP_DB_PASSWORD, OAUTH_GOOGLE_CLIENT_ID, OAUTH_GOOGLE_CLIENT_SECRET, OPENAI_API_KEY, SESSION_SECRET) · vnet.18ka.net/api-vnet.18ka.net 공개 런타임(DNS A record 0, Cloudflare 18ka.net zone 권한 불일치, Google OAuth redirect URI 등록 필요) · 한신대 데이터/SSO 거버넌스(owner/기관 written evidence 필요) · G7 current source 공개 배포, local_whisper/melotts provider/model ready 일치, authenticated public WSS, 명시 동의 물리 마이크와 50분 양방향 soak, 독립 human voice-gain pack · claude_cli↔Messages API 폴백 동일성 · 실제 Windows 재부팅 후 watchdog smoke · Phase 3 파일럿 게이트. owner 결정 7건은 2026-06-30 전건 확정됨. 운영 원칙상 가짜 증거로 DONE 표기하지 않는다. 전체 추적: docs/ops/backlog-2026-06-26.md(B1 구현 후속 · B2 환경제약 · B3 소유자결정 · B4 거버넌스).