회기 원장 상태 복원 보강
This commit is contained in:
parent
13d1c39a33
commit
9a7dd98cb6
7 changed files with 101 additions and 32 deletions
|
|
@ -1324,7 +1324,7 @@ test.describe("학습자 자기주도 전체 루프 — 실제 src UI / route fi
|
||||||
await page
|
await page
|
||||||
.getByRole("button", { name: "이번 회기 연습 근거 확인" })
|
.getByRole("button", { name: "이번 회기 연습 근거 확인" })
|
||||||
.click();
|
.click();
|
||||||
const observation = page.locator(".dp-runtime-observation");
|
let observation = page.locator(".dp-runtime-observation");
|
||||||
await expect(observation).toBeVisible();
|
await expect(observation).toBeVisible();
|
||||||
await expect(
|
await expect(
|
||||||
observation.getByRole("heading", {
|
observation.getByRole("heading", {
|
||||||
|
|
@ -1374,6 +1374,16 @@ test.describe("학습자 자기주도 전체 루프 — 실제 src UI / route fi
|
||||||
await observation.scrollIntoViewIfNeeded();
|
await observation.scrollIntoViewIfNeeded();
|
||||||
await capture(page, testInfo.project.name, "repractice-observation");
|
await capture(page, testInfo.project.name, "repractice-observation");
|
||||||
|
|
||||||
|
await page.reload();
|
||||||
|
observation = page.locator(".dp-runtime-observation");
|
||||||
|
await expect(observation).toBeVisible();
|
||||||
|
await expect(observation).toContainText("원장 반영됨");
|
||||||
|
await expect(observation).toContainText("현재 원장 상태");
|
||||||
|
await expect(observation).toContainText(
|
||||||
|
"이미 기록된 이 회기 근거를 원장에서 다시 불러왔습니다.",
|
||||||
|
);
|
||||||
|
await expect(observation).not.toContainText("학습자만 실행");
|
||||||
|
|
||||||
await observation
|
await observation
|
||||||
.getByRole("button", { name: "반영 상태 다시 확인" })
|
.getByRole("button", { name: "반영 상태 다시 확인" })
|
||||||
.click();
|
.click();
|
||||||
|
|
|
||||||
|
|
@ -216,16 +216,22 @@ function latestEpisodeFor(
|
||||||
prescription: PracticePrescriptionItem,
|
prescription: PracticePrescriptionItem,
|
||||||
episodes: PracticeEpisodeItem[],
|
episodes: PracticeEpisodeItem[],
|
||||||
): PracticeEpisodeItem | null {
|
): PracticeEpisodeItem | null {
|
||||||
const matching = episodes.filter((episode) => {
|
const matching = episodes.filter((episode) =>
|
||||||
|
episodeMatchesPrescription(episode, prescription),
|
||||||
|
);
|
||||||
|
return matching.at(-1) ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function episodeMatchesPrescription(
|
||||||
|
episode: PracticeEpisodeItem,
|
||||||
|
prescription: PracticePrescriptionItem,
|
||||||
|
): boolean {
|
||||||
const payload = episode.assessment_payload as Record<string, unknown>;
|
const payload = episode.assessment_payload as Record<string, unknown>;
|
||||||
return (
|
return (
|
||||||
payload.prescription_id === prescription.prescription_key ||
|
payload.prescription_id === prescription.prescription_key ||
|
||||||
payload.prescription_id ===
|
payload.prescription_id === prescription.prescription_payload.prescription_id ||
|
||||||
prescription.prescription_payload.prescription_id ||
|
|
||||||
payload.competency_id === prescription.competency_id
|
payload.competency_id === prescription.competency_id
|
||||||
);
|
);
|
||||||
});
|
|
||||||
return matching.at(-1) ?? null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface RuntimeObservationSnapshot {
|
interface RuntimeObservationSnapshot {
|
||||||
|
|
@ -272,6 +278,24 @@ function runtimeObservationSnapshot(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function runtimeEpisodeForSession(
|
||||||
|
data: DeliberatePracticeReadModel,
|
||||||
|
intent: PracticeLaunchIntent,
|
||||||
|
sessionId: string,
|
||||||
|
): PracticeEpisodeItem | null {
|
||||||
|
const prescription = prescriptionForLaunchIntent(data, intent);
|
||||||
|
if (!prescription) return null;
|
||||||
|
return (
|
||||||
|
data.episodes
|
||||||
|
.filter(
|
||||||
|
(episode) =>
|
||||||
|
episode.session_id === sessionId &&
|
||||||
|
episodeMatchesPrescription(episode, prescription),
|
||||||
|
)
|
||||||
|
.at(-1) ?? null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function prioritizedPrescriptions(
|
function prioritizedPrescriptions(
|
||||||
data: DeliberatePracticeReadModel,
|
data: DeliberatePracticeReadModel,
|
||||||
): PracticePrescriptionItem[] {
|
): PracticePrescriptionItem[] {
|
||||||
|
|
@ -974,16 +998,25 @@ function RuntimePracticeObservation({
|
||||||
data: DeliberatePracticeReadModel;
|
data: DeliberatePracticeReadModel;
|
||||||
onObserved: () => void;
|
onObserved: () => void;
|
||||||
}) {
|
}) {
|
||||||
|
const persistedRuntimeEpisode = runtimeEpisodeForSession(
|
||||||
|
data,
|
||||||
|
intent,
|
||||||
|
sessionId,
|
||||||
|
);
|
||||||
const [baseline, setBaseline] = useState<RuntimeObservationSnapshot>(() =>
|
const [baseline, setBaseline] = useState<RuntimeObservationSnapshot>(() =>
|
||||||
runtimeObservationSnapshot(data, intent),
|
runtimeObservationSnapshot(data, intent),
|
||||||
);
|
);
|
||||||
const [state, setState] = useState<RuntimeObservationState>("idle");
|
const [state, setState] = useState<RuntimeObservationState>(() =>
|
||||||
|
persistedRuntimeEpisode ? "success" : "idle",
|
||||||
|
);
|
||||||
const [result, setResult] =
|
const [result, setResult] =
|
||||||
useState<PracticeAttemptSubmissionResponse | null>(null);
|
useState<PracticeAttemptSubmissionResponse | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setBaseline(runtimeObservationSnapshot(data, intent));
|
setBaseline(runtimeObservationSnapshot(data, intent));
|
||||||
setState("idle");
|
setState(
|
||||||
|
runtimeEpisodeForSession(data, intent, sessionId) ? "success" : "idle",
|
||||||
|
);
|
||||||
setResult(null);
|
setResult(null);
|
||||||
// read model reload는 반영 전 기준을 바꾸지 않는다. 새 회기나 새 처방만 초기화한다.
|
// read model reload는 반영 전 기준을 바꾸지 않는다. 새 회기나 새 처방만 초기화한다.
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
|
@ -992,6 +1025,8 @@ function RuntimePracticeObservation({
|
||||||
const connectedPrescription = prescriptionForLaunchIntent(data, intent);
|
const connectedPrescription = prescriptionForLaunchIntent(data, intent);
|
||||||
const current = runtimeObservationSnapshot(data, intent);
|
const current = runtimeObservationSnapshot(data, intent);
|
||||||
const currentProgress = result?.progress ?? current.progress;
|
const currentProgress = result?.progress ?? current.progress;
|
||||||
|
const ledgerRecorded = Boolean(result || persistedRuntimeEpisode);
|
||||||
|
const hydratedFromLedger = Boolean(persistedRuntimeEpisode && !result);
|
||||||
|
|
||||||
if (intent.kind === "transfer") {
|
if (intent.kind === "transfer") {
|
||||||
return (
|
return (
|
||||||
|
|
@ -1085,12 +1120,23 @@ function RuntimePracticeObservation({
|
||||||
관찰합니다.
|
관찰합니다.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Badge tone={state === "success" ? "accent" : "neutral"}>
|
<Badge tone={ledgerRecorded ? "accent" : "neutral"}>
|
||||||
{state === "success" ? "원장 반영됨" : "학습자만 실행"}
|
{ledgerRecorded ? "원장 반영됨" : "학습자만 실행"}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="dp-runtime-observation__comparison">
|
<div className="dp-runtime-observation__comparison">
|
||||||
|
{hydratedFromLedger ? (
|
||||||
|
<section className="is-after">
|
||||||
|
<span>현재 원장 상태</span>
|
||||||
|
<strong>{PROGRESS_COPY[currentProgress].label}</strong>
|
||||||
|
<small>
|
||||||
|
익숙한 장면 근거 {current.familiarDemonstrations}회, 새 장면 근거{" "}
|
||||||
|
{current.unseenTransferDemonstrations}회
|
||||||
|
</small>
|
||||||
|
</section>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
<section>
|
<section>
|
||||||
<span>반영 전</span>
|
<span>반영 전</span>
|
||||||
<strong>{PROGRESS_COPY[baseline.progress].title}</strong>
|
<strong>{PROGRESS_COPY[baseline.progress].title}</strong>
|
||||||
|
|
@ -1109,6 +1155,8 @@ function RuntimePracticeObservation({
|
||||||
</small>
|
</small>
|
||||||
</section>
|
</section>
|
||||||
) : null}
|
) : null}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{state === "waiting" ? (
|
{state === "waiting" ? (
|
||||||
|
|
@ -1126,6 +1174,8 @@ function RuntimePracticeObservation({
|
||||||
<p>
|
<p>
|
||||||
{result?.idempotent_replay
|
{result?.idempotent_replay
|
||||||
? "같은 회기 근거를 중복 없이 확인했습니다."
|
? "같은 회기 근거를 중복 없이 확인했습니다."
|
||||||
|
: hydratedFromLedger
|
||||||
|
? "이미 기록된 이 회기 근거를 원장에서 다시 불러왔습니다."
|
||||||
: "새 관찰 근거를 원장에 반영하고 최신 진행 상태를 다시 불러왔습니다."}
|
: "새 관찰 근거를 원장에 반영하고 최신 진행 상태를 다시 불러왔습니다."}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -685,7 +685,10 @@ route fixture는 90건이므로 전체 숫자를 실제 회기 폐루프로 부
|
||||||
전용 engine/DB/API/Web을 만들고 같은 학습자의 SSE→review→G4/G5 0→1→1과 returned-practice desktop/mobile 4건을
|
전용 engine/DB/API/Web을 만들고 같은 학습자의 SSE→review→G4/G5 0→1→1과 returned-practice desktop/mobile 4건을
|
||||||
검증한 뒤 sentinel resource·PID·listener·temp 잔여 0을 영수증으로 남긴다. 공개 8001/55432/9099와 NAS는 금지다.
|
검증한 뒤 sentinel resource·PID·listener·temp 잔여 0을 영수증으로 남긴다. 공개 8001/55432/9099와 NAS는 금지다.
|
||||||
최초 세 full run은 SSE body replay, durable turn 전 종료, 비종료 SSE `response.finished()` 대기를 차례로 찾아 모두
|
최초 세 full run은 SSE body replay, durable turn 전 종료, 비종료 SSE `response.finished()` 대기를 차례로 찾아 모두
|
||||||
fail-closed했고 cleanup은 매번 GREEN이었다. 현재 소스는 durable turn 2개 poll만 완료 신호로 쓰며 첫 GREEN receipt는 아직 없다.
|
fail-closed했다. 4차 clean run은 6.3분에 동일 학습자 SSE·review ready·G4 0→1까지 통과했지만 full reload 뒤 persisted
|
||||||
|
episode를 카드가 hydrate하지 않아 이미 기록된 원장을 `학습자만 실행`으로 표시하는 제품 결함을 검출했다. 네 run 모두
|
||||||
|
public/NAS/active DB 접촉 0, cleanup GREEN이다. UI는 현재 session+prescription episode를 `현재 원장 상태`로 hydrate하도록
|
||||||
|
수정했고 route fixture desktop/mobile 6/6·typecheck를 통과했다. full runner는 재시도하지 않았으므로 첫 GREEN receipt는 아직 없다.
|
||||||
|
|
||||||
## 9. 핵심 변경 파일
|
## 9. 핵심 변경 파일
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -142,8 +142,11 @@
|
||||||
> 매시간 `vignette-e2e` heartbeat 자동화는 ACTIVE다. 공개/NAS 화면과 health는 읽기 전용으로 보여주고,
|
> 매시간 `vignette-e2e` heartbeat 자동화는 ACTIVE다. 공개/NAS 화면과 health는 읽기 전용으로 보여주고,
|
||||||
> 소스 material milestone에서만 전용 disposable 회기 E2E를 실행한다. runner의 clean HEAD/tree·로컬 npipe Docker·
|
> 소스 material milestone에서만 전용 disposable 회기 E2E를 실행한다. runner의 clean HEAD/tree·로컬 npipe Docker·
|
||||||
> loopback 동적 포트·sentinel·exact cleanup 계약과 unit 11/11은 완료됐다. 최초 세 full run은 각각 SSE body 재읽기,
|
> loopback 동적 포트·sentinel·exact cleanup 계약과 unit 11/11은 완료됐다. 최초 세 full run은 각각 SSE body 재읽기,
|
||||||
> durable turn 전 종료, 비종료 SSE의 `response.finished()` 대기라는 harness 결함을 fail-closed로 잡았고 매번 자원 잔여 0이었다.
|
> durable turn 전 종료, 비종료 SSE의 `response.finished()` 대기라는 harness 결함을 fail-closed로 잡았다. 4차 clean run은
|
||||||
> 현재 소스는 durable turn 2개 poll만 완료 신호로 사용하며, 다음 clean single run의 첫 GREEN receipt가 남았다.
|
> 6.3분 안에 동일 학습자 SSE·review ready·G4 원장 0→1까지 통과한 뒤, full reload가 persisted episode를 화면에 hydrate하지
|
||||||
|
> 못해 이미 기록된 상태를 `학습자만 실행`으로 되돌리는 제품 결함을 잡았다. 네 번 모두 public/NAS/active DB 접촉 0·자원 잔여 0이다.
|
||||||
|
> 현재 UI는 현재 session+prescription의 persisted episode를 `현재 원장 상태`로 복원하며 route fixture desktop/mobile 6/6을 통과했다.
|
||||||
|
> full runner는 재시도하지 않았으므로 다음 clean single run의 첫 GREEN receipt가 남았다.
|
||||||
> 증거: [배포 증거](./ops/nas-preview-deployment-evidence-2026-08-07.md), [기계 판독 증거](./ops/evidence/nas-preview-current-deploy-2026-08-07.json), [브라우저 증거](./ops/evidence/nas-preview-live-turn-2026-08-07.png). 이는 G7 물리 마이크·독립 사람 평가 증거를 대체하지 않는다.
|
> 증거: [배포 증거](./ops/nas-preview-deployment-evidence-2026-08-07.md), [기계 판독 증거](./ops/evidence/nas-preview-current-deploy-2026-08-07.json), [브라우저 증거](./ops/evidence/nas-preview-live-turn-2026-08-07.png). 이는 G7 물리 마이크·독립 사람 평가 증거를 대체하지 않는다.
|
||||||
- [ ] **claude_cli ↔ Anthropic API live 동일성** — provider 라우팅·모델 탐색·설정 저장 경로는 구현 완료. 남은 게이트는 연구팀/기관 `ANTHROPIC_API_KEY`를 게이트웨이 호스트에 주입한 뒤 같은 프롬프트의 live 응답·계량·오류 표면화를 비교하는 것이다.
|
- [ ] **claude_cli ↔ Anthropic API live 동일성** — provider 라우팅·모델 탐색·설정 저장 경로는 구현 완료. 남은 게이트는 연구팀/기관 `ANTHROPIC_API_KEY`를 게이트웨이 호스트에 주입한 뒤 같은 프롬프트의 live 응답·계량·오류 표면화를 비교하는 것이다.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -667,6 +667,7 @@
|
||||||
<p class="dg-note">192차 적용(2026-08-09): G8 clean-head 최종 승격을 완료했다. source HEAD <code>61a41d1f…6af</code>·tree <code>87dec55d…3b77</code>에서 2회 동일하게 만든 archive <code>4d15d055d74f…119d4d</code>를 active release로 고정했고 candidate session E2E <b>112/112</b>와 실제 <code>http://100.116.83.60:8088</code> 평문 NAS-origin browser review <b>112/112</b>(11 specs, desktop/mobile/single-run)를 모두 통과했다. 실행 이미지는 API <code>d5021950…e4b1</code>·Web <code>9796c092…4b36</code>이며 health는 <code>status=ok·db=true·engine=true</code>, 비인증 auth 401, OpenAPI 126을 재확인했다. execute 직전 fresh custom dump <code>36ec8748…24db8</code>(1,097,100 bytes, TOC 1,738 / TABLE DATA 129)를 보존했고 이전 active <code>6030a677…c611</code>과 이전 이미지는 rollback 기준으로 남겼다. 앞선 fail-closed·verified rollback 이력은 삭제하지 않는다. 이 증거로 G8을 <b>DONE</b>으로 승격하며 남은 Outcome OS gate는 G7 external proof다.</p>
|
<p class="dg-note">192차 적용(2026-08-09): G8 clean-head 최종 승격을 완료했다. source HEAD <code>61a41d1f…6af</code>·tree <code>87dec55d…3b77</code>에서 2회 동일하게 만든 archive <code>4d15d055d74f…119d4d</code>를 active release로 고정했고 candidate session E2E <b>112/112</b>와 실제 <code>http://100.116.83.60:8088</code> 평문 NAS-origin browser review <b>112/112</b>(11 specs, desktop/mobile/single-run)를 모두 통과했다. 실행 이미지는 API <code>d5021950…e4b1</code>·Web <code>9796c092…4b36</code>이며 health는 <code>status=ok·db=true·engine=true</code>, 비인증 auth 401, OpenAPI 126을 재확인했다. execute 직전 fresh custom dump <code>36ec8748…24db8</code>(1,097,100 bytes, TOC 1,738 / TABLE DATA 129)를 보존했고 이전 active <code>6030a677…c611</code>과 이전 이미지는 rollback 기준으로 남겼다. 앞선 fail-closed·verified rollback 이력은 삭제하지 않는다. 이 증거로 G8을 <b>DONE</b>으로 승격하며 남은 Outcome OS gate는 G7 external proof다.</p>
|
||||||
<p class="dg-note">193차 적용(2026-08-09): 학생 화면의 320×568 신뢰성 게이트를 닫았다. 처방 재연습 prestart CTA를 첫 화면에 유지하고, active 회기의 마이크·일시정지·종료를 모든 밀도 구간에서 최소 44px로 고정했으며, 대시보드의 긴 privacy token과 owner grid가 320px 문서를 384px까지 넓히던 overflow를 제거했다. typecheck, session-layout desktop/mobile <b>8/8</b>, dev-dashboard desktop/mobile <b>10/10</b>, 자기주도 focused <b>16/16</b>을 통과했다. 동시에 NAS 112건을 실 API/DB 22와 route fixture 90으로 정직하게 분리하고, clean HEAD/tree·전용 disposable stack·동일 학습자 SSE→review→G4/G5 0→1→1·exact cleanup을 강제하는 주기 runner를 추가했다. unit <b>11/11</b>과 Compose preflight는 통과했다. G7 human pack은 category·κ≥0.70·preregistration·paired completeness·결측 ITT를 캡처 전에 검증해 부적합 pack이면 마이크를 열지 않는다.</p>
|
<p class="dg-note">193차 적용(2026-08-09): 학생 화면의 320×568 신뢰성 게이트를 닫았다. 처방 재연습 prestart CTA를 첫 화면에 유지하고, active 회기의 마이크·일시정지·종료를 모든 밀도 구간에서 최소 44px로 고정했으며, 대시보드의 긴 privacy token과 owner grid가 320px 문서를 384px까지 넓히던 overflow를 제거했다. typecheck, session-layout desktop/mobile <b>8/8</b>, dev-dashboard desktop/mobile <b>10/10</b>, 자기주도 focused <b>16/16</b>을 통과했다. 동시에 NAS 112건을 실 API/DB 22와 route fixture 90으로 정직하게 분리하고, clean HEAD/tree·전용 disposable stack·동일 학습자 SSE→review→G4/G5 0→1→1·exact cleanup을 강제하는 주기 runner를 추가했다. unit <b>11/11</b>과 Compose preflight는 통과했다. G7 human pack은 category·κ≥0.70·preregistration·paired completeness·결측 ITT를 캡처 전에 검증해 부적합 pack이면 마이크를 열지 않는다.</p>
|
||||||
<p class="dg-note">194차 적용(2026-08-10): 주기 runner의 최초 세 full run은 모두 전용 clean stack에서 fail-closed했다. 1차는 성공 SSE의 body를 다시 읽는 CDP 오류, 2차는 UI 2턴만 보고 durable 저장 전에 종료해 리뷰가 기록 없음, 3차는 서버가 의도적으로 닫지 않는 SSE에 <code>response.finished()</code>를 기다려 20분 상한에 도달한 하네스 결함이었다. 세 번 모두 fixture review-ready·stack health는 통과했고 공개 8001·실 DB 55432·engine 9099·NAS 접촉 0, container/volume/network/listener/temp 잔여 0이었다. 현재 소스는 자동 retry 0과 <code>GET /sessions/{id}</code> durable turn 2개 poll만 완료 신호로 사용한다. 같은 단계 반복 원칙에 따라 이번 턴에는 더 실행하지 않으며, 다음 clean single run의 첫 GREEN receipt 전까지 주기 실회기 검증을 완료로 표기하지 않는다.</p>
|
<p class="dg-note">194차 적용(2026-08-10): 주기 runner의 최초 세 full run은 모두 전용 clean stack에서 fail-closed했다. 1차는 성공 SSE의 body를 다시 읽는 CDP 오류, 2차는 UI 2턴만 보고 durable 저장 전에 종료해 리뷰가 기록 없음, 3차는 서버가 의도적으로 닫지 않는 SSE에 <code>response.finished()</code>를 기다려 20분 상한에 도달한 하네스 결함이었다. 세 번 모두 fixture review-ready·stack health는 통과했고 공개 8001·실 DB 55432·engine 9099·NAS 접촉 0, container/volume/network/listener/temp 잔여 0이었다. 현재 소스는 자동 retry 0과 <code>GET /sessions/{id}</code> durable turn 2개 poll만 완료 신호로 사용한다. 같은 단계 반복 원칙에 따라 이번 턴에는 더 실행하지 않으며, 다음 clean single run의 첫 GREEN receipt 전까지 주기 실회기 검증을 완료로 표기하지 않는다.</p>
|
||||||
|
<p class="dg-note">195차 적용(2026-08-10): clean HEAD <code>13d1c39a…</code>·tree <code>beb93801…</code>의 4차 수동 full run은 앞선 SSE hang을 제거해 6.3분에 종료했고, 실제 동일 학습자 SSE·review ready·G4 첫 POST와 원장 count 0→1까지 통과했다. 이후 full page reload에서 이미 저장된 현재 회기 episode를 카드가 hydrate하지 않아 <code>원장 반영됨</code> 대신 <code>학습자만 실행</code>으로 되돌아가는 제품 정직성 결함을 fail-closed했다. receipt <code>periodic-learner-e2e-20260810-003150.json</code>은 공개·NAS·active DB 접촉 0과 container/volume/network/listener/temp 잔여 0을 증명한다. UI는 현재 session+prescription의 persisted episode를 read model에서 복원해 <code>현재 원장 상태</code>와 재확인 행동을 표시하도록 수정했고, 실제 reload를 포함한 자기주도 route fixture desktop/mobile <b>6/6</b>·typecheck를 통과했다. full runner는 재시도하지 않았으므로 첫 GREEN receipt와 자동 주기 승격은 여전히 미완료다.</p>
|
||||||
<div class="dg-principles" aria-label="디자인 생성 가드레일">
|
<div class="dg-principles" aria-label="디자인 생성 가드레일">
|
||||||
<div><b>래스터만 사용</b><span>이미지 생성 도구 산출물은 PNG 기반 시안이다. SVG·벡터·와이어프레임·로고 시트로 해석하지 않는다.</span></div>
|
<div><b>래스터만 사용</b><span>이미지 생성 도구 산출물은 PNG 기반 시안이다. SVG·벡터·와이어프레임·로고 시트로 해석하지 않는다.</span></div>
|
||||||
<div><b>기능 우선</b><span>메인 라우트의 실제 액션과 정보 구조를 먼저 반영한다. 장식은 기능을 가리지 않는 수준에서만 쓴다.</span></div>
|
<div><b>기능 우선</b><span>메인 라우트의 실제 액션과 정보 구조를 먼저 반영한다. 장식은 기능을 가리지 않는 수준에서만 쓴다.</span></div>
|
||||||
|
|
@ -1154,7 +1155,7 @@
|
||||||
<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>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>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>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> 매시간 heartbeat / <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>current clean source HEAD <code>61a41d1f…6af</code>·tree <code>87dec55d…3b77</code>의 archive <code>4d15d055…119d4d</code>를 전용 Compose 프로젝트·포트·네트워크·named volume에 승격했다. candidate 112/112와 실제 NAS-origin 112/112는 통과했지만 영수증은 실 API/DB 22건과 route fixture 90건을 분리한다. 자동화는 공개/NAS 화면과 health를 읽기 전용으로 매시간 보여준다. material milestone의 동일 학습자 SSE→review→G4/G5 actual 폐루프는 clean HEAD/tree·전용 engine/DB/API/Web·보호 포트 거부·sentinel·exact cleanup runner가 소유하며 unit 11/11은 통과했다. 세 full run은 하네스 결함을 fail-closed로 찾고 cleanup 3/3 GREEN이었으나 첫 실제 폐루프 GREEN은 아직 없다. current 소스는 durable turn 2개 poll만 완료 신호로 사용한다. release gate·NAS preflight·배포 SHA 변경 때만 프리뷰를 갱신한다. G7 외부 mic/provider/human 증거는 별도다.</td></tr>
|
<tr><td>격리 NAS 프리뷰 · 회기 E2E 자동화</td><td><code>http://100.116.83.60:8088</code> / <code>vignette-e2e</code> 매시간 heartbeat / <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>current clean source HEAD <code>61a41d1f…6af</code>·tree <code>87dec55d…3b77</code>의 archive <code>4d15d055…119d4d</code>를 전용 Compose 프로젝트·포트·네트워크·named volume에 승격했다. candidate 112/112와 실제 NAS-origin 112/112는 통과했지만 영수증은 실 API/DB 22건과 route fixture 90건을 분리한다. 자동화는 공개/NAS 화면과 health를 읽기 전용으로 매시간 보여준다. material milestone의 동일 학습자 SSE→review→G4/G5 actual 폐루프는 clean HEAD/tree·전용 engine/DB/API/Web·보호 포트 거부·sentinel·exact cleanup runner가 소유하며 unit 11/11은 통과했다. 1~3차 full run은 하네스 결함, 4차는 G4 첫 0→1 뒤 reload에서 persisted ledger UI가 초기화되는 제품 결함을 fail-closed했고 cleanup 4/4 GREEN이었다. read model hydrate 수정은 route fixture desktop/mobile 6/6을 통과했지만 첫 실제 폐루프 GREEN은 아직 없다. release gate·NAS preflight·배포 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까지 잇는다. 공개 배포·프로세스·source pin은 clean commit <code>b34623f3…</code>와 fresh receipt <code>9f8d1941…</code>로 완료했다. Cloudflare가 Python 기본 User-Agent를 403으로 거부하는 경계도 브라우저 호환 header와 회귀 4/4로 고정했다. 30초 rehearsal은 세 leg 7 samples를 모두 통과했으며 마이크를 열지 않았다. <b>남은 선행 조건:</b> 사용자의 물리 장치 선택과 명시 동의, 독립 human voice-gain pack이다.</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까지 잇는다. 공개 배포·프로세스·source pin은 clean commit <code>b34623f3…</code>와 fresh receipt <code>9f8d1941…</code>로 완료했다. Cloudflare가 Python 기본 User-Agent를 403으로 거부하는 경계도 브라우저 호환 header와 회귀 4/4로 고정했다. 30초 rehearsal은 세 leg 7 samples를 모두 통과했으며 마이크를 열지 않았다. <b>남은 선행 조건:</b> 사용자의 물리 장치 선택과 명시 동의, 독립 human voice-gain pack이다.</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> runner 프로세스 exit는 canonical checker <code>exit 0</code>·<code>gate_closed=true</code>에 결속되고, browser Origin과 API transport host는 별도 allowlist로 검증된다. 세 artifact 교집합은 <code>≥3000s</code> + 120초 margin이고 Windows proof는 detached-clean HEAD/tree·tool SHA·<code>psutil==6.1.1</code>을 pin한다. clean-source 배포와 무마이크 rehearsal은 완료했다. 남은 것은 물리 마이크 3,120초, 동시 runtime/topology, 독립 human pack을 합친 4-artifact 실증뿐이다.</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> runner 프로세스 exit는 canonical checker <code>exit 0</code>·<code>gate_closed=true</code>에 결속되고, browser Origin과 API transport host는 별도 allowlist로 검증된다. 세 artifact 교집합은 <code>≥3000s</code> + 120초 margin이고 Windows proof는 detached-clean HEAD/tree·tool SHA·<code>psutil==6.1.1</code>을 pin한다. clean-source 배포와 무마이크 rehearsal은 완료했다. 남은 것은 물리 마이크 3,120초, 동시 runtime/topology, 독립 human pack을 합친 4-artifact 실증뿐이다.</td></tr>
|
||||||
<tr><td>G8 DONE · clean-head NAS 기준선</td><td><code>vignette-preview-20260807</code> · <code>run-outcome-os-release-agent.py</code></td><td><b>상태는 DONE.</b> active archive <code>4d15d055…119d4d</code>, source HEAD/tree <code>61a41d1f…6af</code>/<code>87dec55d…3b77</code>, API/Web <code>d5021950…e4b1</code>/<code>9796c092…4b36</code> exact running. candidate 112/112와 actual NAS-origin 112/112, health ok·db/engine true·auth 401·OpenAPI 126을 통과했다. fresh dump <code>36ec8748…24db8</code> 1,097,100 bytes, TOC 1,738 / TABLE DATA 129와 previous active <code>6030a677…c611</code>을 보존했다. release agent의 DB restore 비소유·image/Compose/active-state rollback 경계는 유지한다.</td></tr>
|
<tr><td>G8 DONE · clean-head NAS 기준선</td><td><code>vignette-preview-20260807</code> · <code>run-outcome-os-release-agent.py</code></td><td><b>상태는 DONE.</b> active archive <code>4d15d055…119d4d</code>, source HEAD/tree <code>61a41d1f…6af</code>/<code>87dec55d…3b77</code>, API/Web <code>d5021950…e4b1</code>/<code>9796c092…4b36</code> exact running. candidate 112/112와 actual NAS-origin 112/112, health ok·db/engine true·auth 401·OpenAPI 126을 통과했다. fresh dump <code>36ec8748…24db8</code> 1,097,100 bytes, TOC 1,738 / TABLE DATA 129와 previous active <code>6030a677…c611</code>을 보존했다. release agent의 DB restore 비소유·image/Compose/active-state rollback 경계는 유지한다.</td></tr>
|
||||||
|
|
|
||||||
|
|
@ -281,8 +281,10 @@ NAS-origin 112건은 전부 실 API/DB 회기가 아니다. release receipt는 `
|
||||||
`scripts/run-periodic-learner-e2e.py --execute`가 전용 disposable engine/DB/API/Web에서만 실행한다.
|
`scripts/run-periodic-learner-e2e.py --execute`가 전용 disposable engine/DB/API/Web에서만 실행한다.
|
||||||
이 runner는 clean HEAD/tree, 로컬 npipe Docker, loopback 동적 포트, sentinel과 exact cleanup을 강제하며
|
이 runner는 clean HEAD/tree, 로컬 npipe Docker, loopback 동적 포트, sentinel과 exact cleanup을 강제하며
|
||||||
공개 8001·DB 55432·engine 9099와 NAS를 구조적으로 거부한다. 최초 세 full run은 SSE 응답 body 재읽기,
|
공개 8001·DB 55432·engine 9099와 NAS를 구조적으로 거부한다. 최초 세 full run은 SSE 응답 body 재읽기,
|
||||||
durable turn 전 종료, 닫히지 않는 SSE의 `response.finished()` 대기를 각각 fail-closed로 검출했고 자원 잔여는 매번 0이었다.
|
durable turn 전 종료, 닫히지 않는 SSE의 `response.finished()` 대기를 각각 fail-closed로 검출했다. 4차는 앞선 hang을 제거해
|
||||||
현재 계약은 `GET /sessions/{id}`의 durable turn 2개 poll만 완료 신호로 사용한다. 첫 GREEN receipt 전에는 스케줄 등록하지 않는다.
|
6.3분에 G4 원장 0→1까지 진행한 뒤 full reload가 persisted episode를 UI에 hydrate하지 못하는 제품 결함을 검출했다.
|
||||||
|
네 실행 모두 보호 runtime 접촉 0·자원 잔여 0이다. 현재 UI는 session+prescription episode를 `현재 원장 상태`로 복원하며
|
||||||
|
route fixture desktop/mobile 6/6을 통과했다. 첫 GREEN receipt 전에는 full runner를 스케줄 등록하지 않는다.
|
||||||
|
|
||||||
- **병렬 시나리오**: 166 tests (desktop 83 + mobile 83)
|
- **병렬 시나리오**: 166 tests (desktop 83 + mobile 83)
|
||||||
- **`@single-run` 직렬 시나리오**: 49 tests (DB 영속화·세션 MVP·음성 성공경로·인증 시각 테마·회기말 평가 저장·교수자 명시 재평가 저장·교수자 턴 재평가 저장·교수자 UI 평가 재시도·교수자 사용자별 분석·source pack sync·이론모드 저장·동의 철회 후 voice 차단·브라우저 stream PII 마스킹·음성 transcript 저장 실패 UI 표면화 등)
|
- **`@single-run` 직렬 시나리오**: 49 tests (DB 영속화·세션 MVP·음성 성공경로·인증 시각 테마·회기말 평가 저장·교수자 명시 재평가 저장·교수자 턴 재평가 저장·교수자 UI 평가 재시도·교수자 사용자별 분석·source pack sync·이론모드 저장·동의 철회 후 voice 차단·브라우저 stream PII 마스킹·음성 transcript 저장 실패 UI 표면화 등)
|
||||||
|
|
|
||||||
|
|
@ -91,7 +91,7 @@
|
||||||
무마이크 rehearsal은 완료됐지만 물리 마이크·human pack을 대체하지 않는다.
|
무마이크 rehearsal은 완료됐지만 물리 마이크·human pack을 대체하지 않는다.
|
||||||
격리 NAS 프리뷰 `http://100.116.83.60:8088`은 전용 Compose 프로젝트·포트·네트워크·볼륨에 배포했고, 실제 브라우저 회기와
|
격리 NAS 프리뷰 `http://100.116.83.60:8088`은 전용 Compose 프로젝트·포트·네트워크·볼륨에 배포했고, 실제 브라우저 회기와
|
||||||
review API 저장 축어록 2턴을 확인했다. 기존 프로젝트 중단·재생성 명령은 실행하지 않았다. 매시간 `Vignette 앱 상태·회기 E2E 정기 검증`
|
review API 저장 축어록 2턴을 확인했다. 기존 프로젝트 중단·재생성 명령은 실행하지 않았다. 매시간 `Vignette 앱 상태·회기 E2E 정기 검증`
|
||||||
heartbeat(automation id `vignette-e2e`)는 ACTIVE이며 공개/NAS 화면·health는 읽기 전용으로 보여주고, material milestone에서만 전용 disposable 회기 E2E를 실행한다. NAS 112건은 실 API/DB 22와 route fixture 90으로 영수증에서 분리한다. 동일 학습자 SSE→review→G4/G5 actual 폐루프 runner의 clean HEAD/tree·보호 포트 거부·sentinel·exact cleanup과 unit 11/11은 완료됐다. 최초 세 full run은 SSE body 재읽기→durable turn 전 종료→비종료 SSE `response.finished()` 대기 결함을 순서대로 fail-closed했고 매번 public/NAS/active DB 접촉 0·자원 잔여 0이었다. 현재 소스는 durable turn 2개 poll만 완료 신호로 사용하며 다음 clean single run의 첫 GREEN receipt가 남았다. release gate·NAS preflight·배포 SHA 변경 때만 프리뷰를 갱신한다. 2026-08-07 SHA `6030a677af7e87cbfabc422b553d108d53414fd3c446548734a13b036d35c611`의 localhost 108/108과 평문 origin UUID 24건 실패는 역사 기준선으로 보존한다. 실제 receipt-bound rollback은 같은 프리뷰에서 별도 helper로 두 번 실행해 종료했다([런북](./nas-preview-g8-rollback-proof-runbook.md), [기계 판독 증거](./evidence/nas-preview-g8-actual-rollback-2026-08-07.json)). 현재는 source HEAD/tree `61a41d1f…6af`/`87dec55d…3b77`의 active archive `4d15d055…119d4d`, exact API/Web `d5021950…e4b1`/`9796c092…4b36`이 실행 중이다. candidate 112/112와 실제 NAS-origin 112/112, health ok·db/engine true·auth 401·OpenAPI 126을 통과했고 fresh dump `36ec8748…24db8` 1,097,100 bytes·TOC 1,738/TABLE DATA 129와 previous `6030a677…c611`을 보존했다. 증거: [배포 증거](./nas-preview-deployment-evidence-2026-08-07.md),
|
heartbeat(automation id `vignette-e2e`)는 ACTIVE이며 공개/NAS 화면·health는 읽기 전용으로 보여주고, material milestone에서만 전용 disposable 회기 E2E를 실행한다. NAS 112건은 실 API/DB 22와 route fixture 90으로 영수증에서 분리한다. 동일 학습자 SSE→review→G4/G5 actual 폐루프 runner의 clean HEAD/tree·보호 포트 거부·sentinel·exact cleanup과 unit 11/11은 완료됐다. 최초 세 full run은 SSE body 재읽기→durable turn 전 종료→비종료 SSE `response.finished()` 대기 결함을 순서대로 fail-closed했다. 4차는 동일 학습자 SSE·review ready·G4 0→1 뒤 full reload가 persisted episode를 화면에 hydrate하지 못하는 제품 결함을 잡았다. 네 번 모두 public/NAS/active DB 접촉 0·자원 잔여 0이다. UI hydrate 수정은 route fixture desktop/mobile 6/6을 통과했지만 다음 clean single run의 첫 GREEN receipt가 남았다. release gate·NAS preflight·배포 SHA 변경 때만 프리뷰를 갱신한다. 2026-08-07 SHA `6030a677af7e87cbfabc422b553d108d53414fd3c446548734a13b036d35c611`의 localhost 108/108과 평문 origin UUID 24건 실패는 역사 기준선으로 보존한다. 실제 receipt-bound rollback은 같은 프리뷰에서 별도 helper로 두 번 실행해 종료했다([런북](./nas-preview-g8-rollback-proof-runbook.md), [기계 판독 증거](./evidence/nas-preview-g8-actual-rollback-2026-08-07.json)). 현재는 source HEAD/tree `61a41d1f…6af`/`87dec55d…3b77`의 active archive `4d15d055…119d4d`, exact API/Web `d5021950…e4b1`/`9796c092…4b36`이 실행 중이다. candidate 112/112와 실제 NAS-origin 112/112, health ok·db/engine true·auth 401·OpenAPI 126을 통과했고 fresh dump `36ec8748…24db8` 1,097,100 bytes·TOC 1,738/TABLE DATA 129와 previous `6030a677…c611`을 보존했다. 증거: [배포 증거](./nas-preview-deployment-evidence-2026-08-07.md),
|
||||||
[브라우저 증거](./evidence/nas-preview-live-turn-2026-08-07.png). 명시 동의 물리 마이크와 독립 human voice-gain 증거는 아직 없으므로 G7은 external GATE로 유지한다.
|
[브라우저 증거](./evidence/nas-preview-live-turn-2026-08-07.png). 명시 동의 물리 마이크와 독립 human voice-gain 증거는 아직 없으므로 G7은 external GATE로 유지한다.
|
||||||
- [ ] **claude_cli ↔ Anthropic API live 동일성** — provider 라우팅·Anthropic `/v1/models` 탐색·지원 추론 강도·관리자 fail-closed 저장 경로는 구현 완료. 남은 범위는 연구팀/기관 `ANTHROPIC_API_KEY`를 게이트웨이 호스트에 주입한 live 응답·계량·오류 표면화 비교다. Claude CLI·Codex CLI(Terra/Medium)·Agy CLI(Gemini 3.6 Flash/High)는 로컬 live probe를 통과했다.
|
- [ ] **claude_cli ↔ Anthropic API live 동일성** — provider 라우팅·Anthropic `/v1/models` 탐색·지원 추론 강도·관리자 fail-closed 저장 경로는 구현 완료. 남은 범위는 연구팀/기관 `ANTHROPIC_API_KEY`를 게이트웨이 호스트에 주입한 live 응답·계량·오류 표면화 비교다. Claude CLI·Codex CLI(Terra/Medium)·Agy CLI(Gemini 3.6 Flash/High)는 로컬 live probe를 통과했다.
|
||||||
- [ ] **stable-source 재부팅 후 watchdog smoke** — watchdog·로그온 boot는 detached-clean `b34623f3…` stable
|
- [ ] **stable-source 재부팅 후 watchdog smoke** — watchdog·로그온 boot는 detached-clean `b34623f3…` stable
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue