# 同步系统状态定义 (Final Spec)

本文档定义了同步系统的状态机逻辑。逻辑流转遵循以下顺序：
1. **核心状态判定** (Core State)
2. **依赖检查与ID解析** (Dependency)
3. **自动绑定/发现** (Discovery)
4. **策略执行** (Action)

---

## 0. 绑定状态枚举 (BindingStatus)

| 状态值 | 含义 | 来源 | 允许创建 |
| :--- | :--- | :--- | :---: |
| **UNCHECKED** | 未执行绑定检查 | 加载后的初始状态 | ❌ |
| **NORMAL** | 绑定正常 | 核心状态判定 | ❌ |
| **MISSING** | 无绑定记录（孤儿） | 核心状态判定 | ✅ |
| **WARNING** | 业务键不完整 | 自动绑定阶段 | ❌ |
| **ABNORMAL** | 数据损坏 | 核心状态判定 | ❌ |
| **DEPENDENCY_ERROR** | 依赖未就绪 | 依赖检查或自动绑定 | ❌ |

**关键原则**：
- **UNCHECKED 阻止创建**：加载后节点状态为 UNCHECKED，只有执行了 `bind()` 后才会转为其他状态
- **只有 MISSING 状态才能触发 CREATE**：`create()` 方法只查找 `binding_status == MISSING` 的节点
- **如果 bind() 被跳过**（如 skip_sync=True），节点保持 UNCHECKED，不会被创建

---

## 1. 核心状态判定表 (Core Binding Matrix)

**说明**:
*   **Binding Record Pair**: 数据库中的绑定记录 (必须成对出现，已排除单边绑定)。
*   **Data Valid**: 指数据是否可读、Schema校验是否通过。
*   **Warning vs Abnormal**:
    *   `ABNORMAL`: 自身数据损坏。
    *   `WARNING`: 自身健康，但因为对方损坏导致**连接不可用**。

| Bind Pair (Record) | Local Data (Valid?) | Remote Data (Valid?) | -> | **Local Status** | **Remote Status** | **说明** |
| :---: | :---: | :---: | :--- | :--- | :--- | :--- |
| **YES** | **YES** | **YES** | -> | **NORMAL** | **NORMAL** | **稳态**。链路完整，数据健康，可进行 Diff/Update。 |
| **YES** | **YES** | **NO** | -> | **WARNING** | **ABNORMAL** | **远程损坏**。本地虽好，但链路已断 (Link Degraded)。禁止同步。 |
| **YES** | **NO** | **YES** | -> | **ABNORMAL** | **WARNING** | **本地损坏**。远程虽好，但链路已断。禁止同步。 |
| **YES** | **NO** | **NO** | -> | **ABNORMAL** | **ABNORMAL** | **双向损坏**。 |
| **NO** | **YES** | N/A | -> | **MISSING** | N/A | **本地孤儿**。进入“依赖与发现”流程。 |
| **NO** | N/A | **YES** | -> | N/A | **MISSING** | **远程孤儿**。进入“依赖与发现”流程。 |

---

## 2. 依赖与业务键解析 (Dependency & ID Resolution)

**适用范围**: 所有涉及外键依赖 (Foreign Key) 的节点。
**前置条件**: 核心状态 = `MISSING` (准备进行自动绑定或新建)。

在进行 `Auto-Bind` 或 `Create` 之前，必须先尝试构建**远程业务键**。
例如：本地 `Contract(proj_id="P1", code="C01")` 想找远程对象，必须先查询 `P1` 对应的远程 ID `RP_99`，转换成 `(RP_99, "C01")` 才能去远程搜索。

| 依赖项 (Dependency) | 依赖项绑定状态 | -> | **能否构建业务键** | **节点状态** | **说明** |
| :--- | :--- | :--- | :--- | :--- | :--- |
| **无依赖** | N/A | -> | ✅ Yes | **MISSING** | 根节点 (如 Project)，直接进入下一阶段。 |
| **有依赖** | **NORMAL** 且 `data_id` 非空（`status` 任意） | -> | ✅ Yes | **MISSING** | 依赖项已获得远程ID，可进入下一阶段。 |
| **有依赖** | **NORMAL** 且 `data_id` 为空（""，含 `status = FAILED`） | -> | ❌ No | **DEPENDENCY_ERROR** | 依赖项未回填ID，阻断绑定与同步。 |
| **有依赖** | **MISSING / ABNORMAL** | -> | ❌ No | **DEPENDENCY_ERROR** | **依赖阻断**。父节点未同步或损坏，无法计算远程键，无法绑定，无法创建。 |
| **有依赖** | **WARNING** | -> | ❌ No | **DEPENDENCY_ERROR** | 父节点状态不确定（如连接降级），禁止基于此建立新关系。 |

