# M4 迁移验证实现计划(GovDoc → CHS → v1.0)

> **依据**: designs/2026-07-22-m4-migration-design.md(已过人类门 2026-07-22:worktree 工作区、两阶段依赖、judge 走实验室中转)。
> **目标**: 两项目按各自迁移文档完成全量迁移与验收(原测试全绿 + 冒烟 + 回归),缺口回补进库,发 v1.0.0 至 Gitea PyPI。
> **概述**: 先搭 worktree 与 conda 环境并留基线(T0);GovDoc 走 T1-T6(装配净新增为主,风险低);CHS 走 T7-T12(shim + scope 切换 + judge 收编);全过后 T13 发布、T14 文档同步。
> **涉及技术**: git worktree、conda、pip editable、pydantic-settings 多源配置、twine/Gitea PyPI、httpx 事件循环生命周期。

## 0. 全局约定(每个任务都适用)

- **路径**: 库 = `/Users/yuchengzhang/Projects/PolyGateway`(下称 PGW);工作区 = `/Users/yuchengzhang/Projects/m4-worktrees/{GovDoc-SaaS,CHSAnalyzer}`(下称 WT/G、WT/C);蓝本 = `reference/{GovDoc-SaaS,CHSAnalyzer}`(只读,停在 main 作对照)。
- **环境**: GovDoc 用 conda `GovDoc-SaaS`(Py3.11);CHS 用 conda `chs`(Py3.13);库自身用 `PolyGateway`。两项目 Makefile 已硬编码各自环境名,worktree 内直接 `make ...` 即可。
- **硬约束**: reference/ 本体不写;Redis 只用实验室 db3;PG 只用 polygateway 库(严禁 chs_prod 等);凭据先从 PGW `.env` / 用户提供处读真实值再写,读写 `.env` 只在 python 代码内(dotenv);两项目测试不与库 soak 并跑。
- **提交纪律**: worktree 内提交遵循**各自项目**的既有 commit 风格(先 `git log --oneline -10` 核对),英文祈使句、无 AI 签名;每任务至少一个 commit 作回滚点;修复与 commit 分开两次工具调用。
- **测试证据**: 每个行为变更任务出示"先失败后通过"证据;基线/验收控制台输出落 PGW `tests/outputs/m4/`(不提交,验收结论汇入 findings)。
- **缺口口径**: 任一"库能力不足以让原测试通过/原行为复现" = 边界缺口 → 在 PGW 侧走红绿流程回补(feat/m4-migration 分支)→ editable 即时生效 → 项目侧重验;禁止项目侧变通掩盖。

## 1. 文件结构总览

**GovDoc(WT/G,按 migrations/govdoc-saas.md §2 修订版)**
- 删除: `packages/docagent-core/src/docagent_core/llm/`(client/breaker/streaming/redis_cache/telemetry_sqlite/__init__ 六文件)、`packages/docagent-core/tests/test_breaker.py`、`packages/docagent-core/src/docagent_core/retrieval/embedding.py`
- 改写: `packages/docagent-core/src/docagent_core/types.py`(LLMResponse 改 re-export)、`protocols.py`(删 TelemetryRecorder)、`packages/docagent-core/tests/test_imports.py`、`packages/docagent-core/pyproject.toml`(删 redis extra)、根 `pyproject.toml`(加 polygateway 依赖)、`Makefile`(install 行 extras)、`.env.example`(§5 键映射)
- 新增: api lifespan / arq worker 入口的装配段(具体文件在 `src/govdoc/` 内实测后定,现骨架期零 import docagent_core)

**CHS(WT/C,按 migrations/chsanalyzer.md §2 + 设计 §3 漂移修订)**
- 删除: `app/providers/{governance,invokers,selector,streaming}.py`、`app/coordination/{limiter,scripts,provider_gate}.py`、对应自测(`tests/` 内以被删模块为对象的单测,留档清单见 T0)
- 改写: `app/domain/errors.py`(Provider 族删除)、`app/config.py`(治理解析段删)、`app/container.py`(治理装配换库工厂)、`app/ports.py`(限流/熔断端口删;VlmProvider/TableLocator/ProviderOutcome/Usage 保留)、`app/providers/table_locator.py`(消费 OcrLayoutResult)、`app/workers/tracking.py`(except 库异常)、`core/eval/judge.py`(桥接收编)、`.env.example`
- 新增: `app/providers/pgw_shims.py`(PgwVlmProvider;唯一新增文件)

