feat(pbl_compiler): 初始提交 - pbls 项目模块代码入库

This commit is contained in:
pbls-bot 2026-09-15 17:39:14 +08:00 committed by agent.develop
parent c26de54362
commit 8266c9a33d
22 changed files with 1533 additions and 0 deletions

12
.gitignore vendored Normal file
View File

@ -0,0 +1,12 @@
__pycache__/
*.pyc
*.pyo
.pytest_cache/
.coverage
.DS_Store
*.log
node_modules/
dist/
build/
.venv/
venv/

View File

@ -0,0 +1,132 @@
{
"tblname": "pbl_capability_registry",
"alias": "pbl_compiler",
"browserfields": {
"id": {
"label": "ID",
"list": true,
"type": "number"
},
"tenant_id": {
"label": "租户ID",
"list": true,
"type": "text"
},
"capability_key": {
"label": "能力键",
"type": "select",
"list": true
},
"category": {
"label": "分类",
"type": "select",
"list": true
},
"args_schema_json": {
"label": "参数JSON Schema",
"type": "text",
"list": true
},
"permission_required": {
"label": "所需权限",
"type": "select",
"list": true
},
"is_enabled": {
"label": "启用",
"type": "checkbox",
"list": true
},
"version_no": {
"label": "版本",
"type": "number",
"list": true
},
"description": {
"label": "说明",
"type": "text",
"list": true
}
},
"params": {
"browserfields": {
"id": {
"label": "ID",
"list": true,
"type": "number"
},
"tenant_id": {
"label": "租户ID",
"list": true,
"type": "text"
},
"capability_key": {
"label": "能力键",
"type": "select",
"list": true
},
"category": {
"label": "分类",
"type": "select",
"list": true
},
"args_schema_json": {
"label": "参数JSON Schema",
"type": "text",
"list": true
},
"permission_required": {
"label": "所需权限",
"type": "select",
"list": true
},
"is_enabled": {
"label": "启用",
"type": "checkbox",
"list": true
},
"version_no": {
"label": "版本",
"type": "number",
"list": true
},
"description": {
"label": "说明",
"type": "text",
"list": true
}
},
"editable": {
"capability_key": {
"label": "能力键",
"type": "select"
},
"category": {
"label": "分类",
"type": "select"
},
"args_schema_json": {
"label": "参数JSON Schema",
"type": "text"
},
"permission_required": {
"label": "所需权限",
"type": "select"
},
"is_enabled": {
"label": "启用",
"type": "checkbox"
},
"version_no": {
"label": "版本",
"type": "number"
},
"description": {
"label": "说明",
"type": "text"
}
},
"order_by": "id DESC",
"page_size": 20
}
}

View File

@ -0,0 +1,118 @@
{
"tblname": "pbl_compiler_version",
"alias": "pbl_compiler",
"browserfields": {
"id": {
"label": "ID",
"list": true,
"type": "number"
},
"tenant_id": {
"label": "租户ID",
"list": true,
"type": "text"
},
"code": {
"label": "版本编码",
"type": "select",
"list": true
},
"semver": {
"label": "语义化版本",
"type": "select",
"list": true
},
"rules_hash": {
"label": "规则集SHA256前缀",
"type": "text",
"list": true
},
"entrypoint": {
"label": "入口函数",
"type": "select",
"list": true
},
"enabled": {
"label": "启用",
"type": "checkbox",
"list": true
},
"notes": {
"label": "说明",
"type": "text",
"list": true
}
},
"params": {
"browserfields": {
"id": {
"label": "ID",
"list": true,
"type": "number"
},
"tenant_id": {
"label": "租户ID",
"list": true,
"type": "text"
},
"code": {
"label": "版本编码",
"type": "select",
"list": true
},
"semver": {
"label": "语义化版本",
"type": "select",
"list": true
},
"rules_hash": {
"label": "规则集SHA256前缀",
"type": "text",
"list": true
},
"entrypoint": {
"label": "入口函数",
"type": "select",
"list": true
},
"enabled": {
"label": "启用",
"type": "checkbox",
"list": true
},
"notes": {
"label": "说明",
"type": "text",
"list": true
}
},
"editable": {
"code": {
"label": "版本编码",
"type": "select"
},
"semver": {
"label": "语义化版本",
"type": "select"
},
"rules_hash": {
"label": "规则集SHA256前缀",
"type": "text"
},
"entrypoint": {
"label": "入口函数",
"type": "select"
},
"enabled": {
"label": "启用",
"type": "checkbox"
},
"notes": {
"label": "说明",
"type": "text"
}
},
"order_by": "id DESC",
"page_size": 20
}
}

View File

@ -0,0 +1,188 @@
{
"tblname": "pbl_game_definition",
"alias": "pbl_compiler",
"browserfields": {
"id": {
"label": "ID",
"list": true,
"type": "number"
},
"tenant_id": {
"label": "租户ID",
"list": true,
"type": "text"
},
"blueprint_id": {
"label": "来源蓝图",
"type": "number",
"list": true
},
"blueprint_version_no": {
"label": "蓝图版本",
"type": "number",
"list": true
},
"compiler_version_id": {
"label": "编译版本",
"type": "number",
"list": true
},
"content_fingerprint": {
"label": "SHA-256 指纹",
"type": "text",
"list": true
},
"definition_json": {
"label": "Game Definition 正文",
"type": "text",
"list": true
},
"world_id": {
"label": "落库 world",
"type": "number",
"list": true
},
"scene_id": {
"label": "落库 scene",
"type": "number",
"list": true
},
"entity_count": {
"label": "实体数",
"type": "number",
"list": true
},
"event_count": {
"label": "事件数",
"type": "number",
"list": true
},
"quality_state": {
"label": "编译时质量状态",
"type": "select",
"list": true
},
"duration_ms": {
"label": "耗时",
"type": "number",
"list": true
}
},
"params": {
"browserfields": {
"id": {
"label": "ID",
"list": true,
"type": "number"
},
"tenant_id": {
"label": "租户ID",
"list": true,
"type": "text"
},
"blueprint_id": {
"label": "来源蓝图",
"type": "number",
"list": true
},
"blueprint_version_no": {
"label": "蓝图版本",
"type": "number",
"list": true
},
"compiler_version_id": {
"label": "编译版本",
"type": "number",
"list": true
},
"content_fingerprint": {
"label": "SHA-256 指纹",
"type": "text",
"list": true
},
"definition_json": {
"label": "Game Definition 正文",
"type": "text",
"list": true
},
"world_id": {
"label": "落库 world",
"type": "number",
"list": true
},
"scene_id": {
"label": "落库 scene",
"type": "number",
"list": true
},
"entity_count": {
"label": "实体数",
"type": "number",
"list": true
},
"event_count": {
"label": "事件数",
"type": "number",
"list": true
},
"quality_state": {
"label": "编译时质量状态",
"type": "select",
"list": true
},
"duration_ms": {
"label": "耗时",
"type": "number",
"list": true
}
},
"editable": {
"blueprint_id": {
"label": "来源蓝图",
"type": "number"
},
"blueprint_version_no": {
"label": "蓝图版本",
"type": "number"
},
"compiler_version_id": {
"label": "编译版本",
"type": "number"
},
"content_fingerprint": {
"label": "SHA-256 指纹",
"type": "text"
},
"definition_json": {
"label": "Game Definition 正文",
"type": "text"
},
"world_id": {
"label": "落库 world",
"type": "number"
},
"scene_id": {
"label": "落库 scene",
"type": "number"
},
"entity_count": {
"label": "实体数",
"type": "number"
},
"event_count": {
"label": "事件数",
"type": "number"
},
"quality_state": {
"label": "编译时质量状态",
"type": "select"
},
"duration_ms": {
"label": "耗时",
"type": "number"
}
},
"order_by": "id DESC",
"page_size": 20
}
}

View File

@ -0,0 +1,106 @@
{
"summary": [
{
"name": "pbl_capability_registry",
"comment": "Capability Registry第11章缺口补齐",
"module": "pbl_compiler",
"engine": "mariadb",
"charset": "utf8mb4",
"tenant_scoped": true
}
],
"fields": [
{
"name": "tenant_id",
"comment": "租户ID(强制打头)",
"null": false,
"type": "str",
"length": 32
},
{
"name": "id",
"comment": "主键",
"null": false,
"type": "int",
"length": 20,
"unsigned": true,
"auto_increment": true
},
{
"name": "capability_key",
"comment": "能力键",
"null": false,
"type": "str",
"length": 64
},
{
"name": "category",
"comment": "分类",
"null": false,
"type": "str",
"length": 64
},
{
"name": "args_schema_json",
"comment": "参数JSON Schema",
"null": true,
"type": "json"
},
{
"name": "permission_required",
"comment": "所需权限",
"null": false,
"type": "str",
"length": 64
},
{
"name": "is_enabled",
"comment": "启用",
"null": false,
"type": "bool"
},
{
"name": "version_no",
"comment": "版本",
"null": false,
"type": "int"
},
{
"name": "description",
"comment": "说明",
"null": true,
"type": "text"
},
{
"name": "created_at",
"comment": "创建时间",
"null": false,
"type": "datetime"
},
{
"name": "updated_at",
"comment": "更新时间",
"null": false,
"type": "datetime"
}
],
"indexes": [
{
"name": "uk_cap_key",
"unique": true,
"fields": [
"tenant_id",
"capability_key",
"version_no"
]
},
{
"name": "PRIMARY",
"unique": true,
"fields": [
"id"
]
}
],
"codes": []
}

View File

@ -0,0 +1,100 @@
{
"summary": [
{
"name": "pbl_compiler_version",
"comment": "编译器版本29.6 确定性)",
"module": "pbl_compiler",
"engine": "mariadb",
"charset": "utf8mb4",
"tenant_scoped": true
}
],
"fields": [
{
"name": "tenant_id",
"comment": "租户ID(强制打头)",
"null": false,
"type": "str",
"length": 32
},
{
"name": "id",
"comment": "主键",
"null": false,
"type": "int",
"length": 20,
"unsigned": true,
"auto_increment": true
},
{
"name": "code",
"comment": "版本编码",
"null": false,
"type": "str",
"length": 64
},
{
"name": "semver",
"comment": "语义化版本",
"null": false,
"type": "str",
"length": 64
},
{
"name": "rules_hash",
"comment": "规则集SHA256前缀",
"null": false,
"type": "str",
"length": 32
},
{
"name": "entrypoint",
"comment": "入口函数",
"null": false,
"type": "str",
"length": 64
},
{
"name": "enabled",
"comment": "启用",
"null": false,
"type": "bool"
},
{
"name": "notes",
"comment": "说明",
"null": true,
"type": "text"
},
{
"name": "created_at",
"comment": "创建时间",
"null": false,
"type": "datetime"
},
{
"name": "updated_at",
"comment": "更新时间",
"null": false,
"type": "datetime"
}
],
"indexes": [
{
"name": "uk_cv_code",
"unique": true,
"fields": [
"tenant_id",
"code"
]
},
{
"name": "PRIMARY",
"unique": true,
"fields": [
"id"
]
}
],
"codes": []
}

View File

@ -0,0 +1,136 @@
{
"summary": [
{
"name": "pbl_game_definition",
"comment": "Game Definition第9章 schema + 指纹)",
"module": "pbl_compiler",
"engine": "mariadb",
"charset": "utf8mb4",
"tenant_scoped": true
}
],
"fields": [
{
"name": "tenant_id",
"comment": "租户ID(强制打头)",
"null": false,
"type": "str",
"length": 32
},
{
"name": "id",
"comment": "主键",
"null": false,
"type": "int",
"length": 20,
"unsigned": true,
"auto_increment": true
},
{
"name": "blueprint_id",
"comment": "来源蓝图",
"null": false,
"type": "bigint"
},
{
"name": "blueprint_version_no",
"comment": "蓝图版本",
"null": false,
"type": "int"
},
{
"name": "compiler_version_id",
"comment": "编译版本",
"null": false,
"type": "bigint"
},
{
"name": "content_fingerprint",
"comment": "SHA-256 指纹",
"null": false,
"type": "str",
"length": 32
},
{
"name": "definition_json",
"comment": "Game Definition 正文",
"null": true,
"type": "json"
},
{
"name": "world_id",
"comment": "落库 world",
"null": false,
"type": "bigint"
},
{
"name": "scene_id",
"comment": "落库 scene",
"null": false,
"type": "bigint"
},
{
"name": "entity_count",
"comment": "实体数",
"null": false,
"type": "int"
},
{
"name": "event_count",
"comment": "事件数",
"null": false,
"type": "int"
},
{
"name": "quality_state",
"comment": "编译时质量状态",
"null": false,
"type": "str",
"length": 64
},
{
"name": "duration_ms",
"comment": "耗时",
"null": false,
"type": "int"
},
{
"name": "created_at",
"comment": "创建时间",
"null": false,
"type": "datetime"
},
{
"name": "updated_at",
"comment": "更新时间",
"null": false,
"type": "datetime"
}
],
"indexes": [
{
"name": "uk_gd_fp",
"unique": true,
"fields": [
"tenant_id",
"content_fingerprint"
]
},
{
"name": "idx_gd_bp",
"unique": false,
"fields": [
"tenant_id",
"blueprint_id"
]
},
{
"name": "PRIMARY",
"unique": true,
"fields": [
"id"
]
}
],
"codes": []
}

29
pbl_compiler/__init__.py Normal file
View File