> **处理策略**: 处于 `DEPENDENCY_ERROR` 状态的节点，其动作强制为 **SKIP** (或 PENDING)。必须先修复父节点。
>
> **错误信息格式**: `DEPENDENCY_ERROR` 节点的 `error` 字段会包含详细的依赖失败信息：
> - `dep_err: not_found: field_name(node_type)=value` - 依赖节点未找到
> - `dep_err: field_name: status=FAILED` - 依赖节点状态为 FAILED
> - 多个依赖失败时用分号分隔：`dep_err: not_found: project_id(project)=P1; contract_id: status=FAILED`

---

## 3. 自动绑定状态处理 (Auto-Binding State Handling)

此逻辑遵循“去噪后 1:1”原则：只有在排除掉所有健康同步对后，若剩余节点形成绝对的 1:1 关系，才允许自动绑定。

**配置说明与触发机制**:
- **初始化场景**: 通常在系统第一次初始化或首次接入新实体时，配置 `auto_bind = True` 来对齐历史数据。
- **后续同步**: 系统在日常同步中不建议开启全局自动绑定。自动绑定仅作为“发现”手段，一旦建立过关系的节点，其后续绑定操作通常仅在“自动创建”逻辑中伴随产生（详见第 4 节孤儿处理）。
- **执行前提**: 只有当节点状态为 `MISSING` 且依赖解析成功（`DEPENDENCY_ERROR = False`）时，才会尝试以下探测逻辑。
- **业务键完整性**: 若业务键字段缺失（如 `serial_number = null`），直接标记 `binding_status = WARNING`，并在 `node.error` 记录原因（`Auto-bind skipped: missing fields ...`），不参与自动绑定与孤儿创建判定。- **ID字段解析失败**: 若业务键包含ID字段（如 `supplier_id`）且依赖节点未绑定，ID解析失败，标记 `binding_status = DEPENDENCY_ERROR`，阻止孤儿创建。
**核心算法**:
1. **全集搜索**: 寻找本地/远端所有共享同一个业务键（Business Key）的记录。
2. **排除稳态对**: 识别并移除**成对且互绑**的 `NORMAL` 记录。
    - 只有当本地 A 与远端 B 互为绑定对象，且状态皆为 `NORMAL` 时，才将这一对记录从待判定集合中剔除。
    - 若出现单边 `NORMAL` 记录（即其配对项不在本次搜索范围内或状态不属于另一端的 `NORMAL`），则不予剔除，视为环境污染项。
3. **判定剩余**: 检查排除上述稳态对后的剩余集合（包含 `MISSING`, `ABNORMAL`, `WARNING` 及单边已占用的 `NORMAL` 记录）。

> **术语**：
> - `L_remain` = 本地记录中，排除“成对稳态”后剩余的数量。
> - `R_remain` = 远端记录中，排除“成对稳态”后剩余的数量。

| `L_remain` | `R_remain` | -> | **动作** | **说明** |
| :---: | :---: | :--- | :--- | :--- |
| **1** | **1** | -> | **BIND (Link)** | **1对1 自动对齐**：仅当两端剩余记录皆为 `MISSING` 且绝对 1:1 时，执行自动绑定。 |
| **1** | **1** | -> | **SKIP** | **带障不处理**：若剩余记录中包含 `ABNORMAL` 等异常状态，说明存在历史冲突，不执行自动动作。 |
| **N** | **0** | -> | **CREATE** | **可判定缺失**：业务键完整且唯一，本地存在而远端不存在（N:0）时，才允许进入孤儿创建策略。 |
| 其他 | 其他 | -> | **SKIP** | **非 1:1 不处理**：存在多对多、多对一或无候选情况，系统保持原状。 |

**设计优势**:
1. **对故障极端敏感**：环境中存在任何 `ABNORMAL` 记录都会阻断自动绑定，强制人工介入处理异常。
2. **状态纯净**：失败时不产生任何中间态（如已废弃的 `AMBIGUOUS`），保持语义清晰。
3. **静默自愈**: 当外界清理了环境且满足 1:1 条件后，系统下次同步会自动完成绑定。

