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

12 KiB
Raw Permalink Blame History

工作日志 — pbl_domain_extM8QC 退回整改轮 · 2026-09-18

1. 范围与背景

  • 仓库/模块:modules/pbl_domain_extPBL 基础域薄扩展M8Wave 4
  • 任务:[M8] 基础域薄扩展 world/scene/entitytask_kind=new_devPM 派发,带 agent.qc 6 条退回意见)
  • 设计权威:projects/pbls/docs/01-design/modules/pbl_domain_ext.mddata-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_refUNIQUE(tenant_id,ref_type,ref_id) + 4 个辅助索引);sql/pbl_domain_ext.sql 重写为单表 DDLinit.py:OWN_TABLES=['pbl_domain_ref'] models/ 仅 1 个 json测试 test_own_tables_single_and_matches_modelstest_model_json_four_sections(断言 UNIQUE 三元组)通过;表总账回归 tables_by_module.pbl_domain_ext=1tables_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_callabletest_contract_map_covers_dspy_filestest_load_module_registers_env_functionstest_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.jsonpbl_class.jsonpbl_team.jsonjson/domain_ext_pbl_*.jsonpbl_tenant_upsert/pbl_class_save/pbl_class_list/pbl_team_save/pbl_team_list 等越界 dspy团队成员改为只读引用 pbl_governance.pbl_team_memberapi._fetch_team_members,表缺失降级空列表) OWN_TABLES == ['pbl_domain_ref']models/ 一一对应(测试断言);init.py:READONLY_FOREIGN_TABLES=['pbl_team_member'] 明示只读;测试 test_list_teams_by_class_members_from_governancetest_list_teams_without_governance_table_degrades 通过
5 契约端点缺失:无任何 wwwroot/*.dspy 新建 13 个 wwwroot/api/*.dspy 薄封装(与契约一一对应)+ wwwroot/index.ui 入口页;全部遵守 dspy 规范:无 import显式 returndebug() 带文件名前缀、转发全部客户端参数filters/ext_json 兼容 JSON 串与平铺字段,不硬编码 dispatch、错误码→http_status 映射403/404/409/400/500 dspy 审计脚本:dspy files=13 audit_issues=NONE(无 import / 有 return / debug 带文件名 / 无 printtest_contract_map_covers_dspy_files 断言每个契约的 dspy 文件真实存在
6 RBAC 路径注册缺失:scripts/ 无变更,新端点上线即 403 重写 scripts/load_path.pyAPI_PATHS(13) + UI_PATHS(1) 逐条显式注册(禁通配符)、ROLE_GRANTS 按 admin/pbl_teacher/pbl_student 分权(写操作仅教师/管理员,只读查询含学生)、register(env) 幂等注册、selfcheck() 自检、__main__ 可执行校验 python3 scripts/load_path.pypaths=14 api=13 ui=1 / OK 全部路径显式注册、无通配符、角色授权齐备;测试 test_load_path_registers_all_dspy 断言 wwwroot 下每个 .dspy 都已注册且无 *

3. 本轮产出文件

新增/重写(实现)

  • pbl_domain_ext/api.py13 契约实现,~600 行)
  • pbl_domain_ext/init.pyload_pbl_domain_ext + get_contract_map + OWN_TABLES
  • pbl_domain_ext/__init__.py(导出 13 契约 + load 函数,三处同步注册之 ②)
  • pbl_domain_ext/base.py(复用基表只读投影:列名运行时探测、批量取行、子表遍历、悬挂过滤)
  • pbl_domain_ext/db.pySqlorAdapter 生产 / SqliteAdapter 测试;${name}$:name 转换;resolve_dbnameget_module_dbname
  • pbl_domain_ext/errors.pyPBL_E_* 错误码 + HTTP 映射,优先复用 pbl_common,缺失时本地等价实现)

新增/重写(数据与契约)

  • models/pbl_domain_ref.json四段式summary 数组)
  • json/pbl_domain_ref.jsonCRUD 浏览定义 tblname+params
  • sql/pbl_domain_ext.sql(单表 DDL对齐 §J1
  • wwwroot/api/*.dspy × 13、wwwroot/index.ui
  • scripts/load_path.py

删除(偏离设计/越界产出)

  • models/pbl_world_ref.jsonpbl_scene_ref.jsonpbl_entity_ref.jsonQC #1
  • models/pbl_tenant.jsonpbl_class.jsonpbl_team.json + json/domain_ext_pbl_*.json × 3QC #4属 pbl_governance
  • wwwroot/api/pbl_tenant_upsert.dspypbl_class_save/list.dspypbl_team_save/list.dspypbl_domain_materialize_game_definition.dspy(越界契约)
  • pbl_domain_ext/assoc.pysql/pbl_domain_ext_assoc.sqltests/test_assoc.py(旧三表方案残留)

测试与文档

  • tests/fake_db.pysqlite 夹具pbl_domain_ref + 三张基表替身 + pbl_team_memberraw_sql/scalar 辅助)
  • tests/test_domain_ref.py62 用例
  • skill/SKILL.mdREADME.mdpyproject.toml、本工作日志

4. 关键技术决策

  1. 实现向设计对齐,不走设计变更QC #1 给了两条路(建单表 / 先改设计)。选pbl_domain_ref 单表——设计 §2、data-model.md §J1、pbls_spec.json1 表 / 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_idbase.resolve_columninformation_schema → 退化 PRAGMA,避免写死列名导致上线即错。
  5. check_ref_access 返回 bool 不抛异常运行时pbl_runtime_ext 共享会话)热路径每帧可能调用,异常开销大且调用方要 try 包裹;入参非法/无关联/不匹配/悬挂统一 False。但基础设施异常(基表探测失败)放行,避免因 DB 抖动把合法用户判为越权。
  6. 软删而非物理删unbind_refis_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表不存在则返回空 memberslist_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 — OK0 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=136 总账不破)

测试覆盖要点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. 当前分支/提交状态

  • 分支:mainmodules/pbl_domain_ext
  • 本轮改动已落盘工作区;git 收口由引擎在 PM 审核通过后统一执行,本日志不自称已 commit/push以引擎回填的「git 收口核验」段为准)。

6. 测试执行证据QC 历史 #3仅 py_compile 无执行记录 → 本轮补实测)

  • 执行命令:python3 -m unittest discover -s tests -p 'test_*.py' -v(环境无 pytestsite-packages 只读无法安装unittest 为标准库等价执行器,用例本身即 unittest.TestCase非降级替代
  • 实测结果:Ran 62 tests — OK0 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 = OKdspy 审计(grep '^import|^from' wwwroot/ --include='*.dspy'= 0 命中13 个 dspy 均含显式 returnimport 闭包实测 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 追加。