Motif / notemd 数据模型

定义 Vault 的落盘结构、事实来源分层与核心数据类型。时间戳统一为 ISO 8601 字符串。

0. 设计原则

Motif 的存储遵循两条原则:

  1. 文件即事实来源,数据库只是缓存:人的知识与原始归档以 Markdown / JSON / 源文件落盘;SQLite 等索引均可从文件重建、可随时删除。这是与 Zotero(事实锁在 zotero.sqlite)最大的区别。
  2. 渐进式披露(Progressive Disclosure):目录结构本身就是 Agent 的接口。信息按"体量递增、成本递增"分层,Agent 按需逐层下钻,而不是一次性加载整篇论文。

0.1 渐进式披露分层

文件 Agent 何时读 体量
L0 指令 AGENTS.md 会话开始,总是 极小
L1 索引 PAPERS.md 需要"库里有什么" 小(每篇一行)
L2 条目 papers/<id>/NOTES.md 锁定某篇之后
L2.5 证据 papers/<id>/highlights.md 需要用户标注 / 精确引文
L3 正文 papers/<id>/PAPER.md 需要公式 / 实验细节 / 原文
L4 原始 papers/<id>/source/* 需追溯或重新解析 很大

AGENTS.md 是渐进式披露的"总开关":它本身很短,只写清楚库怎么组织、按什么顺序读、引用要带本地路径、写入先走临时文件——相当于一张地图,而不是把内容全塞进去。

0.2 三级事实来源

层级 内容 文件
Tier 1 事实来源 人的知识 + 原始归档 + 结构化元数据 AGENTS.mdNOTES.mdhighlights.mdmetadata.jsonnotes/plans/source/
Tier 2 派生索引 可从 Tier 1 重建的可移植投影 PAPERS.mdlibrary.bibPAPER.mdassets/
Tier 3 缓存 应用私有、可整删重建 .motif/(cache.sqlite、标注坐标、库级配置)
  • 删除 Tier 3:应用重扫 Vault 即可恢复。
  • 删除 Tier 2:可从 Tier 1 重新生成。
  • Tier 1 才是不可再生的用户资产,任何写入都要谨慎(先临时文件、确认后落盘)。

1. Vault 结构

motif-vault/
├── AGENTS.md              # L0 Agent 行为规范与读取协议
├── PAPERS.md              # L1 全局论文索引(派生自各篇 metadata.json,可重建)
├── library.bib            # 派生 BibTeX 汇总(引用导出)
├── papers/                # 文献主体,每篇一个独立目录
│   └── 1706.03762/        # arxiv 用 arXiv ID;非 arxiv 用 citekey 如 vaswani2017attention
│       ├── metadata.json  # 该篇元数据的事实来源(机器写入,UI 编辑)
│       ├── NOTES.md       # L2 结构化笔记(纯人的知识)
│       ├── highlights.md  # L2.5 标注:引文 + 想法(便携 Markdown)
│       ├── PAPER.md       # L3 清洗后的统一可读正文(派生,可选)
│       ├── assets/        # PAPER.md / NOTES.md 引用的图片(派生自 source)
│       └── source/        # L4 原始归档(只增不改)
│           ├── original.pdf
│           ├── arxiv-src/ #   解包后的 LaTeX 工程:main.tex、sections/、figures/、refs.bib
│           ├── original.html
│           └── ocr/       #   OCR / 解析中间产物:page-*.json(含 bbox)、raw-text.txt
├── notes/                 # 自由概念笔记 / 作者 / idea(Markdown + 双链)
│   └── *.md
├── plans/                 # 研究计划、Related Work 草稿
│   └── *.md
└── .motif/                # Tier 3 缓存(进 .gitignore,可整删)
    ├── cache.sqlite       #   元数据 / 全文 / 双链图 / 标注坐标索引
    └── config.json        #   库级设置(非机密)

ID 方案:arxiv 论文用标准 arXiv ID 作为目录名;非 arxiv 来源(如本地 PDF)用 citekey 作为目录名。citekey 由「第一作者姓氏 + 年份 + 标题首个实词」小写拼接生成(如 vaswani2017attention),冲突时追加字母后缀(…a / …b)。目录名即论文唯一标识。

2. 核心文件约定

AGENTS.md(L0,事实来源)

Vault 内的 Agent 行为规范,至少包含:

  • 读取协议:按 PAPERS.md → NOTES.md → highlights.md → PAPER.md → source/ 逐层下钻,仅在需要时进入下一层。
  • 笔记结构规范(三段论)。
  • 引用路径要求:回答必须列出读取过的本地文件路径。
  • 生成内容的双链要求:保留 [[...]] 格式。
  • 写入规范:先写临时文件,用户确认后落盘;不得覆盖用户手写笔记。

PAPERS.md(L1,派生索引)

全局论文索引,是 Agent 的第一层路由表。由各篇 metadata.json 汇总生成,可随时重建,不是手工维护的 master。

推荐字段:

| ID | Title | Authors | Year | Path | Tags | Summary |
| --- | --- | --- | --- | --- | --- | --- |

metadata.json(每篇元数据的事实来源)

位于 papers/<id>/metadata.json,是这篇论文结构化元数据的唯一事实来源。与 NOTES.md 分离,避免机器字段与人的 prose 相互踩踏:用户编辑笔记不会碰坏元数据,importer 也能确定性地写入。PAPERS.mdlibrary.bibcache.sqlite 都是它的派生投影。字段见下文“3.3 论文元数据”。

NOTES.md(L2,事实来源)

单篇论文的结构化压缩笔记,纯粹是人的知识/综合产物,不掺元数据 frontmatter。

默认结构:

# 解决了什么问题

# 方法是什么

# 效果怎么样

highlights.md(L2.5,事实来源)

单篇论文的标注层,与 NOTES.md 分开存放:笔记是"熟的"综合知识,标注是"生的"原始证据(锚定原文位置的引文 + 想法),数量多、带定位。分层后 Agent 想看摘要时不会被一堆坐标噪音污染,而"用户划了哪几句"本身又是高价值的注意力信号。

  • 引文 + 想法留在 highlights.md,是事实来源(是人的知识),保持便携 Markdown。
  • 页码 / bbox 等渲染坐标是纯 UI 数据,不是知识,缓存于 .motif/,按标注 id 关联;丢失后可用引文全文检索重新锚定。
  • 用 Obsidian 块引用 ^id,让 NOTES.md 能精确引用某条标注:[[papers/1706.03762/highlights#^h12]]

格式示例:

### ^h12 · p.3 §3.2
> "The Transformer ... dispensing with recurrence entirely."
想法:核心卖点是彻底抛弃 recurrence。→ [[Self-Attention]]

PAPER.md(L3,派生)

位于 papers/<id>/PAPER.md(注意:在论文目录根部,不在 source/ 内,因为它是派生文件而 source/ 是原始归档)。面向 Agent 阅读的统一可读正文,保留章节、公式、表格、引用等结构,降低 PDF 排版噪音。

source/异构的原始归档(tex / pdf / html),而 PAPER.md 给 Agent 提供一个同构的可读正文出口:

来源情况 source/ 放什么 PAPER.md
arxiv 有 LaTeX 解包的 .tex 工程 + PDF 按需生成(Agent 可直接读 .tex)
非 arxiv 原生 PDF/HTML 原始 PDF/HTML 解析成 Markdown(必生成)
扫描件 原始 PDF + ocr/ 中间产物 OCR → Markdown(必生成,并标注质量)

正文来源与质量记录在 metadata.jsonbody_source / body_quality 字段,Agent 据此判断可信度。PAPER.md 可删可重建,source/ 中的原始文件才是归档事实来源。

3. 数据类型

3.1 Vault

interface VaultInfo {
  id: string;
  name: string;
  root_path: string;
}

3.2 文件树

interface FileNode {
  path: string;
  name: string;
  type: 'file' | 'directory';
  children?: FileNode[];
}

3.3 论文元数据 (metadata.json)

papers/<id>/metadata.json 的落盘结构,是单篇论文元数据的事实来源。

interface PaperMetadata {
  id: string;                 // arXiv ID 或 citekey,等于目录名
  type: 'arxiv' | 'pdf' | 'html' | 'doi' | 'other';
  title: string;
  authors: string[];
  year: number;
  abstract?: string;
  tags: string[];

  // 来源链接(UI 阅读器 PDF/HTML 模式以此为准)
  arxiv_id?: string;
  doi?: string;
  /**
   * 远程 PDF URL(UI 仅流式预览,不落盘)。
   * 推荐 arXiv: `https://arxiv.org/pdf/{arxiv_id}`;缺省且有 arxiv_id 时自动推导。
   */
  pdf_url?: string;
  /**
   * 远程 HTML URL(iframe 预览,不落盘)。
   * 推荐 arXiv: `https://arxiv.org/html/{arxiv_id}`;缺省且有 arxiv_id 时自动推导。
   */
  html_url?: string;
  /** 摘要页等,如 `https://arxiv.org/abs/{arxiv_id}` */
  source_url?: string;

  // 正文来源与质量:渐进式披露与降级策略的决策依据
  body_source?: 'latex' | 'html' | 'pdf' | 'ocr';
  body_quality?: 'high' | 'medium' | 'low';

  // 引用
  bibtex_key?: string;
  citation_count?: number;

  // 状态与时间
  status: 'pending' | 'importing' | 'completed' | 'failed';
  added_at: string;   // ISO 8601
  updated_at: string; // ISO 8601
}