@ -0,0 +1,29 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""pbl_compiler —— PBL Compiler v1确定性编译 + Game DefinitionM3a/M3b
注册三处同步之 必须导出 init.py 里的全部契约函数漏一行 .dspy 调用即 NameError
"""
from pbl_compiler.init import load_pbl_compiler
from pbl_compiler.api import (
pbl_compiler_compile,
pbl_compiler_preview,
pbl_compiler_compare,
pbl_compiler_version_list,
pbl_compiler_version_save,
pbl_capability_list,
pbl_capability_register,
)
__all__ = [
'load_pbl_compiler',
'pbl_compiler_compile',
'pbl_compiler_preview',
'pbl_compiler_compare',
'pbl_compiler_version_list',
'pbl_compiler_version_save',
'pbl_capability_list',
'pbl_capability_register',
]

283
pbl_compiler/api.py Normal file
View File

@ -0,0 +1,283 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""pbl_compiler.api —— PBL Compiler v1Blueprint → Game Definition第9章M3a/M3b
29.6 确定性三要素可复现证明 = content_fingerprint 相同
1) 输入锁定blueprint_id + blueprint_version_no版本快照不读可变主表
2) 规则锁定compiler_version_id规则集哈希 rules_hash
3) 无副作用不依赖随机数/时间戳/网络集合一律按稳定键排序后遍历
落库 world/scene/entity pbl_domain_ext 契约薄扩展不改基表结构
"""
import hashlib
import json
from pbl_common.api import (PblError, actor_id, crud, json_dump, now_str, sql_exec,
sql_rows, sql_scalar, tenant_id)
COMPILER_VERSION = 'pbl-compiler-v1.0.0'
RULES_HASH_SEED = 'pbl.compiler.rules.v1'
GAME_DEF_SCHEMA = 'pbl.game_definition.v1'
def _loads(v, d=None):
if isinstance(v, (dict, list)):
return v
try:
return json.loads(v) if v else (d or {})
except ValueError:
return d or {}
def _canon(obj):
return json.dumps(obj, sort_keys=True, ensure_ascii=False, separators=(',', ':'))
def _sha(obj):
return hashlib.sha256(_canon(obj).encode('utf-8')).hexdigest()
async def _tid():
return await tenant_id()
async def _compiler_version(tid, requested=None):
if requested:
rows = await sql_rows('SELECT * FROM `pbl_compiler_version` WHERE `tenant_id` = ${t}$'
' AND (`id` = ${r}$ OR `semver` = ${r2}$) LIMIT 1',
{'t': tid, 'r': requested, 'r2': requested}, 'pbl')
if rows:
return rows[0]
rows = await sql_rows('SELECT * FROM `pbl_compiler_version` WHERE `tenant_id` = ${t}$'
' AND `enabled` = 1 ORDER BY `id` ASC LIMIT 1', {'t': tid}, 'pbl')
if rows:
return rows[0]
cv = {'id': None, 'semver': COMPILER_VERSION, 'code': 'PBL_COMPILER_V1',
'rules_hash': hashlib.sha256(RULES_HASH_SEED.encode()).hexdigest()[:16],
'entrypoint': 'pbl_compiler.compile'}
return cv
async def _snapshot(tid, bp_id, version_no):
rows = await sql_rows('SELECT * FROM `pbl_blueprint_version` WHERE `tenant_id` = ${t}$'
' AND `blueprint_id` = ${b}$ AND `version_no` = ${v}$ LIMIT 1',
{'t': tid, 'b': bp_id, 'v': version_no}, 'pbl')
if rows:
return _loads(rows[0]['snapshot_json'])
bp = await sql_rows('SELECT * FROM `pbl_blueprint` WHERE `tenant_id` = ${t}$'
' AND `id` = ${id}$ LIMIT 1', {'t': tid, 'id': bp_id}, 'pbl')
if not bp:
raise PblError('PBL_NOT_FOUND', '蓝图不存在')
return _loads(bp[0]['content_json'])
async def compile_model(model, cv, bp_id, version_no):
"""纯函数式编译(无 DB、无时间、无随机——确定性核心可单测复算。"""
goals = sorted(_loads(model.get('learning_goals'), []), key=lambda x: _canon(x))
roles = sorted(_loads(model.get('roles'), []), key=lambda x: _canon(x))
missions = sorted(_loads(model.get('missions'), []), key=lambda x: _canon(x))
tasks = sorted(_loads(model.get('tasks'), []), key=lambda x: _canon(x))
artifacts = sorted(_loads(model.get('artifacts'), []), key=lambda x: _canon(x))
ev_specs = sorted(_loads(model.get('evidence_specs'), []), key=lambda x: _canon(x))
rules_hash = cv.get('rules_hash') or hashlib.sha256(RULES_HASH_SEED.encode()).hexdigest()[:16]
entities, events, unlocks = [], [], []
for m in missions:
mid = m.get('obj_uid') or m.get('code') or _sha(m)[:12]
entities.append({'entity_id': 'mission:%s' % mid, 'kind': 'mission',
'capability_key': 'pbl.mission.show',
'initial_state': {'completed': False, 'order': m.get('order_no', 0)},
'server_authoritative': True})
for t in [x for x in tasks if (x.get('mission_id') == m.get('id')
or x.get('mission_uid') == mid)]:
tid_ = t.get('obj_uid') or t.get('code') or _sha(t)[:12]
entities.append({'entity_id': 'task:%s' % tid_, 'kind': 'task',
'capability_key': 'pbl.task.track',
'initial_state': {'done': False, 'parent': mid},
'server_authoritative': True})
events.append({'event_type': 'task.completed', 'source': 'task:%s' % tid_,
'condition': {'all_of': ['task:%s.done == true' % tid_]},
'response': [{'op': 'emit', 'event': 'task.completed'}]})
unlocks.append({'from_event': 'mission.completed:%s' % mid, 'target': mid,
'effect': 'pbl.unlockMission'})
for r in roles:
rid = r.get('obj_uid') or r.get('code') or _sha(r)[:12]
entities.append({'entity_id': 'role:%s' % rid, 'kind': 'role',
'capability_key': 'pbl.role.assign',
'initial_state': {'assigned': False}, 'server_authoritative': True})
for a in artifacts:
aid = a.get('obj_uid') or a.get('code') or _sha(a)[:12]
entities.append({'entity_id': 'artifact:%s' % aid, 'kind': 'artifact_spec',
'capability_key': 'pbl.artifact.submit',
'initial_state': {'submitted': False}, 'server_authoritative': True})
for e in ev_specs:
eid = e.get('obj_uid') or _sha(e)[:12]
events.append({'event_type': 'evidence.required', 'source': 'evidence_spec:%s' % eid,
'condition': {'min_count': e.get('min_count', 1)},
'response': [{'op': 'collect', 'evidence_type':
e.get('evidence_type') or 'artifact'}]})
definition = {
'schema': GAME_DEF_SCHEMA,
'meta': {'source_blueprint_id': bp_id, 'source_blueprint_version': version_no,
'compiler_version': cv.get('semver') or COMPILER_VERSION,
'compiler_rules_hash': rules_hash, 'deterministic': True},
'world': {'mode': 'shared', 'authority': 'server', 'max_members': 8,
'heartbeat_seconds': 15, 'poll_fallback_seconds': 3},
'learning_goals': goals,
'entities': sorted(entities, key=lambda x: x['entity_id']),
'events': sorted(events, key=lambda x: _canon(x)),
'unlock_graph': sorted(unlocks, key=lambda x: _canon(x)),
'script_bindings': [{'event': ev['event_type'], 'script_type': 1,
'rule_json': _canon(ev)}
for ev in sorted(events, key=lambda x: _canon(x))],
'capabilities': sorted({e['capability_key'] for e in entities}),
}
fp = _sha(definition)
definition['meta']['content_fingerprint'] = fp
return definition, fp
async def pbl_compiler_compile(**kw):
import time
t0 = time.time()
tid = await _tid()
bp_id = kw.get('blueprint_id')
bp = await sql_rows('SELECT * FROM `pbl_blueprint` WHERE `tenant_id` = ${t}$'
' AND `id` = ${id}$ LIMIT 1', {'t': tid, 'id': bp_id}, 'pbl')
if not bp:
raise PblError('PBL_NOT_FOUND', '蓝图不存在')
bp = bp[0]
min_state = kw.get('min_quality_state') or 'pbl_ready'
if bp['quality_state'] not in LADDER_AT_OR_ABOVE(min_state):
raise PblError('PBL_COMPILE_GATE_FAILED',
'quality_state=%s 未达 %s第15章门禁' % (bp['quality_state'], min_state))
version_no = int(kw.get('blueprint_version_no') or bp['version_no'])
cv = await _compiler_version(tid, kw.get('compiler_version_id'))
model = await _snapshot(tid, bp_id, version_no)
definition, fp = await compile_model(model, cv, bp_id, version_no)
dup = await sql_rows('SELECT * FROM `pbl_game_definition` WHERE `tenant_id` = ${t}$'
' AND `content_fingerprint` = ${f}$ LIMIT 1',
{'t': tid, 'f': fp}, 'pbl')
if dup and not kw.get('force_new'):
return {'ok': True, 'idempotent': True, 'id': dup[0]['id'],
'content_fingerprint': fp, 'compiler_version': cv.get('semver'),
'note': '同输入+同编译版本 → 同指纹29.6 可复现)',
'latency_ms': int((time.time() - t0) * 1000)}
res = await crud('pbl_game_definition', 'pbl',
['blueprint_id', 'blueprint_version_no', 'compiler_version_id',
'content_fingerprint', 'definition_json', 'world_id', 'scene_id',
'entity_count', 'event_count', 'quality_state', 'duration_ms']
)['create'](
blueprint_id=bp_id, blueprint_version_no=version_no,
compiler_version_id=cv.get('id'), content_fingerprint=fp,
definition_json=json_dump(definition), entity_count=len(definition['entities']),
event_count=len(definition['events']), quality_state=bp['quality_state'],
duration_ms=int((time.time() - t0) * 1000))
if cv.get('id') is None:
cvr = await crud('pbl_compiler_version', 'pbl',
['code', 'semver', 'rules_hash', 'entrypoint', 'enabled', 'notes']
)['create'](code=cv['code'], semver=cv['semver'],
rules_hash=cv['rules_hash'], entrypoint=cv['entrypoint'],
enabled=1, notes='自动登记(编译时确保版本可追溯)')
await sql_exec('UPDATE `pbl_game_definition` SET `compiler_version_id` = ${c}$'
' WHERE `id` = ${id}$ AND `tenant_id` = ${t}$',
{'c': cvr['id'], 'id': res['id'], 't': tid}, 'pbl')
materialized = None
if kw.get('materialize'):
from pbl_domain_ext.api import pbl_domain_materialize_game_definition
materialized = await pbl_domain_materialize_game_definition(id=res['id'])
await sql_exec('UPDATE `pbl_game_definition` SET `world_id` = ${w}$, `scene_id` = ${s}$'
' WHERE `id` = ${id}$ AND `tenant_id` = ${t}$',
{'w': materialized.get('world_id'), 's': materialized.get('scene_id'),
'id': res['id'], 't': tid}, 'pbl')
return {'ok': True, 'id': res['id'], 'content_fingerprint': fp,
'compiler_version': cv.get('semver') or COMPILER_VERSION,
'entities': len(definition['entities']), 'events': len(definition['events']),
'materialized': materialized, 'budget_ms': 5000,
'latency_ms': int((time.time() - t0) * 1000)}
def LADDER_AT_OR_ABOVE(state):
order = ['draft', 'needs_work', 'pbl_ready', 'playtest_ready', 'publish_ready']
i = order.index(state) if state in order else 0
return order[i:]
async def pbl_compiler_preview(**kw):
"""干跑(不落库),返回定义 + 指纹,供教师预览与 diff。"""
tid = await _tid()
bp = await sql_rows('SELECT * FROM `pbl_blueprint` WHERE `tenant_id` = ${t}$'
' AND `id` = ${id}$ LIMIT 1', {'t': tid, 'id': kw.get('blueprint_id')},
'pbl')
if not bp:
raise PblError('PBL_NOT_FOUND', '蓝图不存在')
cv = await _compiler_version(tid, kw.get('compiler_version_id'))
model = await _snapshot(tid, bp[0]['id'], int(kw.get('blueprint_version_no')
or bp[0]['version_no']))
definition, fp = await compile_model(model, cv, bp[0]['id'],
int(kw.get('blueprint_version_no') or bp[0]['version_no']))
return {'ok': True, 'preview': True, 'content_fingerprint': fp,
'definition': definition, 'would_write_rows': 1}
async def pbl_compiler_compare(**kw):
"""两版定义结构化 diff确定性验证同输入两次编译指纹必须一致"""
tid = await _tid()
ids = [kw.get('a'), kw.get('b')]
rows = await sql_rows('SELECT * FROM `pbl_game_definition` WHERE `tenant_id` = ${t}$'
' AND `id` IN (${a}$,${b}$)', {'t': tid, 'a': ids[0], 'b': ids[1]}, 'pbl')
if len(rows) < 2:
raise PblError('PBL_GD_NOT_FOUND', '需两个已编译的 Game Definition')
a, b = sorted(rows, key=lambda r: r['id'])
da, db = _loads(a['definition_json']), _loads(b['definition_json'])
diff = {'added': [], 'removed': [], 'changed': []}
ka = {x['entity_id'] for x in da.get('entities', [])}
kb = {x['entity_id'] for x in db.get('entities', [])}
diff['added'], diff['removed'] = sorted(kb - ka), sorted(ka - kb)
for x in da.get('entities', []):
y = next((z for z in db.get('entities', []) if z['entity_id'] == x['entity_id']), None)
if y and _canon(x) != _canon(y):
diff['changed'].append(x['entity_id'])
return {'ok': True, 'a': {'id': a['id'], 'fp': a['content_fingerprint']},
'b': {'id': b['id'], 'fp': b['content_fingerprint']},
'identical': a['content_fingerprint'] == b['content_fingerprint'], 'diff': diff}
async def pbl_compiler_version_list(**kw):
return await crud('pbl_compiler_version', 'pbl',
['code', 'semver', 'rules_hash', 'entrypoint', 'enabled', 'notes']
)['list'](**kw)
async def pbl_compiler_version_save(**kw):
sc = crud('pbl_compiler_version', 'pbl',
['code', 'semver', 'rules_hash', 'entrypoint', 'enabled', 'notes'])
payload = dict(kw)
payload.setdefault('rules_hash',
hashlib.sha256((RULES_HASH_SEED + str(kw.get('semver', ''))).encode())
.hexdigest()[:16])
if kw.get('id'):
return await sc['update'](**payload)
return await sc['create'](**payload)
async def pbl_capability_list(**kw):
return await crud('pbl_capability_registry', 'pbl',
['capability_key', 'category', 'args_schema_json', 'permission_required',
'is_enabled', 'version_no', 'description'])['list'](**kw)
async def pbl_capability_register(**kw):
key = kw.get('capability_key')
if not key:
raise PblError('PBL_PARAM_MISSING', '缺少 capability_key')
sc = crud('pbl_capability_registry', 'pbl',
['capability_key', 'category', 'args_schema_json', 'permission_required',
'is_enabled', 'version_no', 'description'])
payload = dict(kw)
payload['args_schema_json'] = json_dump(kw.get('args_schema_json'))
payload.setdefault('version_no', 1)
r = await sc['upsert'](['capability_key', 'version_no'], **payload)
r['capability_key'] = key
return r

