# 工作日志 — 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_` 与 `env.` 双名;新增 `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 追加。