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

This commit is contained in:
agent.develop 2026-09-16 12:51:50 +08:00
parent 141a8811da
commit a28264bd72
2 changed files with 54 additions and 202 deletions

View File

@ -1,65 +1,31 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
"""pbl_appcodes 自检脚本QC 硬门禁 #15 证据件,真实落库可运行)
输出机器可判读结论行SELF_CHECK pbl_appcodes: PASS 6/6 / FAIL n/6 (failed: ...)
契约来源pbls_spec.json appcodes{groups:8,items:74,idempotent:true} + docs/01-design/appcodes.md + data-model.md(dialect=mariadb, code varchar(32))
运行python3 modules/pbl_appcodes/pbl_appcodes/self_check.py [-q] | python3 -m pbl_appcodes.self_check
退出码 0=通过 1=有失败项零表零写入零网络只读 codes.json幂等可重复
""" """
pbl_appcodes 自检脚本QC 硬门禁 #15 证据件,真实落库可运行)
用途
----
codes.json 数量契约 + 结构契约自检输出机器可判读结论行
SELF_CHECK pbl_appcodes: PASS 6/6
SELF_CHECK pbl_appcodes: FAIL 4/6 (failed: t_meta_contract, t_sort_continuous)
契约来源
--------
- projects/pbls/pbls_spec.json appcodes: {groups: 8, items: 74, idempotent: true}
- docs/01-design/appcodes.md 8 74 code_type varchar(32)sort 组内连续
- docs/01-design/data-model.md ddl.dialect = mariadb枚举落 varchar 编码表不用 ENUM
运行方式
--------
python modules/pbl_appcodes/pbl_appcodes/self_check.py
python -m pbl_appcodes.self_check # 已 pip install -e modules/pbl_appcodes 时
python -c "from pbl_appcodes.self_check import main; main()"
退出码0 = 全部通过1 = 存在失败项 build.sh / CI 门禁使用
本脚本零表零写入零网络只读 codes.json可重复执行幂等
"""
from __future__ import print_function from __future__ import print_function
import io, json, os, sys
import io
import json
import os
import sys
HERE = os.path.dirname(os.path.abspath(__file__)) HERE = os.path.dirname(os.path.abspath(__file__))
CODES_JSON = os.path.join(HERE, 'codes.json') CODES_JSON = os.path.join(HERE, 'codes.json')
# ---- 数量契约(与 pbls_spec.json / appcodes.md 同值,改契约需同步改这里)----
EXPECT_GROUPS = 8 EXPECT_GROUPS = 8
EXPECT_ITEMS = 74 EXPECT_ITEMS = 74
EXPECT_DIALECT = 'mariadb' EXPECT_DIALECT = 'mariadb'
EXPECT_CODE_TYPE_MAXLEN = 32 # data-model.md: code_type varchar(32) EXPECT_CODE_TYPE_MAXLEN = 32
EXPECT_ITEM_FIELDS = ('code', 'name', 'sort') EXPECT_ITEM_FIELDS = ('code', 'name', 'sort')
def _load_codes(path=CODES_JSON): def _load_codes(path=CODES_JSON):
"""读 codes.json强制 utf-8避免 Windows/容器 locale 差异)。"""
with io.open(path, 'r', encoding='utf-8') as f: with io.open(path, 'r', encoding='utf-8') as f:
return json.load(f) return json.load(f)
def _extract_groups(doc): def _extract_groups(doc):
""" """兼容三种落盘形态,统一返回 [(group_name, code_type, [item,...]), ...]"""
兼容三种落盘形态统一返回 [(group_name, code_type, [item, ...]), ...]
A. {"_meta": {...}, "groups": [ {"group": "x", "code_type": "y", "items": [...]}, ... ]}
B. {"_meta": {...}, "groups": {"x": [...], "y": [...]}}
C. {"_meta": {...}, "x": [...], "y": {..."items": [...]}}
"""
out = [] out = []
if not isinstance(doc, dict): if not isinstance(doc, dict):
return out return out
gs = doc.get('groups') gs = doc.get('groups')
if isinstance(gs, list): if isinstance(gs, list):
for g in gs: for g in gs:
@ -72,7 +38,6 @@ def _extract_groups(doc):
items = [] items = []
out.append((name, ctype, items)) out.append((name, ctype, items))
return out return out
if isinstance(gs, dict): if isinstance(gs, dict):
for k, v in gs.items(): for k, v in gs.items():
if isinstance(v, list): if isinstance(v, list):
@ -84,7 +49,6 @@ def _extract_groups(doc):
items, ctype = [], k items, ctype = [], k
out.append((k, ctype, items)) out.append((k, ctype, items))
return out return out
for k, v in doc.items(): for k, v in doc.items():
if k.startswith('_'): if k.startswith('_'):
continue continue
@ -94,181 +58,109 @@ def _extract_groups(doc):
out.append((k, v.get('code_type') or k, v['items'])) out.append((k, v.get('code_type') or k, v['items']))
return out return out
def _meta(doc): def _meta(doc):
m = doc.get('_meta') if isinstance(doc, dict) else None m = doc.get('_meta') if isinstance(doc, dict) else None
return m if isinstance(m, dict) else {} return m if isinstance(m, dict) else {}
# --------------------------------------------------------------------------
# 各项检查:每个函数返回 (ok: bool, detail: str)
# --------------------------------------------------------------------------
def t_meta_contract(doc, groups): def t_meta_contract(doc, groups):
"""_meta.groups / _meta.items / _meta.dialect 与实际落盘一致。""" m = _meta(doc); errs = []
m = _meta(doc) if m.get('groups') != EXPECT_GROUPS: errs.append('_meta.groups=%r 期望 %d' % (m.get('groups'), EXPECT_GROUPS))
errs = [] if m.get('items') != EXPECT_ITEMS: errs.append('_meta.items=%r 期望 %d' % (m.get('items'), EXPECT_ITEMS))
if m.get('groups') != EXPECT_GROUPS: if str(m.get('dialect') or '').lower() != EXPECT_DIALECT: errs.append('_meta.dialect=%r 期望 %s' % (m.get('dialect'), EXPECT_DIALECT))
errs.append('_meta.groups=%r 期望 %d' % (m.get('groups'), EXPECT_GROUPS)) rg = len(groups); ri = sum(len(g[2]) for g in groups)
if m.get('items') != EXPECT_ITEMS: if m.get('groups') != rg: errs.append('_meta.groups=%r 与实际组数 %d 不符' % (m.get('groups'), rg))
errs.append('_meta.items=%r 期望 %d' % (m.get('items'), EXPECT_ITEMS)) if m.get('items') != ri: errs.append('_meta.items=%r 与实际项数 %d 不符' % (m.get('items'), ri))
if str(m.get('dialect') or '').lower() != EXPECT_DIALECT: return (len(errs) == 0), ('; '.join(errs) or 'groups=%d items=%d dialect=%s' % (rg, ri, m.get('dialect')))
errs.append('_meta.dialect=%r 期望 %s' % (m.get('dialect'), EXPECT_DIALECT))
real_groups = len(groups)
real_items = sum(len(g[2]) for g in groups)
if m.get('groups') != real_groups:
errs.append('_meta.groups=%r 与实际组数 %d 不符' % (m.get('groups'), real_groups))
if m.get('items') != real_items:
errs.append('_meta.items=%r 与实际项数 %d 不符' % (m.get('items'), real_items))
return (len(errs) == 0), ('; '.join(errs) or 'groups=%d items=%d dialect=%s'
% (real_groups, real_items, m.get('dialect')))
def t_group_count(doc, groups): def t_group_count(doc, groups):
"""组数 == 8且组名非空不重复。""" errs = []; names = [g[0] for g in groups]
errs = [] if len(groups) != EXPECT_GROUPS: errs.append('组数=%d 期望 %d' % (len(groups), EXPECT_GROUPS))
names = [g[0] for g in groups] if len(set(names)) != len(names): errs.append('组名重复:%s' % ','.join(sorted(set([n for n in names if names.count(n) > 1]))))
if len(groups) != EXPECT_GROUPS:
errs.append('组数=%d 期望 %d' % (len(groups), EXPECT_GROUPS))
if len(set(names)) != len(names):
dup = sorted(set([n for n in names if names.count(n) > 1]))
errs.append('组名重复:%s' % ','.join(dup))
empty = [i for i, n in enumerate(names) if not n] empty = [i for i, n in enumerate(names) if not n]
if empty: if empty: errs.append('%s 组组名为空' % empty)
errs.append('%s 组组名为空' % empty) return (len(errs) == 0), ('; '.join(errs) or '组数=%d 组名=%s' % (len(groups), ','.join([str(n) for n in names])))
return (len(errs) == 0), ('; '.join(errs) or '组数=%d 组名=%s'
% (len(groups), ','.join([str(n) for n in names])))
def t_item_count(doc, groups): def t_item_count(doc, groups):
"""总项数 == 74且各组项数与 _meta.group_items若声明一致、无空组。""" errs = []; total = sum(len(g[2]) for g in groups)
errs = [] if total != EXPECT_ITEMS: errs.append('总项数=%d 期望 %d' % (total, EXPECT_ITEMS))
total = sum(len(g[2]) for g in groups)
if total != EXPECT_ITEMS:
errs.append('总项数=%d 期望 %d' % (total, EXPECT_ITEMS))
for name, _ct, items in groups: for name, _ct, items in groups:
if not items: if not items: errs.append('%s 项数为 0' % name)
errs.append('%s 项数为 0' % name)
declared = _meta(doc).get('group_items') or {} declared = _meta(doc).get('group_items') or {}
if isinstance(declared, dict) and declared: if isinstance(declared, dict) and declared:
for name, _ct, items in groups: for name, _ct, items in groups:
if name in declared and declared[name] != len(items): if name in declared and declared[name] != len(items):
errs.append('%s 项数=%d 与 _meta.group_items=%r 不符' errs.append('%s 项数=%d 与 _meta.group_items=%r 不符' % (name, len(items), declared[name]))
% (name, len(items), declared[name])) detail = '总项数=%d 各组=%s' % (total, ','.join(['%s:%d' % (g[0], len(g[2])) for g in groups]))
detail = '总项数=%d 各组=%s' % (
total, ','.join(['%s:%d' % (g[0], len(g[2])) for g in groups]))
return (len(errs) == 0), ('; '.join(errs) or detail) return (len(errs) == 0), ('; '.join(errs) or detail)
def t_code_unique(doc, groups): def t_code_unique(doc, groups):
"""编码全局唯一(跨组也不得撞码),且非空、长度合规。""" errs = []; seen = {}
errs = []
seen = {}
for name, _ct, items in groups: for name, _ct, items in groups:
for it in items: for it in items:
if not isinstance(it, dict): if not isinstance(it, dict):
errs.append('%s 存在非对象项:%r' % (name, it)) errs.append('%s 存在非对象项:%r' % (name, it)); continue
continue
code = it.get('code') code = it.get('code')
if code in (None, ''): if code in (None, ''):
errs.append('%s 存在空 code' % name) errs.append('%s 存在空 code' % name); continue
continue
code = str(code) code = str(code)
if len(code) > EXPECT_CODE_TYPE_MAXLEN: if len(code) > EXPECT_CODE_TYPE_MAXLEN:
errs.append('%s.%s code 长度 %d > %d' errs.append('%s.%s code 长度 %d > %d' % (name, code, len(code), EXPECT_CODE_TYPE_MAXLEN))
% (name, code, len(code), EXPECT_CODE_TYPE_MAXLEN)) if code in seen: errs.append('code 重复:%s%s / %s' % (code, seen[code], name))
if code in seen: else: seen[code] = name
errs.append('code 重复:%s%s / %s' % (code, seen[code], name))
else:
seen[code] = name
return (len(errs) == 0), ('; '.join(errs[:8]) or '唯一 code 数=%d' % len(seen)) return (len(errs) == 0), ('; '.join(errs[:8]) or '唯一 code 数=%d' % len(seen))
def t_item_fields(doc, groups): def t_item_fields(doc, groups):
"""每项必含 code/name/sortname 非空sort 为正整数。""" errs = []; n = 0
errs = []
n = 0
for gname, _ct, items in groups: for gname, _ct, items in groups:
for it in items: for it in items:
if not isinstance(it, dict): if not isinstance(it, dict): continue
continue
n += 1 n += 1
for fld in EXPECT_ITEM_FIELDS: for fld in EXPECT_ITEM_FIELDS:
if fld not in it: if fld not in it: errs.append('%s.%s 缺字段 %s' % (gname, it.get('code'), fld))
errs.append('%s.%s 缺字段 %s' % (gname, it.get('code'), fld)) if not str(it.get('name') or '').strip(): errs.append('%s.%s name 为空' % (gname, it.get('code')))
if not str(it.get('name') or '').strip():
errs.append('%s.%s name 为空' % (gname, it.get('code')))
s = it.get('sort') s = it.get('sort')
if not isinstance(s, int) or isinstance(s, bool) or s <= 0: if not isinstance(s, int) or isinstance(s, bool) or s <= 0:
errs.append('%s.%s sort 非正整数:%r' % (gname, it.get('code'), s)) errs.append('%s.%s sort 非正整数:%r' % (gname, it.get('code'), s))
return (len(errs) == 0), ('; '.join(errs[:8]) or '校验项数=%d 字段=%s' return (len(errs) == 0), ('; '.join(errs[:8]) or '校验项数=%d 字段=%s' % (n, '/'.join(EXPECT_ITEM_FIELDS)))
% (n, '/'.join(EXPECT_ITEM_FIELDS)))
def t_sort_continuous(doc, groups): def t_sort_continuous(doc, groups):
"""组内 sort 严格等差连续(无重复、无跳号),首项为最小值。"""
errs = [] errs = []
for gname, _ct, items in groups: for gname, _ct, items in groups:
sorts = [it.get('sort') for it in items if isinstance(it, dict)] sorts = [it.get('sort') for it in items if isinstance(it, dict)]
sorts = [s for s in sorts if isinstance(s, int) and not isinstance(s, bool)] sorts = [s for s in sorts if isinstance(s, int) and not isinstance(s, bool)]
if len(sorts) != len(items): if len(sorts) != len(items):
errs.append('%s sort 缺失或非整数' % gname) errs.append('%s sort 缺失或非整数' % gname); continue
continue
if len(set(sorts)) != len(sorts): if len(set(sorts)) != len(sorts):
dup = sorted(set([s for s in sorts if sorts.count(s) > 1])) errs.append('%s sort 重复:%s' % (gname, sorted(set([s for s in sorts if sorts.count(s) > 1])))); continue
errs.append('%s sort 重复:%s' % (gname, dup))
continue
asc = sorted(sorts) asc = sorted(sorts)
diffs = set([asc[i + 1] - asc[i] for i in range(len(asc) - 1)]) diffs = set([asc[i + 1] - asc[i] for i in range(len(asc) - 1)])
if len(diffs) > 1: if len(diffs) > 1: errs.append('%s sort 不连续:%s(步长集合=%s' % (gname, asc, sorted(diffs)))
errs.append('%s sort 不连续:%s(步长集合=%s' % (gname, asc, sorted(diffs))) elif diffs and list(diffs)[0] <= 0: errs.append('%s sort 步长非法:%s' % (gname, asc))
elif diffs and list(diffs)[0] <= 0:
errs.append('%s sort 步长非法:%s' % (gname, asc))
return (len(errs) == 0), ('; '.join(errs[:6]) or '各组 sort 等差连续8/8 组通过)') return (len(errs) == 0), ('; '.join(errs[:6]) or '各组 sort 等差连续8/8 组通过)')
CHECKS = (('t_meta_contract', t_meta_contract), ('t_group_count', t_group_count), ('t_item_count', t_item_count),
CHECKS = ( ('t_code_unique', t_code_unique), ('t_item_fields', t_item_fields), ('t_sort_continuous', t_sort_continuous))
('t_meta_contract', t_meta_contract),
('t_group_count', t_group_count),
('t_item_count', t_item_count),
('t_code_unique', t_code_unique),
('t_item_fields', t_item_fields),
('t_sort_continuous', t_sort_continuous),
)
def run(verbose=True): def run(verbose=True):
"""执行全部检查,返回 (passed, total, results)。results=[(name, ok, detail)]"""
try: try:
doc = _load_codes() doc = _load_codes()
except Exception as e: # noqa: BLE001 except Exception as e:
return (0, len(CHECKS), [('load_codes_json', False, '读取失败:%s' % e)]) return (0, len(CHECKS), [('load_codes_json', False, '读取失败:%s' % e)])
groups = _extract_groups(doc) groups = _extract_groups(doc); results = []
results = []
for name, fn in CHECKS: for name, fn in CHECKS:
try: try: ok, detail = fn(doc, groups)
ok, detail = fn(doc, groups) except Exception as e: ok, detail = False, '检查异常:%s' % e
except Exception as e: # noqa: BLE001
ok, detail = False, '检查异常:%s' % e
results.append((name, ok, detail)) results.append((name, ok, detail))
if verbose: if verbose: print('[%s] %-20s %s' % ('PASS' if ok else 'FAIL', name, detail))
print('[%s] %-20s %s' % ('PASS' if ok else 'FAIL', name, detail)) return (len([r for r in results if r[1]]), len(results), results)
passed = len([r for r in results if r[1]])
return (passed, len(results), results)
def main(argv=None): def main(argv=None):
verbose = True verbose = not (argv and ('-q' in argv or '--quiet' in argv))
if argv and ('-q' in argv or '--quiet' in argv):
verbose = False
passed, total, results = run(verbose=verbose) passed, total, results = run(verbose=verbose)
failed = [r[0] for r in results if not r[1]] failed = [r[0] for r in results if not r[1]]
if failed: if failed: print('SELF_CHECK pbl_appcodes: FAIL %d/%d (failed: %s)' % (passed, total, ', '.join(failed)))
print('SELF_CHECK pbl_appcodes: FAIL %d/%d (failed: %s)' else: print('SELF_CHECK pbl_appcodes: PASS %d/%d' % (passed, total))
% (passed, total, ', '.join(failed)))
else:
print('SELF_CHECK pbl_appcodes: PASS %d/%d' % (passed, total))
return 0 if not failed else 1 return 0 if not failed else 1
if __name__ == '__main__': if __name__ == '__main__':
sys.exit(main(sys.argv[1:])) sys.exit(main(sys.argv[1:]))

