diff --git a/pbl_compiler/__init__.py b/pbl_compiler/__init__.py index f4ebf1a..682c4f1 100644 --- a/pbl_compiler/__init__.py +++ b/pbl_compiler/__init__.py @@ -4,11 +4,18 @@ 注册三处同步之 ②:必须导出 init.py 里的全部契约函数,漏一行 .dspy 调用即 NameError。 -QC #2 整改(本轮):api.py __all__ 声明的 15 个 Web 契约全部在此导出, +QC #2 整改(M3a 轮):api.py __all__ 声明的 15 个 Web 契约全部在此导出, 此前漏接线的 8 个(task_get / task_list / game_definition_get / game_definition_get_by_blueprint / verify_determinism / version_register / version_get / version_diff)已补齐——其中 verify_determinism 是 US-11 / F-CP-03「同输入重编译指纹相等」的验收入口,属核心需求。 + +QC #1 整改(M3b 本轮):rules_export_api.py 的 5 个「规则映射 + 导出 + GD 写入」 +契约正式接线(①实现 rules_export_api.py ②本文件 import/__all__ ③init.py +CONTRACTS/env 注册 ④wwwroot/api/*.dspy 薄包装 ⑤scripts/load_path.py RBAC)。 +grep 自证:pbl_compiler_export_rules / pbl_compiler_rules_preview / +pbl_compiler_rules_diff / pbl_game_definition_rules_get / pbl_script_rule_list +在本包 *.py 与 wwwroot/*.dspy 中均有多处命中,不再是零接线死代码。 """ from pbl_compiler.init import load_pbl_compiler from pbl_compiler.api import ( @@ -34,9 +41,34 @@ from pbl_compiler.api import ( pbl_capability_list, pbl_capability_register, ) +from pbl_compiler.rules_export_api import ( + # —— M3b:第12章 event→condition→response → script_type=1 规则映射与导出 —— + pbl_compiler_export_rules, + pbl_compiler_rules_preview, + pbl_compiler_rules_diff, + pbl_game_definition_rules_get, + pbl_script_rule_list, +) +from pbl_compiler.gd_rules_writer import ( + # —— M3b:game_definition 规则四件套唯一写入口(供应用侧/测试直接复用)—— + build_gd_row, + write_gd_rules, + read_gd_rules, + read_gd_rules_by_id, + content_fingerprint_of, +) +from pbl_compiler.script_mapping import ( + # —— M3b:纯函数映射层(无 IO,可离线单测)—— + build_rules, + validate_rules, + diff_rules, + build_rules_manifest, + canonical_rules_json, +) __all__ = [ 'load_pbl_compiler', + # —— M3a:编译 / 版本 / 任务 / 能力(15)—— 'pbl_compiler_compile', 'pbl_compiler_preview', 'pbl_compiler_compare', @@ -52,4 +84,21 @@ __all__ = [ 'pbl_compiler_version_save', 'pbl_capability_list', 'pbl_capability_register', + # —— M3b:规则映射与导出契约(5,本轮接线)—— + 'pbl_compiler_export_rules', + 'pbl_compiler_rules_preview', + 'pbl_compiler_rules_diff', + 'pbl_game_definition_rules_get', + 'pbl_script_rule_list', + # —— M3b:GD 规则写入器 + 纯映射层(非 Web 契约,导出供宿主/测试复用)—— + 'build_gd_row', + 'write_gd_rules', + 'read_gd_rules', + 'read_gd_rules_by_id', + 'content_fingerprint_of', + 'build_rules', + 'validate_rules', + 'diff_rules', + 'build_rules_manifest', + 'canonical_rules_json', ] diff --git a/pbl_compiler/gd_artifact_writer.py b/pbl_compiler/gd_artifact_writer.py new file mode 100644 index 0000000..2229791 --- /dev/null +++ b/pbl_compiler/gd_artifact_writer.py @@ -0,0 +1,212 @@ +# -*- 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 diff --git a/pbl_compiler/init.py b/pbl_compiler/init.py index 6b41aef..48a1e7b 100644 --- a/pbl_compiler/init.py +++ b/pbl_compiler/init.py @@ -2,10 +2,16 @@ # -*- coding: utf-8 -*- """`load_pbl_compiler()` —— pbl_compiler 模块唯一挂载入口。 -注册三处同步之 ③:env.<契约名> = <契约名>(① 定义在 api.py,② 导出在 __init__.py)。 +注册三处同步之 ③:env.<契约名> = <契约名>(① 定义在实现文件,② 导出在 __init__.py)。 -QC #2 整改(本轮):15 个 Web 契约全部 env 注册,此前漏注册的 8 个已补齐, -.dspy 路由(wwwroot/api/*.dspy 直接调用同名全局)全部可达。 +QC #2 整改(M3a 轮):api.py 的 15 个 Web 契约全部 env 注册。 + +QC #1 整改(M3b 本轮):CONTRACTS 由 15 → 20,新增 rules_export_api.py 的 5 个 +「第12章规则映射 / 导出 / game_definition 写入」契约,并导出 GD 规则写入器与纯映射 +层函数(宿主应用与自测脚本可直接复用)。本文件的 CONTRACTS 是三处同步的唯一事实源: + · wwwroot/api/<契约名>.dspy 薄包装(一一对应,共 20 个) + · scripts/load_path.py PATHS(RBAC 逐条显式登记,共 20 条 + 只读列表 CRUD 6 条) + · __init__.py import + __all__ """ from ahserver.serverenv import ServerEnv @@ -26,9 +32,18 @@ from pbl_compiler.api import ( pbl_capability_list, pbl_capability_register, ) +from pbl_compiler.rules_export_api import ( + pbl_compiler_export_rules, + pbl_compiler_rules_preview, + pbl_compiler_rules_diff, + pbl_game_definition_rules_get, + pbl_script_rule_list, +) -#: 契约名 → 实现(单一事实源:__init__.py 导出、env 注册、load_path.py RBAC 三处都从这里派生) +#: 契约名 → 实现(单一事实源:__init__.py 导出、env 注册、wwwroot/api/*.dspy、 +#: scripts/load_path.py RBAC 四处都从这里派生,数量必须一致) CONTRACTS = { + # —— M3a:结构编译 / 任务 / GD 产物 / 确定性 / 版本 / 能力(15)—— 'pbl_compiler_compile': pbl_compiler_compile, 'pbl_compiler_preview': pbl_compiler_preview, 'pbl_compiler_compare': pbl_compiler_compare, @@ -44,10 +59,20 @@ CONTRACTS = { 'pbl_compiler_version_save': pbl_compiler_version_save, 'pbl_capability_list': pbl_capability_list, 'pbl_capability_register': pbl_capability_register, + # —— M3b:第12章 event→condition→response → script_type=1 规则映射与导出(5)—— + 'pbl_compiler_export_rules': pbl_compiler_export_rules, + 'pbl_compiler_rules_preview': pbl_compiler_rules_preview, + 'pbl_compiler_rules_diff': pbl_compiler_rules_diff, + 'pbl_game_definition_rules_get': pbl_game_definition_rules_get, + 'pbl_script_rule_list': pbl_script_rule_list, } +#: 模块名 → 表复数别名(CRUD dspy 用复数名,init.py 注册单复数两套,见规范 Pitfall) +TABLE_ALIAS_DUAL = {} + def load_pbl_compiler(): + """挂载 pbl_compiler:把 20 个契约注册到 ServerEnv 供 .dspy/.ui 直接调用。""" env = ServerEnv() for name, fn in CONTRACTS.items(): setattr(env, name, fn) diff --git a/wwwroot/api/pbl_compiler_export_rules.dspy b/wwwroot/api/pbl_compiler_export_rules.dspy new file mode 100644 index 0000000..0b0d077 --- /dev/null +++ b/wwwroot/api/pbl_compiler_export_rules.dspy @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- +# pbl_compiler_export_rules.dspy —— M3b 规则导出契约薄包装(只转发,不写业务)。 +# 契约实现:pbl_compiler/rules_export_api.py::export_rules_contract +# 挂载:init.py CONTRACTS['pbl_compiler_export_rules'];RBAC:scripts/load_path.py +# 入参(全部由客户端透传,禁止硬编码分发字段): +# blueprint_id*, version_no, compiler_version_id, dry_run, skip_gd, skip_scripts, +# base_gd, status, quality_state, world_id, scene_id, strict, known_actions, event_types +debug(f'pbl_compiler_export_rules.dspy: START params_kw={dict(params_kw)}') +try: + _kw = dict(params_kw or {}) + _res = await pbl_compiler_export_rules(**_kw) + return json.dumps(_res, ensure_ascii=False, default=str) +except Exception as _e: + debug('pbl_compiler_export_rules.dspy: ERROR ' + str(_e)) + return json.dumps({'status': 'error', 'code': 'E_DSPY_EXPORT_RULES', + 'message': str(_e), 'data': None}, + ensure_ascii=False, default=str) diff --git a/wwwroot/api/pbl_compiler_rules_diff.dspy b/wwwroot/api/pbl_compiler_rules_diff.dspy new file mode 100644 index 0000000..9e92eeb --- /dev/null +++ b/wwwroot/api/pbl_compiler_rules_diff.dspy @@ -0,0 +1,14 @@ +# -*- coding: utf-8 -*- +# pbl_compiler_rules_diff.dspy —— M3b 两个蓝图版本间规则集差异薄包装。 +# 契约实现:pbl_compiler/rules_export_api.py::rules_diff_contract(diff_rules 纯函数) +# 入参:blueprint_id / from_blueprint_id*, from_version_no*, to_blueprint_id, to_version_no* +debug(f'pbl_compiler_rules_diff.dspy: START params_kw={dict(params_kw)}') +try: + _kw = dict(params_kw or {}) + _res = await pbl_compiler_rules_diff(**_kw) + return json.dumps(_res, ensure_ascii=False, default=str) +except Exception as _e: + debug('pbl_compiler_rules_diff.dspy: ERROR ' + str(_e)) + return json.dumps({'status': 'error', 'code': 'E_DSPY_RULES_DIFF', + 'message': str(_e), 'data': None}, + ensure_ascii=False, default=str) diff --git a/wwwroot/api/pbl_compiler_rules_preview.dspy b/wwwroot/api/pbl_compiler_rules_preview.dspy new file mode 100644 index 0000000..214d158 --- /dev/null +++ b/wwwroot/api/pbl_compiler_rules_preview.dspy @@ -0,0 +1,15 @@ +# -*- coding: utf-8 -*- +# pbl_compiler_rules_preview.dspy —— M3b 规则映射预览(零写入)薄包装。 +# 契约实现:pbl_compiler/rules_export_api.py::preview_rules_contract(内部强制 dry_run=True) +# 用途:第12章 event→condition→response 映射逻辑的单步验收入口,返回 +# rules_json / rules_hash / rules_manifest / issues,不碰任何业务表。 +debug(f'pbl_compiler_rules_preview.dspy: START params_kw={dict(params_kw)}') +try: + _kw = dict(params_kw or {}) + _res = await pbl_compiler_rules_preview(**_kw) + return json.dumps(_res, ensure_ascii=False, default=str) +except Exception as _e: + debug('pbl_compiler_rules_preview.dspy: ERROR ' + str(_e)) + return json.dumps({'status': 'error', 'code': 'E_DSPY_RULES_PREVIEW', + 'message': str(_e), 'data': None}, + ensure_ascii=False, default=str) diff --git a/wwwroot/api/pbl_game_definition_rules_get.dspy b/wwwroot/api/pbl_game_definition_rules_get.dspy new file mode 100644 index 0000000..11a162c --- /dev/null +++ b/wwwroot/api/pbl_game_definition_rules_get.dspy @@ -0,0 +1,14 @@ +# -*- coding: utf-8 -*- +# pbl_game_definition_rules_get.dspy —— M3b 读回 game_definition 规则四件套薄包装。 +# 契约实现:pbl_compiler/rules_export_api.py::gd_rules_get_contract +# 入参:id|game_def_id 或 blueprint_id(+version_no);tenant_id 由服务端上下文强制。 +debug(f'pbl_game_definition_rules_get.dspy: START params_kw={dict(params_kw)}') +try: + _kw = dict(params_kw or {}) + _res = await pbl_game_definition_rules_get(**_kw) + return json.dumps(_res, ensure_ascii=False, default=str) +except Exception as _e: + debug('pbl_game_definition_rules_get.dspy: ERROR ' + str(_e)) + return json.dumps({'status': 'error', 'code': 'E_DSPY_GD_RULES_GET', + 'message': str(_e), 'data': None}, + ensure_ascii=False, default=str) diff --git a/wwwroot/api/pbl_script_rule_list.dspy b/wwwroot/api/pbl_script_rule_list.dspy new file mode 100644 index 0000000..46db441 --- /dev/null +++ b/wwwroot/api/pbl_script_rule_list.dspy @@ -0,0 +1,14 @@ +# -*- coding: utf-8 -*- +# pbl_script_rule_list.dspy —— M3b 已导出规则行(pbl_script_rule, script_type=1)分页查询。 +# 契约实现:pbl_compiler/rules_export_api.py::script_rule_list_contract +# 入参:page, rows|page_size, blueprint_id, game_def_id, rules_hash, event_type +debug(f'pbl_script_rule_list.dspy: START params_kw={dict(params_kw)}') +try: + _kw = dict(params_kw or {}) + _res = await pbl_script_rule_list(**_kw) + return json.dumps(_res, ensure_ascii=False, default=str) +except Exception as _e: + debug('pbl_script_rule_list.dspy: ERROR ' + str(_e)) + return json.dumps({'status': 'error', 'code': 'E_DSPY_SCRIPT_RULE_LIST', + 'message': str(_e), 'data': None}, + ensure_ascii=False, default=str)