fix(pbl_compiler): M3b RBAC 29条显式登记+CRUD只读拒绝端点+json根键统一+audit BinOp解析修复

- scripts/load_path.py: PATHS_CONTRACTS 20 条(含 M3b 5 个导出契约) + PATHS_READONLY_DENY 9 条
  = 29 条全显式登记,无通配符;与 wwwroot/api/*.dspy(29) 一一对应
- wwwroot/api/: 新增 9 个 CRUD 只读拒绝端点(create/update/delete × 3 表),
  封死 xls2ui 回退默认写入口,保 rules_hash 29.6 确定性指纹唯一写入路径
- json/*.json: 根级 browserfields 合并进 params,三文件统一 tblname/alias/title/params 根键集,
  editable 齐备 new_/update_/delete_data_url 且指向真实 .dspy
- scripts/audit_rbac_parity.py: 修复 PATHS = A + B (BinOp) 解析取空的自身缺陷(假 FAIL 根因),
  新增 json/ 根键白名单 + editable 三 URL + 幽灵/漏登记 检查
- pbl_compiler/init.py: 文件头注释登记条数与代码事实对齐(29=20+9)
- 实跑: audit_rbac_parity.py rc=0 PASS;test_m3b_mapping.py PASS=113/FAIL=0
This commit is contained in:
agent.develop 2026-09-19 11:04:55 +08:00
parent d203811973
commit cb1132e6a9
15 changed files with 491 additions and 394 deletions

View File

@ -1,131 +1,39 @@
{
"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
}
},
"alias": "pbl_capability_registry_list",
"title": "能力注册表第11章缺口",
"_comment": "M3b 整改(QC #3):删除根级冗余 browserfields规范只允许 params.browserfields根键统一为 tblname/alias/title/paramsalias 由 pbl_compiler与模块同名、生成目录会与模块 wwwroot 冲突)改为 pbl_capability_registry_listparams.editable 补齐 new/update/delete_data_url 对象,三写 URL 指向显式拒绝直写的端点(唯一写入口是契约 pbl_capability_register。",
"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
}
"id": {"label": "ID", "type": "number", "list": true},
"tenant_id": {"label": "租户ID", "type": "text", "list": true},
"capability_key": {"label": "能力键", "type": "text", "list": true},
"category": {"label": "分类", "type": "text", "list": true},
"args_schema_json": {"label": "参数JSON Schema", "type": "text", "list": true},
"permission_required": {"label": "所需权限", "type": "text", "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"
"get_data_url": {
"url": "{{entire_url('/pbl_compiler/api/pbl_capability_list.dspy')}}"
},
"category": {
"label": "分类",
"type": "select"
"new_data_url": {
"url": "{{entire_url('/pbl_compiler/api/pbl_capability_registry_create.dspy')}}"
},
"args_schema_json": {
"label": "参数JSON Schema",
"type": "text"
"update_data_url": {
"url": "{{entire_url('/pbl_compiler/api/pbl_capability_registry_update.dspy')}}"
},
"permission_required": {
"label": "所需权限",
"type": "select"
},
"is_enabled": {
"label": "启用",
"type": "checkbox"
},
"version_no": {
"label": "版本",
"type": "number"
},
"description": {
"label": "说明",
"type": "text"
"delete_data_url": {
"url": "{{entire_url('/pbl_compiler/api/pbl_capability_registry_delete.dspy')}}"
}
},
"new_data_url": "{{entire_url('/pbl_compiler/api/pbl_capability_registry_create.dspy')}}",
"update_data_url": "{{entire_url('/pbl_compiler/api/pbl_capability_registry_update.dspy')}}",
"delete_data_url": "{{entire_url('/pbl_compiler/api/pbl_capability_registry_delete.dspy')}}",
"editexclouded": ["id", "tenant_id", "created_at", "updated_at"],
"_crud_note": "能力注册表只读列表:注册/更新一律走契约 pbl_capability_register做 capability_key 规范化、args_schema JSON 校验与租户归属校验),物理删除一律禁止(历史蓝图引用会造成产物不可复现),下线用 is_enabled=0 软标记。三个写端点仅用于满足 editable 格式硬要求并封死 xls2ui 默认写包装,均返回 widgettype=Error。",
"order_by": "id DESC",
"page_size": 20
}

View File

@ -1,117 +1,38 @@
{
"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
}
},
"alias": "pbl_compiler_version_list",
"title": "编译器版本29.6 确定性)",
"_comment": "M3b 整改(QC #3):删除根级冗余 browserfields规范只允许 params.browserfields根键统一为 tblname/alias/title/paramsalias 由 pbl_compiler与模块同名、生成目录会与模块 wwwroot 冲突)改为 pbl_compiler_version_listparams.editable 补齐 new/update/delete_data_url 对象,三写 URL 指向显式拒绝直写的端点(唯一写入口是契约 pbl_compiler_version_register。",
"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
}
"id": {"label": "ID", "type": "number", "list": true},
"tenant_id": {"label": "租户ID", "type": "text", "list": true},
"code": {"label": "版本编码", "type": "text", "list": true},
"semver": {"label": "语义化版本", "type": "text", "list": true},
"rules_hash": {"label": "规则集SHA256前缀", "type": "text", "list": true},
"entrypoint": {"label": "入口函数", "type": "text", "list": true},
"enabled": {"label": "启用", "type": "checkbox", "list": true},
"notes": {"label": "说明", "type": "text", "list": true}
},
"editable": {
"code": {
"label": "版本编码",
"type": "select"
"get_data_url": {
"url": "{{entire_url('/pbl_compiler/api/pbl_compiler_version_list.dspy')}}"
},
"semver": {
"label": "语义化版本",
"type": "select"
"new_data_url": {
"url": "{{entire_url('/pbl_compiler/api/pbl_compiler_version_create.dspy')}}"
},
"rules_hash": {
"label": "规则集SHA256前缀",
"type": "text"
"update_data_url": {
"url": "{{entire_url('/pbl_compiler/api/pbl_compiler_version_update.dspy')}}"
},
"entrypoint": {
"label": "入口函数",
"type": "select"
},
"enabled": {
"label": "启用",
"type": "checkbox"
},
"notes": {
"label": "说明",
"type": "text"
"delete_data_url": {
"url": "{{entire_url('/pbl_compiler/api/pbl_compiler_version_delete.dspy')}}"
}
},
"new_data_url": "{{entire_url('/pbl_compiler/api/pbl_compiler_version_create.dspy')}}",
"update_data_url": "{{entire_url('/pbl_compiler/api/pbl_compiler_version_update.dspy')}}",
"delete_data_url": "{{entire_url('/pbl_compiler/api/pbl_compiler_version_delete.dspy')}}",
"editexclouded": ["id", "tenant_id", "rules_hash", "created_at", "updated_at"],
"_crud_note": "编译器版本行一经登记即不可变(版本 diff 与确定性验证以其 rules_hash 为基线):新增/变更一律走契约 pbl_compiler_version_register计算 rules_hash + semver 唯一性校验),停用走 enabled=0禁止物理删除pbl_game_definition.compiler_version_id 引用它)。三个写端点仅用于满足 editable 格式硬要求并封死 xls2ui 默认写包装,均返回 widgettype=Error。",
"order_by": "id DESC",
"page_size": 20
}

View File

@ -1,105 +1,67 @@
{
"tblname": "pbl_game_definition",
"alias": "pbl_game_definition_list",
"_comment": "M3b 整改(QC #13):主键/外键类字段在 models/pbl_game_definition.json 中为 str(32)CRUD 渲染类型必须为 text原 number 会按数字输入框渲染 32 位字符串主键,排序/校验错位);同时补齐 M3b 规则导出四字段 rules_json/rules_hash/rule_count/rules_manifest 与 compile_status/error_message移除模型中不存在的幽灵字段。",
"title": "Game Definition 编译产物",
"_comment": "M3b 整改(QC #13):主键/外键类字段在 models/pbl_game_definition.json 中为 str(32)CRUD 渲染类型必须为 text补齐 M3b 规则导出四件套 rules_json/rules_hash/rule_count/rules_manifest 与 compile_status/error_message。M3b 整改(QC #2 方案 A)params.editable 由「只有 get_data_url」补成规范要求的四 URL 齐备结构,三个写 URL 指向显式返回 Error 的只读拒绝端点,杜绝 xls2ui 回退生成默认 add_/update_/delete_pbl_game_definition.dspy 绕过 gd_rules_writer 唯一写入口。",
"params": {
"browserfields": {
"id": {
"label": "ID",
"list": true,
"type": "text"
},
"tenant_id": {
"label": "租户ID",
"list": true,
"type": "text"
},
"blueprint_id": {
"label": "来源蓝图",
"type": "text",
"list": true
},
"blueprint_version_no": {
"label": "蓝图版本",
"type": "number",
"list": true
},
"compiler_version_id": {
"label": "编译版本",
"type": "text",
"list": true
},
"content_fingerprint": {
"label": "SHA-256 指纹",
"type": "text",
"list": true
},
"rules_hash": {
"label": "规则集合指纹",
"type": "text",
"list": true
},
"rule_count": {
"label": "规则条数",
"type": "number",
"list": true
},
"compile_status": {
"label": "编译结果状态",
"type": "select",
"list": true
},
"quality_state": {
"label": "编译时质量状态",
"type": "select",
"list": true
},
"world_id": {
"label": "落库 world",
"type": "text",
"list": true
},
"scene_id": {
"label": "落库 scene",
"type": "text",
"list": true
},
"duration_ms": {
"label": "耗时",
"type": "number",
"list": true
},
"definition_json": {
"label": "Game Definition 正文",
"type": "text",
"list": false
},
"rules_json": {
"label": "规则映射产物(script_type=1)",
"type": "text",
"list": false
},
"rules_manifest": {
"label": "规则溯源清单",
"type": "text",
"list": false
},
"error_message": {
"label": "失败原因",
"type": "text",
"list": false
},
"created_at": {
"label": "创建时间",
"type": "text",
"list": true
}
"id": {"label": "ID", "type": "text", "list": true},
"tenant_id": {"label": "租户ID", "type": "text", "list": true},
"blueprint_id": {"label": "来源蓝图", "type": "text", "list": true},
"blueprint_version_no": {"label": "蓝图版本", "type": "number", "list": true},
"compiler_version_id": {"label": "编译版本", "type": "text", "list": true},
"content_fingerprint": {"label": "SHA-256 指纹", "type": "text", "list": true},
"rules_hash": {"label": "规则集合指纹", "type": "text", "list": true},
"rule_count": {"label": "规则条数", "type": "number", "list": true},
"compile_status": {"label": "编译结果状态", "type": "select", "list": true},
"quality_state": {"label": "编译时质量状态", "type": "select", "list": true},
"world_id": {"label": "落库 world", "type": "text", "list": true},
"scene_id": {"label": "落库 scene", "type": "text", "list": true},
"duration_ms": {"label": "耗时", "type": "number", "list": true},
"definition_json": {"label": "Game Definition 正文", "type": "text", "list": false},
"rules_json": {"label": "规则映射产物(script_type=1)", "type": "text", "list": false},
"rules_manifest": {"label": "规则溯源清单", "type": "text", "list": false},
"error_message": {"label": "失败原因", "type": "text", "list": false},
"created_at": {"label": "创建时间", "type": "text", "list": true}
},
"editable": {
"get_data_url": "{{entire_url('/pbl_compiler/api/pbl_game_definition_get.dspy')}}"
"get_data_url": {
"url": "{{entire_url('/pbl_compiler/api/pbl_game_definition_get.dspy')}}"
},
"new_data_url": {
"url": "{{entire_url('/pbl_compiler/api/pbl_game_definition_create.dspy')}}"
},
"update_data_url": {
"url": "{{entire_url('/pbl_compiler/api/pbl_game_definition_update.dspy')}}"
},
"delete_data_url": {
"url": "{{entire_url('/pbl_compiler/api/pbl_game_definition_delete.dspy')}}"
}
},
"_crud_note": "pbl_game_definition 是编译产物表(只读):正文与规则四件套由 pbl_compiler_compile 主流程经 gd_artifact_writer/gd_rules_writer 唯一写入口落库,刻意不提供 new_data_url/update_data_url/delete_data_url避免手工增删改绕过 29.6 确定性指纹与 rules_hash 收口。QC #3 整改params.editable 原被挪用为编辑字段 label/type 映射(非法结构),现改为规范要求的只读 editable 对象;同时删除根级冗余 browserfields规范只允许 params.browserfieldsalias 由 pbl_compiler与模块同名、会与模块 wwwroot 冲突)改为 pbl_game_definition_list。",
"new_data_url": "{{entire_url('/pbl_compiler/api/pbl_game_definition_create.dspy')}}",
"update_data_url": "{{entire_url('/pbl_compiler/api/pbl_game_definition_update.dspy')}}",
"delete_data_url": "{{entire_url('/pbl_compiler/api/pbl_game_definition_delete.dspy')}}",
"editexclouded": [
"id",
"tenant_id",
"content_fingerprint",
"definition_json",
"rules_json",
"rules_hash",
"rule_count",
"rules_manifest",
"compile_status",
"quality_state",
"duration_ms",
"error_message",
"created_by",
"created_at",
"updated_by",
"updated_at",
"is_deleted"
],
"_crud_note": "只读产物表pbl_game_definition 的正文definition_json与规则四件套rules_json/rules_hash/rule_count/rules_manifest只能由 pbl_compiler_compile 主流程经 gd_artifact_writer / gd_rules_writer 唯一写入口落库29.6 确定性指纹 + rules_hash 收口)。为满足 crud-definition-spec「editable 必须齐备 new/update/delete_data_url」的格式硬要求三个写 URL 指向 wwwroot/api/pbl_game_definition_{create,update,delete}.dspy —— 这三个端点一律返回 widgettype=Error『编译产物只读禁止手工增删改』既不产生可写路径也不允许 xls2ui 生成默认写包装。URL 同时写在 params 顶层xls2ddl 从顶层读取,嵌套会被忽略并回退默认 add_ 端点)。",
"order_by": "id DESC",
"page_size": 20
}
}
}

View File

@ -10,7 +10,7 @@ QC #1 整改M3b 本轮CONTRACTS 由 15 → 20新增 rules_export_api
第12章规则映射 / 导出 / game_definition 写入契约并导出 GD 规则写入器与纯映射
层函数宿主应用与自测脚本可直接复用本文件的 CONTRACTS 是三处同步的唯一事实源
· wwwroot/api/<契约名>.dspy 薄包装一一对应 20
· scripts/load_path.py PATHSRBAC 逐条显式登记 20 + 只读列表 CRUD 6
· scripts/load_path.py PATHSRBAC 逐条显式登记 29 = Web 契约端点 20 + CRUD 只读拒绝端点 9无通配符
· __init__.py import + __all__
"""
from ahserver.serverenv import ServerEnv

View File

@ -1,25 +1,29 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""pbl_compiler 接线/RBAC 一致性机械审计QC 硬门禁自证工具)。
"""pbl_compiler 接线 / RBAC / CRUD 定义 一致性机械审计QC 硬门禁自证工具)。
背景连续多轮 QC 退回的根因都是写了但没接线 / 接了线但没登记 RBAC
人肉声明不可信故本脚本用机械比对把四张登记面钉死一致
背景连续多轮 QC 退回的根因都是写了但没接线 / 接了线但没登记 RBAC /
CRUD 定义格式不合规人肉声明不可信故本脚本用机械比对钉死所有登记面
pbl_compiler/init.py CONTRACTS 契约名集合
pbl_compiler/__init__.py __all__ 导出集合 CONTRACTS
wwwroot/api/*.dspy 实际端点文件集合 == CONTRACTS
scripts/load_path.py PATHS 登记集合 == CONTRACTS
并额外检查
· load_path.py 无通配符规范硬规定 % / *
· 每个 .dspy 显式 return sqlor.filter 外无顶层 import ServerEnv() 取请求态
· 实现层契约名必须能在 api.py / rules_export_api.py 中找到同名 async def防幽灵注册
· wwwroot/ 下若出现 CRUD 生成子目录 index.ui + get_/add_/update_/delete_*.dspy
必须在 PATHS 中逐条登记生成目录未登记 403复发
比对集合
期望端点集合 EXPECTED = CONTRACTS READONLY_DENY
pbl_compiler/init.py CONTRACTS 契约名集合20
pbl_compiler/__init__.py __all__ 导出集合 CONTRACTS
wwwroot/api/*.dspy 实际端点文件集合 == EXPECTED
scripts/load_path.py PATHS 登记集合 == EXPECTED
scripts/load_path.py PATHS_READONLY_DENYCRUD 只读拒绝端点非契约
另加 json/*.json 结构审计QC #3改任一 CRUD 文件必须扫 json/ 全部Pitfall 10
· 根键白名单 {tblname, alias, title, params, _comment}禁根级 browserfields
· browserfields 只允许挂在 params
· alias 不得与模块名同名会与模块 wwwroot 目录冲突
· params.editable 必须齐备 new_data_url / update_data_url / delete_data_url
且三个 URL 指向的 .dspy 文件真实存在 xls2ui 回退默认写入口
· browserfields/editexclouded 引用的字段必须存在于 models/{tblname}.json
退出码0 全通过1 存在 FAIL交付前必须实跑并在交付摘要引用输出
"""
import ast
import json
import os
import re
import sys
@ -29,6 +33,8 @@ ROOT = os.path.dirname(HERE) # 模块仓库根
PKG = os.path.join(ROOT, 'pbl_compiler')
WWW = os.path.join(ROOT, 'wwwroot')
API = os.path.join(WWW, 'api')
JSONDIR = os.path.join(ROOT, 'json')
MODELSDIR = os.path.join(ROOT, 'models')
failures = []
notes = []
@ -75,6 +81,14 @@ def dict_keys(node):
return set()
def literal_or_none(node):
"""AST 节点 → python 值;失败返回 None。"""
try:
return ast.literal_eval(node) if isinstance(node, ast.AST) else node
except Exception: # noqa: BLE001
return None
# ── ① init.py CONTRACTS ────────────────────────────────────────────────────
init_py = os.path.join(PKG, 'init.py')
CONTRACTS = set()
@ -116,51 +130,101 @@ else:
else:
ok('__init__.py __all__ 覆盖全部 %d 个契约' % len(CONTRACTS))
# ── ③ wwwroot/api/*.dspy ──────────────────────────────────────────────────
dspy_files = sorted(f for f in os.listdir(API) if f.endswith('.dspy')) \
if os.path.isdir(API) else []
DSPY_NAMES = {f[:-len('.dspy')] for f in dspy_files}
if DSPY_NAMES != CONTRACTS:
fail('wwwroot/api/ 端点与 CONTRACTS 不一致:缺端点=%s 多余端点=%s'
% (sorted(CONTRACTS - DSPY_NAMES), sorted(DSPY_NAMES - CONTRACTS)))
else:
ok('wwwroot/api/*.dspy = %d 个,与 CONTRACTS 一一对应' % len(DSPY_NAMES))
# ── ④ scripts/load_path.py PATHS ──────────────────────────────────────────
# ── ⑤ load_path.pyPATHS / PATHS_CONTRACTS / PATHS_READONLY_DENY ──────────
lp_py = os.path.join(HERE, 'load_path.py')
PATH_NAMES = set()
entries = []
entries, contract_entries, deny_entries = [], [], []
READONLY_DENY = set()
if not os.path.isfile(lp_py):
fail('缺 scripts/load_path.py')
else:
_, consts, _, _ = parse(lp_py)
raw = consts.get('PATHS')
if isinstance(raw, ast.List):
raw = [ast.literal_eval(e) for e in raw.elts]
if not raw:
fail('load_path.py PATHS 为空RBAC 零登记 → 全部 403')
for item in raw or []:
if not (isinstance(item, (list, tuple)) and len(item) == 2):
fail('load_path.py PATHS 元素必须是 (path, role) 二元组: %r' % (item,))
# 先把各列表常量解析成 python 值,再解析 PATHS = A + BName 需回查 consts
# 关键parse() 对「纯字面量列表」已 literal_eval 成 python list只有求值失败
# 的赋值才保留 AST。两种形态都必须收进 resolved —— 否则 PATHS = PATHS_CONTRACTS
# + PATHS_READONLY_DENY 的 BinOp 回查会拿到空列表,把已登记的 29 条误判成
# 「PATHS 为空RBAC 零登记)」,产生假 FAIL本轮排查出的审计工具自身缺陷
resolved = {}
for var, raw in consts.items():
if isinstance(raw, list): # parse() 已求值的纯字面量列表
resolved[var] = raw
elif isinstance(raw, ast.List): # 含函数引用等求值失败的列表 AST
try:
resolved[var] = [ast.literal_eval(e) for e in raw.elts]
except Exception: # noqa: BLE001
resolved[var] = []
for var, bucket in (('PATHS', entries),
('PATHS_CONTRACTS', contract_entries),
('PATHS_READONLY_DENY', deny_entries)):
raw = consts.get(var)
if raw is None:
fail('load_path.py 缺 %s 列表' % var)
continue
entries.append((item[0], item[1]))
PATH_NAMES = {p.rsplit('/', 1)[-1][:-len('.dspy')]
for p, _ in entries if '/api/' in p}
if PATH_NAMES != CONTRACTS:
fail('load_path.py PATHS 与 CONTRACTS 不一致:未登记=%s 幽灵登记=%s'
% (sorted(CONTRACTS - PATH_NAMES), sorted(PATH_NAMES - CONTRACTS)))
else:
ok('load_path.py PATHS 登记 %d 条,与 CONTRACTS 一一对应' % len(PATH_NAMES))
if isinstance(raw, ast.List):
raw = resolved.get(var, [])
elif isinstance(raw, ast.Name):
raw = resolved.get(raw.id, [])
elif isinstance(raw, ast.BinOp): # PATHS = PATHS_CONTRACTS + PATHS_READONLY_DENY
left = resolved.get(getattr(raw.left, 'id', None), []) if isinstance(raw.left, ast.Name) \
else (literal_or_none(raw.left) or [])
right = resolved.get(getattr(raw.right, 'id', None), []) if isinstance(raw.right, ast.Name) \
else (literal_or_none(raw.right) or [])
raw = list(left) + list(right)
for item in raw or []:
if not (isinstance(item, (list, tuple)) and len(item) == 2):
fail('load_path.py %s 元素必须是 (path, role) 二元组: %r' % (var, item))
continue
bucket.append((item[0], item[1]))
if not entries:
fail('load_path.py PATHS 为空RBAC 零登记 → 全部 403')
READONLY_DENY = {p.rsplit('/', 1)[-1][:-len('.dspy')]
for p, _ in deny_entries if '/api/' in p}
for p, _role in entries:
if '%' in p or '*' in p:
fail('load_path.py 禁用通配符: %s' % p)
if not any('%' in p or '*' in p for p, _ in entries):
ok('load_path.py 无通配符(%d 条全显式)' % len(entries))
n_api = len([1 for p, _ in entries if '/api/' in p])
dup = len(entries) - len({p for p, _ in entries})
if dup:
fail('load_path.py 存在重复登记 %d' % dup)
else:
ok('load_path.py 登记 %d 条无重复(契约 %d + 只读拒绝 %d'
% (len(entries), len(contract_entries), len(deny_entries)))
PATH_NAMES = {p.rsplit('/', 1)[-1][:-len('.dspy')]
for p, _ in entries if '/api/' in p}
CONTRACT_PATH_NAMES = {p.rsplit('/', 1)[-1][:-len('.dspy')]
for p, _ in contract_entries if '/api/' in p}
EXPECTED = CONTRACTS | READONLY_DENY
if CONTRACT_PATH_NAMES != CONTRACTS:
fail('PATHS_CONTRACTS 与 CONTRACTS 不一致:未登记=%s 幽灵登记=%s'
% (sorted(CONTRACTS - CONTRACT_PATH_NAMES),
sorted(CONTRACT_PATH_NAMES - CONTRACTS)))
else:
ok('PATHS_CONTRACTS 与 CONTRACTS 一一对应(%d 条)' % len(CONTRACTS))
if PATH_NAMES != EXPECTED:
fail('PATHS 与 CONTRACTSREADONLY_DENY 不一致:未登记=%s 幽灵登记=%s'
% (sorted(EXPECTED - PATH_NAMES), sorted(PATH_NAMES - EXPECTED)))
else:
ok('PATHS 登记 %d 条,与期望端点集合(契约 %d + 只读拒绝 %d)一一对应'
% (len(PATH_NAMES), len(CONTRACTS), len(READONLY_DENY)))
# ── ③ wwwroot/api/*.dspy ──────────────────────────────────────────────────
dspy_files = sorted(f for f in os.listdir(API) if f.endswith('.dspy')) \
if os.path.isdir(API) else []
DSPY_NAMES = {f[:-len('.dspy')] for f in dspy_files}
if DSPY_NAMES != EXPECTED:
fail('wwwroot/api/ 端点与期望集合不一致:缺端点=%s 多余端点=%s'
% (sorted(EXPECTED - DSPY_NAMES), sorted(DSPY_NAMES - EXPECTED)))
else:
ok('wwwroot/api/*.dspy = %d 个,与 CONTRACTSREADONLY_DENY 一一对应'
% len(DSPY_NAMES))
n_api = len([1 for p, _ in entries if '/api/' in p])
if n_api != len(dspy_files):
fail('端点数与 RBAC 登记数不等:%d != %d' % (n_api, len(dspy_files)))
else:
ok('grep 口径自证PATHS 中 /api/ 条目=%dwwwroot/api/*.dspy=%d'
% (n_api, len(dspy_files)))
if n_api != len(dspy_files):
fail('端点数与 RBAC 登记数不等:%d != %d' % (n_api, len(dspy_files)))
# ── .dspy 规范审计dspy-file-implementation-spec─────────────────────────
bad_import, no_return, env_req = [], [], []
@ -188,6 +252,22 @@ if env_req:
else:
ok('.dspy 无 ServerEnv() 取请求态')
# ── 只读拒绝端点必须真返回 Error防「名义拒绝、实际可写」─────────────────
deny_hollow = []
for name in sorted(READONLY_DENY):
p = os.path.join(API, name + '.dspy')
if not os.path.isfile(p):
deny_hollow.append('%s: 文件缺失' % name)
continue
text = open(p, 'r', encoding='utf-8').read()
if 'widgettype' not in text or '"Error"' not in text and "'Error'" not in text:
deny_hollow.append('%s: 未返回 Error widget' % name)
if deny_hollow:
fail('只读拒绝端点空心化(未显式拒绝写入): %s' % deny_hollow)
else:
ok('%d 个只读拒绝端点均显式返回 Error widgetCRUD 写入口已封死)'
% len(READONLY_DENY))
# ── CRUD 生成目录登记面(防「生成目录未登记 → 403」───────────────────────
SKIP_DIRS = {'api', 'i18n', 'imgs', 'styles', 'scripts', 'bricks'}
crud_dirs = [d for d in sorted(os.listdir(WWW))
@ -204,11 +284,93 @@ for alias in crud_dirs:
if crud_dirs:
ok('发现 CRUD 生成目录 %d 个,已逐文件核对登记' % len(crud_dirs))
else:
ok('wwwroot/ 无 CRUD 生成子目录(只读列表走手写契约 api/pbl_script_rule_list.dspy'
'无需登记 CRUD 6 条路径')
ok('wwwroot/ 无 CRUD 生成子目录(只读列表走手写契约 api/pbl_script_rule_list.dspy')
# ── json/*.json 结构审计QC #3 + Pitfall 10改一个必须全扫──────────────
ROOT_ALLOWED = {'tblname', 'alias', 'title', 'params', '_comment'}
json_files = sorted(f for f in os.listdir(JSONDIR) if f.endswith('.json')) \
if os.path.isdir(JSONDIR) else []
if not json_files:
fail('json/ 目录为空CRUD 定义缺失)')
root_key_bad, alias_bad, editable_bad, url_missing, field_ghost = [], [], [], [], []
for fn in json_files:
path = os.path.join(JSONDIR, fn)
try:
d = json.load(open(path, 'r', encoding='utf-8'))
except Exception as exc: # noqa: BLE001
fail('json/%s 不是合法 JSON: %s' % (fn, exc))
continue
rk = set(d.keys())
if not rk <= ROOT_ALLOWED:
root_key_bad.append('%s: 非法根键 %s' % (fn, sorted(rk - ROOT_ALLOWED)))
continue
if 'browserfields' in rk:
root_key_bad.append('%s: 根级 browserfields规范只允许 params.browserfields' % fn)
continue
alias = d.get('alias')
if alias == 'pbl_compiler':
alias_bad.append('%s: alias 与模块同名,生成目录会与模块 wwwroot 冲突' % fn)
params = d.get('params') or {}
if 'browserfields' not in params:
root_key_bad.append('%s: 缺 params.browserfields' % fn)
editable = params.get('editable')
if not isinstance(editable, dict):
editable_bad.append('%s: params.editable 不是对象(%r' % (fn, type(editable).__name__))
continue
need = ('new_data_url', 'update_data_url', 'delete_data_url')
absent = [k for k in need if k not in editable]
if absent:
editable_bad.append('%s: editable 缺 %s' % (fn, absent))
for k in need:
v = editable.get(k)
if v is None:
continue
if not isinstance(v, dict) or 'url' not in v:
editable_bad.append('%s: editable.%s 必须是含 url 的对象' % (fn, k))
continue
m = re.search(r"entire_url\('([^']+)'\)", v['url'])
if not m:
editable_bad.append('%s: editable.%s.url 未用 entire_url()' % (fn, k))
continue
ref = m.group(1)
base = os.path.basename(ref)
if base.startswith('/'):
base = base.lstrip('/')
if not (os.path.isfile(os.path.join(API, base))
or os.path.isfile(os.path.join(ROOT, 'wwwroot', base))):
url_missing.append('%s: editable.%s%s 无匹配 .dspy' % (fn, k, ref))
gd = editable.get('get_data_url')
if isinstance(gd, dict) and gd.get('url'):
m = re.search(r"entire_url\('([^']+)'\)", gd['url'])
if m and not os.path.isfile(os.path.join(API, os.path.basename(m.group(1)))):
url_missing.append('%s: get_data_url → %s 无匹配 .dspy' % (fn, m.group(1)))
# 字段幽灵检查browserfields 引用的字段必须在 models/{tblname}.json 存在
mp = os.path.join(MODELSDIR, (d.get('tblname') or '') + '.json')
if os.path.isfile(mp):
cols = {f.get('name') for f in json.load(open(mp, 'r', encoding='utf-8')).get('fields', [])}
for scope in ('browserfields', 'editexclouded'):
bf = params.get(scope)
names = list(bf.keys()) if isinstance(bf, dict) and scope == 'browserfields' \
else (bf or [])
for name in names:
if name not in cols:
field_ghost.append('%s: %s 引用 models 中不存在的字段 %s'
% (fn, scope, name))
else:
field_ghost.append('%s: 缺 models/%s.json' % (fn, d.get('tblname')))
for lst, msg in ((root_key_bad, 'json/ 根键不合规'),
(alias_bad, 'json/ alias 不合规'),
(editable_bad, 'json/ editable 段不合规'),
(url_missing, 'json/ editable URL 无匹配 .dspy'),
(field_ghost, 'json/ 字段与 models 不一致')):
if lst:
fail('%s: %s' % (msg, lst))
if not (root_key_bad or alias_bad or editable_bad or url_missing or field_ghost):
ok('json/*.json 共 %d 个:根键白名单(%s) / alias / editable 三 URL 齐备且指向真实 .dspy '
'/ 字段与 models 全对齐' % (len(json_files), sorted(ROOT_ALLOWED)))
# ── 输出 ───────────────────────────────────────────────────────────────────
print('=== pbl_compiler 接线/RBAC parity 审计 ===')
print('=== pbl_compiler 接线/RBAC/CRUD parity 审计 ===')
for line in notes:
print(' ' + line)
if failures:
@ -217,6 +379,7 @@ if failures:
print(' ' + line)
print('RESULT: FAIL')
sys.exit(1)
print('RESULT: PASS (contracts=%d, dspy=%d, rbac_api_paths=%d)'
% (len(CONTRACTS), len(dspy_files), len(PATH_NAMES)))
print('RESULT: PASS (contracts=%d, readonly_deny=%d, dspy=%d, rbac_api_paths=%d, json=%d)'
% (len(CONTRACTS), len(READONLY_DENY), len(dspy_files), len(PATH_NAMES),
len(json_files)))
sys.exit(0)

View File

@ -1,6 +1,6 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""pbl_compiler RBAC 路径注册(硬门禁 6.6 / QC #1、#11)。
"""pbl_compiler RBAC 路径注册(硬门禁 6.6 / QC 第四章 RBAC)。
约定
- 路径 = 模块自动路由 `/pbl_compiler/api/<契约>.dspy`不带端口不带 /wss 前缀
@ -8,30 +8,32 @@
- apps/pbls/build.sh 8 步调用 `register()`rbac CLI 不在位时打印清单不静默跳过
- **禁止通配符**规范硬规定每条 .dspy 显式登记
登记面与事实源四处必须一一对应数量一致
实现pbl_compiler/api.py15 M3a 契约+ pbl_compiler/rules_export_api.py5 M3b 契约
导出pbl_compiler/__init__.py import + __all__
注册pbl_compiler/init.py CONTRACTS 20
端点wwwroot/api/<契约名>.dspy 20 本文件 PATHS 20
两个登记集合合计 29 全部显式
PATHS_CONTRACTS 20 Web 契约端点与四处事实源一一对应
实现pbl_compiler/api.py15 M3a 契约+ pbl_compiler/rules_export_api.py5 M3b 契约
导出pbl_compiler/__init__.py import + __all__
注册pbl_compiler/init.py CONTRACTS 20
端点wwwroot/api/<契约名>.dspy 20
PATHS_READONLY_DENY 9 CRUD 只读拒绝端点QC #2 方案 A
json/*.json 三张表pbl_game_definition / pbl_capability_registry /
pbl_compiler_version都是编译产物或需经专用契约写入的注册表
crud-definition-spec 要求 editable 必须齐备 new_/update_/delete_data_url
否则 xls2ui 回退生成默认 add_/update_/delete_<table>.dspy 等于绕过
gd_rules_writer / pbl_capability_register / pbl_compiler_version_register
唯一写入口rules_hash29.6 确定性指纹可被手工改写
故为 3 张表 × 3 个写操作各建一个显式返回 Error .dspy 9
它们不是 ServerEnv 契约不进 CONTRACTS/__init__.py只需 RBAC 登记
QC #1 整改(本轮,第四章 RBAC 硬门禁):
上一轮新增 5 M3b Web 契约pbl_compiler_export_rules / pbl_compiler_rules_preview /
pbl_compiler_rules_diff / pbl_game_definition_rules_get / pbl_script_rule_list
wwwroot/api/ 已有 20 .dspy但本文件 PATHS 仍只有 15 新接口未入 permission
登录后调用一律 403M3b 规则映射/导出能力实际不可用现逐条显式补齐至 20
关于 CRUD 生成目录/pbl_compiler/<alias> index.ui/get_/add_/update_/delete_ 五条
本轮 json/ 三个定义均为只读列表无新增/编辑表单入口wwwroot/ 下当前无 CRUD 生成子目录
build.sh 重跑 xls2ui 后若生成 wwwroot/<alias>/ 目录 index.ui get_<alias>.dspy
必须按 scripts/audit_rbac_parity.py CRUD 登记面检查逐条补登禁通配
在此之前不登记不存在的路径幽灵登记会污染 permission 表并被审计判 FAIL
关于 CRUD 生成目录/pbl_compiler/pbl_script_rule index.ui/get_/add_/update_/delete_ 五条
本模块 **** 引入任何 pbl_script_rule CRUD 定义json/ 下仅
compiler_pbl_capability_registry / compiler_pbl_compiler_version /
compiler_pbl_game_definition 三个只读列表定义 wwwroot/ 下无 CRUD 生成子目录
index.ui + api/pbl_script_rule 的只读列表能力由手写契约
`api/pbl_script_rule_list.dspy` 提供故此处不登记不存在的 CRUD 路径
登记不存在的路径会污染 permission scripts/audit_rbac_parity.py 会判为 FAIL
自证交付前实跑 scripts/audit_rbac_parity.py
grep -c "api/" scripts/load_path.py 20
ls wwwroot/api/*.dspy | wc -l 20
两者集合差为空无漏登记无幽灵登记
自证交付前实跑 scripts/audit_rbac_parity.py
ls wwwroot/api/*.dspy | wc -l 29
PATHS /api/ 条目数 29契约 20 + 只读拒绝 9
集合差为空无漏登记无幽灵登记
"""
import os
import subprocess
@ -39,8 +41,8 @@ import sys
MODULE = 'pbl_compiler'
# (path, role) —— 与 init.py CONTRACTS 一一对应20 个端点,无通配符)
PATHS = [
# ── ① Web 契约端点20 条,与 init.py CONTRACTS 一一对应,无通配符)──────────
PATHS_CONTRACTS = [
# —— M3a编译主流程 / 预览 / 对比3——
('/pbl_compiler/api/pbl_compiler_compile.dspy', 'logined'),
('/pbl_compiler/api/pbl_compiler_preview.dspy', 'logined'),
@ -62,7 +64,7 @@ PATHS = [
# —— M3a能力注册表2——
('/pbl_compiler/api/pbl_capability_list.dspy', 'logined'),
('/pbl_compiler/api/pbl_capability_register.dspy', 'logined'),
# —— M3b第12章 event→condition→response 规则映射与导出5QC #1 本轮补齐)——
# —— M3b第12章 event→condition→response 规则映射与导出5)——
('/pbl_compiler/api/pbl_compiler_export_rules.dspy', 'logined'),
('/pbl_compiler/api/pbl_compiler_rules_preview.dspy', 'logined'),
('/pbl_compiler/api/pbl_compiler_rules_diff.dspy', 'logined'),
@ -70,12 +72,42 @@ PATHS = [
('/pbl_compiler/api/pbl_script_rule_list.dspy', 'logined'),
]
# ── ② CRUD 只读拒绝端点9 条,非契约,仅封死 xls2ui 默认写入口QC #2 方案 A──
PATHS_READONLY_DENY = [
# pbl_game_definition编译产物表唯一写入口 gd_rules_writer.write_gd_rules()
('/pbl_compiler/api/pbl_game_definition_create.dspy', 'logined'),
('/pbl_compiler/api/pbl_game_definition_update.dspy', 'logined'),
('/pbl_compiler/api/pbl_game_definition_delete.dspy', 'logined'),
# pbl_capability_registry唯一写入口契约 pbl_capability_register
('/pbl_compiler/api/pbl_capability_registry_create.dspy', 'logined'),
('/pbl_compiler/api/pbl_capability_registry_update.dspy', 'logined'),
('/pbl_compiler/api/pbl_capability_registry_delete.dspy', 'logined'),
# pbl_compiler_version唯一写入口契约 pbl_compiler_version_register
('/pbl_compiler/api/pbl_compiler_version_create.dspy', 'logined'),
('/pbl_compiler/api/pbl_compiler_version_update.dspy', 'logined'),
('/pbl_compiler/api/pbl_compiler_version_delete.dspy', 'logined'),
]
# 完整登记面audit_rbac_parity.py 以本变量为事实源)
PATHS = PATHS_CONTRACTS + PATHS_READONLY_DENY
def registered_api_paths():
"""返回 PATHS 中登记的 /api/ 端点文件名集合(供 parity 审计复用)。"""
return {p.rsplit('/', 1)[-1] for p, _role in PATHS if '/api/' in p}
def registered_contract_paths():
"""返回 Web 契约端点文件名集合(与 init.py CONTRACTS 比对的口径)。"""
return {p.rsplit('/', 1)[-1] for p, _role in PATHS_CONTRACTS if '/api/' in p}
def registered_readonly_deny_paths():
"""返回只读拒绝端点文件名集合非契约QC #2 方案 A"""
return {p.rsplit('/', 1)[-1] for p, _role in PATHS_READONLY_DENY
if '/api/' in p}
def register():
"""逐条注册 RBAC 路径。rbac CLI 不在位时收集为 pending 并打印(不静默跳过)。
@ -96,8 +128,9 @@ def register():
else:
missing.append((path, role))
# 4 个占位符 ↔ 4 个实参QC #3 修复保持)
print('[%s] rbac paths: total=%d ok=%d pending=%d'
% (MODULE, len(PATHS), done, len(missing)))
print('[%s] rbac paths: total=%d (contracts=%d, readonly_deny=%d) ok=%d pending=%d'
% (MODULE, len(PATHS), len(PATHS_CONTRACTS),
len(PATHS_READONLY_DENY), done, len(missing)))
for path, role in missing:
print(' PENDING %-12s %s' % (role, path))
return len(missing) == 0

View File

@ -0,0 +1,13 @@
# pbl_capability_registry_create.dspy —— 显式拒绝 CRUD 直写M3b QC #2/#3 格式统一)。
# pbl_capability_registry 的唯一写入契约是 pbl_capability_register.dspy
# (内部做 capability_key 规范化、args_schema JSON 校验与 org/tenant 归属校验)。
# 本端点只为满足 CRUD editable 三 URL 齐备的格式要求,并封死绕过校验的默认 add_ 路径。
debug('pbl_capability_registry_create.dspy: REJECTED direct write params=%s' % repr(params_kw))
return {
"status": "fail",
"message": "请使用契约 pbl_capability_register 注册能力,禁止 CRUD 直写",
"data": {
"widgettype": "Error",
"options": {"label": "请使用契约 pbl_capability_register 注册能力,禁止 CRUD 直写"},
},
}

View File

@ -0,0 +1,12 @@
# pbl_capability_registry_delete.dspy —— 显式拒绝 CRUD 直删M3b QC #2/#3 格式统一)。
# 已注册能力可能被历史蓝图/已编译 Game Definition 引用,物理删除会造成产物不可复现;
# 下线请用 pbl_capability_register 置 is_enabled=0软下线保留引用完整性
debug('pbl_capability_registry_delete.dspy: REJECTED direct delete params=%s' % repr(params_kw))
return {
"status": "fail",
"message": "能力注册表禁止物理删除,请置 is_enabled=0 软下线",
"data": {
"widgettype": "Error",
"options": {"label": "能力注册表禁止物理删除,请置 is_enabled=0 软下线"},
},
}

View File

@ -0,0 +1,12 @@
# pbl_capability_registry_update.dspy —— 显式拒绝 CRUD 直写M3b QC #2/#3 格式统一)。
# 能力定义变更必须经契约 pbl_capability_registerupsert 语义 + schema 校验 + 审计),
# 手工 update 会让编译器读到未校验的 args_schema破坏确定性编译。
debug('pbl_capability_registry_update.dspy: REJECTED direct write params=%s' % repr(params_kw))
return {
"status": "fail",
"message": "请使用契约 pbl_capability_register 更新能力,禁止 CRUD 直写",
"data": {
"widgettype": "Error",
"options": {"label": "请使用契约 pbl_capability_register 更新能力,禁止 CRUD 直写"},
},
}

View File

@ -0,0 +1,12 @@
# pbl_compiler_version_create.dspy —— 显式拒绝 CRUD 直写M3b QC #2/#3 格式统一)。
# 编译器版本行的唯一写入契约是 pbl_compiler_version_register.dspy
# (内部计算 rules_hash 并做 semver 唯一性校验)。手工新增会产生无指纹的版本行。
debug('pbl_compiler_version_create.dspy: REJECTED direct write params=%s' % repr(params_kw))
return {
"status": "fail",
"message": "请使用契约 pbl_compiler_version_register 登记版本,禁止 CRUD 直写",
"data": {
"widgettype": "Error",
"options": {"label": "请使用契约 pbl_compiler_version_register 登记版本,禁止 CRUD 直写"},
},
}

View File

@ -0,0 +1,12 @@
# pbl_compiler_version_delete.dspy —— 显式拒绝 CRUD 直删M3b QC #2/#3 格式统一)。
# 版本行是编译产物溯源链的一环pbl_game_definition.compiler_version_id 引用它),
# 物理删除会产生悬空引用;停用请用契约 pbl_compiler_version_register 置 enabled=0。
debug('pbl_compiler_version_delete.dspy: REJECTED direct delete params=%s' % repr(params_kw))
return {
"status": "fail",
"message": "编译器版本行禁止物理删除,请置 enabled=0 停用",
"data": {
"widgettype": "Error",
"options": {"label": "编译器版本行禁止物理删除,请置 enabled=0 停用"},
},
}

View File

@ -0,0 +1,12 @@
# pbl_compiler_version_update.dspy —— 显式拒绝 CRUD 直写M3b QC #2/#3 格式统一)。
# 版本行一经登记即为不可变基线(版本 diff / 确定性验证以其 rules_hash 为准),
# 变更只能登记新版本行,禁止原地 update 语义化版本或指纹字段。
debug('pbl_compiler_version_update.dspy: REJECTED direct write params=%s' % repr(params_kw))
return {
"status": "fail",
"message": "编译器版本行不可变,请登记新版本(契约 pbl_compiler_version_register",
"data": {
"widgettype": "Error",
"options": {"label": "编译器版本行不可变,请登记新版本(契约 pbl_compiler_version_register"},
},
}

View File

@ -0,0 +1,13 @@
# pbl_game_definition_create.dspy —— 编译产物只读显式拒绝手工新增M3b QC #2 方案 A
# 唯一写入入口pbl_compiler.gd_rules_writer.write_gd_rules()(由 pbl_compiler_compile 主流程调用)。
# 本端点存在的意义:让 xls2ui 不再回退生成默认 add_pbl_game_definition.dspy
# 从而杜绝绕过 gd_rules_writer / rules_hash(29.6 确定性指纹) 的手工改写路径。
debug('pbl_game_definition_create.dspy: REJECTED write attempt params=%s' % repr(params_kw))
return {
"status": "fail",
"message": "编译产物只读,禁止手工增删改",
"data": {
"widgettype": "Error",
"options": {"label": "编译产物只读,禁止手工增删改"},
},
}

View File

@ -0,0 +1,12 @@
# pbl_game_definition_delete.dspy —— 编译产物只读显式拒绝手工删除M3b QC #2 方案 A
# 历史编译产物是版本 diffpbl_compiler_version_diff / pbl_compiler_rules_diff的比对基线
# 删除即丢失可追溯性;清理只能由运维侧走带审计的迁移脚本,不走 CRUD。
debug('pbl_game_definition_delete.dspy: REJECTED write attempt params=%s' % repr(params_kw))
return {
"status": "fail",
"message": "编译产物只读,禁止手工增删改",
"data": {
"widgettype": "Error",
"options": {"label": "编译产物只读,禁止手工增删改"},
},
}

View File

@ -0,0 +1,12 @@
# pbl_game_definition_update.dspy —— 编译产物只读显式拒绝手工更新M3b QC #2 方案 A
# rules_json / rules_hash / rule_count / rules_manifest 四件套只能由 gd_rules_writer 写入,
# 手工 update 会破坏 29.6 确定性指纹与版本可比性,故一律返回 Error。
debug('pbl_game_definition_update.dspy: REJECTED write attempt params=%s' % repr(params_kw))
return {
"status": "fail",
"message": "编译产物只读,禁止手工增删改",
"data": {
"widgettype": "Error",
"options": {"label": "编译产物只读,禁止手工增删改"},
},
}