diff --git a/json/compiler_pbl_capability_registry.json b/json/compiler_pbl_capability_registry.json index d8616b2..ab5c40d 100644 --- a/json/compiler_pbl_capability_registry.json +++ b/json/compiler_pbl_capability_registry.json @@ -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/params;alias 由 pbl_compiler(与模块同名、生成目录会与模块 wwwroot 冲突)改为 pbl_capability_registry_list;params.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 } diff --git a/json/compiler_pbl_compiler_version.json b/json/compiler_pbl_compiler_version.json index cb868f9..f3f60b5 100644 --- a/json/compiler_pbl_compiler_version.json +++ b/json/compiler_pbl_compiler_version.json @@ -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/params;alias 由 pbl_compiler(与模块同名、生成目录会与模块 wwwroot 冲突)改为 pbl_compiler_version_list;params.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 } diff --git a/json/compiler_pbl_game_definition.json b/json/compiler_pbl_game_definition.json index 35f6919..47abe10 100644 --- a/json/compiler_pbl_game_definition.json +++ b/json/compiler_pbl_game_definition.json @@ -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.browserfields),alias 由 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 } -} \ No newline at end of file +} diff --git a/pbl_compiler/init.py b/pbl_compiler/init.py index 48a1e7b..2c7cca1 100644 --- a/pbl_compiler/init.py +++ b/pbl_compiler/init.py @@ -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 PATHS(RBAC 逐条显式登记,共 20 条 + 只读列表 CRUD 6 条) + · scripts/load_path.py PATHS(RBAC 逐条显式登记,共 29 条 = Web 契约端点 20 + CRUD 只读拒绝端点 9,无通配符) · __init__.py import + __all__ """ from ahserver.serverenv import ServerEnv diff --git a/scripts/audit_rbac_parity.py b/scripts/audit_rbac_parity.py index d7f0213..3d43ad1 100644 --- a/scripts/audit_rbac_parity.py +++ b/scripts/audit_rbac_parity.py @@ -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_DENY(CRUD 只读拒绝端点,非契约) + 另加 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.py:PATHS / 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 + B(Name 需回查 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 与 CONTRACTS∪READONLY_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 个,与 CONTRACTS∪READONLY_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/ 条目=%d,wwwroot/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 widget(CRUD 写入口已封死)' + % 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) diff --git a/scripts/load_path.py b/scripts/load_path.py index dbf9a4c..1162b6c 100644 --- a/scripts/load_path.py +++ b/scripts/load_path.py @@ -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.py(15 个 M3a 契约)+ pbl_compiler/rules_export_api.py(5 个 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.py(15 个 M3a 契约)+ pbl_compiler/rules_export_api.py(5 个 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_.dspy —— 等于绕过 + gd_rules_writer / pbl_capability_register / pbl_compiler_version_register + 唯一写入口,rules_hash(29.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 表, -登录后调用一律 403,M3b 规则映射/导出能力实际不可用。现逐条显式补齐至 20 条。 +关于 CRUD 生成目录(/pbl_compiler/ 及 index.ui/get_/add_/update_/delete_ 五条): +本轮 json/ 三个定义均为只读列表(无新增/编辑表单入口),wwwroot/ 下当前无 CRUD 生成子目录。 +build.sh 重跑 xls2ui 后若生成 wwwroot// 目录,其 index.ui 与 get_.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 规则映射与导出(5,QC #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 diff --git a/wwwroot/api/pbl_capability_registry_create.dspy b/wwwroot/api/pbl_capability_registry_create.dspy new file mode 100644 index 0000000..98e2e31 --- /dev/null +++ b/wwwroot/api/pbl_capability_registry_create.dspy @@ -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 直写"}, + }, +} diff --git a/wwwroot/api/pbl_capability_registry_delete.dspy b/wwwroot/api/pbl_capability_registry_delete.dspy new file mode 100644 index 0000000..ad81239 --- /dev/null +++ b/wwwroot/api/pbl_capability_registry_delete.dspy @@ -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 软下线"}, + }, +} diff --git a/wwwroot/api/pbl_capability_registry_update.dspy b/wwwroot/api/pbl_capability_registry_update.dspy new file mode 100644 index 0000000..2c9e257 --- /dev/null +++ b/wwwroot/api/pbl_capability_registry_update.dspy @@ -0,0 +1,12 @@ +# pbl_capability_registry_update.dspy —— 显式拒绝 CRUD 直写(M3b QC #2/#3 格式统一)。 +# 能力定义变更必须经契约 pbl_capability_register(upsert 语义 + 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 直写"}, + }, +} diff --git a/wwwroot/api/pbl_compiler_version_create.dspy b/wwwroot/api/pbl_compiler_version_create.dspy new file mode 100644 index 0000000..f2002f5 --- /dev/null +++ b/wwwroot/api/pbl_compiler_version_create.dspy @@ -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 直写"}, + }, +} diff --git a/wwwroot/api/pbl_compiler_version_delete.dspy b/wwwroot/api/pbl_compiler_version_delete.dspy new file mode 100644 index 0000000..3bf65fb --- /dev/null +++ b/wwwroot/api/pbl_compiler_version_delete.dspy @@ -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 停用"}, + }, +} diff --git a/wwwroot/api/pbl_compiler_version_update.dspy b/wwwroot/api/pbl_compiler_version_update.dspy new file mode 100644 index 0000000..c339e1c --- /dev/null +++ b/wwwroot/api/pbl_compiler_version_update.dspy @@ -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)"}, + }, +} diff --git a/wwwroot/api/pbl_game_definition_create.dspy b/wwwroot/api/pbl_game_definition_create.dspy new file mode 100644 index 0000000..672cef2 --- /dev/null +++ b/wwwroot/api/pbl_game_definition_create.dspy @@ -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": "编译产物只读,禁止手工增删改"}, + }, +} diff --git a/wwwroot/api/pbl_game_definition_delete.dspy b/wwwroot/api/pbl_game_definition_delete.dspy new file mode 100644 index 0000000..ef96d5e --- /dev/null +++ b/wwwroot/api/pbl_game_definition_delete.dspy @@ -0,0 +1,12 @@ +# pbl_game_definition_delete.dspy —— 编译产物只读:显式拒绝手工删除(M3b QC #2 方案 A)。 +# 历史编译产物是版本 diff(pbl_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": "编译产物只读,禁止手工增删改"}, + }, +} diff --git a/wwwroot/api/pbl_game_definition_update.dspy b/wwwroot/api/pbl_game_definition_update.dspy new file mode 100644 index 0000000..98b7423 --- /dev/null +++ b/wwwroot/api/pbl_game_definition_update.dspy @@ -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": "编译产物只读,禁止手工增删改"}, + }, +}