world_sync/docs/work-log-2026-09-22-m11b2d-fixture-contract.md
2026-09-22 22:09:01 +08:00

294 lines
21 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# M11b-2d-A3 登记:world_sync 测试夹具「函数式契约」(唯一权威登记文档)
- **任务**: [M11b-2d-A3] 模块仓库卫生:删一次性探针 + 登记函数式夹具契约
- **日期**: 2026-09-22
- **响应**: QC #2(登记文档缺失);同时承载 QC #1(越权回退)与 QC #3(证据错位)的说明与移交
- **文档定位**: **索引式登记**。夹具层实现说明的权威正文在 `tests/_m11b2_compat.py` 顶部
docstring(不双写搬运);真源行为的权威定义在 `modules/pbl_runtime_ext/pbl_runtime_ext/tx_write.py`
源码本身。本文档只做**符号清单 + 逐参数签名 + 返回键语义 + 错误码含义 + 废弃清单 + 引用规约**,
全部内容由本次实读源码抄录(锚点行号见各节,行号以 2026-09-22 HEAD 为准)。
---
## 0. 一句话结论
M11b-2a 把运行时写入实现迁到**函数式 API** `pbl_runtime_ext.tx_write.apply_runtime_event` /
`read_states` 之后,`world_sync` 测试层**只允许存在一条夹具路径**:
`tests/_m11b2_compat.py` 的 `install/uninstall/apply/read/trace_kinds/update_statements/insert_statements`
(真 sqlite + monkeypatch `rtx_db` 三入口,直连真源代码路径)。
旧的「类式垫片」第二路径(`RuntimeStateEventWriter` / `configure_writer` / `build_plan` /
类式 `write_event_with_state` 转发)已在 M11b-2d 删除,**禁止复活**(机械守卫见 §6)。
---
## 1. 夹具层符号清单(`modules/world_sync/tests/_m11b2_compat.py`)
### 1.1 现行唯一夹具路径(公开符号,必须存在)
| 符号 | 定义行 | 签名 | 职责 |
|---|---|---|---|
| `RuntimeEngine` | :166 | `RuntimeEngine(path=":memory:")` | 真 sqlite3 引擎;表结构由 `ddl_statements()` 从 `pbl_runtime_ext/models/*.json` 生成;持有 `sql_log` / `trace` / `broadcasts` 三条录制面 |
| `install` | :391 | `install(engine)` | 把 `rtx_db.transaction` / `rtx_db.dbname` / `rtx_db.q_all` 换成 sqlite 引擎实现,并把 `broadcast.hub.publish` 包成录制版;返回 engine |
| `uninstall` | :418 | `uninstall()` | 逐样还原被 patch 的三入口(`_ORIG` 表驱动),幂等(未安装直接返回) |
| `apply` | :429 | `apply(**kwargs)` → dict | `asyncio.run(apply_runtime_event(**kwargs))` 的同步入口,**零返回键增补** |
| `read` | :434 | `read(**kwargs)` → list | `asyncio.run(read_states(**kwargs))` 的同步入口 |
| `trace_kinds` | :438 | `trace_kinds(engine)` → list[str] | 事务生命周期轨迹(`BEGIN`/`COMMIT`/`ROLLBACK`/`BROADCAST`) |
| `update_statements` | :442 | `update_statements(engine)` → list[(sql, params)] | 引擎实际发出的 `UPDATE` 语句(大写、压空格),供乐观锁/不可变列断言 |
| `insert_statements` | :447 | `insert_statements(engine, table)` → list[(sql, params)] | 指定表的 `INSERT` 语句,供 append-only 断言 |
### 1.2 夹具层直连的真源符号(re-export,不是副本)
| 夹具名 | 来源 | 定义行 |
|---|---|---|
| `apply_runtime_event` | `pbl_runtime_ext.tx_write` | tx_write.py:344 |
| `read_states` | `pbl_runtime_ext.tx_write` | tx_write.py:466 |
| `build_row` | `pbl_runtime_ext.tx_write` | tx_write.py:157 |
| `assert_append_only` | `pbl_runtime_ext.tx_write` | tx_write.py:259 |
| `RtxError` | `pbl_runtime_ext.rtx_db` | rtx_db.py:29 |
| `percentile` | `world_sync.pbl_runtime_sql`(真源唯一实现) | — |
| `SQLITE` / `MYSQL` | `ps.DIALECT_SQLITE` / `ps.DIALECT_MYSQL` | — |
| `EVENT_TABLE` / `STATE_TABLE` / `SNAPSHOT_TABLE` / `PROTECTED_BASE_TABLES` | `pbl_runtime_ext.m11b_config` | m11b_config.py:15-17,24 |
| `ERR_*` 常量 | `pbl_runtime_ext.m11b_config` | m11b_config.py:36-41 |
表名真值(禁在测试里写第二份字面量):`pbl_runtime_event` / `pbl_entity_state` /
`pbl_world_state_snapshot`;基表只读域 `world, scene, entity, scense, scense_runtime, script_engine`。
### 1.3 夹具内部辅助(私有,测试不直接依赖)
`_sql_type`(:91) / `_not_null`(:105) / `_default_clause`(:111) / `model_fields`(:125) /
`ddl_statements`(:132) / `_to_qmark`(:160) / `_Tx`(:262, `rtx_db.transaction` 的 sqlite 替身)。
> ⚠️ 已知夹具渲染口径(写测试前必读):`_default_clause`(:111) 把 models 里 `default: ""`
> (空串)当「无默认值」跳过,于是 `event_code` / `idem_key` 等「NOT NULL DEFAULT ''」列在
> sqlite 侧变成**无默认值的 NOT NULL 列**,真源按语义不填这些列时会撞约束。
> 现行做法(`test_m11b2_single_tx.py` 与 `test_m11b2d_fixture_contract.py` 一致):
> **仅在构造期**临时替换 `_default_clause` 补正空串默认值,建完表立即还原,
> **不改夹具既有定义与行为语义**(夹具属本任务禁改清单)。
---
## 2. 真源 `apply_runtime_event` 逐参数签名(实读抄录)
`modules/pbl_runtime_ext/pbl_runtime_ext/tx_write.py:344-348`,原文:
```python
async def apply_runtime_event(tenant_id=None, world_id=None, session_id=None,
event_type=None, payload=None, state_updates=None,
client_event_id=None, idem_key=None, snapshot=None,
base_version=None, actor_id=None, causation_id=None,
occurred_at=None, env=None):
```
| # | 参数 | 默认 | 语义(真源行为,非推测) |
|---|---|---|---|
| 1 | `tenant_id` | `None` | **强制打头**。`_require_tenant()`(tx_write.py:246) 缺失即抛 `PBL-TENANT-0001`,fail-closed 拒绝读写 |
| 2 | `world_id` | `None` | 必填;`in (None, "")` → `PBL-PARAM-0001`(tx_write.py:355-356)。同时参与广播通道名 `channel_of(world_id, tenant_id)` |
| 3 | `session_id` | `None` | 必填;同上。会话内 `seq` 单调序号的作用域(`_next_seq`,tx_write.py:282) |
| 4 | `event_type` | `None` | 必填(假值即拒);写入时截断 `str(event_type)[:64]` |
| 5 | `payload` | `None` | 事件负载 dict;入库列 `payload_json = _dumps(payload or {})`;参与幂等键计算 |
| 6 | `state_updates` | `None` | **必须是数组**(`None`→`[]`;非 list/tuple 抛 `PBL-PARAM-0001`,tx_write.py:358-359)。逐项交 `_apply_state()`,每项须含 `entity_id`(或 `state_key`)+ `state`;**客户端禁写 `state_version`** |
| 7 | `client_event_id` | `None` | 幂等键来源之一:`idem_key_of(tenant_id, session_id, event_type, payload, client_event_id or idem_key)`(tx_write.py:232) |
| 8 | `idem_key` | `None` | 幂等键显式覆盖(优先级低于 `client_event_id`:`client_event_id or idem_key`) |
| 9 | `snapshot` | `None` | 传入则同事务写 `pbl_world_state_snapshot` 一行 |
| 10 | `base_version` | `None` | 乐观锁期望版本。`None` = 不做冲突校验;不匹配 → `PBL-STATE-CONFLICT` **整体回滚**(tx_write.py:313-318)。**不在 SQL 层做 version CAS**,冲突在 Python 侧裁决 |
| 11 | `actor_id` | `None` | 缺省 `"server"`,截断 `[:32]`,写 `updated_by` |
| 12 | `causation_id` | `None` | 缺省 `""`,截断 `[:32]` |
| 13 | `occurred_at` | `None` | 缺省 `time.strftime("%Y-%m-%d %H:%M:%S")`;真源把它同时写进 `created_at`/`updated_at`/`occurred_at` |
| 14 | `env` | `None` | 宿主 ServerEnv;测试路径由夹具 patch `rtx_db.dbname` 消化,不传 |
**全部 14 个形参默认值均为 `None`**(守卫用例 `test_true_source_signature` 钉死参数名/顺序/默认值,
与本文档同源——任一处漂移即红)。
### 2.1 `state_updates` 单项形状
`_apply_state()`(tx_write.py:293) 逐项校验:
- 非 dict → `PBL-PARAM-0001`「state_updates 每项必须是对象」
- 缺 `entity_id`(且缺 `state_key`)→ `PBL-PARAM-0001`「state_updates 每项必须含 entity_id(客户端禁写 state_version)」;`entity_id` 截断 `[:64]`
- 无 `state`(且无 `state_value`)或值为 `None` → `PBL-PARAM-0001`「state_updates 每项必须含 state」
### 2.2 `read_states` 签名(tx_write.py:466-467)
```python
async def read_states(tenant_id=None, session_id=None, entity_ids=None,
world_id=None, env=None):
```
`tenant_id` 强制;`session_id` 可选过滤;`entity_ids` 可选 IN 过滤;固定 `ORDER BY entity_id ASC LIMIT 500`。
---
## 3. 真源返回键语义(逐键,源码行号锚定)
### 3.1 成功写分支(tx_write.py:456-462,14 键)
| 键 | 语义 |
|---|---|
| `ok` | 恒 `True`(异常一律走 `RtxError`,不返回 `ok=False`) |
| `dedup` | 恒 `False`(未命中幂等) |
| `event_id` | 事件行主键(`tx.insert` 返回) |
| `event_uid` | 幂等键值(`idem_key_of` 产物) |
| `seq` / `seq_no` | 会话内单调序号;`seq_no` 是对外响应**别名**,两者恒等 |
| `cursor` | 广播游标(取 `evt.get("cursor")`,夹具路径下可为 `None`) |
| `state_rows` | `len(states)`(本次写入的状态条数) |
| `states` | `_apply_state()` 返回的逐项列表(键见 §3.4) |
| `latency_ms` | 事务全程裁决耗时(`perf_counter` 差值,`round(...,3)`);SLA 阈值 `SLA_ADJUDICATE_MS=200` |
| `broadcast_ms` | 提交后广播扇出耗时;SLA 阈值 `SLA_BROADCAST_MS=300` |
| `channel` | `broadcast.channel_of(world_id, tenant_id)` → `pbl.world.<tenant>.<world>`(断言取真源产物,不写字面量) |
| `sla_breach` | `_lat.record("adjudicate", ms)` 的返回值(超阈值 True) |
| `stmt_count` | `tx.stmt_count`:**仅统计本事务实际发出的 DML**(幂等命中分支为 1,即只有那次 `SELECT` 后的 commit,见守卫实测) |
### 3.2 幂等命中分支(tx_write.py:380-389 + :441-445,10 键)
键集合 = `{ok, dedup, event_id, event_uid, seq, cursor, state_rows, latency_ms, channel, stmt_count}`
——**没有** `states` / `seq_no` / `broadcast_ms` / `sla_breach`。
- `dedup=True`,`event_id`/`seq` 复用既有事件行(不新写、不重复广播,`state_rows=0`)
- ⚠️ `latency_ms` 初值为 `None`,但真源在返回前统一回填 `dedup_result["latency_ms"] = ms`
(tx_write.py:441-442),**故实际返回数值**。旧断言「幂等分支 `latency_ms is None`」与实现不符,
已在守卫用例改为「数值且 ≥ 0」(本次 QC #4 修正点之一)。
### 3.3 广播与提交次序(不可变契约)
`await tx.commit()` 在 `with` 块内(tx_write.py:428),`hub.publish` 在块外(tx_write.py:449)——
**广播严格晚于提交**,杜绝幽灵更新;广播失败抛 `PBL-BROAD-0001`(数据已提交不回滚,客户端 3s 轮询兜底
`POLL_FALLBACK_MS`)。轨迹断言:`trace_kinds` 中 `index("BROADCAST") > index("COMMIT")`。
### 3.4 `states[]` 单项键(`_apply_state` 返回,tx_write.py:339-340)
`{entity_id, state_version, action, state_key, state_value, seq}`
其中 `action ∈ {"insert","update"}`,`state_version` 由服务端单调递增(新建=1,命中则 `cur+1`),
`state_key`/`state_value` 是 `entity_id`/入参状态的别名(列名映射 `STATE_COL_KEY="entity_id"`、
`STATE_COL_VALUE="state_json"`,tx_write.py:61-62)。
### 3.5 `read_states` 行形状
SELECT 列 `entity_id, state_json, state_version, checksum, updated_by, updated_at`
(tx_write.py:471-472)+ 出参加工:`state`(`_loads(state_json, {})`)、`state_value`(同值别名)、
`entity_id`(列名对齐)。
---
## 4. `RtxError.code` 各码含义
`RtxError(code, msg)` 定义在 `pbl_runtime_ext/rtx_db.py:29`,属性 `.code` / `.msg`,
`str()` 形如 `[PBL-PARAM-0001] xxx`。常量集中在 `m11b_config.py:36-41`。
| code | 常量名 | 抛出点 | 含义与后果 |
|---|---|---|---|
| `PBL-TENANT-0001` | `ERR_TENANT_MISSING` | `_require_tenant` tx_write.py:255 | `tenant_id` 缺失,fail-closed 拒绝读写(读写两侧都校验) |
| `PBL-PARAM-0001` | `ERR_BAD_PARAM` | tx_write.py:356/359/297/300/305 | 必填缺失 / `state_updates` 非数组 / 逐项缺 `entity_id` 或缺 `state` |
| `PBL-STATE-CONFLICT` | `ERR_STATE_CONFLICT` | `_apply_state` tx_write.py:316 | `base_version` 与库中 `state_version` 不符 → **整个事务 ROLLBACK**,两表零增量 |
| `PBL-RTX-0001-APPEND` | `ERR_APPEND_ONLY` | `assert_append_only` tx_write.py:265/268 | 对 `pbl_runtime_event` 发非 INSERT/SELECT,或对基表只读域(`PROTECTED_BASE_TABLES`)写入 |
| `PBL-BROAD-0001` | `ERR_BROADCAST_AFTER_COMMIT` | tx_write.py:454 | 数据已提交但广播失败——**不回滚事实**,由 3s 轮询兜底补齐 |
| `PBL-RTX-0002` | `ERR_TX_UNSUPPORTED` | 仅 `m11b_config.py:39` 定义,`tx_write` 路径当前未抛出 | 预留给「驱动不支持事务」分支;写测试时不要假设它会出现 |
| `PBL-RTX-0003` | 字面量(tx_write.py:179/184) | `build_row` | `models/<table>.json` 不可读,或传入未知列(防静默丢列写空行) |
| `PBL-RTX-0004` | 字面量(tx_write.py:191) | `build_row(full_row=True)` | INSERT 缺 NOT NULL 且无 DB 默认值的列(`required_columns()` 已排除自增列/`NOT NULL DEFAULT` 列) |
| `PBL-RTX-0006` | 字面量(tx_write.py:196) | `build_row(full_row=False)` | UPDATE SET 了不可变列(`tenant_id` / `id` / `created_at`) |
夹具层同名 re-export:`ERR_UNKNOWN_COLUMN="PBL-RTX-0003"`、`ERR_MISSING_NOT_NULL="PBL-RTX-0004"`、
`ERR_IMMUTABLE_COLUMN="PBL-RTX-0006"`(真源以字面量抛出,夹具补常量名供断言,避免测试写魔法字符串)。
---
## 5. 已废弃旧 API 清单与替代
| 废弃符号 | 曾存在位置 | 状态 | 替代(现行唯一路径) |
|---|---|---|---|
| `RuntimeStateEventWriter`(类式垫片) | `tests/_m11b2_compat.py`(M11b-2c 形态) | **已删除**,禁止复活 | `compat.install(RuntimeEngine())` + `compat.apply(**kw)` |
| `configure_writer` | 同上 | **已删除** | 同上(无全局配置态,夹具按用例 `setUp/tearDown` 安装/还原) |
| `build_plan` | 同上 | **已删除** | 直接传 `state_updates=[...]` 给 `apply_runtime_event`;需要看 SQL 用 `update_statements` / `insert_statements` |
| 类式 `write_event_with_state`(夹具转发路径) | 垫片方法 → `world_sync/pbl_runtime_tx.write_event_with_state` | **夹具层禁止使用** | `compat.apply(...)` → 真源 `apply_runtime_event` |
| `_augment` 补假键(`state_inserted` / `expected_version` 等) | 旧垫片 | **已删除** | 直接断言真源返回键(§3),缺键=真源没给,不再由夹具编造 |
> 边界说明(避免误读):`write_event_with_state` 作为**业务实现函数**仍存在于
> `world_sync/pbl_runtime_tx.py:351` 与 `world_sync/pbl_runtime_tx_sqlor.py:126`(宿主连接池版),
> 本任务禁改业务实现,未做任何删除。废弃的是**「把它当测试夹具第二条路径」**这一用法。
> `tests/test_m11b2_wiring.py`(并行任务独占文件)仍 import 该函数,属其自身范围,本任务不碰。
废弃原因(实测,非推测):旧类式垫片把测试钉在「已删除的调用形态 + world_sync 本地同步实现」上,
既没验证现行真源,又与真源返回键不一致,垫片只能靠 `_augment` 补假键维持旧断言,
实测 **22 failed**(`Cannot operate on a closed database.` / `KeyError: 'expected_version'`)。
---
## 6. 机械守卫(旧符号不得复活)
`modules/world_sync/tests/test_m11b2d_fixture_contract.py`(本任务交付物,由一次性探针
`.m11b2d_probe.py` 升级而来,探针已由 `git rm` 移除,删除提交 `81fdb86`):
| 组 | 用例 | 守卫内容 |
|---|---|---|
| `FixtureSurfaceCase` | `test_dead_class_style_symbols_absent` | 4 个旧类式符号不得 `hasattr(compat, ...)` |
| | `test_live_function_style_symbols_present` | 函数式入口 + 真源 re-export 必须齐备 |
| | `test_fixture_apply_is_thin_wrapper_on_true_source` | `compat.apply` 源码内不得出现 `update(`/`setdefault`/`_augment`(零增补) |
| | `test_true_source_signature` | §2 的 14 形参名/顺序/默认值 |
| | `test_read_states_signature` | §2.2 的 5 形参 |
| `ReturnKeysCase` | `test_success_return_keys_exact` | §3.1 键集合逐键 + `states[0]` 键集合(§3.4) |
| | `test_dedup_return_keys_exact_and_no_second_write` | §3.2 键集合 + 不重复写/不重复广播 |
| | `test_read_states_rows_shape` | §3.5 行形状 |
| | `test_snapshot_writes_snapshot_table` | `snapshot` 参数落 `pbl_world_state_snapshot` |
| | `test_broadcast_strictly_after_commit` | §3.3 次序契约 |
| | `test_event_table_is_append_only` | UPDATE 语句不得出现事件表名 |
| `GateErrorCase` | `test_missing_tenant_is_fail_closed` / `test_missing_required_params` / `test_bad_state_updates` / `test_state_conflict_rolls_back` | §4 各码 + 冲突分支回滚不留脏行 |
执行证据:`tests/evidence/m11b2d_a3_fixture_contract_pytest.txt`(**15 passed**)、
`tests/evidence/m11b2d_a3_fixture_contract_collect_only.txt`(collection error = 0)。
---
## 7. QC #1 越权变更的回退记录与移交
**事实**:本任务上一轮收口提交 `32db6aa`(引擎代收口)实际改动了并行任务独占文件
`tests/test_m11b2_single_tx.py`(+23/-,两处:`state_updates` 包装成数组、乐观锁 SET/WHERE 断言口径),
与交付件正文「不改 test_m11b2_single_tx.py」的承诺矛盾。QC 判定成立。
**处置**:本轮以 `git apply -R`(逆向应用 `32db6aa` 中该文件的 diff)**精确剔除本任务的 23 行改动**,
不用 `git restore HEAD^` 之类粗粒度动作——因此**并行任务 A1 自己的 16 行改动(提交 `11aac66`)完整保留**,
未受任何影响。剔除后该文件相对 `81fdb86`(本任务前状态)的差异**只剩 A1 的改动**,可复核:
```
git diff 81fdb86 -- tests/test_m11b2_single_tx.py # 仅 A1 的 3 处外观修正
```
**遗留(移交,不夹带)**:回退后 `tests/test_m11b2_single_tx.py` 实测 **8 failed, 32 passed, 8 xfailed**
(证据 `tests/evidence/m11b2d_a3_single_tx_after_revert.txt`)。失败根因是该文件仍按**旧类式夹具口径**
写断言,与真源不符,需两处修正(本任务无权改,移交该文件独占方 / 另开任务):
1. `TxCase.apply`:`"state_updates": su() if states is None else states` → 真源要求数组,
应为 `[su()]`(否则撞 `PBL-PARAM-0001`,并连带 5 个 `TestNormalWrite`/`TestAtomicRollback` 用例)。
2. `test_update_path_bumps_version_server_side`:真源 `UPDATE` 把新版本写在 **SET 子句**
(`state_version=%s`),`WHERE` 只按 `tenant_id/session_id/entity_id` 定位,**不做 SQL 层 CAS**;
旧断言「SET 不含 state_version」与实现相反,且参数序为
`(state_json, state_version, checksum, updated_by, updated_at, tenant_id, session_id, entity_id)`。
本任务**只登记不修**,避免再次越权。
---
## 8. 后续任务引用规约(硬性)
1. **新测试写运行时事务**:一律 `import _m11b2_compat as compat` → `compat.install(compat.RuntimeEngine())`
→ `compat.apply(...)` / `compat.read(...)`;`addCleanup(compat.uninstall)`。**不得**新建类式垫片、
不得 import `world_sync.pbl_runtime_tx.write_event_with_state` 作为夹具入口。
2. **表名 / 错误码 / 通道名**:只从 `compat` 或真源常量取(`compat.EVENT_TABLE`、`compat.ERR_BAD_PARAM`、
`rtx_broadcast.channel_of(...)`),**禁止第二份字面量**。
3. **返回键断言**:以本文档 §3 为准;新增键须先改真源、再同步 §3 与守卫用例(三者不一致即红)。
4. **`occurred_at` 必须逐事件唯一**:事件表两条唯一索引含分区列 `created_at`,同秒两条会撞 DB 唯一索引
(真源不填 `idem_key`,DEFAULT `''`)。用各测试文件自带的 `stamp()` 取互不相同时间戳。
5. **乐观锁**:只在 Python 侧用 `base_version` 表达期望,**不要**在 SQL 里找 version CAS。
6. **广播**:断言 `BROADCAST` 轨迹晚于 `COMMIT`;广播失败期望 `PBL-BROAD-0001` 且**数据仍在库**。
7. **夹具不可改**:`tests/_m11b2_compat.py` 既有符号只读;需要新能力时在本文件**追加**并同步本文档 §1,
不改既有签名与语义。
8. **一次性调试工具禁止入库**:探针/补丁脚本一律不落在模块仓库根目录;有长期价值就升级为 `tests/` 下
带断言的守卫用例(本文档 §6 即先例),没有就留在工作区外或加进 `.gitignore`。
### 证据索引(消 QC #3「张冠李戴」)
| 文件 | 跑的是什么 | 归属 |
|---|---|---|
| `tests/evidence/m11b2d_a3_fixture_contract_pytest.txt` | `test_m11b2d_fixture_contract.py -v`,15 passed | **本任务交付物执行证据** |
| `tests/evidence/m11b2d_a3_fixture_contract_collect_only.txt` | 同上 `--collect-only -q` | 本任务可收集性证明 |
| `tests/evidence/m11b2d_a3_single_tx_after_revert.txt` | `test_m11b2_single_tx.py -q --tb=no`,8 failed | 本任务越权回退后的真实状态(§7 移交依据) |
| `tests/evidence/m11b2d_a1_pytest.txt` / `m11b2d_a1_collect_only.txt` | `test_m11b2_single_tx.py`(48 项) | **并行任务 M11b-2d-A1 的产物证据**,非本任务交付物;本任务未改名(改名会二次触碰 A1 归档),在此登记归属 |