18 KiB
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/ 内切 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,任务外变更)。