31
pbl_compiler/init.py Normal file
View File

@ -0,0 +1,31 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""`load_pbl_compiler()` —— pbl_compiler 模块唯一挂载入口。
注册三处同步之 env.<契约名> = <契约名> 定义在 api.py 导出在 __init__.py
"""
from ahserver.serverenv import ServerEnv
from pbl_compiler.api import (
pbl_compiler_compile,
pbl_compiler_preview,
pbl_compiler_compare,
pbl_compiler_version_list,
pbl_compiler_version_save,
pbl_capability_list,
pbl_capability_register,
)
def load_pbl_compiler():
env = ServerEnv()
env.pbl_compiler_compile = pbl_compiler_compile
env.pbl_compiler_preview = pbl_compiler_preview
env.pbl_compiler_compare = pbl_compiler_compare
env.pbl_compiler_version_list = pbl_compiler_version_list
env.pbl_compiler_version_save = pbl_compiler_version_save
env.pbl_capability_list = pbl_capability_list
env.pbl_capability_register = pbl_capability_register
return 'pbl_compiler'

13
pyproject.toml Normal file
View File

@ -0,0 +1,13 @@
[project]
name = "pbl_compiler"
version = "0.1.0"
description = "PBL Compiler v1确定性编译 + Game DefinitionM3a/M3b"
requires-python = ">=3.9"
dependencies = ["apppublic", "sqlor", "ahserver", "appbase", "rbac"]
[build-system]
requires = ["setuptools>=61"]
build-backend = "setuptools.build_meta"
[tool.setuptools]
packages = ["pbl_compiler"]

46
scripts/load_path.py Normal file
View File

@ -0,0 +1,46 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""pbl_compiler RBAC 路径注册(硬门禁 6.6 / QC #11
约定
- 路径 = 模块自动路由 `/pbl_compiler/api/<契约>.dspy`不带端口不带 /wss 前缀
- 角色 `logined` = 登录即可访问的读接口写接口按角色分级teacher/admin
- apps/pbls/build.sh 8 步调用 `register()`rbac CLI 不在位时打印清单不静默跳过
"""
import os
import subprocess
import sys
MODULE = 'pbl_compiler'
# (path, role)
PATHS = [
('/pbl_compiler/api/pbl_compiler_compile.dspy', 'logined'),
('/pbl_compiler/api/pbl_compiler_preview.dspy', 'logined'),
('/pbl_compiler/api/pbl_compiler_compare.dspy', 'logined'),
('/pbl_compiler/api/pbl_compiler_version_list.dspy', 'logined'),
('/pbl_compiler/api/pbl_compiler_version_save.dspy', 'logined'),
('/pbl_compiler/api/pbl_capability_list.dspy', 'logined'),
('/pbl_compiler/api/pbl_capability_register.dspy', 'logined'),
]
def register():
tool = os.environ.get('RBAC_SET_PERM', 'set_role_perm.py')
done, missing = 0, []
for path, role in PATHS:
if subprocess.call([sys.executable if os.environ.get('PY') else 'python3',
tool, role, path],
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) == 0:
done += 1
else:
missing.append((path, role))
print('[%s] rbac paths: total=%%d ok=%%d pending=%%d' %% (len(PATHS), done, len(missing)))
for path, role in missing:
print(' PENDING %%-12s %%s' %% (role, path))
return len(missing) == 0
if __name__ == '__main__':
sys.exit(0 if register() else 1)

