diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6579d96 --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +__pycache__/ +*.pyc +*.pyo +.pytest_cache/ +.coverage +.DS_Store +*.log +node_modules/ +dist/ +build/ +.venv/ +venv/ diff --git a/json/compiler_pbl_capability_registry.json b/json/compiler_pbl_capability_registry.json new file mode 100644 index 0000000..d8616b2 --- /dev/null +++ b/json/compiler_pbl_capability_registry.json @@ -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 + } +} diff --git a/json/compiler_pbl_compiler_version.json b/json/compiler_pbl_compiler_version.json new file mode 100644 index 0000000..cb868f9 --- /dev/null +++ b/json/compiler_pbl_compiler_version.json @@ -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 + } +} diff --git a/json/compiler_pbl_game_definition.json b/json/compiler_pbl_game_definition.json new file mode 100644 index 0000000..075348f --- /dev/null +++ b/json/compiler_pbl_game_definition.json @@ -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 + } +} diff --git a/models/pbl_capability_registry.json b/models/pbl_capability_registry.json new file mode 100644 index 0000000..780fbf1 --- /dev/null +++ b/models/pbl_capability_registry.json @@ -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": [] +} diff --git a/models/pbl_compiler_version.json b/models/pbl_compiler_version.json new file mode 100644 index 0000000..7d9aa91 --- /dev/null +++ b/models/pbl_compiler_version.json @@ -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": [] +} diff --git a/models/pbl_game_definition.json b/models/pbl_game_definition.json new file mode 100644 index 0000000..7b25087 --- /dev/null +++ b/models/pbl_game_definition.json @@ -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": [] +} diff --git a/pbl_compiler/__init__.py b/pbl_compiler/__init__.py new file mode 100644 index 0000000..3439665 --- /dev/null +++ b/pbl_compiler/__init__.py @@ -0,0 +1,29 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""pbl_compiler —— PBL Compiler v1(确定性编译 + Game Definition,M3a/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', + +] diff --git a/pbl_compiler/api.py b/pbl_compiler/api.py new file mode 100644 index 0000000..cc6cc63 --- /dev/null +++ b/pbl_compiler/api.py @@ -0,0 +1,283 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""pbl_compiler.api —— PBL Compiler v1:Blueprint → 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 diff --git a/pbl_compiler/init.py b/pbl_compiler/init.py new file mode 100644 index 0000000..9bb168f --- /dev/null +++ b/pbl_compiler/init.py @@ -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' diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..bb4803a --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,13 @@ +[project] +name = "pbl_compiler" +version = "0.1.0" +description = "PBL Compiler v1(确定性编译 + Game Definition,M3a/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"] diff --git a/scripts/load_path.py b/scripts/load_path.py new file mode 100644 index 0000000..c4ae069 --- /dev/null +++ b/scripts/load_path.py @@ -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) diff --git a/skill/SKILL.md b/skill/SKILL.md new file mode 100644 index 0000000..8a8a474 --- /dev/null +++ b/skill/SKILL.md @@ -0,0 +1,27 @@ +# pbl_compiler 模块技能(自动生成骨架 + 人工补充) + +## 定位 +PBL Compiler v1(确定性编译 + Game Definition,M3a/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/.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 路径。 diff --git a/sql/pbl_compiler.sql b/sql/pbl_compiler.sql new file mode 100644 index 0000000..31b671d --- /dev/null +++ b/sql/pbl_compiler.sql @@ -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章缺口补齐)"; + diff --git a/wwwroot/api/pbl_capability_list.dspy b/wwwroot/api/pbl_capability_list.dspy new file mode 100644 index 0000000..f914ede --- /dev/null +++ b/wwwroot/api/pbl_capability_list.dspy @@ -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 diff --git a/wwwroot/api/pbl_capability_register.dspy b/wwwroot/api/pbl_capability_register.dspy new file mode 100644 index 0000000..4ecb8c3 --- /dev/null +++ b/wwwroot/api/pbl_capability_register.dspy @@ -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 diff --git a/wwwroot/api/pbl_compiler_compare.dspy b/wwwroot/api/pbl_compiler_compare.dspy new file mode 100644 index 0000000..0eaf42c --- /dev/null +++ b/wwwroot/api/pbl_compiler_compare.dspy @@ -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 diff --git a/wwwroot/api/pbl_compiler_compile.dspy b/wwwroot/api/pbl_compiler_compile.dspy new file mode 100644 index 0000000..2277d23 --- /dev/null +++ b/wwwroot/api/pbl_compiler_compile.dspy @@ -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 diff --git a/wwwroot/api/pbl_compiler_preview.dspy b/wwwroot/api/pbl_compiler_preview.dspy new file mode 100644 index 0000000..586ce50 --- /dev/null +++ b/wwwroot/api/pbl_compiler_preview.dspy @@ -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 diff --git a/wwwroot/api/pbl_compiler_version_list.dspy b/wwwroot/api/pbl_compiler_version_list.dspy new file mode 100644 index 0000000..18374bc --- /dev/null +++ b/wwwroot/api/pbl_compiler_version_list.dspy @@ -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 diff --git a/wwwroot/api/pbl_compiler_version_save.dspy b/wwwroot/api/pbl_compiler_version_save.dspy new file mode 100644 index 0000000..4e95c4d --- /dev/null +++ b/wwwroot/api/pbl_compiler_version_save.dspy @@ -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 diff --git a/wwwroot/index.ui b/wwwroot/index.ui new file mode 100644 index 0000000..feaf442 --- /dev/null +++ b/wwwroot/index.ui @@ -0,0 +1,231 @@ +{ + "widgettype": "VBox", + "options": { + "width": "100%", + "height": "100%", + "padding": "20px" + }, + "subwidgets": [ + { + "widgettype": "Text", + "options": { + "label": "PBL Compiler v1(确定性编译 + Game Definition,M3a/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" + } + } + ] +}