diff --git a/pbl_runtime_ext/tx_write.py b/pbl_runtime_ext/tx_write.py index 9b6ae39..be33801 100644 --- a/pbl_runtime_ext/tx_write.py +++ b/pbl_runtime_ext/tx_write.py @@ -95,22 +95,84 @@ def table_columns(table): return names or None +def _is_not_null(fd): + """字段是否声明 NOT NULL —— 兼容 models/*.json 并存的两种写法。 + + * ``{"null": false}`` :xlsx→json 产物写法(pbl_entity_state / pbl_world_state_snapshot) + * ``{"nullable": "no"}`` :database-table-definition-spec 规范写法(pbl_runtime_event) + + M11b-2c-A3 修复(PBL-RTX-0004 **漏报侧**根因):此前只认 ``null: false``, + 而 pbl_runtime_event 用的是规范写法 ``nullable: "no"``,使该表 ``required_columns()`` + 恒为 ``[]`` —— build_row 的 INSERT 完整性门禁对事件表**完全失效** + (该报的缺列不报,静默写出半空行)。 + """ + if fd.get("null") is False: + return True + if str(fd.get("nullable") or "").strip().lower() in ("no", "false", "0"): + return True + return False + + +def _has_db_default(fd): + """列是否在 DDL 中声明 DEFAULT(含 CURRENT_TIMESTAMP)。 + + ``NOT NULL DEFAULT x`` 的列在 INSERT 省略时由数据库补值,**不构成"缺 NOT NULL 列"**。 + M11b-2c-A3 修复(PBL-RTX-0004 **误报侧**根因之二):pbl_runtime_event 的 + event_code/idem_key/scene_id/entity_id/actor_id/causation_id/seq/source/state/ + tx_group/broadcast/created_by/created_at/updated_at 等列均为 NOT NULL DEFAULT, + 写路径按语义不填它们;若计入必填集合,合法调用会被误报缺列。 + (与 _recognize nullable 写法配套:只修识别不修默认值排除,事件表主链路会被误挡。) + """ + if "default" not in fd: + return False + dv = fd.get("default") + if dv is None: + return False + if str(dv).strip().upper() in ("NULL", "NONE"): + return False + return True # 含 DEFAULT ''(空串默认值同样是 DDL 默认值,数据库会补值) + + def required_columns(table): - """NOT NULL 且非自增的列(INSERT 必须显式给值,否则 MariaDB 严格模式报错)。""" + """INSERT 必须**显式给值**的列:NOT NULL + 非自增 + 无数据库 DEFAULT。 + + 自增列由数据库生成、NOT NULL DEFAULT 列由默认值补上,两者都不需要调用方提供; + 纳入必填集合只会造成 PBL-RTX-0004 误报。真正的必填列(pbl_entity_state 的 + tenant_id/session_id/entity_id/state_version/checksum/updated_by/created_at/ + updated_at,pbl_runtime_event 的 event_uid/event_type)缺值时数据库严格模式会 + 直接报错,故必须在这里 fail-closed 拦住,不得放宽。 + """ spec = _model_spec(table) if not spec: return [] out = [] for fd in spec.get("fields") or []: - if not isinstance(fd, dict): + if not isinstance(fd, dict) or not fd.get("name"): continue - if fd.get("null") is False and not fd.get("auto_increment"): + if _is_not_null(fd) and not fd.get("auto_increment") and not _has_db_default(fd): out.append(fd["name"]) return out -def build_row(table, wanted, strict=True): - """按表真实列过滤字段。strict=True 时未知列直接抛错(防静默丢列写空行)。""" +def build_row(table, wanted, strict=True, full_row=True): + """按表真实列过滤字段。strict=True 时未知列直接抛错(防静默丢列写空行)。 + + full_row=True(默认,**INSERT 语义**):额外校验 required_columns() 齐备, + 缺列抛 PBL-RTX-0004。 + full_row=False(**UPDATE SET 子句语义**):被改动的行在库中已存在,未出现在 SET + 列表里的 NOT NULL 列保持原值,**不存在"缺列"**,因此不得套用 INSERT 的完整性 + 校验;同时禁止 SET 不可变列(tenant_id/id/created_at),违者抛 PBL-RTX-0006。 + + PBL-RTX-0004 闭环(M11b-2c-A3,A1 判定为业务实现缺陷):误报有两条独立路径—— + (1) UPDATE 分支沿用 INSERT 校验(此前用 strict=False 绕,但 strict 只管未知列, + 缺列校验照跑 → tenant_id/session_id/entity_id/created_at 必被误报)。 + 现按 full_row=False 构造:定位维度走 WHERE、created_at 保持建档原值。 + (2) 必填集合把「自增列 / NOT NULL DEFAULT 列」也算进来 → required_columns() + 已排除(见 _has_db_default)。 + 漏报侧同修:required_columns() 此前不认 ``nullable: "no"`` 写法,事件表门禁恒真。 + 修后语义:该报的(真缺 event_uid/event_type 等无默认 NOT NULL 列)一定报, + 不该报的(UPDATE SET 子集、数据库能补值的列)一定不报。 + """ cols = table_columns(table) if not cols: if strict: @@ -123,10 +185,16 @@ def build_row(table, wanted, strict=True): "PBL-RTX-0003", "%s 未知列 %s(权威列=%s)" % (table, unknown, cols)) row = dict((k, v) for k, v in wanted.items() if k in cols) - missing = [c for c in required_columns(table) if c not in row] - if missing: - raise rtx_db.RtxError("PBL-RTX-0004", - "%s 缺少 NOT NULL 列 %s" % (table, missing)) + if full_row: + missing = [c for c in required_columns(table) if c not in row] + if missing: + raise rtx_db.RtxError("PBL-RTX-0004", + "%s 缺少 NOT NULL 列 %s" % (table, missing)) + else: + immutable = [c for c in ("tenant_id", "id", "created_at") if c in row] + if immutable: + raise rtx_db.RtxError("PBL-RTX-0006", + "%s UPDATE 禁止 SET 不可变列 %s" % (table, immutable)) return row @@ -253,7 +321,7 @@ async def _apply_state(tx, tenant_id, session_id, upd, seq_no, base_version, vals = build_row(STATE_TABLE, { STATE_COL_VALUE: state_version, EVENT_COL_STATE_VERSION: new_ver, "checksum": checksum, "updated_by": actor_id, "updated_at": now, - }, strict=False) + }, strict=True, full_row=False) await tx.execute( "UPDATE %s SET %s WHERE tenant_id=%%s AND session_id=%%s AND %s=%%s" % (STATE_TABLE, ", ".join("%s=%%s" % c for c in vals.keys()), STATE_COL_KEY), @@ -539,6 +607,95 @@ def self_check(): else: msgs.append("tx_write PASS:%s 用到的 %d 列与 models 定义一致" % (tbl, len(cols))) + # ---- PBL-RTX-0004 闭环门禁(M11b-2c-A3):误报侧不报 + 漏报侧必报 ---- + try: + upd_row = build_row(STATE_TABLE, { + STATE_COL_VALUE: "{}", EVENT_COL_STATE_VERSION: 2, "checksum": "c", + "updated_by": "server", "updated_at": "2026-09-21 12:00:00", + }, strict=True, full_row=False) + expect = [STATE_COL_VALUE, EVENT_COL_STATE_VERSION, "checksum", "updated_by", + "updated_at"] + if sorted(upd_row.keys()) != sorted(expect): + ok = False + msgs.append("tx_write FAIL:UPDATE SET 列被意外裁剪 %s(应为 %s)" + % (sorted(upd_row.keys()), sorted(expect))) + else: + msgs.append("tx_write PASS:UPDATE SET 语义不误报缺 NOT NULL 列" + "(PBL-RTX-0004 误报侧已闭环)") + except rtx_db.RtxError as exc: + ok = False + msgs.append("tx_write FAIL:UPDATE SET 仍被 %s 拒绝(PBL-RTX-0004 未闭环)" % exc.code) + + try: + build_row(STATE_TABLE, {"session_id": 11, STATE_COL_KEY: "pos"}) + ok = False + msgs.append("tx_write FAIL:INSERT 真缺 NOT NULL 列未被拦(门禁恒真 = 漏报回归)") + except rtx_db.RtxError as exc: + if exc.code == "PBL-RTX-0004": + msgs.append("tx_write PASS:INSERT 真缺列仍被 PBL-RTX-0004 拦下(fail-closed 未弱化)") + else: + ok = False + msgs.append("tx_write FAIL:INSERT 缺列错误码异常 %s(应为 PBL-RTX-0004)" % exc.code) + + # 事件表用 nullable:"no" 写法,识别修复后必填集合必须非空(否则门禁又退回恒真) + for tbl in (EVENT_TABLE, STATE_TABLE, SNAPSHOT_TABLE): + req = required_columns(tbl) + if not req: + ok = False + msgs.append("tx_write FAIL:%s required_columns 为空" + "(模型 NOT NULL 写法未被识别,INSERT 门禁失效)" % tbl) + else: + msgs.append("tx_write PASS:%s 必填列 %d 个(已排除自增/DEFAULT 列):%s" + % (tbl, len(req), ",".join(req))) + + # 只给「必填列真子集」必须被拦(事件表缺 event_type / 状态表缺 tenant_id) + for tbl, partial in ((EVENT_TABLE, {EVENT_COL_UID: "u1"}), + (STATE_TABLE, {"session_id": 11, STATE_COL_KEY: "pos", + EVENT_COL_STATE_VERSION: 1, "checksum": "c", + "updated_by": "a", "created_at": "2026-09-21", + "updated_at": "2026-09-21"})): + try: + build_row(tbl, partial) + ok = False + msgs.append("tx_write FAIL:%s 只给部分必填列却通过校验" % tbl) + except rtx_db.RtxError as exc: + if exc.code != "PBL-RTX-0004": + ok = False + msgs.append("tx_write FAIL:%s 部分列错误码 %s" % (tbl, exc.code)) + else: + msgs.append("tx_write PASS:%s 部分必填列被 PBL-RTX-0004 拦下" % tbl) + + # 写路径实际使用的 INSERT 列集合必须通过(防修复过头把主链路挡死) + try: + build_row(EVENT_TABLE, { + "tenant_id": "7", EVENT_COL_UID: "u", "session_id": 11, "world_id": 1, + "actor_id": "server", "event_type": "move", EVENT_COL_PAYLOAD: "{}", + "causation_id": "", EVENT_COL_STATE_VERSION: 0, EVENT_COL_SEQ: 1, + "occurred_at": "2026-09-21 12:00:00", "created_at": "2026-09-21 12:00:00", + "updated_at": "2026-09-21 12:00:00"}) + build_row(SNAPSHOT_TABLE, { + "tenant_id": "7", "session_id": 11, "snapshot_uid": "s", + "state_fingerprint": "f", "entities_json": "{}", "events_through_seq": 1, + "reason": "apply_runtime_event", "created_at": "2026-09-21", + "updated_at": "2026-09-21"}) + msgs.append("tx_write PASS:事件表/快照表 INSERT 写路径列集合通过校验(无回归)") + except rtx_db.RtxError as exc: + ok = False + msgs.append("tx_write FAIL:写路径 INSERT 列集合被 %s 拒绝(修复过头)" % exc.code) + + # 不可变列守卫:SET tenant_id/created_at 必须被 PBL-RTX-0006 拒绝 + try: + build_row(STATE_TABLE, {"tenant_id": "7", STATE_COL_VALUE: "{}"}, + strict=True, full_row=False) + ok = False + msgs.append("tx_write FAIL:UPDATE SET 不可变列未被拦") + except rtx_db.RtxError as exc: + if exc.code == "PBL-RTX-0006": + msgs.append("tx_write PASS:UPDATE 禁止 SET 不可变列(PBL-RTX-0006)") + else: + ok = False + msgs.append("tx_write FAIL:不可变列错误码 %s(应为 PBL-RTX-0006)" % exc.code) + for name, fn in (("apply_runtime_event", apply_runtime_event), ("poll_events", poll_events), ("read_states", read_states)): if not inspect.iscoroutinefunction(fn): diff --git a/scripts/m11b2c_a3_patch_tx_write.py b/scripts/m11b2c_a3_patch_tx_write.py new file mode 100644 index 0000000..7ac5aa8 --- /dev/null +++ b/scripts/m11b2c_a3_patch_tx_write.py @@ -0,0 +1,275 @@ +# -*- coding: utf-8 -*- +"""M11b-2c-A3 一次性补丁器:修 pbl_runtime_ext/tx_write.py 中 PBL-RTX-0004 的业务缺陷。 + +四段精确字符串替换;任一片段命中数 != 1 即报错退出(绝不静默跳过)。 +改完由 py_compile + self_check + selftest + pytest 验证。 +""" +import io +import os +import sys + +TARGET = os.path.join("modules", "pbl_runtime_ext", "pbl_runtime_ext", "tx_write.py") + +# ---------------------------------------------------------------- 1) required_columns +OLD_1 = '''def required_columns(table): + """NOT NULL 且非自增的列(INSERT 必须显式给值,否则 MariaDB 严格模式报错)。""" + spec = _model_spec(table) + if not spec: + return [] + out = [] + for fd in spec.get("fields") or []: + if not isinstance(fd, dict): + continue + if fd.get("null") is False and not fd.get("auto_increment"): + out.append(fd["name"]) + return out +''' + +NEW_1 = '''def _is_not_null(fd): + """字段是否声明 NOT NULL —— 兼容 models/*.json 并存的两种写法。 + + * ``{"null": false}`` :xlsx→json 产物写法(pbl_entity_state / pbl_world_state_snapshot) + * ``{"nullable": "no"}`` :database-table-definition-spec 规范写法(pbl_runtime_event) + + M11b-2c-A3 修复(PBL-RTX-0004 **漏报侧**根因):此前只认 ``null: false``, + 而 pbl_runtime_event 用的是规范写法 ``nullable: "no"``,使该表 ``required_columns()`` + 恒为 ``[]`` —— build_row 的 INSERT 完整性门禁对事件表**完全失效** + (该报的缺列不报,静默写出半空行)。 + """ + if fd.get("null") is False: + return True + if str(fd.get("nullable") or "").strip().lower() in ("no", "false", "0"): + return True + return False + + +def _has_db_default(fd): + """列是否在 DDL 中声明 DEFAULT(含 CURRENT_TIMESTAMP)。 + + ``NOT NULL DEFAULT x`` 的列在 INSERT 省略时由数据库补值,**不构成"缺 NOT NULL 列"**。 + M11b-2c-A3 修复(PBL-RTX-0004 **误报侧**根因之二):pbl_runtime_event 的 + event_code/idem_key/scene_id/entity_id/actor_id/causation_id/seq/source/state/ + tx_group/broadcast/created_by/created_at/updated_at 等列均为 NOT NULL DEFAULT, + 写路径按语义不填它们;若计入必填集合,合法调用会被误报缺列。 + (与 _recognize nullable 写法配套:只修识别不修默认值排除,事件表主链路会被误挡。) + """ + dv = fd.get("default") + if dv is None: + return False + s = str(dv).strip() + if s == "" or s.upper() in ("NULL", "NONE"): + return False + return True + + +def required_columns(table): + """INSERT 必须**显式给值**的列:NOT NULL + 非自增 + 无数据库 DEFAULT。 + + 自增列由数据库生成、NOT NULL DEFAULT 列由默认值补上,两者都不需要调用方提供; + 纳入必填集合只会造成 PBL-RTX-0004 误报。真正的必填列(pbl_entity_state 的 + tenant_id/session_id/entity_id/state_version/checksum/updated_by/created_at/ + updated_at,pbl_runtime_event 的 event_uid/event_type)缺值时数据库严格模式会 + 直接报错,故必须在这里 fail-closed 拦住,不得放宽。 + """ + spec = _model_spec(table) + if not spec: + return [] + out = [] + for fd in spec.get("fields") or []: + if not isinstance(fd, dict) or not fd.get("name"): + continue + if _is_not_null(fd) and not fd.get("auto_increment") and not _has_db_default(fd): + out.append(fd["name"]) + return out +''' + +# ---------------------------------------------------------------- 2) build_row +OLD_2 = '''def build_row(table, wanted, strict=True): + """按表真实列过滤字段。strict=True 时未知列直接抛错(防静默丢列写空行)。""" + cols = table_columns(table) + if not cols: + if strict: + raise rtx_db.RtxError("PBL-RTX-0003", + "models/%s.json 不可读,拒绝按猜列写入" % table) + return dict(wanted) + unknown = [k for k in wanted if k not in cols] + if unknown and strict: + raise rtx_db.RtxError( + "PBL-RTX-0003", + "%s 未知列 %s(权威列=%s)" % (table, unknown, cols)) + row = dict((k, v) for k, v in wanted.items() if k in cols) + missing = [c for c in required_columns(table) if c not in row] + if missing: + raise rtx_db.RtxError("PBL-RTX-0004", + "%s 缺少 NOT NULL 列 %s" % (table, missing)) + return row +''' + +NEW_2 = '''def build_row(table, wanted, strict=True, full_row=True): + """按表真实列过滤字段。strict=True 时未知列直接抛错(防静默丢列写空行)。 + + full_row=True(默认,**INSERT 语义**):额外校验 required_columns() 齐备, + 缺列抛 PBL-RTX-0004。 + full_row=False(**UPDATE SET 子句语义**):被改动的行在库中已存在,未出现在 SET + 列表里的 NOT NULL 列保持原值,**不存在"缺列"**,因此不得套用 INSERT 的完整性 + 校验;同时禁止 SET 不可变列(tenant_id/id/created_at),违者抛 PBL-RTX-0006。 + + PBL-RTX-0004 闭环(M11b-2c-A3,A1 判定为业务实现缺陷):误报有两条独立路径—— + (1) UPDATE 分支沿用 INSERT 校验(此前用 strict=False 绕,但 strict 只管未知列, + 缺列校验照跑 → tenant_id/session_id/entity_id/created_at 必被误报)。 + 现按 full_row=False 构造:定位维度走 WHERE、created_at 保持建档原值。 + (2) 必填集合把「自增列 / NOT NULL DEFAULT 列」也算进来 → required_columns() + 已排除(见 _has_db_default)。 + 漏报侧同修:required_columns() 此前不认 ``nullable: "no"`` 写法,事件表门禁恒真。 + 修后语义:该报的(真缺 event_uid/event_type 等无默认 NOT NULL 列)一定报, + 不该报的(UPDATE SET 子集、数据库能补值的列)一定不报。 + """ + cols = table_columns(table) + if not cols: + if strict: + raise rtx_db.RtxError("PBL-RTX-0003", + "models/%s.json 不可读,拒绝按猜列写入" % table) + return dict(wanted) + unknown = [k for k in wanted if k not in cols] + if unknown and strict: + raise rtx_db.RtxError( + "PBL-RTX-0003", + "%s 未知列 %s(权威列=%s)" % (table, unknown, cols)) + row = dict((k, v) for k, v in wanted.items() if k in cols) + if full_row: + missing = [c for c in required_columns(table) if c not in row] + if missing: + raise rtx_db.RtxError("PBL-RTX-0004", + "%s 缺少 NOT NULL 列 %s" % (table, missing)) + else: + immutable = [c for c in ("tenant_id", "id", "created_at") if c in row] + if immutable: + raise rtx_db.RtxError("PBL-RTX-0006", + "%s UPDATE 禁止 SET 不可变列 %s" % (table, immutable)) + return row +''' + +# ---------------------------------------------------------------- 3) UPDATE 调用点 +OLD_3 = ''' "checksum": checksum, "updated_by": actor_id, "updated_at": now, + }, strict=False)''' + +NEW_3 = ''' "checksum": checksum, "updated_by": actor_id, "updated_at": now, + }, strict=True, full_row=False)''' + +# ---------------------------------------------------------------- 4) self_check 闭环门禁 +OLD_4 = ''' for name, fn in (("apply_runtime_event", apply_runtime_event), + ("poll_events", poll_events), ("read_states", read_states)):''' + +NEW_4 = ''' # ---- PBL-RTX-0004 闭环门禁(M11b-2c-A3):误报侧不报 + 漏报侧必报 ---- + try: + upd_row = build_row(STATE_TABLE, { + STATE_COL_VALUE: "{}", EVENT_COL_STATE_VERSION: 2, "checksum": "c", + "updated_by": "server", "updated_at": "2026-09-21 12:00:00", + }, strict=True, full_row=False) + expect = [STATE_COL_VALUE, EVENT_COL_STATE_VERSION, "checksum", "updated_by", + "updated_at"] + if sorted(upd_row.keys()) != sorted(expect): + ok = False + msgs.append("tx_write FAIL:UPDATE SET 列被意外裁剪 %s(应为 %s)" + % (sorted(upd_row.keys()), sorted(expect))) + else: + msgs.append("tx_write PASS:UPDATE SET 语义不误报缺 NOT NULL 列" + "(PBL-RTX-0004 误报侧已闭环)") + except rtx_db.RtxError as exc: + ok = False + msgs.append("tx_write FAIL:UPDATE SET 仍被 %s 拒绝(PBL-RTX-0004 未闭环)" % exc.code) + + try: + build_row(STATE_TABLE, {"session_id": 11, STATE_COL_KEY: "pos"}) + ok = False + msgs.append("tx_write FAIL:INSERT 真缺 NOT NULL 列未被拦(门禁恒真 = 漏报回归)") + except rtx_db.RtxError as exc: + if exc.code == "PBL-RTX-0004": + msgs.append("tx_write PASS:INSERT 真缺列仍被 PBL-RTX-0004 拦下(fail-closed 未弱化)") + else: + ok = False + msgs.append("tx_write FAIL:INSERT 缺列错误码异常 %s(应为 PBL-RTX-0004)" % exc.code) + + # 事件表用 nullable:"no" 写法,识别修复后必填集合必须非空(否则门禁又退回恒真) + for tbl in (EVENT_TABLE, STATE_TABLE, SNAPSHOT_TABLE): + req = required_columns(tbl) + if not req: + ok = False + msgs.append("tx_write FAIL:%s required_columns 为空" + "(模型 NOT NULL 写法未被识别,INSERT 门禁失效)" % tbl) + else: + msgs.append("tx_write PASS:%s 必填列 %d 个(已排除自增/DEFAULT 列):%s" + % (tbl, len(req), ",".join(req))) + + # 只给「必填列真子集」必须被拦(事件表缺 event_type / 状态表缺 tenant_id) + for tbl, partial in ((EVENT_TABLE, {EVENT_COL_UID: "u1"}), + (STATE_TABLE, {"session_id": 11, STATE_COL_KEY: "pos", + EVENT_COL_STATE_VERSION: 1, "checksum": "c", + "updated_by": "a", "created_at": "2026-09-21", + "updated_at": "2026-09-21"})): + try: + build_row(tbl, partial) + ok = False + msgs.append("tx_write FAIL:%s 只给部分必填列却通过校验" % tbl) + except rtx_db.RtxError as exc: + if exc.code != "PBL-RTX-0004": + ok = False + msgs.append("tx_write FAIL:%s 部分列错误码 %s" % (tbl, exc.code)) + else: + msgs.append("tx_write PASS:%s 部分必填列被 PBL-RTX-0004 拦下" % tbl) + + # 写路径实际使用的 INSERT 列集合必须通过(防修复过头把主链路挡死) + try: + build_row(EVENT_TABLE, { + "tenant_id": "7", EVENT_COL_UID: "u", "session_id": 11, "world_id": 1, + "actor_id": "server", "event_type": "move", EVENT_COL_PAYLOAD: "{}", + "causation_id": "", EVENT_COL_STATE_VERSION: 0, EVENT_COL_SEQ: 1, + "occurred_at": "2026-09-21 12:00:00", "created_at": "2026-09-21 12:00:00", + "updated_at": "2026-09-21 12:00:00"}) + build_row(SNAPSHOT_TABLE, { + "tenant_id": "7", "session_id": 11, "snapshot_uid": "s", + "state_fingerprint": "f", "entities_json": "{}", "events_through_seq": 1, + "reason": "apply_runtime_event", "created_at": "2026-09-21", + "updated_at": "2026-09-21"}) + msgs.append("tx_write PASS:事件表/快照表 INSERT 写路径列集合通过校验(无回归)") + except rtx_db.RtxError as exc: + ok = False + msgs.append("tx_write FAIL:写路径 INSERT 列集合被 %s 拒绝(修复过头)" % exc.code) + + # 不可变列守卫:SET tenant_id/created_at 必须被 PBL-RTX-0006 拒绝 + try: + build_row(STATE_TABLE, {"tenant_id": "7", STATE_COL_VALUE: "{}"}, + strict=True, full_row=False) + ok = False + msgs.append("tx_write FAIL:UPDATE SET 不可变列未被拦") + except rtx_db.RtxError as exc: + if exc.code == "PBL-RTX-0006": + msgs.append("tx_write PASS:UPDATE 禁止 SET 不可变列(PBL-RTX-0006)") + else: + ok = False + msgs.append("tx_write FAIL:不可变列错误码 %s(应为 PBL-RTX-0006)" % exc.code) + + for name, fn in (("apply_runtime_event", apply_runtime_event), + ("poll_events", poll_events), ("read_states", read_states)):''' + + +def main(): + with io.open(TARGET, "r", encoding="utf-8") as f: + src = f.read() + pairs = [("required_columns", OLD_1, NEW_1), ("build_row", OLD_2, NEW_2), + ("_apply_state UPDATE 调用点", OLD_3, NEW_3), + ("self_check 闭环门禁", OLD_4, NEW_4)] + for tag, old, new in pairs: + n = src.count(old) + if n != 1: + print("FAIL: 片段[%s] 命中 %d 次(应为 1)" % (tag, n)) + return 2 + src = src.replace(old, new) + with io.open(TARGET, "w", encoding="utf-8") as f: + f.write(src) + print("OK: 已应用 %d 处补丁 -> %s" % (len(pairs), TARGET)) + return 0 + + +if __name__ == "__main__": + sys.exit(main())