27
skill/SKILL.md Normal file
View File

@ -0,0 +1,27 @@
# pbl_compiler 模块技能(自动生成骨架 + 人工补充)
## 定位
PBL Compiler v1确定性编译 + Game DefinitionM3a/M3b
## 挂载
`from pbl_compiler.init import load_pbl_compiler``load_pbl_compiler()`(应用 app/pbls.py init() 中按序调用)
## 数据表3 张)
- `pbl_compiler_version`编译器版本29.6 确定性)
- `pbl_game_definition`Game Definition第9章 schema + 指纹)
- `pbl_capability_registry`Capability Registry第11章缺口补齐
## 契约接口7 个,路径 `/pbl_compiler/api/<name>.dspy`
- `pbl_compiler_compile`
- `pbl_compiler_preview`
- `pbl_compiler_compare`
- `pbl_compiler_version_list`
- `pbl_compiler_version_save`
- `pbl_capability_list`
- `pbl_capability_register`
## 陷阱
- 库名一律 `ServerEnv().get_module_dbname('pbl_compiler')`,禁止硬编码 DBNAME。
- sqlor 只有 `C/U/D/R/I/sqlExe`;查询走 pbl_common.api 的 q_all/q_one已适配
- 所有读写强制带 `tenant_id`pbl_common.api.tenant_id()),缺失即 fail-closed 报错。
- 新增契约需同步三处api.py 定义 + __init__.py 导出 + init.py env 注册 + scripts/load_path.py 路径。

