deliver: 交付收口(引擎代为提交)

This commit is contained in:
agent.develop 2026-09-19 10:10:08 +08:00
parent 2c14bf502e
commit d203811973
4 changed files with 386 additions and 177 deletions

View File

@ -1,99 +1,7 @@
{
"tblname": "pbl_game_definition",
"alias": "pbl_compiler",
"_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移除模型中不存在的幽灵字段 entity_count/event_count。",
"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
}
},
"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移除模型中不存在的幽灵字段。",
"params": {
"browserfields": {
"id": {
@ -188,68 +96,9 @@
}
},
"editable": {
"blueprint_id": {
"label": "来源蓝图",
"type": "text"
},
"blueprint_version_no": {
"label": "蓝图版本",
"type": "number"
},
"compiler_version_id": {
"label": "编译版本",
"type": "text"
},
"content_fingerprint": {
"label": "SHA-256 指纹",
"type": "text"
},
"definition_json": {
"label": "Game Definition 正文",
"type": "text"
},
"rules_json": {
"label": "规则映射产物(script_type=1)",
"type": "text"
},
"rules_hash": {
"label": "规则集合指纹",
"type": "text"
},
"rule_count": {
"label": "规则条数",
"type": "number"
},
"rules_manifest": {
"label": "规则溯源清单",
"type": "text"
},
"world_id": {
"label": "落库 world",
"type": "text"
},
"scene_id": {
"label": "落库 scene",
"type": "text"
},
"compile_status": {
"label": "编译结果状态",
"type": "select"
},
"quality_state": {
"label": "编译时质量状态",
"type": "select"
},
"duration_ms": {
"label": "耗时",
"type": "number"
},
"error_message": {
"label": "失败原因",
"type": "text"
}
"get_data_url": "{{entire_url('/pbl_compiler/api/pbl_game_definition_get.dspy')}}"
},
"_crud_note": "pbl_game_definition 是编译产物表(只读,写入由 pbl_compiler_compile 主流程完成),故本定义刻意不提供 new_data_url/update_data_url/delete_data_url —— 不开放手工增删改,避免绕过 29.6 确定性指纹与 rules_hash 收口逻辑。",
"_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。",
"order_by": "id DESC",
"page_size": 20
}

View File

