approve: [M11b-2c-A3] 业务实现修复(条件触发,仅当 A1 判定 PBL-RTX-0004 为业务缺陷)

This commit is contained in:
agent.develop 2026-09-22 12:08:12 +08:00
parent c051b71be3
commit 2da3cd39f8
4 changed files with 181 additions and 16 deletions

View File

@ -112,8 +112,10 @@ cd models && json2ddl mysql . > mysql.ddl.sql
mysql -h <db_host> -u<user> -p<pwd> <dbname> < mysql.ddl.sql
# (手工建表务必带 COLLATE utf8mb4_unicode_ci否则与 xls2ddl 产物 collation 混用报 1267
# 3) 生成 CRUD 前端(可选;已有手写 api/ dspy 时按 build.sh 流程执行
# 3) 生成 CRUD 前端(build.sh 步骤json/ 下两个文件按 tblname 命名
cd json && xls2ui -m ../models -o ../wwwroot pbl_evidence *.json
# -> 生成 wwwroot/pbl_artifact/ 与 wwwroot/pbl_evidence/ 两个 CRUD 目录(构建产物,
# 已在 .gitignore 中排除,部署后由 build.sh 重新生成,禁止手改其中文件)
# 4) 导入编码种子3 组 appcodespbl_artifact_type / pbl_artifact_status / pbl_evidence_type
# Format B由宿主 scripts/init_data.py或 dbloader读入 init/data.json 落 appcodes + appcodes_kv
@ -122,10 +124,25 @@ python3 -c "import json;d=json.load(open('init/data.json'));print('groups:',[g['
# 5) 登记 RBAC 权限(双层:模块层 + 中央宿主层,见下)
python3 scripts/load_path.py --check # 清单与 wwwroot 磁盘一致性核对(不连库)
RBAC_SET_PERM=/path/to/set_role_perm.py python3 scripts/load_path.py # 实际写权限
python3 scripts/load_path.py --check --generated # 部署机上xls2ui 已跑)连生成物一起核对
python3 scripts/load_path.py --sql | mysql -h <db_host> -u<user> -p<pwd> <rbac_dbname>
```
`scripts/load_path.py` 逐条显式登记(**禁通配符**`/pbl_evidence/index.ui`
`wwwroot/api/` 下全部 9 个 `.dspy`,角色均为 `logined`。中央宿主 `apps/pbls/scripts/load_path.py`
**permission / rolepermission 同步(新增路径必做)**`--sql` 输出与 PATHS 一一对应的幂等
INSERT —— `permission.permcode` = 路径本身,`rolepermission``role.rolecode='logined'`
反查 id 关联不含任何硬编码主键重复执行不产生脏数据。rbac CLIset_role_perm.py不在位时
这条管道就是部署说明给出的登记手段。**新增 .dspy 若忘记登记,登录后访问必 403**
QC 硬门禁 #3 的成因)。
`scripts/load_path.py` 逐条显式登记(**禁通配符**),角色均为 `logined`
- 手写资源 13 条:`/pbl_evidence/index.ui` + `wwwroot/api/` 下全部 **12**`.dspy`
(产出物 CRUD 5 + 证据采集 2 + 查询/统计 2 + CRUD 适配 3
`pbl_artifact_options` / `pbl_evidence_update` / `pbl_evidence_delete`
- CRUD 生成物 12 条(`GENERATED_PATHS``/pbl_evidence/{pbl_artifact,pbl_evidence}` 目录 +
`index.ui` + `get_/add_/update_/delete_*.dspy`(每 alias 6 条)。这些是 xls2ui 产物、开发仓库
不落盘,故 `--check` 默认不计 stale部署后加 `--generated` 再核。
中央宿主 `apps/pbls/scripts/load_path.py`
另有一份等价的硬编码兜底清单(不同代码路径,模块脚本 import 失败时仍能登记)——
双层 RBAC 回退,两层都必须在册。
@ -143,9 +160,16 @@ pip install .
python3 -c "import json;d=json.load(open('init/data.json'));print([g['parentid'] for g in d['appcodes']])"
grep -o "parentid='[a-z_]*'" models/*.json | sort -u
# ③ 三处同步api.py 契约定义数 == init.py env 契约注册数(对外契约应为 10
grep -c "^async def pbl_" pbl_evidence/api.py
# ③ 三处同步api.py(10) + crud_api.py(3) 契约定义 == __init__.py 导出 == init.py env 注册(共 13
grep -c "^async def pbl_" pbl_evidence/api.py pbl_evidence/crud_api.py
grep -c "^ env.pbl_" pbl_evidence/init.py
grep -rn 'pbl_evidence_update' pbl_evidence/ --include='*.py' # 定义/导出/__all__/import/env 全命中
# ③b CRUD 端点存在性json/*.json 引用的每个 ../api/*.dspy 必须真实落盘(否则编辑/删除/下拉 404
python3 scripts/check_crud_endpoints.py
# ③c CRUD JSON 合法键 + alias 唯一性自检
python3 scripts/check_crud_json.py
# ④ RBAC 清单与 wwwroot 磁盘一致性核对(不连库)
python3 scripts/load_path.py --check
@ -192,13 +216,14 @@ pbl_evidence/
├── pbl_evidence/ # Python 包(= 模块名)
│ ├── __init__.py # 导出(三处同步之 ②)
│ ├── init.py # load_pbl_evidence()(三处同步之 ③)
│ ├── api.py # 10 个契约函数(三处同步之 ①)
│ ├── api.py # 10 个业务契约函数(三处同步之 ①)
│ ├── crud_api.py # 3 个 CRUD 框架适配契约options/update/delete三处同步之 ①)
│ ├── collector.py # 事件扫描 → 映射 → 幂等落库list/stats/watermark
│ ├── evidence_map.py # 纯函数层事件→证据类型映射、dedup_key、时间规范化
│ └── db.py # 数据访问层get_module_dbname、列名发现、q_all/q_one/q_exec
├── models/ # pbl_artifact.json / pbl_evidence.json四段式表定义
├── json/ # CRUD 定义tblname + params + editable
├── wwwroot/ # index.ui + api/ 9 个 .dspy自动路由 /pbl_evidence/...
├── wwwroot/ # index.ui + api/ 12 个 .dspy自动路由 /pbl_evidence/...
├── init/data.json # 编码种子Format B3 组 appcodes + appcodes_kv 子项)
├── sql/pbl_evidence.sql # 建表 SQL 快照
├── scripts/load_path.py # RBAC 登记(模块层,禁通配符)+ --check 一致性核对

View File

@ -4,10 +4,30 @@
为什么单独成文件不改 api.py
------------------------------
api.py 已有 390 行契约面产出物 CRUD + 证据幂等采集本文件只补
``json/evidence_pbl_evidence.json`` ``params.editable`` 三个 URL
``browserfields.alters[].dataurl`` 所需的 4 个后端函数保持 api.py 不动
职责清晰api.py = 业务契约crud_api.py = CRUD 框架适配
api.py 已有 390 行业务契约面产出物 CRUD + 证据幂等采集本文件只补
``json/pbl_evidence.json`` ``params.editable`` update/delete 两个 URL
``browserfields.alters[].dataurl`` 所需的 **3 **后端函数数量以本文件
``async def`` 为准pbl_artifact_options / pbl_evidence_update / pbl_evidence_delete
保持 api.py 不动职责清晰api.py = 业务契约crud_api.py = CRUD 框架适配
与既有 api.py 契约的分工避免误解为artifact CRUD 也走本文件
----------------------------------------------------------------
- ``json/pbl_artifact.json`` new/update/delete_data_url 指向
``wwwroot/api/pbl_artifact_{create,update,delete}.dspy``这三个薄包装委托
**api.py** ``pbl_artifact_create/update/delete`` artifact_uid 自动生成
version_no 递增租户归属校验**不经过本文件**本文件只为 artifact 提供
下拉数据源 ``pbl_artifact_options``只读 id/label 两列
- ``json/pbl_evidence.json`` new_data_url 指向 ``pbl_evidence_collect.dspy``
委托 api.py 的幂等采集服务端权威只增不改update/delete 才委托本文件
注册位置三处同步缺一必 NameError
------------------------------------
定义本文件 导出``pbl_evidence/__init__.py``
``from pbl_evidence.crud_api import ...`` + ``__all__``
挂载``pbl_evidence/init.py`` ``load_pbl_evidence()``
``env.pbl_artifact_options / env.pbl_evidence_update / env.pbl_evidence_delete``
对外契约清单见 ``skill/SKILL.md``契约接口表第 11~13 CRUD 适配契约
RBAC 路径见 ``scripts/load_path.py`` PATHS
规范依据crud-definition-spec / module-development-spec
--------------------------------------------------------

View File

@ -0,0 +1,105 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""CRUD 端点存在性自检QC 硬门禁 #2 的 CI 化)。
json/*.json `params.editable` / `new|update|delete_data_url` /
`browserfields.alters[].dataurl` 引用的每个 `../api/xxx.dspy`都必须在
`wwwroot/api/xxx.dspy` 真实落盘 少一个文件前端点了就是 404
用法
python3 scripts/check_crud_endpoints.py # 只核对手写 api/ 端点
python3 scripts/check_crud_endpoints.py --generated # 连 xls2ui 生成物一起核(部署机用)
退出码0=全部命中1=存在缺失端点不连库不依赖第三方包可直接放 CI
"""
import argparse
import io
import json
import os
import re
import sys
ROOT = os.path.normpath(os.path.join(os.path.dirname(os.path.abspath(__file__)), '..'))
JSON_DIR = os.path.join(ROOT, 'json')
API_DIR = os.path.join(ROOT, 'wwwroot', 'api')
# 匹配 ../api/xxx.dspy 与 /pbl_evidence/api/xxx.dspy 两种写法
URL_RE = re.compile(r'api/([A-Za-z0-9_]+\.dspy)')
def referenced_endpoints():
"""返回 {dspy 文件名: [引用它的 json 文件, ...]}。"""
refs = {}
if not os.path.isdir(JSON_DIR):
return refs
for fn in sorted(os.listdir(JSON_DIR)):
if not fn.endswith('.json'):
continue
path = os.path.join(JSON_DIR, fn)
try:
json.loads(io.open(path, encoding='utf-8').read())
except Exception as exc:
refs.setdefault('__INVALID_JSON__:%s' % fn, []).append(str(exc))
continue
text = io.open(path, encoding='utf-8').read()
for name in URL_RE.findall(text):
refs.setdefault(name, []).append('json/' + fn)
return refs
def generated_endpoints():
"""xls2ui 为每个 CRUD 定义生成 wwwroot/{alias}/{get,add,update,delete}_{alias}.dspy。
注意落点是 **wwwroot/{alias}/** 子目录不是 wwwroot/api/那是构建产物目录
开发仓库被 .gitignore 排除只有部署机跑过 build.sh 后才存在
"""
out = []
for fn in sorted(os.listdir(JSON_DIR)):
if not fn.endswith('.json'):
continue
d = json.loads(io.open(os.path.join(JSON_DIR, fn), encoding='utf-8').read())
alias = d.get('alias') or d.get('tblname')
if not alias:
continue
for op in ('get', 'add', 'update', 'delete'):
out.append(os.path.join(alias, '%s_%s.dspy' % (op, alias)))
out.append(os.path.join(alias, 'index.ui'))
return out
def main():
ap = argparse.ArgumentParser(description='verify CRUD-referenced .dspy endpoints exist')
ap.add_argument('--generated', action='store_true',
help='also check xls2ui-generated get_/add_/update_/delete_{alias}.dspy '
'(build artifacts; run on the deployment host after build.sh)')
args = ap.parse_args()
refs = referenced_endpoints()
bad = []
for name, srcs in sorted(refs.items()):
if name.startswith('__INVALID_JSON__:'):
bad.append('%s 不是合法 JSON%s' % (name, srcs[0]))
continue
if not os.path.exists(os.path.join(API_DIR, name)):
bad.append('%s 缺失(被 %s 引用)' % (name, ', '.join(srcs)))
gen_missing = []
if args.generated:
for name in generated_endpoints():
if not os.path.exists(os.path.join(ROOT, 'wwwroot', name)):
gen_missing.append(name)
print('[pbl_evidence] json/ 引用的端点: %d' % len(refs))
for line in bad:
print('MISSING ' + line)
if args.generated:
print('[pbl_evidence] xls2ui 生成物端点: %d' % len(generated_endpoints()))
for name in gen_missing:
print('MISSING(gen) wwwroot/' + name)
ok = not bad and not gen_missing
print('[pbl_evidence] crud endpoints: %s' % ('PASS' if ok else 'FAIL'))
return 0 if ok else 1
if __name__ == '__main__':
sys.exit(main())

View File

@ -20,10 +20,12 @@ load_pbl_evidence() # 宿主 apps/pbls 的 app/pbls.py init() 中按拓扑序
- `pbl_artifact`:学生产出物(版本内联 `version_no`,唯一索引 `uk_art_uid(tenant_id, artifact_uid)`
- `pbl_evidence`:学习证据(唯一索引 `uk_ev_dedup(tenant_id, source_event_id, evidence_type)` 防重)
表定义:`models/{table}.json`(四段式 summary/fields/indexes/codesCRUD`json/evidence_*.json`
表定义:`models/{table}.json`(四段式 summary/fields/indexes/codesCRUD`json/pbl_artifact.json` +
`json/pbl_evidence.json`(文件名 = tblname**不要**给两张不同的表共用同一 alias
xls2ui 会生成到同一目录互相覆盖)。
编码种子:`init/data.json`Format B3 组 appcodes见下「编码字典」
## 契约接口10 个,与 `pbl_evidence/init.py` 契约段 env 注册一一对应)
## 契约接口13 个,与 `pbl_evidence/init.py``load_pbl_evidence()` env 注册一一对应)
| # | 契约函数 | .dspy 路径(`/pbl_evidence/api/...` | 说明 |
|---|----------|--------------------------------------|------|
@ -37,6 +39,9 @@ load_pbl_evidence() # 宿主 apps/pbls 的 app/pbls.py init() 中按拓扑序
| 8 | `pbl_evidence_list` | `pbl_evidence_list.dspy` | 学习者/产出物/会话/蓝图/类型/时间窗过滤 + 分页 |
| 9 | `pbl_evidence_stats` | `pbl_evidence_stats.dspy` | 证据类型分布统计,供 M6 评估与概览页 |
| 10 | `pbl_evidence_watermark` | **无独立 .dspy**(见下) | 返回某会话/蓝图已采集到的时间水位,供 M5b/cron 增量续采 |
| 11 | `pbl_artifact_options` | `pbl_artifact_options.dspy` | **CRUD 适配**`crud_api.py`):产出物下拉数据源,返回**裸数组** `[{value,text}]`,异常降级 `[]`;供 `json/pbl_evidence.json``alters.artifact_id.dataurl` |
| 12 | `pbl_evidence_update` | `pbl_evidence_update.dspy` | **CRUD 适配**`crud_api.py`):人工修订证据,强制 `tenant_id` 归属校验;改唯一索引三元组时同步重算 `dedup_key`;供 `editable.update_data_url` |
| 13 | `pbl_evidence_delete` | `pbl_evidence_delete.dspy` | **CRUD 适配**`crud_api.py`):按 id/ids 删除证据,逐条校验归属;供 `editable.delete_data_url` |
**watermark 的调用方式(第 10 条契约刻意不生成 .dspy**
- 内部调用:宿主/cron/M5b 通过 `ServerEnv().pbl_evidence_watermark(...)` 直调;
@ -46,10 +51,20 @@ load_pbl_evidence() # 宿主 apps/pbls 的 app/pbls.py init() 中按拓扑序
- 若将来需要前端直接轮询,再新增 `wwwroot/api/pbl_evidence_watermark.dspy`,并**同步登记**
`scripts/load_path.py` + 中央 `apps/pbls/scripts/load_path.py`(双层 RBAC否则 403。
> 除上述 10 个对外契约外,`init.py` 还注册了 4 个**内部能力**(供 M5b 回放 / M6 评估复用,不算对外契约、
> 第 11~13 条是 **CRUD 框架适配契约**(定义在 `pbl_evidence/crud_api.py`,不改 `api.py`
> 同样走「三处同步」:`crud_api.py` 定义 → `__init__.py` 导出 → `init.py` `env.xxx = xxx`
> 并各自有 `wwwroot/api/*.dspy` 薄包装与 `scripts/load_path.py` 登记。漏任一处 = dspy 调用
> NameError / 端点 404 / 登录后 403QC 硬门禁三条,实测都踩过)。
> `json/pbl_artifact.json` 的 new/update/delete 指向既有 `pbl_artifact_{create,update,delete}.dspy`
> (委托 `api.py` 第 1/3/4 条契约),**不经过 crud_api**。
> 除上述 13 个对外契约外,`init.py` 还注册了 4 个**内部能力**(供 M5b 回放 / M6 评估复用,不算对外契约、
> 无 .dspy`pbl_collect_evidence_from_events``pbl_resolve_event_table``pbl_evidence_types`
> `pbl_event_to_evidence_map`。核对口径:`api.py` 契约定义 10 == `__init__.py` 契约导出 10
> == `init.py` 契约 env 注册 10 == `wwwroot/api/*.dspy` 9watermark 无 .dspy
> `pbl_event_to_evidence_map`。核对口径:
> `api.py` 契约定义 10 + `crud_api.py` 适配定义 3 == `__init__.py` 契约导出 13
> == `init.py` 契约 env 注册 13 == `wwwroot/api/*.dspy` 12watermark 无 .dspy
> 自查命令:`grep -rn 'pbl_evidence_update' pbl_evidence/ --include='*.py'`
> 必须命中 定义(crud_api.py) / __init__.py import / __all__ / init.py import / env 注册。
## 编码字典init/data.jsonFormat B
| parentid≤22 字符) | 含义 | 子项 |