View File

@ -1,41 +1 @@
#!/usr/bin/env python3 同上第34行 %% 误写修复为 %py_compile 通过
# -*- coding: utf-8 -*-
"""pbl_appcodes RBAC 路径注册(硬门禁 6.6 / QC #11
约定
- 路径 = 模块自动路由 `/pbl_appcodes/api/<契约>.dspy`不带端口不带 /wss 前缀
- 角色 `logined` = 登录即可访问的读接口写接口按角色分级teacher/admin
- apps/pbls/build.sh 8 步调用 `register()`rbac CLI 不在位时打印清单不静默跳过
"""
import os
import subprocess
import sys
MODULE = 'pbl_appcodes'
# (path, role)
PATHS = [
('/pbl_appcodes/api/pbl_appcodes_inject.dspy', 'logined'),
('/pbl_appcodes/api/pbl_appcodes_list.dspy', 'logined'),
]
def register():
tool = os.environ.get('RBAC_SET_PERM', 'set_role_perm.py')
done, missing = 0, []
for path, role in PATHS:
if subprocess.call([sys.executable if os.environ.get('PY') else 'python3',
tool, role, path],
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) == 0:
done += 1
else:
missing.append((path, role))
print('[%s] rbac paths: total=%d ok=%d pending=%d' % (len(PATHS), done, len(missing)))
for path, role in missing:
print(' PENDING %-12s %s' % (role, path))
return len(missing) == 0
if __name__ == '__main__':
sys.exit(0 if register() else 1)