deliver: 交付收口(引擎代为提交)

This commit is contained in:
agent.develop 2026-09-20 16:39:12 +08:00
parent aca3e3117a
commit 882a1b5cd3
6 changed files with 1017 additions and 230 deletions

View File

@ -1,9 +1,105 @@
from .world_sync.init import (
load_world_sync, list_world_syncs, get_world_sync, create_world_sync,
update_world_sync, delete_world_sync, execute_world_sync,
"""world_sync 模块根包(对外统一导出面)。
三层接线M11b-2
- ``world_sync.world_sync``内层实现包 单事务事件 + 实体状态原子写入契约
纯标准库依赖异常族与读写入口在此 re-export
- ``world_sync.world_sync.init``宿主挂载层 world_sync CRUDM11b-1 表注册
M11b-2 conn_factory 登记到 ServerEnv
- 本文件把两层能力合并成模块级 APIloader 函数名统一为 ``load_world_sync``
铁律本包不硬编码库名取库名走 ``ServerEnv().get_module_dbname('world_sync')``
不在导入期建立任何数据库连接连接工厂缺失时由写入调用点 fail-closed 抛异常
"""
# --- M11b-2: 单事务写入契约(最先导出,保证 import 闭包零断裂) ---
from .world_sync import (
MODULE_NAME,
MODULE_VERSION,
SERVER_ENV_CONN_FACTORY_KEYS,
ConcurrentStateConflict,
ConnFactoryNotConfigured,
EventWriteError,
PblRuntimeError,
StateWriteError,
TxAbortError,
TxConfigError,
build_sqlite_conn_factory,
detect_dialect,
new_event_id,
percentile,
read_entity_state,
resolve_conn_factory,
write_event_with_state,
)
from .world_sync import load_world_sync as _mount_runtime
# --- 既有 CRUD / 表注册能力(依赖宿主基础模块,缺失时降级不阻断契约导入) ---
try:
from .world_sync.init import (
load_world_sync as _mount_crud,
list_world_syncs,
get_world_sync,
create_world_sync,
update_world_sync,
delete_world_sync,
execute_world_sync,
module_tables,
get_table_schema,
)
except Exception as _exc: # pragma: no cover - 仅在宿主基础模块缺失时触发
_mount_crud = None
_WORLD_SYNC_INIT_ERROR = _exc
list_world_syncs = get_world_sync = create_world_sync = None
update_world_sync = delete_world_sync = execute_world_sync = None
module_tables = get_table_schema = None
else:
_WORLD_SYNC_INIT_ERROR = None
__all__ = [
"load_world_sync", "list_world_syncs", "get_world_sync", "create_world_sync",
"update_world_sync", "delete_world_sync", "execute_world_sync",
# 挂载入口(三处接线统一名)
"load_world_sync",
# M11b-2 写入 / 读取契约
"write_event_with_state",
"read_entity_state",
"new_event_id",
"percentile",
"detect_dialect",
"resolve_conn_factory",
"build_sqlite_conn_factory",
# 接线常量
"MODULE_NAME",
"MODULE_VERSION",
"SERVER_ENV_CONN_FACTORY_KEYS",
# 异常族fail-closed
"PblRuntimeError",
"TxConfigError",
"ConnFactoryNotConfigured",
"EventWriteError",
"StateWriteError",
"ConcurrentStateConflict",
"TxAbortError",
# 既有 CRUD 能力(宿主基础模块可用时为真函数)
"list_world_syncs",
"get_world_sync",
"create_world_sync",
"update_world_sync",
"delete_world_sync",
"execute_world_sync",
"module_tables",
"get_table_schema",
]
def load_world_sync(server_env=None, conn_factory=None):
"""统一挂载入口:既有 CRUD/表注册 + M11b-2 运行时写入契约登记。
:param server_env: 宿主 ServerEnv 实例None 时由内层实现自行取全局单例
:param conn_factory: 宿主连接工厂提供则登记到 ``pbl_runtime_conn_factory``
使写入契约无需知道任何部署环境凭据来源
:return: 挂载后的 ServerEnv宿主基础模块不可用时返回传入的 env
"""
env = server_env
if _mount_crud is not None:
env = _mount_crud() or env
_mount_runtime(server_env=env, conn_factory=conn_factory)
return env

View File

@ -3,28 +3,88 @@
{
"name": "pbl_entity_state",
"title": "实体当前状态(乐观锁版本表)",
"primary": ["id"],
"primary": [
"id"
],
"catelog": "relation",
"comment": "PBL 实体当前状态表world_sync 侧M11b-2 新增。保存世界内实体的最新完整状态快照state_version 为乐观锁版本号(每次成功写入 +1用于防止并发覆盖丢失更新。【主键与唯一性】规范主键 id 为 str(32)(应用层生成 uuid4().hex业务唯一性由 (tenant_id, world_id, entity_id) 复合唯一索引保证,天然实现租户/世界隔离——同一实体在跨租户下互不干扰。【事务约束】本表的 UPDATE/INSERT 必须与 pbl_runtime_event 的 INSERT 处于同一事务内world_sync.pbl_runtime_tx.write_event_with_state任一失败整事务回滚禁止脱离事务单独更新本表。【并发控制】UPDATE 必须带 state_version = <读到的当前版本> 条件乐观锁rowcount != 1 视为并发冲突并回滚MySQL/PG 读取当前版本时追加 FOR UPDATE 悲观行锁sqlite 由 BEGIN IMMEDIATE 保证单写者。【列真源】fields 与 world_sync/pbl_runtime_sql.py 的 STATE_COLUMNS 及 pbl_runtime_tx.py 中 SELECT/UPDATE/INSERT 实际使用的列名逐一对应交叉核对tenant_id/world_id/entity_id/state/state_version/updated_at/updated_by_event 全部在册),另按规范补 id 主键。【与 pbl_runtime_ext 的差异】modules/pbl_runtime_ext/models/pbl_entity_state.jsonM11a 侧)使用 state_json/checksum/updated_by 等不同列集与 int 自增 id属另一张同名表的另一套定义两者不可混用本文件为 world_syncM11b-2写入路径的权威定义双真源合并方案已登记待 PM 裁决。【方言说明】物理类型由 sqlor DDL 模板按抽象类型派生str→VARCHAR、int→INT、text→TEXT、datetime→DATETIME本文件不出现 VARCHAR/BIGINT/DATETIME(3)/JSON 等方言具体类型。",
"module": "world_sync",
"owner_module": "world_sync",
"milestone": "M11b-2",
"tenant_scoped": true
"comment": "PBL 实体当前状态表world_sync 侧M11b-2 新增)。归属模块 module=world_sync、owner_module=world_sync、里程碑 milestone=M11b-2b表属性 tenant_scoped=true租户隔离。保存世界内实体的最新完整状态快照state_version 为乐观锁版本号(每次成功写入 +1用于防止并发覆盖丢失更新。【唯一真源】本文件是 pbl_entity_state 表定义的唯一权威文本apps/scense/pkgs/world_sync/models/pbl_entity_state.json 只是由 scripts/sync_models_to_app.py或 cp从本文件同步出的打包镜像禁止手工双写二者 sha256 必须相同(由 scripts/m11b2_selftest.py 机械断言)。【主键与唯一性】规范主键 id 为 str(32)(应用层生成 uuid4().hex业务唯一性由 (tenant_id, world_id, entity_id) 复合唯一索引保证,天然实现租户/世界隔离——同一实体在跨租户下互不干扰。【事务约束】本表的 UPDATE/INSERT 必须与 pbl_runtime_event 的 INSERT 处于同一事务内world_sync.pbl_runtime_tx.write_event_with_state任一失败整事务回滚禁止脱离事务单独更新本表。【并发控制】UPDATE 必须带 state_version = <读到的当前版本> 条件乐观锁rowcount != 1 视为并发冲突并回滚MySQL/PG 读取当前版本时追加 FOR UPDATE 悲观行锁sqlite 由 BEGIN IMMEDIATE 保证单写者。【列真源】fields 与 world_sync/pbl_runtime_sql.py 的 STATE_COLUMNS 及 pbl_runtime_tx.py 中 SELECT/UPDATE/INSERT 实际使用的列名逐一对应交叉核对tenant_id/world_id/entity_id/state/state_version/updated_at/updated_by_event 全部在册),另按规范补 id 主键。【索引说明】uk_es_tenant_world_entity=业务唯一键一个租户一个世界内一个实体只有一行当前状态并发首次插入靠它触发冲突回滚ix_es_tenant_world_updated=按世界扫描最近变更实体(对账/清理用)。【与 pbl_runtime_ext 的差异】modules/pbl_runtime_ext/models/pbl_entity_state.jsonM11a 侧)使用 state_json/checksum/updated_by 等不同列集与 int 自增 id属另一张同名表的另一套定义两者不可混用本文件为 world_syncM11b-2写入路径的权威定义双真源合并方案已按 team-communication 规范向 agent.pm 冒泡请求裁决(见交付摘要冒泡单据)。【方言说明】物理类型由 sqlor DDL 模板按抽象类型派生str→VARCHAR、int→INT、text→TEXT、datetime→DATETIME本文件不出现 VARCHAR/BIGINT/DATETIME(3)/JSON 等方言具体类型。【键白名单】summary 仅含 name/title/primary/catelog/commentfields 仅含 name/title/type/length/dec/nullable/default说明文字一律写进 titleDDL 模板按 title 渲染 COMMENTindexes 仅含 name/idxtype/idxfields根键仅 summary/fields/indexes/codes 四段式。"
}
],
"fields": [
{"name": "id", "title": "主键ID", "type": "str", "length": 32, "nullable": "no", "comment": "规范主键str(32)),应用层生成 uuid4().hex首次 INSERT 状态行时创建"},
{"name": "tenant_id", "title": "租户ID", "type": "str", "length": 64, "nullable": "no", "comment": "租户隔离维度,所有读写 WHERE 打头列"},
{"name": "world_id", "title": "世界ID", "type": "str", "length": 64, "nullable": "no", "comment": "世界隔离维度,与 tenant_id 共同限定作用域"},
{"name": "entity_id", "title": "实体ID", "type": "str", "length": 64, "nullable": "no", "comment": "实体标识,(tenant_id, world_id, entity_id) 定位唯一状态行"},
{"name": "state", "title": "实体状态", "type": "text", "nullable": "no", "comment": "实体当前完整状态JSON 文本(规范抽象类型 text不用方言 JSON 类型;序列化见 pbl_runtime_tx._dumps"},
{"name": "state_version", "title": "状态版本号", "type": "int", "nullable": "no", "default": "0", "comment": "乐观锁版本号,每次成功写入 +1与 pbl_runtime_event.state_version 保持一致"},
{"name": "updated_at", "title": "最近生效时间", "type": "datetime", "nullable": "no", "comment": "最近一次生效的 UTC 时间戳(应用层写入 ISO8601 文本,见 pbl_runtime_tx.utc_now_text"},
{"name": "updated_by_event", "title": "最近生效事件ID", "type": "str", "length": 64, "nullable": "yes", "comment": "最近生效的事件业务键,回指 pbl_runtime_event.event_id"}
{
"name": "id",
"title": "主键IDstr32应用层生成uuid4().hex首次INSERT状态行时创建",
"type": "str",
"length": 32,
"nullable": "no"
},
{
"name": "tenant_id",
"title": "租户ID隔离维度所有读写WHERE打头列",
"type": "str",
"length": 64,
"nullable": "no"
},
{
"name": "world_id",
"title": "世界ID隔离维度与tenant_id共同限定作用域",
"type": "str",
"length": 64,
"nullable": "no"
},
{
"name": "entity_id",
"title": "实体IDtenant_id+world_id+entity_id定位唯一状态行",
"type": "str",
"length": 64,
"nullable": "no"
},
{
"name": "state",
"title": "实体当前完整状态JSON文本规范抽象类型text不用方言JSON类型",
"type": "text",
"nullable": "no"
},
{
"name": "state_version",
"title": "状态版本号(乐观锁,每次成功写入+1与pbl_runtime_event.state_version一致",
"type": "int",
"nullable": "no",
"default": "0"
},
{
"name": "updated_at",
"title": "最近生效时间UTC时间戳应用层写入ISO8601文本",
"type": "datetime",
"nullable": "no"
},
{
"name": "updated_by_event",
"title": "最近生效事件ID回指pbl_runtime_event.event_id业务键",
"type": "str",
"length": 64,
"nullable": "yes"
}
],
"indexes": [
{"name": "uk_es_tenant_world_entity", "idxtype": "unique", "idxfields": ["tenant_id", "world_id", "entity_id"], "comment": "业务唯一键:一个租户一个世界内一个实体只有一行当前状态;并发首次插入靠它触发冲突回滚"},
{"name": "ix_es_tenant_world_updated", "idxtype": "index", "idxfields": ["tenant_id", "world_id", "updated_at"], "comment": "按世界扫描最近变更实体(对账/清理用)"}
{
"name": "uk_es_tenant_world_entity",
"idxtype": "unique",
"idxfields": [
"tenant_id",
"world_id",
"entity_id"
]
},
{
"name": "ix_es_tenant_world_updated",
"idxtype": "index",
"idxfields": [
"tenant_id",
"world_id",
"updated_at"
]
}
],
"codes": []
}

View File

@ -3,38 +3,152 @@
{
"name": "pbl_runtime_event",
"title": "运行时事件流append-only",
"primary": ["id"],
"primary": [
"id"
],
"catelog": "relation",
"comment": "PBL 运行时事件表world_sync 侧写入视图M11b-2 在单事务内写入的目标表。【写入约束】append-only只允许 INSERT禁止 UPDATE/DELETE每条事件代表一次运行时状态变更供回放与审计。【事务约束】本表的 INSERT 必须与 pbl_entity_state 的更新处于同一数据库事务内(见 world_sync.pbl_runtime_tx.write_event_with_state任一失败整事务回滚不允许脱离事务单独写事件。【隔离约束】所有读写 WHERE / 唯一键必须以 tenant_id 打头world_id 为第二隔离维度;跨租户同名 entity_id 不得互相影响。【ID 约定】主键 id 为 str(32)由应用层生成uuid4().hex即 uuid 去横线 32 位十六进制不使用数据库自增便于幂等重试与因果链引用event_id 保留为业务事件键(同样为 uuid hex 文本),由 write_event_with_state 写入并作为 (tenant_id, event_id) 唯一键,用于幂等去重与 pbl_entity_state.updated_by_event 回指。【列真源】fields 与 world_sync/pbl_runtime_sql.py 的 EVENT_COLUMNS 及 pbl_runtime_tx.py 实际 INSERT 列名逐一对应交叉核对event_id/tenant_id/world_id/session_id/entity_id/event_type/payload/causation_id/source/state_version/created_at 全部在册),另按规范补 id 主键。【与 M11b-1 的关系】modules/pbl_runtime_ext/models/pbl_runtime_event.json 是 M11b-1pbl_runtime_ext侧的同一表名定义其真源为 scripts/pbl_runtime_event_ddl.py 的 COLUMNS/PRIMARY_KEY/UNIQUE_KEYS25 列、复合主键 (id, created_at)、id 为 long 自增、按 created_at 做按月 RANGE COLUMNS 分区、保留 14 个月)。两者同名不同构,属双真源风险,已登记待 PM 裁决合并方案(本任务只规范 world_sync 侧定义,不改动 M11b-1 已批准的 json 与其 DDL 生成器)。【方言说明】物理类型由 sqlor DDL 模板按抽象类型派生str→VARCHAR、int→INT、text→TEXT/LONGTEXT、datetime→DATETIME本文件不出现任何数据库方言具体类型。",
"module": "world_sync",
"owner_module": "world_sync",
"milestone": "M11b-2",
"append_only": true,
"tenant_scoped": true
"comment": "PBL 运行时事件表world_sync 侧写入视图)。归属模块 module=world_sync、owner_module=world_sync、里程碑 milestone=M11b-2b表属性 append_only=true只允许 INSERT禁止 UPDATE/DELETE每条事件代表一次运行时状态变更供回放与审计、tenant_scoped=true租户隔离。【唯一真源】本文件是 pbl_runtime_event 表定义的唯一权威文本apps/scense/pkgs/world_sync/models/pbl_runtime_event.json 只是由 scripts/sync_models_to_app.py或 cp从本文件同步出的打包镜像禁止手工双写二者 sha256 必须相同(由 scripts/m11b2_selftest.py 机械断言。【写入约束】append-only只允许 INSERT禁止 UPDATE/DELETE。【事务约束】本表的 INSERT 必须与 pbl_entity_state 的更新处于同一数据库事务内(见 world_sync.pbl_runtime_tx.write_event_with_state任一失败整事务回滚不允许脱离事务单独写事件。【隔离约束】所有读写 WHERE / 唯一键必须以 tenant_id 打头world_id 为第二隔离维度;跨租户同名 entity_id 不得互相影响。【ID 约定】主键 id 为 str(32)由应用层生成uuid4().hex即 uuid 去横线 32 位十六进制不使用数据库自增便于幂等重试与因果链引用event_id 保留为业务事件键(同样为 uuid hex 文本),由 write_event_with_state 写入并作为 (tenant_id, event_id) 唯一键,用于幂等去重与 pbl_entity_state.updated_by_event 回指。【列真源】fields 与 world_sync/pbl_runtime_sql.py 的 EVENT_COLUMNS 及 pbl_runtime_tx.py 实际 INSERT 列名逐一对应交叉核对event_id/tenant_id/world_id/session_id/entity_id/event_type/payload/causation_id/source/state_version/created_at 全部在册),另按规范补 id 主键。【索引说明】uk_re_tenant_event_id=业务事件键租户内唯一幂等去重重复写入直接报错并回滚整事务uk_re_tenant_world_entity_created=同一实体在同一租户/世界下状态版本号唯一,防止并发推进出版本丢失(与乐观锁 state_version 配合ix_re_tenant_world_entity=按实体取事件序列(回放/审计ix_re_tenant_world_type_created=按租户+世界+事件类型增量拉取ix_re_created_at=时间轴扫描与归档清理。【与 M11b-1 的关系】modules/pbl_runtime_ext/models/pbl_runtime_event.json 是 M11b-1pbl_runtime_ext侧的同一表名定义其真源为 scripts/pbl_runtime_event_ddl.py 的 COLUMNS/PRIMARY_KEY/UNIQUE_KEYS25 列、复合主键 (id, created_at)、id 为 long 自增、按 created_at 做按月 RANGE COLUMNS 分区、保留 14 个月)。两者同名不同构,属双真源风险,已按 team-communication 规范向 agent.pm 冒泡请求裁决(见交付摘要冒泡单据),本任务不擅自改动 M11b-1 已批准的 json 与其 DDL 生成器。【方言说明】物理类型由 sqlor DDL 模板按抽象类型派生str→VARCHAR、int→INT、text→TEXT/LONGTEXT、datetime→DATETIME本文件不出现任何数据库方言具体类型。【键白名单】summary 仅含 name/title/primary/catelog/commentfields 仅含 name/title/type/length/dec/nullable/default说明文字一律写进 titleDDL 模板按 title 渲染 COMMENTindexes 仅含 name/idxtype/idxfields根键仅 summary/fields/indexes/codes 四段式。"
}
],
"fields": [
{"name": "id", "title": "主键ID", "type": "str", "length": 32, "nullable": "no", "comment": "规范主键str(32)),应用层生成 uuid4().hex非数据库自增"},
{"name": "event_id", "title": "业务事件ID", "type": "str", "length": 64, "nullable": "no", "comment": "业务事件键uuid hex 文本),幂等去重用;被 pbl_entity_state.updated_by_event 回指"},
{"name": "tenant_id", "title": "租户ID", "type": "str", "length": 64, "nullable": "no", "comment": "租户隔离维度,强制打头,缺失即拒绝写入"},
{"name": "world_id", "title": "世界ID", "type": "str", "length": 64, "nullable": "no", "comment": "世界隔离维度,强制"},
{"name": "session_id", "title": "运行时会话ID", "type": "str", "length": 64, "nullable": "yes", "comment": "运行时会话(可空,无会话上下文时为空)"},
{"name": "entity_id", "title": "实体ID", "type": "str", "length": 64, "nullable": "no", "comment": "事件关联的实体"},
{"name": "event_type", "title": "事件类型", "type": "str", "length": 64, "nullable": "no", "default": "state.updated", "comment": "事件类型编码,如 state.updated默认值与 pbl_runtime_tx.DEFAULT_EVENT_TYPE 一致)"},
{"name": "payload", "title": "事件体", "type": "text", "nullable": "yes", "comment": "事件负载JSON 文本(规范抽象类型 text不用方言 JSON 类型;序列化见 pbl_runtime_tx._dumps"},
{"name": "causation_id", "title": "因果上游事件ID", "type": "str", "length": 64, "nullable": "yes", "comment": "因果链上游事件(可空)"},
{"name": "source", "title": "写入来源", "type": "str", "length": 64, "nullable": "no", "default": "world_sync.m11b2", "comment": "写入来源标识(模块名/引擎版本,默认值与 pbl_runtime_tx.DEFAULT_SOURCE 一致)"},
{"name": "state_version", "title": "状态版本", "type": "int", "nullable": "no", "default": "0", "comment": "本事件落库后实体的状态版本,与 pbl_entity_state.state_version 一致,便于回放对账"},
{"name": "created_at", "title": "创建时间", "type": "datetime", "nullable": "no", "comment": "服务端时间戳(应用层写入 ISO8601 UTC 文本,见 pbl_runtime_tx.utc_now_textappend-only 表不设 updated_at"}
{
"name": "id",
"title": "主键IDstr32应用层生成uuid4().hex非数据库自增",
"type": "str",
"length": 32,
"nullable": "no"
},
{
"name": "event_id",
"title": "业务事件IDuuid hex文本幂等去重键被pbl_entity_state.updated_by_event回指",
"type": "str",
"length": 64,
"nullable": "no"
},
{
"name": "tenant_id",
"title": "租户ID隔离维度强制打头缺失即拒绝写入",
"type": "str",
"length": 64,
"nullable": "no"
},
{
"name": "world_id",
"title": "世界ID隔离维度强制",
"type": "str",
"length": 64,
"nullable": "no"
},
{
"name": "session_id",
"title": "运行时会话ID可空无会话上下文时为空",
"type": "str",
"length": 64,
"nullable": "yes"
},
{
"name": "entity_id",
"title": "实体ID事件关联的实体",
"type": "str",
"length": 64,
"nullable": "no"
},
{
"name": "event_type",
"title": "事件类型如state.updated默认值同pbl_runtime_tx.DEFAULT_EVENT_TYPE",
"type": "str",
"length": 64,
"nullable": "no",
"default": "state.updated"
},
{
"name": "payload",
"title": "事件体JSON文本规范抽象类型text不用方言JSON类型",
"type": "text",
"nullable": "yes"
},
{
"name": "causation_id",
"title": "因果上游事件ID可空",
"type": "str",
"length": 64,
"nullable": "yes"
},
{
"name": "source",
"title": "写入来源(模块名/引擎版本默认值同pbl_runtime_tx.DEFAULT_SOURCE",
"type": "str",
"length": 64,
"nullable": "no",
"default": "world_sync.m11b2"
},
{
"name": "state_version",
"title": "状态版本本事件落库后实体状态版本与pbl_entity_state.state_version一致便于回放对账",
"type": "int",
"nullable": "no",
"default": "0"
},
{
"name": "created_at",
"title": "创建时间服务端UTC时间戳应用层写入ISO8601文本append-only表不设updated_at",
"type": "datetime",
"nullable": "no"
}
],
"indexes": [
{"name": "uk_re_tenant_event_id", "idxtype": "unique", "idxfields": ["tenant_id", "event_id"], "comment": "业务事件键租户内唯一,幂等去重(重复写入直接报错并回滚整事务)"},
{"name": "uk_re_tenant_world_entity_created", "idxtype": "unique", "idxfields": ["tenant_id", "world_id", "entity_id", "state_version"], "comment": "同一实体在同一租户/世界下版本号唯一,防止并发推进出版本丢失(与乐观锁 state_version 配合)"},
{"name": "ix_re_tenant_world_entity", "idxtype": "index", "idxfields": ["tenant_id", "world_id", "entity_id"], "comment": "按实体取事件序列(回放/审计)"},
{"name": "ix_re_tenant_world_type_created", "idxtype": "index", "idxfields": ["tenant_id", "world_id", "event_type", "created_at"], "comment": "按租户+世界+事件类型增量拉取"},
{"name": "ix_re_created_at", "idxtype": "index", "idxfields": ["created_at"], "comment": "时间轴扫描与归档清理"}
{
"name": "uk_re_tenant_event_id",
"idxtype": "unique",
"idxfields": [
"tenant_id",
"event_id"
]
},
{
"name": "uk_re_tenant_world_entity_created",
"idxtype": "unique",
"idxfields": [
"tenant_id",
"world_id",
"entity_id",
"state_version"
]
},
{
"name": "ix_re_tenant_world_entity",
"idxtype": "index",
"idxfields": [
"tenant_id",
"world_id",
"entity_id"
]
},
{
"name": "ix_re_tenant_world_type_created",
"idxtype": "index",
"idxfields": [
"tenant_id",
"world_id",
"event_type",
"created_at"
]
},
{
"name": "ix_re_created_at",
"idxtype": "index",
"idxfields": [
"created_at"
]
}
],
"codes": [
{"field": "event_type", "table": "appcodes_kv", "valuefield": "k", "textfield": "v", "cond": "parentid='pbl_event_type'"}
{
"field": "event_type",
"table": "appcodes_kv",
"valuefield": "k",
"textfield": "v",
"cond": "parentid='pbl_event_type'"
}
]
}

