Skip to content

Commit e493eac

Browse files
committed
feat(notes): 第三视图接入容器取数与行为判据
T10(批 8 段 8b · A 容器片):零新 IPC / 零 Rust / 零 schema —— 只消费既有 get_session_detail。 - 新建 hooks/useNoteEvidence.ts(157 行):按 note.session_id 取详情 → 候选轨(转写段/OCR 块); 段落轨**同源复用** T8 的 evidenceLinesOf(唯一派生处,防 paragraphIndex 静默漂移); 四态降级(idle/loading/ready/error)+ 形状守卫(载荷 null/缺字段 ⇒ 空轨,不炸树)+ 同会话去重。 - NotesPage 三行注入(295 → 300,恰在上限):hook 调用 + evidence prop。 - readingColumnLoaders 加第三视图本地加载器(EVIDENCE_LOAD,不 import 注册表)⇒ 视图与模型 合并进宿主 chunk,**不新增懒 chunk**(chunk 数 39 = 上限)。 - NotesReadingColumn 接注入面;NoteEvidenceTrackView 加**可选** lines + 缺省自派生回退。 - 新判据件 NotesReadingColumn.evidence.test.tsx(F12 段落轨同源 / F13 原文不丢+惰性卸载 / F14 页面级畸形载荷不炸树);既有 F1–F11 与 noteViews 逐字零 hunk。 🔴 STOP(未决,已报控制方):真构建包体闸越界 —— 懒侧 639,868 B > 上限 639,314 B + 64(超 490 B)、 NotesPage- 族 49,219 > 48,693 + 64;首屏 106.23 kB / chunk 39 / 未归族 0 均达标。 T8 重冻后两处余量均为 0 ⇒ 任何新增懒侧字节必红。**未**改 scripts/lazyBudget.json,请裁定重冻授权。 报告:.superpowers/sdd/2026-09-13-frontend-redesign-batch8/task-10-report.md
1 parent cc08c95 commit e493eac

7 files changed

Lines changed: 373 additions & 9 deletions

