Files
Find-Word/.agent/RESULT.md
T

411 lines
35 KiB
Markdown
Raw Normal View History

2026-10-14 06:19:24 +00:00
# Find-Word v0.1 작업 결과
작성일: 2026-10-14 UTC. 작업 위치: `/home/hermes/worktrees/find-word-v0.1`.
입력 문서 `.agent/CONTEXT.md`, `TASK.md`, `ACCEPTANCE.md`를 모두 읽고 적용했다.
기존 입력 문서는 수정하지 않았다.
## 구현 결과
- `단어를 맞춰라`, `Designed by OZ at Heat-T Medical Inc.`, `v0.1` 표시.
- 반응형 시작·게임·결과 화면, 10문제 완주, 재시작, 다시 듣기, 중단, 진행률.
- 객관식·주관식·혼합 모드, 10유형 × 10단어 = 100개 저장 문장.
- 4지선다의 고유한 정답 1개·사전 단어 오답 3개, 변형 규칙과 예상 혼동 난이도.
- 8개 합성 배경 환경, +6/+3/0/−3/−6 dB의 5단계 상대 음량.
- 사용자 gesture에서 AudioContext 시작, 음성·배경 gain 분리, 안내 fade,
재생 중 경합 방지, 취소된 로드의 늦은 완료 무시, 오류와 재시도.
- 점수·정답률·연속 정답·단계별 결과, 한글 NFC/공백/문장부호 정규화,
한글 조합 중 Enter·재시작·재청취의 입력 상태 보호.
- 키보드/터치, 질문·피드백·결과 focus, ARIA, reduced motion, 색 대비.
- TypeScript strict, 직접 작성 소스 22개 파일의 한국어 설계 설명.
- 직접 합성 WAV 101개(10,864,582바이트), 결정론적 배경 합성,
생성 도구 실측 버전·원문·파일 해시 manifest.
- 런타임 외부 CDN/TTS/API/분석 요청 없음. 개인정보 저장·로그인·추적 없음.
서버·DNS·인증정보에 접근하지 않았으며 **배포, Git commit, Git push를 실행하지 않았다.**
Playwright의 localhost preview는 개발 검증용으로만 실행했다.
## 실제 strict TDD 기록
먼저 테스트와 타입 계약/비동작 stub을 작성하고 RED 명령의 종료 코드 **1**을
확인한 뒤 최소 동작을 구현했다. 각 GREEN은 종료 코드 **0**이다. 아래 원문
로그는 `evidence/`에 보관했다. 테스트를 건너뛰거나 실패 조건을 지워 GREEN으로
만들지 않았다. 초기 stub의 throw는 구현 전 상태를 표현한다.
| 순서·핵심 동작 | 실제 RED 명령·결과 | 최소 구현 뒤 GREEN 명령·결과 | 원문 로그 |
| --- | --- | --- | --- |
| 01 데이터: 10유형·단어 무결성·8환경·5단계 | `npm test -- tests/content.test.ts`: **3 failed**, 빈 목록의 개수/환경 불일치 | 같은 명령: **3 passed** | [RED](evidence/01-content-red.log), [GREEN](evidence/01-content-green.log) |
| 02 출제·판정·게임 전이·점수 | `npm test -- tests/game.test.ts`: **6 failed**, 출제 미구현·정규화 불일치 | 같은 명령: **6 passed** | [RED](evidence/02-game-red.log), [GREEN](evidence/02-game-green.log) |
| 03 dB·RMS·결정론적 환경 합성 | `npm test -- tests/dsp.test.ts`: **3 failed**, gain=1·잘못된 RMS·무음 | 같은 명령: **3 passed** | [RED](evidence/03-dsp-red.log), [GREEN](evidence/03-dsp-green.log) |
| 04 오디오 gesture·gain·fade·중복 시작·취소·실패·음량 | `npm test -- tests/audio.test.ts`: **5 failed**, 오디오 미구현 | 같은 명령: **5 passed** | [RED](evidence/04-audio-red.log), [GREEN](evidence/04-audio-green.log) |
| 05 실제 저장 음성·원문·해시·포맷·크기 | `npm test -- tests/assets.test.ts`: **1 failed**, manifest 없음(ENOENT) | `npm run audio:generate` 후 `npm test -- tests/App.test.tsx tests/assets.test.ts`: **2 files / 7 passed** (음성 1 + UI 6) | [RED](evidence/05-assets-red.log), [GREEN](evidence/06-ui-05-assets-green.log) |
| 06 시작→재생→답변→결과→재시작·오류·포커스·보안 출력 | `npm test -- tests/App.test.tsx`: **6 failed**, 빈 화면에서 브랜드/시작/입력 역할 없음 | 위 결합 명령: **7 passed** 중 UI **6 passed** | [RED](evidence/06-ui-red.log), [GREEN](evidence/06-ui-05-assets-green.log) |
| 07 production 음성 검증기 | `npm test -- tests/build.test.ts`: **1 failed**, 자산 수 0 ≠ 101 | 같은 명령: **1 passed**, 정상 통과 및 파일 변조·누락 시 실제 실패 검증 | [RED](evidence/07-build-red.log), [GREEN](evidence/07-build-green.log) |
| 08 오답 변형별 혼동 난이도 | `npm test -- tests/content.test.ts`: **1 failed / 3 passed**, 순위 undefined | 같은 명령: **4 passed** | [RED](evidence/08-rule-red.log), [GREEN](evidence/08-rule-green.log) |
| 09 한글 조합 중 제출 방지 | `npm test -- tests/App.test.tsx`: **1 failed / 6 passed**, 조합 중 정답 피드백이 나타남 | 같은 명령: **7 passed** | [RED](evidence/09-ime-red.log), [GREEN](evidence/09-ime-green.log) |
| 10 한글 조합 도중 세션 재시작 | `npm test -- tests/App.test.tsx -t '한글 조합 도중'`: **1 failed / 7 skipped**, 새 세션 제출이 막힘 | 같은 명령: **1 passed / 7 skipped** | [RED](evidence/10-ime-reset-red.log), [GREEN](evidence/10-ime-reset-green.log) |
| 11 실측 음성 도구 버전 provenance | `npm test -- tests/assets.test.ts -t '도구 버전'`: **1 failed / 1 skipped**, 버전 정보 undefined | 생성기 수정·재생성 후 `npm test -- tests/assets.test.ts`: **2 passed** | [RED](evidence/11-provenance-red.log), [GREEN](evidence/11-provenance-green.log) |
| 12 한글 조합 도중 다시 듣기 | 위 10의 테스트에 재청취 전환을 추가한 동일 명령: **1 failed / 7 skipped**, 제출이 막힘 | 같은 명령: **1 passed / 7 skipped** | [RED](evidence/12-ime-replay-red.log), [GREEN](evidence/12-ime-replay-green.log) |
`-t`의 skipped는 해당 회귀만 고른 실행 결과이며 최종 전체 실행에는 **skip 없음**.
중간 실패도 남겼다.
- DSP의 첫 구현에서 음수 0(`-0`)이 경계 샘플로 남아 **1 failed / 2 passed**였다.
경계값을 명시적으로 0으로 써 수정했다. [중간 로그](evidence/03-dsp-intermediate.log)
- UI 첫 구현에서 라디오의 보조 설명에 ‘직접 입력’이 중복되어 **1 failed / 6 passed**였다.
각 라디오의 접근성 이름을 명확히 했다. [중간 로그](evidence/06-ui-intermediate.log)
- 오디오 최초 RED의 미구현 throw로 미처리 rejection 3개도 보고되었다.
실제 엔진의 Promise 종료·정리 구현 뒤 GREEN과 최종 실행에서는 0개다.
- 첫 TypeScript 검사에서 TypedArray의 ArrayBuffer 타입과 테스트 mock의 순환
추론 오류를 발견했다. 반환 타입·mock 타입을 명시하여 strict 검사를 통과시켰다.
## 브라우저 통합 검증과 접근성 수정
실제 Chrome, production `dist/`, 실제 WAV/Web Audio로 검증했다. 모의 재생으로
시간을 단축하지 않고 `ended`까지 기다렸다. 모든 접근성 검사는 axe 규칙을
비활성화하지 않고 실행했다.
명령: `CHROME_PATH=/usr/bin/google-chrome npm run test:browser`
| 실행 | 실제 결과 | 발견·조치 |
| --- | --- | --- |
| [첫 실행](evidence/browser-first.log) | 2 passed / 2 failed | 시작 화면 글자 대비 3.75, 4.07 < 4.5. 두 색을 어둡게 조정 |
| [두 번째](evidence/browser-second.log) | 2 passed / 2 failed | 게임 화면 글자 대비 4.08, 4.41 < 4.5. 두 색을 조정 |
| [세 번째](evidence/browser-third.log) | 2 passed / 2 failed | 두 크기 모두 10문제 완주·100% 도달. 결과 설명 대비 4.38 < 4.5 수정 |
| [수정 후](evidence/browser-final.log) | **4 passed (1.2m)** | 시작·게임·결과 axe 위반 0, 두 크기 완주·재시작·404 후 재시도 통과 |
최종 빌드에서도 **4 passed (1.2m), 종료 코드 0**을 확인했다. reduced motion의
실제 계산된 `animation-name: none`까지 검증했다.
[최종 브라우저 로그](evidence/final-browser.log)에 실제 결과를 보관했다.
스크린샷:
- [1280px 시작](evidence/desktop-home.png), [게임](evidence/desktop-game.png), [결과](evidence/desktop-results.png)
- [360px 시작](evidence/mobile-home.png), [게임](evidence/mobile-game.png), [결과](evidence/mobile-results.png)
## 최종 검증
환경: Node.js **26.7.0**, npm **11.19.0**, Vite **8.3.4**, Vitest **5.0.4**.
최소 Node 안내는 아래 릴리스 재검토에서 **22.22.2 이상**으로 바로잡았다.
실제 Node 22에서의 별도 실행은 하지 않았으며 현재 실행 환경은 아래 최신 검증을 따른다.
| 명령/검증 | 실제 결과 |
| --- | --- |
| `npm run lint` | **PASS**, ESLint + `tsc --noEmit` 오류 0 · [로그](evidence/final-lint.log) |
| `npm test` | **7 files / 29 tests passed**, skip 0 · [로그](evidence/final-test.log) |
| `npm run build` | **PASS**, `dist/` 생성 및 음성 101개 원본 SHA-256 대조 · [로그](evidence/final-build.log) |
| `CHROME_PATH=/usr/bin/google-chrome npm run test:browser` | **4 passed (1.2m)**, 360px/1280px 완주·오류 복구·접근성 위반 0·외부 요청 0·JS 예외 0 · [로그](evidence/final-browser.log) |
| `npm run audio:generate` 후 이전 manifest `files` 비교 | **101개 원문·바이트 수·SHA-256 전부 동일** · [로그](evidence/audio-reproduce.log) |
| `npm audit --audit-level=high` | **found 0 vulnerabilities** · [로그](evidence/final-audit.log) |
| 비밀값 정적 패턴 검사 | **34개 텍스트 파일, 0 matches** · [로그](evidence/final-security.log) |
| 직접 작성 소스의 한글 설명 | **22개 파일, 누락 0**; Python 생성기 AST 파싱 PASS |
| `git diff --check` | 종료 0. 초기 저장소가 비어 새 파일들은 아직 untracked 상태 |
production 주요 번들: JS **244.26 kB**(gzip 76.95 kB), CSS **14.87 kB**(gzip 4.29 kB),
HTML **0.89 kB**. WAV는 별도로 10.86 MB이고 문항 단위로 로드한다.
비밀값 검사는 소스·테스트·스크립트·문서·루트 metadata·입력 `.agent/*.md`에 대해
AWS access-key 형식, GitHub/OpenAI 토큰 형식, private-key 블록, 긴 password/secret/API-key
문자열 할당을 Python 정규식으로 검사했다. 일치 값은 출력하지 않는다.
의존성 디렉터리와 PCM 바이너리는 대상에서 제외했다. 이는 특정 형식의 정적 검사다.
## 변경 파일
초기 저장소에는 입력 문서만 존재했다. 다음 파일을 새로 작성했다. 파일 전체
목록은 [changed-files.txt](evidence/changed-files.txt)에 남긴다(입력 문서는 보존 표시).
- 앱: `src/App.tsx`, `src/main.tsx`, `src/styles.css`, `src/content.ts`, `src/game.ts`,
`src/audio/dsp.ts`, `src/audio/engine.ts`, `index.html`.
- 설정: `package.json`, `package-lock.json`, `tsconfig.json`, `vite.config.ts`,
`eslint.config.js`, `playwright.config.ts`, `.gitignore`.
- 생성/검증: `scripts/generate_audio.py`, `scripts/check-build.mjs`.
- 테스트: `tests/setup.ts`, `tests/content.test.ts`, `tests/game.test.ts`,
`tests/dsp.test.ts`, `tests/audio.test.ts`, `tests/assets.test.ts`,
`tests/App.test.tsx`, `tests/build.test.ts`, `tests/browser/game.spec.ts`.
- 자산: `public/audio/intro.wav`, 100개의 `<pattern>-<word>.wav`,
`public/audio/manifest.json`.
- 문서: `README.md`, `docs/ARCHITECTURE.md`, `docs/AUDIO_PROVENANCE.md`,
`docs/DEPLOYMENT.md`, `docs/WORDS.md`, `.agent/RESULT.md` 및 `evidence/` 로그·화면.
- 생성된 `dist/`, `node_modules/`, `.cache/`와 Playwright 임시 보고서는 Git 제외.
## 남은 위험·인수 사항
1. 한국어 포먼트 합성은 자연스러운 사람의 발화보다 기계적이다. 자동 검증은
오디오의 존재·디코딩·전력·흐름을 확인했고, 실제 사용자의 발음 이해도와 청감
난이도를 평가하지 않았다. Neo의 직접 청취 및 사용자 평가가 필요하다.
2. 환경음은 장소를 추상화한 합성 잡음이다. 실제 시장·카페의 사람 대화 녹음이
아니다. 상대 dB와 오답 혼동 순위는 의료·청력 측정치가 아니다.
3. 모바일은 Chrome의 360px 터치 에뮬레이션으로 검증했다. 실물 iOS Safari,
Android 장치, Bluetooth 출력, 보조공학 장치의 전체 조합은 미검증이다.
4. 사용자 기록은 메모리에만 있어 새로고침 시 사라진다. 오프라인 PWA는 아니며
음성의 첫 로드에는 정적 호스트 연결이 필요하다. 전체 오디오 용량은 약 10.9MB다.
5. 정답은 정적 번들에 포함된다. 개발자 도구로 답을 알아낼 수 있어 보안 시험이나
순위 경쟁 서비스 용도가 아니다.
6. 서버 배포·Nginx/TLS/DNS는 요청 범위 밖이며 실행하지 않았다. Neo가 별도
검토·커밋·배포하고 실제 도메인에서 MIME·캐시·오디오 404·HTTPS를 확인해야 한다.
## Neo 스크린샷 검수 후 출시 전 UI 수정 — 2026-10-14 UTC
앞의 기록과 기존 스크린샷은 보존하고 이번 작업의 RED/GREEN을 이어서 기록한다.
기록 위치는 `.agent/TASK.md`와 `.agent/ACCEPTANCE.md`의 `.agent/RESULT.md` 지정을 따랐다.
### 변경 결과
- 한국어 `h1`·`h2`·`h3`에 `word-break: keep-all`을 적용했다. 실제 Chrome의
360px computed style이 `normal`인 RED부터 확인했고, 열 가지 질문 모두
360px/1280px에서 `keep-all`과 제목 내부 가로 넘침 여부를 검사한다.
- 게임과 결과 난이도를 `SNR +6 dB`, `SNR +3 dB`, `SNR 0 dB`, `SNR -3 dB`,
`SNR -6 dB`로 표시한다. 게임에서는 “음성이 배경보다 6 dB 커요.”,
“음성과 배경의 크기가 같아요.”, “음성이 배경보다 3 dB 작아요.”처럼
크기 관계를 풀어 설명한다. 모바일에서도 설명을 숨기지 않는다.
- 전체 음량은 시작 화면의 설정 카드 안에서 CTA보다 앞에, 게임에서는 환경
패널 안에 배치했다. `전체 음량` 접근성 이름, label 연결, 엔진 전달,
화면 전환 시 값 유지와 결과 화면의 기존 음량 조절을 보존했다.
- 사운드 룸의 `01 / 08`은 실제 환경 수를 이용한 `8가지 환경`으로 바꿨다.
`플레이 방법`의 `#howto` 앵커는 유지하고 장식 화살표를 `↓`로 바꿨다.
CTA의 표시와 접근성 이름은 `게임 시작 · 소리 듣기`로 통일했다.
- 기존 색상·캐릭터·게임 흐름을 유지했다. README의 CTA와 SNR 설명도 맞췄다.
출제·채점·음원·오디오 gain 구현은 수정하지 않았다.
### 추가 strict TDD 기록
각 RED는 해당 구현을 바꾸기 **전에** 실행해 종료 코드 **1**을 확인했다.
각 GREEN은 같은 테스트 계약으로 종료 코드 **0**을 확인했다. 브라우저는
각 단계의 소스를 먼저 빌드한 뒤 localhost production preview로 검사했다.
| 순서·계약 | 실제 RED | 구현 후 GREEN | 증거 |
| --- | --- | --- | --- |
| 13 한국어 제목의 어절 보존 | `CHROME_PATH=/usr/bin/google-chrome npm run test:browser -- tests/browser/ui.spec.ts --project=mobile`: **1 failed**. 시작·질문 heading의 computed `wordBreak`가 `normal` | 같은 명령: **1 passed**, `keep-all` 및 가로 넘침 없음 | [RED](evidence/13-heading-red.log), [GREEN](evidence/13-heading-green.log), [변경 전 빌드](evidence/13-heading-build-before.log), [변경 후 빌드](evidence/13-heading-build-after.log) |
| 14 다섯 SNR·크기 설명·결과 표시 | `npm test -- tests/App.test.tsx -t '다섯 난이도의 SNR'`: **1 failed / 8 skipped**. 열 문제의 SNR·크기 설명과 결과 표기 누락 | 같은 명령: **1 passed / 8 skipped** | [RED](evidence/14-snr-red.log), [GREEN](evidence/14-snr-green.log) |
| 14 모바일 난이도 설명 가시성 | `CHROME_PATH=/usr/bin/google-chrome npm run test:browser -- tests/browser/ui.spec.ts --project=mobile --grep '난이도 설명'`: **1 failed**. `.db-note`가 hidden이고 새 설명·배지 없음 | 같은 명령: **1 passed** | [RED](evidence/14-snr-browser-red.log), [GREEN](evidence/14-snr-browser-green.log), [빌드](evidence/14-snr-build.log) |
| 15 음량의 소속·문서 순서·이름·값·엔진 | `npm test -- tests/App.test.tsx -t '전체 음량을 시작 CTA'`: **1 failed / 9 skipped**. 설정 카드·환경 패널 바깥이고 CTA 뒤에 있음 | 같은 명령: **1 passed / 9 skipped**. 이름·단일 컨트롤·20→60% 전환 및 엔진 전달 확인 | [RED](evidence/15-volume-red.log), [GREEN](evidence/15-volume-green.log) |
| 15 실제 음량 위치·키보드 | `CHROME_PATH=/usr/bin/google-chrome npm run test:browser -- tests/browser/ui.spec.ts --grep '전체 음량'`: **2 failed**. 두 화면 크기에서 컨트롤이 카드 경계와 CTA 아래로 벗어남 | 같은 명령: **2 passed**, 카드 내부·CTA 이전 배치와 방향키·값 유지 확인 | [RED](evidence/15-volume-browser-red.log), [GREEN](evidence/15-volume-browser-green.log), [빌드](evidence/15-volume-build.log) |
| 16 환경 수·앵커 화살표·명확한 CTA | `npm test -- tests/App.test.tsx -t '시작 안내'`: **3 failed / 10 skipped**. `8가지 환경`·`↓`·새 CTA 없음 | 같은 명령: **3 passed / 10 skipped**, CTA 클릭에서 재생 1회 확인 | [RED](evidence/16-copy-red.log), [GREEN](evidence/16-copy-green.log) |
| 17 음량 이동 후 게임 카드·바닥글 여백 | `CHROME_PATH=/usr/bin/google-chrome npm run test:browser -- tests/browser/ui.spec.ts --grep '전체 음량'`: **2 failed**. 스크린샷 검토에서 발견한 두 화면 크기의 카드·바닥글 간격 **0px < 24px** | 게임 하단에 32px 여백 적용 후 같은 명령: **2 passed** | [RED](evidence/17-spacing-red.log), [GREEN](evidence/17-spacing-green.log) |
필터 실행의 skipped는 해당 회귀만 선택한 결과다. 기존 테스트의 시작 버튼
선택자도 새 접근성 이름에 맞췄으며, 이전 이름까지 허용하는 정규식으로
검증을 완화하지 않았다. 최종 브라우저 테스트에는 음량에서 Tab을 누르면
시작 CTA로 이동하는 검사와 열 가지 질문의 computed style 검사도 포함한다.
전체 검증의 첫 타입 검사에서 새 Testing Library `getByRole` 호출에 지원되지
않는 `exact` 옵션을 발견했다. 문자열 `name` 자체가 정확 일치를 하므로 불필요한
옵션만 제거했다. [중간 lint](evidence/neo-intermediate-lint.log)와
[중간 build](evidence/neo-intermediate-build.log)를 보존했다.
여백 회귀는 첫 전체 브라우저 실행 **10 passed (1.5m)** 뒤 스크린샷 검토에서
추가로 발견했다. [당시 로그](evidence/neo-before-spacing-browser.log)를 보존하고
17의 측정 계약을 먼저 실패시킨 뒤 수정했다. 전체 캡처 전에는 스크롤을
맨 위로 맞춰 화면 밖 fixed 건너뛰기 링크가 Chrome의 full-page 이미지에
들어오는 캡처 아티팩트를 방지했다. 앱의 초점 처리와 링크는 바꾸지 않았다.
### 이번 수정의 최종 전체 검증
17의 여백 수정까지 포함한 최종 소스로 아래 명령을 모두 실행했다. 종료 코드는
전부 **0**이며 unit/component와 브라우저 전체 실행 모두 **skip 없음**이다.
| 명령 | 최종 결과 | 로그 |
| --- | --- | --- |
| `npm run lint` | **PASS**, ESLint와 TypeScript 오류 0 | [lint](evidence/neo-final-lint.log) |
| `npm test` | **7 files / 34 tests passed** | [test](evidence/neo-final-test.log) |
| `npm run build` | **PASS**, production 빌드·음성 101개 무결성 확인 | [build](evidence/neo-final-build.log) |
| `CHROME_PATH=/usr/bin/google-chrome npm run test:browser` | **10 passed (1.4m)**, 실제 Chrome 360px/1280px | [browser](evidence/neo-final-browser.log) |
브라우저에서는 실제 WAV 재생을 기다려 혼합 모드 열 문제를 완주하고,
재청취·100% 결과·재시작·404 후 재시도·초점 복귀를 검증했다. 두 화면 크기에서
시작·게임·결과 axe 위반 **0**, 외부 요청 **0**, JavaScript 예외 **0**이며,
reduced motion과 새 줄바꿈·SNR 설명·음량 위치·키보드 계약도 모두 통과했다.
스크린샷을 직접 열어 시작 CTA 앞 음량, 게임 환경 패널의 SNR·음량, 어절을
보존한 질문, 게임 카드와 바닥글의 여백, 결과 표기를 확인했다.
최종 스크린샷(기존 파일과 구별되는 `neo-` 접두사):
- 1280px: [시작](evidence/neo-desktop-home.png), [게임](evidence/neo-desktop-game.png), [결과](evidence/neo-desktop-results.png), [고정 질문](evidence/neo-desktop-heading.png)
- 360px: [시작](evidence/neo-mobile-home.png), [게임](evidence/neo-mobile-game.png), [결과](evidence/neo-mobile-results.png), [고정 질문](evidence/neo-mobile-heading.png)
이번 변경 파일은 `src/App.tsx`, `src/styles.css`, `tests/App.test.tsx`,
`tests/browser/game.spec.ts`, 신규 `tests/browser/ui.spec.ts`, `README.md`,
이 `.agent/RESULT.md` 및 추가 evidence 로그·스크린샷이다. 의존성과 음원은
변경하지 않았다. `git diff --check`는 종료 0이며 저장소의 기존 untracked 상태는
유지했다. 변경 텍스트 7개 파일의 후행 공백 0과 RESULT의 evidence 링크 존재도
별도로 확인했다.
요청된 UI 수정에서 남은 실패는 없다. 실물 모바일·Safari 및 청감 평가의 기존
검증 범위 한계는 그대로다. 이번에도 로컬 preview 검증만 수행했으며,
**서버 배포, Git commit, Git push는 실행하지 않았다.**
## 독립 릴리스 리뷰·Neo literal-preservation 재검토 반영 — 2026-10-14 UTC
이번 사용자 지시를 이전 입력 문서의 브랜드 해석보다 우선 적용했다.
정확한 표기는 **Designed by OZ at Heat-T Medical Inc.**다. UI, README,
`.agent/CONTEXT.md`·`TASK.md`·`ACCEPTANCE.md`의 요구사항과 이 문서의 표기도
일치시켰다. 이전 실행의 raw 로그와 스크린샷은 당시 상태의 증거이므로 보존한다.
아래 검증이 현재 릴리스 상태이며 앞의 결과·바이트 수·테스트 수는 당시 기록이다.
### 변경 내용
- 브랜드 원문을 고정한 컴포넌트·문서·실제 브라우저 회귀 검증을 추가했다.
회사명을 문맥으로 추측해 교정하던 입력 문서 규칙도 제거했다.
- 프로젝트 자체는 `private: true`, `license: UNLICENSED`, root `LICENSE`의
private / all-rights-reserved로 명시했다. package repository·homepage와
README·배포 문서의 canonical URL은 `https://source.ibit.kr/OZ-Group/Find-Word`다.
- root와 public의 `THIRD_PARTY_NOTICES.md`에 잠긴 React 19.3.0, ReactDOM 19.3.0,
scheduler 0.28.0의 MIT 전문을 포함했다. 빌드 검증기는 전이 의존성을 포함한
런타임 목록을 lockfile에서 찾고, 설치된 LICENSE 전문·버전과 root/public/dist의
고지 사본을 대조한다. notice의 누락·축약을 실제 실패시키는 테스트도 추가했다.
- `Pattern.mechanic`에 10개 고유 연산을 지정했다. 직접 목표 식별, 암호 구별,
잊고 기억하기, 첫째·마지막·가운데, 반복, 정정, 신호 조건, 오른쪽 표지를 각각
다르게 듣고 판단하게 했다. `renderSentence`가 실제 보기의 오답도 문장에
삽입하며 UI와 음성 생성기가 같은 원문을 사용한다. 문장 계약 검증은 100개
조합에서 순서·반복 횟수·취소·조건·방향 단서와 질문 의도를 확인한다.
- 101개 WAV와 manifest를 재생성했다. 합계 **20,497,222바이트**, 16kHz mono
PCM이며 기존 20MiB 제한을 유지했다. 방향은 말로 전달하는 의미 단서다.
- package와 lockfile의 Node 최소 버전은 `>=22.22.2`로 맞췄다. README·배포
문서에 잠긴 jsdom 계열의 24.x 최소 24.15.0 요구도 함께 설명했다.
- Playwright는 `chromiumSandbox: true`가 기본이다. 미설정·빈 값·`0`·`false`는
sandbox를 유지하며, `PLAYWRIGHT_NO_SANDBOX=1`일 때만 끈다. Chrome 경로
지정은 sandbox와 독립적이다. 새 브라우저 스크린샷은 `release-` 접두사로 저장한다.
### 실제 strict TDD
아래 RED는 구현을 바꾸기 전에 실제 실행했고 모두 종료 코드 **1**이었다.
GREEN은 같은 계약으로 모두 종료 코드 **0**이었다. 브라우저 전달 계약은
기존 dist에서 먼저 실패시킨 후 새 소스를 빌드했다. 테스트 실패를 제거하거나
허용치를 완화하지 않았다. `-t`로 지정한 실행의 skip은 선택 실행에 한정된다.
| 순서·계약 | RED 명령·관찰 | GREEN 명령·관찰 | 원문 로그 |
| --- | --- | --- | --- |
| 18 literal 보존 | `npm test -- tests/App.test.tsx tests/release.test.ts -t '브랜드'`: **2 failed**, UI·문서의 기존 표기 불일치 | 같은 명령: **2 passed / 12 skipped** | [RED](evidence/18-literal-red.log), [GREEN](evidence/18-literal-green.log) |
| 19 프로젝트 metadata·MIT 고지·배포 검증기 | `npm test -- tests/release.test.ts tests/build.test.ts`: **4 failed / 2 passed**, metadata·LICENSE·고지 누락 및 검증기의 누락/축약 미검출 | 같은 명령: **6 passed** | [RED](evidence/19-notices-red.log), [GREEN](evidence/19-notices-green.log) |
| 20 고유 청취 연산·문장 계약·출제 | `npm test -- tests/content.test.ts tests/game.test.ts`: **12 failed / 10 passed**, mechanic 없음·경쟁 단어/순서/의미 계약 미충족 | 같은 명령: **22 passed** | [RED](evidence/20-mechanics-red.log), [GREEN](evidence/20-mechanics-green.log) |
| 20 변경 원문과 WAV 대응 | `npm test -- tests/assets.test.ts`: **1 failed / 1 passed**, 기존 manifest 원문이 새 문장과 다름 | `npm run audio:generate` 후 `npm test -- tests/content.test.ts tests/game.test.ts tests/assets.test.ts`: **24 passed**, 문장·해시·PCM·비무음·크기·피크 검증 | [RED](evidence/20-audio-red.log), [생성](evidence/20-audio-generate.log), [GREEN](evidence/20-audio-green.log) |
| 21 Node 최소 버전 | `npm test -- tests/release.test.ts -t 'Node 최소'`: **1 failed / 3 skipped**, package/lock/docs 값이 잠금 의존성 요구와 다름 | 같은 명령: **1 passed / 3 skipped** | [RED](evidence/21-node-red.log), [GREEN](evidence/21-node-green.log) |
| 22 sandbox 기본값과 명시적 opt-in | `npm test -- tests/playwright-config.test.ts`: **5 failed**, 기본 sandbox 비활성화·무조건 적용된 비활성화 플래그 | 같은 명령: **5 passed**, 각 환경변수 값을 별도 프로세스로 검증 | [RED](evidence/22-sandbox-red.log), [GREEN](evidence/22-sandbox-green.log) |
| 23 실제 HTTP 배포 응답 | `env -u PLAYWRIGHT_NO_SANDBOX CHROME_PATH=/usr/bin/google-chrome npm run test:browser -- tests/browser/release.spec.ts --project=mobile`: **1 failed**, 이전 dist의 크레딧 불일치와 notice 대신 SPA HTML 응답 | 새 build 후 전체 브라우저 실행: **12 passed (2.2m)**, 두 화면 크기의 전달 계약 포함 | [RED](evidence/23-delivery-browser-red.log), [전체 GREEN](evidence/release-final-browser.log) |
중간 lint에서 새 sandbox 테스트의 env 객체 타입이 좁게 추론되어 TypeScript
오류 2개가 났다. `NodeJS.ProcessEnv`를 명시하여 수정했다.
[실패 원문](evidence/release-intermediate-lint.log)을 보존했다.
### 최종 검증 결과
환경: Node.js **26.7.0**, npm **11.19.0**, 시스템 Chrome
`/usr/bin/google-chrome`. 최소 Node 22.22.2에서 별도로 실행한 결과는 아니다.
새 소스의 production build를 완료한 뒤 다음 명령으로 브라우저 전체를 실행했다.
```bash
env -u PLAYWRIGHT_NO_SANDBOX CHROME_PATH=/usr/bin/google-chrome DEBUG=pw:browser npm run test:browser
```
| 검증 | 실제 결과·증거 |
| --- | --- |
| `npm run lint` | **PASS**, ESLint + TypeScript strict 오류 0 · [로그](evidence/release-final-lint.log) |
| `npm test` | **9 files / 57 tests passed**, skip 0 · [로그](evidence/release-final-test.log) |
| `npm run build` | **PASS**, WAV 101개 SHA-256 및 root/public/dist 런타임 MIT notice 전문 대조 · [로그](evidence/release-final-build.log) |
| 위 Chrome 전체 명령 | **12 passed (2.2m)**, 360px/1280px, 실제 재생·10문제 완주·재청취·404 복구·키보드·SNR·레이아웃·axe·크레딧·MIT HTTP 응답 · [로그](evidence/release-final-browser.log) |
| sandbox 실제 실행 인자 | Chrome 4회 실행 모두 `--no-sandbox` 없음. opt-in 환경변수를 지운 상태로 실행 · [검증](evidence/release-integrity.log) |
| `npm audit --audit-level=high` | **found 0 vulnerabilities**, 개발 의존성 포함 · [로그](evidence/release-final-audit.log) |
| `npm ls --omit=dev` | React / ReactDOM 19.3.0, 해석 오류 없음 |
| 브랜드·기록·정적 산출물 최종 확인 | 현재 UI·테스트·문서에 대체 브랜드 표기 없음, evidence 링크 존재, 101 WAV/notice 검증 통과 · [로그](evidence/release-integrity.log) |
| `git diff --check` | 종료 코드 0. 시작 시 이미 소스 전체가 untracked였으므로 변경 텍스트의 후행 공백도 별도 검사 |
브라우저의 페이지 JS 예외·외부 페이지 요청·axe 위반은 모두 0이었다.
Chrome 자체 진단 출력과 앱 페이지의 오류·네트워크 검증은 구별했다.
실제 360px 게임 스크린샷에서도 올바른 브랜드, 질문 줄바꿈, 보기·음량·SNR 표시를 확인했다.
- [360px 시작](evidence/release-mobile-home.png), [게임](evidence/release-mobile-game.png), [결과](evidence/release-mobile-results.png)
- [1280px 시작](evidence/release-desktop-home.png), [게임](evidence/release-desktop-game.png), [결과](evidence/release-desktop-results.png)
### 이번 변경 파일과 범위
- 앱·출제: `src/App.tsx`, `src/content.ts`, `src/game.ts`.
- 라이선스·metadata·브라우저 설정: `LICENSE`, `THIRD_PARTY_NOTICES.md`,
`public/THIRD_PARTY_NOTICES.md`, `package.json`, `package-lock.json`, `playwright.config.ts`.
의존성 버전과 integrity는 바꾸지 않았다.
- 생성·빌드: `scripts/generate_audio.py`, `scripts/check-build.mjs`,
`public/audio/`의 101 WAV와 manifest. 안내 WAV도 다시 생성했으며 원문은 유지했다.
- 회귀 검증: `tests/App.test.tsx`, `tests/content.test.ts`, `tests/game.test.ts`,
`tests/assets.test.ts`, `tests/build.test.ts`, 새 `tests/release.test.ts`,
`tests/playwright-config.test.ts`, `tests/browser/release.spec.ts`.
기존 `tests/browser/game.spec.ts`·`ui.spec.ts`는 새 스크린샷 이름을 사용한다.
- 문서·증거: `README.md`, `docs/ARCHITECTURE.md`, `docs/AUDIO_PROVENANCE.md`,
`docs/DEPLOYMENT.md`, `.agent/CONTEXT.md`, `TASK.md`, `ACCEPTANCE.md`,
이 `RESULT.md` 및 새 evidence 파일.
요청된 수정·자동 검증에 남은 실패는 없다. 한국어 합성 발음의 청감과 실물
모바일 Safari는 기존과 같이 별도 인수 대상이다. WAV 용량은 약 20.5MB로 늘었고
질문별로 로드한다. **서버 배포, Git commit, Git push는 실행하지 않았다.**
## Neo canonical 오디오 재시도 assertion 회귀 수정 — 2026-10-14 UTC
### RED 근거와 재현 조건
- **Neo가 보고한 기존 canonical 실패를 이번 RED 근거로 사용한다.**
전체 `npm run test:browser`(12 tests, workers=2)에서 desktop의
`오디오 HTTP 실패를 화면에 표시하고 재시도한다`가 재시도 후
`getByRole("group", { name: "답 선택" })`를 global expect timeout
**20,000ms** 안에 보지 못해 1회 실패했다.
- 같은 수정 전 테스트를 desktop 단독 `--workers=1 --repeat-each=3`으로
실행하면 **16.3~17.3초에 3회 모두 통과**했다는 Neo의 보고다.
실패는 병렬 전체 실행에서 간헐적으로 발생했다. 이는 이번 로컬 실행의
원문 로그가 아니며, 새로 실패를 재현했다고 주장하지 않는다.
- 사용자 메시지에 첨부된 **12 passed (2.2m)** 출력은 성공 실행 기록이다.
이를 실패 로그로 취급하지 않으며 기존 evidence도 보존한다.
### 원인 추적과 최소 수정 범위
`systematic-debugging` 절차로 실패한 assertion에서 상태 전이를 역추적했다.
404가 난 최초 재생은 `src/App.tsx`의 `introDone`을 설정하지 않는다.
재시도는 안내와 목표 WAV를 다시 로드·디코드하고, `src/audio/engine.ts`의
목표 소스 `onended` → `play()` 완료 → `ENDED` → `answer` 상태가 된 뒤에야
답 선택 그룹을 표시한다. 출제 seed는 `Date.now()`여서 실행별 목표 길이도
달라진다. 정상 단독 실행과 병렬 실패의 차이는 이 대기 경로의 시간 여유다.
현재 101개 WAV를 Python `wave`의 프레임 수 / sample rate로 실측했다.
안내는 **8.1205초**, 최장 목표 `message-market.wav`는 **10.1725초**다.
엔진의 시작 여유 **0.05초**와 안내→목표 간격 **0.35초**를 더한 최대
재생 일정은 **18.693초**다. 기존 20초 assertion에는 HTTP 로드·디코드·배경
합성·브라우저 스케줄링·DOM 반영·assertion 관찰을 위한 여유가 **1.307초**만
남는다. Neo의 병렬 실패와 단독 성공은 이 좁은 assertion 예산에 부합한다.
당시 trace가 없어 실패 1회의 세부 지연 분포는 확인할 수 없다.
[실측과 제어 흐름 기록](evidence/24-retry-audio-budget.log)
수정은 `tests/browser/game.spec.ts`의 **재시도 완료 `toBeVisible` 하나에만
30,000ms 상한**을 지정한다. 최대 재생 일정 이후 **11.307초**의 여유를 두며
조건이 충족되면 즉시 통과한다. 제품의 준비 제한 15초·재생 종료 후 5초
watchdog, global expect 20초, test timeout 120초, workers=2는 유지한다.
sleep, 자동 재시도, 오디오 mock, 테스트 skip, 제품 코드 변경은 추가하지 않는다.
### 수정 후 검증
환경: Node.js **26.7.0**, npm **11.19.0**, 시스템 Chrome
`/usr/bin/google-chrome`. 새 production build에서 실제 WAV를 사용했다.
브라우저 실행은 `env -u PLAYWRIGHT_NO_SANDBOX CHROME_PATH=/usr/bin/google-chrome`
접두사로 기본 sandbox를 유지했다. 아래 명령은 모두 종료 코드 **0**이다.
| 검증 명령 | 실제 GREEN 결과 | 로그 |
| --- | --- | --- |
| `npm run lint` | **PASS**, ESLint + TypeScript 오류 0 | [lint](evidence/24-retry-final-lint.log) |
| `npm test` | **9 files / 57 tests passed**, skip 0 | [test](evidence/24-retry-final-test.log) |
| `npm run build` | **PASS**, WAV 101개와 런타임 notice 전문 확인 | [build](evidence/24-retry-final-build.log) |
| `npm run test:browser -- tests/browser/game.spec.ts --project=desktop --workers=1 --repeat-each=3 --grep '오디오 HTTP 실패'` | **3 passed (52.9s)**, 각 **17.7s / 15.5s / 16.8s** | [targeted](evidence/24-retry-targeted-green.log) |
| `npm run test:browser` | **12 passed (2.2m)**, desktop/mobile, workers=2, skip 0 | [browser](evidence/24-retry-final-browser.log) |
전체 병렬 실행의 재시도 테스트는 desktop **19.9초**, mobile **14.8초**에
통과했다. 이 수치는 Playwright가 보고한 **테스트 전체 시간**이며 assertion
자체의 대기 시간으로 해석하지 않는다. 실제 404 오류 표시→재시도→답 선택→
처음으로 이동 후 초점 복귀 계약을 유지한 채 통과했다. 기존의 실제 오디오
10문제 완주, 레이아웃·접근성·외부 요청 없음, 릴리스·UI 검사도 모두 통과했다.
이번 변경은 `tests/browser/game.spec.ts`의 assertion 한 곳과 이 기록,
`evidence/24-retry-*.log`에 한정한다. 시작 시 소스 전체가 untracked였으므로
작업 전 SHA-256 snapshot과 비교해 `src/`, 음원, 의존성, Playwright 설정 및
기존 evidence가 그대로임을 확인했다. 전체 브라우저 실행이 다시 생성한
기존 `release-*.png`는 작업 전 사본으로 복원해 과거 검증 증거를 보존했다.
`git diff --check`, 수정 텍스트의 후행 공백 검사와 evidence 링크 확인도
통과했다. [변경 범위와 무결성 확인](evidence/24-retry-integrity.log)
요청한 반복·전체 검증은 모두 GREEN이다. **Git commit, push, 배포는 실행하지 않았다.**