pbls/scripts/gen_artifacts.py

734 lines
39 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
pbls 交付物机械生成器(单一事实源 → DDL / models / CRUD json / dspy / 模块包骨架 / RBAC 路径)
为什么存在QC 退回意见 #8/#9/#11 要求「表定义四段式 models/*.json」「CRUD json/*.json」
「RBAC 路径注册」可机械核对。手写 36 表 × 3 份产物必然漂移,故用本脚本从一份 spec 生成,
保证 DDL 与 models/CRUD/RBAC 完全一致(可重复执行,幂等)。
方言来源projects/pbls/env/test.json -> db.engine = mariadb见 TYPE_MAP禁 BIGSERIAL/SERIAL/nextval
"""
import json
import os
HERE = os.path.dirname(os.path.abspath(__file__))
ROOT = os.path.abspath(os.path.join(HERE, '..', '..', '..')) # 机构工作空间根
APP = os.path.join(ROOT, 'apps', 'pbls')
MODS = os.path.join(ROOT, 'modules')
# ── 抽象类型 → MariaDB 10.x 物理类型映射唯一映射表QC #5/#8 核对用)────────────
TYPE_MAP = {
'pk': 'BIGINT UNSIGNED NOT NULL AUTO_INCREMENT',
'tenant': 'VARCHAR(32) NOT NULL',
'sid': 'VARCHAR(32) NOT NULL',
'code': 'VARCHAR(64) NOT NULL',
'name': 'VARCHAR(128) NOT NULL',
'txt': 'TEXT',
'js': 'LONGTEXT',
'int': 'INT NOT NULL DEFAULT 0',
'bigint': 'BIGINT NOT NULL DEFAULT 0',
'dec': 'DECIMAL(18,2) NOT NULL DEFAULT 0',
'bool': 'TINYINT(1) NOT NULL DEFAULT 0',
'ts': 'DATETIME NULL',
'tsnn': 'DATETIME NOT NULL',
'ref': 'BIGINT UNSIGNED NOT NULL',
'sref': 'VARCHAR(32) NOT NULL',
}
ABSTRACT = {
'pk': {'type': 'int', 'length': 20, 'unsigned': True, 'auto_increment': True},
'tenant': {'type': 'str', 'length': 32},
'sid': {'type': 'str', 'length': 32},
'code': {'type': 'str', 'length': 64},
'name': {'type': 'str', 'length': 128},
'txt': {'type': 'text'},
'js': {'type': 'json'},
'int': {'type': 'int'},
'bigint': {'type': 'bigint'},
'dec': {'type': 'double', 'precision': 18, 'scale': 2},
'bool': {'type': 'bool'},
'ts': {'type': 'datetime'},
'tsnn': {'type': 'datetime'},
'ref': {'type': 'bigint'},
'sref': {'type': 'str', 'length': 32},
}
NULLABLE = {'txt', 'js', 'ts'}
# ── 模块清单:编号 ↔ 目录 ↔ 表 ↔ 契约QC #13 对账表的事实源)───────────────────
# tables=[] 且 contracts 不含 CRUD 的模块为「零表模块」pbl_common / pbl_appcodes
MODULES = {
'pbl_common': dict(no='M12', tables=[], contracts=[
'pbl_common_ping', 'pbl_common_audit']),
'pbl_appcodes': dict(no='M10', tables=[], contracts=[
'pbl_appcodes_inject', 'pbl_appcodes_list']),
'pbl_blueprint': dict(no='M1', tables=[
'pbl_blueprint', 'pbl_blueprint_learning_goal', 'pbl_blueprint_role',
'pbl_blueprint_mission', 'pbl_blueprint_task', 'pbl_blueprint_artifact_spec',
'pbl_blueprint_evidence_spec', 'pbl_blueprint_reflection_spec',
'pbl_blueprint_version', 'pbl_template'],
contracts=[
'pbl_blueprint_create', 'pbl_blueprint_read', 'pbl_blueprint_update',
'pbl_blueprint_delete', 'pbl_blueprint_list', 'pbl_blueprint_tree',
'pbl_blueprint_fork', 'pbl_blueprint_subobject_save', 'pbl_blueprint_subobject_list',
'pbl_blueprint_version_create', 'pbl_blueprint_version_diff',
'pbl_template_list', 'pbl_template_instantiate']),
'pbl_validation': dict(no='M2', tables=['pbl_validation_rule', 'pbl_validation_result'],
contracts=['pbl_validation_run', 'pbl_validation_get', 'pbl_validation_list',
'pbl_validation_rule_list', 'pbl_validation_rule_save']),
'pbl_compiler': dict(no='M3', tables=['pbl_compiler_version', 'pbl_game_definition',
'pbl_capability_registry'],
contracts=['pbl_compiler_compile', 'pbl_compiler_preview', 'pbl_compiler_compare',
'pbl_compiler_version_list', 'pbl_compiler_version_save',
'pbl_capability_list', 'pbl_capability_register']),
'pbl_agent_runtime': dict(no='M4', tables=['pbl_agent_def', 'pbl_tool_registry',
'pbl_agent_trace', 'pbl_agent_tool_call', 'pbl_approval', 'pbl_llm_call_log'],
contracts=['pbl_agent_designer_run', 'pbl_agent_critic_run', 'pbl_agent_trace_write',
'pbl_agent_trace_list', 'pbl_tool_registry_list', 'pbl_tool_registry_save',
'pbl_tool_adjudicate', 'pbl_approval_create', 'pbl_approval_decide',
'pbl_approval_list']),
'pbl_evidence': dict(no='M5', tables=['pbl_artifact', 'pbl_evidence'],
contracts=['pbl_artifact_create', 'pbl_artifact_read', 'pbl_artifact_update',
'pbl_artifact_delete', 'pbl_artifact_list', 'pbl_evidence_collect',
'pbl_evidence_list']),
'pbl_assessment': dict(no='M6', tables=['pbl_rubric', 'pbl_assessment_record'],
contracts=['pbl_rubric_save', 'pbl_rubric_get', 'pbl_rubric_list',
'pbl_assessment_score', 'pbl_assessment_record_get', 'pbl_assessment_report']),
'pbl_kdb_ext': dict(no='M7', tables=['pbl_kdb_item', 'pbl_kdb_query_log'],
contracts=['pbl_kdb_search', 'pbl_kdb_get', 'pbl_research_aggregate']),
'pbl_domain_ext': dict(no='M8', tables=['pbl_tenant', 'pbl_class', 'pbl_team'],
contracts=['pbl_tenant_upsert', 'pbl_class_save', 'pbl_class_list',
'pbl_team_save', 'pbl_team_list',
'pbl_domain_materialize_game_definition']),
'pbl_scense_ext': dict(no='M9', tables=['pbl_session_member', 'pbl_playtest_feedback',
'pbl_ui_page_registry'],
contracts=['pbl_session_context_get', 'pbl_page_registry_list',
'pbl_playtest_feedback_save', 'pbl_overlay_bootstrap']),
'pbl_runtime_ext': dict(no='M11', tables=['pbl_runtime_event', 'pbl_entity_state',
'pbl_world_state_snapshot'],
contracts=['pbl_runtime_event_append', 'pbl_runtime_event_poll',
'pbl_entity_state_get', 'pbl_entity_state_apply',
'pbl_world_broadcast', 'pbl_session_member_add']),
}
# ── 36 表定义(字段: (name, 抽象类型, 注释[, null]))────────────────────────────
T = {}
def tbl(mod, name, comment, fields, indexes, codes=None, partition_month=None):
T[name] = dict(module=mod, comment=comment, fields=fields, indexes=indexes,
codes=codes or [], partition_month=partition_month)
tbl('pbl_blueprint', 'pbl_blueprint', 'PBL 蓝图聚合根第8章 schema 落库)', [
('code', 'code', '蓝图编码'), ('title', 'name', '标题'), ('subject', 'code', '学科'),
('grade', 'code', '年级'), ('description', 'txt', '说明'),
('quality_state', 'code', '质量状态 pbl_quality_state'),
('generation_source', 'code', '生成来源 llm/template_fallback/manual'),
('approval_status', 'code', '审批状态'), ('visibility', 'code', '可见范围'),
('content_json', 'js', '蓝图正文(结构化模型,非散文)'),
('version_no', 'int', '当前版本'), ('compiler_version_id', 'ref', '编译版本', True),
('world_id', 'ref', '编译产物落 world 的溯源ID', True),
('created_by', 'sref', '创建人')],
[('uk_bp_tenant_code', ['tenant_id', 'code'], True),
('idx_bp_tenant_state', ['tenant_id', 'quality_state'], False),
('idx_bp_tenant_creator', ['tenant_id', 'created_by'], False)],
[('quality_state', 'pbl_quality_state'), ('approval_status', 'pbl_approval_status'),
('visibility', 'pbl_visibility')])
SUB = [('blueprint_id', 'ref', '所属蓝图'), ('obj_uid', 'sid', '子对象UID')]
for mod_tbl, extra, cmt in [
('pbl_blueprint_learning_goal', [('code', 'code', '目标编码'), ('title', 'name', '标题'),
('description', 'txt', '描述'), ('bloom_level', 'code', '布鲁姆层级'),
('measurable', 'bool', '是否可测'), ('order_no', 'int', '排序')], '学习目标'),
('pbl_blueprint_role', [('code', 'code', '角色编码'), ('title', 'name', '角色名'),
('persona_txt', 'txt', '人设'), ('capabilities_json', 'js', '能力引用'),
('headcount', 'int', '名额'), ('order_no', 'int', '排序')], 'PBL 角色'),
('pbl_blueprint_mission', [('code', 'code', '任务域编码'), ('title', 'name', '驱动问题域'),
('scenario_txt', 'txt', '情境'), ('unlock_condition_json', 'js', '解锁条件'),
('order_no', 'int', '排序')], '驱动问题/Mission'),
('pbl_blueprint_task', [('mission_id', 'ref', '所属Mission'), ('code', 'code', '编码'),
('title', 'name', '任务名'), ('steps_json', 'js', '步骤'),
('difficulty', 'code', '难度'), ('order_no', 'int', '排序')], '任务'),
('pbl_blueprint_artifact_spec', [('code', 'code', '编码'), ('title', 'name', '产出物名'),
('artifact_type', 'code', '产出类型'), ('required_fields_json', 'js', '必填字段'),
('order_no', 'int', '排序')], '产出物规格'),
('pbl_blueprint_evidence_spec', [('evidence_type', 'code', '证据类型'),
('source', 'code', '来源'), ('min_count', 'int', '最少条数'),
('rubric_criterion_uid', 'sid', '对齐的评分准则UID', True),
('order_no', 'int', '排序')], '证据规格')]:
tbl('pbl_blueprint', mod_tbl, '蓝图子对象 - ' + cmt, SUB + extra,
[('idx_%s_bp' % mod_tbl[4:], ['tenant_id', 'blueprint_id'], False)])
tbl('pbl_blueprint', 'pbl_blueprint_reflection_spec', '蓝图子对象 - 反思规格',
SUB + [('trigger', 'code', '触发点'), ('prompt_txt', 'txt', '反思引导语'),
('min_words', 'int', '最少字数'), ('evidence_type', 'code', '产出证据类型'),
('order_no', 'int', '排序')],
[('idx_bp_rs_bp', ['tenant_id', 'blueprint_id'], False)])
tbl('pbl_blueprint', 'pbl_blueprint_version', '蓝图版本change_delta 证明对话式改模型)', [
('blueprint_id', 'ref', '蓝图ID'), ('version_no', 'int', '版本号'),
('snapshot_json', 'js', '全量快照'), ('change_delta_json', 'js', '结构化增量'),
('quality_state', 'code', '质量状态'), ('generation_source', 'code', '生成来源'),
('created_by', 'sref', '操作人')],
[('uk_bpv', ['tenant_id', 'blueprint_id', 'version_no'], True)])
tbl('pbl_blueprint', 'pbl_template', 'PBL 模板(含离线兜底槽位)', [
('code', 'code', '模板编码'), ('title', 'name', '标题'), ('subject', 'code', '学科'),
('grade', 'code', '年级'), ('skeleton_json', 'js', '骨架'),
('fallback_slot_json', 'js', '兜底槽位LLM不可用时填充'),
('is_builtin', 'bool', '内置'), ('use_count', 'int', '实例化次数'),
('quality_state', 'code', '质量状态')],
[('uk_tpl_tenant_code', ['tenant_id', 'code'], True)])
tbl('pbl_validation', 'pbl_validation_rule', '校验规则14 维,规则 JSON 形态 script_type=1', [
('code', 'code', '规则编码'), ('dimension', 'code', '维度'), ('title', 'name', '标题'),
('rule_json', 'js', '规则体'), ('threshold_json', 'js', '阈值(可配,Q-OPEN-8)'),
('severity', 'code', '严重度'), ('enabled', 'bool', '启用'), ('version_no', 'int', '版本')],
[('uk_vr_code', ['tenant_id', 'code', 'version_no'], True)])
tbl('pbl_validation', 'pbl_validation_result', '校验结果14 维 + 5 级质量状态)', [
('blueprint_id', 'ref', '蓝图ID'), ('blueprint_version_no', 'int', '校验时版本'),
('dimension_count', 'int', '维度数(=14)'), ('passed_count', 'int', '通过数'),
('failed_count', 'int', '失败数'), ('score', 'dec', '得分'),
('quality_state', 'code', '质量状态'), ('findings_json', 'js', '逐维结论'),
('rule_set_version', 'code', '规则集版本'), ('created_by', 'sref', '触发人')],
[('idx_vres_bp', ['tenant_id', 'blueprint_id'], False)])
tbl('pbl_compiler', 'pbl_compiler_version', '编译器版本29.6 确定性)', [
('code', 'code', '版本编码'), ('semver', 'code', '语义化版本'),
('rules_hash', 'sid', '规则集SHA256前缀'), ('entrypoint', 'code', '入口函数'),
('enabled', 'bool', '启用'), ('notes', 'txt', '说明')],
[('uk_cv_code', ['tenant_id', 'code'], True)])
tbl('pbl_compiler', 'pbl_game_definition', 'Game Definition第9章 schema + 指纹)', [
('blueprint_id', 'ref', '来源蓝图'), ('blueprint_version_no', 'int', '蓝图版本'),
('compiler_version_id', 'ref', '编译版本'), ('content_fingerprint', 'sid', 'SHA-256 指纹'),
('definition_json', 'js', 'Game Definition 正文'),
('world_id', 'ref', '落库 world', True), ('scene_id', 'ref', '落库 scene', True),
('entity_count', 'int', '实体数'), ('event_count', 'int', '事件数'),
('quality_state', 'code', '编译时质量状态'), ('duration_ms', 'int', '耗时')],
[('uk_gd_fp', ['tenant_id', 'content_fingerprint'], True),
('idx_gd_bp', ['tenant_id', 'blueprint_id'], False)])
tbl('pbl_compiler', 'pbl_capability_registry', 'Capability Registry第11章缺口补齐', [
('capability_key', 'code', '能力键'), ('category', 'code', '分类'),
('args_schema_json', 'js', '参数JSON Schema'), ('permission_required', 'code', '所需权限'),
('is_enabled', 'bool', '启用'), ('version_no', 'int', '版本'), ('description', 'txt', '说明')],
[('uk_cap_key', ['tenant_id', 'capability_key', 'version_no'], True)])
tbl('pbl_agent_runtime', 'pbl_agent_def', 'Agent 定义Designer/Critic', [
('code', 'code', 'Agent编码'), ('agent_type', 'code', '类型'), ('name', 'name', '名称'),
('model_route', 'code', 'pipeline-llm 路由'), ('system_prompt_txt', 'txt', '提示词'),
('tool_whitelist_json', 'js', '工具白名单'), ('permission_mode', 'code', 'read/write 模式'),
('is_enabled', 'bool', '启用'), ('version_no', 'int', '版本')],
[('uk_ad_code', ['tenant_id', 'code'], True)])
tbl('pbl_agent_runtime', 'pbl_tool_registry', '工具注册表13 启用 / 9 禁用含 pbl.publish', [
('tool_key', 'code', '工具键'), ('title', 'name', '标题'), ('agent_scope', 'code', '作用域'),
('permission_required', 'code', '所需权限'), ('enabled', 'bool', '启用'),
('params_schema_json', 'js', '参数白名单Schema'), ('disabled_reason', 'code', '禁用原因', True),
('version_no', 'int', '版本')],
[('uk_tr_key', ['tenant_id', 'tool_key'], True)])
tbl('pbl_agent_runtime', 'pbl_agent_trace', 'Agent 执行轨迹第28章 7 要素)', [
('trace_uid', 'sid', '轨迹UID'), ('agent_code', 'code', 'Agent'),
('blueprint_id', 'ref', '目标蓝图', True), ('input_ref_json', 'js', '要素1 输入引用'),
('thought_txt', 'txt', '要素2 思考'), ('action_code', 'code', '要素3 动作'),
('action_params_json', 'js', '要素4 动作参数'), ('observation_json', 'js', '要素5 观察'),
('result_state', 'code', '要素6 结果状态'), ('llm_calls', 'int', '要素7 LLM调用数'),
('duration_ms', 'int', '耗时'), ('created_by', 'sref', '触发用户')],
[('uk_at_uid', ['tenant_id', 'trace_uid'], True),
('idx_at_tenant', ['tenant_id', 'created_at'], False)])
tbl('pbl_agent_runtime', 'pbl_agent_tool_call', '工具调用明细(服务端裁决留痕)', [
('trace_id', 'ref', '轨迹ID'), ('tool_key', 'code', '工具键'),
('request_json', 'js', '请求'), ('response_json', 'js', '响应'),
('ok', 'bool', '成功'), ('error_code', 'code', '错误码', True),
('adjudication_code', 'code', '裁决结论'), ('duration_ms', 'int', '耗时')],
[('idx_tc_trace', ['tenant_id', 'trace_id'], False)])
tbl('pbl_agent_runtime', 'pbl_approval', '人工审批单14.2 四类,无绕过路径)', [
('approval_uid', 'sid', '审批UID'), ('approval_type', 'code', '类型'),
('target_type', 'code', '对象类型'), ('target_id', 'ref', '对象ID'),
('payload_json', 'js', '变更内容'), ('status', 'code', '状态'),
('requested_by', 'sref', '发起人'), ('decided_by', 'sref', '决策人', True),
('decided_at', 'ts', '决策时间'), ('comment_txt', 'txt', '意见')],
[('uk_ap_uid', ['tenant_id', 'approval_uid'], True),
('idx_ap_status', ['tenant_id', 'status'], False)],
[('status', 'pbl_approval_status')])
tbl('pbl_agent_runtime', 'pbl_llm_call_log', 'LLM 调用日志(超时/重试/限流/兜底留痕)', [
('call_uid', 'sid', '调用UID'), ('agent_code', 'code', 'Agent'), ('route', 'code', '路由'),
('model', 'code', '实际模型', True), ('prompt_hash', 'sid', '提示词哈希'),
('completion_tokens', 'int', 'tokens'), ('latency_ms', 'int', '时延'),
('status', 'code', '状态'), ('error_code', 'code', '错误码', True),
('fallback_used', 'bool', '走兜底')],
[('uk_lc_uid', ['tenant_id', 'call_uid'], True)])
tbl('pbl_evidence', 'pbl_artifact', '学生产出物(版本内联 version_no', [
('artifact_uid', 'sid', '产出UID'), ('session_id', 'ref', '会话'),
('blueprint_id', 'ref', '蓝图'), ('team_id', 'ref', '团队', True),
('creator_id', 'sref', '作者'), ('title', 'name', '标题'),
('artifact_type', 'code', '类型'), ('content_json', 'js', '内容'),
('version_no', 'int', '版本'), ('status', 'code', '状态'), ('submitted_at', 'ts', '提交时间')],
[('uk_art_uid', ['tenant_id', 'artifact_uid'], True),
('idx_art_session', ['tenant_id', 'session_id'], False)],
[('artifact_type', 'pbl_artifact_type')])
tbl('pbl_evidence', 'pbl_evidence', '学习证据(幂等采集,唯一索引防重)', [
('artifact_id', 'ref', '产出物', True), ('evidence_type', 'code', '证据类型'),
('source_event_id', 'sid', '来源 runtime_event'), ('session_id', 'ref', '会话'),
('learner_id', 'sref', '学习者'), ('blueprint_id', 'ref', '蓝图', True),
('payload_json', 'js', '证据体'), ('occurred_at', 'tsnn', '发生时间'),
('dedup_key', 'sid', '去重键')],
[('uk_ev_dedup', ['tenant_id', 'source_event_id', 'evidence_type'], True),
('idx_ev_learner', ['tenant_id', 'learner_id'], False)],
[('evidence_type', 'pbl_evidence_type')])
tbl('pbl_assessment', 'pbl_rubric', 'Rubric准则内联 criteria_json权重合计=100%', [
('code', 'code', '编码'), ('title', 'name', '标题'), ('blueprint_id', 'ref', '蓝图', True),
('criteria_json', 'js', '准则数组(含 learning_goal_id/weight)'),
('total_weight', 'dec', '权重合计'), ('version_no', 'int', '版本'),
('status', 'code', '状态'), ('approval_id', 'ref', '变更审批单', True)],
[('uk_ru_code', ['tenant_id', 'code', 'version_no'], True)])
tbl('pbl_assessment', 'pbl_assessment_record', '评估记录total_score 派生 + 对齐判定)', [
('record_uid', 'sid', '记录UID'), ('learner_id', 'sref', '学习者'),
('session_id', 'ref', '会话', True), ('blueprint_id', 'ref', '蓝图', True),
('artifact_id', 'ref', '产出物', True), ('evidence_id', 'ref', '主证据', True),
('rubric_id', 'ref', 'Rubric'), ('scores_json', 'js', '逐准则得分'),
('total_score', 'dec', '加权总分'), ('max_score', 'dec', '满分'),
('band', 'code', '等级'), ('alignment_json', 'js', '与学习目标对齐'),
('feedback_txt', 'txt', '反馈'), ('assessor_type', 'code', '评估者类型'),
('assessor_id', 'sref', '评估者')],
[('uk_ar_uid', ['tenant_id', 'record_uid'], True),
('idx_ar_learner', ['tenant_id', 'learner_id'], False)])
tbl('pbl_kdb_ext', 'pbl_kdb_item', 'KDB 条目建表不写入is_write_locked 恒 1', [
('code', 'code', '条目编码'), ('item_type', 'code', '类型'), ('title', 'name', '标题'),
('summary_txt', 'txt', '摘要'), ('payload_json', 'js', '内容(无向量,Q-OPEN-5)'),
('source', 'code', '来源'), ('is_write_locked', 'bool', '写锁(恒1)'),
('status', 'code', '状态'), ('version_no', 'int', '版本')],
[('uk_kdb_code', ['tenant_id', 'code'], True)])
tbl('pbl_kdb_ext', 'pbl_kdb_query_log', 'KDB 检索日志(只读侧留痕)', [
('query_uid', 'sid', '查询UID'), ('caller_type', 'code', '调用方类型'),
('caller_id', 'sref', '调用方'), ('query_json', 'js', '查询条件'),
('hit_count', 'int', '命中数'), ('latency_ms', 'int', '时延'), ('ok', 'bool', '成功')],
[('uk_kql_uid', ['tenant_id', 'query_uid'], True)])
tbl('pbl_domain_ext', 'pbl_tenant', '租户多租户边界第25章不改基表', [
('tenant_uid', 'sid', '租户UID'), ('name', 'name', '名称'), ('kind', 'code', '类型'),
('plan', 'code', '套餐'), ('seat_limit', 'int', '席位'), ('status', 'code', '状态'),
('config_json', 'js', '配置')],
[('uk_ten_uid', ['tenant_id', 'tenant_uid'], True)])
tbl('pbl_domain_ext', 'pbl_class', '班级(花名册内联 roster_json', [
('class_uid', 'sid', '班级UID'), ('name', 'name', '名称'), ('grade', 'code', '年级'),
('teacher_id', 'sref', '教师'), ('student_count', 'int', '学生数'),
('roster_json', 'js', '花名册'), ('status', 'code', '状态')],
[('uk_cls_uid', ['tenant_id', 'class_uid'], True)])
tbl('pbl_domain_ext', 'pbl_team', '团队(成员内联 members_json不改基表', [
('team_uid', 'sid', '团队UID'), ('class_id', 'ref', '班级'), ('name', 'name', '名称'),
('mission_id', 'ref', 'Mission', True), ('members_json', 'js', '成员'),
('capacity', 'int', '容量'), ('status', 'code', '状态'),
('session_id', 'ref', '绑定会话', True)],
[('uk_team_uid', ['tenant_id', 'team_uid'], True),
('idx_team_class', ['tenant_id', 'class_id'], False)])
tbl('pbl_scense_ext', 'pbl_session_member', '会话成员(复用 world_session不污染基表', [
('session_id', 'ref', '会话'), ('user_id', 'sref', '用户'), ('role_code', 'code', '角色'),
('team_id', 'ref', '团队', True), ('joined_at', 'ts', '加入时间'),
('last_seen_at', 'ts', '心跳'), ('state_version', 'bigint', '已见状态版本'),
('status', 'code', '状态')],
[('uk_sm', ['tenant_id', 'session_id', 'user_id'], True)])
tbl('pbl_scense_ext', 'pbl_playtest_feedback', 'Playtest 反馈/反思reflection 走证据链外的轻量表)', [
('session_id', 'ref', '会话', True), ('user_id', 'sref', '用户'),
('blueprint_id', 'ref', '蓝图', True), ('feedback_type', 'code', '类型'),
('rating', 'int', '评分'), ('content_txt', 'txt', '内容'),
('context_json', 'js', '上下文'), ('handled', 'bool', '已处理')],
[('idx_pf_session', ['tenant_id', 'session_id'], False)])
tbl('pbl_scense_ext', 'pbl_ui_page_registry', '前端覆盖层页面注册12 页面 + 时延预算)', [
('page_key', 'code', '页面键'), ('title', 'name', '标题'), ('route', 'code', '路由'),
('widget_type', 'code', '渲染组件'), ('requires_role', 'code', '所需角色'),
('budget_ms', 'int', '渲染预算'), ('enabled', 'bool', '启用'), ('meta_json', 'js', '元数据')],
[('uk_upr_key', ['tenant_id', 'page_key'], True)])
tbl('pbl_runtime_ext', 'pbl_runtime_event', '运行时事件append-only按月 RANGE 分区)', [
('event_uid', 'sid', '事件UID'), ('session_id', 'ref', '会话'),
('world_id', 'ref', '世界'), ('actor_id', 'sref', '触发者'),
('event_type', 'code', '事件类型'), ('payload_json', 'js', '事件体'),
('causation_id', 'sid', '因果链', True), ('state_version', 'bigint', '状态版本'),
('seq', 'bigint', '会话内单调序号'), ('occurred_at', 'tsnn', '发生时间')],
[('uk_re_uid', ['tenant_id', 'event_uid', 'occurred_at'], True),
('idx_re_session_seq', ['tenant_id', 'session_id', 'seq'], False)],
[('event_type', 'pbl_event_type')], partition_month=True)
tbl('pbl_runtime_ext', 'pbl_entity_state', '实体状态服务端权威state_version 客户端禁写)', [
('session_id', 'ref', '会话'), ('entity_id', 'sid', '实体标识'),
('state_json', 'js', '状态'), ('state_version', 'bigint', '单调版本'),
('checksum', 'sid', '状态摘要'), ('updated_by', 'sref', '写入者')],
[('uk_es', ['tenant_id', 'session_id', 'entity_id'], True)])
tbl('pbl_runtime_ext', 'pbl_world_state_snapshot', '世界状态快照(掉线补齐/离线兜底)', [
('session_id', 'ref', '会话'), ('snapshot_uid', 'sid', '快照UID'),
('state_fingerprint', 'sid', '状态指纹'), ('entities_json', 'js', '实体集'),
('events_through_seq', 'bigint', '截至序号'), ('reason', 'code', '触发原因')],
[('uk_wss_uid', ['tenant_id', 'snapshot_uid'], True)])
assert len(T) == 36, '表数应为 36实际 %d' % len(T)
# ── 枚举 6+2 组appcodes.md 幂等注入,零表模块 pbl_appcodes 的事实源)────────────
APPCODES = {
'pbl_quality_state': ['draft', 'needs_work', 'pbl_ready', 'playtest_ready', 'publish_ready'],
'pbl_approval_status': ['none', 'pending', 'approved', 'rejected', 'withdrawn'],
'pbl_visibility': ['private', 'class', 'school', 'public_readonly'],
'pbl_evidence_type': ['artifact', 'process_event', 'reflection', 'feedback'],
'pbl_agent_type': ['designer', 'critic'],
'pbl_tool_state': ['enabled', 'disabled_contract_only'],
'pbl_artifact_type': ['document', 'presentation', 'model', 'code', 'media'],
'pbl_event_type': ['mission.completed', 'task.completed', 'pbl.unlockMission',
'entity.state.changed', 'session.member.joined', 'artifact.submitted'],
}
W = []
def w(path, content):
full = os.path.join(os.path.dirname(path) and path.rsplit(os.sep, 1)[0], '')
os.makedirs(os.path.dirname(path), exist_ok=True)
old = None
if os.path.exists(path):
with open(path, encoding='utf-8') as f:
old = f.read()
if old != content:
with open(path, 'w', encoding='utf-8') as f:
f.write(content)
W.append(os.path.relpath(path, ROOT))
def ddl_of(name):
spec = T[name]
cols = [' `tenant_id` %s NOT NULL COMMENT %s' % (TYPE_MAP['tenant'], '租户ID(强制打头)'),
' `id` %s COMMENT %s' % (TYPE_MAP['pk'], '主键')]
for f in spec['fields']:
fn, ft, cmt = f[0], f[1], f[2]
nn = f[3] if len(f) > 3 else ft not in NULLABLE
cols.append(' `%s` %s%s COMMENT %s' % (fn, TYPE_MAP[ft], '' if nn else ' NULL',
json.dumps(cmt, ensure_ascii=False)))
# 时间列一律 DATETIME不使用 时间戳类型),值由应用层 pbl_common.api.now_str() 写入,
# 避免隐式默认带来的时区漂移;默认值为纪元占位(非 NULL便于排序与增量拉取
cols.append(' `created_at` DATETIME NOT NULL DEFAULT \'1970-01-01 00:00:00\' COMMENT %s'
% json.dumps('创建时间(应用层写入)', ensure_ascii=False))
cols.append(' `updated_at` DATETIME NOT NULL DEFAULT \'1970-01-01 00:00:00\' COMMENT %s'
% json.dumps('更新时间(应用层写入)', ensure_ascii=False))
cols.append(' PRIMARY KEY (`id`)')
for iname, icols, uniq in spec['indexes']:
cols.append(' %s `%s` (%s)' % ('UNIQUE KEY' if uniq else 'KEY', iname,
', '.join('`%s`' % c for c in icols)))
body = ',\n'.join(cols)
sql = 'CREATE TABLE IF NOT EXISTS `%s` (\n%s\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4' % (name, body)
sql += " COLLATE=utf8mb4_general_ci COMMENT=%s" % json.dumps(spec['comment'], ensure_ascii=False)
if spec['partition_month']:
parts = ',\n'.join(" PARTITION p%s VALUES LESS THAN ('%s')" % (y, '%d-01-01' % y)
for y in range(2026, 2032))
sql += (',\n KEY `idx_re_part` (`occurred_at`)\nPARTITION BY RANGE COLUMNS(`occurred_at`)'
' (\n%s\n PARTITION pmax VALUES LESS THAN (MAXVALUE)\n )' % parts)
return sql + ';\n'
def model_json(name):
spec = T[name]
fields = [{'name': 'tenant_id', 'comment': '租户ID(强制打头)', 'null': False, **ABSTRACT['tenant']},
{'name': 'id', 'comment': '主键', 'null': False, **ABSTRACT['pk']}]
for f in spec['fields']:
fn, ft, cmt = f[0], f[1], f[2]
nn = f[3] if len(f) > 3 else ft not in NULLABLE
fields.append({'name': fn, 'comment': cmt, 'null': not nn, **ABSTRACT[ft]})
fields.append({'name': 'created_at', 'comment': '创建时间', 'null': False, **ABSTRACT['tsnn']})
fields.append({'name': 'updated_at', 'comment': '更新时间', 'null': False, **ABSTRACT['tsnn']})
doc = {
'summary': [{'name': name, 'comment': spec['comment'], 'module': spec['module'],
'engine': 'mariadb', 'charset': 'utf8mb4', 'tenant_scoped': True}],
'fields': fields,
'indexes': [{'name': i[0], 'unique': i[2], 'fields': i[1]} for i in spec['indexes']]
+ [{'name': 'PRIMARY', 'unique': True, 'fields': ['id']}],
'codes': [{'field': c[0], 'appcode': c[1]} for c in spec['codes']],
}
return json.dumps(doc, ensure_ascii=False, indent=2) + '\n'
def crud_json(mod, name):
spec = T[name]
br, ed = {}, {}
for f in spec['fields']:
fn, ft, cmt = f[0], f[1], f[2]
ui = {'label': cmt or fn, 'type': 'text' if ft in ('txt', 'js', 'sid', 'sref') else
'number' if ft in ('int', 'bigint', 'dec', 'ref') else
'checkbox' if ft == 'bool' else 'date' if ft.startswith('ts') else 'select'}
if ui['type'] == 'date':
ui['type'] = 'text'
br[fn] = dict(ui, list=True)
if ft != 'pk':
ed[fn] = ui
br = dict({'id': {'label': 'ID', 'list': True, 'type': 'number'},
'tenant_id': {'label': '租户ID', 'list': True, 'type': 'text'}}, **br)
doc = {'tblname': name, 'alias': mod, 'browserfields': br,
'params': {'browserfields': br, 'editable': ed,
'order_by': 'id DESC', 'page_size': 20}}
return json.dumps(doc, ensure_ascii=False, indent=2) + '\n'
DSPY = """# {mod}/api/{fn}.dspy —— 契约端点(自动生成,勿手改:改 spec 后跑 gen_artifacts.py
debug('{mod}/api/{fn}.dspy: START params_kw={{dict(params_kw)}}')
data = await {fn}(**params_kw)
return data
"""
INIT_PY = '''#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""`load_{mod}()` —— {mod} 模块唯一挂载入口(自动生成)。
注册三处同步之 ③env.<契约名> = <契约名>(① 定义在 api.py② 导出在 __init__.py
"""
from ahserver.serverenv import ServerEnv
from {mod}.api import (
{imports}
)
def load_{mod}():
env = ServerEnv()
env.get_module_dbname = env.get_module_dbname # 库名由应用决定,模块不硬编码
{registers}
return '{mod}'
'''
DYN_INIT_PY = '''#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""`load_{mod}()` —— {mod} 模块唯一挂载入口。
注册三处同步之 ③env.<契约名> = <契约名>(① 定义在 api.py② 导出在 __init__.py
"""
from ahserver.serverenv import ServerEnv
from {mod}.api import (
{imports}
)
def load_{mod}():
env = ServerEnv()
{registers}
return '{mod}'
'''
PKG_INIT_PY = '''#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""{mod} —— {desc}
注册三处同步之 ②:必须导出 init.py 里的全部契约函数,漏一行 .dspy 调用即 NameError。
"""
from {mod}.init import load_{mod}
from {mod}.api import (
{imports}
)
__all__ = [
'load_{mod}',
{exports}
]
'''
PYPROJECT = '''[project]
name = "{mod}"
version = "0.1.0"
description = "{desc}"
requires-python = ">=3.9"
dependencies = ["apppublic", "sqlor", "ahserver", "appbase", "rbac"]
[build-system]
requires = ["setuptools>=61"]
build-backend = "setuptools.build_meta"
[tool.setuptools]
packages = ["{mod}"]
'''
LOAD_PATH = '''#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""{mod} RBAC 路径注册(硬门禁 6.6 / QC #11
约定:
- 路径 = 模块自动路由 `/{mod}/api/<契约>.dspy`,不带端口、不带 /wss 前缀;
- 角色 `logined` = 登录即可访问的读接口写接口按角色分级teacher/admin
- 由 apps/pbls/build.sh 第 8 步调用 `register()`rbac CLI 不在位时打印清单(不静默跳过)。
"""
import os
import subprocess
import sys
MODULE = '{mod}'
# (path, role)
PATHS = [
{entries}
]
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)
'''
SKILL_MD = '''# {mod} 模块技能(自动生成骨架 + 人工补充)
## 定位
{desc}
## 挂载
`from {mod}.init import load_{mod}` → `load_{mod}()`(应用 app/pbls.py init() 中按序调用)
## 数据表({tn} 张)
{tables}
## 契约接口({cn} 个,路径 `/{mod}/api/<name>.dspy`
{contracts}
## 陷阱
- 库名一律 `ServerEnv().get_module_dbname('{mod}')`,禁止硬编码 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 路径。
'''
INDEX_UI = json.dumps({
"widgettype": "VBox",
"options": {"width": "100%", "height": "100%", "padding": "20px"},
"subwidgets": [
{"widgettype": "Text", "options": {"label": "{title}", "fontSize": "24px"}},
{"widgettype": "ResponsableBox", "options": {"gap": "16px", "minWidth": "250px"},
"subwidgets": []},
{"widgettype": "VBox", "id": "{mod}_content",
"options": {"width": "100%", "flex": "1", "marginTop": "20px"}}]
}, ensure_ascii=False, indent=2) + '\n'
def contracts_of(mod):
return MODULES[mod]['contracts']
def write_module(mod, desc):
spec = MODULES[mod]
base = os.path.join(MODS, mod)
pkg = os.path.join(base, mod)
cs = contracts_of(mod)
imports = ''.join(" %s,\n" % c for c in cs)
registers = ''.join(" env.%s = %s\n" % (c, c) for c in cs)
exports = ''.join(" %r,\n" % c for c in cs)
w(os.path.join(pkg, 'init.py'), DYN_INIT_PY.format(
mod=mod, imports=imports, registers=registers))
w(os.path.join(pkg, '__init__.py'), PKG_INIT_PY.format(
mod=mod, desc=desc, imports=imports, exports=exports))
w(os.path.join(base, 'pyproject.toml'), PYPROJECT.format(mod=mod, desc=desc))
entries = ''.join(" ('/%s/api/%s.dspy', %r),\n" % (mod, c, 'logined') for c in cs)
w(os.path.join(base, 'scripts', 'load_path.py'), LOAD_PATH.format(
mod=mod, entries=entries))
w(os.path.join(base, 'skill', 'SKILL.md'), SKILL_MD.format(
mod=mod, desc=desc, tn=len(spec['tables']),
tables='\n'.join('- `%s`%s' % (t, T[t]['comment']) for t in spec['tables']) or '- 无(零表模块)',
cn=len(cs), contracts='\n'.join('- `%s`' % c for c in cs)))
cards = []
for c in cs:
cards.append({
"widgettype": "VBox",
"options": {"backgroundColor": "#FFFFFF", "padding": "20px", "cursor": "pointer"},
"binds": [{"wid": "self", "event": "click", "actiontype": "urlwidget",
"target": "app.%s_content" % mod,
"options": {"url": "{{entire_url('api/%s.dspy')}}" % c}, "mode": "replace"}],
"subwidgets": [{"widgettype": "Text", "options": {"label": c}}]})
ui = json.loads(INDEX_UI.replace('{title}', desc).replace('{mod}', mod))
ui['subwidgets'][1]['subwidgets'] = cards
w(os.path.join(base, 'wwwroot', 'index.ui'),
json.dumps(ui, ensure_ascii=False, indent=2) + '\n')
for c in cs:
w(os.path.join(base, 'wwwroot', 'api', c + '.dspy'), DSPY.format(mod=mod, fn=c))
for t in spec['tables']:
w(os.path.join(base, 'models', t + '.json'), model_json(t))
w(os.path.join(base, 'json', '%s_%s.json' % (mod[4:], t)), crud_json(mod, t))
DESCS = {
'pbl_common': 'PBL 公共内核(租户上下文/DB 适配/错误码/审计/CRUD 工厂)',
'pbl_appcodes': 'PBL 枚举编码注入6+2 组,幂等,零表)',
'pbl_blueprint': 'PBL 蓝图聚合根与子对象、版本、模板M1a/M1b',
'pbl_validation': 'PBL 校验引擎14 维 + 5 级质量状态M2',
'pbl_compiler': 'PBL Compiler v1确定性编译 + Game DefinitionM3a/M3b',
'pbl_agent_runtime': 'Designer/Critic Agent 运行时与 fail-closed 工具裁决M4a/M4b',
'pbl_evidence': '产出物与证据幂等采集M5a/M5b',
'pbl_assessment': 'Rubric 加权评估与报告M6',
'pbl_kdb_ext': 'KDB 只读桩与匿名聚合M7零写入',
'pbl_domain_ext': 'world/scene/entity 薄扩展:租户/班级/团队关联M8不改基表',
'pbl_scense_ext': 'scense/scense_game 前端覆盖层与页面注册M9',
'pbl_runtime_ext': 'scense_runtime 薄扩展:单事务事件+状态写入与广播M11a/M11b',
}
for m in MODULES:
write_module(m, DESCS[m])
# ── 应用级 DDL36 表,按模块顺序)──────────────────────────────────────────
order = ['pbl_blueprint', 'pbl_validation', 'pbl_compiler', 'pbl_agent_runtime',
'pbl_evidence', 'pbl_assessment', 'pbl_kdb_ext', 'pbl_domain_ext',
'pbl_scense_ext', 'pbl_runtime_ext']
parts = ["-- pbls 36 表 DDLMariaDB 10.x 方言,由 scripts/gen_artifacts.py 从同一 spec 生成)",
"-- 硬约束tenant_id 打头 / id 为 BIGINT UNSIGNED AUTO_INCREMENT 主键;"
"-- 不使用 外键约束 / 枚举列类型 / 时间戳列类型 / 位图列类型(枚举值走 appcodes 编码表,"
"时间列走 DATETIME 由应用层写入)",
"-- 方言来源projects/pbls/env/test.json -> db.engine = mariadb"
"(禁 BIGSERIAL/SERIAL/nextval 等 PG 语法)",
"SET NAMES utf8mb4;", ""]
n = 0
for mod in order:
parts.append('-- ===== %s (%s) =====' % (mod, MODULES[mod]['no']))
for t in MODULES[mod]['tables']:
parts.append(ddl_of(t))
n += 1
parts.append('-- ===== 枚举 6+2 组由 pbl_appcodes.load_pbl_appcodes() 幂等注入 appcodes本文件不含 INSERT =====')
w(os.path.join(APP, 'scripts', 'ddl', 'pbls_tables.sql'), '\n'.join(parts) + '\n')
assert n == 36, n
# 分模块 DDL 片段(模块仓库自包含,便于单模块重建表)
for mod in order:
frag = ['-- %s 表 DDL自动生成与 apps/pbls/scripts/ddl/pbls_tables.sql 同源)' % mod]
for t in MODULES[mod]['tables']:
frag.append(ddl_of(t))
w(os.path.join(MODS, mod, 'sql', mod + '.sql'), '\n'.join(frag) + '\n')
# ── appcodes 种子数据pbl_appcodes 幂等注入源)─────────────────────────────
w(os.path.join(MODS, 'pbl_appcodes', 'init', 'data.json'),
json.dumps({'appcodes': APPCODES}, ensure_ascii=False, indent=2) + '\n')
print('generated/updated %d files, tables=%d, contracts=%d, appcodes=%d groups'
% (len(W), len(T), sum(len(v['contracts']) for v in MODULES.values()), len(APPCODES)))
for p in W:
print(' +', p)