### 3.1 空数据处理策略

**原则**：**空数据不推送**。

**空数据定义**：
- 初始化时创建但未编辑的业务记录（如空合同、空施工任务）
- 业务键字段为空或关键字段缺失的记录

**处理方式**：
- 在自动绑定阶段，若业务键字段缺失，标记 `binding_status = WARNING`，记录原因
- 不参与自动绑定与孤儿创建判定
- 不推送到远程系统

**设计理由**：
1. **远程系统简化**：远程只需处理完整数据，无需兼容空数据校验逻辑
2. **避免垃圾数据**：空数据通常会被本地删除，若已推送则清理复杂
3. **数据质量保障**：远程系统只能看到本地已正确编辑保存的业务

**配置方式**（Strategy 层）：
```python
# 在 Strategy 配置中指定必填字段
default_config = StrategyConfig(
    auto_bind_fields=["contract_id", "task_type"],  # 业务键字段
    # 如果这些字段为空，节点将被标记为 WARNING 而非 MISSING
)
```

---

## 4. 动作策略表 (Action Policy Matrix)

此表定义了在特定状态下，`execute` 阶段允许产生的 **意图行动 (`SyncAction`)**。

#### A. 稳态更新 (NORMAL)
适用于已有绑定关系且链路健康的节点。

| 状态 | 动作条件 | 执行动作 (`SyncAction`) |
| :--- | :--- | :--- |
| **NORMAL** | `compute_diff` 判定存在差异 | **UPDATE** |
| **NORMAL** | `compute_diff` 判定无差异 | **NONE** |

#### B. 异常处理 (阻断态)
处于这些状态的节点，引擎会自动中止后续的读写操作。

| 状态 | 说明 | 动作 (`SyncAction`) |
| :--- | :--- | :--- |
| **DEPENDENCY_ERROR** | 父节点缺失/异常同步导致阻断 | **NONE** |
| **ABNORMAL / WARNING** | 数据损坏或链路降级，需人工修复 | **NONE** |

#### C. 孤儿处理 (Create Only)
适用于最终探测状态为 **MISSING** 且无法/不满足自动绑定的节点。

**本地孤儿 (Local Orphan)**: 本地有，远程无。
| 策略配置 (`local_orphan_action`) | 对应的同步模式 | 动作 (`SyncAction`) |
| :--- | :--- | :--- |
| **"create_remote"** | Push (本地推远端) | **CREATE** |
| **"delete_local"** | Pull (远端拉本地) | **报错 (未实现)** |
| **"none"** | Any | **NONE** |

**远程孤儿 (Remote Orphan)**: 远程有，本地无。
| 策略配置 (`remote_orphan_action`) | 对应的同步模式 | 动作 (`SyncAction`) |
| :--- | :--- | :--- |
| **"create_local"** | Pull (远端拉本地) | **CREATE** |
| **"delete_remote"** | Push (本地推远端) | **报错 (未实现)** |
| **"none"** | Any | **NONE** |

---

## 5. 执行同步状态转换 (Physical Sync State Transitions)

在策略执行完成、设置好 `action` 后，DataSource 层负责执行实际的同步操作并更新 `status` 字段。

### A. 执行状态枚举 (SyncStatus)

| 状态值 | 含义 | 适用场景 |
| :--- | :--- | :--- |
| **PENDING** | 待执行 | 初始状态、无动作的节点 |
| **IN_PROGRESS** | 执行中 | 异步接口已提交，等待结果回调 |
| **SUCCESS** | 执行成功 | 同步操作完成且成功 |
| **FAILED** | 执行失败 | 同步操作失败，`error` 字段记录原因 |
| **SKIPPED** | 已跳过 | 节点被主动跳过（如依赖未满足） |
| **PRECONDITION_FAILED** | 前置条件失败 | create/update 内部验证发现问题 |

### B. 执行转换矩阵

#### 同步接口（一步式）

适用于同步 API（调用后立即返回成功/失败）。

