world_sync/docs/work-log-2026-09-22-m11b2c.md

189 lines
22 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.

# Work Log — 2026-09-22 · M11b-2c:world_sync 单事务库函数(write_event_with_state / read_entity_state)
- 模块:`world_sync`(宿主镜像/消费方侧,运行时事件与实体状态单事务写入器)
- 里程碑:**M11b-2**(单事务写两表);上游真源模块 `pbl_runtime_ext`(表结构与 `tx_write/tx_read` 能力权威源)
- 本轮任务:`[M11b-2c-T2-a] 写 dev-notes.md 追加章 + work-log 正文`(task_id `XvEexvh10D4dfkvR3xkkf`,role=agent.develop,task_kind=new_dev,父任务 `[M11b-2c-T2] kICTWazBqgIWtKNO7sz8A`)
- 本文按 module-development-spec「Work Logs 强制项」产出:**改了什么 / 数据模型 / 契约接口 / 陷阱与裁决 / 验证证据**(依次对应 §0、§1、§2、§3、§4)。
- 范围纪律:本轮**只写文档正文**,未修改任何业务代码 / `models/*.json` / `scripts/load_path.py`(RBAC 登记归 T3;本里程碑本无新 HTTP 路径,见 §2.3)。
## 0. 本轮(T2-a)写作范围与纪律
| 项 | 口径 |
|---|---|
| 本轮只做 | 写实质正文:本文(模块工作日志)+ `projects/pbls/docs/02-develop/dev-notes.md` 的 `[M11b-2c]` 章 |
| 本轮不做 | 不产出 `code_files.md`(归 `[M11b-2c-T2-b]`,task_id `EBIrHXlTuUkKYl7dPdYgo`);不落盘 `measure.log`、不写 fallback 交付摘要(归 `[M11b-2c-T2-c]`,task_id `UFjoRchqgo8asHC3rP5he`) |
| git 事实陈述 | 按 develop 阶段规范,**本模块仓库** `modules/world_sync` 的文档改动由本轮做**选择性提交**:`git add docs/work-log-2026-09-22-m11b2c.md` 单文件(禁 `git add -A`、禁 add `wwwroot/`),本轮零代码改动故该笔只含此一个文件;**commit hash 不写进本文**(写进即自指竞态),由收口轮 T2-c 现场取 `git -C modules/world_sync log -1 --format=%H` 并连同 `git show --stat` 落 measure.log `[git]` 段。**`projects/pbls` 项目过程文档仓不由本角色提交**,其 dev-notes.md 等仍为未提交状态,待 PM 审核通过后统一提交 |
| 自指竞态 | 本文**不声明自身字节数**;凡涉及 `dev-notes.md` / 本文 / `code_files.md` 三者尺寸之处,一律写「见 measure.log `[final]` 段(由收口子任务落盘)」。代码文件本轮零改动,其字节数可现场 `wc -c` 实测写死(见 §6.2) |
| 占位符 | 全文无字母尺寸槽、无 40 位 `0` 串 hash 槽 |
## 1. 数据模型
M11b-2 写路径涉及两张表,**结构权威源在 `pbl_runtime_ext/models/`**,`world_sync/models/` 下是**消费方内联副本**(不是引用式声明——`database-table-definition-spec` 只认 summary/fields/indexes/codes 四段式,`scripts/validate_models_json.py` 对 `key=schema` 引用式写法直接判 FAIL,QC #5 结论)。
### 1.1 `pbl_runtime_event`(运行时事件,append-only)
- 真源:`modules/pbl_runtime_ext/models/pbl_runtime_event.json`;副本:`modules/world_sync/models/pbl_runtime_event.json`(8782B,25 列)
- 一致性:由 `scripts/m11b2b_column_diff.py` + `scripts/m11b2_selftest.py` 机械核对,`mirror-sha256-equal` 断言真源与 `apps/scense/pkgs/world_sync` 镜像**字节级一致**(本次重跑 PASS)。
- 语义要点:
- **append-only**:写路径只做 INSERT,不 UPDATE / 不 DELETE(`TestScopeAndSqlContract::test_event_sql_is_insert_only` 守这条)。
- `event_uid` 是幂等/重试定位键(默认 uuid4 hex,可由入参指定以便测试因果链);旧口径的 `event_id` 已废弃。
- `state_version` 与状态行生效后的版本号**对齐**(同事务内第 4 步算出再写第 5 步),供 M11b-3 广播去重与 M11b-4 轮询增量拉取。
- `seq_no` 严格单调递增(P99 探针断言 PASS);`source` 默认 `world_sync.m11b2`;`broadcast` 标记位由 M11b-3 消费。
- `payload` 以 JSON **文本**入库(`json.dumps` 后写 `text` 列;抽象类型归一 `json→text`、`bigint→long`,不保留物理修饰)。
### 1.2 `pbl_entity_state`(实体状态,服务端权威版本)
- 真源:`modules/pbl_runtime_ext/models/pbl_entity_state.json`;副本:`modules/world_sync/models/pbl_entity_state.json`(6099B)
- 列集(真源实测 **10 列**,早期「8 列」口径作废):`tenant_id / id / session_id / entity_id / state_json / state_version / checksum / updated_by / created_at / updated_at`
- 定位键 = 唯一索引 **`uk_es (tenant_id, session_id, entity_id)`**,与 `write_event_with_state` / `read_entity_state` 的 WHERE 完全一致(**没有** `world_id` 列,`world_id` 仅作历史兼容入参被忽略)。
- `state_version` 单调 +1,**客户端禁写**;`state_json` 存完整新状态(非 diff)。
- `id` 类型偏离:真源 `BIGINT AUTO_INCREMENT`,world_sync 副本内联为 `str/length=32`(spec 合规写法),主键由应用层生成 32 位 uuid。**写路径 INSERT 不显式提交 id**——生产 MySQL 交给真源 DDL 的 AUTO_INCREMENT,本地/测试 sqlite DDL 用应用层生成值。此偏离为有意选择(副本侧不走「登记放行 id 非 str(32)」路线,因为本任务范围禁改校验器脚本),评审勿判结构缺陷。
- 主键只在 `summary[0].primary=["id"]` 单点声明,`indexes[]` 内不出现 `PRIMARY` 条目(spec 规则 `index_name_primary`)。
## 2. 契约接口
### 2.1 两个库函数(M11b-2 的全部对外面)
```python
# world_sync/pbl_runtime_tx.py:351
def write_event_with_state(event_data=None, state_update=None, conn_factory=None,
conn=None, event_uid=None, source=None, clock=None) -> dict
# world_sync/pbl_runtime_tx.py:310
def read_entity_state(conn_factory=None, tenant_id=None, session_id=None,
entity_id=None, world_id=None, conn=None) -> dict | None
```
- `event_data` 必填:`tenant_id` / `entity_id` / `event_type`;可选 `payload`、`session_id`、`scene_id`、`world_id`、`actor_id`、`causation_id`、`idem_key`、`seq`/`seq_no`、`tx_group`、`source`、`broadcast`。
- `state_update` 必填:`tenant_id` / `session_id` / `entity_id`;新状态键 `state`(或 `state_json`,dict 或 JSON 文本皆可);可选 `expected_version`(乐观锁)、`updated_by`。**传 `None` = 只写事件不改状态**(返回 `action="event_only"`)。
- 成功返回:`{event_uid, state_version, action: updated|inserted|event_only, elapsed_ms, isolation_level_applied, dialect, ...}`。
- `elapsed_ms` 是 P99 统计的采样来源;
- `isolation_level_applied=False` 在 sqlite 下是**正常**(无该概念),MySQL/PG 下应为 True。
- `read_entity_state` 返回 `{"state": ..., "state_version": int, "updated_at": ...}`,查不到返回 `None`;它是**独立短事务**,不与写入事务共用(避免长事务占行锁),供上层冲突后重读版本再重试。
- 复用 `conn` 时仍由本函数负责该连接的 commit/rollback,**调用方不要自行 commit**。
### 2.2 三处同步注册(module-development-spec 铁律 4,已 grep 核验)
| 环节 | 位置 |
|---|---|
| ① 实现 | `world_sync/pbl_runtime_tx.py:310,351`(模块 `__all__` 已导出) |
| ② 包导出 | `world_sync/__init__.py:119-120`(import)+ `:209-210`(`__all__`) |
| ③ 宿主挂载 | `world_sync/init.py:513-514` → `env.write_event_with_state` / `env.read_entity_state` |
适配层 `world_sync/pbl_runtime_tx_sqlor.py:126,154` 提供同名薄包装(宿主连接池接线,**不重复实现事务逻辑**)。
### 2.3 无 HTTP 端点(重要)
M11b-2 **不新增** `.dspy` / `.ui`,因此 `scripts/load_path.py` 中**没有**这两个函数的权限条目,也不该有——它们是 Python 库函数,经 `load_world_sync()` 挂到 `ServerEnv` 供宿主调用,不走 HTTP 路由,不存在 401/403 面。`wwwroot/api/README-N-A-M11b2.md` 是「Not Applicable」的显式留档。正式声明见 `projects/pbls/docs/02-develop/m11b2c-rbac-path-declaration.md`。
### 2.4 事务与冲突契约(详版在 dev-notes.md `[M11b-2c]` 章 §2)
`BEGIN`(sqlite `BEGIN IMMEDIATE`)→ 会话级 `READ COMMITTED`(MySQL/PG;失败降级不阻断,靠乐观锁兜底并透出标志)→ 读 `state_version`(MySQL/PG 加 `FOR UPDATE`)→ `expected_version` 预检 → 状态 CAS 更新/INSERT → 事件 INSERT → `COMMIT`;任一步失败 `_rollback_quiet` + 明确异常(`ConcurrentStateConflict` / `StateWriteError` / `EventWriteError` / `TxAbortError` / `TxConfigError` / `ConnFactoryNotConfigured`),**两表 0 变更**,绝不吞错。
## 3. 陷阱(本模块改动前必读)
1. **别在 world_sync 侧建表**。DDL 归 `pbl_runtime_ext`;`world_sync/models/*.json` 只是消费方契约视图。改真源必须同步本副本并重跑 `m11b2b_column_diff.py` + `m11b2_selftest.py`。
2. **列名不许写死**。列集由 `pbl_runtime_authority` 从真源机械导出,`_assert_contract()` 在建 SQL 之前 fail-closed(`AuthorityResolutionError`)。新增列只改真源 JSON + 同步副本,代码里出现字面量列名清单 = 双写分叉隐患。
3. **定位键是 `tenant_id + session_id + entity_id`**,不是 `world_id`(表里没这列)。传 `world_id` 只被忽略——别指望它做隔离。
4. **`expected_version` 两级校验缺一不可**:预检给友好错误(带 expected/actual),CAS(`rowcount != 1`)防预检与写入之间的窄窗口。只留预检会丢失更新;只留 CAS 则冲突时报错退化为「受影响行数 0」的无语义失败,调用方无法自助重试。
5. **状态行 INSERT 不带 `id`**(见 §1.2 偏离说明),且 UNIQUE 列空串必须归一为 `None`(`''` 会互相撞唯一键)。
6. **写序是先状态、后事件**。反过来会让事件携带一个可能被回滚的 `state_version`,M11b-3 广播与 M11b-4 轮询都会拿到脏版本。
7. **`isolation_level_applied=False` 不等于失败**(sqlite 无该概念)。测试断言要按 dialect 分支;MySQL/PG 下若为 False 先查账号权限/版本。
8. **`conn_factory` 三级优先级**:显式注入(可传 DB URL)→ 复用 `conn` → 宿主 `ServerEnv` 连接池(`POOL_KEYS=("sqlor_pools","db_pools","pools","sqlorPools")`,库名键 `("pbl","world_sync","default")`)。拿不到池就抛 `ConnFactoryNotConfigured`,**禁止**回落到裸连或读 `env/{test,prod}.json` 凭据(project-directory-spec §8.1)。
9. **`m11b2_selftest.py` 的 `cross-check-sql` 检查器**:M11b-2c 重构后 `EVENT_COLUMNS = columns_of(EVENT_TABLE)`(运行时派生),旧正则抓字面量元组会误报「未能解析」。已由 commit `6532d8f` 改为三级解析(A 字面量 / B1 运行时 import / B2 静态回落)。再遇到同类 FAIL 先怀疑检查器而非业务代码。
10. **遗留测试文件是技术债,不是验收依据**:`tests/test_m11b2_single_tx.py`、`tests/test_m11b2_wiring.py` 的 29 条失败源于脚手架 DDL 与真源不一致、仍用已废弃的 `event_id` 关键字、缺显式 dialect、`init.py` 相对路径等(逐条归因见 `projects/pbls/docs/02-develop/m11b2c-tests-result.md` §4.1)。等价覆盖已在 `pbl_runtime_ext/tests/test_m11b2c_runtime_ext.py`(9 passed,含 2 条 P99 门禁与 1 条冲突回滚干净性)。重写需单列 rework 任务,本轮**未修改这两个文件**。
11. **广播/轮询不在此模块的 M11b-2 路径里**:post-commit 广播由 `init.py` 的 hook 把 `write_event_with_state` 包一层(M11b-3),3s 轮询兜底在 `pbl_runtime_polling.py`(M11b-4)。摘掉这两层不影响 M11b-2 写入正确性——排查时先确认看到的是哪一层。
12. **P99 数据是离线 sqlite/内存引擎实测**,不含网络 RTT 与真实 InnoDB 锁等待,只能作为逻辑时延下界;上线后在测试环境按 `pbl_runtime_latency.py` 口径复测。
13. **冲突后的重试姿势**:捕获 `ConcurrentStateConflict` → `read_entity_state` 重读最新 `state_version` → 以该值为新 `expected_version` 重跑 `write_event_with_state`。**不要**盲目循环重试(版本被他人持续推进时应向上报错)。异常族行号见 §6.2。
## 4. 验证证据(本次实跑)
```
$ cd modules/world_sync && .venv/bin/python scripts/m11b2_selftest.py # exit code 0
PASS [mirror-sha256-equal] pbl_runtime_event 真源与镜像字节级一致 -> True
PASS [mirror-sha256-equal] pbl_entity_state 真源与镜像字节级一致 -> True
断言合计 554:PASS 554 / FAIL 0
RESULT: PASS
```
- 全文落盘:`projects/pbls/docs/02-develop/m11b2c-t2-selftest-rerun.log`(64695B)
- 真源侧:`pbl_runtime_ext` pytest **9 passed / 0 failed**(`m11b2c-tests-pbl-runtime-ext-raw.log`);`m11b_selftest.py` **总计 110 项,PASS 110 / FAIL 0 → RESULT: PASS**(`m11b2c-tests-selftest-raw.log`)
- P99(`m11b2c_p99_probe.py`,n=60,SLA 200ms,`m11b2c-tests-p99-probe-raw.log`):`write_event_with_state[INSERT分支] P99=0.2414ms` / `read_entity_state P99=0.2241ms` / `write_event_with_state[UPDATE分支] P99=0.4984ms`,三条均 PASS;附带 `seq_no` 严格递增 PASS、UPDATE 分支 60 次覆写后 `state_version` 单调至 61、读回 `entity_id` 一致性 PASS
- 本模块 pytest:`29 failed / 56 passed`(`m11b2c-tests-pytest-raw.log`)——**遗留测试脚手架漂移,非业务缺陷**,定性依据见 §3 陷阱 10
- 本轮为纯文档轮,未改代码,故无新增编译/端点验证;上述数字全部来自磁盘真实留证日志,可逐条复核
## 5. 关联文档
- 应用级开发说明(本里程碑正文):`projects/pbls/docs/02-develop/dev-notes.md` → `[M11b-2c] world_sync 库函数` 章
- 真源侧:`dev-notes-m11b2c-runtime-ext.md`;迁移:`dev-notes-m11b2a-world-sync-migration.md`;A3 修复:`m11b2c-a3-fix-notes.md`
- RBAC 口径:`m11b2c-rbac-path-declaration.md`;实跑留证:`m11b2c-tests-result.md` 及各 `m11b2c-tests-*-raw.log`
- 模块技能:`modules/world_sync/skill/SKILL.md`;上一份工作日志:`modules/world_sync/docs/work-log-2026-09-20.md`
## 6. 本轮(T2-a)补充:文档收口约定与实测锚点
### 6.1 与前几轮的差异(为什么本文不再有「第三轮刷新/收口」章节)
本模块文档此前几轮把「写作」与「测量取证 + 数字回填 + 提交」混在同一轮做,结果反复在回填一步中断,交出的文档里留下字母尺寸槽与 hash 占位串,且出现「文档声称已提交而磁盘 `git status` 仍是未提交」的陈述与事实矛盾。本轮按 PM 拆分把两件事彻底分离:
- **T2-a(本轮)**:只写正文(本文 + dev-notes `[M11b-2c]` 章)。凡尺寸类自指/互指信息一律改为「见 measure.log `[final]` 段」的引用约定,不写死数字、不留占位串;本模块仓按规范做单文件选择性提交(见 §0 与 §6.4)。
- **T2-b**:产出 `code_files.md` 文件清单(代码文件实测字节数 + `grep -n` 行号锚点)。
- **T2-c**:重跑测量落盘 measure.log(`[final]` / `[git]` 两段)→ 回填**非自指**尺寸 → 写 fallback 交付摘要。
因此本文不声明自身字节数,也不声明 `dev-notes.md` / `code_files.md` 的字节数。
### 6.2 本模块文件锚点(T2-a 现场实测,代码零改动故可直接对账)
| 文件 | 字节数(`wc -c` 实测) | 锚点(`grep -n` 实测) |
|---|---|---|
| `world_sync/pbl_runtime_tx.py` | 23584 | `:296 _set_isolation` / `:310 read_entity_state` / `:351 write_event_with_state` / `:517 isolation_level_applied` |
| `world_sync/pbl_runtime_sql.py` | 8872 | `:136 begin_stmt` / `:148 read_committed_stmt` / `:157 for_update_clause` |
| `world_sync/pbl_runtime_errors.py` | 4819 | `:35 PblRuntimeError` / `:71 TxConfigError` / `:78 ConnFactoryNotConfigured` / `:85 EventWriteError` / `:92 StateWriteError` / `:99 ConcurrentStateConflict` / `:110 TxAbortError` |
| `world_sync/pbl_runtime_tx_sqlor.py` | 5956 | `:126 write_event_with_state` / `:154 read_entity_state` |
| `world_sync/__init__.py` | 9156 | `:119-120` 导入、`:209-210` `__all__` |
| `world_sync/init.py` | 25922 | `:513 env.write_event_with_state`、`:514 env.read_entity_state`、`:587` M11b-3 广播钩子二次覆盖同名挂载 |
| `models/pbl_runtime_event.json` | 8782 | 25 列,四段式合规 |
| `models/pbl_entity_state.json` | 6099 | 10 列,`uk_es=['tenant_id','session_id','entity_id']` |
| `scripts/m11b2_selftest.py` | 18932 | 554 条断言(§4) |
真源侧 `modules/pbl_runtime_ext`:`pbl_runtime_ext/tx_write.py`(35357B,`:344 apply_runtime_event` / `:466 read_states`)、`scripts/m11b2c_p99_probe.py`(P99 探针)、`pbl_runtime_ext/latency.py`(`SLA_P99_MS=200.0`)。
> 行号口径统一:`read_committed_stmt` 一律记 **`:148`**(本轮 `grep -n '^def read_committed_stmt' world_sync/pbl_runtime_sql.py` 实测),dev-notes `[M11b-2c]` 章 §2.2 同口径。
### 6.3 遗留缺口(供 PM 编排)
1. `tests/test_m11b2_single_tx.py` / `tests/test_m11b2_wiring.py` 共 29 条漂移用例需单列 rework(等价覆盖已在真源侧全绿,不阻塞 M11b-2 验收)。
2. 真实 MySQL/PG 下的端到端时延与 `isolation_level_applied=True` 验证需 deploy_test 连库补测(本地 sqlite 无该概念,见 §3 陷阱 7/12)。
### 6.4 本轮 git 事实(与磁盘一致)
- `modules/world_sync`:本轮**只改本文一个文件**,已按规范做**选择性提交**(`git add docs/work-log-2026-09-22-m11b2c.md`,禁 `git add -A`、禁 add `wwwroot/`)。提交信息前缀 `docs(M11b-2c-T2-a):`。hash 与 `git show --stat` 原样输出由收口轮 T2-c 落 measure.log `[git]` 段,本文不写死。
- `projects/pbls`(dev-notes.md 所在的过程文档仓):**本阶段不提交**,待 PM 审核通过后统一提交。
- 本文不含任何 hash 槽位(无字母槽、无 40 位 `0` 串),也不声称「两仓均已提交」。
### 6.5 复杂度评估(本轮)
**简单**——单交付单元(两份文档正文写作),零代码改动、零表定义改动、无跨模块影响,**无需拆分**。父任务 `[M11b-2c-T2]` 整体为复杂(多独立交付单元 + 串行收口依赖),已由 PM 拆为 T2-a / T2-b / T2-c 三子任务并按批次派发,口径见 §6.1。
---
## 7. 收口轮(T2-c)回填笔 · 2026-09-22
由 `[M11b-2c-T2-c]`(task_id `UFjoRchqgo8asHC3rP5he`)追加。本文自身尺寸不写死(自指竞态),见
`projects/pbls/docs/02-develop/m11b2c-t2-code-files-measure.log` `[final]` 段;以下只回填**对方**实测值与**已存在**的真实 hash。
| 项 | 真值 | 口径 |
|---|---|---|
| `projects/pbls/docs/02-develop/dev-notes.md` 字节数 | 61340 | `wc -c projects/pbls/docs/02-develop/dev-notes.md`(T2-c 回填后终值,对方文件非自指;实测时点 2026-09-22T21:22:15+0800) |
| `kICTWazBqgIWtKNO7sz8A_code_files.md` 字节数 | 22717 | `wc -c projects/pbls/deliverables/agent.develop/kICTWazBqgIWtKNO7sz8A_code_files.md`(同上) |
| `modules/world_sync/docs/work-log-2026-09-22-m11b2c.md` 自身字节数 | 见 measure.log `[final]` 段 | **自指 → 不写死** |
| 本文件 T2-a 选择性提交 hash | `da37362628ad6b51425c3de348ce8a0a0737a66a` | `git log -1 --format='%H %s' -- docs/work-log-2026-09-22-m11b2c.md`(只含本文件一笔,无 wwwroot、无代码) |
| 本文件 T2-c 收口笔 hash | `376662cc92076e4d6c9220c7aa5d3f6b09634658`(该笔已完成,引用它不构成自指竞态;本行之后的 T2-c 更正笔 hash 属自指项,只落 measure.log `[receipt]` 段) | `git -C modules/world_sync show --stat --format='%H %ad %s' --date=iso 376662cc92076e4d6c9220c7aa5d3f6b09634658` |
| `read_committed_stmt` 定义行 | `world_sync/pbl_runtime_sql.py:148` | T2-c 重跑 `grep -n '^def read_committed_stmt'` 复核,与 §6.2 同口径 |
> **数字口径说明(T2-c)**:上表尺寸为**回填后终值**(三份文档编辑收敛到固定点后当场 `wc -c` 读数),与 `projects/pbls/docs/02-develop/m11b2c-t2-code-files-measure.log` `[final]` 段同批同源;上一轮把 20:46:05 首跑的回填前基线(54239 / 18269 / 20047)标为「回填时点实测」属口径标注不实,现予更正。本文自身尺寸仍不写死。
关键词逐词 `grep -c`(本文):`M11b-2` 19 / `expected_version` 5 / `READ COMMITTED` 2 / `P99` 7 / `无新 HTTP 路径` 2。
§6.4 第一条「hash 由收口轮落 measure.log、本文不写死」的约定仍然成立;本表补的是**上一笔已存在**的 T2-a hash(不随本文变动而失效)。
`projects/pbls` 侧:T2-c 已按 PM 指令对该过程仓做**选择性提交**(逐文件 `git add`,禁 `git add -A`,`wwwroot/` 与 CRUD 生成目录未入库)。**T2-c 命名收口笔实测共 5 笔**(本模块 1 笔 + pbls 4 笔):world_sync `376662cc92076e4d6c9220c7aa5d3f6b09634658`(本文件,1 文件);pbls 文档笔 `6578d5c49721ecc20d8758bbd768ad279c783c30`(dev-notes.md + code_files.md)、取证笔 `d6661da6644d12cd4425e5015393bc8d1bbe1057`(measure.log `[post-commit]` 段,1 文件)、fallback 收口笔 `732a84c817984ded7f369159e5adc67d04551f7b`(两份 fallback,2 文件)、回执笔(measure.log `[receipt]` 段,末笔不自称 hash)。上一版本此处写「共两笔(文档笔 + 取证笔=measure.log+fallback 合一笔)」与 git 历史不符,现按 `git -C projects/pbls log --grep='T2-c'` 实测更正;并澄清 `f3ecbbae3091dbeeec8a871ae1b0520fc45c0000`(一次 add 12 文件、混入他任务 deliverables 与 m9-closureB 文件)系代收口混笔,本任务不认领。各笔 `git show --stat` 原样输出见 measure.log `[git]` / `[post-commit]` / `[receipt]` 段。各笔真实 40 位 hash 与 `git show --stat` 原样输出见 measure.log `[git]` / `[post-commit]` 段(measure.log 在文档定稿后写入,故可承载他笔 hash);本文自身的 T2-c 收口笔 hash 属自指项,同样只落该段(复核:`git -C modules/world_sync log -1 --format='%H %s'`)。