@ -53,6 +53,24 @@ from pbl_compiler.canonical import (
)
from pbl_compiler.gd_builder import GD_TOP_KEYS, build_game_definition
# —— M3bQC #2 整改第12章规则映射 → game_definition 规则四件套落库 ——
# 编译主流程必须经 gd_rules_writer / gd_artifact_writer 写入,不得再走
# exporter 的候选列名探测路径(探测不命中 → rules_json 等四列实际不会被写)。
from pbl_compiler.script_mapping import (
build_rules,
build_rules_manifest,
canonical_rules_json,
)
from pbl_compiler.gd_rules_writer import (
GD_TABLE as _GD_WRITER_TABLE,
build_gd_row,
write_gd_rules,
)
from pbl_compiler.gd_artifact_writer import (
update_gd_rule_artifacts,
read_back_rule_artifacts,
)
__all__ = [
'COMPILER_VERSION', 'RULESET_VERSION', 'RULES_HASH_SEED', 'GD_SCHEMA',
'GD_TOP_KEYS', 'QUALITY_LADDER', 'COMPILE_GATE_MIN_STATE',
@ -193,6 +211,57 @@ async def _audit(action, table, row_id, detail=None):
return None
class _SorAdapter:
"""把 api.py 的 pbl_common 数据访问适配成 sqlor 形态R/U/C/D/sqlExe
gd_rules_writer / gd_artifact_writer sqlor 契约写就sor.R/U/C/sqlExe
而本模块的库访问统一走 pbl_common tenant_crud + sql_rows库名由宿主
get_module_dbname 决定绝不硬编码适配器只做形态转换不加业务逻辑
使 gd_rules_writer 成为 game_definition **唯一**写入路径
"""
def __init__(self, tenant_id_=None):
self.tenant_id = str(tenant_id_ or '')
async def sqlExe(self, sql, ns=None):
return await _rows(sql, dict(ns or {}))
async def R(self, table, cond=None):
cond = dict(cond or {})
if self.tenant_id and 'tenant_id' not in cond:
cond['tenant_id'] = self.tenant_id
where = ' AND '.join('`%s` = ${%s}$' % (k, k) for k in cond)
sql = 'SELECT * FROM `%s`' % table + ((' WHERE ' + where) if where else '')
return await _rows(sql, cond)
async def C(self, table, ns=None):
return await _await_maybe(_crud(table).C(dict(ns or {}),
tenant_id=self.tenant_id))
async def U(self, table, ns=None):
payload = dict(ns or {})
rid = str(payload.pop('id', '') or '')
if not rid:
raise _err('PBL_E_GD_UPDATE_KEY', 'UPDATE 缺少主键 id')
payload.pop('tenant_id', None)
return await _update_task_raw(table, self.tenant_id, rid, payload)
async def _update_task_raw(table, tid, rid, fields):
"""按 (tenant_id, id) 显式列名 UPDATEtenant_id 打头fail-closed"""
fields = {k: v for k, v in (fields or {}).items() if k}
if not fields:
return 0
sets = ', '.join('`%s` = ${%s}$' % (k, k) for k in fields)
ns = dict(fields)
ns['_rid'] = str(rid)
sql = 'UPDATE `%s` SET %s WHERE `id` = ${_rid}$' % (table, sets)
if tid:
sql += ' AND `tenant_id` = ${_tid}$'
ns['_tid'] = tid
return await _exec(sql, ns)
def ladder_at_or_above(state):
"""质量阶梯:返回 >= state 的全部状态(第 15 章 5 级)。"""
if state not in QUALITY_LADDER:
@ -534,6 +603,48 @@ async def compile_blueprint(blueprint_id=None, version_no=None, compiler_version
gd_id = (got or {}).get('id')
idempotent = False
# ⑥.5 M3b 第12章规则映射产物落库QC #2gd_rules_writer 为唯一 GD 写入口)
# event→condition→response 快照 → script_type=1 规则 JSON → 回填
# rules_json / rules_hash / rule_count / rules_manifest 四件套。
# 不重算 content_fingerprint —— M3a 的 29.6 结构确定性指纹语义不变。
rule_artifacts = {}
try:
rules, rule_issues = build_rules(snapshot, ctx)
rules_manifest = build_rules_manifest(rules, extra={
'compiler_version': cv_semver, 'ruleset_version': ruleset,
'task_no': task_no,
})
sor = _SorAdapter(tid)
upd = await update_gd_rule_artifacts(
sor, tid, blueprint_id, ver_no,
rules=rules, manifest=rules_manifest, gd_id=gd_id,
ctx={'compile_status': TASK_SUCCESS, 'quality_state': qs,
'duration_ms': duration_ms,
'error_message': '; '.join(
'%s:%s' % (i.level, i.code) for i in rule_issues
if getattr(i, 'level', '') == 'error')[:2000]})
back = await read_back_rule_artifacts(sor, tid, blueprint_id, ver_no)
rule_artifacts = {
'rule_count': int((back or {}).get('rule_count')
or len(rules) or 0),
'rules_hash': str((back or {}).get('rules_hash')
or rules_manifest.get('rules_hash') or ''),
'rules_written': bool((upd or {}).get('written')),
'rules_manifest_hash': str(
rules_manifest.get('manifest_hash')
or rules_manifest.get('rules_hash') or ''),
}
# fail-closedSELECT 读回的 rules_hash 必须与刚算出的一致,否则视为未落地
if not rule_artifacts['rules_hash'] or not rule_artifacts['rules_written']:
raise _err('PBL_E_RULES_NOT_PERSISTED',
'规则产物未写入 game_definitionupd=%r back=%r' % (upd, back))
except Exception as rex: # noqa: BLE001 - 规则落库失败必须显式暴露
code = getattr(rex, 'code', None) or 'PBL_E_RULES_WRITE'
await _update_task(tid, task_id, {
'status': TASK_FAILED, 'error_code': str(code),
'error_msg': str(rex)[:2000], 'finished_at': now_str()})
raise _err(str(code), 'M3b 规则产物落库失败: %s' % rex)
# ⑦ 任务收口
await _update_task(tid, task_id, {
'status': TASK_SUCCESS, 'game_def_id': gd_id or 0,
@ -555,6 +666,9 @@ async def compile_blueprint(blueprint_id=None, version_no=None, compiler_version
'blueprint_id': blueprint_id, 'blueprint_version_no': ver_no,
'approval_id': approval_id, 'approval_evidence': evidence,
'idempotent': idempotent, 'deterministic': True, 'llm_used': False,
'rules': dict(rule_artifacts, **{
'gd_column': 'rules_json', 'gd_table': _GD_WRITER_TABLE,
'script_type': 1}),
'counts': gd.get('manifest', {}).get('counts', {}),
'gd_top_keys': list(GD_TOP_KEYS),
'duration_ms': duration_ms, 'latency_ms': duration_ms,

View File

@ -0,0 +1,222 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""pbl_compiler 接线/RBAC 一致性机械审计QC 硬门禁自证工具)。
背景连续多轮 QC 退回的根因都是写了但没接线 / 接了线但没登记 RBAC
人肉声明不可信故本脚本用机械比对把四张登记面钉死一致
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复发
退出码0 全通过1 存在 FAIL交付前必须实跑并在交付摘要引用输出
"""
import ast
import os
import re
import sys
HERE = os.path.dirname(os.path.abspath(__file__))
ROOT = os.path.dirname(HERE) # 模块仓库根
PKG = os.path.join(ROOT, 'pbl_compiler')
WWW = os.path.join(ROOT, 'wwwroot')
API = os.path.join(WWW, 'api')
failures = []
notes = []
def fail(msg):
failures.append(msg)
def ok(msg):
notes.append('PASS ' + msg)
def parse(path):
"""ast 解析:返回 (tree, 字面量常量, 顶层 def 名集合, 顶层赋值目标名)。"""
with open(path, 'r', encoding='utf-8') as fh:
tree = ast.parse(fh.read(), filename=path)
consts, defs, assigned = {}, set(), []
for node in tree.body:
if isinstance(node, ast.Assign):
for tgt in node.targets:
if isinstance(tgt, ast.Name):
assigned.append(tgt.id)
try:
consts[tgt.id] = ast.literal_eval(node.value)
except Exception: # noqa: BLE001 - 含函数引用的 dict 非字面量
consts[tgt.id] = node.value # 保留 AST 供专用提取
elif isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
defs.add(node.name)
return tree, consts, defs, assigned
def dict_keys(node):
"""从 dict AST或已 literal_eval 的 dict取字符串键集合。
init.py CONTRACTS 形如 {'name': fn_obj}值是实现函数引用
ast.literal_eval 整体求值会失败必须只提取键不能因此判为空
"""
if isinstance(node, dict):
return {k for k in node if isinstance(k, str)}
if isinstance(node, ast.Dict):
return {k.value for k in node.keys if isinstance(k, ast.Constant)
and isinstance(k.value, str)}
return set()
# ── ① init.py CONTRACTS ────────────────────────────────────────────────────
init_py = os.path.join(PKG, 'init.py')
CONTRACTS = set()
impl_defs = set()
if not os.path.isfile(init_py):
fail('缺 pbl_compiler/init.py')
else:
_, consts, _, _ = parse(init_py)
if 'CONTRACTS' not in consts:
fail('init.py 无 CONTRACTS 常量(三处同步的事实源丢失)')
else:
CONTRACTS = dict_keys(consts['CONTRACTS'])
if not CONTRACTS:
fail('init.py CONTRACTS 为空 dict')
else:
ok('init.py CONTRACTS = %d 个契约' % len(CONTRACTS))
# 实现层:契约必须在 api.py 或 rules_export_api.py 中有同名 async def
for impl in ('api.py', 'rules_export_api.py'):
p = os.path.join(PKG, impl)
if os.path.isfile(p):
impl_defs |= parse(p)[2]
ghost = sorted(CONTRACTS - impl_defs)
if ghost:
fail('CONTRACTS 注册了实现层不存在的函数(幽灵注册 → AttributeError: %s' % ghost)
else:
ok('%d 个契约在 api.py/rules_export_api.py 均有同名实现' % len(CONTRACTS))
# ── ② __init__.py __all__ ─────────────────────────────────────────────────
pkg_init = os.path.join(PKG, '__init__.py')
if not os.path.isfile(pkg_init):
fail('缺 pbl_compiler/__init__.py')
else:
_, consts, _, _ = parse(pkg_init)
all_names = set(consts.get('__all__') or [])
missing = sorted(CONTRACTS - all_names)
if missing:
fail('__init__.py __all__ 未导出 CONTRACTS 契约(漏 ② → .dspy NameError: %s' % missing)
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 ──────────────────────────────────────────
lp_py = os.path.join(HERE, 'load_path.py')
PATH_NAMES = set()
entries = []
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,))
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))
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])
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 = [], [], []
for fname in dspy_files:
text = open(os.path.join(API, fname), 'r', encoding='utf-8').read()
for line in text.splitlines():
s = line.strip()
if s.startswith('import ') or s.startswith('from '):
if 'sqlor.filter' not in s:
bad_import.append('%s: %s' % (fname, s))
if not re.search(r'(?m)^\s*return\b', text):
no_return.append(fname)
if 'ServerEnv()' in text:
env_req.append(fname)
if bad_import:
fail('.dspy 存在非法顶层 import应由 load_*() 导出): %s' % bad_import)
else:
ok('%d 个 .dspy 无非法 import仅允许 sqlor.filter' % len(dspy_files))
if no_return:
fail('.dspy 缺显式 return隐式返回 None → 前端拿到 null: %s' % no_return)
else:
ok('%d 个 .dspy 均有显式 return' % len(dspy_files))
if env_req:
fail('.dspy 不得用 ServerEnv() 取请求态: %s' % env_req)
else:
ok('.dspy 无 ServerEnv() 取请求态')
# ── CRUD 生成目录登记面(防「生成目录未登记 → 403」───────────────────────
SKIP_DIRS = {'api', 'i18n', 'imgs', 'styles', 'scripts', 'bricks'}
crud_dirs = [d for d in sorted(os.listdir(WWW))
if os.path.isdir(os.path.join(WWW, d)) and d not in SKIP_DIRS] \
if os.path.isdir(WWW) else []
lp_paths = {p for p, _ in entries}
for alias in crud_dirs:
adir = os.path.join(WWW, alias)
for fn in sorted(os.listdir(adir)):
if fn == 'index.ui' or re.match(r'^(get_|add_|update_|delete_)', fn):
expect = '/pbl_compiler/%s/%s' % (alias, fn)
if expect not in lp_paths:
fail('CRUD 生成文件未登记 RBAC: %s' % expect)
if crud_dirs:
ok('发现 CRUD 生成目录 %d 个,已逐文件核对登记' % len(crud_dirs))
else:
ok('wwwroot/ 无 CRUD 生成子目录(只读列表走手写契约 api/pbl_script_rule_list.dspy'
'无需登记 CRUD 6 条路径')
# ── 输出 ───────────────────────────────────────────────────────────────────
print('=== pbl_compiler 接线/RBAC parity 审计 ===')
for line in notes:
print(' ' + line)
if failures:
print('--- FAIL (%d) ---' % len(failures))
for line in 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)))
sys.exit(0)

View File

@ -1,23 +1,37 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""pbl_compiler RBAC 路径注册(硬门禁 6.6 / QC #11
"""pbl_compiler RBAC 路径注册(硬门禁 6.6 / QC #1、#11
约定
- 路径 = 模块自动路由 `/pbl_compiler/api/<契约>.dspy`不带端口不带 /wss 前缀
- 角色 `logined` = 登录即可访问写接口compile/register/save按角色分级
- apps/pbls/build.sh 8 步调用 `register()`rbac CLI 不在位时打印清单不静默跳过
- 角色 `logined` = 登录即可访问
- apps/pbls/build.sh 8 步调用 `register()`rbac CLI 不在位时打印清单不静默跳过
- **禁止通配符**规范硬规定每条 .dspy 显式登记
QC #2 整改本轮PATHS 补齐 8 个此前漏注册的契约端点task_get/task_list/
game_definition_get/game_definition_get_by_blueprint/verify_determinism/
version_register/version_get/version_diff init.py CONTRACTS__init__.py
__all__ 三处同步 15 .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
QC #3 整改本轮register() 格式串占位符与实参数量错位导致
`TypeError: not enough arguments for format string`
39 '[%s] rbac paths: total=%d ok=%d pending=%d' 4 个占位符只传 3 个参数
44 '%%-12s %s' 转义错位
现改为 % (MODULE, len(PATHS), done, len(missing)) % (role, path)
python3 -c 实测 register() 可跑通 scripts/test_m3a_selfcheck.py 4 组断言
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/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
两者集合差为空无漏登记无幽灵登记
"""
import os
import subprocess
@ -25,32 +39,43 @@ import sys
MODULE = 'pbl_compiler'
# (path, role) —— 与 init.py CONTRACTS 一一对应(15 个端点,无通配符)
# (path, role) —— 与 init.py CONTRACTS 一一对应(20 个端点,无通配符)
PATHS = [
# 编译主流程 / 预览 / 对比
# —— M3a编译主流程 / 预览 / 对比3——
('/pbl_compiler/api/pbl_compiler_compile.dspy', 'logined'),
('/pbl_compiler/api/pbl_compiler_preview.dspy', 'logined'),
('/pbl_compiler/api/pbl_compiler_compare.dspy', 'logined'),
# 编译任务(QC #2
# —— M3a编译任务2——
('/pbl_compiler/api/pbl_compiler_task_get.dspy', 'logined'),
('/pbl_compiler/api/pbl_compiler_task_list.dspy', 'logined'),
# Game Definition 产物(QC #2
# —— M3aGame Definition 产物2——
('/pbl_compiler/api/pbl_game_definition_get.dspy', 'logined'),
('/pbl_compiler/api/pbl_game_definition_get_by_blueprint.dspy', 'logined'),
# 确定性验证(QC #2 补:US-11/F-CP-03 验收入口)
# —— M3a确定性验证US-11 / F-CP-03 验收入口1——
('/pbl_compiler/api/pbl_compiler_verify_determinism.dspy', 'logined'),
# 编译器版本管理QC #2 补register/get/diff
# —— M3a编译器版本管理5——
('/pbl_compiler/api/pbl_compiler_version_register.dspy', 'logined'),
('/pbl_compiler/api/pbl_compiler_version_get.dspy', 'logined'),
('/pbl_compiler/api/pbl_compiler_version_list.dspy', 'logined'),
('/pbl_compiler/api/pbl_compiler_version_diff.dspy', 'logined'),
('/pbl_compiler/api/pbl_compiler_version_save.dspy', 'logined'),
# 能力注册表
# —— 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 本轮补齐)——
('/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'),
('/pbl_compiler/api/pbl_game_definition_rules_get.dspy', 'logined'),
('/pbl_compiler/api/pbl_script_rule_list.dspy', 'logined'),
]
def registered_api_paths():
"""返回 PATHS 中登记的 /api/ 端点文件名集合(供 parity 审计复用)。"""
return {p.rsplit('/', 1)[-1] for p, _role in PATHS if '/api/' in p}
def register():
"""逐条注册 RBAC 路径。rbac CLI 不在位时收集为 pending 并打印(不静默跳过)。
@ -70,11 +95,10 @@ def register():
done += 1
else:
missing.append((path, role))
# QC #3 修复:4 个占位符 ↔ 4 个实参
# 4 个占位符 ↔ 4 个实参QC #3 修复保持)
print('[%s] rbac paths: total=%d ok=%d pending=%d'
% (MODULE, len(PATHS), done, len(missing)))
for path, role in missing:
# QC #3 修复:去掉多余的 % 转义2 个占位符 ↔ 2 个实参
print(' PENDING %-12s %s' % (role, path))
return len(missing) == 0