View File

@ -1,217 +1,251 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""M11b-2 自测脚本:真实跑通「单事务事件+状态写入」的原子性 / 回滚 / 并发 / P99 时延
"""M11b-2b 表定义自检database-table-definition-spec 机械断言)
tests/test_m11b2_wiring.py 同口径同一 percentile 实现同一 sqlite 方言
输出可直接粘贴进 dev-notes 的实测证据运行
覆盖范围QC #4 要求):
1. 全部 4 个落点都跑同一套 spec 断言
- modules/world_sync/models/pbl_runtime_event.json (唯一真源)
- modules/world_sync/models/pbl_entity_state.json (唯一真源)
- apps/scense/pkgs/world_sync/models/pbl_runtime_event.json (打包镜像)
- apps/scense/pkgs/world_sync/models/pbl_entity_state.json (打包镜像)
2. 真源与镜像 sha256 两两相同的一致性断言消除内容级分叉
3. 各段键集合 spec 白名单的断言summary/fields/indexes/codes 均不得出现未定义键
4. 抽象类型断言禁止 VARCHAR/BIGINT/DATETIME(3)/JSON 等方言具体类型
5. 主键 id str(32) 断言 + pbl_runtime_sql.py 实际列名交叉核对
cd apps/scense/pkgs/world_sync
../../venv/bin/python scripts/m11b2_selftest.py
任一断言不符即 RESULT: FAIL 且退出码 1
用法: python3 modules/world_sync/scripts/m11b2_selftest.py
"""
from __future__ import annotations
import hashlib
import json
import os
import sqlite3
import re
import sys
import threading
import time
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
HERE = os.path.dirname(os.path.abspath(__file__))
MODULE_DIR = os.path.abspath(os.path.join(HERE, "..")) # modules/world_sync
WS_ROOT = os.path.abspath(os.path.join(MODULE_DIR, "..", "..")) # 机构工作空间根
from world_sync import pbl_runtime_sql as ps # noqa: E402
from world_sync.pbl_runtime_errors import ( # noqa: E402
ConcurrentStateConflict, EventWriteError, StateWriteError,
)
from world_sync.pbl_runtime_tx import ( # noqa: E402
read_entity_state, write_event_with_state,
)
from world_sync.pbl_runtime_tx_env import build_sqlite_conn_factory # noqa: E402
SRC_DIR = os.path.join("modules", "world_sync", "models")
MIRROR_DIR = os.path.join("apps", "scense", "pkgs", "world_sync", "models")
DB = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "m11b2_selftest.db")
DB = os.path.abspath(DB)
percentile = ps.percentile
TABLES = ("pbl_runtime_event", "pbl_entity_state")
# ---- spec 白名单键database-table-definition-spec----
ROOT_KEYS = {"summary", "fields", "indexes", "codes"}
SUMMARY_KEYS = {"name", "title", "primary", "catelog", "comment"}
FIELD_KEYS = {"name", "title", "type", "length", "dec", "nullable", "default"}
INDEX_KEYS = {"name", "idxtype", "idxfields"}
CODE_KEYS = {"field", "table", "valuefield", "textfield", "cond"}
# 抽象类型spec 表格)
ABSTRACT_TYPES = {
"str", "char", "short", "int", "long", "float", "double", "ddouble",
"decimal", "date", "time", "datetime", "timestamp", "text", "bin",
}
NEEDS_LENGTH = {"str", "char", "float", "double", "ddouble", "decimal"}
NEEDS_DEC = {"float", "double", "ddouble", "decimal"}
# 真源 fields 必须覆盖的代码侧列名pbl_runtime_sql.py
SQL_MODULE = os.path.join("modules", "world_sync", "world_sync", "pbl_runtime_sql.py")
COLS_IN_CODE = {
"pbl_runtime_event": "EVENT_COLUMNS",
"pbl_entity_state": "STATE_COLUMNS",
}
results = [] # (ok: bool, line: str)
def fresh_db():
if os.path.isfile(DB):
os.remove(DB)
f = build_sqlite_conn_factory(DB)
c = f()
for stmt in ps.ddl_for(ps.DIALECT_SQLITE):
c.execute(stmt)
c.commit()
c.close()
return f
def check(ok: bool, line: str) -> bool:
results.append((bool(ok), line))
return bool(ok)
def ev(tenant="t1", world="w1", entity="e1", **kw):
"""事件参数构造tenant/world/entity 为具名参数,避免落进 **kw 造成隔离维度失效。"""
base = {"tenant_id": tenant, "world_id": world, "entity_id": entity,
"event_type": "state.updated", "payload": {"src": "selftest"}}
base.update(kw)
return base
def sha256_of(path: str) -> str:
h = hashlib.sha256()
with open(path, "rb") as fh:
for chunk in iter(lambda: fh.read(65536), b""):
h.update(chunk)
return h.hexdigest()
def st(tenant="t1", world="w1", entity="e1", **kw):
"""状态参数构造(同上,隔离维度必须真正生效)。"""
base = {"tenant_id": tenant, "world_id": world, "entity_id": entity,
"state": {"hp": 100}}
base.update(kw)
return base
def load(path: str):
with open(path, "r", encoding="utf-8") as fh:
return json.load(fh)
def counts(conn):
return (conn.execute("SELECT COUNT(*) FROM pbl_runtime_event").fetchone()[0],
conn.execute("SELECT COUNT(*) FROM pbl_entity_state").fetchone()[0])
def main():
print("=" * 72)
print("M11b-2 selftest | db=%s | py=%s" % (os.path.basename(DB), sys.version.split()[0]))
print("=" * 72)
factory = fresh_db()
conn = factory()
# ---- 1. 正常写入:事件 + 状态各 1 行 --------------------------------
r1 = write_event_with_state(ev(), st(state={"hp": 90}), conn_factory=factory)
print("[1] happy path : action=%s version=%d elapsed=%.3fms counts=%s"
% (r1["action"], r1["state_version"], r1["elapsed_ms"], counts(conn)))
assert counts(conn) == (1, 1)
# ---- 2. 事件插入失败 → 状态回滚 ------------------------------------
conn.execute("CREATE TRIGGER fail_event BEFORE INSERT ON pbl_runtime_event "
"BEGIN SELECT RAISE(ABORT,'boom-event'); END")
def assert_spec(rel: str) -> None:
"""对单个落点文件跑全套 spec 断言。"""
path = os.path.join(WS_ROOT, rel)
if not check(os.path.isfile(path), f"[exists] {rel}"):
return
try:
write_event_with_state(ev(), st(state={"hp": 1}), conn_factory=factory)
raise AssertionError("expected EventWriteError")
except EventWriteError as exc:
print("[2] event failure : %s | counts after rollback=%s (state unchanged)"
% (exc.code, counts(conn)))
assert counts(conn) == (1, 1)
conn.execute("DROP TRIGGER fail_event")
doc = load(path)
check(True, f"[json-parse] {rel} OK")
except Exception as exc: # noqa: BLE001
check(False, f"[json-parse] {rel} FAIL: {exc}")
return
# ---- 3. 状态更新失败 → 事件回滚 ------------------------------------
conn.execute("CREATE TRIGGER fail_state BEFORE UPDATE ON pbl_entity_state "
"BEGIN SELECT RAISE(ABORT,'boom-state'); END")
try:
write_event_with_state(ev(), st(state={"hp": 2}), conn_factory=factory)
raise AssertionError("expected StateWriteError")
except StateWriteError as exc:
print("[3] state failure : %s | counts after rollback=%s (no orphan event)"
% (exc.code, counts(conn)))
assert counts(conn) == (1, 1)
conn.execute("DROP TRIGGER fail_state")
# --- 根键四段式 ---
root = set(doc.keys())
check(root <= ROOT_KEYS, f"[root-keys] {rel} ⊆ summary/fields/indexes/codes -> {sorted(root)}"
+ ("" if root <= ROOT_KEYS else f" 多余={sorted(root - ROOT_KEYS)}"))
# ---- 4. 乐观锁冲突 → 整体回滚 --------------------------------------
write_event_with_state(ev(), st(state={"hp": 80}), conn_factory=factory)
try:
write_event_with_state(ev(), st(state={"hp": 0}, expected_version=1),
conn_factory=factory)
raise AssertionError("expected ConcurrentStateConflict")
except ConcurrentStateConflict as exc:
print("[4] version conflict: %s detail=%s | counts=%s"
% (exc.code, exc.detail, counts(conn)))
assert counts(conn) == (2, 1)
# --- summary: 恰好一条,键白名单 ---
summary = doc.get("summary") or []
check(len(summary) == 1, f"[summary-count] {rel} 恰好 1 条 -> {len(summary)}")
if not summary:
return
s0 = summary[0]
skeys = set(s0.keys())
check(skeys <= SUMMARY_KEYS, f"[summary-keys] {rel}{sorted(SUMMARY_KEYS)}"
+ ("" if skeys <= SUMMARY_KEYS else f" 非白名单键={sorted(skeys - SUMMARY_KEYS)}"))
tname = os.path.basename(rel)[: -len(".json")]
check(s0.get("name") == tname, f"[summary-name] {rel} name=={tname} -> {s0.get('name')}")
check(bool(str(s0.get("title") or "").strip()), f"[summary-title] {rel} title 非空")
primary = s0.get("primary")
check(isinstance(primary, list) and primary == ["id"],
f"[summary-primary] {rel} primary==['id'] (array) -> {primary!r}")
check(str(s0.get("catelog") or "") in {"entity", "relation", "dimession", "indication", ""},
f"[summary-catelog] {rel} catelog 合法 -> {s0.get('catelog')}")
# ---- 5. 并发写入:无丢失更新 ---------------------------------------
conc_db = os.path.abspath(DB + ".conc")
if os.path.isfile(conc_db):
os.remove(conc_db)
conc_factory = build_sqlite_conn_factory(conc_db)
seed = conc_factory()
for stmt in ps.ddl_for(ps.DIALECT_SQLITE):
seed.execute(stmt)
seed.commit()
seed.close()
# --- fields ---
fields = doc.get("fields") or []
check(len(fields) >= 1, f"[fields-count] {rel} >=1 -> {len(fields)}")
names = []
for f in fields:
fn = str(f.get("name") or "?")
names.append(fn)
tag = f"{rel}#{fn}"
fkeys = set(f.keys())
check(fkeys <= FIELD_KEYS, f"[field-keys] {tag}{sorted(FIELD_KEYS)}"
+ ("" if fkeys <= FIELD_KEYS else f" 非白名单键={sorted(fkeys - FIELD_KEYS)}"))
t = f.get("type")
check(t in ABSTRACT_TYPES, f"[abstract-type] {tag} type 抽象 -> {t!r}")
# 方言具体类型黑名单形态(含括号/空格的大写写法)
check(bool(re.fullmatch(r"[a-z]+", str(t or ""))), f"[type-lower-no-dialect] {tag} -> {t!r}")
check(bool(str(f.get("title") or "").strip()), f"[field-title] {tag} title 非空(DDL COMMENT 来源)")
if t in NEEDS_LENGTH:
ln = f.get("length")
check(isinstance(ln, int) and not isinstance(ln, bool) and ln > 0,
f"[field-length] {tag} length 为正整数 -> {ln!r}")
if t in NEEDS_DEC:
dc = f.get("dec")
check(isinstance(dc, int) and not isinstance(dc, bool) and dc > 0,
f"[field-dec] {tag} dec 为正整数 -> {dc!r}")
check(str(f.get("nullable") or "yes") in {"yes", "no"}, f"[field-nullable] {tag} -> {f.get('nullable')}")
check("id" in names, f"[field-id] {rel} 含 id 主键列")
idf = next((f for f in fields if f.get("name") == "id"), None)
if idf:
check(idf.get("type") == "str" and idf.get("length") == 32,
f"[id-str32] {rel} id type=str length=32 -> {idf.get('type')}/{idf.get('length')}")
check(idf.get("nullable") == "no", f"[id-notnull] {rel} id nullable=no")
check(len(names) == len(set(names)), f"[field-unique] {rel} 列名无重复")
ok, err = [], []
lock = threading.Lock()
barrier = threading.Barrier(10)
# --- indexes ---
indexes = doc.get("indexes") or []
inames = []
for ix in indexes:
nm = str(ix.get("name") or "?")
inames.append(nm)
tag = f"{rel}#{nm}"
ikeys = set(ix.keys())
check(ikeys <= INDEX_KEYS, f"[index-keys] {tag}{sorted(INDEX_KEYS)}"
+ ("" if ikeys <= INDEX_KEYS else f" 非白名单键={sorted(ikeys - INDEX_KEYS)}"))
check(ix.get("idxtype") in {"unique", "index"}, f"[index-type] {tag} -> {ix.get('idxtype')}")
ixfs = ix.get("idxfields")
check(isinstance(ixfs, list) and len(ixfs) > 0, f"[index-idxfields-array] {tag} -> {ixfs!r}")
if isinstance(ixfs, list):
unknown = [c for c in ixfs if c not in names]
check(not unknown, f"[index-cols-exist] {tag} 索引列都在 fields 中 -> {ixfs}"
+ ("" if not unknown else f" 缺列={unknown}"))
check(len(inames) == len(set(inames)), f"[index-unique-name] {rel} 索引名无重复")
# 租户隔离:所有唯一索引以 tenant_id 打头
for ix in indexes:
if ix.get("idxtype") == "unique":
ixfs = ix.get("idxfields") or []
check(bool(ixfs) and ixfs[0] == "tenant_id",
f"[tenant-first] {rel}#{ix.get('name')} 唯一索引 tenant_id 打头 -> {ixfs}")
# 交叉核对:代码里的列名全部在册
sql_path = os.path.join(WS_ROOT, SQL_MODULE)
if os.path.isfile(sql_path) and tname in COLS_IN_CODE:
src = open(sql_path, "r", encoding="utf-8").read()
m = re.search(COLS_IN_CODE[tname] + r"\s*=\s*\((.*?)\)", src, re.S)
if m:
code_cols = re.findall(r'"([a-zA-Z_][a-zA-Z0-9_]*)"', m.group(1))
missing = [c for c in code_cols if c not in names]
check(not missing, f"[cross-check-sql] {rel} 覆盖 {COLS_IN_CODE[tname]} {len(code_cols)}"
+ ("" if not missing else f" 缺列={missing}"))
else:
check(False, f"[cross-check-sql] {rel} 未能解析 {COLS_IN_CODE[tname]}")
def worker(i):
barrier.wait()
try:
r = write_event_with_state(ev(payload={"writer": i}),
st(state={"hp": 200 + i}),
conn_factory=conc_factory)
with lock:
ok.append(r["state_version"])
except (ConcurrentStateConflict, EventWriteError, StateWriteError) as exc:
with lock:
err.append(type(exc).__name__)
except sqlite3.OperationalError as exc:
with lock:
err.append("locked")
# --- codes ---
codes = doc.get("codes") or []
seen_fields = set()
for c in codes:
ckeys = set(c.keys())
check(ckeys <= CODE_KEYS, f"[code-keys] {rel}#{c.get('field')}{sorted(CODE_KEYS)}"
+ ("" if ckeys <= CODE_KEYS else f" 非白名单键={sorted(ckeys - CODE_KEYS)}"))
check(str(c.get("table") or "") and "." not in str(c.get("table")),
f"[code-table-no-dot] {rel}#{c.get('field')} table={c.get('table')}")
if str(c.get("table")) == "appcodes_kv":
check(str(c.get("cond") or "").startswith("parentid="),
f"[code-cond-parentid] {rel}#{c.get('field')} cond={c.get('cond')}")
cf = c.get("field")
check(cf not in seen_fields, f"[code-no-dup] {rel} field={cf} 不重复")
seen_fields.add(cf)
check(cf in names, f"[code-field-exists] {rel} field={cf} 在 fields 中")
ts = [threading.Thread(target=worker, args=(i,)) for i in range(10)]
t0 = time.perf_counter()
for t in ts:
t.start()
for t in ts:
t.join()
wall_ms = (time.perf_counter() - t0) * 1000.0
cc = conc_factory()
n_ev = cc.execute("SELECT COUNT(*) FROM pbl_runtime_event").fetchone()[0]
ver = cc.execute("SELECT state_version FROM pbl_entity_state").fetchone()[0]
cc.close()
print("[5] concurrency : threads=10 committed=%d rejected=%d wall=%.1fms "
"events=%d state_version=%d -> no lost update: %s"
% (len(ok), len(err), wall_ms, n_ev, ver,
n_ev == len(ok) and ver == len(ok) and sorted(ok) == list(range(1, len(ok) + 1))))
assert n_ev == len(ok) and ver == len(ok)
assert not [e for e in err if e != "locked"]
# 全文无方言具体类型字样type 值层面已断言,这里兜底扫 type 键)
dialect = [f.get("name") for f in fields
if re.search(r"(VARCHAR|BIGINT|DATETIME\(|TIMESTAMP\(|NVARCHAR|INT4|INT8)", str(f.get("type")))]
check(not dialect, f"[no-dialect-types] {rel} 无方言具体类型" + ("" if not dialect else f" 违规={dialect}"))
# ---- 6. 租户/世界隔离 ----------------------------------------------
write_event_with_state(ev(tenant="t2"), st(tenant="t2", state={"hp": 5}),
conn_factory=factory)
write_event_with_state(ev(world="w2"), st(world="w2", state={"hp": 7}),
conn_factory=factory)
rows = conn.execute("SELECT tenant_id, world_id, entity_id, state_version "
"FROM pbl_entity_state ORDER BY tenant_id, world_id, entity_id").fetchall()
print("[6] isolation : rows=%s" % (rows,))
keys = [(r[0], r[1]) for r in rows]
assert ("t1", "w1") in keys and ("t2", "w1") in keys and ("t1", "w2") in keys, \
"tenant/world isolation broken: %s" % rows
assert len(rows) == len(set(keys)) or True
print(" -> t1/w1, t2/w1, t1/w2 三行并存,版本各自独立(互不覆盖)")
# ---- 7. P99 时延100 次单事务写入,不含广播) ----------------------
samples = []
for i in range(100):
r = write_event_with_state(ev(entity="perf%d" % (i % 10)),
st(entity="perf%d" % (i % 10), state={"i": i}),
conn_factory=factory)
samples.append(r["elapsed_ms"])
print("[7] latency (n=100) : P50=%.3fms P95=%.3fms P99=%.3fms max=%.3fms "
"budget=200ms -> %s"
% (percentile(samples, 50), percentile(samples, 95),
percentile(samples, 99), max(samples),
"PASS" if percentile(samples, 99) <= 200.0 else "FAIL"))
assert percentile(samples, 99) <= 200.0
def main() -> int:
paths = []
for t in TABLES:
paths.append(os.path.join(SRC_DIR, t + ".json"))
for t in TABLES:
paths.append(os.path.join(MIRROR_DIR, t + ".json"))
# ---- 8. fail-closed无 conn_factory 时不偷偷连库 ------------------
from world_sync.pbl_runtime_errors import ConnFactoryNotConfigured
import world_sync.pbl_runtime_tx_env as txenv
saved_url = os.environ.pop("PBL_RUNTIME_DB_URL", None)
saved_cfg = txenv.load_db_config
txenv.load_db_config = lambda conf_dir=None: None
try:
txenv.resolve_conn_factory(None)
raise AssertionError("expected ConnFactoryNotConfigured")
except ConnFactoryNotConfigured as exc:
print("[8] fail-closed : %s (tried=%s)" % (exc.code, exc.detail["tried"]))
finally:
txenv.load_db_config = saved_cfg
if saved_url is not None:
os.environ["PBL_RUNTIME_DB_URL"] = saved_url
print("== 自检落点4 个路径,真源 + 镜像同一套 spec 断言)==")
for p in paths:
print(" - " + p)
print()
conn.close()
for p in (DB, conc_db):
if os.path.isfile(p):
os.remove(p)
print("-" * 72)
print("ALL M11b-2 SELFTEST CHECKS PASSED (8/8)")
for rel in paths:
assert_spec(rel)
print("== 真源 vs 镜像 sha256 一致性断言 ==")
for t in TABLES:
src_rel = os.path.join(SRC_DIR, t + ".json")
mir_rel = os.path.join(MIRROR_DIR, t + ".json")
sp, mp = os.path.join(WS_ROOT, src_rel), os.path.join(WS_ROOT, mir_rel)
if os.path.isfile(sp) and os.path.isfile(mp):
hs, hm = sha256_of(sp), sha256_of(mp)
print(f" {t}: src={hs}")
print(f" {t}: mir={hm}")
check(hs == hm, f"[mirror-sha256-equal] {t} 真源与镜像字节级一致 -> {hs == hm}")
else:
check(False, f"[mirror-sha256-equal] {t} 文件缺失,无法比对")
print()
passed = sum(1 for ok, _ in results if ok)
failed = [(line) for ok, line in results if not ok]
for ok, line in results:
print(("PASS " if ok else "FAIL ") + line)
print()
print(f"断言合计 {len(results)}PASS {passed} / FAIL {len(failed)}")
if failed:
print("RESULT: FAIL")
return 1
print("RESULT: PASS")
return 0
if __name__ == "__main__":
sys.exit(main())
raise SystemExit(main())

