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 目录结构(双层分桶)
 
 
Back to Top