File tree

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
// @vitest-environment jsdom
2+
/**
3+
* NotesReadingColumn.evidence.test.tsx — 第三视图(`evidence`)**经容器注入**的行为判据(批 8 T10)。
4+
*
5+
* @ai-context 为什么**另立新文件**(承 T10 卡片的授权「放不下就另立」):`NotesReadingColumn.views.test.tsx`
6+
* 在 T7 拆件后 = 231 行,而本单元的判据要**真的把候选轨推进宿主**(夹具 + 断言 ≈ 40 行)⇒ 塞进原
7+
* 文件会把 F1–F11(既有 11 条,**逐字不许改**)挤到 300 行硬线附近,风险大于收益。原文件**零 hunk**。
8+
* @ai-context 本文件判据(编号续 `views.test.tsx` 的 F11,避免与既有编号撞车):
9+
* **F12 段落轨同源**(T10 的核心风险):注入 `NoteEvidenceTracks`(**含 `lines`**)⇒ ① 第三视图
10+
* 挂载(键 `evidence` 真的路由到了 `EVIDENCE_LOAD`)② DOM 上的 `data-evidence-for` 序列 ==
11+
* **注入的** `lines` 序列(`evidenceLinesOf` 的唯一派生处 ⇒ 容器与视图不各切一份、不错位)
12+
* ③ 命中的候选带 `data-evidence-id`、容差外的段渲染「无证据」串 ⇒ 注入的**候选轨**真的在跑。
13+
* **F13 原文不丢 + 惰性卸载**(规格 §7.3①②;T10 卡片的 F5/F6 同族):在**候选轨已注入**的前提下
14+
* 切到第三视图再切回 `raw` ⇒ ① 常驻子树的 DOM **节点同一性**不变(`===`)② 第三视图**不在 DOM**
15+
* (卸载,不是 `display:none`)③ 常驻层 `display` 复原 ④ 切回后 `data-evidence-for` 归零。
16+
* @ai-context 仪器边界:本文件必须写出 `@tauri-apps/api/core` 的模块 id 字面量(`vi.mock`)—— 一切
17+
* 「按源码扫描」的文字判据都应排除 `*.test.tsx`(T12 的口径),本文件不参与那类判据。
18+
* 副作用:只挂 React 树 + 读写 jsdom 的 `localStorage`;不写盘、不发请求(`invoke` 是可断言的 mock)。
19+
*/
20+
import { act, cleanup, fireEvent, render, screen, waitFor, within } from "@testing-library/react";
21+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
22+
import { viewsFor } from "../../views/registry";
23+
import type { NoteEvidenceTracks } from "../../views/note/NoteEvidenceTrackView";
24+
import { evidenceLinesOf } from "../../views/note/NoteEvidenceTrackView";
25+
import NotesPage from "../../pages/NotesPage";
26+
import { HarnessHost, note, type HarnessProps } from "./readingColumnTestKit";
27+
28+
const { invokeMock } = vi.hoisted(() => ({ invokeMock: vi.fn() }));
29+
vi.mock("@tauri-apps/api/core", () => ({
30+
invoke: invokeMock,
31+
convertFileSrc: (p: string) => `asset://localhost/${p}`,
32+
}));
33+
vi.mock("../RichEditorView", () => ({ default: () => <div data-testid="editor-stub" /> }));
34+
35+
/** 夹具正文:① 段锚在 5000 ms ⇒ 与候选 `{id:7,startMs:5000}` 命中 ② 段锚在 60000 ms ⇒ 容差(1000 ms)
36+
* 外 ⇒ 必须出显式「无证据」 ③ 段**无锚点** ⇒ 0 个 `EvidenceMatch`(E1 反向,T8 的既有契约)。 */
37+
const CONTENT = "甲段 [⏱ 00:05]([[ts:5000]])\n\n乙段 [⏱ 01:00]([[ts:60000]])\n\n丙段无锚点";
38+
const LINES = evidenceLinesOf(CONTENT);
39+
/** 夹具自证(防空真):3 段、序号连续 0..2(若 `evidenceLinesOf` 的口径变了,本文件当场红) */
40+
const EXPECTED_INDEXES = ["0", "1", "2"];
41+
const TRACKS: NoteEvidenceTracks = { segments: [{ id: 7, startMs: 5000 }], ocr: [], lines: LINES };
42+
43+
const Harness = (rest: Partial<HarnessProps>) => <HarnessHost views={viewsFor("note")} {...rest} />;
44+
45+
const clickSegment = async (label: string): Promise<void> => {
46+
await act(async () => {
47+
fireEvent.click(screen.getByText(label, { selector: "button" }));
48+
});
49+
};
50+
51+
/** 第三视图内每个段落节点的 `data-evidence-for` 序列(顺序 = DOM 顺序 = 段落顺序,读的是**真 DOM**) */
52+
const evidenceFors = (root: HTMLElement): string[] =>
53+
[...root.querySelectorAll("[data-evidence-for]")].map((el) => el.getAttribute("data-evidence-for") ?? "");
54+
55+
beforeEach(() => {
56+
invokeMock.mockReset();
57+
invokeMock.mockResolvedValue([]);
58+
window.localStorage.clear();
59+
// 规格 §1 决策 17 默认窗宽 1280(= 大纲列阈值)⇒ 不自动折叠,常驻子树完整(同 views.test 口径)
60+
window.innerWidth = 1280;
61+
});
62+
63+
afterEach(() => {
64+
cleanup();
65+
window.localStorage.clear();
66+
window.innerWidth = 1024;
67+
});
68+
69+
describe("F12/F13 第三视图经容器注入(批 8 T10 · §7.3①② · E1 的 DOM 面)", () => {
70+
it("F12 段落轨同源 + 候选轨在跑:data-evidence-for == 注入 lines;命中带 id、容差外出「无证据」", async () => {
71+
expect(LINES.map((l) => String(l.paragraphIndex)), "夹具自证:段落轨是 3 段、序号连续").toEqual(EXPECTED_INDEXES);
72+
const { container } = render(<Harness selected={{ ...note, content: CONTENT }} evidence={TRACKS} />);
73+
await clickSegment("证据");
74+
const panel = await screen.findByTestId("note-evidence-track");
75+
expect(container.querySelector("[data-evidence-for]"), "面板不在本次渲染的容器里(断言读错了树)").toBeTruthy();
76+
// ① 注入的 `lines` 真的被视图采用(容器是段落轨的派生方 ⇒ 两处不可能各切一份而错位)
77+
expect(evidenceFors(panel), "DOM 段落轨 != 注入的 lines(容器与视图各派生了一份 ⇒ paragraphIndex 会漂移)")
78+
.toEqual(LINES.map((l) => String(l.paragraphIndex)));
79+
expect(panel.querySelectorAll("[data-evidence-for]").length, "段落节点数 != 派生段落数(空行/标题行口径漂了)").toBe(CONTENT.split(/\n/).filter((t) => t.trim() !== "").length);
80+
// ② 候选轨(转写段轨)在跑:命中段有 id,容差外的段出显式「无证据」(E2 的两态都在)
81+
expect(panel.querySelector('[data-evidence-id="7"]'), "命中的转写段候选没渲染(注入的候选轨没进视图)").toBeTruthy();
82+
expect(within(panel).getByText("无证据"), "容差外段落必须出显式失配标记(不许静默取最近邻)").toBeTruthy();
83+
// ③ 无锚点段 = 一等状态:不渲染证据列、也不渲染任何告警(E1 反向)
84+
const rows = [...panel.querySelectorAll("[data-evidence-for]")];
85+
expect(rows[2].querySelectorAll("[data-evidence-cell]").length, "无锚点段渲染了证据列(E1 反向被破)").toBe(0);
86+
expect(rows[0].querySelectorAll("[data-evidence-cell]").length, "有锚点段没有证据列(E1 反向的另一半)").toBe(1);
87+
// ④ 注入面缺席的负对照:注册表直载态(无 `evidence`)⇒ 候选轨空 ⇒ 全部为「无证据」(不假装命中)
88+
cleanup();
89+
const bare = render(<Harness selected={{ ...note, content: CONTENT }} />);
90+
await clickSegment("证据");
91+
expect(within(await screen.findByTestId("note-evidence-track")).queryAllByText("无证据").length, "缺省候选轨必须全是显式「无证据」").toBeGreaterThan(0);
92+
expect(bare.container.querySelectorAll("[data-evidence-id]"), "缺省态凭空出现了命中 id").toHaveLength(0);
93+
});
94+
95+
it("F13 原文不丢 + 惰性卸载:候选轨已注入时切走/切回,常驻子树节点同一性不变、第三视图卸载", async () => {
96+
render(<Harness evidence={TRACKS} />);
97+
const resident = screen.getByTestId("note-resident-view");
98+
const before = screen.getByTestId("outline-panel");
99+
expect(within(resident).getByText("内容段落"), "常驻原文子树的既有锚点").toBeTruthy();
100+
await clickSegment("证据");
101+
expect(await screen.findByTestId("note-evidence-track")).toBeTruthy();
102+
// 切走 ⇒ 第三视图**卸载**(不是 display:none);常驻原文仍在 DOM 且节点同一性不变(§7.3①)
103+
await clickSegment("原文");
104+
await waitFor(() => expect(screen.queryByTestId("note-evidence-track")).toBeNull());
105+
expect(within(resident).getByText("内容段落"), "切回 raw 后原文丢了(§7.3① 被破)").toBeTruthy();
106+
expect(screen.getByTestId("outline-panel"), "常驻子树被卸载重挂(节点引用变了)").toBe(before);
107+
expect((screen.getByTestId("note-resident-view") as HTMLElement).style.display).not.toBe("none");
108+
expect(screen.queryAllByText("无证据"), "第三视图卸载后仍残留证据面").toHaveLength(0);
109+
});
110+
});
111+
112+
/**
113+
* F14 —— **页面级畸形载荷判据**(T10 实测抓到的真缺陷的回归网):`NotesPage` 的
114+
* `useNoteEvidence` 对 `get_session_detail` 的返回值**没有形状假设**。
115+
*
116+
* @ai-context 变异体(实测):把 hook 里 `tracksOf` 的 `Array.isArray` 守卫换成裸 `detail.segments.map`
117+
* ⇒ 本节红在**具名断言**(渲染期炸树 ⇒ 列表行文案查不到),而**不是**「看起来没问题」。
118+
* @ai-context 为什么这条必须落在页面级:缺陷的可见形态是「整棵 React 树被卸载」—— 组件级挂载
119+
* (`HarnessHost`)永远拿不到 `null` 详情,只有在**真页面 + 真 hook** 的形态下才可达。
120+
* 副作用:与同目录 `NotesPage.test.tsx` 同款(只挂树 + mock IPC);不写盘、不发请求。
121+
*/
122+
describe("F14 页面级:畸形 get_session_detail 载荷(null)⇒ 降级空轨、不炸树", () => {
123+
beforeEach(() => {
124+
invokeMock.mockReset();
125+
invokeMock.mockImplementation(async (cmd: string) => {
126+
switch (cmd) {
127+
case "list_note_groups": return [];
128+
case "list_fragments": return [];
129+
// 🔴 本用例的**全部要点**:详情命令回 null(老后端 / 局部失败 / mock 兜底的真实形态)
130+
case "get_session_detail": return null;
131+
// 正文带锚点 ⇒ 空候选轨下必然出显式「无证据」(缺省态的可判形态)
132+
case "list_notes": return [{ ...note, title: "畸形载荷笔记", content: "甲段 [⏱ 00:05]([[ts:5000]])" }];
133+
default: return [];
134+
}
135+
});
136+
window.innerWidth = 1280;
137+
});
138+
139+
it("F14 详情回 null ⇒ 笔记仍可打开、阅读面仍在、第三视图渲染「无证据」(不白屏、不炸树)", async () => {
140+
render(<NotesPage />);
141+
fireEvent.click(await screen.findByText("畸形载荷笔记"));
142+
// ① 树没被炸:右栏标题(常驻原文子树的锚点)与列表行都还在
143+
expect(await screen.findByRole("heading", { name: "畸形载荷笔记" }), "畸形载荷把整棵笔记页炸掉了").toBeTruthy();
144+
// ② 第三视图可切、可渲染:候选轨为空 ⇒ 有锚点段出显式「无证据」,无锚点段 0 列 0 告警
145+
await clickSegment("证据");
146+
const panel = await screen.findByTestId("note-evidence-track");
147+
expect(within(panel).getAllByText("无证据").length, "畸形载荷必须降级成空候选轨(不许抛、不许假装命中)").toBeGreaterThan(0);
148+
expect(panel.querySelectorAll("[data-evidence-id]"), "畸形载荷下凭空出现了命中 id").toHaveLength(0);
149+
// ③ 段落轨仍在(空轨只影响候选轨,不影响段落面):夹具正文 = 1 行 ⇒ 恰 1 个段落节点
150+
expect(panel.querySelectorAll("[data-evidence-for]").length, "畸形载荷把段落轨也带没了").toBe(1);
151+
});
152+
});