元数据来源:

  • arxiv:由 arXiv API 提供权威字段(标题 / 作者 / 年份 / 摘要 / arxiv_id)。
  • pdf:先从 PDF 提取 DOI / arXiv ID,命中则查询 Crossref / arXiv 获取权威字段;未命中或失败时由 Agent 从正文抽取候选,最终一律经用户在入库前确认面板校对后写入。
  • type='pdf'body_sourcepdf(文本层)或 ocr(扫描件),body_quality 由解析后端与结果决定:MinerU → high,liteparse 文本 → medium,OCR → low。

3.4 论文运行时对象 (Paper)

Host 返回给前端的运行时对象 = PaperMetadata 反序列化后 + 解析出的 Vault 相对路径。始终存在的路径便于前端直接打开。

interface Paper extends PaperMetadata {
  vault_path: string;        // papers/<id>/                    始终存在
  metadata_path: string;     // papers/<id>/metadata.json       始终存在
  notes_path: string;        // papers/<id>/NOTES.md            始终存在
  highlights_path: string;   // papers/<id>/highlights.md       始终存在(可为空文件)
  source_dir: string;        // papers/<id>/source/             始终存在
  paper_md_path?: string;    // papers/<id>/PAPER.md            派生,可选
  pdf_path?: string;         // papers/<id>/source/original.pdf 未下载时 undefined
  assets_dir?: string;       // papers/<id>/assets/
}

