diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0418b8c..f6f813e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -13,6 +13,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Cloud-optional backup (encrypted, opt-in)
- Plugin system for custom pipelines
+## [1.3.6] - 2026-09-18
+
+### Fixed
+- **Installing the local speech engine always failed** with a hash mismatch, leaving local
+ transcription unusable. The downloaded parts were checked against bytes counted from the
+ network stream, while the joined archive was checked against what was actually written to
+ disk, so a truncated write passed part verification and only surfaced later as an archive
+ mismatch with no usable diagnostic. Every check now reads the file on disk, the joined
+ archive is size-checked before hashing, mismatch errors report the actual and expected
+ values, and a failed part is discarded and retried up to three times.
+
## [1.3.5] - 2026-09-18
### Fixed
diff --git a/apps/admin-swagger/openapi.json b/apps/admin-swagger/openapi.json
index 54249a4..943a0e2 100644
--- a/apps/admin-swagger/openapi.json
+++ b/apps/admin-swagger/openapi.json
@@ -3,7 +3,7 @@
"info": {
"title": "D3RO-VOICE Admin API",
"description": "Admin CRM Edge Functions for user management, subscription CRUD, payment history, and audit logs.",
- "version": "1.3.5"
+ "version": "1.3.6"
},
"servers": [
{
diff --git a/apps/admin/package.json b/apps/admin/package.json
index 188da87..ec9a77f 100644
--- a/apps/admin/package.json
+++ b/apps/admin/package.json
@@ -1,6 +1,6 @@
{
"name": "@d3ro/admin",
- "version": "1.3.5",
+ "version": "1.3.6",
"private": true,
"description": "D3RO Voice Admin CRM — SaaS 관리 도구",
"scripts": {
diff --git a/apps/api-server/D3ROVoice.Api.csproj b/apps/api-server/D3ROVoice.Api.csproj
index a7ada74..4543483 100644
--- a/apps/api-server/D3ROVoice.Api.csproj
+++ b/apps/api-server/D3ROVoice.Api.csproj
@@ -2,7 +2,7 @@
net10.0
- 1.3.5
+ 1.3.6
enable
enable
diff --git a/apps/desktop/package.json b/apps/desktop/package.json
index d05f6ec..35b1769 100644
--- a/apps/desktop/package.json
+++ b/apps/desktop/package.json
@@ -1,6 +1,6 @@
{
"name": "@d3ro/desktop",
- "version": "1.3.5",
+ "version": "1.3.6",
"productName": "d3ro-voice",
"description": "로컬 AI 음성 어시스턴트 (Electron)",
"main": "./out/main/index.js",
diff --git a/apps/desktop/src/main/services/RuntimeProvisioner.ts b/apps/desktop/src/main/services/RuntimeProvisioner.ts
index 923a3c6..35e012f 100644
--- a/apps/desktop/src/main/services/RuntimeProvisioner.ts
+++ b/apps/desktop/src/main/services/RuntimeProvisioner.ts
@@ -14,8 +14,8 @@
import { EventEmitter, once } from 'events'
import { createHash } from 'node:crypto'
import { createReadStream, createWriteStream, existsSync, statSync } from 'node:fs'
-import { mkdir, rm } from 'node:fs/promises'
-import { Readable } from 'node:stream'
+import { mkdir, rm, stat } from 'node:fs/promises'
+import { Readable, Writable } from 'node:stream'
import { pipeline } from 'node:stream/promises'
import { join } from 'node:path'
import { app } from 'electron'
@@ -69,6 +69,8 @@ export interface RuntimeStatus {
const RUNTIME_DIR_NAME = 'runtime'
const DOWNLOAD_TIMEOUT_MS = 120_000
+/** 부품 다운로드 재시도 횟수 — 전송 중 잘림/일시적 네트워크 오류 대비 */
+const PART_DOWNLOAD_ATTEMPTS = 3
class RuntimeProvisioner extends EventEmitter {
constructor() {
@@ -248,68 +250,101 @@ class RuntimeProvisioner extends EventEmitter {
for (const part of entry.parts) {
const partPath = join(tempDir, part.name)
- const response = await fetch(part.url, { signal: AbortSignal.timeout(DOWNLOAD_TIMEOUT_MS) })
- if (!response.ok || !response.body) {
- throw new D3ROError(
- ErrorCode.STTSidecarSpawnFailed,
- `런타임 부품을 받을 수 없습니다 (HTTP ${response.status}): ${part.name}`,
- )
- }
+ const partSize = await this._downloadPart(part, partPath)
- const hash = createHash('sha256')
- let partBytes = 0
- const source = Readable.fromWeb(response.body as never)
- source.on('data', (chunk: Buffer) => {
- hash.update(chunk)
- partBytes += chunk.length
- downloadedBytes += chunk.length
- const elapsed = Math.max(0.001, (Date.now() - startedAt) / 1000)
- this._emitProgress(
- component,
- 'downloading',
- totalBytes > 0 ? Math.min(100, Math.round((downloadedBytes * 100) / totalBytes)) : 0,
- downloadedBytes,
- totalBytes,
- Math.round(downloadedBytes / elapsed),
- )
- })
-
- await pipeline(source, createWriteStream(partPath))
-
- const actual = hash.digest('hex')
- if (part.sha256 && actual !== part.sha256) {
- throw new D3ROError(
- ErrorCode.STTSidecarSpawnFailed,
- `런타임 부품 해시 불일치 (${part.name})`,
- )
- }
- if (partBytes !== part.size) {
- throw new D3ROError(
- ErrorCode.STTSidecarSpawnFailed,
- `런타임 부품 크기 불일치 (${part.name}: ${partBytes} != ${part.size})`,
- )
- }
+ downloadedBytes += partSize
+ const elapsed = Math.max(0.001, (Date.now() - startedAt) / 1000)
+ this._emitProgress(
+ component,
+ 'downloading',
+ totalBytes > 0 ? Math.min(100, Math.round((downloadedBytes * 100) / totalBytes)) : 0,
+ downloadedBytes,
+ totalBytes,
+ Math.round(downloadedBytes / elapsed),
+ )
}
- // 부품을 순서대로 이어 인다 (스트리밍 — 메모리에 통째로 올리지 않는다)
- const archiveHash = createHash('sha256')
+ // 부품을 순서대로 이어 붙인다 (스트리밍 — 메모리에 통째로 올리지 않는다)
const archiveStream = createWriteStream(archivePath)
for (const part of entry.parts) {
- const source = createReadStream(join(tempDir, part.name))
- source.on('data', (chunk: string | Buffer) => archiveHash.update(chunk))
- await pipeline(source, archiveStream, { end: false })
+ await pipeline(createReadStream(join(tempDir, part.name)), archiveStream, { end: false })
}
archiveStream.end()
await once(archiveStream, 'finish')
- const actualArchive = archiveHash.digest('hex')
+
+ // 크기를 먼저 본다 — 불일치하면 "어디까지 받았는지"가 로그에 남아 진단이 가능하다.
+ const archiveSize = (await stat(archivePath)).size
+ const expectedSize = entry.parts.reduce((sum, part) => sum + part.size, 0)
+ if (archiveSize !== expectedSize) {
+ throw new D3ROError(
+ ErrorCode.STTSidecarSpawnFailed,
+ `런타임 아카이브 크기 불일치 (${component}: ${archiveSize} != ${expectedSize})`,
+ )
+ }
+
+ const actualArchive = await sha256File(archivePath)
if (entry.sha256 && actualArchive !== entry.sha256) {
throw new D3ROError(
ErrorCode.STTSidecarSpawnFailed,
- `런타임 아카이브 해시 불일치 (${component})`,
+ `런타임 아카이브 해시 불일치 (${component}: ${actualArchive} != ${entry.sha256})`,
)
}
}
+ /**
+ * 부품 하나를 디스크로 내려받고 디스크 기준으로 크기·해시를 검증한다.
+ * 전송이 도중에 끊기면 같은 부품을 다시 받는다 (기존에는 1회 실패가 곧 설치 실패였다).
+ */
+ private async _downloadPart(part: RuntimePart, partPath: string): Promise {
+ let lastError: Error | null = null
+
+ for (let attempt = 1; attempt <= PART_DOWNLOAD_ATTEMPTS; attempt += 1) {
+ try {
+ const response = await fetch(part.url, { signal: AbortSignal.timeout(DOWNLOAD_TIMEOUT_MS) })
+ if (!response.ok || !response.body) {
+ throw new D3ROError(
+ ErrorCode.STTSidecarSpawnFailed,
+ `런타임 부품을 받을 수 없습니다 (HTTP ${response.status}): ${part.name}`,
+ )
+ }
+
+ // 스트림을 파일로 저장한 뒤 "디스크에 실제로 남은 파일"에서 크기와 해시를 계산한다.
+ // 메모리 스트림에서 센 값으로 검증하면, 디스크 쓰기가 잘려도 부품 검사를 통과해
+ // 결합 단계에 가서야 해시 불일치로 터진다 — 실측 사고.
+ await pipeline(Readable.fromWeb(response.body as never), createWriteStream(partPath))
+
+ const partSize = (await stat(partPath)).size
+ if (partSize !== part.size) {
+ throw new D3ROError(
+ ErrorCode.STTSidecarSpawnFailed,
+ `런타임 부품 크기 불일치 (${part.name}: ${partSize} != ${part.size})`,
+ )
+ }
+
+ const actualPartHash = await sha256File(partPath)
+ if (part.sha256 && actualPartHash !== part.sha256) {
+ throw new D3ROError(
+ ErrorCode.STTSidecarSpawnFailed,
+ `런타임 부품 해시 불일치 (${part.name})`,
+ )
+ }
+
+ return partSize
+ } catch (err) {
+ lastError = err instanceof Error ? err : new Error(String(err))
+ await rm(partPath, { force: true }).catch(() => undefined)
+ logger.warn(
+ `런타임 부품 다운로드 실패 (${part.name}, ${attempt}/${PART_DOWNLOAD_ATTEMPTS}): ${lastError.message}`,
+ )
+ }
+ }
+
+ throw lastError ?? new D3ROError(
+ ErrorCode.STTSidecarSpawnFailed,
+ `런타임 부품 다운로드 실패 (${part.name})`,
+ )
+ }
+
private _emitProgress(
component: RuntimeComponent,
phase: RuntimeProgressEvent['phase'],
@@ -329,6 +364,18 @@ class RuntimeProvisioner extends EventEmitter {
}
}
+/** 파일 SHA-256 (스트림 — 메모리에 통째로 올리지 않는다) */
+async function sha256File(path: string): Promise {
+ const hash = createHash('sha256')
+ await pipeline(createReadStream(path), new Writable({
+ write(chunk: Buffer, _encoding: BufferEncoding, callback: (error?: Error | null) => void) {
+ hash.update(chunk)
+ callback()
+ },
+ }))
+ return hash.digest('hex')
+}
+
let _instance: RuntimeProvisioner | null = null
export function getRuntimeProvisioner(): RuntimeProvisioner {
@@ -340,4 +387,4 @@ export function getRuntimeProvisioner(): RuntimeProvisioner {
export function resetRuntimeProvisionerForTests(): void {
_instance = null
-}
\ No newline at end of file
+}
diff --git a/apps/mobile-rn/android/app/build.gradle b/apps/mobile-rn/android/app/build.gradle
index 747ebae..745b471 100644
--- a/apps/mobile-rn/android/app/build.gradle
+++ b/apps/mobile-rn/android/app/build.gradle
@@ -151,8 +151,8 @@ def versionSettingsValid = configuredVersionName != null &&
configuredVersionName ==~ strictSemver &&
configuredVersionCodeValue != null &&
configuredVersionCodeValue <= 2100000000L
-def resolvedVersionName = versionSettingsValid ? configuredVersionName : "1.3.5"
-def resolvedVersionCode = versionSettingsValid ? configuredVersionCodeValue.toInteger() : 1031005
+def resolvedVersionName = versionSettingsValid ? configuredVersionName : "1.3.6"
+def resolvedVersionCode = versionSettingsValid ? configuredVersionCodeValue.toInteger() : 1031006
def requiredReleaseSettings = [
D3RO_RELEASE_STORE_FILE: releaseStoreFilePath,
diff --git a/apps/mobile-rn/ios/D3ROVoice.xcodeproj/project.pbxproj b/apps/mobile-rn/ios/D3ROVoice.xcodeproj/project.pbxproj
index 9c745ae..faf7162 100644
--- a/apps/mobile-rn/ios/D3ROVoice.xcodeproj/project.pbxproj
+++ b/apps/mobile-rn/ios/D3ROVoice.xcodeproj/project.pbxproj
@@ -257,7 +257,7 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
- CURRENT_PROJECT_VERSION = 1031005;
+ CURRENT_PROJECT_VERSION = 1031006;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = D3ROVoice/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
@@ -265,7 +265,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
- MARKETING_VERSION = 1.3.5;
+ MARKETING_VERSION = 1.3.6;
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",
@@ -287,14 +287,14 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
- CURRENT_PROJECT_VERSION = 1031005;
+ CURRENT_PROJECT_VERSION = 1031006;
INFOPLIST_FILE = D3ROVoice/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
- MARKETING_VERSION = 1.3.5;
+ MARKETING_VERSION = 1.3.6;
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",
diff --git a/apps/mobile-rn/metadata/android/en-US/changelogs/1031006.txt b/apps/mobile-rn/metadata/android/en-US/changelogs/1031006.txt
new file mode 100644
index 0000000..9435f6d
--- /dev/null
+++ b/apps/mobile-rn/metadata/android/en-US/changelogs/1031006.txt
@@ -0,0 +1 @@
+Stability improvements.
diff --git a/apps/mobile-rn/metadata/android/ko-KR/changelogs/1031006.txt b/apps/mobile-rn/metadata/android/ko-KR/changelogs/1031006.txt
new file mode 100644
index 0000000..e80179f
--- /dev/null
+++ b/apps/mobile-rn/metadata/android/ko-KR/changelogs/1031006.txt
@@ -0,0 +1 @@
+안정성을 개선했습니다.
diff --git a/apps/mobile-rn/package-lock.json b/apps/mobile-rn/package-lock.json
index cf44daf..351b628 100644
--- a/apps/mobile-rn/package-lock.json
+++ b/apps/mobile-rn/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "@d3ro/mobile-rn",
- "version": "1.3.5",
+ "version": "1.3.6",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@d3ro/mobile-rn",
- "version": "1.3.5",
+ "version": "1.3.6",
"dependencies": {
"@d3ro/api-client": "file:../../packages/api-client",
"@d3ro/core": "file:../../packages/core",
@@ -62,7 +62,7 @@
},
"../..": {
"name": "d3ro-voice-monorepo",
- "version": "1.3.5",
+ "version": "1.3.6",
"license": "MIT",
"workspaces": [
"apps/desktop",
@@ -81,7 +81,7 @@
},
"../../packages/api-client": {
"name": "@d3ro/api-client",
- "version": "1.3.5",
+ "version": "1.3.6",
"license": "MIT",
"dependencies": {
"@d3ro/core": "*",
@@ -98,7 +98,7 @@
},
"../../packages/core": {
"name": "@d3ro/core",
- "version": "1.3.5",
+ "version": "1.3.6",
"license": "MIT",
"dependencies": {
"docx": "^9.6.1"
@@ -109,7 +109,7 @@
},
"../../packages/i18n": {
"name": "@d3ro/i18n",
- "version": "1.3.5",
+ "version": "1.3.6",
"license": "MIT",
"devDependencies": {
"@types/react": "^19.0.0"
@@ -120,7 +120,7 @@
},
"../../packages/ui-native": {
"name": "@d3ro/ui-native",
- "version": "1.3.5",
+ "version": "1.3.6",
"license": "MIT",
"devDependencies": {
"@types/react": "*"
diff --git a/apps/mobile-rn/package.json b/apps/mobile-rn/package.json
index e9ccec2..d18dab4 100644
--- a/apps/mobile-rn/package.json
+++ b/apps/mobile-rn/package.json
@@ -1,6 +1,6 @@
{
"name": "@d3ro/mobile-rn",
- "version": "1.3.5",
+ "version": "1.3.6",
"private": true,
"scripts": {
"android": "react-native run-android",
diff --git a/apps/web/package.json b/apps/web/package.json
index a2ae0af..137143b 100644
--- a/apps/web/package.json
+++ b/apps/web/package.json
@@ -1,6 +1,6 @@
{
"name": "@d3ro/web",
- "version": "1.3.5",
+ "version": "1.3.6",
"private": true,
"description": "D3RO Voice 웹 앱 — Next.js 기반 SaaS 인터페이스",
"scripts": {
diff --git a/apps/web/src/components/layout/sidebar.tsx b/apps/web/src/components/layout/sidebar.tsx
index 191b896..b678848 100644
--- a/apps/web/src/components/layout/sidebar.tsx
+++ b/apps/web/src/components/layout/sidebar.tsx
@@ -139,7 +139,7 @@ export function Sidebar(): React.ReactElement {
- v1.3.5
+ v1.3.6
diff --git a/docs/map/10-feature-catalog.md b/docs/map/10-feature-catalog.md
index 20e1714..6a6ae29 100644
--- a/docs/map/10-feature-catalog.md
+++ b/docs/map/10-feature-catalog.md
@@ -194,7 +194,7 @@ Status quick-reference: `[x]` done+verified · `[~]` partial/unverified · `[ ]`
| INFRA-15 | Update & release system | [x] | Canonical Forgejo feed + channels/policy (`release/update-policy.json`, `src/main/update-policy.ts`), canonical publisher `scripts/ci/publish-forgejo-release.mjs`, legacy GitLab mirror; `npm run release:metadata:test`. v1.1.0 was published to Forgejo on 2026-09-15; product version moved to `1.2.0` as a forward-fix with CI-only publication, a same-version re-release guard, and download centers that link the feed instead of repository paths. `1.3.0` (2026-09-18) carries the local-STT fixes; Windows publication still needs the CI signing secrets (`11` GAP-REL-02). |
| INFRA-16 | Desktop STT engine packaging | [x] | `apps/desktop/scripts/setup-sidecar.mjs` + `build-sidecar.mjs`, `electron-builder.yml` `extraResources` (`sidecar-dist/sidecar` → `resources/sidecar`, `resources/ffmpeg`), and `scripts/ci/verify-sidecar-bundle.mjs` run in `package-windows`/`package-macos` before electron-builder. Verified on the real bundle: `sidecar.exe` + `_internal` including `faster_whisper/assets/silero_vad_v6.onnx`, plus a packaged-engine transcription round-trip on GPU. |
| INFRA-17 | 서명 없는 배포 채널 (portable + Scoop) | [x] | `scripts/ci/build-portable.mjs` (95MiB 7z 분할 볼륨 + Scoop 매니페스트), `scripts/ci/publish-portable-release.mjs`, `scripts/local/install-d3ro-voice.ps1`, `bucket/` 버킷, `.forgejo/workflows/portable.yml`; 7z 분할 볼륨(Scoop, 162MiB) + zip 분할 부품(수동 설치, 243MiB, 7-Zip 불필요); updater feed와 분리. 2026-09-18 `portable-1.3.1` 게시 + 실제 설치 검증. |
-| INFRA-18 | 로컬 런타임 온디맨드 설치 | [x] | `RuntimeProvisioner`(부품 다운로드 + SHA-256 검증 + tar 해제, `%APPDATA%/d3ro-voice/runtime`), `POST runtime:ensure` / `runtime:progress` IPC, 설정 > STT 상태/내려받기 UI. 설치본에서 엔진/ffmpeg를 분리해 189MB → 90.6MiB, 업데이트 피드 게시 복구. 2026-09-18 실제 feed 통합 검증(엔진 94.4MiB/17초, ffmpeg 21.7MiB/5초). |
+| INFRA-18 | 로컬 런타임 온디맨드 설치 | [x] | `RuntimeProvisioner`(부품 다운로드 + SHA-256 검증 + tar 해제, `%APPDATA%/d3ro-voice/runtime`), `POST runtime:ensure` / `runtime:progress` IPC, 설정 > STT 상태/내려받기 UI. 설치본에서 엔진/ffmpeg를 분리해 189MB → 90.6MiB, 업데이트 피드 게시 복구. 검증은 전부 디스크에 기록된 파일 기준이며(부품 크기·해시 → 결합본 크기·해시), 부품 다운로드는 최대 3회 재시도한다. 2026-09-18 실제 feed 통합 검증(엔진 94.4MiB/18초, ffmpeg 21.7MiB/5초). |
| INFRA-19 | 네이티브 ABI + updater 설정 게이트 | [x] | `scripts/ci/verify-native-abi.mjs`(패키징된 `better_sqlite3.node`가 Electron ABI인지 호스트 Node 로드 거부로 판별) + `scripts/ci/fix-native-abi.mjs`(로컬 잠금 우회용 주입). GitLab/Forgejo/GitHub 패키징 단계에 검증 삽입. 2026-09-18: Node ABI 모듈로 앱이 시작 즉시 죽은 사고 + 누락으로 자동 업데이트가 죽은 사고를 함께 방지(). |
---
diff --git a/docs/map/11-gap-backlog.md b/docs/map/11-gap-backlog.md
index 43282a1..131d02b 100644
--- a/docs/map/11-gap-backlog.md
+++ b/docs/map/11-gap-backlog.md
@@ -53,6 +53,7 @@ Legend: `[ ]` open · `[~]` in progress · `[!]` blocked externally · `[x]` res
| GAP-REL-04 | Release | canonical feed는 Cloudflare 뒤에 있어 업로드 본문이 **100MiB**를 넘으면 HTTP 413으로 거부한다. | `scripts/ci/publish-updater-release.mjs`, `docs/deployment/unsigned-distribution.md` | `[~]` 2026-09-18: 설치본을 90.6MiB로 줄여 업데이트 피드 게시를 복구했다(GAP-STT-07). 휴대용/Scoop 채널은 여전히 95MiB 분할이 필요하다. |
| GAP-STT-07 | Local STT | 진(사이드카)을 앱 번들에 넣으면 설치본이 100MiB를 넘고 매 업데이트마다 162MiB를 다시 받는다. | `apps/desktop/src/main/services/RuntimeProvisioner.ts`, `apps/desktop/electron-builder.yml` | `[x]` 2026-09-18: 설치본에서 엔진/ffmpeg를 제거하고 처음 필요할 때 `runtime-latest`에서 내려받는다(부품별 + 결합본 SHA-256 검증). 설치본 189MB → 90.6MiB, 런타임 1회 116MiB(엔진 94.4 + ffmpeg 21.7). 실제 feed로 통합 검증 완료. |
| GAP-STT-06 | Local STT | Decode settings were untuned: previous-text conditioning let repeated hallucinations compound, and no VAD parameters meant slow, uneven segments. | `apps/desktop/sidecar/main.py` | `[x]` 2026-09-18: `condition_on_previous_text=false`, bounded low-temperature fallback, `no_speech`/`compression_ratio`/`log_prob` thresholds, 300 ms silence trimming. Same transcript, ~5x faster on the reference machine (7.7 s audio: 1609 ms → 303 ms). |
+| GAP-STT-08 | Local STT | 1.3.5 설치본에서 엔진 설치가 "런타임 아카이브 해시 불일치 (sidecar)"로 항상 실패했다. 부품 검증은 **메모리 스트림**에서 센 값으로, 결합 검증은 **디스크 파일**에서 계산해 기준이 서로 달랐다. 디스크 쓰기가 잘려도 부품 검사를 통과하고 결합 단계에서만 터지므로 원인 파악도 불가능했다. 재시도가 없어 전송이 한 번 끊기면 곧바로 설치 실패였다. | `apps/desktop/src/main/services/RuntimeProvisioner.ts` | `[x]` 2026-09-18: 부품 크기·해시를 디스크 파일 기준으로 통일하고, 결합본은 크기를 먼저 검사한 뒤 해시를 본다(오류 메시지에 실제/기대값 포함). 부품 다운로드는 실패 시 해당 파일을 지우고 최대 3회 재시도한다. 서버 아티팩트는 무결함을 확인했고(부품 2개 해시 일치, 결합본 `e203aa53…` = 인덱스 기대값), 실제 feed로 설치를 재현해 18초 만에 성공. |
---
diff --git a/package-lock.json b/package-lock.json
index 7170c3e..7ac4ac7 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "d3ro-voice-monorepo",
- "version": "1.3.5",
+ "version": "1.3.6",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "d3ro-voice-monorepo",
- "version": "1.3.5",
+ "version": "1.3.6",
"license": "MIT",
"workspaces": [
"apps/desktop",
@@ -25,7 +25,7 @@
},
"apps/admin": {
"name": "@d3ro/admin",
- "version": "1.3.5",
+ "version": "1.3.6",
"dependencies": {
"@d3ro/api-client": "*",
"@d3ro/core": "*",
@@ -109,7 +109,7 @@
},
"apps/desktop": {
"name": "@d3ro/desktop",
- "version": "1.3.5",
+ "version": "1.3.6",
"license": "MIT",
"dependencies": {
"@d3ro/core": "*",
@@ -158,7 +158,7 @@
},
"apps/web": {
"name": "@d3ro/web",
- "version": "1.3.5",
+ "version": "1.3.6",
"dependencies": {
"@d3ro/api-client": "*",
"@d3ro/core": "*",
@@ -16861,7 +16861,7 @@
},
"packages/api-client": {
"name": "@d3ro/api-client",
- "version": "1.3.5",
+ "version": "1.3.6",
"license": "MIT",
"dependencies": {
"@d3ro/core": "*",
@@ -16878,7 +16878,7 @@
},
"packages/core": {
"name": "@d3ro/core",
- "version": "1.3.5",
+ "version": "1.3.6",
"license": "MIT",
"dependencies": {
"docx": "^9.6.1"
@@ -16889,7 +16889,7 @@
},
"packages/i18n": {
"name": "@d3ro/i18n",
- "version": "1.3.5",
+ "version": "1.3.6",
"license": "MIT",
"devDependencies": {
"@types/react": "^19.0.0"
@@ -16900,7 +16900,7 @@
},
"packages/ui": {
"name": "@d3ro/ui",
- "version": "1.3.5",
+ "version": "1.3.6",
"license": "MIT",
"dependencies": {
"@d3ro/core": "*"
@@ -16920,7 +16920,7 @@
},
"packages/ui-native": {
"name": "@d3ro/ui-native",
- "version": "1.3.5",
+ "version": "1.3.6",
"license": "MIT",
"devDependencies": {
"@types/react": "*"
diff --git a/package.json b/package.json
index 495dcfa..0a3e44d 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "d3ro-voice-monorepo",
- "version": "1.3.5",
+ "version": "1.3.6",
"private": true,
"description": "D3RO Voice — 멀티플랫폼 AI 음성 어시스턴트 (Monorepo)",
"author": "D3RO",
diff --git a/packages/api-client/package.json b/packages/api-client/package.json
index e2c0fcb..b3c6e7c 100644
--- a/packages/api-client/package.json
+++ b/packages/api-client/package.json
@@ -1,6 +1,6 @@
{
"name": "@d3ro/api-client",
- "version": "1.3.5",
+ "version": "1.3.6",
"private": true,
"description": "D3RO Voice API 클라이언트 — Supabase 래퍼 (web/desktop/mobile 공유)",
"license": "MIT",
diff --git a/packages/core/package.json b/packages/core/package.json
index 0f3d2fb..55b2f35 100644
--- a/packages/core/package.json
+++ b/packages/core/package.json
@@ -1,6 +1,6 @@
{
"name": "@d3ro/core",
- "version": "1.3.5",
+ "version": "1.3.6",
"private": true,
"description": "D3RO Voice 공유 비즈니스 로직 — 타입, 에러, IPC 채널, 상수, 유틸",
"license": "MIT",
diff --git a/packages/i18n/package.json b/packages/i18n/package.json
index 94b25ab..e7a4b7c 100644
--- a/packages/i18n/package.json
+++ b/packages/i18n/package.json
@@ -1,6 +1,6 @@
{
"name": "@d3ro/i18n",
- "version": "1.3.5",
+ "version": "1.3.6",
"private": true,
"description": "D3RO Voice 공유 i18n — 12개 locale, 타입 안전 키, Context, 포맷 유틸",
"license": "MIT",
diff --git a/packages/ui-native/package.json b/packages/ui-native/package.json
index c18e43f..8e1d456 100644
--- a/packages/ui-native/package.json
+++ b/packages/ui-native/package.json
@@ -1,6 +1,6 @@
{
"name": "@d3ro/ui-native",
- "version": "1.3.5",
+ "version": "1.3.6",
"private": true,
"description": "D3RO Voice React Native DS — MetalCard/PhosphorText/Led/WaveBars etc.",
"license": "MIT",
diff --git a/packages/ui/package.json b/packages/ui/package.json
index 0f6a60f..63c0f40 100644
--- a/packages/ui/package.json
+++ b/packages/ui/package.json
@@ -1,6 +1,6 @@
{
"name": "@d3ro/ui",
- "version": "1.3.5",
+ "version": "1.3.6",
"private": true,
"description": "D3RO Voice 공유 UI — 디자인 시스템 컴포넌트 + 테마 토큰 + CSS 변수 맵",
"license": "MIT",
diff --git a/release/product-version.json b/release/product-version.json
index 5b2648c..4008e30 100644
--- a/release/product-version.json
+++ b/release/product-version.json
@@ -1,8 +1,8 @@
{
"schemaVersion": 1,
- "version": "1.3.5",
- "androidVersionCode": 1031005,
- "iosBuildNumber": 1031005,
+ "version": "1.3.6",
+ "androidVersionCode": 1031006,
+ "iosBuildNumber": 1031006,
"releaseDate": "2026-09-18",
"desktopLicensePublicKeyId": "5c52b765135ee2531c681b53cd4dc0e96fff8737f496c0b04ba8334fc81a887f"
}
diff --git a/site/package-lock.json b/site/package-lock.json
index 8d40b37..e3778a9 100644
--- a/site/package-lock.json
+++ b/site/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "d3ro-voice-site",
- "version": "1.3.5",
+ "version": "1.3.6",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "d3ro-voice-site",
- "version": "1.3.5",
+ "version": "1.3.6",
"dependencies": {
"react": "^19.0.0",
"react-dom": "^19.0.0"
diff --git a/site/package.json b/site/package.json
index 9755ffc..2a62652 100644
--- a/site/package.json
+++ b/site/package.json
@@ -1,7 +1,7 @@
{
"name": "d3ro-voice-site",
"private": true,
- "version": "1.3.5",
+ "version": "1.3.6",
"type": "module",
"scripts": {
"dev": "vite --port 5199 --host",