Files
PolyGateway/research-wiki/designs/2026-07-22-m4-migration-design.md
T

147 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# M4 迁移验证设计(GovDoc-SaaS → CHSAnalyzer,发 v1.0)
> **状态**: 待人类门审批
> **依据**: ROADMAP §5、ARCHITECTURE §11、migrations/{govdoc-saas,chsanalyzer}.md;用户拍板(2026-07-22)四项见 §1。
> **一句话**: 把 polygateway 真实合并进两个项目本体仓库,以"删治理代码 → 换库 import → 原测试全绿 + 真实冒烟 + 真实回归"完成库的最终验收,随后发 v1.0.0 至 Gitea PyPI。
## 1. 输入:用户拍板与既定事实
| 决策项 | 拍板结果(2026-07-22) |
|---|---|
| 仓库与写权限 | 开放 reference/ 修改权限,直接在其中实施;实施前三仓库已更新到最新(CHS 拉到 7eb8482,GovDoc/VT 本已最新) |
| 迁移顺序 | 先 GovDoc-SaaS,后 CHSAnalyzer;**Video-Tree-TRM5 项目已放弃,不迁移** |
| Q1 打包分发 | Gitea PyPI 包注册(gitea.iomgaa.online,版本接口实测可达;Gitea ≥1.17 内置 PyPI registry) |
| 验收方式 | 原测试全绿 + 真实冒烟 + 真实回归;回归具体方案后续与用户商量(§9 给出候选框架) |
**VT 放弃的连带处置**: ROADMAP/ARCHITECTURE §11.2/migrations/video-tree-trm5.md 标注放弃(§15);v1.0 验收标准由"三项目"改"两项目"。库侧**不删减**任何已交付公共 API(OcrTextPort 文本端点、多逻辑角色 from_env 等 VT 倒推的能力已被 soak 与公共承诺消费,回收属任务外变更)。
## 2. 范围与总验收
**做**: ① GovDoc 全量迁移(删 llm/ 六文件 + embedding 换 EmbeddingClient + 装配净新增);② CHS 全量迁移(删 governance/limiter/scripts/provider_gate/invokers/selector/streaming + judge 收编 + OCR 换端口);③ 每项目缺口回补(触发库小版本);④ v1.0.0 打包发布至 Gitea 并以正式依赖形态回装验证;⑤ 五处文档同步。
**不做**: VT 迁移;任务外重构(两项目的业务代码只动迁移文档标注的改写点);库能力删减;CHS 生产部署切换(交付到"feature 分支验收通过 + 部署切换手册",实际上线由用户执行)。
**总验收**: 两项目各自 `make test`(GovDoc 为 `make ci`)在其 conda 环境全绿(skip 剖面不劣于迁移前基线)+ 冒烟/回归证据落 `tests/outputs/`(库仓库)+ 独立 verifier 清零 + Gitea 上可 `pip install polygateway==1.0.0`
## 3. 事实基线与漂移核对(2026-07-22 实测)
| 事实 | 值 |
|---|---|
| GovDoc | conda `GovDoc-SaaS`(Py3.11,本机尚无该环境);`make ci` = check+test+test-core;29 个测试文件;远端 GitHub;快照与迁移文档零漂移 |
| CHS | conda `chs`(Py3.13,本机尚无);`make test` = pytest --cov;134 个测试文件;markers: requires_db/requires_redis 无则 skip;远端 GitHub;**有漂移,见下** |
| 库 | v0.1.0,extras: redis/postgres/structured/sdk;SQLite 遥测零 extra |
| Gitea | https://gitea.iomgaa.online 可达;发布与回装需用户提供 Gitea token(凭据先读后用) |
**CHS 漂移**(5ac9f59→7eb8482,135 文件 +9818/1853,治理关键文件仅 44+/83):
- positioning 子系统重构为纯图像模板分类(`TemplateVascularPositioner`),**position worker 不再消费 VLM**;`PositionCallScheduler` 端口与 `load_single_vlm_capacity` 已删除。
- 影响修订三条:迁移文档 §2"load_single_vlm_capacity 保留改读库聚合"作废;§6-S3"VLM 灰度先切 position worker"作废(VLM 消费点收敛为 extractors.py/classifiers.py,同属 pipeline worker,灰度只剩一段);调用点清单中 vascular_positioner.py:329 一行作废。
- governance/invokers/limiter/scripts/selector/streaming/errors/judge/contracts_limiter **零实质变更**(仅格式化);迁移文档其余证据**语义有效,行号以实施时实测为准**(tracking/config/container/ports 有小幅行号漂移)。
## 4. 决策 1:迁移工作区机制
| 方案 | 内容 | 优点 | 缺点 |
|---|---|---|---|
| A 直接改 reference/(用户原话方向) | 放开 pre-tool-guard 对 reference/ 的写拦截(文件工具 + Bash 的 checkout/sed/rm 等),CLAUDE.md 铁律措辞同步改;在 reference/<proj> 内切 feature 分支实施 | 与用户直觉一致;路径最少 | ① 钩子里 `git checkout` 也在拦截清单,A 必须动钩子,硬边界整体弱化(误删保护一并失去);② 分支切走后**旧版治理代码从工作区消失**,而迁移全程要逐段对照旧实现验证 shim 等价性,只能靠 `git show` 翻历史;③ 迁移文档/ARCHITECTURE 的 file:line 证据在工作区失效 |
| **B git worktree(推荐)** | 保持 reference/ 工作区只读与钩子零改动;`git -C reference/<proj> worktree add /Users/yuchengzhang/Projects/m4-worktrees/<proj> -b feat/polygateway-migration`;实施全部发生在 worktree(路径不含 reference/,钩子天然放行;`worktree`/`commit`/`push` 子命令均不在拦截清单) | ① 硬边界一行不改;② reference/ 本体停在 main:旧版代码、蓝本、file:line 证据全程可对照——这正是"迁移前先做旧版行为审计"的物理保障;③ commit/push 走同一 origin,与 A 的 git 效果完全相同;④ 项目自带 `using-git-worktrees` skill,惯例现成 | ① 多一个目录,且在 PolyGateway 工作目录之外——**前置**: 需把 `/Users/yuchengzhang/Projects/m4-worktrees/` 加入工具写权限(settings `additionalDirectories` 或人类门口头授权),否则实施期逐次询问;② 措辞澄清: worktree 的 add/commit/push 会向 `reference/<proj>/.git` 写入 refs 与对象,"只读"实质收窄为"工作区文件与 main 检出不变"——本设计将此收窄视为可接受并明示 |
| C 外部独立 clone | 在项目外重新 clone 两仓库实施 | 与 B 同等隔离 | 双份仓库易漂移(reference/ 更新后 clone 不同步);B 是 C 的严格上位 |
**推荐 B**。它交付的正是用户要的东西(在项目本体仓库的 feature 分支上真实迁移、可 push),但不付出硬边界与蓝本对照的代价;用户拍板 A 的实质诉求是"能改真仓库",B 完全满足。若人类门坚持 A,则钩子改法为:reference/ 写拦截整段替换为"仅拦 rm -rf 类危险命令",CLAUDE.md §1/§5 的"只读勿改"措辞改为"M4 起为迁移工作区;蓝本对照以 git 历史为准"。
## 5. 决策 2:依赖形态(两阶段)
| 方案 | 内容 | 权衡 |
|---|---|---|
| **两阶段(推荐)** | **开发期**: 两项目 conda 环境 `pip install -e /Users/yuchengzhang/Projects/PolyGateway[extras]`(editable,缺口回补即时生效);pyproject 先写 `polygateway[extras]>=0.1` 声明依赖关系。**发布期**(两项目验收全过后): 库 bump `1.0.0``python -m build` → twine 上传 Gitea → 新建一次性干净环境从 `--index-url https://gitea.iomgaa.online/api/packages/iomgaa/pypi/simple/` 回装并重跑两项目测试 → 项目 pyproject 定稿 `polygateway[...]==1.0.*` 并在 README/Makefile 写明 index 安装命令 | 迭代快(缺口回补不经发版);发布验证真实(干净环境回装);唯一成本是发布期多一轮回装测试 |
| 全程 Gitea wheel | 每次库改动都 build+upload+install | 缺口回补一次一发版,迭代极慢;否决 |
| git+https 依赖 | pyproject 写 `polygateway @ git+https://...` | 可行但放弃了已拍板的 pip index 价值(版本可枚举、锁版本、无需 git 凭据);仅作 Gitea PyPI 故障时的退路 |
extras 分配: GovDoc → `polygateway[redis,structured]`(SQLite 遥测零 extra);CHS → `polygateway[redis,postgres,structured]`
## 6. 决策 3:Q6 CHS judge 迁移路径
judge(`core/eval/judge.py`,快照后零变更)默认 provider=anthropic,同步裸调 SDK。
| 方案 | 内容 | 权衡 |
|---|---|---|
| **走实验室 OpenAI 兼容中转(推荐)** | judge 配 `JUDGE__{PROVIDER}__1__*` 指向实验室中转网关,claude 模型名经中转;`_call_llm` 改同步桥接,解析换库 `JsonRepairStrategy`。**桥接方案钉死**: 每次调用一个完整 `asyncio.run(...)`,client 的构造(from_env)与 `aclose()` 都在该协程内完成——httpx 连接池绑定事件循环,跨 `asyncio.run` 复用常驻 client 会触发 "Event loop is closed"(经典坑);judge 是低频评估路径,放弃连接复用换正确性。备选(judge 调用量大时): 常驻单线程事件循环 + `run_coroutine_threadsafe` | 零库改动;**前置确认**: 中转网关是否代理 judge 所需 claude 模型(人类门时确认,不通则回退方案 3) |
| 库新增 Anthropic 原生 transport | D2 有端口预留 | 新子系统 = 新公共承诺 + 独立设计与测试,为单一消费点开新承诺违反 YAGNI;M4 内否决,留 D2 储备 |
| judge 暂缓收编 | judge.py 保持裸调,标注技术债 | 违背"全量迁移"验收口径;仅作中转不可用时的降级,需用户显式同意 |
## 7. GovDoc 迁移方案(migrations/govdoc-saas.md §6 修订版)
前置: 创建 conda 环境 `GovDoc-SaaS`(Py3.11)→ `make install` → 基线 `make ci` 留存(含 skip 剖面)。
| # | 步骤 | 验证 | 回滚 |
|---|---|---|---|
| G1 | worktree + feature 分支;editable 装 `polygateway[redis,structured]` | `import polygateway` | 分支起点 |
| G2 | 装配 spike: `from_env()` + AgentLoop 真实调用一次(实验室网关),校验 `isinstance(client, LLMProvider)` 与响应字段 | spike 输出 + 遥测落库 | 丢弃 spike |
| G3 | `.env`/`.env.example` 改键(平铺单源 → `LLM__{PROVIDER}__1__*`;新增 namespace 等;凭据先读 .env 真实值再写) | `from_env()` 无缺配置报错 | commit |
| G4 | 装配进 api lifespan 与 arq worker 入口(显式 `retryable_exceptions=(TransientError, AllSourcesExhausted)` + `aclose()`) | govdoc 业务测试绿;注错验证步级重试可触发 | commit |
| G5 | `retrieval/embedding.py` 删除换 `EmbeddingClient`(映射: batch_size→`EMBED__BATCH_SIZE`、dimension→`EMBED__EXPECTED_DIM`、on_usage→遥测) | retrieval 相关测试绿 | commit |
| G6 | 删 llm/ 六文件 + test_breaker.py;改写 types.py(re-export)/protocols.py(删 TelemetryRecorder)/test_imports.py;docagent-core pyproject 移除 redis extra、根 pyproject 加 polygateway;**Makefile install 行同步改**(现硬编码 `docagent-core[redis,dev]`,extra 删除后引用失效) | `make ci` 全绿 + import-linter 过 | commit |
| G7 | 验收: 验收公式逐项核对 + 冒烟/回归(§9)+ 独立 verifier | ci 输出 + verifier 报告 | — |
GovDoc 特有事实(迁移文档 §1): 装配层从未存在、`GovernedLLMClient` 零生产调用方、零异常捕获点——业务代码对治理层消费零改动,风险≈0;缓存 key 全变无成本(未上生产)。
## 8. CHS 迁移方案(migrations/chsanalyzer.md §6 漂移修订版)
前置: 创建 conda 环境 `chs`(Py3.13)→ 按 requirements 安装 → 基线 `make test` 留存 skip 剖面(本机无 CHS 专用 Postgres/Redis 时 requires_db/requires_redis 用例 skip 属基线的一部分,迁移后不得劣化)。
| # | 步骤 | 验证 | 回滚 |
|---|---|---|---|
| C1 | worktree + feature 分支;editable 装 `polygateway[redis,postgres,structured]`;基线留存 | `make test` 基线 | 分支起点 |
| C2 | `PgwVlmProvider` shim(bytes+instruction → content 数组 → chat → `ProviderOutcome`)+ G1 异常翻译 shim(库 `GatewayUnavailableError``ProviderUnavailableError`,或 tracking.py 直接 except 库异常——实施时按改动面最小者定,二选一在 plan 钉死) | shim 单测(含字段映射与 per_source_reasons 透传) | commit |
| C3 | **VLM scope 切换**(漂移修订: position worker 已不消费 VLM,灰度只剩 pipeline worker 一段,原 S3 两步并一步): container 治理装配换库工厂,extractors/classifiers 经 shim 走库;灰度期 CacheMW 先关(迁移文档 §8 裁决) | 提取/分类单测 + integration;双实现对拍一批样本 | 装配开关切回 legacy |
| C4 | **OCR scope 切换**: `table_locator.py` 改写消费 `OcrLayoutResult`(首个 type=="table" 元素 + int 四元组,约 5 行) | extract_table integration 全绿 | commit |
| C5 | judge 收编(§6 方案落地) | test_eval_judge 绿 + 真实 judge 调用一次 | 独立文件单独回滚 |
| C6 | 清场: §2 删除清单执行、errors.py Provider 族删除、tracking.py 定稿、拆开关;config.py 治理解析段删除换 from_env | **原测试全绿(skip 剖面≥基线)** | C5 前 commit |
| C7 | 验收: 冒烟/回归(§9)+ 独立 verifier | 报告落盘 | — |
CHS 特有约束: 遥测后端配 `PGW_TELEMETRY_BACKEND=postgres` 指 PG 实例 **polygateway 库**(严禁触碰 chs_prod 等在用库);本机验证用实验室 Redis **db3**;生产切换(新旧 Redis key 前缀不同、整 scope 原子切换、低峰执行)只交付手册不代执行。
## 9. 回归验收框架(候选,细节与用户商量后定)
| 项目 | 冒烟(已定) | 回归候选 |
|---|---|---|
| GovDoc | G2 的 spike: AgentLoop 真实调用 + 遥测/缓存命中实测 | 无旧遥测基线(骨架期),**建议冒烟即回归**;可加"重跑同请求缓存命中率非零"一条 |
| CHS | 单张真实样本走完整提取 pipeline(worktree 内,治理走库) | (a)**小批对拍(建议)**: 选 20-50 张真实样本,旧版(reference/ main)与新版(worktree)各跑提取,对比结构化结果一致率 + 错误率/延迟(新版读库遥测,旧版读运行日志);(b) testing 环境 docker 部署跑 e2e;(c) 只冒烟不批跑。**先行写死两条**: 新旧两轮**串行执行**(并行则对同一真实上游 RPM 双份消耗,同"禁止双栈并跑"精神);旧版跑批同样只用实验室 Redis db3 与 PG polygateway 库,不触生产后端。样本量、一致率阈值、跑批环境届时拍板 |
## 10. v1.0 发布清单(两项目验收全过后)
① 库版本 bump `1.0.0` + 汇总 changelog + **库自身 `make ci` 重跑全绿(发布门)**;② `python -m build` 出 wheel+sdist;③ twine 上传 Gitea PyPI(token 用户提供,写 `~/.pypirc` 或环境变量,不入任何仓库);④ 一次性干净 conda 环境从 Gitea index 回装,重跑两项目测试套件绿;⑤ 两项目 pyproject 定稿 `==1.0.*` 并记录安装命令;⑥ 打 git tag `v1.0.0`;⑦ 文档同步(§15)。发布后库仓库亦推送至 Gitea 托管(用户提及"托管到 gitea",与 GitHub 双远端或迁移,人类门确认)。
## 11. 旧版行为审计
迁移类设计的审计已由两份迁移文档 §7 承载(逐条"保留/替换/修复/有意放弃",GovDoc 17 条、CHS 26 条),本设计**继承其全部结论不重复**;漂移带来的增量修订仅 §3 三条(均为"作废"而非语义变化)。实施时每步对照迁移文档审计表执行,任何新发现的未登记行为差异按流程回写迁移文档后再动代码。
## 12. 非功能维度
- **并发与取消**: 治理层行为全部由库承载(M1-M3 已验收);项目侧新增物仅 shim(纯映射,无状态)与装配代码。CHS judge 桥接按 §6 钉死: client 构造与 aclose 均在单次 `asyncio.run` 协程内,杜绝 httpx 连接池跨事件循环复用;同步 runner 线程无嵌套循环风险(实施时以 `asyncio.get_running_loop()` 探测断言钉住)。CancelledError 穿透:shim 不含 try/except 包裹,天然穿透。
- **降级方向**: 继承库铁律(缓存/遥测静默、限流/熔断报错);GovDoc B15 步级重试作为业务侧兜底显式保留(G4 注入库异常类型,静默失效风险以注错测试钉死)。
- **幂等与重复**: 迁移步骤均以 commit 为回滚点、可重入(重复执行 editable install/改键无副作用);Gitea 同版本重复上传会被 409 拒绝,重发布必须 bump 版本。
- **持久化与原子性**: 项目侧不新增持久化;CHS 生产切换的 Redis key 前缀切换风险(短暂限流清零)已在迁移文档 §8 论述,手册中标注低峰执行。
## 13. 错误处理与测试策略
- 缺口回补的判定口径: 迁移中任何"库能力不足以让原测试通过/原行为复现"即边界缺口 → 回补进库(走库侧常规红绿流程 + 小版本)→ 项目侧重验;禁止在项目侧用变通代码掩盖库缺口。
- 项目侧新增代码(shim/装配)的失败路径: 全部落库四分类语义,shim 不吞不改异常类型;每个 shim 有先失败后通过的单测证据(测试结果门照旧)。
- 测试基线纪律: 迁移前基线的 pass/skip 剖面留档,并同步留存**预期测试增删清单**(G6/C6 删除的自测文件导致分母变化,核对时按清单修正);迁移后全绿且 skip 不增;两项目测试在各自 conda 环境跑,不与库 soak 并跑(Redis db3 FLUSHDB 冲突)。
## 14. 风险
| 风险 | 缓解 |
|---|---|
| 隐性行为依赖迁移中才暴露(ROADMAP 已预警) | 迁移文档审计表逐条对照 + worktree 下旧版代码全程可对照 + 每步 commit 回滚 |
| CHS 本机测试环境与生产差异(db/redis skip 剖面) | 基线剖面留档对齐;requires_db/redis 用例若需真实后端,用实验室资源(PG polygateway 库/Redis db3)非生产库 |
| 中转网关不支持 judge 的 claude 模型 | 人类门确认;不支持则 §6 方案 3 降级(用户显式同意) |
| Gitea PyPI 首次发布路径不通(版本 26.4.0 为 CalVer,包注册细节未实测) | 发布前先用 0.1.0 做一次试发布+回装演练;不通则退 git+https 依赖(§5) |
| 两项目 conda 环境从零创建的依赖地狱 | 各项目 CLAUDE.md/requirements 为准;环境创建失败即停,与用户对齐 |
## 15. 文档同步清单(实施尾声执行)
① ROADMAP §1/§5: M4 范围改两项目、VT 标注放弃(2026-07-22 用户拍板)、状态推进;② ARCHITECTURE §11.2: 标注 VT 已放弃不迁移(能力倒推记录保留);§13 Q1/Q6 落拍板结果;③ migrations/video-tree-trm5.md 头部标注放弃;④ migrations/chsanalyzer.md: 漂移修订(§3)与实施结果回写;migrations/govdoc-saas.md: extras 名更正(文中 `[redis,telemetry-sqlite]` 不存在,实为 `[redis,structured]`)、"不迁 embedding 待 Q3"已被拍板推翻(G5 迁移)、实施结果回写;⑤ CLAUDE.md: 若人类门选方案 A 则改 reference/ 铁律措辞;选 B 则钩子与铁律条文零改动,但"只读"语义收窄(§4-B ②)在 CLAUDE.md reference/ 条目加一行括注;⑥ 库 README/发布说明: Gitea 安装命令;⑦ **迁移合并后 reference/ 本体收尾同步**: 用户在各项目远端合并 feature 分支后,`git -C reference/<proj> pull --ff-only`(不在钩子拦截清单)把本体推进到迁移后 main,并移除 worktree(`git worktree remove`)——蓝本自此指向迁移后代码,属预期终态。
## 16. 被否决的备选汇总
外部独立 clone(§4-C,双份漂移);全程 wheel 分发与 git+https(§5);Anthropic 原生 transport 与 judge 暂缓(§6);为 VT 放弃回收库能力(§1,任务外变更)。