1368 lines
524 KiB
HTML
1368 lines
524 KiB
HTML
<!doctype html>
|
||
<html lang="ko">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
<title>Vignette 개발 커맨드센터</title>
|
||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/orioncactus/pretendard/dist/web/variable/pretendardvariable.min.css">
|
||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;700&display=swap" rel="stylesheet">
|
||
<style>
|
||
:root{
|
||
--paper:#FBFAF8; --paper-2:#F1EDE6; --paper-3:#ECE7DE;
|
||
--surface:#FFFFFF; --hair:#E5E1DA; --hair-2:#D8D2C8;
|
||
--ink:#1C2A2A; --ink-2:#566260; --ink-3:#93A09C;
|
||
--accent:#3E7A6E; --accent-deep:#2B5249; --accent-tint:#EBF2F0;
|
||
--clay:#B0735C; --clay-deep:#8F5641; --clay-tint:#F6EBE4;
|
||
--pos:#2C6E49; --warn:#8A5A00; --crit:#A8352C; --plan:#3C7299;
|
||
--pos-bg:#E7F2EB; --warn-bg:#F8EFDD; --crit-bg:#F9E7E4; --plan-bg:#E8F0F6;
|
||
--font:"Pretendard Variable",Pretendard,-apple-system,"Apple SD Gothic Neo","Noto Sans KR",system-ui,sans-serif;
|
||
--mono:"JetBrains Mono",ui-monospace,"SFMono-Regular",monospace;
|
||
}
|
||
*{box-sizing:border-box;}
|
||
html{scroll-padding-top:76px;}
|
||
body{margin:0;background:var(--paper);color:var(--ink);font-family:var(--font);line-height:1.55;-webkit-font-smoothing:antialiased;}
|
||
a{color:var(--accent-deep);}
|
||
/* header */
|
||
header{position:sticky;top:0;z-index:20;display:flex;align-items:center;justify-content:space-between;gap:18px;min-height:56px;padding:0 26px;background:rgba(251,250,248,.9);border-bottom:1px solid var(--hair);backdrop-filter:blur(12px);}
|
||
.brand{display:flex;align-items:center;gap:11px;font-weight:780;letter-spacing:-.01em;}
|
||
.mark{width:24px;height:24px;border:2px solid var(--accent);border-radius:50%;position:relative;flex:none;}
|
||
.mark:after{content:"";position:absolute;left:7px;top:6px;width:6px;height:6px;border-radius:50%;background:var(--clay);}
|
||
.brand small{color:var(--ink-3);font:600 11px/1 var(--mono);letter-spacing:.04em;}
|
||
.head-right{display:flex;align-items:center;gap:12px;}
|
||
.owner-pill{display:inline-flex;align-items:center;gap:8px;padding:6px 13px;border-radius:999px;background:var(--clay-tint);border:1px solid color-mix(in srgb,var(--clay) 34%,var(--hair));color:var(--clay-deep);font:700 12.5px/1 var(--font);text-decoration:none;}
|
||
.owner-pill b{font:800 13px/1 var(--mono);}
|
||
.owner-pill .d{width:8px;height:8px;border-radius:50%;background:var(--clay);}
|
||
.stamp{color:var(--ink-3);font:600 11.5px/1 var(--mono);}
|
||
main{width:min(1240px,calc(100% - 40px));margin:0 auto;padding:30px 0 80px;}
|
||
.eyebrow{display:inline-flex;align-items:center;gap:7px;color:var(--accent);font:700 11px/1 var(--mono);letter-spacing:.12em;text-transform:uppercase;}
|
||
.eyebrow:before{content:"";width:6px;height:6px;border-radius:50%;background:var(--accent);}
|
||
h1{margin:11px 0 0;font-size:27px;line-height:1.25;letter-spacing:-.015em;}
|
||
h1 em{font-style:normal;color:var(--accent-deep);}
|
||
.sec-head{display:flex;align-items:flex-end;justify-content:space-between;gap:16px;margin:0 0 14px;}
|
||
.sec-head h2{margin:0;font-size:18px;line-height:1.3;letter-spacing:-.01em;}
|
||
.sec-head .sub{margin:5px 0 0;color:var(--ink-3);font-size:12.5px;max-width:780px;}
|
||
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{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;}
|
||
.pr-fill{height:100%;border-radius:999px;background:linear-gradient(90deg,var(--accent),var(--accent-deep));}
|
||
.pr-seg.cur .pr-fill{background:linear-gradient(90deg,var(--warn),#a86b00);}
|
||
.pr-seg.idle .pr-fill{background:var(--plan);opacity:.5;}
|
||
.pr-meta{display:flex;justify-content:space-between;align-items:baseline;margin-top:7px;}
|
||
.pr-id{font:700 10.5px/1 var(--mono);color:var(--ink-2);}
|
||
.pr-pct{font:700 11px/1 var(--mono);color:var(--ink-3);}
|
||
.pr-name{display:block;margin-top:3px;color:var(--ink-3);font-size:10.5px;line-height:1.3;}
|
||
.pr-seg.cur .pr-id{color:var(--warn);}
|
||
/* KPI cards */
|
||
.kpis{display:grid;grid-template-columns:1fr 1fr;gap:12px;}
|
||
.kpi{position:relative;background:var(--surface);border:1px solid var(--hair);border-radius:14px;padding:16px 17px;box-shadow:0 1px 2px rgba(28,42,42,.05);display:flex;flex-direction:column;justify-content:space-between;min-height:108px;text-decoration:none;color:inherit;}
|
||
.kpi .lab{display:flex;align-items:center;gap:7px;color:var(--ink-3);font-size:11.5px;font-weight:680;}
|
||
.kpi .lab .dot{width:8px;height:8px;border-radius:50%;}
|
||
.kpi .num{font:800 34px/1 var(--mono);letter-spacing:-.02em;margin-top:auto;}
|
||
.kpi .num small{font:700 14px/1 var(--mono);color:var(--ink-3);margin-left:3px;}
|
||
.kpi .hint{margin-top:5px;color:var(--ink-3);font-size:11px;}
|
||
.kpi.k-done .num{color:var(--pos);} .kpi.k-done .dot{background:var(--pos);}
|
||
.kpi.k-doing .num{color:var(--warn);} .kpi.k-doing .dot{background:var(--warn);}
|
||
.kpi.k-plan .num{color:var(--plan);} .kpi.k-plan .dot{background:var(--plan);}
|
||
.kpi.k-owner{background:linear-gradient(165deg,var(--clay-tint),var(--surface) 62%);border-color:color-mix(in srgb,var(--clay) 32%,var(--hair));}
|
||
.kpi.k-owner .num{color:var(--clay-deep);} .kpi.k-owner .dot{background:var(--clay);} .kpi.k-owner .lab{color:var(--clay-deep);}
|
||
.kpi.k-owner:after{content:"→";position:absolute;right:15px;top:14px;color:var(--clay);font-weight:800;}
|
||
.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;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);}
|
||
.mchip.crit b{color:var(--crit);}
|
||
/* OWNER board */
|
||
.owner-board{background:linear-gradient(180deg,var(--clay-tint),transparent 120px);border:1px solid color-mix(in srgb,var(--clay) 26%,var(--hair));border-radius:16px;padding:20px 22px 22px;}
|
||
.owner-board .sec-head h2{color:var(--clay-deep);}
|
||
.owner-board .sec-head h2:before{content:"";display:inline-block;width:9px;height:9px;border-radius:50%;background:var(--clay);margin-right:9px;vertical-align:middle;}
|
||
.ocols{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:14px;}
|
||
.ocol-h{display:flex;align-items:center;gap:8px;margin:2px 0 11px;font:700 12px/1 var(--font);color:var(--ink-2);}
|
||
.ocol-h .ct{font:700 11px/1 var(--mono);color:var(--clay-deep);background:var(--clay-tint);border:1px solid color-mix(in srgb,var(--clay) 26%,var(--hair));border-radius:999px;padding:2px 7px;}
|
||
.ostack{display:grid;gap:10px;}
|
||
.ocard{background:var(--surface);border:1px solid var(--hair);border-left:3px solid var(--clay);border-radius:11px;padding:13px 14px;box-shadow:0 1px 2px rgba(28,42,42,.04);}
|
||
.ocard .otop{display:flex;align-items:center;gap:7px;margin-bottom:7px;}
|
||
.otag{font:700 9.5px/1 var(--mono);letter-spacing:.05em;text-transform:uppercase;padding:3px 7px;border-radius:5px;}
|
||
.otag.block{background:var(--crit-bg);color:var(--crit);}
|
||
.otag.decide{background:var(--clay-tint);color:var(--clay-deep);}
|
||
.otag.ext{background:var(--plan-bg);color:var(--plan);}
|
||
.oblocks{font-size:10.5px;color:var(--ink-3);}
|
||
.ocard h3{margin:0 0 6px;font-size:13.5px;line-height:1.35;}
|
||
.ocard p{margin:0;color:var(--ink-2);font-size:12px;line-height:1.5;}
|
||
.ocard p+p{margin-top:5px;}
|
||
.ocard .ask{color:var(--ink);}
|
||
.ocard .why{color:var(--clay-deep);}
|
||
.ocard .opt{display:inline-block;margin-top:7px;font:600 11px/1.4 var(--mono);color:var(--ink-2);background:var(--paper-2);border-radius:6px;padding:4px 8px;}
|
||
code{font-family:var(--mono);font-size:11.5px;background:var(--paper-2);padding:1px 5px;border-radius:5px;color:var(--accent-deep);overflow-wrap:anywhere;word-break:break-word;}
|
||
/* GRAPHS */
|
||
.graphs{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1.1fr);gap:18px;}
|
||
.gcard{background:var(--surface);border:1px solid var(--hair);border-radius:14px;padding:18px 20px;box-shadow:0 1px 2px rgba(28,42,42,.05);}
|
||
.gcard h3{margin:0 0 4px;font-size:13.5px;}
|
||
.gcard .gsub{margin:0 0 15px;color:var(--ink-3);font-size:11.5px;}
|
||
.stackbar{display:flex;height:30px;border-radius:8px;overflow:hidden;border:1px solid var(--hair);}
|
||
.stackbar i{display:flex;align-items:center;justify-content:center;font:700 11px/1 var(--mono);color:#fff;min-width:0;transition:flex-basis .4s ease;}
|
||
.sb-done{background:var(--pos);} .sb-doing{background:var(--warn);} .sb-plan{background:var(--plan);}
|
||
.glegend{display:flex;flex-wrap:wrap;gap:14px;margin-top:12px;}
|
||
.glegend span{display:inline-flex;align-items:center;gap:6px;font-size:11.5px;color:var(--ink-2);}
|
||
.glegend i{width:9px;height:9px;border-radius:3px;display:inline-block;}
|
||
.tracks{display:grid;gap:9px;}
|
||
.track{display:grid;grid-template-columns:96px 1fr 46px;align-items:center;gap:10px;}
|
||
.track .tname{font-size:11.5px;color:var(--ink-2);font-weight:650;}
|
||
.track .tbar{height:9px;border-radius:999px;background:var(--paper-3);overflow:hidden;}
|
||
.track .tbar i{display:block;height:100%;border-radius:999px;background:linear-gradient(90deg,var(--accent),var(--accent-deep));}
|
||
.track .tnum{font:700 11px/1 var(--mono);color:var(--ink-3);text-align:right;}
|
||
/* MAP */
|
||
.map-wrap{background:var(--surface);border:1px solid var(--hair);border-radius:16px;padding:16px;box-shadow:0 1px 2px rgba(28,42,42,.05);overflow-x:auto;}
|
||
.map-wrap svg{width:100%;min-width:min(760px,100%);height:auto;display:block;}
|
||
.map-legend{display:flex;flex-wrap:wrap;gap:16px;margin:12px 4px 2px;}
|
||
.map-legend span{display:inline-flex;align-items:center;gap:7px;font-size:11.5px;color:var(--ink-2);}
|
||
.map-legend i{width:13px;height:13px;border-radius:4px;display:inline-block;border:1.5px solid;}
|
||
.ml-owner i{background:var(--clay-tint);border-color:var(--clay);}
|
||
.ml-crit i{background:#fff;border-color:var(--crit);}
|
||
.ml-gate i{background:var(--accent-tint);border-color:var(--accent);}
|
||
/* DESIGN GUIDE */
|
||
.dg-shell{background:linear-gradient(180deg,var(--surface),var(--paper) 100%);border:1px solid var(--hair);border-radius:16px;padding:18px;box-shadow:0 1px 2px rgba(28,42,42,.05);}
|
||
.dg-link{display:inline-flex;align-items:center;justify-content:center;min-height:34px;padding:8px 12px;border:1px solid color-mix(in srgb,var(--accent) 34%,var(--hair));border-radius:999px;background:var(--accent-tint);color:var(--accent-deep);font-size:12px;font-weight:750;text-decoration:none;white-space:nowrap;}
|
||
.dg-note{margin:0 0 14px;padding:12px 13px;border:1px dashed color-mix(in srgb,var(--clay) 38%,var(--hair));border-radius:11px;background:var(--clay-tint);color:var(--clay-deep);font-size:12.5px;line-height:1.55;}
|
||
.dg-principles{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:10px;margin:0 0 16px;}
|
||
.dg-principles div{background:var(--surface);border:1px solid var(--hair);border-radius:12px;padding:12px 13px;}
|
||
.dg-principles b{display:block;margin-bottom:4px;font-size:12.5px;color:var(--ink);}
|
||
.dg-principles span{display:block;color:var(--ink-2);font-size:11.5px;line-height:1.5;}
|
||
.dg-table-wrap{overflow-x:auto;margin:0 0 16px;}
|
||
.dg-table{min-width:min(760px,100%);margin:0;table-layout:fixed;}
|
||
.dg-gallery{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:14px;}
|
||
.dg-card{min-width:0;background:var(--surface);border:1px solid var(--hair);border-radius:13px;overflow:hidden;box-shadow:0 1px 2px rgba(28,42,42,.04);}
|
||
.dg-card img{display:block;width:100%;aspect-ratio:1672/941;object-fit:cover;background:var(--paper-2);border-bottom:1px solid var(--hair);}
|
||
.dg-card-body{padding:12px 13px 13px;}
|
||
.dg-card b{display:block;margin-bottom:4px;font-size:13px;color:var(--ink);}
|
||
.dg-card span{display:block;color:var(--ink-2);font-size:11.5px;line-height:1.45;}
|
||
.dg-card code{display:inline-block;margin-top:8px;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
|
||
/* BOARD */
|
||
.filterbar{display:flex;flex-wrap:wrap;align-items:center;gap:8px;margin:0 0 16px;}
|
||
.filterbar .fl{font:700 10.5px/1 var(--mono);letter-spacing:.08em;text-transform:uppercase;color:var(--ink-3);margin-right:4px;}
|
||
.fbtn{appearance:none;border:1px solid var(--hair);background:var(--surface);color:var(--ink-2);border-radius:999px;padding:6px 13px;font:650 12px/1 var(--font);cursor:pointer;display:inline-flex;align-items:center;gap:6px;}
|
||
.fbtn .fn{font:700 10.5px/1 var(--mono);color:var(--ink-3);}
|
||
.fbtn:hover{background:var(--accent-tint);color:var(--accent-deep);border-color:color-mix(in srgb,var(--accent) 30%,var(--hair));}
|
||
.fbtn[aria-pressed="true"]{background:var(--ink);color:#fff;border-color:var(--ink);}
|
||
.fbtn[aria-pressed="true"] .fn{color:rgba(255,255,255,.7);}
|
||
.fbtn.f-owner[aria-pressed="true"]{background:var(--clay-deep);border-color:var(--clay-deep);}
|
||
.track-group{margin-top:22px;}
|
||
.track-group:first-of-type{margin-top:0;}
|
||
.tg-head{display:flex;align-items:baseline;gap:10px;margin:0 0 11px;}
|
||
.tg-head h3{margin:0;font-size:14.5px;letter-spacing:-.01em;}
|
||
.tg-head .tg-note{color:var(--ink-3);font-size:11.5px;}
|
||
.scards{display:grid;gap:9px;}
|
||
.scard{background:var(--surface);border:1px solid var(--hair);border-radius:12px;overflow:hidden;box-shadow:0 1px 2px rgba(28,42,42,.04);}
|
||
.scard[data-owner="1"]{border-left:3px solid var(--clay);}
|
||
.scard.hide{display:none;}
|
||
.scard-head{width:100%;display:grid;grid-template-columns:auto minmax(0,1fr) auto;align-items:center;gap:13px;padding:13px 15px;background:none;border:0;text-align:left;cursor:pointer;font-family:inherit;}
|
||
.scard-head:hover{background:var(--paper);}
|
||
.scard-head:focus-visible{outline:2px solid var(--accent);outline-offset:-2px;}
|
||
.chip{display:inline-flex;align-items:center;gap:6px;white-space:nowrap;font:700 10.5px/1 var(--mono);letter-spacing:.02em;padding:5px 9px;border-radius:6px;}
|
||
.chip:before{content:"";width:6px;height:6px;border-radius:50%;}
|
||
.c-done{background:var(--pos-bg);color:var(--pos);} .c-done:before{background:var(--pos);}
|
||
.c-doing{background:var(--warn-bg);color:var(--warn);} .c-doing:before{background:var(--warn);}
|
||
.c-plan{background:var(--plan-bg);color:var(--plan);} .c-plan:before{background:var(--plan);}
|
||
.c-block{background:var(--crit-bg);color:var(--crit);} .c-block:before{background:var(--crit);}
|
||
.scard-mid{min-width:0;}
|
||
.scard-title{display:block;font-size:13.5px;font-weight:700;color:var(--ink);line-height:1.4;}
|
||
.scard-title .critbadge{display:inline-block;margin-left:7px;font:800 9px/1 var(--mono);letter-spacing:.06em;color:var(--crit);background:var(--crit-bg);padding:3px 6px;border-radius:5px;vertical-align:middle;}
|
||
.scard-title .ownbadge{display:inline-block;margin-left:7px;font:800 9px/1 var(--mono);letter-spacing:.06em;color:var(--clay-deep);background:var(--clay-tint);padding:3px 6px;border-radius:5px;vertical-align:middle;}
|
||
.scard-sum{display:block;margin-top:3px;color:var(--ink-2);font-size:12px;line-height:1.45;}
|
||
.caret{flex:none;width:9px;height:9px;border-right:2px solid var(--ink-3);border-bottom:2px solid var(--ink-3);transform:rotate(-45deg);transition:transform .18s ease;margin-right:3px;}
|
||
.scard.open .caret{transform:rotate(45deg);}
|
||
.scard-body{display:none;padding:2px 16px 16px 16px;border-top:1px solid var(--hair);}
|
||
.scard.open .scard-body{display:block;}
|
||
.kv{display:grid;grid-template-columns:64px 1fr;gap:12px;padding:11px 0;border-bottom:1px dashed var(--hair);}
|
||
.kv:last-child{border-bottom:0;}
|
||
.kv b{font:700 11px/1.5 var(--mono);color:var(--accent-deep);text-transform:uppercase;letter-spacing:.04em;}
|
||
.kv.k-warn b{color:var(--clay-deep);}
|
||
.kv p{margin:0;color:var(--ink-2);font-size:12.5px;line-height:1.6;}
|
||
.kv p b{font:inherit;color:var(--ink);text-transform:none;letter-spacing:0;}
|
||
/* DEEP (folded reference) + legacy */
|
||
.deep-note{margin:0 0 4px;color:var(--ink-3);font-size:12px;}
|
||
.fold{margin-top:14px;border:1px solid var(--hair);border-radius:14px;background:var(--surface);overflow:hidden;}
|
||
.fold>summary{list-style:none;cursor:pointer;padding:15px 18px;display:flex;align-items:center;gap:12px;font-weight:700;font-size:14px;}
|
||
.fold>summary::-webkit-details-marker{display:none;}
|
||
.fold>summary:hover{background:var(--paper);}
|
||
.fold>summary .fcaret{width:9px;height:9px;border-right:2px solid var(--ink-3);border-bottom:2px solid var(--ink-3);transform:rotate(-45deg);transition:transform .18s ease;}
|
||
.fold[open]>summary .fcaret{transform:rotate(45deg);}
|
||
.fold>summary .fmeta{margin-left:auto;font:650 11px/1 var(--mono);color:var(--ink-3);}
|
||
.fold-inner{padding:6px 18px 20px;border-top:1px solid var(--hair);}
|
||
.grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:14px;}
|
||
.roadmap{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:11px;margin-top:6px;}
|
||
.phase-card{min-width:0;background:var(--paper);border:1px solid var(--hair);border-radius:12px;padding:14px;}
|
||
.phase-card.current{border-color:color-mix(in srgb,var(--accent) 42%,var(--hair));background:linear-gradient(180deg,var(--accent-tint),var(--surface) 58%);}
|
||
.phase-card.next{border-color:color-mix(in srgb,var(--warn) 35%,var(--hair));}
|
||
.phase-top{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:9px;}
|
||
.phase-id{font:760 12px/1 var(--mono);color:var(--accent-deep);}
|
||
.phase-card h3{margin:0;font-size:14px;line-height:1.35;}
|
||
.phase-card p{margin:7px 0 0;color:var(--ink-2);font-size:11.5px;}
|
||
.phase-list{display:grid;gap:5px;margin:11px 0 0;padding:0;list-style:none;}
|
||
.phase-list li{color:var(--ink-2);font-size:11.5px;line-height:1.45;}
|
||
.phase-list li b{color:var(--ink);}
|
||
.tag{display:inline-flex;align-items:center;gap:6px;padding:3px 9px;border-radius:999px;font-size:11px;font-weight:680;}
|
||
.tag:before{content:"";width:7px;height:7px;border-radius:50%;}
|
||
.tag.doing{color:var(--warn);background:var(--warn-bg);} .tag.doing:before{background:var(--warn);}
|
||
.tag.info{color:var(--plan);background:var(--plan-bg);} .tag.info:before{background:var(--plan);}
|
||
.source-note{margin-top:13px;padding:13px 15px;border:1px solid var(--hair);border-radius:11px;background:var(--paper-2);color:var(--ink-2);font-size:12.5px;}
|
||
.tabs{border:1px solid var(--hair);border-radius:12px;background:var(--surface);overflow:hidden;margin-top:10px;}
|
||
.tabbar{display:flex;overflow-x:auto;border-bottom:1px solid var(--hair);background:var(--paper-2);}
|
||
.tab-button{appearance:none;border:0;border-right:1px solid var(--hair);background:transparent;color:var(--ink-2);padding:12px 15px;font:700 12.5px/1.2 var(--font);white-space:nowrap;cursor:pointer;}
|
||
.tab-button:hover{background:rgba(62,122,110,.08);color:var(--ink);}
|
||
.tab-button[aria-selected="true"]{background:var(--surface);color:var(--accent-deep);box-shadow:inset 0 -2px 0 var(--accent);}
|
||
.tab-panel{padding:18px;} .tab-panel[hidden]{display:none;}
|
||
.detail-head{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:16px;align-items:start;margin-bottom:14px;}
|
||
.detail-head h3{margin:0;font-size:16px;} .detail-head p{margin:5px 0 0;color:var(--ink-2);font-size:12.5px;}
|
||
.mini-metrics{display:flex;flex-wrap:wrap;gap:7px;justify-content:flex-end;}
|
||
.mini-metrics span{display:inline-flex;align-items:center;min-height:26px;padding:4px 9px;border:1px solid var(--hair);border-radius:999px;background:var(--paper);color:var(--ink-2);font:650 11px/1 var(--mono);}
|
||
.task-list{display:grid;border:1px solid var(--hair);border-radius:11px;overflow:hidden;background:var(--hair);gap:1px;}
|
||
.task-row{display:grid;grid-template-columns:108px minmax(200px,1fr) minmax(170px,.8fr) minmax(180px,.9fr);background:var(--surface);}
|
||
.task-row.r3{grid-template-columns:108px minmax(220px,1.4fr) minmax(200px,1fr);}
|
||
.task-row>div{padding:12px 13px;min-width:0;}
|
||
.task-row b{display:block;margin-bottom:4px;font-size:12.5px;color:var(--ink);}
|
||
.task-row p{margin:0;color:var(--ink-2);font-size:11.5px;line-height:1.45;}
|
||
.task-status{display:inline-flex;align-items:center;justify-content:center;min-width:72px;min-height:26px;padding:4px 8px;border-radius:999px;font:760 11px/1 var(--mono);}
|
||
.s-done{background:var(--pos-bg);color:var(--pos);} .s-doing{background:var(--warn-bg);color:var(--warn);}
|
||
.s-risk{background:var(--crit-bg);color:var(--crit);} .s-plan{background:var(--plan-bg);color:var(--plan);}
|
||
.detail-blocks{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:11px;margin-top:13px;}
|
||
.detail-block{border:1px solid var(--hair);border-radius:11px;padding:13px;background:var(--paper);}
|
||
.detail-block h4{margin:0 0 7px;font-size:12.5px;} .detail-block ul{margin:0;padding-left:17px;color:var(--ink-2);font-size:11.5px;line-height:1.55;}
|
||
.detail-block li+li{margin-top:4px;}
|
||
.tab-subhead{margin:17px 0 9px;} .tab-subhead h4{margin:0;font-size:13.5px;} .tab-subhead p{margin:4px 0 0;color:var(--ink-3);font-size:11.5px;}
|
||
.commands{display:grid;gap:9px;}
|
||
pre{margin:0;overflow:auto;padding:13px;border-radius:8px;background:#1E2724;color:#F6F0EC;font:11.5px/1.6 var(--mono);}
|
||
table{width:100%;border-collapse:collapse;background:var(--surface);border:1px solid var(--hair);border-radius:11px;overflow:hidden;}
|
||
th,td{padding:10px 12px;border-bottom:1px solid var(--hair);text-align:left;vertical-align:top;font-size:12px;}
|
||
th{color:var(--ink-3);font-weight:700;background:var(--paper-2);}
|
||
tr:last-child td{border-bottom:0;}
|
||
.wide{grid-column:1/-1;}
|
||
@media (max-width:1000px){
|
||
.pulse{grid-template-columns:1fr;}
|
||
.graphs{grid-template-columns:1fr;}
|
||
.ocols{grid-template-columns:1fr;}
|
||
.dg-principles,.dg-gallery{grid-template-columns:repeat(2,minmax(0,1fr));}
|
||
}
|
||
@media (max-width:760px){
|
||
header{padding:0 16px;} main{width:calc(100% - 28px);}
|
||
.kpis{grid-template-columns:1fr 1fr;}
|
||
.grid,.roadmap,.detail-head,.detail-blocks{grid-template-columns:1fr;}
|
||
.dg-principles,.dg-gallery{grid-template-columns:1fr;}
|
||
.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;} }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<header>
|
||
<div class="brand"><span class="mark" aria-hidden="true"></span><span>Vignette 커맨드센터<small>팀장 리뷰 · dev status</small></span></div>
|
||
<div class="head-right">
|
||
<a class="owner-pill" href="#owner"><span class="d" aria-hidden="true"></span>내 차례 <b id="pill-owner">0</b></a>
|
||
<span class="stamp">2026-08-09 KST</span>
|
||
</div>
|
||
</header>
|
||
|
||
<main>
|
||
<!-- ░░ PULSE ░░ -->
|
||
<section class="pulse">
|
||
<div class="pulse-main">
|
||
<span class="eyebrow">project pulse</span>
|
||
<h1>지금 위치: <em>G0~G6 internal DONE</em>.<br><em>G8 runtime 재검증</em>과 <em>G7 external proof</em>를 닫는 중.</h1>
|
||
<p class="pulse-state"><b>현재:</b> 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을 재확인했다. <b>다음:</b> clean commit에서 exact 120 재실행 → NAS-origin 0 failure → G7 공개 음성·물리 마이크·독립 사람 증거 순서로 진행한다.</p>
|
||
<details class="pulse-details">
|
||
<summary>운영 복구·watchdog·배포 상세 보기</summary>
|
||
<p class="pulse-state">2026-08-07 18:26 KST 공개 DB를 <code>vignette_recovered_prod_20260807</code> named volume으로 전환했고 원본 container·dump·volume은 삭제하지 않았다. 기존 auth session은 복사하지 않아 사용자는 Google 재로그인이 필요하다. 2026-08-09 18:02:55 KST Docker Desktop UI가 active DB container에 직접 stop을 보내 public <code>db=false</code>가 재발했지만 같은 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는 다시 <code>status=ok·db=true·engine=true</code>다. 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 인자 부재로 <code>LastTaskResult=1</code>이며 자동복구가 의도적으로 fail-closed다. clean commit 뒤 두 task를 동일 stable release root로 재등록하고 action pin·result 0·로그온/reboot smoke를 확인해야 한다. G8 NAS SHA <code>6030a677…c611</code>은 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다.</p>
|
||
</details>
|
||
<div class="phase-rail" id="phase-rail" aria-label="Phase 진행 추정"></div>
|
||
<div class="mstrip" aria-label="검증 지표">
|
||
<span class="mchip warn">Fixture E2E <b>110 + dashboard 10 분할 GREEN</b> · clean HEAD 120/120 대기</span>
|
||
<span class="mchip good">현재 전체 회귀 · gateway <b>58</b> · backend <b>921</b></span>
|
||
<span class="mchip good">Outcome clean E2E <b>126</b> · visual <b>15/15</b></span>
|
||
<span class="mchip good">Alliance G1 baseline 1.2 <b>9/9</b> · 내부 DONE</span>
|
||
<span class="mchip">Tailnet <b>이전 확인 OK</b></span>
|
||
<span class="mchip good">Public API <b>119 paths · G1~G8</b></span>
|
||
<span class="mchip good">Direct-runtime preflight <b>APP ROLE OK</b></span>
|
||
<span class="mchip crit">critical 3 <b>1차 완료·gate</b></span>
|
||
<span class="mchip">Decision <b>P0 조건부</b></span>
|
||
</div>
|
||
</div>
|
||
<div class="kpis" aria-label="상태 요약">
|
||
<a class="kpi k-done" href="#board"><span class="lab"><span class="dot"></span>완료</span><span class="num" id="kpi-done">0</span><span class="hint" id="kpi-pct">전체의 0%</span></a>
|
||
<a class="kpi k-doing" href="#board"><span class="lab"><span class="dot"></span>진행 중</span><span class="num" id="kpi-doing">0</span><span class="hint">검증/배선 진행</span></a>
|
||
<a class="kpi k-plan" href="#board"><span class="kpi-badge" id="kpi-critbadge" aria-hidden="true"></span><span class="lab"><span class="dot"></span>계획 카드</span><span class="num" id="kpi-plan">0</span><span class="hint" id="kpi-crit">외부 gate는 내 차례에 분리</span></a>
|
||
<a class="kpi k-owner" href="#owner"><span class="lab"><span class="dot"></span>내 차례</span><span class="num" id="kpi-owner">0</span><span class="hint">블로커 2 · 결정 0 · 외부조율 2</span></a>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- ░░ OWNER BOARD ░░ -->
|
||
<section class="block owner-board" id="owner">
|
||
<div class="sec-head">
|
||
<div>
|
||
<h2>당신 차례 — 이걸 풀어주면 진행이 열린다</h2>
|
||
<p class="sub">람다가 단독으로 못 정하거나(월권), 실제 계정·하드웨어·외부 합의가 필요한 항목. 각 카드는 <b>무엇을 막는지 · 무엇이 필요한지 · 왜 당신인지</b>를 담았다. 팀장이 우선순위를 골라 하나씩 풀어주면 된다.</p>
|
||
</div>
|
||
</div>
|
||
<div class="ocols">
|
||
<div class="ocol">
|
||
<div class="ocol-h">🚧 GO를 막는 블로커 <span class="ct" id="ct-block">0</span></div>
|
||
<div class="ostack" data-owner-col="block">
|
||
<article class="ocard" data-owner="1"><div class="otop"><span class="otag block">블로커</span><span class="oblocks">Phase 0 마감 · P1 진입</span></div><h3>공개 Google OAuth 실제 <code>/turn</code> 증명</h3><p class="ask">필요: 로그인 가능한 계정으로 storageState 캡처 → <code>public-auth-turn</code> E2E 1회 통과.</p><p class="why">왜 당신인가: 실제 Google 계정 인터랙티브 로그인이라 자동화 불가. (owner 지시로 현재 보류 상태)</p><span class="opt">증거: public-auth.json + public /turn 성공 로그</span></article>
|
||
<article class="ocard" data-owner="1"><div class="otop"><span class="otag block">게이트</span><span class="oblocks">Phase 3 파일럿 · 데이터</span></div><h3>한신대 데이터/SSO 거버넌스</h3><p class="ask">필요: SSO claim 스키마, 추가 축어록 수급, 미성년 원본 활용동의, 개인정보 처리방침에 대한 한신대/데이터 steward의 written evidence.</p><p class="why">왜 당신인가: 기관 공문·협의 영역. 로컬 문서 골격은 준비됨(<code>hanshin-data-governance-gate.md</code>).</p></article>
|
||
</div>
|
||
</div>
|
||
<div class="ocol">
|
||
<div class="ocol-h">🧭 소유자 결정 (DECIDE) <span class="ct" id="ct-decide">0</span></div>
|
||
<div class="ostack" data-owner-col="decide">
|
||
<article class="ocard" data-owner="0"><div class="otop"><span class="otag decide">결정됨</span><span class="oblocks">아키텍처·일정</span></div><h3>백엔드 언어 방향</h3><p class="ask">소유자 결정: 장기 교체 대상과 신규 분리 서비스는 Node.js 우선. 단, 현재 FastAPI 전면 재작성은 일정 리스크라 계약 우선 strangler 방식으로 간다.</p><p class="why">영향: 팀 역량은 Node로 맞추되, 납품 핵심 경로는 기존 Python 동작을 보존하며 교체 가능한 경계부터 만든다.</p></article>
|
||
<article class="ocard" data-owner="0"><div class="otop"><span class="otag decide">결정됨</span><span class="oblocks">Phase 3 측정</span></div><h3>평가설계 (H1: 실험/통제군·3척도·κ/ICC)</h3><p class="ask">소유자 결정(2026-06-30): 평가자 신뢰도 목표 κ≥0.70 & ICC≥0.75, 환각률 ≤3%, 통계검정은 독립표본 t-검정(α=0.05), 20명을 실험/통제군으로 무작위 배정, 단회기 50분·3척도 pre-post는 doc4대로 유지. 설계 파라미터는 확정됐고, 실제 20명 파일럿 evidence·통계해석은 별도 GATE로 남는다.</p><p class="why">영향: 평가 타당도, Phase 3 측정력. 설계는 확정, 효과성 판정은 파일럿 데이터 이후.</p></article>
|
||
<article class="ocard" data-owner="0"><div class="otop"><span class="otag decide">결정됨</span><span class="oblocks">음성 아키텍처</span></div><h3>s2s 2차 PoC 채택 여부</h3><p class="ask">소유자 결정(2026-06-30): 캐스케이드 1차를 유지하면서 s2s 2차 PoC를 계속 연다. <code>voice-s2s-poc.md</code> 기준표(transcript·safety·audit·latency·failure·privacy)로 비교 리포트를 만든 뒤 최종 채택/폐기를 다시 판단한다.</p><p class="opt">방향: s2s 2차 PoC 유지 → 비교 PoC 구현·실측이 후속</p></article>
|
||
<article class="ocard" data-owner="0"><div class="otop"><span class="otag decide">결정됨</span><span class="oblocks">페르소나 음성</span></div><h3>서비스용 음성 모델·권리 정책</h3><p class="ask">소유자 결정(2026-06-30): 서비스 배포 음성은 상업 라이선스가 명확한 대체 TTS provider로 간다. Higgs 무참조 synthetic 샘플과 dev-only sample provider는 개발 플래그로만 유지하고, 실존 인물/성우 ref 사용 금지를 유지한다.</p><p class="opt">방향: 대체 TTS provider 채택 → provider 선정·통합이 후속</p></article>
|
||
<article class="ocard" data-owner="0"><div class="otop"><span class="otag decide">결정됨</span><span class="oblocks">데이터주권·레이턴시</span></div><h3>fast-loop 외부 API 허용 여부</h3><p class="ask">소유자 결정(2026-06-30): 로컬 상주 분류기를 기본으로 하되 외부 Haiku/Solar 폴백을 허용한다. 외부 전송 시 데이터주권·로그 보존 정책과 동의 범위를 함께 적용한다.</p><p class="opt">방향: 외부 폴백 허용 → 폴백 경로·전송 가드 구현이 후속</p></article>
|
||
<article class="ocard" data-owner="0"><div class="otop"><span class="otag decide">결정됨</span><span class="oblocks">운영 감사·장애 대응</span></div><h3>운영 보존기간·에스컬레이션 정책</h3><p class="ask">소유자 결정(2026-06-30): 헬스 raw 이벤트 90일, 일별 rollup 365일 보존. 우선순위 escalation은 운영자 수동 승인, SLA는 미표기(현 값은 관측 샘플 이력으로 유지), 담당 그룹 자동 배정은 도입하지 않는다.</p><p class="opt">방향: raw 90일/rollup 365일 · 수동 escalation · SLA 미표기 → retention 기본값 적용이 후속</p></article>
|
||
<article class="ocard" data-owner="0"><div class="otop"><span class="otag decide">결정됨</span><span class="oblocks">동의서·데이터셋</span></div><h3>재귀학습 fine-tuning 범위</h3><p class="ask">소유자 결정(2026-06-30): few-shot 자동갱신에 더해 fine-tuning까지 범위에 포함한다. 단 실제 fine-tuning 데이터셋은 동의 범위·축어록 수급 결정이 닫힌 뒤에 구성한다.</p><p class="opt">방향: fine-tuning 포함 → 동의/데이터셋 게이트 충족 후 파이프라인 구축</p></article>
|
||
<article class="ocard" data-owner="0"><div class="otop"><span class="otag decide">결정됨</span><span class="oblocks">안전 정책</span></div><h3>자유연습 기본값</h3><p class="ask">결정: 기본 ON + 피드백 권장 배지 + 교수자 토글. 미동의·미성년 개인정보 gate만 차단한다.</p><p class="opt">근거: MASTERPLAN_REVISIONS F-10 승인</p></article>
|
||
<article class="ocard" data-owner="0"><div class="otop"><span class="otag decide">결정됨</span><span class="oblocks">평가 타당도·다양성</span></div><h3>추가 축어록 수급 규모/일정</h3><p class="ask">소유자 결정(2026-06-30): 추가 사례 20건을 1개월 내 수급한다. n=1 시드 한계를 해소하고 페르소나 다양성·Phase 3 측정력을 확보하기 위한 목표다.</p><p class="why">영향: 평가 타당도, 페르소나 다양성, Phase 3 측정력. 실제 수급·동의·정제는 임상/거버넌스 게이트와 연계.</p></article>
|
||
</div>
|
||
</div>
|
||
<div class="ocol">
|
||
<div class="ocol-h">🤝 외부·임상팀 조율 <span class="ct" id="ct-ext">0</span></div>
|
||
<div class="ostack" data-owner-col="ext">
|
||
<article class="ocard" data-owner="1"><div class="otop"><span class="otag ext">조율</span><span class="oblocks">위기게이트·평가·이론</span></div><h3>임상팀 콘텐츠 (구훈정·어유경)</h3><p class="ask">필요: C1 루브릭 항목·척도·가중치·anchor 예시·컷오프·검수 기준의 임상팀 확정본, CBT 이론별 단계 프롬프트 체인, 위기 스크립트·생명유지서약 문안, H2 평가 골든셋. 코드는 C1 schema/loader/validation scaffold까지만 준비됨.</p><p class="why">왜 당신인가: doc3 R&R상 콘텐츠는 임상팀 산출물. 코드는 '편집 가능 구조'만 선제 구축 → 조율·수급 필요.</p></article>
|
||
<article class="ocard" data-owner="1"><div class="otop"><span class="otag ext">거버넌스</span><span class="oblocks">계약·일정</span></div><h3>스택 정합성·저작권 등재 (L1)</h3><p class="ask">방향은 Node.js 교체 가능성 우선으로 정했다. 남은 일은 FastAPI 유지 사유, 계약 우선 전환 계획, 저작권 등재 문서 반영을 거버넌스 증거로 남기는 것이다.</p><p class="why">권고: <code>docs/decisions/backend-node-transition.md</code> 기준으로 신청서/등재 문서의 스택 설명을 정합화한다.</p></article>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- ░░ GRAPHS ░░ -->
|
||
<section class="block" id="progress">
|
||
<div class="sec-head">
|
||
<div>
|
||
<h2>진행 현황</h2>
|
||
<p class="sub">아래 "상태 보드"의 작업 카드에서 자동 집계. 완료/진행/계획 비율과 트랙별 완료도. (Phase 진행률은 단계별 증거 기반 추정치 — 정밀 수치 아님)</p>
|
||
</div>
|
||
</div>
|
||
<div class="graphs">
|
||
<div class="gcard">
|
||
<h3>상태 분포</h3>
|
||
<p class="gsub">추적 작업 <b id="g-total">0</b>건 기준</p>
|
||
<div class="stackbar" id="stackbar" aria-label="상태 분포 막대"></div>
|
||
<div class="glegend">
|
||
<span><i style="background:var(--pos)"></i>완료 <b id="lg-done" style="font-family:var(--mono)">0</b></span>
|
||
<span><i style="background:var(--warn)"></i>진행 <b id="lg-doing" style="font-family:var(--mono)">0</b></span>
|
||
<span><i style="background:var(--plan)"></i>계획 카드 <b id="lg-plan" style="font-family:var(--mono)">0</b></span>
|
||
</div>
|
||
</div>
|
||
<div class="gcard">
|
||
<h3>트랙별 완료도</h3>
|
||
<p class="gsub">카테고리별 완료 카드 / 전체 카드</p>
|
||
<div class="tracks" id="tracks"></div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- ░░ RELATIONSHIP MAP ░░ -->
|
||
<section class="block" id="map">
|
||
<div class="sec-head">
|
||
<div>
|
||
<h2>관계도 — 무엇이 무엇을 막는가</h2>
|
||
<p class="sub">왼쪽 <b style="color:var(--clay-deep)">내 차례(clay)</b> 노드를 풀면 가운데 게이트/작업이 열리고, 오른쪽 <b style="color:var(--crit)">납품·마일스톤</b>으로 이어진다. 화살표를 따라가면 "내가 지금 풀어야 임계경로가 뚫리는 것"이 보인다.</p>
|
||
</div>
|
||
</div>
|
||
<div class="map-wrap">
|
||
<svg viewBox="0 0 1040 560" role="img" aria-label="의존 관계도: 내 차례 노드가 게이트와 납품을 막는 구조">
|
||
<defs>
|
||
<marker id="arw" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0 0 L10 5 L0 10 z" fill="#9aa6a2"/></marker>
|
||
<marker id="arwc" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0 0 L10 5 L0 10 z" fill="#c79a86"/></marker>
|
||
</defs>
|
||
<!-- column labels -->
|
||
<text x="140" y="24" text-anchor="middle" font-family="JetBrains Mono" font-size="12" font-weight="700" fill="#8F5641">내 차례 (OWNER)</text>
|
||
<text x="520" y="24" text-anchor="middle" font-family="JetBrains Mono" font-size="12" font-weight="700" fill="#2B5249">게이트 · 작업</text>
|
||
<text x="900" y="24" text-anchor="middle" font-family="JetBrains Mono" font-size="12" font-weight="700" fill="#A8352C">납품 · 마일스톤</text>
|
||
<!-- edges (drawn first, under nodes) -->
|
||
<g fill="none" stroke="#c79a86" stroke-width="1.8" marker-end="url(#arwc)">
|
||
<path d="M240 64 C330 64 330 64 420 64"/>
|
||
<path d="M240 154 C330 160 340 168 420 168" stroke-dasharray="5 4"/>
|
||
<path d="M240 270 C330 270 330 270 420 270"/>
|
||
<path d="M240 270 C330 300 340 352 420 360"/>
|
||
<path d="M240 380 C330 410 340 452 420 460"/>
|
||
<path d="M240 466 C420 540 660 548 802 482" stroke-dasharray="5 4"/>
|
||
</g>
|
||
<g fill="none" stroke="#9aa6a2" stroke-width="1.8" marker-end="url(#arw)">
|
||
<path d="M620 64 C710 70 720 96 802 110"/>
|
||
<path d="M620 168 C710 150 720 124 802 116"/>
|
||
<path d="M620 270 C710 285 720 300 802 315"/>
|
||
<path d="M620 360 C710 345 720 330 802 321"/>
|
||
<path d="M620 460 C710 460 720 460 802 460"/>
|
||
<path d="M900 343 L900 432" stroke-dasharray="5 4"/>
|
||
</g>
|
||
<!-- owner nodes (clay) -->
|
||
<g font-family="Pretendard Variable" font-size="13" font-weight="700">
|
||
<g><rect x="40" y="40" width="200" height="48" rx="10" fill="#F6EBE4" stroke="#B0735C" stroke-width="1.6"/><text x="140" y="69" text-anchor="middle" fill="#8F5641">OAuth /turn 증명</text></g>
|
||
<g><rect x="40" y="130" width="200" height="48" rx="10" fill="#F6EBE4" stroke="#B0735C" stroke-width="1.6"/><text x="140" y="159" text-anchor="middle" fill="#8F5641">백엔드 언어 결정</text></g>
|
||
<g><rect x="40" y="246" width="200" height="48" rx="10" fill="#F6EBE4" stroke="#B0735C" stroke-width="1.6"/><text x="140" y="275" text-anchor="middle" fill="#8F5641">임상팀 콘텐츠</text></g>
|
||
<g><rect x="40" y="356" width="200" height="48" rx="10" fill="#F6EBE4" stroke="#B0735C" stroke-width="1.6"/><text x="140" y="385" text-anchor="middle" fill="#8F5641">평가설계 (H1)</text></g>
|
||
<g><rect x="40" y="442" width="200" height="48" rx="10" fill="#F6EBE4" stroke="#B0735C" stroke-width="1.6"/><text x="140" y="471" text-anchor="middle" fill="#8F5641">한신대 거버넌스</text></g>
|
||
</g>
|
||
<!-- mid nodes -->
|
||
<g font-family="Pretendard Variable" font-size="13" font-weight="700">
|
||
<g><rect x="420" y="40" width="200" height="48" rx="10" fill="#EBF2F0" stroke="#3E7A6E" stroke-width="1.6"/><text x="520" y="69" text-anchor="middle" fill="#2B5249">Phase 0 마감</text></g>
|
||
<g><rect x="420" y="144" width="200" height="48" rx="10" fill="#EBF2F0" stroke="#3E7A6E" stroke-width="1.6"/><text x="520" y="173" text-anchor="middle" fill="#2B5249">Phase 1 MVP</text></g>
|
||
<g><rect x="420" y="246" width="200" height="48" rx="10" fill="#fff" stroke="#A8352C" stroke-width="1.8"/><text x="520" y="268" text-anchor="middle" fill="#A8352C">위기게이트 109</text><text x="520" y="284" text-anchor="middle" font-size="10" font-weight="600" fill="#A8352C">1차 구현 · 검증</text></g>
|
||
<g><rect x="420" y="336" width="200" height="48" rx="10" fill="#fff" stroke="#A8352C" stroke-width="1.8"/><text x="520" y="358" text-anchor="middle" fill="#A8352C">교수자 성장 UI</text><text x="520" y="374" text-anchor="middle" font-size="10" font-weight="600" fill="#A8352C">1차 구현 · 검증</text></g>
|
||
<g><rect x="420" y="436" width="200" height="48" rx="10" fill="#EBF2F0" stroke="#3E7A6E" stroke-width="1.6"/><text x="520" y="465" text-anchor="middle" fill="#2B5249">평가 KPI (H1)</text></g>
|
||
</g>
|
||
<!-- right nodes -->
|
||
<g font-family="Pretendard Variable" font-size="13" font-weight="700">
|
||
<g><rect x="800" y="88" width="200" height="48" rx="10" fill="#EBF2F0" stroke="#3E7A6E" stroke-width="1.6"/><text x="900" y="117" text-anchor="middle" fill="#2B5249">Phase 1 GO</text></g>
|
||
<g><rect x="800" y="291" width="200" height="52" rx="10" fill="#F9E7E4" stroke="#A8352C" stroke-width="2"/><text x="900" y="313" text-anchor="middle" fill="#A8352C">납품 필수조건</text><text x="900" y="329" text-anchor="middle" font-size="10" font-weight="600" fill="#A8352C">법적 생존선</text></g>
|
||
<g><rect x="800" y="436" width="200" height="48" rx="10" fill="#E8F0F6" stroke="#3C7299" stroke-width="1.6"/><text x="900" y="465" text-anchor="middle" fill="#3C7299">Phase 3 파일럿</text></g>
|
||
</g>
|
||
</svg>
|
||
<div class="map-legend">
|
||
<span class="ml-owner"><i></i>내 차례 — 윤찬이 풀어야 열림</span>
|
||
<span class="ml-crit"><i></i>납품 필수 — 미구현 임계작업</span>
|
||
<span class="ml-gate"><i></i>게이트·마일스톤</span>
|
||
<span style="color:var(--ink-3);font-size:11px">··· 점선 = 약한/아키텍처 의존</span>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- ░░ DESIGN DIRECTION ░░ -->
|
||
<section class="block" id="design-direction">
|
||
<div class="sec-head">
|
||
<div>
|
||
<h2>디자인 시안 지침 — 기능 추출 기반 이미지 생성</h2>
|
||
<p class="sub">2026-06-27 기준 주요 페이지 기능을 추출해 PC·태블릿·모바일 반응형 시안 7장과 온화한 이미지 요소 보드 2장을 만들었다. 결과물은 전부 PNG 래스터 이미지이며, 구현 확정안이 아니라 레이아웃·톤·이미지 요소 판단 기준이다.</p>
|
||
</div>
|
||
<a class="dg-link" href="design-concepts/prompts/vignette-design-prompts-2026-06-27.md">프롬프트 문서</a>
|
||
</div>
|
||
<div class="dg-shell">
|
||
<p class="dg-note">적용 원칙: 생성 이미지 안의 문구는 참고하지 말고 실제 카피는 코드/기획서 기준으로 다시 쓴다. 세션 아바타는 모든 페르소나에서 기존 SVG 도형 기반 파라미터 리그만 사용하며, 생성 이미지/PSD 래스터 파츠는 보존하되 제품에 연결하지 않는다. 교수·관리자 내비게이션은 현재 <code>AppShell</code>·<code>Sidebar</code> 구조를 유지하고, 배경 일러스트·패턴은 빈 상태·히어로·세션 주변 질감에 소량만 쓴다.</p>
|
||
<p class="dg-note">1차 적용(2026-06-27): 온화한 이미지 요소 보드는 구현 참고용으로만 두고, 앱 배경에는 전용 래스터 텍스처 `apps/web/public/design-elements/clinical-paper-ambient.png`를 연결했다. 보드·타일셋·누끼 시트 형태 이미지는 직접 UI 배경으로 쓰지 않는다.</p>
|
||
<p class="dg-note">2차 적용(2026-06-27): 생성된 관리자/설정/리뷰 시안 기준으로 admin 역할의 상단바·사이드바를 어두운 운영 콘솔 크롬으로 전환했고, 관리자 사용자 관리 행을 조밀한 운영 테이블 밀도로 낮췄다. 회기 리뷰는 헤더·인사이트 카드·피드백 스테이지에 따뜻한 래스터 질감과 낮은 채도 틴트를 적용했다.</p>
|
||
<p class="dg-note">3차 적용(2026-06-27): 상담 세션 시작/진행 화면을 시안 기준의 어두운 상담 스테이지로 재정렬했다. 시작 전 화면은 내담자 무대·브리핑·진행 초점 3영역으로 유지하고, 진행 중 화면은 어두운 캔버스 위에 중앙 스테이지와 자막/컨트롤을 배치한다.</p>
|
||
<p class="dg-note">4차 적용(2026-06-27): 교수 콘솔은 시안의 triage 콘솔 구조에 맞춰 KPI를 전폭 6칸으로 재배치하고, 종료 회기 검토 큐와 최근 회기 기록을 첫 화면 2열 핵심 영역으로 올렸다. 학생별 장기 추이와 전체 회기 타임라인은 별도 학생 분석 메뉴로 분리하고, 교수자 사이드바는 좁은 teal rail로 전환했다.</p>
|
||
<p class="dg-note">5차 적용(2026-06-27): 회기 리뷰는 시안의 좌·중·우 작업대 구조에 맞춰 요약/액션 레일, 감정·흐름 중앙 영역, 우측 인사이트 패널로 재배치했다. 모바일에서는 트랜스크립트를 워크시트보다 먼저 노출하고, 사례개념화 워크시트는 제한 높이의 보조 패널로 낮춰 긴 문서처럼 보이지 않게 했다.</p>
|
||
<p class="dg-note">6차 적용(2026-06-27): 설정 화면은 글로벌 셸을 숨기고 설정 전용 좌측 레일을 주 내비로 승격했다. 계정, AI 운영, 테마, 음성, 알림 카드를 시안의 차분한 설정 워크스페이스 흐름에 맞춰 단일 컬럼으로 재배치했고, 모바일은 상단 고정 섹션 탭으로 모든 항목이 잘리지 않게 정리했다.</p>
|
||
<p class="dg-note">7차 적용(2026-06-27): 로그인 화면은 별도 생성한 래스터 상담실 배경(<code>login-counseling-room.png</code>)을 왼쪽 브랜드 패널에 적용해 시안의 실제 상담실 분위기를 반영했다. 학습자 홈은 모바일 grid 행 계산 때문에 생기던 리스트-상세 간 큰 공백을 제거하고, 회기 준비 보드가 한 흐름으로 이어지게 정리했다.</p>
|
||
<p class="dg-note">8차 적용(2026-06-27): 다크 테마가 라이트 primitive 토큰과 역할 accent에 덮여 흰 섹션이 섞이던 문제를 수정했다. 앱 부트 시 테마를 먼저 적용하고, 다크 모드에서 <code>paper/surface/hair/ink</code> primitive까지 스왑한다. 상담 세션 진행 화면은 테마 토글과 무관하게 어두운 콘솔 패널로 고정했고, 드래그형 종료 제스처는 명시적 <code>회기 종료</code> 버튼 + 확인 다이얼로그로 교체했다. 회기 리뷰도 동일한 다크 카드/축어록/워크시트 재질로 보정했다.</p>
|
||
<p class="dg-note">9차 적용(2026-06-27): 관리자 콘솔 IA를 <code>/admin</code> 운영 홈과 <code>/admin/users</code> 사용자 관리, <code>/admin/access</code> 접근 권한, <code>/admin/tickets</code> 운영 티켓 메뉴로 분리했다. 운영 홈은 실제 API가 있는 교육 진행·온라인 사용자·헬스·AI 비용·서비스 리소스를 우선 표시하고, 티켓/업타임 이력은 <code>app.support_ticket</code>·<code>app.admin_health_event</code> 기반 저장 모델로 연결했다. 세분 그룹 권한 enforcement는 아직 정책 화면이며 가짜 권한 효과를 표시하지 않는다.</p>
|
||
<p class="dg-note">17차 적용(2026-06-28): 운영 홈의 티켓 요약은 <code>resolved</code>/<code>closed</code> 이력을 진행 중 위험으로 세지 않는다. <code>high_priority_count</code>와 미니 티켓 목록은 미해결 티켓 기준이며, E2E 운영 티켓 테스트는 제목·본문에 테스트임을 명시하고 실패 시에도 cleanup patch로 해결 처리한다.</p>
|
||
<p class="dg-note">19차 적용(2026-06-28): <code>/admin/tickets</code>는 현재 범위를 미해결 큐 목록과 상태 변경으로 좁혔다. 정적 분류/인사이트 탭은 제거하고, 카드에 카테고리·신고자·경로·접수/갱신 시각을 표시한다. 해결된 항목은 최근 해결 이력으로 내려간다. 필터·카테고리 큐는 이후 26차에서 처리했고, Claude Recipe headless 자동 처리·이슈 등록은 백로그 <code>운영 티켓 자동 분류·처리 2차</code>로 남겼다.</p>
|
||
<p class="dg-note">26차 적용(2026-06-28): <code>/admin/tickets</code>에 검색, 상태·카테고리·우선순위·담당그룹·정체 필터와 카테고리 quick queue를 추가했다. 서버는 동일 query를 DB에서 처리하고, 티켓 접수 및 관리자 수동 처리 변경은 <code>audit.audit_log</code>에 metadata-only로 남긴다. Claude Recipe 자동 수정 후보, 이슈/PR/작업 스레드 생성은 운영자 승인 전까지 계속 후속으로 둔다.</p>
|
||
<p class="dg-note">28차 적용(2026-06-28): 학습자 Settings에 <code>지원 요청</code> 섹션을 추가했다. <code>GET /users/support-tickets</code>는 현재 사용자 <code>reporter_id</code>의 티켓만 최신순으로 반환하고, 본문·신고자 이메일·신고자 이름은 응답에서 제외한다. Settings는 상태·카테고리·우선순위·담당 그룹·해결 메모만 읽기 전용으로 보여주며, 조회 실패는 설정 전체 로딩을 막지 않는다.</p>
|
||
<p class="dg-note">29차 적용(2026-06-28): <code>/admin/health</code>와 synthetic sampler가 같은 <code>record_admin_health_sample()</code> 경로를 사용한다. <code>scripts/record-admin-health-sample.py</code>는 브라우저 admin 세션 없이 <code>app.admin_health_event</code>에 서비스별 샘플을 append하고, <code>scripts/install-health-sampler-task.ps1</code>는 Windows Scheduled Task로 주기 실행을 설치한다. 최신 로컬 one-shot은 <code>recorded_count=5</code>, <code>status=ok</code>, <code>engine_mode=claude_cli</code>다.</p>
|
||
<p class="dg-note">30차 적용(2026-06-28): 헬스 샘플 이력은 raw <code>app.admin_health_event</code>와 daily <code>app.admin_health_daily_rollup</code>으로 분리했다. <code>scripts/maintain-admin-health-events.py</code>는 <code>--rollup-days</code>/<code>--retention-days</code> 명시 인자를 요구하고 기본 dry-run으로 동작한다. apply 모드는 rollup upsert 후 retention window 밖 raw row만 삭제하며, non-dev apply는 추가 확인 플래그 없이는 막는다. <code>/admin/uptime</code> 요약은 raw+rollup을 중복 없이 합산하고 상세 이벤트 목록은 최근 raw 샘플만 보여준다.</p>
|
||
<p class="dg-note">31차 적용(2026-06-28): 운영 티켓 중복 처리는 자동 병합이 아니라 수동 연결 1차로 닫았다. 사용자 티켓 생성은 결정론적 <code>fingerprint</code>를 저장하고, <code>/admin/tickets</code>는 같은 fingerprint 후보 수·oldest parent 후보·하위 티켓 수를 반환한다. 관리자는 기존 <code>PATCH /admin/tickets/{id}</code>로 <code>parent_ticket_id</code>를 지정/해제하며, self-parent와 cycle은 서버가 거부한다. Settings의 사용자별 티켓 조회에는 fingerprint/parent를 노출하지 않는다.</p>
|
||
<p class="dg-note">27차 적용(2026-06-28): 백엔드 언어 방향은 Node.js 우선 교체 가능성으로 정리하되 FastAPI 전면 재작성은 보류했다. 첫 경계로 <code>app/contracts/engine_gateway.py</code>를 추가해 <code>/v1/generate</code>, <code>/v1/stream</code>, SSE <code>token/done/error</code> 계약을 FastAPI client와 현재 Python gateway가 함께 쓰게 했다. 결정 기록은 <code>docs/decisions/backend-node-transition.md</code>에 둔다.</p>
|
||
<p class="dg-note">32차 적용(2026-06-28): Engine gateway SSE parsing을 <code>app/contracts/engine_gateway.py</code>의 <code>token/done/error</code> wire contract로 이동했다. gateway token payload, done telemetry, error detail 검증은 공유 계약 decoder가 맡고, gateway error는 저장 턴을 만들지 않는 기존 경계를 유지한다. 34차에서 이 raw line 해석 소유권은 <code>EngineClient.stream_packets()</code> 경계로 올려 Node.js gateway 교체를 위한 계약 증거를 강화했다.</p>
|
||
<p class="dg-note">33차 적용(2026-06-28): H1 pre/post evidence 원장을 학습자 <code>SessionReview</code> 화면에 연결했다. 학습자는 자기효능감·기술숙련도·수련만족도 3척도의 사전/사후 1~5 aggregate score를 조회·저장할 수 있고, 화면 문구는 효과 판정·통계 검정이 평가설계 확정 이후 별도 산출임을 명시한다. 구현 중 React 이벤트 값을 상태 updater 안에서 늦게 참조해 앱이 검은 배경만 남는 크래시도 확인해 수정했다.</p>
|
||
<p class="dg-note">34차 적용(2026-06-28): Node.js gateway 교체 가능성을 위해 엔진 스트림 wire parsing 소유권을 <code>EngineClient.stream_packets()</code>로 올렸다. 앱 orchestrator는 raw SSE line이 아니라 <code>EngineGatewaySsePacket</code>만 처리하고, Python gateway의 <code>/v1/generate</code>도 <code>GenerateResponse.model_dump()</code>로 반환해 hand-mirrored dict drift를 줄였다. focused gateway/orchestrator/session 회귀는 46 passed.</p>
|
||
<p class="dg-note">35차 적용(2026-06-28): X2 비용 통제 후속으로 fast/deep evaluator structured 결과에 인메모리 semantic cache 1차를 추가했다. 캐시 키는 canonical evaluator request의 SHA-256이며 원문 prompt·completion은 저장하지 않는다. 성공 파싱 결과만 TTL/entry 제한 안에서 재사용하고, engine error·parse error는 캐시하지 않는다. focused evaluator/runtime/admin 회귀는 40 passed.</p>
|
||
<p class="dg-note">36차 적용(2026-06-28): H1 pre/post 원장에서 Phase 3 evidence root용 <code>02-measures/prepost_measures.csv</code>와 <code>02-measures/kpi_report.json</code>을 생성하는 1차 계산기를 추가했다. participant id는 <code>P3-###</code>로 가명화하고, 3척도 paired normalized mean pre/post/delta, complete/missing pair를 report에 채운다. 공식 효과성 판정·통계검정·실험/통제군 배정은 아직 평가설계 게이트다. focused H1/backend 회귀는 45 passed.</p>
|
||
<p class="dg-note">37차 적용(2026-06-28): H4 한국어 NAME/ORG 마스킹에 합성 fixture 기반 평가 harness를 추가했다. 초기 NAME/ORG baseline은 <code>scripts/evaluate-pii-masking.py</code>로 entity recall·forbidden substring removal·unexpected entity violation을 출력했고, fixture가 잡은 <code>소속</code>/<code>안내</code> NAME 오탐은 stopword로 보정했다. 이 baseline은 62차에서 11-case schema-backed dry-run으로 확장했다.</p>
|
||
<p class="dg-note">38차 적용(2026-06-28): M2 다회기 케이스 아크 2차로 세션 종료 시 마스킹 축어록 기반 fallback <code>session_summary.digest</code>와 <code>case_profile.case_digest</code>, <code>rapport_trajectory</code>, <code>alliance_level</code>을 갱신한다. 다음 회기 seed recall은 <code>case_digest</code>·직전 요약·client-visible <code>pinned_fact</code>를 함께 조립한다. 당시 후속이던 <code>pinned_fact</code> 자동 쓰기는 43차에서 보수적 identity/agreement 범위로 연결했다. focused M2 회귀는 43 passed.</p>
|
||
<p class="dg-note">39차 적용(2026-06-28): X2 evaluator semantic cache 운영 관측을 <code>/admin/usage</code>와 관리자 <code>AI 비용</code> 카드에 노출했다. 응답은 cache key나 prompt/completion 없이 enabled, entries, hits, misses, stores, evictions, requests, hit_rate만 반환한다. 당시 X2/backend focused 40 passed였고, 최신 X2 리포트 라운드는 42 backend passed + CLI sample OK다. API typecheck/build, admin E2E 1 passed.</p>
|
||
<p class="dg-note">40차 적용(2026-06-28): H3 페르소나 draft 생성 프롬프트에 <code>persona-draft-rag@2026-06-28.1</code> bundle pinning을 추가했다. <code>POST /personas/drafts/generate</code>는 engine metadata와 draft <code>source_provenance</code>에 prompt bundle id/version/hash를 남겨 source/chunk 증거와 생성 프롬프트 버전을 함께 추적한다. focused persona review 회귀는 31 passed.</p>
|
||
<p class="dg-note">41차 적용(2026-06-28): X2 장기 비용 추이 1차로 <code>/admin/usage</code>에 일별 <code>daily_cost</code> bucket(day, turns, tokens_in/out, cost_usd)을 추가하고, 관리자 <code>AI 비용</code> 카드에 최근 일별 비용/턴 추이를 표시했다. DB와 dev runtime fallback 모두 같은 shape를 반환한다. 자동 차단·한도 enforcement는 운영 정책 결정이 필요해 후속으로 남긴다. backend 40 passed, API typecheck/build, admin E2E 1 passed.</p>
|
||
<p class="dg-note">42차 적용(2026-06-28): M1 비언어 이벤트 2차로 voice learner turn에 <code>provider_events</code> JSONB 보존 슬롯을 추가했다. WebSocket control/STT provider 이벤트는 allowlist·개수·문자열 길이 제한을 거쳐 <code>app.turns.provider_events</code>에 저장되고, raw transcript/text payload는 보존하지 않는다. 당시 기준으로 review/public API에는 아직 노출하지 않고 기존 <code>nonverbal</code> 칩은 audio/silence/pace/barge-in 파생만 유지했다. focused voice/session 회귀는 44 passed.</p>
|
||
<p class="dg-note">43차 적용(2026-06-28): M2 다회기 메모리 3차로 세션 종료 시 마스킹된 client-visible 발화에서 보수적 <code>pinned_fact</code> 후보를 자동 실적재한다. 1차 범위는 <code>[NAME]</code>/<code>[ORG]</code> identity와 명시적 상담 약속만이며, 관계갈등·위기·임상 추론은 pinning하지 않는다. learner-owned <code>case_profile</code> 기준 RLS insert/update 정책을 추가했고, 다음 회기 recall은 기존 client-visible <code>pinned_fact</code> 읽기 경로를 그대로 사용한다. focused M2 회귀는 60 passed.</p>
|
||
<p class="dg-note">44차 적용(2026-06-28): M2 다회기 메모리 4차로 <code>pinned_fact</code> 삽입 또는 값 변경 시 <code>pinned_fact_history</code>에 append-only 이력을 남긴다. 같은 값 재확인은 history를 늘리지 않고, <code>locked</code> fact는 계속 건드리지 않는다. 이유값은 최초 기록 <code>progression</code>, 값 변경 <code>clarification</code>로 제한했으며, 자동 모순 판정은 아직 후속이다. focused M2 회귀는 62 passed.</p>
|
||
<p class="dg-note">45차 적용(2026-06-28): M1 비언어 이벤트 3차로 저장되는 <code>provider_events</code>에 내부 taxonomy <code>event_type</code>/<code>category</code>를 붙인다. raw provider <code>type</code>/<code>kind</code>/<code>label</code>은 보존하되 sigh/cry/laugh/voice_activity/noise 등은 canonical key로 정규화한다. raw transcript/text payload는 계속 drop했고, 당시 기준으로 review/public API 노출은 하지 않았다. focused voice/session 회귀는 45 passed.</p>
|
||
<p class="dg-note">46차 적용(2026-06-28): C3 이론모드 2차로 세션 시작 전 화면에 <code>humanistic</code>/<code>cbt</code>/<code>integrative</code> 명시 선택 segmented control을 추가했다. 기본값은 기존처럼 페르소나 <code>theory_target</code>에서 잡고, 사용자가 바꾼 값은 <code>POST /sessions</code>의 <code>theory_mode</code>로 전송된다. CBT 체인·임상 문안·이론부합 루브릭은 만들지 않았다. 검증: web typecheck/build, C3 backend focused 31 passed, session-layout desktop/mobile 8 passed, session-mvp 1 passed, layout visual gate 9 passed.</p>
|
||
<p class="dg-note">47차 적용(2026-06-28): H3 seed/version materializer runner 1차로 <code>scripts/materialize-persona-seeds.py</code>를 추가했다. 기본은 DB를 건드리지 않는 dry-run manifest 보고이고, <code>--json</code>은 P1~P7 <code>seed_version</code>/<code>persona_id</code>/<code>source_provenance</code>를 기계 판독 형태로 내보내며, <code>--apply</code>일 때만 DB pool을 초기화한 뒤 기존 idempotent <code>materialize_seed_personas()</code>를 호출한다. 당시 후속이던 source pack <code>content_hash</code>/document version 증분 sync runner는 48차에서, 원문 격리 정책 1차는 55차에서 처리했다. 검증: runner py_compile, <code>py -3.11 scripts\materialize-persona-seeds.py --json</code> dry-run <code>manifest_count=7</code>, focused persona review 33 passed.</p>
|
||
<p class="dg-note">48차 적용(2026-06-28): H3/H2 repo-managed source pack version sync runner 1차로 <code>app.services.source_pack_sync</code>와 <code>scripts/sync-persona-sources.py</code>를 추가했다. 기존 <code>POST /kb/live-coach/source-packs/sync</code>도 같은 service를 사용하며, active <code>kb.document</code>의 <code>content_hash</code>가 같으면 skip하고 다르면 <code>previous_version + 1</code>로 색인해 <code>UNIQUE(source_id, doc_uri, version)</code> 충돌을 피한다. dry-run은 DB 상태를 읽되 write하지 않고, <code>--apply</code>에서만 <code>kb.source</code> upsert와 <code>kb.document/kb.chunk</code> 색인을 수행한다. 당시 남은 원문 격리 정책 1차는 55차에서 처리했고, 임상팀 최종 검수 evidence는 여전히 후속이다. 검증: source sync py_compile, <code>py -3.11 scripts\sync-persona-sources.py --help</code>, <code>test_live_coach_sources.py</code> 4 passed.</p>
|
||
<p class="dg-note">49차 적용(2026-06-28): M1 비언어 이벤트 4차로 인증된 회기 리뷰의 <code>nonverbal</code> 칩에 정규화된 provider taxonomy 일부를 제한 노출한다. <code>sigh</code>/<code>cry</code>/<code>laugh</code>/<code>breath</code>, prosody, background noise 같은 <code>event_type</code>은 한글 label/detail로 파생하고, provider/source/raw type/text/transcript는 응답에 싣지 않는다. 공개 공유 카드도 축어록과 provider raw를 계속 제외한다. 검증: M1 backend focused 45 passed, <code>npm run check:api-types</code>, <code>npm run typecheck</code>, <code>npm run build</code>, <code>session-review.spec.ts</code> 3 passed.</p>
|
||
<p class="dg-note">50차 적용(2026-06-28): M2 pinned fact 모순 처리 1차로 명시적 상담 약속 철회/부정만 <code>agreement:counseling</code> 기존 fact의 <code>status='contradicted'</code>로 격리한다. 새 contradicted fact를 임의 생성하지 않고, non-locked 기존 fact가 있을 때만 업데이트하며 history reason은 <code>contradiction</code>으로 남긴다. 관계갈등·위기·임상 추론과 값 다름 일반 케이스는 계속 자동 모순 처리하지 않는다. 당시 검증: <code>test_session_memory.py</code> 11 passed, M2 focused 64 passed.</p>
|
||
<p class="dg-note">51차 적용(2026-06-28): L1 Node.js 전환 계약 증거 2차로 <code>apps/api/engine_gateway/golden/engine_gateway_contract.v1.json</code> golden fixture와 <code>engine_gateway_schema.v1.json</code> schema artifact를 추가했다. fixture는 <code>/v1/generate</code> request/response, <code>/v1/stream</code> token/done/error frame과 decoded packet, provider <code>[DONE]</code> compatibility line을 고정한다. schema는 Node.js gateway가 Python import 없이 검증할 request/response/event shape를 고정한다. Python 계약 테스트가 schema drift와 fixture validation을 같이 보므로, 미래 Node.js gateway는 Python JSON whitespace가 아니라 같은 artifact의 의미론적 packet 동등성으로 conformance를 증명하면 된다. 당시 검증: gateway contract 18 passed, L1 focused 49 passed.</p>
|
||
<p class="dg-note">52차 적용(2026-06-28): L1 Node.js 전환 계약 증거 3차로 <code>scripts/check-engine-gateway-contract.mjs</code> conformance runner를 추가했다. 이 runner는 Python import 없이 golden fixture와 schema artifact를 읽고, generate request/response shape, stream <code>token/done/error</code> frame decoding, provider <code>[DONE]</code> compatibility line 무시를 검증한다. <code>engine_gateway/test_gateway_model.py</code>도 Node runner를 subprocess로 호출해 Python 계약 테스트와 Node 계약 검증이 같은 artifact를 공유하도록 했다. 검증: <code>node scripts/check-engine-gateway-contract.mjs --json</code>, gateway contract 19 passed, L1 focused 50 passed.</p>
|
||
<p class="dg-note">53차 적용(2026-06-28): M2 episodic embedding writer 1차로 세션 종료 저장 성공 뒤 <code>_write_episodic_embeddings()</code> background task를 예약한다. writer는 <code>speaker='client'</code>, <code>turn_id</code> 있음, <code>client</code> visible, <code>text_masked</code> nonempty 조건만 통과시키고 원문 <code>text</code>, 상담자 발화, evaluator-only turn, 평가 payload는 색인하지 않는다. <code>app.turn_embedding</code> 쓰기는 BGE-M3 dense/sparse를 <code>ON CONFLICT (turn_id) DO NOTHING</code>으로 idempotent 저장하며, DB/pgvector/embedder 미구성은 종료 저장을 실패시키지 않는다. 검증: <code>test_session_memory.py</code> 14 passed, M2 focused 67 passed.</p>
|
||
<p class="dg-note">54차 적용(2026-06-28): X2 모델별 비용 리포트 1차로 <code>app.services.usage_report</code>와 <code>scripts/report-ai-usage.py</code>를 추가했다. 기존 <code>/admin/usage</code> JSON을 입력으로 provider/model별 cost share, token share, cost/turn, cost/1k tokens, metered coverage, budget/cache warning을 산출하되 자동 차단이나 한도 enforcement 정책은 만들지 않는다. 검증: usage report 2 tests, X2 focused 42 passed, CLI sample report schema <code>vignette.ai_usage_model_cost_report.v1</code> 생성.</p>
|
||
<p class="dg-note">55차 적용(2026-06-28): H3 원문 격리본 정책 1차로 <code>kb.raw_source_artifact</code> hash-only 레코드를 추가하고, <code>POST /personas/sources</code>가 raw 원문 hash와 sanitized RAG source를 분리해 기록하게 했다. <code>rag.index_document()</code>는 <code>sensitivity=3</code> 또는 raw source marker 청크를 DB 접근 전에 <code>IndexPolicyViolation</code>으로 차단하므로 raw 원문은 <code>kb.chunk</code>/embedding/FTS로 들어가지 않는다. 암호화 blob/vault 저장은 후속. 검증: raw isolation py_compile, H3 focused 38 passed.</p>
|
||
<p class="dg-note">56차 적용(2026-06-28): H2 회기 리뷰 workbench 1차로 학습자 <code>SessionReview</code> 데스크톱 레이아웃을 축어록 타임라인과 요약·감정·흐름·루브릭·강점·개선점·pre/post·워크시트·피드백 작업영역으로 재배치했다. 교수자 검토 화면과 모바일/태블릿 반응형은 기존 규칙을 유지한다. 검증: web typecheck, <code>session-review.spec.ts</code> 3 passed, <code>layout-visual-gate.spec.ts</code> 9 passed, 1440 desktop screenshot 직접 확인.</p>
|
||
<p class="dg-note">57차 적용(2026-06-28): L1 Node.js 전환 준비 4차로 브라우저-facing 세션 read-model을 <code>app/session_read_model.py</code>로 분리했다. <code>routes/sessions.py</code>는 route/auth/RLS DB read/persistence를 유지하고, 세션 목록·대시보드·상세·리뷰·공유 payload DTO와 deterministic builder는 read-model 모듈이 소유한다. OpenAPI field shape와 hidden-turn/evaluator-only filtering, saved worksheet 우선순위, teacher review read-only 경계는 유지했다. 검증: session read-model py_compile, focused backend 48 passed, <code>npm run check:api-types</code>, <code>npm run typecheck</code>.</p>
|
||
<p class="dg-note">58차 적용(2026-06-28): 학습 세션 텍스트 턴은 학습자 발화 버블을 전송 즉시 표시하고, 내담자 응답 전에는 임시 대기 버블만 보여준다. 채팅 입력은 응답 생성 대기 중에만 막고, 음성 TTS 재생 중에는 자막이 먼저 남은 상태에서 텍스트 입력을 유지하며 <code>음성 건너뛰기</code> 버튼으로 재생을 중단할 수 있다. 검증: web typecheck, <code>session-mvp.spec.ts</code> 1 passed, <code>voice-success.spec.ts</code> synthetic UI 1 passed.</p>
|
||
<p class="dg-note">59차 적용(2026-06-28): L1 Node.js 전환 준비 5차로 브라우저-facing 페르소나 DTO와 deterministic mapper를 <code>app/persona_read_model.py</code>로 분리했다. <code>routes/personas.py</code>는 route/auth/teacher-admin gate/DB repository/RAG source 등록/LLM draft generation/error mapping을 유지한다. <code>PersonaSummary</code>, <code>PersonaReviewSummary</code>, <code>PersonaDraftPayload</code>, <code>PersonaDraftDetail</code>, source/evidence 응답 schema 이름과 <code>source/degraded</code> fail-closed catalog 경계는 유지했다. 검증: persona read-model py_compile, <code>test_persona_review.py</code> 33 passed, persona/session-start 55 passed, <code>npm run check:api-types</code>, <code>npm run typecheck</code>.</p>
|
||
<p class="dg-note">60차 적용(2026-06-28): H3 항목형 저작 UI 9차로 PersonaStudio의 자동사고, 회기 시나리오, 말투 목록, 역린·금기 목록을 줄바꿈 textarea에서 행 추가/삭제 UI로 바꿨다. 저장 payload는 기존 배열 schema를 유지하고, 빈 항목은 저장 직전에 제거한다. 검증: <code>npm run typecheck</code>, <code>npm run build</code>, <code>teacher.spec.ts</code> 7 passed, <code>layout-visual-gate.spec.ts</code> 9 passed, <code>session-layout.spec.ts</code> desktop/mobile 8 passed.</p>
|
||
<p class="dg-note">61차 적용(2026-06-28): H3 프롬프트 미리보기 10차로 PersonaStudio 프롬프트 탭의 raw JSON textarea를 제거하고, 같은 draft 데이터를 L1 카드·인적 범주·임상 배경·말투·수치 파라미터·역린·회기 시나리오·추가 계약 섹션으로 검토하게 했다. 런타임 prompt bundle, API 계약, 임상 문안은 바꾸지 않았다. 검증: <code>npm run typecheck</code>, <code>npm run build</code>, <code>npm run check:api-types</code>, <code>teacher.spec.ts</code> 8 passed, 프롬프트 탭 포함 <code>layout-visual-gate.spec.ts</code> 9 passed.</p>
|
||
<p class="dg-note">62차 적용(2026-06-28): H4 마스킹 평가 준비 3차로 합성 fixture를 5건에서 11건으로 확장하고, NAME/ORG 외 PHONE/EMAIL/RRN/NUMID/DATE/MONEY/ADDR/negative-control까지 <code>source</code>/<code>category</code>/<code>severity</code> metadata로 평가한다. <code>pii-masking-eval-input.schema.json</code>과 <code>pii-masking-eval-report.schema.json</code>을 추가했고, 기본 리포트는 <code>masked_text</code> 원문 증거를 빼는 summary-only <code>technical_dry_run</code>이다. 실제 운영 말뭉치 기반 평가는 아직 외부 데이터·reviewer label·법무/guardian evidence gate다. 콘솔의 <code>/favicon.ico</code> 404는 SVG favicon link 추가로 해소했다. 검증: H4 focused 42 passed, CLI fixture 11/11 pass, py_compile.</p>
|
||
<p class="dg-note">63차 적용(2026-06-28): C1 루브릭 외부화 3차로 <code>data/rubrics/case-worksheet-rubric.json</code> scaffold와 Draft 2020-12 schema, <code>app.services.case_worksheet_rubric</code>, <code>scripts/check-case-worksheet-rubric.py</code>를 추가했다. scaffold는 서버 생성 워크시트 5개 section/28개 item key와 맞는지만 검증하고 <code>status=scaffold_only</code>, <code>scoring_enabled=false</code>를 유지한다. 임상팀 확정 criteria/anchor/가중치/컷오프 없이는 AI 채점이나 점수 노출을 켜지 않는다. 검증: rubric focused 4 passed, CLI PASS with sha256 provenance, py_compile.</p>
|
||
<p class="dg-note">64차 적용(2026-06-28): refactor-governance 1차로 C1 생성 워크시트 section/item spec을 <code>CASE_WORKSHEET_SECTION_SPECS</code>와 <code>case_worksheet_template_item_keys()</code>로 올려 루브릭 validator/CLI/test가 더미 <code>ReviewTurn</code> 없이 같은 템플릿 키를 참조하게 했다. API/DB/frontend 응답 shape와 저장본 우선순위는 바꾸지 않았다. 검증: C1 focused 34 passed, rubric CLI PASS, py_compile, residual grep.</p>
|
||
<p class="dg-note">65차 적용(2026-06-28): refactor-governance 2차로 프론트 inline CSS template literal을 제거했다. <code>Login</code>은 <code>pages/login/LoginBrand.tsx</code>, <code>LoginPanel.tsx</code>, <code>login.css</code>로 분리해 인증 상태 로직과 프레젠테이션을 나눴고, <code>PendingApproval</code>, <code>Onboarding</code>, <code>LearnerHome</code>, <code>Professor</code>, <code>PersonaStudio</code>, <code>ClientAvatar</code>의 <code>*_CSS</code> 상수는 인접 CSS 파일 import로 옮겼다. 검증: inline <code><style>{..._CSS}</style></code> residual 0건, <code>npm run typecheck</code>, <code>npm run build</code>, 로그인 Playwright 렌더 스모크.</p>
|
||
<p class="dg-note">66차 적용(2026-06-28): H4 자연 발화형 한국어 이름 마스킹 보강으로 <code>제 이름은 김서연입니다</code>, <code>보호자 이름은 박민수입니다</code>, <code>저는 최하늘입니다</code> 패턴과 <code>이름은 중요하지 않다</code> negative control을 합성 fixture에 추가했다. <code>이름</code> stopword와 자기소개형 이름 패턴을 generic 성씨+조사 패턴보다 앞에 두어 라벨 단어만 마스킹하고 실명을 남기던 구멍을 막았다. 검증: H4 focused 43 passed, CLI fixture 15/15 pass, py_compile.</p>
|
||
<p class="dg-note">67차 적용(2026-06-28): M2 다음 회기 회상 주입 증거를 route-level로 고정했다. REST submit과 SSE stream이 공통 <code>_prepare_turn_context()</code> 경계에서 seed recall, pinned fact, 최근 client-visible 턴, KB cue를 조립하게 했고, 새 테스트가 DB <code>case_digest</code>·직전 <code>session_summary</code>·client-visible <code>pinned_fact</code>가 다음 턴 EngineMessage의 L2/L4 블록에 들어가며 raw 이름은 <code>[NAME]</code>으로 마스킹되는지 검증한다. 검증: M2 focused 68 passed, py_compile.</p>
|
||
<p class="dg-note">68차 적용(2026-06-28): refactor-governance P1 delete-only로 <code>routes/sessions.py</code>의 미사용 <code>_load_prev_case_summary()</code>와 죽은 <code>_LEARNER_VISIBLE_AI_ROLE</code> 상수를 제거했다. 동작 변경 없이 다음 M2 작업 표면을 줄인 정리다. 검증: residual grep 0건, M2 focused 68 passed, py_compile.</p>
|
||
<p class="dg-note">69차 적용(2026-06-28): refactor-governance P3 계약 축소로 리뷰 도식 <code>ReviewPhaseSegment.key/label</code>, <code>SessionReviewResponse.reachedPhase</code>, 평가 조회 <code>EvaluationSummary.stage</code>, 평가 트리거 응답 stage, 교수 콘솔 <code>TeacherSessionSummary</code>/<code>TeacherGrowthPoint</code> stage를 OpenAPI union으로 좁혔다. 공유 카드 <code>reachedPhase</code>와 live-coach event stage는 저장 payload/legacy 호환 검토가 필요해 별도 후속으로 남겼다. 검증: backend focused 50 passed, <code>npm run check:api-types</code>, <code>npm run typecheck</code>, <code>npm run build</code>.</p>
|
||
<p class="dg-note">70차 적용(2026-06-28): H4 마스킹 경계를 refactor-governance 기준으로 좁혀 <code>guardrail.mask_pii()</code> 내부에 선택형 한국어 PII recognizer adapter를 추가했다. adapter는 import-time hard dependency가 아니며 실패 시 예외를 삼키고 기존 regex fallback을 마지막 안전망으로 유지한다. fake adapter 회귀는 regex가 못 잡는 별명/기관 span을 <code>[NAME]</code>/<code>[ORG]</code>로 마스킹하고 같은 문장의 전화번호는 후단 regex가 <code>[PHONE]</code>으로 처리하는지, adapter 실패 시에도 regex fallback이 유지되는지 검증한다. span 중복 처리는 문자별 set 할당 대신 정렬된 <code>last_end</code> 비교로 정리했다. 검증: H4 focused 47 passed, CLI fixture 15/15 pass, py_compile.</p>
|
||
<p class="dg-note">71차 적용(2026-06-28): refactor-governance P3 계약 축소 후속으로 공유 카드 <code>PublicSessionShareResponse.reachedPhase</code>와 <code>LiveCoachEvent.stage</code>도 OpenAPI enum/null 계약으로 좁혔다. 저장된 legacy JSON/TEXT 값은 <code>stage_label_or_none()</code> validator에서 한글 라벨 또는 기존 code key만 살리고, 빈 문자열·임의 문자열은 <code>null</code>로 낮춰 기존 공유 링크·live-coach 이력이 500으로 깨지지 않게 했다. 세션 UI 코칭 이력은 null stage를 <code>단계 미상</code>으로 표시한다. 검증: share/live-coach focused 43 passed, <code>npm run check:api-types</code>, <code>npm run typecheck</code>, <code>npm run build</code>.</p>
|
||
<p class="dg-note">72차 적용(2026-06-28): refactor-governance P3 계약 SSOT 후속으로 stage 라벨과 리뷰 phase key 변환 규칙을 <code>app/stage_contract.py</code>로 분리했다. <code>turn_runtime.py</code>의 legacy code 정규화, <code>session_read_model.py</code>의 <code>StageLabel</code>/<code>ReviewPhaseKey</code>, <code>routes/eval.py</code>의 nullable summary stage membership 체크가 같은 모듈을 참조한다. OpenAPI 응답 shape는 유지하고, enum/legacy/unknown 처리만 한 곳으로 모았다. 검증: stage contract focused 42 passed, <code>npm run check:api-types</code>, <code>npm run typecheck</code>, <code>npm run build</code>.</p>
|
||
<p class="dg-note">73차 적용(2026-06-28): C1 워크시트 교수자 수동 검수 골격을 추가했다. 기존 <code>app.session_review_status</code>에 <code>worksheet_status</code>/<code>worksheet_note</code>/<code>worksheet_reviewed_at</code>을 붙이고, <code>PUT /teacher/sessions/{id}/review-status</code>가 승인·수정요청·반려 결정을 저장한다. <code>/sessions/{id}/review</code>의 <code>teacherReview</code>는 교수자/관리자에게만 이 결정을 노출하고, <code>SessionReview</code> 교수자 카드에는 학습자 저장 워크시트가 있을 때만 수동 검수 버튼을 활성화한다. AI 채점·루브릭 점수·자동 승인·재제출 잠금 정책은 만들지 않았다. 검증: C1 focused 40 passed, API types/typecheck/build, session-review E2E 3 passed.</p>
|
||
<p class="dg-note">74차 적용(2026-06-28): refactor-governance P4로 H2 live-coach source pack 캐시 경계를 명시했다. <code>live_coach</code>의 로컬 source pack snapshot은 API 프로세스 생명주기 캐시로 유지하고, 관리자 <code>POST /kb/live-coach/source-packs/sync</code>와 <code>scripts/sync-persona-sources.py</code>는 <code>refresh=True</code>로 캐시를 비운 뒤 repo 파일을 다시 읽는다. live turn 경로에는 매 턴 파일 IO를 추가하지 않았고, RAG content_hash/version 비교와 evaluator-only payload 정책은 유지했다. 파일 변경 뒤 stale hash가 refresh로 새 hash/version bump에 반영되는 회귀를 추가했다. 검증: source pack focused 9 passed, H2 related 44 passed, sync CLI help, py_compile.</p>
|
||
<p class="dg-note">75차 적용(2026-06-28→29): 다크 UI v2 시안을 실제 코드 적용 라운드로 완료했다. P1 라이브 상담 세션은 <code>Session.tsx</code>/<code>session.css</code> 구조까지 조정해 내담자 무대 발화, 좌측 세션 진행, 우측 라이브 신호/안전 점검을 다크 훈련 화면으로 재구성했다. P2 회기 리뷰는 learner filled 상태를 3열 workbench로 바꿔 요약/흐름, 축어록, 평가 rail, 워크시트를 분리했고 empty review는 2열 이하로 유지했다. P3 학습자 홈은 대시보드에서 연습 대상 레일을 제거하고, 진행 회기/리뷰 대기/최근 평가/라포 흐름을 상단 CTA 바로 올렸으며, 오늘 이어갈 회기·AI 코치·다음 연습 추천·최근 피드백을 하나의 <code>lh-work-cluster</code> 작업 묶음으로 재배치했다. 오른쪽 레일은 최근 기록, 리뷰 대기, 반복 대상만 담당한다. 새 증거 캡처는 <code>docs/design-verification/full-pages/02-learner-home-{desktop,tablet,mobile}.png</code>, <code>03-session-active-{desktop,tablet,mobile}.png</code>, <code>04-session-review-{desktop,tablet,mobile}.png</code>, <code>04-session-review-empty-{desktop,tablet,mobile}.png</code>다. 검증: <code>npm run typecheck</code>, <code>npm run build</code>, <code>layout-visual-gate.spec.ts</code> single-run 9 passed.</p>
|
||
<p class="dg-note">76차 적용(2026-06-28): refactor-governance P2로 M2 digest 계약 경계를 고정했다. <code>SessionDigestInput</code>/<code>SessionDigestResult</code>/<code>SessionSummaryWrite</code>를 추가해 LLM worker 후보가 raw text, evaluator-only turn, 평가 payload, CCD, deterministic carry를 우회하지 못하게 했다. <code>/sessions/{id}/end</code>의 <code>digest_pending</code>은 여전히 CompressionJob 생성 여부를 알리는 비동기 압축 필요 신호일 뿐이며, live provider 압축·운영 품질 검증 완료를 뜻하지 않는다. 검증: M2 focused 74 passed, <code>npm run check:api-types</code>, <code>npm run typecheck</code>, py_compile.</p>
|
||
<p class="dg-note">77차 적용(2026-06-28): H1 Phase 3 KPI report 계약을 <code>phase3_kpi_contract.py</code>로 모아 exporter/checker/test의 metric 이름·필수키·status 중복 선언을 줄였다. <code>kpi_report.json</code> metric은 <code>computed_prepost</code>와 <code>design_pending</code> status를 구분하므로, 3척도 pre/post로 계산 가능한 파일럿 evidence와 평가설계 전이라 계산하지 않는 KPI를 기계적으로 분리한다. 공식 효과성 판정·통계해석·20명 파일럿 evidence는 계속 외부/파일럿 gate다. 검증: H1 KPI focused 10 passed, H1 broad 46 passed, exporter/checker help, py_compile.</p>
|
||
<p class="dg-note">78차 적용(2026-06-29): refactor-governance P3로 L1 Node 전환 준비의 <code>gateway-default</code> 모델 sentinel을 <code>app/contracts/engine_gateway.py</code> 계약 상수와 <code>normalize_engine_gateway_model()</code>로 올렸다. FastAPI <code>EngineClient</code>, 현재 Python gateway, orchestrator stream audit/done fallback, 관리자 기본 engine config가 같은 sentinel을 참조하고, <code>engine_gateway_schema.v1.json</code>과 Node conformance runner도 기본 라우팅 sentinel을 검증한다. <code>gateway-default</code>는 실제 provider 모델명이 아니라 explicit <code>--model</code>을 보내지 않는 gateway 기본 라우팅 신호다. 검증: py_compile, Node conformance OK, 당시 gateway contract focused 20 passed, L1 focused 55 passed, admin/runtime/evaluator focused 40 passed.</p>
|
||
<p class="dg-note">79차 적용(2026-06-29): refactor-governance P3/P8로 <code>GenerateResponse</code> 구조화 JSON fallback을 <code>structured_payload_from_response()</code> 계약 helper로 모았다. evaluator, live-coach, persona draft route가 structured field 우선, fenced JSON, embedded JSON object fallback을 같은 경계에서 처리하므로 Node gateway 교체 시 보존해야 할 legacy response 해석 규칙이 한 곳에 남는다. 학습자 <code>SessionReview</code> 입력은 name/autocomplete/aria-label과 공유 focus token(<code>--border-focus</code>, <code>--focus-ring</code>)으로 정리했고, 발화 이동 버튼에도 목적 발화 aria-label을 추가했다. 검증: py_compile, Node conformance OK, gateway contract 23 passed, backend focused 89 passed, <code>npm run check:api-types</code>, <code>npm run typecheck</code>, <code>npm run build</code>, session-review E2E 3 passed, layout visual gate 9 passed.</p>
|
||
<p class="dg-note">80차 적용(2026-06-29): refactor-governance P3로 H3/L1 페르소나 draft generation 계약을 <code>app/persona_generation_contract.py</code>로 분리했다. 이 모듈이 draft structured schema, prompt bundle id/version/hash, <code>GenerateResponse</code> payload extraction, generated draft coerce/default fallback을 소유한다. <code>routes/personas.py</code>는 teacher/admin auth, source 등록·조회, RAG evidence, engine 호출, provenance 조립, HTTP error mapping을 계속 소유한다. 검증: py_compile, persona contract+review 39 passed, backend focused 95 passed, Node conformance OK, <code>npm run check:api-types</code>, <code>npm run typecheck</code>.</p>
|
||
<p class="dg-note">81차 적용(2026-06-29): refactor-governance P2/P3로 engine structured payload 계약과 session evaluation write packet 경계를 더 줄였다. evaluator/live-coach의 로컬 <code>_structured_payload()</code> alias를 제거해 <code>structured_payload_from_response()</code>를 직접 호출하게 했고, <code>SessionEvaluationWrite</code>가 <code>app.session_evaluation</code> 저장 packet(<code>status/source/scope/stage/payload/error</code>)을 소유한다. <code>routes/sessions.py</code>와 <code>routes/eval.py</code>는 named packet을 만들고, SQL·fallback cache·응답 shape는 유지한다. 검증: py_compile과 당시 focused suite 통과, 이후 최신 L1 focused 120 passed와 Node conformance OK로 재검증.</p>
|
||
<p class="dg-note">82차 적용(2026-06-29): refactor-governance P8로 <code>Session.tsx</code>의 음성/세션 UI 파생 상태 계산을 <code>sessionVoiceStatusView()</code> helper로 분리했다. 마이크 busy/label, 세션 상태 label, transcript live label, 텍스트 입력 차단, 응답/음성 상태 chip 계산을 named view-model 경계에서 유지한다. API/DB 계약, 세션 시작·전송·종료, voice WebSocket/TTS lifecycle, live-coach payload, DOM/CSS는 변경하지 않았다. 검증: <code>npm run typecheck</code>, <code>npm run build</code>, <code>session-mvp</code> 1 passed, <code>voice-success</code> 2 passed, <code>session-layout</code> desktop/mobile 8 passed.</p>
|
||
<p class="dg-note">83차 적용(2026-06-29): refactor-governance P3로 학습자/교수자 성장 지표가 턴 평가 기법 라벨을 읽는 경계를 <code>session_metrics.turn_technique_label()</code>로 명시했다. DB 정규화 후 재수화되는 <code>TechniqueTag.label_ko</code>를 우선 사용하고, 기존 <code>label/name/id/code</code> legacy fallback은 유지한다. <code>alternative_utterances</code>는 future adapter 추출 전에 문자열 배열과 <code>text/suggestion</code> dict 배열을 모두 row로 정규화하는 호환 테스트로 고정했다. 검증: py_compile, evaluation/learner/teacher focused 59 passed.</p>
|
||
<p class="dg-note">84차 적용(2026-06-29): refactor-governance P2로 <code>Session.tsx</code>의 음성 guard/view-model 경계를 한 번 더 줄였다. <code>isVoiceStatusBusy()</code>가 마이크 busy 판정을 소유하고, <code>sessionVoiceStatusView()</code>가 마이크 disabled와 aria label까지 반환해 JSX와 <code>toggleMic()</code>의 중복 조건을 제거했다. <code>recording</code>은 계속 busy가 아니므로 두 번째 마이크 클릭으로 발화 전송이 가능하다. API/DB 계약, WebSocket/TTS lifecycle, STT/TTS provider 동작, live-coach payload, DOM/CSS는 변경하지 않았다. 검증: <code>npm run typecheck</code>, <code>npm run build</code>, <code>session-mvp</code> 1 passed, <code>voice-success</code> 2 passed, <code>session-layout</code> desktop/mobile 8 passed.</p>
|
||
<p class="dg-note">85차 적용(2026-06-29): refactor-governance P3로 Node 전환 계약 검증의 <code>gateway-default</code> sentinel 중복을 줄였다. <code>scripts/check-engine-gateway-contract.mjs</code>는 더 이상 sentinel 문자열을 자체 상수로 들고 있지 않고, <code>engine_gateway_schema.v1.json</code>의 <code>x-engine-gateway-default-model-sentinel</code> extension을 읽어 schema artifact를 SSOT로 검증한다. 공개 런타임 SSOT도 재확인해 custom domain asset을 <code>assets/index-Cz4PBoO3.js</code>/<code>assets/index-DtVTvS9j.css</code>로 갱신했고, OAuth start는 동작하지만 인증된 <code>/turn</code> proof는 여전히 owner storageState gate로 남겼다. <code>vnet.18ka.net</code>/<code>api-vnet.18ka.net</code> DNS A record는 0건이라 DONE 처리하지 않는다.</p>
|
||
<p class="dg-note">86차 적용(2026-06-29): refactor-governance P2/P3로 Session mic capture를 <code>AudioWorklet</code>-first 구조로 전환하고 기존 <code>MediaRecorder</code>를 fallback으로 남겼다. Worklet은 mono PCM16 chunk를 <code>/voice/ws</code> binary frame으로 보내고, control packet은 <code>format=pcm</code>, <code>sample_rate</code>, <code>channels</code>, <code>sample_width</code>를 명시한다. 백엔드는 <code>pcm</code> 업로드를 STT 호출 직전 RIFF/WAV로 정규화해 raw PCM을 <code>audio/wav</code>로 오인하던 계약 틈을 닫았다. 실제 Deepgram streaming, 물리 마이크, public WSS, 50분 양방향 실측은 계속 외부/실측 gate다. 검증: <code>app/test_voice_ws.py</code> 11 passed, <code>npm run typecheck</code>, <code>voice-success</code> chromium-single-run 2 passed.</p>
|
||
<p class="dg-note">87차 적용(2026-06-29): refactor-governance P3로 <code>/voice/ws</code>에 provider-neutral <code>stt_result</code> control을 추가해 EOT helper를 route 경계에 연결했다. 새 control은 <code>text</code>, <code>final</code>, <code>silence_ms</code>, <code>provider_events</code>를 받아 <code>assess_end_of_turn()</code>이 ready를 판단하고, pending이면 턴 실행 없이 <code>state=listening</code>으로 돌아간다. ready일 때만 transcript를 emit하고 <code>_run_turn_and_speak()</code>로 진행한다. 기존 <code>audio_start</code>→binary→<code>audio_end</code>, <code>text_turn</code>, ping/close 계약은 유지했다. 실제 Deepgram interim/final WSS 연결과 public WSS/물리 마이크/50분 실측은 여전히 남은 gate다. 검증: <code>py_compile</code>, <code>app/test_voice_ws.py app/test_voice_service.py</code> 27 passed.</p>
|
||
<p class="dg-note">88차 적용(2026-06-29): refactor-governance P3로 Node conformance runner의 stream event/payload 검증을 schema-driven으로 바꿨다. <code>scripts/check-engine-gateway-contract.mjs</code>는 더 이상 <code>token/done/error</code> 이벤트 목록과 payload 필드를 로컬 상수·분기로 들고 있지 않고, <code>engine_gateway_schema.v1.json</code>의 <code>StreamPacket.oneOf</code>, payload <code>$ref</code>, <code>x-engine-gateway-sse-events</code>를 읽어 golden fixture를 검증한다. raw token text fallback과 <code>data: [DONE]</code> compatibility line은 유지했다. 검증: <code>node scripts\check-engine-gateway-contract.mjs --json</code>, 당시 gateway/backend focused suite 통과, 이후 최신 L1 focused 120 passed로 재검증.</p>
|
||
<p class="dg-note">89차 적용(2026-06-29): refactor-governance P3로 <code>SessionEvaluationWrite</code>에 <code>from_result()</code>/<code>from_error()</code> factory를 추가해 session evaluation 저장 packet 생성 규칙을 한 곳으로 모았다. <code>routes/sessions.py</code>와 <code>routes/eval.py</code>는 더 이상 <code>status/source/scope/stage/payload/error</code> 묶음을 직접 조립하지 않는다. 성공·평가오류는 <code>result.to_dict()</code>와 <code>status="ready|error"</code> 규칙을 유지하고, 실행 예외 fallback은 기존처럼 <code>payload={}</code>, <code>error=str(exc)</code>를 저장한다. 검증: <code>py_compile</code>, <code>app/test_evaluation_persistence.py</code> 12 passed, evaluation/learner/teacher focused 53 passed, backend focused 116 passed, residual <code>SessionEvaluationWrite(</code> route/test direct constructor 0건.</p>
|
||
<p class="dg-note">90차 적용(2026-06-29): refactor-governance P2로 M2 턴 메모리 전달 경계를 <code>TurnMemory</code> 값 객체로 단일화했다. <code>recall_summary</code>/<code>pinned_facts</code>/<code>recent_turns</code>/<code>kb_behavior_cues</code>는 <code>TurnContext.memory</code>에 보관되고, <code>prepare_turn(memory=...)</code>에서 기존처럼 PII 마스킹된 뒤 <code>build_turn_messages(memory=...)</code>로 전달된다. REST/SSE/voice와 재평가 경로는 <code>RecallContext</code>, 최근 client-visible 턴, KB cue를 <code>TurnMemory</code>로 조립한다. 이는 live provider 압축이나 운영 품질 검증 완료가 아니라, Node.js 전환 전 내부 계약 표면을 줄인 구조 개선이다. 검증: <code>py_compile</code>, M2/voice/evaluation focused 115 passed, gateway contract 23 passed, Node conformance OK, 구 <code>ctx.recall_summary</code>/<code>ctx.recent_turns</code> 직접 접근 0건.</p>
|
||
<p class="dg-note">91차 적용(2026-06-29): refactor-governance P2로 auth managed-user 생성/재활성화 입력 경계를 <code>ManagedUserUpsertInput</code> dataclass로 고정했다. <code>create_session()</code>, 관리자 <code>POST /admin/users</code>, auth 회귀 테스트의 <code>upsert_managed_user()</code> 호출은 더 이상 <code>email/display_name/role/admin_access/cohort_ids/user_id/external_id/affiliation/account_status/reactivate</code> 키워드 묶음을 직접 넘기지 않고 단일 입력 객체를 만든다. 기존 <code>ManagedUserMemoryInput</code>은 DB→메모리 fallback/store 동기화용, <code>ManagedUserPatch</code>는 <code>/users/me</code>·온보딩·관리자 수정용 partial update 경계로 유지했다. 외부 API/DB schema/온보딩 정책 변경은 아니며, <code>admin_access</code>·<code>super_admin</code> 위임과 <code>onboarding_required</code> 선차단 동작은 기존 계약을 보존한다. 검증: <code>py_compile</code>, auth/admin/session focused 103 passed, 구 keyword call 0건, <code>npm run check:api-types</code>.</p>
|
||
<p class="dg-note">92차 적용(2026-06-29): refactor-governance P3로 현재 Python engine gateway의 응답 메타데이터 중복 선언을 줄이고, Node conformance runner가 <code>gateway-default</code>를 실제 provider 모델명이 아닌 default-routing sentinel로 검증하게 했다. <code>GATEWAY_PROVIDER</code>, <code>GATEWAY_FALLBACK_MODEL_NAME</code>, <code>_response_model_name()</code>이 <code>/v1/generate</code>와 <code>/v1/stream</code>의 <code>provider/model</code> 반환 규칙을 함께 소유한다. <code>scripts/check-engine-gateway-contract.mjs</code>는 golden fixture의 request model이 schema sentinel과 같고, response/done packet model은 resolved provider model인지 확인한다. wire schema, SSE frame shape, session reuse, error event 동작은 바꾸지 않는 순수 구조 정리다. 검증: <code>py_compile</code>, <code>node scripts\check-engine-gateway-contract.mjs --json</code> OK(<code>defaultModelSentinel=gateway-default</code>), gateway contract 23 passed.</p>
|
||
<p class="dg-note">93차 적용(2026-06-29): refactor-governance P2로 Python engine gateway의 메시지 분해 경계를 <code>GatewayPromptParts</code> dataclass로 이름 붙였다. <code>_split_messages()</code>는 더 이상 <code>tuple[str, str]</code>를 반환하지 않고 <code>system_prompt</code>/<code>user_payload</code>를 명시하며, 실제로 쓰지 않던 history 조립 흔적과 주석 모순을 제거했다. 동작은 기존과 같다: system 메시지는 공백 제외 후 <code>\n\n</code>로 합치고, 마지막 user 발화만 이번 턴 payload로 보내며, user가 없으면 session resolution 전에 <code>/v1/generate</code>/<code>/v1/stream</code> 모두 400이다. 검증: <code>py_compile</code>, <code>node scripts\check-engine-gateway-contract.mjs --json</code> OK, gateway contract 27 passed, L1 backend focused 120 passed.</p>
|
||
<p class="dg-note">94차 적용(2026-06-29): refactor-governance P2로 M2 LLM digest 후보의 local quality harness를 추가했다. <code>DigestQualityAssessment</code>/<code>SessionDigestWorkerOutcome</code>와 <code>build_llm_digest_worker_outcome()</code>이 빈/짧은 digest, raw forbidden substring, 내부 평가·CCD·상태 marker, 잘못된 <code>S{session_no}:</code> prefix를 reject하고, 통과한 후보만 <code>SessionDigestResult(source="llm")</code>로 승격한다. <code>open_threads</code>와 session/case id는 입력 계약에서 그대로 보존된다. 이는 live provider 호출, 운영 scheduler, 임상 골든셋 품질평가, 재압축 완료가 아니다. 검증: <code>py_compile</code>, <code>py -3.11 -X utf8 -B -m pytest -p no:cacheprovider app/test_session_memory.py -q</code> 20 passed.</p>
|
||
<p class="dg-note">95차 적용(2026-06-29): refactor-governance P2/P3로 M2 one-shot session digest worker 경계를 추가했다. <code>app/services/session_digest_worker.py</code>가 <code>CompressionJob</code>을 Node-compatible <code>GenerateRequest</code>/<code>EngineMessage</code>로 변환하고, 주입형 engine·<code>audit_hook</code>으로 단발 호출한 뒤 local quality gate를 통과한 결과만 <code>session_summary.digest/compressed_by/token_count</code>와 <code>case_profile.case_digest</code>에 idempotent 적용할 수 있게 한다. DB loader는 persisted fallback summary와 client-visible masked transcript만 재구성하며 raw text, evaluator-only turn, CCD, end_state는 prompt에 넣지 않는다. 이는 provider live run, background scheduler, 임상 골든셋 품질평가, 재압축 완료가 아니다. 검증: <code>py -3.11 -X utf8 -B -m pytest -p no:cacheprovider app/test_session_digest_worker.py app/test_session_memory.py -q</code> 25 passed, <code>py -3.11 -X utf8 -B -m pytest -p no:cacheprovider app/test_session_digest_worker.py app/test_session_memory.py app/test_session_turn_persistence.py app/test_orchestrator_masking.py app/test_runtime_policy.py -q</code> 82 passed.</p>
|
||
<p class="dg-note">96차 적용(2026-06-29): M2 digest worker를 운영자가 명시 실행할 수 있도록 <code>scripts/run-session-digest-worker.py</code> dry-run/apply runner를 추가했다. 기본은 metadata-only dry-run이고, <code>--apply</code>를 줘야 accepted 후보를 DB에 반영한다. runner는 load → DB release → engine generate → short apply 순서로 동작해 LLM 호출 동안 DB transaction을 잡지 않는다. digest 본문은 민감할 수 있어 <code>--show-digest</code>를 명시할 때만 출력한다. 이는 실 provider 장시간 운영 완료가 아니다. 검증: <code>py -3.11 -X utf8 -B -m py_compile scripts\run-session-digest-worker.py apps\api\app\services\session_digest_worker.py apps\api\app\test_session_digest_worker.py</code>, <code>py -3.11 -X utf8 scripts\run-session-digest-worker.py --help</code>, M2 focused 25 passed.</p>
|
||
<p class="dg-note">97차 적용(2026-06-29): <code>SESSION_DIGEST_WORKER_ENABLED=false</code> 기본값을 두고, opt-in일 때만 세션 종료 성공 뒤 digest worker를 background task로 예약한다. scheduler는 persisted fallback row를 load한 뒤 DB connection을 놓고 engine을 호출하며, accepted plan만 짧은 apply transaction으로 반영한다. 이미 <code>compressed_by</code>가 있는 세션은 loader에서 제외하고, apply도 <code>compressed_by IS NULL</code> CAS로 보호하며, fallback summary 재작성 시 compression metadata를 초기화한다. 이는 bounded/default-off scheduler 골격이며, 실 provider 장시간 운영·임상 골든셋 품질평가·재압축 정책 완료가 아니다. 검증: <code>py_compile</code>, runner <code>--help</code>, M2 focused 30 passed.</p>
|
||
<p class="dg-note">99차 적용(2026-06-29): 대시보드 SSOT drift gate를 보강했다. <code>scripts/check-dev-dashboard-ssot.py</code>는 현재 카드 상태(<code>done=25</code>, <code>planned=0</code>, <code>doing=0</code>), CHECK 잔여 0, owner board counts(<code>block=2</code>, <code>decide=7</code>, <code>ext=2</code>), 외부 <code>GATE</code> 상세 6행, 결정 탭 7행, 자유연습 F-10 결정 문구, M2 30/87 검증 수치, default-off scheduler/CAS 문구, 실 provider·임상 골든셋·재압축 GATE 보존, stale 82/25/26 passed 및 낡은 결정 8건 문구 제거를 함께 확인한다. 검증: <code>py_compile</code>, <code>py -3.11 -X utf8 scripts\check-dev-dashboard-ssot.py --json</code> PASS. 아래 적용 노트 일부는 historical snapshot이라 당시 통과 수치를 그대로 보존한다.</p>
|
||
<p class="dg-note">100차 적용(2026-06-29): 관리자·교수자 메일링 시스템 1차를 추가했다. 신규 외부 로그인 pending 계정은 <code>account_pending_approval:{user_id}</code>, 회기 종료 후 평가/error record 저장 완료 세션은 <code>session_review_ready:{session_id}</code>, 명시 테스트 메일은 <code>admin_test_email:{uuid}</code> idempotency key로 <code>app.notification_event</code>/<code>app.notification_delivery</code>에 큐잉된다. 테스트 메일은 대량 오발송을 피하기 위해 <code>AUTH_SUPER_ADMIN_EMAILS</code> 대상에게만 큐잉한다. 메일 HTML은 Vignette 톤앤매너(종이 배경, 세이지 CTA, 8px 카드)를 inline style로 유지하고, 본문에는 축어록·평가 전문을 넣지 않고 <code>/admin/users</code>, <code>/teach/session/:sessionId/review</code>, <code>/admin</code> 딥링크만 제공한다. <code>NOTIFICATION_EMAIL_PROVIDER=smtp</code>와 <code>SMTP_*</code>가 설정된 경우 실제 발송하며, <code>GET /admin/notifications</code>, <code>POST /admin/notifications/process</code>, <code>POST /admin/notifications/test</code>, <code>scripts/run-notification-worker.py</code>로 delivery 상태와 재시도를 운영한다.</p>
|
||
<p class="dg-note">101차 적용(2026-06-30): <code>yunchan@twentyoz.kr</code>처럼 기본 역할이 learner로 복원되는 슈퍼관리자가 타 학습자 회기 리뷰 딥링크를 열 때 <code>session does not belong to user</code> 403에 갇히던 문제를 고쳤다. <code>/sessions/{id}/review</code>는 본인 학습자 리뷰를 우선 시도하고, 실패한 경우 슈퍼관리자만 admin 감독자 읽기 주체로 재시도한다. 일반 learner와 단순 <code>admin_access</code> 위임 계정의 타인 회기 접근 차단은 유지한다. 프론트는 같은 403뿐 아니라 이미 <code>teacherReview</code>가 포함된 감독자 200 응답도 <code>/teach/session/:sessionId/review</code>로 정규화해 학습자 전용 사전/사후·공유 UI가 보이지 않게 한다. 검증: <code>app/test_rbac_idor.py</code> 11 passed, RBAC/auth/teacher focused 46 passed, supervisor route E2E 2 passed(<code>PLAYWRIGHT_PORT=5176</code>), <code>npm run typecheck</code>, <code>npm run build</code>.</p>
|
||
<p class="dg-note">102차 적용(2026-06-30): 관리자(<code>role=admin</code>)가 학습자·교수자·관리자 워크스페이스를 모두 직접 오갈 수 있도록 권한 경계를 정리했다. 백엔드는 <code>Principal.can_access_role()</code>이 <code>role=admin</code>이면 모든 역할 접근을 허용하고, <code>require_role()</code>은 super_admin뿐 아니라 admin도 허용 역할로 승격한다. 학습자 전용 경로(<code>/consent</code> accept/withdraw, <code>/voice/ws</code>)의 super_admin-only 승격 분기도 <code>can_access_role(Role.LEARNER)</code> 기준으로 바꿔 admin이 동일하게 통과한다. 프론트는 <code>auth.tsx</code>의 <code>canAccessRole()</code>에 admin 전체 접근을 반영하고, 역할 전환 메뉴는 <code>accessibleRolesFor()</code> 단일 헬퍼가 소유하며, <code>Sidebar</code> admin 내비에 <code>운영 홈</code>·<code>교수 콘솔</code>·<code>학습자 홈</code>·<code>학습</code>·<code>기록</code> 진입을 추가했다. 외부 API 계약·DB schema·승인 게이트(<code>account_status</code>)·super_admin 권한 부여/회수 동작은 바꾸지 않았다. 검증: <code>npm run typecheck</code>, <code>npm run check:api-types</code>, <code>py -3.11 -X utf8 -B -m pytest app/ -q</code> 290 passed(RBAC/runtime 30 포함), admin 워크스페이스 내비 E2E 1 passed, layout 시각게이트 9/9, session-layout 4 passed.</p>
|
||
<p class="dg-note">103차 적용(2026-06-30): 소유자(윤찬) 결정 4건이 확정되어 owner 보드 결정 카드 7→3으로 줄었다. (1) <b>s2s 2차 PoC 유지</b> — 캐스케이드 1차를 운영 기준으로 두면서 <code>voice-s2s-poc.md</code> 비교 PoC를 계속 연다(최종 채택은 비교 리포트 이후). (2) <b>fast-loop 외부 폴백 허용</b> — 로컬 상주 분류기 기본 + 외부 Haiku/Solar 폴백 허용, 전송 시 데이터주권·보존·동의 가드 적용. (3) <b>재귀학습 fine-tuning 포함</b> — few-shot 자동갱신에 더해 fine-tuning까지 범위에 포함하되, 데이터셋은 동의·추가 축어록 수급 게이트가 닫힌 뒤 구성. (4) <b>서비스 음성 = 대체 TTS provider</b> — 상업 라이선스가 명확한 대체 provider 채택, Higgs synthetic·dev sample provider는 개발 플래그로만 유지, 실존 인물/성우 ref 금지 유지. 네 결정 모두 방향 확정이며 구현/실측은 후속 build 항목으로 남는다. 남은 owner 결정은 평가설계(H1 κ/ICC), 운영 보존·에스컬레이션, 추가 축어록 수급 3건이다. SSOT drift 게이트(<code>check-dev-dashboard-ssot.py</code>)의 기대 카운트도 결정 3·decision panel 3행으로 동기화했다.</p>
|
||
<p class="dg-note">104차 적용(2026-06-30): 남은 소유자 결정 3건이 확정되어 owner 결정 7건이 전건 확정(결정 필요 3→0)됐다. (1) <b>평가설계 H1</b> — 평가자 신뢰도 κ≥0.70 & ICC≥0.75, 환각률 ≤3%, 독립표본 t-검정(α=0.05), 20명 실험/통제군 무작위 배정, 단회기 50분·3척도 pre-post 유지. (2) <b>운영 보존·에스컬레이션</b> — 헬스 raw 90일·rollup 365일, escalation 운영자 수동 승인, SLA 미표기(관측 샘플 이력 유지), 담당 그룹 자동 배정 미도입. (3) <b>추가 축어록</b> — 20건을 1개월 내 수급. 이 결정들은 설계·정책 확정이며, KPI threshold 반영·retention 기본값 적용·실제 수급/파일럿 evidence·통계해석은 후속 build/GATE로 남는다. 즉 owner 보드의 남은 항목은 모두 외부 증거·기관 의존(공개 OAuth 실 <code>/turn</code>, vnet DNS, 한신대 거버넌스, 임상팀 콘텐츠, 20명 파일럿·법무·물리 실측)이며, 운영 원칙상 E2E/외부 증거 없이는 DONE으로 체크하지 않는다. SSOT drift 게이트의 기대 카운트도 결정 0·decision panel 0행으로 동기화했다.</p>
|
||
<p class="dg-note">105차 적용(2026-06-30): 공개 배포 도메인(<code>vnet.18ka.net</code>)에서 슈퍼관리자가 관리자 페이지 전체를 블랭크로 보던 문제를 고쳤다. 원인은 권한이 아니라 <code>vite preview</code> 서버의 host 검증이었다. 배포는 <code>npm run preview</code>를 cloudflared 터널로 공개 도메인에 연결하는데, <code>vite.config.ts</code>에 <code>preview.allowedHosts</code>가 없어 공개 Host 요청이 <code>Blocked request. This host is not allowed.</code> 403 텍스트만 받아 SPA가 통째로 로드되지 못했다(에러 바운더리 부재로 전체 흰 화면). <code>server.allowedHosts</code>와 동일 목록을 <code>preview</code>에도 적용하고 기본 허용에 <code>vnet.18ka.net</code>/<code>api-vnet.18ka.net</code>/<code>vignette.chanpaca.net</code>를 추가했다. 검증: 재현(<code>curl -H "Host: vnet.18ka.net" 127.0.0.1:5174</code> → 수정 전 Blocked, 수정 후 <code>200</code>+정상 index), 로컬 dev·preview에서 일반/슈퍼 관리자 전 admin·teach·learn 페이지 정상 렌더(pageerror 0), <code>npm run typecheck</code>. 회귀 방지로 <code>npm run check:preview-hosts</code>(<code>scripts/check-preview-hosts.mjs</code>)를 추가해 preview를 띄워 공개 Host 3종 200 허용·임의 Host 403 차단을 행동 검증한다(공개 3 allowed·unlisted blocked 통과). 공개 OAuth 실 <code>/turn</code> 증명은 여전히 별도 BLOCK이다.</p>
|
||
<p class="dg-note">106차 적용(2026-06-30): 대시보드 자체를 레드팀 대상으로 삼아 unit + E2E 게이트를 추가했다. <code>scripts/test_dev_dashboard_ssot.py</code>는 SSOT checker import/stateless/owner parser를 검증하고, <code>apps/web/e2e/dev-dashboard.spec.ts</code>는 local file, <code>127.0.0.1</code>, <code>localhost</code> origin에서 필터 오순서·빠른 재시도·Enter/Space 키보드·details/tabs 토글·hash anchor·링크/이미지 무결성·desktop/mobile overflow를 확인한다. 같은 라운드에서 live public API <code>https://api-vignette.chanpaca.net/health</code>와 <code>/personas</code>가 Cloudflare 530/error code 1033으로 실패하는 것을 발견했고, 원인은 Docker Desktop 중단으로 <code>vignette-dev-db</code>가 꺼진 상태였다. Docker Desktop/DB 재기동 후 <code>scripts/start-public-runtime.ps1</code>를 다시 실행해 public health 200, unauth <code>/personas</code> 401, auth config 200, Google login start 302, watchdog healthy: engine, api, web-preview, cloudflared, public-api까지 복구를 확인했다. 검증: SSOT unit 3 OK, dashboard checker PASS, dev-dashboard E2E redteam 10 passed, backend RBAC/auth/teacher 47 passed, supervisor route E2E 2 passed, <code>npm run typecheck</code>, <code>npm run build</code>.</p>
|
||
<p class="dg-note">107차 적용(2026-07-01): 하린/P6 102분 회기의 <code>SUPERVISOR AI 평가 실패</code>를 실제 DB와 감사 로그로 추적했다. 종료 시각과 평가 row 갱신 시각 차이가 45초였고, 기존 회기말 평가 background task가 <code>wait_for(..., 45.0)</code> 하드캡에서 <code>asyncio.TimeoutError</code>를 맞은 뒤 <code>str(exc)==""</code>를 저장해 <code>payload={}</code>, <code>error=""</code>가 된 것이 원인이었다. 회기말 평가는 <code>SESSION_EVALUATION_TIMEOUT</code> 또는 <code>ENGINE_TIMEOUT</code>을 쓰도록 바꾸고, 빈 예외 문자열은 예외 타입/명시 메시지로 저장한다. 명시 재평가 API는 실패 결과를 먼저 저장한 뒤 502/503으로 노출한다. 프론트는 종료 실패 시 리뷰로 이동하지 않고 alert를 보여주며, 평가 대기 리뷰는 제한 폴링으로 ready 전환을 기다리고, 교수자 검토 화면에는 <code>AI 평가 재시도</code> 버튼을 추가했다. 재시도 실패 UI 유지와 저장된 평가 실패 row의 read-model 노출도 회귀 테스트로 고정했다. 모바일 코칭 모드에서 우측 컬럼 숨김 때문에 AI 튜터 말풍선이 보이지 않던 회귀도 함께 수정했다. 검증: backend 전체 297 passed, <code>npm run check:api-types</code>, <code>npm run typecheck</code>, <code>npm run build</code>, <code>session-review</code> desktop/mobile 16 passed, <code>session-mvp</code> desktop/mobile/single-run 5 passed, <code>session-layout</code> desktop/mobile 8 passed, <code>layout-visual-gate</code> 9 passed.</p>
|
||
<p class="dg-note">108차 적용(2026-07-01): mock E2E가 가리던 브라우저 실제 SSE 저장 경로를 추가로 검증했다. 기존 <code>session-persistence.spec.ts</code>는 real API/DB를 쓰지만 <code>/api/sessions/{id}/turn</code>를 직접 호출해 UI의 <code>openSessionStream()</code> → backend <code>/stream</code> → <code>finalize_completed_turn()</code> 저장 경로를 타지 않았다. 새 <code>@single-run</code> 테스트는 <code>/api/health</code>에서 <code>db=true</code>, <code>engine=true</code>를 확인한 뒤 실제 Session UI에서 회기를 시작하고, 학습자 발화를 브라우저 fetch stream으로 보내며, 입력창이 다시 enabled 될 때까지 stream 완료를 기다린 다음 <code>GET /sessions/{id}/review</code>에서 learner/client 저장 축어록을 확인한다. 최신 session-persistence 묶음 검증: <code>PLAYWRIGHT_PORT=5205 npx playwright test e2e/session-persistence.spec.ts --project=chromium-single-run --workers=1</code> 7 passed, <code>npm run typecheck</code> passed, 현재 Playwright 수집 기준 <code>162 tests in 18 files</code>.</p>
|
||
<p class="dg-note">109차 적용(2026-07-01): 원래 E2E silent error 목표에 맞춰 AI 튜터와 위기 게이트 경로를 추가로 좁혔다. <code>POST /sessions/{id}/live-coach</code>는 코칭 생성 뒤 <code>save_live_coach_event()</code> 실패를 <code>except/pass</code>로 묻고 있어, 사용자는 코칭 카드를 봤지만 재로딩/이력에는 사라지는 상태가 가능했다. 라우트에서 저장 실패를 삼키지 않게 하고, 실패가 503으로 표면화되는 백엔드 회귀와 실제 Session UI의 코칭 모드 → 턴 완료 → DB-backed <code>GET /live-coach</code> 이력 → 재로딩 후 <code>C</code> 마커 E2E를 추가했다. 또한 crisis <code>safety</code>/<code>done.conversation_stopped</code> 또는 음성 <code>reply.conversation_stopped</code>가 빈 응답으로 끝날 때 <code>내담자 응답 없음</code> 신호가 안전 게이트를 덮지 않게 막고, 위기 안전 자원 109·입력 disabled·live-coach 미호출을 E2E로 고정했다. 검증: <code>python -X utf8 -m pytest -p no:cacheprovider app/test_session_turn_persistence.py -q</code> 27 passed, <code>PLAYWRIGHT_PORT=5193 npx playwright test e2e/session-mvp.spec.ts --project=chromium-single-run --workers=1</code> 2 passed, <code>PLAYWRIGHT_PORT=5199 npx playwright test e2e/session-persistence.spec.ts --project=chromium-single-run --workers=1</code> 5 passed, <code>npm run typecheck</code> passed.</p>
|
||
<p class="dg-note">110차 적용(2026-07-01): 음성 비언어 메타데이터가 fixture-only로 통과하는 silent gap을 줄였다. <code>db.healthcheck()</code>, runtime table readiness, deploy preflight DB mode는 이제 <code>app.turns.provider_events</code> 하나가 아니라 <code>audio_ref</code>/<code>silence_ms</code>/<code>speech_rate</code>/<code>barge_in</code>/<code>provider_events</code> 5개 컬럼 전체를 확인한다. dev/runtime bootstrap은 누락된 nullable 음성 컬럼도 보강한다. 새 DB-backed <code>@single-run</code> E2E는 실제 세션을 만든 뒤 <code>/api/voice/ws</code> <code>stt_result</code> control로 침묵·끼어듦·provider event를 보내고, <code>GET /sessions/{id}/review</code>에서 제한된 <code>nonverbal</code> 칩으로 파생되는지 확인한다. 검증: <code>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</code> 77 passed, <code>PLAYWRIGHT_PORT=5199 npx playwright test e2e/session-persistence.spec.ts --project=chromium-single-run --workers=1</code> 5 passed, <code>python -X utf8 -m py_compile app/db.py app/auth_sessions.py app/test_runtime_policy.py ..\..\scripts\check-deploy-preflight.py</code>, preflight skip-db PASS.</p>
|
||
<p class="dg-note">111차 적용(2026-07-01): 실제 Session 마이크 UI가 비언어 메타를 만들지 않던 공백을 줄였다. <code>AudioWorklet</code> 캡처는 PCM RMS/peak로 voice activity와 trailing silence를 추정해 <code>audio_end</code>에 <code>silence_ms</code>, <code>barge_in=false</code>, browser source <code>provider_events</code>를 보낸다. <code>MediaRecorder</code> fallback은 raw PCM을 볼 수 없어 duration 기반 voice activity만 보낸다. 리뷰 read-model은 turn-level <code>silence_ms</code>가 이미 침묵 칩을 만들면 provider <code>silence</code> 칩을 중복 노출하지 않는다. 검증: <code>npm run typecheck</code>, backend voice/session focused 77 passed, <code>PLAYWRIGHT_PORT=5197 npx playwright test e2e/voice-success.spec.ts --project=chromium-single-run --workers=1</code> 2 passed, 최신 <code>PLAYWRIGHT_PORT=5205 npx playwright test e2e/session-persistence.spec.ts --project=chromium-single-run --workers=1</code> 7 passed, 현재 Playwright 수집 기준 <code>162 tests in 18 files</code>.</p>
|
||
<p class="dg-note">112차 적용(2026-07-01): 교수자 회기 리뷰 데스크톱에서 <code>overview</code>/<code>transcript</code>가 여러 CSS Grid row를 가로질러 오른쪽 검토 rail 높이를 나눠 먹으며 중앙에 큰 빈 row가 생기던 문제를 제거했다. 1181px 이상 교수자 리뷰는 main column + review rail 2열 wrapper로 분리하고, main 내부는 요약 전체폭, 차트/회기 흐름 2열, 축어록 전체폭으로 배치해 서로 다른 rail 높이가 빈공간을 만들지 않게 했다. 회귀 방지로 <code>layout-visual-gate.spec.ts</code>에 교수자 리뷰 폭별 dead vertical gap 검증을 추가했다. 검증: <code>npm run typecheck</code>, <code>npm run build</code>, <code>session-review</code> desktop 8 passed, <code>layout-visual-gate</code> 10 passed, <code>session-layout</code> desktop/mobile 8 passed.</p>
|
||
<p class="dg-note">113차 적용(2026-07-01): 학습자 <code>/learn/practice</code> 페르소나 리스트가 긴 한글 주호소 요약에서 카드 내부를 클립하던 문제를 수정했다. <code>.lh-list-pane</code>의 viewport 고정 높이와 내부 스크롤을 풀고, <code>.lh-personas</code>/<code>.lh-persona</code>가 콘텐츠 높이를 그대로 만들게 했으며, <code>.lh-persona__summary</code> 3줄 clamp를 제거했다. <code>learner.spec.ts</code>에는 각 페르소나 카드의 summary/body/card scroll overflow가 0인지 재는 regression guard를 추가했다. 검증: <code>npm run typecheck</code>, <code>npm run build</code>, 브라우저 rect smoke 1095px/1440px에서 카드 overflow 0 및 문서 가로 overflow 0. 현재 로컬 <code>127.0.0.1:8000/health</code>가 timeout이라 real API 기반 전체 <code>learner.spec.ts</code>는 이번 패스에서 미실행.</p>
|
||
<p class="dg-note">114차 적용(2026-07-01): 회기 종료 후 background deep 평가가 실제 durable DB row로 저장되는지 fixture 없이 검증했다. <code>_schedule_session_evaluation()</code>은 생성한 task를 반환하고 done callback에서 crash/cancel을 로그로 관찰하며, 평가 결과·timeout/error 저장이 durable store에 닿지 않으면 명시 error 로그를 남긴다. 교수자 명시 재평가 API도 평가 산출물이 생성됐지만 저장 실패하면 503으로 표면화한다. <code>GET /eval/sessions/{id}/evaluation</code>은 <code>status/error/durable</code>을 노출해 캐시 착시와 DB 저장 증거를 구분한다. 새 DB-backed E2E는 학습자 세션 생성→턴→종료→teacher 로그인→<code>/eval/.../evaluation</code> polling→교수자 리뷰 UI <code>평가 완료</code>까지 실제 API/DB/엔진으로 확인한다. 검증: API 전체 309 passed, <code>npm run check:api-types</code>, <code>npm run typecheck</code>, <code>PLAYWRIGHT_PORT=5205 npx playwright test e2e/session-persistence.spec.ts --project=chromium-single-run --workers=1</code> 7 passed, <code>PLAYWRIGHT_PORT=5203 npx playwright test e2e/session-review.spec.ts --project=chromium-desktop --project=chromium-mobile --workers=1</code> 16 passed, 현재 Playwright 수집 기준 <code>162 tests in 18 files</code>.</p>
|
||
<p class="dg-note">115차 적용(2026-07-01): 본래 E2E goal의 AI 평가 축을 계속 이어서 검증했다. 새 DB-backed <code>@single-run</code>은 학습자 세션과 턴을 실제 DB/API/엔진으로 만든 뒤 교수자 <code>POST /eval/sessions/{id}/reevaluate</code>를 호출하고, durable <code>app.session_evaluation</code> row와 <code>GET /eval/.../evaluation</code>/<code>GET /sessions/{id}/review</code>의 <code>평가 완료</code> 반영을 확인한다. 추가 backend focused 회귀는 종료 회기가 timeout+grace 이후에도 evaluation row 없이 남을 때 교수자 read-model이 무한 <code>평가 대기</code>로 고착되지 않고 <code>평가 실패</code>와 <code>AI 평가 재시도가 필요합니다</code> 사유를 표면화하는지 검증한다. E2E 문서는 route fixture UI 회귀와 DB-backed 증거를 분리해 표시하도록 정리했다. 검증: <code>PLAYWRIGHT_PORT=5237 npx playwright test e2e/session-persistence.spec.ts --project=chromium-single-run --workers=1 --grep "explicit teacher session reevaluation"</code> 1 passed, <code>py -3.11 -X utf8 -m pytest -p no:cacheprovider app/test_session_turn_persistence.py -k "session_evaluation or stale_missing" -q</code> 3 passed, 현재 Playwright 수집 기준 <code>181 tests in 19 files</code>.</p>
|
||
<p class="dg-note">116차 적용(2026-07-01): session evaluation startup recovery로 종료 평가 background task 유실 공백을 줄였다. API 기동 뒤 <code>SESSION_EVALUATION_TIMEOUT</code>/<code>ENGINE_TIMEOUT</code> + grace보다 오래됐고, client-visible turn은 있지만 durable <code>app.session_evaluation</code> row가 없는 종료 회기를 evaluator AI context로 조회해 기존 session-end 평가 생성 경로를 재예약한다. 같은 session_id의 process-local 중복 background 평가는 in-flight set으로 막는다. 자동 종료 평가 DB-backed E2E도 같은 row가 교수자 리뷰뿐 아니라 <code>/teacher/dashboard</code>의 <code>evaluation_status=ready</code>, <code>review_ready=true</code>, <code>supervisor_state=평가 완료</code>로 반영되는지 확장했다. 검증: <code>py -3.11 -X utf8 -m pytest -p no:cacheprovider app/test_notifications.py app/test_session_turn_persistence.py app/test_evaluation_persistence.py -k "session_evaluation or missing_session_evaluation or scheduled_session_evaluation" -q</code> 16 passed, <code>npm run typecheck</code> passed, <code>PLAYWRIGHT_PORT=5238 npx playwright test e2e/session-persistence.spec.ts --project=chromium-single-run --workers=1 --grep "finishes session end evaluation"</code> 1 passed. 후속 리스크는 다중 프로세스 durable claim/job table 없이 startup sweep만으로는 여러 API instance 간 소유권을 완전히 보장하지 못한다는 점이다.</p>
|
||
<p class="dg-note">115차 적용(2026-07-01): 교수자 학생 분석을 <code>/teach</code> 대시보드에서 분리해 좌측 메뉴의 <code>/teach/analysis</code> 전용 작업면으로 이동했다. 교수 콘솔은 검토 큐·위기 알림·페르소나 검수·최근 회기 triage만 맡고, 학생 분석은 전체 학습자 검색 테이블을 먼저 보여준다. 각 학습자는 한 줄 행으로 표시하고, 우측 펼침 버튼은 행 아래에 미니 추이·기법 태그·최근 기록을 확장한다. 학습자 이름 또는 상세 보기 버튼은 <code>?learner=</code> 상세 드릴다운으로 이동하며, 상세 화면은 추이·전체 회기·단계 분석 탭으로 선택 학습자의 전체 회기 이력을 나눈다. 회귀 방지로 <code>teacher.spec.ts</code>는 콘솔에 분석 패널이 없고 메뉴를 통해 학생 분석 페이지로 이동한 뒤 검색·행 펼침·상세 진입·전체 회기 탭을 확인하며, <code>layout-visual-gate.spec.ts</code>는 학생 분석 목록/상세 화면을 7개 폭에서 별도 캡처·검사한다. 검증: <code>npm run typecheck</code>, <code>npm run build</code>, <code>npm run check:api-types</code>, <code>PLAYWRIGHT_PORT=5215 npx playwright test e2e/teacher.spec.ts --project=chromium-single-run --workers=1</code> 9 passed, <code>PLAYWRIGHT_PORT=5217 npx playwright test e2e/layout-visual-gate.spec.ts --project=chromium-single-run --workers=1</code> 12 passed, 현재 Playwright 수집 기준 166 tests / 18 files.</p>
|
||
<p class="dg-note">116차 적용(2026-07-01): 원천문서 갭 E2E 목표에 맞춰 C1/C2 silent failure를 추가로 닫았다. C1은 학습자가 저장한 사례개념화 워크시트가 <code>app.case_worksheet</code>를 거쳐 교수자 검토 화면 read-only 값으로 복원되고, 교수자 <code>수정요청</code> 검수 메모가 <code>teacherReview.worksheetStatus</code>/<code>worksheetNote</code>로 재조회되는지 실제 브라우저/API/DB E2E로 고정했다. C2는 <code>app.safety_events</code>가 runtime bootstrap/healthcheck에서 빠져 DB 정상처럼 보일 수 있던 구멍과, <code>jsonb</code> codec 위에 이미 직렬화한 문자열을 넘겨 detail이 JSON string으로 저장되던 구멍을 수정했다. <code>record_safety_event()</code>는 RLS용 <code>ai_context=true</code>로 저장하고, alert list 실패는 non-dev에서 fail-closed한다. 검증: <code>python -X utf8 -m pytest -p no:cacheprovider app/test_session_turn_persistence.py app/test_teacher_dashboard.py app/test_runtime_policy.py -q</code> 59 passed, <code>PLAYWRIGHT_PORT=5228 npx playwright test e2e/session-persistence.spec.ts --project=chromium-single-run --workers=1 --grep "case worksheet|crisis safety"</code> 2 passed, 현재 Playwright 수집 기준 <code>166 tests in 18 files</code>.</p>
|
||
<p class="dg-note">117차 적용(2026-07-01): 관리자가 관리 콘솔에 들어가도 비어 보일 수 있던 권한/라우팅 경계를 닫았다. 백엔드 <code>require_admin_access()</code>는 raw <code>admin_access</code> 플래그 대신 <code>Principal.can_access_role(Role.ADMIN)</code>을 사용해 <code>role=admin</code> 세션의 스테일 플래그를 허용한다. 프론트 <code>OnboardingGate</code>는 승인된 관리자 또는 <code>admin_access</code> 계정이 <code>/admin*</code>에 있을 때 학습자 온보딩으로 우회시키지 않고, 루트/승인대기 복귀도 관리 콘솔로 보낸다. 학습자·교수자 공간의 온보딩 게이트와 미승인 계정의 <code>/pending</code> 게이트는 유지한다. 검증: backend admin/auth focused 13 passed, <code>npm run typecheck</code>, <code>npx playwright test e2e/admin.spec.ts --project=chromium-desktop --project=chromium-mobile --workers=1 --grep "approved admin without onboarding"</code> 2 passed, 현재 Playwright 수집 기준 <code>175 tests in 19 files</code>.</p>
|
||
<p class="dg-note">118차 적용(2026-07-01): 코칭 모드 사용권을 회기별 기본/최대 3회로 제한하고, 좋은 발화가 실제 내담자 변화 신호로 이어질 때 1개씩 재충전하도록 했다. <code>app.live_coach_events</code>는 기존 코칭 payload에 더해 <code>event_type</code>(<code>use</code>/<code>recharge</code>), <code>credit_delta</code>, <code>credit_balance</code>, <code>reason</code>을 저장한다. <code>POST /sessions/{id}/live-coach</code>는 잔여 기회가 없으면 409로 막고, <code>GET /sessions/{id}/live-coach</code>는 <code>quota</code>와 <code>credit_events</code>를 함께 반환한다. 세션 UI는 코칭 기회 점 표시, 사용/충전 펄스, 이력 모달의 사용권 로그를 제공한다. 충전 조건은 <code>appropriateness=pos</code>, <code>rapport_signal>=0.35</code>, 단계 전환 또는 <code>effective_openness</code> 상승을 동시에 만족할 때로 제한했다. 검증: focused <code>py_compile</code>, FastAPI app import 84 routes, <code>python -m pytest app\test_live_coach_privacy.py -q</code> 2 passed, <code>python -m pytest -p no:cacheprovider app\test_session_turn_persistence.py -k live_coach -q</code> 5 passed, <code>npm run generate:api-types</code>, <code>npm run check:api-types</code>, <code>npm run typecheck</code>, <code>npm run build</code>, <code>PLAYWRIGHT_BASE_URL=http://localhost:5173 npx playwright test e2e/session-persistence.spec.ts --project=chromium-single-run --workers=1 --grep "AI tutor coaching history"</code> 1 passed.</p>
|
||
<p class="dg-note">119차 적용(2026-07-01): AI 튜터 live coach의 <code>status=degraded</code> 200 fallback이 정상 AI 코칭처럼 보일 수 있는 silent error 후보를 닫았다. 엔진/structured payload 실패 시 규칙 기반 제안 자체는 유지하지만, 세션 UI는 카드, 근거 모달, 이력 모달, 턴 <code>C</code> 마커에서 <code>대체 코칭</code>/<code>AI 응답 대체</code>를 노출한다. DB-backed E2E는 실제 엔진 코칭 응답과 DB history payload가 <code>status=ready</code>, <code>latency_ms>0</code>인지 확인하고, fixture E2E는 degraded fallback이 사용자에게 replacement coaching으로 표시되는지 별도 검증한다. 저장 실패는 기존대로 503 fail-closed라 degraded fallback과 persistence failure를 같은 성공으로 취급하지 않는다. 검증: <code>npm run typecheck</code>, <code>py -3.11 -X utf8 -m pytest -p no:cacheprovider app/test_session_turn_persistence.py -k "live_coach" -q</code> 5 passed, <code>PLAYWRIGHT_PORT=5240 npx playwright test e2e/session-mvp.spec.ts --project=chromium-desktop --workers=1 --grep "AI tutor coaching|degraded AI tutor"</code> 2 passed, <code>PLAYWRIGHT_PORT=5241 npx playwright test e2e/session-persistence.spec.ts --project=chromium-single-run --workers=1 --grep "AI tutor coaching history"</code> 1 passed, 현재 Playwright 수집 기준 <code>181 tests in 19 files</code>.</p>
|
||
<p class="dg-note">120차 적용(2026-07-01): 본래 E2E goal의 원천문서 gap 축을 이어서 H4/C2 silent failure 후보를 추가로 고정했다. H4 새 DB-backed <code>@single-run</code>은 실제 Session UI stream으로 한국어 이름·기관·전화번호가 포함된 학습자 발화를 보내고, <code>GET /sessions/{id}</code> 상세와 <code>/review</code> 응답 모두 raw 값 없이 <code>[NAME]</code>/<code>[ORG]</code>/<code>[PHONE]</code>으로 마스킹되는지 확인한다. C2는 위기 발화가 교수자 검토용 learner turn으로 저장되지만 client AI reply turn은 저장되지 않고, DB-backed 교수자 안전 큐의 109 알림이 유지되는지 확인한다. 검증: <code>npm run typecheck</code>, <code>PLAYWRIGHT_PORT=5244 npx playwright test e2e/session-persistence.spec.ts --project=chromium-single-run --workers=1 --grep "Korean PII|crisis safety event"</code> 2 passed, 현재 Playwright 수집 기준 <code>181 tests in 19 files</code>.</p>
|
||
<p class="dg-note">121차 적용(2026-07-01): 본래 E2E goal의 AI 평가 silent error 축을 이어서 fast-loop 평가 실패와 수동 재시도 경로를 더 좁혔다. 상담 턴 저장은 평가 훅 예외 때문에 막지 않되, 실패는 <code>evaluation.error</code>로 남기고 교수자 리뷰에 <code>턴 평가 실패</code> 노트로 표시한다. evaluator error dict도 원문 예외 메시지를 사용자-facing payload에 싣지 않으며, <code>session_metrics</code>는 error가 있는 fast-loop 결과를 성장 점수와 최근 피드백에서 제외한다. 새 DB-backed UI E2E는 admin engine config를 실패 endpoint로 잠깐 바꿔 실제 <code>app.session_evaluation</code> 실패 row를 만든 뒤 원복하고, 교수자 리뷰의 <code>AI 평가 재시도</code> 클릭이 실제 <code>POST /eval/sessions/{id}/reevaluate</code> → durable ready row → <code>/review</code> <code>평가 완료</code> → <code>/teacher/dashboard</code> ready 상태까지 이어지는지 검증한다. 검증: <code>py -3.11 -X utf8 -m pytest -p no:cacheprovider app/test_session_turn_persistence.py -k "fast_loop_evaluation" -q</code> 2 passed, <code>py -3.11 -X utf8 -m pytest -p no:cacheprovider app/test_learner_dashboard.py -k "failed_fast_loop" -q</code> 1 passed, <code>npm run typecheck</code>, <code>PLAYWRIGHT_PORT=5245 npx playwright test e2e/session-persistence.spec.ts --project=chromium-single-run --workers=1 --grep "manual AI evaluation retry"</code> 1 passed, 현재 Playwright 수집 기준 <code>181 tests in 19 files</code>.</p>
|
||
<p class="dg-note">122차 적용(2026-07-01): 본래 E2E goal의 턴 단위 AI 평가 silent error 후보를 닫았다. 기존 교수자 <code>POST /eval/sessions/{id}/turn</code>은 fast-loop 재평가 결과를 응답 바디로만 돌려주고, <code>app.feedback_scores</code>/<code>app.turn_technique</code>/<code>app.turn_client_state</code>/<code>app.supervisor_comment</code>/<code>app.alternative_utterance</code>에 durable 저장하지 않아 이후 리뷰/분석 read-model이 바뀌지 않았다. <code>replace_turn_evaluation()</code>을 추가해 같은 <code>turn_id</code>의 normalized 평가 row를 교체 저장하고, 저장 실패는 503, 평가 error는 저장 후 502/503으로 표면화한다. normalized evaluation table delete RLS policy도 정적 SQL과 runtime ensure에 동기화했다. 새 DB-backed E2E는 실제 세션/턴을 만든 뒤 교수자 턴 재평가 응답의 technique 라벨이 <code>/review</code> learner turn에 hydrate되는지 확인한다. 검증: <code>py -3.11 -X utf8 -m pytest -p no:cacheprovider app/test_eval_routes.py app/test_evaluation_persistence.py -q</code> 29 passed, <code>npm run typecheck</code>, <code>PLAYWRIGHT_PORT=5246 npx playwright test e2e/session-persistence.spec.ts --project=chromium-single-run --workers=1 --grep "teacher turn reevaluation"</code> 1 passed, 현재 Playwright 수집 기준 <code>181 tests in 19 files</code>.</p>
|
||
<p class="dg-note">123차 적용(2026-07-01): 음성 WebSocket에서 <code>transcript final</code>이 먼저 화면에 보인 뒤 턴 저장이 실패하면, 학습자 발화가 DB/review에는 없는데 브라우저 transcript에는 정상 발화처럼 남을 수 있던 silent error 후보를 닫았다. 백엔드는 턴 저장 503을 <code>code=turn_persistence_unavailable</code>로 구조화해 내려주고, Session UI는 해당 임시 발화를 <code>저장 실패</code> 배지로 표시하며 턴 카운트/코칭/리뷰 성공 턴으로 확정하지 않는다. 대기 중 내담자 응답 말풍선은 제거하고 텍스트 입력과 마이크 오류 alert를 복구한다. 검증: <code>py -3.11 -X utf8 -m pytest -p no:cacheprovider app/test_voice_ws.py -q</code> 16 passed, <code>npm run typecheck</code>, <code>PLAYWRIGHT_PORT=5248 npx playwright test e2e/session-mvp.spec.ts --project=chromium-single-run --workers=1 --grep "pending voice transcript"</code> 1 passed, 현재 Playwright 수집 기준 <code>182 tests in 19 files</code>.</p>
|
||
<p class="dg-note">124차 적용(2026-07-01): 본래 E2E goal을 계속 이어서 AI 튜터/평가 주변 silent error 후보 4개를 추가로 닫았다. <code>GET /live-coach</code> 실패는 빈 이력으로 보이지 않고 이력 모달 alert로 표시한다. <code>POST /live-coach</code>는 <code>persistence_source</code>를 반환하고, DB가 아닌 runtime 이력은 Session 카드와 이력 모달에서 임시 저장으로 드러낸다. 기존 pre/post 점수를 지우면 더 이상 <code>저장된 값 기준</code>으로 오인하지 않고 invalid 입력과 저장 차단으로 표시한다. 교수자 대시보드 pending queue는 턴이 0개인 종료 세션을 제외해 리뷰 준비 안 된 세션이 검토 대기처럼 보이지 않게 했다. 검증: <code>C:\Users\encep\AppData\Local\Programs\Python\Python311\python.exe -m pytest apps/api/app/test_teacher_dashboard.py -q</code> 11 passed, <code>C:\Users\encep\AppData\Local\Programs\Python\Python311\python.exe -m pytest apps/api/app/test_session_turn_persistence.py -q -k "live_coach_turn_marks_runtime_persistence_source"</code> 1 passed, <code>npm run typecheck</code>, <code>PLAYWRIGHT_PORT=5251 npx playwright test e2e/session-mvp.spec.ts --project=chromium-desktop --workers=1 --grep "AI tutor|runtime AI tutor"</code> 4 passed, <code>PLAYWRIGHT_PORT=5252 npx playwright test e2e/session-review.spec.ts --project=chromium-desktop --workers=1 --grep "pre/post"</code> 1 passed, 현재 Playwright 수집 기준 <code>186 tests in 19 files</code>. 로컬 작업트리 기준이며 배포 전이다.</p>
|
||
<p class="dg-note">125차 적용(2026-07-01): 긴 회기 평가가 기존 짧은 리뷰 polling window를 넘기면 사용자는 <code>평가 대기</code>에 고착된 것처럼 볼 수 있었다. <code>SessionReview</code>의 ready polling window를 약 90초로 늘리고, 10번째 review 응답에서야 <code>평가 완료</code>가 되는 route-fixture 회귀를 추가했다. 검증: <code>npm run typecheck</code>, <code>PLAYWRIGHT_PORT=5253 npx playwright test e2e/session-review.spec.ts --project=chromium-desktop --workers=1 --grep "long-running session evaluation"</code> 1 passed, 현재 Playwright 수집 기준 <code>188 tests in 19 files</code>. 로컬 작업트리 기준이며 배포 전이다. 서브에이전트가 찾은 추가 후보(teacher dashboard stale-missing 불일치, 중복 <code>POST /end</code> 재평가 덮어쓰기, RAG/source-pack skip)는 이번 라운드에서는 후속으로 남겼다.</p>
|
||
<p class="dg-note">126차 적용(2026-07-02): 125차 후속으로 남긴 AI 평가 silent error 후보를 닫았다. 교수자 대시보드도 상세 리뷰와 같은 missing session evaluation 규칙을 써서 timeout+grace 이후 evaluation row가 없으면 <code>평가 실패</code>와 재시도 필요 사유를 표시한다. 이미 종료된 세션에 <code>POST /sessions/{id}/end</code>가 다시 들어와도 background session evaluation을 재예약하지 않아 기존 ready row가 새 실패로 덮이지 않는다. AI 평가가 ready가 아니면 교수자 리뷰를 <code>검토 완료</code>로 닫아 pending queue에서 숨길 수 없고, UI의 <code>검토 완료</code> 버튼도 비활성화된다. <code>kb-source-packs.spec.ts</code>는 source-scoped evaluator retrieval 503을 더 이상 skip하지 않는다. 검증: <code>C:\Users\encep\AppData\Local\Programs\Python\Python311\python.exe -X utf8 -m pytest -p no:cacheprovider apps/api/app/test_teacher_dashboard.py apps/api/app/test_session_turn_persistence.py -k "teacher_cannot_close_review_before_ai_session_evaluation_ready or teacher_can_mark_session_review_closed_with_note or end_session_does_not_reschedule_evaluation_for_already_ended_session or session_evaluation or stale_missing" -q</code> 8 passed, <code>npm run typecheck</code>, <code>PLAYWRIGHT_PORT=5254 npx playwright test e2e/session-review.spec.ts --project=chromium-desktop --workers=1 --grep "manual AI retry|long-running session evaluation|retry a failed AI session evaluation|retry fails"</code> 4 passed, <code>PLAYWRIGHT_PORT=5255 npx playwright test e2e/kb-source-packs.spec.ts --project=chromium-single-run --workers=1</code> 1 passed, 현재 Playwright 수집 기준 <code>188 tests in 19 files</code>. 로컬 작업트리 기준이며 배포 전이다.</p>
|
||
<p class="dg-note">127차 적용(2026-07-02): 마무리 전 AI 튜터/음성 위기 UI의 silent error 두 개만 좁게 닫았다. 세션 UI의 로컬 코칭 quota가 0으로 stale해도 즉시 차단하지 않고 서버 <code>GET /live-coach</code> quota를 재조회한 뒤 실제 잔여 기회가 있으면 <code>POST /live-coach</code>를 계속 진행한다. quota exhaustion이나 재조회 실패는 이전 코칭 카드 아래에 묻히지 않도록 <code>coachError</code>를 우선 표시한다. 음성 <code>reply.conversation_stopped</code>가 빈 텍스트로 끝나는 위기 경로는 TTS 종료 이벤트를 기다리지 않고 socket을 닫아 109 안전 게이트와 입력 disabled 상태를 유지한다. 검증: <code>npm run typecheck</code>, <code>PLAYWRIGHT_PORT=5259 npx playwright test e2e/session-mvp.spec.ts --project=chromium-desktop --workers=1 --grep "AI tutor|stale empty AI tutor quota|quota exhaustion|degraded AI tutor|voice conversation stop"</code> 7 passed, 현재 Playwright 수집 기준 <code>206 tests in 19 files</code>. 로컬 작업트리 기준이며 배포 전이다. 서브에이전트가 지적한 live coach LLM audit 실패 은폐는 다음 라운드 후보로 남긴다.</p>
|
||
<p class="dg-note">128차 적용(2026-07-03): 127차 후속 후보였던 live coach LLM audit 실패 은폐를 닫았다. <code>record_llm_call_audit()</code>가 DB pool 또는 <code>audit.llm_call_log</code> insert 실패로 <code>False</code>를 반환해도 기존에는 <code>generate_live_coaching()</code>이 이를 무시해 정상 <code>status=ready</code> AI 코칭처럼 보일 수 있었다. 이제 live coach는 audit hook의 <code>False</code> 또는 예외를 감사 저장 미확인으로 보고, LLM structured 응답을 버린 뒤 규칙 기반 <code>status=degraded</code> 코칭으로 내려간다. 상담 루프는 막지 않되 사용자에게 정상 AI 코칭처럼 보이지 않게 하는 조치다. 검증: <code>C:\Users\encep\AppData\Local\Programs\Python\Python311\python.exe -X utf8 -m py_compile apps/api/app/services/live_coach.py apps/api/app/test_session_turn_persistence.py</code>, <code>C:\Users\encep\AppData\Local\Programs\Python\Python311\python.exe -X utf8 -m pytest -p no:cacheprovider apps/api/app/test_session_turn_persistence.py -k "live_coach" -q</code> 7 passed. 로컬 작업트리 기준이며 배포 전이다.</p>
|
||
<p class="dg-note">129차 적용(2026-07-13): 재부팅 뒤 Chrome이 기존 <code>/admin</code> 탭의 문서 스크롤을 복원하면서 sticky topbar/sidebar만 남고 관리자 본문은 화면 위로 밀리던 silent blank를 재현·수정했다. RED 재현은 길이 2200px 문서에서 <code>scrollY=900</code>을 만든 뒤 persisted <code>pageshow</code>를 보내 수정 전 <code>scrollY=1300</code>과 빈 main을 확인했다. <code>App.tsx</code>는 <code>history.scrollRestoration=manual</code>, <code>pageshow</code> reset, pathname layout-effect와 다음 frame reset을 함께 적용한다. 로컬 focused E2E는 desktop/mobile route 이동·복원 탭 4 passed, typecheck/build passed. Cloudflare Pages production <code>0daabb1b</code> 배포 뒤 custom domain은 <code>assets/index-BhyOF3rL.js</code>를 서빙하며, 15분 일회성 관리자 세션으로 운영 홈·사용자·권한·티켓과 복원 탭을 공개 사이트에서 확인한 <code>public-admin-visual</code> 3 passed 후 세션을 즉시 삭제했다. 이후 concurrent avatar 변경이 섞인 배포는 Cloudflare Pages rollback API로 <code>0daabb1b</code>에 되돌렸다.</p>
|
||
<p class="dg-note">130차 적용(2026-07-13): 운영 <code>/voice/health</code>는 OpenAI 설정을 정상으로 보고했고 실제 운영 키 직접 smoke도 98,133-byte/11.68초 MP3를 반환했지만, 운영 로그에는 사용자의 voice WebSocket 연결이 없어 텍스트 턴 AI 응답이 TTS 경로에 연결되지 않은 공백으로 좁혔다. 인증된 <code>POST /voice/speech</code>는 <code>session_id</code>/<code>turn_seq</code>로 소유 회기와 이미 저장된 client-visible 내담자 응답을 다시 검증한 뒤 기존 voice map/OpenAI TTS로 MP3를 반환한다. Session은 텍스트 전송 user gesture에서 AudioContext를 먼저 unlock하고, 합성 Blob을 기존 Web Audio/Analyser 재생기로 보내며 새 턴이 시작되면 이전 요청을 무효화한다. 검증: backend voice 32 passed, API types check, web typecheck, <code>voice-success</code> text TTS+별도 mic 회기 UI focused 1 passed.</p>
|
||
<p class="dg-note">144차 적용(2026-07-27): 143차 전수 순회에서 소유자 결정 대기로 남긴 7건을 <b>질문 도구로 전건 결정받아 6건 구현 완료</b>(RED→GREEN, 5개 병렬 구현 에이전트). ① <b>설정 아바타 변경</b> — 계정 섹션에 온보딩과 같은 계약(3MB·PNG/JPEG/WebP)의 업로드 컨트롤+미리보기, PATCH /users/me avatar_url 저장 후 refresh로 톱바·레일까지 갱신. ② <b>동의 관리·철회 전체 흐름</b> — 새 <code>#set-consent</code> 섹션(실습 동의 시각·약관/개인정보 버전 표시), learner 철회 버튼(2단계 확인)→차단 안내→"다시 동의" 재동의, 세션 프리스타트의 기존 동의 게이트와 연결(철회 함정 상태 해소, dead code였던 <code>withdrawConsent</code> 실사용화), e2e는 고유 계정으로만 철회 수행. ③ <b>신규 학습자 기초 우선 추천</b> — 회기 0건 학습자의 기본 선택·첫 회기 준비 카드를 난도 오름차순 첫 페르소나로(<code>starterPersona()</code>), 기록 보유·조회 실패 시 기존 규칙 유지. ④ <b>학생 분석 테이블 가상화</b> — <code>@tanstack/react-virtual</code>(신규 의존성) 세로 가상화, 동적 행 높이 measureElement·펼침 재측정, 300행 fixture에서 렌더 행 <60 검증, 데드스페이스 없게 max-height는 내용 이하로. ⑤ <b>저작 모드 좌측 레일 마크업 정리</b> — 어떤 경로에서도 보이지 않던 죽은 DOM(ps-rail)과 전용 CSS 삭제, 특성화 테스트 toHaveCount(0) 강화. ⑥ <b>감정 밸런스 타임라인 차트 구현</b>(143차 미구현 항목) — 혼합 접근: deep 평가 스키마에 <code>turn_valence</code>(seq·v −1~+1, 극단값 회피 지시) 신설 + 저장 데이터 결정론 파생 폴백(counselorBaseline=학습자 턴 rapport_signal 누적 이동평균, clientValence=deep 값 우선/client_state 극성+appropriateness 결합 폴백), 2점 미만 빈 배열·10점 초과 균등 리샘플·축 라벨 4개, hidden_turns 시 비산출로 프라이버시 계약 유지. dev-login 재승인은 현행 유지로 종결. <b>검증</b>: 각 과제 RED 관측→GREEN(에이전트별 증거 로그), 백엔드 pytest <code>421 passed</code>(신규 <code>test_review_valence.py</code> 9 포함), gateway 29, typecheck/build/design-ssot/dead-code/duplication(0.03%) 통과, focused 무회귀 44/42/40/20/14, professor·persona 레이아웃 게이트 통과, 감정 차트는 실DB 공감 5턴 세션에서 baseline·client 각 5포인트/축 4라벨 라이브 실측. 전 스위트 재완주 수치는 체크리스트 문서 §0에 기록. 부수 관찰: <code>smoke-resistance-openness-db.py</code>의 advice_jump turn_client_state ≥5 단언이 엔진 변동성에 민감(결함 로그 #23, 코드 회귀 아님). 체크리스트 415항목 미체크 0 도달. <b>배포(2026-07-27)</b>: commit <code>4511383c</code>로 커밋 후 Cloudflare Pages production <code>d8ebcb61</code> 배포 — 커스텀 도메인이 <code>index-Dqr2stZ-.js</code> 서빙, prod API 8001은 <code>start-public-runtime.ps1</code>로 새 코드 재기동, 공개 헬스·auth config(dev login disabled)·비인증 401·OAuth 302·watchdog 전체 healthy 실측.</p>
|
||
<p class="dg-note">145차 적용(2026-07-27): 142·144차가 <b>소유자 결정 대기</b>로 남겨둔 시각 언어·구조 항목 11건을 질문 도구로 전건 결정받아 구현하고, 새로 만든 브레이크포인트 스윕이 "기존 결함"으로 관용하던 시각 붕괴까지 없앴다(8+2 병렬 구현 에이전트, 파일 그룹 분할로 충돌 0). <b>결정·구현</b>: ① <b>Kicker accent dot</b> = "밀집 화면만 정리" — 기본값 <code>dot=true</code>는 유지하고 상태를 전달하지 않는 장식만 <code>dot={false}</code>. 회기 진행 화면 7→2(진행 상태·안전 점검만 유지), <code>/learn</code> 0, <code>/teach</code> 0(화면에 남은 점은 <code><Dot tone></code> 실제 상태 컴포넌트). ② <b>영문 대문자 eyebrow 한글화</b> — <code>SPEND CONTROL</code>→예산 관리, <code>RUNTIME ROUTE</code>→엔진 경로, <code>DAILY JOURNAL</code>→일별 기록, <code>SESSION</code>/<code>SUPERVISOR</code>→회기/지도. <code>MODEL LEDGER</code>·<code>EVALUATOR CACHE</code>·<code>METERING INTEGRITY</code>는 번역 대신 제목과 중복이라 삭제 → 화면상 영문 대문자 eyebrow 0개. 영문 전용 트래킹(<code>letter-spacing:0.12em</code>, 10px)도 한글 판독에 맞게 정리. ③ <b>가운뎃점 라인당 1개</b> — 페르소나 제목·세션바·메타 줄을 어절 단위로 분리(<code>splitPersonaTitle</code>/<code>splitMetaLines</code>), 나열형은 쉼표로 낮춤. ④ <b>eyebrow 중복 제거</b> — 바로 아래(또는 위) 제목과 같은 말인 것만 삭제, 새 정보를 주는 것은 유지. ⑤ <b>빈 상태 중립화</b> — <code>/teach/analysis</code> 345행×5열의 "평가 부족/변화 부족/라포 부족/기법 부족" 반복을 <code>-</code>로 바꾸고 설명은 열 머리에서 한 번만, 셀에는 <code>title</code>로 보조. ⑥ <b>탭 구분 강화</b> — 비활성 탭이 배경과 같아 탭으로 안 보이던 학습 대시보드·회기 리뷰에 구분선. ⑦ <b>비용 표시 정밀도</b> — 합계는 소수 2자리(<code>$41.4887</code>→<code>$41.49</code>), 단가는 4자리(<code>$0.101439</code>→<code>$0.1014</code>), 원본은 <code>title</code>에 보존, 차트 라벨 9px→11px. ⑧ <b>기본 테마 OS 추종</b> — <code>readInitialTheme()</code>이 저장값 → <code>prefers-color-scheme</code> → dark 폴백 순으로 바뀌었다(§8.C 위반 해소). ⑨ <b>Pretendard self-host</b> — jsDelivr 렌더 블로킹 스타일시트와 preconnect를 없애고 <code>public/fonts/PretendardVariable.woff2</code>(2,057,688 B) + <code>@font-face</code> + preload로 전환. ⑩ <b>프리스타트 정보 중복 제거</b> — 호소·대상·난도가 아바타 카드와 본문에 두 번 나오던 것을 본문만 남김. ⑪ <b>브레이크포인트 스윕을 E2E 스펙으로 편입</b> — <code>e2e/breakpoint-sweep.spec.ts</code>(797줄)가 CSS에서 <code>@media</code> 폭을 자동 추출해 페이지별 B-1/B/B+1을 돌고 겹침·잘림·붕괴·화면밖·문서오버플로를 검사한다. 오탐 4종(<code>line-clamp</code>, visually-hidden, 원형 마스크, 폰트메트릭 1~2px)은 구조적으로 제외하고, <b>실제 결함 3건을 되살려 잡히는지 증명하는 테스트</b>를 포함해 검출력 없는 게이트가 되지 않게 했다. <b>결정의 후속으로 깨진 것들</b>: ⑧ 때문에 <code>layout-visual-gate</code>의 <code>gateScreen()</code>이 전면 실패했다(1 failed·14 미실행). 이 스펙은 "다크 시작 → 라이트 토글 → 다크 복귀" 흐름이라 프로젝트 <code>colorScheme:dark</code>로 풀면 라이트 토글 테스트가 깨진다. <code>beforeEach</code>에서 <code>vignette.theme="dark"</code>를 심어 게이트가 자기 전제를 명시하게 고쳤다. ⑦③ 때문에 옛 카피를 박아둔 단언 3건(<code>$6.6212</code>, 페르소나 옵션 접근이름, <code>설정 임상·회기·말투</code>)이 stale이 돼 앱이 아니라 테스트를 갱신했다. <code>AdminAi.tsx</code>의 <code>remaining_usd</code>(<code>number|null</code>) 무가드 전달로 <b>빌드가 깨져 있던 것</b>도 수정했다. <b>스윕이 관용하던 결함 제거</b>: 새 스펙의 <code>KNOWN_APP_DEFECTS</code>에 있던 3건을 실제로 고쳐 <b>배열을 비웠다</b>(재발 시 즉시 실패). ㉮ <code>/learn/practice</code> 1181~1360px에서 프리뷰 제목이 글자 단위로 쪼개져 흘러내리던 것(<code>서연/(가/명)·/고2/우울//자살사/고</code>) — <code>.lh-preview__hero</code>의 2열 트랙 max가 <code>auto</code>라 액션 열이 280px까지 먼저 자라고 제목 열이 47px만 남은 게 원인. flex 전환 + <code>flex:1 1 340px</code>로 최소 폭 확보, <code>overflow-wrap:anywhere</code>→<code>break-word</code>. 제목 열 47px→343px. ㉯ 세션 카드 본문이 67px로 뭉개지던 것 — 같은 뷰포트에서도 카드 폭이 391/431/794px로 달라 <code>@media</code>로는 못 잡아 저장소 최초로 <code>@container</code>를 도입해 카드 폭 기준으로 접었다. 본문 67→211px. ㉰ 리캡 아바타가 320px에서 48px 잘리던 것 — 인라인 <code>style={{width:196}}</code>이 암시적 트랙을 부풀린 게 원인, <code>minmax(0,1fr)</code>+<code>min-width:0</code>+<code>aspect-ratio:1</code>로 해소. <b>검증</b>: typecheck·check:design-ssot 통과, raw color 예산 전부 이내(learner-home 21/25 · session 215/216 · session-review 29/29 · login 10/11), <code>breakpoint-sweep</code> 7/7(관용 목록을 비운 뒤 재실행도 7/7), <code>layout-visual-gate</code> 15/15, learner+teacher+session-layout 44/44, admin·learner·teacher 66/66. 폰트 self-host는 <code>cdn.jsdelivr.net</code> 요청 0건·<code>document.fonts.check()</code> true·woff2 200으로, 테마 기본값은 localStorage 빈 컨텍스트에서 light/dark 양방향으로 실증했다. <b>주의</b>: <code>session-review.css</code> raw color가 29/29로 여유 0이라 하나만 더 넣어도 게이트가 깨진다. <code>.lh-recap__avatar .vg-avatar__stage</code>에 이 파일 최초의 <code>!important</code>가 들어갔다(ClientAvatar가 높이를 인라인으로 박아 다른 수단이 없음. 컴포넌트가 size를 CSS 변수로 노출하면 제거 가능). <code>@container</code>는 저장소 최초 사용이라 구형 브라우저에서는 수정 전 동작으로 폴백된다. <b>미결(소유자 결정)</b>: <code>/learn</code> 메트릭 카드는 여전히 "평가 부족"을 쓴다(⑤가 <code>/teach/analysis</code>로 스코프됨), persona-studio 사용 현황 표는 ≤760px에서 뒤 2개 지표를 숨기는 기존 설계를 유지했다. 로컬 작업트리 기준이며 배포 전이다.</p>
|
||
<p class="dg-note">144차 적용(2026-07-27): <b>브레이크포인트 경계 전수 스윕</b>. 142차 접근성 감사가 1440·390 두 폭만 봤고 기존 <code>layout-visual-gate</code>도 7개 폭(390/720/861/900/1024/1280/1440)만 검사한다는 소유자 지적에서 출발했다. CSS 전체에서 고유 width 브레이크포인트 <b>25개</b>(340~1420)를 추출해 파일→페이지로 매핑하고, 페이지마다 자기에게 영향을 주는 브레이크포인트의 <b>B-1 / B / B+1</b> 과 실기기 대표 폭을 조합해(페이지당 17~36폭 × 높이 2종) 순회하며 <b>요소 겹침 · 텍스트 잘림 · 폭 붕괴 · 화면 밖 이탈 · 문서 오버플로</b>를 계산으로 검출했다. 1차 스윕에서 <b>156개 문제 뷰포트 / 11종 결함</b>이 나왔다. <b>수정한 결함</b>: ① <b>설정 서브네비 라벨 전면 소멸</b>(721~1080px 전 구간) — 공용 <code>@media (max-width:1080px)</code>의 <code>.vg-set__rail{display:grid; grid-template-columns:minmax(220px,280px) minmax(0,1fr)}</code>가 <code>body[data-page="settings"]</code>까지 덮어써 244px 레일 안이 다시 2열로 쪼개졌고 둘째 트랙이 0px로 붕괴, nav 폭 0 · nav-item 28px · 라벨 span 7개(계정/AI 운영/지원 요청/동의/테마/알림/음성) 폭 0이 됐다. 721~980px 는 별개로 <code>.vg-set__nav-item span{display:none}</code>이 라벨을 통째로 숨기고 있었다. 721~1080px 를 상단 가로 탭으로 통일해 라벨을 항상 노출한다. ② <b>세션 프리스타트 목표 fieldset 겹침</b>(전 폭 72뷰포트) — <code>.sx-prestart__note</code>의 <code>margin-top:-12px</code> 음수 마진이 앞 형제 <code>fieldset.sx-goals</code> 마지막 줄을 88x12px 파고들었다. ③ <b>프리스타트 dd·desc 세로 잘림</b> — <code>-webkit-line-clamp:2</code>가 좁은 폭에서 문장을 잘라먹던 것을 해제하고, 데스크톱 한 화면 제약은 <code>max-height + overflow-y:auto</code>로 대체. ④ <b>프리스타트 우측 "진행 초점" 패널 195px 잘림</b>(1041~1240px, 200개 폭) — 3열 <code>minmax(220px,…) minmax(360px,…) minmax(260px,…)</code>는 최소 888px를 요구하는데 이 구간의 실제 컨테이너는 735~886px뿐이라 트랙이 축소되지 못하고 우측 패널이 통째로 잘렸다. <code>overflow-x:hidden</code>이라 스크롤도 안 되고 <b>문서 오버플로는 0</b>이라 기존 "contained" 게이트가 구조적으로 못 잡던 유형이다. 트랙 최소치를 0으로 낮추고 2열 전환점을 1040→1260px로 올려 이중 안전망을 뒀다. ⑤ <b>티켓 필터 "초기화" 버튼 화면 밖 이탈</b>(1180~1257px) — 7트랙 grid가 최소 954px를 요구하는데 셸 사이드바를 뺀 컨테이너가 876~950px라 마지막 트랙이 밀려났다. 4트랙 2줄로 낮추고 유연 트랙 최소치를 0으로. ⑥ <b>페르소나 이름 세로 잘림</b>(320px) — 이름 칸이 150.6px뿐이라 3줄이 필요한데 <code>line-clamp:2</code>에 걸려 3번째 줄이 사라졌다. ≤420px 밴드에서 3줄 허용. <b>오탐으로 판정해 고치지 않은 것</b>: <code>.aic-ledger b</code>·<code>.lh-metric-card b</code>의 2px 세로 초과는 <code>line-height</code>가 Pretendard content-area(ascent+descent≈1.09em)보다 작아 생기는 폰트메트릭 아티팩트다. overflow hidden/visible 6x DPR 캡처를 20개 폭에서 바이트 비교해 <code>pixelDiff=0</code>(잘린 픽셀 0)을 확인했고, 1180/1280px에서는 같은 렌더링인데 반올림 방향만 바뀌어 탐지에서 빠지는 것도 증거다. <code>.vg-set__voice-info .vd</code>(의도된 <code>line-clamp:2</code>)와 <code>.vg-set__skel-sr</code>(visually-hidden), <code>.vg-avatar__stage</code>(<code>border-radius:50%</code> 원형 마스크)도 오탐이다. <b>2차 스윕</b>으로 1차가 못 본 사각지대(높이 브레이크포인트 620/820 경계, 탭 전환 등 인터랙션 후 상태, 와이드 폭 1600~2560, 초협소 320)를 14개 라우트에서 추가 순회했고 <b>새 결함 0</b>. <b>결과</b>: 진짜 결함 0(잔여 66뷰포트는 위 오탐 3종). <b>검증</b>: typecheck · check:design-ssot 통과, session.css raw color 216/216 유지, <code>layout-visual-gate</code> 15/15, session-layout+session-mvp+learner+settings <b>56/56</b>, 앞선 병렬 수리 검증에서 session-layout+settings+admin <b>55/55</b>. <b>미결(소유자 결정)</b>: 프리스타트에서 호소·대상 정보가 <code>.sx-prestart__case</code>(좌측 아바타 카드)와 <code>.sx-prestart__facts</code>(본문)에 <b>중복 렌더</b>된다(3열·2열 양쪽 모두, 이번 변경과 무관한 기존 IA 문제). 로컬 작업트리 기준이며 배포 전이다.</p>
|
||
<p class="dg-note">143차 적용(2026-07-27): 전 페이지·버튼 단위 <b>전수 E2E 순회 캠페인</b>. IA 전 라우트(로그인/온보딩/승인대기/학습자 3뷰·세션·리뷰/교수자 콘솔·분석·페르소나/관리자 5섹션/설정/아바타 랩)의 기능 412개를 소스 기준 인벤토리로 만들어 <code>docs/ops/e2e-full-sweep-2026-07-27.md</code> 체크리스트 백로그(- [ ]/- [x])로 관리하고, RED→GREEN으로 결함을 수정했다. 신규 스펙 10파일 <code>e2e/full-sweep-*.spec.ts</code>(수집 424의 절반가량)를 추가했고, 소유자 결정 대기 항목은 test.fixme + 백로그 B3-신규 6건으로 분리했다. <b>수정한 결함(RED 재현→GREEN)</b>: ① <code>/admin/ai</code> 부트 watchdog 오탐 오버레이(index.html 셀렉터에 <code>.aic</code> 추가), ② 한국어 PII 마스킹이 "유의점은·방치되는·마무리했을" 같은 일반 단어를 <code>[NAME]</code>으로 치환해 평가 문장을 훼손하던 과잉 마스킹(guardrail 꼬리 글자 가드), ③ 온보딩 연락처 무검증(서버 pydantic 검증+클라이언트 안내), ④ 리뷰 요약 조사 오류("하늘와의"→받침 인식 와/과)와 난도 원문 노출(easy→기초), ⑤ /pending 새로고침 무피드백+아이콘 오용(refresh 아이콘 신설), ⑥ 학습 대시보드 탭 패널 aria-label 고정, ⑦ 회기 종료 다이얼로그 반말 문체, ⑧ 설정 스크롤 스파이 마지막 섹션 미활성(0 임계값+밴드 최하단 규칙+바닥 폴백), ⑨ 모바일 세션 겹침 2건(타임바 <code>:has</code> 4행 템플릿, 코치 넛지 플로우 배치), ⑩ <b>관리자 콘솔 무한 렌더 루프 프리즈</b>(TanStack <code>autoResetPageIndex</code>↔setState — users/services 메모화+<code>autoResetPageIndex:false</code>, 결정적 재현 스펙 2건을 회귀 가드로 유지), ⑪ <b>복수 코호트 조용한 유실</b>(POST/PATCH <code>/admin/users</code>가 cohort_ids 첫 항목만 저장 → 콤마 직렬화+알림 SQL 배열 겹침 비교), ⑫ <b>공개 페르소나 보관 전면 불능</b>(DELETE <code>/personas/{id}</code>가 실DB에서 항상 503 — <code>archive_persona_family</code> UPDATE…FROM RETURNING의 무한정 <code>code</code> 모호성 → card. 한정, 실API RED 503→GREEN archived 실측). 마무리 탐색 순회(실브라우저 6에이전트, 68항목)는 67 GREEN·감정 밸런스 차트 데이터 소스 미구현(서버가 항상 빈 배열 — 사문 렌더 경로, 로드맵 기록) 1건을 추가로 확인. <b>검증</b>: 백엔드 pytest <code>412 passed</code>(신규 회귀 테스트 8개 포함), gateway 29 passed, typecheck/build/check:api-types/design-ssot/dead-code/duplication(0.03%) 통과, layout-visual-gate 15/15, session-layout 8/8, 최종 <code>npm run e2e</code> 재완주 — 병렬 424 수집 그린(fixme 4·모바일 의도 숨김 2 스킵), 직렬 <code>49/49 passed</code>(14.4m), exit 0. 열린 항목: B3-신규 6건(설정 아바타 변경/동의 철회·재동의 흐름/첫 추천 난도/분석 페이지네이션/저작 레일 잔존 마크업/dev-login 재승인 정책), 공개 OAuth 스모크는 기존 B2 유지. 로컬 작업트리 기준이며 배포 전이다.</p>
|
||
<p class="dg-note">142차 적용(2026-07-27): <code>design-taste-frontend</code> 스킬의 보편 규칙(§4.5 대비 검사, §6.B/6.C 모션·다크모드, §9.F/9.G AI tell)을 기준으로 <b>전 20개 라우트를 3역할·라이트/다크·데스크톱/모바일 54개 조합</b>으로 순회 감사하고 결함을 수정했다. 감사는 눈대중이 아니라 페이지마다 WCAG 대비비를 계산하는 Playwright 스크립트로 측정했다. <b>(A) 라이트 팔레트 AA 미달</b> — <code>--ink-3</code>(캡션·메타)가 모든 라이트 표면에서 2.7~3.1:1, <code>--neutral-400</code>(placeholder)이 2.4:1이었다. 색조를 유지한 채 <code>#6a6c65</code>/<code>#6f6e68</code>로 낮추고 위계 유지를 위해 <code>--ink-2</code>도 <code>#4d514b</code>로 조정했다(강도 13.9 · 본문 6.9 · 캡션 4.6:1). 다크는 <code>--ink-3</code> <code>#979a8f</code>, <code>--neutral-400</code> <code>#949b8f</code>. <b>(B) 다크 토큰 이중 진실원본</b> — 다크 블록이 <code>--text-muted</code> 등을 <code>var(--ink-3)</code> 별칭 대신 값으로 하드코딩해 primitive 수정이 다크에 반영되지 않았다. 라이트와 동일하게 별칭으로 통일했다. <b>(C) 채움색을 글자색으로 전용</b> — <code>--accent</code>는 채움 기준값이라 어두운 표면 위 본문 크기에서 4.3:1이다. <code>--text-accent</code>(= <code>--accent-deep</code>) 별칭을 신설해 kicker·secondary 버튼·역할 라벨 등 텍스트 용도를 옮겼다. <b>(D) 로그인 안내 배너 1.19:1</b> — <code>.lg-config</code>가 다크 패널 전제(반투명 갈색 + 밝은 앰버)라 라이트에서 사실상 읽을 수 없었다. 테마별로 스왑되는 warn 의미 토큰으로 교체(5.3:1). <b>(E) 미정의 CSS 변수 12종</b> — <code>--text-dim</code>·<code>--good-solid</code>·<code>--border-faint</code>·<code>--fs-xl</code> 등이 정의 없이 참조돼 색/배경이 조용히 무효화되고 있었고, <code>learner-home</code>의 <code>--bg-inset</code>/<code>--border-soft</code> 폴백은 다크 전제 값이 라이트 페이지에 적용되고 있었다. 전부 기존 의미 토큰으로 매핑했다. <b>(F) 학습자 분석 테이블 컬럼 침범</b> — <code>.pf-learner-row__id</code>의 암시적 그리드 트랙이 자식의 <code>width:max-content</code>를 수용해 261px 셀이 288px로 늘어나 <code>max-width:100%</code>가 무력화됐고, 긴 이름 28행이 옆 컬럼을 27px 침범했다. <code>minmax(0,1fr)</code>로 고정해 0건. <b>(G) 한글 조판</b> — <code>word-break</code> 기본값 때문에 "오전"이 "오/전", "검토 대기"가 "검/토 대기"로 쪼개졌다(5건). 세션 화면이 쓰던 <code>keep-all</code> + <code>overflow-wrap:break-word</code>를 전역으로 올려 0건. <b>(H) 탭 타깃</b> — WCAG 2.2 SC 2.5.8(24px) 기준으로 약관 동의 라벨 19px→28px, 학습자 이름 버튼 19px→25px, 설정 range 슬라이더 16px→24px(투명 테두리 + <code>background-clip:content-box</code>로 시각 크기는 유지). <b>(I) LCP</b> — 렌더 블로킹 CDN 폰트에 <code>preconnect</code>/<code>dns-prefetch</code> 추가(Pretendard self-host는 별도 과제). <b>결과</b>: 54개 조합 전부 대비 위반 0 · 탭 타깃 위반 0 · 라벨/alt 누락 0 · 가로 오버플로 0(감사 전: 대비 2,043건, 탭 타깃 355건). <b>검증</b>: <code>npm run typecheck</code>, <code>check:design-ssot</code> 통과, <code>layout-visual-gate</code> 15/15, <code>session-layout</code> 8/8, auth-visual·teacher·admin·session-review·settings·learner 포함 E2E 87 passed + 14 passed. <b>미결(소유자 결정 필요)</b>: kicker의 accent dot이 기본 <code>true</code>라 회기 화면 한 곳에 11개 이상 찍히는 점(§9.F는 장식 dot 0을 요구, 다만 DESIGN_CONCEPT가 명시한 강조 장치라 임의 변경하지 않음), 영문 대문자 eyebrow(<code>SPEND CONTROL</code>·<code>RUNTIME ROUTE</code>·<code>DAILY JOURNAL</code>·<code>SESSION</code>·<code>SUPERVISOR</code>)의 한글화, 섹션당 eyebrow 밀도(§4.7은 섹션 3개당 1개), 중점(·) 라인당 다중 사용(<code>/teach</code> 33행), <code>prefers-color-scheme</code> 무시하고 다크 고정인 기본 테마(§8.C), 관리자 AI 비용 표시 정밀도(<code>$37.6560</code>·<code>$0.102885</code>). 로컬 작업트리 기준이며 배포 전이다.</p>
|
||
<p class="dg-note">141차 적용(2026-07-15): 공통 <code>Surface inset</code>는 중첩 카드처럼 보이던 border와 radius를 제거하고 반투명 광원·blur만 유지한다. 관리자 사용자 목록은 사용자별 <code>article</code> grid와 겹치던 <code>ad-user__meta</code>를 제거하고 <code>@tanstack/react-table</code> 기반 semantic table로 전환했다. 사용자/역할/관리 권한/승인 상태/소속/코호트/활성 회기/최근 접속/작업 열을 고정하고, 헤더 클릭 정렬과 sticky header, 1440px 최소 폭을 제공한다. 좁은 화면에서는 문서가 넓어지지 않고 표 region만 가로 스크롤된다. <b>검증</b>: typecheck, 관리자 사용자 생성·승인·편집·비활성화 및 모바일 스크롤 focused E2E 4/4, layout visual gate 15/15. 로컬 작업트리 기준이며 배포 전이다.</p>
|
||
<p class="dg-note">136차 적용(2026-07-15): 135차의 학습 대시보드 한정 보태니컬 스타일을 전 페이지 공통 디자인 시스템으로 승격했다. <code>AppShell</code>은 역할·라우트와 무관하게 단일 <code>vg-shell--botanical</code> 크롬을 사용하며, 과거 관리자·교수자·대시보드 전용 톱바/GNB override를 삭제했다. GNB는 라이트/다크 고해상도 투명 원본(846×1858, 922×1706)을 사용하고 이미지 시작점을 메뉴 아래로 내려 텍스트 침범을 막는다. 데스크톱 GNB는 shell body 안에서 <code>top:0</code>, 모바일 drawer에서만 <code>top:var(--topbar-h)</code>를 사용해 상단 빈 공간을 없앴다. 공통 캔버스는 우상단 보태니컬 배경과 같은 이미지를 180° 회전한 좌하단 배경을 함께 사용하며, main은 투명하게 둔다. 표면은 셸 SSOT에서 큰 패널용 glass와 내부 행·필드·요약용 inset glass 두 단계로 통합해 학습자·교수자·관리자·설정·페르소나·회기 리뷰·진행 세션의 모바일 재배치까지 같은 반투명 그라데이션, 저채도 황금 hairline, blur/saturation을 사용한다. 로그인·온보딩·승인대기도 같은 글래스 패널을 쓴다. 라이트 테마는 흰 카드와 흰 캔버스가 겹쳐 유리 효과가 사라지던 토큰을 수정해, 반투명 본체+스페큘러 광원 2층, 안쪽 굴절선, 샴페인 hairline과 세이지/골드 캔버스 광원을 사용한다. 모바일에서도 보태니컬 캔버스를 제거하지 않는다. 학습 대시보드의 <code>오늘의 회기</code>/<code>기록 · 리뷰</code>/<code>성장 지표</code> 탭과 회기 리뷰의 <code>축어록</code>/<code>피드백</code>/<code>워크시트</code> 탭은 모든 폭에서 유지한다. Live2D 주변의 이중 외곽 컨테이너는 제거하고 투명 clipping 영역에서 캐릭터·잎 장식만 우하단으로 잘리게 했다. <code>object-separate --check</code>로 로컬 GPU 분리 런타임을 확인했고, 제공 원본이 정상 RGBA alpha(0–255)라 재분리 없이 원본을 사용했다. <b>검증</b>: <code>npm run typecheck</code>, <code>npm run build</code>, layout-visual-gate <code>15/15</code>, learner/session-layout/session-review desktop+mobile <code>46/46</code>, 라이트·다크·관리자·교수자·설정·리뷰·진행 세션 1440px 및 390px 스크린샷 직접 확인. 로컬 작업트리 기준이며 배포 전이다.</p>
|
||
<p class="dg-note">135차 적용(2026-07-15): 제공된 <code>Vignette-Asset-Set</code>의 512px 최적화 PNG 14종을 <code>public/design-elements/learner-dashboard/</code>로 자산화하고 학습 대시보드(<code>/learn</code>)에만 보태니컬 시안을 적용했다. 라이트/다크 각각의 사이드바·우상단 배경과 메트릭 카드 4종·AI 코치 장식을 테마별로 교체하며, 카드 표면은 크림/차콜의 비대칭 광원 그라데이션과 저채도 황금 헤어라인을 사용한다. 이 범위 제한은 136차에서 전 페이지 공통 글래스 시스템으로 대체됐다. <code>layout-visual-gate</code>는 라이트 테마 자산 연결까지 computed style로 확인하고 테마를 다크로 복귀한다. <b>검증</b>: <code>npm run typecheck</code>, <code>npm run build</code>, layout-visual-gate <code>12/12</code>, session-layout <code>8/8</code>, learner desktop/mobile <code>14/14</code>, 라이트·다크 1440px 및 다크 390px 스크린샷 직접 확인. 로컬 작업트리 기준이며 배포 전이다.</p>
|
||
<p class="dg-note">135차 적용(2026-07-15): 소유자 후속 결정으로 회기 리뷰 탭을 중간 폭 한정에서 <b>전 폭 공통</b>으로 확장 완성했다. 소유자가 <code>SessionReview.tsx</code>·e2e에 남긴 방향(탭 바 전 폭 노출, "기존 데스크톱 3컬럼은 시선 이동과 빈 공간이 커서 사용하지 않는다")에 맞춰, 데스크톱에서도 축어록/피드백/워크시트 중 활성 탭 하나만 전폭 단일 컬럼으로 보여주도록 CSS를 정리했다 — 파일 끝의 '전 폭 탭' 블록을 단일 진실로 삼고 앞부분의 구 3컬럼·중간폭 전용 규칙과 중복 정의를 제거(같은 셀렉터의 후행 전역 규칙이 탭별 컬럼 규칙을 덮던 충돌 해소). 피드백 탭은 인사이트 흐름 위 / 평가·검토 레일 아래 세로 스택. e2e는 전 폭 탭 계약으로 갱신: <code>session-review.spec</code> 영역 헬퍼(활성 탭 단일 컬럼 + 인사이트→평가 세로 순서 + 밴드가 그리드 위), <code>teacher.spec</code>·<code>session-persistence.spec</code>(워크시트/프리포스트/검토 카드 단언 전 해당 탭 오픈 — '평가 대기' 대기 자리표시자 포함), layout gate는 소유자 갱신본(전 폭 columnCount 1 계약) 그대로 통과. 검증: typecheck, 브라우저 실측(데스크톱 2005px·좁은 폭 603px 탭 전환/스택/무오버플로), <code>session-review+teacher+session-mvp</code> 64/64, layout gate 12/12, persistence worksheet·prepost @single-run 2/2, 로컬 프로젝트 전체 재실행 그린(동시 실행 플레이크 2건은 단독 재실행 통과). 부수: voice consent-withdrawal e2e가 공유 learner 계정 데이터를 파기하는 특성 확인 — 로컬 수동 점검 시 계정 온보딩 재완료 필요(<code>terms_accepted</code>/<code>privacy_accepted</code> 포함). 로컬 작업트리 기준이며 배포 전이다.</p>
|
||
<p class="dg-note">140차 적용(2026-07-15): <code>/teach/personas</code>를 기존 3열 저작 스튜디오에서 상단 horizontal tab 기반 워크스페이스로 재구성했다. 1차 탭은 <b>대시보드 / 카탈로그 / 페르소나</b>, 페르소나 내부 2차 탭은 <b>전체 페르소나 / 검수 현황</b>이다. 대시보드는 공개·활성 페르소나, 실제 학습 인원·진행/종료 세션, 저장된 상담자 턴 평가와 라포 신호만 집계한 성과·진척도, 검수 요약 및 페르소나별 최근 기록을 표시한다. 카탈로그는 승인본 공개·보관·버전 규칙과 이론 분포를 설명한다. 전체 목록은 검색/구분 필터가 있는 table로 시스템(P1~P11)/사용자 생성, 공개·검수·초안 상태, 버전과 행 작업을 한 번에 보여준다. 행 상세는 소개·학습 현황·설정 탭 drilldown이며, 신규/수정은 자료→초안→설정→검토 4단계 작업면과 이전/다음 이동, 브라우저+서버 중간 저장을 사용한다. 모바일 table은 가로 밀림 대신 카드형 행으로 재배치한다. 기존 RAG·draft·검수·승인·archive API 계약은 유지했다. 부수로 기존 동시 작업의 <code>LearnerHome.tsx</code> 중복 <code>className</code> 컴파일 오류를 동일 interactive surface 호출로 병합했다. <b>검증</b>: <code>npm run typecheck</code>, <code>npm run build</code>, 페르소나 focused E2E 6/6, 새 IA·작성 step focused 2/2, layout visual gate 14/14, session-layout desktop/mobile 8/8, 1440px·390px 대시보드/상세/작성 캡처 직접 확인. 로컬 작업트리 기준이며 배포 전이다.</p>
|
||
<p class="dg-note">139차 적용(2026-07-15): 앱 셸을 뷰포트 높이의 고정 프레임으로 바꿔 톱바와 GNB는 화면에 남고 <code>vg-main</code>만 독립적으로 세로 스크롤하게 했다. 문서 전체 위에 절대 위치로 떠 있던 좌하단 보태니컬 <code>vg-shell__body::after</code> 레이어는 제거하고, GNB 장식은 <code>vg-nav</code> 자체 배경에만 유지했다. 모바일 가로 GNB도 같은 프레임의 첫 행에 고정된다. learner layout gate는 메인 scrollTop 변화, GNB 위치 변화 1px 이하, window scroll 불변을 390~1440px에서 확인한다. 검증: layout visual gate 13/13, session layout desktop/mobile 8/8. 전체 <code>npm run typecheck</code>/<code>build</code>는 동시 작업 중인 <code>PersonaStudio.tsx</code> 미사용 선언 오류로 차단됐으며 이번 셸 변경과 무관하다. 로컬 작업트리 기준이며 배포 전이다.</p>
|
||
<p class="dg-note">138차 적용(2026-07-15): 학습 대시보드 메트릭 카드의 장식 이미지는 카드별 <code>--lh-art-right</code>/<code>--lh-art-bottom</code>/<code>--lh-art-width</code>/<code>--lh-art-height</code> 변수로 크롭을 독립 조정하는 현행 구조를 유지했다. 회기 리캡 아바타 뒤의 잎 장식은 인물 식별을 흐려 제거하고, <code>lh-session-focus</code> 패널 우측 하단으로 이동해 <code>right:-20px</code>, <code>bottom:-28px</code>에서 보더에 걸려 잘리게 했다. 검증: <code>npm run typecheck</code>, <code>npm run build</code>, learner home layout visual gate 1/1(390~1440px 7개 폭 + 1200px 라이트/다크), 1200px 라이트·다크 캡처 직접 확인. 로컬 작업트리 기준이며 배포 전이다.</p>
|
||
<p class="dg-note">137차 적용(2026-07-15): 회기 시작 전 화면의 <code>sx-prestart</code> 고정 1120px 폭을 제거해 <code>sx-head</code>와 같은 가용 폭을 사용하고, 내담자·설정·진행 초점 3열을 비율형으로 재조정했다. 진행 초점은 고정 문구 3개 대신 현재 단계, 선택 이론의 실제 대화 기준, 선택 목표, 회기 시간·알림·안전 원칙을 반영하는 동적 브리핑으로 확장했다. layout visual gate에 전 폭 헤더/준비 카드 폭 일치와 브리핑 항목 회귀를 추가했다. 검증: <code>npm run typecheck</code>, <code>npm run build</code>, session prestart layout visual gate 1/1(390~1440px 7개 폭), 1440px·1024px 캡처 직접 확인. 로컬 작업트리 기준이며 배포 전이다.</p>
|
||
<p class="dg-note">134차 적용(2026-07-15): 소유자 실사용 피드백 3건과 그 과정에서 드러난 회귀·결함을 일괄 처리했다. <b>(A) 중간 폭 가로 탭 전환</b> — 1158px 실측 피드백("2컬럼 굳이 필요한가, 상단 horizontal tab이 낫지 않나" + "사례 워크시트까지 탭에")에 따라 <code>SessionReview</code>는 ≤1180px에서 3컬럼 대신 상단 가로 탭(<code>축어록</code>/<code>피드백</code>/<code>워크시트</code>)으로 영역을 전환한다(<code>.sr-tabs</code> + <code>.sr-cols--tab-*</code>, 데스크톱 3컬럼 무변경). 같은 패턴으로 학습 대시보드(<code>/learn</code>)도 ≤1180px에서 <code>오늘의 회기</code>/<code>기록 · 리뷰</code>/<code>성장 지표</code> 탭으로 재구성했다(<code>.lh-tabs</code> + <code>.lh-grid--tab-*</code>, 메트릭 카드·헤더는 탭 밖 상시 노출). e2e는 탭 인식 헬퍼로 갱신(session-review 모바일 supervisor 계열 5건 포함). <b>(B) 관리자 배경 rect 제거</b> — <code>/admin</code> 우측 상단의 이상한 밝은 사각형은 <code>.ad-root::before</code>가 <code>clinical-paper-ambient.png</code>(560×420, opacity 0.045)를 다크 배경 위에 깔던 장식으로, 이미지 경계가 rect로 드러나던 것 — 규칙 자체를 제거했다. <b>(C) 회귀·결함 수정</b> — ① 신규 회기 시작 직후 URL 전환이 resume 재로딩을 트리거해 서버 스냅샷(0턴)이 전송 직후의 낙관적 학습자 발화를 덮어쓰던 race를 <code>justStartedSessionRef</code> 가드로 수정(@single-run SSE persistence 재현·해결). 같은 가드로 신규 회기 초기 코칭 이력 동기화는 수행하되 stale quota가 기본 기회 수를 덮지 않게 <code>applyQuota:false</code>로 결정론화(이전에는 재로딩 race 승패에 의존). ② 모바일(≤880px) 코칭 카드에서 <code>근거 보기</code>/<code>이력 보기</code> 버튼을 숨기던 규칙 제거 — 근거 감사 접근은 코칭 신뢰성의 핵심이라 모바일에서도 유지(공간 절약은 발화 예시 blockquote 숨김까지만). ③ <b>안전 결함</b>: 모바일 압축 레이아웃이 위기 신호 시에도 안전 패널(109 리소스 포함)을 통째로 숨기던 문제를 <code>sx-has-safety</code> 페이지 클래스 + 위기 시 노출 규칙(≤1180/≤880 양쪽)으로 수정. ④ 세션 프리플라이트(목표 선택 포함)가 데스크톱에서 문서 스크롤을 6~13px 넘치던 회귀를 <code>.vg-main:has(.sx-page--prestart)</code> 패딩 축소로 수정, 모바일은 세로 스크롤 허용으로 e2e 계약 조정. ⑤ e2e 드리프트 정리: 학습 현황 라벨(<code>aria-label</code>) 정합, readiness CTA 단언 갱신, dev 엔진 음성 턴 실측(90초+)에 맞춘 voice persistence 타임아웃 상향(150s/240s). <b>검증</b>: <code>npm run typecheck</code>, 로컬 프로젝트 전체 <code>164 passed</code>(desktop+mobile), layout-visual-gate 12/12, 실엔진 @single-run 직렬 전체 <code>44 passed</code>(20.8분 — race·타임아웃 수정 반영 후 전 그린), 브라우저 실측(learn 탭 3종 전환·데스크톱 1440 회귀·admin rect 소멸 스크린샷). 로컬 작업트리 기준이며 배포 전이다.</p>
|
||
<p class="dg-note">133차 적용(2026-07-15): 두 갈래를 일괄 처리했다. <b>(A) 회기 리뷰 화면 재설계</b> — 소유자 피드백(도킹된 리뷰 요약 레일이 스크롤을 따라다니며 콘텐츠를 가림 + 3컬럼 빈 공간 과다 + 세로 나열 요약 카드)에 따라 <code>SessionReview</code> 레이아웃을 재구성했다. 리뷰 요약은 sticky 제거 후 상단 전폭 밴드(요약 문장 + 팩트 칩 3개 가로 + 액션 버튼 가로)로 승격, 본문은 행 높이 공유로 데드 갭을 만들던 grid-areas 행 배치 대신 컬럼별 자체 흐름 스택(<code>.sr-left</code>/<code>.sr-right</code> 래퍼 grid)으로 바꿔 좌 인사이트 / 중앙 축어록(주역) / 우 평가 스택 구조를 만들었고, 워크시트는 전폭 하단으로 내렸다. 사이드 레일 대기 자리표시자는 컴팩트 스타일로 축소. e2e 기하 계약(session-review.spec 헬퍼·layout-visual-gate 학습자 워크벤치)은 새 설계 기준(밴드가 그리드 위, 3컬럼 chart→transcript→rubric 순서)으로 갱신했다. 검증: <code>session-review.spec.ts</code> 24/24, layout gate 12/12, typecheck/build. <b>(B) 연구팀 태깅 축어록 4종 활용</b> — 첫회기 BPS·게슈탈트 3회기·자해 DBT·우울 CBT 태깅 엑셀을 실명 사전 치환 후 신규 <code>POST /personas/sources/upload</code>로 KB 등록(101청크 색인·임베딩, hash-only 증거, 원본 미저장). <code>taxonomy.py</code> v1.1.0 — 연구팀 태그 정합으로 Technique 9종·ClientState 9종 append-only 추가(<code>docs/taxonomy.md</code> 조작적 정의 동반). 가드레일 <code>_MEANS_TERMS</code> 자해 도구·방법 확장(경계 설정 표현 오차단 방지 테스트 포함). 라이브 코칭 소스팩 3종 신설·색인(DBT 위기·전념 / 게슈탈트 무브 / 부모 첫회기 무브, 17청크). 등록 근거로 RAG 페르소나 초안 4종 생성·저장 — <b>P8 미영</b>(45세 부모측·통합/CBT), <b>P9 유진</b>(자해 DBT·hard·게이티드 — 초안 warnings에 '교수 검수·안전장치 테스트 전 활성화 금지' 명시), <b>P10 선혜</b>(게슈탈트 한부모), <b>P11 정호</b>(Beck CBT 비자발 남성). 전부 <code>draft</code> 상태로 교수 검수 큐 노출 — 승인·활성화는 연구팀 몫(월권 방지). 부수 수정: 게이트웨이 <code>/v1/generate</code> 턴 타임아웃 120s 하드코딩 → <code>ENGINE_GENERATE_TIMEOUT_SECONDS</code>(기본 300s)로 설정화(대형 구조화 생성 대응). 실행 기록·임상팀 게이트 후속은 <code>ops/tagged-data-utilization-2026-07-15.md</code>. 검증: 백엔드 pytest <code>396 passed</code>, 게이트웨이 29 passed, typecheck/build. 로컬 작업트리 기준이며 배포 전이다.</p>
|
||
<p class="dg-note">132차 적용(2026-07-15): 회기 목표 선택 상한을 1~2개 → <b>1~4개</b>로 확장했다(소유자 지시). 백엔드 <code>goal_stages</code> 검증 <code>max_length=4</code>+중복 제거, 프론트 토글은 교체 로직 없이 자유 토글, 준비 화면 문구 <code>1~4개 선택</code>. 회의 스펙(2개 수준)과의 차이는 소유자 결정으로 기록한다. 검증: 백엔드 pytest 394 passed(목표 계약 테스트 4개 갱신), typecheck, 브라우저에서 4개 선택→시작→목표 배지 4개 확인.</p>
|
||
<p class="dg-note">131차 적용(2026-07-14): 2026-07-13 한신대 회의 결정(P1~P5)과 소유자 추가 지시(코칭 고도화·수치 상세·포탈·다크모드 채도·UI 컬랩싱)를 일괄 구현했다. <b>P1 시간 기반 회기</b> — 세션은 60분(<code>SESSION_DURATION_MINUTES</code>) 기준으로 진행하고 종료 10분 전 앰버 알람 바, 시간 만료 시 정리 유도 다이얼로그 강제 오픈, 유예(+10분) 이후 신규 턴은 서버가 <code>409 session_time_over</code>로 거부한다(텍스트/음성 공용). 시작 전 준비 화면에서 이번 회기 목표 단계를 1~2개 선택하는 <code>goal_stages</code> 계약을 추가했고(app.sessions.session_goals JSONB + 런타임 스키마 가드 + readiness probe 갱신), 목표를 달성해도 시간 내에는 계속 진행한다(목표 달성 넛지만 표시). <b>P2 누적 게이지·수치 상세</b> — 설계 1p <code>ops/session-progress-gauge-design-2026-07-14.md</code> 후 <code>build_session_progress()</code> 순수 파생으로 단계별 누적 게이지(라포 임계 대비 %, 회기 간 ×0.7 이월로 누적)와 방어(저항)/유효 개방도/라포 누적+이번 회기 증가분 %를 세션 트랙·관찰 신호 패널·상세 응답·스트림 done·대시보드 페르소나 카드에 노출했다. <b>코칭 고도화</b> — 미동작 원인 3중(기본 ambient 모드에서 호출 자체 차단 / 미커밋 audit 강등이 dev 무DB에서 정상 LLM 응답 폐기 / 3중 AND 충전 조건으로 사실상 충전 불가)을 근원 수정: 코칭 탭이 닫혀 있으면 기회를 소모하지 않고 컨텍스추얼 넛지 칩으로 대기시키고, <code>record_llm_call_audit</code>는 dev 폴백(무DB)을 기록 실패로 보지 않으며, 충전은 완화된 성과 조건 + 6턴 페이싱 충전으로 재설계했다. 프롬프트에 회기 목표·직전 코칭을 주입하고 폴백도 단계별 문장으로 개선했다. <b>P4 업로드</b> — 자유 양식 엑셀 경로가 없어 신규 구축: <code>POST /personas/sources/upload</code>(xlsx/csv→텍스트 결정론 변환, 원본 바이트 미저장·hash-only 증거) + PersonaStudio 파일 첨부. 파란 라벨 샘플 실업로드로 NAME 마스킹·chunk 색인·임베딩·raw hash-only를 확인했다. <b>P3 부하</b> — <code>scripts/load-test-sessions.py</code>로 20 동시 세션 생성 20/20(p95 125ms, RAG warm 병목 없음), 5 동시 실턴 p50 55s로 claude_cli 상주 풀 병목 실측 → 파일럿 전 <code>ENGINE_MODE=claude_api</code> 전환 권고. 이관 초안 <code>ops/server-migration-runbook-draft.md</code>, 파일럿 지침 <code>ops/pilot-usage-guide-draft.md</code>, 협업 가이드 <code>guides/collaboration-for-researchers.md</code>. <b>포탈</b> — 학습자 대시보드에 보완할 부분(평가 파생 약점 신호)·내담자별 라포 누적 게이지·연습 이정표(기존 미노출 achievements)를 추가하고 중간 폭(1181~1420px) 리캡 카드 붕괴를 수정했다. <b>다크모드 채도 완화</b> — imagegen2 시안 3종(<code>docs/design/darkmode-2026-07-14/</code>) 중 A(따뜻한 차콜+마른 세이지 <code>#7da294</code>대)를 채택해 tokens.css 다크 블록과 session/shell/login/settings/learner-home/session-review CSS의 하드코딩 고채도 틸을 일괄 저채도로 치환했다(라이트 모드 무변경). <b>컬랩싱</b> — 세션 화면 내담자 컨텍스트·안전 점검 패널 기본 접힘 + 위기 신호 시 안전 점검 강제 오픈, 관찰 신호 접기 토글. <b>검증</b>: 백엔드 pytest <code>393 passed</code>(신규 시간/목표 9종·게이지 7종·엑셀 변환 9종·코칭 충전 4종 포함), <code>npm run typecheck</code>/<code>generate:api-types</code>, <code>session-layout.spec.ts</code> 8/8, <code>layout-visual-gate.spec.ts</code> 12/12, 실브라우저 E2E(1분 회기 알람→만료→409, 코칭 넛지→실LLM 코칭 카드 3→2, 2회기 carry 65%→46%=×0.7 정확, 대시보드 실데이터). 회의 스펙의 '정리 발화 시 자동 종료'는 백엔드에 원래 존재하지 않는 것으로 실측 확인 — 수동 조기 종료 경로가 이를 대체하며 스펙 문서와의 차이는 이 노트가 기록한다. 로컬 작업트리 기준이며 배포 전이다.</p>
|
||
<p class="dg-note">20차 적용(2026-06-28): 신규 Google/SAML 사용자는 <code>account_status=pending</code>으로 시작하고 승인 전에는 <code>/pending</code> 안내 화면만 본다. <code>yunchan@twentyoz.kr</code>는 슈퍼 관리자 allowlist로 admin+approved를 받으며, <code>/admin/users</code>는 가입 승인 탭에서 pending 계정을 승인 또는 보류 처리한다.</p>
|
||
<p class="dg-note">21차 적용(2026-06-28): 관리자 페이지 진입권을 기본 역할과 분리해 <code>app_user.admin_access</code>로 저장한다. <code>AUTH_SUPER_ADMIN_EMAILS</code> 기본값은 <code>yunchan@twentyoz.kr</code>, <code>hoonjungkoo@hs.ac.kr</code>이며, 슈퍼 관리자는 학습자·교수자·관리자 공간 전환과 관리자 권한 부여/회수를 할 수 있다. 학생·교수 계정도 <code>admin_access=true</code>면 우측 상단 관리자 진입이 노출된다. 구성 슈퍼 관리자의 권한 회수와 계정 비활성화는 차단한다.</p>
|
||
<p class="dg-note">25차 적용(2026-06-28): <code>/admin/users</code>에서 허용 도메인 밖 이메일도 정확한 계정 단위로 강제 등록할 수 있다. Google/SAML/dev-login은 미리 등록된 이메일만 도메인 게이트 예외로 통과시키고, provider 로그인 시 기존 관리 row의 역할·코호트·승인 상태를 이어받는다. 미등록 외부 도메인 로그인은 계속 차단한다.</p>
|
||
<p class="dg-note">10차 적용(2026-06-27): 학습자 영역을 <code>/learn</code> 대시보드, <code>/learn/practice</code> 연습 대상, <code>/learn/history</code> 기록으로 분리했다. 홈은 최근/진행 회기를 기준으로 Live2D 아바타·감정 힌트·마지막 내담자 반응·학습자 마지막 발화·AI 코치 문장을 보여주는 세션 리캡 중심 대시보드로 바꾸고, 새 회기 시작은 학습 탭으로 낮췄다.</p>
|
||
<p class="dg-note">11차 적용(2026-06-27): 현재 <code>/learn</code> 화면을 다시 캡처한 뒤 UX 원칙(사용자 필요, progressive disclosure, 정보 향, adaptive layout)을 기준으로 이미지 생성 시안 3장을 만들었다. UI 지침에는 <code>11-learner-dashboard-redesign-concept-b.png</code>, <code>12-learner-dashboard-redesign-concept-b-alt.png</code>만 채택하고, 상담실 배경으로 나온 <code>10-learner-dashboard-redesign-concept-a.png</code>는 폐기했다. 구현은 회기 리캡과 AI 코치/이어하기 CTA를 홈 첫 블록으로 묶고, 독립 <code>다음 행동</code> 카드는 제거했다.</p>
|
||
<p class="dg-note">16차 적용(2026-06-28): 생성 시안과 더 맞추기 위해 <code>/learn</code> 하단을 <code>리뷰 대기</code>, <code>다음 연습 추천</code>, <code>최근 기록</code> 3분할 카드로 재배치했다. AI 코치 패널에서는 진행/리뷰 카운트와 보조 링크를 제거하고, 코칭 문장·3개 체크 힌트·단일 <code>이어하기</code> CTA만 남겼다. 최신 검증 캡처는 <code>docs/design-verification/learner-home-redesign/final-*.png</code>다.</p>
|
||
<p class="dg-note">18차 적용(2026-06-28): <code>/learn/history</code>는 표형 기존 회기 목록을 제거하고 <code>전체</code>, <code>진행 중</code>, <code>리뷰 필요</code>, <code>보관됨</code> 상태 필터와 검색, 카드형 회기 기록 작업대로 재구성했다. 설계 지침은 <code>docs/archive/design-concepts/prompts/learner-history-archive-redesign-2026-06-28.md</code>, 검증 캡처는 <code>docs/design-verification/learner-history-redesign/history-*.png</code>다.</p>
|
||
<p class="dg-note">22차 적용(2026-06-28): 회기 아카이브를 실제 저장 상태로 연결했다. 학습자 종료 회기는 <code>POST /sessions/{id}/archive</code>/<code>restore</code>로 보관·복원하고, <code>app.session_archive_state</code>는 삭제가 아니라 학습자별 보기 상태만 저장한다. 회기·턴·리뷰·공유 링크·연구/감사 증거는 유지하며, 보관된 회기는 <code>/learn/history</code>의 <code>보관됨</code> 필터와 dashboard <code>archived_sessions</code>에 반영되고 리뷰 대기 행동 큐에서는 제외된다.</p>
|
||
<p class="dg-note">24차 적용(2026-06-28): 운영 콘솔 <code>/admin/usage</code>의 DB 집계 쿼리에서 <code>ORDER BY tokens_in + tokens_out</code>가 PostgreSQL 원본 컬럼 참조로 해석되어 prod에서 503 fail-closed 되던 문제를 수정했다. provider/model별 정렬은 <code>SUM(tokens_in)+SUM(tokens_out)</code> 집계식 기준으로 바꿨고, public API 프로세스의 실제 HTTP smoke는 <code>source=database</code>, <code>durable=true</code>로 200을 반환한다.</p>
|
||
<p class="dg-note">23차 적용(2026-06-28): <code>app.persona_voice_map</code>을 실제 <code>/voice/ws</code> TTS voice 선택에 연결했다. 명시 query preset이 최우선이고, 없으면 세션의 <code>persona_id/persona_version</code> 또는 dev persona catalog의 voice map을 읽으며, unsupported provider는 기존 persona-code fallback으로 fail-safe 처리한다. seed materializer는 기본 OpenAI voice map row를 <code>ON CONFLICT DO NOTHING</code>으로 생성하고, dev 런타임 스키마 보강은 기존 DB의 <code>persona_voice_map</code> 누락도 함께 복구한다.</p>
|
||
<p class="dg-note">12차 적용(2026-06-27): 회기 리뷰 데스크톱 레이아웃은 sticky 요약 레일을 유지하되 트랜스크립트가 같은 컬럼을 공유하지 않도록 재배치했다. 왼쪽 요약 문장은 보조 브리핑 밀도로 낮추고, 축어록은 중앙 근거 작업대에 고정해 스크롤 중 레일이 발화 내용을 가리지 않게 했다.</p>
|
||
<p class="dg-note">13차 적용(2026-06-27): 회기 리뷰의 <code>적절한 개입</code> 등 평가 콜아웃은 제한 markdown을 기본 렌더링한다. <code>effective_openness</code>는 <code>effective openness(유효 개방도)</code>로 학술 용어화하고, 상담자 질문 발화는 <code>ReviewNote.quote</code> 인용 블록으로 분리해 카드 내부에서 시각화한다.</p>
|
||
<p class="dg-note">14차 적용(2026-06-27): 활성 상담 세션은 시작 직후 실제 <code>session_id</code> URL로 고정해 새로고침 후에도 같은 회기 상세를 복원한다. 전역 topbar는 몰입형 화면에서 계속 숨기되, 내부 세션바에 <code>기록으로</code>/<code>학습 홈</code> 이동을 추가했다. 종료된 세션 상세도 곧바로 리뷰로 밀어내지 않고 읽기 전용 기록 화면에 머물며, 리뷰 이동은 명시 버튼으로만 수행한다.</p>
|
||
<p class="dg-note">15차 적용(2026-06-28): 학습자·교수자·운영·페르소나 스튜디오 화면을 같은 작업형 폭 정책으로 정렬했다. 교수 콘솔은 검토 대기·위기 알림·페르소나 검수·저작실 순서가 DOM과 화면 모두 일치하고, 페르소나 스튜디오는 SSOT 첨부/RAG 생성/항목 편집/검증 큐가 한 작업면에서 이어지는 구조로 유지한다. 적용 리포트와 대표 캡처는 <code>docs/archive/ops/layout-research-2026-06-28/dashboard-layout-alignment-report.md</code>에 남겼다.</p>
|
||
<p class="dg-note">146차 적용(2026-07-28): 공개 <code>/login</code>에서 직전 배포 엔트리 <code>index-FVj73yvV.js</code>가 이미 삭제된 lazy 청크 <code>Login-BMWWhZXH.js</code>를 요청해 화면 진단으로 떨어진 배포 전환 결함을 실증·복구했다. 새 <code>chunkRecovery</code>는 Vite <code>vite:preloadError</code>를 받아 경로별로 문서를 정확히 한 번만 다시 받고, 정상 라우트 커밋 뒤 잠금을 해제한다. 연속 실패는 무한 새로고침하지 않고 ErrorBoundary로 보내며 진단 화면에 <b>최신 버전 다시 불러오기</b>·<b>홈으로 이동</b> 액션을 제공한다. Cloudflare Pages production <code>e7d40ffa</code>는 격리 작업공간에서 base commit <code>facc4ad2</code>+이번 3파일 수정만 빌드했고, 새 엔트리 <code>index-BBHxK-uX.js</code>와 함께 운영 직전 두 세대(<code>index-Dqr2stZ-.js</code>/<code>Login-DQyYRTcl.js</code>, <code>index-FVj73yvV.js</code>/<code>Login-BMWWhZXH.js</code>) 자산을 실제 JavaScript로 보존했다. <b>검증</b>: typecheck·build 통과, 개발 서버 재로드 잠금 focused 1/1, 프로덕션 preview 청크 첫 요청 강제 실패 1/1, 공개 도메인 동일 강제 실패 1/1, 공개 로그인 버튼 2개 enabled·브라우저 오류 0, API health/auth config 200.</p>
|
||
<p class="dg-note">147차 적용(2026-07-30): 재부팅·엔진 readiness 지연이 관리자 페이지 미노출로 전파되던 운영 결함을 분리했다. 기존 부팅/워치독은 <code>db=true</code>여도 <code>engine=false</code>면 전체 런타임을 실패로 보고 API·웹·터널까지 재시작할 수 있었고, 브라우저의 최초 <code>/auth/me</code>에는 timeout이 없어 <code>불러오는 중…</code>에 오래 머물 수 있었다. 이제 제어면 readiness는 <code>environment=prod + db=true</code>, 엔진 readiness는 별도 게이트다. 엔진 단독 장애 시 살아 있는 API·웹·cloudflared는 유지하고 엔진만 복구하며, 브라우저는 요청별 5초 상한·3회 재시도 후 같은 HttpOnly 세션으로 다시 연결하는 명시 화면을 낸다. <code>401</code>만 로그아웃으로 확정한다. <b>실증</b>: role admin·super-admin·delegated admin 각각 관리자 5경로 렌더, 인증 503 3회 뒤 재연결 2 focused passed, desktop/mobile 관리자 20 passed, typecheck·build, 백엔드 admin/auth 48 passed, PowerShell 5.1 parser 3파일 오류 0, 엔진 상태 검사 중 API PID <code>36824</code> 유지, watchdog healthy, 예약 작업 <code>LastTaskResult=0</code>, failcount <code>0</code>. Cloudflare Pages production <code>564dbed9</code>는 새 엔트리 <code>index-Cwtfyfq5.js</code>와 직전 3세대 lazy 자산을 함께 게시했다. 공개 청크 강제 실패 1 passed, 공개 임시 관리자 세션으로 5개 관리자 경로·사이드바·브라우저 오류 0을 확인하고 세션을 즉시 삭제했다. 마지막 강제 엔진 readiness도 실제 응답 <code>OK</code>로 통과해 공개 <code>/health</code>가 <code>status=ok</code>, <code>db=true</code>, <code>engine=true</code>로 회복됐다.</p>
|
||
<p class="dg-note">148차 적용(2026-07-30): 캐시·청크가 아니라 실제 슈퍼 관리자 계정의 durable entitlement와 cross-role navigation을 다시 추적했다. 운영 DB에서 primary role이 learner인 슈퍼 관리자 계정의 <code>admin_access=false</code>와 활성 learner 세션을 확인했고, 공개 <code>/auth/me</code>는 설정 기반으로 <code>super_admin/admin_access=true</code>를 계산했지만 기본 역할 사이드바에는 관리자 진입점이 없어 학습자 작업면으로 이동하면 운영 페이지가 UI에서 사라지는 silent path가 있었다. 이제 설정 기반 슈퍼 관리자/관리자 entitlement는 로그인·세션 복원 때 <code>app.app_user.admin_access=true</code>로 영속화하고, learner/teacher 사이드바에도 권한이 있으면 <code>운영 콘솔</code>을 표시한다. 실운영 계정 두 active session은 유지한 채 durable 접근권을 즉시 고정했고 API만 PID <code>15280</code>으로 교체했다. <b>실증</b>: backend auth/admin 49 passed, typecheck/build, PowerShell 5.1 parser 0 errors, admin·super-admin·delegated admin 5경로 및 cross-role 복귀 desktop/mobile 4 passed. Cloudflare Pages production <code>1332ccbe</code>는 격리 빌드 엔트리 <code>index-D4Twx0A1.js</code>와 직전 3개 배포 자산을 보존했다. 실제 Google 슈퍼 관리자 사용자 행에 연결한 임시 세션으로 공개 관리자 5경로, learner 화면의 운영 콘솔 링크, restored-tab 복원을 4 passed로 확인했고 임시 세션은 <code>DELETE 1</code>로 제거했다. 최종 public API는 <code>status=ok</code>, <code>environment=prod</code>, <code>db=true</code>, <code>engine=true</code>다.</p>
|
||
<p class="dg-note">149차 적용(2026-07-30): 148차의 관리자 접근권 영속화 배포 직후 신규 Google OAuth 콜백이 <code>managed user persistence unavailable; runtime fallback is disabled in prod</code> 503으로 실패한 회귀를 긴급 수정했다. 원인은 이메일 기반 기존 사용자와 OAuth external id를 연결하는 선행 UPDATE에서 nullable 관리자 플래그 <code>$7</code>의 PostgreSQL 타입을 명시하지 않아, 실제 쿼리가 <code>AmbiguousParameterError: could not determine data type of parameter $7</code>로 prepare 단계에서 실패한 것이었다. 선행 UPDATE와 본 upsert의 관리자 플래그를 모두 <code>::boolean</code>으로 고정하고, non-dev 저장 실패는 원래 예외를 서버 로그에 남기게 했다. <b>실증</b>: 운영 DB rollback 트랜잭션에서 수정 전 오류를 재현, 수정 후 실제 Google 사용자 행의 <code>upsert_managed_user()</code>가 learner·<code>admin_access=true</code>·approved로 통과, auth/admin backend 50 passed와 py_compile 통과. API만 PID <code>40992</code>로 재시작했고 local/public health는 prod·db=true·engine=true다. 같은 실제 사용자 행으로 새 브라우저 세션을 생성해 공개 <code>/auth/me</code> 200, email 일치, learner, <code>admin_access=true</code>, <code>super_admin=true</code>, approved를 확인한 뒤 임시 세션을 즉시 폐기했다.</p>
|
||
<p class="dg-note">150차 적용(2026-07-30): 사용자가 계속 보고한 “서버/API는 200인데 관리자 본문 픽셀이 비는” 현상을 실제 렌더 기준으로 다시 추적했다. 관리자 셸의 실제 세로 스크롤 소유자는 document가 아니라 <code>.vg-main</code>(<code>overflow-y:auto</code>)인데, 129·147차 수정과 E2E는 <code>window.scrollY</code>만 조작해 잘못된 대상을 검증하고 있었다. 테스트를 <code>.vg-main.scrollTop=900</code>으로 바꾸자 긴 관리자 페이지→권한 페이지 이동과 restored <code>pageshow</code> 모두 900이 남아 RED가 됐고, 실패 픽셀에서도 제목·상단 상태·KPI가 화면 위로 사라진 것이 확인됐다. <code>AppShell</code>이 mount·pathname 변경·<code>pageshow</code>마다 내부 스크롤을 직접 0으로 복원하도록 고쳤고, 관리자 blank probe와 HTML boot watchdog은 실제 <code>.vg-main</code> viewport와 교차하지 않는 DOM을 visible node로 세지 않게 했다. <b>실증</b>: 정확한 RED 2 failed → GREEN 2 passed, 관리자 복원·5경로 desktop/mobile 8 passed, typecheck/build 통과. 격리 Cloudflare Pages production <code>f323a41d</code>는 <code>index-Dun9umCY.js</code>/<code>AppShell-B-RAIuNL.js</code>/<code>Admin-CGJKO18P.js</code>를 서빙하고 직전 4세대 자산을 보존한다. 실제 Google 슈퍼 관리자 사용자 행의 15분 임시 세션으로 공개 관리자 5경로, learner→운영 콘솔 복귀, 내부 scrollTop 900의 restored-tab을 4 passed로 확인하고 공개 관리자 홈 픽셀 캡처에서 제목·상태·KPI·내비·본문을 직접 확인한 뒤 세션을 폐기했다.</p>
|
||
<p class="dg-note">151차 적용(2026-07-30): 실제 사용자 캡처와 같은 “셸·내비·관리자 API 200은 남지만 본문만 완전히 비는” 화면을 공식 EasyList 일반 cosmetic rule로 재현해 원인을 확정했다. 관리자 DOM/CSS 접두사 <code>ad-*</code> 중 <code>.ad-root</code>와 <code>.ad-section</code>이 EasyList 광고 숨김 목록에 정확히 포함되어 uBlock Origin·AdGuard·Brave 계열 차단기가 운영 콘솔 전체를 광고로 오인했다. 같은 두 selector에 <code>display:none!important</code>를 주입한 RED가 사용자 캡처와 같은 픽셀을 만들었고, API 상태와 무관하게 heading이 숨겨졌다. 관리자 클래스 487곳을 광고 의미가 없는 <code>vgops-*</code> 전용 namespace로 교체하고, 루트/진단은 중립 <code>data-vignette-admin-*</code> marker를 쓴다. HTML watchdog도 관리자 selector 존재 여부가 아니라 marker의 실제 rect/computed visibility를 판정하므로 이후 외부 CSS가 화면을 숨기면 React root 밖 진단을 띄운다. <b>로컬 실증</b>: EasyList RED 1 failed → GREEN 1 passed, admin route guards 12/12, admin full-sweep 20/20, admin 7폭 layout focus 3/3, session-layout desktop/mobile 8/8, typecheck 통과. <b>배포·공개 실증</b>: 격리 Cloudflare Pages production <code>0a339b77</code>는 <code>index-PqLVP802.js</code>/<code>Admin-DQbkd1Xc.js</code>/<code>Admin-CUeOG5q6.css</code>를 서빙하고 최근 4세대 엔트리를 보존한다. 실제 Google 슈퍼 관리자 사용자 행의 임시 세션에서 같은 EasyList 규칙을 켠 채 관리자 5경로가 모두 visible, legacy <code>ad-*</code> class 0, boot/admin diagnostic 0, 브라우저 오류 0임을 픽셀 캡처로 확인하고 세션을 즉시 폐기했다. 공개 health는 prod·db=true·engine=true다.</p>
|
||
<p class="dg-note">152차 적용(2026-07-30): 동일 장애가 사람의 naming 주의에만 의존해 다시 들어오지 않도록 release gate로 승격했다. <code>check-cosmetic-filter-safety.mjs</code>는 프로덕션 TS/TSX/CSS와 <code>index.html</code> 81개를 매 빌드마다 검사하고 <code>ad-*</code>·<code>ads-*</code>·<code>advert*</code>·<code>sponsor*</code> class/id가 있으면 배포 번들 생성을 중단한다. 탐지기 자체도 위험 fixture는 반드시 잡고 <code>vgops-root</code>·<code>upload-card</code>·<code>head-faceless</code> 같은 안전 식별자는 허용하는 자체 검사를 매번 먼저 통과해야 한다. 이 gate를 <code>npm run build</code>와 <code>npm run lint</code> 앞단에 연결해 public runtime 시작 경로도 우회할 수 없게 했다. 로컬 EasyList E2E는 규칙 주입 후 첫 HTML watchdog 판정까지 기다린 뒤 root/heading 실제 가시성·legacy class 0·진단 0을 검사하고, 공개 스펙은 같은 규칙을 첫 paint부터 적용한 관리자 5경로 검증을 소유한다. <b>검증·배포</b>: detector self-test 통과, production source 81개 통과, typecheck/build 통과, 강화된 로컬 EasyList 회귀 1/1 통과. gate를 실제 통과한 격리 빌드를 Cloudflare Pages production <code>1b3b6d5b</code>로 다시 게시했다. custom/preview 모두 <code>index-PqLVP802.js</code>를 서빙하고 공개 <code>Admin-DQbkd1Xc.js</code>는 격리 산출물과 SHA-256 일치, <code>vgops-root</code> 존재, <code>ad-root</code> 부재다. 직전 4세대 엔트리는 모두 JavaScript 200이며 공개 health는 prod·db=true·engine=true다.</p>
|
||
<p class="dg-note">153차 적용(2026-07-30): 관리자 AI 엔진 설정을 자유 문자열 입력에서 게이트웨이 capability 기반 선택으로 바꿨다. <code>claude_cli</code>·<code>claude_api</code>·<code>codex_cli</code>·<code>agy_cli</code>·<code>openai</code>·<code>solar</code> 6종을 같은 계약으로 관리하고, <code>GET /v1/capabilities</code>와 <code>GET /admin/engine-capabilities</code>가 설치·인증 상태, 사용 가능 모델, 모델별 추론 강도, 기본값을 전달한다. 관리자 콘솔과 설정은 provider·모델·추론 강도를 드롭다운으로 제한하며 Codex 기본은 <code>gpt-5.6-terra / medium</code>, Agy 기본은 <code>gemini-3.6-flash-high / high</code>다. 연결 주소를 바꾸면 이전 주소의 catalog를 즉시 폐기하고, 편집 중인 새 URL을 <code>engine_url</code> query로 넘겨 목록을 다시 확인하기 전에는 저장할 수 없다. PATCH도 제안된 게이트웨이의 capability를 다시 확인해 연결 불가·존재하지 않는 모델·지원하지 않는 강도를 422로 막고 durable 설정을 보존한다. 기존에는 <code>engine_mode</code>가 사실상 설정 표시값에 가까웠으나 이제 generate/stream/readiness가 실제 provider 실행기로 라우팅되며 <code>reasoning_effort</code>도 DB·API·게이트웨이까지 영속된다. Codex app-server <code>model/list</code>에서 실제 모델을 조회하고 Terra/Medium 생성 응답 <code>OK</code>, Agy <code>models</code>에서 실제 모델을 조회하고 Gemini 3.6 Flash/High 생성 응답 <code>VIGNETTE_ISOLATED_OK</code>를 격리 작업 디렉터리에서 실측했다. Anthropic API는 현재 키가 없어 의도대로 선택 불가이며 live 동일성은 B2에 남긴다. 임시 신규 게이트웨이/API 연결에서는 Codex·Agy 선택 저장과 Claude CLI 원복을 확인했고, backend 전체 <code>466 passed</code>, web build, 관리자 AI desktop/mobile <code>1+1</code>, admin AI 전수 <code>7</code>, settings 레이아웃·모바일 <code>3</code>이 통과했다. 현재 로컬 9099도 신규 게이트웨이 코드로 구동되어 Codex <code>7개 / Terra / medium</code>, Agy <code>11개 / Gemini 3.6 Flash High / high</code> live catalog를 반환하고, 로컬 API의 관리자 capability 라우트는 미인증 요청을 401로 차단한다. 121차의 “고장 URL을 저장해 실패 row 생성” E2E는 새 fail-closed 계약과 충돌해 저장 거부·기존 설정 보존 증거로 대체했고, 평가 재시도 UI 회귀는 route fixture spec이 계속 소유한다. <b>현재 상태</b>: 로컬 작업트리·게이트웨이 검증 완료, 공개 웹 배포 전이다.</p>
|
||
<p class="dg-note">154차 적용(2026-07-30): 153차 AI 엔진 capability 변경을 공개 런타임까지 반영했다. API PID <code>11788</code>과 gateway PID <code>44488</code>를 신규 코드로 교체하고 Cloudflare Pages production <code>2f52f8e3-f992-441f-ada4-f6396cbb1a2b</code>를 게시했다. 최종 custom domain은 <code>index-B54GMBSf.js</code>와 <code>AdminAi-ClAJvCvx.js</code>를 <code>application/javascript</code> 200으로 제공한다. 최초 production <code>a3819e6e</code> 검증에서 5세대 전 엔트리 <code>index-Cwtfyfq5.js</code>의 보존 누락을 발견했기 때문에, <code>scripts/preserve-pages-assets.ps1</code>가 최근 production preview의 HTML·JS·CSS 의존 그래프를 재귀적으로 수집하고 MIME을 검사해 현재 dist에 병합하도록 고친 뒤 최종 배포로 교체했다. custom domain에서 현재와 이전 4세대 엔트리가 모두 JavaScript 200이며, lazy 청크 첫 요청 강제 실패도 문서 재로드 1회 뒤 1/1 복구됐다. 실제 Google 슈퍼 관리자 사용자 행에 연결한 15분 임시 세션으로 공개 <code>/admin/ai</code>에서 공급자 6개, Codex <code>7개 / gpt-5.6-terra / medium</code>, Agy <code>11개 / gemini-3.6-flash-high / high</code>를 확인했고 저장 없이 세션을 즉시 삭제했다. 공개 health는 <code>status=ok</code>, <code>environment=prod</code>, <code>db=true</code>, <code>engine=true</code>이며 auth config 200, 비인증 personas·admin capability 401, watchdog <code>LastTaskResult=0</code>다. 153차의 공개 배포 전 상태 표기는 이 증거로 대체한다.</p>
|
||
<p class="dg-note">155차 적용(2026-07-31): 세션 텍스트 입력이 AI 생성·TTS 종료까지 잠기고 답변 표시 뒤에도 evaluator가 끝날 때까지 다음 전송이 약 24초 묶이던 학습 UX를 실제 브라우저와 gateway/API 계량으로 분해했다. textarea는 회기 종료·일시정지에서만 잠기며 생성·음성 준비·재생 중에는 다음 질문 초안을 계속 작성·보존하고, 내담자 응답 저장 직후에는 음성 재생과 무관하게 다음 전송을 허용한다. 관리자 DB의 Agy/Gemini 설정은 evaluator/review에 유지하되 실시간 client 역할은 <code>VIGNETTE_LIVE_CLIENT_PROVIDER=claude_cli</code> 전용 lane으로 분리해 회기별 <code>claude -p</code> 프로세스를 재사용한다. stream learner/client turn과 결정론 상태를 먼저 durable 저장하고 SSE <code>done</code>을 보낸 뒤 fast-loop 평가는 백그라운드에서 같은 learner turn normalized row에 붙이며 평가 기반 코칭 충전도 한 번만 적용한다. Agy fallback도 <code>stream-json</code> delta를 실제 SSE token으로 전달한다. 로컬 음성은 <code>-UseHiggsVoice</code>로 설치된 <code>higgs-audio-v3-tts-4b</code>를 loopback 상주시켜 P1 실제 답변을 24kHz WAV로 합성하며 저장소의 무참조 synthetic seed만 reference로 허용하고 non-dev는 fail-closed한다. <b>실증</b>: 변경 전 full API 완료 24.17/24.57초, 변경 후 실제 UI 첫 응답·전송 해제 9.58초와 상주 연속 턴 6.96초, 생성 중 초안 enabled/보존, TTS 중 보내기 enabled. Higgs full API 362,924 bytes/7.56초 WAV, provider/model 헤더 확인. backend 432/432, gateway 44/44, typecheck·API types·build, session stream 1/1, voice skip/draft 1/1 통과. <b>배포·공개 실증</b>: commit <code>c7883434</code>를 origin/master에 push하고 Cloudflare Pages production <code>505155c4-01f3-4eac-ad59-c2d323a0bb51</code>로 게시했다. custom/preview 모두 <code>index-BU5JjRnR.js</code>를 JavaScript 200으로 서빙하고 최근 5개 배포의 의존 자산을 보존한다. 공개 API는 prod·db=true·engine=true, auth config Google configured/dev login disabled, 비인증 personas·voice speech 401, Google 시작 302다. 실제 공개 로그인 1280×720 렌더에서 버튼 2개 enabled, 브라우저 warning/error 0을 확인했다. 공개 음성은 라이선스 경계상 <code>openai / gpt-4o-mini-tts</code>를 유지한다.</p>
|
||
<p class="dg-note">156차 적용(2026-07-31): 활성 상담 세션이 전역 테마와 무관하게 <code>.sx-page--active</code>에서 다크 팔레트를 다시 정의해, 라이트 설정에서도 화면은 다크인데 공통 <code>Surface inset</code>인 AI 코치만 밝게 뜨는 혼합 테마 결함을 수정했다. 세션 전용 <code>--bg-surface</code>/<code>--text-*</code>/<code>--accent</code> 덮어쓰기를 제거하고 전역 <code>data-theme</code>·<code>tokens.css</code>·공통 <code>Surface</code>를 그대로 사용해 라이트는 종이·글래스, 다크는 차콜·세이지로 함께 전환한다. 우측 레일은 <b>AI 코치 → 현재 신호 → 음성·AI 응답 상태</b> 순으로 재배치하고 의미 없는 equalizer 막대와 중복 화면 모드 행을 제거했다. 700px 이하 높이에서는 발화 예시·보조 흐름을 먼저 접고 코치 판단과 근거/이력 액션을 보존하며, 기존 사각지대였던 881~1180px에서도 코칭 모드에 AI 코치 카드만 전폭으로 남긴다. <b>검증</b>: <code>npm run check:design-ssot</code>, typecheck, build 통과, 세션 전수 desktop/mobile <code>30 passed / 2 skipped</code>, session-layout <code>8/8</code>, 7폭 layout-visual-gate <code>15/15</code>, 라이트 1366×640 코치·1024×640 압축 코치 및 다크 1440/1024/390 캡처 직접 확인. 로컬 작업트리 기준이며 배포 전이다.</p>
|
||
<p class="dg-note">157차 적용(2026-07-31): 사용자 제공 1536×1024 활성 세션 시안을 실제 세션 화면에 반영했다. 데스크톱을 <b>좌측 단일 컨텍스트 레일 326px / 중앙 인물 무대+자막 / 우측 단일 라이브 코칭 레일 357px / 하단 116px 제어바</b>로 재구성하고, 상태 신호 모드에서도 AI 코치의 개입 시점·남은 기회·코칭 포인트를 미리 이해할 수 있게 했다. 제공된 수채화 잎 5장은 BEN2로 투명 분리한 뒤 600×800 이하 알파 WebP로 최적화해 합계 약 158KB로 줄였고, 좌우 캔버스와 아바타 뒤에 장식 계층으로 연결했다. 새 E2E는 1408px 상단/본문, 1468px 제어바, 326/357px 레일, WebP 응답과 computed background 연결을 1536×1024에서 고정한다. 1366×768 이하에서는 스테이지를 압축해 자막이 주 작업영역을 잃지 않게 했다. <b>검증</b>: <code>npm run check:design-ssot</code>, typecheck, build 통과, 세션 전수 desktop/mobile <code>32 passed / 2 skipped</code>, session-layout <code>8/8</code>, 7폭 layout-visual-gate <code>15/15</code>, 1536×1024 라이트 캡처 직접 확인. 앱 commit <code>12bdc594</code>를 Cloudflare Pages production에 배포하고 배포 증거 commit <code>32c0b97d</code>를 연동한 최종 production <code>f9b06388</code>까지 완료했다. 커스텀 도메인의 <code>index-CzevzoM7.js</code>, <code>Session-BWMFTqgE.js</code>, <code>Session-D7DZVnh0.css</code>, <code>leaf-1.webp</code>가 각각 올바른 JavaScript/CSS/WebP MIME으로 200을 반환했다. 공개 로그인 실렌더·브라우저 warning/error 0, API <code>prod/db/engine=true</code>, 비로그인 personas 401, Google OAuth 시작 302, watchdog 5항목 healthy도 확인했다.</p>
|
||
<p class="dg-note">158차 적용(2026-07-31): 관리자 <b>Provider·모델별 사용 원장</b>에서 Claude CLI만 비용이 잡히고 Agy/Gemini가 0달러로 보이던 원인을 수정했다. Claude CLI는 SDK가 반환한 호출별 비용 추정값을 우선하고, 비용이 없는 Agy/Gemini·Codex·Claude API는 버전 고정 공식 참조단가로 입력·캐시 입력·출력 토큰을 환산한다. 기존 DB에 비용 0으로 저장된 행도 <code>/admin/usage</code> 조회 시 보정하며, 공식 단가가 등록되지 않은 모델은 0달러 대신 <code>미산정</code>으로 표시한다. 응답과 리포트는 <code>recorded_cost_usd</code>, <code>estimated_cost_usd</code>, <code>cost_basis</code>, 단가 라벨·출처를 분리한다. 공개 DB-backed 30일 원장에서 <code>gemini-3.6-flash-high</code> 5호출·입력 35,703·출력 1,129가 <code>$0.062022</code>로 보정되고 총비용에도 합산되는 것을 인증 요청으로 확인했다. <b>검증</b>: backend/gateway focused <code>66 passed</code>, API type generation/check, typecheck, build, 관리자 desktop/mobile focused E2E <code>2 passed</code>, 7폭 focused layout visual gate <code>1 passed</code>. 공개 API 프로세스를 재시작하고 Cloudflare Pages production <code>11b3e11f</code>에 배포했으며, 커스텀 도메인의 <code>AdminAi-BfxMUlzj.js</code>와 <code>AdminAi-XHkda_d4.css</code>가 올바른 MIME으로 200을 반환한다.</p>
|
||
<p class="dg-note">159차 적용(2026-07-31): Claude CLI result에 실제 <code>usage</code>/<code>modelUsage</code>가 있는데도 게이트웨이의 generate·SSE done 응답이 <code>tokens_in=0</code>, <code>tokens_out=0</code>을 하드코딩해 DB 원장에 0을 저장하던 근본 원인을 수정했다. <code>modelUsage</code>/<code>model_usage</code>가 있으면 메인 호출과 서브에이전트를 모델별로 합산하고, 없을 때 최상위 <code>usage</code>로 폴백한다. 입력 원장은 비캐시 입력 + cache read + cache creation 합계다. 과거 미수집 행은 토큰을 비용으로 역산하지 않고 <code>token_unmetered_turns</code>로 분리해 화면에 <code>미계량</code>으로 표시하며, Claude <code>total_cost_usd</code>는 청구 실비가 아니라 SDK 추정치이므로 <code>provider_estimate</code>/<code>SDK 추정</code>으로 바로잡았다. <b>검증</b>: backend 전체 <code>436 passed</code>, gateway 전체 <code>45 passed</code>, API type generation/check, typecheck, build, 관리자 desktop/mobile <code>2 passed</code>, 7폭 focused layout gate <code>1 passed</code>. 신규 live Opus generate는 입력 <code>31,918</code>·출력 <code>4</code>·비용 추정 <code>$0.124862</code>, Haiku SSE done은 입력 <code>30,450</code>·출력 <code>159</code>·비용 추정 <code>$0.061685</code>를 반환했다. 운영 DB 30일 원장의 과거 Claude <code>194</code>건은 <code>token_unmetered_turns=194</code>, <code>cost_basis=provider_estimate</code>로 확인했다. 공개 런타임을 새 gateway/API로 교체하고 Cloudflare Pages production <code>10c2bfc3</code>에 배포했다. 커스텀 도메인의 <code>index-BDlbUpT7.js</code>, <code>AdminAi-r9AkYKAl.js</code>, <code>AdminAi-D7JtoYC_.css</code>는 올바른 MIME으로 200이며, 실제 관리자 임시 세션의 공개 브라우저에서 <code>미계량</code>·<code>SDK 추정</code>·토큰 커버리지와 Codex/Agy catalog를 확인하고 세션·storage state를 즉시 폐기했다.</p>
|
||
<p class="dg-note">160차 적용(2026-07-31): 159차 이전 Claude 토큰 0/0 행을 비용에서 역산하지 않고 실제 로컬 Claude 실행 JSONL로 제한 백필했다. JSONL <code>3,908</code>개의 assistant usage record <code>3,848</code>개를 본문 출력 없이 읽고, 정규화 응답 SHA-256과 DB 생성 시각 창(-30초~+180초)이 모두 맞으며 후보가 정확히 1개인 경우만 허용했다. 운영 전체 Claude <code>442</code>건 중 <code>169</code>건을 실제 입력 <code>5,179,999</code>·출력 <code>46,910</code> 토큰으로 복구했고, 후보 없음 <code>273</code>건은 계속 <code>미계량</code>, 모호한 후보는 <code>0</code>건이다. apply는 <code>--expected-matches 169</code>와 행별 <code>UPDATE 1</code> 가드를 통과했으며 적용 후 dry-run의 추가 exact match는 <code>0</code>건이다. 확인 시점 30일 창은 Claude <code>183</code>건 중 계량 <code>125</code>·미계량 <code>58</code>건이었다. <b>검증</b>: backend 전체 <code>439 passed</code>, gateway 전체 <code>45 passed</code>, 공개 관리자 브라우저 <code>1 passed</code>에서 Claude 실제 토큰과 부분 계량 <code>125/183회</code>, 잔여 미계량 설명, 브라우저 오류 0을 확인하고 임시 인증 세션·storage state를 폐기했다. DB-only 백필이므로 추가 웹 배포 없이 공개 UI가 live API 결과를 즉시 반영한다.</p>
|
||
<p class="dg-note">161차 적용(2026-08-06): Outcome & Alliance OS <b>G1 Alliance Core</b>를 증거 게이트로 닫았다. 수련생 자기평가를 먼저 append-only로 잠근 뒤 가상내담자와 독립 관찰자를 별도 모델 실행·세션으로 평가하며, 공개 전에는 외부 관점을 숨기고 terminal 상태에서만 3관점 × goal/task/bond 9개 축을 함께 보여준다. 교수자 재평정은 근거 장면·메모를 강제하고 이전 평정을 supersede하는 append-only 원장으로 남긴다. 학습자 리뷰는 축별 총점 없이 관점 차이·근거 장면·provenance를 데스크톱 표와 모바일 카드로 제공한다. <b>검증:</b> focused backend <code>40 passed</code>, 실 PostgreSQL RLS·상태전이·재시도 model-run·교수자 supersession smoke, Alliance Pulse desktop/mobile <code>6/6</code>와 기존 회기 리뷰 <code>12/12</code>. prompt <code>1.2.0</code> 합성 gold는 방향 정확도 <code>9/9</code>, 최종 실패 <code>0</code>, 근거 recall <code>88.9%</code>이며 실패한 첫 시도 2건도 provenance로 보존됐다. G2 종단 성과 궤적을 병렬 구현 중이다.</p>
|
||
<p class="dg-note">162차 적용(2026-08-06): Outcome & Alliance OS <b>G2 Longitudinal Outcome Trajectory</b>를 증거 게이트로 닫고 G3를 BUILD로 승격했다. 교육용 합성 5회기 기대분포와 고통 부담·일상 기능·학습 참여 세 축을 총점 없이 추적하며, 누락은 무점수, safety는 별도 참조, 관계 합의·미해결 균열은 역할별 문장으로 분리한다. 학습자 리뷰에는 1~5회기 타임라인, 축별 상태·불확실성·근거·반대근거·다음 질문과 최신 회기 자기보고 체크인을 연결했다. <b>검증:</b> 4-case×5-session benchmark status 정확도 <code>1.0</code>, false alert·critical miss <code>0</code>; 실제 DB/API 한 case S1~S5·15개 관측, 동일 제출 멱등성, 변경 payload 409, 학습자 소유권·교수자 cohort·교차 cohort 차단, role-safe 관계 기억 통과. G2 desktop/mobile <code>12/12</code>, G1+기존 리뷰 <code>15/15</code>, G2/G3 통합 backend <code>90 passed</code>다.</p>
|
||
<p class="dg-note">163차 적용(2026-08-06): Outcome & Alliance OS <b>G3 Rupture & Repair Lab</b>을 증거 게이트로 닫고 G4를 BUILD로 승격했다. 9종 파열 상태머신·append-only 관측/재조정/안전참조 원장, 텍스트·SSE·음성 공통 durable-turn 감지, 후속 발화의 missed/partial/resolved reconciliation, 역할별 리뷰·교수자 supersession, 4~6턴 희소 Scenario Director와 prompt/SSE 내부 라벨 누출 차단을 연결했다. <b>검증:</b> 11-case benchmark 유형 macro-F1 <code>1.0</code>, 복구 상태 정확도 <code>1.0</code>, critical miss·judge gaming·암기문구 false resolution <code>0</code>; Scenario/G3 집중 <code>107 passed</code>, G0~G8 통합 계약 <code>251 passed</code>, 리뷰 desktop/mobile <code>50/50</code>. 실제 DB/API 회기에서 durable turn UUID 2개, 내부 토큰 write, 멱등 retry/변조 409, learner/cohort RLS, 교수자 append-only correction과 최종 <code>resolved</code>를 확인했다.</p>
|
||
<p class="dg-note">164차 적용(2026-08-06): Outcome & Alliance OS <b>G4 Deliberate Practice Engine</b>을 증거 게이트로 닫고 G5를 BUILD로 승격했다. 평가 장면을 관찰 가능한 원자 행동과 replay·branch·constrained response·voice retry·difficulty ladder 5종 prescription으로 연결하고, competency graph의 약점·망각 위험에 따라 다음 연습을 고른다. 익숙한 장면 반복이나 자기 성공 주장만으로 mastery를 열지 않고 미지 사례 전이를 요구한다. <b>검증:</b> G4 core/store/API <code>54 passed</code>, G4 리뷰 desktop/mobile <code>8/8</code>, G1~G4+기존 리뷰 <code>58/58</code>. 실제 DB/API 회기에서 durable turn 근거 처방→학습자 시도→교수자 append-only correction, 안정 멱등 retry/변조 409, learner/cohort RLS, 보상 해킹 차단, 총점·XP 없음과 비임상 경계를 확인했다.</p>
|
||
<p class="dg-note">165차 적용(2026-08-06): Outcome & Alliance OS <b>G5 Calibration Mirror & Transfer</b>를 증거 게이트로 닫고 G6를 BUILD로 승격했다. 외부평가 전 prediction revision과 불가역 lock, 독립 runtime observation reveal, 역량별 calibration error·95% 구간·과신/과소신 처방, 네 변주 축 unseen transfer와 암기 문구 차단, 합성 subgroup drift를 역할 안전 원장/UI에 연결했다. <b>검증:</b> G5 core/store/API <code>41 passed</code>, 4-case expectation accuracy <code>1.0</code>, 보정 MAE <code>0.667→0.267</code>, unseen transfer 4/4, 암기 false verification 0, drift gap 0.667. 실제 DB/API revision→lock→reveal·멱등/변조 409·post-lock 422·RLS, G5 UI 4/4와 G1~G5 리뷰 <code>62/62</code>, typecheck/build를 통과했다.</p>
|
||
<p class="dg-note">166차 적용(2026-08-06): Outcome & Alliance OS <b>G6 Supervision & Research OS</b>를 증거 게이트로 닫고 G7을 BUILD로 승격했다. 위험·정체·미해결 관계 사건 attention queue와 3클릭 원장 drilldown, metadata-only 교수자/AI 불일치 dataset·audit, curriculum gap, model/prompt/instrument/subgroup drift, Phase 3 네 도메인 artifact provenance를 역할 안전 원장/UI에 연결했다. <b>검증:</b> G6 core/store/schema <code>28 passed</code>, 6-signal queue 순서 정확, raw transcript row 0, drift expected/actual 일치, manifest 네 도메인 전건. 실제 DB/API 멱등/409·AI-view·역할/cohort RLS·audit, version/subgroup/artifact hydrate 통과. G6 UI desktop/mobile <code>6/6</code>, typecheck/build를 통과했다.</p>
|
||
<p class="dg-note">167차 적용(2026-08-06→07): Outcome & Alliance OS <b>G7 Multimodal Alliance</b>의 내부 제품 범위를 완성했다. 동의 preflight, 한 오디오 시계의 익명 STT word·silence/overlap/interruption/prosody, 독립 text/voice 측정과 calibrated no-gain fusion, 비임상 상호작용 신호 계약, 원음/metadata 역할 경계, 철회와 audio/derived tombstone을 실제 API/DB 원장에 연결했다. 회기 리뷰에는 시간축·독립 측정·보존 경계와 음성 재연습 CTA를 제공한다. <b>검증:</b> backend G0~G8 묶음 <code>393 passed</code>, 실제 API 멱등/변조409·RLS·융합 <code>[true,false,false]</code>·삭제, G7 desktop/mobile <code>8/8</code>, session review <code>24/24</code>, layout <code>15/15 + 8/8</code>, G1~G8 최종 UI 묶음 <code>84/84</code>. 물리 마이크·공개 WSS·50분 장시간 실증은 B2 외부 게이트로 남아 있다.</p>
|
||
<p class="dg-note">168차 적용(2026-08-06→07): Outcome & Alliance OS <b>G8 Autonomous Content & Continuous Improvement</b>의 내부 제품 범위를 완성했다. source→draft→독립 red-team→benchmark→human catalog gate, 모델·릴리스별 baseline/threshold/provenance/rollback 증거 rail, append-only 승인·monitor·rollback lifecycle, 운영 incident의 4-node 회귀 DAG를 관리자 릴리스 제어석과 실제 API/DB에 연결했다. 자동 게시 버튼은 없으며 human gate 전에는 효과가 없다. <b>검증:</b> G8 UI desktop/mobile <code>8/8</code>, 기존 admin <code>40/40</code>, 실제 API 멱등/변조409·401/403·4종 artifact·lifecycle 6·DAG 4/4, 금지 데이터 0. 현재 공개 OpenAPI에는 G7/G8 route가 각각 <code>0</code>개여서 실제 배포·public proof를 DONE으로 표시하지 않는다.</p>
|
||
<p class="dg-note">169차 적용(2026-08-07): G7/G8 외부 승격 전 로컬 릴리스 준비를 강화했다. 실제 loopback <code>/voice/ws</code> short soak는 8.015초 동안 연결 <code>83/83</code>·성공 발화 79·철회/재연결/overflow 복구를 통과했고 API/gateway 전체 <code>805 passed</code>지만, 물리 마이크·public WSS·실행된 50분 증거로 확대하지 않는다. G8 시각 QA는 승인 차단 이유를 상시 문구·<code>aria-describedby</code>·semantic form으로 고쳐 desktop/mobile <code>16/16</code>과 build를 통과했다. 배포 프리플라이트는 G3~G8 독립 32자 토큰과 G0~G8 migration sentinel을 fail-closed로 검사한다. 당시 199파일 release-only patch는 clean HEAD <code>git apply --check</code>를 통과했으나 후속 병렬 편집으로 manifest와 달라져 무효화했으며, 재분류·결정적 재빌드 전에는 배포에 사용할 수 없다. 공개 OpenAPI의 G7/G8 경로는 재확인 시점에도 각각 0개라 두 카드 상태는 BUILD로 유지한다.</p>
|
||
<p class="dg-note">170차 적용(2026-08-07): 병렬 개발 결과를 학생 자기주도 수련의 실제 실행 경로로 통합했다. G1은 회기 전 pre pulse를 fail-closed로, 첫 라운드 뒤 mid pulse를 비차단형으로 연결했고 desktop/mobile focused <code>2/2</code>와 시각 증거 4장을 보존했다. G4/G5 카드의 replay·branch·제약 응답·음성 재시도·난이도 사다리·미지 전이는 학습자 홈→사전 설정→실제 새 Session까지 typed launch intent를 유지하며 관련 UI <code>22/22</code>와 통합 묶음 <code>59/59</code>를 통과했다. 현행 API 기준 learner/session/layout 묶음은 <code>37/37</code>, API/gateway는 <code>767 + 45 passed</code>, typecheck/build/API 계약도 통과했다. dirty 250개는 Outcome 관련 228·혼합 4·보존 제외 18로 전건 분류했고, 혼합 문서 exact hunk와 clean-source API 타입 재생성으로 만든 223파일 release-only patch를 두 번 동일하게 조립해 clean HEAD와 canonical clean index 적용 검사를 통과했다. 바이너리 시각 증거 4장은 manifest에는 관련 증거로 등록하되 텍스트 patch와 분리했다. G7 물리 마이크·공개 WSS·50분 실측과 G8 실제 배포·public proof는 여전히 BUILD다.</p>
|
||
<p class="dg-note">171차 적용(2026-08-07): G8 agentic worker를 기본 비활성 opt-in scheduler로 연결했다. repo-approved 비식별 source는 immutable <code>app.ci_agentic_job</code>, <code>FOR UPDATE SKIP LOCKED</code> lease, retry 상태로 관리하며 source usage·classification·본문 hash·PII를 engine 전에 재검증한다. 실제 <code>claude_cli/claude-opus-4-8</code>에서 generator 1·독립 reviewer 2·variant generator 1·독립 judge 3, 총 7 calls를 실행해 dev PostgreSQL reviewer 2·variant 3·pass rate 1.0·<code>pending_human_approval</code>을 저장했다. 동일 UUID replay는 model call 0, human approval/catalog row는 0, clinical claim은 false다. engine timeout·answer leakage·red-team finding 실패도 pipeline 저장 0건으로 증명했다. 전체 API/gateway <code>858 passed</code>, Ruff·API type check·web typecheck·SSOT checker를 통과했다. 운영 drift trigger·사람 payload 검수 E2E·release-agent 폐루프 전까지 G8은 BUILD다.</p>
|
||
<p class="dg-note">171차 적용(2026-08-07): G7 voice code/internal 경계를 Deepgram streaming adapter와 운영 전환용 fail-closed 계약까지 확장했다. interim/final·word timestamp·provider event를 처리하고, word 원문 대신 deployment secret 기반 HMAC pseudonym만 저장한다. 열린 stream은 동의를 1초마다 재검사해 철회 시 abort하며, Session은 음성 단절 뒤 작성 중 텍스트와 텍스트 계속하기·키보드 재연결을 보존한다. WSS <code>ready</code>는 실제 STT/TTS provider/model을 반환하고 public runner v3의 mic-free preflight가 네 기대값을 exact match한다. full soak는 명시적 확인 전 장치를 열거하지 않는다. 운영 TTS는 OpenAI API <code>gpt-4o-mini-tts</code>로 고정하고 공식 앱 통합·출력 소유 근거와 회기 전·중 AI 생성 음성 고지를 반영했다. <b>검증:</b> voice/G7 backend <code>73 passed</code>, public runner <code>12 passed</code>, disclosure desktop/mobile E2E, SSOT checker green. 실제 Deepgram key·quota live, expected provider/model authenticated public ready, 명시 동의 물리 마이크와 50분 soak가 없어 G7은 계속 BUILD다.</p>
|
||
<p class="dg-note">172차 적용(2026-08-07): G7 voice hardening을 235파일 release-only patch <code>3197805058e3…2bc</code>로 clean worktree에서 재조립·검증한 뒤 공개 API 8001과 Cloudflare Pages production <code>6d7e89d5</code>에 재배포했다. API는 새 clean 경로에서 <code>prod/db/engine=true</code>, OpenAPI 119 paths, 인증 401·Google OAuth 302를 유지하고 <code>/voice/health</code>가 STT <code>openai/gpt-4o-transcribe</code>·TTS <code>openai/gpt-4o-mini-tts</code>를 반환한다. 공개 WSS 비인증 차단은 <code>1008</code>, custom/immutable web은 새 <code>index-DZZoYvZq.js</code>와 AI 생성 음성 고지 포함 Session 청크를 올바른 MIME으로 제공하며, watchdog도 새 clean 경로로 재등록해 <code>LastTaskResult=0</code>을 확인했다. 운영 Deepgram key가 없어 streaming provider는 아직 활성화하지 않았고 authenticated ready·명시 동의 물리 마이크·50분 soak도 미실행이므로 G7은 계속 BUILD다.</p>
|
||
<p class="dg-note">173차 적용(2026-08-07): G7 완료감사에서 내부 과대주장을 바로잡았다. benchmark는 calibration 점수 재진술 대신 synthetic gold target에 대한 text-only와 calibrated multimodal의 <code>1-MAE 0.9533→0.9673</code>, gain <code>+0.014</code>(최소 <code>0.01</code>)를 직접 계산하고 gold 변조 반례를 통과한다. consent mutation·timeline·measurement는 session 공통 PostgreSQL transaction lock으로 직렬화했으며 실제 개발 DB 동시 철회에서 미커밋 writer 대기, 커밋 뒤 저장 차단, timeline row <code>0</code>을 확인했다. route는 10MiB append 전 거부, transcript event queue <code>32</code>, Deepgram receive queue <code>16</code>을 사용하고 3초 loopback에서 연결 <code>61/61</code>·발화 <code>57</code>·overflow 복구·10MiB RSS probe를 통과했다. 다만 synthetic label은 독립 임상/파일럿 효과 증거가 아니고 단일 프로세스 probe는 실제 worker·Uvicorn·OS·Cloudflare high-water를 대체하지 않는다.</p>
|
||
<p class="dg-note">174차 적용(2026-08-07): 학생 자기주도 수련의 <b>홈 추천→목표 선택→텍스트/SSE 회기→종료·리뷰→G4 처방→새 회기 재연습</b>을 실제 <code>src</code> 제품 화면 하나의 상태 흐름으로 묶었다. desktop/mobile <code>2/2</code>, typecheck와 고정 캡처 9장 직접 QA를 통과했다. 홈 핵심 CTA는 44px·4.5:1 이상 대비로 보정했고, 390×844·320×568 활성 회기의 아바타/문구 겹침 0·축어록/입력창 카드 내부 포함을 고정했다. 리뷰→재연습의 내부 criterion/counterevidence/UUID/영문 이론키는 사용자용 한국어로 치환했다. 이 브라우저 증거는 미소유 <code>/api/**</code>를 404로 닫은 로그인 없는 route fixture이며 실제 DB 폐루프가 아니다. 실 PostgreSQL production caller 증거와 UI 소비 증거를 분리해 기록한다. 리뷰 1~5 척도는 320px에서 3+2로 재배치해 다섯 선택지 44px 이상·내부/페이지 overflow 0과 desktop/mobile <code>8/8</code>을 확인했다. 상세: <code>ops/outcome-os-self-directed-learning-loop-ui-evidence-2026-08-07.md</code>.</p>
|
||
<p class="dg-note">175차 적용(2026-08-07): <b>격리 NAS 프리뷰</b>를 <code>http://100.116.83.60:8088</code>에 배포하고 실제 브라우저 회기와 review API의 저장 축어록 2턴을 확인했다. 전용 Compose 프로젝트·포트·네트워크·볼륨만 대상으로 배포했으며 기존 프로젝트 중단·재생성 명령은 실행하지 않았다. 매일 04:30 KST <b>Vignette 회기 E2E 정기 검증</b>(automation id <code>vignette-e2e</code>)을 ACTIVE로 구성했다. 새 기능이 material milestone이고 release-only patch 결정성·manifest·clean-index·NAS preflight가 모두 통과하며 배포 SHA가 달라질 때만 같은 격리 프리뷰를 승격한다. 실행 이력은 첫 스케줄 뒤 별도 증거로 남긴다. 증거: <a href="./ops/nas-preview-deployment-evidence-2026-08-07.md">배포 증거</a>, <a href="./ops/evidence/nas-preview-live-turn-2026-08-07.png">브라우저 증거</a>. 이 프리뷰와 자동화는 G7 종료 증거가 아니다. 물리 마이크 캡처는 미승인이고 Deepgram 운영 키가 없어 G7은 계속 <b>BUILD</b>다.</p>
|
||
<p class="dg-note">176차 적용(2026-08-07): G8의 세 내부 결손을 병렬 구현·독립 감사로 닫았다. G6 drift 원장의 canonical 임계값·subgroup 근거를 대조하는 default-off trigger는 실 DB incident 1·DAG 4·job 1, 멱등·동시성·approval/catalog 0을 통과했고 문자열만 믿던 fail-open을 RED로 수정했다. 기존 실제 Claude pending 후보는 새 모델 호출 0회로 격리 8011/5181 관리자 browser에서 catalog 0→1·approval 1, learner/teacher 403·replay 멱등·변조 409·민감 projection 비노출을 확인했다. 별도 실엔진 후보의 answer leakage는 422·저장 0으로 차단했다. release agent는 material+changed SHA·patch 2회 결정성·manifest/clean candidate·API/Web/회기 gate·NAS snapshot/semantic runtime/browser·rollback·exact-SHA SSOT handoff를 구현했고, 독립 감사에서 active-state drift·경로 탈출·secret archive·병렬 volume 충돌 네 결함을 수정했다. 전체 API/gateway <code>891 passed</code>, release governance <code>35/35</code>, 관리자·자기주도·회기 desktop/mobile <code>26/26</code>, API type·typecheck·build를 통과했다. 변경 SHA의 실제 격리 NAS 승격 전까지 G8은 <b>BUILD</b>다.</p>
|
||
<p class="dg-note">177차 적용(2026-08-07): G8 release-only SHA <code>e9b482ccba61…e99</code>를 격리 NAS <code>vignette-preview-20260807</code>에 실제 승격했다. Windows clean candidate의 <code>bash\r</code> DB init 실패, legacy env 경로 오판, 교체 직후 readiness 502를 각각 fail-closed RED로 재현하고 LF 정규화·snapshot env 추적/preflight·180초 bounded readiness로 수정했다. 선행 502 실행은 기존 이미지로 자동 rollback하고 health·OpenAPI 122·auth 401·이미지 ID를 재검증했다. 최종 실행은 API/Web 새 이미지, health ok·db/engine true, OpenAPI 123·auth 401·G0~G8, readiness 3회, 실제 postdeploy browser SSE→DB-backed review를 통과했으며 물리 마이크는 사용하지 않았다. release-agent <code>20/20</code>, final session E2E <code>559.812s</code>, browser review <code>26.921s</code>. exact SHA를 TODO·백로그·manifest와 동기화해 G8은 <b>DONE</b>이다. G7 외부 증거는 그대로 BUILD다.</p>
|
||
<p class="dg-note">178차 적용(2026-08-07): G7을 가짜 외부 증거로 닫을 수 없도록 제품 동의와 종료 checker를 연결했다. Session은 처리 목적·전송/전사·침묵/속도/끼어듦 파생·30일 보존·원음 미보존·철회/삭제·텍스트 대안을 설명하고, 서버 consent ledger 성공 전 <code>getUserMedia=0</code>·voice WebSocket <code>0</code>을 유지한다. 원장 지연·실패와 늦은 권한 응답도 capture를 남기지 않으며 Space를 제거하고 <code>Alt+M</code>을 안내한다. public runner v4는 turn별 interim ≥1·speech-final=1과 latency를, 관리자 runtime sampler는 worker/Uvicorn queue high-water를, Linux sampler는 exact image/container/cgroup/proc/Docker/TCP high-water를 기록한다. 별도 human contract는 synthetic/raw material을 거부하고 30명/50회기/150축·blind 2인·ICC/κ·10,000 cluster bootstrap을 재계산한다. 네 artifact는 같은 public host·겹치는 50분 시간창이어야 canonical checker를 통과한다. <b>검증:</b> API/G7 <code>92 passed</code>, evidence runner/checker·release gate <code>31 passed</code>, voice UI <code>2/2</code>, typecheck·API type·Ruff·py_compile. 실제 마이크·운영 Deepgram·실제 참가자는 사용하지 않아 G7은 계속 <b>BUILD</b>다.</p>
|
||
<p class="dg-note">179차 적용(2026-08-07): G7 내부 hardening release-only SHA <code>a8c27b0aadcb9dacea86c4be5e62e19277b9cc2678ebca51aa02998d2c2d1a0d</code>를 격리 NAS <code>vignette-preview-20260807</code>에 승격했다. 첫 release-agent 실행은 새 동의 모달을 따르지 않던 DB-backed mic fixture 1건을 23/24에서 fail-closed로 차단하고 원격을 변경하지 않았다. fixture를 마이크→동의 원장→synthetic capture 순서로 고친 뒤 focused <code>1/1</code>, clean candidate session E2E <code>24/24</code>, API 전체, API contract·typecheck·build를 통과했다. 최종 API/Web 이미지 <code>79ec…4450</code>/<code>c530…2f28</code>, health ok·db/engine true, OpenAPI 124·auth 401·G0~G8, readiness 3회, NAS browser SSE→DB review를 확인했다. 물리 마이크는 사용하지 않았고 G7 외부 four-artifact gate는 계속 <b>BUILD</b>다.</p>
|
||
<p class="dg-note">180차 적용(2026-08-07): current source release-only SHA <code>6030a677af7e…c611</code>를 격리 NAS <code>vignette-preview-20260807</code>에 승격했다. 최초 전체 gate는 108개 중 5개를 fixture drift 2종과 runtime-critical <code>shell.css</code> 분류 누락으로 차단해 원격을 바꾸지 않았고, G5→G4 무호출·StrictMode retry·learner 모바일 4열 packaging을 focused 6/6과 manifest 회귀로 고정했다. 최종 candidate browser E2E <code>108/108</code>, postdeploy 실제 SSE→DB review <code>32.437s</code>, health <code>3/3</code>, OpenAPI 126·auth 401·G0~G8 route를 통과했다. API/Web 이미지는 <code>52e0…8b2d</code>/<code>6fdb…f215</code>이며 named DB volume과 배포 전 dump를 보존했다. 이 승격은 current runtime 증거이고 G8 receipt-bound 실제 image rollback은 별도 GATE다.</p>
|
||
<p class="dg-note">181차 적용(2026-08-07): <b>G8의 마지막 게이트인 실제 receipt-bound image rollback을 격리 NAS에서 실행</b>해 DONE으로 승격했다. 먼저 성립 불가능한 Gate6 계약을 정정했다. 감사 대상 current API 이미지 자체가 <code>com.docker.compose.project=vignette-preview-20260807</code>·<code>service=api</code> image label을 갖고 있어 "helper의 compose label 0개"는 감사되지 않은 다른 이미지를 쓰지 않는 한 성립하지 않는다. 그래서 계약을 <b>key 부재가 아니라 소속</b>으로 바꿔 <code>scripts/launch-nas-preview-g8-helpers.py</code>에 고정했다. image 상속 label을 baseline으로 읽고, container의 모든 compose label이 baseline과 같거나 선언된 격리 override인지 검사하며, 최종 project는 <code>vignette-g8-helper-*</code>·service는 <code>g8-rollback-*</code>이어야 하고, <code>docker run</code> argv에 target project/service label을 주입하면 fake-runner 테스트가 먼저 깨진다(<code>37/37</code>). 실행은 executor <code>127.0.0.1:18149</code>(host network·read-only rootfs·cap-drop ALL·no-new-privileges·uid1028/gid100·group101)와 별도 lifecycle의 control-plane API <code>127.0.0.1:8018</code>로 구성했다. 계획했던 Windows SSH 터널은 NAS sshd가 <code>administratively prohibited</code>로 direct-tcpip를 거부해 사용할 수 없었고, sshd 설정을 바꾸는 대신 같은 격리 계약의 NAS-side probe 컨테이너에서 실행해 control-plane(8018)·preview(8088) origin 분리를 그대로 유지했다. 결과는 <code>rollback-old</code> receipt <code>nas-g8-723eeef2…</code>, <code>restore-current</code> receipt <code>nas-g8-2738846c…</code>로 실제 이미지가 <code>52e0/6fdb</code>→<code>79ec/c530</code>→<code>52e0/6fdb</code>로 왕복했고, release gate·approval 2회 멱등, DB <code>audit.ci_lifecycle_event</code> <code>rollback/executed</code> 2건과 receipt id 결속, silent auto-promotion 0, HMAC journal 6-record 체인 검증, health 3/3·OpenAPI 126·auth 401·Web 200, helper 0·listener 0 복귀와 비밀 env 파기를 확인했다. <code>down</code>·<code>volume rm</code>·<code>prune</code>은 실행하지 않았고 공개 런타임은 건드리지 않았다. G7은 Deepgram live·인증 WSS·명시 동의 물리 마이크 50분·독립 라벨 human pack이 남아 <b>외부 GATE</b>로 유지한다.</p>
|
||
<p class="dg-note">182차 적용(2026-08-08): <b>회기 전 자기점검(pre alliance gate) 화면을 데스크톱·모바일 전 폭에서 다시 짰다.</b> 계측이 먼저였다. gate 상태에서 카드가 <code>align-self:stretch</code> + <code>grid-template-rows: auto minmax(0,1fr) auto auto</code>인데 base flex의 <code>align-items:center</code>가 살아 있어 3축 블록이 1fr 행 한가운데 떠 있었다. 1920×1080 기준 카드의 <b>66.5%가 빈 공간</b>이고 축 위·아래로 각각 <code>277px</code>의 죽은 여백이 생겼다. 척도 라벨은 <code>0.54rem</code>+<code>nowrap</code>+ellipsis라 901~1279px에서 최대 <b>12개가 "전허 아…"</b>로 잘렸고, ≤620px에서는 <code>display:none</code>으로 <b>15개 전부 사라져</b> 숫자 1~5만 남았다. 320×568에서는 <code>@media (max-width:420px) and (max-height:620px)</code>의 collapsed 전용 <code>40px</code> 고정 행이 gate에도 걸려 카드가 <b>40px로 눌리고 헤더와 축이 겹쳐</b> 화면이 사실상 깨져 있었다. 수정은 네 가지다. (1) gate 카드는 행 높이만큼 늘리지 않고 <code>align-self:center</code>+<code>max-height:100%</code>+내부 스크롤로 화면 중앙에 서게 했다. (2) 3열 격자를 버리고 축마다 <b>질문 한 줄 + 전폭 5칸 척도</b> 세로 스택으로 바꿔 어느 폭에서도 라벨을 자르거나 숨기지 않는다(<code>word-break:keep-all</code>+<code>overflow-wrap:normal</code>로 어절 경계만 줄바꿈). (3) 정보가 없던 <code>3 AXES</code> 모노 배지를 제출 버튼이 왜 잠겨 있는지 답하는 <code>0/3 축 선택함</code> 진행 표시(<code>aria-live</code>)로 교체했다. (4) 40px 행 규칙에 <code>:not(.sx-alliance-gated)</code>를 걸어 collapsed mid 전용으로 되돌렸다. 선택 상태 글자색도 반투명 <code>--surface</code>에서 <code>--text-on-accent</code>로 고쳤다(라이트 4.85:1 · 다크 6.36:1, AA). 15개 뷰포트 재계측 결과 빈 공간 비율 <b>0.665→0.127</b>(1920), 라벨 잘림 <b>12→0</b>, 숨김 <b>15→0</b>, 최소 탭 타깃 <b>38×38→46×60</b>, 문서 가로 오버플로 전 구간 0이고, 320×568에서는 카드 내부 스크롤로 제출까지 도달해 실제 잠금·발화 입력 활성까지 확인했다. 인라인 mid 카드는 ≥1080px에서만 3열로 눕혀 축어록을 밀지 않게 두었고 높이는 오히려 <b>약 55px 줄었다</b>. 무회귀: <code>typecheck</code> 통과, <code>session-layout</code> <b>8/8</b>, <code>alliance-pulse</code> <b>8/8</b>, pre/mid 잠금 E2E <b>2/2</b>, <code>layout-visual-gate</code> 22/23(실패 1건 <code>settings</code>는 변경 전 baseline에서도 동일 실패로 확인된 기존 회귀). session-mvp 음성 4건 실패도 baseline 동일이라 이 변경과 무관하다.</p>
|
||
<p class="dg-note">183차 적용(2026-08-08): 182차 뒤 소유자 스크린샷에서 <b>시간 만료 상태의 게이트 화면</b>이 여전히 깨져 있어 네 개의 별개 결함을 계측으로 특정하고 닫았다. (1) <code>.sx-timebar</code>는 세션이 다크 stage 팔레트를 강제하던 시절의 잔재로, 배경이 <b>어디에도 정의된 적 없는</b> <code>--bg-panel</code>의 하드코딩 다크 폴백으로 칠해지는데 글자만 테마 토큰을 따라가 라이트에서 제목 <b>1.13:1</b>·설명 <b>1.76:1</b>이었다. 의미색 토큰(<code>--warn-*</code>/<code>--crit-*</code>)으로 되돌려 <b>13.88:1</b>·<b>6.94:1</b>(다크 13.86·9.30)로 올렸다. (2) 같은 이유로 <code>.sx-end-dialog__panel</code>이 어두운 gradient 위 어두운 글자라 제목 <b>2.68:1</b>·본문 <b>1.14:1</b>로 사실상 보이지 않았다. 불투명 <code>--bg-app</code> 표면으로 바꿔 <b>15.79:1</b>·<b>5.19:1</b>(다크 15.18·6.52)이 됐다. (3) 보태니컬 장식 위로 본문을 올리는 <code>.sx-page--active > *{position:relative}</code>가 같은 특이도(0,1,0)로 <b>뒤에 있어</b> 종료 모달의 <code>position:fixed</code>를 덮어썼다. 오버레이가 화면을 덮는 대신 그리드 한 칸(<code>1532×32px</code>)으로 눌려 들어갔다. <code>:not(.sx-end-dialog)</code>로 제외해 <code>fixed</code> <b>1600×900 뷰포트 전체</b>를 회복했다. (4) 게이트 행 정의 <code>.sx-page--active.sx-alliance-gated</code>(0,2,0)가 <code>:has()</code> 두 개짜리 규칙(0,3,0)보다 특이도가 낮아 <b>시간 만료 바가 뜨는 순간 밀려났다</b>. 숨긴 축어록·컨트롤바의 빈 트랙이 남아 화면 절반이 비었다(<code>80px 59px 628px 0px 116px</code> 5행). 게이트 규칙에도 같은 <code>:has()</code>를 붙여 <b>3행</b>(<code>80px 59px 693px</code>)으로 정정했다. 무회귀: <code>typecheck</code> 통과, <code>session-layout</code>+<code>alliance-pulse</code>+<code>layout-visual-gate</code> <b>31/31</b>(182차에서 실패했던 <code>settings</code>도 통과), 시간만료 종료 다이얼로그 E2E <b>2/2</b>, pre/mid 잠금 E2E <b>2/2</b>, <code>full-sweep-session</code>+<code>self-directed-learning-loop</code> 30 pass. 남은 음성 4건(<code>voice skip</code>·<code>Space mic</code>)은 변경분 stash 후 baseline에서도 동일 실패라 이 변경과 무관한 기존 회귀다.</p>
|
||
<p class="dg-note">184차 적용(2026-08-08): 182·183차 변경이 <b>로컬 워크트리에만</b> 있어 소유자가 보던 공개 사이트에는 반영될 수 없던 상태를 배포로 닫았다. 소유자 스크린샷의 <code>3 AXES</code>는 이미 소스에서 사라진 문자열이었고, 접속 주소가 <code>https://vignette.chanpaca.net</code>이라 커밋 전 변경이 보일 수 없는 것이 원인이었다. 커밋 <code>f0c2240f</code>로 고정한 뒤 <code>npm run build</code>(Session 청크 <code>Session-DnhnKvTy.js</code>·<code>Session-Xn1JLv-d.css</code>), <code>scripts\preserve-pages-assets.ps1</code>로 직전 5개 production 배포의 지연 로드 자산 <b>104개</b>를 보존하고(진행 중 세션의 lazy chunk 404 방지), <code>wrangler pages deploy dist --project-name vignette --branch main --commit-hash f0c2240f</code>로 배포했다(deployment <code>a12c195c</code>, 신규 업로드 37 / 기존 666). 커스텀 도메인 검증은 <code>index-BTkJUPLZ.js</code> 200, Session JS/CSS 모두 올바른 MIME 200이며 <b>신규 마커</b>(<code>축 선택함</code>·<code>is-gate</code>·<code>:not(.sx-end-dialog)</code>) 각 1건, <b>구버전 마커</b>(<code>3 AXES</code>·<code>--bg-panel</code>·척도 <code>text-overflow</code>) 전부 0건이다. 첫 조회에서 edge가 이전 index를 준 건 배포 직후 타이밍이었고 재조회에서 신규 index로 확정됐다. 공개 API·런타임은 건드리지 않았다.</p>
|
||
<p class="dg-note">185차 적용(2026-08-09): 177·181차의 G8 DONE 판정은 <b>rollback receipt gate 완료</b>로 범위를 정정했다. 108/108은 localhost candidate 결과였고 같은 NAS 평문 origin에서는 secure-context 전용 <code>crypto.randomUUID</code> 예외로 회기 E2E 24건이 실패했다. 소스는 <code>randomUuid()</code>로 통일해 insecure-origin 6/6·API type·typecheck·build를 통과했지만 NAS에는 아직 미배포다. 따라서 G8 카드는 <b>BUILD · RECEIPT DONE · RUNTIME REVALIDATION</b>으로 되돌리고, clean-head release mode 구현→수정 배포→실제 NAS-origin 전체 회기 E2E GREEN까지를 남긴다. G7은 운영 기본 <code>local_whisper</code>/<code>melotts</code>의 current 공개 배포·인증 WSS·명시 동의 mic 50분·동시 runtime/topology·독립 human pack 전까지 외부 GATE다.</p>
|
||
<p class="dg-note">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 <code>72698232…5d41c</code>(1,015,222 bytes·TOC 1,752)를 보존했다. G7 재감사에서는 공개 OpenAPI 119·<code>/admin/voice-runtime</code> 부재, OpenAI voice provider, local sidecar listener 0, Windows host topology와 Compose-only collector 불일치를 확인했으므로 사람 증거 전에 launcher·topology 선행 구현이 필요하다.</p>
|
||
<p class="dg-note">187차 적용(2026-08-09): G8 clean-head dry-run은 active <code>6030a677…c611</code>→desired archive <code>6b1b15bd…a3f7</code>를 mutation 0으로 결속했다. execute 1은 archive CRLF/API type byte-check에서, execute 2는 API·types·build 통과 뒤 session E2E <code>109 passed / 5 failed</code>에서 모두 승격 전에 fail-closed되어 NAS active/image/DB는 바뀌지 않았다. source-only UUID 4건은 production 번들에서 <code>/src/lib/uuid.ts</code> import가 불가능한 gate 배치 오류라 별도 Vite gate 6/6으로 분리했다. 남은 실제 제품 blocker는 timezone 없는 <code>started_at</code>을 KST 브라우저가 약 9시간 경과로 오인해 시간종료 모달이 정상 코칭 이력을 가리는 문제다. backend UTC offset/Z 계약과 focused disposable DB/browser 회귀를 닫고 새 clean commit을 만든 뒤에만 NAS 배포를 재개한다.</p>
|
||
<p class="dg-note">188차 적용(2026-08-09): 회기 시간 P0를 우회 없이 수정했다. browser-facing <code>started_at</code>/<code>ended_at</code>과 learner/teacher dashboard timestamp는 UTC <code>+00:00</code>을 반환하고, 리뷰의 달력 날짜는 서버 locale 대신 KST(+09:00)를 사용한다. backend 관련 65 passed, 후속 voice 계약을 포함한 API 전체 921 passed, Ruff·compile·web typecheck를 통과했다. 고유 Compose DB/API/Web/engine과 <code>Asia/Seoul</code> 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가 남았다.</p>
|
||
<p class="dg-note">189차 적용(2026-08-09): G7 외부 실행 전 P0를 소스에서 닫았다. production runner exit 0은 canonical checker 0·<code>gate_closed=true</code>에 결속하고, 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, <code>psutil==6.1.1</code>을 매 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다.</p>
|
||
<p class="dg-note">190차 적용(2026-08-09): exact include 52개를 clean commit <code>5221f79e…1c69</code>으로 고정하고 tree <code>e4f15001…308b</code>, 2회 동일 archive <code>1109bf86…0f4b</code>를 결속했다. fresh NAS dump <code>6f4b95a7…b529f</code>(1,015,222 bytes·TOC 1,752/TABLE DATA 129) 뒤 execute는 API 921·types·typecheck·build·insecure-context 6/6·candidate DB 회기 후반까지 통과했지만 <b>110/112</b>에서 승격 전에 fail-closed했다. 실패 2건은 Vite entry <code>/src/main.tsx</code>만 차단하던 boot 진단 테스트가 production hashed entry <code>/assets/index-*.js</code>를 차단하지 못한 환경 계약 누락이었다. 두 entry를 함께 차단하도록 고쳐 Vite source 2/2와 production preview 2/2를 통과했다. NAS mutation·rollback·DB migration은 0이며, 테스트 수정의 새 clean commit을 재결속한 한 번의 execute가 남았다.</p>
|
||
<p class="dg-note">191차 적용(2026-08-09): 후속 clean commit <code>21461ab3…6fde</code>·tree <code>5c5f12a8…524e</code>·archive <code>20d49694…fa1a</code>는 candidate <b>112/112</b>를 통과하고 NAS 새 API/Web <code>d5021950…</code>/<code>376a3aa8…</code>를 올렸다. 실제 NAS-origin postdeploy G4 desktop/mobile 2건이 110/112에서 실패하자 active-state commit 전에 이전 <code>52e0e816…</code>/<code>6fdbb646…</code>로 자동 rollback했고 health·auth 401·OpenAPI 126·G0~G8 route·image ID를 재검증해 rollback <b>verified</b>로 종료했다. exact-image 평문 origin에서 문장 fingerprint의 <code>crypto.subtle.digest</code> 의존이 요청 전 예외를 내는 원인을 확인했다. 원문 외부 전송 없이 portable SHA-256 fallback을 추가하고 표준 digest를 payload에 exact 고정해 secure localhost 2/2와 Tailnet insecure origin 2/2를 통과했다. 새 clean commit의 전체 candidate+NAS-origin 0 failure 전까지 G8은 runtime REVALIDATION이다.</p>
|
||
<div class="dg-principles" aria-label="디자인 생성 가드레일">
|
||
<div><b>래스터만 사용</b><span>이미지 생성 도구 산출물은 PNG 기반 시안이다. SVG·벡터·와이어프레임·로고 시트로 해석하지 않는다.</span></div>
|
||
<div><b>기능 우선</b><span>메인 라우트의 실제 액션과 정보 구조를 먼저 반영한다. 장식은 기능을 가리지 않는 수준에서만 쓴다.</span></div>
|
||
<div><b>온화하지만 임상적</b><span>종이 질감, 낮은 채도, 차분한 빛은 허용하되 상담 훈련 도구의 신뢰감을 먼저 세운다.</span></div>
|
||
<div><b>반응형 세트</b><span>각 핵심 화면은 한 장 안에 PC·태블릿·모바일을 함께 배치해 전환 규칙을 비교한다.</span></div>
|
||
</div>
|
||
<div class="dg-table-wrap">
|
||
<table class="dg-table">
|
||
<thead>
|
||
<tr><th>화면</th><th>추출한 핵심 기능</th><th>디자인 의도</th></tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr><td>로그인/진입</td><td>Google OAuth, 허용 도메인 안내, 로컬 역할 선택, 교육용 비치료 고지</td><td>신뢰·보안이 먼저 보이는 조용한 진입 화면</td></tr>
|
||
<tr><td>학습자 홈</td><td>학습 요약, 진행/리뷰 가능 회기, 마지막 세션 리캡, Live2D 감정 힌트, AI 코치, 학습·기록 드릴다운</td><td>오늘 이어갈 회기와 학습 상태가 먼저 보이는 개인 대시보드</td></tr>
|
||
<tr><td>상담 세션</td><td>회기 단계, 내담자 컨텍스트, 아바타, 음성 오브, 실시간 자막, 텍스트 폴백, 안전 점검</td><td>화면 중심은 내담자, 피드백은 조용한 신호</td></tr>
|
||
<tr><td>회기 리뷰</td><td>요약, 감정 밸런스, 회기 흐름, 축어록, 기법 태그, 개선점, 오디오/PDF</td><td>점수 폭격이 아니라 근거 기반 회고</td></tr>
|
||
<tr><td>교수 콘솔</td><td>리뷰 대기 큐, 코호트 현황, 페르소나 검수, 종료 회기 목록</td><td>awareness보다 triage에 가까운 교수자 작업대</td></tr>
|
||
<tr><td>관리자 콘솔</td><td>운영 홈: 교육 진행·온라인 사용자·헬스 샘플 이력·AI 비용·운영 티켓 요약. 하위 메뉴: 사용자 관리, 접근 권한, 운영 티켓 처리 큐. 헬스 샘플은 화면 조회와 synthetic sampler가 같은 DB 이력에 남김</td><td>상태 관측과 관리 작업을 분리한 실제 운영 콘솔</td></tr>
|
||
<tr><td>설정</td><td>계정, 관리자 AI 운영 설정, 지원 요청 현황, 테마, 음성 프리셋, 속도, 알림</td><td>역할별 설정과 본인 지원 요청 상태만 보이는 조용한 환경 관리</td></tr>
|
||
<tr><td>이미지 요소</td><td>종이 질감, 상담실 책상, 노트, 식물, 부드러운 빛, 패턴, 아바타 분위기</td><td>UI 뒤에서만 느껴지는 따뜻함과 반구상 내담자 톤</td></tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
<div class="dg-gallery" aria-label="생성된 디자인 시안 갤러리">
|
||
<article class="dg-card">
|
||
<img src="design-concepts/generated/01-login-responsive.png" alt="로그인 화면 PC 태블릿 모바일 반응형 디자인 시안" loading="lazy">
|
||
<div class="dg-card-body"><b>로그인/진입</b><span>OAuth, 허용 도메인, 로컬 역할 선택, 교육용 고지를 신뢰 중심으로 정리한 진입 화면.</span><code>01-login-responsive.png</code></div>
|
||
</article>
|
||
<article class="dg-card">
|
||
<img src="design-concepts/generated/02-learner-home-responsive-v2-dark-unified.png" alt="학습자 홈 PC 태블릿 모바일 다크 테마 디자인 시안" loading="lazy">
|
||
<div class="dg-card-body"><b>학습자 홈</b><span>내담자 선택, 회기 시작, 이전 회기 이어가기, 리뷰 가능 상태를 다크 작업대 톤으로 배치.</span><code>02-learner-home-responsive-v2-dark-unified.png</code></div>
|
||
</article>
|
||
<article class="dg-card">
|
||
<img src="design-concepts/generated/11-learner-dashboard-redesign-concept-b.png" alt="학습자 대시보드 회기 리캡 중심 재설계 시안" loading="lazy">
|
||
<div class="dg-card-body"><b>학습자 대시보드 재설계</b><span>회기 리캡, AI 코치, 이어하기 CTA를 첫 화면의 단일 결정 영역으로 묶음.</span><code>11-learner-dashboard-redesign-concept-b.png</code></div>
|
||
</article>
|
||
<article class="dg-card">
|
||
<img src="design-concepts/generated/12-learner-dashboard-redesign-concept-b-alt.png" alt="학습자 대시보드 대안 재설계 시안" loading="lazy">
|
||
<div class="dg-card-body"><b>학습자 대시보드 대안</b><span>마지막 내담자 반응과 내 마지막 반응을 더 강하게 드러낸 대안 레이아웃.</span><code>12-learner-dashboard-redesign-concept-b-alt.png</code></div>
|
||
</article>
|
||
<article class="dg-card">
|
||
<img src="design-concepts/generated/03-session-responsive-v2-dark-unified.png" alt="라이브 상담 세션 PC 태블릿 모바일 다크 테마 디자인 시안" loading="lazy">
|
||
<div class="dg-card-body"><b>라이브 상담 세션</b><span>내담자 무대, 자막, 입력, AI 코치, 안전 상태를 전역 라이트·다크 테마와 같은 토큰으로 구성.</span><code>03-session-responsive-v2-dark-unified.png</code></div>
|
||
</article>
|
||
<article class="dg-card">
|
||
<img src="design-concepts/generated/04-session-review-responsive-v2-dark-unified.png" alt="회기 리뷰 PC 태블릿 모바일 다크 테마 디자인 시안" loading="lazy">
|
||
<div class="dg-card-body"><b>회기 리뷰</b><span>감정 흐름, 축어록, 기법 태그, 교수자 콜아웃을 다크 근거 기반 회고 화면으로 묶음.</span><code>04-session-review-responsive-v2-dark-unified.png</code></div>
|
||
</article>
|
||
<article class="dg-card">
|
||
<img src="design-concepts/generated/05-professor-responsive.png" alt="교수 콘솔 PC 태블릿 모바일 반응형 디자인 시안" loading="lazy">
|
||
<div class="dg-card-body"><b>교수 콘솔</b><span>리뷰 대기, 코호트 현황, 페르소나 검수, 종료 회기를 triage 중심으로 배치.</span><code>05-professor-responsive.png</code></div>
|
||
</article>
|
||
<article class="dg-card">
|
||
<img src="design-concepts/generated/06-admin-responsive.png" alt="관리자 콘솔 PC 태블릿 모바일 반응형 디자인 시안" loading="lazy">
|
||
<div class="dg-card-body"><b>관리자 콘솔</b><span>운영 홈은 서비스 상태 관측에 집중하고 사용자·권한·티켓은 별도 메뉴로 분리. 티켓과 헬스 샘플은 DB 기반 모델을 사용하고 sampler가 주기 적재 가능.</span><code>06-admin-responsive.png</code></div>
|
||
</article>
|
||
<article class="dg-card">
|
||
<img src="design-concepts/generated/07-settings-responsive.png" alt="설정 화면 PC 태블릿 모바일 반응형 디자인 시안" loading="lazy">
|
||
<div class="dg-card-body"><b>설정</b><span>계정, AI 운영, 지원 요청, 테마, 음성, 알림을 역할별로 노출하는 차분한 설정 화면.</span><code>07-settings-responsive.png</code></div>
|
||
</article>
|
||
<article class="dg-card">
|
||
<img src="design-concepts/generated/08-warm-visual-elements.png" alt="온화한 배경과 패턴 이미지 요소 보드" loading="lazy">
|
||
<div class="dg-card-body"><b>온화한 이미지 요소</b><span>종이 질감, 상담실 책상, 노트, 식물, 낮은 채도의 빛을 UI 보조 요소로 정리.</span><code>08-warm-visual-elements.png</code></div>
|
||
</article>
|
||
<article class="dg-card">
|
||
<img src="design-concepts/generated/09-avatar-pattern-elements.png" alt="반구상 아바타와 패턴 이미지 요소 보드" loading="lazy">
|
||
<div class="dg-card-body"><b>아바타/패턴 요소</b><span>반구상 내담자 분위기와 배경 패턴 방향. 실제 페르소나 아이덴티티는 별도 제작 기준을 따른다.</span><code>09-avatar-pattern-elements.png</code></div>
|
||
</article>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- ░░ STATUS BOARD ░░ -->
|
||
<section class="block" id="board">
|
||
<div class="sec-head">
|
||
<div>
|
||
<h2>상태 보드</h2>
|
||
<p class="sub">현재 추적 중인 작업을 카드로. <b>요약만 노출</b>되고, 상세 근거·검증 로그는 카드를 펼쳐야 보인다. 필터로 "내 차례"·블로커만 추려 볼 수 있다.</p>
|
||
</div>
|
||
</div>
|
||
<div class="filterbar" role="group" aria-label="상태 필터">
|
||
<span class="fl">필터</span>
|
||
<button class="fbtn" data-filter="all" aria-pressed="true">전체 <span class="fn" data-count="all">0</span></button>
|
||
<button class="fbtn" data-filter="done" aria-pressed="false">완료 <span class="fn" data-count="done">0</span></button>
|
||
<button class="fbtn" data-filter="doing" aria-pressed="false">진행 <span class="fn" data-count="doing">0</span></button>
|
||
<button class="fbtn" data-filter="planned" aria-pressed="false">계획 카드 <span class="fn" data-count="planned">0</span></button>
|
||
<button class="fbtn f-owner" data-filter="owner" aria-pressed="false">오너 의존 <span class="fn" data-count="owner">0</span></button>
|
||
<button class="fbtn" data-filter="crit" aria-pressed="false">납품 필수 <span class="fn" data-count="crit">0</span></button>
|
||
</div>
|
||
|
||
<!-- TRACK 1 -->
|
||
<div class="track-group">
|
||
<div class="tg-head"><h3>한신대 PDF 정합성 · 이번 세션 발견</h3><span class="tg-note">원천 기술분석 PDF(트웬티온스×한신대, 19쪽) 전수 정독 대조 (2026-06-27)</span></div>
|
||
<div class="scards">
|
||
<article class="scard" data-status="done" data-cat="코어·차별" data-owner="0">
|
||
<button class="scard-head" aria-expanded="false"><span class="chip c-done">정합 ✓</span><span class="scard-mid"><span class="scard-title">원안과 잘 맞는 코어(유지)</span><span class="scard-sum">저항·반응 조절·상태머신·RAG·이론모드·가드레일·평가 JSON Schema가 이미 구현·정렬됨.</span></span><span class="caret" aria-hidden="true"></span></button>
|
||
<div class="scard-body"><div class="kv"><b>현재</b><p>PDF가 차별 기술로 꼽은 <b>'저항·반응 조절'</b>(AI가 너무 착하지 않게, 실제 환자처럼)·상태머신(라포→탐색→개입→정리)·RAG(자료검색)·이론모드(인간중심/CBT)·답변과 분리된 가드레일·SSE 스트리밍·React19·<b>피드백 JSON Schema 강제</b>(벤더중립)가 이미 구현·정렬됨. Stanford RCT(피드백 없는 연습=역효과)도 H2 턴평가로 충족 방향.</p></div><div class="kv"><b>권고</b><p>이 코어를 '교육용(비의료)' 포지션으로 유지. 평가는 교수자 검토용 보조지표로 표기.</p></div></div>
|
||
</article>
|
||
<article class="scard" data-status="done" data-cat="코어·차별" data-owner="0">
|
||
<button class="scard-head" aria-expanded="false"><span class="chip c-done">신규 DONE</span><span class="scard-mid"><span class="scard-title">역린(逆鱗)·트리거 + 수련생 misconduct 반응</span><span class="scard-sum">무례·모욕·조롱 시 현실적 동맹 균열 + 핵심상처 역린 블록 추가. 무회귀 77/77.</span></span><span class="caret" aria-hidden="true"></span></button>
|
||
<div class="scard-body"><div class="kv"><b>현재</b><p>라이브 테스트에서 수련생이 내담자를 "수동적 패배자"라 비난·조롱("웃음이 나오십니까")해도 민재가 공손히 흡수 → 훈련 가치 훼손. 수정: ① L0에 무례·모욕·조롱·인신공격 시 <b>현실적 동맹 균열</b>(위축·방어·따지기·종결의향) 지침 ② <b>PersonaCard.triggers(역린)</b> 필드 + CCD 핵심상처 파생 역린 블록(상담자가 핵심상처를 조롱·확정·강요·평가절하 시 가장 강한 반응). PDF '저항·반응 조절' 핵심 차별 강화. 무회귀 77/77.</p></div><div class="kv k-warn"><b>잔여</b><p>P1~P3 명시 triggers는 임상팀 정밀 저작(현재 CCD 파생 기본값으로 작동). 라이브 재현 검증.</p></div></div>
|
||
</article>
|
||
<article class="scard" data-status="done" data-cat="안전·위기" data-owner="0">
|
||
<button class="scard-head" aria-expanded="false"><span class="chip c-done">CRITICAL 1차 DONE</span><span class="scard-mid"><span class="scard-title">위기 안전게이트<span class="critbadge">납품 필수</span></span><span class="scard-sum">자해·자살 감지 시 LLM 호출 전 중단, 109 안내, safety_events 적재, 교수자 대시보드 알림 큐까지 연결.</span></span><span class="caret" aria-hidden="true"></span></button>
|
||
<div class="scard-body"><div class="kv"><b>현재</b><p>PDF(9·16·18쪽): 자해·자살 감지 시 <b>대화 중단 + 자살예방상담 109 안내 + 지도교수 자동 알림</b>, 안전은 <b>LLM 밖 별도 CrisisGate</b>. 구현: <code>CRISIS_HOTLINE_NUMBER=109</code>, <code>TurnResponse.crisis_resource</code>/<code>conversation_stopped</code>, REST·SSE·voice 경로의 엔진 호출 전 중단, <code>app.safety_events</code> detail 적재, 교수자 대시보드 안전 알림 큐.</p></div><div class="kv"><b>검증</b><p><code>pytest app/test_session_turn_persistence.py app/test_voice_ws.py -q</code> 20 passed, 안전 이벤트 DB insert payload 테스트 포함. <code>pytest app/test_teacher_dashboard.py app/test_session_turn_persistence.py app/test_voice_ws.py -q</code> 21 passed.</p></div><div class="kv k-warn"><b>잔여</b><p>비밀보장 예외고지·생명유지서약 등 임상 문안, 실시간 push/메일 알림, 지역 자원 확장, 운영 정책 문구는 소유자·임상팀 확정 필요. 현재 교수자 알림은 <code>safety_events</code> 기반 대시보드 큐다.</p></div></div>
|
||
</article>
|
||
<article class="scard" data-status="done" data-cat="교수자·평가" data-owner="0">
|
||
<button class="scard-head" aria-expanded="false"><span class="chip c-done">CRITICAL UX 1차 DONE</span><span class="scard-mid"><span class="scard-title">교수자 '학습자 이력·성장 추적' UI/UX<span class="critbadge">핵심 가치</span></span><span class="scard-sum">학생 분석 메뉴에서 전체 학습자 검색 테이블, 행 펼침 요약, 학습자별 상세 탭과 전체 회기 타임라인을 확인.</span></span><span class="caret" aria-hidden="true"></span></button>
|
||
<div class="scard-body"><div class="kv"><b>현재</b><p><code>TeacherDashboardResponse.learner_growth</code>와 <code>GET /teacher/learners/{learner_id}/analysis</code>가 턴별 fast-loop 평가의 <code>appropriateness</code>, <code>rapport_signal</code>, 기법 태그를 학습자별로 집계한다. <code>/teach</code>는 검토 큐·위기 알림·페르소나 검수·최근 회기 triage만 맡고, <code>/teach/analysis</code>는 검색 가능한 1행 학습자 테이블을 먼저 보여준다. 우측 펼침 버튼은 행 아래에 회기별 미니 추이, 평균 라포, 변화폭, 기법 태그, 최근 기록을 확장하고, 이름 또는 상세 보기 버튼은 <code>?learner=</code> 상세 화면으로 들어간다. 상세 화면은 추이·전체 회기·단계 분석 탭으로 나뉘며, 회기 행은 <code>/teach/session/{id}/review</code>로 진입해 같은 회기 리뷰를 교수자 읽기 전용으로 연다. 평가가 없는 회기는 "평가 부족"으로 남겨 가짜 곡선을 만들지 않고, 워크시트는 학습자 제출물 수정 없이 검토 전용으로 표시한다.</p></div><div class="kv"><b>검증</b><p><code>python -X utf8 -m pytest -p no:cacheprovider app/test_teacher_dashboard.py -q</code> 6 passed, <code>npm run check:api-types</code>, <code>npm run typecheck</code>, <code>npm run build</code> passed. <code>PLAYWRIGHT_PORT=5215 npx playwright test e2e/teacher.spec.ts --project=chromium-single-run --workers=1</code> 9 passed, <code>PLAYWRIGHT_PORT=5217 npx playwright test e2e/layout-visual-gate.spec.ts --project=chromium-single-run --workers=1</code> 12 passed.</p></div><div class="kv k-warn"><b>잔여</b><p>자기효능감·기술숙련도·수련만족도 사전사후(H1)와 교수자 코멘트/검수 워크플로 확장은 별도 평가설계·임상팀 문항 확정이 필요하다.</p></div></div>
|
||
</article>
|
||
<article class="scard" data-status="done" data-cat="교수자·평가" data-owner="0">
|
||
<button class="scard-head" aria-expanded="false"><span class="chip c-done">검토 상태 1차 DONE</span><span class="scard-mid"><span class="scard-title">교수자 회기 검토 상태 저장</span><span class="scard-sum"><code>app.session_review_status</code>와 <code>PUT /teacher/sessions/{session_id}/review-status</code>로 검토 메모·완료 상태를 저장.</span></span><span class="caret" aria-hidden="true"></span></button>
|
||
<div class="scard-body"><div class="kv"><b>현재</b><p>교수자/관리자는 <code>/sessions/{id}/review</code>를 읽을 수 있고, 학습자 워크시트 저장은 learner 전용으로 유지한다. 교수자 검토 상태는 <code>app.session_review_status</code>에 <code>review_status</code>, <code>review_note</code>, <code>reviewed_at</code>로 저장되며, teacher dashboard 응답과 <code>SessionReview</code>가 이 값을 표시한다. <code>PUT /teacher/sessions/{session_id}/review-status</code>는 교수자 메모 저장과 검토 완료 처리를 담당하고, 검토 완료된 회기는 pending queue에서 제외한다.</p></div><div class="kv"><b>검증</b><p><code>pytest app/test_teacher_dashboard.py app/test_rbac_idor.py app/test_learner_dashboard.py -q</code> 12 passed, <code>npm run check:api-types</code>, <code>npm run typecheck</code>, <code>npm run build</code>, <code>teacher.spec.ts</code> 4 passed. 교수 콘솔 검토 큐 노출과 학습자 리뷰의 교수자 전용 빈 grid 제거는 E2E/시각 게이트로 재검증했다.</p></div><div class="kv k-warn"><b>잔여</b><p>임상 루브릭·최종 검수 기준은 외부 정의 전까지 완료 처리하지 않는다.</p></div></div>
|
||
</article>
|
||
<article class="scard" data-status="done" data-cat="학습자·UX" data-owner="0">
|
||
<button class="scard-head" aria-expanded="false"><span class="chip c-done">개인화 1차 DONE</span><span class="scard-mid"><span class="scard-title">학습자 홈 대시보드 / 최근 회기 리캡</span><span class="scard-sum"><code>GET /sessions/dashboard</code>와 <code>LearnerHome</code>가 실제 세션 기반 성장·피드백·페르소나 진행을 표시.</span></span><span class="caret" aria-hidden="true"></span></button>
|
||
<div class="scard-body"><div class="kv"><b>현재</b><p>교수자 성장 집계와 중복되던 점수·라포·기법 계산을 <code>app.services.session_metrics</code>로 공용화했다. <code>GET /sessions/dashboard</code>는 학습자 본인 세션만 집계해 <code>overview</code>, <code>growth</code>, <code>persona_progress</code>, <code>achievements</code>, <code>recent_feedback</code>를 반환하고, <code>LearnerHome</code>은 누적 회기·리뷰 대기·최근 평가·라포 흐름·페르소나별 진행·최근 피드백·마지막 세션 리캡을 실제 서버 데이터로 보여준다. 성취는 공식 수료/등급이 아니라 실제 연습 milestone만 사용한다.</p></div><div class="kv"><b>검증</b><p><code>pytest app/test_learner_dashboard.py app/test_teacher_dashboard.py app/test_rbac_idor.py -q</code> 12 passed, <code>npm run check:api-types</code>, <code>npm run typecheck</code>, <code>npm run build</code>, <code>learner.spec.ts</code> 6 passed, <code>session-review.spec.ts</code> 2 passed, <code>layout-visual-gate.spec.ts</code> 9 passed, <code>session-layout.spec.ts</code> 4 passed.</p></div><div class="kv k-warn"><b>잔여</b><p>UX 감사 기준으로 2차에서는 카드 격자 비중을 줄이고 게이지·small multiples·테이블 중심으로 정리해야 한다. 모바일에서 임상 요약을 단순 숨김 처리하는 부분도 접힘/요약으로 바꾸는 게 맞다.</p></div></div>
|
||
</article>
|
||
<article class="scard" data-status="done" data-cat="거버넌스·아키텍처" data-owner="0">
|
||
<button class="scard-head" aria-expanded="false"><span class="chip c-done">Node 계약 DONE · 외부 GATE 분리</span><span class="scard-mid"><span class="scard-title">2-tier 모델 정책 + Node 전환 계약 경계</span><span class="scard-sum">Node 교체 방향과 계약 경계는 완료. 2-tier 운영 정책, 장기 한도, 등재 문서 반영은 owner board gate로 분리.</span></span><span class="caret" aria-hidden="true"></span></button>
|
||
<div class="scard-body"><div class="kv"><b>현재</b><p>① PDF: 고성능(Claude·GPT=내담자발화·실시간평가)/저비용 한국어(Solar=단순응답·사후요약) 2단계 + 캐싱 90%절감. 현재 client AI는 claude_cli 기본 라우팅, fast/deep evaluator는 <code>EVALUATOR_FAST_MODEL</code>/<code>EVALUATOR_DEEP_MODEL</code> 설정 시 모델 override를 gateway에 전달한다. evaluator structured 결과는 canonical request SHA-256 기반 인메모리 semantic cache 1차로 중복 호출을 줄이며, 원문 prompt·completion은 캐시에 저장하지 않는다. 벤더중립 인터페이스와 L0~L2 cache hint/session reuse도 유지한다. ② 백엔드 방향은 <b>Node.js 우선 교체 가능성</b>으로 정했다. 단, 현재 <b>Python/FastAPI</b>는 납품 핵심 경로라 전면 재작성하지 않고, 계약 경계를 먼저 고정해 갈아치울 수 있게 만든다. 이번 패스에서 앱 orchestrator가 raw gateway SSE line을 직접 해석하지 않도록 <code>EngineClient.stream_packets()</code> 경계를 추가했고, Python gateway의 <code>/v1/generate</code> 응답은 <code>GenerateResponse</code> 계약 모델로 반환한다. <code>gateway-default</code>는 실제 provider 모델명이 아니라 gateway 기본 라우팅 sentinel로 계약 모듈이 소유하며, FastAPI client와 Python gateway는 이 값을 explicit model override 없이 정규화한다. 추가로 <code>app/session_read_model.py</code>와 <code>app/persona_read_model.py</code>가 세션·페르소나 브라우저 응답 DTO와 builder/mapper를 소유하고, <code>app/persona_generation_contract.py</code>가 페르소나 draft schema/prompt/payload coercion을, <code>SessionEvaluationWrite.from_result()</code>/<code>from_error()</code>가 session evaluation write packet 생성을 소유하게 해 Node read/write API가 따라야 할 contract 경계를 분리했다.</p></div><div class="kv k-warn"><b>권고</b><p>2-tier 후속은 client AI/요약 모델 정책, evaluator cache의 운영 hit-rate 관측, 장기 한도 정책으로 점진 도입한다. Node 전환은 이미 고정한 <code>engine_gateway</code>, <code>structured_payload_from_response()</code>, <code>session_read_model</code>, <code>persona_read_model</code>, <code>persona_generation_contract</code>, <code>SessionEvaluationWrite</code> 계약을 유지한다. live Node gateway endpoint conformance는 실제 Node gateway 구현이 생긴 뒤 붙이고, admin mapper cleanup은 RBAC/audit/RLS blast radius 때문에 뒤로 둔다.</p></div></div>
|
||
</article>
|
||
<article class="scard" data-status="done" data-cat="코어·차별" data-owner="0">
|
||
<button class="scard-head" aria-expanded="false"><span class="chip c-done">검증 ✓</span><span class="scard-mid"><span class="scard-title">무회귀 검증 종합</span><span class="scard-sum">과거 전체 E2E 113은 기준선, 최신 라운드는 focused 검증으로 재확인.</span></span><span class="caret" aria-hidden="true"></span></button>
|
||
<div class="scard-body"><div class="kv"><b>현재</b><p>2026-06-27/28 기준선에서는 SEO/GEO 공유 카드, 온보딩 반영 E2E, 학습자 홈, 회기 시작/종료, 리뷰 공유 버튼, 음성 성공 경로, 레이아웃 회귀를 <b><code>npm run e2e</code> 113 passed + <code>npm run typecheck</code>/<code>npm run build</code></b>로 닫았다. 이후 다크 UI v2와 Node 전환 경계 보강은 현재 변경 범위에 맞춰 focused gate로 재검증한다. 최신 다크 UI v2 증거는 <code>learner.spec.ts</code>+<code>session-review.spec.ts</code> 9 passed, <code>session-layout.spec.ts</code> 8 passed, <code>layout-visual-gate.spec.ts</code> 9 passed다.</p></div><div class="kv"><b>다음</b><p>남은 원천문서 갭, 임상 루브릭/최종 평가설계, public Google OAuth 실제 <code>/turn</code>, 배포지 운영 비밀값 주입을 순서대로 닫는다.</p></div></div>
|
||
</article>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- TRACK 2 -->
|
||
<div class="track-group">
|
||
<div class="tg-head"><h3>원천문서 갭 분석</h3><span class="tg-note">산학협력 원천문서 5종 정독 + 코드 교차조사 (2026-06-26). 근거: source-docs-gap-analysis-2026-06-26.md · critical 3 / high 4 / medium+ 6</span></div>
|
||
<div class="scards">
|
||
<article class="scard" data-status="done" data-cat="콘텐츠·산출물" data-owner="0">
|
||
<button class="scard-head" aria-expanded="false"><span class="chip c-done">C1 4차 DONE</span><span class="scard-mid"><span class="scard-title">사례개념화·치료계획 워크시트 저장형 구조<span class="critbadge">critical</span></span><span class="scard-sum">학습자 저장, 임상팀 루브릭 외부화 scaffold, 교수자 수동 검수 상태 저장을 연결했다. 실제 루브릭 콘텐츠는 외부 의존.</span></span><span class="caret" aria-hidden="true"></span></button>
|
||
<div class="scard-body"><div class="kv"><b>현재</b><p>doc1/4/5가 1급 산출물로 요구하는 워크시트 축(탐색 11항목·호소 5영역·인지삼제·1/2차 감정·보호/방해 4사분면·생물심리사회 목표)을 <code>SessionReviewResponse.caseWorksheet</code>로 노출한다. 서버는 저장된 축어록만 근거로 규칙 기반 초안을 만들고, 프론트 리뷰 화면은 근거 턴으로 점프 가능한 워크시트를 편집할 수 있다. 저장본은 <code>PUT /sessions/{id}/review/worksheet</code> → <code>app.case_worksheet</code>에 저장되고, 이후 <code>GET /review</code>는 <code>saved_by_learner</code> 워크시트를 자동 초안보다 우선 반환한다. 3차에서는 임상팀 확정 루브릭을 코드 수정 없이 받을 수 있도록 <code>data/rubrics/case-worksheet-rubric.json</code>, <code>case-worksheet-rubric.schema.json</code>, <code>app.services.case_worksheet_rubric</code>, <code>scripts/check-case-worksheet-rubric.py</code>를 추가했다. 현재 rubric은 <code>status=scaffold_only</code>, <code>scoring_enabled=false</code>이고, 5개 section/28개 worksheet item key가 서버 생성 워크시트와 일치하는지만 검증한다. 64차에서는 템플릿 key source를 <code>CASE_WORKSHEET_SECTION_SPECS</code>/<code>case_worksheet_template_item_keys()</code>로 명시해 validator/CLI/test가 같은 생성 워크시트 spec을 참조한다. 73차에서는 <code>app.session_review_status</code>에 워크시트 수동 검수 상태·메모·시각을 붙여 교수자/관리자가 승인·수정요청·반려 결정을 남길 수 있게 했다.</p></div><div class="kv"><b>검증</b><p><code>py -3.11 -X utf8 -m pytest -p no:cacheprovider app/test_session_turn_persistence.py app/test_teacher_dashboard.py app/test_rbac_idor.py app/test_case_worksheet_rubric.py -q</code> 40 passed, <code>py -3.11 -X utf8 scripts/check-case-worksheet-rubric.py --json</code> PASS(<code>items_total=28</code>, <code>scoring_enabled=false</code>, sha256 provenance), <code>py -3.11 -X utf8 -m py_compile</code> focused files, <code>npm run generate:api-types</code>, <code>npm run check:api-types</code>, <code>npm run typecheck</code>, <code>npm run build</code>, <code>session-review.spec.ts</code> 3 passed. 로컬 API smoke: 세션 생성→워크시트 저장→<code>GET /review</code>가 <code>saved_by_learner:local smoke saved worksheet</code> 반환.</p></div><div class="kv k-warn"><b>잔여</b><p>임상팀 확정 루브릭 콘텐츠, level anchor, 가중치, 컷오프, 예시 답안, AI 채점 적용/calibration, 승인 후 잠금·재제출 정책, 재귀학습 데이터셋 approved 연계는 후속이다. 현재 scaffold는 형식 검증과 로드 경로, 교수자 수동 판정 저장만 보장하며, 초안/저장본은 숨은 정답키나 임상 점수 없이 학습자 편집 산출물로만 취급한다.</p></div></div>
|
||
</article>
|
||
<article class="scard" data-status="done" data-cat="안전·위기" data-owner="0">
|
||
<button class="scard-head" aria-expanded="false"><span class="chip c-done">C2 구조 DONE</span><span class="scard-mid"><span class="scard-title">위기 에스컬레이션·safety_events 배선</span><span class="scard-sum">crisis.risk_level→ideation_observed 전달 + escalate 시 safety_events 적재. 라이브 검증.</span></span><span class="caret" aria-hidden="true"></span></button>
|
||
<div class="scard-body"><div class="kv"><b>현재</b><p>prepare_turn→state_machine.evolve에 crisis.risk_level을 ideation_observed로 전달(위기 관측 상태머신 반영). escalate=True 시 REST·SSE·voice가 엔진 호출 전 중단하고 <code>app.safety_events</code>에 109 resource와 <code>conversation_stopped</code>를 적재한다. 교수자 대시보드는 이 레코드를 안전 알림 큐로 표시한다. 안전 이벤트 insert payload는 <code>test_record_safety_event_writes_teacher_alert_payload</code>로 회귀화했다.</p></div><div class="kv k-warn"><b>잔여(임상팀)</b><p>비밀보장 예외고지·단계적 탐색·생명유지서약 스크립트 콘텐츠 + 실시간 교수자 push/메일 + 위기탐색 누락 회기리뷰 감점.</p></div></div>
|
||
</article>
|
||
<article class="scard" data-status="done" data-cat="교수자·평가" data-owner="0">
|
||
<button class="scard-head" aria-expanded="false"><span class="chip c-done">C3 2차 DONE</span><span class="scard-mid"><span class="scard-title">이론모드 스레딩 + 시작 전 명시 선택 UI</span><span class="scard-sum">theory_mode backend 배선, persona 기본값, humanistic/CBT/integrative segmented 선택과 POST 전송 검증.</span></span><span class="caret" aria-hidden="true"></span></button>
|
||
<div class="scard-body"><div class="kv"><b>현재</b><p>TurnContext+prepare_turn에 theory_mode, sessions/voice가 sess.theory_mode 전달, evaluator._theory_mode가 세션 이론(학습자 선택) 우선. 교수자 수동 session 재평가도 persona 기본 <code>theory_target</code>보다 session <code>theory_mode</code>를 우선하고, turn 재평가는 <code>TurnContext.theory_mode</code>를 채운다. <code>build_turn_messages</code>도 <code>THEORY_MODE_GUIDANCE</code>를 통해 인간중심·CBT·통합 이론 프레이밍을 엔진 메시지에 넣는다. 프론트 <code>Session.tsx</code>는 기본값을 <code>persona.theory_target</code>에서 잡되, 세션 시작 전 <code>humanistic</code>/<code>cbt</code>/<code>integrative</code> segmented control로 학습자가 명시 선택할 수 있다. 선택값은 <code>POST /sessions</code>의 <code>theory_mode</code>로 전송된다. 검증: <code>npm run typecheck</code>, <code>npm run build</code>, C3 backend focused 31 passed, eval route focused 5 passed, 평가/알림/세션 focused 39 passed, <code>session-layout.spec.ts</code> desktop/mobile 8 passed, <code>session-mvp.spec.ts</code> single-run 1 passed, <code>layout-visual-gate.spec.ts</code> 9 passed.</p></div><div class="kv k-warn"><b>잔여(임상팀)</b><p>CBT 이론별 단계 프롬프트 체인 콘텐츠 + 이론부합 채점 루브릭은 후속이다. 이번 UI는 임상 문안·루브릭·백엔드 enum을 확장하지 않았다.</p></div></div>
|
||
</article>
|
||
<article class="scard" data-status="done" data-cat="교수자·평가" data-owner="0">
|
||
<button class="scard-head" aria-expanded="false"><span class="chip c-done">HIGH H1 3차 DONE</span><span class="scard-mid"><span class="scard-title">계약 평가 KPI(자기효능감·기술숙련도·수련만족도 사전사후) 수집·계산 2차</span><span class="scard-sum">3척도 pre-post evidence 원장, 학습자 입력 UI, Phase 3 CSV/KPI report 계산기와 metric status 계약까지 연결됨. 공식 문항·통계해석은 평가설계 게이트.</span></span><span class="caret" aria-hidden="true"></span></button>
|
||
<div class="scard-body"><div class="kv"><b>현재</b><p>doc4가 요구한 3척도 pre-post에 대해 <code>app.learner_prepost_measure</code> 원장, 학습자 본인용 <code>GET/PUT /users/me/prepost-measures</code>, <code>SessionReview</code>의 파일럿 증거 원장 카드를 연결했다. 저장 shape는 <code>measure_name</code>(self_efficacy/skill_proficiency/training_satisfaction), <code>timepoint</code>(pre/post), raw/min/max score, instrument_version, item_count만 받으며, 문항 텍스트·효과크기·p-value·pass/fail은 만들지 않는다. <code>app.services.phase3_kpi_export</code>와 <code>scripts/export-phase3-kpi.py</code>는 원장 row를 Phase 3 evidence root의 <code>02-measures/prepost_measures.csv</code>와 <code>02-measures/kpi_report.json</code>으로 산출한다. participant id는 <code>P3-###</code>로 가명화하고, 3척도 paired normalized mean pre/post/delta, complete/missing pair를 report에 채운다. 77차에서는 <code>phase3_kpi_contract.py</code>가 metric 이름·필수키·status를 소유하게 해 exporter와 checker의 계약 drift를 줄였다. report metric status는 계산된 3척도 pre/post evidence를 <code>computed_prepost</code>, 공식 평가설계 전이라 계산하지 않는 지표를 <code>design_pending</code>으로 구분한다. 화면과 report 모두 공식 효과성·성적·수료 판정이 아니라 파일럿 evidence 계산임을 유지한다.</p></div><div class="kv"><b>검증</b><p><code>py -3.11 -X utf8 -m py_compile apps\api\app\services\phase3_kpi_contract.py apps\api\app\services\phase3_kpi_export.py apps\api\app\test_phase3_kpi_export.py apps\api\app\test_phase3_artifact_checker.py scripts\check-phase3-artifacts.py scripts\export-phase3-kpi.py</code>, <code>py -3.11 -X utf8 -m pytest -p no:cacheprovider app/test_phase3_kpi_export.py app/test_phase3_artifact_checker.py -q</code> 10 passed, <code>py -3.11 -X utf8 -m pytest -p no:cacheprovider app/test_user_support_tickets.py app/test_runtime_policy.py app/test_admin_ops.py app/test_phase3_kpi_export.py app/test_phase3_artifact_checker.py -q</code> 46 passed, <code>PLAYWRIGHT_PORT=5205 npx playwright test e2e/session-persistence.spec.ts --project=chromium-single-run --workers=1</code> 7 passed, <code>py -3.11 -X utf8 scripts\export-phase3-kpi.py --help</code>, <code>py -3.11 -X utf8 scripts\check-phase3-artifacts.py --help</code>. 기존 API/UI 검증은 API types/typecheck, SessionReview focused E2E 3 passed, visual gate 2 passed.</p></div><div class="kv k-warn"><b>잔여</b><p>평가설계·공식 문항 확정, 실험/통제군 배정, 추이 시각화, 통계 검정 종류/alpha/결측 처리, 실제 20명 파일럿 evidence와 steward/legal/IAA 검수는 아직 남아 있다.</p></div></div>
|
||
</article>
|
||
<article class="scard" data-status="done" data-cat="교수자·평가" data-owner="0">
|
||
<button class="scard-head" aria-expanded="false"><span class="chip c-done">H2 LIVE DONE</span><span class="scard-mid"><span class="scard-title">턴별 fast-loop + 라이브 코칭·이력</span><span class="scard-sum">평가 정규화에 더해 워크북·DSM·공식 지침/RAG 기반 코칭 아바타, 근거 모달, 발화별 이력 오버레이와 source pack RAG sync를 연결.</span></span><span class="caret" aria-hidden="true"></span></button>
|
||
<div class="scard-body"><div class="kv"><b>현재</b><p>텍스트 <code>/turn</code>·<code>/stream</code>·음성 경로에서 fast-loop 턴 평가를 <code>app.feedback_scores</code>, <code>app.turn_technique</code>, <code>app.turn_client_state</code>, <code>app.supervisor_comment</code>, <code>app.alternative_utterance</code>로 정규화 적재 후 리뷰 조회에서 hydrate한다. 평가 훅 예외와 evaluator error dict는 정상 neutral 점수로 둔갑하지 않게 <code>evaluation.error</code>로 남기고, 리뷰에는 <code>턴 평가 실패</code> 노트로 표면화하며, 성장 점수·최근 피드백 집계에서는 제외한다. 이번 패스에서 <code>app/services/live_coach.py</code>, <code>data/kb/live_coaching_workbook_0615.json</code>, <code>data/kb/live_coaching_sources/*.json</code>, <code>POST /sessions/{id}/live-coach</code>, <code>GET /sessions/{id}/live-coach</code>, <code>POST /kb/live-coach/source-packs/sync</code>, <code>app.live_coach_events</code>를 추가했다. 세션 UI는 코칭 모드에서 AI 코치 아바타 말풍선으로 즉시 개입하고, "근거 보기" 모달과 학습자 발화 우측 <code>C</code> 마커를 통해 받은 코칭 이력을 채팅 위 스크롤 오버레이로 보여준다. 저장 payload는 PII 마스킹 excerpt + 코칭 구조화 JSON이며, 공식 자료는 허가된 source pack의 version/citation/summary로 넣고 장문 원문·공식 문항을 재현하지 않는다. 같은 source pack은 <code>app.services.source_pack_sync</code>를 통해 evaluator 전용 RAG에 증분 색인하며, hash 변경 시 최신 active document version+1로 새 문서를 만든다. 74차에서는 로컬 source pack 캐시의 key/lifetime/무효화 경계를 명시해 live turn은 API 프로세스 snapshot을 재사용하고, 관리자 sync/CLI만 <code>refresh=True</code>로 repo 파일을 다시 읽게 했다. 학습자 <code>SessionReview</code> 데스크톱은 요약/흐름, 축어록, 평가 rail의 3열 workbench와 하단 워크시트로 재배치했고, 교수자 검토 화면과 모바일/태블릿은 기존 반응형 규칙을 유지한다.</p></div><div class="kv"><b>검증</b><p><code>python -m compileall app -q</code>, 과거 <code>pytest app/test_live_coach_sources.py app/test_orchestrator_masking.py app/test_session_turn_persistence.py -q</code> 27 passed, source sync focused <code>py -3.11 -X utf8 -m pytest -p no:cacheprovider app/test_live_coach_sources.py -q</code> 9 passed, H2 related <code>py -3.11 -X utf8 -m pytest -p no:cacheprovider app/test_live_coach_sources.py app/test_orchestrator_masking.py app/test_session_turn_persistence.py -q</code> 44 passed, fast-loop failure focused <code>py -3.11 -X utf8 -m pytest -p no:cacheprovider app/test_session_turn_persistence.py -k "fast_loop_evaluation" -q</code> 2 passed, failed metric exclusion focused <code>py -3.11 -X utf8 -m pytest -p no:cacheprovider app/test_learner_dashboard.py -k "failed_fast_loop" -q</code> 1 passed, <code>py -3.11 -X utf8 scripts\sync-persona-sources.py --help</code>, <code>py -3.11 -X utf8 -m py_compile</code> focused files, <code>npm run generate:api-types</code>, <code>npm run check:api-types</code>, <code>npm run typecheck</code>, <code>npm run build</code>, <code>npm run e2e -- e2e/layout-visual-gate.spec.ts e2e/session-layout.spec.ts</code> 15 passed. 최신 workbench 검증: <code>npm run typecheck</code>, <code>npm run build</code>, <code>npx playwright test e2e/session-review.spec.ts --project=chromium-desktop --workers=1</code> 3 passed, <code>npx playwright test e2e/layout-visual-gate.spec.ts --project=chromium-single-run --workers=1</code> 9 passed, <code>04-session-review-desktop.png</code> 직접 확인. 로컬 DB smoke: source pack sync 4 sources/15 chunks embedded, 재호출 <code>skipped_unchanged=4</code>, <code>official_suicide_risk_guidelines</code> source filter 검색 성공.</p></div><div class="kv k-warn"><b>잔여</b><p>임상팀 골든셋·라이브 코칭 루브릭 검수, 공식 지침/논문 source pack 추가와 임상 검수 상태 운영정책 보강이 남아 있다.</p></div></div>
|
||
</article>
|
||
<article class="scard" data-status="done" data-cat="콘텐츠·산출물" data-owner="0">
|
||
<button class="scard-head" aria-expanded="false"><span class="chip c-done">HIGH H3 12차 DONE</span><span class="scard-mid"><span class="scard-title">페르소나 워크스페이스 + RAG 저작·검수</span><span class="scard-sum">운영 대시보드·카탈로그·table·상세 drilldown과 자료 기반 단계형 저작/검수를 한 작업면으로 제공한다.</span></span><span class="caret" aria-hidden="true"></span></button>
|
||
<div class="scard-body"><div class="kv"><b>현재</b><p>teacher/admin 전용 <code>/teach/personas</code>는 상단 horizontal tab 기반 워크스페이스다. <code>대시보드</code>는 공개/활성 페르소나·학습 인원·세션·실제 평가/라포·검수 요약, <code>카탈로그</code>는 공개 규칙·구성·버전, <code>페르소나</code>는 검색/필터 table과 내부 <code>검수 현황</code> 탭을 소유한다. 행 상세는 소개·학습 현황·설정 drilldown이며 신규/수정은 자료→초안→설정→검토 단계, 이전/다음 이동, 브라우저+서버 중간 저장을 제공한다. 기존 RAG 자료 등록과 개요/임상/저항/회기/말투/안전/프롬프트 편집, 우측 검증·생성 근거, 승인/반려 결정은 단계형 작업면 안에서 유지한다. 2026-06-28 항목형 목록 UI 정리에서 자동사고, 회기 시나리오, 말투 filler/verbal tic/nonverbal cue, 역린·금기 응답·금기어를 행 추가/삭제 UI로 전환했고, API payload는 기존 배열 schema를 유지하며 빈 항목만 저장 직전에 제거한다. 프롬프트 탭은 raw JSON textarea 대신 L1 카드·인적 범주·임상 배경·말투·수치 파라미터·역린·회기 시나리오·추가 계약 섹션을 라벨형 검토 UI로 보여주며, 런타임 prompt bundle과 API 계약은 변경하지 않는다. 레이아웃 근거는 <code>docs/ops/layout-research-2026-06-27/persona-dashboard-layout-guideline.md</code>에 동기화했다. <code>POST /personas/sources</code>는 첨부/붙여넣기 자료를 PII 마스킹 후 <code>kb.raw_source_artifact</code>에 raw hash-only 격리 증거를 남기고, 별도 sanitized 파생본만 <code>kb.source/document/chunk</code>에 evaluator 전용(<code>visible_to=['evaluator']</code>, <code>sensitivity=2</code>) 근거 문서로 등록한다. <code>POST /personas/drafts/generate</code>는 <code>source_id</code> 기반 RAG 검색 결과만 엔진에 넘기고 source/doc/content hash·chunk·prompt bundle provenance를 남긴다. 승인된 카드만 학습자 catalog에 노출되며, 시스템 페르소나와 사용자 생성 페르소나를 구분한다. 공개본 수정은 같은 <code>persona_id</code>의 다음 버전 draft, 삭제는 기존 회기 FK 보존을 위한 <code>archived</code> 전환이다.</p></div><div class="kv"><b>검증</b><p>현행 재구성: <code>npm run typecheck</code>, <code>npm run build</code>, 페르소나 focused E2E 6 passed, IA·작성 단계 focused 2 passed, <code>layout-visual-gate.spec.ts</code> 14 passed, <code>session-layout.spec.ts</code> desktop/mobile 8 passed, 1440px·390px 대시보드/상세/작성 캡처 직접 확인. 백엔드 RAG·저작·검수 계약은 기존 <code>app/test_persona_generation_contract.py</code>·<code>app/test_persona_review.py</code> 검증을 유지한다.</p></div><div class="kv k-warn"><b>잔여</b><p>암호화 blob/vault 기반 원문 실저장, 임상팀 최종 검수/서면 evidence, 루브릭·이론 콘텐츠 외부화는 후속이다. 임상팀이 제공하지 않은 페르소나 내용을 최종 승인본처럼 확정하지 않는다.</p></div></div>
|
||
</article>
|
||
<article class="scard" data-status="done" data-cat="보안·PII" data-owner="0">
|
||
<button class="scard-head" aria-expanded="false"><span class="chip c-done">H4 부분 DONE</span><span class="scard-mid"><span class="scard-title">PII 마스킹 + LLM 호출 감사 + 프로필 온보딩·동의 게이트</span><span class="scard-sum">metadata-only llm_call_log, 16-case summary-only 마스킹 평가 계약, 전역 URL 하드게이트.</span></span><span class="caret" aria-hidden="true"></span></button>
|
||
<div class="scard-body"><div class="kv"><b>현재</b><p>정규식 폴백에 한국어 고위험 패턴 추가: 구체 날짜(2001.4.18→[DATE])·금액(1200원→[MONEY])·행정구역 주소(서울시 강남구 역삼동→[ADDR])에 더해, 이름/성명 라벨·성씨+이름+조사/호칭·대학교/학과/병원/센터 등 suffix 기반 한국어 NAME/ORG 로컬 휴리스틱 1차를 추가했다. 이번 패스에서 <code>app.services.pii_masking_eval</code>, <code>data/privacy/pii-masking-ko-fixtures.json</code>, <code>scripts/evaluate-pii-masking.py</code>를 확장해 합성 fixture 16개로 NAME/ORG/PHONE/EMAIL/RRN/NUMID/DATE/MONEY/ADDR/negative-control을 <code>source</code>/<code>category</code>/<code>severity</code> metadata와 함께 entity recall·forbidden substring removal·unexpected entity violation으로 평가한다. <code>data/privacy/pii-masking-eval-input.schema.json</code>과 <code>data/privacy/pii-masking-eval-report.schema.json</code>은 Node.js 전환 후에도 공유 가능한 입력/리포트 계약이며, 기본 리포트는 <code>evidence_text_included=false</code>로 <code>masked_text</code>/<code>forbidden_remaining</code> 원문 증거를 제외한다. fixture가 잡은 <code>소속</code>/<code>안내</code>/<code>이름</code> 및 <code>진로는</code> NAME 오탐은 보정했고, 66차에서는 자연 발화형 이름 라벨·자기소개 케이스를 추가해 라벨 단어만 마스킹하고 실명을 남기던 구멍을 막았다. Presidio가 설치돼도 한국어 누락을 막기 위해 regex fallback을 후단에 한 번 더 태운다. 70차에서는 <code>guardrail.mask_pii()</code> 내부에 선택형 한국어 PII recognizer adapter 경계를 추가했다. adapter는 명시 등록 전에는 비활성이고, 실패해도 예외를 밖으로 내보내지 않으며, fake adapter 테스트가 adapter span 마스킹 뒤 후단 regex fallback이 전화번호를 계속 처리하는지 고정한다. 상담 생성(generate/stream)·fast/deep 평가의 외부 LLM 호출 직후 <code>audit.llm_call_log</code>에 provider/model/token/cost/inference_geo/latency만 적재한다. prompt/completion 본문은 저장하지 않는다. fast evaluator prompt의 내담자 응답과 DB에 저장되는 client turn <code>text_masked</code>도 마스킹본을 쓰게 보강했다. 로컬 dev-login 실제 <code>/turn</code> smoke에서 <code>audit.llm_call_log</code>가 8→11로 3행 증가했다. 이번 패스에서 <code>app_user</code>에 이름·소속·학과·학년/직위·연락처·주소/수령지·닉네임·자기소개·아바타 URL·약관/개인정보 동의 시각·버전을 추가했다. 로그인 후 <code>onboarding_completed_at</code>이 없으면 전역 <code>OnboardingGate</code>가 <code>/onboarding</code> 외 URL(<code>/learn</code>, <code>/settings</code>, <code>/admin</code>, <code>/dev/avatar-preview</code>, <code>/login</code> 포함)을 모두 온보딩으로 돌리고, 온보딩 중에는 공용 셸 메뉴 없이 가입 직후 사용자 정보 입력 폼만 보여준다. learner 회기 시작도 온보딩 완료 전에는 막는다. <code>/users/me/avatar</code>는 PNG/JPG/WebP MIME·시그니처·3MB 제한 후 파일을 <code>USER_UPLOAD_DIR/profile-avatars</code>에 저장하고 URL만 보관한다. 약관·개인정보 처리방침 초안은 <code>/users/legal-docs</code>에서 제공한다. 관리자/교수자 권한은 온보딩에서 받지 않고 allowlist/관리자 사용자 관리로만 부여한다. 검증: <code>py -3.11 -X utf8 -m pytest -p no:cacheprovider app/test_pii_masking_eval.py app/test_orchestrator_masking.py app/test_evaluation_persistence.py app/test_session_turn_persistence.py -q</code> 47 passed, <code>py -3.11 -X utf8 scripts/evaluate-pii-masking.py --json</code> 16/16 pass, 최신 보정 검증 <code>py -3.11 -X utf8 -m pytest app/test_orchestrator_masking.py app/test_pii_masking_eval.py app/test_session_turn_persistence.py app/test_voice_ws.py -q</code> 59 passed, <code>py -3.11 -X utf8 -m py_compile apps/api/app/services/guardrail.py apps/api/app/services/pii_masking_eval.py apps/api/app/test_pii_masking_eval.py apps/api/app/test_orchestrator_masking.py scripts/evaluate-pii-masking.py</code>, 과거 온보딩/동의 검증은 <code>pytest app/ -q</code> 178 passed, <code>pytest engine_gateway/ -q</code> 11 passed, <code>npm run check:api-types</code>, <code>npm run typecheck</code>, <code>npm run build</code>, auth E2E 7 passed, 최종 <code>PLAYWRIGHT_PORT=5174 npm run e2e</code> 113 passed.</p></div><div class="kv k-warn"><b>잔여</b><p>한국어 이름/기관과 contact/national/quasi-identifier 합성 technical dry-run은 16-case summary-only harness 및 선택형 ko recognizer adapter 배선 회귀까지 완료했지만, 실제 운영 모델/provider 선정, 운영 말뭉치 기반 오탐/미탐 평가, 미성년/guardian 및 법무 검토가 필요한 최종 서명 동의서·개인정보 처리방침·약관 evidence는 외부/후속 게이트다. 공개 Google OAuth 실제 <code>/turn</code> proof는 별도 운영 게이트.</p></div></div>
|
||
</article>
|
||
<article class="scard" data-status="done" data-cat="리팩터·정리" data-owner="0">
|
||
<button class="scard-head" aria-expanded="false"><span class="chip c-done">MED M1~M3 내부 DONE</span><span class="scard-mid"><span class="scard-title">비언어 임상이벤트(M1)·다회기 케이스 아크(M2)·SSO claim/식별자/감사(M3)</span><span class="scard-sum">M1 provider_events/taxonomy/review 칩, M2 digest worker+runner+default-off scheduler/CAS, M3 cohort/external_id 1차 완료. live provider·기관 IdP는 gate.</span></span><span class="caret" aria-hidden="true"></span></button>
|
||
<div class="scard-body"><div class="kv"><b>현재</b><p>M1: voice learner turn의 audio_ref, silence_ms, speech_rate, barge_in을 리뷰 API <code>nonverbal</code> 이벤트로 파생하고, 회기 리뷰 축어록에 침묵·발화 속도·끼어듦·음성 입력 칩으로 표시한다. 2차로 <code>provider_events</code> JSONB 보존 슬롯을 추가해 control/STT provider 이벤트를 allowlist·size limit 후 저장한다. 3차에서는 저장 이벤트에 내부 taxonomy <code>event_type</code>/<code>category</code>를 추가해 sigh/cry/laugh/voice_activity/noise 등 canonical key를 남긴다. 4차에서는 인증된 회기 리뷰에 한숨·울음·웃음·호흡·운율·배경소음 계열만 한글 label/detail 칩으로 파생 노출한다. raw transcript/text payload, provider/source/raw type, 공개 공유 카드 노출은 제외했다. 실제 provider 기반 한숨·울음 감지와 live 마이크/WSS 장시간 실측은 아직 별도다. M2: <code>case_profile</code>을 learner-persona 안정 case로 upsert하고, <code>session_no</code>를 트랜잭션 안에서 원자 증가시킨다. 세션 종료 시 마스킹된 client-visible 축어록으로 fallback <code>session_summary.digest</code>와 case-level <code>case_digest</code>를 갱신하고, <code>rapport_trajectory</code>/<code>alliance_level</code>을 결정론 상태 수치에서 갱신한다. 종료 시점에는 마스킹된 client-visible 발화에서 <code>[NAME]</code>/<code>[ORG]</code> identity와 명시적 상담 약속만 <code>pinned_fact</code>로 upsert한다. 삽입 또는 값 변경 시에는 <code>pinned_fact_history</code>에 append-only 이력을 남기고, 같은 값 재확인과 <code>locked</code> fact는 이력을 늘리지 않는다. 5차에서는 명시적 상담 약속 철회/부정 문장만 기존 non-locked <code>agreement:counseling</code> fact를 <code>contradicted</code>로 격리하고 history reason <code>contradiction</code>을 남긴다. 새 contradicted fact를 임의 생성하거나 관계갈등·위기·임상 추론을 자동 모순 처리하지 않는다. 6차에서는 종료 저장 성공 뒤 background task로 마스킹된 client-visible 내담자 발화만 <code>app.turn_embedding</code>에 idempotent 색인한다. 7차에서는 시작 경로가 seed recall을 cache에 적재하고, REST submit/SSE stream은 공통 <code>_prepare_turn_context()</code>에서 recall, pinned fact, 최근 client-visible 턴, KB cue를 TurnContext/EngineMessage로 조립한다. route-level 테스트는 DB <code>case_digest</code>·직전 <code>session_summary</code>·client-visible <code>pinned_fact</code>가 다음 턴 L2/L4 블록에 들어가고 raw 이름이 <code>[NAME]</code>으로 마스킹되는지 고정한다. 76차에서는 <code>SessionDigestInput</code>/<code>SessionDigestResult</code>/<code>SessionSummaryWrite</code>로 종료 digest 입력·fallback 결과·DB write 인자 경계를 명시했다. 94차에서는 <code>DigestQualityAssessment</code>/<code>SessionDigestWorkerOutcome</code>로 LLM digest 후보의 local quality harness를 추가해 빈/짧은 digest, raw forbidden substring, 내부 평가·CCD·상태 marker, 잘못된 세션 prefix를 fallback 유지 대상으로 판정한다. 95차에서는 <code>session_digest_worker.py</code>가 <code>CompressionJob</code>→<code>GenerateRequest</code> 변환, 주입형 engine/audit 호출, accepted-only <code>session_summary</code>/<code>case_profile</code> 적용 계획과 one-shot DB loader를 소유하게 했다. 96차에서는 <code>scripts/run-session-digest-worker.py</code>가 명시 session id dry-run/apply runner를 제공한다. 97차에서는 <code>SESSION_DIGEST_WORKER_ENABLED=false</code> 기본값의 세션 종료 scheduler 골격과 <code>compressed_by IS NULL</code> loader/apply CAS를 추가했다. 입력 후보는 client-visible masked turn과 open thread만 받으며, 종료 상태 수치·평가 payload·CCD·raw text는 압축 prompt에 넣지 않는다. <code>digest_pending</code>은 CompressionJob 생성 여부 신호로 유지한다. M3: Google/SAML/dev-login이 <code>AUTH_EMAIL_COHORT_MAP</code>/<code>AUTH_DOMAIN_COHORT_MAP</code> 및 SAML cohort claim을 <code>cohort_ids</code>로 전달하고, DB <code>app_user.external_id</code>는 provider subject(<code>google:</code>/<code>saml:</code>/<code>dev:</code>) 기반으로 저장한다.</p></div><div class="kv k-warn"><b>권고</b><p>M1 실제 provider 기반 한숨·울음/억양 감지, 장시간 마이크/WSS 실측과 역량 지표화 정책, M2 관계/임상 fact 승격 기준·실 provider 장시간 운영·임상 골든셋 품질평가·재압축, M3 운영 SAML 서명검증·기관 claim schema/test tenant·deprovisioning audit는 후속이다.</p></div></div>
|
||
</article>
|
||
<article class="scard" data-status="done" data-cat="거버넌스·아키텍처" data-owner="0">
|
||
<button class="scard-head" aria-expanded="false"><span class="chip c-done">TRACK X1·X2·L1 내부 DONE</span><span class="scard-mid"><span class="scard-title">재귀학습 export(X1)·API 비용 관측(X2)·스택 정합성·일정(L1)</span><span class="scard-sum">X1 dry-run export, X2 비용 관측·모델 override·cache·모델별 리포트, L1 Node 계약 경계 완료. 승인/export/enforcement/등재는 gate.</span></span><span class="caret" aria-hidden="true"></span></button>
|
||
<div class="scard-body"><div class="kv"><b>현재</b><p>X1: <code>scripts/export-recursive-dataset.py</code>와 <code>app.services.dataset_export</code>로 masked-text JSONL dry-run, PII scan, κ/ICC 계산, approved export 게이트를 구현했다. exporter는 동의가 남아 있고 learner-visible인 <code>text_masked</code> 턴만 선별하며, supervisor comment raw text는 JSONL에 넣지 않는다. artifact checker는 approved/dry-run JSONL의 필수 key·row count·privacy·PII shape를 검사한다. 기본은 <code>technical_dry_run</code>이고 <code>ds.*</code> write는 <code>--write-dataset</code> 명시 시에만 수행한다. X2: <code>app.turns</code>의 provider/model/tokens/cost를 최근 7일 기준으로 집계하는 <code>GET /admin/usage</code>를 추가했고, 운영 콘솔 <code>/admin</code>에 누적 비용·입출력 토큰·계량 커버리지·provider/model breakdown, 일별 <code>daily_cost</code> 추이, <code>ADMIN_USAGE_BUDGET_USD</code> 기반 예산 경고(ok/warn/exceeded)를 표시한다. <code>EVALUATOR_FAST_MODEL</code>/<code>EVALUATOR_DEEP_MODEL</code> 설정 시 fast/deep 평가 호출만 해당 모델 override로 gateway에 전달하고, 비워두면 기존 default 라우팅을 유지한다. fast/deep evaluator structured 결과는 canonical request SHA-256 키의 인메모리 semantic cache로 재사용한다. 캐시는 <code>EVALUATOR_SEMANTIC_CACHE_ENABLED</code>, <code>EVALUATOR_SEMANTIC_CACHE_TTL_SECONDS</code>, <code>EVALUATOR_SEMANTIC_CACHE_MAX_ENTRIES</code>로 제한하며, 원문 prompt·completion은 저장하지 않고 성공 파싱 결과만 캐시한다. <code>/admin/usage</code>는 evaluator cache enabled/entries/hits/misses/stores/evictions/requests/hit_rate도 함께 반환하고, 관리자 <code>AI 비용</code> 카드가 hit-rate와 일별 비용/턴 추이를 표시한다. <code>app.services.usage_report</code>와 <code>scripts/report-ai-usage.py</code>는 같은 usage JSON에서 provider/model별 cost share, token share, cost/turn, cost/1k tokens, budget/cache warning을 산출한다. DB 미가용 dev에서는 runtime store로 fallback하지만 prod에서는 fail-closed한다. L1: 소유자 결정으로 Node.js 우선 교체 가능성 방향을 채택했다. <code>docs/decisions/backend-node-transition.md</code>에 FastAPI 유지 사유와 strangler 전환 원칙을 기록했고, <code>app/contracts/engine_gateway.py</code>를 추가해 Python client와 현재 gateway가 같은 요청/응답/SSE 계약을 공유한다. 이어서 <code>EngineClient.stream_packets()</code>와 gateway <code>GenerateResponse</code> 반환으로 서비스 레이어의 wire-format 의존을 줄였다. <code>engine_gateway_contract.v1.json</code> golden fixture와 <code>engine_gateway_schema.v1.json</code> schema artifact로 generate request/response와 stream token/done/error packet을 고정했고, <code>scripts/check-engine-gateway-contract.mjs</code>가 같은 artifact를 Node.js에서 Python import 없이 검증한다. 78차에서는 <code>gateway-default</code> 기본 라우팅 sentinel과 모델 override 정규화를 같은 계약 모듈로 올려 FastAPI client, Python gateway, stream audit/done fallback, 관리자 기본 config가 같은 의미를 공유하게 했다. 79차에서는 <code>structured_payload_from_response()</code>가 structured field 우선, fenced JSON, embedded JSON object fallback을 소유해 evaluator/live-coach/persona draft의 legacy generate response 해석을 같은 계약 경계로 모았다. 80차에서는 <code>app/persona_generation_contract.py</code>가 persona draft schema, prompt bundle, payload extraction, generated draft coercion을 소유하게 했고, 81차/89차에서는 evaluator/live-coach가 structured payload 계약 helper를 직접 호출하며 <code>SessionEvaluationWrite.from_result()</code>/<code>from_error()</code>가 session evaluation write packet 생성을 소유하게 했다. <code>app/session_read_model.py</code>는 세션 목록·대시보드·상세·리뷰·공유 payload의 DTO와 builder를 route에서 분리했고, <code>app/persona_read_model.py</code>는 페르소나 catalog/review/draft/source/evidence DTO와 mapper를 route에서 분리했다. 각 route는 auth/RLS DB read/persistence/RAG/LLM side effect를 유지한다.</p></div><div class="kv k-warn"><b>권고</b><p>X1 approved export·골든셋 승격은 steward/legal 승인, reviewer disposition, IAA 게이트(κ≥0.70/ICC≥0.75) 이후만 진행한다. X2 후속은 자동 차단·한도 enforcement 정책이다. L1의 남은 게이트는 신청서/저작권 등재 문서에 FastAPI 유지 사유와 Node 전환 계획을 반영하는 외부 거버넌스 증거다. H3 항목형 목록 저작 UI와 프롬프트 미리보기 de-JSON은 60~61차에서 처리됐다. 실제 운영 말뭉치 기반 H4 평가는 외부 데이터·reviewer 정답 라벨·법무/guardian evidence 전까지 gate로 남기고, 내부 후보였던 합성 fixture 확장과 운영 평가 입력/report schema 준비는 62차에서 technical_dry_run으로 완료했고, 실제 운영 평가는 여전히 gate다.</p></div></div>
|
||
</article>
|
||
</div>
|
||
<div class="detail-blocks">
|
||
<div class="detail-block"><h4>즉시 착수 가능(내부 코드, 외부 합의 불요)</h4><ul><li>C1은 저장형 워크시트와 외부 루브릭 schema/loader/validation scaffold까지 3차 완료. C2는 1차 구조 완료, C3은 이론모드 배선과 명시 선택 UI까지 2차 완료했고 수동 session/turn 재평가도 세션의 <code>theory_mode</code>를 우선하도록 회귀화했다. H2는 라이브 코칭 1차와 source pack version sync 및 학습자 리뷰 3열 workbench, H3은 페르소나 저작 CRUD/RAG 생성/seed materializer runner/source pack sync runner/raw source hash-only isolation/항목형 목록 저작 UI/프롬프트 미리보기 de-JSON까지 완료. 후속은 임상 스크립트, CBT 체인, 루브릭·이론 콘텐츠 외부화처럼 데이터·정책 고도화 영역이다.</li><li>H4(부분): 한국어 날짜/금액/주소 및 이름/기관 로컬 휴리스틱 마스킹, 16-case 합성 fixture 평가 harness와 입력/report schema, metadata-only LLM call audit 경로, 로컬 live row proof, learner 동의 수락/철회/회기 시작 하드게이트 골격은 완료. 실제 운영 말뭉치 기반 오탐/미탐 평가는 외부 데이터·reviewer 라벨·법무/guardian evidence가 필요하므로 gate로 남긴다. 내부 후보였던 합성 fixture 확장과 운영 평가 입력/report schema·summary-only dry-run 준비는 62차에서 완료했고, 자연 발화형 이름 누수 보강은 66차에서 완료했다. 70차에서는 선택형 ko recognizer adapter 인터페이스와 fake/failure 회귀를 추가해, 실제 모델/provider를 고르기 전에도 같은 <code>mask_pii()</code> 경계에서 adapter→regex fallback 순서를 검증할 수 있게 했다. X1 dry-run export 1차와 X2 비용 관측·예산 경고/evaluator 모델 override·semantic cache·hit-rate 관측·일별 비용 추이·모델별 비용 검증 리포트는 완료했고, 자동 차단·한도 enforcement는 정책 결정 전까지 보류한다. M1 provider_events 보존·taxonomy·인증 리뷰 파생 칩 4차, M2 case memory+pinned_fact 보수적 실적재·append-only history·명시적 상담약속 철회 contradiction·episodic embedding writer·다음턴 주입회귀 7차/digest contract + local quality harness + one-shot worker/runner/default-off scheduler 경계, M3 claim 1차, L1 schema-backed engine gateway golden fixture·default-routing sentinel·structured payload parser·Node.js conformance runner·session/persona read-model 분리는 완료. 운영 SAML 서명검증은 기관 IdP 의존이다.</li></ul></div>
|
||
<div class="detail-block"><h4>소유자 결정 / 외부(임상팀·기관) 의존</h4><ul><li>임상팀 산출물: C1 루브릭 항목·척도·가중치·anchor 예시·컷오프·검수 기준, C3 CBT 콘텐츠·체인, C2 위기 스크립트·서약 문안, H2 골든셋 코딩.</li><li>소유자 평가설계: H1 실험/통제군·3척도 문항·50분 흐름, κ/ICC·환각률 목표(doc4 미명시).</li><li>기관(한신 IT): M3 SSO IdP·test tenant·claim 스키마·인증서, 거버넌스 증거.</li><li>거버넌스: L1 Node 전환 방향은 결정됨. 남은 것은 FastAPI 유지 사유와 계약 우선 전환 계획의 저작권 등재 문서화·IP 협의.</li></ul></div>
|
||
<div class="detail-block"><h4>판독 한계 (정직성)</h4><ul><li>핵심 축어록 2단 표는 PDF 시각 판독이 1차 근거(txt 일부 누락), OCR 잔재 존재.</li><li>doc4 κ/ICC·환각률은 신청서 본문 미명시 — 계약 확정 지표로 단정 금지.</li><li>'현재상태'는 grep/코드 사실 기반에서 시작했으나 C1·C2·C3 2차 UI·H1~H4·M1 provider_events 보존/taxonomy/인증 리뷰 파생 노출·M2 case memory/pinned_fact/history/명시철회 contradiction/episodic embedding writer/digest contract/local quality harness/one-shot worker/runner/default-off scheduler boundary·M3·X1·X2·L1 Node conformance/default-routing sentinel와 session/persona read-model 분리는 구현 후 재검증 완료 기준이다. M2는 fallback digest, <code>digest_pending</code> 응답 계약, LLM 후보 local quality harness, accepted-only one-shot worker/runner/default-off scheduler 경계까지이며, 실 provider 장시간 운영·임상 골든셋 품질평가·재압축은 완료로 보지 않는다. H3 원문 격리는 hash-only artifact와 RAG index 차단까지이며, 암호화 blob/vault 원문 실저장은 아직 후속이다. 실제 provider 기반 한숨·울음 감지, H4 legal/guardian evidence, M3 운영 IdP, L1 외부 거버넌스는 아직 게이트다.</li></ul></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- TRACK 3 -->
|
||
<div class="track-group">
|
||
<div class="tg-head"><h3>코드품질 · 로컬 개발 스택</h3><span class="tg-note">리팩토링 연구 + 1-커맨드 dev 스택 (2026-06-26~27). code-quality-research-2026-06-26.md · local-development.md</span></div>
|
||
<div class="scards">
|
||
<article class="scard" data-status="doing" data-cat="인프라·스택·RAG" data-owner="0">
|
||
<button class="scard-head" aria-expanded="false"><span class="chip c-doing">DB RECOVERY · PUBLIC LIVE</span><span class="scard-mid"><span class="scard-title">공개 계정·회기 복구와 무손실 전환</span><span class="scard-sum">복구 named volume을 공개 55432에 전환했다. 데이터·auth·public health는 정상이고 task 자동복구는 stable-source 재등록을 기다린다.</span></span><span class="caret" aria-hidden="true"></span></button>
|
||
<div class="scard-body"><div class="kv k-good"><b>공개 전환</b><p>2026-08-07 18:26 KST owner 승인으로 <code>vignette_recovered_prod_20260807</code>을 <code>vignette-dev-db:55432</code>에 연결했다. 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다.</p></div><div class="kv"><b>보존·rollback</b><p>전환 직전 DB는 <code>vignette-dev-db-pre-recovery-20260807-182642</code>로 중지 보존했고 exact dump SHA는 <code>6b84d5c8…8af1f</code>다. 원본 volume, post-merge dump <code>d24eaa75…2e32</code>, 최종 recovered dump <code>d7bcc396…68ed</code>, 자동 rollback을 실증한 실패 후보 2개도 삭제하지 않았다.</p></div><div class="kv k-warn"><b>사용자 확인</b><p>보안상 기존 <code>auth_session</code>은 복사하지 않아 모두 Google 재로그인이 필요하다. 계정·회기 원장은 복구돼 있다. 실제 사용자가 재로그인한 뒤 과거 소유 회기 목록이 보이는지 확인할 때까지 이 카드는 안정화 중으로 유지한다. 상세: <code>ops/public-db-recovery-rehearsal-2026-08-07.md</code>.</p></div></div>
|
||
</article>
|
||
<article class="scard" data-status="done" data-cat="인프라·스택·RAG" data-owner="0">
|
||
<button class="scard-head" aria-expanded="false"><span class="chip c-done">DEV STACK · DB 보존 패치</span><span class="scard-mid"><span class="scard-title">로컬 개발 스택 1-커맨드 기동 — <code>scripts/dev-up.ps1</code></span><span class="scard-sum">정지 DB는 start 우선, 신규 PGDATA는 고정 named volume, 일반 dev-up의 DB 삭제 경로는 0.</span></span><span class="caret" aria-hidden="true"></span></button>
|
||
<div class="scard-body"><div class="kv"><b>현재</b><p>게이트웨이(9099 claude_cli)+API(8000)+웹(5173)을 결정론적으로 기동한다. DB는 이름만 같은 새 container로 바꾸지 않는다. 기존 container가 stopped이면 <code>docker start</code>와 readiness를 먼저 수행하고, 그 외 비정상 상태·시작 실패·readiness timeout은 container/volume을 보존한 채 fail closed한다. 신규 DB만 <code>vignette-dev-db-pgdata</code> named volume을 사용하며 일반 경로에는 container rm·volume rm/prune·compose down -v가 없다.</p></div><div class="kv k-good"><b>검증</b><p><code>scripts/test_dev_up_db_safety.py</code> 5 passed, Ruff, Windows PowerShell 5.1 AST parser, diff check 통과. 실행 중인 DB container ID·생성 시각·PGDATA volume은 테스트 전후 동일했다.</p></div></div>
|
||
</article>
|
||
<article class="scard" data-status="done" data-cat="인프라·스택·RAG" data-owner="0">
|
||
<button class="scard-head" aria-expanded="false"><span class="chip c-done">VOICE 로컬 작동</span><span class="scard-mid"><span class="scard-title">음성 채팅 무음 해결 — provider + 브라우저 재생 경계 보강</span><span class="scard-sum">OPENAI_API_KEY 설정 확인, Web Audio unlock 재생, local live STT/TTS WS smoke 통과.</span></span><span class="caret" aria-hidden="true"></span></button>
|
||
<div class="scard-body"><div class="kv"><b>현재</b><p>직접 WS 재현으로 근본원인 규명: 키 없으면 백엔드가 <code>degraded</code>+1011 즉시 close → 마이크 깜빡·녹음 미도달. 현재 로컬 <code>/voice/health</code>는 <code>available:true</code>이고, 실제 P2 회기에서 MP3 발화를 <code>/voice/ws</code>로 보내 <b>STT transcript 52자 → client reply 105자 → TTS 바이너리 66 chunks/266,496 bytes</b>를 수신했다. 이 증거는 로컬 캐스케이드 검증이며, 물리 마이크·public WSS·50분 장시간 실측 완료를 뜻하지 않는다.</p></div><div class="kv"><b>부수 수정</b><p>① 미설정 시 마이크 버튼 사전 비활성+명확 안내 ② <b>TTS 지문 제거</b>: (고개 끄덕)·(한숨)·[침묵] 등 무대지시를 음성으로 안 읽음(<code>speakable_text</code>, 자막엔 유지) ③ <b>아바타 애니메이션</b>: 텍스트 턴 speaking 시 입 정지(0) 버그 수정 + thinking 표현 보강 ④ <b>브라우저 자동재생 차단 대응</b>: 마이크 클릭 시 <code>AudioContext.resume()</code>으로 unlock하고 TTS Blob을 Web Audio buffer source로 재생, 실패 시 기존 <code><audio></code> fallback ⑤ <code>AudioWorklet</code>-first PCM16 capture와 EOT <code>stt_result</code> route wiring. typecheck+voice테스트+voice E2E+local live WS smoke PASS.</p></div></div>
|
||
</article>
|
||
<article class="scard" data-status="done" data-cat="코어·차별" data-owner="0">
|
||
<button class="scard-head" aria-expanded="false"><span class="chip c-done">페르소나 격리</span><span class="scard-mid"><span class="scard-title">AI 내담자 캐릭터 붕괴·개발맥락 누출 버그 수정</span><span class="scard-sum">--append→--system-prompt 교체 + dynamic 섹션 제외. 누출 0. 게이트웨이 11 passed.</span></span><span class="caret" aria-hidden="true"></span></button>
|
||
<div class="scard-body"><div class="kv"><b>증상</b><p>라이브 회기에서 내담자(민재)가 "저는 Claude Code인데… Vignette 코드베이스… git status…"로 캐릭터를 깨고 개발 맥락 노출. 원인: ENGINE_MODE=claude_cli가 로컬 claude(=Claude Code)를 호출하는데 게이트웨이가 페르소나를 <code>--append-system-prompt</code>로 코딩 어시스턴트 정체성에 <b>덧붙이고</b>, cwd=repo라 CLAUDE.md·git status·env가 시스템프롬프트에 포함됨.</p></div><div class="kv"><b>수정·검증</b><p><code>engine_gateway/gateway.py</code>: <code>--append-system-prompt</code>→<code>--system-prompt</code>(페르소나만 <b>교체</b> 주입) + <code>--exclude-dynamic-system-prompt-sections</code>(cwd/env/git/메모리 섹션 제거). 라이브: 깨던 입력 그대로 → 민재 완벽 인캐릭터, 누출 0. 게이트웨이 11 passed + 전체 API 178 passed.</p></div></div>
|
||
</article>
|
||
<article class="scard" data-status="done" data-cat="리팩터·정리" data-owner="0">
|
||
<button class="scard-head" aria-expanded="false"><span class="chip c-done">REFACTOR 1 2차 DONE</span><span class="scard-mid"><span class="scard-title">턴 처리 파이프라인 중복(REST/WS) 제거</span><span class="scard-sum"><code>turn_runtime.py</code>로 세션 로드·권한·턴 append·상태 갱신·안전 이벤트 finalize를 공통화.</span></span><span class="caret" aria-hidden="true"></span></button>
|
||
<div class="scard-body"><div class="kv"><b>현재</b><p><code>app/turn_runtime.py</code>를 추가해 <code>load_owned_session</code>, <code>record_completed_turn</code>, <code>append_completed_turn</code>, <code>update_session_state</code>, <code>record_safety_event</code>를 공통화했다. 이번 패스에서 <code>finalize_completed_turn</code>을 추가해 <code>record_completed_turn</code> + <code>record_safety_event</code> 호출쌍을 REST submit/stream과 voice WS 라우트에서 제거했다. stream fast-loop 평가 누락, voice 세션 recall cache, visible_to/evaluator turn 분리는 보존.</p></div><div class="kv"><b>검증</b><p><code>pytest app/test_session_turn_persistence.py app/test_voice_ws.py engine_gateway/test_gateway_model.py app/test_phase3_artifact_checker.py app/test_dataset_export.py -q</code> 40 passed.</p></div><div class="kv k-warn"><b>잔여</b><p>더 큰 <code>finalize_turn</code>/<code>SessionRepository</code> 경계, 장기적으로는 세션 저장소 인터페이스 SSOT화가 남아 있다.</p></div></div>
|
||
</article>
|
||
<article class="scard" data-status="done" data-cat="리팩터·정리" data-owner="0">
|
||
<button class="scard-head" aria-expanded="false"><span class="chip c-done">REFACTOR 2 7차 DONE</span><span class="scard-mid"><span class="scard-title">백엔드 Pydantic ↔ 프론트 <code>lib/api.ts</code> 계약 SSOT화</span><span class="scard-sum">OpenAPI→<code>api.gen.ts</code> 생성 + CI stale 체크 게이트. 세션·리뷰·평가·교수·공유·live-coach stage 계약과 라벨 정규화를 union/null SSOT로 축소.</span></span><span class="caret" aria-hidden="true"></span></button>
|
||
<div class="scard-body"><div class="kv"><b>현재</b><p><code>apps/api/scripts/export-openapi.py</code>와 <code>apps/web/scripts/generate-api-types.mjs</code>로 FastAPI OpenAPI를 <code>apps/web/src/lib/api.gen.ts</code>에 생성한다. <code>npm run check:api-types</code>가 재생성 누락을 잡는다. <code>MeResponse</code>, <code>AuthConfigResponse</code>, <code>ConsentResponse</code>, <code>PersonaSummary</code>, <code>PersonaReviewSummary</code>, admin/user/engine DTO, review leaf DTO, <code>CrisisResourceResponse</code>, teacher safety/growth leaf DTO, <code>LearnerSessionsResponse</code>, <code>LearnerDashboardResponse</code>, <code>SessionReviewResponse</code>, worksheet aggregate, <code>TeacherDashboardResponse</code>, <code>TeacherSessionSummary</code>, <code>TeacherLearnerGrowth</code>에 이어 <code>SessionStartResponse</code>, <code>SessionDetailResponse</code>, <code>SessionDetailTurn</code>도 <code>ApiSchema<...></code> alias로 전환했다. 세션 응답 stage는 backend <code>StageLabel</code> enum으로 고정해 OpenAPI가 <code>라포|탐색|개입|정리</code> union을 생성한다. 69차에서는 리뷰 도식 <code>ReviewPhaseSegment.key</code>를 <code>rapport|explore|intervene|closing</code>, <code>ReviewPhaseSegment.label</code>과 <code>SessionReviewResponse.reachedPhase</code>를 <code>StageLabel</code>로 좁혔다. 평가 조회 <code>EvaluationSummary.stage</code>는 평가 미생성 시 <code>null</code>, 평가 트리거 응답 stage와 교수 콘솔 <code>TeacherSessionSummary</code>/<code>TeacherGrowthPoint</code> stage는 <code>StageLabel</code> union으로 생성된다. 71차에서는 공개 공유 응답 <code>PublicSessionShareResponse.reachedPhase</code>와 live-coach 이력 <code>LiveCoachEvent.stage</code>도 <code>StageLabel | null</code> 계약으로 축소했다. 72차에서는 <code>app/stage_contract.py</code>가 <code>StageLabel</code>, <code>ReviewPhaseKey</code>, legacy stage code 정규화, unknown→null 판단, 리뷰 phase key 매핑을 소유하게 해 <code>turn_runtime.py</code>, <code>session_read_model.py</code>, <code>routes/eval.py</code>의 중복 규칙을 제거했다. generated optional/default 차이는 화면 form state, notification default helper, 배열 렌더링 fallback, live-coach <code>단계 미상</code> 표시로 흡수한다. <code>.github/workflows/api-contract.yml</code>은 PR/master push에서 같은 stale check를 실행해 OpenAPI type drift를 CI에서 막는다.</p></div><div class="kv"><b>검증</b><p><code>py -3.11 -X utf8 -m pytest -p no:cacheprovider app/test_evaluation_persistence.py app/test_evaluator_model_routing.py app/test_teacher_dashboard.py app/test_rbac_idor.py app/test_session_turn_persistence.py -q</code> 50 passed. 공유/live-coach 후속 검증: <code>py -3.11 -X utf8 -m pytest -p no:cacheprovider app/test_session_share.py app/test_live_coach_sources.py app/test_session_turn_persistence.py app/test_orchestrator_masking.py -q</code> 43 passed. Stage contract SSOT 검증: <code>py -3.11 -X utf8 -m pytest -p no:cacheprovider app/test_stage_contract.py app/test_session_share.py app/test_session_turn_persistence.py app/test_evaluation_persistence.py app/test_teacher_dashboard.py -q</code> 42 passed. <code>py -3.11 -X utf8 -m py_compile apps/api/app/stage_contract.py apps/api/app/turn_runtime.py apps/api/app/session_read_model.py apps/api/app/routes/eval.py apps/api/app/routes/sessions.py apps/api/app/routes/share.py apps/api/app/services/live_coach.py</code>, <code>npm run check:api-types</code>, <code>npm run typecheck</code>, <code>npm run build</code> passed. 과거 영향 범위 E2E: <code>learner.spec.ts</code> + <code>session-review.spec.ts</code> + <code>teacher.spec.ts</code> + <code>layout-visual-gate.spec.ts</code> 32 passed.</p></div><div class="kv k-warn"><b>잔여</b><p>남은 자유 문자열 stage는 evaluator 내부 payload, DB 저장 컬럼, dataset export, runtime store 같은 저장·분석 payload다. 라벨/phase key 변환 규칙은 <code>app/stage_contract.py</code> 하나로 이동했고, 브라우저-facing OpenAPI 응답 stage 계약은 현재 확인된 session/review/eval/teacher/share/live-coach 표면에서 enum/null로 고정했다.</p></div></div>
|
||
</article>
|
||
<article class="scard" data-status="done" data-cat="인프라·스택·RAG" data-owner="0">
|
||
<button class="scard-head" aria-expanded="false"><span class="chip c-done">SEO/GEO 공유</span><span class="scard-mid"><span class="scard-title">검색 메타 + 회기 리뷰 URL 공유 카드</span><span class="scard-sum">robots/sitemap/llms + OG/JSON-LD. 종료 회기는 토큰 공유 URL로 교수자에게 요약·썸네일 전달.</span></span><span class="caret" aria-hidden="true"></span></button>
|
||
<div class="scard-body"><div class="kv"><b>현재</b><p><code>apps/web/index.html</code>에 canonical, description, Open Graph/Twitter Card, JSON-LD(WebApplication)를 추가했고, <code>public/robots.txt</code>, <code>sitemap.xml</code>, <code>llms.txt</code>로 공개 사이트와 AI 검색용 설명을 정리했다. 인증 내부 경로는 robots에서 차단한다. 학습자 회기 리뷰는 종료 후 <code>POST /sessions/{id}/share</code>로 공개 토큰을 만들고, <code>GET /share/session/{token}</code>이 서버 렌더 HTML/OG/JSON-LD를 반환한다. <code>app.session_share_link</code>에는 토큰 해시와 sanitized preview payload만 저장하며 원문 축어록·학습자 식별자는 저장하지 않는다.</p></div><div class="kv"><b>검증</b><p><code>pytest app/test_session_share.py app/test_session_turn_persistence.py -q</code> 21 passed, <code>python -m pytest app/ -q</code> 178 passed, <code>python -m pytest engine_gateway/ -q</code> 11 passed, <code>npm run check:api-types</code>, <code>npm run typecheck</code>, <code>npm run build</code> passed.</p></div><div class="kv k-warn"><b>판정</b><p>공개 제품 페이지는 색인 가능, 회기 공유 페이지는 교수자 전달용 unfurl 카드라 <code>noindex</code>와 API robots <code>Disallow: /share/</code>를 유지한다. 실제 공개 도메인 unfurl 검증은 배포 후 1회 필요하다.</p></div></div>
|
||
</article>
|
||
<article class="scard" data-status="done" data-cat="리팩터·정리" data-owner="0">
|
||
<button class="scard-head" aria-expanded="false"><span class="chip c-done">REFACTOR 3 일부</span><span class="scard-mid"><span class="scard-title">죽은 확장점/계약 정리 — LogHook·tier 제거 적용</span><span class="scard-sum">caller 0 타입·분기·export 제거. 잔여는 패치 문서에 스테이징. 현재 backend 178 기준 유지.</span></span><span class="caret" aria-hidden="true"></span></button>
|
||
<div class="scard-body"><div class="kv"><b>현재</b><p><code>LogHook</code>/<code>log_hook</code>(caller 0) 타입·파라미터·분기·export 제거 · 게이트웨이/엔진클라 <code>tier</code> dead-contract 제거(write-only, gateway는 model만 사용). 잔여(RMS 힌트·Live2D 고아 자산·파라미터객체 init_state/prepare_turn/upsert)는 <code>docs/archive/ops/refactor-rag-patches-2026-06-26.{md,json}</code>에 스테이징(단계 적용).</p></div><div class="kv"><b>참고</b><p>일괄 적용 대신 라이브 스택 보호 위해 그룹별 적용+pytest 검증.</p></div></div>
|
||
</article>
|
||
<article class="scard" data-status="done" data-cat="인프라·스택·RAG" data-owner="0">
|
||
<button class="scard-head" aria-expanded="false"><span class="chip c-done">RAG 활성화</span><span class="scard-mid"><span class="scard-title">"좋은 RAG" 미배선 → 라이브 활성화 + 성능 회귀 수정</span><span class="scard-sum">BGE-M3 핀+배선, 임베더 동기로드 블로킹을 백그라운드 task로. E2E 27/27·11/11·4/4.</span></span><span class="caret" aria-hidden="true"></span></button>
|
||
<div class="scard-body"><div class="kv"><b>현재</b><p>임베더 BGE-M3 검증+버전핀(1.4.0 dtype버그→1.2.11/transformers 4.44.2). <code>embed_query</code>를 asyncio.to_thread로 offload. 턴 파이프라인 배선: <code>retrieve_persona_memory</code>→episodic recall, <code>search_kb</code>→kb_behavior_cues. KB seed 3청크 적재. rag.py latent 버그 3건 수정. GraphRAG는 2차 보류.</p></div><div class="kv"><b>성능 회귀 수정</b><p>최초 RAG 배선이 회기 시작/턴에 임베더 로드(~7-13s)를 <b>동기 await</b>해 <code>POST /sessions</code>가 막힘 → session-layout E2E 4건 타임아웃. 수정: 회상·KB 회수를 <code>_warm_rag_caches</code> <b>백그라운드 task</b>로 옮기고 턴은 캐시만 읽음(비차단). 이번 동의 게이트 E2E 재검증 중 여러 세션 warm task가 BGE-M3를 동시에 지연 로드해 API health/dev-login timeout이 반복된 문제는 embedder load/encode <code>RLock</code>과 warm semaphore 1개로 안정화했다. 재검증: session-layout 4/4 + <b>핵심 E2E 27/27·11/11·아바타 4/4</b>, 최신 layout/session 조합 <b>11/11</b> 통과.</p></div><div class="kv"><b>시각 게이트</b><p>admin 1280 input "text-clipped-x" 1건은 폼 input 값 스크롤(설계상 정상) 오탐 — form 컨트롤을 text-clip 판정에서 제외했다. 당시 게이트는 <b>7/7</b>, 현재 strict gate는 빈 회기리뷰 포함 <b>9/9</b>다.</p></div></div>
|
||
</article>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- TRACK 4: OUTCOME & ALLIANCE OS -->
|
||
<div class="track-group" id="outcome-alliance-os">
|
||
<div class="tg-head"><h3>Outcome & Alliance OS · 정식 승격 로드맵</h3><span class="tg-note">제약이 아닌 증거 게이트로 진행 · 사람 개발자 계획 없음 · 2026-08-06 승격</span></div>
|
||
<div class="scards">
|
||
<article class="scard" data-status="done" data-cat="Outcome OS·측정" data-owner="0">
|
||
<button class="scard-head" aria-expanded="false"><span class="chip c-done">G0 DONE · INTERNAL</span><span class="scard-mid"><span class="scard-title">측정 진실 기반 — Measurement Truth Foundation</span><span class="scard-sum">G5 실제 수행 원장까지 포함한 current producer census 29/29와 provenance 위반 0을 고정했다.</span></span><span class="caret" aria-hidden="true"></span></button>
|
||
<div class="scard-body"><div class="kv"><b>구현</b><p><code>app.measurement_event</code>·<code>app.measurement_instrument</code>·<code>audit.model_run</code>과 6종 provenance, Python→JSON Schema→TypeScript 계약, 기존 rapport/alliance/fast/deep/Phase 3 adapter, 혼합층 집계 차단, 8개 deterministic benchmark 장면을 구축했다. <b>AOS-001~004 완료</b>.</p></div><div class="kv k-good"><b>종료 증거</b><p>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 증거도 유지한다. 상세: <code>ops/evidence/g0-provenance-census-scratch-post-g6-g8-20260807.json</code>, <code>ops/evidence/measurement-migration-rehearsal-2026-08-07.json</code>.</p></div></div>
|
||
</article>
|
||
<article class="scard" data-status="done" data-cat="Outcome OS·동맹" data-owner="0">
|
||
<button class="scard-head" aria-expanded="false"><span class="chip c-done">G1 DONE · INTERNAL · EXTERNAL ICC 분리</span><span class="scard-mid"><span class="scard-title">Alliance Core — goal/task/bond 다관점 측정</span><span class="scard-sum">승격 prompt 1.2는 24/24 ready·방향 9/9·오류 0으로 재검증했고 human-AI ICC는 외부 파일럿 품질 목표로 분리한다.</span></span><span class="caret" aria-hidden="true"></span></button>
|
||
<div class="scard-body"><div class="kv"><b>구현</b><p>회기 pulse, legacy alliance 격리, 자기평가 선잠금, 독립 모델 실행·재시도, 학습자 관점차 리뷰, 교수자 append-only 재평가를 구축했다. 실제 Session은 첫 발화 전 pre를 fail-closed로 잠그고 첫 왕복 뒤 흐름을 막지 않는 mid 30초 점검을 제공한다. <b>AOS-005~010 완료</b>.</p></div><div class="kv k-good"><b>종료 증거</b><p>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는 한 회기의 <code>pre(degraded/insufficient_transcript) → mid(ready) → post(ready)</code>를 실제 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%다. 상세: <code>ops/evidence/alliance-calibration-comparison-prompt-1.2.0-readskewfix-2026-08-07.json</code>, <code>ops/evidence/alliance-pulse-pre-mid-post-live-2026-08-07.json</code>.</p></div></div>
|
||
</article>
|
||
<article class="scard" data-status="done" data-cat="Outcome OS·종단" data-owner="0">
|
||
<button class="scard-head" aria-expanded="false"><span class="chip c-done">G2 DONE · INTERNAL</span><span class="scard-mid"><span class="scard-title">Longitudinal Outcome Trajectory — 종단 성과 궤적</span><span class="scard-sum">5회기 예상 궤적·악화·관계 사건을 세 개 독립 축과 장면 근거로 조기에 추적한다.</span></span><span class="caret" aria-hidden="true"></span></button>
|
||
<div class="scard-body"><div class="kv"><b>구현</b><p>교육용 합성 5회기 arc, 세 축 outcome 원장·append-only revision, 누락·오류 무점수, safety 분리, early warning·다음 질문, 역할별 관계 기억, 학습자 체크인과 desktop/mobile 타임라인을 구축했다.</p></div><div class="kv k-good"><b>종료 증거</b><p>4-case×5-session benchmark status 정확도 1.0·false alert/critical miss 0. 기존 실제 DB/API on-track case의 S1~S5·관측 15개·멱등/409·RLS·role-safe 기억에 더해, production measurement producer→store→evaluator를 통과하는 실제 PostgreSQL 두 case·10회기·30개 측정으로 악화 <code>on_track→off_track→deteriorating</code>과 단발성 noise <code>on_track→watch→on_track</code>·alert 승격 0건을 검증했다. case별 revision 5개와 learner/teacher projection 일치, 합성·비임상 표시를 보존했다. G2 focused 40 passed, 전체 API 855 passed, G2 UI 12/12, G1+기존 리뷰 15/15. 상세: <code>ops/outcome-trajectory-live-integration-evidence-2026-08-06.md</code>, <code>ops/evidence/outcome-trajectory-alerts-live-2026-08-07.json</code>.</p></div></div>
|
||
</article>
|
||
<article class="scard" data-status="done" data-cat="Outcome OS·균열" data-owner="0">
|
||
<button class="scard-head" aria-expanded="false"><span class="chip c-done">G3 DONE · INTERNAL</span><span class="scard-mid"><span class="scard-title">Rupture & Repair Lab — 관계 균열과 복구</span><span class="scard-sum">균열을 자연스럽게 만들고 탐지·복구·재연습의 전 과정을 장면 단위로 검증한다.</span></span><span class="caret" aria-hidden="true"></span></button>
|
||
<div class="scard-body"><div class="kv"><b>구현</b><p>9종 rupture/repair 상태머신·append-only 원장, Scenario Director, 텍스트·SSE·음성 공통 finalizer 감지 경계, 회기 후 근거 장면·복구 분기, fast/deep reconciliation, 교수자 supersession을 구축했다.</p></div><div class="kv k-good"><b>종료 증거</b><p>11-case benchmark macro-F1 1.0·복구 상태 accuracy 1.0, critical miss·judge gaming·암기문구 false resolution 0. Scenario/G3 107 passed, 통합 계약 251 passed, 리뷰 desktop/mobile 50/50. 기존 수동 write API 계약과 별도로 실제 텍스트 <code>submit_turn</code>·공용 finalizer callback만 사용한 PostgreSQL smoke가 missed·partial·resolved 3 episode, 10개 append-only observation, 3개 reconciliation, learner/teacher projection, 세션 밖 evidence 참조 0건을 통과했다. SSE·음성 개별 transport 실 DB smoke는 이 증거 범위가 아니다. 상세: <code>ops/rupture-repair-live-integration-evidence-2026-08-06.md</code>, <code>ops/evidence/rupture-runtime-auto-live-2026-08-07.json</code>.</p></div></div>
|
||
</article>
|
||
<article class="scard" data-status="done" data-cat="Outcome OS·수련" data-owner="0">
|
||
<button class="scard-head" aria-expanded="false"><span class="chip c-done">G4 DONE · INTERNAL</span><span class="scard-mid"><span class="scard-title">Deliberate Practice Engine — 의도적 수련 엔진</span><span class="scard-sum">실제 API/DB/엔진과 route-mock 0 브라우저에서 처방→후속 회기→observer 폐루프를 닫았다.</span></span><span class="caret" aria-hidden="true"></span></button>
|
||
<div class="scard-body"><div class="kv"><b>구현</b><p>원자적 practice prescription, 5개 연습 모드, competency graph·attempt evidence, 약점/망각 우선 선택, append-only PostgreSQL/API·RLS·내부 토큰, 학습자/교수자 리뷰 UI를 연결했다. ready 회기말 평가 저장 뒤 production <code>session_learning_producer</code>가 durable turn intent deviation을 실제 처방으로 변환한다. 학습자 CTA는 처방·원본 회기·성공 기준을 학습 홈→시작 전 브리핑→실제 새 세션 URL까지 보존한다.</p></div><div class="kv k-good"><b>종료 증거</b><p>실제 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. 상세: <code>ops/evidence/scratch-browser-returned-practice-closed-loop-evidence-20260807.json</code>, <code>ops/outcome-os-g4-g5-production-caller-evidence-2026-08-07.md</code>.</p></div></div>
|
||
</article>
|
||
<article class="scard" data-status="done" data-cat="Outcome OS·보정" data-owner="0">
|
||
<button class="scard-head" aria-expanded="false"><span class="chip c-done">G5 DONE · INTERNAL</span><span class="scard-mid"><span class="scard-title">Calibration Mirror & Transfer — 자기보정과 전이</span><span class="scard-sum">학생 actual execution을 실제 API/DB/엔진과 브라우저에서 독립 observer·provenance까지 닫았다.</span></span><span class="caret" aria-hidden="true"></span></button>
|
||
<div class="scard-body"><div class="kv"><b>구현</b><p>prediction revision/lock/reveal, 독립 수행 관측, calibration error·95% interval·메타인지 처방, 미지 transfer suite·암기 차단·합성 drift 원장/API/RLS와 역할 안전 UI를 연결했다. production lock callback은 ready 평가와 같은 competency의 durable turn evidence가 있을 때만 failed 독립 관찰을 파생하며 pass·mastery·transfer는 자동 추론하지 않는다. 실패한 unseen trial과 자기보정 처방을 학습 홈과 실제 새 세션까지 보존한다.</p></div><div class="kv k-good"><b>종료 증거</b><p>실제 API/DB/엔진 smoke는 execution·model-inferred·independent observer·instrument v1·durable evidence·distinct model-run을 각각 1로 고정하고 멱등 및 오류 출처·미종료·타학습자·교차 cohort 차단을 통과했다. post-merge disposable clone 브라우저는 route mock 0으로 desktop/mobile 4/4, actual POST→read reload→before/after와 1→1 멱등 재확인을 통과했고 UUID 노출·마이크 호출·중복 원장 0이다. 4-case benchmark와 lock/reveal/RLS 계약도 유지한다. 상세: <code>ops/evidence/scratch-browser-returned-practice-closed-loop-evidence-20260807.json</code>, <code>ops/calibration-transfer-live-integration-evidence-2026-08-06.md</code>.</p></div></div>
|
||
</article>
|
||
<article class="scard" data-status="done" data-cat="Outcome OS·슈퍼비전" data-owner="0">
|
||
<button class="scard-head" aria-expanded="false"><span class="chip c-done">G6 DONE · INTERNAL</span><span class="scard-mid"><span class="scard-title">Supervision & Research OS — 교수자·연구 운영체계</span><span class="scard-sum">current safety event를 raw detail 없이 최우선 queue와 직접 원장 pointer로 실증했다.</span></span><span class="caret" aria-hidden="true"></span></button>
|
||
<div class="scard-body"><div class="kv"><b>구현</b><p>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 트랜잭션으로 실행한다.</p></div><div class="kv k-good"><b>종료 증거</b><p>기존 6-signal queue·3-click·네 도메인 manifest·실제 API/DB/RLS/audit·version/subgroup 증거에 더해 post-merge disposable clone의 current source에서 safety event와 deterioration 경쟁 신호를 실제 derive했다. 결과는 <code>safety_boundary(1) > deterioration(2)</code>, direct pointer 각 1, source detail canary 1·G6 projection canary 0, raw transcript/임상 주장 0이고 replay가 멱등이다. 상세: <code>ops/evidence/g6-safety-priority-runtime-scratch-20260807.json</code>, <code>ops/outcome-os-g6-version-comparison-producer-evidence-2026-08-07.md</code>.</p></div></div>
|
||
</article>
|
||
<article class="scard" data-status="doing" data-cat="Outcome OS·음성" data-owner="0">
|
||
<button class="scard-head" aria-expanded="false"><span class="chip c-doing">G7 GATE · INTERNAL DONE · EXTERNAL PROOF</span><span class="scard-mid"><span class="scard-title">Multimodal Alliance — 음성·비언어 동맹 신호</span><span class="scard-sum">내부 안전·복구·용량 계약은 닫았고 current 공개 runtime·동의 마이크·사람 평가 증거를 기다린다.</span></span><span class="caret" aria-hidden="true"></span></button>
|
||
<div class="scard-body"><div class="kv k-good"><b>내부 구현 DONE</b><p>consent→단일 clock→독립 text/voice/fusion→철회·tombstone, streaming interim/final·word timestamp·provider event, HMAC word pseudonym, 열린 stream의 1초 동의 재검사·abort, 텍스트 보존·음성 재연결 UX를 연결했다. 운영 기본 provider는 노트북 상주 <code>local_whisper</code>/<code>melotts</code>이고 외부 Deepgram/OpenAI adapter는 fallback으로 보존한다. 격리 synthetic PCM soak는 59/59 연결 종료·성공 발화 55·철회 차단 2·미동의 차단 1·overflow 거부/복구 1/1·10MiB 선초과 거부를 통과했다. Windows host topology는 detached-clean commit/tree·도구 SHA·<code>psutil</code> 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, <code>local_whisper/small/cpu-int8</code>·<code>melotts/melotts-korean</code> exact readiness, provider env, Uvicorn <code>--ws websockets --ws-max-queue 4</code>, fresh API/cloudflared provenance receipt를 연결했다. runner/checker/topology 86/86·launcher/sidecar 80/80·통합 166/166·API voice 71/71을 통과했다.</p></div><div class="kv k-warn"><b>외부 종료 GATE</b><p>새 launcher는 아직 public에 실행하지 않았다. 현재 public API는 Git metadata가 없는 냉동 release cwd와 OpenAI voice provider로 실행돼 새 Windows provenance gate를 통과하지 못한다. 검증된 clean commit 기반 runtime으로 정렬하고 <code>/admin/voice-runtime</code>과 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가 <code>scripts/check-g7-external-proof.py</code> exit 0을 만들기 전에는 메인 상태를 DONE으로 바꾸지 않는다.</p></div></div>
|
||
</article>
|
||
<article class="scard" data-status="doing" data-cat="Outcome OS·에이전틱" data-owner="0">
|
||
<button class="scard-head" aria-expanded="false"><span class="chip c-doing">G8 BUILD · RECEIPT DONE · RUNTIME REVALIDATION</span><span class="scard-mid"><span class="scard-title">Autonomous Content & Continuous Improvement</span><span class="scard-sum">실제 rollback receipt는 완료됐다. 비-secure origin UUID 수정 배포와 NAS-origin 회기 E2E 재검증이 남았다.</span></span><span class="caret" aria-hidden="true"></span></button>
|
||
<div class="scard-body"><div class="kv"><b>agentic worker + 운영 환류 + human gate</b><p>승인 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으로 관리한다. 실제 <code>claude_cli/claude-opus-4-8</code> 7 calls와 dev PostgreSQL reviewer 2·variant 3·pass rate 1.0을 거쳐 synthetic payload를 <code>pending_human_approval</code>로 저장했다. G6 canonical drift trigger는 metadata-only incident→DAG→job을 원자 enqueue하고, 실제 pending 후보의 관리자 검수는 catalog 0→1·approval 1과 역할 403·멱등/409·민감정보 비노출을 통과했다. 별도 누수 후보는 422·저장 0이다. 상세: <code>ops/outcome-os-g8-agentic-worker-evidence-2026-08-07.md</code>.</p></div><div class="kv k-warn"><b>NAS-origin runtime 재검증</b><p>SHA <code>6030a677…c611</code>의 API/Web 이미지는 health 3/3·db/engine true, OpenAPI 126·auth 401·실제 SSE→DB-backed review를 통과했다. 그러나 108/108은 localhost candidate 결과였고 같은 NAS 평문 origin에서는 <code>crypto.randomUUID</code> 예외로 24건이 실패했다. 소스의 <code>randomUuid()</code> 수정은 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로 고정했다. 첫 current clean commit <code>5221f79e</code>와 fresh dump <code>6f4b95a7…b529f</code>를 결속한 execute는 API·types·build·candidate DB 후반까지 통과했지만 production entry 차단 테스트 2건으로 110/112에서 승격 전 중단돼 mutation 0이다. entry 수정은 source/production preview 각 2/2를 통과했다. 새 clean commit을 결속하고 동일 NAS-origin 회기 묶음이 GREEN이 되기 전에는 G8 전체를 DONE으로 올리지 않는다.</p></div><div class="kv k-good"><b>실제 rollback receipt DONE</b><p>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(<code>127.0.0.1:8018</code>)와 별도 executor(<code>127.0.0.1:18149</code>)로 <b>실제 image rollback 2회</b>를 실행했다. <code>rollback-old</code>는 receipt <code>nas-g8-723eeef2…</code>로 previous 이미지 <code>79ec…4450</code>/<code>c530…2f28</code>를, <code>restore-current</code>는 receipt <code>nas-g8-2738846c…</code>로 current 이미지 <code>52e0…8b2d</code>/<code>6fdb…f215</code>를 활성화했다. release gate·approval 각각 2회 멱등, lifecycle <code>rollback/executed</code> 2건, 승인 <code>authorize_rollback</code> 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하는 계약으로 검증했다(<code>launch-nas-preview-g8-helpers.py</code>, 37/37). 상세: <a href="./ops/nas-preview-g8-rollback-proof-runbook.md">런북</a> · <a href="./ops/evidence/nas-preview-g8-actual-rollback-2026-08-07.json">기계 판독 증거</a>.</p></div></div>
|
||
</article>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- ░░ DEEP / FOLDED REFERENCE ░░ -->
|
||
<section class="block" id="deep">
|
||
<div class="sec-head">
|
||
<div>
|
||
<h2>심층 상세 · 검증 로그</h2>
|
||
<p class="sub">로드맵 단계표, 단계별 상세 작업·검증 명령 표, 이번 세션 내러티브. 기본 접힘 — 필요할 때만 펼쳐서 본다.</p>
|
||
</div>
|
||
</div>
|
||
|
||
<details class="fold" id="fold-roadmap">
|
||
<summary><span class="fcaret" aria-hidden="true"></span>전체 개발 로드맵 (기존 5단계 + Outcome OS)<span class="fmeta">MASTERPLAN + OUTCOME & ALLIANCE OS</span></summary>
|
||
<div class="fold-inner">
|
||
<div class="roadmap">
|
||
<article class="phase-card current"><div class="phase-top"><span class="phase-id">Phase 0</span><span class="tag doing">closing</span></div><h3>기반 정렬 / 운영 안전장치</h3><p>인프라, 인증 경계, fail-closed, 공개 배포, 회귀 테스트 안정화.</p><ul class="phase-list"><li><b>상태:</b> 대부분 완료, 공개 <code>/turn</code> proof만 남음</li><li><b>검증:</b> build + backend 178 기준선 + Playwright 113 기준선, Node/backend focused 120</li><li><b>운영:</b> prod API 8001, engine 9099, tunnel 1개</li></ul></article>
|
||
<article class="phase-card next"><div class="phase-top"><span class="phase-id">Phase 1</span><span class="tag doing">next</span></div><h3>MVP 코어 루프</h3><p>1 페르소나 상담, 실제 턴 저장, 회기말 피드백, 저항 엔진 검증.</p><ul class="phase-list"><li><b>DoD:</b> 로그인 → P1 상담 완주 → 피드백 수신</li><li><b>추가:</b> 음성 왕복 1턴 PoC 포함</li><li><b>증거:</b> cost/turn DB 적재 + openness 곡선 대조</li></ul></article>
|
||
<article class="phase-card"><div class="phase-top"><span class="phase-id">Phase 2a</span><span class="tag info">planned</span></div><h3>3역할 / OAuth / 페르소나</h3><p>학습자, 교수자, 관리자 권한 분리와 페르소나 확장.</p><ul class="phase-list"><li><b>DoD:</b> 타인 session 직접 접근 403</li><li><b>범위:</b> OAuth BFF, RBAC, 교수 검수 루프</li><li><b>주의:</b> visible_to와 인간 권한은 별도 레이어</li></ul></article>
|
||
<article class="phase-card"><div class="phase-top"><span class="phase-id">Phase 2b</span><span class="tag info">planned</span></div><h3>음성 캐스케이드</h3><p>STT → 엔진 → TTS 왕복, WSS/터널 안정성, barge-in 로깅.</p><ul class="phase-list"><li><b>DoD:</b> 50분 WSS 끊김 실측</li><li><b>범위:</b> local_whisper STT + MeloTTS 기본, Deepgram/OpenAI fallback</li><li><b>리스크:</b> 실시간 레이턴시는 실측 전 완료 표기 금지</li></ul></article>
|
||
<article class="phase-card"><div class="phase-top"><span class="phase-id">Phase 3</span><span class="tag info">planned</span></div><h3>교육용 파일럿 / 효과성</h3><p>수련생 파일럿, 사전사후 효과 측정, 재귀학습 데이터셋 산출.</p><ul class="phase-list"><li><b>DoD:</b> 20명 완주 + KPI 충족</li><li><b>측정:</b> SUS, 자기효능감, κ/ICC, 환각률</li><li><b>정정:</b> IRB 게이트가 아니라 데이터 활용동의/개인정보 게이트</li></ul></article>
|
||
<article class="phase-card next"><div class="phase-top"><span class="phase-id">Outcome OS</span><span class="tag doing">G7 external · G8 runtime</span></div><h3>성과·동맹·수련 폐루프</h3><p>측정 → 이상 탐지 → 원인 가설 → 재연습 → 전이 검증 → 숙련도 갱신.</p><ul class="phase-list"><li><b>상태:</b> G0~G6 internal DONE · G8 실제 NAS rollback receipt DONE/runtime 재검증 중 · G7 외부 voice/resource/human 대기</li><li><b>완료 DAG:</b> AOS-001~012</li><li><b>운영:</b> NAS SHA <code>6030a677…c611</code>은 UUID 수정 전 빌드 · G8 receipt 2건 완료 · 수정 배포/NAS-origin E2E와 G7 canonical four-artifact checker exit 0 필요</li></ul></article>
|
||
</div>
|
||
<div class="source-note">정정 기준: <code>MASTERPLAN</code> 원본의 IRB 신청/승인 게이트는 <code>MASTERPLAN_REVISIONS</code> 기준으로 제거한다. Phase 2는 2a(3역할/OAuth/페르소나)와 2b(음성 캐스케이드)로 분리하고, Phase 1 DoD에는 얇은 음성 왕복 1턴 PoC를 포함한다.</div>
|
||
</div>
|
||
</details>
|
||
|
||
<details class="fold" id="fold-tabs">
|
||
<summary><span class="fcaret" aria-hidden="true"></span>단계별 상세 작업 · 검증 명령 표<span class="fmeta">P0~결정 / 검증 50+행</span></summary>
|
||
<div class="fold-inner">
|
||
<div class="tabs">
|
||
<div class="tabbar" role="tablist" aria-label="개발 상세 탭">
|
||
<button class="tab-button" id="tab-p0" role="tab" aria-selected="true" aria-controls="panel-p0" data-tab="p0">P0 마감</button>
|
||
<button class="tab-button" id="tab-p1" role="tab" aria-selected="false" aria-controls="panel-p1" data-tab="p1">P1 MVP</button>
|
||
<button class="tab-button" id="tab-p2a" role="tab" aria-selected="false" aria-controls="panel-p2a" data-tab="p2a">P2a 권한/페르소나</button>
|
||
<button class="tab-button" id="tab-p2b" role="tab" aria-selected="false" aria-controls="panel-p2b" data-tab="p2b">P2b 음성</button>
|
||
<button class="tab-button" id="tab-p3" role="tab" aria-selected="false" aria-controls="panel-p3" data-tab="p3">P3 파일럿</button>
|
||
<button class="tab-button" id="tab-ops" role="tab" aria-selected="false" aria-controls="panel-ops" data-tab="ops">운영/검증</button>
|
||
<button class="tab-button" id="tab-decisions" role="tab" aria-selected="false" aria-controls="panel-decisions" data-tab="decisions">결정 필요</button>
|
||
</div>
|
||
<div class="tab-panel" id="panel-p0" role="tabpanel" aria-labelledby="tab-p0">
|
||
<div class="detail-head"><div><h3>Phase 0 마감 상세</h3><p>지금 단계는 구현을 더 벌리는 단계가 아니라, 운영 증거를 닫고 P1 진입 조건을 고정하는 단계다.</p></div><div class="mini-metrics"><span>상태: 조건부</span><span>우선순위: P0</span><span>블로커: public /turn</span></div></div>
|
||
<div class="task-list">
|
||
<div class="task-row"><div><span class="task-status s-risk">BLOCK</span></div><div><b>공개 Google OAuth 실제 <code>/turn</code> proof 확보</b><p>로그인 가능한 계정으로 storageState를 저장한 뒤 public-auth-turn E2E를 실행한다.</p></div><div><b>산출물</b><p><code>apps/web/node_modules/.tmp/public-auth.json</code>, public <code>/turn</code> 성공 로그</p></div><div><b>검증</b><p><code>E2E_PUBLIC_AUTH=1</code> + <code>chromium-public-auth</code> 1회 통과</p></div></div>
|
||
<div class="task-row"><div><span class="task-status s-done">DONE</span></div><div><b>engine config 운영값 재확인</b><p><code>GET /admin/engine-config</code>로 운영값을 직접 확인했다. <code>engine_mode=claude_cli</code>, <code>engine_url=http://127.0.0.1:9099</code>, <code>model=gateway-default</code>, <code>durable=true</code>, <code>source=database</code>로 의도값과 일치한다.</p></div><div><b>산출물</b><p>config API 응답(durable, db source)</p></div><div><b>검증</b><p><code>claude_cli / http://127.0.0.1:9099 / gateway-default</code> 의도값 대조 완료</p></div></div>
|
||
<div class="task-row"><div><span class="task-status s-done">DONE</span></div><div><b>과거 Live2D demo 자산 제거</b><p>Mao/Haru 샘플, Pixi/Cubism 런타임, 공개 <code>/live2d/*</code> 캐시 잔여 접근을 차단했다.</p></div><div><b>산출물</b><p><code>apps/web/functions/live2d/[[path]].js</code>, SVG 도형 기반 파라미터 리그</p></div><div><b>검증</b><p>운영 <code>/live2d/mao/*</code>, Cubism core <code>404</code></p></div></div>
|
||
<div class="task-row"><div><span class="task-status s-done">DONE</span></div><div><b>프로세스 난립 정리</b><p>운영 확인용 프로세스와 로컬/Tailnet 검증용 dev 프로세스를 의도적으로 분리해 유지한다.</p></div><div><b>산출물</b><p>9099 engine, 8001 prod API, 8000/8010 dev API, 5173 web, cloudflared tunnel 1개</p></div><div><b>검증</b><p><code>Get-NetTCPConnection</code>에서 대상 포트별 listener 확인</p></div></div>
|
||
<div class="task-row"><div><span class="task-status s-plan">GATE</span></div><div><b>한신대 공문/데이터 거버넌스 게이트 정리</b><p>SSO 클레임, 추가 축어록 수급, 미성년 원본 활용동의, 개인정보 처리방침을 P1 진입 전 외부 의존성으로 명확히 둔다. 로컬 문서 골격은 준비됐지만 한신대/데이터 steward의 written evidence는 아직 필요하다.</p></div><div><b>산출물</b><p><code>docs/ops/hanshin-data-governance-gate.md</code>, 공문 질의 항목, 동의 범위 체크리스트, SSO claim mapping 표</p></div><div><b>검증</b><p>문서 artifact 작성 완료; IRB 게이트가 아니라 데이터/SSO 게이트로 외부 증거 필요 상태 유지</p></div></div>
|
||
<div class="task-row"><div><span class="task-status s-done">DONE</span></div><div><b>한신대 실사용 피드백 1차 개선팩</b><p>오류분석 자료를 컨텍스트 보존, 내담자 출력 품질, 리뷰 privacy/오류문구 UX, 교수자 설명 가능성으로 분해했고 1차 구현을 완료했다. P1은 role mapping + client-only history injection을 원자 패치로 닫았고, P2/P3/P4는 저장형 fallback 없이 빠른 UX 개선으로 배포 가능한 상태다.</p></div><div><b>산출물</b><p><code>gateway._split_messages(ai_role=client)</code>, <code>guardrail.sanitize_client_reply()</code> 품질 게이트, SSE full-response buffer, 리뷰 표시 자연어화, raw error mapper, 교수자 <code>AI 평가 범위</code> 패널, 계획/레드팀/대레드팀 문서</p></div><div><b>검증</b><p><code>engine_gateway/test_gateway_model.py app/test_orchestrator_masking.py app/test_client_reply_quality.py app/test_session_turn_persistence.py</code> 84 passed, <code>npm run typecheck</code>, <code>npm run check:api-types</code>, <code>session-review.spec.ts</code> focused 8 passed, <code>layout-visual-gate.spec.ts</code> 12 passed, <code>session-layout.spec.ts</code> 8 passed.</p></div></div>
|
||
</div>
|
||
<div class="source-note"><b>팀장 판정:</b> 로컬 회귀와 공개 prod-safe 게이트는 통과했다. 2026-06-29 수동 Cloudflare Pages production 배포가 현재 custom domain asset으로 확인됐고, 운영 반영 전 남은 핵심 증거는 공개 Google OAuth 실제 <code>/turn</code> smoke다. 런타임 mock/demo 자산은 운영 번들에서 제거하고 테스트 fixture만 남긴다.</div>
|
||
<div class="tab-subhead"><h4>Phase 0 완료 상세 목록</h4><p>이전 "완료" 카드 내용을 탭 안에서 항목별로 보존한다.</p></div>
|
||
<div class="task-list">
|
||
<div class="task-row"><div><span class="task-status s-done">DONE</span></div><div><b>DB persona 기반 테스트</b><p><code>session-persistence</code>, <code>teacher</code>, <code>voice</code>는 <code>/api/personas</code>에서 실제 DB persona를 가져온다.</p></div><div><b>산출물</b><p>seed persona 직접 결합 제거, 실제 persona catalog 기반 E2E</p></div><div><b>검증</b><p>persona 관련 E2E가 DB-backed persona로 통과</p></div></div>
|
||
<div class="task-row"><div><span class="task-status s-done">DONE</span></div><div><b>비개발 런타임 보호</b><p>prod/staging에서 dev login, seed fallback, OAuth 누락, 기본 secret, localhost CORS를 거부한다.</p></div><div><b>산출물</b><p>fail-closed runtime config validation</p></div><div><b>검증</b><p>backend runtime policy unittest 통과</p></div></div>
|
||
<div class="task-row"><div><span class="task-status s-done">DONE</span></div><div><b>compose OAuth 주입</b><p><code>ENVIRONMENT</code>, Google OAuth vars, <code>CORS_ORIGINS</code>, <code>OPENAI_BASE_URL</code>을 API 컨테이너에 명시했다.</p></div><div><b>산출물</b><p>compose 환경 주입 정리</p></div><div><b>검증</b><p><code>docker compose -f infra\docker-compose.yml --env-file infra\.env.example config --quiet</code> 더미 필수 env 세트로 통과</p></div></div>
|
||
<div class="task-row"><div><span class="task-status s-done">DONE</span></div><div><b>admin 모바일 overflow 수정</b><p>긴 readiness detail을 안정화했고, 사용자 변경은 DB 저장소가 durable할 때만 허용한다.</p></div><div><b>산출물</b><p>admin responsive layout, durable store gate, AI health copy</p></div><div><b>검증</b><p>admin manage-users desktop/mobile 2 passed</p></div></div>
|
||
<div class="task-row"><div><span class="task-status s-done">DONE</span></div><div><b>global engine config 테스트 분리</b><p><code>db-persistence</code>는 전역 engine config를 바꾸므로 <code>@single-run</code>으로 분리했다.</p></div><div><b>산출물</b><p>Playwright single-run tagging</p></div><div><b>검증</b><p>전체 Playwright 113 passed</p></div></div>
|
||
<div class="task-row"><div><span class="task-status s-done">DONE</span></div><div><b>handoff와 문서 갱신</b><p><code>docs/HANDOFF.md</code>, <code>docs/dev_dashboard.html</code>, <code>apps/web/e2e/README.md</code>를 최신 상태로 맞췄다.</p></div><div><b>산출물</b><p>handoff, dashboard, E2E README</p></div><div><b>검증</b><p>최신 커밋 기준 상태/검증/운영 포트 일치</p></div></div>
|
||
<div class="task-row"><div><span class="task-status s-done">DONE</span></div><div><b>공개 API prod 전환</b><p><code>cloudflared</code> ingress를 <code>127.0.0.1:8001</code> prod API로 돌렸고 public readiness gate가 통과했다.</p></div><div><b>산출물</b><p>prod API 8001, tunnel config</p></div><div><b>검증</b><p><code>https://api-vignette.chanpaca.net/health</code> prod/db/engine OK</p></div></div>
|
||
<div class="task-row"><div><span class="task-status s-done">DONE</span></div><div><b>공개 런타임 재기동 스크립트</b><p><code>scripts/start-public-runtime.ps1</code>와 <code>scripts/stop-public-runtime.ps1</code>로 API 8001과 cloudflared를 재현 가능하게 만들었다.</p></div><div><b>산출물</b><p>public runtime start/stop scripts</p></div><div><b>검증</b><p>engine 9099 + API 8001 + cloudflared only</p></div></div>
|
||
<div class="task-row"><div><span class="task-status s-done">DONE</span></div><div><b>persona catalog 인증화</b><p><code>/personas</code>는 이제 비로그인 401이다. 로컬과 공개 API 모두 직접 확인했고 E2E 회귀를 추가했다.</p></div><div><b>산출물</b><p>authenticated persona catalog boundary</p></div><div><b>검증</b><p>local/public unauth <code>GET /personas</code> 401</p></div></div>
|
||
<div class="task-row"><div><span class="task-status s-done">DONE</span></div><div><b>public API base fallback 보강</b><p>public 웹에서 <code>/api/auth/config</code>가 SPA HTML로 떨어지지 않도록 public API origin fallback을 추가했다.</p></div><div><b>산출물</b><p>public API origin fallback</p></div><div><b>검증</b><p>public login screen E2E 1 passed</p></div></div>
|
||
<div class="task-row"><div><span class="task-status s-done">DONE</span></div><div><b>학습자 홈 0회 기록 상태</b><p>학습자 홈은 fake 기록 없이 실제 서버 세션 0개를 <code>기존 회기</code> 빈 상태와 0 카운트로 표시한다.</p></div><div><b>산출물</b><p>empty history UX, zero count, no fake stats</p></div><div><b>검증</b><p>learner screenshots: document overflow 0, empty history visible</p></div></div>
|
||
<div class="task-row"><div><span class="task-status s-done">DONE</span></div><div><b>로컬 5175 로그인 재기동 검증</b><p><code>127.0.0.1:5175</code> web, <code>8000</code> API, <code>9099</code> engine을 재기동하고 dev-login 서버 세션과 <code>/learn</code> redirect를 확인했다.</p></div><div><b>산출물</b><p>local dev-login flow proof</p></div><div><b>검증</b><p>desktop/mobile auth flow passed</p></div></div>
|
||
<div class="task-row"><div><span class="task-status s-done">DONE</span></div><div><b>active session dense viewport 보정</b><p>375px급 active session에서 stage 내부가 1px 넘게 잘리던 문제를 avatar/padding 조정으로 제거했다.</p></div><div><b>산출물</b><p>session layout responsive adjustment</p></div><div><b>검증</b><p>session-layout desktop/mobile 8 passed</p></div></div>
|
||
<div class="task-row"><div><span class="task-status s-done">DONE</span></div><div><b>텍스트 턴 SSE 전환</b><p><code>POST /sessions/{id}/stream</code> token 이벤트로만 내담자 자막을 쌓고, HTTP 거절과 SSE error 모두 실패 턴을 transcript와 저장소에 남기지 않는다.</p></div><div><b>산출물</b><p>real SSE stream turn path, failed turn discard</p></div><div><b>검증</b><p>session layout/turn E2E passed</p></div></div>
|
||
<div class="task-row"><div><span class="task-status s-done">DONE</span></div><div><b>음성 실패 턴 미저장</b><p>음성 회기도 엔진 응답 생성 성공 후에만 learner/client turn을 저장한다. 실패한 transcript는 pending 상태에서 제거한다.</p></div><div><b>산출물</b><p>voice failure persistence guard</p></div><div><b>검증</b><p>voice + voice-success + session-layout 15 passed</p></div></div>
|
||
<div class="task-row"><div><span class="task-status s-done">DONE</span></div><div><b>비영구 상태 문구 정리</b><p>admin health와 voice degraded reason은 내부 fallback/seed 표현 대신 비영구 런타임 상태를 사용자용 문구로 표시한다.</p></div><div><b>산출물</b><p>operator-facing copy cleanup</p></div><div><b>검증</b><p>settings/admin focused E2E 통과</p></div></div>
|
||
<div class="task-row"><div><span class="task-status s-done">DONE</span></div><div><b>직접 URL persona 차단</b><p><code>/learn/session/:code</code>로 직접 들어와도 DB 원본 persona가 아니면 회기 시작을 막는다.</p></div><div><b>산출물</b><p>route guard, degraded/seed fallback regression</p></div><div><b>검증</b><p>unavailable persona code E2E 통과</p></div></div>
|
||
<div class="task-row"><div><span class="task-status s-done">DONE</span></div><div><b>demo Live2D 자산 제거</b><p>Mao/Haru 샘플과 Pixi/Cubism 런타임을 공개 배포물에서 제거했다. 세션 화면은 SVG 도형 기반 파라미터 리그만 렌더링한다.</p></div><div><b>산출물</b><p>SVG 파라미터 리그, legacy <code>/live2d/*</code> 404 function</p></div><div><b>검증</b><p>Mao/Haru/Cubism core routes 404, no Live2D/Pixi chunks</p></div></div>
|
||
</div>
|
||
</div>
|
||
<div class="tab-panel" id="panel-p1" role="tabpanel" aria-labelledby="tab-p1" hidden>
|
||
<div class="detail-head"><div><h3>Phase 1 MVP 코어 루프 상세</h3><p>목표는 "P1 내담자와 실제 상담 1회 완주 후 피드백을 받는 공개 시연 가능 상태"다.</p></div><div class="mini-metrics"><span>우선순위: P0</span><span>범위: 텍스트+음성 1턴</span><span>완료보고: E2E 증거 후</span></div></div>
|
||
<div class="task-list">
|
||
<div class="task-row">
|
||
<div><span class="task-status s-done">DONE</span></div>
|
||
<div><b>LLMEngine 정규화 계약 고정</b><p>gateway SSE <code>token/done/error</code>와 generate cost 계약은 회귀화했다. 이번 패스에서 <code>app/contracts/engine_gateway.py</code>를 추가해 FastAPI <code>engine_client</code>와 현재 Python gateway가 같은 <code>GenerateRequest</code>/<code>GenerateResponse</code>/<code>Stream*Event</code> 계약을 import한다. 이어서 <code>EngineGatewaySseLineDecoder</code>로 token/done/error decode와 validation도 같은 계약 모듈에 모았다. 최신 패스에서는 raw SSE line 해석을 <code>EngineClient.stream_packets()</code> 경계로 올리고, Python gateway <code>/v1/generate</code>가 <code>GenerateResponse.model_dump()</code>로 응답하게 해 hand-mirrored dict drift를 줄였다. 78차에서는 <code>gateway-default</code>가 실제 provider 모델명이 아니라 기본 라우팅 sentinel이라는 의미를 계약 상수·정규화 함수·schema artifact·Node conformance runner에 고정했다. 79차에서는 <code>structured_payload_from_response()</code>가 structured field와 legacy text JSON fallback 해석을 소유해 evaluator/live-coach/persona draft가 같은 <code>GenerateResponse</code> parser를 쓰게 했고, 81차에서는 evaluator/live-coach의 로컬 alias도 제거했다. 88차에서는 Node runner가 stream event 목록과 payload 검증을 <code>StreamPacket.oneOf</code>와 payload schema <code>$ref</code>에서 읽게 해 runner 내부 중복 선언을 제거했다. 92차에서는 Python gateway의 <code>provider/model</code> 응답 메타 반환 규칙을 helper로 모으고, Node runner가 fixture request의 <code>gateway-default</code>는 default-routing sentinel이며 response/done packet의 model은 resolved provider model이어야 한다고 검증한다. 93차에서는 <code>GatewayPromptParts</code>가 current-turn <code>system_prompt</code>/<code>user_payload</code> 분해 결과를 소유한다. Node.js gateway로 갈아치울 때 보존해야 할 wire/default-routing/structured-payload/response-metadata/current-turn prompt contract다. claude_cli와 Messages API live parity는 <code>ANTHROPIC_API_KEY</code> 의존 B2 항목으로 분리한다.</p></div>
|
||
<div><b>산출물</b><p><code>app/contracts/engine_gateway.py</code>, <code>app/engine_client.py</code>, <code>app/services/orchestrator.py</code>, <code>app/routes/admin.py</code>, <code>engine_gateway/gateway.py</code>, <code>engine_gateway/test_gateway_model.py</code>, <code>engine_gateway/golden/*</code>, <code>scripts/check-engine-gateway-contract.mjs</code> shared contract + stream packet tests</p></div>
|
||
<div><b>검증</b><p><code>node scripts/check-engine-gateway-contract.mjs --json</code>, <code>py -3.11 -X utf8 -B -m pytest -p no:cacheprovider engine_gateway/test_gateway_model.py -q</code> 27 passed, <code>py -3.11 -X utf8 -B -m pytest -p no:cacheprovider engine_gateway/test_gateway_model.py app/test_evaluation_persistence.py app/test_evaluator_model_routing.py app/test_session_turn_persistence.py app/test_live_coach_sources.py app/test_persona_generation_contract.py app/test_persona_review.py -q</code> 120 passed, <code>npm run check:api-types</code>, <code>npm run typecheck</code>.</p></div>
|
||
</div>
|
||
<div class="task-row"><div><span class="task-status s-done">DONE</span></div><div><b>상주 <code>claude -p</code> 엔진풀 실증</b><p><code>session_id</code>가 살아 있는 gateway session을 재사용하고 missing session은 ephemeral로 닫히는 구조를 회귀화했고, live 게이트웨이(9099)로 실측까지 마쳤다. <code>probe-engine-gateway.py</code>가 단일 session_id로 2회 reused stream을 돌려 TTFT 1667–4199ms, cost 누적 0.068→0.123(turns 1→2), engine=claude_p, model=opus-4-8을 측정했다.</p></div><div><b>산출물</b><p><code>engine_gateway/test_gateway_model.py</code>, <code>scripts/probe-engine-gateway.py</code>, live probe JSON</p></div><div><b>검증</b><p>session reuse 7 tests OK; live probe 2 reused streams, TTFT/cost 실측, session_id 재사용 확인</p></div></div>
|
||
<div class="task-row"><div><span class="task-status s-done">DONE</span></div><div><b>마스킹 게이트 + LLM call audit</b><p>current turn, recall, pinned fact, recent turns와 live coach recent/evaluation prompt, turn-evaluation persistence rationale/comment/alternative, legacy DB row text fallback, session-end deep evaluation payload/read-model이 Presidio/가명처리 후에만 engine request/DB 저장·복원·리뷰 응답 경계로 들어가게 막았다. 한국어 이름/기관 로컬 휴리스틱 1차와 16-case summary-only 합성 fixture 평가 harness/input-report schema를 추가했고, fast evaluator prompt와 client turn <code>text_masked</code>도 마스킹본을 쓰게 보강했다. 66차에서는 자연 발화형 이름 라벨·자기소개 케이스를 추가해 라벨 단어만 마스킹하고 실명을 남기던 구멍을 막았다. 70차에서는 선택형 ko recognizer adapter를 <code>mask_pii()</code> 경계에 추가하고, adapter 성공/실패 모두 regex fallback이 마지막 안전망으로 남는지 회귀화했다. 외부 LLM 호출은 <code>audit.llm_call_log</code>에 provider/model/token/cost/inference_geo/latency metadata만 남기고 prompt/completion 본문은 저장하지 않는다.</p></div><div><b>산출물</b><p><code>app/services/guardrail.py</code>, <code>app/services/pii_masking_eval.py</code>, <code>data/privacy/pii-masking-ko-fixtures.json</code>, <code>data/privacy/pii-masking-eval-input.schema.json</code>, <code>data/privacy/pii-masking-eval-report.schema.json</code>, <code>scripts/evaluate-pii-masking.py</code>, <code>app/test_pii_masking_eval.py</code>, <code>app/test_orchestrator_masking.py</code>, <code>app/test_evaluation_persistence.py</code>, <code>app/test_session_turn_persistence.py</code>, <code>app/test_live_coach_privacy.py</code>, <code>session_persistence.record_llm_call_audit</code></p></div><div><b>검증</b><p>phone/email/RRN 및 한국어 NAME/ORG raw 값이 generate/stream/evaluator payload에 없음, adapter fake span과 fallback phone 마스킹 경로 검증, adapter 실패 시 regex fallback 유지, 감사 payload 본문 미포함, fixture 16/16 pass, 최신 보정 회귀 59 passed, H4 prompt/persistence privacy 50 passed, 현재 전체 API 178 기준 유지, 로컬 live <code>/turn</code> smoke 후 <code>audit.llm_call_log</code> 8→11</p></div></div>
|
||
<div class="task-row"><div><span class="task-status s-done">DONE</span></div><div><b>P1 상담 완주 플로우</b><p>로컬 DB-backed 런타임에서 P1 선택, SSE 상담, 종료, 회기말 리뷰 피드백을 한 번에 통과시켰다. 실제 운영 로그인 proof는 P0 public OAuth blocker와 분리해 남긴다.</p></div><div><b>산출물</b><p><code>e2e/session-mvp.spec.ts</code>, turn telemetry 저장 경로</p></div><div><b>검증</b><p><code>npx playwright test e2e/session-mvp.spec.ts --project=chromium-single-run --workers=1</code> 1 passed.</p></div></div>
|
||
<div class="task-row"><div><span class="task-status s-done">DONE</span></div><div><b>저항 엔진 정량 검증</b><p>공감 발화와 조언점프 발화 5턴 세트를 상태머신 회귀 테스트와 실제 API+DB smoke로 비교했다. live smoke는 dev-login→onboarding→P1 세션 2개→각 5턴을 수행하고 <code>app.session_state</code>, <code>app.turns</code>, <code>app.turn_client_state</code>를 직접 조회했다. 공감 세션은 3턴부터 <code>탐색</code>으로 열리고 openness가 <code>0.171</code>까지 상승했다. 조언점프 세션은 <code>라포</code>와 openness <code>0.0</code>을 유지했다.</p></div><div><b>산출물</b><p><code>app/test_state_machine_resistance.py</code>, <code>scripts/smoke-resistance-openness-db.py</code>, <code>docs/ops/resistance-openness-db-smoke-2026-06-28.json</code></p></div><div><b>검증</b><p><code>pytest app/test_state_machine_resistance.py app/test_evaluation_persistence.py -q</code> 9 passed; live DB smoke PASS.</p></div></div>
|
||
<div class="task-row"><div><span class="task-status s-done">DONE</span></div><div><b>음성 왕복 1턴 PoC</b><p>Session UI 마이크 버튼 경로에서 synthetic <code>getUserMedia</code>/<code>MediaRecorder</code>로 실제 <code>/voice/ws</code>를 열고 audio_start, binary chunk, audio_end, transcript, reply, TTS 재생 상태까지 검증했다. 추가로 로컬 live API에서 실제 STT/TTS provider를 통과해 TTS 바이너리를 수신했다. 물리 마이크, 공개 WSS, 50분 양방향 실측은 별도 운영 smoke로 남아 있다.</p></div><div><b>산출물</b><p><code>apps/web/e2e/voice-success.spec.ts</code>, controlled OpenAI/engine providers, Web Audio browser voice UI probe, live <code>/voice/ws</code> smoke</p></div><div><b>검증</b><p><code>npx playwright test e2e/voice-success.spec.ts --project=chromium-single-run</code> 2 passed; live P2 WS smoke binary 66 chunks/266,496 bytes</p></div></div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="tab-panel" id="panel-p2a" role="tabpanel" aria-labelledby="tab-p2a" hidden>
|
||
<div class="detail-head"><div><h3>Phase 2a 권한, OAuth, 페르소나 상세</h3><p>음성과 섞지 않고 3역할, 권한격리, 페르소나 확장을 독립 트랙으로 닫는다.</p></div><div class="mini-metrics"><span>주차: 9-13</span><span>우선순위: P1</span><span>IDOR 검증 필수</span></div></div>
|
||
<div class="task-list">
|
||
<div class="task-row"><div><span class="task-status s-done">DONE</span></div><div><b>OAuth BFF / HttpOnly 세션 완성</b><p>Auth Code + PKCE/state와 opaque HttpOnly/Secure cookie 계약을 회귀화했다. OAuth state는 HMAC 서명 토큰과 HttpOnly state cookie를 함께 검증해 API 재시작 뒤 콜백이 <code>invalid_state</code>로 떨어지지 않으면서도 브라우저 시작 흐름에 묶인다. Google provider error는 <code>access_denied</code>/<code>provider_error</code>로 분리하고 로그인 화면에 reason code를 표시한다. 공개 Google OAuth 실제 <code>/turn</code> proof는 P0 blocker로 남아 있다.</p></div><div><b>산출물</b><p><code>app/test_auth_providers.py</code>, cookie-bound signed PKCE state, OAuth callback cookie path, login reason UI</p></div><div><b>검증</b><p><code>python -B -m pytest -p no:cacheprovider app/test_auth_providers.py -q</code> 30 passed; <code>npm run typecheck</code> passed.</p></div></div>
|
||
<div class="task-row"><div><span class="task-status s-done">DONE</span></div><div><b>학습자/교수자/관리자 권한 격리</b><p>runtime fallback에서 타 learner session 직접 접근 403과 learner list 필터를 회귀화했고, teacher/admin durable read audit insert 경로를 추가했다. live Postgres(NOBYPASSRLS <code>vignette_app</code> 역할)로 RLS/audit smoke를 실행해 운영 증거를 확보했다: learner 세션/턴 격리, teacher cohort 스코프, teacher/admin read_session audit insert가 실제 정책 하에서 PASS. 이번 패스에서 역할(role)과 관리자 페이지 진입권(<code>admin_access</code>)을 분리했고, <code>AUTH_SUPER_ADMIN_EMAILS</code> 슈퍼 관리자만 관리자 권한을 부여·회수할 수 있게 했다. 슈퍼 관리자는 학습자/교수자 라우트도 통과할 수 있지만, 일반 <code>admin_access</code>는 관리자 콘솔 진입권으로만 쓰인다.</p></div><div><b>산출물</b><p><code>app_user.admin_access</code>, <code>require_admin_access()</code>, <code>/admin/users</code> 권한 위임 UI, <code>app/test_rbac_idor.py</code>, <code>audit.audit_log</code> read_session insert, <code>scripts/check-postgres-rls-audit.py</code>(live PASS)</p></div><div><b>검증</b><p>타인 <code>session_id</code> 직접 입력 403(unit), live RLS smoke 5 checks PASS, 관리자 권한 위임 회귀 <code>app/test_auth_providers.py app/test_admin_ops.py</code> 32 passed</p></div></div>
|
||
<div class="task-row"><div><span class="task-status s-done">DONE</span></div><div><b><code>visible_to</code>와 인간 권한 레이어 분리</b><p>learner detail/review와 client engine history에서 evaluator-only turn을 제외한다. live Postgres smoke가 evaluator-only DB 정책을 실제로 검사해 PASS: evaluator-only turn이 learner/client/counselor AI에는 숨겨지고 evaluator AI에만 보인다.</p></div><div><b>산출물</b><p><code>TurnRecord.visible_to</code>, learner/client/evaluator filters, <code>docs/ops/postgres-rls-audit-smoke.md</code></p></div><div><b>검증</b><p>evaluator-only turn 숨김 unit proof + live RLS smoke <code>visible_to.evaluator_only_hidden</code> PASS</p></div></div>
|
||
<div class="task-row"><div><span class="task-status s-done">DONE</span></div><div><b>페르소나 P2/P3 교수 검수 루프</b><p>approved catalog와 draft/review 교수 검수 큐를 분리하고, teacher/admin 승인·반려 mutation과 교수 콘솔 UI proof를 추가했다. 교수 콘솔의 404 raw 노출도 제거(404→정상 빈상태)했다.</p></div><div><b>산출물</b><p><code>/personas/review</code>, <code>POST /personas/review/{persona_id}</code>, professor review panel</p></div><div><b>검증</b><p><code>app.test_persona_review</code> 14 tests; teacher UI single-run E2E 1 passed; live RLS smoke PASS</p></div></div>
|
||
<div class="task-row"><div><span class="task-status s-done">DONE</span></div><div><b>SAML SP 스캐폴드 선반영</b><p>Google OIDC와 별도 provider 상태를 <code>/auth/config</code>에 노출하고, Redirect-binding AuthnRequest와 dev-only unsigned fixture ACS를 backend proof 수준으로 구현했다. 운영 SAML 서명검증과 기관 test tenant는 외부 IdP 증거가 필요하며, fingerprint 설정 시 unsigned fixture ACS는 거부한다.</p></div><div><b>산출물</b><p><code>AUTH_SAML_ENABLED</code>, <code>SAML_SP_ENTITY_ID</code>, <code>SAML_SSO_URL</code>, RelayState store, <code>/auth/saml/acs</code> fixture endpoint</p></div><div><b>검증</b><p><code>app/test_auth_providers.py</code> 30 passed: AuthnRequest issuer/ACS/destination, RelayState 검증, opaque HttpOnly cookie, bad RelayState/signature-required reject.</p></div></div>
|
||
</div>
|
||
</div>
|
||
<div class="tab-panel" id="panel-p2b" role="tabpanel" aria-labelledby="tab-p2b" hidden>
|
||
<div class="detail-head"><div><h3>Phase 2b 음성 캐스케이드 상세</h3><p>음성은 "되게 보이는 UI"가 아니라, 전송, STT, EOT, 엔진, TTS, 저장까지 끊기지 않아야 한다.</p></div><div class="mini-metrics"><span>주차: 13-17</span><span>WSS 50분</span><span>실측 전 완료 금지</span></div></div>
|
||
<div class="task-list">
|
||
<div class="task-row"><div><span class="task-status s-done">LOCAL DONE</span></div><div><b>voice gateway WebSocket</b><p><code>/voice/ws</code> backend contract를 fake WebSocket으로 회귀화했고, Session UI는 <code>AudioWorklet</code>-first PCM16 capture와 <code>MediaRecorder</code> fallback으로 한 턴 synthetic audio를 통과했다. 백엔드는 <code>format=pcm</code> raw chunk를 STT 업로드 직전 WAV로 감싼다. 로컬 API에서는 실제 OpenAI STT/TTS 경로 live WS smoke도 통과했다.</p></div><div><b>산출물</b><p><code>app/test_voice_ws.py</code>, <code>apps/web/public/worklets/voice-capture-worklet.js</code>, <code>apps/web/e2e/voice-success.spec.ts</code>, audio_start/binary/audio_end/text_turn/stt_result/ping contract</p></div><div><b>검증</b><p><code>app/test_voice_ws.py app/test_voice_service.py</code> 27 passed; voice-success chromium-single-run 2 passed; live P2 WS smoke transcript 52자, reply 105자, TTS binary 66 chunks/266,496 bytes.</p></div></div>
|
||
<div class="task-row"><div><span class="task-status s-done">LOCAL DONE</span></div><div><b>Streaming STT + EOT</b><p>provider-neutral EOT helper와 route-level <code>stt_result</code> control을 연결했다. final transcript와 silence threshold가 ready일 때만 턴을 실행하고, pending이면 <code>state=listening</code>으로 돌아간다. 이 행은 로컬 route 계약 완료 범위다.</p></div><div><b>산출물</b><p><code>assess_end_of_turn</code>, <code>EndOfTurnDecision</code>, <code>stt_result</code> control, EOT silence threshold</p></div><div><b>검증</b><p><code>app/test_voice_ws.py app/test_voice_service.py</code> 27 passed; final pending, empty transcript, silence threshold, route-level ready/pending 케이스</p></div></div>
|
||
<div class="task-row"><div><span class="task-status s-done">DONE</span></div><div><b>OpenAI TTS voice preset 추상화</b><p>P1/P2/P3 persona voice preset, invalid preset fallback, speed clamp, instructions payload 계약을 회귀화했고, <code>app.persona_voice_map</code>을 live <code>/voice/ws</code> TTS 선택에 연결했다. 명시 preset은 DB map보다 우선하고, OpenAI가 아닌 provider row는 기존 fallback으로 안전하게 흡수한다.</p></div><div><b>산출물</b><p><code>resolve_voice</code>, <code>resolve_voice_from_map</code>, <code>build_tts_payload</code>, <code>get_session_voice_map</code>, seed voice map row, dev runtime schema guard</p></div><div><b>검증</b><p><code>pytest app/test_runtime_policy.py app/test_persona_review.py app/test_voice_service.py app/test_voice_ws.py -q</code> 75 passed; full API 178 passed + gateway 11 passed.</p></div></div>
|
||
<div class="task-row"><div><span class="task-status s-done">POC DONE</span></div><div><b>P1 서연 무참조 synthetic voice 샘플</b><p>Higgs v3 로컬 런타임에서 실존 reference voice 없이 synthetic seed를 먼저 만들고, 그 seed만 재참조해 우울·피로·불안·라포·회복감 5개 변주를 생성했다. 개발용 <code>VIGNETTE_VOICE_POC_SAMPLE_TTS</code> 플래그로 <code>/voice/ws</code> P1 sample TTS provider에도 연결했다.</p></div><div><b>산출물</b><p><code>docs/archive/voice-art/bimec-persona-voice-poc-2026-06-27.md</code>, <code>docs/voice-art/p1-seoyeon-higgs-v3-20260627/manifest.json</code>, <code>apps/api/app/services/voice.py</code></p></div><div><b>검증</b><p>mp3/wav 6개(seed+5) 생성, 24kHz, 7.72~11.68초. live <code>/voice/ws</code> text_turn smoke: provider <code>p1-sample-poc</code>, binary 20 chunks/78,573 bytes. 샘플 provider는 TTS override만 담당하고 마이크/STT는 OpenAI 키가 필요하다. 프로덕션 탑재는 모델 라이선스·권리정책 결정 전 금지.</p></div></div>
|
||
<div class="task-row"><div><span class="task-status s-done">LOCAL DONE</span></div><div><b>barge-in / paralinguistic logging</b><p>음성 learner turn에 audio_ref, silence_ms, speech_rate, barge_in을 nullable 메타로 저장하고, 회기 리뷰에서 침묵·발화 속도·끼어듦·음성 입력 비언어 칩으로 표시한다. 2차로 <code>provider_events</code> JSONB 보존 슬롯을 추가해 control/STT provider 이벤트를 allowlist·size limit 후 저장하고, 3차로 내부 taxonomy <code>event_type</code>/<code>category</code>를 붙인다. 4차로 인증된 회기 리뷰에는 한숨·울음·웃음·호흡·운율·배경소음 같은 정규화 이벤트만 label/detail 칩으로 제한 노출한다. 실제 Session 마이크 UI는 browser voice activity/trailing silence 메타를 <code>audio_end</code>에 싣고, readiness는 음성 메타 5개 컬럼 전체를 확인한다. raw transcript/text/provider/source/raw type은 응답에서 제외하고, 공개 공유 카드는 축어록과 provider raw를 계속 싣지 않는다.</p></div><div><b>산출물</b><p><code>app.turns.provider_events</code>, <code>TurnRecord.provider_events</code>, voice route 이벤트 sanitizing/taxonomy, <code>ReviewNonverbalEvent</code>, <code>SessionReview</code> 칩 UI, browser voice metadata capture, voice metadata readiness guard</p></div><div><b>검증</b><p><code>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</code> 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 검증. <code>PLAYWRIGHT_PORT=5197 npx playwright test e2e/voice-success.spec.ts --project=chromium-single-run --workers=1</code> 2 passed, <code>PLAYWRIGHT_PORT=5199 npx playwright test e2e/session-persistence.spec.ts --project=chromium-single-run --workers=1</code> 5 passed, <code>npm run typecheck</code>.</p></div></div>
|
||
<div class="task-row"><div><span class="task-status s-plan">GATE</span></div><div><b>공개·실기기 음성 장시간 실측</b><p>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와 <code>local_whisper</code>/<code>melotts</code> exact ready, authenticated public WSS, 명시 동의 물리 마이크, 50분 양방향 soak, 독립 라벨 voice-gain benchmark, 실제 운영 topology resource high-water와 provider 기반 한숨·울음 표본이다. Deepgram/OpenAI는 fallback이며 mic-free preflight와 synthetic E2E/label은 외부 증거를 대체하지 않는다.</p></div><div><b>산출물</b><p>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</p></div><div><b>검증</b><p>실제 공개/실기기/운영 provider 환경에서만 완료 가능. 현재는 B2 음성 캐스케이드 live gate로 추적한다.</p></div></div>
|
||
<div class="task-row"><div><span class="task-status s-done">DECIDED</span></div><div><b>s2s 2차 PoC 판단</b><p>소유자 결정(2026-06-30): s2s 2차 PoC 유지. 캐스케이드 1차를 운영 기준으로 두고 <code>voice-s2s-poc.md</code> 비교 PoC를 계속 연다. 최종 채택/폐기는 비교 리포트 이후 재판단한다. 채택 여부 자체는 확정됐고, 비교 PoC 구현·실측이 후속 build로 남는다.</p></div><div><b>산출물</b><p><code>docs/decisions/voice-s2s-poc.md</code></p></div><div><b>검증</b><p>transcript, safety, audit, latency, failure, privacy 기준 명시. 실제 비교 실측은 후속.</p></div></div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="tab-panel" id="panel-p3" role="tabpanel" aria-labelledby="tab-p3" hidden>
|
||
<div class="detail-head"><div><h3>Phase 3 교육용 파일럿 / 효과성 상세</h3><p>IRB 승인 게이트가 아니라 교육 활용 파일럿과 데이터/개인정보 게이트로 관리한다.</p></div><div class="mini-metrics"><span>주차: 18-20+</span><span>표본: 20명</span><span>first measure</span></div></div>
|
||
<div class="task-list">
|
||
<div class="task-row"><div><span class="task-status s-plan">GATE</span></div><div><b>20명 교육용 파일럿 운영</b><p>20명 파일럿을 실행하기 위한 roster, consent, session completion evidence gate를 문서화했다. checker는 CSV enum(<code>withdrawal_state</code>, <code>completion_state</code>, <code>timepoint</code>, withdrawal <code>scope/status</code>)을 검증한다. 실제 참여자 완주 증거는 아직 필요하다.</p></div><div><b>산출물</b><p><code>docs/phase3/pilot-runbook.md</code>, evidence root layout, operator gates, <code>scripts/check-phase3-artifacts.py</code></p></div><div><b>검증</b><p><code>python scripts/check-phase3-artifacts.py --help</code>, <code>py -3.11 -X utf8 -m pytest -p no:cacheprovider app/test_dataset_export.py app/test_phase3_artifact_checker.py -q</code> 16 passed. 실제 evidence root validation은 파일럿 시점에 실행.</p></div></div>
|
||
<div class="task-row"><div><span class="task-status s-plan">GATE</span></div><div><b>효과성/KPI 측정</b><p>SUS, 자기효능감, κ/ICC, 환각률, 피드백 타당도 기준과 report schema를 고정했다. 3척도 pre/post aggregate score를 저장하는 <code>app.learner_prepost_measure</code>, <code>/users/me/prepost-measures</code> API, 학습자 <code>SessionReview</code> 입력 카드에 더해, <code>scripts/export-phase3-kpi.py</code>가 원장 row를 <code>prepost_measures.csv</code>와 <code>kpi_report.json</code> scaffold로 산출한다. checker는 KPI report top-level keys와 metric 필수 필드(<code>value/threshold/pass/status/method/source_files</code> 등)를 error로 검증한다. metric status는 <code>computed_prepost</code>/<code>design_pending</code>으로 제한해 계산 가능한 evidence와 평가설계 미확정 지표를 분리한다. 실제 수치 충족 여부와 통계 해석은 파일럿 데이터 이후 판단한다.</p></div><div><b>산출물</b><p><code>docs/phase3/kpi-measurement-plan.md</code>, KPI report schema, checker tests, <code>UserPrepostMeasure*</code> API contract, <code>PrepostMeasureCard</code>, <code>app/services/phase3_kpi_contract.py</code>, <code>app/services/phase3_kpi_export.py</code>, <code>scripts/export-phase3-kpi.py</code></p></div><div><b>검증</b><p>checker가 <code>prepost_measures</code>, <code>sus_responses</code>, <code>kpi_report.json</code> evidence와 metric shape/status를 요구. H1 KPI contract focused 10 passed, exporter/checker <code>--help</code> passed, 기존 API types/typecheck passed, SessionReview focused E2E 3 passed, DB-backed pre/post session-persistence E2E 7 passed, session review visual gate 2 passed.</p></div></div>
|
||
<div class="task-row"><div><span class="task-status s-done">DONE</span></div><div><b>재귀학습 데이터셋 1차 산출</b><p>masked-text JSONL dry-run exporter, PII scan, κ/ICC 계산, approved export 차단 게이트를 구현했다. 이번 패스에서 exporter 선별 조건과 artifact checker JSONL shape 검증을 보강해 동의 없는 학습자, evaluator-only 턴, raw supervisor comment, 빈 `{}` JSONL false-positive를 막았다. 실제 approved export와 데이터 steward 검수는 남아 있다.</p></div><div><b>산출물</b><p><code>scripts/export-recursive-dataset.py</code>, <code>app.services.dataset_export</code>, <code>docs/phase3/dataset-export-manifest.md</code>, <code>app/test_dataset_export.py</code>, <code>app/test_phase3_artifact_checker.py</code></p></div><div><b>검증</b><p><code>py -3.11 -X utf8 -m pytest -p no:cacheprovider app/test_dataset_export.py app/test_phase3_artifact_checker.py -q</code> 16 passed, exporter/checker py_compile 통과. approved/dry-run JSONL은 필수 key·row count·privacy·PII shape를 검사하고, approved manifest는 PII pass·κ/ICC·withdrawn exclusion·consent scope·file sha256을 요구.</p></div></div>
|
||
<div class="task-row"><div><span class="task-status s-plan">GATE</span></div><div><b>개인정보/동의 감사</b><p>동의, 철회, privacy audit, 해외 전송 고지 evidence checklist를 만들었다. 실제 법무/개인정보 검토 서명은 파일럿 전 gate로 남아 있다.</p></div><div><b>산출물</b><p><code>docs/phase3/privacy-consent-audit.md</code>, withdrawal log contract</p></div><div><b>검증</b><p>동의 철회 후 session/export 제외 evidence를 checker 입력으로 요구</p></div></div>
|
||
</div>
|
||
</div>
|
||
<div class="tab-panel" id="panel-ops" role="tabpanel" aria-labelledby="tab-ops" hidden>
|
||
<div class="detail-head"><div><h3>운영 / 검증 상세</h3><p>실제 운영 도메인에 반영됐는지, 서버가 하나로 정리됐는지, 완료라고 말할 증거가 있는지 확인하는 탭이다.</p></div><div class="mini-metrics"><span>웹: 200</span><span>API: recovered OK</span><span>Compose smoke: OK</span></div></div>
|
||
<div class="task-list">
|
||
<div class="task-row"><div><span class="task-status s-done">DONE</span></div><div><b>운영 웹 asset 확인</b><p>커스텀 도메인의 로그인 페이지는 계속 정상 응답한다. 2026-06-30 재확인 기준 로그인 HTML HEAD가 200이다.</p></div><div><b>산출물</b><p><code>https://vignette.chanpaca.net/login</code></p></div><div><b>검증</b><p><code>curl.exe -I https://vignette.chanpaca.net/login?dev_dashboard_redteam=20260630-recovered</code> → 200</p></div></div>
|
||
<div class="task-row"><div><span class="task-status s-done">DONE</span></div><div><b>공개/API 런타임 health</b><p>2026-06-30과 2026-07-01 17:26에 Docker/DB 중단으로 public API Cloudflare 530이 관측됐지만 현재 실측 기준으로 복구됐다. 2026-07-01 재확인에서 <code>https://api-vignette.chanpaca.net/health</code>는 <code>status=ok</code>, <code>db=true</code>, <code>engine=true</code>이고, <code>https://vignette.chanpaca.net/login</code>은 200, 비로그인 <code>GET /personas</code>는 401을 반환한다. 현재 남은 항목은 public OAuth 실제 계정 <code>/turn</code> smoke와 재부팅 후 watchdog 실측이다. 로컬 dirty worktree 변경은 아직 배포 전이다.</p></div><div><b>산출물</b><p><code>https://api-vignette.chanpaca.net/health</code> OK, <code>https://vignette.chanpaca.net/login</code> 200, unauth <code>/personas</code> 401</p></div></div>
|
||
<div class="task-row"><div><span class="task-status s-done">DONE</span></div><div><b>Tailscale Tailnet 런타임</b><p>로컬 web/api/engine을 Tailscale Serve로 묶어 같은 Tailnet PC·모바일에서 HTTPS로 접근한다. Vite proxy와 API 포트가 어긋나지 않도록 <code>dev-up.ps1</code>에서 <code>VITE_API_PROXY_TARGET</code>을 <code>ApiPort</code>와 동기화한다. Tailnet dev-origin에서는 공개 OAuth callback으로 세션이 섞이지 않게 dev-login을 기본 검증 경로로 둔다.</p></div><div><b>산출물</b><p><code>https://alpaca-home.taile93291.ts.net</code>, <code>scripts/start-tailscale-runtime.ps1</code>, <code>VITE_ALLOWED_HOSTS</code>, <code>vite.config.ts</code>, <code>auth.py</code></p></div><div><b>검증</b><p><code>/login</code> 200, <code>/api/auth/config</code> 200, <code>redirect_uri=https://api-vignette.chanpaca.net/auth/callback</code>, <code>dev_login_enabled:true</code>. Vite host-header block 재현 안 됨.</p></div></div>
|
||
<div class="task-row"><div><span class="task-status s-done">DONE</span></div><div><b>Docker compose 패키징 smoke</b><p>더미 운영 env 기준 fresh compose 기동을 통과했다. API 컨테이너 import 실패, 구현 없는 RAG 사이드카, 로컬 <code>apps/api/.env</code> 이미지 혼입, startup owner-DDL, 빈 <code>admin_engine_config</code> 행 때문에 prod가 죽던 경로를 수정했다. 루트 계산은 <code>app.paths.repo_root()</code>로 모았고, backend requirements는 exact pin으로 고정했다.</p></div><div><b>운영 주의</b><p>패키징은 OK지만 실제 이식 전에는 배포지 <code>infra/.env</code>에 <code>APP_DB_PASSWORD</code>, OAuth client id/secret, <code>OPENAI_API_KEY</code>, <code>SESSION_SECRET</code> 등 소유자 비밀값을 채워야 한다. <code>data/kb</code> live-coaching source pack은 commit 또는 같은 경로 provision이 필요하다. 현재 로컬 env의 <code>ENGINE_MODE=claude_cli</code>와 prod 금지 샘플 TTS 플래그는 운영값으로 쓰면 안 된다.</p></div><div><b>검증</b><p><code>docker compose -f infra/docker-compose.yml config --quiet</code>, API/Web image build, API image <code>.env*</code> 미포함, prod import smoke, fresh <code>docker compose up -d --build</code> → <code>/api/health</code> 200, preflight <code>OK requirements_pinned/live_coach_kb/env_file</code>.</p></div></div>
|
||
<div class="task-row"><div><span class="task-status s-risk">BLOCKED</span></div><div><b>공개 vnet.18ka.net 런타임</b><p><code>vnet.18ka.net</code>과 <code>api-vnet.18ka.net</code>을 정식 공개 도메인으로 허용하도록 코드 경로는 보강했다.</p></div><div><b>산출물</b><p><code>FRONTEND_ORIGIN_MAP</code>, 공개 host별 API 매핑, <code>scripts/start-public-runtime.ps1</code> vnet ingress 옵션</p></div><div><b>블로커</b><p>현재 DNS 미해결, Cloudflare credential의 18ka.net zone 권한 불일치, Google OAuth redirect URI 등록 필요.</p></div></div>
|
||
<div class="task-row"><div><span class="task-status s-done">DONE</span></div><div><b>서버 정리 상태</b><p>운영 API와 로컬/Tailnet 테스트 API를 구분해 유지한다.</p></div><div><b>산출물</b><p>8001 prod API, 8000 local API, 8010 Tailnet API, 5173 web, 9099 engine, 20241 cloudflared</p></div><div><b>검증</b><p><code>Get-NetTCPConnection</code> 대상 포트 확인</p></div></div>
|
||
<div class="task-row"><div><span class="task-status s-plan">GATE</span></div><div><b>재부팅 후 런타임 재기동</b><p>engine gateway, prod API, cloudflared를 health-check하고 복구하는 watchdog과 Scheduled Task installer는 준비됐다. 아직 DNS가 없는 <code>api-vnet.18ka.net</code>은 기본 watchdog 검사에서 제외해 불필요한 restart loop를 막고, 실제 개통 후 <code>-AdditionalPublicHealthUrls</code>로 명시 추가한다. 남은 것은 실제 Windows 재부팅 후 자동 복구와 authenticated public <code>/turn</code> smoke 증거다.</p></div><div><b>산출물</b><p><code>watch-public-runtime.ps1</code>, <code>install-public-runtime-task.ps1</code>, <code>docs/ops/public-runtime-watchdog.md</code></p></div><div><b>검증</b><p>PowerShell parser OK. 2026-06-30 복구 후 <code>powershell -NoProfile -ExecutionPolicy Bypass -File scripts\watch-public-runtime.ps1 -CheckOnly</code>는 healthy: engine, api, web-preview, cloudflared, public-api. 실제 Windows 재부팅은 아직 미실행.</p></div></div>
|
||
<div class="task-row"><div><span class="task-status s-done">DONE</span></div><div><b>레이아웃 감사팀 / 전면 레이아웃 개선</b><p>서브에이전트 전담으로 1차 overflow/clipping 수리 후 2차 전면 재설계를 진행했고, 2026-06-28 라운드에서 빈 회기리뷰까지 포함해 7개 권장 너비 시각 수용을 닫았다. 세션, 학습자 홈/리뷰, 교수 콘솔, 관리자, 설정 화면을 작업 화면 기준으로 재배치했고, 과한 빈칸·튀어나감·버튼/텍스트 잘림·모바일 테이블 사용성을 줄였다. 당시 엄격한 시각 게이트 + 화면별 적대적 시각 재검수에서 critical/major 0, 회귀 0을 받아 DONE으로 닫았다.</p></div><div><b>산출물</b><p><code>docs/archive/ops/layout-redesign-handoff-2026-06-26.md</code>, page-by-page redesign patches, <code>apps/web/e2e/layout-visual-gate.spec.ts</code></p></div><div><b>검증</b><p><code>npm run typecheck</code> OK, 최신 엄격 시각 게이트 9 passed, layout focused E2E desktop/mobile 54 passed, 2026-06-28 적대적 시각 재검수 7/7 accept·회귀 0.</p></div></div>
|
||
<div class="task-row"><div><span class="task-status s-done">DONE</span></div><div><b>엄격한 레이아웃 시각 게이트 구축 · 병렬 수정 · 적대적 재검수</b><p>핸드오프가 요구한 7개 권장 너비(390/720/861/900/1024/1280/1440) 시각 수용을 자동 게이트로 고정했다. 현재는 빈 회기리뷰 전용 화면까지 9개 화면을 각 너비에서 렌더링해 가로 overflow 0, 컨트롤 하드클립/텍스트클립 0, 다크 테마 캡처를 강제하고 화면당 7장 총 63장 풀페이지 스크린샷을 남긴다. 2026-06-28 당시 화면별 1에이전트 병렬 시각 리뷰로 49장 직접 판독 → 6개 병렬 수정 에이전트(파일 비중첩)가 결함 수정 → 화면별 적대적 재검수로 해소·무회귀를 확정했다.</p></div><div><b>산출물</b><p><code>apps/web/e2e/layout-visual-gate.spec.ts</code>, <code>node_modules/.tmp/layout-gate/*.png</code> 63장, 2× workflow(review/verify)·6× fix agent</p></div><div><b>검증</b><p>최신 게이트 9 passed(재스크린샷). 해소된 핵심 결함: 학습자홈 1280/1440 3열 가운데 컬럼 붕괴(critical → 2열 분기 상향 + word-break:keep-all), 교수 'API 404' raw 배너 제거, 리뷰 빈상태 위계 역전과 1280 sparse third-column, 관리자 스켈레톤, 설정 와이드 좌측 데드존 제거, 세션 모바일 44px 터치타깃. 2026-06-28 적대적 재검수 7/7 accept, regression 0.</p></div></div>
|
||
<div class="task-row"><div><span class="task-status s-done">DONE</span></div><div><b>레이아웃 cosmetic minor 폴리시 일괄 처리</b><p>적대적 재검수 잔여 cosmetic minor를 5개 병렬 폴리시 에이전트로 처리했다. 학습자홈 헬퍼문구 고아 글자, 세션 1024 일시정지 아이콘 정리 + 보내기 버튼 대비 강화, 설정 2x2 칩 행 높이 균일, 교수 검토 큐 카드 갭 제거, 관리자 2열 높이 동기화를 적용했다.</p></div><div><b>산출물</b><p>page-by-page polish patches(LearnerHome/Session/Settings/Professor/Admin)</p></div><div><b>검증</b><p><code>npm run typecheck</code> OK, 당시 cosmetic 시각 게이트 7 passed, 최신 통합 시각 게이트 9 passed, 레이아웃 E2E desktop/mobile 54 passed, session-layout 8/8 — 회귀 0.</p></div></div>
|
||
<div class="task-row"><div><span class="task-status s-done">DONE</span></div><div><b>회기 아카이브 저장/복원 API</b><p><code>/learn/history</code>의 <code>보관됨</code>을 실제 학습자별 저장 상태로 연결했다. 종료 회기는 <code>POST /sessions/{id}/archive</code>/<code>restore</code>로 보관·복원하고, <code>app.session_archive_state</code>는 보기 상태만 저장한다. 진행 중 회기는 보관하지 못하며, 보관은 transcript/review/share/audit evidence를 삭제하지 않는다.</p></div><div><b>산출물</b><p><code>SessionArchiveResponse</code>, <code>LearnerSessionSummary.archived</code>, <code>LearnerDashboardOverview.archived_sessions</code>, <code>session_archive_state</code> RLS, <code>LearnerHome</code> 보관/복원 버튼</p></div><div><b>검증</b><p><code>python -B -m pytest app/ -q</code> 178 passed, <code>python -B -m pytest engine_gateway/ -q</code> 11 passed, <code>npm run check:api-types</code>, <code>npm run typecheck</code>, <code>npm run build</code>, <code>npx playwright test e2e/learner.spec.ts --project=chromium-desktop --workers=1</code> 6 passed.</p></div></div>
|
||
<div class="task-row"><div><span class="task-status s-done">DONE</span></div><div><b>세션 종료 UX와 테마 SSOT</b><p>세션 하단의 드래그형 종료 컴포넌트를 명시 확인 다이얼로그로 바꿔 모바일 오발동과 제스처 실패를 줄였다. 테마 저장·초기화·OS 추종은 <code>lib/theme.ts</code> 단일 경로가 소유하며, 활성 세션도 전역 <code>data-theme</code>와 <code>tokens.css</code> 의미 토큰을 그대로 따른다. 세션 전용 다크 팔레트 재정의는 제거했고 active session 스테이지 상태 배지는 아바타와 겹치지 않는 3행 구조로 유지한다.</p></div><div><b>산출물</b><p><code>Session.tsx</code>, <code>AvatarPreview.tsx</code>, <code>session.css</code>, <code>full-sweep-session.spec.ts</code>, <code>session-layout.spec.ts</code></p></div><div><b>검증</b><p>라이트·다크 세션, 종료 UX, 짧은 높이·중간 폭 AI 코치 가시성, 3행 스테이지 구조를 시각 회귀 묶음으로 확인했다.</p></div></div>
|
||
<div class="task-row"><div><span class="task-status s-done">DONE</span></div><div><b>래스터 아바타 비활성화·SVG 리그 복귀</b><p>사용자 시각 피드백에 따라 서연 P1과 P4~P7의 생성 이미지/PSD 파츠 연결을 전부 제거했다. 제품·학습자 홈·세션·dev 미리보기는 기존 SVG 도형 기반 파라미터 리그만 렌더링한다.</p></div><div><b>산출물</b><p><code>ClientAvatar.tsx</code>, <code>persona.ts</code>, <code>Session.tsx</code>, <code>LearnerHome.tsx</code>, <code>AvatarPreview.tsx</code>, <code>avatar-expression.spec.ts</code></p></div><div><b>검증</b><p>typecheck/build 통과, 아바타·세션 desktop/mobile 18 passed, 전체 레이아웃 시각 게이트 12 passed, 커스텀 도메인 P1 공개 번들 desktop/mobile 2 passed. <code>data-render-mode="svg"</code>, 래스터 DOM 0개, SVG primitive·neck·표정 전환을 확인했다. 래스터 렌더러와 자산은 재검토용으로 삭제하지 않았다.</p></div></div>
|
||
<div class="task-row"><div><span class="task-status s-done">DONE</span></div><div><b>잔여(비차단) — 공용 셸 단일 항목 → 처리</b><p>축소 사이드바 세로 구분선이 본문 전체 높이까지 닿지 않던 건을 <code>components/shell/shell.css</code>에서 처리했다. <code>.vg-nav border-right</code> 제거 후 <code>.vg-shell__body</code> 컨테이너 배경 하어라인으로 본문 그리드 전체 높이 구분선을 그리고, 그리드·구분선 폭을 <code>--nav-cur</code>로 동기화. learner-home 로딩 스켈레톤 밀도도 실제 카드 구조 모사로 보강했다.</p></div><div><b>판정</b><p>구현 완료. 검증: <code>npm run typecheck</code> PASS + <code>vite build</code> PASS. 전 페이지 시각 회귀 게이트는 web+api+DB 스택으로 이 워크스테이션 미실행 — 스택 가용 시 1회 시각 확인 권장.</p></div></div>
|
||
<div class="task-row"><div><span class="task-status s-done">DONE</span></div><div><b>디자인 SSOT 리팩터링 · 인증 화면 복구</b><p>테마 알림은 <code>lib/theme.ts</code>/<code>lib/useTheme.ts</code>, 앱 크롬은 <code>AppShell</code>, 글래스 표면은 <code>Surface</code>가 각각 단독 소유한다. 로그인·온보딩·승인대기는 <code>AuthShell</code>의 100vw/100dvh 캔버스를 공유하고, 학습·세션·리뷰·교수자·관리자·페르소나·설정의 카드/인셋은 공통 Surface variant로 이관했다. <code>shell.css</code>의 페이지 클래스 나열과 <code>!important</code> 표면 덮어쓰기는 제거했다. 라이트 Surface도 2중 반투명 광원, 24px backdrop blur, 굴절 hairline을 공통 토큰으로 사용하고 모바일 셸의 보태니컬 배경을 유지한다.</p></div><div><b>검증</b><p><code>npm run check:design-ssot</code>, typecheck/build 통과. 인증 라이트·다크 × 390/1280 시각 게이트 1 passed, 14개 화면 × 7개 폭 레이아웃 게이트 14 passed, 학습·세션·리뷰 desktop/mobile 46 passed, 로그인→온보딩→학습 desktop/mobile 2 passed. 라이트 글래스의 gradient/blur/hairline/shadow와 모바일 배경 자산은 computed style 단언으로 고정했다.</p></div></div>
|
||
<div class="task-row"><div><span class="task-status s-done">DONE</span></div><div><b>로그인 전체 캔버스 테마 정합</b><p>라이트 테마에서 로그인 패널만 밝고 상담실 캔버스·좌측 브랜드는 다크 전용 값으로 남던 불일치를 수정했다. <code>AuthShell</code> room variant가 라이트에서는 아이보리 베일·다크 텍스트·밝은 정책 표면을, 다크에서는 차콜 베일·밝은 텍스트·어두운 정책 표면을 함께 제공한다.</p></div><div><b>검증</b><p><code>npm run check:design-ssot</code> 통과. <code>auth-visual.spec.ts</code>는 라이트/다크의 상담실 배경 변경과 좌측 브랜드·패널 텍스트 명도 계열을 computed style로 확인하며, 로그인·온보딩 × light/dark × 390/1280px 1 passed 및 생성 캡처 직접 확인. 전체 typecheck는 동시 작업 중인 <code>Admin.tsx</code>의 이름 불일치로 별도 차단.</p></div></div>
|
||
<div class="task-row"><div><span class="task-status s-done">DONE</span></div><div><b>전 저장소 리팩터 거버넌스 P1~P8</b><p>죽은 코드·긴 인자·계약 drift·캐시·중복 workflow·반복 IO·초기 번들·디자인 소유권을 전 영역에서 감사하고 증거가 있는 항목을 구조 개선했다. 평가/런타임/LLM 감사/알림/페르소나 시각 규칙을 단일 소유자로 모으고 대형 화면의 순수 모델과 음성 캡처를 분리했다. 최종 회귀에서 관리자 티켓 pending 소유권·대량 렌더 상한과 Settings 중복 로드 race까지 수정했다.</p></div><div><b>측정</b><p>생산 중복 0.60%→0.03%(15줄), 초기 JS gzip 195.75→77.13kB, 초기 CSS gzip 51.46→6.45kB. <code>check:dead-code</code>, <code>check:duplication</code>, <code>check:design-ssot</code>가 회귀를 막는다.</p></div><div><b>검증</b><p>API 400 passed, gateway 29 passed, type/API/design/dead-code/duplication/build/audit 통과. Playwright fixture 166/166 + DB/engine/provider 직렬 49/49, 합계 215/215 passed. 상세: <a href="ops/refactor-governance-2026-07-15.md">실행 기록</a>.</p></div></div>
|
||
</div>
|
||
<div class="tab-subhead"><h4>검증 실행 상세 표</h4><p>요약하지 않고 실행 범위, 명령, 결과를 행 단위로 남긴다.</p></div>
|
||
<table>
|
||
<thead><tr><th>범위</th><th>명령 / 확인 대상</th><th>결과</th></tr></thead>
|
||
<tbody>
|
||
<tr><td>Web typecheck</td><td><code>npm run typecheck</code></td><td>Passed</td></tr>
|
||
<tr><td>Design SSOT / auth visual</td><td><code>npm run check:design-ssot</code> / <code>npx playwright test e2e/auth-visual.spec.ts --project=chromium-single-run --reporter=line</code> / <code>npx playwright test e2e/layout-visual-gate.spec.ts --project=chromium-single-run --reporter=line</code></td><td>SSOT checker passed; login/onboarding light-dark desktop-mobile 1 passed; 14 core screens × 7 widths visual gate 14 passed.</td></tr>
|
||
<tr><td>Full Playwright E2E baseline</td><td><code>npm run e2e:parallel</code> / <code>npm run e2e:single-run</code> / <code>npm run e2e:list</code></td><td>2026-08-09 현재 수집은 <b>620 tests / 44 files</b>다. 현 작업트리의 620개 전체 GREEN은 아직 검증 전이며, current 핵심 fixture는 불변 110 + 최신 dashboard 10의 분할 GREEN이다. 이전 단일 120/120과 2026-07-15의 fixture desktop/mobile 166/166 + DB/engine/provider 직렬 49/49 = 215/215는 범위가 다른 역사 기준선으로 보존한다.</td></tr>
|
||
<tr><td>Refactor governance P1~P8</td><td><code>ruff check app</code> / <code>pytest -q app</code> / <code>pytest -q engine_gateway</code> / <code>npm run typecheck</code> / <code>npm run check:api-types</code> / <code>npm run check:design-ssot</code> / <code>npm run check:dead-code</code> / <code>npm run check:duplication</code> / <code>npm run build</code> / <code>npm audit --audit-level=high</code> / full Playwright</td><td>Backend 400 passed, gateway 29 passed, web gates/build/audit passed, vulnerabilities 0, production duplication 1 clone/15 lines/0.03%, Playwright 215/215 passed. 상세 근거는 <code>ops/refactor-governance-2026-07-15.md</code>.</td></tr>
|
||
<tr><td>API typegen SSOT</td><td><code>npm run check:api-types</code></td><td>Passed; FastAPI OpenAPI → <code>src/lib/api.gen.ts</code> stale check</td></tr>
|
||
<tr><td>Outcome & Alliance OS G0</td><td><code>py -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</code> / <code>scripts/check-measurement-ledger.sql</code> / measurement·API contract checks / web typecheck / DB-backed <code>session-persistence</code> focused E2E 3종</td><td>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 완료.</td></tr>
|
||
<tr><td>Outcome & Alliance OS G1</td><td><code>pytest test_alliance_measurement.py test_alliance_calibration.py test_measurement_routes.py test_runtime_schema_ssot.py test_measurement_contract.py</code> / <code>scripts/smoke-alliance-pulse-api.py</code> / <code>alliance-pulse.spec.ts</code> / <code>session-review.spec.ts</code> / prompt 1.2 calibration compare</td><td>backend 40 passed, live DB에서 202→awaiting_agents→ready·공개 전 외부 관점 비노출·3관점×3축·교수자 재평정 supersession·attempt별 model-run provenance를 확인했다. desktop/mobile Alliance Pulse 6/6와 기존 회기 리뷰 12/12 통과. 합성 gold 방향 9/9, 최종 실패 0, 근거 recall 88.9%. G1/AOS-005~010 완료.</td></tr>
|
||
<tr><td>Outcome & Alliance OS G2</td><td><code>pytest test_outcome_trajectory.py test_outcome_trajectory_schema.py test_outcome_trajectory_store.py</code> / <code>scripts/smoke-outcome-trajectory-api.py</code> / <code>scripts/smoke-outcome-trajectory-alerts.py</code> / <code>outcome-trajectory.spec.ts</code></td><td>focused 40 passed. 기존 실제 DB/API on-track 5회기·멱등/409·RLS에 더해 합성 교육 fixture 두 개가 production measurement producer→store→evaluator를 통과했다. 실제 10회기·30개 측정·case별 5 revision에서 악화는 S3 off_track·S4~S5 deteriorating, 단발성 noise는 S3 watch 뒤 회복해 alert 승격 0건이었다. 최신 observation 30개가 원본 측정 30개를 모두 참조하고 learner/teacher projection이 일치한다. 결과 JSON은 source score를 담지 않으며 임상 주장을 금지한다.</td></tr>
|
||
<tr><td>Outcome & Alliance OS G3</td><td><code>pytest test_rupture_scenario_director.py test_rupture_runtime.py test_rupture_repair.py test_rupture_repair_store.py</code> / <code>scripts/smoke-rupture-repair-api.py</code> / <code>scripts/smoke-rupture-runtime-auto.py</code> / <code>rupture-repair.spec.ts</code> / G1~G3 review regression</td><td>benchmark macro-F1 1.0·상태 accuracy 1.0·critical miss/judge gaming/암기 false resolution 0. 최신 G3 focused 70 passed; 누적 Scenario/G3 107 passed, G0~G8 계약 251 passed, desktop/mobile review 50/50. 수동 write API는 durable UUID·인증·멱등·409·RLS·교수자 correction을 검증했고, 별도 자동 runtime smoke는 실제 텍스트 learner turn/finalizer callback으로 3 episode·10 observations·3 reconciliations, missed/partial/resolved, learner/teacher projection, evidence 참조 무결성을 PostgreSQL에서 검증했다.</td></tr>
|
||
<tr><td>Outcome & Alliance OS G4/G5 caller</td><td><code>pytest test_session_learning_producer.py test_deliberate_practice.py test_deliberate_practice_store.py test_calibration_transfer.py test_calibration_transfer_store.py</code> / <code>scripts/smoke-session-learning-producer.py</code></td><td>focused 100 passed, 전체 API 855 passed. 실제 종료 회기·durable turn 2개·ready 평가에서 production caller가 G4 처방 원장 5종을 각 1개 생성하고 deterministic replay한다. G5는 prediction lock 전 observation 0, lock callback 뒤 failed 독립 관찰과 evaluator model-run 각 1개이며 replay 중복 0이다. learner/teacher projection이 일치하고 mastery/pass/assessment/transfer 자동 승격은 전부 0건이다. 합성 교육 fixture이며 임상·실제 숙달 주장이 아니다.</td></tr>
|
||
<tr><td>학생 자기주도 전체 루프 UI</td><td><code>npx playwright test e2e/self-directed-learning-loop.spec.ts --project=chromium-desktop --project=chromium-mobile --workers=1 --reporter=dot</code> / <code>npx playwright test e2e/alliance-pulse.spec.ts --project=chromium-desktop --project=chromium-mobile --workers=2</code> / <code>npm run typecheck</code> / 고정 캡처 직접 QA</td><td>실제 <code>src</code> 홈 추천→목표 선택→텍스트/SSE 회기→종료·리뷰→G4 처방 키보드 CTA→새 회기 재연습 시작이 desktop/mobile 2/2, typecheck를 통과했다. 페이지 overflow 0, 홈 CTA 44px·4.5:1 이상 대비, typed launch intent와 replay 모드 보존, 내부 criterion/counterevidence/UUID 비노출을 확인했다. 390×844·320×568 활성 회기는 아바타/문구 겹침 0과 축어록/입력창 내부 포함을 통과했다. 320px 리뷰 1~5 척도는 다섯 선택지 44px 이상·내부/페이지 overflow 0이고 전체 desktop/mobile 8/8이다. 모든 미소유 API는 fixture 404이며 실제 로그인/API/DB 증거가 아니다.</td></tr>
|
||
<tr><td>Contract SSOT aggregate DTO</td><td><code>py -3.11 -X utf8 -m pytest -p no:cacheprovider app/test_evaluation_persistence.py app/test_evaluator_model_routing.py app/test_teacher_dashboard.py app/test_rbac_idor.py app/test_session_turn_persistence.py -q</code> / <code>npm run check:api-types</code> / <code>npm run typecheck</code> / <code>npm run build</code></td><td>50 backend passed; learner sessions, session review/worksheet, teacher dashboard, session start/detail DTOs use generated <code>ApiSchema</code> aliases with UI fallback. Stage responses are OpenAPI enum unions, including review phase key/label, reached phase, evaluation summary/trigger responses, and teacher session/growth stage. <code>.github/workflows/api-contract.yml</code> runs <code>npm run check:api-types</code> on API/Web contract changes.</td></tr>
|
||
<tr><td>SEO/GEO share cards</td><td><code>pytest app/test_session_share.py app/test_session_turn_persistence.py -q</code> / <code>npm run generate:api-types</code> / <code>npm run typecheck</code></td><td>21 passed; session share creates hashed-token public unfurl payload without raw transcript, revoked token returns 404, OpenAPI generated share DTOs, review screen share button typechecks. Static <code>robots.txt</code>/<code>sitemap.xml</code>/<code>llms.txt</code> added.</td></tr>
|
||
<tr><td>Backend pytest baseline</td><td><code>pytest -q app</code></td><td>2026-07-31 전체 실행 439 passed. 외부 Starlette 의존성의 <code>python_multipart</code> PendingDeprecationWarning 1건만 존재한다.</td></tr>
|
||
<tr><td>X2 evaluator routing/cache/cost trend</td><td><code>python -X utf8 -m pytest -p no:cacheprovider app/test_llm_pricing.py app/test_admin_ops.py app/test_usage_report.py engine_gateway/test_provider_registry.py engine_gateway/test_gateway_model.py -q</code> / API type generation/check / web typecheck/build / admin focused E2E·layout visual gate / authenticated public API smoke</td><td>최신 backend 439 passed, gateway 45 passed, desktop/mobile E2E 2 passed, 7폭 focused visual gate 1 passed. Claude CLI SDK 비용 추정값과 Agy/Gemini·Codex·Claude API 공식 참조단가를 분리하고, 기존 0달러 행의 조회 시 보정·단가 미등록 모델의 <code>미산정</code> 표시·예산 합산·리포트 경고까지 고정했다. Claude 토큰은 전체 agent tree와 캐시 입력을 포함한다. 과거 0/0 Claude 442건은 로컬 JSONL의 실제 usage와 유일 일치한 169건만 백필했고, 273건은 <code>token_unmetered_turns</code>로 남겼다. 인증된 공개 관리자 화면은 확인 시점 30일 Claude 183건 중 계량 125·미계량 58건을 표시했다.</td></tr>
|
||
<tr><td>H3 raw source isolation</td><td><code>python -B -m pytest -p no:cacheprovider app/test_persona_review.py app/test_live_coach_sources.py -q</code></td><td>38 passed; persona source registration records raw hash-only artifacts separately from sanitized evaluator-only RAG chunks, and <code>rag.index_document()</code> rejects <code>sensitivity=3</code>/raw-marker chunks before DB access.</td></tr>
|
||
<tr><td>H3 persona itemized authoring UI</td><td><code>npm run typecheck</code> / <code>npm run build</code> / <code>npm run check:api-types</code> / <code>npx playwright test e2e/teacher.spec.ts --project=chromium-single-run --workers=1</code> / <code>npx playwright test e2e/layout-visual-gate.spec.ts --project=chromium-single-run --workers=1</code> / <code>npx playwright test e2e/session-layout.spec.ts --project=chromium-desktop --project=chromium-mobile --workers=1</code></td><td>Typecheck/build/API type drift check passed; teacher PersonaStudio focused 8 passed including structured list payload and labeled prompt preview without raw JSON; visual gate 9 passed with the prompt tab opened; session-layout desktop/mobile 8 passed.</td></tr>
|
||
<tr><td>H2 session review 3-column workbench</td><td><code>npm run typecheck</code> / <code>npm run build</code> / <code>npx playwright test e2e/session-review.spec.ts --project=chromium-desktop --workers=1</code> / <code>npx playwright test e2e/layout-visual-gate.spec.ts --project=chromium-single-run --workers=1</code></td><td>Typecheck/build passed; 3 focused review E2E passed; 9 visual gates passed with dark-theme assertion. Filled learner review uses overview/flow, transcript, and evaluation rail as a 3-column desktop workbench with worksheet below; empty review remains 2 columns or less to avoid sparse gaps.</td></tr>
|
||
<tr><td>M2 case memory</td><td><code>py_compile</code> / <code>py -3.11 -X utf8 scripts\run-session-digest-worker.py --help</code> / <code>py -3.11 -X utf8 -B -m pytest -p no:cacheprovider app/test_session_digest_worker.py app/test_session_memory.py -q</code> / <code>py -3.11 -X utf8 -B -m pytest -p no:cacheprovider app/test_session_memory.py -q</code> / <code>py -3.11 -X utf8 -m pytest -p no:cacheprovider app/test_session_memory.py app/test_session_turn_persistence.py app/test_orchestrator_masking.py app/test_runtime_policy.py -q</code> / <code>py -3.11 -X utf8 -m pytest app/test_orchestrator_masking.py app/test_session_turn_persistence.py app/test_rbac_idor.py app/test_evaluator_model_routing.py app/test_evaluation_persistence.py app/test_session_memory.py app/test_voice_ws.py app/test_voice_service.py app/test_learner_dashboard.py app/test_teacher_dashboard.py -q</code> / <code>node scripts\check-engine-gateway-contract.mjs --json</code></td><td>기존 74 backend passed 증거에 더해 TurnMemory 구조 정리 focused 115 passed, M2 local digest quality harness 20 passed, M2 digest worker + memory focused 30 passed, runner compile/help passed, M2 주변 회귀 87 passed, 최신 gateway contract 27 passed, Node conformance OK. session end writes masked fallback digest, case_digest, rapport trajectory, alliance EWMA, conservative identity/agreement pinned_fact upserts, append-only pinned_fact_history, explicit counseling-agreement withdrawal contradiction, episodic writer, next-session L2/L4 주입 proof, digest contract/local quality harness/accepted-only one-shot worker/runner/default-off scheduler/CAS 경계가 유지된다. <code>TurnMemory</code>는 recall/pinned/recent/KB cue를 <code>TurnContext.memory</code>로 묶고, <code>SessionEndResponse.digest_pending</code>은 여전히 compression-needed 신호이며 실 provider 운영 완료가 아니다.</td></tr>
|
||
<tr><td>L1 session read-model split</td><td><code>python -B -m py_compile app/session_read_model.py app/routes/sessions.py</code> / <code>python -B -m pytest -p no:cacheprovider app/test_session_turn_persistence.py app/test_evaluation_persistence.py app/test_session_share.py app/test_learner_dashboard.py app/test_rbac_idor.py app/test_teacher_dashboard.py -q</code> / <code>npm run check:api-types</code> / <code>npm run typecheck</code></td><td>py_compile passed; focused backend 48 passed; OpenAPI generated API type check and web typecheck passed. <code>app/session_read_model.py</code> now owns session list/dashboard/detail/review/share DTOs and deterministic builders while <code>routes/sessions.py</code> keeps auth, RLS-backed reads, persistence, and route decorators.</td></tr>
|
||
<tr><td>L1 persona read/generation split</td><td><code>py -3.11 -X utf8 -B -m py_compile app/persona_generation_contract.py app/persona_read_model.py app/routes/personas.py app/test_persona_generation_contract.py app/test_persona_review.py</code> / <code>py -3.11 -X utf8 -B -m pytest -p no:cacheprovider app/test_persona_generation_contract.py app/test_persona_review.py -q</code> / <code>npm run check:api-types</code> / <code>npm run typecheck</code></td><td>py_compile passed; persona contract+review 39 passed; OpenAPI generated API type check and web typecheck passed. <code>app/persona_read_model.py</code> owns persona catalog/review/draft/source/evidence DTOs and mappers, while <code>app/persona_generation_contract.py</code> owns draft schema, prompt bundle, <code>GenerateResponse</code> payload extraction, and generated draft defaults/coercion. <code>routes/personas.py</code> keeps auth, teacher/admin gates, DB repository calls, RAG source registration, engine invocation, provenance assembly, and HTTP error mapping.</td></tr>
|
||
<tr><td>M1 provider_events preservation</td><td><code>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</code></td><td>77 passed; voice control/STT provider events are allowlisted, size-limited, and tagged with internal <code>event_type</code>/<code>category</code> taxonomy into <code>app.turns.provider_events</code>. 인증된 review <code>nonverbal</code>은 정규화 taxonomy 일부만 파생 칩으로 노출하며 raw transcript/text/provider/source/raw type은 응답하지 않는다. Runtime readiness now requires all five turn voice metadata columns.</td></tr>
|
||
<tr><td>C1 case worksheet</td><td><code>py -3.11 -X utf8 -m pytest -p no:cacheprovider app/test_session_turn_persistence.py app/test_teacher_dashboard.py app/test_rbac_idor.py app/test_case_worksheet_rubric.py -q</code> / <code>scripts/check-case-worksheet-rubric.py --json</code> / <code>PLAYWRIGHT_PORT=5228 npx playwright test e2e/session-persistence.spec.ts --project=chromium-single-run --workers=1 --grep "case worksheet"</code> / local API smoke</td><td>40 passed for worksheet persistence/RBAC/teacher review plus rubric scaffold; session review exposes transcript-grounded <code>caseWorksheet</code>, learner-edited <code>saved_by_learner</code> worksheet persists through <code>app.case_worksheet</code>, teacher/admin <code>teacherReview</code> exposes manual <code>worksheetStatus</code>/<code>worksheetNote</code>/<code>worksheetReviewedAt</code>, and full browser+API+DB E2E now verifies learner save → teacher read-only review → <code>수정요청</code> persistence. <code>case-worksheet-rubric@scaffold-2026-06-28</code> still validates 5 sections/28 items with <code>scoring_enabled=false</code>.</td></tr>
|
||
<tr><td>M3 auth claim mapping</td><td><code>python -B -m pytest -p no:cacheprovider app/test_auth_providers.py -q</code></td><td>30 passed; Google/SAML cohort maps, provider external_id, provider error reason, PKCE/state, HttpOnly cookie, SAML fixture ACS covered</td></tr>
|
||
<tr><td>Auth managed-user upsert boundary</td><td><code>py_compile</code> / <code>py -3.11 -X utf8 -B -m pytest -p no:cacheprovider app/test_auth_providers.py app/test_admin_ops.py app/test_rbac_idor.py app/test_runtime_policy.py app/test_session_turn_persistence.py app/test_learner_dashboard.py app/test_teacher_dashboard.py -q</code> / <code>npm run check:api-types</code></td><td><code>ManagedUserUpsertInput</code> owns managed-user create/reactivate inputs; <code>create_session()</code>, admin user create, and direct auth regression calls no longer pass long keyword bags. <code>ManagedUserMemoryInput</code> remains DB→memory fallback/store sync, and <code>ManagedUserPatch</code> remains partial profile/admin update. Focused backend 103 passed; API type drift check passed.</td></tr>
|
||
<tr><td>CrisisGate 109</td><td><code>python -m pytest app/test_session_turn_persistence.py app/test_voice_ws.py -q</code> / <code>PLAYWRIGHT_PORT=5228 npx playwright test e2e/session-persistence.spec.ts --project=chromium-single-run --workers=1 --grep "crisis safety"</code> / <code>PLAYWRIGHT_PORT=5244 npx playwright test e2e/session-persistence.spec.ts --project=chromium-single-run --workers=1 --grep "crisis safety event"</code></td><td>real crisis stops before engine, returns 109 resource, and DB insert uses <code>ai_context=true</code>. <code>db.healthcheck()</code> now requires <code>app.safety_events</code>; alert detail handles legacy double-encoded JSON string rows; full browser+API+DB E2E verifies <code>safety_events</code> reaches the DB-backed teacher safety queue with resource 109. 최신 E2E는 위기 발화가 교수자 검토용 learner turn으로 저장되고 client AI reply turn은 저장되지 않는지도 확인한다.</td></tr>
|
||
<tr><td>Professor review workflow</td><td><code>python -X utf8 -m pytest -p no:cacheprovider app/test_teacher_dashboard.py app/test_rbac_idor.py::TeacherAdminAuditTest::test_teacher_list_recent_sessions_inserts_read_audit_log app/test_rbac_idor.py::TeacherAdminAuditTest::test_admin_load_session_inserts_read_audit_log -q</code> / <code>PLAYWRIGHT_BASE_URL=http://localhost:5210 npx playwright test e2e/teacher.spec.ts --project=chromium-single-run --grep "shows selected learner analysis"</code> / <code>PLAYWRIGHT_BASE_URL=http://localhost:5210 npx playwright test e2e/layout-visual-gate.spec.ts --project=chromium-single-run --grep "professor (student analysis overview|learner detail analysis)"</code> / <code>npm run check:api-types</code> / <code>npm run typecheck</code> / <code>npm run build</code></td><td>10 teacher-dashboard backend passed + student-analysis E2E 1 passed + strict visual focused gate 2 passed; <code>/teach</code> keeps triage queues, and <code>/teach/analysis</code> starts with a searchable one-row-per-learner table using <code>app_user</code> display labels, right-side row expansion, and <code>?learner=</code> detail. Teacher session summaries now expose AI <code>supervisor_state</code>/<code>evaluation_error</code> separately from manual review status, so failed AI evaluations remain visible in professor queues. The learner detail default tab is now persona-by-persona drilldown: each practiced persona summarizes its sessions, score, rapport, review count, and expands into that persona's session rows before the full timeline/stage tabs.</td></tr>
|
||
<tr><td>Professor student list cap</td><td><code>C:\Users\encep\AppData\Local\Programs\Python\Python311\python.exe -X utf8 -m pytest -p no:cacheprovider app/test_teacher_dashboard.py -q</code></td><td>10 passed; teacher dashboard API now calls <code>session_persistence.list_all_sessions()</code> while learner/general lists call <code>list_recent_sessions()</code>, so the newest 100 sessions from one learner cannot hide the rest of the student analysis list. <code>total_learners</code> and <code>learner_growth</code> stay on the same full learner set.</td></tr>
|
||
<tr><td>Learner/teacher growth metrics</td><td><code>py -3.11 -X utf8 -B -m pytest -p no:cacheprovider app/test_evaluation_persistence.py app/test_evaluator_model_routing.py app/test_session_turn_persistence.py app/test_rbac_idor.py app/test_teacher_dashboard.py app/test_learner_dashboard.py -q</code></td><td>59 backend passed; shared <code>session_metrics</code> now prefers rehydrated <code>label_ko</code> for technique labels while preserving <code>label/name/id/code</code> fallback. <code>alternative_utterances</code> string and <code>text/suggestion</code> dict inputs are pinned before any future evaluator adapter extraction.</td></tr>
|
||
<tr><td>Teacher review status</td><td><code>pytest app/test_teacher_dashboard.py app/test_rbac_idor.py app/test_learner_dashboard.py -q</code> / <code>npm run typecheck</code> / <code>npm run build</code> / <code>teacher.spec.ts</code></td><td>최신 focused teacher dashboard 11 passed. <code>app.session_review_status</code>, teacher dashboard <code>review_status/review_note/reviewed_at</code>, worksheet <code>worksheet_status/worksheet_note/worksheet_reviewed_at</code>, and <code>PUT /teacher/sessions/{session_id}/review-status</code> are documented. 검토 큐 visible 회귀와 교수자 전용 review grid 영역도 테스트로 고정했다. 124차에서는 턴이 0개인 종료 세션을 pending queue에서 제외해 <code>reviewReady=false</code> 세션이 검토 대기처럼 보이는 mismatch를 막았다.</td></tr>
|
||
<tr><td>Persona authoring + generation contract</td><td><code>py -3.11 -X utf8 -B -m pytest -p no:cacheprovider app/test_persona_generation_contract.py app/test_persona_review.py -q</code></td><td>39 passed; teacher/admin draft create/read/update/review submit, approved persona revision clone, code-family archive delete, learner blocked, approved-only catalog maintained. <code>app/persona_generation_contract.py</code> owns draft structured schema, prompt bundle id/version/hash, legacy <code>GenerateResponse</code> payload extraction, and generated draft defaults/coercion.</td></tr>
|
||
<tr><td>Repo source pack sync</td><td><code>py -3.11 -X utf8 -m pytest -p no:cacheprovider app/test_live_coach_sources.py -q</code> / <code>PLAYWRIGHT_PORT=5255 npx playwright test e2e/kb-source-packs.spec.ts --project=chromium-single-run --workers=1</code> / <code>py -3.11 -X utf8 scripts\sync-persona-sources.py --help</code></td><td>9 backend passed + latest full browser/API/DB E2E 1 passed; dry-run reports next version without writes, apply bumps changed hash to latest+1, identical hash skips indexing, CLI exposes DB-backed dry-run/apply modes, and admin/CLI sync boundaries refresh the process-local live-coach source pack cache before comparing repo files. E2E verifies unauth 401, learner 403, admin 202, four licensed live-coach source ids, non-empty-or-unchanged indexed items, and source-scoped evaluator retrieval 200; retrieval 503 is now a failing test, not skipped evidence.</td></tr>
|
||
<tr><td>P1 cost telemetry</td><td><code>app.test_session_turn_persistence.SessionTurnPersistenceTest</code></td><td>Passed; generate/stream client AI turns store provider/model/tokens/cost</td></tr>
|
||
<tr><td>Engine stream contract</td><td><code>node scripts/check-engine-gateway-contract.mjs --json</code> / <code>py -3.11 -X utf8 -B -m pytest -p no:cacheprovider engine_gateway/test_gateway_model.py -q</code> / <code>py -3.11 -X utf8 -B -m pytest -p no:cacheprovider engine_gateway/test_gateway_model.py app/test_evaluation_persistence.py app/test_evaluator_model_routing.py app/test_session_turn_persistence.py app/test_live_coach_sources.py app/test_persona_generation_contract.py app/test_persona_review.py -q</code></td><td>Node conformance runner OK, gateway contract 27 passed, backend focused 120 passed; <code>EngineClient.stream_packets()</code> owns raw SSE decode, <code>EngineGatewaySseLineDecoder</code> parses token/done/error from the shared contract, keeps legacy token text compatibility, treats provider <code>[DONE]</code> as an ignored compatibility line rather than completion, rejects unknown gateway events, validates schema-backed <code>engine_gateway_contract.v1.json</code>, and gateway error event does not create saved turns. Node runner는 <code>StreamPacket.oneOf</code>, payload schema <code>$ref</code>, <code>x-engine-gateway-sse-events</code>, <code>x-engine-gateway-default-model-sentinel</code>에서 stream/default-routing 계약을 읽는다. <code>gateway-default</code> request sentinel과 response/done resolved model 분리, <code>GatewayPromptParts</code> current-turn prompt split, user 없는 generate/stream 400 경계, <code>GenerateResponse</code> structured payload fallback, persona draft generation contract, <code>SessionEvaluationWrite.from_result()</code>/<code>from_error()</code> packet factory는 계약 모듈과 Node runner/focused tests가 함께 검증한다.</td></tr>
|
||
<tr><td>Engine session reuse</td><td><code>engine_gateway.test_gateway_model</code></td><td>27 tests OK; shared engine contract, JSON Schema + golden fixture validation, Node.js artifact conformance runner, <code>GenerateResponse</code> response validation, structured payload fallback parser, current-turn prompt split, direct evaluator/live-coach parser ownership, SSE frames/decoder, live session_id reuse, <code>gateway-default</code> 기본 라우팅 sentinel 정규화, missing-user 400 guard, ephemeral close fixed</td></tr>
|
||
<tr><td>P1/H4 masking gate</td><td><code>app/test_pii_masking_eval.py app/test_orchestrator_masking.py app/test_evaluation_persistence.py app/test_session_turn_persistence.py</code> / <code>PLAYWRIGHT_PORT=5244 npx playwright test e2e/session-persistence.spec.ts --project=chromium-single-run --workers=1 --grep "Korean PII"</code></td><td>47 passed; 최신 보정 회귀 59 passed + DB-backed browser PII E2E 1 passed. phone/email/RRN 및 한국어 NAME/ORG raw 값이 generate/stream/evaluator payload와 client <code>text_masked</code>에 남지 않음. Optional ko recognizer fake span은 <code>[NAME]</code>/<code>[ORG]</code>로 마스킹되고 같은 문장의 phone은 후단 regex가 처리하며, adapter 실패 시에도 regex fallback이 유지된다. Synthetic ko fixture 16/16 pass, 자연 발화형 이름 라벨·자기소개와 negative control 포함, input/report schema validation, summary-only report(<code>evidence_text_included=false</code>), entity recall 1.0, forbidden substring removal 1.0, unexpected entity violations 0. 최신 브라우저 stream E2E는 한국어 이름·기관·전화번호 발화가 DB-backed <code>GET /sessions/{id}</code> 상세와 <code>/review</code> 모두에서 raw 값 없이 <code>[NAME]</code>/<code>[ORG]</code>/<code>[PHONE]</code>으로 남는지 확인한다.</td></tr>
|
||
<tr><td>P2a RBAC/audit/visibility</td><td><code>app.test_rbac_idor</code></td><td>11 tests OK; other learner 403, read_session audit, evaluator-only hidden, teacher session review read allowed, super_admin learner review principal is promoted to admin for evaluation/worksheet/review-status loaders, admin_access-only learner remains blocked, and learner worksheet write remains 403</td></tr>
|
||
<tr><td>Admin access delegation</td><td><code>C:\Users\encep\AppData\Local\Programs\Python\Python311\python.exe -X utf8 -m unittest app.test_auth_providers app.test_admin_ops</code> / <code>npx playwright test e2e/admin.spec.ts --project=chromium-desktop --workers=1 --grep "restores every admin page|keeps the session recoverable"</code> / <code>npm run typecheck</code></td><td>2026-07-30 auth/admin 50 passed + 관리자 재구동 focused E2E 2 passed + web typecheck/build passed. 실제 <code>admin</code> role은 <code>admin_access=false</code> 세션이어도 관리자 API를 열고, super-admin과 일반 역할+<code>admin_access</code> 세션도 관리자 5개 경로와 sidebar를 모두 복원한다. nullable 관리자 권한을 저장하는 PostgreSQL 인자는 <code>::boolean</code>으로 고정해 OAuth upsert prepare 오류를 회귀 방지한다. 최초 <code>/auth/me</code>의 네트워크/5xx 실패는 3회 재시도와 명시 복구 화면으로 처리하며 <code>401</code>만 로그아웃으로 확정한다.</td></tr>
|
||
<tr><td>SPA route/restored-tab/admin entitlement</td><td><code>admin.spec.ts</code> desktop/mobile / <code>public-admin-visual.spec.ts</code> / Pages deploy</td><td>document가 아니라 실제 스크롤 컨테이너인 <code>.vg-main</code>을 <code>AppShell</code>이 mount·pathname·<code>pageshow</code>에서 0으로 복원하고, 설정 기반 관리자 entitlement를 DB에 영속화한다. primary learner/teacher 화면에도 <code>운영 콘솔</code> 진입점을 유지한다. 2026-07-30 custom domain은 <code>assets/index-PqLVP802.js</code>를 서빙하며 정확한 내부 스크롤 RED 2 failed→GREEN 2 passed, 로컬 관리자 복원·5경로 desktop/mobile 8 passed, 실제 Google 슈퍼 관리자 사용자 행 기반 공개 관리자 5경로·운영 콘솔 복귀·내부 scrollTop 900 복원 탭 4 passed와 실제 픽셀 캡처를 확인했다.</td></tr>
|
||
<tr><td>Admin silent-blank diagnostics</td><td><code>npm run check:cosmetic-filter-safety</code> / <code>npm run build</code> / <code>admin.spec.ts</code> EasyList regression / <code>public-admin-visual.spec.ts</code></td><td>관리자 payload 정규화·route error boundary·0-height 진단에 더해 공식 EasyList의 <code>.ad-root</code>/<code>.ad-section</code> 충돌을 RED로 고정했다. 관리자 DOM/CSS는 <code>vgops-*</code> namespace와 <code>data-vignette-admin-*</code> marker를 쓰며, HTML boot watchdog은 marker 존재가 아니라 실제 rect/computed visibility를 검사한다. 위험 namespace 정적 검사는 <code>build</code>/<code>lint</code> 앞단에서 재유입을 차단하고 탐지기 자체 fixture도 매번 검증한다. Typecheck/build, detector 81파일, 강화된 로컬 EasyList 1/1, route guards 12/12, admin full-sweep 20/20, admin layout 3/3, session-layout 8/8 통과. production <code>0a339b77</code>에서 실제 슈퍼 관리자 임시 세션과 EasyList 규칙을 함께 사용해 관리자 5경로 visible, legacy <code>ad-*</code> 0, 진단 오버레이 0, 브라우저 오류 0을 확인했다. 동일 런타임을 새 build gate로 통과시킨 production <code>1b3b6d5b</code>의 공개 Admin asset도 격리 산출물과 SHA-256이 일치한다.</td></tr>
|
||
<tr><td>OAuth/SAML admin landing</td><td><code>C:\Users\encep\AppData\Local\Programs\Python\Python311\python.exe -X utf8 -m pytest -p no:cacheprovider app/test_auth_providers.py -q</code> / public runtime restart</td><td>36 passed; OAuth/SAML callback now normalizes generic saved <code>next</code> paths such as <code>/learn</code> and <code>/teach</code> to <code>/admin</code> for admin-entitled users while preserving deep links such as <code>/learn/session/...</code>. Live proof after commits <code>998acb44</code>/<code>6b6241f4</code> and latest boot-diagnostic deploy <code>b67fd5d4</code>: local/public <code>/health</code> returned <code>prod</code>, <code>db=true</code>, <code>engine=true</code>; custom domain now serves <code>assets/index-R5KK7hZI.js</code>/<code>assets/index-DzdAkRsn.css</code> plus boot diagnostic HTML; unauthenticated <code>/personas</code> remains <code>401</code>.</td></tr>
|
||
<tr><td>Session read-model DB readiness</td><td><code>python scripts\check-deploy-preflight.py --skip-db --env-file infra\.env.example --allow-placeholder-secrets</code> / <code>https://api-vignette.chanpaca.net/health</code></td><td>2026-06-29 prod 503 원인은 운영 DB의 <code>app.turns.provider_events</code> 컬럼 누락이었다. 운영 DB hotfix 후 <code>/teacher/dashboard</code> code path는 <code>source=database</code>로 복구됐다. 현재 <code>db.healthcheck()</code>, runtime table readiness, deploy preflight DB mode는 <code>app.turns</code> 음성 메타 컬럼 5개(<code>audio_ref</code>, <code>silence_ms</code>, <code>speech_rate</code>, <code>barge_in</code>, <code>provider_events</code>), <code>app.session_review_status</code> worksheet 컬럼, <code>app.safety_events</code> 필수 컬럼을 함께 검증한다. 2026-06-30에는 Docker Desktop/DB 중단으로 public API 530/error code 1033이 재발했지만, Docker Desktop/DB 재기동 뒤 public health가 <code>environment=prod</code>, <code>db=true</code>, <code>engine=true</code>로 복구됐다.</td></tr>
|
||
<tr><td>Admin usage persistence</td><td><code>python -m pytest app/test_admin_ops.py app/test_runtime_policy.py -q</code> / authenticated local public-API smoke</td><td>26 passed; <code>/admin/usage</code> returns 200 with <code>source=database</code>, <code>durable=true</code>. Cloudflare blocked raw Python public smoke with 1010, so app-level HTTP was verified against <code>127.0.0.1:8001</code> using the same prod process.</td></tr>
|
||
<tr><td>Admin AI operations subpage</td><td><code>npx playwright test e2e/admin.spec.ts --project=chromium-desktop --project=chromium-mobile --workers=1 --grep="shows detailed AI metering"</code> / <code>npx playwright test e2e/layout-visual-gate.spec.ts --project=chromium-single-run --workers=1</code> / <code>public-admin-visual.spec.ts --grep="shows the live Codex and Agy model catalogs"</code></td><td><code>/admin/ai</code>는 7·30·90일 DB 원장, 비용·토큰·토큰 계량 커버리지·호출당 단가, 일별 비용, provider/model breakdown, evaluator cache를 표시하고 엔진 설정을 실제 저장한다. 비용은 <code>SDK 추정</code>·<code>Provider 보고</code>·<code>참조단가</code>·<code>미산정</code>을 구분한다. 토큰 입력은 비캐시·캐시 읽기·캐시 생성 합계이며, 실제 usage가 복구된 행은 토큰과 <code>계량/전체 회</code> 커버리지를, 복구 불가 0/0 행은 <code>미계량</code>을 표시한다. 최신 focused desktop/mobile 2 passed, 7폭 layout 1 passed, 실제 공개 관리자 브라우저 1 passed다.</td></tr>
|
||
<tr><td>Synthetic health sampler</td><td><code>record_admin_health_sample()</code>, <code>record-admin-health-sample.py</code>, <code>install-health-sampler-task.ps1</code></td><td>Backend focused 31 passed. Python compile/help passed; PowerShell parser + <code>-PrintOnly</code> passed. Local one-shot appended 5 service rows to <code>app.admin_health_event</code>: status ok, engine_mode claude_cli. This remains sample history, not an SLA claim.</td></tr>
|
||
<tr><td>Health retention/rollup</td><td><code>admin_health_daily_rollup</code>, <code>maintain-admin-health-events.py</code>, <code>/admin/uptime</code></td><td>Backend focused 36 passed. Python compile/help passed. Local dry-run returned <code>rollup_event_count=0</code>, <code>prunable_event_count=0</code>. Uptime summary now combines raw samples with daily rollups when raw rows have been pruned; detailed events stay raw-only.</td></tr>
|
||
<tr><td>Ticket duplicate linking</td><td><code>support_ticket.fingerprint</code>, <code>parent_ticket_id</code>, <code>/admin/tickets</code></td><td>Backend focused 38 passed. API types, web typecheck, and build passed. Admin tickets E2E 2 passed after restarting local API 8000 on the new code. Duplicate handling is deterministic hint + admin manual parent link/unlink only; no automatic merge, escalation, issue, PR, or thread creation.</td></tr>
|
||
<tr><td>User support ticket self-view</td><td><code>app/test_user_support_tickets.py</code>, <code>app/test_admin_ops.py</code>, <code>app/test_runtime_policy.py</code>, <code>settings.spec.ts</code>, <code>admin.spec.ts --grep "operation tickets"</code></td><td>Backend 29 passed; <code>/users/support-tickets</code> filters by current <code>reporter_id</code> and excludes ticket body/reporter PII. API types, typecheck, build passed. Settings E2E 7 passed; admin operation ticket E2E 1 passed.</td></tr>
|
||
<tr><td>Turn evaluation SSOP</td><td><code>pytest app/test_session_turn_persistence.py app/test_evaluation_persistence.py app/test_voice_ws.py</code> / <code>py -3.11 -X utf8 -m pytest -p no:cacheprovider app/test_session_turn_persistence.py -k "fast_loop_evaluation" -q</code> / <code>py -3.11 -X utf8 -m pytest -p no:cacheprovider app/test_learner_dashboard.py -k "failed_fast_loop" -q</code> / <code>py -3.11 -X utf8 -m pytest -p no:cacheprovider app/test_eval_routes.py app/test_evaluation_persistence.py -q</code> / <code>PLAYWRIGHT_PORT=5246 npx playwright test e2e/session-persistence.spec.ts --project=chromium-single-run --workers=1 --grep "teacher turn reevaluation"</code></td><td>20 tests OK baseline; submit/stream/voice fast-loop evaluation normalized mapping + evaluator-only raw RLS + turn insert id 확인. 추가 focused 2 passed + 1 passed는 stream 평가 훅 예외를 <code>evaluation.error</code>로 남겨 리뷰 <code>턴 평가 실패</code> 노트로 표면화하고, error fast-loop 결과를 성장 점수와 최근 피드백에서 제외해 neutral 점수처럼 집계하지 않는지 검증한다. 최신 backend 29 passed + DB-backed E2E 1 passed는 교수자 <code>POST /eval/sessions/{id}/turn</code> 결과가 응답 바디에만 머물지 않고 normalized evaluation row를 교체 저장해 <code>/review</code> learner turn technique으로 hydrate되는지 검증한다.</td></tr>
|
||
<tr><td>Postgres RLS/audit smoke checker</td><td><code>VIGNETTE_RLS_AUDIT_DSN=…vignette_app… check-postgres-rls-audit.py --write-fixtures</code></td><td>live DSN 5 checks PASS. help-only safe default 검증은 과거 준비 단계이며 live proof로 superseded.</td></tr>
|
||
<tr><td>OAuth/BFF cookie security</td><td><code>python -B -m pytest -p no:cacheprovider app/test_auth_providers.py -q</code></td><td>30 tests OK; PKCE/state, cookie-bound signed state restart recovery, state cookie mismatch reject, opaque HttpOnly/Secure cookie, no browser tokens, SAML AuthnRequest/dev-only fixture ACS, dev/Tailnet OAuth guard, public-origin dev-login closed</td></tr>
|
||
<tr><td>H4 consent gate</td><td><code>py -3.11 -X utf8 -m pytest -p no:cacheprovider app/test_voice_ws.py -q</code> / <code>PLAYWRIGHT_PORT=5231 npx playwright test e2e/voice.spec.ts --project=chromium-single-run --workers=1 --grep "consent withdrawal"</code></td><td>16 backend passed + browser WS 1 passed; voice binding now checks onboarding/consent before both existing <code>session_id</code> and dev <code>persona_code</code> paths. 동의 철회 뒤 기존 voice session id 재접속도 <code>consent_required</code>로 닫히며, 같은 suite가 voice turn 저장 실패 구조화 error도 검증한다.</td></tr>
|
||
<tr><td>H1 pre/post evidence API + KPI export</td><td><code>app/test_user_support_tickets.py</code>, <code>app/test_runtime_policy.py</code>, <code>app/test_admin_ops.py</code>, <code>app/test_phase3_kpi_export.py</code>, <code>app/test_phase3_artifact_checker.py</code>, <code>npm run check:api-types</code>, <code>npm run typecheck</code>, <code>e2e/session-review.spec.ts</code>, <code>e2e/session-persistence.spec.ts</code></td><td>Current H1 broad 46 passed; KPI contract focused 10 passed. <code>app.learner_prepost_measure</code> stores learner-scoped aggregate pre/post scores for self-efficacy, skill proficiency, and training satisfaction. <code>scripts/export-phase3-kpi.py</code> emits pseudonymous <code>prepost_measures.csv</code> and KPI report scaffold with paired normalized deltas. <code>phase3_kpi_contract.py</code> owns metric names, required keys, and <code>computed_prepost</code>/<code>design_pending</code> status values used by exporter/checker/tests. Generated API contract exposes <code>UserPrepostMeasure*</code>. 학습자 <code>SessionReview</code>는 1~5 aggregate score 조회/저장 카드를 표시하고, 기존 저장 점수를 비우는 입력은 invalid로 표면화해 <code>저장된 값 기준</code>으로 오인하지 않게 한다. 최신 route-fixture pre/post focused E2E 1 passed, DB-backed pre/post 저장·재조회 E2E까지 포함한 session-persistence 7 passed + visual gate 2 passed.</td></tr>
|
||
<tr><td>Persona review workflow</td><td><code>app.test_persona_review</code></td><td>31 tests OK; approved-only catalog, teacher/admin queue, draft authoring, approved persona revision, archive audit, learner 403</td></tr>
|
||
<tr><td>Voice WS/REST contract</td><td><code>app.test_voice_ws</code></td><td>18 tests OK; auth guard, onboarding/consent guard, <code>audio_start</code> PCM metadata, binary chunks, <code>audio_end</code>, <code>text_turn</code>, <code>stt_result</code> ready/pending, ping, max audio cap, TTS chunk envelope, structured <code>turn_persistence_unavailable</code> error payload, and owned persisted client-visible turn only <code>POST /voice/speech</code> synthesis</td></tr>
|
||
<tr><td>Voice preset/EOT</td><td><code>app.test_voice_service</code></td><td>14 tests OK; TTS payload, preset fallback, DB voice-map resolver, unsupported provider fallback, P1 sample TTS, EOT readiness</td></tr>
|
||
<tr><td>Voice metadata</td><td><code>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</code> + <code>PLAYWRIGHT_PORT=5197 npx playwright test e2e/voice-success.spec.ts --project=chromium-single-run --workers=1</code> + <code>PLAYWRIGHT_PORT=5205 npx playwright test e2e/session-persistence.spec.ts --project=chromium-single-run --workers=1</code></td><td>77 backend passed + voice-success 2 passed + latest session-persistence 7 passed; audio_ref/silence_ms/speech_rate/barge_in/provider_events persisted or readiness-checked on learner voice turn, Session 마이크 UI는 browser voice activity/trailing silence 메타를 <code>audio_end</code>에 전송, provider_events JSONB 보존·sanitizing·taxonomy·client turn 미혼입 검증, DB-backed review <code>nonverbal</code>은 timing/audio와 제한된 paralinguistic/prosody/audio_quality 칩을 중복 침묵 없이 노출</td></tr>
|
||
<tr><td>Session end/evaluation recovery + AI tutor E2E</td><td><code>PLAYWRIGHT_PORT=5205 npx playwright test e2e/session-persistence.spec.ts --project=chromium-single-run --workers=1</code> / <code>PLAYWRIGHT_PORT=5238 npx playwright test e2e/session-persistence.spec.ts --project=chromium-single-run --workers=1 --grep "finishes session end evaluation"</code> / <code>PLAYWRIGHT_PORT=5237 npx playwright test e2e/session-persistence.spec.ts --project=chromium-single-run --workers=1 --grep "explicit teacher session reevaluation"</code> / <code>PLAYWRIGHT_PORT=5245 npx playwright test e2e/session-persistence.spec.ts --project=chromium-single-run --workers=1 --grep "manual AI evaluation retry"</code> / <code>PLAYWRIGHT_PORT=5254 npx playwright test e2e/session-review.spec.ts --project=chromium-desktop --workers=1 --grep "manual AI retry|long-running session evaluation|retry a failed AI session evaluation|retry fails"</code> / <code>py -3.11 -X utf8 -m pytest -p no:cacheprovider app/test_notifications.py app/test_session_turn_persistence.py app/test_evaluation_persistence.py -k "session_evaluation or missing_session_evaluation or scheduled_session_evaluation" -q</code></td><td>DB-backed session-persistence 7 passed가 실제 DB/API/엔진으로 세션 생성→턴→종료→background deep 평가 durable row→교수자 리뷰 <code>평가 완료</code>까지 검증한다. 추가 자동 평가 E2E 1 passed는 같은 persisted row가 <code>/teacher/dashboard</code>의 <code>evaluation_status=ready</code>, <code>review_ready=true</code>, <code>supervisor_state=평가 완료</code>로 반영되는지 확인한다. 명시 재평가 1 passed는 교수자 <code>POST /eval/sessions/{id}/reevaluate</code>가 durable <code>app.session_evaluation</code> row를 저장하고 <code>/eval/.../evaluation</code>과 <code>/review</code>가 <code>평가 완료</code>로 반영되는지 확인한다. 수동 UI 재시도 E2E 1 passed는 실제 실패 row를 만든 뒤 교수자 리뷰의 <code>AI 평가 재시도</code> 버튼 클릭이 real <code>POST /eval/sessions/{id}/reevaluate</code> → durable ready row → review/dashboard ready 상태로 이어지는지 확인한다. 최신 backend/UI focused 8 passed + 4 passed는 오래된 missing evaluation row가 교수자 대시보드에서도 <code>평가 실패</code>로 보이고, 이미 종료된 session <code>/end</code> 재호출이 평가를 재예약하지 않으며, 평가 ready 전에는 <code>검토 완료</code>로 큐에서 숨길 수 없음을 검증한다. Backend recovery focused 16 passed는 종료됐지만 evaluation row가 없는 오래된 DB 세션을 startup recovery가 evaluator AI context로 찾아 기존 session-end 평가를 재예약하고, 같은 session_id 중복 background 평가를 in-flight set으로 막는지 검증한다. 남은 후속은 다중 프로세스 durable claim/job table 수준의 재시도 소유권이다.</td></tr>
|
||
<tr><td>Browser SSE stream + AI tutor persistence</td><td><code>PLAYWRIGHT_PORT=5241 npx playwright test e2e/session-persistence.spec.ts --project=chromium-single-run --workers=1 --grep "AI tutor coaching history"</code> / <code>PLAYWRIGHT_PORT=5251 npx playwright test e2e/session-mvp.spec.ts --project=chromium-desktop --workers=1 --grep "AI tutor|runtime AI tutor"</code> / <code>PLAYWRIGHT_PORT=5259 npx playwright test e2e/session-mvp.spec.ts --project=chromium-desktop --workers=1 --grep "AI tutor|stale empty AI tutor quota|quota exhaustion|degraded AI tutor|voice conversation stop"</code> / <code>C:\Users\encep\AppData\Local\Programs\Python\Python311\python.exe -X utf8 -m pytest -p no:cacheprovider apps/api/app/test_session_turn_persistence.py -k "live_coach" -q</code></td><td>DB-backed focused 1 passed + mock UI focused 7 passed + backend live coach focused 7 passed. 실제 Session UI 코칭 모드에서 <code>POST /live-coach</code> 응답과 DB-backed history payload가 <code>status=ready</code>, <code>latency_ms>0</code>인지 확인하고, 0615 source pack <code>version/citation</code>이 <code>근거 보기</code> 모달, DB-backed <code>GET /live-coach</code> 이력, reload 후 학습자 발화의 <code>C</code> 마커 history dialog까지 보존되는지 검증한다. Mock MVP는 source title/kind/version/citation 렌더, <code>status=degraded</code> replacement 표시, <code>GET /live-coach</code> 실패 alert, <code>persistence_source/source=runtime</code> 임시 이력 경고, stale empty quota 서버 재조회, quota exhaustion 우선 표시를 각각 고정한다. Backend focused는 live coach event 저장 실패 503뿐 아니라 LLM audit 저장 실패가 정상 <code>ready</code> 코칭으로 보이지 않고 <code>degraded</code> fallback으로 내려가는지도 고정한다.</td></tr>
|
||
<tr><td>C3 theory-mode reevaluation</td><td><code>C:\Users\encep\AppData\Local\Programs\Python\Python311\python.exe -X utf8 -m pytest -p no:cacheprovider app/test_eval_routes.py -q</code> / <code>PLAYWRIGHT_PORT=5229 npx playwright test e2e/session-persistence.spec.ts --project=chromium-single-run --workers=1 --grep "selected CBT theory mode"</code> / <code>app/test_eval_routes.py app/test_notifications.py app/test_session_turn_persistence.py</code></td><td>eval route 5 passed, focused backend 39 passed, DB-backed browser E2E 1 passed. 실제 Session UI에서 선택한 <code>CBT</code>가 <code>POST /sessions</code> payload와 DB-backed <code>GET /sessions/{id}</code> 상세의 <code>theory_mode=cbt</code>로 보존된다. 수동 session 재평가는 persona 기본 <code>theory_target</code>보다 학습자가 선택한 session <code>theory_mode</code>를 우선하고, turn 재평가도 <code>TurnContext.theory_mode</code>에 같은 값을 전달한다.</td></tr>
|
||
<tr><td>Crisis safety gate UI</td><td><code>PLAYWRIGHT_PORT=5193 npx playwright test e2e/session-mvp.spec.ts --project=chromium-single-run --workers=1</code> / <code>PLAYWRIGHT_PORT=5259 npx playwright test e2e/session-mvp.spec.ts --project=chromium-desktop --workers=1 --grep "voice conversation stop"</code></td><td>mock SSE <code>safety</code> + <code>done.conversation_stopped</code>와 mock voice <code>reply.conversation_stopped</code>에서 안전 자원 109와 입력 disabled가 유지되고, 빈 client reply를 <code>내담자 응답 없음</code>으로 덮지 않으며 live-coach를 호출하지 않는지 검증한다. 음성 위기 경로는 TTS 종료 이벤트 없이도 socket close를 확인한다.</td></tr>
|
||
<tr><td>Persona/session review UI E2E</td><td><code>PLAYWRIGHT_PORT=5215 npx playwright test e2e/teacher.spec.ts --project=chromium-single-run --workers=1</code></td><td>9 passed mixed suite; real server-row rendering과 route-fixture UI 흐름을 함께 포함한다. 교수자가 pending persona를 승인하고, pending/recent review detail로 진입하며, 별도 student analysis 메뉴·학습자 검색·행 펼침·full learner timeline drilldown을 검증한다.</td></tr>
|
||
<tr><td>Voice UI synthetic E2E</td><td><code>npx playwright test e2e/voice-success.spec.ts --project=chromium-single-run</code> / <code>PLAYWRIGHT_PORT=5248 npx playwright test e2e/session-mvp.spec.ts --project=chromium-single-run --workers=1 --grep "pending voice transcript"</code></td><td>voice-success 2 passed + pending transcript failure UI 1 passed; direct WS cascade, Session 텍스트 턴의 persisted reply <code>/voice/speech</code>→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 <code>저장 실패</code> turn instead of a silent successful transcript</td></tr>
|
||
<tr><td>G7 explicit consent · external proof</td><td><code>pytest app/test_g7_voice_gain_evidence.py app/test_voice_runtime.py ...</code> / <code>python -m unittest scripts/test_g7_external_proof.py scripts/test_g7_topology_evidence.py ...</code> / <code>voice-success.spec.ts</code></td><td>내부 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를 통과하지 못하고 <code>local_whisper</code>/<code>melotts</code> sidecar도 미기동이다. 실제 명시 동의 마이크·50분·동시 runtime/topology·human evidence가 없어 G7 external GATE다.</td></tr>
|
||
<tr><td>Voice s2s decision memo</td><td><code>docs/decisions/voice-s2s-poc.md</code></td><td>criteria recorded; keep/drop decision remains owner DECIDE</td></tr>
|
||
<tr><td>Hanshin data/SSO gate</td><td><code>docs/ops/hanshin-data-governance-gate.md</code></td><td>artifact created; written external evidence still required</td></tr>
|
||
<tr><td>Hanshin feedback 1차 improvement pack</td><td><code>docs/ops/hanshin-feedback-improvement-plan-2026-07-03.md</code> / <code>docs/redteam/hanshin-feedback-plan-redteam-2026-07-03.md</code> / <code>docs/redteam/hanshin-feedback-plan-counter-redteam-2026-07-03.md</code> / backend+web implementation</td><td>1차 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.</td></tr>
|
||
<tr><td>Resistance engine</td><td><code>app.test_state_machine_resistance</code> + <code>scripts/smoke-resistance-openness-db.py</code></td><td>9 focused tests OK; live DB smoke PASS. Empathy P1 curve <code>0→0→0.095→0.133→0.171</code> and DB final <code>stage=탐색</code>. Advice-jump curve stayed <code>0</code> and DB final <code>stage=라포</code>.</td></tr>
|
||
<tr><td>Python compile</td><td><code>python -m compileall app engine_gateway</code></td><td>Passed</td></tr>
|
||
<tr><td>Phase 3 artifact gates</td><td><code>py -3.11 -X utf8 scripts\check-phase3-artifacts.py --help</code> + <code>py -3.11 -X utf8 -m pytest -p no:cacheprovider app/test_dataset_export.py app/test_phase3_artifact_checker.py -q</code></td><td>16 passed; checker now enforces CSV enums, KPI metric required fields/status, approved export PII/agreement/consent/withdrawal/file-hash gates, and approved/dry-run dataset JSONL required keys, row count, privacy, and PII shape. Actual pilot evidence still external.</td></tr>
|
||
<tr><td>Admin AI provider capability</td><td><code>GET /v1/capabilities</code> / <code>GET /admin/engine-capabilities</code> / <code>public-admin-visual.spec.ts</code></td><td>Backend 전체 466 passed, web build, 관리자 AI desktop/mobile 2 passed, 전수 7 passed. 공개 인증 E2E 1 passed에서 공급자 6개와 live Codex 7개(<code>gpt-5.6-terra / medium</code>), Agy 11개(<code>gemini-3.6-flash-high / high</code>) dropdown을 확인했다. Anthropic API는 키가 없어 fail-closed unavailable 상태다.</td></tr>
|
||
<tr><td>Web build</td><td><code>npm run build</code></td><td>Passed; capability dropdown bundle <code>AdminAi-ClAJvCvx.js</code></td></tr>
|
||
<tr><td>Pages production deploy</td><td><code>wrangler pages deploy dist --project-name vignette --branch main --commit-hash <HEAD> --commit-dirty=true</code></td><td>2026-07-30 production deploy <code>2f52f8e3-f992-441f-ada4-f6396cbb1a2b</code>, source marker <code>facc4ad</code>, preview <code>https://2f52f8e3.vignette-b1q.pages.dev</code>. 첫 배포 <code>a3819e6e</code>에서 이전 자산 보존 누락을 live 검출한 뒤 최근 5개 production preview의 HTML·JS·CSS 의존 그래프를 병합해 최종 배포로 교체했다.</td></tr>
|
||
<tr><td>Custom domain assets</td><td><code>https://vignette.chanpaca.net/admin/ai?deploy=2f52f8e3</code></td><td>HTML 200, 현재 <code>index-B54GMBSf.js</code>와 <code>AdminAi-ClAJvCvx.js</code>, 이전 4세대 엔트리 모두 <code>application/javascript</code> 200. 공개 lazy 청크 강제 실패 복구 1 passed, 인증된 관리자 AI live dropdown 1 passed, 공개 API health/auth config 200, 비인증 personas/admin capability 401.</td></tr>
|
||
<tr><td>Legacy Live2D routes</td><td><code>/live2d/mao/* / /live2d/haru/* / /live2d/live2dcubismcore.min.js</code></td><td>404</td></tr>
|
||
<tr><td>Compose template</td><td><code>docker compose -f infra\docker-compose.yml config --quiet</code></td><td>Template path is valid with dummy required env. API build context now uses repo root; stale <code>rag.server</code> sidecar removed.</td></tr>
|
||
<tr><td>Docker image smoke</td><td><code>docker build -f apps/api/Dockerfile .</code> / <code>docker run ... python -c "import app.main"</code> / <code>docker build -f apps/web/Dockerfile apps/web</code></td><td>API/Web image build and API import smoke pass after packaging cleanup. Web build uses npm lockfile and ignores host <code>node_modules</code>; API image excludes local <code>.env</code> files.</td></tr>
|
||
<tr><td>Deploy preflight</td><td><code>python scripts\check-deploy-preflight.py --skip-db --env-file infra\.env.example --allow-placeholder-secrets</code> / DB mode with local <code>DATABASE_URL</code></td><td>Passed: exact-pinned API requirements, live coaching <code>data/kb</code> source pack, env template keys, and DB readiness (<code>current_user=vignette</code>). DB mode can additionally check app-role DSN with <code>--require-app-role</code> and now verifies session read-model columns including <code>app.turns</code> voice metadata columns(<code>audio_ref</code>/<code>silence_ms</code>/<code>speech_rate</code>/<code>barge_in</code>/<code>provider_events</code>) plus worksheet review columns.</td></tr>
|
||
<tr><td>Fresh compose smoke</td><td><code>docker compose -p vignette-packaging-smoke -f infra/docker-compose.yml up -d --build</code> + proxy <code>/api/health</code></td><td>Passed with dummy production-safe env: API healthy, DB healthy, web/proxy up, <code>http://localhost:18080/api/health</code> 200 with <code>db:true</code>, <code>engine:true</code>, <code>engine_mode:"claude_cli"</code>. Smoke volumes/network removed after run.</td></tr>
|
||
<tr><td>격리 NAS 프리뷰 · 회기 E2E 자동화</td><td><code>http://100.116.83.60:8088</code> / <code>vignette-e2e</code> 매일 04:30 KST / <a href="./ops/nas-preview-deployment-evidence-2026-08-07.md">배포 증거</a> / <a href="./ops/evidence/nas-preview-current-deploy-2026-08-07.json">기계 판독 증거</a></td><td>SHA <code>6030a677…c611</code>을 전용 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 증거는 별도다.</td></tr>
|
||
<tr><td>G7 외부 증거 3-artifact 동시 시간창 오케스트레이터</td><td><code>scripts/run-g7-external-proof-window.py</code> · <a href="./ops/outcome-os-g7-external-proof-readiness-2026-08-07.md">준비 문서</a></td><td>soak·runtime·topology 세 캡처를 같은 host의 공통 3,000초 시간창으로 실행하고 checker까지 잇는다. CLI는 동의·장치 없음, human pack 없음, 3,120초 미만, host 불일치를 전부 fail-closed하며 <code>--rehearse</code>는 마이크를 열지 않는다. 기존 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로 연결했지만 아직 공개에 실행하지 않았다. <b>남은 선행 조건:</b> 공개 런타임은 119 paths 구버전이고 <code>/admin/voice-runtime</code>이 없으며 <code>/voice/health</code>는 OpenAI STT/TTS, local sidecar 9882/9883은 내려가 있다. API는 Git 없는 냉동 release cwd라 provenance gate도 닫혀 있다. clean committed source runtime으로 정렬한 뒤에만 <b>물리 마이크 50분</b>과 <b>독립 human voice-gain pack</b>을 수집한다.</td></tr>
|
||
<tr><td>G7 external GATE · 최종 감사 정정</td><td><code>run-g7-external-proof-window.py</code> → <code>check-g7-external-proof.py</code></td><td><b>상태는 external GATE 유지.</b> 물리 마이크 전 코드 P0 3건은 완료됐다. runner 프로세스 exit는 canonical checker <code>exit 0</code>·<code>gate_closed=true</code>에 결속되고, browser Origin <code>vignette.chanpaca.net</code>은 API/WSS/admin/topology host <code>api-vignette.chanpaca.net</code>과 분리된 HTTPS allowlist로 검증되며, 세 artifact 교집합은 <code>≥3000s</code> + 120초 sampler margin으로 강제된다. Windows proof는 detached-clean HEAD/tree·runner/collector/checker script SHA·exact <code>psutil==6.1.1</code>을 pin한다. 공개 창에서는 legacy PID를 재사용하지 않고 API/cloudflared를 pinned executable/config와 stable release cwd에서 강제 재시작해 safe provenance receipt를 결속한다. 남은 것은 clean-source 실제 배포와 4-artifact 실증이다.</td></tr>
|
||
<tr><td>G8 runtime REVALIDATION · 17:07 NAS 기준선</td><td><code>vignette-preview-20260807</code> · <code>run-outcome-os-release-agent.py</code></td><td><b>상태는 runtime REVALIDATION 유지.</b> active SHA <code>6030a677…c611</code>, API/Web <code>52e0…8b2d</code>/<code>6fdb…f215</code> exact running, previous <code>79ec…4450</code>/<code>c530…2f28</code> 보존. <code>api/web/db/proxy</code> 4개 running + <code>unless-stopped</code>, volume <code>vignette-preview-20260807_pgdata</code>, health 3/3·db/engine true·auth 401·OpenAPI 126 + G1~G8 routes·root/JS/CSS 200, helper/listener 0이다. dump는 <code>92ed…1570f</code> 960,970 bytes와 <code>726982…5d41c</code> 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 <code>0777</code> P1은 최소권한 preflight로 닫는다.</td></tr>
|
||
<tr><td>노트북 로컬 음성 스택 — faster-whisper STT + MeloTTS TTS</td><td><code>scripts/local-whisper-stt-server.py</code> · <code>scripts/start-local-whisper-stt.ps1</code> · <code>voice_stt_provider=local_whisper</code></td><td>2026-08-08 소유자 결정으로 음성 양쪽을 노트북 상주 로컬 모델로 정했다. TTS는 처음 Higgs로 잡았으나 연구/비상업 라이선스라 <code>config.py</code>가 운영에서 차단하고 있었고 그 가드를 푸는 건 법적 판단이라, 상업 사용이 허용된 <b>MeloTTS Korean(MIT)</b>으로 바꿔 가드 자체를 불필요하게 만들었다. Kokoro-82M은 Apache 2.0이지만 공식 <code>VOICES.md</code>에 <b>한국어가 없어</b> 탈락했다. local Whisper는 현재 호스트의 cuDNN 부재를 반영해 <code>small/cpu-int8</code>를 운영 기본으로 고정했고, 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 전용이다.</td></tr>
|
||
<tr><td>비-secure origin 회기 리뷰 크래시 수정</td><td><code>apps/web/src/lib/uuid.ts</code> · <code>apps/web/e2e/insecure-context-uuid.spec.ts</code></td><td>격리 NAS 프리뷰(평문 HTTP·비-localhost)에서 회기 리뷰 라우트가 <code>crypto.randomUUID is not a function</code>으로 error boundary에 잡혔다. 이 API는 secure context 전용인데 제품 코드 18곳이 fallback 없이 호출했고 <code>RuptureRepairCard</code>는 렌더 시점 호출이라 화면 전체가 죽었다. 릴리스 게이트는 localhost 후보 스택에서만 돌아 미검출. <code>randomUuid()</code>로 통일하고 fallback도 <code>getRandomValues</code> 우선으로 예측 불가능성을 유지했다. 회귀 <code>6/6</code>(직접 호출 0건 검사 포함), typecheck·build 통과. <b>NAS 프리뷰에는 아직 미배포</b>이며 배포된 SHA <code>6030a677…c611</code>은 여전히 결함 빌드다.</td></tr>
|
||
<tr><td>공개 워치독 engine readiness 사각지대(해결)</td><td><code>scripts/watch-public-runtime.ps1</code> · <code>scripts/start-public-runtime.ps1</code> · <code>apps/api/engine_gateway/gateway.py</code></td><td>2026-08-07 22:0x KST 소유자 승인으로 9099를 재기동해 복구했다. 재기동 직후 <code>/ready?force=true</code> 200 <code>ok:true·detail:OK</code>, API 8001과 공개 <code>api-vignette.chanpaca.net</code> 모두 <code>status:ok·engine:true·engine_detail:OK</code>다. 원인은 16:06부터 상주한 9099 프로세스 하나였다(CLI·코드·cwd는 정상 — 동일 인자 직접 실행과 <code>EngineSession</code> 재현이 <code>"OK"</code>를 반환). 재발 방지 3건: (1) 워치독·기동 스크립트의 engine 판정을 <code>/health</code>(프로세스 liveness)에서 <code>/ready</code>(실제 생성)로 바꾸고 API 판정에 <code>engine</code>을 추가, shared secret 인스턴스용 토큰 헤더와 503 본문 로깅 포함. (2) 게이트웨이가 자식 <code>claude -p</code>의 stderr를 상시 드레인해(PIPE 미독 시 자식 블록도 차단) 실패 detail에 <code>exit</code>·stderr를 붙인다 — 실증: <code>empty engine response (error: unknown option '--vignette-nonexistent-flag-xyz')</code>. (3) 기동 시 게이트웨이 로그 회전, <code>Stop-UvicornByPort</code>에 <code>Name -like python*</code> 추가(호출자 자기 자신 종료 방지). 검증: <code>pytest engine_gateway</code> 58/58, 워치독 <code>-CheckOnly</code> healthy 5/5, 장애 재현(9299·<code>CLAUDE_BIN</code> 부재)에서 <code>/health</code>는 <code>ok:true</code>로 통과하고 <code>/ready</code>는 503으로 검출되어 워치독이 <code>unhealthy (1/3)</code> + 503 본문을 남겼다. <b>NAS용 9100(06:27 상주)은 실사용 세션 7개가 있어 미접촉</b>이라 게이트웨이 stderr 개선은 다음 재기동 때 반영된다.</td></tr>
|
||
<tr><td>G8 실제 receipt-bound image rollback</td><td><code>scripts/launch-nas-preview-g8-helpers.py</code> / <code>scripts/serve-nas-preview-rollback-executor.py</code> / <code>scripts/probe-nas-preview-g8-rollback.py</code> / <a href="./ops/nas-preview-g8-rollback-proof-runbook.md">런북</a> / <a href="./ops/evidence/nas-preview-g8-actual-rollback-2026-08-07.json">기계 판독 증거</a></td><td>격리 NAS에서 별도 executor(18149)·control-plane(8018) helper로 실제 image rollback 2회를 실행했다. receipt <code>nas-g8-723eeef2…</code>로 previous <code>79ec…4450</code>/<code>c530…2f28</code>, receipt <code>nas-g8-2738846c…</code>로 current <code>52e0…8b2d</code>/<code>6fdb…f215</code>를 활성화했다. release gate·approval 각각 2회 멱등, <code>audit.ci_lifecycle_event</code> 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로 고정했다. <code>down</code>/<code>volume rm</code>/<code>prune</code> 미실행, 공개 런타임 미접촉.</td></tr>
|
||
<tr><td>Actual deployment env</td><td><code>infra/.env</code> owner-secret fill-in</td><td>Remaining external step: deployment target must provide real <code>APP_DB_PASSWORD</code>, OAuth client id/secret, <code>OPENAI_API_KEY</code>, <code>SESSION_SECRET</code>, and production-safe engine/voice flags. Current local stray values such as <code>ENGINE_MODE=claude_cli</code> and prod sample TTS must not be copied.</td></tr>
|
||
<tr><td>Focused E2E</td><td><code>admin + db-persistence + voice-success</code></td><td>Latest admin spec: 8 passed on chromium desktop, including all-workspace admin navigation, health dashboard, real server-known users, operation tickets, mobile controls, and tablet form containment.</td></tr>
|
||
<tr><td>Dev dashboard redteam</td><td><code>scripts/test_dev_dashboard_ssot.py</code> / <code>e2e/dev-dashboard.spec.ts</code></td><td>Unit: SSOT checker 4 tests OK + current dashboard check PASS. E2E: local file, <code>127.0.0.1</code>, <code>localhost</code> 외부형 origin, 필터 오순서/빠른 재시도/키보드, details/tabs 접기, hash anchor, 링크·이미지 무결성, desktop/mobile overflow를 검증해 chromium desktop/mobile 10 passed.</td></tr>
|
||
<tr><td>Admin manage-users</td><td><code>admin.spec.ts --grep manage real server-known users</code></td><td>desktop/mobile 2 passed</td></tr>
|
||
<tr><td>Full E2E baseline</td><td><code>PLAYWRIGHT_PORT=5174 npm run e2e</code></td><td>2026-06-27/28 기준선 113 passed</td></tr>
|
||
<tr><td>Public auth discovery</td><td><code>E2E_PUBLIC_AUTH=1 npx playwright test --list --project=chromium-public-auth</code></td><td>2 tests listed</td></tr>
|
||
<tr><td>Public pre-auth readiness</td><td><code>public-auth-turn.spec.ts --grep production-safe</code></td><td>production-safe smoke 1 passed. 인증된 public <code>/turn</code> proof는 아직 미완료이며 owner storageState가 필요하다.</td></tr>
|
||
<tr><td>Public OAuth start</td><td><code>/auth/config</code> + <code>/auth/login?provider=google</code></td><td>2026-06-30 복구 후 auth config 200, Google configured true, redirect URI <code>https://api-vignette.chanpaca.net/auth/callback</code>, dev-login disabled. Google login start는 GET 302와 <code>__Host-vignette_oauth_state</code> HttpOnly/Secure cookie를 반환한다. Authenticated public <code>/turn</code> proof는 owner storageState가 필요하다.</td></tr>
|
||
<tr><td>Persona auth boundary</td><td><code>GET /personas</code></td><td>2026-06-30 복구 후 public unauth <code>/personas</code>는 401 <code>not authenticated</code>를 반환한다.</td></tr>
|
||
<tr><td>Public login</td><td><code>auth.spec.ts --grep public login</code></td><td>1 passed</td></tr>
|
||
<tr><td>Public runtime scripts · source pin</td><td><code>start/watch/boot-public-runtime*.ps1</code> · <code>install-public-runtime-task.ps1</code> · <code>register-boot-task.ps1</code></td><td>PowerShell 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. <b>실제 task migration은 아직 미완료</b>: watchdog과 로그온 task action이 shared worktree/legacy 인자를 가리켜 최신 5분 watchdog은 mutation 전에 exit하고 <code>LastTaskResult=1</code>이다. 사용자 승인 clean commit 뒤 동일 stable release root로 두 task를 재등록하고 action pin·result 0·로그온/reboot smoke를 확인해야 한다.</td></tr>
|
||
<tr><td>Public API health</td><td><code>http://127.0.0.1:8001/health</code> / <code>https://api-vignette.chanpaca.net/health</code></td><td>2026-08-09 핸드오프 read-only 확인에서 local/public 모두 <code>status=ok</code>, <code>db=true</code>, <code>engine=true</code>다. DB container는 recovered named volume과 <code>unless-stopped</code>를 유지한다. 이는 source-pin task migration이나 G7 current-source 배포 완료 증거가 아니다.</td></tr>
|
||
<tr><td>Public runtime 17:54 read-only snapshot</td><td><code>health + OpenAPI + voice + listeners + Scheduled Tasks</code></td><td>local/public health <code>status=ok·db=true·engine=true</code>, 공개 OpenAPI <code>119 paths</code>, <code>/admin/voice-runtime</code> 없음, <code>/voice/health</code> <code>openai/gpt-4o-transcribe</code> + <code>openai/gpt-4o-mini-tts</code>, local Whisper 9882·MeloTTS 9883 listener <code>0</code>. watchdog·로그온 task action은 여전히 legacy shared worktree이고 17:54 KST watchdog <code>LastTaskResult=1</code>이다. 이는 필수 pin 인자 부재로 mutation 전 fail-closed한 값이며 G7 current-source 배포 완료가 아니다.</td></tr>
|
||
<tr><td>Public/local/Tailnet login recovery</td><td><code>https://vignette.chanpaca.net/login</code> / <code>https://api-vignette.chanpaca.net/health</code> / <code>https://alpaca-home.taile93291.ts.net/login</code></td><td>2026-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.</td></tr>
|
||
<tr><td>Local 5175 login</td><td><code>PLAYWRIGHT_BASE_URL=http://127.0.0.1:5175 auth.spec.ts</code></td><td>desktop/mobile passed</td></tr>
|
||
<tr><td>Learner/readiness E2E</td><td><code>learner.spec.ts + readiness.spec.ts desktop/mobile</code></td><td>14 passed</td></tr>
|
||
<tr><td>Learner screenshots</td><td><code>learn-empty-desktop.png / learn-empty-mobile-compact.png</code></td><td>document overflow 0, empty history visible</td></tr>
|
||
<tr><td>Session layout/turn</td><td><code>session-layout.spec.ts desktop/mobile</code></td><td>8 passed</td></tr>
|
||
<tr><td>Session voice guard/view-model</td><td><code>npm run typecheck</code> / <code>npm run build</code> / <code>npx playwright test e2e/session-mvp.spec.ts --project=chromium-single-run --workers=1</code> / <code>npx playwright test e2e/voice-success.spec.ts --project=chromium-single-run --workers=1</code> / <code>npx playwright test e2e/session-layout.spec.ts --project=chromium-desktop --project=chromium-mobile --workers=1</code></td><td>typecheck/build passed; P1 text MVP 1 passed, voice cascade + Session text/mic UI 2 passed, session-layout desktop/mobile 8 passed. <code>isVoiceStatusBusy()</code> and <code>sessionVoiceStatusView()</code> own mic busy/disabled, aria label, transcript/response/status labels, and text-turn blocking; persisted text-turn replies now share the OpenAI TTS Web Audio lifecycle without changing the recording click-to-send path.</td></tr>
|
||
<tr><td>Session draft + live client latency + Higgs TTS</td><td><code>pytest app/ -q</code> / <code>pytest engine_gateway/ -q</code> / <code>session-mvp.spec.ts --grep "runs login, P1 text stream"</code> / <code>full-sweep-session.spec.ts --grep "shows the voice skip button"</code> / actual local P1 browser + <code>POST /voice/speech</code></td><td>Backend 432/432, gateway 44/44, focused E2E 2/2, typecheck/API types/build passed. 실제 UI에서 생성 중 textarea enabled·초안 보존, TTS 중 보내기 enabled. 응답·전송 해제는 첫 9.58초, 상주 연속 6.96초이며 사후 fast-loop 평가는 같은 learner turn에 durable 저장된다. Higgs P1은 synthetic-seed-only, 24kHz WAV 362,924 bytes/7.56초로 확인했다. 로컬 변경이며 공개 미배포다.</td></tr>
|
||
<tr><td>Session end + dark theme + SVG avatar UX</td><td><code>npm run typecheck</code>, <code>npm run build</code>, <code>layout-visual-gate</code>, <code>session-layout</code>, <code>avatar-expression</code>, <code>settings</code>, <code>session-review</code></td><td>typecheck/build passed; 아바타·세션 desktop/mobile 18 passed + 전체 레이아웃 시각 게이트 12 passed + 커스텀 도메인 P1 SVG smoke desktop/mobile 2 passed. 세션·학습자 홈·dev 미리보기는 P1/P4~P7 모두 <code>data-render-mode="svg"</code>인 도형 기반 파라미터 리그를 사용하고 생성 이미지/PSD 래스터 DOM은 렌더하지 않는다.</td></tr>
|
||
<tr><td>Voice/session focused</td><td><code>voice + voice-success + session-layout</code></td><td>15 passed</td></tr>
|
||
<tr><td>Layout redesign handoff</td><td><code>docs/archive/ops/layout-redesign-handoff-2026-06-26.md</code></td><td>subagent scopes, files, verification, remaining visual review recorded</td></tr>
|
||
<tr><td>Layout redesign focused E2E</td><td><code>npx playwright test e2e/learner.spec.ts e2e/session-layout.spec.ts e2e/session-review.spec.ts e2e/admin.spec.ts e2e/settings.spec.ts e2e/teacher.spec.ts --project=chromium-desktop --project=chromium-mobile</code></td><td>54 passed after isolated Admin tablet flake rerun</td></tr>
|
||
<tr><td>Strict layout visual gate</td><td><code>PLAYWRIGHT_PORT=5217 npx playwright test e2e/layout-visual-gate.spec.ts --project=chromium-single-run --workers=1</code></td><td>12 passed; 12 screens × 7 widths(390/720/861/900/1024/1280/1440), horizontal overflow 0 + control clip 0, 84 full-page screenshots. 학생 분석 목록 테이블/상세 화면과 빈 회기리뷰 fixture 포함.</td></tr>
|
||
<tr><td>Parallel visual quality review</td><td><code>workflow layout-visual-quality-review (7 agents)</code></td><td>49 screenshots read; per-screen findings(critical 1, major 8+, minor) → fix verdicts</td></tr>
|
||
<tr><td>Parallel layout fixes</td><td><code>6 agent team: learner-home/session/review/professor/admin/settings (disjoint files)</code></td><td>all fixes applied; <code>npm run typecheck</code> OK; no cross-file conflicts</td></tr>
|
||
<tr><td>Strict gate re-run (post-fix)</td><td><code>PLAYWRIGHT_PORT=5217 npx playwright test e2e/layout-visual-gate.spec.ts --project=chromium-single-run --workers=1</code></td><td>12 passed; re-screenshotted 84</td></tr>
|
||
<tr><td>Layout focused E2E re-run (post-fix)</td><td><code>learner/session-layout/session-review/admin/settings/teacher × desktop+mobile</code></td><td>54 passed; no regression</td></tr>
|
||
<tr><td>Adversarial visual verify</td><td><code>workflow layout-visual-verify (7 agents)</code></td><td>7/7 accept; critical/major resolved, regression 0; minor polish backlog recorded</td></tr>
|
||
<tr><td>Engine config 운영값</td><td><code>GET /admin/engine-config</code></td><td>claude_cli / http://127.0.0.1:9099 / gateway-default, durable, source=database (의도값 일치)</td></tr>
|
||
<tr><td>Engine gateway probe (live)</td><td><code>scripts/probe-engine-gateway.py --base-url http://127.0.0.1:9099 --reuse-runs 2</code></td><td>claude_p / opus-4-8; 단일 session_id 2회 reused stream; TTFT 1667–4199ms; cost 누적 0.068→0.123(turns 1→2)</td></tr>
|
||
<tr><td>Engine gateway RSS (live)</td><td><code>docs/ops/engine-rss-smoke-2026-06-28.md</code></td><td>gateway session 1개, <code>health.sessions=1</code>; gateway PID 51592 RSS 16.2MB; resident <code>claude -p</code> child PID 51688 RSS 363.3MB / Private 628.8MB after one turn; plugin bun children RSS 178.6MB.</td></tr>
|
||
<tr><td>Postgres RLS/audit smoke (live)</td><td><code>VIGNETTE_RLS_AUDIT_DSN=…vignette_app… check-postgres-rls-audit.py --write-fixtures</code></td><td>5 checks PASS: preflight, learner 세션/턴 격리, teacher cohort 스코프, visible_to evaluator-only 숨김, teacher/admin read audit insert (NOBYPASSRLS app role, fixtures 자동 정리)</td></tr>
|
||
<tr><td>Turn cost/token telemetry (live DB)</td><td><code>SELECT … FROM app.turns WHERE actor_kind='client_ai'</code> / live gateway generate·SSE / <code>scripts/backfill-claude-token-usage.py</code></td><td>Claude CLI의 <code>total_cost_usd</code>는 청구 실비가 아닌 SDK 비용 추정값으로 저장한다. 신규 gateway generate·SSE는 <code>modelUsage</code> 전체 agent tree의 비캐시·cache read/create 입력과 출력을 전달한다. 수정 전 전체 0/0 Claude 442건 중 로컬 JSONL의 정규화 응답 SHA-256·시각이 유일하게 일치한 169건만 실제 입력 5,179,999·출력 46,910 토큰으로 복구했다. 불일치 273건은 비용 역산 없이 <code>token_unmetered_turns</code>로 남겼다.</td></tr>
|
||
<tr><td>Cosmetic polish re-verify</td><td><code>5 polish agents → typecheck + gate + layout E2E re-run</code></td><td>typecheck OK; current gate 9 passed; layout focused E2E 54 passed; session-layout 8/8; regression 0</td></tr>
|
||
<tr><td>Dashboard layout alignment</td><td><code>npm run typecheck</code> / <code>npm run build</code> / <code>layout-visual-gate</code> / <code>session-layout</code> / <code>session-review</code> / <code>learner+teacher+admin E2E</code></td><td>current rerun: typecheck/build passed; layout gate 9 passed; session-layout+session-review chromium-desktop 6 passed. 이전 정렬 라운드: session-layout desktop/mobile 8 passed; learner+teacher+admin chromium-desktop 17 passed. 대표 캡처는 <code>docs/archive/ops/layout-research-2026-06-28/final-gate-*.png</code>.</td></tr>
|
||
<tr><td>Learner session archive</td><td><code>python -B -m pytest app/ -q</code> / <code>python -B -m pytest engine_gateway/ -q</code> / <code>npm run check:api-types</code> / <code>npm run typecheck</code> / <code>npm run build</code> / <code>npx playwright test e2e/learner.spec.ts --project=chromium-desktop --workers=1</code></td><td>Archive 당시 178 backend passed + 11 gateway passed; 최신 gateway contract gate는 27 passed. OpenAPI generated archive DTOs are current; learner history can archive/restore an ended session and keeps archived sessions out of the review queue while preserving records.</td></tr>
|
||
</tbody>
|
||
</table>
|
||
<div class="tab-subhead"><h4>다음 실행 명령</h4><p>남은 public OAuth <code>/turn</code> proof 확보용 명령이다.</p></div>
|
||
<div class="commands">
|
||
<pre>cd D:\workspace\vignette\apps\web
|
||
npx playwright codegen https://vignette.chanpaca.net/login --save-storage=.\node_modules\.tmp\public-auth.json
|
||
|
||
$env:E2E_PUBLIC_AUTH="1"
|
||
$env:E2E_PUBLIC_STORAGE_STATE=".\node_modules\.tmp\public-auth.json"
|
||
.\node_modules\.bin\playwright.cmd test e2e/public-auth-turn.spec.ts --project=chromium-public-auth</pre>
|
||
<pre>cd D:\workspace\vignette
|
||
powershell -NoProfile -ExecutionPolicy Bypass -File scripts\start-public-runtime.ps1</pre>
|
||
</div>
|
||
<div class="source-note"><b>닫은 항목(live 증거):</b> ① engine config 운영값(<code>claude_cli / 9099 / gateway-default</code>, durable, db). ② 상주 엔진풀 probe — 단일 session_id 2회 reused stream, TTFT 1667–4199ms, cost 누적 0.068→0.123. ③ 상주 엔진풀 RSS — gateway session 1개에서 <code>claude -p</code> child RSS 363.3MB, gateway RSS 16.2MB. ④ Postgres RLS/audit smoke 5 checks PASS. ⑤ turn cost telemetry — <code>app.turns</code> client_ai 13행에 provider/model/cost_usd 실적재(합 $1.22). ⑥ 레이아웃 시각 게이트 최신 9/9 + 2026-06-28 적대적 재검수 7/7 accept.</div>
|
||
<div class="source-note"><b>환경 제약으로 이 세션에서 못 닫는 항목(정직 표기):</b> 공개 Google OAuth 실제 <code>/turn</code>(OAuth start는 2026-06-30 재확인 완료, 실제 계정 storageState 필요) · 실배포 <code>infra/.env</code> owner-secret fill-in(<code>APP_DB_PASSWORD</code>, <code>OAUTH_GOOGLE_CLIENT_ID</code>, <code>OAUTH_GOOGLE_CLIENT_SECRET</code>, <code>OPENAI_API_KEY</code>, <code>SESSION_SECRET</code>) · <code>vnet.18ka.net</code>/<code>api-vnet.18ka.net</code> 공개 런타임(DNS A record 0, Cloudflare 18ka.net zone 권한 불일치, Google OAuth redirect URI 등록 필요) · 한신대 데이터/SSO 거버넌스(owner/기관 written evidence 필요) · G7 current source 공개 배포, <code>local_whisper</code>/<code>melotts</code> 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 표기하지 않는다. 전체 추적: <code>docs/ops/backlog-2026-06-26.md</code>(B1 구현 후속 · B2 환경제약 · B3 소유자결정 · B4 거버넌스).</div>
|
||
</div>
|
||
<div class="tab-panel" id="panel-decisions" role="tabpanel" aria-labelledby="tab-decisions" hidden>
|
||
<div class="detail-head"><div><h3>윤찬 결정 상세 (전건 확정)</h3><p>람다가 단독으로 정하면 월권인 항목이었다. 2026-06-30 owner 결정으로 전건 확정됐고, 각 결정의 구현·실측은 후속 build/GATE로 남는다.</p></div><div class="mini-metrics"><span>결정 0개(전건 확정)</span><span>조건부 GO 사유</span></div></div>
|
||
<div class="task-list">
|
||
<div class="task-row"><div><span class="task-status s-done">DECIDED</span></div><div><b>평가설계 (H1: 실험/통제군·3척도·κ/ICC)</b><p>소유자 결정(2026-06-30): 평가자 신뢰도 κ≥0.70 & ICC≥0.75, 환각률 ≤3%, 독립표본 t-검정(α=0.05), 20명 실험/통제군 무작위 배정, 단회기 50분·3척도 pre-post 유지. 설계 파라미터 확정.</p></div><div><b>후속</b><p>KPI 측정계획·report threshold에 κ/ICC/환각률 반영. 실제 20명 파일럿 evidence·통계해석·공식 효과성 판정은 별도 GATE.</p></div></div>
|
||
<div class="task-row"><div><span class="task-status s-done">DECIDED</span></div><div><b>s2s 2차 PoC 채택 여부</b><p>소유자 결정(2026-06-30): s2s 2차 PoC를 유지한다. 캐스케이드 1차를 운영 기준으로 두면서 <code>voice-s2s-poc.md</code> 기준표로 비교 리포트를 만든 뒤 최종 채택/폐기를 재판단한다.</p></div><div><b>후속</b><p>s2s 비교 PoC 구현·레이턴시/안전/감사 실측. 최종 채택 판단은 비교 리포트 이후.</p></div></div>
|
||
<div class="task-row"><div><span class="task-status s-done">DECIDED</span></div><div><b>서비스용 페르소나 음성 모델·권리 정책</b><p>소유자 결정(2026-06-30): 실서비스 음성은 상업 라이선스가 명확한 대체 TTS provider로 간다. Higgs 무참조 synthetic 샘플과 dev-only sample provider는 개발 플래그로만 유지하고, 실존 인물/성우 ref 사용 금지를 유지한다.</p></div><div><b>후속</b><p>대체 TTS provider 선정·통합, dev 플래그와 서비스 경로 분리 유지.</p></div></div>
|
||
<div class="task-row"><div><span class="task-status s-done">DECIDED</span></div><div><b>fast-loop 외부 API 허용 여부</b><p>소유자 결정(2026-06-30): 로컬 상주 분류기를 기본으로 하되 외부 Haiku/Solar 폴백을 허용한다. 외부 전송 시 데이터주권·로그 보존·동의 범위 가드를 함께 적용한다.</p></div><div><b>후속</b><p>외부 폴백 경로·전송 가드·관측 구현. 로컬 우선/외부 폴백 라우팅 정책 명시.</p></div></div>
|
||
<div class="task-row"><div><span class="task-status s-done">DECIDED</span></div><div><b>운영 보존기간·에스컬레이션 정책</b><p>소유자 결정(2026-06-30): 헬스 raw 90일·rollup 365일 보존, 우선순위 escalation은 운영자 수동 승인, SLA 미표기(관측 샘플 이력 유지), 담당 그룹 자동 배정 미도입.</p></div><div><b>후속</b><p>retention 기본값(raw 90/rollup 365) 적용·검증. escalation/그룹 자동화는 미도입으로 고정.</p></div></div>
|
||
<div class="task-row"><div><span class="task-status s-done">DECIDED</span></div><div><b>재귀학습 fine-tuning 범위</b><p>소유자 결정(2026-06-30): few-shot 자동갱신에 더해 fine-tuning까지 범위에 포함한다. 단 실제 fine-tuning 데이터셋은 동의 범위·추가 축어록 수급 결정이 닫힌 뒤에 구성한다.</p></div><div><b>후속</b><p>동의/데이터셋 게이트 충족 후 fine-tuning 파이프라인 구축. few-shot 자동갱신은 선행 가능.</p></div></div>
|
||
<div class="task-row"><div><span class="task-status s-done">DONE</span></div><div><b>자유연습 기본값</b><p>기본 ON + 피드백 권장 배지 + 교수자 토글로 결정했다. 미동의·미성년 개인정보 gate만 차단한다.</p></div><div><b>근거</b><p><code>docs/redteam/MASTERPLAN_REVISIONS.md</code> F-10 승인.</p></div></div>
|
||
<div class="task-row"><div><span class="task-status s-done">DECIDED</span></div><div><b>추가 축어록 수급 규모/일정</b><p>소유자 결정(2026-06-30): 추가 사례 20건을 1개월 내 수급한다. n=1 시드 한계 해소와 페르소나 다양성 확보가 목표다.</p></div><div><b>후속</b><p>실제 수급·동의·정제는 임상/한신대 거버넌스 게이트와 연계. 목표=20건/1개월.</p></div></div>
|
||
</div>
|
||
<div class="detail-blocks">
|
||
<div class="detail-block"><h4>완료라고 말하는 기준</h4><ul><li>실제 로그인 기반 E2E 증거가 있어야 한다.</li><li>DB 쿼리나 로그로 저장/권한/비용이 확인되어야 한다.</li><li>시각 확인만으로 기능 완료를 선언하지 않는다.</li></ul></div>
|
||
<div class="detail-block"><h4>문서 기준</h4><ul><li><code>MASTERPLAN</code>은 원본 보존 문서다.</li><li><code>MASTERPLAN_REVISIONS</code>가 Phase/IRB/음성 정정 기준이다.</li><li><code>dev_dashboard.html</code>은 팀장 판단용 현재판이다.</li></ul></div>
|
||
<div class="detail-block"><h4>운영 원칙</h4><ul><li>mock/demo 자산은 운영 번들에서 제거한다.</li><li>ignored runtime 파일은 커밋하지 않는다.</li><li>서버는 목적별 1개씩만 유지한다.</li><li><code>frontenddesign</code> 스킬이 있으면 먼저 쓰고, 없으면 <code>DESIGN_CONCEPT</code>를 SSOT로 따른다.</li><li>UX는 <code>docs/DESIGN_CONCEPT.md</code>를 기준으로 하고, fake 기록/통계는 금지한다.</li><li>OAuth secret은 출력하지 않고 환경 파일/인프라 설정에만 둔다.</li></ul></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</details>
|
||
|
||
<details class="fold" id="fold-narrative">
|
||
<summary><span class="fcaret" aria-hidden="true"></span>이번 세션 내러티브 (historical snapshot · 현재 숫자 아님)<span class="fmeta">team lead review</span></summary>
|
||
<div class="fold-inner">
|
||
<p style="color:var(--ink-2);font-size:13px;max-width:840px">
|
||
이번 세션은 seed persona 결합 제거, 비개발 fail-closed 설정, compose OAuth 주입, admin 모바일 overflow 수정, persona 인증 경계 보강, 전체 Playwright 재통과, 공개 API prod 전환까지 완료했다. 이번 이어받기에서는 내담자 AI 생성 턴의 provider/model/token/cost 텔레메트리를 <code>/turn</code>, SSE stream, 음성 생성 경로에서 DB <code>app.turns</code>까지 적재하도록 연결했고, gateway stream <code>done/error</code> 계약을 구조화했으며, P1 상담 시작→텍스트 stream→종료→리뷰 피드백 UI E2E와 저항 엔진 정량 회귀 테스트를 추가했다. 이어서 PII 마스킹 게이트 우회 차단, RBAC/IDOR·감사 insert 경로, 음성 paralinguistic 메타 저장, 공개 런타임 watchdog 스크립트와 runbook을 보강했다. 추가로 상주 engine session reuse, SAML provider 스캐폴드, <code>visible_to</code> 런타임 필터, EOT/TTS payload 계약까지 회귀화했다. 백엔드 회귀 테스트는 84개로 재검증했고, 페르소나 승인/반려 API와 교수 콘솔 검수 UI proof를 추가했다. Postgres RLS/audit live smoke checker와 s2s 2차 PoC 판단 메모도 준비했지만, live DB 실행과 s2s 채택 결정은 별도 증거가 필요하다. 이후 로컬 5175 런타임 재기동, admin 사용자 관리 E2E 안정화, 학습자 홈 0회 기록 UX, active session 실제 SSE 스트림 전환, 텍스트/음성 실패 턴 미저장, 직접 URL persona 우회 차단, Live2D demo 자산 제거, AI 운영 fail-closed 보강까지 전체 Playwright로 확인했다. 2026-06-26 15:18 KST 기준 공개 API 터널을 복구한 뒤 전체 Playwright 72개가 desktop/mobile/single-run 모두 통과했다. Cloudflare Pages production 배포 <code>2a58ca49</code>도 완료했고, 커스텀 도메인이 새 asset을 서빙하는 것을 확인했다. 과거 Mao/Haru Live2D demo 자산은 repo와 번들에서 제거했고, <code>/live2d/*</code> 운영 경로는 404로 차단했다. 현재 런타임은 prod API 8001, dev API 8000/8010, web 5173, engine gateway 9099, cloudflared tunnel 1개를 의도적으로 유지한다. 이번 라운드에서는 레이아웃 작업을 서브에이전트 전담으로 분리해 세션, 학습자 홈/리뷰, 관리자/교수자, 설정 화면을 overflow 수리 수준이 아니라 실제 작업 화면 재설계 수준으로 갱신했다. 통합 레이아웃 E2E는 desktop/mobile 54개를 최종 통과했고, 자세한 인수인계는 <code>docs/archive/ops/layout-redesign-handoff-2026-06-26.md</code>에 남겼다. 이번 이어받기에서는 핸드오프가 남긴 "실기기 시각 수용"을 엄격한 자동 게이트로 닫았다. 7개 권장 너비(390/720/861/900/1024/1280/1440)에서 초기 7개 재설계 화면의 가로 overflow 0·컨트롤 클립 0을 강제하는 <code>layout-visual-gate.spec.ts</code>(초기 7 passed/49장, 현재 빈 회기리뷰 포함 9 passed/63장)를 만들고, 화면별 1에이전트 병렬 시각 리뷰 → 6개 병렬 수정 에이전트 → 화면별 적대적 재검수까지 돌려 7/7 accept(critical/major 0, 회귀 0)를 확정했다. 학습자홈 1280 3열 가운데 컬럼 붕괴(critical)와 교수 'API 404' raw 배너를 포함한 결함을 모두 해소했고, 레이아웃 항목을 DONE으로 닫았다. 이번 로그인 복구 라운드에서는 dev/Tailnet OAuth direct start까지 <code>local_oauth_unavailable</code>로 막고, public OAuth state를 HttpOnly 쿠키에 묶인 서명 토큰으로 바꿔 API 재시작 후 콜백이 <code>invalid_state</code>로 실패하지 않게 했으며, callback 실패 reason/status 진단 로그와 화면 메시지 분기를 추가했다. 추가로 Google provider error를 <code>access_denied</code>/<code>provider_error</code>로 분리하고 로그인 화면에 reason code를 노출했다. 이어서 C1 사례개념화 워크시트는 리뷰 API·편집 UI·<code>app.case_worksheet</code> 저장/재조회 경로까지 2차로 연결했다. 공개 Google OAuth 실제 <code>/turn</code> 증명만 storageState 캡처 후 남아 있다.
|
||
</p>
|
||
</div>
|
||
</details>
|
||
</section>
|
||
</main>
|
||
<script>
|
||
(function(){
|
||
var $ = function(id){ return document.getElementById(id); };
|
||
var setTxt = function(id,v){ var e=$(id); if(e) e.textContent=v; };
|
||
|
||
// ── tabs ──
|
||
var tabButtons = Array.prototype.slice.call(document.querySelectorAll("[role='tab'][data-tab]"));
|
||
var tabPanels = Array.prototype.slice.call(document.querySelectorAll("[role='tabpanel']"));
|
||
function activateTab(btn){
|
||
tabButtons.forEach(function(b){ b.setAttribute("aria-selected", String(b===btn)); b.tabIndex = b===btn?0:-1; });
|
||
tabPanels.forEach(function(p){ p.hidden = p.id !== btn.getAttribute("aria-controls"); });
|
||
}
|
||
tabButtons.forEach(function(btn,i){
|
||
btn.addEventListener("click", function(){ activateTab(btn); });
|
||
btn.addEventListener("keydown", function(e){
|
||
if(["ArrowLeft","ArrowRight","Home","End"].indexOf(e.key)<0) return;
|
||
e.preventDefault(); var n=i;
|
||
if(e.key==="ArrowLeft") n=(i-1+tabButtons.length)%tabButtons.length;
|
||
if(e.key==="ArrowRight") n=(i+1)%tabButtons.length;
|
||
if(e.key==="Home") n=0; if(e.key==="End") n=tabButtons.length-1;
|
||
tabButtons[n].focus(); activateTab(tabButtons[n]);
|
||
});
|
||
});
|
||
|
||
// ── status card fold ──
|
||
Array.prototype.slice.call(document.querySelectorAll(".scard-head")).forEach(function(h){
|
||
h.addEventListener("click", function(){
|
||
var c=h.closest(".scard"); var open=c.classList.toggle("open");
|
||
h.setAttribute("aria-expanded", String(open));
|
||
});
|
||
});
|
||
|
||
// ── derive KPI / graphs from board cards ──
|
||
var scards = Array.prototype.slice.call(document.querySelectorAll(".scard"));
|
||
var total = scards.length;
|
||
var cnt = function(s){ return scards.filter(function(c){ return c.dataset.status===s; }).length; };
|
||
var done=cnt("done"), doing=cnt("doing"), planned=total-done-doing;
|
||
var critN = scards.filter(function(c){ return c.querySelector(".critbadge"); }).length;
|
||
var boardOwner = scards.filter(function(c){ return c.dataset.owner==="1"; }).length;
|
||
var owners = document.querySelectorAll('.ocard[data-owner="1"]').length;
|
||
|
||
setTxt("kpi-done",done); setTxt("kpi-doing",doing); setTxt("kpi-plan",planned);
|
||
setTxt("g-total",total); setTxt("lg-done",done); setTxt("lg-doing",doing); setTxt("lg-plan",planned);
|
||
var pct = total? Math.round(done/total*100):0;
|
||
setTxt("kpi-pct","전체의 "+pct+"%");
|
||
setTxt("kpi-crit","납품 필수 "+critN+"건 포함");
|
||
var critBadge=$("kpi-critbadge");
|
||
if(critBadge && critN){ critBadge.textContent="● 납품 필수 "+critN; critBadge.style.display="inline-flex"; }
|
||
setTxt("kpi-owner",owners); setTxt("pill-owner",owners);
|
||
|
||
// owner column counts
|
||
["block","decide","ext"].forEach(function(k){
|
||
var col=document.querySelector('[data-owner-col="'+k+'"]');
|
||
var c = col? col.querySelectorAll('.ocard[data-owner="1"]').length:0;
|
||
setTxt("ct-"+k, c);
|
||
});
|
||
|
||
// stacked bar
|
||
var sb=$("stackbar");
|
||
if(sb){
|
||
function seg(cls,v){ if(!v) return ""; return '<i class="'+cls+'" style="flex:'+v+' 1 0;">'+v+'</i>'; }
|
||
sb.innerHTML = seg("sb-done",done)+seg("sb-doing",doing)+seg("sb-plan",planned);
|
||
}
|
||
|
||
// tracks by category
|
||
var cats={}; var order=[];
|
||
scards.forEach(function(c){
|
||
var k=c.dataset.cat||"기타";
|
||
if(!cats[k]){ cats[k]={d:0,t:0}; order.push(k); }
|
||
cats[k].t++; if(c.dataset.status==="done") cats[k].d++;
|
||
});
|
||
order.sort(function(a,b){ return cats[b].t-cats[a].t; });
|
||
var tr=$("tracks");
|
||
if(tr){
|
||
tr.innerHTML = order.map(function(k){
|
||
var o=cats[k]; var p=Math.round(o.d/o.t*100);
|
||
return '<div class="track"><span class="tname">'+k+'</span><span class="tbar"><i style="width:'+p+'%"></i></span><span class="tnum">'+o.d+'/'+o.t+'</span></div>';
|
||
}).join("");
|
||
}
|
||
|
||
// ── filter ──
|
||
var counts = { all: total, done: done, doing: doing, planned: planned, owner: boardOwner, crit: critN };
|
||
Object.keys(counts).forEach(function(k){
|
||
var e=document.querySelector('[data-count="'+k+'"]'); if(e) e.textContent=counts[k];
|
||
});
|
||
var fbtns = Array.prototype.slice.call(document.querySelectorAll(".fbtn"));
|
||
function applyFilter(f){
|
||
scards.forEach(function(c){
|
||
var show = f==="all"
|
||
|| (f==="owner" ? c.dataset.owner==="1"
|
||
: (f==="crit" ? !!c.querySelector(".critbadge")
|
||
: c.dataset.status===f));
|
||
c.classList.toggle("hide", !show);
|
||
});
|
||
// hide empty track groups
|
||
Array.prototype.slice.call(document.querySelectorAll("#board .track-group")).forEach(function(g){
|
||
var any = Array.prototype.slice.call(g.querySelectorAll(".scard")).some(function(c){ return !c.classList.contains("hide"); });
|
||
g.style.display = any? "":"none";
|
||
});
|
||
}
|
||
fbtns.forEach(function(b){
|
||
b.addEventListener("click", function(){
|
||
fbtns.forEach(function(x){ x.setAttribute("aria-pressed", String(x===b)); });
|
||
applyFilter(b.dataset.filter);
|
||
});
|
||
});
|
||
|
||
// ── phase rail ──
|
||
var phases=[
|
||
{id:"P0",name:"기반·안전장치",pct:92,cls:"cur"},
|
||
{id:"P1",name:"MVP 코어루프",pct:78,cls:""},
|
||
{id:"P2a",name:"권한·페르소나",pct:74,cls:""},
|
||
{id:"P2b",name:"음성 캐스케이드",pct:40,cls:""},
|
||
{id:"P3",name:"파일럿·효과",pct:6,cls:"idle"},
|
||
{id:"AOS",name:"성과·동맹 OS",pct:22,cls:"cur"}
|
||
];
|
||
var rail=$("phase-rail");
|
||
if(rail){
|
||
rail.innerHTML = phases.map(function(p){
|
||
return '<div class="pr-seg '+p.cls+'"><div class="pr-bar"><div class="pr-fill" style="width:'+p.pct+'%"></div></div>'
|
||
+'<div class="pr-meta"><span class="pr-id">'+p.id+'</span><span class="pr-pct">'+p.pct+'%</span></div>'
|
||
+'<span class="pr-name">'+p.name+'</span></div>';
|
||
}).join("");
|
||
}
|
||
})();
|
||
</script>
|
||
</body>
|
||
</html>
|