‎app/src/components/notes/NotesReadingColumn.tsx‎

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ import { useViewMemory } from "../../views/useViewMemory";
6161
import { ShellFallback } from "../../shell/ShellFallback";
6262
import { StatusLine, ViewSwitcher } from "../../ui/primitives";
6363
import { ResidentNoteView } from "./NotesReadingColumn.parts";
64-
import { CARD_FLOW_LOAD, lazyMapOf } from "./readingColumnLoaders";
64+
import { CARD_FLOW_LOAD, EVIDENCE_LOAD, lazyMapOf, type NoteEvidenceSlot } from "./readingColumnLoaders";
6565

6666
/** `views` 缺席时的空清单:**模块级常量**(稳定引用 ⇒ 下面的 `useMemo` 不每渲染重算) */
6767
const NO_VIEWS: readonly ViewSpec<NoteViewSlot>[] = [];
@@ -109,6 +109,9 @@ interface Props {
109109
onCleanNotice: (groupNames: string[]) => void;
110110
/** 视图清单(`NotesPage` 注入 `viewsFor("note")` —— C1①;缺席 ⇒ 单视图退化,见文件头) */
111111
views?: readonly ViewSpec<NoteViewSlot>[];
112+
/** 批 8 T10:第三视图(`evidence`)的**候选轨 + 同源段落轨**(页面 `useNoteEvidence` 产出;
113+
* 缺席 ⇒ 视图自派生段落、候选轨为空 ⇒ 有锚点段渲染显式「无证据」——T8 的缺省态,不是缺陷)。 */
114+
evidence?: NoteEvidenceSlot;
112115
}
113116