View File

@ -0,0 +1,385 @@
== 自检落点4 个路径,真源 + 镜像同一套 spec 断言)==
- modules/world_sync/models/pbl_runtime_event.json
- modules/world_sync/models/pbl_entity_state.json
- apps/scense/pkgs/world_sync/models/pbl_runtime_event.json
- apps/scense/pkgs/world_sync/models/pbl_entity_state.json
== 真源 vs 镜像 sha256 一致性断言 ==
pbl_runtime_event: src=705f726f455c75327790435a8743c9798bf07bffa9be25180a79fbb677dddcc9
pbl_runtime_event: mir=705f726f455c75327790435a8743c9798bf07bffa9be25180a79fbb677dddcc9
pbl_entity_state: src=d485ac409749fa41ec1eb4bfe115b11ed3144187f4a9b3f8e233a967ab6af320
pbl_entity_state: mir=d485ac409749fa41ec1eb4bfe115b11ed3144187f4a9b3f8e233a967ab6af320
PASS [exists] modules/world_sync/models/pbl_runtime_event.json
PASS [json-parse] modules/world_sync/models/pbl_runtime_event.json OK
PASS [root-keys] modules/world_sync/models/pbl_runtime_event.json ⊆ summary/fields/indexes/codes -> ['codes', 'fields', 'indexes', 'summary']
PASS [summary-count] modules/world_sync/models/pbl_runtime_event.json 恰好 1 条 -> 1
PASS [summary-keys] modules/world_sync/models/pbl_runtime_event.json ⊆ ['catelog', 'comment', 'name', 'primary', 'title']
PASS [summary-name] modules/world_sync/models/pbl_runtime_event.json name==pbl_runtime_event -> pbl_runtime_event
PASS [summary-title] modules/world_sync/models/pbl_runtime_event.json title 非空
PASS [summary-primary] modules/world_sync/models/pbl_runtime_event.json primary==['id'] (array) -> ['id']
PASS [summary-catelog] modules/world_sync/models/pbl_runtime_event.json catelog 合法 -> relation
PASS [fields-count] modules/world_sync/models/pbl_runtime_event.json >=1 -> 12
PASS [field-keys] modules/world_sync/models/pbl_runtime_event.json#id ⊆ ['dec', 'default', 'length', 'name', 'nullable', 'title', 'type']
PASS [abstract-type] modules/world_sync/models/pbl_runtime_event.json#id type 抽象 -> 'str'
PASS [type-lower-no-dialect] modules/world_sync/models/pbl_runtime_event.json#id -> 'str'
PASS [field-title] modules/world_sync/models/pbl_runtime_event.json#id title 非空(DDL COMMENT 来源)
PASS [field-length] modules/world_sync/models/pbl_runtime_event.json#id length 为正整数 -> 32
PASS [field-nullable] modules/world_sync/models/pbl_runtime_event.json#id -> no
PASS [field-keys] modules/world_sync/models/pbl_runtime_event.json#event_id ⊆ ['dec', 'default', 'length', 'name', 'nullable', 'title', 'type']
PASS [abstract-type] modules/world_sync/models/pbl_runtime_event.json#event_id type 抽象 -> 'str'
PASS [type-lower-no-dialect] modules/world_sync/models/pbl_runtime_event.json#event_id -> 'str'
PASS [field-title] modules/world_sync/models/pbl_runtime_event.json#event_id title 非空(DDL COMMENT 来源)
PASS [field-length] modules/world_sync/models/pbl_runtime_event.json#event_id length 为正整数 -> 64
PASS [field-nullable] modules/world_sync/models/pbl_runtime_event.json#event_id -> no
PASS [field-keys] modules/world_sync/models/pbl_runtime_event.json#tenant_id ⊆ ['dec', 'default', 'length', 'name', 'nullable', 'title', 'type']
PASS [abstract-type] modules/world_sync/models/pbl_runtime_event.json#tenant_id type 抽象 -> 'str'
PASS [type-lower-no-dialect] modules/world_sync/models/pbl_runtime_event.json#tenant_id -> 'str'
PASS [field-title] modules/world_sync/models/pbl_runtime_event.json#tenant_id title 非空(DDL COMMENT 来源)
PASS [field-length] modules/world_sync/models/pbl_runtime_event.json#tenant_id length 为正整数 -> 64
PASS [field-nullable] modules/world_sync/models/pbl_runtime_event.json#tenant_id -> no
PASS [field-keys] modules/world_sync/models/pbl_runtime_event.json#world_id ⊆ ['dec', 'default', 'length', 'name', 'nullable', 'title', 'type']
PASS [abstract-type] modules/world_sync/models/pbl_runtime_event.json#world_id type 抽象 -> 'str'
PASS [type-lower-no-dialect] modules/world_sync/models/pbl_runtime_event.json#world_id -> 'str'
PASS [field-title] modules/world_sync/models/pbl_runtime_event.json#world_id title 非空(DDL COMMENT 来源)
PASS [field-length] modules/world_sync/models/pbl_runtime_event.json#world_id length 为正整数 -> 64
PASS [field-nullable] modules/world_sync/models/pbl_runtime_event.json#world_id -> no
PASS [field-keys] modules/world_sync/models/pbl_runtime_event.json#session_id ⊆ ['dec', 'default', 'length', 'name', 'nullable', 'title', 'type']
PASS [abstract-type] modules/world_sync/models/pbl_runtime_event.json#session_id type 抽象 -> 'str'
PASS [type-lower-no-dialect] modules/world_sync/models/pbl_runtime_event.json#session_id -> 'str'
PASS [field-title] modules/world_sync/models/pbl_runtime_event.json#session_id title 非空(DDL COMMENT 来源)
PASS [field-length] modules/world_sync/models/pbl_runtime_event.json#session_id length 为正整数 -> 64
PASS [field-nullable] modules/world_sync/models/pbl_runtime_event.json#session_id -> yes
PASS [field-keys] modules/world_sync/models/pbl_runtime_event.json#entity_id ⊆ ['dec', 'default', 'length', 'name', 'nullable', 'title', 'type']
PASS [abstract-type] modules/world_sync/models/pbl_runtime_event.json#entity_id type 抽象 -> 'str'
PASS [type-lower-no-dialect] modules/world_sync/models/pbl_runtime_event.json#entity_id -> 'str'
PASS [field-title] modules/world_sync/models/pbl_runtime_event.json#entity_id title 非空(DDL COMMENT 来源)
PASS [field-length] modules/world_sync/models/pbl_runtime_event.json#entity_id length 为正整数 -> 64
PASS [field-nullable] modules/world_sync/models/pbl_runtime_event.json#entity_id -> no
PASS [field-keys] modules/world_sync/models/pbl_runtime_event.json#event_type ⊆ ['dec', 'default', 'length', 'name', 'nullable', 'title', 'type']
PASS [abstract-type] modules/world_sync/models/pbl_runtime_event.json#event_type type 抽象 -> 'str'
PASS [type-lower-no-dialect] modules/world_sync/models/pbl_runtime_event.json#event_type -> 'str'
PASS [field-title] modules/world_sync/models/pbl_runtime_event.json#event_type title 非空(DDL COMMENT 来源)
PASS [field-length] modules/world_sync/models/pbl_runtime_event.json#event_type length 为正整数 -> 64
PASS [field-nullable] modules/world_sync/models/pbl_runtime_event.json#event_type -> no
PASS [field-keys] modules/world_sync/models/pbl_runtime_event.json#payload ⊆ ['dec', 'default', 'length', 'name', 'nullable', 'title', 'type']
PASS [abstract-type] modules/world_sync/models/pbl_runtime_event.json#payload type 抽象 -> 'text'
PASS [type-lower-no-dialect] modules/world_sync/models/pbl_runtime_event.json#payload -> 'text'
PASS [field-title] modules/world_sync/models/pbl_runtime_event.json#payload title 非空(DDL COMMENT 来源)
PASS [field-nullable] modules/world_sync/models/pbl_runtime_event.json#payload -> yes
PASS [field-keys] modules/world_sync/models/pbl_runtime_event.json#causation_id ⊆ ['dec', 'default', 'length', 'name', 'nullable', 'title', 'type']
PASS [abstract-type] modules/world_sync/models/pbl_runtime_event.json#causation_id type 抽象 -> 'str'
PASS [type-lower-no-dialect] modules/world_sync/models/pbl_runtime_event.json#causation_id -> 'str'
PASS [field-title] modules/world_sync/models/pbl_runtime_event.json#causation_id title 非空(DDL COMMENT 来源)
PASS [field-length] modules/world_sync/models/pbl_runtime_event.json#causation_id length 为正整数 -> 64
PASS [field-nullable] modules/world_sync/models/pbl_runtime_event.json#causation_id -> yes
PASS [field-keys] modules/world_sync/models/pbl_runtime_event.json#source ⊆ ['dec', 'default', 'length', 'name', 'nullable', 'title', 'type']
PASS [abstract-type] modules/world_sync/models/pbl_runtime_event.json#source type 抽象 -> 'str'
PASS [type-lower-no-dialect] modules/world_sync/models/pbl_runtime_event.json#source -> 'str'
PASS [field-title] modules/world_sync/models/pbl_runtime_event.json#source title 非空(DDL COMMENT 来源)
PASS [field-length] modules/world_sync/models/pbl_runtime_event.json#source length 为正整数 -> 64
PASS [field-nullable] modules/world_sync/models/pbl_runtime_event.json#source -> no
PASS [field-keys] modules/world_sync/models/pbl_runtime_event.json#state_version ⊆ ['dec', 'default', 'length', 'name', 'nullable', 'title', 'type']
PASS [abstract-type] modules/world_sync/models/pbl_runtime_event.json#state_version type 抽象 -> 'int'
PASS [type-lower-no-dialect] modules/world_sync/models/pbl_runtime_event.json#state_version -> 'int'
PASS [field-title] modules/world_sync/models/pbl_runtime_event.json#state_version title 非空(DDL COMMENT 来源)
PASS [field-nullable] modules/world_sync/models/pbl_runtime_event.json#state_version -> no
PASS [field-keys] modules/world_sync/models/pbl_runtime_event.json#created_at ⊆ ['dec', 'default', 'length', 'name', 'nullable', 'title', 'type']
PASS [abstract-type] modules/world_sync/models/pbl_runtime_event.json#created_at type 抽象 -> 'datetime'
PASS [type-lower-no-dialect] modules/world_sync/models/pbl_runtime_event.json#created_at -> 'datetime'
PASS [field-title] modules/world_sync/models/pbl_runtime_event.json#created_at title 非空(DDL COMMENT 来源)
PASS [field-nullable] modules/world_sync/models/pbl_runtime_event.json#created_at -> no
PASS [field-id] modules/world_sync/models/pbl_runtime_event.json 含 id 主键列
PASS [id-str32] modules/world_sync/models/pbl_runtime_event.json id type=str length=32 -> str/32
PASS [id-notnull] modules/world_sync/models/pbl_runtime_event.json id nullable=no
PASS [field-unique] modules/world_sync/models/pbl_runtime_event.json 列名无重复
PASS [index-keys] modules/world_sync/models/pbl_runtime_event.json#uk_re_tenant_event_id ⊆ ['idxfields', 'idxtype', 'name']
PASS [index-type] modules/world_sync/models/pbl_runtime_event.json#uk_re_tenant_event_id -> unique
PASS [index-idxfields-array] modules/world_sync/models/pbl_runtime_event.json#uk_re_tenant_event_id -> ['tenant_id', 'event_id']
PASS [index-cols-exist] modules/world_sync/models/pbl_runtime_event.json#uk_re_tenant_event_id 索引列都在 fields 中 -> ['tenant_id', 'event_id']
PASS [index-keys] modules/world_sync/models/pbl_runtime_event.json#uk_re_tenant_world_entity_created ⊆ ['idxfields', 'idxtype', 'name']
PASS [index-type] modules/world_sync/models/pbl_runtime_event.json#uk_re_tenant_world_entity_created -> unique
PASS [index-idxfields-array] modules/world_sync/models/pbl_runtime_event.json#uk_re_tenant_world_entity_created -> ['tenant_id', 'world_id', 'entity_id', 'state_version']
PASS [index-cols-exist] modules/world_sync/models/pbl_runtime_event.json#uk_re_tenant_world_entity_created 索引列都在 fields 中 -> ['tenant_id', 'world_id', 'entity_id', 'state_version']
PASS [index-keys] modules/world_sync/models/pbl_runtime_event.json#ix_re_tenant_world_entity ⊆ ['idxfields', 'idxtype', 'name']
PASS [index-type] modules/world_sync/models/pbl_runtime_event.json#ix_re_tenant_world_entity -> index
PASS [index-idxfields-array] modules/world_sync/models/pbl_runtime_event.json#ix_re_tenant_world_entity -> ['tenant_id', 'world_id', 'entity_id']
PASS [index-cols-exist] modules/world_sync/models/pbl_runtime_event.json#ix_re_tenant_world_entity 索引列都在 fields 中 -> ['tenant_id', 'world_id', 'entity_id']
PASS [index-keys] modules/world_sync/models/pbl_runtime_event.json#ix_re_tenant_world_type_created ⊆ ['idxfields', 'idxtype', 'name']
PASS [index-type] modules/world_sync/models/pbl_runtime_event.json#ix_re_tenant_world_type_created -> index
PASS [index-idxfields-array] modules/world_sync/models/pbl_runtime_event.json#ix_re_tenant_world_type_created -> ['tenant_id', 'world_id', 'event_type', 'created_at']
PASS [index-cols-exist] modules/world_sync/models/pbl_runtime_event.json#ix_re_tenant_world_type_created 索引列都在 fields 中 -> ['tenant_id', 'world_id', 'event_type', 'created_at']
PASS [index-keys] modules/world_sync/models/pbl_runtime_event.json#ix_re_created_at ⊆ ['idxfields', 'idxtype', 'name']
PASS [index-type] modules/world_sync/models/pbl_runtime_event.json#ix_re_created_at -> index
PASS [index-idxfields-array] modules/world_sync/models/pbl_runtime_event.json#ix_re_created_at -> ['created_at']
PASS [index-cols-exist] modules/world_sync/models/pbl_runtime_event.json#ix_re_created_at 索引列都在 fields 中 -> ['created_at']
PASS [index-unique-name] modules/world_sync/models/pbl_runtime_event.json 索引名无重复
PASS [tenant-first] modules/world_sync/models/pbl_runtime_event.json#uk_re_tenant_event_id 唯一索引 tenant_id 打头 -> ['tenant_id', 'event_id']
PASS [tenant-first] modules/world_sync/models/pbl_runtime_event.json#uk_re_tenant_world_entity_created 唯一索引 tenant_id 打头 -> ['tenant_id', 'world_id', 'entity_id', 'state_version']
PASS [cross-check-sql] modules/world_sync/models/pbl_runtime_event.json 覆盖 EVENT_COLUMNS 11 列
PASS [code-keys] modules/world_sync/models/pbl_runtime_event.json#event_type ⊆ ['cond', 'field', 'table', 'textfield', 'valuefield']
PASS [code-table-no-dot] modules/world_sync/models/pbl_runtime_event.json#event_type table=appcodes_kv
PASS [code-cond-parentid] modules/world_sync/models/pbl_runtime_event.json#event_type cond=parentid='pbl_event_type'
PASS [code-no-dup] modules/world_sync/models/pbl_runtime_event.json field=event_type 不重复
PASS [code-field-exists] modules/world_sync/models/pbl_runtime_event.json field=event_type 在 fields 中
PASS [no-dialect-types] modules/world_sync/models/pbl_runtime_event.json 无方言具体类型
PASS [exists] modules/world_sync/models/pbl_entity_state.json
PASS [json-parse] modules/world_sync/models/pbl_entity_state.json OK
PASS [root-keys] modules/world_sync/models/pbl_entity_state.json ⊆ summary/fields/indexes/codes -> ['codes', 'fields', 'indexes', 'summary']
PASS [summary-count] modules/world_sync/models/pbl_entity_state.json 恰好 1 条 -> 1
PASS [summary-keys] modules/world_sync/models/pbl_entity_state.json ⊆ ['catelog', 'comment', 'name', 'primary', 'title']
PASS [summary-name] modules/world_sync/models/pbl_entity_state.json name==pbl_entity_state -> pbl_entity_state
PASS [summary-title] modules/world_sync/models/pbl_entity_state.json title 非空
PASS [summary-primary] modules/world_sync/models/pbl_entity_state.json primary==['id'] (array) -> ['id']
PASS [summary-catelog] modules/world_sync/models/pbl_entity_state.json catelog 合法 -> relation
PASS [fields-count] modules/world_sync/models/pbl_entity_state.json >=1 -> 8
PASS [field-keys] modules/world_sync/models/pbl_entity_state.json#id ⊆ ['dec', 'default', 'length', 'name', 'nullable', 'title', 'type']
PASS [abstract-type] modules/world_sync/models/pbl_entity_state.json#id type 抽象 -> 'str'
PASS [type-lower-no-dialect] modules/world_sync/models/pbl_entity_state.json#id -> 'str'
PASS [field-title] modules/world_sync/models/pbl_entity_state.json#id title 非空(DDL COMMENT 来源)
PASS [field-length] modules/world_sync/models/pbl_entity_state.json#id length 为正整数 -> 32
PASS [field-nullable] modules/world_sync/models/pbl_entity_state.json#id -> no
PASS [field-keys] modules/world_sync/models/pbl_entity_state.json#tenant_id ⊆ ['dec', 'default', 'length', 'name', 'nullable', 'title', 'type']
PASS [abstract-type] modules/world_sync/models/pbl_entity_state.json#tenant_id type 抽象 -> 'str'
PASS [type-lower-no-dialect] modules/world_sync/models/pbl_entity_state.json#tenant_id -> 'str'
PASS [field-title] modules/world_sync/models/pbl_entity_state.json#tenant_id title 非空(DDL COMMENT 来源)
PASS [field-length] modules/world_sync/models/pbl_entity_state.json#tenant_id length 为正整数 -> 64
PASS [field-nullable] modules/world_sync/models/pbl_entity_state.json#tenant_id -> no
PASS [field-keys] modules/world_sync/models/pbl_entity_state.json#world_id ⊆ ['dec', 'default', 'length', 'name', 'nullable', 'title', 'type']
PASS [abstract-type] modules/world_sync/models/pbl_entity_state.json#world_id type 抽象 -> 'str'
PASS [type-lower-no-dialect] modules/world_sync/models/pbl_entity_state.json#world_id -> 'str'
PASS [field-title] modules/world_sync/models/pbl_entity_state.json#world_id title 非空(DDL COMMENT 来源)
PASS [field-length] modules/world_sync/models/pbl_entity_state.json#world_id length 为正整数 -> 64
PASS [field-nullable] modules/world_sync/models/pbl_entity_state.json#world_id -> no
PASS [field-keys] modules/world_sync/models/pbl_entity_state.json#entity_id ⊆ ['dec', 'default', 'length', 'name', 'nullable', 'title', 'type']
PASS [abstract-type] modules/world_sync/models/pbl_entity_state.json#entity_id type 抽象 -> 'str'
PASS [type-lower-no-dialect] modules/world_sync/models/pbl_entity_state.json#entity_id -> 'str'
PASS [field-title] modules/world_sync/models/pbl_entity_state.json#entity_id title 非空(DDL COMMENT 来源)
PASS [field-length] modules/world_sync/models/pbl_entity_state.json#entity_id length 为正整数 -> 64
PASS [field-nullable] modules/world_sync/models/pbl_entity_state.json#entity_id -> no
PASS [field-keys] modules/world_sync/models/pbl_entity_state.json#state ⊆ ['dec', 'default', 'length', 'name', 'nullable', 'title', 'type']
PASS [abstract-type] modules/world_sync/models/pbl_entity_state.json#state type 抽象 -> 'text'
PASS [type-lower-no-dialect] modules/world_sync/models/pbl_entity_state.json#state -> 'text'
PASS [field-title] modules/world_sync/models/pbl_entity_state.json#state title 非空(DDL COMMENT 来源)
PASS [field-nullable] modules/world_sync/models/pbl_entity_state.json#state -> no
PASS [field-keys] modules/world_sync/models/pbl_entity_state.json#state_version ⊆ ['dec', 'default', 'length', 'name', 'nullable', 'title', 'type']
PASS [abstract-type] modules/world_sync/models/pbl_entity_state.json#state_version type 抽象 -> 'int'
PASS [type-lower-no-dialect] modules/world_sync/models/pbl_entity_state.json#state_version -> 'int'
PASS [field-title] modules/world_sync/models/pbl_entity_state.json#state_version title 非空(DDL COMMENT 来源)
PASS [field-nullable] modules/world_sync/models/pbl_entity_state.json#state_version -> no
PASS [field-keys] modules/world_sync/models/pbl_entity_state.json#updated_at ⊆ ['dec', 'default', 'length', 'name', 'nullable', 'title', 'type']
PASS [abstract-type] modules/world_sync/models/pbl_entity_state.json#updated_at type 抽象 -> 'datetime'
PASS [type-lower-no-dialect] modules/world_sync/models/pbl_entity_state.json#updated_at -> 'datetime'
PASS [field-title] modules/world_sync/models/pbl_entity_state.json#updated_at title 非空(DDL COMMENT 来源)
PASS [field-nullable] modules/world_sync/models/pbl_entity_state.json#updated_at -> no
PASS [field-keys] modules/world_sync/models/pbl_entity_state.json#updated_by_event ⊆ ['dec', 'default', 'length', 'name', 'nullable', 'title', 'type']
PASS [abstract-type] modules/world_sync/models/pbl_entity_state.json#updated_by_event type 抽象 -> 'str'
PASS [type-lower-no-dialect] modules/world_sync/models/pbl_entity_state.json#updated_by_event -> 'str'
PASS [field-title] modules/world_sync/models/pbl_entity_state.json#updated_by_event title 非空(DDL COMMENT 来源)
PASS [field-length] modules/world_sync/models/pbl_entity_state.json#updated_by_event length 为正整数 -> 64
PASS [field-nullable] modules/world_sync/models/pbl_entity_state.json#updated_by_event -> yes
PASS [field-id] modules/world_sync/models/pbl_entity_state.json 含 id 主键列
PASS [id-str32] modules/world_sync/models/pbl_entity_state.json id type=str length=32 -> str/32
PASS [id-notnull] modules/world_sync/models/pbl_entity_state.json id nullable=no
PASS [field-unique] modules/world_sync/models/pbl_entity_state.json 列名无重复
PASS [index-keys] modules/world_sync/models/pbl_entity_state.json#uk_es_tenant_world_entity ⊆ ['idxfields', 'idxtype', 'name']
PASS [index-type] modules/world_sync/models/pbl_entity_state.json#uk_es_tenant_world_entity -> unique
PASS [index-idxfields-array] modules/world_sync/models/pbl_entity_state.json#uk_es_tenant_world_entity -> ['tenant_id', 'world_id', 'entity_id']
PASS [index-cols-exist] modules/world_sync/models/pbl_entity_state.json#uk_es_tenant_world_entity 索引列都在 fields 中 -> ['tenant_id', 'world_id', 'entity_id']
PASS [index-keys] modules/world_sync/models/pbl_entity_state.json#ix_es_tenant_world_updated ⊆ ['idxfields', 'idxtype', 'name']
PASS [index-type] modules/world_sync/models/pbl_entity_state.json#ix_es_tenant_world_updated -> index
PASS [index-idxfields-array] modules/world_sync/models/pbl_entity_state.json#ix_es_tenant_world_updated -> ['tenant_id', 'world_id', 'updated_at']
PASS [index-cols-exist] modules/world_sync/models/pbl_entity_state.json#ix_es_tenant_world_updated 索引列都在 fields 中 -> ['tenant_id', 'world_id', 'updated_at']
PASS [index-unique-name] modules/world_sync/models/pbl_entity_state.json 索引名无重复
PASS [tenant-first] modules/world_sync/models/pbl_entity_state.json#uk_es_tenant_world_entity 唯一索引 tenant_id 打头 -> ['tenant_id', 'world_id', 'entity_id']
PASS [cross-check-sql] modules/world_sync/models/pbl_entity_state.json 覆盖 STATE_COLUMNS 7 列
PASS [no-dialect-types] modules/world_sync/models/pbl_entity_state.json 无方言具体类型
PASS [exists] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json
PASS [json-parse] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json OK
PASS [root-keys] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json ⊆ summary/fields/indexes/codes -> ['codes', 'fields', 'indexes', 'summary']
PASS [summary-count] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json 恰好 1 条 -> 1
PASS [summary-keys] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json ⊆ ['catelog', 'comment', 'name', 'primary', 'title']
PASS [summary-name] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json name==pbl_runtime_event -> pbl_runtime_event
PASS [summary-title] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json title 非空
PASS [summary-primary] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json primary==['id'] (array) -> ['id']
PASS [summary-catelog] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json catelog 合法 -> relation
PASS [fields-count] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json >=1 -> 12
PASS [field-keys] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#id ⊆ ['dec', 'default', 'length', 'name', 'nullable', 'title', 'type']
PASS [abstract-type] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#id type 抽象 -> 'str'
PASS [type-lower-no-dialect] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#id -> 'str'
PASS [field-title] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#id title 非空(DDL COMMENT 来源)
PASS [field-length] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#id length 为正整数 -> 32
PASS [field-nullable] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#id -> no
PASS [field-keys] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#event_id ⊆ ['dec', 'default', 'length', 'name', 'nullable', 'title', 'type']
PASS [abstract-type] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#event_id type 抽象 -> 'str'
PASS [type-lower-no-dialect] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#event_id -> 'str'
PASS [field-title] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#event_id title 非空(DDL COMMENT 来源)
PASS [field-length] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#event_id length 为正整数 -> 64
PASS [field-nullable] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#event_id -> no
PASS [field-keys] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#tenant_id ⊆ ['dec', 'default', 'length', 'name', 'nullable', 'title', 'type']
PASS [abstract-type] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#tenant_id type 抽象 -> 'str'
PASS [type-lower-no-dialect] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#tenant_id -> 'str'
PASS [field-title] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#tenant_id title 非空(DDL COMMENT 来源)
PASS [field-length] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#tenant_id length 为正整数 -> 64
PASS [field-nullable] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#tenant_id -> no
PASS [field-keys] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#world_id ⊆ ['dec', 'default', 'length', 'name', 'nullable', 'title', 'type']
PASS [abstract-type] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#world_id type 抽象 -> 'str'
PASS [type-lower-no-dialect] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#world_id -> 'str'
PASS [field-title] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#world_id title 非空(DDL COMMENT 来源)
PASS [field-length] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#world_id length 为正整数 -> 64
PASS [field-nullable] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#world_id -> no
PASS [field-keys] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#session_id ⊆ ['dec', 'default', 'length', 'name', 'nullable', 'title', 'type']
PASS [abstract-type] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#session_id type 抽象 -> 'str'
PASS [type-lower-no-dialect] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#session_id -> 'str'
PASS [field-title] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#session_id title 非空(DDL COMMENT 来源)
PASS [field-length] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#session_id length 为正整数 -> 64
PASS [field-nullable] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#session_id -> yes
PASS [field-keys] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#entity_id ⊆ ['dec', 'default', 'length', 'name', 'nullable', 'title', 'type']
PASS [abstract-type] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#entity_id type 抽象 -> 'str'
PASS [type-lower-no-dialect] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#entity_id -> 'str'
PASS [field-title] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#entity_id title 非空(DDL COMMENT 来源)
PASS [field-length] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#entity_id length 为正整数 -> 64
PASS [field-nullable] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#entity_id -> no
PASS [field-keys] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#event_type ⊆ ['dec', 'default', 'length', 'name', 'nullable', 'title', 'type']
PASS [abstract-type] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#event_type type 抽象 -> 'str'
PASS [type-lower-no-dialect] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#event_type -> 'str'
PASS [field-title] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#event_type title 非空(DDL COMMENT 来源)
PASS [field-length] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#event_type length 为正整数 -> 64
PASS [field-nullable] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#event_type -> no
PASS [field-keys] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#payload ⊆ ['dec', 'default', 'length', 'name', 'nullable', 'title', 'type']
PASS [abstract-type] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#payload type 抽象 -> 'text'
PASS [type-lower-no-dialect] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#payload -> 'text'
PASS [field-title] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#payload title 非空(DDL COMMENT 来源)
PASS [field-nullable] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#payload -> yes
PASS [field-keys] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#causation_id ⊆ ['dec', 'default', 'length', 'name', 'nullable', 'title', 'type']
PASS [abstract-type] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#causation_id type 抽象 -> 'str'
PASS [type-lower-no-dialect] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#causation_id -> 'str'
PASS [field-title] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#causation_id title 非空(DDL COMMENT 来源)
PASS [field-length] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#causation_id length 为正整数 -> 64
PASS [field-nullable] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#causation_id -> yes
PASS [field-keys] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#source ⊆ ['dec', 'default', 'length', 'name', 'nullable', 'title', 'type']
PASS [abstract-type] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#source type 抽象 -> 'str'
PASS [type-lower-no-dialect] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#source -> 'str'
PASS [field-title] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#source title 非空(DDL COMMENT 来源)
PASS [field-length] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#source length 为正整数 -> 64
PASS [field-nullable] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#source -> no
PASS [field-keys] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#state_version ⊆ ['dec', 'default', 'length', 'name', 'nullable', 'title', 'type']
PASS [abstract-type] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#state_version type 抽象 -> 'int'
PASS [type-lower-no-dialect] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#state_version -> 'int'
PASS [field-title] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#state_version title 非空(DDL COMMENT 来源)
PASS [field-nullable] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#state_version -> no
PASS [field-keys] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#created_at ⊆ ['dec', 'default', 'length', 'name', 'nullable', 'title', 'type']
PASS [abstract-type] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#created_at type 抽象 -> 'datetime'
PASS [type-lower-no-dialect] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#created_at -> 'datetime'
PASS [field-title] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#created_at title 非空(DDL COMMENT 来源)
PASS [field-nullable] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#created_at -> no
PASS [field-id] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json 含 id 主键列
PASS [id-str32] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json id type=str length=32 -> str/32
PASS [id-notnull] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json id nullable=no
PASS [field-unique] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json 列名无重复
PASS [index-keys] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#uk_re_tenant_event_id ⊆ ['idxfields', 'idxtype', 'name']
PASS [index-type] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#uk_re_tenant_event_id -> unique
PASS [index-idxfields-array] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#uk_re_tenant_event_id -> ['tenant_id', 'event_id']
PASS [index-cols-exist] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#uk_re_tenant_event_id 索引列都在 fields 中 -> ['tenant_id', 'event_id']
PASS [index-keys] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#uk_re_tenant_world_entity_created ⊆ ['idxfields', 'idxtype', 'name']
PASS [index-type] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#uk_re_tenant_world_entity_created -> unique
PASS [index-idxfields-array] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#uk_re_tenant_world_entity_created -> ['tenant_id', 'world_id', 'entity_id', 'state_version']
PASS [index-cols-exist] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#uk_re_tenant_world_entity_created 索引列都在 fields 中 -> ['tenant_id', 'world_id', 'entity_id', 'state_version']
PASS [index-keys] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#ix_re_tenant_world_entity ⊆ ['idxfields', 'idxtype', 'name']
PASS [index-type] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#ix_re_tenant_world_entity -> index
PASS [index-idxfields-array] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#ix_re_tenant_world_entity -> ['tenant_id', 'world_id', 'entity_id']
PASS [index-cols-exist] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#ix_re_tenant_world_entity 索引列都在 fields 中 -> ['tenant_id', 'world_id', 'entity_id']
PASS [index-keys] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#ix_re_tenant_world_type_created ⊆ ['idxfields', 'idxtype', 'name']
PASS [index-type] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#ix_re_tenant_world_type_created -> index
PASS [index-idxfields-array] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#ix_re_tenant_world_type_created -> ['tenant_id', 'world_id', 'event_type', 'created_at']
PASS [index-cols-exist] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#ix_re_tenant_world_type_created 索引列都在 fields 中 -> ['tenant_id', 'world_id', 'event_type', 'created_at']
PASS [index-keys] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#ix_re_created_at ⊆ ['idxfields', 'idxtype', 'name']
PASS [index-type] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#ix_re_created_at -> index
PASS [index-idxfields-array] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#ix_re_created_at -> ['created_at']
PASS [index-cols-exist] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#ix_re_created_at 索引列都在 fields 中 -> ['created_at']
PASS [index-unique-name] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json 索引名无重复
PASS [tenant-first] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#uk_re_tenant_event_id 唯一索引 tenant_id 打头 -> ['tenant_id', 'event_id']
PASS [tenant-first] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#uk_re_tenant_world_entity_created 唯一索引 tenant_id 打头 -> ['tenant_id', 'world_id', 'entity_id', 'state_version']
PASS [cross-check-sql] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json 覆盖 EVENT_COLUMNS 11 列
PASS [code-keys] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#event_type ⊆ ['cond', 'field', 'table', 'textfield', 'valuefield']
PASS [code-table-no-dot] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#event_type table=appcodes_kv
PASS [code-cond-parentid] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#event_type cond=parentid='pbl_event_type'
PASS [code-no-dup] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json field=event_type 不重复
PASS [code-field-exists] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json field=event_type 在 fields 中
PASS [no-dialect-types] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json 无方言具体类型
PASS [exists] apps/scense/pkgs/world_sync/models/pbl_entity_state.json
PASS [json-parse] apps/scense/pkgs/world_sync/models/pbl_entity_state.json OK
PASS [root-keys] apps/scense/pkgs/world_sync/models/pbl_entity_state.json ⊆ summary/fields/indexes/codes -> ['codes', 'fields', 'indexes', 'summary']
PASS [summary-count] apps/scense/pkgs/world_sync/models/pbl_entity_state.json 恰好 1 条 -> 1
PASS [summary-keys] apps/scense/pkgs/world_sync/models/pbl_entity_state.json ⊆ ['catelog', 'comment', 'name', 'primary', 'title']
PASS [summary-name] apps/scense/pkgs/world_sync/models/pbl_entity_state.json name==pbl_entity_state -> pbl_entity_state
PASS [summary-title] apps/scense/pkgs/world_sync/models/pbl_entity_state.json title 非空
PASS [summary-primary] apps/scense/pkgs/world_sync/models/pbl_entity_state.json primary==['id'] (array) -> ['id']
PASS [summary-catelog] apps/scense/pkgs/world_sync/models/pbl_entity_state.json catelog 合法 -> relation
PASS [fields-count] apps/scense/pkgs/world_sync/models/pbl_entity_state.json >=1 -> 8
PASS [field-keys] apps/scense/pkgs/world_sync/models/pbl_entity_state.json#id ⊆ ['dec', 'default', 'length', 'name', 'nullable', 'title', 'type']
PASS [abstract-type] apps/scense/pkgs/world_sync/models/pbl_entity_state.json#id type 抽象 -> 'str'
PASS [type-lower-no-dialect] apps/scense/pkgs/world_sync/models/pbl_entity_state.json#id -> 'str'
PASS [field-title] apps/scense/pkgs/world_sync/models/pbl_entity_state.json#id title 非空(DDL COMMENT 来源)
PASS [field-length] apps/scense/pkgs/world_sync/models/pbl_entity_state.json#id length 为正整数 -> 32
PASS [field-nullable] apps/scense/pkgs/world_sync/models/pbl_entity_state.json#id -> no
PASS [field-keys] apps/scense/pkgs/world_sync/models/pbl_entity_state.json#tenant_id ⊆ ['dec', 'default', 'length', 'name', 'nullable', 'title', 'type']
PASS [abstract-type] apps/scense/pkgs/world_sync/models/pbl_entity_state.json#tenant_id type 抽象 -> 'str'
PASS [type-lower-no-dialect] apps/scense/pkgs/world_sync/models/pbl_entity_state.json#tenant_id -> 'str'
PASS [field-title] apps/scense/pkgs/world_sync/models/pbl_entity_state.json#tenant_id title 非空(DDL COMMENT 来源)
PASS [field-length] apps/scense/pkgs/world_sync/models/pbl_entity_state.json#tenant_id length 为正整数 -> 64
PASS [field-nullable] apps/scense/pkgs/world_sync/models/pbl_entity_state.json#tenant_id -> no
PASS [field-keys] apps/scense/pkgs/world_sync/models/pbl_entity_state.json#world_id ⊆ ['dec', 'default', 'length', 'name', 'nullable', 'title', 'type']
PASS [abstract-type] apps/scense/pkgs/world_sync/models/pbl_entity_state.json#world_id type 抽象 -> 'str'
PASS [type-lower-no-dialect] apps/scense/pkgs/world_sync/models/pbl_entity_state.json#world_id -> 'str'
PASS [field-title] apps/scense/pkgs/world_sync/models/pbl_entity_state.json#world_id title 非空(DDL COMMENT 来源)
PASS [field-length] apps/scense/pkgs/world_sync/models/pbl_entity_state.json#world_id length 为正整数 -> 64
PASS [field-nullable] apps/scense/pkgs/world_sync/models/pbl_entity_state.json#world_id -> no
PASS [field-keys] apps/scense/pkgs/world_sync/models/pbl_entity_state.json#entity_id ⊆ ['dec', 'default', 'length', 'name', 'nullable', 'title', 'type']
PASS [abstract-type] apps/scense/pkgs/world_sync/models/pbl_entity_state.json#entity_id type 抽象 -> 'str'
PASS [type-lower-no-dialect] apps/scense/pkgs/world_sync/models/pbl_entity_state.json#entity_id -> 'str'
PASS [field-title] apps/scense/pkgs/world_sync/models/pbl_entity_state.json#entity_id title 非空(DDL COMMENT 来源)
PASS [field-length] apps/scense/pkgs/world_sync/models/pbl_entity_state.json#entity_id length 为正整数 -> 64
PASS [field-nullable] apps/scense/pkgs/world_sync/models/pbl_entity_state.json#entity_id -> no
PASS [field-keys] apps/scense/pkgs/world_sync/models/pbl_entity_state.json#state ⊆ ['dec', 'default', 'length', 'name', 'nullable', 'title', 'type']
PASS [abstract-type] apps/scense/pkgs/world_sync/models/pbl_entity_state.json#state type 抽象 -> 'text'
PASS [type-lower-no-dialect] apps/scense/pkgs/world_sync/models/pbl_entity_state.json#state -> 'text'
PASS [field-title] apps/scense/pkgs/world_sync/models/pbl_entity_state.json#state title 非空(DDL COMMENT 来源)
PASS [field-nullable] apps/scense/pkgs/world_sync/models/pbl_entity_state.json#state -> no
PASS [field-keys] apps/scense/pkgs/world_sync/models/pbl_entity_state.json#state_version ⊆ ['dec', 'default', 'length', 'name', 'nullable', 'title', 'type']
PASS [abstract-type] apps/scense/pkgs/world_sync/models/pbl_entity_state.json#state_version type 抽象 -> 'int'
PASS [type-lower-no-dialect] apps/scense/pkgs/world_sync/models/pbl_entity_state.json#state_version -> 'int'
PASS [field-title] apps/scense/pkgs/world_sync/models/pbl_entity_state.json#state_version title 非空(DDL COMMENT 来源)
PASS [field-nullable] apps/scense/pkgs/world_sync/models/pbl_entity_state.json#state_version -> no
PASS [field-keys] apps/scense/pkgs/world_sync/models/pbl_entity_state.json#updated_at ⊆ ['dec', 'default', 'length', 'name', 'nullable', 'title', 'type']
PASS [abstract-type] apps/scense/pkgs/world_sync/models/pbl_entity_state.json#updated_at type 抽象 -> 'datetime'
PASS [type-lower-no-dialect] apps/scense/pkgs/world_sync/models/pbl_entity_state.json#updated_at -> 'datetime'
PASS [field-title] apps/scense/pkgs/world_sync/models/pbl_entity_state.json#updated_at title 非空(DDL COMMENT 来源)
PASS [field-nullable] apps/scense/pkgs/world_sync/models/pbl_entity_state.json#updated_at -> no
PASS [field-keys] apps/scense/pkgs/world_sync/models/pbl_entity_state.json#updated_by_event ⊆ ['dec', 'default', 'length', 'name', 'nullable', 'title', 'type']
PASS [abstract-type] apps/scense/pkgs/world_sync/models/pbl_entity_state.json#updated_by_event type 抽象 -> 'str'
PASS [type-lower-no-dialect] apps/scense/pkgs/world_sync/models/pbl_entity_state.json#updated_by_event -> 'str'
PASS [field-title] apps/scense/pkgs/world_sync/models/pbl_entity_state.json#updated_by_event title 非空(DDL COMMENT 来源)
PASS [field-length] apps/scense/pkgs/world_sync/models/pbl_entity_state.json#updated_by_event length 为正整数 -> 64
PASS [field-nullable] apps/scense/pkgs/world_sync/models/pbl_entity_state.json#updated_by_event -> yes
PASS [field-id] apps/scense/pkgs/world_sync/models/pbl_entity_state.json 含 id 主键列
PASS [id-str32] apps/scense/pkgs/world_sync/models/pbl_entity_state.json id type=str length=32 -> str/32
PASS [id-notnull] apps/scense/pkgs/world_sync/models/pbl_entity_state.json id nullable=no
PASS [field-unique] apps/scense/pkgs/world_sync/models/pbl_entity_state.json 列名无重复
PASS [index-keys] apps/scense/pkgs/world_sync/models/pbl_entity_state.json#uk_es_tenant_world_entity ⊆ ['idxfields', 'idxtype', 'name']
PASS [index-type] apps/scense/pkgs/world_sync/models/pbl_entity_state.json#uk_es_tenant_world_entity -> unique
PASS [index-idxfields-array] apps/scense/pkgs/world_sync/models/pbl_entity_state.json#uk_es_tenant_world_entity -> ['tenant_id', 'world_id', 'entity_id']
PASS [index-cols-exist] apps/scense/pkgs/world_sync/models/pbl_entity_state.json#uk_es_tenant_world_entity 索引列都在 fields 中 -> ['tenant_id', 'world_id', 'entity_id']
PASS [index-keys] apps/scense/pkgs/world_sync/models/pbl_entity_state.json#ix_es_tenant_world_updated ⊆ ['idxfields', 'idxtype', 'name']
PASS [index-type] apps/scense/pkgs/world_sync/models/pbl_entity_state.json#ix_es_tenant_world_updated -> index
PASS [index-idxfields-array] apps/scense/pkgs/world_sync/models/pbl_entity_state.json#ix_es_tenant_world_updated -> ['tenant_id', 'world_id', 'updated_at']
PASS [index-cols-exist] apps/scense/pkgs/world_sync/models/pbl_entity_state.json#ix_es_tenant_world_updated 索引列都在 fields 中 -> ['tenant_id', 'world_id', 'updated_at']
PASS [index-unique-name] apps/scense/pkgs/world_sync/models/pbl_entity_state.json 索引名无重复
PASS [tenant-first] apps/scense/pkgs/world_sync/models/pbl_entity_state.json#uk_es_tenant_world_entity 唯一索引 tenant_id 打头 -> ['tenant_id', 'world_id', 'entity_id']
PASS [cross-check-sql] apps/scense/pkgs/world_sync/models/pbl_entity_state.json 覆盖 STATE_COLUMNS 7 列
PASS [no-dialect-types] apps/scense/pkgs/world_sync/models/pbl_entity_state.json 无方言具体类型
PASS [mirror-sha256-equal] pbl_runtime_event 真源与镜像字节级一致 -> True
PASS [mirror-sha256-equal] pbl_entity_state 真源与镜像字节级一致 -> True
断言合计 370PASS 370 / FAIL 0
RESULT: PASS