**PGW(库仓库,feat/m4-migration 分支)**: 缺口回补代码(如有)、`pyproject.toml` 版本 bump、ROADMAP/ARCHITECTURE/migrations/CLAUDE.md 文档同步、findings 报告。

## 2. 任务清单

### T0 基础设施与基线(GovDoc + CHS 一次做完)

- [ ] worktree: `git -C reference/GovDoc-SaaS worktree add /Users/yuchengzhang/Projects/m4-worktrees/GovDoc-SaaS -b feat/polygateway-migration`;CHS 同式。验证: 两 worktree 内 `git status` 干净、分支正确;reference/ 本体仍在 main。**前置**: `~/Projects/m4-worktrees/` 写权限已随人类门批准(设计 §4-B);首次写入若权限系统仍询问,按该会话授权放行。
- [ ] conda 环境: `conda create -n GovDoc-SaaS python=3.11 -y` → WT/G 内 `make install`;`conda create -n chs python=3.13 -y` → WT/C 内 `conda run -n chs pip install -r requirements.txt -r requirements-dev.txt`。任一失败即停,与用户对齐(设计 §14 风险)。
- [ ] editable 装库: 两环境各 `pip install -e "/Users/yuchengzhang/Projects/PolyGateway[redis,structured]"`(chs 加 `postgres`)。验证: `conda run -n <env> python -c "import polygateway; print(polygateway.__version__)"`。
- [ ] 基线留档: WT/G `make ci`、WT/C `make test`(worktree 内容 = main,即旧版基线)。控制台全文落 `tests/outputs/m4/{govdoc,chs}_baseline.log`,并整理**pass/skip 剖面 + 预期测试增删清单**(G 侧将删 test_breaker.py 及 test_imports 两行;C 侧将删的治理自测文件名逐一列出)至 `tests/outputs/m4/baseline_profile.md`。CHS 侧 requires_db/requires_redis 的 skip 属基线组成部分。
- [ ] 提交点: 无代码变更,不提交;基线文件留盘。

### T1 GovDoc 冒烟 spike(迁移文档 §6-3)

- [ ] 从 PGW `.env` 用 python(dotenv)读实验室网关真实凭据,生成 WT/G `.env`;必填键全集(config.py `_require` 实测): 单源 `LLM__{PROVIDER}__1__*`、`PGW_TELEMETRY_BACKEND=sqlite` + `PGW_TELEMETRY_SQLITE_PATH`、`PGW_CACHE_BACKEND=redis` + `PGW_CACHE_NAMESPACE=govdoc-saas` + `PGW_CACHE_TTL_S`、`PGW_LIMITER_BACKEND`/`PGW_BREAKER_BACKEND`、`REDIS_URL`(db3)。不提交 `.env`。
- [ ] spike 脚本放 PGW scratchpad(不进 GovDoc 仓库),两段: ① 真实冒烟——`from_env()` 装配 → `assert isinstance(client, docagent_core.protocols.LLMProvider)` → `await client.chat(...)` 真实调用一次 → 打印 LLMResponse 全字段 → 查询遥测 db 该 call_id 行存在;② AgentLoop 接线验证(离线)——实测签名 `AgentLoop(llm, max_steps=N, retryable_exceptions=...)`(max_steps 必填)且 `run()` 需要 ToolDispatcher 与 Thinking+JSON 格式,故用 fake LLMProvider + 桩 dispatcher 构造可终止对话,验证 retryable_exceptions=(TransientError, AllSourcesExhausted) 注入后步级重试对库异常可触发。
- [ ] 验证: spike 输出 + 遥测行;失败即首个缺口(按 §0 缺口口径处理)。
- [ ] 提交点: WT/G 仅 `.env.example` 若有同步则提交,否则无提交。