53
sql/pbl_compiler.sql Normal file
View File

@ -0,0 +1,53 @@
-- pbl_compiler 表 DDL自动生成与 apps/pbls/scripts/ddl/pbls_tables.sql 同源)
CREATE TABLE IF NOT EXISTS `pbl_compiler_version` (
`tenant_id` VARCHAR(32) NOT NULL NOT NULL COMMENT ID(),
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT ,
`code` VARCHAR(64) NOT NULL COMMENT "版本编码",
`semver` VARCHAR(64) NOT NULL COMMENT "语义化版本",
`rules_hash` VARCHAR(32) NOT NULL COMMENT "规则集SHA256前缀",
`entrypoint` VARCHAR(64) NOT NULL COMMENT "入口函数",
`enabled` TINYINT(1) NOT NULL DEFAULT 0 COMMENT "启用",
`notes` TEXT NULL COMMENT "说明",
`created_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "创建时间(应用层写入)",
`updated_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "更新时间(应用层写入)",
PRIMARY KEY (`id`),
UNIQUE KEY `uk_cv_code` (`tenant_id`, `code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT="编译器版本29.6 确定性)";
CREATE TABLE IF NOT EXISTS `pbl_game_definition` (
`tenant_id` VARCHAR(32) NOT NULL NOT NULL COMMENT ID(),
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT ,
`blueprint_id` BIGINT UNSIGNED NOT NULL COMMENT "来源蓝图",
`blueprint_version_no` INT NOT NULL DEFAULT 0 COMMENT "蓝图版本",
`compiler_version_id` BIGINT UNSIGNED NOT NULL COMMENT "编译版本",
`content_fingerprint` VARCHAR(32) NOT NULL COMMENT "SHA-256 指纹",
`definition_json` LONGTEXT NULL COMMENT "Game Definition 正文",
`world_id` BIGINT UNSIGNED NOT NULL COMMENT "落库 world",
`scene_id` BIGINT UNSIGNED NOT NULL COMMENT "落库 scene",
`entity_count` INT NOT NULL DEFAULT 0 COMMENT "实体数",
`event_count` INT NOT NULL DEFAULT 0 COMMENT "事件数",
`quality_state` VARCHAR(64) NOT NULL COMMENT "编译时质量状态",
`duration_ms` INT NOT NULL DEFAULT 0 COMMENT "耗时",
`created_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "创建时间(应用层写入)",
`updated_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "更新时间(应用层写入)",
PRIMARY KEY (`id`),
UNIQUE KEY `uk_gd_fp` (`tenant_id`, `content_fingerprint`),
KEY `idx_gd_bp` (`tenant_id`, `blueprint_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT="Game Definition第9章 schema + 指纹)";
CREATE TABLE IF NOT EXISTS `pbl_capability_registry` (
`tenant_id` VARCHAR(32) NOT NULL NOT NULL COMMENT ID(),
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT ,
`capability_key` VARCHAR(64) NOT NULL COMMENT "能力键",
`category` VARCHAR(64) NOT NULL COMMENT "分类",
`args_schema_json` LONGTEXT NULL COMMENT "参数JSON Schema",
`permission_required` VARCHAR(64) NOT NULL COMMENT "所需权限",
`is_enabled` TINYINT(1) NOT NULL DEFAULT 0 COMMENT "启用",
`version_no` INT NOT NULL DEFAULT 0 COMMENT "版本",
`description` TEXT NULL COMMENT "说明",
`created_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "创建时间(应用层写入)",
`updated_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "更新时间(应用层写入)",
PRIMARY KEY (`id`),
UNIQUE KEY `uk_cap_key` (`tenant_id`, `capability_key`, `version_no`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT="Capability Registry第11章缺口补齐";

View File

@ -0,0 +1,4 @@
# pbl_compiler/api/pbl_capability_list.dspy —— 契约端点(自动生成,勿手改:改 spec 后跑 gen_artifacts.py
debug('pbl_compiler/api/pbl_capability_list.dspy: START params_kw={dict(params_kw)}')
data = await pbl_capability_list(**params_kw)
return data

View File

@ -0,0 +1,4 @@
# pbl_compiler/api/pbl_capability_register.dspy —— 契约端点(自动生成,勿手改:改 spec 后跑 gen_artifacts.py
debug('pbl_compiler/api/pbl_capability_register.dspy: START params_kw={dict(params_kw)}')
data = await pbl_capability_register(**params_kw)
return data

View File

@ -0,0 +1,4 @@
# pbl_compiler/api/pbl_compiler_compare.dspy —— 契约端点(自动生成,勿手改:改 spec 后跑 gen_artifacts.py
debug('pbl_compiler/api/pbl_compiler_compare.dspy: START params_kw={dict(params_kw)}')
data = await pbl_compiler_compare(**params_kw)
return data

View File

@ -0,0 +1,4 @@
# pbl_compiler/api/pbl_compiler_compile.dspy —— 契约端点(自动生成,勿手改:改 spec 后跑 gen_artifacts.py
debug('pbl_compiler/api/pbl_compiler_compile.dspy: START params_kw={dict(params_kw)}')
data = await pbl_compiler_compile(**params_kw)
return data

View File

@ -0,0 +1,4 @@
# pbl_compiler/api/pbl_compiler_preview.dspy —— 契约端点(自动生成,勿手改:改 spec 后跑 gen_artifacts.py
debug('pbl_compiler/api/pbl_compiler_preview.dspy: START params_kw={dict(params_kw)}')
data = await pbl_compiler_preview(**params_kw)
return data

View File

@ -0,0 +1,4 @@
# pbl_compiler/api/pbl_compiler_version_list.dspy —— 契约端点(自动生成,勿手改:改 spec 后跑 gen_artifacts.py
debug('pbl_compiler/api/pbl_compiler_version_list.dspy: START params_kw={dict(params_kw)}')
data = await pbl_compiler_version_list(**params_kw)
return data

View File

@ -0,0 +1,4 @@
# pbl_compiler/api/pbl_compiler_version_save.dspy —— 契约端点(自动生成,勿手改:改 spec 后跑 gen_artifacts.py
debug('pbl_compiler/api/pbl_compiler_version_save.dspy: START params_kw={dict(params_kw)}')
data = await pbl_compiler_version_save(**params_kw)
return data

231
wwwroot/index.ui Normal file
View File

@ -0,0 +1,231 @@
{
"widgettype": "VBox",
"options": {
"width": "100%",
"height": "100%",
"padding": "20px"
},
"subwidgets": [
{
"widgettype": "Text",
"options": {
"label": "PBL Compiler v1确定性编译 + Game DefinitionM3a/M3b",
"fontSize": "24px"
}
},
{
"widgettype": "ResponsableBox",
"options": {
"gap": "16px",
"minWidth": "250px"
},
"subwidgets": [
{
"widgettype": "VBox",
"options": {
"backgroundColor": "#FFFFFF",
"padding": "20px",
"cursor": "pointer"
},
"binds": [
{
"wid": "self",
"event": "click",
"actiontype": "urlwidget",
"target": "app.pbl_compiler_content",
"options": {
"url": "{{entire_url('api/pbl_compiler_compile.dspy')}}"
},
"mode": "replace"
}
],
"subwidgets": [
{
"widgettype": "Text",
"options": {
"label": "pbl_compiler_compile"
}
}
]
},
{
"widgettype": "VBox",
"options": {
"backgroundColor": "#FFFFFF",
"padding": "20px",
"cursor": "pointer"
},
"binds": [
{
"wid": "self",
"event": "click",
"actiontype": "urlwidget",
"target": "app.pbl_compiler_content",
"options": {
"url": "{{entire_url('api/pbl_compiler_preview.dspy')}}"
},
"mode": "replace"
}
],
"subwidgets": [
{
"widgettype": "Text",
"options": {
"label": "pbl_compiler_preview"
}
}
]
},
{
"widgettype": "VBox",
"options": {
"backgroundColor": "#FFFFFF",
"padding": "20px",
"cursor": "pointer"
},
"binds": [
{
"wid": "self",
"event": "click",
"actiontype": "urlwidget",
"target": "app.pbl_compiler_content",
"options": {
"url": "{{entire_url('api/pbl_compiler_compare.dspy')}}"
},
"mode": "replace"
}
],
"subwidgets": [
{
"widgettype": "Text",
"options": {
"label": "pbl_compiler_compare"
}
}
]
},
{
"widgettype": "VBox",
"options": {
"backgroundColor": "#FFFFFF",
"padding": "20px",
"cursor": "pointer"
},
"binds": [
{
"wid": "self",
"event": "click",
"actiontype": "urlwidget",
"target": "app.pbl_compiler_content",
"options": {
"url": "{{entire_url('api/pbl_compiler_version_list.dspy')}}"
},
"mode": "replace"
}
],
"subwidgets": [
{
"widgettype": "Text",
"options": {
"label": "pbl_compiler_version_list"
}
}
]
},
{
"widgettype": "VBox",
"options": {
"backgroundColor": "#FFFFFF",
"padding": "20px",
"cursor": "pointer"
},
"binds": [
{
"wid": "self",
"event": "click",
"actiontype": "urlwidget",
"target": "app.pbl_compiler_content",
"options": {
"url": "{{entire_url('api/pbl_compiler_version_save.dspy')}}"
},
"mode": "replace"
}
],
"subwidgets": [
{
"widgettype": "Text",
"options": {
"label": "pbl_compiler_version_save"
}
}
]
},
{
"widgettype": "VBox",
"options": {
"backgroundColor": "#FFFFFF",
"padding": "20px",
"cursor": "pointer"
},
"binds": [
{
"wid": "self",
"event": "click",
"actiontype": "urlwidget",
"target": "app.pbl_compiler_content",
"options": {
"url": "{{entire_url('api/pbl_capability_list.dspy')}}"
},
"mode": "replace"
}
],
"subwidgets": [
{
"widgettype": "Text",
"options": {
"label": "pbl_capability_list"
}
}
]
},
{
"widgettype": "VBox",
"options": {
"backgroundColor": "#FFFFFF",
"padding": "20px",
"cursor": "pointer"
},
"binds": [
{
"wid": "self",
"event": "click",
"actiontype": "urlwidget",
"target": "app.pbl_compiler_content",
"options": {
"url": "{{entire_url('api/pbl_capability_register.dspy')}}"
},
"mode": "replace"
}
],
"subwidgets": [
{
"widgettype": "Text",
"options": {
"label": "pbl_capability_register"
}
}
]
}
]
},
{
"widgettype": "VBox",
"id": "pbl_compiler_content",
"options": {
"width": "100%",
"flex": "1",
"marginTop": "20px"
}
}
]
}