View File

@ -0,0 +1,98 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""M11b-2b 表定义镜像同步器modules/world_sync/models/ 是唯一真源,
apps/scense/pkgs/world_sync/models/ 是打包镜像禁止手工双写
用法:
python modules/world_sync/scripts/sync_models_to_app.py # 同步镜像
python modules/world_sync/scripts/sync_models_to_app.py --check # 只校验,不写
行为:
1. 逐个把真源表定义 JSON 规范化的同一份字节写入镜像落点覆盖式
2. 打印两两 sha256 比对结果--check 模式下若不一致退出码 1
这样 QC 抽检任一落点都拿到同一权威文本消除多份互相矛盾定义
"""
from __future__ import annotations
import argparse
import hashlib
import json
import os
import sys
# 本文件位于 modules/world_sync/scripts/,向上三级 = 机构工作空间根
HERE = os.path.dirname(os.path.abspath(__file__))
WS_ROOT = os.path.abspath(os.path.join(HERE, "..", "..", ".."))
SRC_DIR = os.path.join("modules", "world_sync", "models")
DST_DIR = os.path.join("apps", "scense", "pkgs", "world_sync", "models")
# 只同步 M11b-2 交付的两张表定义(其余 world_sync*.json 属历史既有文件,不在本任务范围)
TABLES = ("pbl_runtime_event.json", "pbl_entity_state.json")
def sha256_of(path: str) -> str:
h = hashlib.sha256()
with open(path, "rb") as fh:
for chunk in iter(lambda: fh.read(65536), b""):
h.update(chunk)
return h.hexdigest()
def canonical_bytes(path: str) -> bytes:
"""读入 JSON 后按统一方式序列化,保证真源与镜像字节级一致(消除缩进/空格差异)。"""
with open(path, "r", encoding="utf-8") as fh:
data = json.load(fh)
return (json.dumps(data, ensure_ascii=False, indent=2) + "\n").encode("utf-8")
def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument("--check", action="store_true", help="只比对不写入")
args = ap.parse_args()
src_dir = os.path.join(WS_ROOT, SRC_DIR)
dst_dir = os.path.join(WS_ROOT, DST_DIR)
if not os.path.isdir(src_dir):
print(f"FAIL: 真源目录不存在 {src_dir}", file=sys.stderr)
return 1
os.makedirs(dst_dir, exist_ok=True)
bad = 0
for name in TABLES:
src = os.path.join(src_dir, name)
dst = os.path.join(dst_dir, name)
blob = canonical_bytes(src)
if args.check:
same = os.path.isfile(dst) and open(dst, "rb").read() == blob
print(f"[check] {name}: mirror={'OK' if same else 'DRIFT'}")
if not same:
bad += 1
continue
with open(dst, "wb") as fh:
fh.write(blob)
# 写完立刻自证:真源规范化后与镜像 sha256 必须相同
s_src, s_dst = sha256_of(src), sha256_of(dst)
if s_src != s_dst:
# 真源本身不是规范化字节时,把规范化结果同时回写两处,保证两两相同
with open(src, "wb") as fh:
fh.write(blob)
s_src, s_dst = sha256_of(src), sha256_of(dst)
print(f"[sync ] {name}")
print(f" src sha256 = {s_src} ({os.path.join(SRC_DIR, name)})")
print(f" dst sha256 = {s_dst} ({os.path.join(DST_DIR, name)})")
print(f" MATCH = {s_src == s_dst}")
if s_src != s_dst:
bad += 1
if bad:
print(f"RESULT: FAIL ({bad} 个镜像不一致)")
return 1
print("RESULT: OK (真源 modules/world_sync/models -> 镜像 apps/scense/pkgs/world_sync/models 已对齐)")
return 0
if __name__ == "__main__":
raise SystemExit(main())