### T2 GovDoc 配置迁移(§6-4)

- [ ] `.env.example` 按 govdoc-saas.md §5 映射改键(平铺 → `LLM__{PROVIDER}__1__*`;新增 T1 列出的 `PGW_*` 必填键;旧键 `REDIS_CACHE_TTL` **改名**为库键 `PGW_CACHE_TTL_S` 且 TTL>0 必填,注释说明)。`.env` 同步(python 内改写)。
- [ ] 先失败后通过: 删一个关键键跑 `from_env()` 须报缺配置错(防御验证),补回后通过。
- [ ] 验证: spike 重跑通过。提交点: `git commit`(.env.example + 相关注释)。

### T3 GovDoc 装配进入口(§6-5)

- [ ] 实测 `src/govdoc/` 现有 api 入口(骨架期可能仅 `api/deps.py`);把装配段(约 5 行,设计文档 §7-G4 形态: from_env + AgentLoop retryable_exceptions 显式传参 + lifespan/shutdown 处 `await client.aclose()`)接入 api lifespan;若 arq worker 入口尚不存在则仅 api 侧,并在 commit message 说明。
- [ ] 先失败后通过: 注错测试——用 fake LLMProvider(chat 首调抛 TransientError,次调成功)+ 桩 dispatcher(同 T1-② 的 harness),断言 AgentLoop 步级重试确实触发(防"静默失效",迁移文档 B15);不传 retryable_exceptions 时同一 fake 必须失败(红),显式传参后通过(绿)。
- [ ] 验证: `make ci` 绿。提交点: commit。

### T4 GovDoc embedding 迁移(设计 §7-G5)

- [ ] 删 `retrieval/embedding.py`,消费点换 `polygateway.EmbeddingClient`(映射: batch_size→`EMBED__BATCH_SIZE`、dimension 校验→`EMBED__EXPECTED_DIM`、on_usage 回调→改读遥测;retrieval 内调用点以 grep 实测为准)。
- [ ] 先失败后通过: 删除后 retrieval 测试先红,改写消费点后绿;EXPECTED_DIM 不符抛 ResultInvalidError 有断言。
- [ ] 验证: `make ci` 绿。提交点: commit。

### T5 GovDoc 清场(§6-6)

- [ ] 执行 §1 删除清单;`types.py` 改 `from polygateway import LLMResponse`(re-export 一行,conftest 旧 11 字段构造零改动);`protocols.py` 删 TelemetryRecorder;`test_imports.py` 改 import 目标;两处 pyproject + Makefile install 行改 extras(`docagent-core[dev]`,redis extra 删除)。
- [ ] 验证: `make ci` 全绿 + import-linter 过 + `grep -r "docagent_core.llm" packages/ src/` 零命中。
- [ ] 提交点: commit。

### T6 GovDoc 验收

- [ ] 验收公式逐项核对(govdoc-saas.md §1);`make ci` 输出与基线剖面对照(全绿、skip 不增、分母按预期增删清单修正)。
- [ ] 冒烟即回归(设计 §9): spike 复跑 + 重跑同请求断言缓存命中(cache_hit=True)。
- [ ] 全新上下文 verifier subagent(只读)按迁移文档逐条核验,问题清零。
- [ ] 提交点: WT/G 最终 commit;PGW 侧若有缺口回补则库测试同步绿。**T6 全过后才进入 T7。**

### T7 CHS shim 与异常分支(设计 §8-C2;二选一已钉死)

