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

This commit is contained in:
agent.develop 2026-09-22 12:08:18 +08:00
parent 2da3cd39f8
commit fd6580a859

130
scripts/check_crud_json.py Normal file
View File

@ -0,0 +1,130 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""CRUD 定义json/*.json规范自检QC 退回意见 #4/#5/#6 的 CI 化)。
三条硬规则依据 crud-definition-spec / module-development-spec
1. **合法键**`params` 只允许规范定义的键`order_by` / `page_size` **自创键**
排序用 `sortby`数组分页由 sqlor `sqlPaging()` ns `page`/`rows` 处理
CRUD JSON 里硬编码 ORDER BY / 页大小属禁项且与 sortby 语义冲突
2. **alias 唯一**alias 只用于同一张表的多个逻辑视图两张**不同**的表共用同一
aliasxls2ui 会生成到同一个 `wwwroot/{alias}/` 目录互相覆盖
3. **文件名**`json/{table_name}.json` `{alias}.json`文件名与 tblname/alias 都不
匹配时人和工具都对不上号曾出现 evidence_pbl_artifact.json 这种两头不靠的命名
退出码 0=PASS1=FAIL不连库无第三方依赖可直接放 CI
"""
import io
import json
import os
import sys
ROOT = os.path.normpath(os.path.join(os.path.dirname(os.path.abspath(__file__)), '..'))
JSON_DIR = os.path.join(ROOT, 'json')
# crud-definition-spec 中 list/tree 视图 params 的合法键集合
ALLOWED_PARAM_KEYS = {
'sortby', 'logined_userorgid', 'logined_userid', 'data_filter',
'confidential_fields', 'editor', 'browserfields', 'editexclouded',
'edit_exclouded_fields', 'subtables', 'idField', 'textField',
'parentField', 'editable', 'uitype',
# editable 三 URL 规范上要求「params 顶层」xls2ddl 从顶层读),同时兼容
# 旧写法嵌在 editable 内 —— 两处都允许,但顶层必须存在(见 TOP_LEVEL_URL_KEYS
'new_data_url', 'update_data_url', 'delete_data_url',
# 展示/检索辅助键(规范内已文档化的可选项)
'filter_labels', 'filter_label', 'filter_title', 'data_url',
}
FORBIDDEN_PARAM_KEYS = {'order_by', 'page_size', 'page', 'rows', 'limit', 'offset',
'fields', 'joins', 'select_fields', 'grid', 'form', 'components'}
TOP_LEVEL_URL_KEYS = ('new_data_url', 'update_data_url', 'delete_data_url')
def main():
problems = []
alias_map = {}
if not os.path.isdir(JSON_DIR):
print('[pbl_evidence] no json/ directory')
return 1
for fn in sorted(os.listdir(JSON_DIR)):
if not fn.endswith('.json'):
continue
rel = 'json/' + fn
path = os.path.join(JSON_DIR, fn)
try:
d = json.loads(io.open(path, encoding='utf-8').read())
except Exception as exc:
problems.append('%s 不是合法 JSON%s' % (rel, exc))
continue
tbl = d.get('tblname')
if not tbl:
problems.append('%s 缺 tblname禁 tablename/name/type' % rel)
alias = d.get('alias')
if alias:
alias_map.setdefault(alias, set()).add(tbl or rel)
# 规则 3文件名必须等于 {tblname}.json 或 {alias}.json
stem = fn[:-5]
allowed_names = {tbl, alias} - {None}
# 多逻辑视图约定 alias = {table}_{view},故也接受以 tbl 开头的下划线名
if stem not in allowed_names and not any(
stem == t or stem.startswith(t + '_') for t in allowed_names):
problems.append('%s 文件名与 tblname=%s / alias=%s 均不匹配'
'(应为 {tblname}.json 或 {alias}.json' % (rel, tbl, alias))
params = d.get('params')
if not isinstance(params, dict) or not params:
problems.append('%s 缺 params 或为空' % rel)
continue
# 规则 1合法键 / 禁键
for key in params:
if key in FORBIDDEN_PARAM_KEYS:
problems.append('%s params.%s 是自创/禁用键(排序用 sortby 数组,'
'分页由 sqlor sqlPaging 用 ns.page/rows 处理)' % (rel, key))
elif key not in ALLOWED_PARAM_KEYS:
problems.append('%s params.%s 不在 crud-definition-spec 合法键内' % (rel, key))
# sortby 必须是数组
if 'sortby' in params and not isinstance(params['sortby'], list):
problems.append('%s params.sortby 必须是数组' % rel)
# editable顶层三 URL 必须齐xls2ddl 从 params 顶层读,嵌套写法会被静默忽略)
for key in TOP_LEVEL_URL_KEYS:
if key not in params:
problems.append('%s params 顶层缺 %s(嵌套在 editable 内会被 xls2ui 静默忽略,'
'生成默认 add_{table}.dspy' % (rel, key))
ed = params.get('editable')
if isinstance(ed, dict):
for key in TOP_LEVEL_URL_KEYS:
if key not in ed:
problems.append('%s params.editable 缺 %s' % (rel, key))
# browserfields.alters 的 uitype=code 项必须有 data 或 dataurl
bf = params.get('browserfields') or {}
for fname, spec in (bf.get('alters') or {}).items():
if not isinstance(spec, dict):
problems.append('%s alters.%s 不是对象' % (rel, fname))
continue
if spec.get('uitype') != 'code':
problems.append('%s alters.%s uitype 应为 "code"' % (rel, fname))
if not spec.get('data') and not spec.get('dataurl'):
problems.append('%s alters.%s 既无 data 也无 dataurl' % (rel, fname))
if spec.get('data_field'):
problems.append('%s alters.%s 用了已废弃的 data_field' % (rel, fname))
# 规则 2alias 不得被两张不同的表共用
for alias, tbls in sorted(alias_map.items()):
if len(tbls) > 1:
problems.append('alias "%s" 被多张表共用 %s —— xls2ui 会生成到同一 '
'wwwroot/%s/ 目录互相覆盖' % (alias, sorted(tbls), alias))
for line in problems:
print('FAIL ' + line)
print('[pbl_evidence] crud json: %s (%d 项问题)'
% ('PASS' if not problems else 'FAIL', len(problems)))
return 0 if not problems else 1
if __name__ == '__main__':
sys.exit(main())