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

This commit is contained in:
agent.develop 2026-09-21 13:14:12 +08:00
parent 6abc600f89
commit 35e272b01a

193
README.md
View File

@ -1,2 +1,195 @@
# pbl_evidence
PBL项目式学习的**产出物与学习证据**模块。它做两件事:维护学生产出物(`pbl_artifact`
以及把运行时产生的学习事件**幂等地**采集成学习证据(`pbl_evidence`)。
- 模块类型业务模块Python 包),**不是**独立部署单元 —— 没有 app.py、没有自己的端口、
没有 Dockerfile由宿主应用`apps/pbls`)调用 `load_pbl_evidence()` 挂载后运行。
- 交付里程碑M5a事件 → 证据 幂等采集 + 产出物 CRUD、M5b回放 / 增量游标 / 统计)。
- 下游M6 评估Rubric 加权打分)以本模块的证据表为唯一事实源。
## Features能力清单
| 能力 | 契约函数 | DSPY 路径 | 说明 |
|------|----------|-----------|------|
| 产出物新增 | `pbl_artifact_create` | `/pbl_evidence/api/pbl_artifact_create.dspy` | 自动补 `tenant_id` / `artifact_uid` / `created_at` |
| 产出物读取 | `pbl_artifact_read` | `/pbl_evidence/api/pbl_artifact_read.dspy` | 按 id 读,强制租户过滤 |
| 产出物更新 | `pbl_artifact_update` | `/pbl_evidence/api/pbl_artifact_update.dspy` | 白名单列更新,禁改 `tenant_id` |
| 产出物删除 | `pbl_artifact_delete` | `/pbl_evidence/api/pbl_artifact_delete.dspy` | 带归属校验 |
| 产出物列表 | `pbl_artifact_list` | `/pbl_evidence/api/pbl_artifact_list.dspy` | 分页 + 会话/蓝图/作者过滤 |
| 单事件落证据 | `pbl_evidence_collect` | `/pbl_evidence/api/pbl_evidence_collect.dspy` | 一条事件 → 一条证据,重复调用返回 `deduped=True` |
| **批量采集M5a 主能力)** | `pbl_evidence_collect_from_events` | `/pbl_evidence/api/pbl_evidence_collect_from_events.dspy` | 扫 `pbl_runtime_event` → 映射 → 幂等落库,返回 scanned/created/skipped/updated/ignored/failed + watermark |
| 证据列表 | `pbl_evidence_list` | `/pbl_evidence/api/pbl_evidence_list.dspy` | 学习者/产出物/会话/蓝图/类型/时间窗过滤 + 分页 |
| 证据统计 | `pbl_evidence_stats` | `/pbl_evidence/api/pbl_evidence_stats.dspy` | 证据类型分布,供 M6 与概览页 |
| 增量游标 | `pbl_evidence_watermark` | (无独立 dspy供 M5b 回放 / cron 与内部调用) | 已采集最大事件时间 + 总量3s 轮询兜底的起点 |
模块入口页:`/pbl_evidence/index.ui`bricks 卡片导航,聚合上述 9 个 .dspy
### 幂等语义(本模块的核心承诺)
重复采集**绝不**产生第二条证据,也**绝不**报错(学生侧无感):
1. **数据库层**:唯一索引 `uk_ev_dedup(tenant_id, source_event_id, evidence_type)` ——
同一租户下「同一来源事件 + 同一证据类型」最多一行。这是唯一可信的防重屏障。
2. **应用层双保险**:写入前先按 `(tenant_id, source_event_id, evidence_type)` 预查,
命中即 `skipped`(或 `update_existing=True` 时刷新 payload未命中才
`INSERT ... ON DUPLICATE KEY UPDATE`,因此并发/重放同时命中也不会抛 IntegrityError。
3. **去重键**`dedup_key = md5(tenant_id|source_event_id|evidence_type)`32 位定长,
与 VARCHAR(32) 严格对齐),便于外部对账与排障。
4. **事件 → 证据映射是配置化常量表**`evidence_map.EVENT_TO_EVIDENCE`37 条同义词映射 +
7 条忽略黑名单),未知事件类型降级为 `observation` 而不丢事件;不靠 SQL 硬编码、不靠猜。
### 硬约束 C-3上线顺序必须遵守
本模块批量采集读取的 `pbl_runtime_event` 表由 **scense_runtime 的 M11b** 建表并写入。
- **M5 不得早于 M11b 上线**,且 M5 与 M11 不得并行开发联调。
- 事件表缺失时 `collector.resolve_event_table()` 返回 None → 抛 `CollectError`
契约层转成明确错误码 `PBL_E_DB_UNAVAILABLE`**不会**静默返回空成功fail-closed便于运维定位
- 兼容层:事件表名按候选顺序发现 `pbl_runtime_event``runtime_event``pbl_event`
列名一律经 `information_schema` 发现,**不猜列名**(分区表/改名不会导致整链 500
## Data tables数据表
表定义在 `models/{table}.json`(四段式 summary/fields/indexes/codesDDL 由 `json2ddl` 生成,
建表 SQL 快照在 `sql/pbl_evidence.sql`
### `pbl_artifact` —— 学生产出物
| 列 | 类型 | 说明 |
|----|------|------|
| `id` | str(32) PK | 主键 |
| `tenant_id` | str(32) NOT NULL | 租户(强制打头,缺失 fail-closed |
| `artifact_uid` | str(64) | 业务唯一码,与 tenant 组成唯一键 |
| `session_id` / `blueprint_id` / `team_id` | str(32) | 会话 / 蓝图 / 团队归属 |
| `creator_id` | str(32) | 作者(学习者) |
| `title` | str(255) | 标题 |
| `artifact_type` | str(64) | 类型(编码 `pbl_artifact_type` |
| `content_json` | text | 产出物内容(序列化 JSON |
| `version_no` | str(32) | 版本号(内联,版本快照由蓝图侧维护) |
| `status` | str(32) | 状态(编码 `pbl_artifact_status` |
| `submitted_at` / `created_at` / `updated_at` | datetime | 时间与审计列(**应用层写入**sqlor 不自动填) |
索引:`uk_art_uid(tenant_id, artifact_uid)` 唯一;`idx_art_session` / `idx_art_blueprint` /
`idx_art_creator`(均以 `tenant_id` 打头)。
### `pbl_evidence` —— 学习证据
| 列 | 类型 | 说明 |
|----|------|------|
| `id` | str(32) PK | 主键 |
| `tenant_id` | str(32) NOT NULL | 租户 |
| `artifact_id` | str(32) | 归属产出物(`'0'` = 无强归属的观测类证据) |
| `evidence_type` | str(64) | 证据类型(编码 `pbl_evidence_type`8 类) |
| `source_event_id` | str(32) NOT NULL | 来源 runtime_event 事件 ID幂等锚点 |
| `session_id` / `learner_id` / `blueprint_id` | str(32) | 查询维度 |
| `payload_json` | text | 证据体(事件摘要序列化 JSON |
| `occurred_at` | datetime | 事件发生时间 |
| `dedup_key` | str(32) | `md5(tenant\|event\|type)` |
| `collect_batch_id` | str(32) | 采集批次(排障用) |
| `created_at` / `updated_at` | datetime | 审计列(应用层写入) |
索引:
- **`uk_ev_dedup` UNIQUE `(tenant_id, source_event_id, evidence_type)`** —— 幂等的唯一事实源。
含义:同一租户内,一条来源事件的一种证据类型只允许一行;重放、双通道(广播 + 3s 轮询兜底)
命中同一事件时第二次写入被唯一键吸收,不产生重复证据。
- `idx_ev_learner(tenant_id, learner_id, occurred_at)``idx_ev_artifact(tenant_id, artifact_id)`
`idx_ev_type_time(tenant_id, evidence_type, occurred_at)``idx_ev_batch(tenant_id, collect_batch_id)`
CRUD 定义在 `json/evidence_pbl_artifact.json` / `json/evidence_pbl_evidence.json`
`tblname` + `params``editable` 的 new/update/delete_data_url 指向 `wwwroot/api/` 自定义 dspy
## Installation
作为模块,它随宿主应用一起安装(不单独起服务):
```bash
# 1) 安装 Python 包(在宿主 pkgs/ 或机构 modules/ 下)
cd pbl_evidence && pip install -e .
# 2) 生成建表 DDL 并执行models/ 为四段式 JSON用 json2ddl
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 流程执行)
cd json && xls2ui -m ../models -o ../wwwroot pbl_evidence *.json
# 4) 登记 RBAC 权限(双层:模块层 + 中央宿主层,见下)
python3 scripts/load_path.py --check # 清单与 wwwroot 磁盘一致性核对(不连库)
RBAC_SET_PERM=/path/to/set_role_perm.py python3 scripts/load_path.py # 实际写权限
```
`scripts/load_path.py` 逐条显式登记(**禁通配符**`/pbl_evidence/index.ui`
`wwwroot/api/` 下全部 9 个 `.dspy`,角色均为 `logined`。中央宿主 `apps/pbls/scripts/load_path.py`
另有一份等价的硬编码兜底清单(不同代码路径,模块脚本 import 失败时仍能登记)——
双层 RBAC 回退,两层都必须在册。
自检(无需连库,可放 CI
```bash
python3 scripts/selfcheck_m5a.py # 三处同步 / RBAC 清单 vs 磁盘 / 模型四段式 / 幂等键语义
```
## Integration宿主挂载方式
模块只依赖基础包sqlor、ahserver ServerEnv、appPublic 工具)、`pbl_common`(租户上下文/
错误码/时间序列化)、以及自己的两张表。**不依赖任何宿主入口文件或宿主的 wwwroot 路径。**
```python
# apps/pbls/app/pbls.py
from ahserver.serverenv import ServerEnv
from appbase.init import load_appbase
from rbac.init import load_rbac
from bricks_for_python.init import load_pybricks
def get_module_dbname(m):
# 库名由宿主决定;本模块绝不硬编码 DBNAME
return MODULE_DBNAME_MAP.get(m, DEFAULT_DBNAME)
def init():
env = ServerEnv() # 必须在 load_rbac() 之前
env.get_module_dbname = get_module_dbname # ← 宿主必须先注册,否则本模块 fail-closed
load_appbase(); load_rbac(); load_pybricks()
from pbl_evidence.init import load_pbl_evidence
load_pbl_evidence() # 唯一集成点
```
**前置条件**:宿主必须在 `ServerEnv` 上注册 `get_module_dbname`。取不到库名时本模块直接抛
`PblError`fail-closed不会退化成查错库。租户上下文由 `pbl_common.api.tenant_id()` 提供,
所有读写强制带 `tenant_id`,缺失即报错。
函数注册三处同步(改一处必须改三处):
`pbl_evidence/api.py` 定义 → ② `pbl_evidence/__init__.py` 导出 → ③ `pbl_evidence/init.py`
`env.<name> = <name>`。漏 ② → import 期 ImportError漏 ③ → .dspy 调用 NameError。
新增对外路径还要同步 ④ `scripts/load_path.py`+ 中央 `apps/pbls/scripts/load_path.py`)。
## 目录结构
```
pbl_evidence/
├── pbl_evidence/ # Python 包(= 模块名)
│ ├── __init__.py # 导出(三处同步之 ②)
│ ├── init.py # load_pbl_evidence()(三处同步之 ③)
│ ├── api.py # 10 个契约函数(三处同步之 ①)
│ ├── 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/...
├── sql/pbl_evidence.sql # 建表 SQL 快照
├── scripts/load_path.py # RBAC 登记(模块层,禁通配符)+ --check 一致性核对
├── scripts/selfcheck_m5a.py # 离线自检(三处同步 / RBAC / 模型 / 幂等键)
├── skill/SKILL.md # 面向 agent 的模块技能文档
└── pyproject.toml
```
## 已知边界(如实说明)
- 批量采集(`pbl_evidence_collect_from_events`)的真实回放**未在本地环境跑通**:该环境
`information_schema.tables` 中不存在 `pbl_runtime_event`M11b 未上线),采集会按设计抛
`CollectError → PBL_E_DB_UNAVAILABLE`。已在 M11b 落库的联调环境验证前,此项标注为「待联调」。
- `pbl_evidence_watermark` 只作为契约函数注册(供 M5b/cron 内部调用),未单独暴露 .dspy
如需外部轮询调用,新增 .dspy 后必须同步登记 RBAC。