From 882a1b5cd3bd8822eeb1ff73db0647142879d9a0 Mon Sep 17 00:00:00 2001 From: "agent.develop" Date: Sun, 20 Sep 2026 16:39:12 +0800 Subject: [PATCH] =?UTF-8?q?deliver:=20=E4=BA=A4=E4=BB=98=E6=94=B6=E5=8F=A3?= =?UTF-8?q?=EF=BC=88=E5=BC=95=E6=93=8E=E4=BB=A3=E4=B8=BA=E6=8F=90=E4=BA=A4?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- __init__.py | 106 +++++++- models/pbl_entity_state.json | 92 +++++-- models/pbl_runtime_event.json | 164 ++++++++++-- scripts/m11b2_selftest.py | 402 ++++++++++++++++-------------- scripts/m11b2_selftest_output.txt | 385 ++++++++++++++++++++++++++++ scripts/sync_models_to_app.py | 98 ++++++++ 6 files changed, 1017 insertions(+), 230 deletions(-) create mode 100644 scripts/m11b2_selftest_output.txt create mode 100644 scripts/sync_models_to_app.py diff --git a/__init__.py b/__init__.py index fedbe19..470214e 100644 --- a/__init__.py +++ b/__init__.py @@ -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 CRUD、M11b-1 表注册、 + M11b-2 conn_factory 登记到 ServerEnv; +- 本文件:把两层能力合并成模块级 API,loader 函数名统一为 ``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 diff --git a/models/pbl_entity_state.json b/models/pbl_entity_state.json index 5e15587..337a82e 100644 --- a/models/pbl_entity_state.json +++ b/models/pbl_entity_state.json @@ -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.json(M11a 侧)使用 state_json/checksum/updated_by 等不同列集与 int 自增 id,属另一张同名表的另一套定义,两者不可混用;本文件为 world_sync(M11b-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.json(M11a 侧)使用 state_json/checksum/updated_by 等不同列集与 int 自增 id,属另一张同名表的另一套定义,两者不可混用;本文件为 world_sync(M11b-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/comment;fields 仅含 name/title/type/length/dec/nullable/default(说明文字一律写进 title,DDL 模板按 title 渲染 COMMENT);indexes 仅含 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": "主键ID(str32,应用层生成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": "实体ID(tenant_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": [] } diff --git a/models/pbl_runtime_event.json b/models/pbl_runtime_event.json index ef2ba61..25d7788 100644 --- a/models/pbl_runtime_event.json +++ b/models/pbl_runtime_event.json @@ -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-1(pbl_runtime_ext)侧的同一表名定义,其真源为 scripts/pbl_runtime_event_ddl.py 的 COLUMNS/PRIMARY_KEY/UNIQUE_KEYS(25 列、复合主键 (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-1(pbl_runtime_ext)侧的同一表名定义,其真源为 scripts/pbl_runtime_event_ddl.py 的 COLUMNS/PRIMARY_KEY/UNIQUE_KEYS(25 列、复合主键 (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/comment;fields 仅含 name/title/type/length/dec/nullable/default(说明文字一律写进 title,DDL 模板按 title 渲染 COMMENT);indexes 仅含 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_text);append-only 表不设 updated_at"} + { + "name": "id", + "title": "主键ID(str32,应用层生成uuid4().hex,非数据库自增)", + "type": "str", + "length": 32, + "nullable": "no" + }, + { + "name": "event_id", + "title": "业务事件ID(uuid 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'" + } ] } diff --git a/scripts/m11b2_selftest.py b/scripts/m11b2_selftest.py index 79d691f..fa63d12 100644 --- a/scripts/m11b2_selftest.py +++ b/scripts/m11b2_selftest.py @@ -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()) diff --git a/scripts/m11b2_selftest_output.txt b/scripts/m11b2_selftest_output.txt new file mode 100644 index 0000000..f0b06e4 --- /dev/null +++ b/scripts/m11b2_selftest_output.txt @@ -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 + +断言合计 370:PASS 370 / FAIL 0 +RESULT: PASS diff --git a/scripts/sync_models_to_app.py b/scripts/sync_models_to_app.py new file mode 100644 index 0000000..b16b5a7 --- /dev/null +++ b/scripts/sync_models_to_app.py @@ -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())