12 KiB
12 KiB
工作日志 — pbl_domain_ext(M8)QC 退回整改轮 · 2026-09-18
1. 范围与背景
- 仓库/模块:
modules/pbl_domain_ext(PBL 基础域薄扩展,M8,Wave 4) - 任务:
[M8] 基础域薄扩展 world/scene/entity(task_kind=new_dev,PM 派发,带 agent.qc 6 条退回意见) - 设计权威:
projects/pbls/docs/01-design/modules/pbl_domain_ext.md、data-model.md§J、projects/pbls/pbls_spec.json - 本轮性质:按 QC 退回意见重做交付件(非 bug 修复,未走 bug 状态机)
2. QC 退回意见逐条整改
| # | QC 问题 | 整改动作 | 验证证据 |
|---|---|---|---|
| 1 | 表结构偏离设计:实交 3 张 pbl_world_ref/pbl_scene_ref/pbl_entity_ref,无 pbl_domain_ref,破坏 36 表总账 |
删除 3 张偏离表模型;按 data-model.md §J1 建 唯一自有表 pbl_domain_ref(UNIQUE(tenant_id,ref_type,ref_id) + 4 个辅助索引);sql/pbl_domain_ext.sql 重写为单表 DDL;init.py:OWN_TABLES=['pbl_domain_ref'] |
models/ 仅 1 个 json;测试 test_own_tables_single_and_matches_models、test_model_json_four_sections(断言 UNIQUE 三元组)通过;表总账回归 tables_by_module.pbl_domain_ext=1、tables_total=36,无需修订 spec/data-model(实现向设计对齐,未走设计变更) |
| 2 | 设计 §3 的 13 个契约接口无实现证据;init.py 自述契约名(pbl_*_ref_upsert/_list/pbl_domain_scope_resolve)与设计不符 |
api.py 逐条实现 13 个接口(§3.1×5 + §3.2×5 + §3.3×3),函数名与设计完全一致;init.py 注册 env.pbl_<name> 与 env.<name> 双名;新增 get_contract_map() 输出「接口→实现位置→dspy 端点」映射;CONTRACT_INTERFACES 常量固化 13 项清单 |
测试 test_contract_13_interfaces_all_callable、test_contract_map_covers_dspy_files、test_load_module_registers_env_functions、test_package_exports_match_init 通过;映射表见 skill/SKILL.md §3 与 README.md |
| 3 | 表定义四段式不合规:summary 为字符串而非数组 |
models/pbl_domain_ref.json 改为 {"summary":[表名,中文名,主键,说明],"fields":[...],"indexes":[...],"codes":[...]},summary 为 4 元素数组(array primary) |
测试 test_model_json_four_sections 断言 isinstance(summary, list) 且 summary[0]=='pbl_domain_ref'、主键唯一、UNIQUE 索引存在 |
| 4 | OWN_TABLES 自相矛盾:声明 pbl_tenant/pbl_class/pbl_team 但 models/ 无定义 |
这 3 张表属 pbl_governance 模块,不属本任务范围 → 从 OWN_TABLES 删除;同时删除误建的 models/pbl_tenant.json、pbl_class.json、pbl_team.json 与 json/domain_ext_pbl_*.json、pbl_tenant_upsert/pbl_class_save/pbl_class_list/pbl_team_save/pbl_team_list 等越界 dspy;团队成员改为只读引用 pbl_governance.pbl_team_member(api._fetch_team_members,表缺失降级空列表) |
OWN_TABLES == ['pbl_domain_ref'] 与 models/ 一一对应(测试断言);init.py:READONLY_FOREIGN_TABLES=['pbl_team_member'] 明示只读;测试 test_list_teams_by_class_members_from_governance、test_list_teams_without_governance_table_degrades 通过 |
| 5 | 契约端点缺失:无任何 wwwroot/*.dspy |
新建 13 个 wwwroot/api/*.dspy 薄封装(与契约一一对应)+ wwwroot/index.ui 入口页;全部遵守 dspy 规范:无 import、显式 return、debug() 带文件名前缀、转发全部客户端参数(filters/ext_json 兼容 JSON 串与平铺字段,不硬编码 dispatch)、错误码→http_status 映射(403/404/409/400/500) |
dspy 审计脚本:dspy files=13 audit_issues=NONE(无 import / 有 return / debug 带文件名 / 无 print);test_contract_map_covers_dspy_files 断言每个契约的 dspy 文件真实存在 |
| 6 | RBAC 路径注册缺失:scripts/ 无变更,新端点上线即 403 |
重写 scripts/load_path.py:API_PATHS(13) + UI_PATHS(1) 逐条显式注册(禁通配符)、ROLE_GRANTS 按 admin/pbl_teacher/pbl_student 分权(写操作仅教师/管理员,只读查询含学生)、register(env) 幂等注册、selfcheck() 自检、__main__ 可执行校验 |
python3 scripts/load_path.py → paths=14 api=13 ui=1 / OK 全部路径显式注册、无通配符、角色授权齐备;测试 test_load_path_registers_all_dspy 断言 wwwroot 下每个 .dspy 都已注册且无 * |
3. 本轮产出文件
新增/重写(实现)
pbl_domain_ext/api.py(13 契约实现,~600 行)pbl_domain_ext/init.py(load_pbl_domain_ext+get_contract_map+OWN_TABLES)pbl_domain_ext/__init__.py(导出 13 契约 + load 函数,三处同步注册之 ②)pbl_domain_ext/base.py(复用基表只读投影:列名运行时探测、批量取行、子表遍历、悬挂过滤)pbl_domain_ext/db.py(SqlorAdapter生产 /SqliteAdapter测试;${name}$→:name转换;resolve_dbname走get_module_dbname)pbl_domain_ext/errors.py(PBL_E_* 错误码 + HTTP 映射,优先复用pbl_common,缺失时本地等价实现)
新增/重写(数据与契约)
models/pbl_domain_ref.json(四段式,summary 数组)json/pbl_domain_ref.json(CRUD 浏览定义 tblname+params)sql/pbl_domain_ext.sql(单表 DDL,对齐 §J1)wwwroot/api/*.dspy× 13、wwwroot/index.uiscripts/load_path.py
删除(偏离设计/越界产出)
models/pbl_world_ref.json、pbl_scene_ref.json、pbl_entity_ref.json(QC #1)models/pbl_tenant.json、pbl_class.json、pbl_team.json+json/domain_ext_pbl_*.json× 3(QC #4,属 pbl_governance)wwwroot/api/pbl_tenant_upsert.dspy、pbl_class_save/list.dspy、pbl_team_save/list.dspy、pbl_domain_materialize_game_definition.dspy(越界契约)pbl_domain_ext/assoc.py、sql/pbl_domain_ext_assoc.sql、tests/test_assoc.py(旧三表方案残留)
测试与文档
tests/fake_db.py(sqlite 夹具:pbl_domain_ref + 三张基表替身 + pbl_team_member;raw_sql/scalar辅助)tests/test_domain_ref.py(62 用例)skill/SKILL.md、README.md、pyproject.toml、本工作日志
4. 关键技术决策
- 实现向设计对齐,不走设计变更:QC #1 给了两条路(建单表 / 先改设计)。选建
pbl_domain_ref单表——设计 §2、data-model.md§J1、pbls_spec.json(1 表 / 36 总账)三处一致,改设计成本高且无收益。 ref_type + ref_id泛化关联替代三张同构表:一张表承载 world/scene/entity 三类关联,UNIQUE(tenant_id,ref_type,ref_id)保证同租户同记录唯一;idx(ref_type,ref_id)支撑悬挂引用一致性左连接。- 租户隔离用「白名单先行」:基表无 tenant_id → 先查
pbl_domain_ref拿本租户 ref_id 白名单,再按白名单只读回查基表;未绑定即不可见(不是"查到再判权",从根上杜绝跨租户读基表)。 - 基表列名运行时探测:复用模块版本可能改列名(
world_id/worldid/parent_id),base.resolve_column走information_schema→ 退化PRAGMA,避免写死列名导致上线即错。 check_ref_access返回 bool 不抛异常:运行时(pbl_runtime_ext 共享会话)热路径每帧可能调用,异常开销大且调用方要 try 包裹;入参非法/无关联/不匹配/悬挂统一 False。但基础设施异常(基表探测失败)放行,避免因 DB 抖动把合法用户判为越权。- 软删而非物理删:
unbind_ref置is_deleted=1,保留审计痕迹;重绑走"复活"分支(幂等),避免 UNIQUE 冲突。 - db 适配层双后端:生产
SqlorAdapter(只用sor.R/sor.sqlExe,结果在async with外 return);测试SqliteAdapter(同步驱动包 async),使 62 用例可离线跑真实 SQL 逻辑而不依赖 MySQL/宿主挂载。 pbl_team_member只读降级:团队成员属 pbl_governance,本模块不建表不写入;优先探宿主 read 契约,退化只读 SELECT,表不存在则返回空 members(list_teams_by_class仍正常返回分组)。
5. 验证记录
| 验证项 | 命令 | 结果 |
|---|---|---|
| Python 语法 | python3 -m py_compile pbl_domain_ext/*.py scripts/load_path.py tests/*.py |
PYCOMPILE OK |
| 单元/契约测试 | python3 tests/test_domain_ref.py |
Ran 62 tests — OK(0 fail / 0 error) |
| RBAC 路径自检 | python3 scripts/load_path.py |
paths=14 api=13 ui=1 / OK 无通配符、授权齐备 |
| dspy 审计 | grep 脚本(import/return/debug 前缀/print) | dspy files=13 audit_issues=NONE |
| JSON 可解析 | index.ui / models / json | JSON parse OK |
| 假 sqlor API 扫描 | grep sor.save/list/insert/query/delete/one |
NONE |
| 表总账对账 | models/ 文件数 vs OWN_TABLES vs spec |
1 = 1 = tables_by_module.pbl_domain_ext=1(36 总账不破) |
测试覆盖要点:13 契约正常路径 + 错误分支(VALIDATION/NOT_FOUND/DUPLICATE/FORBIDDEN)、跨租户隔离(US-21)、分页与过滤、悬挂引用标记与过滤、软删与重绑幂等、update_ref 字段白名单、团队成员降级、薄扩展铁律(基表行数/列集合前后不变、禁止基表出现 tenant_id 列)、三处同步注册、dspy 端点齐备、load_path 注册齐备。
环境受限未跑项(如实记录):
- 未启动 pbls 应用做 HTTP 端到端(宿主
apps/pbls需 ServerEnv/MySQL/rbac 全量挂载,本地无该环境)→ 以 dspy 静态审计 + 契约层单测替代; - 未在真实 MySQL 上执行
sql/pbl_domain_ext.sql(无库连接)→ DDL 逐列对齐data-model.md§J1,并在 sqlite 建等价表跑通全部 SQL 逻辑; pbl_common/pbl_governance未挂载路径走的是本地降级分支(errors 本地实现、members 空列表),已各有测试覆盖。
6. 当前分支/提交状态
- 分支:
main(modules/pbl_domain_ext) - 本轮改动已落盘工作区;git 收口由引擎在 PM 审核通过后统一执行,本日志不自称已 commit/push(以引擎回填的「git 收口核验」段为准)。
6. 测试执行证据(QC 历史 #3:仅 py_compile 无执行记录 → 本轮补实测)
- 执行命令:
python3 -m unittest discover -s tests -p 'test_*.py' -v(环境无 pytest,site-packages 只读无法安装;unittest 为标准库等价执行器,用例本身即 unittest.TestCase,非降级替代) - 实测结果:Ran 62 tests — OK(0 failed / 0 error),完整逐条输出落盘
docs/test-run-2026-09-18.txt - 覆盖维度(对应 QC 要求的「租户 / 权限 / 正常 / 异常」四类用例):
用例类 覆盖点 TestBindRefbind_ref 正常绑定、重复绑定 PBL_E_DUPLICATE、基表记录不存在PBL_E_NOT_FOUND、ref_type 非法PBL_E_VALIDATION、ext_json JSON 串与 dict 双形态TestUnbindGetUpdateunbind 软删(is_deleted=1)、unbind 不存在、get_ref 正常、get_ref 跨租户返回 NOT_FOUND(不泄露存在性)、update_ref 字段白名单 TestListRefsfilters 组合(ref_type/blueprint_id/class_id/team_id)、分页 total/items、悬挂 ref 左连接过滤 TestTenantIsolationQuerieslist_worlds_by_tenant 仅返回本租户绑定、无绑定返回空、按 class 过滤、list_scenes 跨租户 PBL_E_FORBIDDEN、仅绑定 scene 可见、悬挂过滤TestTeamContractslist_teams_by_class 成员聚合、bind_team_to_world 重复/不存在、get_team_worlds TestCheckRefAccess租户匹配、班级/团队维度匹配、跨租户拒绝 TestThinExtensionInvariants13 契约全部 callable、contract_map 覆盖全部 dspy 文件、load 注册 env 函数、包导出与 init 一致、OWN_TABLES 单表且与 models/ 一一对应、模型四段式(summary 为数组 + UNIQUE 三元组)、load_path 注册全部 dspy 且无通配符、基表零 ALTER(只读投影) - 机械核验:
py_compile全部 .py = OK;dspy 审计(grep '^import|^from' wwwroot/ --include='*.dspy')= 0 命中;13 个 dspy 均含显式return;import 闭包实测CONTRACT_INTERFACES=13 missing=[]、包__all__全可取;grep pbl_domain_materialize_game_definition ../pbl_compiler/= 0 命中(历史 #1 陈旧引用已不存在)。
7. 当前分支/提交状态
- 分支:
main;本轮改动由引擎在交付收口时统一 commit(开发侧不自行 push)。 - 本轮工作树新增:
docs/test-run-2026-09-18.txt(测试执行原始输出)+ 本日志 §6/§7 追加。