pbl_compiler/pbl_compiler/gd_rules_writer.py
2026-09-19 09:00:57 +08:00

270 lines
11 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# -*- coding: utf-8 -*-
"""pbl_compiler.gd_rules_writer —— M3b 第12章收口Game Definition 规则产物落库。
背景(本轮 M3b 整改的真实缺口):
exporter.write_game_definition() 用「候选列名探测」写 GD候选集是
('game_definition','gd_json','definition','content'),而 models/pbl_game_definition.json
的真实列名是 `definition_json` / `content_fingerprint` / `rules_json` / `rules_hash` /
`rule_count` / `rules_manifest` / `compile_status` / `quality_state` / `world_id` /
`scene_id` / `duration_ms` / `error_message`。探测不命中 → M3b 的规则映射产物
rules_json / rules_hash / rule_count / rules_manifest根本没写进 game_definition
「规则导出与 game_definition 写入」这条需求等于没落地。
本文件是 M3b 的**权威 GD 写入器**:列名直接对齐 models/pbl_game_definition.json
显式列名 INSERT/UPDATE不靠位置、不靠猜并把规则四件套一并落库。
铁律:
- 所有 SELECT/INSERT/UPDATE 一律 tenant_id 打头,缺失即抛 CompileErrorfail-closed
- 幂等:同 (tenant_id, blueprint_id, blueprint_version_no) 重复写只更新不新增;
同输入必同 content_fingerprint / rules_hash29.6 确定性,无时间戳无 uuid 进正文)。
- created_at/updated_at 属审计字段,只写审计列,绝不参与指纹计算。
"""
import json
from .script_mapping import (
CompileError,
Issue,
canonical_rules_json,
build_rules_manifest,
canonical_dumps,
sha256_text,
strip_volatile,
to_int,
)
__all__ = [
'GD_TABLE',
'GD_COLUMNS',
'gd_primary_id',
'build_gd_row',
'write_gd_rules',
'read_gd_rules',
'read_gd_rules_by_id',
]
#: 表名与列集合,唯一事实源 = models/pbl_game_definition.json勿在此表外造列
GD_TABLE = 'pbl_game_definition'
GD_COLUMNS = (
'id', 'tenant_id', 'blueprint_id', 'blueprint_version_no', 'compiler_version_id',
'content_fingerprint', 'definition_json',
'rules_json', 'rules_hash', 'rule_count', 'rules_manifest',
'world_id', 'scene_id', 'compile_status', 'quality_state',
'duration_ms', 'error_message',
'created_by', 'created_at', 'updated_by', 'updated_at', 'is_deleted',
)
#: 参与内容指纹的列29.6:排除审计/耗时/状态等易变列)
_FINGERPRINT_COLUMNS = ('blueprint_id', 'blueprint_version_no', 'definition_json',
'rules_json')
def _text(value):
"""任意对象 → 稳定文本None → 空串dict/list → canonical JSON"""
if value is None:
return ''
if isinstance(value, str):
return value
if isinstance(value, (dict, list)):
return canonical_dumps(strip_volatile(value))
return str(value)
def gd_primary_id(tenant_id, blueprint_id, blueprint_version_no, content_fingerprint):
"""确定性主键sha256(tenant|bp|ver|fp) 前 32 位十六进制。
不用 uuid/不自增 —— 保证「同输入重编译 → 同一行」,幂等 upsert 才成立。
"""
key = '|'.join([str(tenant_id or ''), str(blueprint_id or ''),
str(blueprint_version_no or ''), str(content_fingerprint or '')])
return sha256_text(key)[:32]
def content_fingerprint_of(row):
"""按参与指纹的列计算 GD 内容指纹(列序固定,与 sort_keys 无关)。"""
parts = []
for col in _FINGERPRINT_COLUMNS:
parts.append('%s=%s' % (col, _text(row.get(col))))
return sha256_text('\n'.join(parts))
def build_gd_row(gd, ctx, rules=None, manifest=None, base_definition_json=None,
issues=None):
"""组装一行 pbl_game_definition 记录(列名对齐 models不猜列
gd: M3a gd_builder + M3b rules 顶层键合成后的完整 GD dict10 顶层键 + rules
ctx: tenant_id* / blueprint_id* / blueprint_version* / compiler_version_id /
world_id / scene_id / quality_state / compile_status / duration_ms /
created_by / error_message
rules: build_rules() 产出的 script_type=1 规则列表
返回 (row_dict, issues)
"""
issues = list(issues or [])
ctx = dict(ctx or {})
tenant_id = str(ctx.get('tenant_id') or '').strip()
if not tenant_id:
raise CompileError('E_TENANT_REQUIRED', 'tenant_id 缺失,拒绝写入 game_definition')
blueprint_id = str(ctx.get('blueprint_id') or ctx.get('bp_id') or '').strip()
blueprint_version_no = to_int(ctx.get('blueprint_version')
or ctx.get('blueprint_version_no'), 0)
if not blueprint_id or blueprint_version_no <= 0:
raise CompileError('E_GD_KEY_MISSING',
'blueprint_id / blueprint_version_no 缺失或非法,无法定位 game_definition')
rules = list(rules or [])
manifest = manifest or build_rules_manifest(rules)
gd = dict(gd or {})
gd_text = canonical_dumps(strip_volatile(gd))
rules_text = canonical_rules_json(rules)
row = {
'tenant_id': tenant_id,
'blueprint_id': blueprint_id,
'blueprint_version_no': blueprint_version_no,
'compiler_version_id': _text(ctx.get('compiler_version_id')
or ctx.get('compiler_version') or '')[:32],
'definition_json': base_definition_json if base_definition_json is not None else gd_text,
'rules_json': rules_text,
'rules_hash': _text(manifest.get('rules_hash'))[:64],
'rule_count': to_int(manifest.get('rule_count'), len(rules)),
'rules_manifest': canonical_dumps(strip_volatile(manifest)),
'world_id': _text(ctx.get('world_id'))[:32],
'scene_id': _text(ctx.get('scene_id'))[:32],
'compile_status': _text(ctx.get('compile_status') or ctx.get('status')
or 'compiled')[:32],
'quality_state': _text(ctx.get('quality_state'))[:64],
'duration_ms': to_int(ctx.get('duration_ms'), 0),
'error_message': _text(ctx.get('error_message'))[:60000],
'created_by': _text(ctx.get('created_by') or ctx.get('actor_id'))[:32],
'is_deleted': '0',
}
row['content_fingerprint'] = content_fingerprint_of(row)
row['id'] = gd_primary_id(row['tenant_id'], row['blueprint_id'],
row['blueprint_version_no'], row['content_fingerprint'])
if not row['rules_hash']:
issues.append(Issue('error', 'E_RULES_HASH_EMPTY',
'rules_manifest 未给出 rules_hash规则产物不可信',
'', 'rules_hash'))
if row['rule_count'] != len(rules):
issues.append(Issue('warning', 'W_RULE_COUNT_MISMATCH',
'rule_count=%s 与实际规则数=%s 不一致'
% (row['rule_count'], len(rules)), '', 'rule_count'))
return row, issues
async def _available_columns(sor, table):
"""探测实际库中该表已有列(跨宿主/未跑 DDL 时降级,不猜列名写 SQL"""
try:
recs = await sor.sqlExe(
'SELECT COLUMN_NAME AS c FROM INFORMATION_SCHEMA.COLUMNS '
'WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME=${tbl}$', {'tbl': table})
cols = set()
for r in recs or []:
v = getattr(r, 'c', None) or getattr(r, 'COLUMN_NAME', None) \
or getattr(r, 'column_name', None)
if v:
cols.add(str(v))
if cols:
return cols
except Exception:
pass
return set(GD_COLUMNS)
async def write_gd_rules(sor, row, ctx=None):
"""幂等 upsert 一行 game_definition含规则四件套
唯一键tenant_id + blueprint_id + blueprint_version_noidx_gd_bp
返回 {'written','mode','gd_id','content_fingerprint','rules_hash','rule_count'}
"""
ctx = dict(ctx or {})
tenant_id = str(row.get('tenant_id') or '').strip()
if not tenant_id:
raise CompileError('E_TENANT_REQUIRED', 'tenant_id 缺失,拒绝写入 game_definition')
cols = await _available_columns(sor, GD_TABLE)
payload = {k: v for k, v in row.items() if k in cols and k != 'id'}
missing = [c for c in ('definition_json', 'rules_json', 'rules_hash', 'rule_count')
if c not in cols]
if missing:
raise CompileError('E_GD_COLUMN_MISSING',
'pbl_game_definition 缺少 M3b 规则产物列 %s,请先执行 models/'
'pbl_game_definition.json 生成的 DDLjson2ddl' % missing)
where = {'tenant_id': tenant_id,
'blueprint_id': str(row.get('blueprint_id') or ''),
'blueprint_version_no': int(row.get('blueprint_version_no') or 0)}
try:
exist = await sor.R(GD_TABLE, dict(where, is_deleted='0'))
except Exception as e:
raise CompileError('E_GD_QUERY', '查询 game_definition 失败: %s' % e)
now = ctx.get('now_str') or ''
try:
if exist:
old_id = str(getattr(exist[0], 'id', '') or '')
upd = dict(payload)
if 'updated_at' in cols and now:
upd['updated_at'] = now
await sor.U(GD_TABLE, dict(upd, **{'id': old_id}))
mode, gd_id = 'update', old_id
else:
ins = dict(payload)
if 'id' in cols:
ins['id'] = row['id']
if 'created_at' in cols and now:
ins['created_at'] = now
await sor.C(GD_TABLE, ins)
mode, gd_id = 'insert', str(row.get('id') or '')
except Exception as e:
raise CompileError('E_GD_WRITE', '写入 game_definition 失败: %s' % e)
return {
'written': True,
'mode': mode,
'gd_id': gd_id,
'content_fingerprint': row.get('content_fingerprint'),
'rules_hash': row.get('rules_hash'),
'rule_count': row.get('rule_count'),
}
async def read_gd_rules(sor, tenant_id, blueprint_id, blueprint_version_no=None):
"""读回 GD含规则四件套rules_json 解析成 list。"""
if not tenant_id:
raise CompileError('E_TENANT_REQUIRED', 'tenant_id 缺失')
where = {'tenant_id': str(tenant_id), 'blueprint_id': str(blueprint_id or '')}
if blueprint_version_no not in (None, ''):
where['blueprint_version_no'] = int(blueprint_version_no)
recs = await sor.R(GD_TABLE, where)
out = []
for r in recs or []:
d = {c: getattr(r, c, None) for c in GD_COLUMNS}
if d.get('rules_json'):
try:
d['rules'] = json.loads(d['rules_json'])
except Exception:
d['rules'] = []
out.append(d)
return out
async def read_gd_rules_by_id(sor, tenant_id, gd_id):
"""按主键读单行(租户打头,跨租户查不到即 None"""
if not tenant_id or not gd_id:
raise CompileError('E_PARAM_MISSING', 'tenant_id / id 均为必填')
recs = await sor.R(GD_TABLE, {'tenant_id': str(tenant_id), 'id': str(gd_id)})
if not recs:
return None
r = recs[0]
d = {c: getattr(r, c, None) for c in GD_COLUMNS}
for key in ('rules_json', 'rules_manifest', 'definition_json'):
if d.get(key):
try:
d[key + '_parsed'] = json.loads(d[key])
except Exception:
d[key + '_parsed'] = None
return d