|
| 1 | +# v0.11 大目标版本系列设计:笔记组与学习循环 |
| 2 | + |
| 3 | +> 状态:已批准(用户授权全自主执行,2026-08-22) |
| 4 | +> 依据:[v4 路线图](../../Foresight/v4-roadmap-build-order-analysis.md)(已裁决采纳)· |
| 5 | +> [需求池 v0.11 区段](../../product/requirements-pool.md)(REQ-195~198)· |
| 6 | +> [pain-points-v4](../../product/pain-points-v4.md) §7.4/§7.5/§11/附录B |
| 7 | +> 范围:v0.11.0(组基建·容器侧)→ v0.11.1(feed 进料)→ v0.11.2(学习循环统一)→ v0.11.3(组结算) |
| 8 | +
|
| 9 | +## 一、总架构 |
| 10 | + |
| 11 | +统一产物层「笔记组」:一切采集产物归入组,组是唯一容器;容器地形(课程)与 |
| 12 | +feed 地形(碎片)只是组的两种形成方式(v4 §7.4 融合点)。自底向上四层: |
| 13 | + |
| 14 | +``` |
| 15 | +原子层:note_groups/fragments/flashcards schema + 路由/调度/结算纯函数(TDD) |
| 16 | +业务层:db_note_groups.rs / group_route.rs / spacy scheduler / settlement |
| 17 | +系统层:commands_groups.rs / commands_fragments.rs / commands_flashcards.rs |
| 18 | +UI 层:NotesPage 组侧栏 + 组卡片(路由可见可改)+ 复习面(最小化)+ 结算向导 |
| 19 | +``` |
| 20 | + |
| 21 | +硬约束(写入每层实现): |
| 22 | +- 提取优先:复习"先回忆再看",无被动重读模式 |
| 23 | +- 弹性承诺:周契约,断签不清零(review_logs 无 streak 字段) |
| 24 | +- 动机App化防御:UI 最小化,引擎先有真牙 |
| 25 | +- 碎片身份诚实:fragments 独立表,不与 notes 混装 |
| 26 | +- feed 功能开关默认关;路由误判可见可改 |
| 27 | + |
| 28 | +## 二、v0.11.0 笔记组基建·容器侧(REQ-195~198) |
| 29 | + |
| 30 | +### 2.1 数据模型(db_migrations.rs 惯例:建表幂等 + ensure_column) |
| 31 | + |
| 32 | +```sql |
| 33 | +CREATE TABLE IF NOT EXISTS note_groups ( |
| 34 | + id INTEGER PRIMARY KEY AUTOINCREMENT, |
| 35 | + name TEXT NOT NULL, |
| 36 | + terrain TEXT NOT NULL DEFAULT 'container', -- container/feed(管线分叉前提) |
| 37 | + kind TEXT NOT NULL DEFAULT 'standalone', -- course 课程组/topic 主题组/standalone 独立组 |
| 38 | + domain_tag TEXT, -- DomainKind kebab-case(主题组必填) |
| 39 | + source TEXT NOT NULL DEFAULT 'route', -- route/series/manual |
| 40 | + series_key TEXT, -- series_detect 系列名(课程组幂等键) |
| 41 | + route_reason TEXT, -- 路由理由 JSON(命中信号明细,REQ-198) |
| 42 | + route_overridden INTEGER NOT NULL DEFAULT 0, -- 用户改判标记(修改即记忆) |
| 43 | + created_at INTEGER NOT NULL, |
| 44 | + updated_at INTEGER NOT NULL |
| 45 | +); |
| 46 | +CREATE UNIQUE INDEX IF NOT EXISTS idx_groups_series |
| 47 | + ON note_groups(series_key) WHERE series_key IS NOT NULL; |
| 48 | +CREATE INDEX IF NOT EXISTS idx_groups_domain ON note_groups(domain_tag); |
| 49 | +-- notes 补列(ensure_column 幂等;ON DELETE SET NULL——删组不断笔记资产) |
| 50 | +ALTER TABLE notes ADD COLUMN group_id INTEGER REFERENCES note_groups(id) ON DELETE SET NULL; |
| 51 | +CREATE INDEX IF NOT EXISTS idx_notes_group ON notes(group_id); |
| 52 | +``` |
| 53 | + |
| 54 | +### 2.2 结构密度路由纯函数(group_route.rs,TDD golden 用例先行) |
| 55 | + |
| 56 | +复用 vote_detect/vote_tier 加权投票范式(每信号一票 → 计票 → 置信): |
| 57 | + |
| 58 | +```rust |
| 59 | +pub struct GroupRouteSignals { |
| 60 | + pub has_series: bool, // series_detect 命中 → 直接课程组(不进投票) |
| 61 | + pub chapter_density: f32, // 章节数/小时(chapter_detect 产出) |
| 62 | + pub glossary_terms: usize, // 术语表条目数(glossary 产出) |
| 63 | + pub ocr_text_density: f32, // OCR 文本块密度 0..1 |
| 64 | + pub profile_unknown: bool, // 形态 unknown(四维解耦信号) |
| 65 | + pub tier_rich: bool, // 画面档 rich/mid(video_tier_detect) |
| 66 | + pub domain_kind: Option<DomainKind>, // 领域命中(归主题组的前提) |
| 67 | +} |
| 68 | +pub enum RouteAction { OwnGroup, TopicGroup, NeedConfirm } |
| 69 | +pub struct GroupRouteDecision { |
| 70 | + pub action: RouteAction, |
| 71 | + pub reasons: Vec<String>, // 命中信号明细(前端路由理由展示) |
| 72 | +} |
| 73 | +pub fn route_group(signals: &GroupRouteSignals) -> GroupRouteDecision; |
| 74 | +``` |
| 75 | + |
| 76 | +投票规则(附录B 判据:高密度自成一组、低密度归主题组): |
| 77 | +- 高密度票:chapter_density ≥ 2.0/h、glossary_terms ≥ 5、ocr_text_density ≥ 0.5、tier_rich |
| 78 | +- 低密度票:profile_unknown、chapter_density < 0.5、glossary_terms == 0 |
| 79 | +- 高密度 ≥2 票 → OwnGroup;低密度 ≥2 票且 domain 命中 → TopicGroup; |
| 80 | + 票数不足/高低冲突 → NeedConfirm(诚实不乱判,vote_tier 同口径) |
| 81 | +- 零信号 → OwnGroup 兜底(会话产物默认独立成组,不埋进主题组——埋没比空洞更伤) |
| 82 | + |
| 83 | +### 2.3 容器侧组化接线(commands_session_note.rs convert_to_note 内) |
| 84 | + |
| 85 | +笔记落库后(单一落库出口): |
| 86 | +1. `extract_series(session.title)` 命中 → 课程组(series_key 幂等查找/创建, |
| 87 | + kind=course,source=series)——兑现 REQ-078 语义升级 |
| 88 | +2. 无系列 → 收集信号跑 `route_group`: |
| 89 | + - OwnGroup → 独立组(name=笔记标题) |
| 90 | + - TopicGroup → 按 domain_tag 查/建主题组(kind=topic,terrain=container) |
| 91 | + - NeedConfirm → 独立组 + route_reason 标记待确认(UI 高亮) |
| 92 | +3. `notes.group_id` 回写;route_reason 落组(REQ-198 可见) |
| 93 | + |
| 94 | +### 2.4 Commands(commands_groups.rs)+ UI |
| 95 | + |
| 96 | +- `list_note_groups`(含组内笔记数)/ `get_note_group` / `create_topic_group` |
| 97 | +- `override_group_route(group_id, kind, domain_tag)`——改判即记忆 |
| 98 | + (route_overridden=1,改判记录进 route_reason,REQ-198) |
| 99 | +- `move_note_to_group(note_id, group_id)` / `rename_note_group` |
| 100 | +- 前端:NotesPage 左侧栏新增「组」区段(按 v4 §7.5 容器组在每日/会话面语义内); |
| 101 | + 组卡片显示路由理由明细 + 待确认徽标 + 一键改判(v0.9.0 检测卡交互哲学延续); |
| 102 | + 笔记列表支持按组过滤 |
| 103 | + |
| 104 | +## 三、v0.11.1 feed 进料口(功能开关默认关) |
| 105 | + |
| 106 | +### 3.1 fragments 原料层(独立表——碎片不是笔记,身份诚实) |
| 107 | + |
| 108 | +```sql |
| 109 | +CREATE TABLE IF NOT EXISTS fragments ( |
| 110 | + id INTEGER PRIMARY KEY AUTOINCREMENT, |
| 111 | + text TEXT NOT NULL, |
| 112 | + image_path TEXT, -- 剪贴板图片(存 data_dir/fragments/) |
| 113 | + domain_tag TEXT, -- detect_domain 自动归组依据 |
| 114 | + group_id INTEGER REFERENCES note_groups(id) ON DELETE SET NULL, |
| 115 | + source TEXT NOT NULL DEFAULT 'manual', -- manual/clipboard |
| 116 | + created_at INTEGER NOT NULL |
| 117 | +); |
| 118 | +``` |
| 119 | + |
| 120 | +### 3.2 功能开关与捕获 |
| 121 | + |
| 122 | +- `feature_flags.json`(应用数据目录):`{ "feed_capture": false }`——默认关, |
| 123 | + SettingsPage 开关;关闭时捕获 command 拒绝执行(不信任前端隐藏) |
| 124 | +- `capture_fragment(text, image_b64?)`:文本 ≤2000 字;图片解码白名单格式 |
| 125 | + 落 data_dir/fragments/;`detect_domain` 命中标题词 → 查/建 terrain=feed |
| 126 | + 主题组并挂 group_id;未命中 → 无组(结算面兜底归组) |
| 127 | +- DomainTag 归组粒度对齐领域(契约一:拒绝大类) |
| 128 | + |
| 129 | +### 3.3 UI |
| 130 | + |
| 131 | +SettingsPage 开关 + NotesPage 组侧栏显示 feed 主题组(仅开关开启时); |
| 132 | +快速捕获输入框(组侧栏顶部,文本粘贴即捕获;图片走剪贴板)。 |
| 133 | + |
| 134 | +## 四、v0.11.2 学习循环统一(最大新建块) |
| 135 | + |
| 136 | +### 4.1 调度器(FSRS 主 / SM-2 降级,接口不变) |
| 137 | + |
| 138 | +- 优先引入 `fsrs = "6"` crate(纯算法零网络,FSRS-6;补 ADR-018); |
| 139 | + 若构建环境受限 → 手写 `sm2.rs` 纯函数兜底(EF 系数版,调度接口一致) |
| 140 | +- 统一接口(scheduler.rs): |
| 141 | + |
| 142 | +```rust |
| 143 | +pub enum Rating { Again, Hard, Good, Easy } |
| 144 | +pub struct CardState { pub stability: f32, pub difficulty: f32, |
| 145 | + pub reps: u32, pub lapses: u32, pub last_review_ms: u64 } |
| 146 | +pub struct ScheduleOutcome { pub next_state: CardState, pub due_at_ms: u64 } |
| 147 | +pub fn schedule(state: &CardState, rating: Rating, now_ms: u64) -> ScheduleOutcome; |
| 148 | +``` |
| 149 | + |
| 150 | +- 弹性承诺:无 streak 概念;欠账卡仅按 due_at 排序呈现,不追债不清零 |
| 151 | + |
| 152 | +### 4.2 数据模型 |
| 153 | + |
| 154 | +```sql |
| 155 | +CREATE TABLE IF NOT EXISTS flashcards ( |
| 156 | + id INTEGER PRIMARY KEY AUTOINCREMENT, |
| 157 | + group_id INTEGER NOT NULL REFERENCES note_groups(id) ON DELETE CASCADE, |
| 158 | + note_id INTEGER REFERENCES notes(id) ON DELETE SET NULL, |
| 159 | + fragment_id INTEGER REFERENCES fragments(id) ON DELETE SET NULL, |
| 160 | + front TEXT NOT NULL, back TEXT NOT NULL, |
| 161 | + kind TEXT NOT NULL DEFAULT 'fact', -- 内容分型预埋:fact 先做 |
| 162 | + state_json TEXT NOT NULL, -- CardState 序列化 |
| 163 | + due_at INTEGER NOT NULL, |
| 164 | + created_at INTEGER NOT NULL |
| 165 | +); |
| 166 | +CREATE INDEX IF NOT EXISTS idx_cards_due ON flashcards(due_at); |
| 167 | +CREATE TABLE IF NOT EXISTS review_logs ( |
| 168 | + id INTEGER PRIMARY KEY AUTOINCREMENT, |
| 169 | + card_id INTEGER NOT NULL REFERENCES flashcards(id) ON DELETE CASCADE, |
| 170 | + rating TEXT NOT NULL, -- again/hard/good/easy |
| 171 | + reviewed_at INTEGER NOT NULL |
| 172 | +); |
| 173 | +CREATE TABLE IF NOT EXISTS metrics_events ( |
| 174 | + id INTEGER PRIMARY KEY AUTOINCREMENT, |
| 175 | + kind TEXT NOT NULL, -- card_reviewed/fragment_upgraded/ |
| 176 | + -- group_settled/self_test_done |
| 177 | + payload_json TEXT NOT NULL DEFAULT '{}', |
| 178 | + created_at INTEGER NOT NULL |
| 179 | +); |
| 180 | +``` |
| 181 | + |
| 182 | +### 4.3 组→闪卡生成(本地规则版先行) |
| 183 | + |
| 184 | +- `generate_group_cards(group_id)`:组内笔记的词汇表块术语(structure_note |
| 185 | + ## 词汇表 行解析)+ glossary 候选 → front=术语 / back=术语首次出现的上下文句 |
| 186 | + (锚点回链);fragment 文本 >2 句时首句为 front 其余为 back(提取优先) |
| 187 | +- AI 生成为可选增强:复用 ai_client 授权/成本/审计全套,默认关 |
| 188 | +- 生成幂等:同 group+front 查重跳过 |
| 189 | + |
| 190 | +### 4.4 复习面(UI 最小化)+ 埋点 |
| 191 | + |
| 192 | +- NotesPage 组卡片「复习」入口 → 复习队列(due_at ≤ now,按组过滤): |
| 193 | + 先显示 front → 点击「回忆完成」→ 展开 back → 四档评分(Again/Hard/Good/Easy) |
| 194 | +- 主题自测最轻形态:组内随机 5 卡快答(仅评分不展示对错叙事——N14 防御) |
| 195 | +- 每次评分落 review_logs + metrics_events(card_reviewed); |
| 196 | + fragment→card 落 metrics_events(fragment_upgraded)——Phase 4 门控数据从第一天记 |
| 197 | + |
| 198 | +## 五、v0.11.3 组结算机制(防沼泽仪式) |
| 199 | + |
| 200 | +### 5.1 触发纯函数(settlement.rs) |
| 201 | + |
| 202 | +```rust |
| 203 | +pub struct SettlementSignals { pub item_count: usize, pub oldest_at_ms: u64, |
| 204 | + pub last_settled_at_ms: Option<u64>, pub now_ms: u64 } |
| 205 | +pub fn settlement_due(s: &SettlementSignals) -> bool; |
| 206 | +// 阈值:item_count ≥ 50;或 周期:≥90 天未结算且 item_count ≥ 20 |
| 207 | +``` |
| 208 | + |
| 209 | +### 5.2 结算向导(用户可见仪式,三步) |
| 210 | + |
| 211 | +1. **核心提炼**:组内词汇表/高频术语汇总为「组核心」笔记(本地规则; |
| 212 | + AI 精修可选,复用 AiRefine 协议) |
| 213 | +2. **重复合并**:碎片/卡文本 Jaccard 去重(复用 asr_dedupe 归一化口径), |
| 214 | + 展示合并对由用户确认(不静默删) |
| 215 | +3. **低价值归档**:无卡绑定且 180 天未访问的碎片标记 archived |
| 216 | + (fragments 补 status 列;不删除——可恢复) |
| 217 | + |
| 218 | +### 5.3 settlements 记录 |
| 219 | + |
| 220 | +```sql |
| 221 | +CREATE TABLE IF NOT EXISTS settlements ( |
| 222 | + id INTEGER PRIMARY KEY AUTOINCREMENT, |
| 223 | + group_id INTEGER NOT NULL REFERENCES note_groups(id) ON DELETE CASCADE, |
| 224 | + stats_json TEXT NOT NULL, -- 提炼条数/合并对数/归档数 |
| 225 | + created_at INTEGER NOT NULL |
| 226 | +); |
| 227 | +-- fragments 补列(ensure_column):status TEXT NOT NULL DEFAULT 'active' |
| 228 | +``` |
| 229 | + |
| 230 | +结算完成落 metrics_events(group_settled)——北极星组成之一。 |
| 231 | + |
| 232 | +## 六、测试策略 |
| 233 | + |
| 234 | +- 纯函数全 TDD(AAA + golden 用例):group_route(高/低/冲突/零信号/系列直判)、 |
| 235 | + schedule(四档评分间隔单调性、Again 重置)、settlement_due(阈值/周期边界)、 |
| 236 | + 卡片生成幂等、去重合并 |
| 237 | +- 数据层用 `:memory:` 库(db_notes_tests 同款 mem_db 范式) |
| 238 | +- command 层仅参数校验与编排,无业务逻辑(AGENTS.md §6) |
| 239 | +- 模型文件零依赖(调度器纯算法;FSRS crate 无模型资产) |
| 240 | + |
| 241 | +## 七、版本交付与提交纪律 |
| 242 | + |
| 243 | +每个小版本独立交付闭环(代码 + 测试 + 需求池状态同步 + 版本文档 + 提交): |
| 244 | +- v0.11.0:`feat(groups): ...` 3-5 个原子提交(原子层/接线/commands/UI/文档) |
| 245 | +- v0.11.1 同上;v0.11.2 启动时补 ADR-018(fsrs 依赖引入) |
| 246 | +- v0.11.3 收尾全系列;需求池 REQ-195~198 状态逐版推进 |
| 247 | +- 单文件 ≤300 行(超限即拆 + 豁免登记);所有新模块含 @ai-context 注释 |
0 commit comments