pbl_compiler/pbl_compiler/gd_artifact_writer.py
2026-09-19 10:00:33 +08:00

213 lines
9.6 KiB
Python
Raw 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_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_messagefail-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整行 upsertexporter.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 生成的 DDLjson2ddl' % 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=(',', ':'))
# ---------------------------------------------------------------------------
# 入口 3SELECT 读回四件套(自测 / 前端)
# ---------------------------------------------------------------------------
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