diff --git a/docs/calibration-model-contract.zh.md b/docs/calibration-model-contract.zh.md
new file mode 100644
index 0000000..49a8060
--- /dev/null
+++ b/docs/calibration-model-contract.zh.md
@@ -0,0 +1,228 @@
+# 统一力标定模型契约(草案)
+
+Purpose: plan. Scope: SDK 标定文件选择、SN 绑定和推断接口。状态:2026-10-18 提案,未实现。
+保持 `cal_path` 与现有单点文件;新增空间模型和机器学习模型使用同一入口。
+
+## 现状与边界
+
+现有实现见 [calibration.py](../src/orisys/calibration.py)、
+[Sensor](../src/orisys/sensor.py)、[profile resolver](../src/orisys/sensors/calibration.py)。
+当前读 schema 1/2,按 SN 加载要求三个通道,转换只接收三个力标量。
+`ForceReadout.apply` 会分别转换滤波前后数据,不能直接挂接带状态的空间或 ML 模型。
+
+| 所有者 | 责任 |
+|---|---|
+| 离线实验 / AutoCal | 采集、训练、验证、导出模型;将实验坐标转换为契约坐标 |
+| SDK | 选文件、校验身份与输入、准备特征、推断、输出逐轴有效性 |
+| 应用 | 提供 SN 与文件选择,展示状态;不自己套用第二次增益 |
+| 设计资产 | 几何与坐标定义;不提供每台设备的实测标定值 |
+
+SDK 不训练模型、不导入应用代码、不读取训练 CSV,不运行文件内指定的 Python。
+实验 `finger_multical.experiment_model/1` 必须显式导出,不能直接当 SDK 文件加载。
+
+## 文件选择:保留路径,文件决定模型
+
+不增加 `model_type` 构造参数或全局活动模型。`Sensor`、`FingerRuntime.open` 和
+profile 创建处理器统一复用内部选择器;现有入口参数只映射为选择请求。
+
+| 请求 | 文件与行为 |
+|---|---|
+| 非空 `cal_path` | 精确加载该文件;缺失、错误或不兼容时报错,不换文件 |
+| `calibration_dir` + `device_id` | 保持 `
//.json`;缺文件保持 raw 并报告 missing,不回退产品模板 |
+| 两者都未提供 | 保持产品包默认解析;有 descriptor 时用 `default_calibration`,旧包保留既有候选顺序 |
+| profile 的 `CalibrationSelection.NONE` | 明确不标定;不赋予 `cal_path="none"` 新语义 |
+
+`cal_path` 与 `calibration_dir` 仍互斥。构造入口 `device_id` 对应文件 `identity.sn`,
+不新造另一个 SN 字段。未指定设备的录像可显式选文件,但实际 SN 一致性标记为 unknown。
+有 `device_id` 时严格匹配 `sensor_type`、`sn`;不根据相机编号猜 SN。
+保留 `device_id` 单独传入时的参数错误;目录本身不存在也报错,不能当成 SN 文件缺失。
+
+```python
+# 沿用现有调用参数;schema 3 需未来实现支持后使用。
+runtime = FingerRuntime.open(
+ config="finger", video="test.mp4",
+ device_id="DD02-GL-001",
+ cal_path="calibrations/finger/DD02-GL-001.json",
+)
+# 自动按 SN 选择仍使用 calibration_dir="calibrations",不同时传 cal_path。
+```
+
+同一 SN 比较不同模型时,显式指定不同 JSON;默认 SN 文件只代表一个选定版本。
+不扫描目录选“最新模型”,不在每帧重新加载。
+
+## 最小文件:schema 3
+
+复用 `force_calibration.json` 文件角色,增加 schema 3;文件名不参与模型类型判断。
+以下是三点结构示例,数值和 SHA 占位值不是设备标定结果。
+
+```json
+{
+ "schema_version": 3,
+ "unit": "N",
+ "identity": {"sensor_type": "finger", "sn": "DD02-GL-001"},
+ "pipeline": {
+ "stitch_resolution": [700, 750],
+ "dis_mode": 4,
+ "hhd_downsample": 3,
+ "source_scale": 0.42,
+ "filters": {"measurement": {"flow_ema_alpha": 1.0}},
+ "feature_signature_sha256": "0000000000000000000000000000000000000000000000000000000000000000",
+ "geometry_sha256": "0000000000000000000000000000000000000000000000000000000000000000",
+ "coordinates": {
+ "frame": "autocal_flat",
+ "unit": "px",
+ "origin_xy": [511, 1021],
+ "map_sha256": "0000000000000000000000000000000000000000000000000000000000000000"
+ }
+ },
+ "model": {
+ "type": "spatial_gain/1",
+ "input_profile": "fn_delta_autocal_uv/1",
+ "output_frame": "sdk_force/1",
+ "outputs": ["fn"],
+ "params": {
+ "points_uv": [[-145, 604], [145, 604], [5, 430]],
+ "gains_N_per_raw": [0.00015, 0.00020, 0.00025],
+ "triangles": [[0, 1, 2]],
+ "valid_force_range_N": [0.0, 3.0]
+ }
+ }
+}
+```
+
+| 字段 | 规则 |
+|---|---|
+| `schema_version` | 整数 3;未知版本拒绝 |
+| `unit` | 固定 `N` |
+| `identity` | `sensor_type`、非空 `sn` 必填;文件位置不能替代身份 |
+| `pipeline` | 输入 profile 要求的实际处理参数、几何和坐标身份;缺少必需项或不匹配则拒绝 |
+| `model.type` | SDK 已注册的版本化标识;决定参数解析和推断,不是模块路径 |
+| `input_profile` | SDK 已实现的特征、单位、形状、预处理和时序契约 |
+| `output_frame` | `sdk_force/1` 延续对应处理 profile 的 FN/FX/FY 符号和基方向;不是世界坐标合力模长 |
+| `outputs` | 非空、无重复的 `fn/fx/fy` 子集;支持 FN-only,不冒充三轴标定 |
+| `params` | 模型类型私有参数;通用加载器不解释点数或网络层 |
+
+`provenance` 可选,保存数据摘要、训练版本和验证指标;不作为运行时输入。
+其余未知语义字段拒绝,不能靠忽略字段误读新模型。新增模型类型不必升级外层 schema;
+修改输入或算法语义必须升级相应 `/1` 标识。
+
+`geometry_sha256` 是所用计算 mesh 文件字节的 SHA-256;`map_sha256` 是 SDK 坐标映射定义的
+内容摘要,包含坐标变换及其版本,不能只比较图幅。pipeline 的必需键由输入 profile 固定,
+覆盖该 backend 所有影响测量的设置;只检查示例中的部分配置不算兼容校验。
+新增 profile 必须定义闭合的签名字段表。首个 profile 的 `feature_signature_sha256` 摘要
+对象包含 `feature_version`、`processing_backend`、有效的图像预处理/光流/HHD 配置和计算资产哈希;
+不含文件绝对路径、SN、显示资源或力读数滤波。以 UTF-8、键排序、无多余空格的 JSON 计算 SHA-256。
+特征定义或单位改变须升级 `feature_version`,配置覆盖后重新计算,不能用配置文件名代替。
+
+## 空间增益模型
+
+`spatial_gain/1` 固定输出 `fn`,使用 `fn_delta_autocal_uv/1`:
+
+- 输入为同一帧的未经过力读数滤波的 FN raw、有效会话零点及接触 U/V。
+ 上游光流滤波属于 pipeline 身份;不得混用不同特征链。
+ `delta_raw`、U、V 为有限浮点标量,前者单位 raw,后两者为上述有符号像素。
+- SDK 会话准备 `delta_raw = fn_raw - zero_raw`,模型不再次扣零。
+ 零点必须来自明确的归零操作或已标注窗口;模型文件不保存某段录像的启动零点。
+- U 向右、V 向上,`x=origin_x+U`、`y=origin_y-V`。
+ SDK 提供并校验坐标转换;不能让模型偷偷按比例换算,不能运行时依赖 AutoCal。
+- `points_uv` 为 N×2,`gains_N_per_raw` 长度 N;N≥3 且非共线、坐标唯一、增益有限且为正。
+ `triangles` 为任意数量的三索引,必须构成支持点凸包内完整、无重叠的非退化三角网。
+ 保存连接关系以避免重新剖分引入变化;点数由数组决定,不固定为 9/15。
+- 对三角形内查询,`g=Σw_i*g_i`,`fn_N=g*delta_raw`;不插值倒数,不外推。
+ 未覆盖、位置失败、归零无效均返回无效,不能偷偷改用 M0。
+- `valid_force_range_N` 必填。估计超范围时保留数值并标记 `out_of_range`,不截断。
+
+单点/两点数据不自动切换算法;可导出独立的 `global_gain/1`,参数为各输出轴的增益和有效范围,
+输入 profile 为 `force_delta/1`,由 SDK 按同一归零语义准备对应 raw 增量。
+这不同于旧曲线的绝对 raw 输入;兼容适配器不得替旧文件额外扣零。
+
+## 机器学习扩展
+
+同一 SN、路径和外层字段不变,只替换 `model`。下面只预留 `onnx/1` 示例,
+不表示 SDK 已支持 ONNX;后端实现和输入 profile 验证通过前必须拒绝加载。
+
+```json
+{
+ "type": "onnx/1",
+ "input_profile": "fn_delta_autocal_uv/1",
+ "output_frame": "sdk_force/1",
+ "outputs": ["fn"],
+ "params": {
+ "input_tensor": "features",
+ "output_tensor": "force_N",
+ "feature_order": ["delta_raw", "u", "v"],
+ "dtype": "float32",
+ "shape": [1, 3],
+ "valid_force_range_N": [0.0, 3.0]
+ },
+ "artifacts": [
+ {
+ "role": "weights",
+ "path": "DD02-GL-001/model.onnx",
+ "sha256": "0000000000000000000000000000000000000000000000000000000000000000"
+ }
+ ]
+}
+```
+
+此标量 profile 的规范输入即上述顺序、类型、形状;输出 float32 `[1, len(outputs)]`,
+轴序严格按 `outputs`。归一化及单位转换封装在已验证的模型图内,不由应用另做。
+`fn_delta_autocal_uv/1` 只允许 `outputs=["fn"]`,不能凭 FN 特征宣称三轴已标定。
+需要图像、完整光流或历史序列时新增输入 profile,明确 H/W、通道、掩码、单位和时间窗口;
+没有对应特征的处理器拒绝加载,不能用零张量凑齐输入。
+
+`artifacts` 只在有外部文件时必填;路径相对所选 JSON 的父目录,禁止绝对路径、URL、
+`..` 或链接逃逸。校验 SHA-256、角色和文件完整性后才初始化受支持的后端。
+不加载 pickle、任意 Python 或模型自带扩展库;缺推断依赖须明确报错,不自动安装。
+文件与所有附件共同构成内容指纹,不能只哈希 JSON。
+附件间依赖也须逐项声明和校验,未声明的外部权重不可加载。
+
+## 每帧推断与有效性
+
+内部统一生命周期如下;是拟议接口,不是现有 API。
+
+```text
+load(selected_file, device_identity, pipeline) -> CalibrationModel
+model.predict(frame_id, timestamp_s, profile_features, reference_id) -> CalibrationResult
+model.reset(reference_id)
+
+CalibrationResult:
+ values_N: {axis: finite_float | null}
+ status: {axis: ok | no_contact | invalid_input | zero_unavailable |
+ coordinate_unmapped | outside_coverage | out_of_range}
+```
+
+FN 空间模型输入仅允许单一接触;无法确认单接触时不能宣称已标定总力。
+无接触、失败帧的校准输出为空,raw 另行保留;缺失值不是 0 N。
+`out_of_range` 有数值但不在验证范围,其余失败状态无数值;未声明轴不可用、单位仍为 raw。
+不把某轴 raw 填进 `values_N`,也不只靠全局 `force_unit` 判断三轴单位。
+schema 3 的旧数值读数接口以 NaN 表示已声明轴的无效输出,并同步暴露上述状态;
+`force_calibrated` 表示已加载模型能力,不表示当前帧有效。
+
+一个模型实例属于一个流;按帧顺序每帧推断一次,读数和录像复用结果。
+清零、重连、倒播或更换模型时重置状态;不同 SN 不共享可变状态。
+schema 3 先做上游测量与归零、模型推断,再做有效 N 读数的显示滤波;失败帧不进滤波历史。
+schema 1/2 仍保留旧滤波/转换顺序。状态模型不能塞入会调用两次的旧 `convert` 回调。
+
+加载诊断至少包含路径、来源、SN 校验状态、模型/输入 profile、轴集合、内容指纹和失败原因。
+加载不匹配失败与逐帧暂时无效分开;替换时先完成加载校验,失败不得破坏原活动模型。
+
+## 兼容、实现位置与验收
+
+新读取器并存支持 schema 1/2/3;旧文件不重写,SN 校验的旧三通道要求保持。
+schema 3 按声明轴校验,允许 FN-only;旧 SDK 遇 schema 3 必须拒绝,不能降级猜读。
+回退通过恢复旧文件的 `cal_path`;SN 默认文件切换前保留旧副本,不覆盖唯一证据。
+
+| 修改位置 | 最小工作 |
+|---|---|
+| `calibration.py` | schema 分流、身份和附件校验;旧曲线适配器 |
+| `sensors/calibration.py` | 统一选择器与指纹;支持 SN 请求 |
+| 新 `calibration_models.py` | 受控类型表、空间增益与模型生命周期;ML 后端另行实现 |
+| `sensor.py`、`finger/runtime.py`、处理器适配层 | 提供同帧位置/零点,缓存逐轴结果和状态 |
+| `force_readout.py` | 保留旧路径,为 schema 3 分离推断与读数滤波 |
+| 离线导出器 | 实验模型到 schema 3;SDK 不依赖实验目录 |
+
+实现验收必须覆盖:旧结果与单位不变;显式路径/SN/默认/关闭选择;错误 SN 不串用;
+3/14/更多点、退化网格与凸包外;节点和插值预测;零点不重复扣除;缺坐标/无接触;
+FN-only 的 FX/FY 不冒充 N;未知模型、附件损坏和依赖缺失;状态模型每帧只调用一次及 reset。
+另以独立录像验证物理准确度,契约或单元测试通过不代表标定合格。