- [ ] **WT/C `.env` 生成**(与 T1 对称,CHS 段全部真实调用的前置): python(dotenv)从 PGW `.env` 读实验室网关凭据配 `VLM__{PROVIDER}__1__*`(经实验室网关调 qwen-vl 系模型;若用户要求生产同款 dashscope 直连源,凭据向用户索取,严禁编造);`OCR__MONKEY__{1,2}__*`(10.77.0.20:7866/7867,api_key=none,TRUST_ENV=false);`JUDGE__{PROVIDER}__1__*`(实验室中转 + claude 模型名);`PGW_LIMITER_BACKEND=redis`/`PGW_BREAKER_BACKEND=redis`/`PGW_TELEMETRY_BACKEND=postgres`(PG polygateway 库 DSN,从 PGW `.env` 读)/`PGW_CACHE_BACKEND=none`(灰度期关缓存)、`REDIS_URL`(db3)。不提交。
- [ ] **依赖声明**(设计 §5 开发期承诺): WT/C `requirements.txt` 加 `polygateway[redis,postgres,structured]>=0.1` 行(CHS 无 `[project]` 依赖段,载体是 requirements.txt;editable 安装在 T0 已就位,此行是声明)。
- [ ] 新增 `app/providers/pgw_shims.py`: PgwVlmProvider 骨架(字段映射按 chsanalyzer.md §3,以 WT/C `app/ports.py` 实测签名为准):

```python
class PgwVlmProvider:
    """实现 app.ports.VlmProvider: bytes+instruction → 库多模态 chat → ProviderOutcome。"""
    def __init__(self, client: GatewayClient) -> None:
        self._client = client
    async def complete(self, image: bytes, instruction: str) -> ProviderOutcome:
        data_url = "data:image/jpeg;base64," + base64.b64encode(image).decode()
        resp = await self._client.chat([{"role": "user", "content": [
            {"type": "text", "text": instruction},
            {"type": "image_url", "image_url": {"url": data_url}},
        ]}])
        # 映射按 chsanalyzer.md §3: usage.total_tokens ← prompt+completion,
        # elapsed_s ← latency_ms/1000;Usage/ProviderOutcome 构造以 WT/C ports.py 实测字段为准。
        # 注意库 LLMResponse 无 raw 字段: 实施时 grep ProviderOutcome.raw 消费点,
        # 无消费则置 {},有消费按消费需求组装(如塞 call_id/provider 元数据)。
        return ProviderOutcome(text=resp.content, source_name=resp.source_name,
                               model=resp.model,
                               usage=Usage(total_tokens=resp.prompt_tokens + resp.completion_tokens,
                                           elapsed_s=resp.latency_ms / 1000),
                               raw={})
```

- [ ] 异常分支**不做翻译 shim**,`app/workers/tracking.py` 直接 `except GatewayUnavailableError`(理由: C10 清场本就删除 Provider 错误族,翻译层是死代码;库异常自带 scope/reason/retry_after_s/per_source_reasons,G1 已闭)。arq defer 时长改读 `exc.retry_after_s`(注意 M2.5 后上限 300s,迁移文档追记已声明属期望)。
- [ ] 先失败后通过(两组): ① shim 字段映射单测(含 usage 换算)先红后绿;② tracking.py 改动——既有 `tests/unit/test_tracking.py` 的 defer 路径用例在改 except 库异常后先红(旧 import ProviderUnavailableError 失配),改写断言消费 `exc.retry_after_s`/`per_source_reasons` 后绿。
- [ ] 验证: `conda run -n chs pytest tests/unit/test_tracking.py <shim 单测文件> -v` 绿。提交点: commit。

### T8 CHS VLM scope 切换(§8-C3,漂移修订后单段灰度)

- [ ] `app/container.py` VLM 治理装配(约 96 行)换 `GatewayClient.from_env("VLM")` + PgwVlmProvider;extractors/classifiers 经业务端口零改动;灰度期 CacheMW 关闭(`PGW_CACHE_BACKEND=none`,迁移文档 §8 裁决);`.env` 键原样继承(`VLM__QWEN__1__*` 命名即库约定)。
- [ ] **保真校验**(迁移类硬门): 对照 chsanalyzer.md §7 审计表逐条核验迁移后行为(重点: 六道闸语义、预扣结算、探针租约、RequestRejected 二分、429 细分、ResultInvalid 不熔断);发现库侧未落地项即缺口回补。蓝本对照读 reference/CHSAnalyzer(main)。
- [ ] 双实现对拍: 同一小批样本(3-5 张)旧栈(reference 侧代码逻辑,经基线记录)与新栈输出结构对齐;真实调用走实验室网关配额,串行执行。
- [ ] 红绿豁免声明: 本任务是**行为保持型切换**(无新行为可先红),测试证据由"审计表 26 条保真核验 + 双实现对拍 + 既有测试全绿"共同构成,豁免 §0 先红后绿要求。
- [ ] 验证: 提取/分类相关单测 + integration 绿。提交点: commit。

