diff --git a/README.md b/README.md index 43ce285..0da7144 100644 --- a/README.md +++ b/README.md @@ -127,8 +127,24 @@ RBAC_SET_PERM=/path/to/set_role_perm.py python3 scripts/load_path.py # 实际 自检(无需连库,可放 CI): +> **占位说明**:离线自检脚本(三处同步 / RBAC 清单 vs 磁盘 / 模型四段式 / 幂等键语义) +> 由后续子任务 M5a-2 产出,**当前磁盘上尚不存在**,故本 README 不引用其文件名。 +> 在它落地前,用下列**当前磁盘上真实存在**的命令完成同等自检: + ```bash -python3 scripts/selfcheck_m5a.py # 三处同步 / RBAC 清单 vs 磁盘 / 模型四段式 / 幂等键语义 +# ① 依赖可安装(pyproject 只声明 sqlor,不拉基础包) +pip install . + +# ② 编码种子 JSON 合法 + parentid 与 models/*.json 的 codes.cond 一致 +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 +grep -c "^ env.pbl_" pbl_evidence/init.py + +# ④ RBAC 清单与 wwwroot 磁盘一致性核对(不连库) +python3 scripts/load_path.py --check ``` ## Integration(宿主挂载方式) @@ -179,9 +195,9 @@ pbl_evidence/ ├── models/ # pbl_artifact.json / pbl_evidence.json(四段式表定义) ├── json/ # CRUD 定义(tblname + params + editable) ├── wwwroot/ # index.ui + api/ 9 个 .dspy(自动路由 /pbl_evidence/...) +├── init/data.json # 编码种子(Format B:3 组 appcodes + appcodes_kv 子项) ├── sql/pbl_evidence.sql # 建表 SQL 快照 ├── scripts/load_path.py # RBAC 登记(模块层,禁通配符)+ --check 一致性核对 -├── scripts/selfcheck_m5a.py # 离线自检(三处同步 / RBAC / 模型 / 幂等键) ├── skill/SKILL.md # 面向 agent 的模块技能文档 └── pyproject.toml ``` diff --git a/init/data.json b/init/data.json new file mode 100644 index 0000000..94700c4 --- /dev/null +++ b/init/data.json @@ -0,0 +1,44 @@ +{ + "appcodes": [ + { + "parentid": "pbl_artifact_type", + "parentname": "PBL产出物类型", + "items": [ + {"k": "document", "v": "文档报告"}, + {"k": "code", "v": "程序代码"}, + {"k": "model", "v": "模型/场景"}, + {"k": "presentation", "v": "演示汇报"}, + {"k": "poster", "v": "海报展板"}, + {"k": "video", "v": "音视频作品"}, + {"k": "dataset", "v": "数据/素材集"}, + {"k": "other", "v": "其他"} + ] + }, + { + "parentid": "pbl_artifact_status", + "parentname": "PBL产出物状态", + "items": [ + {"k": "draft", "v": "草稿"}, + {"k": "submitted", "v": "已提交"}, + {"k": "reviewing", "v": "评审中"}, + {"k": "returned", "v": "退回修改"}, + {"k": "graded", "v": "已评分"}, + {"k": "archived", "v": "已归档"} + ] + }, + { + "parentid": "pbl_evidence_type", + "parentname": "PBL学习证据类型", + "items": [ + {"k": "artifact", "v": "产出物"}, + {"k": "assessment", "v": "测评作答"}, + {"k": "peer_review", "v": "同伴互评"}, + {"k": "reflection", "v": "反思日志"}, + {"k": "milestone", "v": "里程碑达成"}, + {"k": "collaboration", "v": "协作行为"}, + {"k": "resource", "v": "资源使用"}, + {"k": "observation", "v": "一般观测"} + ] + } + ] +} diff --git a/pyproject.toml b/pyproject.toml index b77b39e..b66ba59 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,12 @@ name = "pbl_evidence" version = "0.1.0" description = "产出物与证据幂等采集(M5a/M5b)" requires-python = ">=3.9" -dependencies = ["apppublic", "sqlor", "ahserver", "appbase", "rbac"] +# 只声明直接代码依赖(PyPI 可解析)。 +# 基础包 apppublic / ahserver / appbase / rbac 由宿主应用 build.sh 单独 git 安装, +# 不在 PyPI —— 写进来会让 `pip install .` 直接失败(module-development-spec 铁律)。 +# bricks_for_python:本模块 .py 未直接 import(前端仅 .ui/.dspy 静态资源),故不声明。 +# pbl_common:同机构 modules/ 下的兄弟模块,由宿主 pip install -e 装入,非 PyPI 包,故不声明。 +dependencies = ["sqlor"] [build-system] requires = ["setuptools>=61"] diff --git a/skill/SKILL.md b/skill/SKILL.md index dda67d4..3c375e0 100644 --- a/skill/SKILL.md +++ b/skill/SKILL.md @@ -1,26 +1,86 @@ -# pbl_evidence 模块技能(自动生成骨架 + 人工补充) +--- +name: pbl_evidence +description: 产出物与证据幂等采集模块(M5a/M5b)——pbl_artifact/pbl_evidence 两表 CRUD、事件→证据幂等采集、证据列表/统计/水位查询。改本模块前先读完「契约接口」「陷阱」两节。 +--- + +# pbl_evidence 模块技能 ## 定位 -产出物与证据幂等采集(M5a/M5b) +产出物(artifact)管理与学习证据(evidence)**幂等采集**。M5a 提供产出物 CRUD + 单事件/批量事件落证据; +M5b(回放/增量续采)直接复用本模块的采集器与水位接口。 ## 挂载 -`from pbl_evidence.init import load_pbl_evidence` → `load_pbl_evidence()`(应用 app/pbls.py init() 中按序调用) +```python +from pbl_evidence.init import load_pbl_evidence +load_pbl_evidence() # 宿主 apps/pbls 的 app/pbls.py init() 中按拓扑序调用 +``` +模块是 Python 包(**无 app.py / 无端口 / 无独立部署单元**),前端仅 `wwwroot/` 下 .ui/.dspy 静态资源。 ## 数据表(2 张) -- `pbl_artifact`:学生产出物(版本内联 version_no) -- `pbl_evidence`:学习证据(幂等采集,唯一索引防重) +- `pbl_artifact`:学生产出物(版本内联 `version_no`,唯一索引 `uk_art_uid(tenant_id, artifact_uid)`) +- `pbl_evidence`:学习证据(唯一索引 `uk_ev_dedup(tenant_id, source_event_id, evidence_type)` 防重) -## 契约接口(7 个,路径 `/pbl_evidence/api/.dspy`) -- `pbl_artifact_create` -- `pbl_artifact_read` -- `pbl_artifact_update` -- `pbl_artifact_delete` -- `pbl_artifact_list` -- `pbl_evidence_collect` -- `pbl_evidence_list` +表定义:`models/{table}.json`(四段式 summary/fields/indexes/codes);CRUD:`json/evidence_*.json`。 +编码种子:`init/data.json`(Format B,3 组 appcodes,见下「编码字典」)。 + +## 契约接口(10 个,与 `pbl_evidence/init.py` 契约段 env 注册一一对应) + +| # | 契约函数 | .dspy 路径(`/pbl_evidence/api/...`) | 说明 | +|---|----------|--------------------------------------|------| +| 1 | `pbl_artifact_create` | `pbl_artifact_create.dspy` | 新建产出物,自动补 `tenant_id`/`artifact_uid`/`version_no=1`/`status=draft`/`created_at` | +| 2 | `pbl_artifact_read` | `pbl_artifact_read.dspy` | 按 id 读,强制租户过滤 | +| 3 | `pbl_artifact_update` | `pbl_artifact_update.dspy` | 白名单列更新,禁改 `tenant_id`/`id` | +| 4 | `pbl_artifact_delete` | `pbl_artifact_delete.dspy` | 带归属校验的删除 | +| 5 | `pbl_artifact_list` | `pbl_artifact_list.dspy` | 分页 + 会话/蓝图/作者/类型/状态过滤 | +| 6 | `pbl_evidence_collect` | `pbl_evidence_collect.dspy` | 单事件 → 一条证据;重复调用返回 `deduped=True` | +| 7 | `pbl_evidence_collect_from_events` | `pbl_evidence_collect_from_events.dspy` | **M5a 主能力**:扫 `pbl_runtime_event` → 映射 → 幂等落库,返回 scanned/created/skipped/updated/ignored/failed + watermark | +| 8 | `pbl_evidence_list` | `pbl_evidence_list.dspy` | 学习者/产出物/会话/蓝图/类型/时间窗过滤 + 分页 | +| 9 | `pbl_evidence_stats` | `pbl_evidence_stats.dspy` | 证据类型分布统计,供 M6 评估与概览页 | +| 10 | `pbl_evidence_watermark` | **无独立 .dspy**(见下) | 返回某会话/蓝图已采集到的时间水位,供 M5b/cron 增量续采 | + +**watermark 的调用方式(第 10 条契约刻意不生成 .dspy)**: +- 内部调用:宿主/cron/M5b 通过 `ServerEnv().pbl_evidence_watermark(...)` 直调; +- 备选(若需走 HTTP):`pbl_evidence_stats.dspy` 返回值中**扩展** `watermark` 字段(stats 内嵌), + 当前实现 `collector.evidence_stats` 只返回 `total/by_type`,**尚未内嵌** —— 需要时改 stats 的 + 返回聚合(`res['watermark'] = await _watermark(tid)`)即可,不必新增 .dspy; +- 若将来需要前端直接轮询,再新增 `wwwroot/api/pbl_evidence_watermark.dspy`,并**同步登记** + `scripts/load_path.py` + 中央 `apps/pbls/scripts/load_path.py`(双层 RBAC),否则 403。 + +> 除上述 10 个对外契约外,`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` 9(watermark 无 .dspy)。 + +## 编码字典(init/data.json,Format B) +| parentid(≤22 字符) | 含义 | 子项 | +|---|---|---| +| `pbl_artifact_type` | 产出物类型 | document/code/model/presentation/poster/video/dataset/other(8) | +| `pbl_artifact_status` | 产出物状态 | draft/submitted/reviewing/returned/graded/archived(6) | +| `pbl_evidence_type` | 证据类型 | 取自 `evidence_map.EVIDENCE_TYPES` 8 类:artifact/assessment/peer_review/reflection/milestone/collaboration/resource/observation | + +`models/*.json` 的 `codes[].cond` 一律 `parentid='...'`(**绝不用 `id=`**),且三组 parentid 与 +`models/pbl_artifact.json`、`models/pbl_evidence.json` 中的 cond 完全一致。 ## 陷阱 -- 库名一律 `ServerEnv().get_module_dbname('pbl_evidence')`,禁止硬编码 DBNAME。 -- sqlor 只有 `C/U/D/R/I/sqlExe`;查询走 pbl_common.api 的 q_all/q_one(已适配)。 -- 所有读写强制带 `tenant_id`(pbl_common.api.tenant_id()),缺失即 fail-closed 报错。 -- 新增契约需同步三处:api.py 定义 + __init__.py 导出 + init.py env 注册 + scripts/load_path.py 路径。 +- 库名一律 `get_module_dbname('pbl_evidence')`(.dspy 直接调用;.py 用 `ServerEnv().get_module_dbname(...)`), + **禁止硬编码 DBNAME**;取不到库名直接抛 `PblError`(fail-closed),不退化成查错库。 +- sqlor 只有 `C/U/D/R/I/sqlExe`;查询统一走 `pbl_common.api` 的 `q_all/q_one`(已适配 sqlor 上下文)。 + `sor.I` 只接 1 个参数(表名由上下文决定),`sor.I('t', data)` 会 TypeError。 +- 所有读写强制带 `tenant_id`(`pbl_common.api.tenant_id()`),缺失即 fail-closed 报错。 +- `dedup_key` = md5(`tenant_id|source_event_id|evidence_type`),32 位定长,必须与 `uk_ev_dedup` 语义对齐; + 改唯一索引必须同步改 `evidence_map.build_dedup_key`。 +- 时间统一格式化为 `YYYY-MM-DD HH:MM:SS` 字符串(`normalize_dt`),避免 datetime/str 混用导致 + sqlor 占位符绑定失败。 +- 未知事件类型降级为 `observation`(不丢事件);`IGNORED_EVENTS` 白名单外置在 `evidence_map.py`, + 新增噪声事件改这里,别在 SQL 里写死过滤。 +- **函数注册三处同步**:① `api.py` 定义 → ② `__init__.py` 导出 → ③ `init.py` `env.=`; + 新增对外路径再加第 ④ 处 `scripts/load_path.py`(+ 中央宿主清单,双层 RBAC,禁通配符)。 + 漏 ② → import 期 ImportError;漏 ③ → .dspy NameError;漏 ④ → 403。 +- 批量采集依赖 M11b 的 `pbl_runtime_event`;该表不存在时按设计抛 + `CollectError → PBL_E_DB_UNAVAILABLE`(本地环境未联调,勿当成代码缺陷)。 +- `wwwroot/{alias}/`(xls2ui 生成物)是只读构建产物,改逻辑改 `json/*.json` + `models/*.json` 后重跑 build。 + +## 依赖 +- 直接:`sqlor`(见 `pyproject.toml`,仅此一项) +- 基础包(宿主 build.sh 安装,不写进 pyproject):`apppublic`、`ahserver`、`appbase`、`rbac` +- 兄弟模块:`pbl_common`(租户上下文/错误码/时间序列化/查询封装)