Skip to content

Commit 99ac683

Browse files
committed
refactor(session): 音频槽类型下沉会话域
1 parent 7c1899e commit 99ac683

6 files changed

Lines changed: 78 additions & 68 deletions

File tree

‎app/src/components/session-detail/useSessionAudio.test.tsx‎

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,14 +28,26 @@ const { invokeMock, convertFileSrcMock } = vi.hoisted(() => ({
2828
vi.mock("@tauri-apps/api/core", () => ({ invoke: invokeMock, convertFileSrc: convertFileSrcMock }));
2929

3030
import { NO_AUDIO, SESSION_AUDIO_COMMAND, toSessionAudioState, useSessionAudio } from "./useSessionAudio";
31-
import type { SessionAudioRef } from "../../types/sessionAudio";
31+
import type { SessionAudioRef } from "../../types/session";
32+
import type { SessionAudioState } from "../../views/registry";
3233

3334
/** 会话 WAV 的真实路径形态(`{data_dir}/session-audio/{id}.wav`;Windows 反斜杠 + 空格) */
3435
const WAV_A = "C:\\Users\\x\\AppData\\Roaming\\com.entropydecrease.app\\session-audio\\42.wav";
3536
const WAV_B = "C:\\Users\\x\\AppData\\Roaming\\com.entropydecrease.app\\session-audio\\43.wav";
3637
/** Tauri 的真实 asset URL 形态(`scripts/core.js:13-20`)——期望值独立写在断言侧,不借被测模块构造 */
3738
const assetUrl = (p: string): string => `http://asset.localhost/${encodeURIComponent(p)}`;
3839

40+
/**
41+
* **编译期绑定**(A6 同族,零运行时断言):容器产出的形状必须可赋给**视图槽**的槽位类型 ——
42+
* 槽契约漂移(改字段/改类型)或容器产出改形 ⇒ `tsc` 当场红。这里能 import 视图层的类型而**生产**
43+
* 文件不能:A2④ 的「非 `views/**` 导入者**恰 5 个**」只数生产文件(`prodUnder` 排除 `*.test.tsx`)。
44+
*/
45+
const SLOT_BINDING: readonly SessionAudioState[] = [
46+
toSessionAudioState(null),
47+
toSessionAudioState({ path: WAV_A, aligned: true, durationMs: 1 }),
48+
];
49+
void SLOT_BINDING;
50+
3951
/** 探针:把 hook 的返回值渲染成属性(`data-pending` = **尚未取到**;`none` = 值为 `null`) */
4052
function Probe({ sessionId }: { sessionId: number }) {
4153
const audio = useSessionAudio(sessionId);

‎app/src/components/session-detail/useSessionAudio.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
*/
3030
import { useEffect, useState } from "react";
3131
import { convertFileSrc, invoke } from "@tauri-apps/api/core";
32-
import type { SessionAudioRef, SessionAudioState } from "../../types/sessionAudio";
32+
import type { SessionAudioRef, SessionAudioState } from "../../types/session";
3333

3434
/** IPC 命令名(T22 交付;`check-command-registry` 计数 313/313 里的那一条)。 */
3535
export const SESSION_AUDIO_COMMAND = "session_audio_path";

‎app/src/types/session.ts‎

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -311,3 +311,57 @@ export interface TextFilterStatus {
311311
quotaRemaining: number;
312312
mock: boolean;
313313
}
314+
315+
/**
316+
* 会话音频引用(批 6 T24 · R5.5 / R5.5-b)—— T22 `session_audio_path` 的出参
317+
* (Rust `SessionAudioRef`,`serde(rename_all = "camelCase")`)。
318+
*
319+
* @ai-context 命令**只给路径**:URL 一律由前端 `convertFileSrc` 构造(Tauri `scripts/core.js:13-20`
320+
* 的 Windows 形态 = `http://asset.localhost/<encodeURIComponent(绝对路径)>`)⇒ 后端不碰 URL 形态。
321+
* 适用范围(硬边界):**只有实时采集会话有音频** —— 导入会话的音轨落在
322+
* `%TEMP%/entropy-import-{id}/audio.wav` 且导入结束即删、`%TEMP%` 不在 asset scope ⇒ 那条路
323+
* 今天**没有承载面**;故契约只表达「有音频 / 无音频」二分,**不猜来源**。
324+
*/
325+
export interface SessionAudioRef {
326+
/** 应用数据目录内的**绝对路径**(`{data_dir}/session-audio/{id}.wav`)—— **不是** URL。 */
327+
readonly path: string;
328+
/**
329+
* 该录音的 WAV 轴是否与会话轴对齐(T23 的对齐簿记 sidecar)。
330+
* `false` 的语义是「**不能保证**对齐」(含**历史录音无 sidecar**),**不是**「一定没对齐」
331+
* ⇒ UI 文案不得写成断言式的「未对齐」。精度残余 = **块粒度 ±200 ms**(sidecar 未加键
332+
* ⇒ 毫秒级定位需要新契约)。
333+
*/
334+
readonly aligned: boolean;
335+
/** 时长(毫秒;由 WAV 头 `data` 长度换算);`null` = 未 finalize / 未知 ⇒ **不可播**。 */
336+
readonly durationMs: number | null;
337+
}
338+
339+
/**
340+
* 视图槽 `SessionViewSlot.audio` 的音频引用(**容器**产出、视图**只读**)。
341+
* `url === null` ⇒ 无音频(或取数失败降级)。
342+
*
343+
* @ai-context 为什么声明在**会话域**而不是与槽同住 `views/registry.ts`(**计划缺陷的就地处置**):
344+
* 两条既有断言从两侧把注册表的类型图冻死了 —— ① `registryResolution.test.ts:46` 的 G7① 逐字
345+
* 冻结了 `views/registry.ts` 顶部的 **4 个 import 模块名**(连 `import type` 也算;本任务实测:
346+
* 多一个模块 ⇒ 该用例当场红);② `views/architecture.guard.test.ts` 的 A2④ 逐字冻结了「非
347+
* `views/**` 的导入者**恰 5 个**」(新的容器文件 `import type … from "views/registry"` ⇒ 当场红)。
348+
* 两条都**不在**本批授权改动清单内(R1.3 / 计划表 5)⇒ 唯一同时满足两者的落点 = **已在 ① 的
349+
* 4 个模块之内的会话域文件**(`../types/session`):注册表只 `export type` 转出、容器从会话域取。
350+
* 收益:依赖方向也更贴规格 §7.1(领域 → 视图 → 容器;容器不反向依赖视图层模块)。
351+
* 实证与反例读数见 `.superpowers/sdd/2026-09-12-frontend-redesign-batch6-motion/task-24-report.md`。
352+
* 副作用:无(纯类型声明;消费方一律 `import type` ⇒ 构建期整体擦除、零运行时字节)。
353+
*/
354+
export interface SessionAudioState {
355+
/** 可播放 URL(`convertFileSrc(path)` 的产物);`null` = 路径不可得。 */
356+
readonly url: string | null;
357+
/** WAV 轴是否与会话轴对齐(**如实透传**,不许在容器里改写成「看起来更好」的值)。 */
358+
readonly aligned: boolean;
359+
/**
360+
* 只有**已 finalize** 的 WAV 可播(R5.5-b 约束 3:WAV 头的 `data` 长度创建时写 0,
361+
* `finalize()` 是唯一回填点)⇒ `false` 时 UI **必须**禁用播放并如实提示(一行
362+
* `StatusLine kind="error"`,**不是** toast)。
363+
*/
364+
readonly playable: boolean;
365+
/** 时长(毫秒;**如实透传**,含 `0`);`null` = 未知。 */
366+
readonly durationMs: number | null;
367+
}

‎app/src/types/sessionAudio.ts‎

Lines changed: 0 additions & 58 deletions
This file was deleted.

‎app/src/views/registry.ts‎

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,10 @@
1313
* 全部**可选** ⇒ 既有 9 个字段与既有夹具一字不动):播放头(R5.5 #5)必须在本目录的
1414
* **零 Tauri 边界**内工作 ⇒ 「音频引用」这一份数据**只能**由容器注入,取数与 URL 拼接
1515
* (`invoke("session_audio_path")` + `convertFileSrc`)落在
16-
* `components/session-detail/useSessionAudio.ts`。槽类型 `SessionAudioState` 从领域层
17-
* `types/sessionAudio.ts` 转出(理由见该文件头注:A2④ 的导入者集合断言 + §7.1 的依赖方向)。
16+
* `components/session-detail/useSessionAudio.ts`。槽类型 `SessionAudioState` 声明在会话域
17+
* `types/session.ts` **并在本文件转出**(Why:本文件顶部的 import 模块名被 G7① 逐字冻结、
18+
* A2④ 又冻结了「非 `views/**` 的导入者集合」⇒ 新增 import 模块或让容器直接 import 本文件都会
19+
* 让既有断言变红,而两条断言都**不在**本批授权改动清单内)。
1820
* @ai-context **中间态声明(批 5 T6 的依赖顺序造成,不是遗漏)**:视图模块要 `import type` 本文件的
1921
* 槽类型 ⇒ **契约必须先落地**;而「视图清单」(`viewsFor` / `keysFor` / `FROZEN_VIEW_KEYS`)只有
2022
* 在视图模块落地后才满足判据 G3「每个 `load()` 都能解析出真实模块」。故本文件此刻**只含契约**,
@@ -24,13 +26,12 @@
2426
*/
2527
import type { ComponentType } from "react";
2628
import type { Note } from "../types/notes";
27-
import type { SessionDetail, SessionOcrBlock } from "../types/session";
28-
import type { SessionAudioState } from "../types/sessionAudio";
29+
import type { SessionAudioState, SessionDetail, SessionOcrBlock } from "../types/session";
2930
import type { IconName } from "../ui/icons";
3031

31-
// 槽类型**转出**:`SessionAudioState` 的声明在 `types/sessionAudio.ts`(领域层)—— 见该文件头注
32-
// 「为什么不与槽同住本文件」(A2④ 的导入者集合断言 + §7.1 的依赖方向)。这里只转出,视图侧
33-
// 仍可从注册表一处取到整个槽契约。
32+
// 槽类型**转出**:`SessionAudioState` 的声明在会话域 `types/session.ts`(**不新增 import 模块** ——
33+
// `registryResolution.test.ts` 的 G7① 逐字冻结了本文件顶部的 4 个模块名,见该测试 `:46`)。
34+
// 这里只转出 ⇒ 视图侧仍可从注册表一处取到整个槽契约;容器侧从会话域取(A2④ 的导入者集合冻结)。
3435
export type { SessionAudioState };
3536

3637
/** 视图记忆的**对象类型粒度**(C5 / 规格决策 23):**不含 `kind`** —— web/photo/video 共记一份。 */

0 commit comments

Comments
 (0)