114117
export default function NotesReadingColumn(props: Props) {
@@ -146,15 +149,17 @@ export default function NotesReadingColumn(props: Props) {
146149
);
147150

148151
/** 惰性映射(键常量与工厂已搬到 `readingColumnLoaders`);`useMemo` 留在宿主 —— 工厂是**纯函数**、不持有记忆。 */
149-
const lazyOf = useMemo(() => lazyMapOf<NoteViewSlot>(specs, CARD_FLOW_LOAD), [specs]);
152+
const lazyOf = useMemo(() => lazyMapOf<NoteViewSlot>(specs, CARD_FLOW_LOAD, EVIDENCE_LOAD), [specs]);
150153
const isDefault = viewKey === defaultKey;
151154
const LazyView = isDefault ? null : lazyOf.get(viewKey) ?? null;
152155
/** 非默认视图的槽(数据全在这里,视图自身零取数 —— C14②);空态 ⇒ `null`(连卡片流都不挂)。
153-
* T17:`onOpenSessionAt` 不进 `NoteViewSlot`(批 6 已裁)⇒ 只在**传参处**补成包装件的 props。 */
156+
* T17:`onOpenSessionAt` 不进 `NoteViewSlot`(批 6 已裁)⇒ 只在**传参处**补成包装件的 props。
157+
* T10:`evidence` 同款处理 —— 候选轨只在**传参处**并入(视图 props ⊆ slot 的类型级判据
158+
* `architecture.slots.test.ts` A6 **不受影响**:那是视图组件的 props 面,不是宿主的传参)。 */
154159
const slot: NoteViewSlot | null = selected
155160
? { note: selected, onTaskToggle, onOpenSession, onImageOpen }
156161
: null;
157-
const viewProps = slot === null ? null : { ...slot, onOpenSessionAt };
162+
const viewProps = slot === null ? null : { ...slot, onOpenSessionAt, evidence: props.evidence };
158163

159164
/** 原文常驻子树的**展示装配**已搬到 `NotesReadingColumn.parts`(本件把整份 props 原样透传 —— 见该件文件头的字节预算理由)。 */
160165
const residentView = <ResidentNoteView {...props} />;

‎app/src/components/notes/readingColumnLoaders.ts‎

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,23 @@
1212
* 永不进 `React.lazy`(构造性证据);② `lazy()` **只按 `specs` 引用建一次**(每渲染新建会让
1313
* 子树恒重挂 —— F5 的挂载计数会当场红);③ **卡片流**经**带 seek 包装件**加载(T17/C10.3:
1414
* 该槽缺 `onOpenSessionAt`;键漂移会让它静默失效 ⇒ F11 用例即守卫)。
15+
* @ai-context: **批 8 T10 追加第三视图(`evidence`)的两条模块级事实**:① `EVIDENCE_LOAD` 是**本地加载
16+
* 器**(照 `CARD_FLOW_LOAD` 的先例、**不 import 注册表** —— 见上条 A2①/A2④);本件是它**唯一**
17+
* 的静态导入点 ⇒ 视图 + 模型层**合并进宿主 chunk**(不新增懒 chunk —— `lazyBudget.json` 的
18+
* `lazyChunkCountMax = 39` 今天**贴住**,第 40 个 chunk 会当场红,见 T10 报告 V-包体);②
19+
* `NOTE_EVIDENCE_KEY` 与注册表的 `NOTE_VIEWS[2].key` 必须逐字相等 —— 漂移 ⇒ `lazyMapOf`
20+
* 里查不到 ⇒ 该视图**静默不渲染**(`F12` 用例即这条守卫,请勿单独改一处)。
21+
* @ai-context: T10 的注入面走**类型转出**(不改视图、不加槽):`NoteEvidenceSlot` = 视图的
22+
* `NoteEvidenceTracks` **逐字转出**(`import type`)⇒ 容器侧(`hooks/useNoteEvidence.ts`)
23+
* 与视图**不可能各自漂移** —— 那是 `import type` **边**、不是 A2④ 计数的运行时/触碰边;
24+
* 本件**不**从注册表取任何类型(那才会让 A2④ 的集合变 6,见第二条)。
1525
* 副作用:无(纯数据 + `lazy()` 构造,不碰 IPC / DOM)。边界:本件在六棘轮的 PROD 域内 ⇒
1626
* 不得出现颜色 / 字号 / 边框 / 圆角 / 阴影字面量,也不得有原生 `<button>`。
1727
*/
1828
import { lazy } from "react";
1929
import type { ComponentType, LazyExoticComponent } from "react";
2030
import type CardFlowWithSeek from "../../views/note/NoteCardFlowWithSeek";
31+
import type { NoteEvidenceTracks } from "../../views/note/NoteEvidenceTrackView";
2132

2233
/** 惰性视图规格的**结构最小面**(= 注册表 `ViewSpec` 里与本件相关的两个字段,逐字同型)。 */
2334
export interface LazyViewSpec<P> {
@@ -29,19 +40,40 @@ export interface LazyViewSpec<P> {
2940
export const CARD_FLOW_KEY = "cardflow";
3041
export const CARD_FLOW_LOAD = (): Promise<{ default: typeof CardFlowWithSeek }> => import("../../views/note/NoteCardFlowWithSeek");
3142

43+
/**
44+
* 第三视图键(= 注册表的 `NOTE_VIEWS[2]`)与其本地加载器(T10;两处必须逐字相等,见文件头)。
45+
* ⚠️ 返回类型里的 `ComponentType<never>` 是**结构占位**(`never` 只描述「props 面与 `lazyMapOf` 的
46+
* `P` 无关」):调用侧把它当 `ComponentType<P>` 用时**必须**在宿主那一处显式断言(本仓不引 `any`)。
47+
*/
48+
export const NOTE_EVIDENCE_KEY = "evidence";
49+
export const EVIDENCE_LOAD = (): Promise<{ default: ComponentType<never> }> =>
50+
import("../../views/note/NoteEvidenceTrackView") as Promise<{ default: ComponentType<never> }>;
51+
52+
/** 第三视图的**注入面**(T8 的 `NoteEvidenceTracks` 逐字转出;容器侧 `useNoteEvidence` 产出)。 */
53+
export type NoteEvidenceSlot = NoteEvidenceTracks;
54+
3255
/**
3356
* 惰性映射:**默认视图无 `load` ⇒ 值为 `null`**,永不进 `React.lazy`(§7.3② 的构造性证据)。
3457
* `lazy()` 只按 `specs` 引用建一次 —— 每渲染新建会让子树恒重挂(F5 的挂载计数会当场红)。
35-
* T17:**卡片流**经包装件加载(键漂移会让它静默失效 ⇒ F11 用例即守卫)。
58+
* T17:**卡片流**经包装件加载(键漂移会让它静默失效 ⇒ F11 用例即守卫);**T10**:第三视图经
59+
* `evidence` 加载器(同上,键漂移 ⇒ 该视图静默不渲染 ⇒ F12 用例即守卫)。
3660
* ⚠️ 本件是**纯函数**(不持有记忆)⇒ 调用侧必须把返回值包进 `useMemo(..., [specs])`,
3761
* 否则每次渲染都新建 `LazyExoticComponent` ⇒ 子树恒重挂(上一条的机理同源)。
3862
*/
3963
export function lazyMapOf<P>(
4064
specs: readonly LazyViewSpec<P>[],
4165
cardFlow: () => Promise<{ default: ComponentType<P> }>,
66+
evidence: () => Promise<{ default: ComponentType<never> }>,
4267
): ReadonlyMap<string, LazyExoticComponent<ComponentType<P>> | null> {
4368
return new Map<string, LazyExoticComponent<ComponentType<P>> | null>(
44-
specs.map((spec): readonly [string, LazyExoticComponent<ComponentType<P>> | null] =>
45-
[spec.key, spec.load ? lazy(spec.key === CARD_FLOW_KEY ? cardFlow : spec.load) : null]),
69+
specs.map((spec): readonly [string, LazyExoticComponent<ComponentType<P>> | null] => {
70+
// 键 → 加载器的路由只有这一处;默认视图(无 `load`)保持 `null` ⇒ 永不进 `lazy()`
71+
if (!spec.load) return [spec.key, null];
72+
if (spec.key === CARD_FLOW_KEY) return [spec.key, lazy(cardFlow)];
73+
// 🔴 `evidence` 的 props 面与宿主的 `P` 无关(它是 slot 的**超集**:+ 可选 `evidence`)⇒
74+
// 此处是**唯一**的类型断言点,调用侧无需再断言(断言集中在加载器路由这一处,便于审查)。
75+
if (spec.key === NOTE_EVIDENCE_KEY) return [spec.key, lazy(evidence as () => Promise<{ default: ComponentType<P> }>)];
76+
return [spec.key, lazy(spec.load)];
77+
}),
4678
);
4779
}

0 commit comments

Comments
 (0)