3.5 标注 (Highlight)

highlights.md 中每条标注的逻辑结构。quote / comment / links 来自 Markdown(事实来源);page / bbox 是缓存于 .motif/ 的渲染坐标,可由 quote 全文检索重建。

interface Highlight {
  id: string;          // 块引用 id,对应 highlights.md 中的 ^id(如 h12)
  paper_id: string;
  quote: string;       // 引文原文(事实来源)
  comment?: string;    // 想法 / 评论
  links: string[];     // 双链目标,如 ['Self-Attention']

  // 以下为渲染定位,缓存于 .motif,可重建
  page?: number;
  bbox?: [number, number, number, number];
}

4. 事实来源与缓存规则

.motif/cache.sqlite 存储元数据、全文检索、双链图与标注坐标的派生副本,用于文件系统做不快的查询(按作者/年份/标签过滤排序、全文检索、反链聚合、大规模)。它不是第二个事实来源,须遵守三条纪律(双链边模型与重建纪律详见 docs/backend/wikilinks.md):

  1. 可整删重建:删除 cache.sqlite 后,应用重扫 Vault(metadata.json + Markdown + 双链)即可完全恢复。
  2. 写入 file-first:先写 metadata.json / Markdown,再更新 SQLite 与 PAPERS.md;notify 监听外部编辑(如在 Obsidian 中修改)自动增量重建。
  3. 冲突时文件赢:SQLite 与文件不一致时以文件为准并触发重索引;Agent 可查 SQLite 加速路由,但最终引用与展示必须落回本地文件路径。

5. Agent 运行时类型(应用配置,非 Vault 文件)

以下类型由 Host 配置与会话层持有, 写入 Vault 目录;模型密钥不在此模型中出现。

/** 用户本机 ACP Agent 注册项(BYOA) */
interface AgentDescriptor {
  id: string;
  name: string;
  template: 'opencode' | 'gemini' | 'claude-acp' | 'codex-acp' | 'qodercli' | 'custom';
  command: string;
  args: string[];
  env?: Record<string, string>;
  available: boolean; // PATH / 绝对路径探测结果
  last_error?: string;
}

interface AgentSession {
  id: string;
  agent_id: string;
  vault_path: string;
  workflow: 'summary' | 'qa' | 'related_work' | 'free';
  created_at: string;
  status: 'idle' | 'running' | 'awaiting_permission' | 'closed' | 'failed';
}

interface AgentResult {
  session_id: string;
  message_id: string;
  content: string;
  sources: string[]; // Vault 相对路径
  draft_path?: string; // 待用户确认的临时草稿
}

6. 相关文档

  • docs/development/prd.md:产品需求与验收标准(§5 文件结构)。
  • docs/development/technical-plan.md:存储分层与入库/Agent 数据流(§4.5、§5);ACP Client + BYOA。
  • docs/backend/api.md:Host 命令与数据模型引用。