pbl_domain_ext/docs/work-log-2026-09-18.md

12 KiB
Raw Blame History

工作日志 — 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.ui
  • scripts/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. 关键技术决策

  1. 实现向设计对齐,不走设计变更:QC #1 给了两条路(建单表 / 先改设计)。选建 pbl_domain_ref 单表——设计 §2、data-model.md §J1、pbls_spec.json(1 表 / 36 总账)三处一致,改设计成本高且无收益。
  2. ref_type + ref_id 泛化关联替代三张同构表:一张表承载 world/scene/entity 三类关联,UNIQUE(tenant_id,ref_type,ref_id) 保证同租户同记录唯一;idx(ref_type,ref_id) 支撑悬挂引用一致性左连接。
  3. 租户隔离用「白名单先行」:基表无 tenant_id → 先查 pbl_domain_ref 拿本租户 ref_id 白名单,再按白名单只读回查基表;未绑定即不可见(不是"查到再判权",从根上杜绝跨租户读基表)。
  4. 基表列名运行时探测:复用模块版本可能改列名(world_id/worldid/parent_id),base.resolve_column 走 information_schema → 退化 PRAGMA,避免写死列名导致上线即错。
  5. check_ref_access 返回 bool 不抛异常:运行时(pbl_runtime_ext 共享会话)热路径每帧可能调用,异常开销大且调用方要 try 包裹;入参非法/无关联/不匹配/悬挂统一 False。但基础设施异常(基表探测失败)放行,避免因 DB 抖动把合法用户判为越权。
  6. 软删而非物理删:unbind_ref 置 is_deleted=1,保留审计痕迹;重绑走"复活"分支(幂等),避免 UNIQUE 冲突。
  7. db 适配层双后端:生产 SqlorAdapter(只用 sor.R/sor.sqlExe,结果在 async with 外 return);测试 SqliteAdapter(同步驱动包 async),使 62 用例可离线跑真实 SQL 逻辑而不依赖 MySQL/宿主挂载。
  8. 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 要求的「租户 / 权限 / 正常 / 异常」四类用例):
    用例类 覆盖点
    TestBindRef bind_ref 正常绑定、重复绑定 PBL_E_DUPLICATE、基表记录不存在 PBL_E_NOT_FOUND、ref_type 非法 PBL_E_VALIDATION、ext_json JSON 串与 dict 双形态
    TestUnbindGetUpdate unbind 软删(is_deleted=1)、unbind 不存在、get_ref 正常、get_ref 跨租户返回 NOT_FOUND(不泄露存在性)、update_ref 字段白名单
    TestListRefs filters 组合(ref_type/blueprint_id/class_id/team_id)、分页 total/items、悬挂 ref 左连接过滤
    TestTenantIsolationQueries list_worlds_by_tenant 仅返回本租户绑定、无绑定返回空、按 class 过滤、list_scenes 跨租户 PBL_E_FORBIDDEN、仅绑定 scene 可见、悬挂过滤
    TestTeamContracts list_teams_by_class 成员聚合、bind_team_to_world 重复/不存在、get_team_worlds
    TestCheckRefAccess 租户匹配、班级/团队维度匹配、跨租户拒绝
    TestThinExtensionInvariants 13 契约全部 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 追加。