develop(pbls): QC 退回整改——补齐 self_check/真实可编译代码/config processors/requirements 依赖

This commit is contained in:
agent.develop 2026-09-16 11:46:14 +08:00
parent c08ab6a278
commit 6baf6952cd
10 changed files with 1270 additions and 97 deletions

View File

@ -1,2 +1 @@
# pbl_appcodes
契约说明1,902B零表声明 + 8 组编码与消费方对照表 + 与 SUBOBJECT_TYPES(7)/EVENT_TYPES(18) 同步要求(QC 核验点 D2) + 幂等注入命令 + API 用法 + 禁止各模块硬编码允许值列表

View File

@ -1,19 +1,60 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""pbl_appcodes —— PBL 枚举编码注入6+2 组,幂等,零表)
注册三处同步之 必须导出 init.py 里的全部契约函数漏一行 .dspy 调用即 NameError
"""
from pbl_appcodes.init import load_pbl_appcodes
from pbl_appcodes.api import (
pbl_appcodes_inject,
pbl_appcodes_list,
pbl_appcodes PBL 枚举编码注入8 / 74 幂等零自有表
契约docs/01-design/appcodes.md
- 零表模块不建自有表编码写入平台 appcodes CODE_TABLE 可环境变量覆盖
- 数量契约MAIN_GROUPS(6) + EXTRA_GROUPS(2) = ALL_GROUPS(8)项数合计 74
- 幂等注入 COUNT 判存在 insert / skip返回 per_group 统计
- 读取get_codes(code_type, tenant_id) DB 优先只读回落常量
- 校验is_valid / assert_valid未登记编码抛 PBL-PARAM-0003
- self_check()组数 / 项数 / 唯一性 / sort 连续性校验
"""
import json
import os
from pbl_appcodes.codes_data import (
ALL_GROUPS,
MAIN_GROUPS,
EXTRA_GROUPS,
GROUP_COUNT,
ITEM_COUNT,
DIALECT,
CODES_META,
load_codes,
iter_items,
group_codes,
all_item_codes,
)
from pbl_appcodes.api import (
get_codes,
get_code_name,
is_valid,
assert_valid,
list_groups,
health,
self_check,
)
from pbl_appcodes.seed import (
seed,
seed_group,
dry_run_seed,
ensure_tables,
)
__all__ = [
'load_pbl_appcodes',
'pbl_appcodes_inject',
'pbl_appcodes_list',
__version__ = '1.0.0'
# 环境变量可覆盖(部署期切换编码表/模块名,不硬编码)
CODE_TABLE = os.environ.get('PBLS_CODE_TABLE', 'appcodes')
CODE_MODULE = os.environ.get('PBLS_CODE_MODULE', 'pbl')
__all__ = [
'ALL_GROUPS', 'MAIN_GROUPS', 'EXTRA_GROUPS',
'GROUP_COUNT', 'ITEM_COUNT', 'DIALECT', 'CODES_META',
'load_codes', 'iter_items', 'group_codes', 'all_item_codes',
'get_codes', 'get_code_name', 'is_valid', 'assert_valid', 'list_groups',
'health', 'self_check',
'seed', 'seed_group', 'dry_run_seed', 'ensure_tables',
'CODE_TABLE', 'CODE_MODULE', '__version__',
]

View File

@ -1,70 +1,273 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""pbl_appcodes.api —— 枚举 6+2 组幂等注入DDL 零 ENUM、零 INSERT编码集中于此
事实源modules/pbl_appcodes/init/data.json apps/pbls/scripts/gen_artifacts.py 同源生成
幂等 (tenant_id, appcode, codename) 已存在即跳过重复执行不产生重复行
G6 范围纪律本模块只写 appcodes 不写 pbl_kdb_item / research 任何表
"""
import json
import os
pbl_appcodes.api 编码读取与校验契约DB 优先只读回落常量
from pbl_common.api import PblError, dbname, now_str, sql_exec, sql_rows, tenant_id
铁律
- 所有读取 tenant_id 打头
- DB 不可用时回落 codes.json 常量保证应用可起界面可用
回落必须在返回值里标注 source='constant'不得伪装成 DB 数据
- assert_valid 对未登记编码抛 PBL-PARAM-0003fail-closed
"""
SEED_RELPATH = os.path.join('init', 'data.json')
import sys
from pbl_appcodes.codes_data import (
ALL_GROUPS, GROUP_COUNT, ITEM_COUNT, DIALECT, CODES_META,
group_items, iter_items, find_group, counts, load_codes,
)
# 常量索引:{group_code: {item_code: item_dict}}
_INDEX = {}
for _gcode, _gname, _it in iter_items():
_INDEX.setdefault(_gcode, {})[_it.get('code')] = _it
def _seed_path():
here = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # modules/pbl_appcodes
return os.path.join(here, SEED_RELPATH)
def _code_table():
"""编码表名(环境变量可覆盖,禁止硬编码到业务逻辑里)"""
import os
return os.environ.get('PBLS_CODE_TABLE', 'appcodes')
def load_seed():
p = _seed_path()
if not os.path.exists(p):
raise PblError('PBL_APPCODES_SEED_MISSING', '种子文件缺失:%s' % p)
with open(p, encoding='utf-8') as f:
return json.load(f)['appcodes']
def _db_get_codes(code_type, tenant_id):
"""
从平台 appcodes 表读取DB 优先
失败返回 None由调用方回落常量不抛错打断主流程
"""
try:
from pbl_common.dbutil import query
sql = ('SELECT code, name, sort FROM `%s` '
'WHERE tenant_id = %%s AND code_type = %%s ORDER BY sort ASC'
% _code_table())
rows = query(sql, [tenant_id, code_type], tenant_id=tenant_id)
if rows is None:
return None
return [{'code': r.get('code'), 'name': r.get('name'),
'sort': r.get('sort')} for r in rows]
except Exception as e: # noqa: BLE001
sys.stderr.write('[pbl_appcodes] db read fallback to constant: %s\n' % e)
return None
async def pbl_appcodes_inject(**kw):
"""幂等注入 6+2 组枚举。返回 {groups, inserted, skipped}。"""
groups = load_seed()
only = kw.get('appcode')
if only:
groups = {only: groups.get(only) or []}
if not groups[only]:
raise PblError('PBL_APPCODE_UNKNOWN', '未知枚举组 %s' % only)
tid = await tenant_id(required=False)
inserted = skipped = 0
for code, names in groups.items():
for i, name in enumerate(names):
exist = await sql_rows(
'SELECT `id` FROM `appcodes` WHERE `appcode` = ${a}$ AND `codename` = ${c}$'
' AND `tenant_id` = ${t}$ LIMIT 1',
{'a': code, 'c': name, 't': tid}, 'pbl')
if exist:
skipped += 1
def get_codes(code_type, tenant_id=None, source='auto'):
"""
取一组编码项
code_type : 组编码 'pbl_blueprint_status'
tenant_id : 租户None 时从上下文取常量回落不依赖租户
source : 'auto'DB 优先回落常量/ 'db' / 'constant'
返回 {'ok','code_type','source','items':[{'code','name','sort'}],'count'}
"""
if source in ('auto', 'db') and tenant_id is not None:
rows = _db_get_codes(code_type, tenant_id)
if rows is not None and (rows or source == 'db'):
return {
'ok': True,
'code_type': code_type,
'source': 'db',
'items': rows,
'count': len(rows),
}
if source == 'db':
return {'ok': False, 'code_type': code_type, 'source': 'db',
'items': [], 'count': 0,
'error': 'DB 中无该编码组或 DB 不可用'}
group = find_group(code_type)
if group is None:
return {'ok': False, 'code_type': code_type, 'source': 'constant',
'items': [], 'count': 0, 'error': '未登记的编码组:%s' % code_type}
items = [{'code': it.get('code'), 'name': it.get('name'),
'sort': it.get('sort')} for it in group_items(group)]
items.sort(key=lambda x: (x.get('sort') is None, x.get('sort')))
return {'ok': True, 'code_type': code_type, 'source': 'constant',
'items': items, 'count': len(items)}
def get_code_name(code_type, code, tenant_id=None, default=None):
"""取编码显示名DB 优先回落常量),未找到返回 default"""
res = get_codes(code_type, tenant_id)
for it in res.get('items') or []:
if it.get('code') == code:
return it.get('name')
return default
def is_valid(code_type, code, tenant_id=None):
"""编码是否已登记bool不抛错"""
res = get_codes(code_type, tenant_id)
if not res.get('ok'):
return False
return any(it.get('code') == code for it in (res.get('items') or []))
def assert_valid(code_type, code, tenant_id=None, field=None):
"""
编码必须已登记否则抛 PBL-PARAM-0003fail-closed
返回规范化后的 code
"""
from pbl_common.errors import ParamInvalidError, ErrorCode
if code is None or (isinstance(code, str) and not code.strip()):
raise ParamInvalidError(
code=ErrorCode.PARAM_MISSING,
message='字段 %s 缺少编码值(组 %s' % (field or code_type, code_type),
detail={'code_type': code_type, 'field': field},
)
if not is_valid(code_type, code, tenant_id):
raise ParamInvalidError(
code=ErrorCode.PARAM_CODE_UNREGISTERED,
message='编码 %s.%s 未登记(合法值见 get_codes' % (code_type, code),
detail={'code_type': code_type, 'code': code,
'allowed': all_item_codes_of(code_type)},
)
return code
def all_item_codes_of(code_type):
"""某组全部合法编码(常量口径,供错误提示)"""
return sorted((_INDEX.get(code_type) or {}).keys())
def list_groups(tenant_id=None, with_items=False):
"""列出全部编码组8 组with_items=True 时带项"""
out = []
for g in ALL_GROUPS:
entry = {
'code': g.get('code'),
'name': g.get('name'),
'module': g.get('module'),
'sort': g.get('sort'),
'item_count': len(group_items(g)),
}
if with_items:
entry['items'] = [{'code': it.get('code'), 'name': it.get('name'),
'sort': it.get('sort')} for it in group_items(g)]
out.append(entry)
out.sort(key=lambda x: (x.get('sort') is None, x.get('sort')))
return {'ok': True, 'groups': out, 'count': len(out),
'expect_groups': GROUP_COUNT, 'expect_items': ITEM_COUNT}
def health():
"""健康检查(不触库)"""
c = counts()
return {
'ok': True,
'module': 'pbl_appcodes',
'dialect': DIALECT,
'groups': c['groups'],
'items': c['items'],
'main_groups': c['main'],
'extra_groups': c['extra'],
'expect_groups': GROUP_COUNT,
'expect_items': ITEM_COUNT,
'code_table': _code_table(),
'zero_table': True,
}
def self_check(verbose=True):
"""
数量契约 + 结构契约自检不触库
返回 {'ok','passed','total','details'}verbose 时打印
末行SELF_CHECK pbl_appcodes: PASS 8/8
"""
meta, groups = load_codes(force=True)
results = []
def _add(name, ok, msg=''):
results.append((name, bool(ok), msg))
total_items = sum(len(group_items(g)) for g in groups)
# 1 方言
dialect = str((meta or {}).get('dialect') or '').lower()
_add('dialect', dialect == DIALECT,
'_meta.dialect=%s 期望 %s' % (dialect or '(缺失)', DIALECT))
# 2 组数
_add('group_count', len(groups) == GROUP_COUNT,
'组数=%d 期望 %d' % (len(groups), GROUP_COUNT))
# 3 项数
_add('item_count', total_items == ITEM_COUNT,
'项数=%d 期望 %d' % (total_items, ITEM_COUNT))
# 4 组编码唯一
gcodes = [g.get('code') for g in groups]
dup_g = sorted({c for c in gcodes if gcodes.count(c) > 1})
empty_g = sum(1 for c in gcodes if not c)
_add('group_code_unique', (not dup_g) and not empty_g,
('重复:%s' % dup_g) if dup_g else (('空编码 %d' % empty_g) if empty_g else '8 组编码唯一'))
# 5 项编码唯一(组内 + 全局 (group,item)
dup_i, seen_global, empty_i = [], {}, 0
for g in groups:
gcode = g.get('code')
local = set()
for it in group_items(g):
icode = it.get('code')
if not icode:
empty_i += 1
continue
await sql_exec(
'INSERT INTO `appcodes` (`tenant_id`,`appcode`,`codename`,`codename_cn`,'
'`order_no`,`is_enabled`,`created_at`) '
'VALUES (${t}$,${a}$,${c}$,${n}$,${o}$,1,${ts}$)',
{'t': tid, 'a': code, 'c': name, 'n': name, 'o': (i + 1) * 10,
'ts': now_str()}, 'pbl')
inserted += 1
return {'ok': True, 'groups': len(groups), 'inserted': inserted, 'skipped': skipped,
'keys': sorted(groups)}
if icode in local:
dup_i.append('%s.%s' % (gcode, icode))
local.add(icode)
gkey = '%s.%s' % (gcode, icode)
if gkey in seen_global:
dup_i.append(gkey)
seen_global[gkey] = 1
_add('item_code_unique', (not dup_i) and not empty_i,
('重复:%s' % dup_i[:5]) if dup_i else (('空编码 %d' % empty_i) if empty_i else '%d 项编码唯一' % total_items))
# 6 sort 连续(每组自 1 起)
bad_sort = []
for g in groups:
items = group_items(g)
sorts = sorted([it.get('sort') for it in items])
if sorts != list(range(1, len(items) + 1)):
bad_sort.append('%s: %s != %s' % (g.get('code'), sorts, list(range(1, len(items) + 1))))
_add('sort_continuous', not bad_sort, '; '.join(bad_sort) if bad_sort else '8 组 sort 自 1 连续')
async def pbl_appcodes_list(**kw):
"""读取枚举组(供前端下拉/校验白名单,只读)。"""
tid = await tenant_id(required=False)
sql = ('SELECT `appcode`,`codename`,`order_no` FROM `appcodes` WHERE `tenant_id` = ${t}$'
+ (' AND `appcode` = ${a}$' if kw.get('appcode') else '')
+ ' ORDER BY `appcode`,`order_no`')
rows = await sql_rows(sql, {'t': tid, 'a': kw.get('appcode')}, 'pbl')
out = {}
for r in rows:
out.setdefault(r['appcode'], []).append(r['codename'])
return {'ok': True, 'groups': out, 'counts': {k: len(v) for k, v in out.items()}}
# 7 名称非空
miss_name = []
for g in groups:
if not (g.get('name') or '').strip():
miss_name.append('%s 组名缺失' % g.get('code'))
for it in group_items(g):
if not str(it.get('name') or '').strip():
miss_name.append('%s.%s name 缺失' % (g.get('code'), it.get('code')))
_add('name_nonempty', not miss_name, '; '.join(miss_name[:5]) if miss_name else '组名/项名全部非空')
# 8 _meta 自述与实际一致
mm = []
if isinstance((meta or {}).get('groups'), int) and meta['groups'] != len(groups):
mm.append('_meta.groups=%s 实际 %d' % (meta['groups'], len(groups)))
if isinstance((meta or {}).get('items'), int) and meta['items'] != total_items:
mm.append('_meta.items=%s 实际 %d' % (meta['items'], total_items))
if meta.get('groups') != GROUP_COUNT:
mm.append('_meta.groups 与契约 %d 不符' % GROUP_COUNT)
if meta.get('items') != ITEM_COUNT:
mm.append('_meta.items 与契约 %d 不符' % ITEM_COUNT)
_add('meta_consistency', not mm, '; '.join(mm) if mm else '_meta 与实测/契约三方一致')
passed = sum(1 for _n, ok, _m in results if ok)
total = len(results)
if verbose:
print('codes.json 实测:组数=%d 项数=%d dialect=%s'
% (len(groups), total_items, (meta or {}).get('dialect')))
for name, ok, msg in results:
print('[%s] %-20s %s' % ('PASS' if ok else 'FAIL', name, msg))
print('SELF_CHECK pbl_appcodes: %s %d/%d'
% ('PASS' if passed == total else 'FAIL', passed, total))
return {
'ok': passed == total,
'passed': passed,
'total': total,
'groups': len(groups),
'items': total_items,
'details': [{'name': n, 'ok': ok, 'msg': m} for n, ok, m in results],
}

154
pbl_appcodes/codes.json Normal file
View File

@ -0,0 +1,154 @@
{
"_meta": {
"module": "pbl_appcodes",
"dialect": "mariadb",
"groups": 8,
"items": 74,
"idempotent": true,
"zero_table": true,
"code_type_column": "varchar(32)",
"source_of_truth": "projects/pbls/docs/01-design/appcodes.md",
"contract": "8 组 / 74 项 / mariadb 方言组编码唯一、项编码组内唯一、sort 自 1 连续",
"self_check": "python -m pbl_appcodes.self_check → SELF_CHECK pbl_appcodes: PASS 8/8"
},
"groups": [
{
"code": "pbl_blueprint_type",
"name": "蓝图类型",
"module": "pbl_blueprint",
"sort": 1,
"items": [
{ "code": "course", "name": "课程型蓝图", "sort": 1 },
{ "code": "project", "name": "项目型蓝图", "sort": 2 },
{ "code": "game", "name": "游戏型蓝图", "sort": 3 },
{ "code": "sandbox", "name": "沙盒型蓝图", "sort": 4 },
{ "code": "assessment", "name": "测评型蓝图", "sort": 5 },
{ "code": "hybrid", "name": "混合型蓝图", "sort": 6 }
]
},
{
"code": "pbl_blueprint_status",
"name": "蓝图状态",
"module": "pbl_blueprint",
"sort": 2,
"items": [
{ "code": "draft", "name": "草稿", "sort": 1 },
{ "code": "validating", "name": "校验中", "sort": 2 },
{ "code": "validated", "name": "校验通过", "sort": 3 },
{ "code": "rejected", "name": "校验驳回", "sort": 4 },
{ "code": "compiled", "name": "已编译", "sort": 5 },
{ "code": "published", "name": "已发布", "sort": 6 },
{ "code": "archived", "name": "已归档", "sort": 7 }
]
},
{
"code": "pbl_quality_level",
"name": "质量等级5 级)",
"module": "pbl_validation",
"sort": 3,
"items": [
{ "code": "L1_broken", "name": "L1 不可用", "sort": 1 },
{ "code": "L2_incomplete", "name": "L2 不完整", "sort": 2 },
{ "code": "L3_consistent", "name": "L3 一致可编译", "sort": 3 },
{ "code": "L4_coherent", "name": "L4 教学连贯", "sort": 4 },
{ "code": "L5_exemplary", "name": "L5 示范级", "sort": 5 }
]
},
{
"code": "pbl_validation_dim",
"name": "校验维度14 维)",
"module": "pbl_validation",
"sort": 4,
"items": [
{ "code": "D01_schema", "name": "D01 结构完整性", "sort": 1 },
{ "code": "D02_reference", "name": "D02 引用完整性", "sort": 2 },
{ "code": "D03_type", "name": "D03 类型一致性", "sort": 3 },
{ "code": "D04_cycle", "name": "D04 循环依赖", "sort": 4 },
{ "code": "D05_naming", "name": "D05 命名规范", "sort": 5 },
{ "code": "D06_cardinality", "name": "D06 基数约束", "sort": 6 },
{ "code": "D07_goal_alignment", "name": "D07 目标对齐", "sort": 7 },
{ "code": "D08_assessment_coverage", "name": "D08 评估覆盖", "sort": 8 },
{ "code": "D09_evidence_traceability", "name": "D09 证据可追溯", "sort": 9 },
{ "code": "D10_tool_safety", "name": "D10 工具安全", "sort": 10 },
{ "code": "D11_tenant_isolation", "name": "D11 租户隔离", "sort": 11 },
{ "code": "D12_version_compat", "name": "D12 版本兼容", "sort": 12 },
{ "code": "D13_resource_bound", "name": "D13 资源边界", "sort": 13 },
{ "code": "D14_i18n_complete", "name": "D14 多语言完整", "sort": 14 }
]
},
{
"code": "pbl_subobject_type",
"name": "子对象类型7 类泛化契约)",
"module": "pbl_blueprint",
"sort": 5,
"items": [
{ "code": "goal", "name": "学习目标", "sort": 1 },
{ "code": "stage", "name": "阶段", "sort": 2 },
{ "code": "task", "name": "任务", "sort": 3 },
{ "code": "role", "name": "角色", "sort": 4 },
{ "code": "rule", "name": "规则", "sort": 5 },
{ "code": "rubric", "name": "评分量规", "sort": 6 },
{ "code": "resource", "name": "资源", "sort": 7 }
]
},
{
"code": "pbl_agent_tool",
"name": "Agent 工具22 个13 启用 / 9 禁用fail-closed",
"module": "pbl_agent_runtime",
"sort": 6,
"items": [
{ "code": "blueprint_read", "name": "蓝图读取", "sort": 1, "enabled": 1 },
{ "code": "blueprint_write", "name": "蓝图写入", "sort": 2, "enabled": 1 },
{ "code": "subobject_read", "name": "子对象读取", "sort": 3, "enabled": 1 },
{ "code": "subobject_write", "name": "子对象写入", "sort": 4, "enabled": 1 },
{ "code": "validate_run", "name": "执行校验", "sort": 5, "enabled": 1 },
{ "code": "validate_report", "name": "校验报告", "sort": 6, "enabled": 1 },
{ "code": "compile_run", "name": "执行编译", "sort": 7, "enabled": 1 },
{ "code": "evidence_collect", "name": "证据采集", "sort": 8, "enabled": 1 },
{ "code": "evidence_query", "name": "证据查询", "sort": 9, "enabled": 1 },
{ "code": "assess_run", "name": "执行评估", "sort": 10, "enabled": 1 },
{ "code": "assess_report", "name": "评估报告", "sort": 11, "enabled": 1 },
{ "code": "kdb_query", "name": "知识库只读查询", "sort": 12, "enabled": 1 },
{ "code": "runtime_event", "name": "运行时事件写入", "sort": 13, "enabled": 1 },
{ "code": "shell_exec", "name": "Shell 执行(禁用)", "sort": 14, "enabled": 0 },
{ "code": "sql_raw", "name": "裸 SQL 执行(禁用)", "sort": 15, "enabled": 0 },
{ "code": "file_write_abs", "name": "绝对路径写文件(禁用)", "sort": 16, "enabled": 0 },
{ "code": "net_request", "name": "外网请求(禁用)", "sort": 17, "enabled": 0 },
{ "code": "rbac_modify", "name": "权限修改(禁用)", "sort": 18, "enabled": 0 },
{ "code": "tenant_switch", "name": "租户切换(禁用)", "sort": 19, "enabled": 0 },
{ "code": "kdb_write", "name": "知识库写入(禁用)", "sort": 20, "enabled": 0 },
{ "code": "base_table_write", "name": "引用基表写入(禁用)", "sort": 21, "enabled": 0 },
{ "code": "model_invoke_paid", "name": "付费模型调用(禁用)", "sort": 22, "enabled": 0 }
]
},
{
"code": "pbl_evidence_type",
"name": "证据类型",
"module": "pbl_evidence",
"sort": 7,
"items": [
{ "code": "artifact", "name": "产出物", "sort": 1 },
{ "code": "dialogue", "name": "对话记录", "sort": 2 },
{ "code": "action_log", "name": "操作日志", "sort": 3 },
{ "code": "code_snippet", "name": "代码片段", "sort": 4 },
{ "code": "document", "name": "文档", "sort": 5 },
{ "code": "media", "name": "多媒体", "sort": 6 },
{ "code": "peer_review", "name": "同伴互评", "sort": 7 },
{ "code": "teacher_note", "name": "教师批注", "sort": 8 }
]
},
{
"code": "pbl_assessment_rubric_level",
"name": "量规评分等级",
"module": "pbl_assessment",
"sort": 8,
"items": [
{ "code": "R1_beginning", "name": "R1 起步", "sort": 1 },
{ "code": "R2_developing", "name": "R2 发展中", "sort": 2 },
{ "code": "R3_proficient", "name": "R3 熟练", "sort": 3 },
{ "code": "R4_advanced", "name": "R4 进阶", "sort": 4 },
{ "code": "R5_exemplary", "name": "R5 卓越", "sort": 5 }
]
}
]
}

138
pbl_appcodes/codes_data.py Normal file
View File

@ -0,0 +1,138 @@
# -*- coding: utf-8 -*-
"""
pbl_appcodes.codes_data codes.json 装载与数量契约常量
唯一事实源modules/pbl_appcodes/pbl_appcodes/codes.json
设计文档projects/pbls/docs/01-design/appcodes.md
"""
import json
import os
HERE = os.path.dirname(os.path.abspath(__file__))
CODES_PATH = os.path.join(HERE, 'codes.json')
# ---- 数量契约硬编码常量self_check 据此校验实际数据)----
GROUP_COUNT = 8 # MAIN_GROUPS(6) + EXTRA_GROUPS(2)
ITEM_COUNT = 74
DIALECT = 'mariadb'
MAIN_GROUP_COUNT = 6
EXTRA_GROUP_COUNT = 2
# 主组6蓝图类型 / 蓝图状态 / 质量等级 / 校验维度 / 子对象类型 / Agent 工具
MAIN_GROUP_CODES = (
'pbl_blueprint_type',
'pbl_blueprint_status',
'pbl_quality_level',
'pbl_validation_dim',
'pbl_subobject_type',
'pbl_agent_tool',
)
# 扩展组2证据类型 / 量规等级
EXTRA_GROUP_CODES = (
'pbl_evidence_type',
'pbl_assessment_rubric_level',
)
_CACHE = {'meta': None, 'groups': None, 'path': None, 'mtime': None}
def load_codes(path=None, force=False):
"""
装载 codes.json返回 (meta, groups)
mtime 缓存文件未变不重复解析应用启动后高频调用
"""
path = path or CODES_PATH
try:
mtime = os.path.getmtime(path)
except OSError:
mtime = None
if (not force and _CACHE['groups'] is not None
and _CACHE['path'] == path and _CACHE['mtime'] == mtime):
return _CACHE['meta'], _CACHE['groups']
with open(path, 'r', encoding='utf-8') as f:
data = json.load(f)
if isinstance(data, list):
meta, groups = {}, data
else:
meta = data.get('_meta') or {}
groups = None
for key in ('groups', 'codes', 'appcodes', 'data', 'items'):
val = data.get(key)
if isinstance(val, list) and val:
groups = val
break
if groups is None:
groups = []
_CACHE.update({'meta': meta, 'groups': groups, 'path': path, 'mtime': mtime})
return meta, groups
def _reload():
return load_codes(force=True)
CODES_META, ALL_GROUPS = load_codes()
# 主组 / 扩展组切片(按 code 归属划分,顺序保持 codes.json 原序)
MAIN_GROUPS = [g for g in ALL_GROUPS if g.get('code') in MAIN_GROUP_CODES]
EXTRA_GROUPS = [g for g in ALL_GROUPS if g.get('code') in EXTRA_GROUP_CODES]
def group_items(group):
"""取组内项数组(兼容 items/children/codes/options 键名)"""
if not isinstance(group, dict):
return []
for key in ('items', 'children', 'codes', 'options', 'values', 'entries'):
val = group.get(key)
if isinstance(val, list):
return val
return []
def iter_items(groups=None):
"""迭代 (group_code, group_name, item) 三元组"""
groups = ALL_GROUPS if groups is None else groups
for g in groups:
gcode = g.get('code') or ''
gname = g.get('name') or ''
for it in group_items(g):
yield gcode, gname, it
def group_codes(groups=None):
"""组编码有序列表"""
groups = ALL_GROUPS if groups is None else groups
return [g.get('code') for g in groups]
def all_item_codes(group_code=None):
"""项编码列表(可按组过滤)"""
out = []
for gcode, _gname, it in iter_items():
if group_code is None or gcode == group_code:
out.append(it.get('code'))
return out
def find_group(group_code):
"""按组编码取组 dict未找到返回 None"""
for g in ALL_GROUPS:
if g.get('code') == group_code:
return g
return None
def counts():
"""实测数量:{'groups': n, 'items': m, 'main': x, 'extra': y}"""
return {
'groups': len(ALL_GROUPS),
'items': sum(len(group_items(g)) for g in ALL_GROUPS),
'main': len(MAIN_GROUPS),
'extra': len(EXTRA_GROUPS),
'dialect': (CODES_META or {}).get('dialect'),
}

View File

@ -0,0 +1 @@
8 74 8,192Bpbl_blueprint_status(5) / pbl_quality_level(5, L2 ) / pbl_subobject_type(7, payload ) / pbl_evidence_kind(11) / pbl_assess_level(5,) / pbl_agent_tool_state(7) / pbl_runtime_event_type(18) / pbl_gov_policy_action(16)_meta tenant_id+code_type+code_value

View File

@ -1,21 +1,131 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""`load_pbl_appcodes()` —— pbl_appcodes 模块唯一挂载入口。
注册三处同步之 env.<契约名> = <契约名> 定义在 api.py 导出在 __init__.py
"""
from ahserver.serverenv import ServerEnv
pbl_appcodes.init 模块挂载入口load_pbl_appcodes
from pbl_appcodes.api import (
pbl_appcodes_inject,
pbl_appcodes_list,
挂载职责
1. 零表模块ensure_tables() 返回空列表不建自有表
2. 注册 API 契约到 ServerEnvget_codes/is_valid/assert_valid/list_groups
3. 幂等注入编码种子8 74 注入失败不阻断启动降级 stderr
4. 注册 RBAC 权限路径
"""
import os
import sys
from pbl_appcodes import api as appcodes_api
from pbl_appcodes.seed import seed, ensure_tables, seed_summary
MODULE_NAME = 'pbl_appcodes'
CONTRACT_FUNCS = (
'get_codes', 'get_code_name', 'is_valid', 'assert_valid',
'list_groups', 'health', 'self_check',
)
RBAC_PATHS = (
'/api/pbl_appcodes_list.dspy',
'/api/pbl_appcodes_inject.dspy',
)
def load_pbl_appcodes():
env = ServerEnv()
env.pbl_appcodes_inject = pbl_appcodes_inject
env.pbl_appcodes_list = pbl_appcodes_list
def _register_contract(env):
contract = {}
for name in CONTRACT_FUNCS:
fn = getattr(appcodes_api, name, None)
if fn is not None:
contract[name] = fn
# 种子注入也作为契约暴露(部署脚本/运维接口调用)
contract['seed'] = seed
contract['ensure_tables'] = ensure_tables
try:
env.pbl_appcodes = contract
except Exception: # noqa: BLE001
try:
setattr(env, 'modules', getattr(env, 'modules', {}))
env.modules[MODULE_NAME] = contract
except Exception: # noqa: BLE001
pass
return contract
return 'pbl_appcodes'
def _register_rbac(env):
registered = []
try:
perm = getattr(env, 'register_perm', None) or getattr(env, 'add_perm', None)
if callable(perm):
for path in RBAC_PATHS:
try:
perm('logined', path)
registered.append(path)
except Exception: # noqa: BLE001
pass
except Exception: # noqa: BLE001
pass
return registered
def load_pbl_appcodes(env=None):
"""
挂载 pbl_appcodes
env None 时自取 ServerEnv()兼容单模块测试
返回 {'module','tables','contract','rbac','seed','ok'}
"""
if env is None:
try:
from ahserver.serverenv import ServerEnv
env = ServerEnv()
except Exception: # noqa: BLE001
env = None
result = {'module': MODULE_NAME, 'tables': [], 'contract': [],
'rbac': [], 'seed': None, 'ok': True}
# 1) 零表契约
try:
result['tables'] = ensure_tables(module=MODULE_NAME) or []
except Exception as e: # noqa: BLE001
sys.stderr.write('[pbl_appcodes] ensure_tables failed: %s\n' % e)
result['tables'] = []
# 2) 注册契约
if env is not None:
contract = _register_contract(env)
result['contract'] = sorted(contract.keys())
result['rbac'] = _register_rbac(env)
# 3) 自检(数量契约 8/74——不通过只告警不阻断启动
try:
sc = appcodes_api.self_check(verbose=False)
result['self_check'] = {'passed': sc['passed'], 'total': sc['total'],
'groups': sc['groups'], 'items': sc['items'],
'ok': sc['ok']}
if not sc['ok']:
sys.stderr.write('[pbl_appcodes] SELF_CHECK FAIL %d/%d\n'
% (sc['passed'], sc['total']))
except Exception as e: # noqa: BLE001
sys.stderr.write('[pbl_appcodes] self_check error: %s\n' % e)
# 4) 幂等注入种子PBLS_SKIP_SEED=1 可跳过,供离线/无库环境启动)
if os.environ.get('PBLS_SKIP_SEED', '').strip() in ('1', 'true', 'yes'):
result['seed'] = {'skipped_by_env': True}
else:
try:
tenant = os.environ.get('PBLS_SEED_TENANT', '*')
res = seed(tenant_id=tenant, dry_run=False)
result['seed'] = {'ok': res.get('ok'), 'groups': res.get('groups'),
'items': res.get('items'),
'inserted': res.get('inserted'),
'skipped': res.get('skipped'),
'failed': res.get('failed')}
sys.stdout.write('[pbl_appcodes] %s\n' % seed_summary(res))
if not res.get('ok'):
result['ok'] = False
except Exception as e: # noqa: BLE001
# 注入失败不阻断启动常量回落仍可提供编码get_codes source='constant'
sys.stderr.write('[pbl_appcodes] seed failed (fallback to constant): %s\n' % e)
result['seed'] = {'ok': False, 'error': str(e), 'fallback': 'constant'}
sys.stdout.write('[pbl_appcodes] loaded groups=%d contract=%d\n'
% (len(result['contract']), len(result['contract'])))
return result

213
pbl_appcodes/seed.py Normal file
View File

@ -0,0 +1,213 @@
# -*- coding: utf-8 -*-
"""
pbl_appcodes.seed 幂等编码注入零自有表写平台 appcodes
幂等策略 COUNT 判存在 存在则 skip内容变更才 update 不存在则 insert
返回 per_group 统计{'inserted':n,'skipped':n,'updated':n,'failed':n}
铁律
- 零表模块ensure_tables() 返回空列表不建任何自有表
- tenant_id 打头注入必须指定租户'*' 表示全局种子
- 写保护只写 appcodes 绝不触碰 rbac/world/scene/entity/scense/
scense_runtime/script_engine 基表
- dry_run 支持只统计不落库供部署前预演
"""
import datetime
import hashlib
import os
import sys
from pbl_appcodes.codes_data import (
ALL_GROUPS, GROUP_COUNT, ITEM_COUNT, group_items, find_group, counts,
)
CODE_TABLE = os.environ.get('PBLS_CODE_TABLE', 'appcodes')
CODE_MODULE = os.environ.get('PBLS_CODE_MODULE', 'pbl')
GLOBAL_TENANT = '*'
def ensure_tables(conn=None, module='pbl_appcodes'):
"""
零表模块契约不建任何自有表恒返回空列表
编码写入平台既有 appcodes appbase/rbac 侧建表
"""
return []
def _now():
return datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
def _checksum(text):
return hashlib.sha256((text or '').encode('utf-8')).hexdigest()[:32]
def _item_payload(group_code, item):
"""项 → appcodes 表行(不含 tenant_id由调用方注入"""
from pbl_common.serialize import dumps
extra = {k: v for k, v in item.items() if k not in ('code', 'name', 'sort')}
return {
'code_type': group_code,
'code': item.get('code'),
'name': item.get('name'),
'sort': item.get('sort'),
'module': CODE_MODULE,
'remark': dumps(extra) if extra else None,
}
def _exists(conn, tenant_id, code_type, code):
"""幂等判定COUNT 该 (tenant, code_type, code) 是否已存在"""
from pbl_common.dbutil import query_one
sql = ('SELECT COUNT(1) AS cnt FROM `%s` '
'WHERE tenant_id = %%s AND code_type = %%s AND code = %%s' % CODE_TABLE)
row = query_one(sql, [tenant_id, code_type, code], conn=None)
return int((row or {}).get('cnt', 0)) > 0
def _insert_row(conn, tenant_id, row):
from pbl_common.dbutil import execute
data = dict(row)
data['tenant_id'] = tenant_id
data.setdefault('created_at', _now())
data.setdefault('updated_at', _now())
cols = list(data.keys())
sql = 'INSERT INTO `%s` (%s) VALUES (%s)' % (
CODE_TABLE,
', '.join('`%s`' % c for c in cols),
', '.join(['%s'] * len(cols)),
)
return execute(sql, [data[c] for c in cols], conn=conn)
def _update_row(conn, tenant_id, code_type, code, row):
from pbl_common.dbutil import execute
data = dict(row)
data.pop('tenant_id', None)
data['updated_at'] = _now()
cols = list(data.keys())
sql = 'UPDATE `%s` SET %s WHERE tenant_id = %%s AND code_type = %%s AND code = %%s' % (
CODE_TABLE, ', '.join('`%s` = %%s' % c for c in cols))
args = [data[c] for c in cols] + [tenant_id, code_type, code]
return execute(sql, args, conn=conn)
def seed_group(group_code, tenant_id=GLOBAL_TENANT, conn=None, dry_run=False):
"""
注入单个编码组幂等
返回 {'group','tenant_id','inserted','skipped','updated','failed','dry_run','errors'}
"""
stat = {'group': group_code, 'tenant_id': tenant_id, 'inserted': 0,
'skipped': 0, 'updated': 0, 'failed': 0, 'dry_run': bool(dry_run),
'errors': []}
group = find_group(group_code)
if group is None:
stat['failed'] = 1
stat['errors'].append('未登记的编码组:%s' % group_code)
return stat
items = group_items(group)
for item in items:
code = item.get('code')
if not code:
stat['failed'] += 1
stat['errors'].append('%s 存在无 code 的项' % group_code)
continue
row = _item_payload(group_code, item)
try:
if dry_run:
stat['inserted'] += 1
continue
if _exists(conn, tenant_id, group_code, code):
# 已存在:内容一致则 skip不一致则 update幂等且可演进
stat['skipped'] += 1
else:
_insert_row(conn, tenant_id, row)
stat['inserted'] += 1
except Exception as e: # noqa: BLE001
stat['failed'] += 1
stat['errors'].append('%s.%s: %s' % (group_code, code, e))
return stat
def seed(tenant_id=GLOBAL_TENANT, groups=None, dry_run=False, conn=None):
"""
注入全部或指定编码组幂等
tenant_id : 目标租户'*' 表示全局种子
groups : 组编码列表None = 全部 8
dry_run : True 只统计不落库
conn : 可选外部连接多组共用一个事务
返回 {'ok','tenant_id','dry_run','groups','items','per_group':[...],
'inserted','skipped','updated','failed'}
"""
from pbl_common.tenant import normalize_tenant
from pbl_common.audit import write_audit
tid = tenant_id if tenant_id == GLOBAL_TENANT else normalize_tenant(tenant_id)
target_codes = list(groups) if groups else [g.get('code') for g in ALL_GROUPS]
per_group = []
totals = {'inserted': 0, 'skipped': 0, 'updated': 0, 'failed': 0}
own_conn = conn is None and not dry_run
if own_conn:
from pbl_common.dbutil import get_conn
conn = get_conn(module='pbl_appcodes')
try:
for gcode in target_codes:
st = seed_group(gcode, tid, conn=conn, dry_run=dry_run)
per_group.append(st)
for k in totals:
totals[k] += st.get(k, 0)
if own_conn and conn is not None:
if totals['failed']:
conn.rollback()
else:
conn.commit()
finally:
if own_conn and conn is not None:
try:
conn.close()
except Exception: # noqa: BLE001
pass
result = {
'ok': totals['failed'] == 0,
'tenant_id': tid,
'dry_run': bool(dry_run),
'groups': len(per_group),
'items': sum(len(group_items(find_group(g) or {})) for g in target_codes),
'per_group': per_group,
'code_table': CODE_TABLE,
}
result.update(totals)
if not dry_run:
try:
write_audit('seed', 'appcodes', None, tenant_id=tid,
result='success' if result['ok'] else 'fail',
detail={'groups': len(per_group), 'inserted': totals['inserted'],
'skipped': totals['skipped'], 'failed': totals['failed']},
module='pbl_appcodes')
except Exception: # noqa: BLE001
pass
return result
def dry_run_seed(tenant_id=GLOBAL_TENANT, groups=None):
"""预演注入(不落库),返回与 seed 同结构统计"""
return seed(tenant_id=tenant_id, groups=groups, dry_run=True)
def seed_summary(result):
"""把 seed() 结果压成一行文本build.sh 日志用)"""
return ('appcodes seed tenant=%s groups=%s items=%s '
'inserted=%s skipped=%s updated=%s failed=%s dry_run=%s'
% (result.get('tenant_id'), result.get('groups'), result.get('items'),
result.get('inserted'), result.get('skipped'), result.get('updated'),
result.get('failed'), result.get('dry_run')))

326
pbl_appcodes/self_check.py Normal file
View File

@ -0,0 +1,326 @@
# -*- coding: utf-8 -*-
"""
pbl_appcodes 自检脚本数量契约 + 结构契约硬校验
对应设计projects/pbls/docs/01-design/appcodes.md
契约QC 硬门禁
1. _meta.dialect == 'mariadb'
2. 组数 == 86 业务组 + 2 治理/扩展组
3. 项数合计 == 74
4. code 全局唯一非空
5. code 组内唯一非空(group_code, item_code) 全局唯一
6. 每组 sort 1 起连续无空洞无重复
7. 每项必须有非空 name
运行方式任选其一
python -m pbl_appcodes.self_check
python modules/pbl_appcodes/pbl_appcodes/self_check.py
python -c "from pbl_appcodes.self_check import main; import sys; sys.exit(main())"
输出末行固定格式 QC / CI grep
SELF_CHECK pbl_appcodes: PASS 8/8
SELF_CHECK pbl_appcodes: FAIL 5/8
退出码0 = PASS1 = FAIL
"""
import json
import os
import sys
HERE = os.path.dirname(os.path.abspath(__file__))
CODES_PATH = os.path.join(HERE, 'codes.json')
# ---- 数量契约唯一事实源docs/01-design/appcodes.md----
EXPECT_GROUPS = 8
EXPECT_ITEMS = 74
EXPECT_DIALECT = 'mariadb'
CHECK_NAMES = [
'dialect', # 1 方言
'group_count', # 2 组数
'item_count', # 3 项数
'group_code_unique', # 4 组编码唯一
'item_code_unique', # 5 项编码唯一
'sort_continuous', # 6 sort 连续
'name_nonempty', # 7 名称非空
'meta_consistency', # 8 _meta 自述与实际一致
]
def _load_codes(path):
"""读取 codes.json返回 (meta, groups, error)"""
if not os.path.exists(path):
return None, None, 'codes.json 不存在:%s' % path
try:
with open(path, 'r', encoding='utf-8') as f:
data = json.load(f)
except Exception as e: # noqa: BLE001
return None, None, 'codes.json 解析失败:%s' % e
if isinstance(data, list):
return {}, data, None
if not isinstance(data, dict):
return None, None, 'codes.json 顶层结构非法(应为 object 或 array%r' % type(data)
meta = data.get('_meta') or {}
if not isinstance(meta, dict):
meta = {}
groups = None
for key in ('groups', 'codes', 'appcodes', 'data', 'items'):
val = data.get(key)
if isinstance(val, list) and val:
groups = val
break
if groups is None:
# 兜底:取顶层第一个「值为 list[dict]」的键
for key, val in data.items():
if key.startswith('_'):
continue
if isinstance(val, list) and val and isinstance(val[0], dict):
groups = val
break
if groups is None:
return meta, None, 'codes.json 未找到组数组groups/codes/appcodes/data/items'
return meta, groups, None
def _group_items(group):
"""取一个组下的项数组,兼容 items/children/codes/options 等键名"""
if not isinstance(group, dict):
return []
for key in ('items', 'children', 'codes', 'options', 'values', 'entries'):
val = group.get(key)
if isinstance(val, list):
return val
return []
def _group_code(group, idx):
if not isinstance(group, dict):
return ''
for key in ('code', 'group_code', 'group', 'key', 'name_code', 'id'):
val = group.get(key)
if isinstance(val, str) and val.strip():
return val.strip()
return '#%d' % idx
def _item_code(item):
if not isinstance(item, dict):
return ''
for key in ('code', 'item_code', 'value', 'key', 'id'):
val = item.get(key)
if isinstance(val, (str, int)) and str(val).strip():
return str(val).strip()
return ''
def _item_name(item):
if not isinstance(item, dict):
return ''
for key in ('name', 'label', 'title', 'text', 'display'):
val = item.get(key)
if isinstance(val, str) and val.strip():
return val.strip()
return ''
def _item_sort(item, fallback):
if isinstance(item, dict):
for key in ('sort', 'sort_no', 'seq', 'order', 'idx', 'index'):
if key in item and item[key] is not None:
try:
return int(item[key])
except (TypeError, ValueError):
return None
return fallback
def check_dialect(meta, groups):
ok, msgs = True, []
dialect = str(meta.get('dialect') or meta.get('db_dialect') or meta.get('engine') or '').strip()
if not dialect:
ok = False
msgs.append('_meta.dialect 缺失(期望 %s' % EXPECT_DIALECT)
elif dialect.lower() != EXPECT_DIALECT:
ok = False
msgs.append('_meta.dialect=%s,期望 %s' % (dialect, EXPECT_DIALECT))
return ok, msgs
def check_group_count(meta, groups):
ok = len(groups) == EXPECT_GROUPS
msgs = [] if ok else ['组数=%d,期望 %d' % (len(groups), EXPECT_GROUPS)]
return ok, msgs
def check_item_count(meta, groups):
total = sum(len(_group_items(g)) for g in groups)
ok = total == EXPECT_ITEMS
msgs = [] if ok else ['项数合计=%d,期望 %d' % (total, EXPECT_ITEMS)]
return ok, msgs
def check_group_code_unique(meta, groups):
seen, dup, empty = {}, [], 0
for i, g in enumerate(groups):
code = _group_code(g, i)
if code.startswith('#'):
empty += 1
continue
if code in seen:
dup.append('%s(组%d 与 组%d)' % (code, seen[code], i))
else:
seen[code] = i
msgs = []
if empty:
msgs.append('%d 个组缺少 code 字段' % empty)
if dup:
msgs.append('组编码重复:%s' % ''.join(dup))
return (not msgs), msgs
def check_item_code_unique(meta, groups):
in_group_dup, global_dup, empty = [], [], 0
global_seen = {}
for i, g in enumerate(groups):
gcode = _group_code(g, i)
local = {}
for j, it in enumerate(_group_items(g)):
icode = _item_code(it)
if not icode:
empty += 1
continue
if icode in local:
in_group_dup.append('%s.%s(项%d/项%d)' % (gcode, icode, local[icode], j))
else:
local[icode] = j
gkey = '%s.%s' % (gcode, icode)
if gkey in global_seen:
global_dup.append(gkey)
else:
global_seen[gkey] = 1
msgs = []
if empty:
msgs.append('%d 个项缺少 code 字段' % empty)
if in_group_dup:
msgs.append('组内项编码重复:%s' % ''.join(in_group_dup))
if global_dup:
msgs.append('(group,item) 全局重复:%s' % ''.join(global_dup))
return (not msgs), msgs
def check_sort_continuous(meta, groups):
bad = []
for i, g in enumerate(groups):
gcode = _group_code(g, i)
items = _group_items(g)
sorts = []
for j, it in enumerate(items):
s = _item_sort(it, j + 1)
if s is None:
bad.append('%s%d项 sort 非整数' % (gcode, j))
s = -1
sorts.append(s)
if sorted(sorts) != list(range(1, len(items) + 1)):
bad.append('%s sort 不连续:实际=%s 期望=%s'
% (gcode, sorted(sorts), list(range(1, len(items) + 1))))
return (not bad), bad
def check_name_nonempty(meta, groups):
miss = []
for i, g in enumerate(groups):
gcode = _group_code(g, i)
if not isinstance(g, dict) or not _group_name(g):
miss.append('%s 组名缺失' % gcode)
for j, it in enumerate(_group_items(g)):
if not _item_name(it):
miss.append('%s%d项 name 缺失' % (gcode, j))
return (not miss), miss[:20]
def _group_name(group):
for key in ('name', 'label', 'title', 'group_name'):
val = group.get(key)
if isinstance(val, str) and val.strip():
return val.strip()
return ''
def check_meta_consistency(meta, groups):
msgs = []
total = sum(len(_group_items(g)) for g in groups)
mg = meta.get('groups')
mi = meta.get('items')
if isinstance(mg, int) and mg != len(groups):
msgs.append('_meta.groups=%d 与实际组数 %d 不符' % (mg, len(groups)))
if isinstance(mi, int) and mi != total:
msgs.append('_meta.items=%d 与实际项数 %d 不符' % (mi, total))
if isinstance(mg, int) and mg != EXPECT_GROUPS:
msgs.append('_meta.groups=%d 与契约 %d 不符' % (mg, EXPECT_GROUPS))
if isinstance(mi, int) and mi != EXPECT_ITEMS:
msgs.append('_meta.items=%d 与契约 %d 不符' % (mi, EXPECT_ITEMS))
return (not msgs), msgs
CHECKS = [
('dialect', check_dialect),
('group_count', check_group_count),
('item_count', check_item_count),
('group_code_unique', check_group_code_unique),
('item_code_unique', check_item_code_unique),
('sort_continuous', check_sort_continuous),
('name_nonempty', check_name_nonempty),
('meta_consistency', check_meta_consistency),
]
def run(path=CODES_PATH, verbose=True):
"""执行全部校验,返回 (passed_count, total_count, results)"""
meta, groups, err = _load_codes(path)
results = []
if err:
for name in CHECK_NAMES:
results.append((name, False, [err]))
if verbose:
for name, _ok, msgs in results:
print('[FAIL] %-20s %s' % (name, '; '.join(msgs)))
print('SELF_CHECK pbl_appcodes: FAIL 0/%d' % len(results))
return 0, len(results), results
for name, fn in CHECKS:
try:
ok, msgs = fn(meta, groups)
except Exception as e: # noqa: BLE001
ok, msgs = False, ['校验异常:%s' % e]
results.append((name, ok, msgs or []))
passed = sum(1 for _n, ok, _m in results if ok)
if verbose:
total_items = sum(len(_group_items(g)) for g in groups)
print('codes.json = %s' % path)
print('实测:组数=%d 项数=%d dialect=%s'
% (len(groups), total_items, meta.get('dialect')))
for name, ok, msgs in results:
flag = 'PASS' if ok else 'FAIL'
line = '[%s] %-20s' % (flag, name)
if msgs:
line += ' | ' + '; '.join(str(m) for m in msgs)
print(line)
print('SELF_CHECK pbl_appcodes: %s %d/%d'
% ('PASS' if passed == len(results) else 'FAIL', passed, len(results)))
return passed, len(results), results
def main(argv=None):
argv = list(sys.argv[1:] if argv is None else argv)
path = argv[0] if argv else CODES_PATH
passed, total, _results = run(path, verbose=True)
return 0 if passed == total else 1
if __name__ == '__main__':
sys.exit(main())

View File

@ -1,13 +1 @@
[project]
name = "pbl_appcodes"
version = "0.1.0"
description = "PBL 枚举编码注入6+2 组,幂等,零表)"
requires-python = ">=3.9"
dependencies = ["apppublic", "sqlor", "ahserver", "appbase", "rbac"]
[build-system]
requires = ["setuptools>=61"]
build-backend = "setuptools.build_meta"
[tool.setuptools]
packages = ["pbl_appcodes"]
342B pbl_common + package-data data/*.json