| 输入: action | API 调用结果 | → | 输出: status | 副作用 |
| :--- | :--- | :--- | :--- | :--- |
| **CREATE** | 成功返回 (200/201) | → | **SUCCESS** | `data_id` 设置为远程返回的ID |
| **CREATE** | 失败返回 (4xx/5xx) | → | **FAILED** | `error` 记录错误信息 |
| **UPDATE** | 成功返回 (200) | → | **SUCCESS** | - |
| **UPDATE** | 失败返回 (4xx/5xx) | → | **FAILED** | `error` 记录错误信息 |
| **DELETE** | 成功返回 (200/204) | → | **SUCCESS** | 节点可能被移除 |
| **DELETE** | 失败返回 (4xx/5xx) | → | **FAILED** | `error` 记录错误信息 |
| **NONE** | - | → | **PENDING** | 不执行，保持原状 |

#### 异步接口（两步式）

适用于异步 API（提交后需要轮询或回调获取结果）。

| 输入: action | API 提交结果 | → | 输出: status | 后续操作 |
| :--- | :--- | :--- | :--- | :--- |
| **CREATE** | 提交成功 (202 Accepted) | → | **IN_PROGRESS** | 记录 task_id，等待轮询 |
| **CREATE** | 提交失败 (4xx/5xx) | → | **FAILED** | `error` 记录错误 |
| **IN_PROGRESS** | 轮询返回成功 | → | **SUCCESS** | `data_id` 设置为远程ID |
| **IN_PROGRESS** | 轮询返回失败 | → | **FAILED** | `error` 记录错误 |
| **IN_PROGRESS** | 超时未完成 | → | **FAILED** | `error` 记录 "Timeout" |

### B+. Handler 降级（action 取消）

> **代码实现**：各 `domain/*/api_handler.py` 中的 `update()` 方法。

当 Handler 在构建 API 请求数据时，发现实际无需更新的字段（如 schema 校验后所有字段与远程一致），
Handler 会主动将节点的 `action` 从 `UPDATE` 降级为 `NONE`：

```
action=UPDATE → Handler 校验 → 无实际变更字段 → action=NONE（不提交 API）
```

此机制确保 DataSource 不会向远程发送无意义的空更新请求。

### C. 关键副作用

| 动作 | 成功时的副作用 | 说明 |
| :--- | :--- | :--- |
| **CREATE** | 临时绑定已建立 | 绑定关系可在提交前创建，用于推送与追踪；是否成功不影响该绑定存在 |
| **CREATE** | 设置 `data_id` | 成功后从API响应中提取远程分配的ID，更新到节点 |
| **CREATE** | 回写 `data` 主键 | DataSource 将远程返回的ID写回 `SyncNode.data`（如 `id`/`*_id` 字段） |
| **CREATE** | 更新绑定记录 | 若使用临时ID，成功后替换为真实的远程ID |
| **UPDATE** | 更新 `origin_data` | 将当前 `data` 保存为新的 `origin_data` |
| **DELETE** | 移除绑定记录 | 删除成功后，清除对应的绑定关系 |

### D. 错误处理

| 错误类型 | 记录方式 | 重试策略 |
| :--- | :--- | :--- |
| **网络错误** | `error = "Network timeout"` | 可重试 |
| **权限错误** | `error = "403 Forbidden"` | 不可重试，需人工介入 |
| **数据校验错误** | `error = "Validation failed: ..."` | 不可重试，需修复数据 |
| **依赖缺失** | `status = PRECONDITION_FAILED` | 不执行，等待依赖修复 |

---

## 6. 同步检查规范 (Sync Check Specification)

在执行同步前后，需要进行状态检查以确保操作的合法性和完整性。

### A. 内联前置检查（create/update 阶段）

> **实现说明**：原 `_pre_commit_check` 方法已移除（保留空壳兼容接口）。
> 所有有效检查已内联到 `strategy_ops/create_ops.py::add_create_action()` 和 `strategy_ops/update_ops.py::add_update_action()` 中。

**create() 阶段内联检查**：

| 检查类别 | 检查条件 | 不通过时 | 说明 |
| :--- | :--- | :--- | :--- |
| **数据完整性** | `src_node.get_data()` 不为空 | 跳过该节点，记录日志 | MISSING 但数据为空 |
| **依赖 ID 解析** | `id_resolver.resolve_and_validate()` 成功 | `binding_status → DEPENDENCY_ERROR` | 依赖字段无法解析到目标系统 ID |

**update() 阶段内联检查**：

