deliver: 交付收口(引擎代为提交)

This commit is contained in:
agent.develop 2026-09-19 10:00:33 +08:00
parent c276af5d9d
commit 2c14bf502e
8 changed files with 365 additions and 5 deletions

View File

@ -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 (
# —— M3bgame_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',
# —— M3bGD 规则写入器 + 纯映射层(非 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',
]

View File

@ -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_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

View File

@ -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 PATHSRBAC 逐条显式登记 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)

View File

@ -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']RBACscripts/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)

View File

@ -0,0 +1,14 @@
# -*- coding: utf-8 -*-
# pbl_compiler_rules_diff.dspy —— M3b 两个蓝图版本间规则集差异薄包装。
# 契约实现pbl_compiler/rules_export_api.py::rules_diff_contractdiff_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)

View File

@ -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)

View File

@ -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)

View File

@ -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)