# dsh-study-buddy 架构选型(文档式笔记重构) > 版本基线:v1.0.0 | 状态:**已定稿 · 待制定重构计划** | 读者:插件维护者、二次开发者 > 配套:[`需求分析.md`](需求分析.md)(需求基线,本文件的输入)、[`设计文档.md`](../设计文档.md)(现行 v0.9.1 设计,重构完成后整体重写)、 > [`技术文档.md`](../技术文档.md)(现行实现口径)、[`README.md`](../README.md)(文档地图与维护约定) > > 本文件回答"**怎么做**":模块划分、接口契约、数据流、门禁机制、扩展点与验收门。 > 需求层"要什么"以 [`需求分析.md`](需求分析.md) 为准;本文件只记实现选型与其取舍。 --- ## 一、选型总览 | # | 选型问题 | 结论 | 备选与否决理由 | | :-- | :-- | :-- | :-- | | A1 | 插件结构 | **单插件行、单 `VaultStore`、内部模块化**(沿用现状) | 拆分多插件行:需要 isolate realm 且工具作用域复杂化,而本插件不发布 Cordis 服务,没有跨会话共享需求 | | A2 | 生命周期 | `apply()` 只做"注册工具 + 装配 store";**所有会话数据(索引、门禁状态)挂在 store 实例上** | 模块级可变单例:第二次挂载会串会话 | | A3 | 门禁实现 | **状态栈(`.study/`)+ 工具侧硬校验**,不用进程内存 | 仅存内存:DSH 重启后门禁凭空失效;仅靠 persona:不可强制(需求 R25 明确否决) | | A4 | 索引驻留 | **索引不再常驻正文**:只驻留元数据 + 倒排 token;snippet 命中后按路径现读 | 现状常驻全库正文;文档式笔记单块可达 10KB 级,常驻会线性膨胀(需求 N2) | | A5 | 存储层抽象 | 新建**薄存储层**(`plans` / `archive` / `expect`) | 直接裸写 JSON:三处重复权限/损坏/越界处理;本地 JSON 数据库:在 vault 内引入第二真相,违反不变量 1 | | A6 | 规划凭据 | `.study/plans/.json`:**提案 + 消费记录** | 只在对话里留 prompt 文本:无法核验;写进 vault 可见文件:需求 R11 明确否决 | | A7 | 目录导航 | 索引重建时增量维护 **DirIndex**(路径 → 块数与签名) | 每次 `note_list` 现扫该子树:目录多时 N 次 walk | | A8 | 覆盖度 | 新增 **`overview.ts` 纯函数**(按 `来源章节` 聚合),不改造 `insight.ts` | 复用 `insight.ts`:跨卡冲突/趋势的输入(模板与分值)已被裁掉,复用会留死代码 | | A9 | lint 计分 | **废弃 100 分制**,改为 finding 清单 + 严重级 | 保留分值:模板类扣分项删除后,"分数"不再可比,误导用户(需求 §六 F6) | | A10 | 模板渲染 | 工具描述由 **`tools.ts` 的注册表派生**(沿用 `ruleCatalog()` / `templateTable()` 的手法) | 手写长描述:与规则/键表漂移,且推高每请求固定开销 | | A11 | 期望文件读取 | 专属工具 `note_expect_get` 读取并**按文件签名标记已读** | 允许 `note_get` 代读:任意 `note_get` 都能开门禁,门禁形同虚设 | | A12 | 旧能力处置 | `card_*` 工具行**不保留**;旧卡按"无 `来源章节`"识别为存量,只读可用 | 保留过渡工具:每轮固定开销 + 长期两套口径 | | A13 | 块关键词口径 | 取消"一句话定义"语义,frontmatter 用 `简介`(一句话定位,无长度硬限,正文首行引用块自动回填) | 沿用 `定义` 名字:与旧"≤60 字硬拒绝"语义纠缠,用户与模型都会继承旧约束 | | A14 | 期望文件缺失时是否临时回退到内建写法 | **不回退**:fail-loud + 创建指引(把 `presets/study/assets/笔记期望.md` 复制到 vault 根) | 允许回退:`src/` 里就还得留一份默认写法,"约束只从期望文件来"重新变成两处口径(需求 F1 的初衷被抵消) | | A15 | `legacy` 卡补齐 `来源章节` 后的身份 | **自动转 `block`**:`note_update` 写入 `来源章节` 即完成升级,不新增升级工具 | 新增 `note_upgrade` 工具:18 个工具再加一个,而这个动作本就是一次普通更新 | ### 1.1 已定稿的接口决策(开工前冻结,阶段 0) | # | 决策 | 结论 | 影响面 | | :-- | :-- | :-- | :-- | | C1 | frontmatter 保留"一句话定位"键 | **保留 `简介`**:检索权重里"简介 > 正文"这一档是召回质量的主要来源;无长度硬限,不再是约束 | `frontmatter.ts` / `search.ts` / `note.ts` | | C2 | `legacy` 卡的身份转换 | **自动转 `block`**(= A15) | `note.ts` / `note_update` / `overview.ts` | | C3 | 期望文件缺失时的行为 | **不回退**(= A14) | `gate.ts` / `note_write` | | C4 | 索引头读取失败(frontmatter 超长/无换行巨行) | **回退读全文一次**并标记该块:宁可慢一次,不可索引缺字段 | `search.ts` / `vault.ts` | ### 1.2 三条不变量(与需求一致,架构上如何保证) | 不变量 | 架构落点 | | :-- | :-- | | 磁盘是唯一真相 | 索引是对磁盘的派生物,`invalidate()` 后重扫;不引入第二份真相(A5);`.study/` 只存"过程状态",删掉不丢笔记 | | 决定权在用户 | 写入必须携带**用户可打断的规划凭据**(A6);`note_update` 的变更语义由工具按期望文件与用户指令执行,不做自动改写 | | fail-loud | 门禁失败、状态损坏、路径越界、越界写入一律抛错并给修复步骤(§七) | --- ## 二、模块划分 ### 2.1 目标树 ``` src/ ├── index.ts VaultStore 业务编排 + apply + 配置校验(fail-loud) [已完成] ├── tools.ts 18 个工具定义(描述由注册表派生) [已完成] ├── note.ts ID/校验/渲染/三动作更新/wikilink 关联 [已完成] ├── notemodel.ts 段落模型:小节切分/块插入/块抹除/inlineText/围栏语言 [已完成] ├── frontmatter.ts 解析与生成(六键 + 来源章节/顺序/简介) [已完成] ├── dirs.ts 目录索引 DirIndex + 层级校验 + 笔记期望路径 + listDir [已完成] ├── search.ts 分词/加权/多根索引(不常驻正文) [已完成] ├── gate.ts 硬门禁:期望已读 + 规划有效 + 路径在规划内 + 提案渲染 [已完成] ├── planstore.ts .study/plans/.json 读写与状态机 [已完成] ├── archive.ts 历史存档:写/列/读/恢复 + archiveThenWrite 不变量 [已完成] ├── store.ts .study/session.json:期望签名、已读规划 [已完成] ├── overview.ts 主题清单 + 按来源章节的覆盖度(纯函数) [已完成] ├── sourceSection.ts `来源章节` 语法与无损归一 [已完成] ├── links.ts 关联增删 + 改名/入链重写/断链检测(原 rename.ts 并入) [已完成] ├── vault.ts 扫描/跳过/路径安全/原子写/落盘解析/readNoteSource [已完成] ├── lint.ts RULES 注册表(4 条)+ 报告(无分值)+ 期望检查项解析 [已完成] ├── lintrules.ts 会话残留/代码块语言/外部资源判定 + 共享正则 [已完成] ├── study.ts progress.json + memory.json [未拆:仍在 state.ts + memory.ts] └── opener.ts 开场门禁(提示段 + 预步提醒) [不动] ``` > 实况(阶段 6 收口后):`src/` 共 **20 个模块**。计划里"`state`+`memory` 合并为 `study`"未执行—— > 两者职责清晰、用例齐备,合并只减少一个文件而增加一次回归面,判定为**不做**(记入下方"计划偏离")。 > `opener.ts` 按计划未动。 模块数 16 → **20**(删 `template.ts` / `insight.ts` / `rename.ts` 三席;新增 `dirs` / `gate` / `planstore` / `archive` / `store` / `overview` / `sourceSection` 七席;`rename.ts` 的改名与入链能力并入 `links.ts`)。 ``` presets/study/ ├── preset.yml 显示名与描述(不变) ├── agent.cordis.yml persona + 工具行 + 插件行 config(键收敛见 §8) ├── assets/笔记期望.md 默认期望模板(新建;不自动写入 vault,用户复制后自定义) └── skills/ 6 个技能(study-loop / card-format → note-format / file-reading / incremental-update / domain-adaptation / memory-auto) ``` ### 2.2 模块职责与依赖 | 模块 | 职责 | 依赖 | 纯度 | | :-- | :-- | :-- | :-- | | `vault.ts` | 扫描/跳过清单/路径安全/原子写/落盘目录解析/`notePathFor` | node:fs | IO | | `search.ts` | token 化、倒排、字段加权、多根去重、snippet 现读 | frontmatter, vault | IO(snippet) | | `frontmatter.ts` | 六键 + 文档键解析/生成;`简介` 提取 | notemodel(inlineText) | 纯 | | `note.ts` | ID 生成、必填校验、渲染、`append`/`replace` 应用 | frontmatter, notemodel | 纯 | | `notemodel.ts` | 小节切分/渲染/标题匹配/块插入/块抹除/围栏语言/层级/列表计数 | — | 纯 | | `dirs.ts` | 路径规范化、DirIndex 维护、层级与命名校验、期望路径解析 | vault | 纯 + 少量 IO | | `gate.ts` | 门禁判定(读期望 / 规划有效 / 路径在规划内 / 未消费) | store, planstore, dirs | 纯判定 + IO 读 | | `planstore.ts` | 规划记录的创建/读取/确认/消费/清理 | node:fs | IO | | `archive.ts` | 存档写入(`.md`)、列目录、读取、恢复 | node:fs, frontmatter | IO | | `store.ts` | `.study/session.json`:期望签名与"已读"标记、plan 指针 | node:fs | IO | | `overview.ts` | 主题块清单、来源章节聚合、缺口计算 | notemodel, frontmatter | 纯 | | `lint.ts` | `RULES` 注册表(瘦身)+ 判定 + 报告(无分值) | notemodel, dirs | 纯 | | `links.ts` | 关联行归一/去重、wikilink 重写、断链检测、文件名计划 | frontmatter, notemodel, vault | 纯 | | `study.ts` | progress/memory 持久化(含 `_autoPrefs`) | vault | IO | | `opener.ts` | 开场门禁(不变) | — | 纯 | | `tools.ts` | 18 个工具定义(schema 即文档) | 各能力的类型 | 纯 | | `index.ts` | `VaultStore`(编排)+ `apply` | 全部 | IO | **结构约束(沿用现行设计)**:除 `vault/search/planstore/archive/store` 外,全部模块是纯文本变换、零宿主依赖、可独立单测;`@deepseek-ai/*` 保持构建 external。 ### 2.3 按需加载与性能控制点 | 操作 | 读盘量 | 说明 | | :-- | :-- | :-- | | `note_list` / `note_toc` / `note_overview` | 目录结构来自 DirIndex;每个块的 frontmatter 头 4KB | **不读正文**,大块不影响导航 | | `note_search` | 索引常驻;仅对最终 top-N 命中现读正文算 snippet | 现状对全部命中算 snippet 的老问题一并解决 | | `note_get` | 命中即读全文 | 按 ref 解析路径后可直读,跳过强制重扫 | | `note_write` | 门禁读(期望 + 规划记录)+ 目标目录 stat | 规划记录 O(1) | --- ## 三、工具面设计(18 个) | # | 工具 | 必填 | 可选 | 关键返回 | | --: | :-- | :-- | :-- | :-- | | 1 | `note_library` | `action(check)` | — | vault 可写性 / 期望文件状态与签名 / 顶层目录数 / 块数 / 存量卡数 | | 2 | `note_expect_get` | — | — | `笔记期望.md` 全文 + **标记已读(按签名)**;缺失时给创建指引 | | 3 | `note_list` | — | `path`、`depth`(默认 1) | 子目录 / 主题目录 / 块(标题·简介·来源章节·顺序),并标出**是否已有微目录** | | 4 | `note_get` | `ref` | — | `路径:…` + 完整原文(无截断) | | 5 | `note_search` | `query` | `domain`、`status`、`kind(block\|legacy\|note)`、`path`、`limit` | 命中列表(标题/ID/类型/路径/领域/状态/来源/来源章节/简介/片段) | | 6 | `note_overview` | — | `path`、`material` | 每主题块清单 + 按 `来源章节` 聚合的覆盖情况与缺口 | | 7 | `note_plan` | `rootPath`、`items[]` | `material`、`notes`、`action(create\|abandon)` | 提案全文(复用标注 / 新建目录 / 每块归属与顺序)+ `planId` | | 8 | `note_write` | `planId`、`title`、`source`、`content` | `sourceSection`、`order`、`domain`、`status`、`links`、`dryRun` | `已写入:rel` + `ID:…` + 规划归属 + 微目录待刷新提示 | | 9 | `note_update` | `ref`、`action(append\|replace\|move)` | `changes`、`newContent`、`targetPath`、`source`、`dryRun` | `已更新:rel`(replace 时回报存档路径) | | 10 | `note_toc` | `dir` | `dryRun`、`title` | `已生成:rel(收录 N 块)` + 全文 | | 11 | `note_link` | `from`、`to`、`kind(prev\|next\|sibling)` | — | 建立结果 + 双向改动清单 | | 12 | `note_unlink` | `from`、`to` | — | 移除结果(两侧都清) | | 13 | `note_rename` | `ref`、`title` | `dryRun` | 标题/文件名变更 + wikilink 改动清单 + 断链检测 | | 14 | `note_history` | `ref` | `action(list\|read)`、`id` | 该块的存档清单 / 某份存档全文 | | 15 | `note_restore` | `ref` | `archiveId`、`dryRun` | 恢复结果(当前正文先转入存档,不丢内容) | | 16 | `note_lint` | — | `ref`、`scope(vault\|all)`、`limit`、`rule` | finding 清单(规则/严重级/行号/摘录/建议),**无总分** | | 17 | `study_progress` | `action(get\|set\|clear)` | `material`、`section`、`pendingQuestions[]`、`touchedIds[]` | 语义不变 | | 18 | `study_memory` | `action(get\|set\|append\|remove\|clear)` | `key`、`value` | 语义不变(含 `_autoPrefs`) | **设计约定** - **一个工具一个动词**:不把"列目录/看单篇/搜全库"塞进一个参数分支(工具描述本身是 prompt,语义清晰优先)。 - 所有工具 `output.schema.type === 'string'`,返回纯文本含回显行(沿用现行风格)。 - `note_library` 的 `action` 保留枚举形状(未来可加 `repair`),当前只实现 `check`。 - 每请求固定开销预算:18 个工具 + persona + 技能 ≈ 现状(21 工具 / ~15KB)水平;长描述全部由注册表派生(A10)。 **能力删除清单**:`card_moc`(由 `note_toc`/`note_overview` 承担)、`card_id`(并入写入流程)、`card_create`/`card_update`/`card_link`/`card_search`/`card_get`/`card_lint`/`card_history`/`card_rename`(由同名 `note_*` 取代),`insight` 的 `cross`/`trend`/`rating` 三开关(随模板与分值一起退场)。 --- ## 四、数据契约 ### 4.1 frontmatter ```yaml --- ID: 202609092139_92d2ab # 新块必须;缺失 = 存量卡(只读 + 可显式升级) 标题: 高斯消元法 领域: #计算方法-线性方程组 # 首个标签 = 领域键(可作落盘快捷方式) 来源: 计算方法课件 状态: 已确认 # 草稿 / 已确认 / 需更新 来源章节: 《计算方法》第2章 线性方程组数值解法 / 2.1节 顺序: 3 # 同目录阅读顺序(可选;微目录排序用) 简介: 用初等行变换把系数矩阵化为上三角,再回代求解;主元为 0 时需换行。 --- ``` | 键 | 必填 | 说明 | | :-- | :-- | :-- | | `ID` | 是(新块) | `YYYYMMDDHHmm_6位hex` 自动生成;手改 ID 会被视为新对象 | | `标题` | 是 | 单行;文件名清洗后与之一致(改名由 `note_rename` 同步) | | `领域` | 是 | 空格分隔标签,首个为领域键;用于检索过滤与 `domainFolders` 快捷落盘 | | `来源` | 是 | 资料名(如课程名、书名、项目名) | | `状态` | 是 | 枚举 `草稿/已确认/需更新`;不再与"是否需要重写"耦合 | | `来源章节` | 否(新块建议必填) | 覆盖度唯一数据源;语法见 §4.2 | | `顺序` | 否 | 整数;缺省按标题字典序(微目录排序的兜底) | | `简介` | 否 | 一句话定位,无长度硬限;缺省时从正文首个引用块自动提取(提取不到则不写该键,`note_search` 命中行省略该行) | **兼容解析**:`模板` 键被忽略(不再有模板概念,读取时静默丢弃,不报错);`定义` 作为 `简介` 的别名接受(存量卡可读)。旧笔记(无 frontmatter、无 ID)仍可索引为 `kind=note`。 **类型判定**(三态,取代旧的 card/note 二元): | kind | 判据 | 可写 | | :-- | :-- | :-- | | `block` | 有 `ID` **且**有 `来源章节` | 是(完整能力) | | `legacy` | 有 `ID`,无 `来源章节` | 是(`note_update` 可补 `来源章节` 完成升级;不自动改写) | | `note` | 无 `ID`(旧笔记,任一检索根) | 仅 vault 内、且需 `linkIntoNotes: true` | > `legacy` 只读的说法更正为"可显式升级":升级动作由用户命令触发,工具负责补键、可选迁目录、纳入微目录(需求 R19 的显式路径)。 ### 4.2 `来源章节` 语法与归一 ``` 《资料名》第N章 章标题 / N.N节 《资料名》第N章 章标题 ← 无节号时合法 ``` - 归一规则:全角括号→`《》`、`第 2 章`→`第2章`、章节号统一阿拉伯数字、多余空白折叠、`/` 两侧留一个空格。 - 解析产物:`{ material, chapter, chapterTitle, section }`;解析失败不报错,块列入 `note_overview` 的"未归类"。 - `来源` 与 `来源章节` 的 `资料名` 不一致时,以 `来源章节` 为准并在返回值里回显提示。 ### 4.3 `.study/` 过程状态(可删,删了不丢笔记) ``` .study/ ├── progress.json # 学习进度(不变) ├── memory.json # 跨会话记忆(不变,含 _autoPrefs) ├── session.json # 门禁状态:期望文件签名(mtime|ctime|size)+ 已确认的 planId ├── plans/.json # 规划记录:提案 + 用户确认 + 消费记录 └── archive//<时间戳>.md # 历史存档:被推翻/替换的块正文 ``` **`plans/.json`** ```jsonc { "planId": "20260910_ab12cd", "createdAt": "2026-09-10T21:39:00+08:00", "confirmed": true, // 用户拍板后由 note_plan(action=create, confirm=true) 写入 "confirmedAt": "…", "rootPath": "计算机通识/计算方法/第2章 线性方程组数值解法", "material": "《计算方法》", "items": [ { "title": "高斯消元法", "path": "…/直接法/高斯消元法.md", "sourceSection": "…/2.1节", "order": 1 }, { "title": "列主元消元", "path": "…/直接法/列主元消元.md", "sourceSection": "…/2.2节", "order": 2 } ], "createdDirs": ["…/直接法"], "consumed": [ { "title": "高斯消元法", "rel": "…/直接法/高斯消元法.md", "at": "…" } ] } ``` **`session.json`** ```jsonc { "expectSignature": "1712345678900|1712345678900|4120", // 已读的那一版期望文件 "expectPath": "笔记期望.md", "activePlanId": "20260910_ab12cd", "updatedAt": "…" } ``` **存档文件头** ```yaml --- 存档自: 202609092139_92d2ab 原路径: 计算机通识/计算方法/第2章 线性方程组数值解法/直接法/高斯消元法.md 存档时间: 2026-09-10T21:45:00+08:00 原因: 推翻:主元为 0 的处理结论有误 --- <被替换的正文原文,一字不改> ``` ### 4.4 微目录 ```markdown # 线性方程组数值解法 > 一句话导读:本章解决 Ax=b 的直接法与迭代法两条线……(人工可写,工具不动) ## 块清单 1. [[高斯消元法]] —— 初等行变换化上三角后回代(2.1节) 2. [[列主元消元]] —— 主元过小时换行使算法稳定(2.2节) ``` - **生成段有边界注释**;`note_toc` 只重写注释之间的内容,用户手写的导读与补充段落原样保留(解决"工具生成的目录盖掉人的叙述")。 - 排序:`顺序` 键 → 标题字典序;同名块用**相对路径** wikilink 消歧。 - 自排除:`微目录.md` 不计入块数,也不出现在自己的清单里。 ### 4.5 关联(wikilink) - `note_link(kind=prev|next|sibling)` 在两侧写 `## 关联` 小节: `- 前置:[[目标块]]` / `- 后续:[[目标块]]` / `- 兄弟:[[目标块]]`;`prev`/`next` 互为反向,`sibling` 两侧同向。 - 目标为 `legacy`/`note`(无 wikilink 锚点或跨根)时用根限定路径:`- 后续:[[工作目录/子目录/笔记.md|旧笔记标题]]`。 - 去重口径(沿用现行 D6):同小节内按 `[[目标]]` 目标串判重,标题改名后由 `note_rename` 统一改写,不靠文本匹配。 --- ## 五、数据流 ### 5.1 写入一次块(归档主路径) ``` 用户:"整理笔记" │ ├─(1) study_progress(get) 未答追问未清 → 先提示,不进入归档 ├─(2) note_library(check) 读 .study/session.json + 期望文件签名 │ └─ 期望签名不匹配 → 必须(3) ├─(3) note_expect_get 返回全文 → session.json 记签名(= 门禁开门) ├─(4) note_list(path) ×N 探明已有层级与已有块(复用判断的依据) ├─(5) note_plan(create) 生成提案 + planId;呈现给用户 │ └─ 用户修正 → 再调一次 note_plan(覆盖同一 planId) │ └─ 用户拍板 → note_plan(create, confirm=true) 写 confirmed=true ├─(6) note_write(planId, …) ×N 门禁三连校验 → 原子写 → 消费记录 ├─(7) note_link(...) 建关联(Wikilink) ├─(8) note_toc(dir) ×N 生成/刷新各级微目录 ├─(9) note_overview(path) 回报覆盖情况与缺口 └─(10) study_progress(set touchedIds…) + study_memory(set lastSummary=…) ``` **门禁三连校验(`note_write` 内部顺序,任一失败即抛错且磁盘零改动)** | 序 | 检查 | 失败文案(要点) | | --: | :-- | :-- | | 1 | `session.json.expectSignature` 与当前 `笔记期望.md` 签名一致 | `未读取笔记期望:请先调用 note_expect_get(vault 根:笔记期望.md)` | | 2 | 规划记录存在、`confirmed === true`、未过期(默认 24h) | `规划未确认或已过期:请先 note_plan 提案并让用户确认(planId:…)` | | 3 | 目标路径在 `rootPath` 之下、`title` 在 `items` 中、该项未被消费 | `"" 不在本次规划范围(规划根:)/ 该块已在本规划中写入过` | ### 5.2 索引重建(内存上的关键改动) ``` walkRoots(多根、去重、跳过清单、安全阀) → 每个文件:stat(已有)→ 读 frontmatter 头(≤4KB) → IndexedCard 元数据(无正文)+ 四路倒排 token(标题/简介/标签/正文) → DirIndex:目录 → { 块数、微目录是否存在、最后修改时间 } → titleHints(同名提示 O(1),沿用 N2 手法) note_search 命中 top-N → 按 path 现读正文 → 算 snippet(仅 N 篇) note_get 命中 → 按 ref 解析路径 → 直读;miss 且来自缓存 → force 重扫一次 ``` > **正文 token 仍需读一次全文**(关键词检索的输入):这次读取是构建索引的一次性成本,读完即丢弃正文、只留 token 计数。因此索引常驻内存从"全文"降到"倒排表",而单次重建的读盘量不变。 ### 5.3 一次 `note_update(action=replace)` ``` resolve(ref)(ID → 标题 → 路径/文件名,沿用现行口径) → 读全文 → 校验可写根 → archive.write(ID, 原文, 原因) 先存档(失败即中止,不写正文) → note.render(newContent 或 changes) 重渲染 frontmatter + 正文 → atomicWrite 临时文件 + rename → invalidate() + 消费/更新状态 → 返回:已更新:rel(旧正文已存档:.study/archive//<时间戳>.md) ``` **顺序不变量**:**先存档、后写正文**。任一步失败都不产生"正文已改、历史已丢"的组合(沿用现行 BIZ-1/BIZ-2 的"先校验后提交"纪律)。 --- ## 六、关键机制 ### 6.1 门禁为什么放在工具侧 需求 R25 要求"未读期望/未确认规划 → 拒绝写入"。persona 是 prompt,模型可能忘;进程内存门禁会在 DSH 重启后失效。因此状态落 `.study/`(磁盘唯一真相的延伸——它记录的是**过程**,不是笔记),由工具读写并强制。 - 期望"已读"用**文件签名**判定:用户中途改了期望 → 签名变 → 下次写入前必须重读(需求 F1 的"立即生效")。 - 规划凭据**可放弃**:`note_plan(action=abandon)` 清 `activePlanId`;同时提供 24h 过期,避免死锁。 - 死锁兜底:`note_library(check)` 单点自检并给出修复步骤;期望文件缺失时明确指引创建(需求 R28),**不静默回退**到内建写法。 ### 6.2 目录索引与层级校验 - `DirIndex` 在索引重建时整体构建,写入后**增量维护**(与 `titleHints` 同手法)。 - 层级上限软约束 6 层(超过给出提示但不拒绝);目录名禁止空段与 Windows 非法字符,沿用手写清洗(`sanitizeFilename`)。 - 未知路径(规划里新建的目录)在 `note_plan` 确认时**不落盘**,由 `note_write` 首次写入时创建(`ensureDir`),保证"没确认的东西不落盘"。 ### 6.3 覆盖度(`overview.ts`) ``` 输入:某主题(或某资料)下的块集合(来自 DirIndex + frontmatter 头) 分组:按 来源章节 → material → chapter → [section] 聚合 输出: 资料《计算方法》 第1章 误差与有效数字 3 块(1.1节 ✓ / 1.2节 ✓ / 1.3节 ✗) 第2章 线性方程组数值解法 2 块 未归类 1 块(缺 来源章节:<路径>) ``` - **不虚构**未出现的章节:完整性基线 = 已出现的章节文件 + 用户声明的材料,符合需求 R15。 - 缺口提示只给"未归类块"与"同一资料内章节序号跳号",不做臆测补全。 ### 6.4 lint 瘦身 | 保留规则 | 级别 | 判据 | | :-- | :-- | :-- | | `session-residue` | warn(可配 off/error) | 本机路径 / 源码行号 / 会话时间词 / 阶段代号 / 第二人称 | | `code-language` | warn | 存在未标语言的代码块 | | `external-resource` | info | `