213 lines
9.6 KiB
Python
213 lines
9.6 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""pbl_compiler.gd_artifact_writer —— M3b 唯一 GD 写入入口(QC #2 整改)。
|
||
|
||
背景(本轮 QC #2 指出的真实缺口):
|
||
· exporter.write_game_definition() 用「候选列名探测」写 GD(候选集
|
||
('game_definition','gd_json','definition','content') 等),与
|
||
models/pbl_game_definition.json 的真实列名(definition_json / rules_json /
|
||
rules_hash / rule_count / rules_manifest / compile_status / quality_state …)
|
||
不匹配 → 探测不命中,M3b 规则四件套根本没落库;
|
||
· gd_rules_writer.write_gd_rules / build_gd_row 只被同样未接线的
|
||
rules_export_api 引用,编译主流程 pbl_compiler_compile 完全没调用 → 死代码。
|
||
|
||
本文件把三者收口成一条链,并作为 **GD 规则产物的唯一写入口**:
|
||
编译主流程 api.compile_blueprint ─┐
|
||
Web 契约 rules_export_api ────────┼→ gd_artifact_writer → gd_rules_writer → pbl_game_definition
|
||
exporter.write_game_definition ───┘(改为委托本模块,候选列名探测路径已停用)
|
||
|
||
三个入口函数:
|
||
write_gd_definition_via_writer(sor, gd, gd_text, gd_hash, ctx, issues)
|
||
整行 upsert(供 exporter.write_game_definition 委托调用,签名向后兼容)。
|
||
update_gd_rule_artifacts(sor, ...)
|
||
只回填规则四件套(供 M3a 结构编译落库后写 rules_json/rules_hash/rule_count/
|
||
rules_manifest,不重算 content_fingerprint,保持 29.6 确定性指纹语义不变)。
|
||
read_back_rule_artifacts(sor, tenant_id, blueprint_id[, blueprint_version_no])
|
||
SELECT 读回四件套(自测与前端展示用)。
|
||
|
||
铁律:tenant_id 打头;显式列名(不靠位置、不猜列名);同输入必同产物(无随机、
|
||
无时间戳进正文);error 级 issue 存在时规则正文写空并记 error_message(fail-closed)。
|
||
"""
|
||
|
||
from pbl_compiler.gd_rules_writer import (
|
||
GD_COLUMNS,
|
||
GD_TABLE,
|
||
build_gd_row,
|
||
read_gd_rules,
|
||
write_gd_rules,
|
||
)
|
||
from pbl_compiler.script_mapping import (
|
||
CompileError,
|
||
Issue,
|
||
build_rules_manifest,
|
||
canonical_rules_json,
|
||
has_errors,
|
||
to_int,
|
||
)
|
||
|
||
__all__ = [
|
||
'GD_TABLE',
|
||
'RULE_ARTIFACT_COLUMNS',
|
||
'write_gd_definition_via_writer',
|
||
'update_gd_rule_artifacts',
|
||
'read_back_rule_artifacts',
|
||
]
|
||
|
||
#: M3b 规则产物列(唯一事实源 = models/pbl_game_definition.json)
|
||
RULE_ARTIFACT_COLUMNS = ('rules_json', 'rules_hash', 'rule_count', 'rules_manifest',
|
||
'compile_status', 'quality_state', 'duration_ms', 'error_message')
|
||
|
||
|
||
async def _cols(sor, table=GD_TABLE):
|
||
"""可用列集合(拿不到时退回 models 声明的列集,绝不猜额外列名)。"""
|
||
try:
|
||
from pbl_compiler.gd_rules_writer import _available_columns
|
||
cols = await _available_columns(sor, table)
|
||
if cols:
|
||
return set(cols)
|
||
except Exception:
|
||
pass
|
||
return set(GD_COLUMNS)
|
||
|
||
|
||
def _require(tenant_id):
|
||
tid = str(tenant_id or '').strip()
|
||
if not tid:
|
||
raise CompileError('E_TENANT_REQUIRED', 'tenant_id 缺失,拒绝写入 game_definition')
|
||
return tid
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 入口 1:整行 upsert(exporter.write_game_definition 委托到此)
|
||
# ---------------------------------------------------------------------------
|
||
|
||
async def write_gd_definition_via_writer(sor, gd, gd_text, gd_hash, ctx, issues=None):
|
||
"""把 GD(含第12章规则四件套)整行 upsert 到 pbl_game_definition。
|
||
|
||
列名对齐 models/pbl_game_definition.json,不再做候选列名探测。
|
||
返回结构与旧 exporter.write_game_definition 兼容:
|
||
{'written','unchanged','table','gd_hash','id','content_fingerprint','issues'}
|
||
"""
|
||
issues = list(issues or [])
|
||
ctx = dict(ctx or {})
|
||
tid = _require(ctx.get('tenant_id'))
|
||
gd = gd if isinstance(gd, dict) else {}
|
||
rules_block = gd.get('rules') or {}
|
||
rules = list(rules_block.get('rules') or [])
|
||
manifest = rules_block.get('manifest') or build_rules_manifest(rules)
|
||
if has_errors(issues):
|
||
rules = [] # fail-closed:不合格规则绝不落库
|
||
manifest = build_rules_manifest([])
|
||
row, row_issues = build_gd_row(
|
||
gd, ctx, rules, manifest,
|
||
base_definition_json=gd_text, issues=issues)
|
||
issues.extend(row_issues)
|
||
if has_errors(row_issues):
|
||
raise CompileError('E_GD_ROW_INVALID', 'GD 行组装校验未通过,拒绝写入',
|
||
{'issues': [i for i in row_issues if i['severity'] == 'error']})
|
||
res = await write_gd_rules(sor, row, ctx)
|
||
res = dict(res or {})
|
||
res.setdefault('table', GD_TABLE)
|
||
res['gd_hash'] = gd_hash
|
||
res['content_fingerprint'] = row['content_fingerprint']
|
||
res['rules_hash'] = row['rules_hash']
|
||
res['rule_count'] = row['rule_count']
|
||
res['issues'] = issues
|
||
return res
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 入口 2:只回填规则四件套(M3a 结构编译落库后的规则写入点)
|
||
# ---------------------------------------------------------------------------
|
||
|
||
async def update_gd_rule_artifacts(sor, tenant_id, blueprint_id, blueprint_version_no,
|
||
rules=None, manifest=None, gd_id=None, ctx=None):
|
||
"""把 M3b 规则产物 UPDATE 到已存在的 pbl_game_definition 行。
|
||
|
||
只写规则产物列(RULE_ARTIFACT_COLUMNS),**不重算 content_fingerprint** ——
|
||
M3a 的 29.6 结构指纹语义保持不变(重编译指纹相等断言不受影响)。
|
||
幂等:同输入 → 同 rules_json/rules_hash,重复执行结果一致。
|
||
"""
|
||
ctx = dict(ctx or {})
|
||
tid = _require(tenant_id)
|
||
rules = list(rules or [])
|
||
manifest = manifest or build_rules_manifest(rules)
|
||
rules_text = canonical_rules_json(rules)
|
||
cols = await _cols(sor)
|
||
missing = [c for c in ('rules_json', 'rules_hash', 'rule_count', 'rules_manifest')
|
||
if c not in cols]
|
||
if missing:
|
||
raise CompileError('E_GD_COLUMN_MISSING',
|
||
'pbl_game_definition 缺少 M3b 规则产物列 %s,请先执行 '
|
||
'models/pbl_game_definition.json 生成的 DDL(json2ddl)' % missing)
|
||
|
||
payload = {
|
||
'rules_json': rules_text,
|
||
'rules_hash': str(manifest.get('rules_hash') or '')[:64],
|
||
'rule_count': to_int(manifest.get('rule_count'), len(rules)),
|
||
'rules_manifest': str(manifest.get('rules_manifest') or _manifest_text(manifest))[:60000],
|
||
'compile_status': str(ctx.get('compile_status') or ctx.get('status') or 'compiled')[:32],
|
||
'quality_state': str(ctx.get('quality_state') or '')[:64],
|
||
'duration_ms': to_int(ctx.get('duration_ms'), 0),
|
||
'error_message': str(ctx.get('error_message') or '')[:60000],
|
||
}
|
||
payload = {k: v for k, v in payload.items() if k in cols}
|
||
if not payload.get('rules_hash'):
|
||
raise CompileError('E_RULES_HASH_EMPTY', 'rules_manifest 未给出 rules_hash,拒绝落库')
|
||
|
||
where = {'tenant_id': tid}
|
||
if gd_id:
|
||
where['id'] = str(gd_id)
|
||
else:
|
||
where['blueprint_id'] = str(blueprint_id or '')
|
||
where['blueprint_version_no'] = int(to_int(blueprint_version_no, 0) or 0)
|
||
try:
|
||
recs = await sor.R(GD_TABLE, dict(where, is_deleted='0'))
|
||
except Exception:
|
||
recs = await sor.R(GD_TABLE, dict(where))
|
||
rows = list(recs or [])
|
||
if not rows:
|
||
return {'written': False, 'not_found': True, 'table': GD_TABLE,
|
||
'where': where, 'rule_count': payload['rule_count'],
|
||
'rules_hash': payload['rules_hash']}
|
||
row_id = str(getattr(rows[0], 'id', '') or (rows[0].get('id') if hasattr(rows[0], 'get') else '') or '')
|
||
await sor.U(GD_TABLE, dict(payload, **{'id': row_id}))
|
||
return {'written': True, 'mode': 'update', 'table': GD_TABLE, 'gd_id': row_id,
|
||
'rule_count': payload['rule_count'], 'rules_hash': payload['rules_hash'],
|
||
'columns': sorted(payload.keys())}
|
||
|
||
|
||
def _manifest_text(manifest):
|
||
"""manifest → 规范化文本(与 gd_rules_writer 同源,避免两处格式漂移)。"""
|
||
try:
|
||
from pbl_compiler.script_mapping import canonical_dumps, strip_volatile
|
||
return canonical_dumps(strip_volatile(manifest or {}))
|
||
except Exception:
|
||
import json as _json
|
||
return _json.dumps(manifest or {}, ensure_ascii=False, sort_keys=True,
|
||
separators=(',', ':'))
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 入口 3:SELECT 读回四件套(自测 / 前端)
|
||
# ---------------------------------------------------------------------------
|
||
|
||
async def read_back_rule_artifacts(sor, tenant_id, blueprint_id, blueprint_version_no=None):
|
||
"""读回 GD 的规则产物列(QC #2 验收:编译后 SELECT 能读回四件套)。"""
|
||
tid = _require(tenant_id)
|
||
rows = await read_gd_rules(sor, tid, str(blueprint_id or ''), blueprint_version_no)
|
||
out = []
|
||
for r in rows or []:
|
||
out.append({
|
||
'id': r.get('id'),
|
||
'blueprint_id': r.get('blueprint_id'),
|
||
'blueprint_version_no': r.get('blueprint_version_no'),
|
||
'rule_count': to_int(r.get('rule_count'), 0),
|
||
'rules_hash': str(r.get('rules_hash') or ''),
|
||
'rules_len': len(str(r.get('rules_json') or '')),
|
||
'rules_manifest_len': len(str(r.get('rules_manifest') or '')),
|
||
'compile_status': r.get('compile_status'),
|
||
'quality_state': r.get('quality_state'),
|
||
'content_fingerprint': str(r.get('content_fingerprint') or ''),
|
||
})
|
||
return out
|