deliver: 交付收口(引擎代为提交)
This commit is contained in:
parent
2c14bf502e
commit
d203811973
@ -1,99 +1,7 @@
|
|||||||
{
|
{
|
||||||
"tblname": "pbl_game_definition",
|
"tblname": "pbl_game_definition",
|
||||||
"alias": "pbl_compiler",
|
"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,移除模型中不存在的幽灵字段 entity_count/event_count。",
|
"_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,移除模型中不存在的幽灵字段。",
|
||||||
"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
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"params": {
|
"params": {
|
||||||
"browserfields": {
|
"browserfields": {
|
||||||
"id": {
|
"id": {
|
||||||
@ -188,68 +96,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"editable": {
|
"editable": {
|
||||||
"blueprint_id": {
|
"get_data_url": "{{entire_url('/pbl_compiler/api/pbl_game_definition_get.dspy')}}"
|
||||||
"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"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"_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.browserfields),alias 由 pbl_compiler(与模块同名、会与模块 wwwroot 冲突)改为 pbl_game_definition_list。",
|
||||||
"order_by": "id DESC",
|
"order_by": "id DESC",
|
||||||
"page_size": 20
|
"page_size": 20
|
||||||
}
|
}
|
||||||
|
|||||||
@ -53,6 +53,24 @@ from pbl_compiler.canonical import (
|
|||||||
)
|
)
|
||||||
from pbl_compiler.gd_builder import GD_TOP_KEYS, build_game_definition
|
from pbl_compiler.gd_builder import GD_TOP_KEYS, build_game_definition
|
||||||
|
|
||||||
|
# —— M3b(QC #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__ = [
|
__all__ = [
|
||||||
'COMPILER_VERSION', 'RULESET_VERSION', 'RULES_HASH_SEED', 'GD_SCHEMA',
|
'COMPILER_VERSION', 'RULESET_VERSION', 'RULES_HASH_SEED', 'GD_SCHEMA',
|
||||||
'GD_TOP_KEYS', 'QUALITY_LADDER', 'COMPILE_GATE_MIN_STATE',
|
'GD_TOP_KEYS', 'QUALITY_LADDER', 'COMPILE_GATE_MIN_STATE',
|
||||||
@ -193,6 +211,57 @@ async def _audit(action, table, row_id, detail=None):
|
|||||||
return 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) 显式列名 UPDATE(tenant_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):
|
def ladder_at_or_above(state):
|
||||||
"""质量阶梯:返回 >= state 的全部状态(第 15 章 5 级)。"""
|
"""质量阶梯:返回 >= state 的全部状态(第 15 章 5 级)。"""
|
||||||
if state not in QUALITY_LADDER:
|
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')
|
gd_id = (got or {}).get('id')
|
||||||
idempotent = False
|
idempotent = False
|
||||||
|
|
||||||
|
# ⑥.5 M3b 第12章规则映射产物落库(QC #2:gd_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-closed:SELECT 读回的 rules_hash 必须与刚算出的一致,否则视为未落地
|
||||||
|
if not rule_artifacts['rules_hash'] or not rule_artifacts['rules_written']:
|
||||||
|
raise _err('PBL_E_RULES_NOT_PERSISTED',
|
||||||
|
'规则产物未写入 game_definition(upd=%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, {
|
await _update_task(tid, task_id, {
|
||||||
'status': TASK_SUCCESS, 'game_def_id': gd_id or 0,
|
'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,
|
'blueprint_id': blueprint_id, 'blueprint_version_no': ver_no,
|
||||||
'approval_id': approval_id, 'approval_evidence': evidence,
|
'approval_id': approval_id, 'approval_evidence': evidence,
|
||||||
'idempotent': idempotent, 'deterministic': True, 'llm_used': False,
|
'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', {}),
|
'counts': gd.get('manifest', {}).get('counts', {}),
|
||||||
'gd_top_keys': list(GD_TOP_KEYS),
|
'gd_top_keys': list(GD_TOP_KEYS),
|
||||||
'duration_ms': duration_ms, 'latency_ms': duration_ms,
|
'duration_ms': duration_ms, 'latency_ms': duration_ms,
|
||||||
|
|||||||
222
scripts/audit_rbac_parity.py
Normal file
222
scripts/audit_rbac_parity.py
Normal 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/ 条目=%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 = [], [], []
|
||||||
|
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)
|
||||||
@ -1,23 +1,37 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
"""pbl_compiler RBAC 路径注册(硬门禁 6.6 / QC #11)。
|
"""pbl_compiler RBAC 路径注册(硬门禁 6.6 / QC #1、#11)。
|
||||||
|
|
||||||
约定:
|
约定:
|
||||||
- 路径 = 模块自动路由 `/pbl_compiler/api/<契约>.dspy`,不带端口、不带 /wss 前缀;
|
- 路径 = 模块自动路由 `/pbl_compiler/api/<契约>.dspy`,不带端口、不带 /wss 前缀;
|
||||||
- 角色 `logined` = 登录即可访问;写接口(compile/register/save)按角色分级;
|
- 角色 `logined` = 登录即可访问;
|
||||||
- 由 apps/pbls/build.sh 第 8 步调用 `register()`;rbac CLI 不在位时打印清单(不静默跳过)。
|
- 由 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/
|
① 实现:pbl_compiler/api.py(15 个 M3a 契约)+ pbl_compiler/rules_export_api.py(5 个 M3b 契约)
|
||||||
version_register/version_get/version_diff),与 init.py CONTRACTS、__init__.py
|
② 导出:pbl_compiler/__init__.py import + __all__
|
||||||
__all__ 三处同步,共 15 个 .dspy 端点。
|
③ 注册:pbl_compiler/init.py CONTRACTS(共 20 条)
|
||||||
|
④ 端点:wwwroot/api/<契约名>.dspy(共 20 个)→ 本文件 PATHS(共 20 条)
|
||||||
|
|
||||||
QC #3 整改(本轮):register() 格式串占位符与实参数量错位导致
|
QC #1 整改(本轮,第四章 RBAC 硬门禁):
|
||||||
`TypeError: not enough arguments for format string`——
|
上一轮新增 5 个 M3b Web 契约(pbl_compiler_export_rules / pbl_compiler_rules_preview /
|
||||||
第 39 行 '[%s] rbac paths: total=%d ok=%d pending=%d' 有 4 个占位符只传 3 个参数;
|
pbl_compiler_rules_diff / pbl_game_definition_rules_get / pbl_script_rule_list)时,
|
||||||
第 44 行 '%%-12s %s' 转义错位。
|
wwwroot/api/ 已有 20 个 .dspy,但本文件 PATHS 仍只有 15 条 → 新接口未入 permission 表,
|
||||||
现改为 % (MODULE, len(PATHS), done, len(missing)) 与 % (role, path),
|
登录后调用一律 403,M3b 规则映射/导出能力实际不可用。现逐条显式补齐至 20 条。
|
||||||
python3 -c 实测 register() 可跑通(见 scripts/test_m3a_selfcheck.py 第 4 组断言)。
|
|
||||||
|
关于 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 os
|
||||||
import subprocess
|
import subprocess
|
||||||
@ -25,32 +39,43 @@ import sys
|
|||||||
|
|
||||||
MODULE = 'pbl_compiler'
|
MODULE = 'pbl_compiler'
|
||||||
|
|
||||||
# (path, role) —— 与 init.py CONTRACTS 一一对应(15 个端点,无通配符)
|
# (path, role) —— 与 init.py CONTRACTS 一一对应(20 个端点,无通配符)
|
||||||
PATHS = [
|
PATHS = [
|
||||||
# 编译主流程 / 预览 / 对比
|
# —— M3a:编译主流程 / 预览 / 对比(3)——
|
||||||
('/pbl_compiler/api/pbl_compiler_compile.dspy', 'logined'),
|
('/pbl_compiler/api/pbl_compiler_compile.dspy', 'logined'),
|
||||||
('/pbl_compiler/api/pbl_compiler_preview.dspy', 'logined'),
|
('/pbl_compiler/api/pbl_compiler_preview.dspy', 'logined'),
|
||||||
('/pbl_compiler/api/pbl_compiler_compare.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_get.dspy', 'logined'),
|
||||||
('/pbl_compiler/api/pbl_compiler_task_list.dspy', 'logined'),
|
('/pbl_compiler/api/pbl_compiler_task_list.dspy', 'logined'),
|
||||||
# Game Definition 产物(QC #2 补)
|
# —— M3a:Game Definition 产物(2)——
|
||||||
('/pbl_compiler/api/pbl_game_definition_get.dspy', 'logined'),
|
('/pbl_compiler/api/pbl_game_definition_get.dspy', 'logined'),
|
||||||
('/pbl_compiler/api/pbl_game_definition_get_by_blueprint.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'),
|
('/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_register.dspy', 'logined'),
|
||||||
('/pbl_compiler/api/pbl_compiler_version_get.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_list.dspy', 'logined'),
|
||||||
('/pbl_compiler/api/pbl_compiler_version_diff.dspy', 'logined'),
|
('/pbl_compiler/api/pbl_compiler_version_diff.dspy', 'logined'),
|
||||||
('/pbl_compiler/api/pbl_compiler_version_save.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_list.dspy', 'logined'),
|
||||||
('/pbl_compiler/api/pbl_capability_register.dspy', 'logined'),
|
('/pbl_compiler/api/pbl_capability_register.dspy', 'logined'),
|
||||||
|
# —— M3b:第12章 event→condition→response 规则映射与导出(5,QC #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():
|
def register():
|
||||||
"""逐条注册 RBAC 路径。rbac CLI 不在位时收集为 pending 并打印(不静默跳过)。
|
"""逐条注册 RBAC 路径。rbac CLI 不在位时收集为 pending 并打印(不静默跳过)。
|
||||||
|
|
||||||
@ -70,11 +95,10 @@ def register():
|
|||||||
done += 1
|
done += 1
|
||||||
else:
|
else:
|
||||||
missing.append((path, role))
|
missing.append((path, role))
|
||||||
# QC #3 修复:4 个占位符 ↔ 4 个实参
|
# 4 个占位符 ↔ 4 个实参(QC #3 修复保持)
|
||||||
print('[%s] rbac paths: total=%d ok=%d pending=%d'
|
print('[%s] rbac paths: total=%d ok=%d pending=%d'
|
||||||
% (MODULE, len(PATHS), done, len(missing)))
|
% (MODULE, len(PATHS), done, len(missing)))
|
||||||
for path, role in missing:
|
for path, role in missing:
|
||||||
# QC #3 修复:去掉多余的 % 转义,2 个占位符 ↔ 2 个实参
|
|
||||||
print(' PENDING %-12s %s' % (role, path))
|
print(' PENDING %-12s %s' % (role, path))
|
||||||
return len(missing) == 0
|
return len(missing) == 0
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user