| 术语 | 定义 |
|------|------|
| Area | JD 顶级分类(00-99) |
| Category | Area 内分类(000-999) |
| ID | Category 内序号(0001-9999) |
| 短 ID | Git 风格的简化 ID(如 10.1.15 → `10.001.0015`) |
| Namespace | 资源逻辑分组,对应目录 |
| Frontmatter | Markdown 文件头部 YAML 元数据 |
| GTD | Getting Things Done 方法论 |
| CORE | Capture, Organize, Review, Execute |
| Sidecar Index | SQLite 索引文件(`.synapse/index.db`) |
| Fuzzy Finder | 模糊搜索功能,支持标题匹配 |
| Command Palette | 命令面板,类似 VS Code Ctrl+P |
| Refile | 将资源从 Inbox 移动到目标位置(Emacs Org-mode 术语) |
| Link Refactoring | Refile 时自动更新所有引用该资源的链接 |
| Transaction Rollback | 事务回滚,利用 Git 暂存区实现原子性操作 |
| Hybrid Search | 混合搜索,结合全文(Ripgrep)和元数据(SQLite) |
| Application-Layer Tokenization | 应用层分词,写入/搜索前用 jieba 预处理 |
| Stopword Filtering | 停用词过滤,去除标点和无意义字符 |
| Session Persistence | 会话持久化,记录用户上下文状态 |
| Suspension Context | 暂停上下文,Git 操作期间暂停 watchdog 避免自激 |
| watchdog | Python 文件系统监听库 |
| Textual Workers | Textual 框架的异步任务机制(@work 装饰器) |

---

## 附录 B:参考资源