### T9 CHS OCR scope 切换(§8-C4)

- [ ] `table_locator.py` 改消费 `OcrLayoutPort.parse_layout`(约 5 行: 首个 `type=="table"` 元素 + `int()` 四元组,M3 设计 §1.2 已 35 样本取证 bbox 一致);container 装配 `OcrClient.from_env("OCR")`(MonkeyOCR 双端点 10.77.0.20:7866/7867,`OCR__MONKEY__N__TRUST_ENV=false`)。
- [ ] 先失败后通过: table_locator 单测按新返回类型先红后绿;无表样本合法空语义有断言。
- [ ] 验证: `extract_table` integration 全绿(真实 MonkeyOCR)。提交点: commit。

### T10 CHS judge 收编(§6 方案 1,桥接已钉死)

- [ ] `core/eval/judge.py` `_call_llm` 改同步桥接(client 生命周期封在单次 asyncio.run 内,杜绝 httpx 跨事件循环复用):

```python
def _call_llm(self, prompt: str) -> str:
    async def _once() -> str:
        client = GatewayClient.from_env("JUDGE")
        try:
            resp = await client.chat([{"role": "user", "content": prompt}])
            return resp.content
        finally:
            await client.aclose()
    return asyncio.run(_once())
```

- [ ] JSON 解析换库 `JsonRepairStrategy`(import 路径 `polygateway.structured.json_repair.JsonRepairStrategy`——非顶层导出,用 `parse(text)` 方法;删手写 find/rfind);桥接函数首行加 `asyncio.get_running_loop()` 探测断言(设计 §12: 确认调用方无事件循环,防嵌套 run)。`JUDGE__{PROVIDER}__1__*` 已随 T7 `.env` 就位。**先真实探测一次**中转是否代理该模型;不通即停,回人类门(设计 §6 回退)。
- [ ] 先失败后通过: test_eval_judge 桩测试(含解析失败重问路径)先红后绿 + 真实 judge 调用一次输出落 tests/outputs/m4/。
- [ ] 提交点: commit(judge 独立文件,单独可回滚)。

### T11 CHS 清场(§8-C6)

- [ ] 执行 §1 删除清单;`errors.py` Provider 族删除(业务异常保留);`config.py` 治理解析段删除;`ports.py` 限流/熔断端口删除;`.env.example` 定稿(新增 `PGW_LIMITER_BACKEND=redis`、`PGW_BREAKER_BACKEND=redis`、`PGW_TELEMETRY_BACKEND=postgres` 指 PG polygateway 库、缓存 namespace=`chsanalyzer:{scope}`)。
- [ ] 验证: `make test` 全绿(**skip 不增**,分母按增删清单修正)+ 残留 import 双向 grep(按被删模块名 `governance|invokers|selector|streaming|limiter|scripts|provider_gate` 在 `app/ core/ tests/` 全范围查,兼顾 `from app.providers import governance` 形态;tests/ 内命中即该测试属预期删除清单或需改写)。
- [ ] 提交点: commit。

### T12 CHS 验收与回归

- [ ] 冒烟: 单张真实样本完整提取 pipeline(治理走库,Redis db3 + PG polygateway 库)。
- [ ] 回归对拍: **先与用户商定**样本量/一致率阈值/跑批环境(设计 §9 候选 a: 20-50 张新旧串行对拍);商定前不跑批。
- [ ] 全新上下文 verifier subagent 按 chsanalyzer.md 全文逐条核验(含审计表 26 条、能力对标清单、生产切换手册是否交付),问题清零。
- [ ] 生产切换手册: WT/C `research-wiki/`(或 deploy/ 说明)新增一页——整 scope 原子切换、禁混版 worker、低峰执行、Redis key 前缀自然过期(迁移文档 §6/§8),只交付不代执行。
- [ ] 提交点: WT/C 最终 commit。

