From 9d7b187673854fa21f180f0de205c54013cf89ce Mon Sep 17 00:00:00 2001 From: jmj Date: Wed, 19 Aug 2026 02:31:48 +0900 Subject: [PATCH 1/5] =?UTF-8?q?ci:=20=EB=A6=B0=ED=8A=B8=20=EC=9B=8C?= =?UTF-8?q?=ED=81=AC=ED=94=8C=EB=A1=9C=EB=A5=BC=20=EC=8B=A4=EC=A0=9C=20?= =?UTF-8?q?=EB=B8=8C=EB=9E=9C=EC=B9=98=EC=97=90=EC=84=9C=20=EB=8F=8C?= =?UTF-8?q?=EA=B2=8C=20=ED=95=98=EA=B3=A0=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=9E=A1=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `lint.yml` 의 트리거가 main/develop 이었다. 이 저장소의 기본 브랜치는 dev 이고 (`origin/HEAD -> origin/dev`) develop 브랜치는 존재하지 않는다. 즉 워크플로가 **한 번도 실행된 적이 없다**. 그 사이 dev 에 eslint 에러 3건, flake8 에러 4건, 의존성 취약점 13건이 그대로 쌓였다. 트리거를 dev·main 으로 고치는 것만으로는 부족해서, 돌면 바로 깨지던 것들도 같이 정리했다. - `npm run type-check` 를 호출하는데 package.json 에 해당 스크립트가 없었다 → 추가 - Go 린트를 `./backend` 에서 실행 — go.mod 가 없어 조용히 통과했고, 정작 Go 코드가 있는 `realtime/` 은 검증된 적이 없다 → working-directory 를 realtime 으로 - Go 버전이 1.22 하드코딩 (realtime/go.mod 는 1.26.3) → `go-version-file` 로 위임 - `./gradlew checkstyleMain` — build.gradle 에 checkstyle 플러그인이 없어 태스크 부재 → 제거하고 플러그인 도입 PR 에서 추가하도록 주석으로 남김 - 테스트 잡이 아예 없었다. vitest 68건 / JUnit 39파일 / pytest 321건 / go 11파일이 전부 CI 밖 → 레이어별 4개 잡으로 병렬 실행 + 프론트 build 까지 `lint.yml` → `ci.yml` 로 이름 변경(더 이상 lint 만 돌지 않는다). 브랜치 전략 문서도 실제(dev 기준)와 일치시켰다 — 이 문서가 develop 을 전제한 게 애초 원인이었다. --- .github/workflows/ci.yml | 130 +++++++++++++++++++++++++++++++++++++ .github/workflows/lint.yml | 92 -------------------------- docs/git-conventions.md | 33 ++++++---- frontend/package.json | 1 + 4 files changed, 150 insertions(+), 106 deletions(-) create mode 100644 .github/workflows/ci.yml delete mode 100644 .github/workflows/lint.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..d2b5d832 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,130 @@ +name: CI + +# 기본 브랜치는 dev 다(origin/HEAD -> origin/dev). 이전 lint.yml 은 트리거가 +# main/develop 이라 한 번도 실행되지 않았고, 그 사이 dev 에 eslint 에러와 +# 의존성 취약점이 그대로 쌓였다. 트리거를 실제 브랜치에 맞추고 레이어별 +# 테스트까지 함께 돌린다. +on: + pull_request: + branches: [dev, main] + push: + branches: [dev, main] + +# 같은 브랜치에 연속 push 하면 이전 실행은 취소한다(러너 점유 방지). +concurrency: + group: ci-${{ github.ref }} + cancel-in-progress: true + +jobs: + frontend: + name: Frontend (lint · types · test · build) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version-file: frontend/.nvmrc + cache: npm + cache-dependency-path: frontend/package-lock.json + + - name: Install + working-directory: ./frontend + run: npm ci + + - name: Lint + working-directory: ./frontend + run: npm run lint + + - name: Type check + working-directory: ./frontend + run: npm run type-check + + - name: Test + working-directory: ./frontend + run: npm run test + + # 타입체크가 통과해도 번들 단계에서 깨지는 경우(정적 자산·플러그인)를 잡는다. + - name: Build + working-directory: ./frontend + run: npm run build + + backend: + name: Backend (Core, Spring) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: '21' + cache: gradle + + # checkstyle 은 build.gradle 에 플러그인이 없어 태스크 자체가 없다. + # 도입하는 PR 에서 이 잡에 단계를 추가한다. + - name: Test + working-directory: ./backend + run: ./gradlew test --no-daemon + + - name: Upload test report + if: failure() + uses: actions/upload-artifact@v4 + with: + name: backend-test-report + path: backend/build/reports/tests/test + + ai: + name: AI (FastAPI/LangChain) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: '3.13' + + - uses: astral-sh/setup-uv@v5 + with: + enable-cache: true + + - name: Install + working-directory: ./ai + run: uv sync --extra dev + + - name: Lint (flake8) + working-directory: ./ai + run: uv run flake8 . + + - name: Format check (black) + working-directory: ./ai + run: uv run black --check . + + - name: Test + working-directory: ./ai + run: uv run pytest -q + + realtime: + name: RealTime (Go) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + # Go 코드는 realtime/ 에 있다. 이전 워크플로는 ./backend 에서 실행해 + # go.mod 가 없어 조용히 통과했고, realtime 은 검증된 적이 없다. + - uses: actions/setup-go@v5 + with: + go-version-file: realtime/go.mod + cache-dependency-path: realtime/go.sum + + - name: Build + working-directory: ./realtime + run: go build ./... + + - name: Vet + working-directory: ./realtime + run: go vet ./... + + - name: Test + working-directory: ./realtime + run: go test ./... diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml deleted file mode 100644 index 9f9772df..00000000 --- a/.github/workflows/lint.yml +++ /dev/null @@ -1,92 +0,0 @@ -name: Lint Check - -on: - pull_request: - branches: - - main - - develop - push: - branches: - - main - - develop - -jobs: - lint: - runs-on: ubuntu-latest - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: '20' - cache: 'npm' - - - name: Setup Python - uses: actions/setup-python@v5 - with: - python-version: '3.13' - - - name: Install uv - uses: astral-sh/setup-uv@v5 - - - name: Setup Java - uses: actions/setup-java@v4 - with: - distribution: 'temurin' - java-version: '21' - - - name: Setup Go - uses: actions/setup-go@v5 - with: - go-version: '1.22' - - # Frontend (TypeScript) - - name: Install frontend dependencies - working-directory: ./frontend - run: npm ci - - - name: Lint frontend - working-directory: ./frontend - run: npm run lint - - - name: Type check frontend - working-directory: ./frontend - run: npm run type-check - - # Backend (Java + Go) - - name: Lint backend Java - working-directory: ./backend - run: | - if [ -f "pom.xml" ]; then - mvn checkstyle:check - elif [ -f "build.gradle" ] || [ -f "build.gradle.kts" ]; then - ./gradlew checkstyleMain checkstyleTest - fi - - - name: Lint backend Go - working-directory: ./backend - run: | - if [ -f "go.mod" ]; then - go fmt ./... - go vet ./... - if ! command -v golangci-lint &> /dev/null; then - curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin - fi - golangci-lint run - fi - - # AI (Python) - - name: Install AI dependencies - working-directory: ./ai - run: uv sync --extra dev - - - name: Lint AI (flake8) - working-directory: ./ai - run: uv run flake8 . - - - name: Check AI formatting (black) - working-directory: ./ai - run: uv run black --check . diff --git a/docs/git-conventions.md b/docs/git-conventions.md index 5b979fa5..b0480db3 100644 --- a/docs/git-conventions.md +++ b/docs/git-conventions.md @@ -1,22 +1,27 @@ # Git 컨벤션 -> 브랜치 전략, 커밋 메시지, PR 규약. 본 컨벤션은 `.github/PULL_REQUEST_TEMPLATE.md` 와 `.github/workflows/lint.yml` 의 전제와 일치한다. +> 브랜치 전략, 커밋 메시지, PR 규약. 본 컨벤션은 `.github/pull_request_template.md` 와 `.github/workflows/ci.yml` 의 전제와 일치한다. --- ## 1. 브랜치 전략 ### 1.1 브랜치 종류 + +**기본 브랜치는 `dev` 다** (`origin/HEAD -> origin/dev`). 모든 작업 브랜치는 `dev` 로 PR 을 낸다. +`develop` 브랜치는 존재하지 않는다 — 과거 문서/워크플로가 이 이름을 전제해 CI 가 실행되지 않았으므로, +새 워크플로를 만들 때 트리거 브랜치는 반드시 `dev` (필요 시 `main` 추가) 로 둔다. + | 브랜치 | 용도 | 머지 대상 | |--------|------|-----------| +| `dev` | 통합 개발 + 자동 배포 기준 (기본 브랜치) | main | | `main` | 배포 가능한 최신 상태 | (보호) | -| `develop` | 통합 개발 (옵션 — 본 프로젝트 기준 직접 main 사용 가능) | main | -| `feature/*` | 신규 기능 | main (또는 develop) | -| `fix/*` | 버그 수정 | main | -| `hotfix/*` | 긴급 운영 수정 | main + cherry-pick to develop | -| `chore/*` | 비기능 변경 (lint, deps, doc) | main | -| `refactor/*` | 동작 변경 없는 구조 개선 | main | -| `docs/*` | 문서만 | main | +| `feature/*` | 신규 기능 | dev | +| `fix/*` | 버그 수정 | dev | +| `hotfix/*` | 긴급 운영 수정 | dev (+ 필요 시 main cherry-pick) | +| `chore/*` | 비기능 변경 (lint, deps, doc) | dev | +| `refactor/*` | 동작 변경 없는 구조 개선 | dev | +| `docs/*` | 문서만 | dev | ### 1.2 명명 ``` @@ -31,7 +36,7 @@ chore/add-renovate-config ### 1.3 브랜치 수명 - feature: ≤ 1주 (장기 시 재분기 검토) -- 매일 main → feature 병합으로 충돌 최소화 (`git rebase main` 권장) +- 매일 dev → feature 병합으로 충돌 최소화 (`git rebase dev` 권장) --- @@ -95,7 +100,7 @@ docs(architecture): RabbitMQ 토폴로지 다이어그램 추가 커밋 메시지와 동일 포맷. squash merge 시 그대로 commit이 됨. ### 3.3 본문 (PR 템플릿) -`.github/PULL_REQUEST_TEMPLATE.md` 가 자동 적용. 다음 섹션 권장: +`.github/pull_request_template.md` 가 자동 적용. 다음 섹션 권장: ```markdown ## 작업 내용 @@ -127,20 +132,20 @@ docs(architecture): RabbitMQ 토폴로지 다이어그램 추가 ### 3.5 머지 정책 - **Squash merge** 기본 (히스토리 깔끔) - 단, 의미 있는 커밋이 여러 개라면 일반 merge도 허용 -- main 직접 푸시 금지 -- 1명 이상 approve + CI green 필수 +- dev 직접 푸시 금지 +- 1명 이상 approve + CI green 필수 (`ci.yml` 의 frontend·backend·ai·realtime 4개 잡) --- ## 4. 충돌 해결 -- 본인 브랜치에서 `git rebase main` (merge 대신) +- 본인 브랜치에서 `git rebase dev` (merge 대신) - 충돌 발생 시 양쪽 변경의 의도를 확인 (단순 텍스트 머지 X) - DB 마이그레이션 충돌: 버전 번호 재할당 + 순서 정합성 확인 --- -## 5. 보호 정책 (main) +## 5. 보호 정책 (dev · main) - 직접 push 금지 - PR + CI green + 1 approve 필수 diff --git a/frontend/package.json b/frontend/package.json index ddd1484a..a121ff8c 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -7,6 +7,7 @@ "dev": "vite", "build": "tsc -b && vite build", "lint": "eslint .", + "type-check": "tsc -b", "preview": "vite preview", "openapi": "openapi-typescript ../backend/openapi.json -o src/shared/api/generated.ts", "test": "vitest run", From eb31b1e3d6952a66eb66764382ec5b1e71f1c69e Mon Sep 17 00:00:00 2001 From: jmj Date: Wed, 19 Aug 2026 02:32:04 +0900 Subject: [PATCH 2/5] =?UTF-8?q?fix(frontend):=20=EB=A0=8C=EB=8D=94=20?= =?UTF-8?q?=EC=A4=91=20ref=20=EA=B0=B1=EC=8B=A0=20=EC=A0=9C=EA=B1=B0=20(re?= =?UTF-8?q?act-hooks/refs=203=EA=B1=B4)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit dev 에 eslint 에러 3건이 살아있었다 — CI 가 돌지 않아 잡히지 않았다. `useTypewriter`, `useInterviewSocket` 이 렌더 함수 본문에서 `ref.current = x` 로 최신 prop 을 ref 에 밀어넣고 있었다. React 는 렌더를 순수하게 유지해야 하는데(중단·재실행 가능성) 렌더 중 ref 를 쓰면 재실행 시 값이 어긋난다. 커밋 후 이펙트로 옮겼다. 두 훅 모두 ref 를 읽는 이펙트(타자기 인터벌 / WS 연결)보다 **먼저** 선언해서, 같은 커밋 안에서 ref 가 항상 먼저 최신화되도록 순서를 보장했다. useRef 초기값이 이미 첫 렌더의 값이므로 마운트 시점 동작도 그대로다. 동작 변경 의도 없음. eslint 0건, tsc 통과, vitest 68/68 통과. --- frontend/src/features/interview/lib/useTypewriter.ts | 6 +++++- .../src/features/interview/model/useInterviewSocket.ts | 9 +++++++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/frontend/src/features/interview/lib/useTypewriter.ts b/frontend/src/features/interview/lib/useTypewriter.ts index c6ee7509..792b2721 100644 --- a/frontend/src/features/interview/lib/useTypewriter.ts +++ b/frontend/src/features/interview/lib/useTypewriter.ts @@ -5,7 +5,11 @@ import { useEffect, useRef, useState } from 'react' export function useTypewriter(fullText: string, enabled: boolean): string { const [count, setCount] = useState(enabled ? 0 : fullText.length) const targetRef = useRef(fullText) - targetRef.current = fullText + // 렌더 중 ref 대입은 react-hooks/refs 위반이라 커밋 후 이펙트에서 갱신한다. + // 아래 타자기 이펙트보다 먼저 선언해, 같은 커밋에서 ref 가 항상 먼저 최신화되게 한다. + useEffect(() => { + targetRef.current = fullText + }, [fullText]) useEffect(() => { if (!enabled) { diff --git a/frontend/src/features/interview/model/useInterviewSocket.ts b/frontend/src/features/interview/model/useInterviewSocket.ts index 97e5b7cb..52daaa3a 100644 --- a/frontend/src/features/interview/model/useInterviewSocket.ts +++ b/frontend/src/features/interview/model/useInterviewSocket.ts @@ -26,8 +26,13 @@ export function useInterviewSocket({ const socketRef = useRef(null) const onEventRef = useRef(onEvent) const onStatusRef = useRef(onStatusChange) - onEventRef.current = onEvent - onStatusRef.current = onStatusChange + // 콜백은 매 렌더 새로 만들어지므로 ref 로 최신 것을 넘긴다(연결 이펙트를 재실행시키지 않기 위해). + // 렌더 중 대입은 react-hooks/refs 위반이라 커밋 후 이펙트에서 갱신하고, 아래 연결 이펙트보다 + // 먼저 선언해 같은 커밋에서 ref 가 항상 먼저 최신화되게 한다. + useEffect(() => { + onEventRef.current = onEvent + onStatusRef.current = onStatusChange + }, [onEvent, onStatusChange]) useEffect(() => { if (!enabled || sessionId == null) return From a988def184bd7210a236797f8200817e1e6ba321 Mon Sep 17 00:00:00 2001 From: jmj Date: Wed, 19 Aug 2026 02:32:23 +0900 Subject: [PATCH 3/5] =?UTF-8?q?chore(frontend):=20=EC=9D=98=EC=A1=B4?= =?UTF-8?q?=EC=84=B1=20=EC=B7=A8=EC=95=BD=EC=A0=90=2013=EA=B1=B4=20?= =?UTF-8?q?=ED=95=B4=EC=86=8C=20(high=209=EA=B1=B4)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `npm audit` 13건(high 9). semver 범위 안에서 전부 해결되어 package.json 변경 없이 lockfile 만 갱신했다. 결과: 0건. 특히 **react-router-dom 은 프로덕션 의존성**이고 권고 5건이 걸려 있었다 — ``·`useNavigate` 의 백슬래시 open redirect, RSC 에러 핸들러 XSS, deserializeErrors 임의 constructor 주입, 라우트 매칭 DoS, RSC CSRF 우회. react-router-dom 7.15.0 → 7.18.2 vite 는 dev 서버 계열(optimized deps `.map` path traversal, `server.fs.deny` 우회, WebSocket 임의 파일 읽기)로 배포물에는 영향이 없지만 개발자 로컬이 노출된다. vite 8.0.x → 8.2.1 검증: eslint 0건, tsc 통과, vitest 68/68, `npm run build` 성공. --- frontend/package-lock.json | 808 +++++++++++++++++++++++-------------- 1 file changed, 503 insertions(+), 305 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 732a3eb8..f8c399b5 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -102,13 +102,13 @@ "license": "MIT" }, "node_modules/@babel/code-frame": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", - "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.28.5", + "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" }, @@ -117,9 +117,9 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", - "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", "dev": true, "license": "MIT", "engines": { @@ -127,21 +127,21 @@ } }, "node_modules/@babel/core": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", - "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.29.0", - "@babel/generator": "^7.29.0", - "@babel/helper-compilation-targets": "^7.28.6", - "@babel/helper-module-transforms": "^7.28.6", - "@babel/helpers": "^7.28.6", - "@babel/parser": "^7.29.0", - "@babel/template": "^7.28.6", - "@babel/traverse": "^7.29.0", - "@babel/types": "^7.29.0", + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", @@ -158,14 +158,14 @@ } }, "node_modules/@babel/generator": { - "version": "7.29.1", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", - "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.8.tgz", + "integrity": "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "^7.29.0", - "@babel/types": "^7.29.0", + "@babel/parser": "^7.29.8", + "@babel/types": "^7.29.8", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" @@ -175,14 +175,14 @@ } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", - "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", "dev": true, "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.28.6", - "@babel/helper-validator-option": "^7.27.1", + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" @@ -192,9 +192,9 @@ } }, "node_modules/@babel/helper-globals": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", - "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", "dev": true, "license": "MIT", "engines": { @@ -202,29 +202,29 @@ } }, "node_modules/@babel/helper-module-imports": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", - "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", "dev": true, "license": "MIT", "dependencies": { - "@babel/traverse": "^7.28.6", - "@babel/types": "^7.28.6" + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", - "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-module-imports": "^7.28.6", - "@babel/helper-validator-identifier": "^7.28.5", - "@babel/traverse": "^7.28.6" + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -234,9 +234,9 @@ } }, "node_modules/@babel/helper-string-parser": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", "dev": true, "license": "MIT", "engines": { @@ -244,9 +244,9 @@ } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", - "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", "dev": true, "license": "MIT", "engines": { @@ -254,9 +254,9 @@ } }, "node_modules/@babel/helper-validator-option": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", - "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", "dev": true, "license": "MIT", "engines": { @@ -264,27 +264,27 @@ } }, "node_modules/@babel/helpers": { - "version": "7.29.2", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", - "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/template": "^7.28.6", - "@babel/types": "^7.29.0" + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.29.2", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", - "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.8.tgz", + "integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.29.0" + "@babel/types": "^7.29.8" }, "bin": { "parser": "bin/babel-parser.js" @@ -303,33 +303,33 @@ } }, "node_modules/@babel/template": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", - "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.28.6", - "@babel/parser": "^7.28.6", - "@babel/types": "^7.28.6" + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", - "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.8.tgz", + "integrity": "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.29.0", - "@babel/generator": "^7.29.0", - "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.29.0", - "@babel/template": "^7.28.6", - "@babel/types": "^7.29.0", + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.8", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.8", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.8", "debug": "^4.3.1" }, "engines": { @@ -337,14 +337,14 @@ } }, "node_modules/@babel/types": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", - "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.28.5" + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -503,37 +503,6 @@ "node": ">=20.19.0" } }, - "node_modules/@emnapi/core": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.1.tgz", - "integrity": "sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA==", - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.2.0", - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/runtime": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.1.tgz", - "integrity": "sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA==", - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/wasi-threads": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.0.tgz", - "integrity": "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==", - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, "node_modules/@eslint-community/eslint-utils": { "version": "4.9.1", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", @@ -806,26 +775,10 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@napi-rs/wasm-runtime": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.1.tgz", - "integrity": "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==", - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "^1.7.1", - "@emnapi/runtime": "^1.7.1", - "@tybys/wasm-util": "^0.10.1" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Brooooooklyn" - } - }, "node_modules/@oxc-project/types": { - "version": "0.122.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.122.0.tgz", - "integrity": "sha512-oLAl5kBpV4w69UtFZ9xqcmTi+GENWOcPF7FCrczTiBbmC0ibXxCwyvZGbO39rCVEuLGAZM84DH0pUIyyv/YJzA==", + "version": "0.144.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.144.0.tgz", + "integrity": "sha512-nuhZIOLuI6TFQ32I/WnUx+SCPY7SdSKwgnFHydAuoS1+Z4BRcaP+RRJmGzl9lw+0OFF7UmaESf7KQRXaNLHypg==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/Boshen" @@ -863,9 +816,9 @@ "license": "MIT" }, "node_modules/@redocly/openapi-core": { - "version": "1.34.15", - "resolved": "https://registry.npmjs.org/@redocly/openapi-core/-/openapi-core-1.34.15.tgz", - "integrity": "sha512-HAwCnNyKcs5XGQqms+9t7OdAPM/5TDstmhF+0i7tdCFato2QKuYIlyWETwkXd8c5zbltr1oB+6y9NTeQLr2d6Q==", + "version": "1.34.19", + "resolved": "https://registry.npmjs.org/@redocly/openapi-core/-/openapi-core-1.34.19.tgz", + "integrity": "sha512-o/0VgsBXgwcY1lyeqcVtSGdTQAPnVggo0fbFVPlxl5XVDKUcVH0OLRqt3CbkwByT5FU305E0iE0O7MzThjDblw==", "dev": true, "license": "MIT", "dependencies": { @@ -874,7 +827,7 @@ "colorette": "1.4.0", "https-proxy-agent": "7.0.6", "js-levenshtein": "1.1.6", - "js-yaml": "4.1.1", + "js-yaml": "4.3.1", "minimatch": "5.1.9", "pluralize": "8.0.0", "yaml-ast-parser": "0.0.43" @@ -895,9 +848,9 @@ } }, "node_modules/@redocly/openapi-core/node_modules/brace-expansion": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz", - "integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz", + "integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==", "dev": true, "license": "MIT", "dependencies": { @@ -932,9 +885,9 @@ } }, "node_modules/@rolldown/binding-android-arm64": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.12.tgz", - "integrity": "sha512-pv1y2Fv0JybcykuiiD3qBOBdz6RteYojRFY1d+b95WVuzx211CRh+ytI/+9iVyWQ6koTh5dawe4S/yRfOFjgaA==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.4.tgz", + "integrity": "sha512-jHC2cnyKz5xU2fhECtFl8OZ83cYNt13GZQD+0uMJ/X3o+ijmd56okHhTUwxVSHPx1IRVIJEZ1/1pPzeLCU6XKA==", "cpu": [ "arm64" ], @@ -948,9 +901,9 @@ } }, "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.12.tgz", - "integrity": "sha512-cFYr6zTG/3PXXF3pUO+umXxt1wkRK/0AYT8lDwuqvRC+LuKYWSAQAQZjCWDQpAH172ZV6ieYrNnFzVVcnSflAg==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-Dc5mPD8F5F/FS8i01syd7FTF6yB2fVthH/TRkjwJkzUK6EpoxHtqvZQP5Zwq80/5z19TWYHIg1KOHboCgVx/aQ==", "cpu": [ "arm64" ], @@ -964,9 +917,9 @@ } }, "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.12.tgz", - "integrity": "sha512-ZCsYknnHzeXYps0lGBz8JrF37GpE9bFVefrlmDrAQhOEi4IOIlcoU1+FwHEtyXGx2VkYAvhu7dyBf75EJQffBw==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.4.tgz", + "integrity": "sha512-fpDm4oBo6SqLvWUYCmFhdde3U9KH2fRNNMeAnAPAIwxRL345xutL0EtEUcuoxsoazdJGv/MuDBQHlCDrtbvqOg==", "cpu": [ "x64" ], @@ -980,9 +933,9 @@ } }, "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.12.tgz", - "integrity": "sha512-dMLeprcVsyJsKolRXyoTH3NL6qtsT0Y2xeuEA8WQJquWFXkEC4bcu1rLZZSnZRMtAqwtrF/Ib9Ddtpa/Gkge9Q==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.4.tgz", + "integrity": "sha512-rSJoreDE/HoIzoaib6MTp5jQtCTdMHKIvItAKT/ImS6Y6Ww76oUaeMyp4Vc/fAgd/ehji068IxetHXAnqUwN9A==", "cpu": [ "x64" ], @@ -996,9 +949,9 @@ } }, "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.12.tgz", - "integrity": "sha512-YqWjAgGC/9M1lz3GR1r1rP79nMgo3mQiiA+Hfo+pvKFK1fAJ1bCi0ZQVh8noOqNacuY1qIcfyVfP6HoyBRZ85Q==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.4.tgz", + "integrity": "sha512-/jm8OGHgn7oGaJu3i/qZI9spUGcJ+y/lk43ttQ/iO1tOd9NissG6o97bighBCiL+BKRngmcDuR6ikfwYdJmVuQ==", "cpu": [ "arm" ], @@ -1012,9 +965,9 @@ } }, "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.12.tgz", - "integrity": "sha512-/I5AS4cIroLpslsmzXfwbe5OmWvSsrFuEw3mwvbQ1kDxJ822hFHIx+vsN/TAzNVyepI/j/GSzrtCIwQPeKCLIg==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.4.tgz", + "integrity": "sha512-tIP06BeD9EqvECBrPZ+sqdPlYrT+aYaAiu1wYziVx5elRK/ftm33JxVDy2bXGbr6J0CrtirCkR87/X5a2euEng==", "cpu": [ "arm64" ], @@ -1028,9 +981,9 @@ } }, "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.12.tgz", - "integrity": "sha512-V6/wZztnBqlx5hJQqNWwFdxIKN0m38p8Jas+VoSfgH54HSj9tKTt1dZvG6JRHcjh6D7TvrJPWFGaY9UBVOaWPw==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.4.tgz", + "integrity": "sha512-Ql1Q0EQqVThvn9VAVlwNzsUvbSFtCMGjLpRRi4pk5i7NZZ4n5ISiLMjHYtus4VQ2PvkSw24zyaCVsiS+sXPj1w==", "cpu": [ "arm64" ], @@ -1044,9 +997,9 @@ } }, "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.12.tgz", - "integrity": "sha512-AP3E9BpcUYliZCxa3w5Kwj9OtEVDYK6sVoUzy4vTOJsjPOgdaJZKFmN4oOlX0Wp0RPV2ETfmIra9x1xuayFB7g==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.4.tgz", + "integrity": "sha512-GjbjXD4XXfN19D0LZNbmiCBUoDiRACsYHr0yaIbbn8aFsXjHZifcYqu/W5Er5X2X990WjHXFrxarn5chzItorQ==", "cpu": [ "ppc64" ], @@ -1060,9 +1013,9 @@ } }, "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.12.tgz", - "integrity": "sha512-nWwpvUSPkoFmZo0kQazZYOrT7J5DGOJ/+QHHzjvNlooDZED8oH82Yg67HvehPPLAg5fUff7TfWFHQS8IV1n3og==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.4.tgz", + "integrity": "sha512-p5WR0NOwaRmJ/B1b6IjEFLLivwEsf3PrdBIhRbhTCQisbo2SvHHpG4ELB/+FgQNnB88LTOF86upmJmbvZdQ2lw==", "cpu": [ "s390x" ], @@ -1076,9 +1029,9 @@ } }, "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.12.tgz", - "integrity": "sha512-RNrafz5bcwRy+O9e6P8Z/OCAJW/A+qtBczIqVYwTs14pf4iV1/+eKEjdOUta93q2TsT/FI0XYDP3TCky38LMAg==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.4.tgz", + "integrity": "sha512-4/GyVjmhR+Tc6HLJvwc1sOhPqAZtySiSMesOZyX6JQ5XBxoTDEMKQzvo07NIK6nTon/SivlZqvhzvuVBNQhObQ==", "cpu": [ "x64" ], @@ -1092,9 +1045,9 @@ } }, "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.12.tgz", - "integrity": "sha512-Jpw/0iwoKWx3LJ2rc1yjFrj+T7iHZn2JDg1Yny1ma0luviFS4mhAIcd1LFNxK3EYu3DHWCps0ydXQ5i/rrJ2ig==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.4.tgz", + "integrity": "sha512-l9eeLsCNvPpmSXUej0etw/J1eqV0Jj1D5G/xG6YTijmE6dkv6E2QezgWbTfQk63v952DPqrjOCoiqxq7Bw0YUQ==", "cpu": [ "x64" ], @@ -1108,9 +1061,9 @@ } }, "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.12.tgz", - "integrity": "sha512-vRugONE4yMfVn0+7lUKdKvN4D5YusEiPilaoO2sgUWpCvrncvWgPMzK00ZFFJuiPgLwgFNP5eSiUlv2tfc+lpA==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.4.tgz", + "integrity": "sha512-e0F355MSTMm3+UOqtV3L24gFUp2N5m1f8L/7d56deik6va+AXdrt9F8LbzGpeWGWRbZEDq4m8NVnJDeBtf9DZg==", "cpu": [ "arm64" ], @@ -1123,26 +1076,10 @@ "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@rolldown/binding-wasm32-wasi": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.12.tgz", - "integrity": "sha512-ykGiLr/6kkiHc0XnBfmFJuCjr5ZYKKofkx+chJWDjitX+KsJuAmrzWhwyOMSHzPhzOHOy7u9HlFoa5MoAOJ/Zg==", - "cpu": [ - "wasm32" - ], - "license": "MIT", - "optional": true, - "dependencies": { - "@napi-rs/wasm-runtime": "^1.1.1" - }, - "engines": { - "node": ">=14.0.0" - } - }, "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.12.tgz", - "integrity": "sha512-5eOND4duWkwx1AzCxadcOrNeighiLwMInEADT0YM7xeEOOFcovWZCq8dadXgcRHSf3Ulh1kFo/qvzoFiCLOL1Q==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.4.tgz", + "integrity": "sha512-AWLi0uBRYh6QlE7OKhiz+phZC0qwtij2QZmhmOdsLdFn64m7oMpooE9ICE3lhm9xMb4SpDo2WbHcxX1iFLFtqw==", "cpu": [ "arm64" ], @@ -1156,9 +1093,9 @@ } }, "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.12.tgz", - "integrity": "sha512-PyqoipaswDLAZtot351MLhrlrh6lcZPo2LSYE+VDxbVk24LVKAGOuE4hb8xZQmrPAuEtTZW8E6D2zc5EUZX4Lw==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.4.tgz", + "integrity": "sha512-UwSDJOg3dqCAejWdxclJjCsh3Qq4vLYMDxmyHqo1btz3stK2VqgwNd3mm5tuIwzSlGIQ/1H9Hr+Zn09mrezNqQ==", "cpu": [ "x64" ], @@ -1873,16 +1810,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", - "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^4.0.2" }, "engines": { - "node": "18 || 20 || >=22" + "node": "20 || >=22" } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { @@ -2231,13 +2168,13 @@ "license": "MIT" }, "node_modules/axios": { - "version": "1.16.1", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.16.1.tgz", - "integrity": "sha512-caYkukvroVPO8KrzuJEb50Hm07KwfBZPEC3VeFHTsqWHvKTsy54hjJz9BS/cdaypROE2rH6xvm9mHX4fgWkr3A==", + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.19.0.tgz", + "integrity": "sha512-ht/iuYZXEjFxLH/Hkezgd7m6JKlHHXEUSneaDz8uZe1Gj5QZtCnpyDsckvAiEnT89OEbCLmnte4R4sn7P0EKFw==", "license": "MIT", "dependencies": { "follow-redirects": "^1.16.0", - "form-data": "^4.0.5", + "form-data": "^4.0.6", "https-proxy-agent": "^5.0.1", "proxy-from-env": "^2.1.0" } @@ -2259,9 +2196,9 @@ } }, "node_modules/baseline-browser-mapping": { - "version": "2.10.11", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.11.tgz", - "integrity": "sha512-DAKrHphkJyiGuau/cFieRYhcTFeK/lBuD++C7cZ6KZHbMhBrisoi+EvhQ5RZrIfV5qwsW8kgQ07JIC+MDJRAhg==", + "version": "2.11.15", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.15.tgz", + "integrity": "sha512-FwMjJJ7HnyZpWe+oWxegG0fezZyBZUagI5LZEoO3GCbtbKNwRfMH9Ue5d5v01PNePBy1QSfPSDTTeVL0Hb9EzA==", "dev": true, "license": "Apache-2.0", "bin": { @@ -2282,9 +2219,9 @@ } }, "node_modules/brace-expansion": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", - "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", "dev": true, "license": "MIT", "dependencies": { @@ -2293,9 +2230,9 @@ } }, "node_modules/browserslist": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", - "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "version": "4.28.8", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.8.tgz", + "integrity": "sha512-V2NpofLblG64mfOtSgDhOJESZEGogzDMBv/q+W6oc4LXWP/q75eOXoOaaOu1EOadB9U4Bwx/e0yzbvwKH8zalA==", "dev": true, "funding": [ { @@ -2313,11 +2250,11 @@ ], "license": "MIT", "dependencies": { - "baseline-browser-mapping": "^2.9.0", - "caniuse-lite": "^1.0.30001759", - "electron-to-chromium": "^1.5.263", - "node-releases": "^2.0.27", - "update-browserslist-db": "^1.2.0" + "baseline-browser-mapping": "^2.11.12", + "caniuse-lite": "^1.0.30001809", + "electron-to-chromium": "^1.5.402", + "node-releases": "^2.0.53", + "update-browserslist-db": "^1.3.0" }, "bin": { "browserslist": "cli.js" @@ -2350,9 +2287,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001781", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001781.tgz", - "integrity": "sha512-RdwNCyMsNBftLjW6w01z8bKEvT6e/5tpPVEgtn22TiLGlstHOVecsX2KHFkD5e/vRnIE4EGzpuIODb3mtswtkw==", + "version": "1.0.30001809", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001809.tgz", + "integrity": "sha512-xxWVywk6a6Arlk+hymeycyn/VgqEfLDxupvhH/xiY5SJ/18kmi9o6MiO320DCUzypORHLtvh0I4i04tUhCNHNQ==", "dev": true, "funding": [ { @@ -2636,9 +2573,9 @@ "peer": true }, "node_modules/dompurify": { - "version": "3.4.7", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.7.tgz", - "integrity": "sha512-2jBxDJY4RR06tQNy4w5FlFH7kfxsQZlufd0sbv+chfHCxeJwrFw2baUDsSwvBISD4K4RDbd0PTfy3uNXsR6siA==", + "version": "3.4.13", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.13.tgz", + "integrity": "sha512-2vmYIoqjze2d+kakP8S/nS5shfsl587kzwEjcGlTdiksUVgFHnFCsLYDVj/JNqJVOQZGSYBTmuycv0PodwmnMQ==", "license": "(MPL-2.0 OR Apache-2.0)", "optional": true, "optionalDependencies": { @@ -2660,9 +2597,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.328", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.328.tgz", - "integrity": "sha512-QNQ5l45DzYytThO21403XN3FvK0hOkWDG8viNf6jqS42msJ8I4tGDSpBCgvDRRPnkffafiwAym2X2eHeGD2V0w==", + "version": "1.5.409", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.409.tgz", + "integrity": "sha512-ChI4N44d0B4A6C8prnNjMOaGgE59fUyEVYcRYm2XEXIjMbbvF5i9UL1cblDbpGqiU0uS8FE8UcKxqZqTXdmzbQ==", "dev": true, "license": "ISC" }, @@ -2718,9 +2655,9 @@ "license": "MIT" }, "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0" @@ -3114,16 +3051,16 @@ } }, "node_modules/form-data": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", - "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.12" + "hasown": "^2.0.4", + "mime-types": "^2.1.35" }, "engines": { "node": ">= 6" @@ -3281,9 +3218,9 @@ } }, "node_modules/hasown": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", - "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -3492,10 +3429,20 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.1.tgz", + "integrity": "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -4012,9 +3959,9 @@ "license": "MIT" }, "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", "funding": [ { "type": "github", @@ -4037,11 +3984,14 @@ "license": "MIT" }, "node_modules/node-releases": { - "version": "2.0.36", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.36.tgz", - "integrity": "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==", + "version": "2.0.53", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.53.tgz", + "integrity": "sha512-D9UOmYG3UH1V+ENW56t5QXBwJw1YEY18ruVeus89Rw+SyIgjPkCO84bRzO3uNIYosJbNwiabWVn48o3uJLjxFQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">=18" + } }, "node_modules/obug": { "version": "2.1.1", @@ -4229,9 +4179,9 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", "license": "MIT", "engines": { "node": ">=12" @@ -4251,9 +4201,9 @@ } }, "node_modules/postcss": { - "version": "8.5.8", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", - "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "version": "8.5.26", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz", + "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==", "funding": [ { "type": "opencollective", @@ -4270,7 +4220,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.11", + "nanoid": "^3.3.17", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -4393,9 +4343,9 @@ "peer": true }, "node_modules/react-router": { - "version": "7.15.0", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.15.0.tgz", - "integrity": "sha512-HW9vYwuM8f4yx66Izy8xfrzCM+SBJluoZcCbww9A1TySax11S5Vgw6fi3ZjMONw9J4gQwngL7PzkyIpJJpJ7RQ==", + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.18.2.tgz", + "integrity": "sha512-aUVMjFm3GAPTTZL7oYr5E7ETiqfQCHRLH+B+5afnICvf0r7kkK4eR6SMuwbSTJw/7t+12khT/Kahij49fqOCIg==", "license": "MIT", "dependencies": { "cookie": "^1.0.1", @@ -4415,12 +4365,12 @@ } }, "node_modules/react-router-dom": { - "version": "7.15.0", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.15.0.tgz", - "integrity": "sha512-VcrVg64Fo8nwBvDscajG8gRTLIuTC6N50nb22l2HOOV4PTOHgoGp8mUjy9wLiHYoYTSYI36tUnXZgasSRFZorQ==", + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.18.2.tgz", + "integrity": "sha512-AIKJ/jgGlFb3EbfCXk5Gzshiwt+l3mqbCrNjmEWMMjqQxNJ3svBa6bgzFyCC2Sw3RA0VWF1kg3uQf2OFhxb8hw==", "license": "MIT", "dependencies": { - "react-router": "7.15.0" + "react-router": "7.18.2" }, "engines": { "node": ">=20.0.0" @@ -4482,13 +4432,13 @@ } }, "node_modules/rolldown": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.12.tgz", - "integrity": "sha512-yP4USLIMYrwpPHEFB5JGH1uxhcslv6/hL0OyvTuY+3qlOSJvZ7ntYnoWpehBxufkgN0cvXxppuTu5hHa/zPh+A==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.4.tgz", + "integrity": "sha512-rSr7irW0K7QRWzjdJXqZowkcRdDtjRduh43rBltnVKd0VFq839l1lJoDvGJb6gl7+4rTTCrPWu+YfujUL8Ug7w==", "license": "MIT", "dependencies": { - "@oxc-project/types": "=0.122.0", - "@rolldown/pluginutils": "1.0.0-rc.12" + "@oxc-project/types": "=0.144.0", + "@rolldown/pluginutils": "^1.0.0" }, "bin": { "rolldown": "bin/cli.mjs" @@ -4497,27 +4447,26 @@ "node": "^20.19.0 || >=22.12.0" }, "optionalDependencies": { - "@rolldown/binding-android-arm64": "1.0.0-rc.12", - "@rolldown/binding-darwin-arm64": "1.0.0-rc.12", - "@rolldown/binding-darwin-x64": "1.0.0-rc.12", - "@rolldown/binding-freebsd-x64": "1.0.0-rc.12", - "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.12", - "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.12", - "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.12", - "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.12", - "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.12", - "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.12", - "@rolldown/binding-linux-x64-musl": "1.0.0-rc.12", - "@rolldown/binding-openharmony-arm64": "1.0.0-rc.12", - "@rolldown/binding-wasm32-wasi": "1.0.0-rc.12", - "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.12", - "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.12" + "@rolldown/binding-android-arm64": "1.2.4", + "@rolldown/binding-darwin-arm64": "1.2.4", + "@rolldown/binding-darwin-x64": "1.2.4", + "@rolldown/binding-freebsd-x64": "1.2.4", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.4", + "@rolldown/binding-linux-arm64-gnu": "1.2.4", + "@rolldown/binding-linux-arm64-musl": "1.2.4", + "@rolldown/binding-linux-ppc64-gnu": "1.2.4", + "@rolldown/binding-linux-s390x-gnu": "1.2.4", + "@rolldown/binding-linux-x64-gnu": "1.2.4", + "@rolldown/binding-linux-x64-musl": "1.2.4", + "@rolldown/binding-openharmony-arm64": "1.2.4", + "@rolldown/binding-win32-arm64-msvc": "1.2.4", + "@rolldown/binding-win32-x64-msvc": "1.2.4" } }, "node_modules/rolldown/node_modules/@rolldown/pluginutils": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.12.tgz", - "integrity": "sha512-HHMwmarRKvoFsJorqYlFeFRzXZqCt2ETQlEDOb9aqssrnVBB1/+xgTGtuTrIk5vzLNX1MjMtTf7W9z3tsSbrxw==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", "license": "MIT" }, "node_modules/saxes": { @@ -4720,13 +4669,13 @@ } }, "node_modules/tinyglobby": { - "version": "0.2.15", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", - "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", "license": "MIT", "dependencies": { "fdir": "^6.5.0", - "picomatch": "^4.0.3" + "picomatch": "^4.0.4" }, "engines": { "node": ">=12.0.0" @@ -4876,9 +4825,9 @@ } }, "node_modules/undici": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.27.0.tgz", - "integrity": "sha512-+t2Z/GwkZQDtu00813aP66ygViGtPHKhhoFZpQKpKrE+9jIgES+Zw+mFNaDWOVRKiuJjuqKHzD3B1sfGg8+ZOQ==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.29.0.tgz", + "integrity": "sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw==", "dev": true, "license": "MIT", "engines": { @@ -4893,9 +4842,9 @@ "license": "MIT" }, "node_modules/update-browserslist-db": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", - "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.1.tgz", + "integrity": "sha512-ZZ61DsRsOnakl74HAmp3oSN4aXUmEWXf+i/yv0h7tIBfICc3VdrFErQKUUKPgu3AMsTUMbcongALEN4l6GSUrQ==", "dev": true, "funding": [ { @@ -4950,16 +4899,16 @@ } }, "node_modules/vite": { - "version": "8.0.3", - "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.3.tgz", - "integrity": "sha512-B9ifbFudT1TFhfltfaIPgjo9Z3mDynBTJSUYxTjOQruf/zHH+ezCQKcoqO+h7a9Pw9Nm/OtlXAiGT1axBgwqrQ==", + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.2.1.tgz", + "integrity": "sha512-EU/eS7BH3XROHh2YnBefjM6DBKA6ZeMZEYQbj7NLWg5wHYlhB8B/Mayd5XsgWq+NFYccDOTemRpdETWR6Ka/lw==", "license": "MIT", "dependencies": { - "lightningcss": "^1.32.0", - "picomatch": "^4.0.4", - "postcss": "^8.5.8", - "rolldown": "1.0.0-rc.12", - "tinyglobby": "^0.2.15" + "lightningcss": "^1.33.0", + "picomatch": "^4.0.5", + "postcss": "^8.5.25", + "rolldown": "~1.2.1", + "tinyglobby": "^0.2.17" }, "bin": { "vite": "bin/vite.js" @@ -4975,8 +4924,8 @@ }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", - "@vitejs/devtools": "^0.1.0", - "esbuild": "^0.27.0", + "@vitejs/devtools": "^0.4.0", + "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", @@ -5026,6 +4975,255 @@ } } }, + "node_modules/vite/node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" + } + }, + "node_modules/vite/node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, "node_modules/vitest": { "version": "4.1.8", "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.8.tgz", From 4397b5650b6f7b62704017538c1628a9e054bc61 Mon Sep 17 00:00:00 2001 From: jmj Date: Wed, 19 Aug 2026 02:32:33 +0900 Subject: [PATCH 4/5] =?UTF-8?q?fix(ai):=20flake8=20=EC=97=90=EB=9F=AC=204?= =?UTF-8?q?=EA=B1=B4=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CI 를 켜면 걸리는 것들. 미사용 import 3건과 미정의 이름 1건. `test_followup_consumer.py` 의 F821 은 실제 문제였다. `_make_req()` 가 반환형을 문자열 `"GenerateFollowupRequest"` 로 적고 그 타입을 함수 본문에서 지역 import 했는데, 모듈 최상단이 이미 같은 모듈(`ai_server.model.messages.followup`)에서 다른 심볼을 import 하고 있고 `from __future__ import annotations` 도 걸려 있다. 지역 import 와 문자열 인용이 둘 다 불필요해서 최상단 import 로 합쳤다. pytest 321건 통과 유지. --- ai/tests/test_envelope.py | 1 - ai/tests/test_followup_consumer.py | 5 ++--- ai/tests/test_settings.py | 2 -- 3 files changed, 2 insertions(+), 6 deletions(-) diff --git a/ai/tests/test_envelope.py b/ai/tests/test_envelope.py index 69c7ae7b..b3f01aac 100644 --- a/ai/tests/test_envelope.py +++ b/ai/tests/test_envelope.py @@ -1,5 +1,4 @@ import json -from datetime import datetime, timezone from uuid import uuid4 import pytest diff --git a/ai/tests/test_followup_consumer.py b/ai/tests/test_followup_consumer.py index d75ddf93..f72c9b17 100644 --- a/ai/tests/test_followup_consumer.py +++ b/ai/tests/test_followup_consumer.py @@ -12,6 +12,7 @@ from ai_server.model.messages.followup import ( AnswerEvaluation, FollowupCallbackPayload, + GenerateFollowupRequest, ) @@ -555,9 +556,7 @@ async def test_callback_includes_answer_message_id(): # --------------------------------------------------------------------------- -def _make_req() -> "GenerateFollowupRequest": - from ai_server.model.messages.followup import GenerateFollowupRequest - +def _make_req() -> GenerateFollowupRequest: return GenerateFollowupRequest( session_id=99, parent_message_id=501, diff --git a/ai/tests/test_settings.py b/ai/tests/test_settings.py index 6baef0c6..84937cf8 100644 --- a/ai/tests/test_settings.py +++ b/ai/tests/test_settings.py @@ -1,5 +1,3 @@ -import os - import pytest from ai_server.config.settings import Settings From 939e3d8134a30855c0f8432d9b329a52f949d7b7 Mon Sep 17 00:00:00 2001 From: jmj Date: Wed, 19 Aug 2026 02:32:42 +0900 Subject: [PATCH 5/5] =?UTF-8?q?style(ai):=20black=20=ED=8F=AC=EB=A7=B7=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9=20(22=20=ED=8C=8C=EC=9D=BC)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `black --check` 가 22 파일에서 실패하고 있었다. CI 에서 이 게이트를 켜기 위해 `uv run black .` 결과를 그대로 커밋한다. 포맷만 바뀌었고 로직 변경은 없다 (pytest 321건 그대로 통과). 대부분 최근 음성/TTS 작업 경로(`voice/stt/*`, `voice/tts/*`, tts_consumer, llm_logging_callback)와 그 테스트다 — 포맷 검사가 한 번도 돌지 않아 누적됐다. --- ai/src/ai_server/api/voice_stream.py | 6 ++- .../messaging/consumers/tts_consumer.py | 4 +- ai/src/ai_server/model/messages/tts.py | 6 ++- ai/src/ai_server/model/messages/voice.py | 6 ++- .../observability/llm_logging_callback.py | 33 +++++++++++++---- ai/src/ai_server/voice/stt/deepgram_live.py | 29 +++++++++++---- ai/src/ai_server/voice/stt/live.py | 8 ++-- ai/src/ai_server/voice/stt/mock_live.py | 14 +++++-- ai/src/ai_server/voice/tts/mock.py | 4 +- ai/src/ai_server/voice/tts/openai_tts.py | 4 +- ai/tests/test_deepgram_stt.py | 22 +++++++++-- ai/tests/test_embedding_step.py | 4 +- ai/tests/test_feedback_highlights.py | 14 ++++++- ai/tests/test_feedback_rag_query.py | 14 +++++-- ai/tests/test_followup_parse.py | 18 +++++++-- ai/tests/test_followup_streaming.py | 4 +- ai/tests/test_mock_live.py | 4 +- ai/tests/test_short_confirmation.py | 37 ++++++++++++++----- ai/tests/test_stt_sanitize.py | 8 +++- ai/tests/test_tts_consumer.py | 4 +- ai/tests/test_voice_metrics.py | 8 +++- ai/tests/test_voice_stream_endpoint.py | 6 ++- 22 files changed, 195 insertions(+), 62 deletions(-) diff --git a/ai/src/ai_server/api/voice_stream.py b/ai/src/ai_server/api/voice_stream.py index 6dd1e49e..0e8e4656 100644 --- a/ai/src/ai_server/api/voice_stream.py +++ b/ai/src/ai_server/api/voice_stream.py @@ -86,7 +86,11 @@ async def pump_transcripts() -> None: await asyncio.wait_for(pump, timeout=10.0) except asyncio.TimeoutError: pump.cancel() - log.warn("voice_stream.pump.timeout", session_id=session_id, message_id=message_id) + log.warn( + "voice_stream.pump.timeout", + session_id=session_id, + message_id=message_id, + ) result = await session.result() await session.close() diff --git a/ai/src/ai_server/messaging/consumers/tts_consumer.py b/ai/src/ai_server/messaging/consumers/tts_consumer.py index 6e1dcdf1..c76dceab 100644 --- a/ai/src/ai_server/messaging/consumers/tts_consumer.py +++ b/ai/src/ai_server/messaging/consumers/tts_consumer.py @@ -225,9 +225,7 @@ async def _synth(sentence: str) -> TtsResult: ext = self._EXT_BY_CONTENT_TYPE.get( res.content_type.split(";")[0].strip(), "mp3" ) - seg_key = ( - f"interview/tts/{req.session_id}/{req.message_id}/seg-{seq}.{ext}" - ) + seg_key = f"interview/tts/{req.session_id}/{req.message_id}/seg-{seq}.{ext}" try: await self._storage.put_bytes( seg_key, res.audio_bytes, content_type=res.content_type diff --git a/ai/src/ai_server/model/messages/tts.py b/ai/src/ai_server/model/messages/tts.py index bb12f0eb..213fa557 100644 --- a/ai/src/ai_server/model/messages/tts.py +++ b/ai/src/ai_server/model/messages/tts.py @@ -7,10 +7,11 @@ class GenerateTtsRequest(BaseModel): """Core 가 질문 메시지 commit 후 발행. AI 가 text 를 TTS 합성.""" + model_config = camel_config() session_id: int - message_id: int # interview_messages.id (INTERVIEWER 질문) + message_id: int # interview_messages.id (INTERVIEWER 질문) text: str mode: Literal["PERSONALITY", "TECHNICAL", "INTEGRATED"] job_category: Literal["FRONTEND", "BACKEND", "INFRA", "DBA"] @@ -18,11 +19,12 @@ class GenerateTtsRequest(BaseModel): class TtsCallbackPayload(BaseModel): """AI → Core. 합성 결과(S3 키) 또는 실패. Core TtsCallbackPayload 와 호환.""" + model_config = camel_config() session_id: int message_id: int status: Literal["SUCCEEDED", "FAILED"] - audio_key: str | None = None # camelCase 직렬화 → audioKey + audio_key: str | None = None # camelCase 직렬화 → audioKey duration_sec: float | None = None error_code: str | None = None diff --git a/ai/src/ai_server/model/messages/voice.py b/ai/src/ai_server/model/messages/voice.py index 3853c72b..95e96ef4 100644 --- a/ai/src/ai_server/model/messages/voice.py +++ b/ai/src/ai_server/model/messages/voice.py @@ -9,10 +9,11 @@ class AnalyzeVoiceRequest(BaseModel): """Core 가 음성 답변 업로드 commit 후 발행.""" + model_config = camel_config() session_id: int - message_id: int # interview_messages.id (placeholder, STT 후 content 채움) + message_id: int # interview_messages.id (placeholder, STT 후 content 채움) parent_question_message_id: int | None = None audio_s3_key: str content_type: str @@ -23,6 +24,7 @@ class AnalyzeVoiceRequest(BaseModel): class VoiceCallbackPayload(BaseModel): """AI → Core. STT 결과 + 음성 지표.""" + model_config = camel_config() session_id: int @@ -32,4 +34,4 @@ class VoiceCallbackPayload(BaseModel): silence_duration_sec: float | None = None filler_word_counts: dict[str, int] = Field(default_factory=dict) pronunciation_accuracy: float | None = None - error_code: str | None = None # 실패 시 코드 (Core 가 message FAILED 마킹) + error_code: str | None = None # 실패 시 코드 (Core 가 message FAILED 마킹) diff --git a/ai/src/ai_server/observability/llm_logging_callback.py b/ai/src/ai_server/observability/llm_logging_callback.py index 7ccefdbf..4ae28975 100644 --- a/ai/src/ai_server/observability/llm_logging_callback.py +++ b/ai/src/ai_server/observability/llm_logging_callback.py @@ -22,16 +22,22 @@ class CoreAiLogCallback(AsyncCallbackHandler): metadata 에서 토큰 수를 추출한다. """ - def __init__(self, *, core_client: CoreClient, request_type: str, default_model: str) -> None: + def __init__( + self, *, core_client: CoreClient, request_type: str, default_model: str + ) -> None: self._core = core_client self._request_type = request_type self._default_model = default_model self._start_at: dict[UUID, float] = {} - async def on_llm_start(self, serialized, prompts, *, run_id: UUID, **kwargs: Any) -> None: + async def on_llm_start( + self, serialized, prompts, *, run_id: UUID, **kwargs: Any + ) -> None: self._start_at[run_id] = time.perf_counter() - async def on_llm_end(self, response: LLMResult, *, run_id: UUID, **kwargs: Any) -> None: + async def on_llm_end( + self, response: LLMResult, *, run_id: UUID, **kwargs: Any + ) -> None: latency_ms = self._latency_ms(run_id) in_tok, out_tok, model = _extract_usage(response, self._default_model) await self._fire( @@ -44,7 +50,9 @@ async def on_llm_end(self, response: LLMResult, *, run_id: UUID, **kwargs: Any) error_message=None, ) - async def on_llm_error(self, error: BaseException, *, run_id: UUID, **kwargs: Any) -> None: + async def on_llm_error( + self, error: BaseException, *, run_id: UUID, **kwargs: Any + ) -> None: latency_ms = self._latency_ms(run_id) await self._fire( request_type=self._request_type, @@ -69,10 +77,13 @@ async def _do() -> None: await self._core.record_ai_log(**kwargs) except Exception as exc: log.warn("core.ai_log.background_failed", error=str(exc), **kwargs) + asyncio.create_task(_do()) -def _extract_usage(response: LLMResult, default_model: str) -> tuple[int | None, int | None, str]: +def _extract_usage( + response: LLMResult, default_model: str +) -> tuple[int | None, int | None, str]: in_tok = out_tok = None model = default_model try: @@ -90,8 +101,16 @@ def _extract_usage(response: LLMResult, default_model: str) -> tuple[int | None, info = getattr(gen, "generation_info", None) or {} usage = info.get("usage_metadata") or info.get("token_usage") or {} if isinstance(usage, dict): - in_tok = in_tok or usage.get("input_tokens") or usage.get("prompt_tokens") - out_tok = out_tok or usage.get("output_tokens") or usage.get("completion_tokens") + in_tok = ( + in_tok + or usage.get("input_tokens") + or usage.get("prompt_tokens") + ) + out_tok = ( + out_tok + or usage.get("output_tokens") + or usage.get("completion_tokens") + ) except Exception as exc: log.debug("ai_log.usage_extract_failed", error=str(exc)) return in_tok, out_tok, model diff --git a/ai/src/ai_server/voice/stt/deepgram_live.py b/ai/src/ai_server/voice/stt/deepgram_live.py index e901ae0b..23195830 100644 --- a/ai/src/ai_server/voice/stt/deepgram_live.py +++ b/ai/src/ai_server/voice/stt/deepgram_live.py @@ -10,7 +10,11 @@ import websockets from ai_server.voice.stt.base import TranscriptionResult, TranscriptionSegment -from ai_server.voice.stt.live import LiveSttProvider, LiveSttSession, LiveTranscriptEvent +from ai_server.voice.stt.live import ( + LiveSttProvider, + LiveSttSession, + LiveTranscriptEvent, +) from ai_server.voice.stt.sanitize import ( sanitize_transcription, strip_stt_hallucinations, @@ -20,8 +24,16 @@ class _DeepgramLiveSession(LiveSttSession): - def __init__(self, *, api_key: str, url: str, model: str, language: str, - endpointing_ms: int, content_type: str) -> None: + def __init__( + self, + *, + api_key: str, + url: str, + model: str, + language: str, + endpointing_ms: int, + content_type: str, + ) -> None: self._api_key = api_key self._url = url self._model = model @@ -63,7 +75,7 @@ async def _recv_loop(self) -> None: msg = json.loads(raw) mtype = msg.get("type") if mtype == "Results": - alt = (((msg.get("channel") or {}).get("alternatives") or [{}])[0]) + alt = ((msg.get("channel") or {}).get("alternatives") or [{}])[0] text = str(alt.get("transcript") or "") is_final = bool(msg.get("is_final")) speech_final = bool(msg.get("speech_final")) @@ -135,8 +147,9 @@ async def close(self) -> None: class DeepgramLiveSttProvider(LiveSttProvider): model_name = "deepgram-live" - def __init__(self, *, api_key: str, url: str, model: str, language: str, - endpointing_ms: int) -> None: + def __init__( + self, *, api_key: str, url: str, model: str, language: str, endpointing_ms: int + ) -> None: if not api_key: raise ValueError("Deepgram API key 누락") self._api_key = api_key @@ -145,7 +158,9 @@ def __init__(self, *, api_key: str, url: str, model: str, language: str, self._language = language self._endpointing_ms = endpointing_ms - def open_session(self, *, content_type: str, language: str | None) -> LiveSttSession: + def open_session( + self, *, content_type: str, language: str | None + ) -> LiveSttSession: return _DeepgramLiveSession( api_key=self._api_key, url=self._url, diff --git a/ai/src/ai_server/voice/stt/live.py b/ai/src/ai_server/voice/stt/live.py index d77c202a..e2a9de75 100644 --- a/ai/src/ai_server/voice/stt/live.py +++ b/ai/src/ai_server/voice/stt/live.py @@ -10,8 +10,8 @@ @dataclass(frozen=True) class LiveTranscriptEvent: text: str - is_final: bool # 발화(utterance) 확정 조각인지 - speech_final: bool # 발화 끝(턴 종료 신호) + is_final: bool # 발화(utterance) 확정 조각인지 + speech_final: bool # 발화 끝(턴 종료 신호) class LiveSttSession(ABC): @@ -42,4 +42,6 @@ class LiveSttProvider(ABC): model_name: str = "live-stt" @abstractmethod - def open_session(self, *, content_type: str, language: str | None) -> LiveSttSession: ... + def open_session( + self, *, content_type: str, language: str | None + ) -> LiveSttSession: ... diff --git a/ai/src/ai_server/voice/stt/mock_live.py b/ai/src/ai_server/voice/stt/mock_live.py index d6550952..16d22026 100644 --- a/ai/src/ai_server/voice/stt/mock_live.py +++ b/ai/src/ai_server/voice/stt/mock_live.py @@ -4,7 +4,11 @@ from collections.abc import AsyncIterator from ai_server.voice.stt.base import TranscriptionResult, TranscriptionSegment -from ai_server.voice.stt.live import LiveSttProvider, LiveSttSession, LiveTranscriptEvent +from ai_server.voice.stt.live import ( + LiveSttProvider, + LiveSttSession, + LiveTranscriptEvent, +) class _MockLiveSession(LiveSttSession): @@ -21,7 +25,9 @@ async def push(self, chunk: bytes) -> None: # 청크마다 부분 transcript 한 조각 방출 (스크립트 순서대로). idx = min(self._pushes, len(self._script) - 1) await self._queue.put( - LiveTranscriptEvent(text=self._script[idx], is_final=False, speech_final=False) + LiveTranscriptEvent( + text=self._script[idx], is_final=False, speech_final=False + ) ) self._pushes += 1 @@ -63,5 +69,7 @@ class MockLiveSttProvider(LiveSttProvider): def __init__(self, script: list[str] | None = None) -> None: self._script = script or ["부분", "부분 transcript", "부분 transcript 완성"] - def open_session(self, *, content_type: str, language: str | None) -> LiveSttSession: + def open_session( + self, *, content_type: str, language: str | None + ) -> LiveSttSession: return _MockLiveSession(self._script) diff --git a/ai/src/ai_server/voice/tts/mock.py b/ai/src/ai_server/voice/tts/mock.py index 638b9e35..f39b3a7e 100644 --- a/ai/src/ai_server/voice/tts/mock.py +++ b/ai/src/ai_server/voice/tts/mock.py @@ -10,4 +10,6 @@ async def synthesize(self, text: str, *, voice: str) -> TtsResult: # 텍스트 길이에 비례한 가짜 오디오 바이트 + 추정 길이(분당 ~300자 가정). payload = ("MOCKMP3:" + text).encode("utf-8") duration = max(0.5, round(len(text) / 5.0, 2)) - return TtsResult(audio_bytes=payload, duration_sec=duration, content_type="audio/mpeg") + return TtsResult( + audio_bytes=payload, duration_sec=duration, content_type="audio/mpeg" + ) diff --git a/ai/src/ai_server/voice/tts/openai_tts.py b/ai/src/ai_server/voice/tts/openai_tts.py index 02e4b328..36d2e07f 100644 --- a/ai/src/ai_server/voice/tts/openai_tts.py +++ b/ai/src/ai_server/voice/tts/openai_tts.py @@ -47,4 +47,6 @@ async def synthesize(self, text: str, *, voice: str) -> TtsResult: if not audio: raise TtsError("TTS_EMPTY_AUDIO", "empty audio body") # OpenAI 는 duration 을 주지 않음 → None (Core completeTts 가 null 허용). - return TtsResult(audio_bytes=audio, duration_sec=None, content_type="audio/mpeg") + return TtsResult( + audio_bytes=audio, duration_sec=None, content_type="audio/mpeg" + ) diff --git a/ai/tests/test_deepgram_stt.py b/ai/tests/test_deepgram_stt.py index 5abd70fc..d9ee9d2e 100644 --- a/ai/tests/test_deepgram_stt.py +++ b/ai/tests/test_deepgram_stt.py @@ -34,14 +34,26 @@ async def test_deepgram_returns_segments_and_logprob_from_utterances(): } ], "utterances": [ - {"start": 0.0, "end": 2.5, "transcript": "안녕하세요", "confidence": 0.95}, - {"start": 3.0, "end": 6.0, "transcript": "백엔드 지원자입니다", "confidence": 0.88}, + { + "start": 0.0, + "end": 2.5, + "transcript": "안녕하세요", + "confidence": 0.95, + }, + { + "start": 3.0, + "end": 6.0, + "transcript": "백엔드 지원자입니다", + "confidence": 0.88, + }, ], }, } async with _client(status=200, body=response) as client: provider = DeepgramSttProvider(api_key="k", client=client) - result = await provider.transcribe(audio_bytes=b"fake", content_type="audio/webm") + result = await provider.transcribe( + audio_bytes=b"fake", content_type="audio/webm" + ) assert result.text.startswith("안녕하세요") assert result.duration_sec == 6.0 @@ -66,7 +78,9 @@ async def test_deepgram_fallback_segment_when_no_utterances(): } async with _client(status=200, body=response) as client: provider = DeepgramSttProvider(api_key="k", client=client) - result = await provider.transcribe(audio_bytes=b"fake", content_type="audio/webm") + result = await provider.transcribe( + audio_bytes=b"fake", content_type="audio/webm" + ) assert result.text == "한 줄 답변" assert len(result.segments) == 1 diff --git a/ai/tests/test_embedding_step.py b/ai/tests/test_embedding_step.py index b52acac3..9e3f1406 100644 --- a/ai/tests/test_embedding_step.py +++ b/ai/tests/test_embedding_step.py @@ -8,7 +8,9 @@ def test_contextualize_prefixes_summary_and_heading() -> None: - out = _contextualize("본문 내용", heading_path="주요 경험 > 결제", summary="백엔드 지원자") + out = _contextualize( + "본문 내용", heading_path="주요 경험 > 결제", summary="백엔드 지원자" + ) assert out.startswith("[백엔드 지원자 > 주요 경험 > 결제]") assert out.endswith("본문 내용") diff --git a/ai/tests/test_feedback_highlights.py b/ai/tests/test_feedback_highlights.py index 4565aa2a..fcc5ba19 100644 --- a/ai/tests/test_feedback_highlights.py +++ b/ai/tests/test_feedback_highlights.py @@ -18,9 +18,19 @@ def test_keeps_only_verbatim_substrings(): def test_dedup_and_cap(): body = "가가 나나 다다 라라 마마 바바 사사 가가" out = _filter_highlights( - ["가가", "가가", "나나", "다다", "라라", "마마", "바바", "사사"], body, None, cap=6 + ["가가", "가가", "나나", "다다", "라라", "마마", "바바", "사사"], + body, + None, + cap=6, ) - assert out == ["가가", "나나", "다다", "라라", "마마", "바바"] # 중복 제거 + 6개 상한 + assert out == [ + "가가", + "나나", + "다다", + "라라", + "마마", + "바바", + ] # 중복 제거 + 6개 상한 def test_empty_when_no_match_or_no_body(): diff --git a/ai/tests/test_feedback_rag_query.py b/ai/tests/test_feedback_rag_query.py index 992c55b6..b0956b0a 100644 --- a/ai/tests/test_feedback_rag_query.py +++ b/ai/tests/test_feedback_rag_query.py @@ -4,14 +4,22 @@ from ai_server.model.messages.feedback import FeedbackMessageItem -def _msg(mid: int, role: str, content: str, seq: int | None = None) -> FeedbackMessageItem: - return FeedbackMessageItem(id=mid, sequence_number=seq or mid, role=role, content=content) +def _msg( + mid: int, role: str, content: str, seq: int | None = None +) -> FeedbackMessageItem: + return FeedbackMessageItem( + id=mid, sequence_number=seq or mid, role=role, content=content + ) def test_joins_substantive_answers_excludes_short_confirmations() -> None: msgs = [ _msg(1, "INTERVIEWER", "인덱스를 어떻게 튜닝했나요?"), - _msg(2, "INTERVIEWEE", "복합 인덱스를 user_id, created_at 으로 잡아 풀스캔을 제거했습니다."), + _msg( + 2, + "INTERVIEWEE", + "복합 인덱스를 user_id, created_at 으로 잡아 풀스캔을 제거했습니다.", + ), _msg(3, "INTERVIEWER", "그럼 커버링 인덱스였나요?"), _msg(4, "INTERVIEWEE", "네 맞습니다."), # 짧은 확인 → 제외 _msg(5, "INTERVIEWER", "캐시는요?"), diff --git a/ai/tests/test_followup_parse.py b/ai/tests/test_followup_parse.py index 6c3bac96..25b734cd 100644 --- a/ai/tests/test_followup_parse.py +++ b/ai/tests/test_followup_parse.py @@ -26,7 +26,9 @@ def test_extract_question_only_helper(): assert extract_question_span("NORMAL안녕") == "안녕" assert ( - extract_question_span("NORMAL안녕{}") + extract_question_span( + "NORMAL안녕{}" + ) == "안녕" ) @@ -40,7 +42,9 @@ def test_streaming_keeps_lt_in_body(): == "a < b 인지 확인하세요" ) assert ( - extract_question_span("NORMAL제네릭 List 를 설명하세요") + extract_question_span( + "NORMAL제네릭 List 를 설명하세요" + ) == "제네릭 List 를 설명하세요" ) @@ -49,8 +53,14 @@ def test_streaming_strips_partial_closing_or_meta_tag(): # 청크 경계로 잘려 들어온 닫는/메타 태그 partial 만 제거한다. from ai_server.chain.followup_generation_chain import extract_question_span - assert extract_question_span("NORMAL질문입니다?NORMAL끝났나요?NORMAL질문입니다?NORMAL끝났나요?DONT_KNOW모르면 이렇게{}"] + pieces = [ + "DONT_KNOW모르면 이렇게{}" + ] prompt = ChatPromptTemplate.from_messages([("human", "{answer_text}")]) gen = StreamingFollowupGenerator(prompt=prompt, llm=_FakeStreamLLM(pieces)) seen = [] diff --git a/ai/tests/test_mock_live.py b/ai/tests/test_mock_live.py index 303c3481..d7ce92d8 100644 --- a/ai/tests/test_mock_live.py +++ b/ai/tests/test_mock_live.py @@ -5,7 +5,9 @@ @pytest.mark.asyncio async def test_mock_live_emits_partials_then_final(): - provider = MockLiveSttProvider(script=["안녕", "안녕하세요", "안녕하세요 반갑습니다"]) + provider = MockLiveSttProvider( + script=["안녕", "안녕하세요", "안녕하세요 반갑습니다"] + ) session = provider.open_session(content_type="audio/webm", language="ko") await session.start() # 오디오 청크 3개 투입 diff --git a/ai/tests/test_short_confirmation.py b/ai/tests/test_short_confirmation.py index 07180e18..f938ac31 100644 --- a/ai/tests/test_short_confirmation.py +++ b/ai/tests/test_short_confirmation.py @@ -41,15 +41,34 @@ def test_keeps_substantive_answers(text: str) -> None: def test_coaching_pairs_skip_short_confirmation() -> None: msgs = [ - FeedbackMessageItem(id=1, sequence_number=1, role="INTERVIEWER", - content="그럼 인덱스를 직접 만드셨나요?", category="TECH_CHOICE"), - FeedbackMessageItem(id=2, sequence_number=2, role="INTERVIEWEE", - content="네 맞습니다.", parent_message_id=1), - FeedbackMessageItem(id=3, sequence_number=3, role="INTERVIEWER", - content="어떤 컬럼으로 복합 인덱스를 구성했나요?", category="TECH_CHOICE"), - FeedbackMessageItem(id=4, sequence_number=4, role="INTERVIEWEE", - content="조회 조건인 user_id 와 created_at 으로 복합 인덱스를 잡았습니다.", - parent_message_id=3), + FeedbackMessageItem( + id=1, + sequence_number=1, + role="INTERVIEWER", + content="그럼 인덱스를 직접 만드셨나요?", + category="TECH_CHOICE", + ), + FeedbackMessageItem( + id=2, + sequence_number=2, + role="INTERVIEWEE", + content="네 맞습니다.", + parent_message_id=1, + ), + FeedbackMessageItem( + id=3, + sequence_number=3, + role="INTERVIEWER", + content="어떤 컬럼으로 복합 인덱스를 구성했나요?", + category="TECH_CHOICE", + ), + FeedbackMessageItem( + id=4, + sequence_number=4, + role="INTERVIEWEE", + content="조회 조건인 user_id 와 created_at 으로 복합 인덱스를 잡았습니다.", + parent_message_id=3, + ), ] pairs = _collect_coachable_pairs(msgs) answer_ids = [a.id for _, a in pairs] diff --git a/ai/tests/test_stt_sanitize.py b/ai/tests/test_stt_sanitize.py index 557fe539..f796bf57 100644 --- a/ai/tests/test_stt_sanitize.py +++ b/ai/tests/test_stt_sanitize.py @@ -56,7 +56,9 @@ def test_sanitize_transcription_drops_hallucination_segment() -> None: language="ko", duration_sec=30.0, segments=[ - TranscriptionSegment(start_sec=0.0, end_sec=4.0, text="타임아웃을 잡았습니다."), + TranscriptionSegment( + start_sec=0.0, end_sec=4.0, text="타임아웃을 잡았습니다." + ), TranscriptionSegment( start_sec=29.0, end_sec=30.0, text="MBC 뉴스 김재경입니다." ), @@ -69,5 +71,7 @@ def test_sanitize_transcription_drops_hallucination_segment() -> None: assert len(cleaned.segments) == 1 assert cleaned.segments[0].text == "타임아웃을 잡았습니다." # 환각이 없는 결과는 동일 객체를 그대로 반환(불필요한 재할당 방지). - clean_in = TranscriptionResult(text="정상 답변입니다.", language="ko", duration_sec=2.0, segments=[]) + clean_in = TranscriptionResult( + text="정상 답변입니다.", language="ko", duration_sec=2.0, segments=[] + ) assert sanitize_transcription(clean_in) is clean_in diff --git a/ai/tests/test_tts_consumer.py b/ai/tests/test_tts_consumer.py index 53319647..1b13d44e 100644 --- a/ai/tests/test_tts_consumer.py +++ b/ai/tests/test_tts_consumer.py @@ -50,7 +50,9 @@ class _RecordingNotifier: def __init__(self): self.audio: list[dict] = [] - async def emit_audio(self, *, session_id, message_id, seq, ext, duration_sec, trace_id): + async def emit_audio( + self, *, session_id, message_id, seq, ext, duration_sec, trace_id + ): self.audio.append( {"session_id": session_id, "message_id": message_id, "seq": seq, "ext": ext} ) diff --git a/ai/tests/test_voice_metrics.py b/ai/tests/test_voice_metrics.py index caf2bcd1..c9b761f8 100644 --- a/ai/tests/test_voice_metrics.py +++ b/ai/tests/test_voice_metrics.py @@ -14,8 +14,12 @@ def test_word_count_and_wpm(): language="ko", duration_sec=60.0, segments=[ - TranscriptionSegment(start_sec=0.0, end_sec=60.0, text="저는 백엔드 개발자 입니다", - avg_logprob=-0.2), + TranscriptionSegment( + start_sec=0.0, + end_sec=60.0, + text="저는 백엔드 개발자 입니다", + avg_logprob=-0.2, + ), ], ) m = analyze(result, filler_pattern=FILLER_PATTERN) diff --git a/ai/tests/test_voice_stream_endpoint.py b/ai/tests/test_voice_stream_endpoint.py index 28f76a43..39d191e2 100644 --- a/ai/tests/test_voice_stream_endpoint.py +++ b/ai/tests/test_voice_stream_endpoint.py @@ -14,7 +14,7 @@ async def publish(self, **kwargs): class _Settings: - core_internal_api_key = "" # 빈 값이면 인증 skip + core_internal_api_key = "" # 빈 값이면 인증 skip voice_filler_pattern = r"(음+|어+|그+)" ai_callback_routing_voice = "callback.voice" @@ -31,7 +31,9 @@ def _app(publisher): def test_stream_publishes_callback_voice_on_stop(): publisher = _FakePublisher() client = TestClient(_app(publisher)) - with client.websocket_connect("/internal/voice/stream?sessionId=7&messageId=42") as ws: + with client.websocket_connect( + "/internal/voice/stream?sessionId=7&messageId=42" + ) as ws: ws.send_bytes(b"chunk1") ws.send_bytes(b"chunk2") # 부분 자막 수신