- [Johnny.Decimal](https://johnnydecimal.com/)
- [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/)
- [k9s](https://k9scli.io/)
- [GTD](https://gettingthingsdone.com/)
- [GraphRAG](https://github.com/microsoft/graphrag)
- [LiteLLM](https://github.com/BerriAI/litellm)
- [Textual](https://textual.textualize.io/) - Python TUI 框架
- [Ripgrep](https://github.com/BurntSushi/ripgrep) - 高性能搜索工具
- [watchdog](https://github.com/gorakhargosh/watchdog) - Python 文件系统监听
- [jieba](https://github.com/fxsjy/jieba) - 中文分词(FTS5 增强) A system to organise your life
| 操作 | 数据源 |
|------|--------|
| syn list, syn search (Frontmatter) | SQLite |
| syn search --content (全文) | Ripgrep → 再查 SQLite 补充元数据 |
| syn show, syn edit | 实际 Markdown 文件 |

### 13.5 混合搜索(Hybrid Search)

**场景**:用户想搜 "Project X"(全文)且 "Status=Todo"(元数据)。

**实现逻辑**:

1. Ripgrep 搜索全文 → 返回匹配文件路径列表
2. 用路径列表查询 SQLite → 过滤 Status=Todo
3. 返回最终结果(带完整元数据)


**命令示例**:

# 混合搜索:全文包含 "kubernetes" 且状态为 todo
syn search "kubernetes" --status todo

# 混合搜索:全文包含 "设计" 且标签包含 urgent
syn search "设计" --tag urgent

# 纯元数据搜索(due-before 基于 due 字段)
syn search --type task --priority high --due-before 2024-01-15


**时间筛选参数说明**:

| 参数 | 对应字段 | 说明 |
|------|----------|------|
| --due-before | due | 截止日期早于指定日期的任务 |
| --due-after | due | 截止日期晚于指定日期的任务 |
| --created-before | created | 创建时间早于指定日期 |
| --created-after | created | 创建时间晚于指定日期 |
| --modified-before | modified | 修改时间早于指定日期 |
| --modified-after | modified | 修改时间晚于指定日期 |

**示例**:

# 查找本周内到期的任务
syn search --type task --due-after monday --due-before sunday

# 查找过去 7 天修改过的笔记
syn search --type note --modified-after "7 days ago"


### 13.6 批量操作与异步机制

针对 10,000+ 文件规模的操作优化:

**SQLite 事务批量写入**:

# ❌ 错误:单条插入 1000 次 ≈ 5秒
for item in items:
    cursor.execute("INSERT INTO resources ...", item)
    conn.commit()

# ✅ 正确:事务批量插入 1000 次 ≈ 0.05秒
cursor.execute("BEGIN TRANSACTION")
for item in items:
    cursor.execute("INSERT INTO resources ...", item)
cursor.execute("COMMIT")


**Textual 异步 Workers**:

from textual import work

class SynapseApp(App):
    @work(exclusive=True)
    async def action_batch_update_tags(self, ids: list, add_tag: str):
        """耗时操作放入后台线程,不阻塞 UI"""
        self.notify("正在批量更新标签...")
        
        # 执行文件 IO 和 DB 更新
        await processor.batch_add_tag(ids, add_tag)
        
        self.notify(f"✓ 已更新 {len(ids)} 个资源")
        self.refresh_list()


**进度反馈**:
- 操作耗时 > 500ms 时,TUI 底部状态栏显示 Spinner
- 批量操作(如 Refile 多个文件)显示进度条

---

## 14. 成功标准

1. 快速捕获到 Inbox 并高效处理
2. Johnny.Decimal 编号清晰组织(支持短 ID 和模糊匹配)
3. Git 完整审计追踪(Conventional Commits)
4. k9s 风格 TUI(列表/日历/时间线/图谱视图 + 命令面板)
5. CORE 工作流:捕获 → 组织 → 回顾 → 执行
6. 本地 Hook 优先 + GitHub Actions 远程通知
7. 仅 Markdown,仓库精简(`syn media` 处理附件)
8. Namespace 支持项目级配置
9. 10,000 资源列表 < 500ms(SQLite 索引 + 批量事务)
10. 多端同步无痛(自动 rebase + 智能字段合并)
11. Refile 时链接自动更新,永不断裂
12. 中文全文搜索精准(jieba 应用层分词)
13. 会话状态持久化,随时恢复上下文

---

## 15. 边缘情况处理

| 场景 | 处理方案 |
|------|----------|
| title 修改 | 文件名同步更新(包含 title),但链接仅使用 ID,不受影响 |
| Refile 链接断裂 | 自动全局搜索替换旧 ID → 新 ID(见 5.5) |
| 脏工作区 Refile | 拒绝操作,TUI 提供快捷 Commit/Stash 选项(见 5.5) |
| Refile 中途失败 | 前置检查确保工作区干净后,失败可安全回滚(见 5.5) |
| 短 ID 歧义 | CLI 报错列出候选项,TUI 弹窗二选一(见 3.2) |
| 并发编辑 | watchdog 实时监听 + Hash 检测(见 7.6) |
| watchdog 自激 | Git 操作期间暂停监听(SuspensionContext),避免误报(见 7.6) |
| 多端同步 | 启动 `pull --rebase`,退出 `push`,智能字段合并(见 7.4-7.5) |
| 索引损坏 | syn index rebuild 重建 |
| 大文件 | .gitignore 排除,`syn media` 上传到外部存储 |
| 耗时操作 | Textual @work 异步执行,底部显示进度(见 13.6) |
| 中文搜索噪音 | 停用词过滤 + 标点清理,减小索引体积(见 13.2) |

---

## 16. 范围外(初始版本)

**优先级低,预留设计的功能**:
- **§8 媒体管理**:初始版本仅支持外部链接,`syn media` 命令预留
- **§10 知识图谱**:初始版本不实现,预留 syn graph 接口
- **§11 邮件集成**:初始版本仅支持手动创建,API 集成预留

**明确排除的功能**:
- 完整 LLM GraphRAG(仅预留接口)
- 实时协作(多人同时编辑)
- Web/移动端
- 邮件 API 自动集成
- 自定义 FTS5 分词器(使用应用层分词替代)

---

## 附录 A:术语表
| 协议 | 认证方式 | 说明 |
|------|----------|------|
| Gmail API | OAuth2 | 需要 Google Cloud 项目 |
| IMAP | SSL/TLS | 传统邮箱协议 |
| Microsoft Graph | OAuth2 | Outlook/Office365 |

**凭证配置(预留)**:

# config.yaml
email:
  provider: gmail  # gmail | imap | microsoft
  gmail:
    client_id_env: GMAIL_CLIENT_ID
    client_secret_env: GMAIL_CLIENT_SECRET
  imap:
    server: imap.example.com
    port: 993
    username_env: IMAP_USERNAME
    password_env: IMAP_PASSWORD


---

## 12. 配置

### 12.1 配置文件示例


# ~/.config/synapse/config.yaml

workspace: ~/synapse-data
default_namespace: personal

namespaces:
  - personal      # 1
  - work-project  # 2
  - study         # 3

editor:
  windows: { default: notepad, options: [notepad, typora, code] }
  linux: { default: vim, options: [vim, typora] }
  macos: { default: vim, options: [vim, typora] }

git:
  auto_commit: true
  commit_style: conventional  # conventional | simple | llm
  llm:
    enabled: false
    provider: litellm
    model: gpt-4o-mini
    api_base: https://api.openai.com/v1
    api_key_env: OPENAI_API_KEY
  remotes:
    - name: origin
      url: [email protected]:user/synapse-data.git
      auto_push: true

johnny_decimal:
  areas:
    00: "Inbox"
    10: "个人管理"
    20: "工作项目"
    30: "学习成长"

review:
  daily_time: "08:00"
  weekly_day: friday
  weekly_time: "18:00"


---

## 13. 性能与索引机制

### 13.1 性能目标

| 操作 | 目标 | 实现方式 |
|------|------|----------|
| 列表 10,000 资源 | < 500ms | SQLite 索引 |
| 全文搜索 | < 1s | Ripgrep |
| 创建/更新 | < 300ms | 增量索引更新 |

### 13.2 SQLite Sidecar Index

**问题**:10,000 个 Markdown 文件逐个解析 Frontmatter,IO 开销巨大。

**方案**:`.synapse/index.db` 缓存所有元数据。


-- 主索引表
CREATE TABLE resources (
    id TEXT PRIMARY KEY,        -- "10.001.0015"
    namespace TEXT,
    area TEXT,                  -- "10"
    category TEXT,              -- "001"
    type TEXT,
    title TEXT,
    status TEXT,
    priority TEXT,
    due DATE,
    created DATETIME,
    modified DATETIME,
    tags TEXT,                  -- JSON array
    content_hash TEXT,          -- 用于变更检测
    file_path TEXT
);

-- 全文搜索 (FTS5) - 存储预分词后的内容
CREATE VIRTUAL TABLE resources_fts USING fts5(
    id, title_tokens, tags_tokens, content_tokens,
    tokenize = 'simple'  -- 使用默认分词器,输入已分词字符串
);


**中文搜索:应用层分词方案**(推荐)

SQLite 原生不支持加载 Python jieba 作为分词器。最佳实践是**应用层分词**:


import jieba
import re

# 停用词表(标点、空格、无意义字符)
STOPWORDS = set(['的', '了', '是', '在', '我', '有', '和', '就', 
                  '不', '人', '都', '一', '一个', '上', '也', '很',
                  '到', '说', '要', '去', '你', '会', '着', '没有'])

class SearchEngine:
    def _tokenize(self, text: str) -> str:
        """分词 + 停用词过滤 + 标点清理"""
        # 1. 移除标点符号和特殊字符
        text = re.sub(r'[^\w\s\u4e00-\u9fff]', ' ', text)
        
        # 2. jieba 分词
        tokens = jieba.cut_for_search(text)
        
        # 3. 停用词过滤 + 去除空白
        filtered = [t.strip() for t in tokens 
                    if t.strip() and t not in STOPWORDS]
        
        return " ".join(filtered)
    
    def update_index(self, id: str, title: str, content: str):
        """写入索引前,用 jieba 预处理 + 停用词过滤"""
        title_tokens = self._tokenize(title)
        content_tokens = self._tokenize(content)
        
        cursor.execute("""
            INSERT INTO resources_fts (id, title_tokens, content_tokens) 
            VALUES (?, ?, ?)
        """, (id, title_tokens, content_tokens))
    
    def search(self, query: str):
        """搜索时,同样分词处理"""
        query_tokens = " AND ".join(self._tokenize(query).split())
        
        cursor.execute("""
            SELECT id FROM resources_fts 
            WHERE content_tokens MATCH ?
        """, (query_tokens,))


**停用词过滤的好处**:
- 减小 index.db 体积(去除 30%+ 无意义词)
- 提高搜索精度(避免"的"、"了"等高频词干扰)
- 加快搜索速度

**工作流程**:
- **写入**:"完成了需求设计!" → 清理标点 → jieba → 过滤停用词 → "完成 需求 设计" → 存入 FTS
- **搜索**:用户搜 "需求" → 同样处理 → "需求" → MATCH 成功

### 13.3 索引更新策略

| 触发时机 | 动作 |
|----------|------|
| syn 启动 | 检测 Git 变更,增量更新索引 |
| CRUD 操作 | 实时更新索引 |
| Git hook | post-merge, post-checkout 触发重建 |
| 手动 | syn index rebuild 强制重建 |

### 13.4 查询路由
# config.yaml
media:
  provider: s3  # s3 | github | local
  
  # 凭证配置
  credentials:
    # 方式 1:环境变量(推荐)
    aws_access_key_env: AWS_ACCESS_KEY_ID
    aws_secret_key_env: AWS_SECRET_ACCESS_KEY
    # 方式 2:配置文件(不推荐,敏感信息)
    # aws_access_key: "AKIA..."
    # aws_secret_key: "..."
  
  s3:
    bucket: your-bucket
    region: us-east-1
    prefix: synapse/           # 存储前缀
    # 最终路径:s3://your-bucket/synapse/{resource_id}/{filename}
  
  github:
    repo: user/media
    branch: main
    token_env: GITHUB_TOKEN    # GitHub Personal Access Token
  
  local:
    path: ~/synapse-media      # 本地存储路径(.gitignore 隔离)
    base_url: file://~/synapse-media


### 8.5 TUI 集成(预留)

在编辑模式下:
- Ctrl+V`:检测剪贴板图片 → 自动调用 `syn media paste → 插入链接

---

## 9. 自动化工作流

### 9.1 本地自动化(优先)

对于生成类任务,优先使用**本地 Hook**,无需等待网络:


# 本地执行,立即生成并打开
syn review daily      # → 生成今日回顾,直接打开编辑
syn review weekly     # → 生成周报,直接打开编辑
syn review monthly    # → 生成月报,直接打开编辑
syn standup           # → 生成站会报告

# LLM 辅助生成(可选)
syn review daily --llm     # → 使用 LLM 生成摘要
syn review weekly --llm    # → 使用 LLM 生成周报
syn review monthly --llm   # → 使用 LLM 生成月报


#### 9.1.1 Review 资源类型与存储

Review 生成的资源是 note 类型,存储在配置的 review Area/Category 下:


personal/
└── 90/                    # Area 90: 回顾与复盘
    ├── 001/               # Category 001: 每日回顾
    │   ├── 90.001.0001-note-2024-01-15-daily-review.md
    │   └── 90.001.0002-note-2024-01-16-daily-review.md
    ├── 002/               # Category 002: 每周回顾
    │   └── 90.002.0001-note-2024-W03-weekly-review.md
    ├── 003/               # Category 003: 每月回顾
    │   └── 90.003.0001-note-2024-01-monthly-review.md
    └── 004/               # Category 004: 站会报告
        └── 90.004.0001-note-2024-01-15-standup.md


**Frontmatter 示例**:

---
jd_number: "90.001.0015"
type: note
namespace: personal
title: "2024-01-15 每日回顾"
tags: [review, daily]
created: 2024-01-15T18:00:00Z
modified: 2024-01-15T18:30:00Z
review_type: daily          # daily | weekly | monthly | standup
review_period: "2024-01-15"
llm_generated: false        # 是否使用 LLM 生成
---


**本地 Hook 配置**:

# config.yaml
hooks:
  review_daily:
    command: "python ~/.synapse/hooks/daily_review.py"
    auto_open: true   # 生成后自动打开编辑器
  review_weekly:
    command: "python ~/.synapse/hooks/weekly_review.py"
    auto_open: true
  review_monthly:
    command: "python ~/.synapse/hooks/monthly_review.py"
    auto_open: true
  standup:
    command: "python ~/.synapse/hooks/standup.py"
    auto_open: true

review:
  area: "90"              # Review 存储的 Area
  daily_category: "001"   # syn review daily
  weekly_category: "002"  # syn review weekly
  monthly_category: "003" # syn review monthly
  standup_category: "004" # syn standup
  llm:
    enabled: true
    prompt_template: "~/.synapse/prompts/review.md"


### 9.2 GitHub Actions 模板(远程)

用于**跨设备通知**和**长期归档**:

| 模板 | 触发 | 功能 |
|------|------|------|
| daily-standup | 工作日 9:00 | 昨日完成、今日计划、阻塞问题 |
| weekly-review | 周五 18:00 | 周报生成 |
| due-reminder | 每天 8:00 | 到期/逾期提醒 |
| inbox-reminder | 工作日 17:00 | Inbox 清理提醒 |
| monthly-retro | 每月 1 日 | 月度复盘归档 |
| graph-sync | push 触发 | 图谱同步 |

**设计理念**:
- **本地 Hook**:生成类(review, standup)→ 即时响应,无网络依赖
- **GitHub Actions**:通知类、归档类 → 跨设备、定时触发

---

## 10. 知识图谱(优先级低,预留设计)

> ⚠️ 本章节为预留设计,待后续迭代完善。初始版本不实现图谱功能。

### 10.1 预留接口


syn graph                    # 启动图谱视图(TUI)
syn graph export             # 导出图谱数据
syn graph sync               # 同步到 Nebula Graph(如配置)


### 10.2 数据模型(预留)

**节点类型**:Resource, Namespace, Tag, Person, TimePoint, Chunk

**边类型**:RELATED_TO, BELONGS_TO, CREATED_BY, ASSIGNED_TO, TAGGED_WITH, BLOCKED_BY 等

### 10.3 GraphRAG 集成(预留)

- 使用 Nebula Graph 存储知识图谱
- 支持 LLM 驱动的图谱查询和推理
- 自动发现资源间的隐含关系

---

## 11. 邮件集成(优先级低,预留设计)

> ⚠️ 本章节为预留设计,待后续迭代完善。初始版本仅支持手动创建邮件资源。

### 11.1 手动创建(初始版本支持)


syn new email --subject "项目进度汇报"


- 支持剪贴板粘贴邮件内容
- 自动解析标题和正文
- 发件人/收件人需手动填写

### 11.2 API 集成(预留)
**字段级合并策略配置**:

# config.yaml
git:
  merge_strategy:
    # 默认策略:取 modified 最新的一方 (Last Write Wins)
    default: "take_latest_modified"
    
    # 特殊字段策略
    fields:
      # 标签:取并集。A端加了[urgent], B端加了[work] -> [urgent, work]
      tags: "union"
      
      # 关联:取并集
      related: "union"
      
      # 状态:取最新修改方(或自定义优先级:done > in-progress > todo)
      status: "take_latest_modified"
      
      # 正文内容:标准 Git 文本合并
      content: "git_standard"


**冲突安全网**:
- 当 Merge Driver 无法自动解决(如正文同一行被修改):
- 生成 .conflict 备份文件
- TUI 启动时检测到冲突文件 → 进入"冲突解决向导"模式
- 用户可选择:保留本地 / 保留远程 / 手动合并

### 7.6 并发编辑检测

**场景**:TUI 打开时,用户用 VS Code 编辑同一文件。

**方案 1:Hash 检测**(基础)
1. syn edit 前:记录文件 hash
2. 外部编辑器关闭后:检查 hash
3. 如果 hash 变化且非当前进程修改:
- 提示冲突
- 显示 diff
- 用户选择:覆盖 / 合并 / 放弃

**方案 2:watchdog 实时监听**(增强)

使用 watchdog 库监听文件系统事件,TUI 运行时实时感知外部修改:


# 伪代码示意 - 注意线程安全!
from watchdog.observers import Observer
from watchdog.events import FileSystemEventHandler

class SynapseFileWatcher(FileSystemEventHandler):
    def __init__(self, app):
        self.app = app  # Textual App 实例
    
    def on_modified(self, event):
        if event.src_path == self.app.current_open_file:
            # ⚠️ 关键:watchdog 在独立线程,必须用 call_from_thread
            self.app.call_from_thread(
                self.app.show_file_modified_warning,
                event.src_path
            )


**线程安全注意**:
- watchdog 运行在独立线程
- Textual UI 更新必须在主线程
- 必须使用 app.call_from_thread() 跨线程调用,否则 TUI 会崩溃

**自激抑制(Suspension Context)**:

⚠️ **问题**:当 Synapse 执行 git pullgit checkout 时,文件系统变化会触发 watchdog,错误地向用户报告"外部修改警告"。


class FileWatcher:
    def __init__(self):
        self._suspended = False
    
    def pause(self):
        """暂停监听(Git 操作前调用)"""
        self._suspended = True
    
    def resume(self):
        """恢复监听(Git 操作后调用)"""
        self._suspended = False
    
    def on_modified(self, event):
        if self._suspended:
            return  # 忽略自身触发的变更
        # ... 正常处理外部修改

class SynapseApp:
    def perform_git_operation(self):
        self.file_watcher.pause()  # 暂停监听
        try:
            git.pull()
            self.index.refresh()   # 主动刷新索引
        finally:
            self.file_watcher.resume()  # 恢复监听


**行为**:
- TUI 打开时,自动启动 watchdog **仅监听当前 Namespace 目录**(性能优化)
- 切换 Namespace 时,watchdog 同步切换监听目标
- 检测到当前查看/编辑的文件被外部修改 → 实时弹出警告
- 用户可选择:重新加载 / 忽略 / 查看 diff
- **Git 操作期间自动暂停监听**,避免误报


class SynapseFileWatcher:
    def __init__(self, app):
        self.app = app
        self.observer = Observer()
        self._current_path = None
        self._started = False
    
    def start(self):
        """启动观察者线程(TUI 启动时调用)"""
        if not self._started:
            self.observer.start()
            self._started = True
    
    def stop(self):
        """停止观察者线程(TUI 退出时调用)"""
        if self._started:
            self.observer.stop()
            self.observer.join()  # 等待线程结束
            self._started = False
    
    def watch_namespace(self, namespace_path: str):
        """切换监听目标到指定 Namespace"""
        # 1. 取消之前的监听
        if self._current_path:
            self.observer.unschedule_all()
        
        # 2. 调度新的监听目标
        self._current_path = namespace_path
        self.observer.schedule(
            self, 
            namespace_path, 
            recursive=True  # 监听 Namespace 下所有子目录
        )
        
        # 3. 确保观察者已启动
        self.start()


---

## 8. 媒体管理(优先级低,预留设计)

> ⚠️ 本章节为预留设计,待后续迭代完善。初始版本仅支持外部链接引用媒体。

### 8.1 设计原则

- **仓库纯净**:禁止二进制文件直接入库
- **体验友好**:提供便捷的截图/贴图工作流

### 8.2 syn media 命令


# 从剪贴板上传图片
syn media paste
# → 自动上传到配置的存储 → 返回 Markdown 链接

# 上传指定文件
syn media upload /path/to/image.png

# 输出示例
![image](https://your-bucket.s3.amazonaws.com/synapse/10.001.0015/abc123.png)


### 8.3 媒体存储目录结构

**建议使用资源 ID 作为目录名**,便于关联和管理:


媒体存储根目录/
├── 10.001.0015/                # 资源 ID 为目录名
│   ├── screenshot-001.png
│   └── diagram.svg
├── 10.002.0003/
│   └── meeting-photo.jpg
└── _orphan/                    # 未关联资源的媒体(待清理)
    └── temp-upload.png


### 8.4 媒体存储配置
**TUI 交互**:

┌─────────────────────────────────────────────────────────┐
│ ⚠️  Refile 需要干净的工作区                              │
├─────────────────────────────────────────────────────────┤
│ 检测到以下未提交的修改:                                 │
│   M  10/001/10.001.0005-note-学习笔记.md               │
│   M  20/001/20.001.0003-task-开发任务.md               │
│                                                         │
│ 请先提交或暂存这些修改:                                 │
│   [C] 快速提交 (commit -m 'WIP')                        │
│   [S] 暂存 (git stash)                                  │
│   [Q] 取消 Refile                                       │
└─────────────────────────────────────────────────────────┘


**行为**:
- Refile 前**必须检查**工作区状态
- 脏工作区时拒绝操作,提示用户先提交/暂存
- TUI 提供快捷操作:一键 Commit WIP 或 Stash
- 绝不丢失用户的其他修改

**更新的引用类型**:

| 引用形式 | 示例 | 搜索方式 | 更新方式 |
|----------|------|----------|----------|
| Wiki-style 链接 | [[10.001.0001]] | Ripgrep | 字符串替换 |
| Frontmatter related | related: ["10.001.0001"] | SQLite 索引 | 解析 YAML,更新数组 |

设计原则:精确引用更新,避免误改

⚠️ **不进行盲目的全文替换**:只更新明确的引用语法,不替换正文中的裸 ID 字符串。


---
related: ["10.001.0001"]  # ← YAML 数组,精确更新 ✓
---

# 设计说明

参考 [[10.001.0001]] 的架构思路...     # ← Wiki-style 链接,更新 ✓
任务 10.001.0001 定义了核心接口...      # ← 裸 ID,不更新 ✗(可能是历史描述)


为什么不替换裸 ID?
- 裸 ID 可能出现在非引用上下文:`"历史记录显示 10.001.0001 已于 2023 年完成"`
- 格式说明文本:`"ID 格式为 10.001.0001,其中 10 表示 Area..."`
- 盲目替换会导致语义错误和数据损坏
- 用户若需更新裸 ID,可通过 --dry-run 预览后手动处理

为什么 related 字段仍需 SQLite?

related 是 YAML 数组格式,直接全文替换可能破坏 YAML 结构:


# 原始
related: ["10.001.0001", "10.002.0003"]

# Ripgrep 替换后可能出现的问题(如果替换逻辑不够精确)
related: ["20.001.0005", "10.002.0003"]  # ✓ 正确
related: ["20.001.0005", "20.001.0005"]  # ✗ 如果 10.002.0003 也被误改


**SQLite 方案确保**:
- 查询精确定位到 related 字段
- 解析 JSON 数组后,只替换匹配的元素
- 重新序列化,保证 YAML 格式正确

**Refile 预览模式**:

为避免意外修改,支持 --dry-run 预览变更:


syn inbox move 00.000.0001 --to work-project --dry-run

# 输出预览:
Will refile: 00.000.0001 → 20.001.0003
Source file:
  R  inbox/00.000.0001-unprocessed-xxx.md → work-project/20/001/20.001.0003-task-xxx.md
     - Frontmatter: jd_number: "00.000.0001" → "20.001.0003"

References to update (2 files):
  M  personal/10/001/10.001.0005-note-设计笔记.md
     - Line 5: related: ["00.000.0001"]["20.001.0003"]
     - Line 12: [[00.000.0001]][[20.001.0003]]
  M  personal/10/002/10.002.0001-task-开发任务.md
     - Line 15: [[00.000.0001]][[20.001.0003]]

Skipped (bare IDs not updated):
  -  personal/10/003/10.003.0001-note-历史记录.md
     - Line 8: "任务 00.000.0001 已于 2023 年完成"(裸 ID,需手动处理)

Proceed? [y/N]


> **注意**:预览中的 "Skipped" 部分提示用户哪些裸 ID 不会被自动更新,用户可根据需要手动处理。

---

## 6. 工作流与回顾

### 6.1 回顾类型

| 类型 | 触发 | 内容 |
|------|------|------|
| 每日 | syn review daily | 今日到期、今日会议、昨日未完成 |
| 每周 | syn review weekly | 本周完成、下周计划、项目进度 |
| Inbox | syn inbox process | 处理未分类项目 |

### 6.2 智能提醒

- 任务接近截止日期时高亮显示
- 资源长期未访问时在回顾中提示
- 阻塞任务链识别和提示

---

## 7. 存储与版本控制

### 7.1 Markdown 格式

**文件命名**:`{Area}.{Category}-{resource-type}-{title}.md`

**Frontmatter 示例**:

---
jd_number: "10.001.0015"
type: task
namespace: work-project
title: "完成项目设计"
status: in-progress
priority: high
due: 2024-01-15
created: 2024-01-01T10:00:00Z
modified: 2024-01-05T14:30:00Z
tags: [design, urgent]
related: ["10.001.0012", "10.002.0003"]
---


### 7.2 文件约束

- 仅允许 .md 文件
- 禁止 图片、视频、音频等二进制文件
- 必须 使用外部链接引用媒体资源
- .gitignore 配置忽略常见二进制格式

### 7.3 Git 集成

- 自动提交:所有 CRUD 操作
- commit 格式:Conventional Commits
- 支持多远程仓库
- 支持 SSH/HTTPS 认证

### 7.4 Git 同步策略

| 时机 | 动作 |
|------|------|
| syn 启动 | git pull --rebase |
| CRUD 操作 | git add + git commit |
| syn 退出 / syn sync | git push |

### 7.5 智能冲突合并

**问题**:多端同步时,`modified` 时间戳经常冲突。

**方案**:自定义 Git merge driver + 细粒度字段策略


# .gitattributes
*.md merge=synapse-frontmatter



# .git/config 或全局配置
[merge "synapse-frontmatter"]
    name = Synapse Frontmatter Merge
    driver = syn merge-driver %O %A %B %P
def refile_with_link_update(old_id, new_id, old_path, new_path, workspace):
    # 0. 前置检查:工作区必须干净
    if not is_working_tree_clean():
        raise RefileError(
            "Refile requires a clean working tree.\n"
            "Please commit or stash your changes first:\n"
            "  syn commit -m 'WIP'  或  git stash"
        )
    
    try:
        # 1. 移动源文件(暂存)
        git_mv(old_path, new_path)
        
        # 1b. 更新源文件的 Frontmatter(jd_number 字段)
        source_content = read_file(new_path)  # 文件已移动,从新路径读取
        source_fm, source_body = parse_frontmatter(source_content)
        source_fm['jd_number'] = new_id  # 更新 ID
        source_fm['modified'] = datetime.now().isoformat()  # 更新修改时间
        write_file(new_path, serialize_frontmatter(source_fm) + source_body)
        git_add(new_path)  # ⚠️ 必须暂存源文件的内容修改(git_mv 只暂存重命名)
        
        # 2. 查找并更新所有引用(其他文件)
        # ⚠️ 顺序重要:先 YAML 精确处理,再全文替换,避免竞态
        modified_files = set()
        files_with_related = set()  # 记录有 related 字段的文件
        
        # 2a. 先处理 Frontmatter 中的 related 字段(YAML 精确解析)
        # 必须先于全文替换,否则 old_id 已被替换,条件判断会失败
        related_refs = db_query(
            "SELECT file_path FROM resources WHERE related LIKE ?",
            (f'%"{old_id}"%',)  # 在 JSON 数组字符串中搜索
        )
        for (file_path,) in related_refs:
            content = read_file(file_path)
            frontmatter, body = parse_frontmatter(content)
            # 安全检查:确保 related 是非空列表
            related = frontmatter.get('related')
            if isinstance(related, list) and old_id in related:
                # 精确更新 YAML 数组
                frontmatter['related'] = [
                    new_id if rid == old_id else rid 
                    for rid in related
                ]
                # 只替换 wiki-style 链接(避免误改非引用上下文)
                body = body.replace(f"[[{old_id}]]", f"[[{new_id}]]")
                content = serialize_frontmatter(frontmatter) + body
                write_file(file_path, content)
                modified_files.add(file_path)
                files_with_related.add(file_path)
        
        # 2b. 搜索 wiki-style 链接:[[old_id]]
        # ⚠️ 只更新明确的引用语法,不盲目替换正文中的 ID 字符串
        wiki_refs = rg_search(f"\\[\\[{old_id}\\]\\]", workspace)
        for file_path in wiki_refs:
            if file_path in files_with_related:
                continue  # 已被 2a 处理,跳过
            content = read_file(file_path)
            content = content.replace(f"[[{old_id}]]", f"[[{new_id}]]")
            write_file(file_path, content)
            modified_files.add(file_path)
        
        # 3. 暂存所有修改
        for f in modified_files:
            git_add(f)
        
        # 4. 提交事务(+1 是源文件本身)
        git_commit(f"refactor: refile {old_id}{new_id}, update {len(modified_files) + 1} files")
        
    except Exception as e:
        # 4. 安全回滚(因为工作区干净,此时可以安全 reset)
        git_reset_hard()
        logger.error(f"Refile failed, rolled back: {e}")
        raise RefileError("操作失败,已回滚所有修改")

def is_working_tree_clean() -> bool:
    """检查工作区是否干净"""
    result = subprocess.run(
        ["git", "status", "--porcelain"],
        capture_output=True, text=True
    )
    return len(result.stdout.strip()) == 0


策略 B:智能回滚(复杂场景)

如果必须允许脏工作区 Refile,回滚时需精确还原:


def smart_rollback(modified_files: list, old_path: str, new_path: str):
    """精确回滚,不影响其他未提交修改"""
    # 1. Unstage 暂存的文件
    for f in modified_files:
        subprocess.run(["git", "reset", "HEAD", f])
    
    # 2. 还原被修改的文件内容
    for f in modified_files:
        subprocess.run(["git", "checkout", "--", f])
    
    # 3. 移回源文件
    if os.path.exists(new_path):
        subprocess.run(["git", "mv", new_path, old_path])
┌─────────────────────────────────────────────┐
│ Refile: 00.000.0001 - 快速记录              │
├─────────────────────────────────────────────┤
│ [1] personal                                │
│ [2] work-project                            │
│ [3] study                                   │
│ [q] Cancel                                  │
└─────────────────────────────────────────────┘
         ↓ 按 2
┌─────────────────────────────────────────────┐
│ Refile to: work-project                     │
├─────────────────────────────────────────────┤
│ [20] 项目管理                               │
│ [21] 开发                                   │
│ [22] 设计                                   │
│ [+] 新建 Area                               │
│ [q] Back                                    │
└─────────────────────────────────────────────┘
         ↓ 按 20
┌─────────────────────────────────────────────┐
│ Refile to: work-project/20                  │
├─────────────────────────────────────────────┤
│ [001] 任务                                  │
│ [002] 会议                                  │
│ [+] 新建 Category                           │
│ [q] Back                                    │
└─────────────────────────────────────────────┘
         ↓ 按 001
✓ Moved to: work-project/20/001/20.001.0003-task-快速记录.md


**关键设计**:
- 用户只需选"要去哪个区",**不需要手动输入 ID**
- 系统自动分配下一个可用 ID
- 支持快捷键直接跳转(数字键选择)
- 支持 / 搜索过滤 Namespace/Area/Category

### 5.5 引用自动更新(Link Refactoring)

**问题**:Refile 导致 ID 变化时,其他文件中的引用会断裂。


场景:
1. Inbox 中有 00.000.0001(想法A)
2. 笔记 10.002.0005 中写道:"参考 [[00.000.0001]]"
3. Refile 后,A 变为 20.001.0003
4. 结果:[[00.000.0001]] 成为 Dead Link


解决方案:自动重构所有引用

当执行 syn inbox move 且 ID 发生变化时,更新**两种引用形式**:


1. 搜索旧 ID 的引用(使用不同策略):
   - Wiki-style 链接:Ripgrep 搜索 [[00.000.0001]]
   - Frontmatter related:SQLite 索引查询(精确匹配,避免误改)
2. 自动替换为新 ID
3. 将所有修改一并加入 Git Commit


**实现逻辑(带安全回滚)**:

⚠️ **原子性保证**:Refile 涉及跨多文件修改,必须保证原子性。

⚠️ **安全警告**:绝对不能使用 `git reset --hard`!它会清空**所有**未提交的修改,包括与 Refile 无关的用户编辑。

策略 A:前置检查(推荐)

在开始 Refile 前,检查工作区是否干净:
## 5. Inbox 处理与最佳实践

### 5.1 Inbox 概念

- Inbox 是特殊 namespace(`_inbox`),Area 固定为 00
- 快速捕获的内容默认进入 Inbox,状态为 unprocessed
- Inbox 中的资源无需完整,支持最小化输入

### 5.2 处理操作

| 操作 | 说明 | 命令 |
|------|------|------|
| 归类 (Refile) | 移动到目标 namespace,分配 Area.Category | syn inbox move <id> --to <ns> --type <type> |
| 转换 | 转换资源类型(如 note → task) | syn inbox convert <id> --type task |
| 归档 | 原地标记 status=`archived`,不移动文件 | syn inbox archive <id> |
| 删除 | 永久删除(Git 可恢复) | syn inbox delete <id> |

**归档说明**:
- 归档操作**不移动文件**,仅修改 Frontmatter 中的 status 字段为 archived
- 归档后的资源在列表中默认隐藏,可通过 --include-archived 显示
- 如需移动到特定归档目录,应使用 syn inbox move --to archive-namespace
- **`unprocessed` 类型特殊处理**:由于 unprocessed 状态不可变,归档时自动转换为 note 类型并标记 status=archived

  syn inbox archive 00.000.0001
  # unprocessed → note (status=archived)
  # 文件重命名:00.000.0001-unprocessed-xxx.md → 00.000.0001-note-xxx.md
  


### 5.3 Inbox 处理最佳实践(GTD 风格)

处理流程决策树:


                    ┌─────────────┐
                    │  Inbox 项目  │
                    └──────┬──────┘
                           │
                    ┌──────▼──────┐
                    │ 这是什么?   │
                    │ 需要行动吗? │
                    └──────┬──────┘
                           │
              ┌────────────┼────────────┐
              │            │            │
         ┌────▼────┐  ┌────▼────┐  ┌────▼────┐
         │  不需要  │  │ 需要行动 │  │ 参考资料 │
         └────┬────┘  └────┬────┘  └────┬────┘
              │            │            │
         ┌────▼────┐       │       ┌────▼────┐
         │  删除   │       │       │ 归类为  │
         │  或归档  │       │       │  note   │
         └─────────┘       │       └─────────┘
                           │
              ┌────────────┼────────────┐
              │            │            │
         ┌────▼────┐  ┌────▼────┐  ┌────▼────┐
         │ < 2分钟  │  │ 委派他人 │  │ 需要规划 │
         │ 立即做! │  │         │  │         │
         └────┬────┘  └────┬────┘  └────┬────┘
              │            │            │
         ┌────▼────┐  ┌────▼────┐  ┌────▼────┐
         │ 完成后   │  │ 创建任务 │  │ 创建任务 │
         │ 归档/删除│  │ 分配给   │  │ 设置    │
         └─────────┘  │ 他人     │  │ 截止日期 │
                      └─────────┘  └─────────┘


最佳实践规则:

1. **2 分钟规则**:如果一件事 2 分钟内能完成,立即做,不要放入任务列表
2. **每日清空**:每天至少处理一次 Inbox,目标是 Inbox Zero
3. **快速决策**:每个项目只处理一次,立即决定其去向
4. **最小化输入**:捕获时只记录关键信息,处理时再补充细节

处理快捷流程:


# 批量处理 Inbox
syn inbox process

# 交互式处理(逐项显示,提供操作选项)
# [m]ove | [c]onvert | [a]rchive | [d]elete | [s]kip

# 快速转换为任务
syn inbox convert <id> --type task --due tomorrow --priority high

# 快速归类
syn inbox move <id> --to work-project --type meeting


TUI Inbox 处理快捷键:

| 快捷键 | 操作 | 说明 |
|--------|------|------|
| Enter | 查看详情 | 与全局快捷键一致 |
| e | 编辑后处理 | 打开编辑器,关闭后弹出处理选项 |
| m | 移动(Refile) | 层级菜单选择目标位置 |
| t | 快速转换为 task | 一键转换,保留原位置 |
| a | 归档 | 标记 status=archived |
| d | 删除 | 永久删除(Git 可恢复) |

### 5.4 Refile 交互优化(类似 Emacs Org-mode)

m (move) 是最高频操作,TUI 实现层级菜单式 Refile:
┌─────────────────────────────────────────────┐
@work-project/20 │ ← 范围搜索
├─────────────────────────────────────────────┤
│ 显示 work-project namespace Area 20 资源 │
└─────────────────────────────────────────────┘


### 4.5 上下文感知交互

| 视图 | `Enter` | `Space` | `e` |
|------|---------|---------|-----|
| Task 列表 | 查看详情 | 切换 status: `todo` → `in-progress` → `done` | 编辑 |
| Meeting 列表 | 查看详情 | 切换 status: `scheduled` ↔ `completed` | 编辑 |
| Email 列表 | 查看详情 | 切换 status: `unread` → `read` → `replied` | 编辑 |
| Note 列表 | 查看详情 | (无活跃状态循环,使用 `a` 键归档) | 编辑 |
| Calendar (日) | 资源详情 | 同上(根据资源类型) | 新建事项 |
| Inbox | 查看详情 | (unprocessed 状态不可切换) | 编辑后处理 |

**Space 状态循环**(仅限活跃状态):
- Task: `todo` → `in-progress` → `done` → `todo`
- Meeting: `scheduled` ↔ `completed`(二元切换)
- Email: `unread` → `read` → `replied` → `unread`
- Note: **无 Space 循环**(仅有 `active`/`archived` 两态,使用 `a` 键切换)

**终态处理**(`archived` / `cancelled`):
- `archived` 和 `cancelled` 是**终态**,不参与 Space 循环
- 当资源处于终态时,Space 键**无响应**(或显示提示"资源已归档/取消")
- 进入/退出终态:使用 `a` 键(归档/取消归档)或编辑 Frontmatter

| 状态 | 类型 | 进入方式 | 退出方式 |
|------|------|----------|----------|
| `archived` | task, email | `a` 键归档 | `a` 键取消归档(恢复到 `todo`/`unread`) |
| `archived` | note | `a` 键归档 | `a` 键取消归档(恢复到 `active`) |
| `cancelled` | meeting | 编辑 status 字段 | 编辑 status 字段 |

**`a` 键行为**:
- 活跃状态 → `archived`(归档)
- `archived` → 恢复到默认活跃状态(task→`todo`, email→`unread`, note→`active`)

### 4.6 编辑器配置

| 平台 | 默认 | 可选 |
|------|------|------|
| Windows | notepad | typora, vscode |
| Linux | vim | typora |
| macOS | vim | typora |

### 4.7 日历视图

**时间来源**:
- task: `due` / `start_date`
- meeting: `datetime` / `duration`

**条带说明**:每个条带代表一个资源,高度表示时间跨度,颜色表示类型/优先级。

**交互**:
- `↑↓` 在同一天的条带间切换选中
- `←→` 在日期间导航
- 选中条带时,底部显示资源摘要
- `Enter` 查看资源详情


Calendar - Week (2024-W03) [personal]
┌────────┬────────┬────────┬────────┬────────┬────────┬────────┐
│ Mon 15 │ Tue 16 │ Wed 17 │ Thu 18 │ Fri 19 │ Sat 20 │ Sun 21 │
├────────┼────────┼────────┼────────┼────────┼────────┼────────┤
│ ▓▓▓▓ │ ██ │ ▓▓▓▓▓▓ │ ████ │ ██ │ │ │
│ ████ │ │ ▓▓▓▓ │ ▓▓ │ │ │ │
├────────┴────────┴────────┴────────┴────────┴────────┴────────┤
[Wed 14:00] 项目评审会议 (meeting) - 14:00~16:00 │
│ Attendees: @alice, @bob │
└──────────────────────────────────────────────────────────────┘
[↑↓] Select [←→] Navigate [Enter] Details [w/m] Week/Month [q] Back

图例:▓▓ = meeting (紫色) ██ = task (蓝色) ░░ = note (灰色)


**跨天显示**:start_date 到 due 的任务、跨天会议,在每天都显示连续条带。

### 4.8 时间线视图

**时间来源**:Git commit 时间戳(资源的 `modified` 时间)

**条带说明**:每列代表一天,条带高度表示当天活动密度,每个条带对应一次 commit。

**交互**:
- `←→` 在时间轴上导航
- `↑↓` 在同一天的 commit 间切换
- 选中 commit 时,底部显示 commit 信息和涉及的资源
- `Enter` 跳转到该资源


Timeline - Last 7 Days [personal]
─────┬──────┬──────┬──────┬──────┬──────┬──────┬─────────
│ ▓▓ │ ████ │ ██ │ ████ │ ▓▓ │██████│ ██
│ ████ │ ▓▓ │ ████ │ ██ │ ████ │ ▓▓▓▓ │ ████
─────┴──────┴──────┴──────┴──────┴──────┴──────┴─────────
Mon Tue Wed Thu Fri Sat Sun

[Sun 14:32] feat(task): add new task for review
10.001.0015-task-完成项目设计.md
──────────────────────────────────────────────────────────
[↑↓] Select [←→] Navigate [Enter] Resource [d/w/m/y] Scale [q] Back

图例:▓▓ = meeting ██ = task ░░ = note (颜色同日历视图)


### 4.9 会话持久化(Session Persistence)

系统在 `.synapse/session.json`(不入 Git)中记录当前上下文状态:

```json
{
  "last_active_namespace": "work-project",
  "namespaces": {
    "personal": {
      "last_view": "calendar",
      "selected_date": "2024-01-20",
      "recent_files": ["10.001.0001", "10.002.0005"]
    },
    "work-project": {
      "last_view": "list",
      "filter_state": {"status": "todo", "tag": "urgent"},
      "recent_files": ["20.001.0003"]
    }
  },
  "global_history": [
    "syn show 10.001.0001",
    "syn search kubernetes"
  ]
}


**行为**:
- 启动 syn 时,自动恢复到上次退出的 Namespace 和 View
- Ctrl+Tab 可在最近打开的两个文件/视图间快速切换
- 每个 Namespace 独立记录筛选状态和最近文件

---
workspace/
├── .synapse/                              # 系统目录
│   ├── config.yaml                       # 全局配置
│   ├── index.db                          # SQLite 索引
│   └── hooks/                            # Git hooks
├── personal/                              # namespace
│   ├── .synapse.yaml                     # namespace 配置
│   ├── 10/                               # Area 10: 个人管理
│   │   ├── 001/                          # Category 001: 任务
│   │   │   ├── 10.001.0001-task-完成报告.md
│   │   │   └── 10.001.0002-task-学习计划.md
│   │   └── 002/                          # Category 002: 笔记
│   │       └── 10.002.0001-note-学习笔记.md
│   └── 11/                               # Area 11: 会议
│       └── 001/
│           └── 11.001.0001-meeting-周会.md
├── work-project/
│   ├── .synapse.yaml
│   └── 20/
│       └── 001/
│           ├── 20.001.0001-meeting-需求评审.md
│           └── 20.001.0002-task-开发任务.md
└── _inbox/                                # 特殊 namespace
    └── 00/
        └── 000/                          # Inbox 固定使用 00.000
            ├── 00.000.0001-unprocessed-快速记录.md
            └── 00.000.0002-unprocessed-临时想法.md


**设计说明**:
- **双层分桶**:Area → Category,避免单目录大量文件
- 每个 Category 目录最多 9999 个文件,人眼可管理
- 查询走 SQLite 索引,目录层级不影响性能
- Area/Category 子目录由系统自动创建

---

## 4. 用户界面

### 4.1 统一命令设计

CLI 和 TUI 使用统一的命令/操作命名:

| 操作 | CLI 命令 | TUI 快捷键 | 说明 |
|------|----------|------------|------|
| 新建 | syn new [type] | n | 创建新资源 |
| 列表 | syn list [type] | l | 列表视图 |
| 查看 | syn show <id> | Enter | 查看详情 |
| 编辑 | syn edit <id> | e | 编辑资源 |
| 删除 | syn delete <id> | d | 删除资源 |
| 搜索 | syn search <query> | / | 搜索过滤 |
| 日历 | syn calendar | c | 日历视图 |
| 时间线 | syn timeline | t | 时间线视图 |
| 图谱 | syn graph | g | 图谱视图 |

### 4.2 CLI 命令示例


# TUI 启动
syn

# 资源操作
syn new task --title "完成设计" --priority high --due 2024-01-15
syn new note --title "会议记录"
syn list task --status todo
syn show 10.001.0015        # 完整 ID
syn show 10.1.15            # 短 ID(自动补零)
syn show 15                 # 最短 ID(当前 Namespace 内唯一)
syn edit 10.001.0015
syn delete 10.001.0015
syn search "kubernetes" --type note --tag devops

# Inbox 操作
syn inbox list
syn inbox process
syn inbox move <id> --to <namespace> --type <type>

# 回顾操作
syn review daily
syn review weekly

# Namespace 操作
syn ns list
syn ns new my-project
syn ns switch work-project


### 4.3 TUI 快捷键

| 快捷键 | 功能 |
|--------|------|
| n | 新建资源 |
| e | 编辑(外部编辑器) |
| d | 删除 |
| a | 归档/取消归档(切换 archived 状态) |
| / | 搜索/Fuzzy Finder |
| Enter | 查看详情 |
| Space | 切换活跃状态(见 §4.5 状态循环) |
| r | 刷新 |
| q | 退出/返回 |
| ? | 帮助 |
| Ctrl+P | 命令面板 |
| l | 列表视图 |
| c | 日历视图 |
| t | 时间线视图 |
| g | 图谱视图 |
| 0 | 切换到 Inbox |
| 1-9 | 切换 Namespace |

### 4.4 命令面板 (Command Palette)

参考 VS Code / Obsidian 的交互逻辑,区分不同模式:

| 快捷键 | 模式 | 功能 |
|--------|------|------|
| Ctrl+P | 文件跳转 | Fuzzy Search Title/ID |
| Ctrl+Shift+P> | 命令执行 | 执行命令 |
| # | Tag 搜索 | 按标签过滤 |
| @ | 范围搜索 | 按 Namespace/Area 过滤 |

**交互示例**:
`
┌─────────────────────────────────────────────┐
│ 设计文档 │ ← 默认:文件跳转
├─────────────────────────────────────────────┤
│ 10.001.0015 - 完成项目设计 │
│ 10.002.0003 - 设计规范笔记 │
└─────────────────────────────────────────────┘

┌─────────────────────────────────────────────┐
│ > review │ ← 命令模式
├─────────────────────────────────────────────┤
│ syn review daily │
│ syn review weekly │
└─────────────────────────────────────────────┘

┌─────────────────────────────────────────────┐
#urgent │ ← Tag 搜索
├─────────────────────────────────────────────┤
│ 10.001.0015 - 紧急任务 [urgent, work] │
└─────────────────────────────────────────────┘
def allocate_next_id(namespace: str, area: str, category: str) -> str:
    """
    原子分配下一个可用 ID
    使用 IMMEDIATE 事务模式 + UNIQUE 约束确保并发安全
    """
    max_retries = 3
    for attempt in range(max_retries):
        try:
            # ⚠️ 关键:使用 IMMEDIATE 模式,事务开始时立即获取写锁
            # 这会阻塞其他写事务,避免空 Category 的竞态条件
            cursor.execute("BEGIN IMMEDIATE")
            
            # 1. 查询当前 Category 下最大 ID
            # 同时查询 id_reservations 表(可能有预占但未使用的 ID)
            cursor.execute("""
                SELECT MAX(id_num) FROM (
                    SELECT CAST(SUBSTR(jd_number, -4) AS INTEGER) as id_num
                    FROM resources 
                    WHERE namespace = ? AND jd_number LIKE ?
                    UNION ALL
                    SELECT CAST(SUBSTR(jd_number, -4) AS INTEGER) as id_num
                    FROM id_reservations
                    WHERE namespace = ? AND jd_number LIKE ?
                )
            """, (namespace, f"{area}.{category}.%", 
                  namespace, f"{area}.{category}.%"))
            
            row = cursor.fetchone()
            max_id = (row[0] if row else None) or 0
            next_id = max_id + 1
            
            if next_id > 9999:
                cursor.execute("ROLLBACK")
                raise OverflowError(f"Category {area}.{category} ID exhausted (max 9999)")
            
            # 2. 预占 ID(UNIQUE 约束是最后一道防线)
            new_jd = f"{area}.{category}.{next_id:04d}"
            cursor.execute("""
                INSERT INTO id_reservations (jd_number, namespace, reserved_at)
                VALUES (?, ?, datetime('now'))
            """, (new_jd, namespace))
            
            cursor.execute("COMMIT")
            return new_jd
            
        except sqlite3.IntegrityError:
            # UNIQUE 约束冲突,说明有并发分配,重试
            cursor.execute("ROLLBACK")
            if attempt == max_retries - 1:
                raise AllocationError(f"Failed to allocate ID after {max_retries} retries")
            continue  # 重试

def release_reservation(jd_number: str):
    """创建失败时释放预占的 ID"""
    cursor.execute("DELETE FROM id_reservations WHERE jd_number = ?", (jd_number,))


**id_reservations 表结构**:


CREATE TABLE id_reservations (
    jd_number TEXT PRIMARY KEY,   -- ⚠️ UNIQUE 约束,防止重复分配
    namespace TEXT NOT NULL,
    reserved_at TEXT NOT NULL,
    INDEX idx_namespace_jd (namespace, jd_number)
);

-- 定期清理过期预占(超过 1 小时未使用)
DELETE FROM id_reservations 
WHERE reserved_at < datetime('now', '-1 hour');


**并发控制机制**:

| 层级 | 机制 | 作用 |
|------|------|------|
| 1 | BEGIN IMMEDIATE | 事务级写锁,阻塞其他写事务 |
| 2 | 查询含 id_reservations | 感知其他进程的预占 |
| 3 | jd_number PRIMARY KEY | UNIQUE 约束,最后防线 |
| 4 | 重试机制 | 冲突时自动重试 |


**批量导入优化**:

```bash
# 批量模式:一次性预占 N 个 ID,减少锁竞争
syn import --batch /path/to/files/ --count 100

# 内部实现:单次事务分配 100 个连续 ID


**ID 耗尽处理**:
- 每个 Category 最多 9999 个资源
- 接近上限(>9000)时 CLI/TUI 显示警告
- 建议用户创建新 Category 或清理归档资源

### 3.2 短 ID 支持

为降低 CLI 输入摩擦,支持 Git 风格的短 ID:

| 输入方式 | 示例 | 匹配规则 |
|----------|------|----------|
| 完整 ID | 10.001.0015 | 精确匹配 |
| 短 ID | 10.1.15 | 自动补零 → 10.001.0015 |
| 最短 ID | 15 | 当前 Namespace 内唯一匹配 |
| 标题模糊 | 设计文档 | Fuzzy Finder 模糊搜索 |


syn show 15           # 当前 Namespace 内唯一,直接匹配
syn show 10.1.15      # 自动补零
syn show 设计         # Fuzzy Finder


**歧义处理**:当短 ID 匹配到多个结果时:
- **CLI**:报错并列出候选项,用户需输入更精确的 ID
- **TUI**:弹窗让用户选择


$ syn show 15
Error: Ambiguous ID '15'. Did you mean:
  [1] 10.001.0015 - 完成项目设计 (task)
  [2] 20.001.0015 - 会议纪要 (meeting)
Use full ID or choose: syn show 10.001.0015


**Shell 自动补全**:CLI 必须实现 Tab completion(支持 Bash/Zsh/Fish)。

### 3.3 目录结构(双层分桶)
# Synapse (syn) - 需求规格说明书

> Synapse - 神经突触,象征知识连接与传递。缩写 syn 简短易记,与知识管理高度契合。

---

## 1. 引言

### 1.1 系统愿景

Synapse 是一个基于 Python 3 构建的命令行/TUI 工作流管理工具,提供统一的知识与任务管理平台。

核心理念: "快速捕获、清晰组织、经常回顾、有效执行" (CORE)

### 1.2 核心原则

| 原则 | 描述 |
|------|------|
| 快速捕获 | 最小化录入摩擦,新内容默认进入 Inbox |
| 清晰组织 | Johnny.Decimal 变体 + k8s 风格资源管理 |
| 经常回顾 | GTD、周期性和智能回顾模式 |
| 有效执行 | 任务跟踪、状态管理、自动化工作流 |

### 1.3 目标用户

- **个人用户**:知识管理、任务追踪
- **团队用户**:协作、项目管理
- 支持个人使用,可扩展到团队协作

### 1.4 技术栈

| 组件 | 选型 | 说明 |
|------|------|------|
| TUI | Textual | CSS 布局,鼠标支持,适合 k9s 风格 |
| 存储 | Markdown | 仅 .md 文件,禁止二进制 |
| 索引 | SQLite + FTS5 | Sidecar Index,加速查询 |
| 搜索 | Ripgrep | 全文搜索,高性能 |
| 文件监听 | watchdog | 实时检测外部修改 |
| 版本控制 | Git | 本地 + 远程 |
| 图谱 | Nebula Graph | GraphRAG 就绪(优先级低) |
| 自动化 | 本地 Hook + GitHub Actions | 本地优先,远程通知 |

---

## 2. 资源管理

### 2.1 资源标识

**文件路径**(双层分桶:Area → Category):

{namespace}/{Area}/{Category}/{Area.Category.ID}-{resource-type}-{title}.md


**示例**:

personal/10/001/10.001.0001-task-完成报告.md
personal/10/002/10.002.0001-note-学习笔记.md
work-project/20/001/20.001.0001-meeting-需求评审.md
_inbox/00/000/00.000.0001-unprocessed-快速记录.md


| 组件 | 说明 |
|------|------|
| namespace | 用户自定义(个人/项目名称),对应目录 |
| Area | JD 顶级分类(00-99),对应子目录 |
| Category | Area 内分类(000-999),对应子目录 |
| ID | Category 内序号(0001-9999) |
| resource-type | 资源类型(task, note, meeting 等) |
| title | 资源标题(可包含中文) |

### 2.2 资源操作规范

任何资源被操作时(CRUD):
- 自动触发 Git 提交
- 更新 modified 时间戳
- commit message 遵循 [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/)
- 可选使用 LiteLLM 自动生成 commit message

**commit 类型**:`feat` | fix | docs | refactor | chore | archive

### 2.3 Namespace 管理

Namespace 作为项目/领域的管理容器:
- 对应目录:`{workspace}/{namespace}/`
- 可包含配置文件 .synapse.yaml
- 支持自定义资源类型和 SOP

### 2.4 预设资源类型

#### 2.4.1 类型与字段定义

| 类型 | 必填字段 | 可选字段 |
|------|----------|----------|
| task | title | status, priority, due, start_date, assignee, blocked_by |
| note | title | status, tags, links |
| meeting | title, datetime | status, duration, location, attendees, action_items |
| email | subject | status, from_addr, to_addr, cc, body |
| contact | name | organization, email, phone, custom_fields |
| unprocessed | (无) | status*, title, content |

> \* unprocessedstatus 字段为系统固定值,不可由用户修改(见 §2.4.2)。

**contact 支持自定义字段**:IP、MAC、微信号等任意键值对。

#### 2.4.2 状态定义

| 类型 | 状态字段 | 可选值 | 默认值 |
|------|----------|--------|--------|
| task | status | todo, in-progress, done, archived | todo |
| meeting | status | scheduled, completed, cancelled | scheduled |
| email | status | unread, read, replied, archived | unread |
| note | status | active, archived | active |
| contact | (无状态) | - | - |
| unprocessed | (固定) | unprocessed | unprocessed |

> **说明**:`unprocessed` 类型状态不可变,归档时自动转换为 note 类型(见 §5.2)。

#### 2.4.3 优先级定义(仅 task)

| 优先级 | 含义 | TUI 颜色 |
|--------|------|----------|
| high | 紧急重要 | 🔴 红色 |
| medium | 一般 | 🟡 黄色 |
| low | 可延后 | 🟢 绿色 |

#### 2.4.4 类型颜色标识

| 类型 | TUI 颜色 | 说明 |
|------|----------|------|
| task | 蓝色 | 待办任务 |
| note | 灰色 | 笔记 |
| meeting | 紫色 | 会议 |
| email | 青色 | 邮件 |
| contact | 橙色 | 联系人 |
| unprocessed | 白色/默认 | 未处理 |

---

## 3. Johnny.Decimal 组织结构

### 3.1 编号规则

采用标准 JD 三级结构,便于分桶管理:


Area.Category.ID
 │      │     │
 │      │     └─ 4位数字 (0001-9999)
 │      └─ 3位数字 (000-999)
 └─ 2位数字 (00-99)


**示例**:`10.001.0015` = Area 10 > Category 001 > 第 15 项

#### 3.1.1 ID 分配器与并发控制

**问题**:快速连续创建文件(如脚本批量导入)时,获取"下一个可用 ID"存在竞态条件。


# 并发场景示例
$ for i in {1..100}; do syn new task --title "Task $i" & done
# 如果无锁机制,多个进程可能拿到相同的 ID


解决方案:SQLite IMMEDIATE 事务 + UNIQUE 约束

⚠️ 为什么 `FOR UPDATE` 在空 Category 不起作用?
- FOR UPDATE 只能锁定 SELECT 返回的行
- 空 Category 的 MAX() 查询返回零行,无行可锁
- 多个进程可能同时得到 max_id = 0`,都尝试分配 `0001

正确方案:BEGIN IMMEDIATE + UNIQUE 约束双重保障
Text-to-SQL新范式:智能体自主探索搞定超大 AutoLink... http://xhslink.com/o/2coEcSKRwMe
复制后打开【小红书】查看笔记!
将alb的timeout设置为60秒,以在Spring WebClient的idletime中从alb取消连接,从而引发socket closed issue
将WebClient上的maxIdleTime设置为59秒,以便在60秒之前关闭idle time
Back to Top