# -*- 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