### T13 v1.0.0 发布(设计 §10;需用户提供 Gitea token)

- [ ] 先用 0.1.0 做试发布演练: `python -m build` → twine 传 `https://gitea.iomgaa.online/api/packages/iomgaa/pypi` → 临时 conda 环境 `pip install --index-url .../pypi/simple/ polygateway==0.1.0` 回装成功。不通则退 git+https(设计 §5)并告知用户。
- [ ] PGW `pyproject.toml` bump `1.0.0` + changelog(README 或 CHANGELOG 节)→ `make ci` 全绿(发布门)→ build → 上传 → 干净环境回装 `==1.0.0` 并重跑**两项目**测试套件绿 → 依赖定稿: GovDoc 根 `pyproject.toml` 写 `polygateway[redis,structured]==1.0.*`,CHS `requirements.txt` 写 `polygateway[redis,postgres,structured]==1.0.*`(CHS 无 [project] 依赖段)+ 两项目 README/Makefile 记 Gitea index 安装命令 → PGW 打 tag `v1.0.0`。
- [ ] token 处置: 用户提供后写 `~/.pypirc` 或环境变量,不入任何仓库。
- [ ] 库仓库是否同步推 Gitea 托管: **未拍板**,发布完成后与用户另议(设计 §10 末句),本计划不执行。
- [ ] 提交点: PGW commit(bump/changelog)+ 两 worktree 各一 commit(依赖定稿)。

### T14 文档同步与收尾(设计 §15 七项)

- [ ] ROADMAP §1/§5(两项目范围、VT 放弃、状态推进);ARCHITECTURE §11.2 标注 VT 放弃、§13 Q1/Q6 落拍板;migrations/video-tree-trm5.md 头部标注放弃;migrations/{govdoc-saas,chsanalyzer}.md 回写(extras 更正、embedding 拍板、漂移修订、实施结果);CLAUDE.md reference/ 条目加"只读=工作区与 main 检出不变"括注;库 README 加 Gitea 安装命令。
- [ ] findings 报告: `research-wiki/findings/2026-07-XX-m4-acceptance.md`(≤300 行: 两项目验收证据、缺口回补清单、回归数字、发布记录)。
- [ ] 迁移合并后收尾(用户在远端合并 feature 分支后执行): `git -C reference/<proj> pull --ff-only` + `git -C reference/<proj> worktree remove <路径>`。
- [ ] 记忆文件更新(MEMORY.md + scope-decisions: M4 完成态、v1.0 已发、VT 放弃)。
- [ ] 提交点: PGW commit;合并方式交用户定。

## 3. 保真校验声明(迁移类计划必做)

本计划**不向库内迁移新蓝本**(治理代码 M1-M3 已迁毕),但属"库替换项目治理层"的反向迁移——保真对象是**项目旧行为**: 以 migrations/{govdoc-saas §7(17 条),chsanalyzer §7(26 条)} 审计表为准绳,T5/T8/T11 各含逐条核验检查点;"保留"项行为必须复现,"替换/修复"项按表内声明执行,发现表外行为差异先回写迁移文档再动代码。蓝本对照一律读 reference/(main),这是 worktree 方案保住的能力。

## 4. 中断恢复与风险

- 每任务一 commit,任务内失败回退上一 commit;两 worktree 与 PGW 分支互不阻塞(但 T6 门控 T7,T12 门控 T13)。
- 环境创建失败、中转不代理 claude、Gitea 发布不通三事项均"即停 → 与用户对齐",设计 §14 已列缓解。
- CHS requires_db/requires_redis 若需真实后端解锁更多用例,只允许指向实验室 Redis db3 / PG polygateway 库,并在基线剖面注明差异。