| 检查类别 | 检查条件 | 不通过时 | 说明 |
| :--- | :--- | :--- | :--- |
| **FAILED 保护** | `src_node.status != FAILED` | 跳过该节点 | 不覆盖上次失败的状态和错误 |
| **数据完整性** | `src_node.get_data()` 不为空 | 跳过该节点 | NORMAL 但数据为空 |
| **目标节点存在** | 通过绑定找到目标节点 | 跳过该节点 | 绑定记录指向的节点不在集合中 |
| **data_id 完整性** | `target_node.data_id` 非空 | `status → PRECONDITION_FAILED` | UPDATE 必须有业务主键 |
| **依赖 ID 解析** | `id_resolver.resolve_and_validate()` 成功 | `status → PRECONDITION_FAILED` | 依赖字段无法解析到目标系统 ID |
| **差异检测** | `_needs_update()` 返回 True | 跳过该节点 | 无差异则不更新 |

> **注意**：以下检查项在当前实现中 **未覆盖**【未实现】：
> - 状态合法性：`binding_status` 必须是已知状态
> - 动作合法性：`CREATE/UPDATE` 只能用于 `NORMAL` 节点
> - 绑定一致性：`NORMAL` 节点必须有绑定记录

### B. 同步后检查 (Post-Sync Check)

**目标**: 验证同步结果的完整性，发现残留问题。

> **实现说明**：当前 `_stage4_post_sync_check` 只打印摘要表格，不做异常检测。以下检查项均为【未实现】。

#### 检查项清单【未实现】

| 检查类别 | 检查条件 | 异常情况 | 严重程度 |
| :--- | :--- | :--- | :--- |
| **CREATE 完成性** | CREATE 节点应该有 `data_id` | `action = CREATE, status = SUCCESS` 但 `data_id = ""` | 🔴 ERROR |
| **CREATE 绑定** | CREATE 节点应该有绑定记录 | CREATE 成功但无绑定 | 🔴 ERROR |
| **UPDATE 完成性** | UPDATE 节点应该成功 | `action = UPDATE` 但 `status = FAILED` | ⚠️ WARNING |
| **绑定保持** | UPDATE 节点的绑定应该保持 | UPDATE 后绑定记录丢失 | 🔴 ERROR |
| **残留异常** | 不应有 ABNORMAL 节点 | 同步后仍有 `binding_status = ABNORMAL` | ⚠️ WARNING |
| **残留依赖错误** | 不应有 DEPENDENCY_ERROR | 同步后仍有依赖阻断 | ⚠️ WARNING |

#### 统计指标

| 指标 | 计算方式 | 用途 |
| :--- | :--- | :--- |
| **成功率** | `SUCCESS / (CREATE + UPDATE)` | 评估同步质量 |
| **异常率** | `(ABNORMAL + WARNING) / total` | 发现数据质量问题 |
| **阻断率** | `DEPENDENCY_ERROR / total` | 发现依赖关系问题 |

#### 决策规则

| 检查结果 | ERROR 数量 | 决策 |
| :--- | :--- | :--- |
| ✅ 通过 | 0 | **继续持久化**，正常结束 |
| ⚠️ 有警告 | 0, WARNING ≥1 | **继续持久化**，打印警告报告 |
| ❌ 有错误 | ≥1 | **继续持久化**（已同步无法回滚），打印错误报告 |

> **注意**: Post-Sync Check 即使有 ERROR 也不会终止流程，因为同步已经执行，无法回滚。检查目的是发现问题并记录，供后续修复。

---

## 附录: 状态字段说明

### SyncNode 核心字段

| 字段 | 类型 | 设置阶段 | 含义 |
| :--- | :--- | :--- | :--- |
| `binding_status` | BindingStatus | Bind 阶段 | 绑定健康状态 |
| `action` | SyncAction | Create/Update 阶段 | 待执行动作 |
| `status` | SyncStatus | Physical Sync 阶段 | 执行结果状态 |
| `data_id` | str | 数据加载 / CREATE 后 | 业务主键ID（CREATE 前为 ""） |
| `depend_ids` | List[str] | 数据加载 | 依赖节点的 node_id 列表 |
| `error` | str \| None | Physical Sync 阶段 | 错误信息（失败时） |

### 状态流转时序

```
[数据加载]          [Bind]              [Create/Update]     [Physical Sync]
binding_status:   UNCHECKED  →  NORMAL/MISSING/...  →  (保持)          →  (保持)
action:           NONE       →  NONE                →  CREATE/UPDATE   →  (保持)
status:           PENDING    →  PENDING             →  PENDING         →  SUCCESS/FAILED
data_id:          从源加载    →  (保持)              →  可能为 ""      →  CREATE 后设置
```