From 881d50a6692c34e001f7bcba603bb5c55b87e8c1 Mon Sep 17 00:00:00 2001 From: "agent.develop" Date: Tue, 22 Sep 2026 11:56:06 +0800 Subject: [PATCH] =?UTF-8?q?deliver:=20=E4=BA=A4=E4=BB=98=E6=94=B6=E5=8F=A3?= =?UTF-8?q?=EF=BC=88=E5=BC=95=E6=93=8E=E4=BB=A3=E4=B8=BA=E6=8F=90=E4=BA=A4?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/work-log-2026-09-22-m11b2c-a1-qc12.md | 53 +++++++++ scripts/m11b2c_a3_replay_check.py | 119 +++++++++++++++++++++ 2 files changed, 172 insertions(+) create mode 100644 docs/work-log-2026-09-22-m11b2c-a1-qc12.md create mode 100644 scripts/m11b2c_a3_replay_check.py diff --git a/docs/work-log-2026-09-22-m11b2c-a1-qc12.md b/docs/work-log-2026-09-22-m11b2c-a1-qc12.md new file mode 100644 index 0000000..f092468 --- /dev/null +++ b/docs/work-log-2026-09-22-m11b2c-a1-qc12.md @@ -0,0 +1,53 @@ +# Work Log — 2026-09-22(M11b-2c-A1:回退越权核查 + QC #12 根因表述修正) + +- **模块仓库**: `modules/pbl_runtime_ext`(PBL scense_runtime 薄扩展,M11a/M11b 单事务事件+状态写入与广播) +- **任务**: [M11b-2c-A1] 回退越权改动并在夹具层收敛 PBL-RTX-0004(QC 第 3 次退回,核心意见 #12) +- **执行环境**: Python 3.10.12 / pytest 9.1.1 / venv=`.venv`(已 `pip install -e .`) +- **起始 HEAD**: `ecc7f23 deliver: 交付收口(引擎代为提交)` +- **结束状态**: `git status --short` 为空(工作区干净);`9 passed, 0 failed` + +## 1. 本轮时间线(命令与实测结果) + +| # | 命令 | 结果 | 退出码 | +|---|------|------|--------| +| 1 | `git status --short` / `git diff HEAD --stat -- pbl_runtime_ext/tx_write.py` | 均无输出 ⇒ 工作区与 HEAD 一致,**无本轮未提交的业务越权改动残留** | 0 | +| 2 | `.venv/bin/python -m py_compile pbl_runtime_ext/tx_write.py` | OK | 0 | +| 3 | `python3 -c "...[(x['name'],x.get('null')) for x in json.load(open('models/pbl_entity_state.json'))['fields']]"` | 四列 `tenant_id/session_id/entity_id/created_at` 全部 `null=False`(QC #12 要求的取证) | 0 | +| 4 | 旧/新 `required_columns()` 判据对两表实测对比 | state 表 OLD==NEW(旧实现本就能识别);event 表 OLD=`[]` → NEW=`['event_uid','event_type']` | 0 | +| 5 | `grep -rn "state_updates" scripts/ tests/` | 14 处调用点,`state_key/state_value` 形态正确,四列非 state_updates 合法入参 | 0 | +| 6 | `.venv/bin/python -m pytest tests/ -v --tb=short` | **9 passed in 0.41s**(0 failed) | 0 | +| 7 | `.venv/bin/python scripts/m11b_selftest.py` | 总计 110 项,PASS 110,FAIL 0,RESULT: PASS | 0 | + +## 2. QC #12 响应(核心) + +旧交付件根因判定 #3 表述错误,已删除并重写,要点: + +1. **前提错**:`pbl_entity_state` 四列并非「无 NOT NULL 声明」,实测均为 `{"null": false}`;`type` 标注 `str/bigint/str/datetime` 与声明一致。 +2. **结论错**:旧 `required_columns()` 判据 `fd.get("null") is False`(见 `scripts/m11b2c_a3_patch_tx_write.py` `OLD_1` 第 23 行)**识别的正是 `null: False` 写法**,对 state 表这四列能正确判为必填 ⇒ 不存在「nullable 写法识别缺失」。实测 OLD 与 NEW 对 state 表返回集合完全相同。 +3. **推论删除**:「夹具无论怎么补建表列都无法消除该报错」基于错误前提,已删去。 +4. **归因统一**:state 表 PBL-RTX-0004 误报唯一归因到 #4 —— `_apply_state()` UPDATE 分支沿用 INSERT 全行校验,调用方按 UPSERT-UPDATE 语义不传定位维度(`tenant_id/session_id/entity_id` 走 WHERE)与 `created_at`(保持建档原值)→ 被误判缺列。 +5. **nullable 识别缺失的真实归属**:只影响 `pbl_runtime_event`(该表用规范写法 `nullable: "no"`,旧判据读不到 → OLD=`[]` → 事件表 INSERT 门禁**恒真 = 漏报**)。误报(state)与漏报(event)两向缺陷已在结论中分表列清,不再混谈。 + +修正后复跑 pytest = **9 passed**,符合 QC #12 验收要求。 + +## 3. 关键技术判定与偏差上报 + +- **不回退 `5542cf5` 的业务改动**:该 commit 是 PBL-RTX-0004 的唯一有效修复。若回退,`build_row()` 恢复「UPDATE 也跑 INSERT 全行校验」,T3 必然重新失败(回到 `3 failed, 6 passed`),与验收标准「`X passed, 0 failed`」冲突。 +- **夹具层无可补之处**(三条路径全部实测排除):① `tests/conftest.py` 只注入 sys.path,不建表,无「夹具建表缺列」;② `state_updates` 调用参数形态正确,四列不是其合法入参;③ models 两表 NOT NULL 声明均完整。⇒ 属「夹具/调用参数已正确仍报缺列」的业务实现缺陷,任务描述中「夹具层可收敛」的前提不成立,已如实记录。 +- **禁止整文件 `git checkout` 已遵守**:未执行任何 checkout/restore,避免误删前轮合法改动(删 append-only 死代码 + build-system 规范化)。 +- 修复后语义:`build_row(full_row=False)` 表达 UPDATE SET 子集语义并禁止 SET 不可变列(`tenant_id/id/created_at`,违者 `PBL-RTX-0006`);`_is_not_null()` 双写法识别 + `_has_db_default()` 排除自增/DEFAULT 列。 + +## 4. 允许范围与产出边界 + +- 允许修改的四个夹具文件(`scripts/m11b_selftest.py`、`scripts/m11b2c_p99_probe.py`、`tests/conftest.py`、`tests/test_m11b2c_runtime_ext.py`)本轮**零改动**;`pbl_runtime_ext/*.py` 业务实现与 `json/` 表定义本轮**零改动**。 +- 本轮产出仅两份文本:本工作日志 + `.m11b2c_tmp/m11b2c_a1_conclusion.md`(待归档文本,`.m11b2c_tmp/` 已在 `.gitignore` 登记)。 +- **未创建、未覆盖 `projects/pbls/docs/02-develop/` 下任何 log 终稿**(留证与交付件由 A2 承接)。 + +## 5. 环境限制说明 + +- 本机 MySQL/MariaDB 走 socket 不可用(`/run/mysqld/mysqld.sock` 不存在),实库 DDL 装载 runner `tests/run_m11b1a_localdb.sh` 需 TCP `127.0.0.1:3306` 沙箱库;本轮验证以离线 harness + 内存引擎 + selftest 110 项为准,实库回归归 A2/部署阶段。 + +## 6. 分支/提交状态 + +- 分支:`master`(HEAD `ecc7f23`);工作区干净,本轮无新增未提交改动。 +- git 收口由引擎代为执行,本日志不自称已 commit/push。 diff --git a/scripts/m11b2c_a3_replay_check.py b/scripts/m11b2c_a3_replay_check.py new file mode 100644 index 0000000..93f6d4a --- /dev/null +++ b/scripts/m11b2c_a3_replay_check.py @@ -0,0 +1,119 @@ +# -*- coding: utf-8 -*- +"""M11b-2c-A3 交付自洽性重放校验器(可复跑,QC 复核用)。 + +背景(QC 退回 #10):上一轮引擎在交付时又代为提交了一次补丁器修正(51c3f26), +说明 agent 交付的仓库状态并非最终自洽状态——补丁器与定稿 tx_write.py 不一致, +靠引擎收口补齐。本脚本把「HEAD 即最终态」变成可机械复验的断言。 + +做法(不污染工作区,全部在临时目录内完成): + 1. 从 git 取 HEAD 版本的补丁器 scripts/m11b2c_a3_patch_tx_write.py; + 2. 从 git 取 5542cf5^(修复前)版本的 pbl_runtime_ext/tx_write.py 原文; + 3. 在临时目录里按补丁器自身的 TARGET 相对路径摆好文件,直接 import 并调用其 + main()(等价于在机构工作空间根执行该补丁器); + 4. 把重放产物与 HEAD 版本的 tx_write.py 逐字节比对(cmp + sha256 双口径); + 5. 一致 -> REPLAY_IDENTICAL: YES,退出码 0;不一致 -> NO,退出码 1。 + +用法(在模块仓库内或机构工作空间根均可): + python3 scripts/m11b2c_a3_replay_check.py +""" +import hashlib +import io +import os +import shutil +import subprocess +import sys +import tempfile + +REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +PATCHER = "scripts/m11b2c_a3_patch_tx_write.py" +TX_WRITE = "pbl_runtime_ext/tx_write.py" +FIX_COMMIT = "5542cf5d5182130aa929e5aa37d0c22a043cee25" + + +def _git_rev_parse(rev): + return _git("rev-parse", rev).strip() + + +def _git(*args): + out = subprocess.run(["git", "-C", REPO] + list(args), + check=True, stdout=subprocess.PIPE) + return out.stdout.decode("utf-8") + + +def _sha(path): + with io.open(path, "rb") as f: + return hashlib.sha256(f.read()).hexdigest() + + +def main(): + print("== M11b-2c-A3 补丁器/定稿一致性重放校验 ==") + print("repo : %s" % REPO) + head = _git_rev_parse("HEAD") + print("HEAD : %s" % head) + print("HEAD subject : %s" % _git("log", "-1", "--format=%s").strip()) + print("fix commit : %s" % FIX_COMMIT) + print("patcher@HEAD : %s" % _git_rev_parse("HEAD:%s" % PATCHER)) + print("tx_write@HEAD : %s" % _git_rev_parse("HEAD:%s" % TX_WRITE)) + print("tx_write@fix^ : %s" % _git_rev_parse("%s^:%s" % (FIX_COMMIT, TX_WRITE))) + + tmp = tempfile.mkdtemp(prefix="m11b2c_a3_replay_") + rc = 1 + try: + # 1) 取 HEAD 版补丁器 + patcher_dir = os.path.join(tmp, "inst", "modules", "pbl_runtime_ext", "scripts") + target_dir = os.path.join(tmp, "inst", "modules", "pbl_runtime_ext", "pbl_runtime_ext") + os.makedirs(patcher_dir) + os.makedirs(target_dir) + with io.open(os.path.join(patcher_dir, os.path.basename(PATCHER)), "w", + encoding="utf-8") as f: + f.write(_git("show", "HEAD:%s" % PATCHER)) + + # 2) 取修复前 tx_write.py 原文,摆到补丁器 TARGET 的相对位置 + pre_path = os.path.join(target_dir, "tx_write.py") + with io.open(pre_path, "w", encoding="utf-8") as f: + f.write(_git("show", "%s^:%s" % (FIX_COMMIT, TX_WRITE))) + head_path = os.path.join(tmp, "tx_write_HEAD.py") + with io.open(head_path, "w", encoding="utf-8") as f: + f.write(_git("show", "HEAD:%s" % TX_WRITE)) + + pre_sha = _sha(pre_path) + head_sha = _sha(head_path) + print("pre-fix sha256 : %s" % pre_sha) + print("HEAD sha256 : %s" % head_sha) + + # 3) 在临时目录内执行 HEAD 版补丁器(TARGET 是相对路径,故 cwd 必须是 inst/) + sys.path.insert(0, patcher_dir) + mod_name = "m11b2c_a3_patch_tx_write" + if mod_name in sys.modules: + del sys.modules[mod_name] + patcher = __import__(mod_name) + cwd = os.getcwd() + os.chdir(os.path.join(tmp, "inst")) + try: + prc = patcher.main() + finally: + os.chdir(cwd) + print("patcher exit rc : %d (0=已应用 4 处补丁, 2=片段命中数异常)" % prc) + if prc != 0: + print("REPLAY_IDENTICAL: NO(补丁器在修复前原文上执行失败,rc=%d)" % prc) + return 1 + + # 4) 逐字节比对 + replayed_sha = _sha(pre_path) + print("replayed sha256 : %s" % replayed_sha) + cmp_rc = subprocess.run(["cmp", pre_path, head_path]).returncode + print("cmp 退出码 : %d (0=逐字节一致)" % cmp_rc) + if replayed_sha == head_sha and cmp_rc == 0: + print("REPLAY_IDENTICAL: YES(补丁器重放产物 == HEAD 定稿,逐字节一致)") + print("结论:HEAD 即最终自洽态,补丁器与定稿 tx_write.py 一致,无待引擎收口内容。") + rc = 0 + else: + print("REPLAY_IDENTICAL: NO(补丁器与定稿不一致,仓库状态非最终态)") + rc = 1 + return rc + finally: + shutil.rmtree(tmp, ignore_errors=True) + + +if __name__ == "__main__": + sys.exit(main())