G0~G8 성과·동맹 측정 OS 작업 일괄 고정

8월 7일까지 워킹트리에만 남아 있던 미커밋 작업을 커밋한다. 여러 사본
폴더(worktree·clone)에 흩어져 있던 중간 스냅샷을 정리하기 전에 원본을
git 이력으로 고정하는 것이 목적이다.

- contracts/routes/services: measurement, outcome_trajectory, rupture_repair,
  deliberate_practice, calibration_transfer, supervision_research,
  multimodal_alliance, continuous_improvement 계열 신규 모듈과 테스트
- infra/db/init: 07~16 마이그레이션(측정 기반~calibration transfer 실행)
- apps/web: 세션 리뷰 카드·관리 화면·E2E 스펙 추가
- docs/ops: G0~G8 라이브 통합·배포·롤백 증거 문서와 evidence JSON/PNG
- scripts: smoke·ledger·릴리스 에이전트·NAS 프리뷰 운영 스크립트

engine.public 로그 .bak과 apps/web/test-results 산출물은 커밋에서 제외했다.
This commit is contained in:
Yun Chan 2026-08-08 01:30:53 +09:00
parent 93dd8f82d7
commit 16e791e044
390 changed files with 243188 additions and 499 deletions

View file

@ -59,6 +59,33 @@
var(--glass-canvas);
color: var(--text-strong);
}
.sx-page--active:has(.sx-timebar) {
grid-template-rows: min-content min-content min-content minmax(0, 1fr) min-content;
}
.sx-page--active.sx-alliance-gated {
grid-template-rows: min-content minmax(0, 1fr);
}
.sx-page--active.sx-alliance-gated .sx-grid {
display: none;
}
.sx-page--active.sx-alliance-gated .sx-controlbar {
display: none;
}
.sx-page--active.sx-alliance-gated .sx-alliance-checkpoint {
height: 100%;
min-height: 0;
margin-top: 0;
overflow: hidden;
align-self: stretch;
}
.sx-page--active.sx-alliance-gated .sx-alliance-checkpoint.is-expanded {
display: grid;
grid-template-rows: auto minmax(0, 1fr) auto auto;
}
.sx-page--active.sx-alliance-gated .sx-alliance-checkpoint__axes {
align-content: start;
overflow-y: auto;
}
[data-theme="light"] .sx-page--active {
color-scheme: light;
}
@ -1232,6 +1259,11 @@
display: flex;
flex-direction: column;
}
.sx-utt__status.is-live {
border-color: color-mix(in srgb, var(--accent-deep) 28%, transparent);
background: color-mix(in srgb, var(--accent-tint) 76%, var(--paper));
color: var(--accent-deep);
}
.sx-signal__head {
order: 0;
margin-bottom: 9px;
@ -2129,6 +2161,57 @@
overflow: hidden;
text-overflow: ellipsis;
}
.sx-mic-block__shortcut {
display: inline-flex;
align-items: center;
gap: 3px;
margin-top: 3px;
color: var(--text-muted);
font-size: 10.5px;
line-height: 1.3;
}
.sx-mic-block__shortcut kbd {
min-width: 19px;
padding: 1px 4px;
border: 1px solid var(--border-strong);
border-radius: 4px;
background: var(--bg-surface-2);
color: var(--text-body);
font-family: var(--font-mono);
font-size: 9.5px;
font-weight: 700;
text-align: center;
box-shadow: inset 0 -1px 0 color-mix(in srgb, var(--text-muted) 18%, transparent);
}
.sx-mic-block__disclosure,
.sx-controlbar__voice-disclosure,
.sx-prestart__voice-disclosure {
color: var(--text-muted);
font-size: 10.5px;
line-height: 1.4;
}
.sx-mic-block__disclosure,
.sx-controlbar__voice-disclosure {
align-items: flex-start;
gap: 5px;
}
.sx-mic-block__disclosure {
display: flex;
margin-top: 3px;
white-space: normal;
text-wrap: pretty;
}
.sx-mic-block__disclosure svg,
.sx-controlbar__voice-disclosure svg,
.sx-prestart__voice-disclosure svg {
flex: 0 0 auto;
margin-top: 1px;
color: var(--accent-deep);
}
.sx-controlbar__voice-disclosure {
display: none;
margin: 0;
}
.sx-voice-skip {
flex: none;
height: 34px;
@ -2676,6 +2759,311 @@
line-height: 1.6;
max-width: 58ch;
}
.sx-voice-consent {
position: fixed;
inset: 0;
z-index: 82;
display: grid;
place-items: center;
padding: var(--sp-4);
background: rgba(5, 12, 10, 0.58);
backdrop-filter: blur(9px);
}
.sx-voice-consent__panel {
box-sizing: border-box;
width: min(100%, 560px);
max-height: min(760px, calc(100% - 32px));
overflow: hidden;
display: grid;
grid-template-rows: auto minmax(0, 1fr) auto;
padding: clamp(20px, 4vw, 28px);
border: 1px solid var(--border-strong);
border-radius: var(--radius-lg);
background: var(--bg-surface);
box-shadow: 0 28px 78px color-mix(in srgb, var(--bg-canvas) 58%, transparent);
color: var(--text-body);
}
.sx-voice-consent__body {
min-height: 0;
overflow-y: auto;
padding-right: 3px;
scrollbar-gutter: stable;
}
.sx-voice-consent__head {
display: grid;
grid-template-columns: auto minmax(0, 1fr);
gap: var(--sp-3);
align-items: start;
}
.sx-voice-consent__icon {
width: 40px;
height: 40px;
border-radius: var(--radius);
display: inline-flex;
align-items: center;
justify-content: center;
background: var(--accent-tint);
color: var(--accent-deep);
}
.sx-voice-consent h2 {
margin: 0;
color: var(--text-strong);
font-size: 20px;
font-weight: 760;
line-height: 1.35;
}
.sx-voice-consent p {
margin: 6px 0 0;
color: var(--text-muted);
font-size: var(--fs-sm);
line-height: 1.6;
}
.sx-voice-consent__facts {
margin: var(--sp-5) 0 0;
}
.sx-voice-consent__facts > div {
display: grid;
grid-template-columns: 92px minmax(0, 1fr);
gap: var(--sp-3);
padding: 11px 0;
border-bottom: 1px solid var(--border-subtle);
}
.sx-voice-consent__facts > div:first-child {
border-top: 1px solid var(--border-subtle);
}
.sx-voice-consent__facts dt,
.sx-voice-consent__facts dd {
margin: 0;
font-size: 12.5px;
line-height: 1.55;
}
.sx-voice-consent__facts dt {
color: var(--text-strong);
font-weight: 740;
}
.sx-voice-consent__facts dd {
color: var(--text-body);
}
.sx-voice-consent .sx-voice-consent__boundary {
margin-top: var(--sp-4);
padding: 11px 12px;
border-radius: var(--radius);
background: color-mix(in srgb, var(--accent-tint) 54%, var(--bg-surface-2));
color: var(--text-body);
font-size: 11.5px;
}
.sx-voice-consent .sx-voice-consent__error {
margin-top: var(--sp-3);
padding: 10px 12px;
border: 1px solid color-mix(in srgb, var(--danger) 42%, var(--border-subtle));
border-radius: var(--radius);
background: color-mix(in srgb, var(--danger) 10%, var(--bg-surface));
color: var(--danger);
font-size: 12px;
font-weight: 680;
}
.sx-voice-consent__actions {
display: flex;
justify-content: flex-end;
gap: var(--sp-2);
margin-top: var(--sp-5);
padding-top: var(--sp-3);
border-top: 1px solid var(--border-subtle);
flex-wrap: wrap;
}
.sx-voice-consent__secondary,
.sx-voice-consent__primary {
min-height: 42px;
padding: 10px 15px;
border-radius: var(--radius);
font-family: var(--font-sans);
font-size: var(--fs-sm);
font-weight: 720;
cursor: pointer;
transition:
transform var(--dur-fast) var(--ease-out),
background var(--dur-base) var(--ease-out),
border-color var(--dur-base) var(--ease-out);
}
.sx-voice-consent__secondary {
border: 1px solid var(--border-strong);
background: var(--bg-surface-2);
color: var(--text-body);
}
.sx-voice-consent__primary {
border: 1px solid var(--accent-deep);
background: var(--accent-deep);
color: var(--text-on-accent);
}
.sx-voice-consent__secondary:hover {
border-color: var(--border-focus);
color: var(--text-strong);
}
.sx-voice-consent__primary:hover {
background: var(--accent);
}
.sx-voice-consent__secondary:active,
.sx-voice-consent__primary:active {
transform: scale(0.98);
}
.sx-voice-consent__secondary:disabled,
.sx-voice-consent__primary:disabled {
cursor: wait;
opacity: 0.68;
transform: none;
}
@media (max-width: 560px) {
.sx-voice-consent {
place-items: end center;
padding: 10px;
}
.sx-voice-consent__panel {
max-height: calc(100% - 20px);
border-radius: 16px;
padding-bottom: max(16px, env(safe-area-inset-bottom));
}
.sx-voice-consent__facts > div {
grid-template-columns: 1fr;
gap: 3px;
}
.sx-voice-consent__actions {
display: grid;
grid-template-columns: minmax(0, 0.86fr) minmax(0, 1.14fr);
}
.sx-voice-consent__actions button {
width: 100%;
}
}
.sx-voice-retry {
flex: none;
min-height: 38px;
padding: 0 12px;
border: 1px solid color-mix(in srgb, var(--accent-deep) 44%, var(--border-strong));
border-radius: var(--radius-sm);
background: color-mix(in srgb, var(--accent-tint) 74%, var(--bg-surface));
color: var(--accent-deep);
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
font-family: var(--font-sans);
font-size: 12px;
font-weight: 800;
cursor: pointer;
white-space: nowrap;
}
.sx-voice-retry:hover {
border-color: var(--border-focus);
background: var(--accent-tint);
}
.sx-sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
.sx-voice-practice-context {
display: flex;
align-items: flex-start;
gap: var(--sp-3);
margin-top: var(--sp-4);
padding: var(--sp-3);
border: 1px solid var(--border-strong);
border-radius: var(--radius-sm);
color: var(--text-body);
background: var(--accent-tint);
}
.sx-voice-practice-context > svg {
flex: none;
color: var(--text-accent);
}
.sx-voice-practice-context h3,
.sx-voice-practice-context p,
.sx-voice-practice-context small {
display: block;
}
.sx-voice-practice-context h3 {
margin: 0;
color: var(--text-strong);
font-size: var(--fs-sm);
}
.sx-voice-practice-context p {
margin: 4px 0 0;
color: var(--text-body);
font-size: var(--fs-sm);
}
.sx-voice-practice-context small {
margin-top: 3px;
color: var(--text-muted);
font-size: var(--fs-xs);
line-height: 1.5;
}
.sx-practice-launch-context {
border-color: color-mix(in srgb, var(--accent) 45%, var(--border-subtle));
background: color-mix(in srgb, var(--accent-tint) 86%, var(--bg-surface));
}
.sx-prestart__settings {
min-width: 0;
margin-top: var(--sp-3);
}
.sx-prestart__settings:not(.is-prescribed) > summary {
display: none;
}
.sx-prestart__settings > summary {
min-width: 0;
min-height: 44px;
display: grid;
grid-template-columns: auto minmax(0, 1fr) auto;
align-items: center;
gap: 10px;
padding: 10px 12px;
border: 1px solid var(--glass-inset-border);
border-radius: var(--radius-sm);
background: var(--glass-surface-inset);
color: var(--text-body);
cursor: pointer;
list-style: none;
}
.sx-prestart__settings > summary::-webkit-details-marker {
display: none;
}
.sx-prestart__settings > summary span {
color: var(--accent-deep);
font-size: 11.5px;
font-weight: 760;
}
.sx-prestart__settings > summary b {
min-width: 0;
overflow: hidden;
color: var(--text-strong);
font-size: 12.5px;
font-weight: 720;
text-overflow: ellipsis;
white-space: nowrap;
}
.sx-prestart__settings > summary svg {
transition: transform var(--dur-base) var(--ease-out);
}
.sx-prestart__settings[open] > summary svg {
transform: rotate(90deg);
}
.sx-prestart__settings > summary:focus-visible {
outline: 2px solid var(--focus-ring);
outline-offset: 2px;
}
.sx-prestart__settings-body {
min-width: 0;
}
.sx-prestart__settings.is-prescribed[open] .sx-prestart__settings-body {
padding-top: var(--sp-2);
}
/* 사실 목록. repeat(3, 1fr) 고정이면 항목이 1개뿐인 상태(페르소나 로딩/폴백)에서도
칸이 1/3 쪼개져 값이 세로로 길게 잘렸다(결함 #2). auto-fit 으로 바꿔 남는 트랙을
접고, 항목 ·폭에 따라 1~3열로 스스로 접히게 한다. */
@ -2854,6 +3242,15 @@
.sx-consent .vg-btn {
justify-self: start;
}
.sx-prestart__voice-disclosure {
width: min(100%, 520px);
display: flex;
align-items: flex-start;
gap: 7px;
margin: var(--sp-3) 0 0;
font-size: 11.5px;
text-wrap: pretty;
}
.sx-prestart__actions {
display: flex;
align-items: center;
@ -3316,7 +3713,13 @@
display: none;
}
.sx-page--active.sx-feedback-coached .sx-grid {
grid-template-rows: auto minmax(0, 1fr) auto;
grid-template-rows: max-content minmax(360px, max-content) max-content;
align-content: start;
overflow-y: auto;
overscroll-behavior: contain;
}
.sx-page--active.sx-feedback-coached .sx-col-center {
min-height: 360px;
}
.sx-page--active.sx-feedback-coached .sx-col-right {
display: flex;
@ -3373,6 +3776,7 @@
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
grid-template-areas:
"voice-disclosure voice-disclosure"
"mic actions"
"mode mode";
gap: 8px;
@ -3397,6 +3801,15 @@
.sx-page--active .sx-mic-block__h {
display: none;
}
.sx-page--active .sx-mic-block__disclosure {
display: none;
}
.sx-page--active .sx-controlbar__voice-disclosure {
grid-area: voice-disclosure;
display: flex;
max-width: 100%;
padding-inline: 2px;
}
.sx-page--active .sx-seg-block {
grid-area: mode;
gap: 3px;
@ -3422,6 +3835,12 @@
width: 44px;
height: 44px;
}
.sx-page--active .sx-voice-retry {
min-height: 44px;
}
.sx-page--active .sx-mic-block.is-recovery .sx-mic-block__ms {
display: none;
}
.sx-page--active .sx-cb-actions {
grid-area: actions;
margin-left: 0;
@ -3675,6 +4094,32 @@
.sx-page--active .sx-stage {
padding: 6px 8px;
}
.sx-page--active .sx-col-center {
grid-template-rows: minmax(104px, auto) minmax(0, 1fr);
}
.sx-page--active .sx-mobile-context {
gap: 6px;
padding: 7px;
}
.sx-page--active .sx-mobile-context__brief {
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 6px;
}
.sx-page--active .sx-mobile-context__brief span {
padding: 6px 7px;
}
/* 현재 상태와 감정은 모바일 요약·아바타 자체가 이미 전달한다. 좁은 stage의
번째 행을 중복 문구로 채우면 아바타와 겹치므로 420px 이하에서는 제거한다. */
.sx-page--active .sx-stage__now {
display: none;
}
.sx-page--active .sx-stage__client {
align-self: center;
gap: 4px;
}
.sx-page--active .sx-stage__client p {
-webkit-line-clamp: 2;
}
.sx-page--active .sx-orb {
inset: -5px;
}
@ -3694,7 +4139,9 @@
padding: 7px 6px;
}
.sx-page--active .sx-transcript__scroll {
min-height: 152px;
/* 축어록은 남은 높이를 쓰는 flex 자식이다. 고정 최소 높이를 두면 390px 폭에서도
텍스트 입력 폴백이 카드 아래로 잘리므로, 카드 안에서 유연하게 스크롤시킨다. */
min-height: 0;
}
.sx-page--active .sx-controlbar {
padding: 8px;
@ -3741,6 +4188,19 @@
.sx-page--prestart .sx-prestart__visual {
padding: 6px 8px;
}
.sx-page--prestart .sx-prestart--prescribed .sx-prestart__visual {
display: none;
}
.sx-page--prestart .sx-prestart--prescribed .sx-prestart__actions {
position: sticky;
z-index: 2;
bottom: max(8px, env(safe-area-inset-bottom));
padding: 6px;
border: 1px solid var(--glass-inset-border);
border-radius: var(--radius);
background: var(--glass-surface);
box-shadow: var(--glass-shadow);
}
}
@media (max-width: 1180px) and (max-height: 820px) {
@ -3859,6 +4319,16 @@
.sx-page--active .sx-transcript__scroll {
min-height: 110px;
}
.sx-page--active .sx-transcript {
padding: 5px 8px;
}
.sx-page--active .sx-transcript__head {
margin-bottom: 0;
}
.sx-page--active .sx-compose {
margin-top: 1px;
padding-top: 1px;
}
.sx-page--active .sx-transcript__empty {
grid-template-columns: minmax(0, 1fr);
gap: 2px;
@ -3913,6 +4383,17 @@
.sx-page--active .sx-mobile-context__row--live {
display: none;
}
.sx-page--active .sx-controlbar {
gap: 4px;
padding: 6px;
}
.sx-page--active .sx-controlbar__voice-disclosure {
overflow: hidden;
font-size: 9.5px;
line-height: 1.2;
text-overflow: ellipsis;
white-space: nowrap;
}
.sx-page--active .sx-col-center {
grid-template-rows: minmax(78px, auto) minmax(0, 1fr);
gap: 8px;
@ -3945,15 +4426,15 @@
padding: 3px 6px;
}
.sx-page--active .sx-transcript__head {
margin-bottom: 2px;
margin-bottom: 0;
}
.sx-page--active .sx-transcript__empty {
gap: 8px;
padding: 8px 6px;
}
.sx-page--active .sx-compose {
margin-top: 3px;
padding-top: 3px;
margin-top: 1px;
padding-top: 1px;
}
.sx-page--active .sx-compose textarea {
min-height: 36px;
@ -4206,9 +4687,9 @@
white-space: nowrap;
padding: 7px 12px;
border-radius: 999px;
border: 1px solid rgba(151, 175, 166, 0.5);
background: var(--bg-panel, rgba(16, 22, 21, 0.97));
color: var(--text-strong, #eef4f2);
border: 1px solid color-mix(in srgb, var(--text-on-accent) 42%, transparent);
background: var(--accent-deep);
color: var(--text-on-accent);
font-size: 12px;
font-weight: 640;
cursor: pointer;
@ -4222,21 +4703,21 @@
left: 50%;
transform: translateX(-50%);
border: 5px solid transparent;
border-top-color: rgba(151, 175, 166, 0.5);
border-top-color: color-mix(in srgb, var(--text-on-accent) 42%, transparent);
}
.sx-coach-nudge:hover {
border-color: rgba(151, 175, 166, 0.8);
border-color: color-mix(in srgb, var(--text-on-accent) 72%, transparent);
}
.sx-coach-nudge em {
font-style: normal;
font-weight: 780;
color: var(--accent-bright, #8fc0b4);
color: currentColor;
}
.sx-coach-nudge__dot {
width: 7px;
height: 7px;
border-radius: 50%;
background: var(--accent-bright, #8fc0b4);
background: currentColor;
animation: sx-timebar-pulse 1.8s ease-out infinite;
}
.sx-segmented__badge.is-pulsing {
@ -4322,6 +4803,95 @@
margin-left: auto;
}
/* Alliance checkpoint가 세션바와 그리드 사이에 들어오면 명시적으로 행을 소유한다.
하단의 대형 화면 재설계 규칙보다 구체적인 selector로 implicit-grid 겹침을 막는다. */
.sx-page--active:has(> .sx-alliance-checkpoint-slot) {
grid-template-rows: min-content min-content minmax(0, 1fr) min-content;
}
.sx-page--active:has(> .sx-timebar):has(> .sx-alliance-checkpoint-slot) {
grid-template-rows: min-content min-content min-content minmax(0, 1fr) min-content;
}
.sx-page--active.sx-alliance-gated {
grid-template-rows: min-content minmax(0, 1fr);
}
@media (min-width: 1280px) {
.sx-page--active:has(> .sx-alliance-checkpoint-slot) {
grid-template-rows: 80px min-content minmax(0, 1fr) 116px;
}
.sx-page--active:has(> .sx-timebar):has(> .sx-alliance-checkpoint-slot) {
grid-template-rows: 80px min-content min-content minmax(0, 1fr) 116px;
}
.sx-page--active.sx-alliance-gated {
grid-template-rows: 80px minmax(0, 1fr);
}
}
@media (min-width: 1280px) and (max-height: 820px) {
.sx-page--active:has(> .sx-alliance-checkpoint-slot) {
grid-template-rows: 52px min-content minmax(0, 1fr) 82px;
}
.sx-page--active:has(> .sx-timebar):has(> .sx-alliance-checkpoint-slot) {
grid-template-rows: 52px min-content min-content minmax(0, 1fr) 82px;
}
.sx-page--active.sx-alliance-gated {
grid-template-rows: 52px minmax(0, 1fr);
}
}
/* 320×568 같은 저높이 폰에서는 mid 자기점검의 설명/버튼 줄바꿈만으로
축어록 행이 사라진다. G1 상태는 그대로 두고 collapsed 표현만 줄로 압축한다. */
@media (max-width: 420px) and (max-height: 620px) {
.sx-page--active:has(> .sx-alliance-checkpoint-slot) {
grid-template-rows: min-content 40px minmax(0, 1fr) min-content;
}
.sx-page--active > .sx-alliance-checkpoint.is-collapsed {
display: grid;
grid-template-columns: auto minmax(0, 1fr) auto;
align-items: center;
gap: 7px;
width: 100%;
min-height: 40px;
margin: 0;
padding: 5px 7px;
flex-wrap: nowrap;
}
.sx-page--active > .sx-alliance-checkpoint.is-collapsed > div > span {
display: none;
}
.sx-page--active > .sx-alliance-checkpoint.is-collapsed > div > b {
overflow: hidden;
font-size: 11px;
text-overflow: ellipsis;
white-space: nowrap;
}
.sx-page--active > .sx-alliance-checkpoint.is-collapsed .vg-btn {
width: auto;
min-height: 30px;
padding: 6px 8px;
font-size: 11px;
}
.sx-page--active .sx-coach-nudge {
gap: 5px;
margin-bottom: 1px;
padding: 5px 9px;
font-size: 0;
}
.sx-page--active .sx-coach-nudge em {
font-size: 11px;
}
/* 낮은 화면에서는 현재 내담자/마이크/진행 상태만 남기고, 이미 시작 전에
확인한 호소·이론 요약은 접는다. 확보한 높이는 축어록과 텍스트 폴백에 준다. */
.sx-page--active .sx-mobile-context__brief {
display: none;
}
/* 110px 고정 최소 높이는 입력창을 카드 바깥으로 밀어냈다. 중앙 그리드가
남은 높이를 배분하게 하되, 스크롤과 입력창 모두 transcript 안에 둔다. */
.sx-page--active .sx-transcript__scroll {
min-height: 0;
}
}
.vg-main:has(.sx-page--active) {
overflow: hidden;
scrollbar-gutter: auto;
@ -5015,6 +5585,25 @@
border-color: rgba(159, 142, 111, 0.24);
background: rgba(255, 255, 255, 0.5);
}
[data-theme="light"] .sx-page--active .sx-mic.is-off {
border-color: #7a857c;
background: #eef1ed;
color: #245f50;
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.92),
0 5px 15px rgba(45, 67, 57, 0.1);
}
[data-theme="light"] .sx-page--active .sx-mic.is-off:hover {
border-color: #557064;
background: #e3e9e4;
color: #164f42;
}
[data-theme="light"] .sx-page--active .sx-mic.is-on {
border-color: #176b56;
background: #176b56;
color: #f5fff9;
box-shadow: 0 7px 20px rgba(23, 107, 86, 0.24);
}
.sx-page--active .sx-mic-block__l {
color: #181e1a;
font-size: 16px;