10 Commits

Author SHA1 Message Date
iomgaa b2ee4fa383 docs: record v1.0.0 release and close out M4 roadmap status 2026-07-22 11:47:16 -04:00
iomgaa 6380ba7514 fix: sync __version__ with pyproject and pin test to single source 2026-07-22 11:28:20 -04:00
iomgaa 8559f10403 chore: bump version to 1.0.0 with changelog 2026-07-22 11:18:50 -04:00
iomgaa e09362dcd1 docs: sync M4 outcomes into roadmap, architecture, and migration docs
VT migration abandoned (v1.0 scope becomes two projects), Q1 resolved
to Gitea PyPI, Q6 resolved as judge exemption (unwired zero-consumer
eval scaffolding), migration docs annotated with implementation errata,
and the reference/ read-only rule clarified for the worktree workflow.
2026-07-22 10:57:00 -04:00
iomgaa 7ac51f1f55 docs: register m4-acceptance finding in wiki 2026-07-22 10:36:33 -04:00
iomgaa f50df42084 docs: record M4 two-project migration acceptance 2026-07-22 10:36:33 -04:00
iomgaa 3846305634 fix: scope postgres telemetry test to run-prefixed rows
The fixture used to DROP the shared llm_calls table on every run, wiping
concurrent migration-batch telemetry (and its own count assertion was
polluted in return). Assertions now filter by a per-run call_id prefix
and teardown deletes only its own rows.
2026-07-22 10:32:15 -04:00
iomgaa 547141cf0a docs: revise M4 plan per independent review (4I/6M) 2026-07-22 05:46:14 -04:00
iomgaa 84e23dc5c3 docs: add M4 migration implementation plan 2026-07-22 05:33:05 -04:00
iomgaa ee1bc403ad docs: revise M4 design per independent review (3I/6M) and register wiki entity 2026-07-22 05:12:45 -04:00
20 changed files with 413 additions and 39 deletions
+18
View File
@@ -0,0 +1,18 @@
# Changelog
## 1.0.0(2026-07-22)
首个正式版。统一 LLM/VLM/OCR/Embedding 调度与中转库,治理单位为一次模型调用;经 GovDoc-SaaS 与 CHSAnalyzer 两个真实项目全量迁移验收(ARCHITECTURE §11)。
- **M1 核心**: types/errors/ports 内核、OpenAI 兼容 httpx transport(SSE + 非流式)、三层活性看门狗、自研重试(错误四分类驱动,换源/退避/Retry-After)、多源多账号 + 选源 + 源冷却、内存限流/熔断、Redis/内存响应缓存(key 含 namespace/salt/多模态摘要)、SQLite 遥测(18 字段必录)、结构化输出阶梯(json_repair/原生 schema + 有界重问)、provider 注册表、`from_env` 装配。
- **M2 分布式**: Redis 六道闸限流(Lua,契约测试双后端共用)、跨进程熔断(单探针租约 + epoch fencing)、背压 stall 双条件判定、Postgres 遥测、pricing 成本、EmbeddingClient(分批/维度校验)。
- **M2.5 治理韧性**: 双通道熔断(失败率窗 + 连败 + 健康证据抑制)、健康感知选源(EWMA×在途 P2C 缺省)、AIMD 自适应并发、429 免重试预算、健康门槛降权;故障混编 soak 同场景 58.1%→98.96%。
- **M3 OCR**: OcrTextPort/OcrLayoutPort 端口族 + MonkeyOCR 双端点 transport(数值防御下沉)、OcrClient 独立治理循环、`check_health()` 逐源预检;OCR soak 1500 调用 99.73%。
- **M4 迁移验证**: GovDoc 与 CHS 全量迁移(合计约 −6800 行项目治理代码由库继任),原测试全绿 + 真实冒烟 + 50 样本回归;Gitea PyPI 分发。
安装(实验室 Gitea PyPI):
```bash
pip install --index-url https://gitea.iomgaa.online/api/packages/iomgaa/pypi/simple/ \
--extra-index-url https://pypi.org/simple/ "polygateway[redis,postgres,structured]==1.0.*"
```
+1 -1
View File
@@ -8,7 +8,7 @@
## 1. 项目元数据 ## 1. 项目元数据
- **核心目标**: PolyGateway = 统一的大语言模型(LLM/VLM/OCR,音频预留)调度与中转库。治理单位是**一次模型调用**:请求封装、多源多账号、限流、错误分类与重试、熔断、Redis 响应缓存、流式看门狗、遥测(含成本)、结构化输出策略。全组件端口化可插拔。 - **核心目标**: PolyGateway = 统一的大语言模型(LLM/VLM/OCR,音频预留)调度与中转库。治理单位是**一次模型调用**:请求封装、多源多账号、限流、错误分类与重试、熔断、Redis 响应缓存、流式看门狗、遥测(含成本)、结构化输出策略。全组件端口化可插拔。
- **架构权威文档**: `research-wiki/ARCHITECTURE.md`(架构单一事实源,含 D1-D14 决策及讨论过程、子系统设计、三项目迁移验收标准;**不受 400 行设计文档限制**,以无歧义传达既有讨论为准绳)。开发顺序见 `research-wiki/ROADMAP.md`;`research-wiki/designs/` 仅存放每次实现具体功能的设计文档。 - **架构权威文档**: `research-wiki/ARCHITECTURE.md`(架构单一事实源,含 D1-D14 决策及讨论过程、子系统设计、三项目迁移验收标准;**不受 400 行设计文档限制**,以无歧义传达既有讨论为准绳)。开发顺序见 `research-wiki/ROADMAP.md`;`research-wiki/designs/` 仅存放每次实现具体功能的设计文档。
- **参考项目**: `reference/` 下三个项目是本库的需求来源与代码蓝本(**只读,勿改**);库必须能按 ARCHITECTURE.md §11 被它们迁移接入,否则即边界缺口。 - **参考项目**: `reference/` 下三个项目是本库的需求来源与代码蓝本(**只读,勿改**;M4 起"只读"指工作区文件与 main 检出不变——迁移实施经 `git worktree``~/Projects/m4-worktrees/` 的 feature 分支进行,worktree 的 git 操作会写 `reference/*/.git` 元数据,属预期);库必须能按 ARCHITECTURE.md §11 被它们迁移接入,否则即边界缺口。
- **技术栈**: Python 3.11+,核心仅依赖 `httpx` + `pydantic`,其余(redis/sqlite/postgres/json_repair/openai)一律 optional extras。conda 环境 `PolyGateway` - **技术栈**: Python 3.11+,核心仅依赖 `httpx` + `pydantic`,其余(redis/sqlite/postgres/json_repair/openai)一律 optional extras。conda 环境 `PolyGateway`
## 2. 常用命令 ## 2. 常用命令
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "polygateway" name = "polygateway"
version = "0.1.0" version = "1.0.0"
description = "PolyGateway:实验室统一的大语言模型(LLM/VLM/OCR)调度与中转库——多源、限流、重试、熔断、缓存、遥测" description = "PolyGateway:实验室统一的大语言模型(LLM/VLM/OCR)调度与中转库——多源、限流、重试、熔断、缓存、遥测"
requires-python = ">=3.11" requires-python = ">=3.11"
dependencies = [ dependencies = [
+5 -3
View File
@@ -525,7 +525,9 @@ src/polygateway/
| `protocols.py``LLMProvider.chat(messages, *, session_id, parent_call_id)` 签名 | 库保持兼容(或一行 shim) | | `protocols.py``LLMProvider.chat(messages, *, session_id, parent_call_id)` 签名 | 库保持兼容(或一行 shim) |
| 倒推的库需求 | `from_env` 工厂(GovDoc 装配层本就缺失,库直接补上)、Postgres 遥测、缓存 key namespace 含租户 | | 倒推的库需求 | `from_env` 工厂(GovDoc 装配层本就缺失,库直接补上)、Postgres 遥测、缓存 key namespace 含租户 |
### 11.2 Video-Tree-TRM5(难度中) ### 11.2 Video-Tree-TRM5(难度中)——**已放弃迁移(2026-07-22 用户拍板: 项目本体已放弃)**
> 下表保留作历史记录与能力倒推依据(VT 倒推的库能力——多逻辑角色、cache salt、多模态摘要进 hash、OcrTextPort 等——均已交付且被其他消费方使用,不回收);v1.0 验收标准相应改为 §11.1 + §11.3 两项目。
| 项目侧 | 处置 | | 项目侧 | 处置 |
|---|---| |---|---|
@@ -564,9 +566,9 @@ src/polygateway/
| # | 问题 | 建议 | | # | 问题 | 建议 |
|---|---|---| |---|---|---|
| Q1 | 打包与分发: 内网 pip index / git+ssh 依赖 / submodule? | git+ssh 起步,稳定后内网 index | | Q1 | 打包与分发 | **已拍板(2026-07-22 用户)**: Gitea PyPI 包注册(gitea.iomgaa.online,内置 registry;twine 上传、项目侧 `pip install --index-url .../api/packages/iomgaa/pypi/simple/`);git+https 留作退路 |
| Q2 | Python 最低版本 | 3.11(覆盖三项目: 3.11×2 + 3.13×1) | | Q2 | Python 最低版本 | 3.11(覆盖三项目: 3.11×2 + 3.13×1) |
| Q3 | Embedding 客户端是否纳入。**勘误(2026-07-20,VT 迁移文档 R11)**: 初版称"各有一套独立重试实现"不实——GovDoc 的 `OpenAICompatEmbedding` 有自研退避,但 Video-Tree 的 `RemoteEmbeddingProvider` 是**同步 SDK 裸调、无任何重试**;纳入库还需异步化其端口 | **已拍板(2026-07-20 人类)**: 纳入 M2(消灭无治理的裸调 + 统一重试),含端口异步化;Embedding 端口为公共 API,随 M2 设计文档过人类门 | | Q3 | Embedding 客户端是否纳入。**勘误(2026-07-20,VT 迁移文档 R11)**: 初版称"各有一套独立重试实现"不实——GovDoc 的 `OpenAICompatEmbedding` 有自研退避,但 Video-Tree 的 `RemoteEmbeddingProvider` 是**同步 SDK 裸调、无任何重试**;纳入库还需异步化其端口 | **已拍板(2026-07-20 人类)**: 纳入 M2(消灭无治理的裸调 + 统一重试),含端口异步化;Embedding 端口为公共 API,随 M2 设计文档过人类门 |
| Q6 | CHSAnalyzer 的 judge(`core/eval/judge.py`,同步裸调 anthropic SDK)迁移路径: 走 OpenAI 兼容中转网关(零库改动)还是库提供 Anthropic 原生 transport(D2 有端口预留,未排里程碑) | 待人类拍板(CHS 迁移文档 G7) | | Q6 | CHSAnalyzer 的 judge 迁移路径 | **已拍板(2026-07-22 用户)**: M4 实测 judge/core-eval 评估流水线**零调用方、从未接线**(全仓仅自测消费),且实验室网关无 claude 系模型——本轮**豁免不动**,judge.py 原样保留;待评估流水线真正启用时再收编走库(届时裁判模型从网关现有模型选) |
| Q4 | conda 环境名 | `PolyGateway` | | Q4 | conda 环境名 | `PolyGateway` |
| Q5 | 本仓库工程脚手架(git init、`.claude/` skills、Makefile、pyproject、import-linter)何时落地 | 本文档终审通过后、M1 编码前一次落地 | | Q5 | 本仓库工程脚手架(git init、`.claude/` skills、Makefile、pyproject、import-linter)何时落地 | 本文档终审通过后、M1 编码前一次落地 |
+3 -3
View File
@@ -18,7 +18,7 @@
| M1 核心 | 内核类型 + httpx transport + 治理中间件(内存后端)+ 多源多账号 + 缓存 + SQLite 遥测 + 结构化输出 + from_env | GovDoc、Video-Tree | ✅ 完成(2026-07-20;239 测试全绿、覆盖 92%、真实网关+真实 Redis 验收通过、独立 verifier 问题清零;见 designs/2026-07-20-m1-core-design.md 状态行) | | M1 核心 | 内核类型 + httpx transport + 治理中间件(内存后端)+ 多源多账号 + 缓存 + SQLite 遥测 + 结构化输出 + from_env | GovDoc、Video-Tree | ✅ 完成(2026-07-20;239 测试全绿、覆盖 92%、真实网关+真实 Redis 验收通过、独立 verifier 问题清零;见 designs/2026-07-20-m1-core-design.md 状态行) |
| M2 分布式 | Redis 限流/熔断后端、多源 × Redis 联合验证、背压、Postgres 遥测、成本、Embedding(Q3)、压测 harness | CHSAnalyzer(治理) | ✅ 完成(2026-07-21;契约双后端全绿 + 时间语义真实等待变体 10/10、跨连接联合验证、CHS 对标十项打钩、P3 真实验收 300 次双进程七不变量 PASS、独立 verifier 0 Critical 且 Important 清零;见 designs/2026-07-20-m2-distributed-design.md 与 findings/m2-verifier-fixes.md。压测收官: P5 故障混编 40 次八不变量 PASS,P6 首跑 58.1% 暴露三大治理盲区(findings/2026-07-21-p6-soak-baseline.md)→ **M2.5 治理韧性**六轮迭代(双通道熔断/健康选源/AIMD/429 免预算/健康门槛降权/连败抑制)后同场景 **98.96%**、八不变量全 PASS,验收见 findings/2026-07-21-m25-acceptance.md 与 designs/2026-07-21-m25-resilience-design.md) | | M2 分布式 | Redis 限流/熔断后端、多源 × Redis 联合验证、背压、Postgres 遥测、成本、Embedding(Q3)、压测 harness | CHSAnalyzer(治理) | ✅ 完成(2026-07-21;契约双后端全绿 + 时间语义真实等待变体 10/10、跨连接联合验证、CHS 对标十项打钩、P3 真实验收 300 次双进程七不变量 PASS、独立 verifier 0 Critical 且 Important 清零;见 designs/2026-07-20-m2-distributed-design.md 与 findings/m2-verifier-fixes.md。压测收官: P5 故障混编 40 次八不变量 PASS,P6 首跑 58.1% 暴露三大治理盲区(findings/2026-07-21-p6-soak-baseline.md)→ **M2.5 治理韧性**六轮迭代(双通道熔断/健康选源/AIMD/429 免预算/健康门槛降权/连败抑制)后同场景 **98.96%**、八不变量全 PASS,验收见 findings/2026-07-21-m25-acceptance.md 与 designs/2026-07-21-m25-resilience-design.md) |
| M3 OCR | OCR 端口族 + MonkeyOCR transport | CHSAnalyzer(全量)、Video-Tree(OCR 升级) | ✅ 完成(2026-07-22;真实服务双端点集成 6 用例全绿(含 Redis 熔断联调与 tables/para_blocks 一致性护栏)、P7 OCR soak 故障池 1500 调用 **99.73%** 且 13 不变量全 PASS(redis 双后端跨进程,findings/2026-07-22-p7-ocr-soak.md)、G1/R9/R10 三缺口销账;见 designs/2026-07-21-m3-ocr-design.md) | | M3 OCR | OCR 端口族 + MonkeyOCR transport | CHSAnalyzer(全量)、Video-Tree(OCR 升级) | ✅ 完成(2026-07-22;真实服务双端点集成 6 用例全绿(含 Redis 熔断联调与 tables/para_blocks 一致性护栏)、P7 OCR soak 故障池 1500 调用 **99.73%** 且 13 不变量全 PASS(redis 双后端跨进程,findings/2026-07-22-p7-ocr-soak.md)、G1/R9/R10 三缺口销账;见 designs/2026-07-21-m3-ocr-design.md) |
| M4 迁移验证 | 三项目逐一按 ARCHITECTURE §11 验收,缺口回补,发 v1.0 | 全部 | ⬜ 未开始 | | M4 迁移验证 | **两项目**(GovDoc→CHS)按 ARCHITECTURE §11 验收,缺口回补,发 v1.0;**Video-Tree-TRM5 项目已放弃,不迁移(2026-07-22 用户拍板)** | GovDoc、CHS | ✅ 完成(2026-07-22;GovDoc verifier 清零、CHS 回归 50/50 + verifier 1I3M 全清,见 findings/2026-07-22-m4-acceptance.md;**v1.0.0 已发布至 Gitea PyPI** 并经干净环境回装 + 两项目对正式版重验全绿;两项目迁移分支已发 PR: GovDoc-SaaS#2 / CHSAnalyzer#19) |
## 2. M1 核心(目标: GovDoc / Video-Tree 可试点接入) ## 2. M1 核心(目标: GovDoc / Video-Tree 可试点接入)
@@ -58,9 +58,9 @@
**验收出口**: 对真实 MonkeyOCR 服务(LAN)双端点集成测试通过;CHSAnalyzer 的 `MonkeyOcrParseInvoker` 路径可替换;Video-Tree 的裸调 OCR 换库后获得重试/熔断。 **验收出口**: 对真实 MonkeyOCR 服务(LAN)双端点集成测试通过;CHSAnalyzer 的 `MonkeyOcrParseInvoker` 路径可替换;Video-Tree 的裸调 OCR 换库后获得重试/熔断。
## 5. M4 迁移验证(目标: 项目验收,发 v1.0) ## 5. M4 迁移验证(目标: 项目验收,发 v1.0)
**顺序**(按难度递增,每个项目的缺口回补后再迁下一个): ① GovDoc-SaaS → ② Video-Tree-TRM5 CHSAnalyzer。每项目按其迁移文档(`research-wiki/migrations/<project>.md`,含删除清单、调用点映射、配置迁移、分步回滚点、旧版行为审计)执行,原测试全绿为过关;发现的边界缺口回补进库(可能触发小版本迭代)后重验。全部通过后打 `v1.0.0`,分发方式按 Q1 拍板结果执行 **范围修订(2026-07-22 用户拍板)**: Video-Tree-TRM5 项目已放弃,退出迁移范围;v1.0 验收标准改为 GovDoc-SaaS 与 CHSAnalyzer 两项目全过。**顺序**: ① GovDoc-SaaS CHSAnalyzer。每项目按其迁移文档执行,原测试全绿 + 真实冒烟 + 真实回归为过关;边界缺口回补进库后重验。全部通过后打 `v1.0.0`,发布至 Gitea PyPI(Q1 拍板)。**执行方式**: git worktree(reference/ 本体停 main 作蓝本,feature 分支在 `~/Projects/m4-worktrees/`);实施记录见 designs/plans/findings 的 2026-07-22-m4-* 三件套
**主要风险**: 迁移中发现隐性行为依赖(参考 Video-Tree CLAUDE.md 的"前序版本对照"教训)→ 每项目迁移前先做旧版行为审计(brainstorming skill 已内置该环节)。 **主要风险**: 迁移中发现隐性行为依赖(参考 Video-Tree CLAUDE.md 的"前序版本对照"教训)→ 每项目迁移前先做旧版行为审计(brainstorming skill 已内置该环节)。
@@ -35,14 +35,14 @@
**CHS 漂移**(5ac9f59→7eb8482,135 文件 +9818/1853,治理关键文件仅 44+/83): **CHS 漂移**(5ac9f59→7eb8482,135 文件 +9818/1853,治理关键文件仅 44+/83):
- positioning 子系统重构为纯图像模板分类(`TemplateVascularPositioner`),**position worker 不再消费 VLM**;`PositionCallScheduler` 端口与 `load_single_vlm_capacity` 已删除。 - 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 一行作废。 - 影响修订三条:迁移文档 §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 **零实质变更**(仅格式化),迁移文档其余 file:line 证据仍有效 - governance/invokers/limiter/scripts/selector/streaming/errors/judge/contracts_limiter **零实质变更**(仅格式化);迁移文档其余证据**语义有效,行号以实施时实测为准**(tracking/config/container/ports 有小幅行号漂移)
## 4. 决策 1:迁移工作区机制 ## 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 证据在工作区失效 | | 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,惯例现成 | 多一个目录;用户在别处打开项目时需知道实施分支在 worktree 里(向用户交代路径即可) | | **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 的严格上位 | | C 外部独立 clone | 在项目外重新 clone 两仓库实施 | 与 B 同等隔离 | 双份仓库易漂移(reference/ 更新后 clone 不同步);B 是 C 的严格上位 |
**推荐 B**。它交付的正是用户要的东西(在项目本体仓库的 feature 分支上真实迁移、可 push),但不付出硬边界与蓝本对照的代价;用户拍板 A 的实质诉求是"能改真仓库",B 完全满足。若人类门坚持 A,则钩子改法为:reference/ 写拦截整段替换为"仅拦 rm -rf 类危险命令",CLAUDE.md §1/§5 的"只读勿改"措辞改为"M4 起为迁移工作区;蓝本对照以 git 历史为准"。 **推荐 B**。它交付的正是用户要的东西(在项目本体仓库的 feature 分支上真实迁移、可 push),但不付出硬边界与蓝本对照的代价;用户拍板 A 的实质诉求是"能改真仓库",B 完全满足。若人类门坚持 A,则钩子改法为:reference/ 写拦截整段替换为"仅拦 rm -rf 类危险命令",CLAUDE.md §1/§5 的"只读勿改"措辞改为"M4 起为迁移工作区;蓝本对照以 git 历史为准"。
@@ -63,7 +63,7 @@ judge(`core/eval/judge.py`,快照后零变更)默认 provider=anthropic,同步
| 方案 | 内容 | 权衡 | | 方案 | 内容 | 权衡 |
|---|---|---| |---|---|---|
| **走实验室 OpenAI 兼容中转(推荐)** | judge 配 `JUDGE__{PROVIDER}__1__*` 指向实验室中转网关,claude 模型名经中转;`_call_llm` `asyncio.run(client.chat(...))`(Judge 端口同步、runner 无事件循环,桥接安全),解析换库 `JsonRepairStrategy` | 零库改动;**前置确认**: 中转网关是否代理 judge 所需 claude 模型(人类门时确认,不通则回退方案 3) | | **走实验室 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 储备 | | 库新增 Anthropic 原生 transport | D2 有端口预留 | 新子系统 = 新公共承诺 + 独立设计与测试,为单一消费点开新承诺违反 YAGNI;M4 内否决,留 D2 储备 |
| judge 暂缓收编 | judge.py 保持裸调,标注技术债 | 违背"全量迁移"验收口径;仅作中转不可用时的降级,需用户显式同意 | | judge 暂缓收编 | judge.py 保持裸调,标注技术债 | 违背"全量迁移"验收口径;仅作中转不可用时的降级,需用户显式同意 |
@@ -78,7 +78,7 @@ judge(`core/eval/judge.py`,快照后零变更)默认 provider=anthropic,同步
| G3 | `.env`/`.env.example` 改键(平铺单源 → `LLM__{PROVIDER}__1__*`;新增 namespace 等;凭据先读 .env 真实值再写) | `from_env()` 无缺配置报错 | commit | | G3 | `.env`/`.env.example` 改键(平铺单源 → `LLM__{PROVIDER}__1__*`;新增 namespace 等;凭据先读 .env 真实值再写) | `from_env()` 无缺配置报错 | commit |
| G4 | 装配进 api lifespan 与 arq worker 入口(显式 `retryable_exceptions=(TransientError, AllSourcesExhausted)` + `aclose()`) | govdoc 业务测试绿;注错验证步级重试可触发 | 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 | | 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 | `make ci` 全绿 + import-linter 过 | 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 报告 | — | | G7 | 验收: 验收公式逐项核对 + 冒烟/回归(§9)+ 独立 verifier | ci 输出 + verifier 报告 | — |
GovDoc 特有事实(迁移文档 §1): 装配层从未存在、`GovernedLLMClient` 零生产调用方、零异常捕获点——业务代码对治理层消费零改动,风险≈0;缓存 key 全变无成本(未上生产)。 GovDoc 特有事实(迁移文档 §1): 装配层从未存在、`GovernedLLMClient` 零生产调用方、零异常捕获点——业务代码对治理层消费零改动,风险≈0;缓存 key 全变无成本(未上生产)。
@@ -104,11 +104,11 @@ CHS 特有约束: 遥测后端配 `PGW_TELEMETRY_BACKEND=postgres` 指 PG 实例
| 项目 | 冒烟(已定) | 回归候选 | | 项目 | 冒烟(已定) | 回归候选 |
|---|---|---| |---|---|---|
| GovDoc | G2 的 spike: AgentLoop 真实调用 + 遥测/缓存命中实测 | 无旧遥测基线(骨架期),**建议冒烟即回归**;可加"重跑同请求缓存命中率非零"一条 | | GovDoc | G2 的 spike: AgentLoop 真实调用 + 遥测/缓存命中实测 | 无旧遥测基线(骨架期),**建议冒烟即回归**;可加"重跑同请求缓存命中率非零"一条 |
| CHS | 单张真实样本走完整提取 pipeline(worktree 内,治理走库) | (a)**小批对拍(建议)**: 选 20-50 张真实样本,旧版(reference/ main)与新版(worktree)各跑提取,对比结构化结果一致率 + 错误率/延迟(新版读库遥测,旧版读运行日志);(b) testing 环境 docker 部署跑 e2e;(c) 只冒烟不批跑。样本量、一致率阈值、跑批环境届时拍板 | | CHS | 单张真实样本走完整提取 pipeline(worktree 内,治理走库) | (a)**小批对拍(建议)**: 选 20-50 张真实样本,旧版(reference/ main)与新版(worktree)各跑提取,对比结构化结果一致率 + 错误率/延迟(新版读库遥测,旧版读运行日志);(b) testing 环境 docker 部署跑 e2e;(c) 只冒烟不批跑。**先行写死两条**: 新旧两轮**串行执行**(并行则对同一真实上游 RPM 双份消耗,同"禁止双栈并跑"精神);旧版跑批同样只用实验室 Redis db3 与 PG polygateway 库,不触生产后端。样本量、一致率阈值、跑批环境届时拍板 |
## 10. v1.0 发布清单(两项目验收全过后) ## 10. v1.0 发布清单(两项目验收全过后)
① 库版本 bump `1.0.0` + 汇总 changelog;② `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 双远端或迁移,人类门确认)。 ① 库版本 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. 旧版行为审计 ## 11. 旧版行为审计
@@ -116,7 +116,7 @@ CHS 特有约束: 遥测后端配 `PGW_TELEMETRY_BACKEND=postgres` 指 PG 实例
## 12. 非功能维度 ## 12. 非功能维度
- **并发与取消**: 治理层行为全部由库承载(M1-M3 已验收);项目侧新增物仅 shim(纯映射,无状态)与装配代码。CHS `asyncio.run` 桥接点(judge)在同步 runner 线程调用,无嵌套事件循环风险(实施时以 `asyncio.get_running_loop()` 探测断言钉住)。CancelledError 穿透:shim 不含 try/except 包裹,天然穿透。 - **并发与取消**: 治理层行为全部由库承载(M1-M3 已验收);项目侧新增物仅 shim(纯映射,无状态)与装配代码。CHS judge 桥接按 §6 钉死: client 构造与 aclose 均在单次 `asyncio.run` 协程内,杜绝 httpx 连接池跨事件循环复用;同步 runner 线程无嵌套循环风险(实施时以 `asyncio.get_running_loop()` 探测断言钉住)。CancelledError 穿透:shim 不含 try/except 包裹,天然穿透。
- **降级方向**: 继承库铁律(缓存/遥测静默、限流/熔断报错);GovDoc B15 步级重试作为业务侧兜底显式保留(G4 注入库异常类型,静默失效风险以注错测试钉死)。 - **降级方向**: 继承库铁律(缓存/遥测静默、限流/熔断报错);GovDoc B15 步级重试作为业务侧兜底显式保留(G4 注入库异常类型,静默失效风险以注错测试钉死)。
- **幂等与重复**: 迁移步骤均以 commit 为回滚点、可重入(重复执行 editable install/改键无副作用);Gitea 同版本重复上传会被 409 拒绝,重发布必须 bump 版本。 - **幂等与重复**: 迁移步骤均以 commit 为回滚点、可重入(重复执行 editable install/改键无副作用);Gitea 同版本重复上传会被 409 拒绝,重发布必须 bump 版本。
- **持久化与原子性**: 项目侧不新增持久化;CHS 生产切换的 Redis key 前缀切换风险(短暂限流清零)已在迁移文档 §8 论述,手册中标注低峰执行。 - **持久化与原子性**: 项目侧不新增持久化;CHS 生产切换的 Redis key 前缀切换风险(短暂限流清零)已在迁移文档 §8 论述,手册中标注低峰执行。
@@ -125,7 +125,7 @@ CHS 特有约束: 遥测后端配 `PGW_TELEMETRY_BACKEND=postgres` 指 PG 实例
- 缺口回补的判定口径: 迁移中任何"库能力不足以让原测试通过/原行为复现"即边界缺口 → 回补进库(走库侧常规红绿流程 + 小版本)→ 项目侧重验;禁止在项目侧用变通代码掩盖库缺口。 - 缺口回补的判定口径: 迁移中任何"库能力不足以让原测试通过/原行为复现"即边界缺口 → 回补进库(走库侧常规红绿流程 + 小版本)→ 项目侧重验;禁止在项目侧用变通代码掩盖库缺口。
- 项目侧新增代码(shim/装配)的失败路径: 全部落库四分类语义,shim 不吞不改异常类型;每个 shim 有先失败后通过的单测证据(测试结果门照旧)。 - 项目侧新增代码(shim/装配)的失败路径: 全部落库四分类语义,shim 不吞不改异常类型;每个 shim 有先失败后通过的单测证据(测试结果门照旧)。
- 测试基线纪律: 迁移前基线的 pass/skip 剖面留档;迁移后全绿且 skip 不增;两项目测试在各自 conda 环境跑,不与库 soak 并跑(Redis db3 FLUSHDB 冲突)。 - 测试基线纪律: 迁移前基线的 pass/skip 剖面留档,并同步留存**预期测试增删清单**(G6/C6 删除的自测文件导致分母变化,核对时按清单修正);迁移后全绿且 skip 不增;两项目测试在各自 conda 环境跑,不与库 soak 并跑(Redis db3 FLUSHDB 冲突)。
## 14. 风险 ## 14. 风险
@@ -139,7 +139,7 @@ CHS 特有约束: 遥测后端配 `PGW_TELEMETRY_BACKEND=postgres` 指 PG 实例
## 15. 文档同步清单(实施尾声执行) ## 15. 文档同步清单(实施尾声执行)
① ROADMAP §1/§5: M4 范围改两项目、VT 标注放弃(2026-07-22 用户拍板)、状态推进;② ARCHITECTURE §11.2: 标注 VT 已放弃不迁移(能力倒推记录保留);§13 Q1/Q6 落拍板结果;③ migrations/video-tree-trm5.md 头部标注放弃;④ migrations/{govdoc-saas,chsanalyzer}.md: 漂移修订(§3)与实施结果回写;⑤ CLAUDE.md: 若人类门选方案 A 则改 reference/ 铁律措辞,选 B 则零改动;⑥ 库 README/发布说明: Gitea 安装命令 ① 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. 被否决的备选汇总 ## 16. 被否决的备选汇总
+14
View File
@@ -0,0 +1,14 @@
---
type: design
node_id: design:m4-migration
title: "M4 迁移验证设计(GovDoc→CHS,发 v1.0)"
date: 2026-07-22
---
# M4 迁移验证设计(GovDoc→CHS,发 v1.0)
正文: `2026-07-22-m4-migration-design.md`
- **选定方案**: 迁移工作区用 git worktree(reference/ 本体保持只读、钩子零改动,worktree 在项目外承载 feature 分支);依赖两阶段(开发期 editable → 验收后发 v1.0.0 至 Gitea PyPI 回装);CHS judge 走实验室 OpenAI 兼容中转。
- **被否决备选及理由**: 直接改 reference/ + 放开钩子(硬边界弱化、旧版蓝本对照丢失——若人类门坚持则按 §4-A 的钩子改法执行);外部独立 clone(双份漂移);全程 wheel 分发(迭代慢)与 git+https(放弃 pip index 价值,留作退路);Anthropic 原生 transport(单一消费点开新公共承诺,违 YAGNI);为 VT 放弃回收库能力(任务外变更)。
- **范围拍板**: VT 项目已放弃不迁移(2026-07-22 用户),v1.0 验收改两项目。
@@ -0,0 +1,57 @@
# M4 迁移验证验收记录(GovDoc + CHS;发布段待 T13 回填)
> **状态**: 两项目迁移验收完成(2026-07-22);v1.0 发布与文档同步进行中。
> 工作区: `~/Projects/m4-worktrees/{GovDoc-SaaS,CHSAnalyzer}` 各自 `feat/polygateway-migration` 分支;reference/ 本体全程停 main 作蓝本。
## 1. GovDoc-SaaS(全量迁移 ✅,verifier 清零)
| 项 | 结果 |
|---|---|
| 删除 | llm/ 六文件(client 582/breaker 70/streaming 131/redis_cache 94/telemetry_sqlite 204)+ test_breaker + 手写 embedding 客户端,净 1171 行 |
| 改写 | types.py(LLMResponse re-export)、protocols.py(删 TelemetryRecorder)、test_imports、两 pyproject + Makefile(删 redis extra、加 polygateway 依赖) |
| 净新增 | `api/assembly.py` 装配点(from_env + `AGENT_RETRYABLE_EXCEPTIONS`)+ app.py lifespan;`PgwEmbeddingProvider` 适配器 |
| 测试 | `make ci` 全绿: 146+85 passed / 65+19 skipped(基线 143+85/65+19,skip 零增) |
| 冒烟 | 真实网关调用 + isinstance(LLMProvider) + 遥测行 + 二次调用缓存命中(跨进程) |
| 关键行为证据 | B15 两态钉死: 缺省 retryable_exceptions 下库 TransientError 致 `stop_reason=="error"`(静默失效态);注入后步级重试触发(test_agent_loop.py 新增两用例) |
| verifier | 清零通过(0C/0I/3 Minor 均为文档措辞,归 T14 回写) |
## 2. CHSAnalyzer(全量迁移 ✅,verifier 1I/3M 全清)
| 项 | 结果 |
|---|---|
| 删除 | governance/invokers/selector/streaming + coordination limiter/scripts/provider_gate + errors.py Provider 族 + config.py 治理解析段 + ports.py 限流/熔断端口 + 18 个自测/fake,累计约 5600 行 |
| 新增/改写 | `pgw_shims.py`(PgwVlmProvider,magic bytes 逐字保真)、table_locator(+PgwTableLocator,几何纯函数零改动)、container 换库工厂(lease/stall 守卫归库 G6)、tracking 直接 except 库 `GatewayUnavailableError`/`RequestRejectedError`、startup 活性守卫改读 GatewaySettings |
| 测试 | `make test` 全绿: 836 passed / 99 skipped / 覆盖 86%(基线 961/111;分母经 verifier 独立复算闭合: 删除 131、新增 +6,skip −12 全来自被删文件) |
| 冒烟 | 真实 MonkeyOCR 定位命中 + 真实网关 VLM(qwen3.7-plus)提取,3 样本全过业务解析门 |
| 回归(用户拍板 50 样本) | **提取解析 50/50(100%)**、定位零异常(35 命中,15 为合法无表/无 marker 语义)、p50 22.4s、错误分类零散落;遥测完整性以干净窗口 10/10 专项复验(证据 tests/outputs/m4/) |
| judge | **豁免**(用户拍板 2026-07-22): core/eval/ 评估流水线实测零调用方、从未接线,judge.py 原样保留;待评估流水线真正启用时再收编走库 |
| 交付物 | 生产切换手册 `research-wiki/findings/2026-07-22-polygateway-cutover-runbook.md`(原子切换/禁混跑/低峰/回滚/行为差异五节) |
| verifier | 代码本体逐条合格(审计 26 条抽查、shim 逐字段、首表口径等价、越界零);1 Important + 3 Minor 全部处置见 §3 |
## 3. verifier findings 处置
| # | 内容 | 处置 |
|---|---|---|
| I1 | 库 `test_postgres_telemetry` fixture 每次 `DROP TABLE llm_calls`——pre-commit 钩子跑测试即清空共享遥测表,曾抹掉回归批跑证据(双向污染: 批跑行也曾使该测试 51==50 失败) | **已修**(PGW 3846305): 测试改 run 级 call_id 前缀隔离 + teardown 只删自己的行,零 DROP;6/6 绿 |
| M1 | WT/C `tests/contracts_limiter.py` 成孤儿 | 已删(6d098ec) |
| M2 | 遥测 10/10 复验未落档且行被 I1 抹掉 | I1 修复后重跑并落档 `tests/outputs/m4/chs_telemetry_10_rerun.log`(10/10) |
| M3 | 4 处 docstring 引用已删的 ProviderError 族 | 已更新(6d098ec) |
## 4. 缺口回补清单(库侧)
M4 全程唯一库侧改动即 §3-I1 的测试隔离修复;**零功能缺口**——两项目所有旧行为均由库既有能力承接(G1-G6/R1-R12 在 M1-M3 已销账的判断经实迁验证成立)。
## 5. 教训(已入记忆)
- 共享后端(Redis db3 与 PG polygateway 库)靠**时序**隔离: 真实批跑期间执行 `git commit`(钩子跑库测试)= 并跑事故;
- tmux 里跑 conda 程序须 `conda run --no-capture-output ... python -u`,否则输出全程缓冲;
- 网关模型清单要先探测再定方案(Q6"走中转调 claude"因网关无 claude 系而作废,judge 最终因未接线豁免)。
## 6. 发布记录(T13,2026-07-22)
- 0.1.0 试发布演练通过(twine → Gitea PyPI → 干净 conda 环境匿名回装);
- bump 1.0.0 + CHANGELOG.md;发布门 `make ci` 507 过 0 败;发布中发现 `__version__` 未随 pyproject 同步,已修(测试改为对照 pyproject 单一版本源),Gitea 侧删旧包重传;
- **v1.0.0 已发布**: 干净环境回装 `polygateway[redis,postgres,structured]==1.0.0` 版本核验通过;两项目环境切正式版后套件重验全绿(GovDoc 146+85 / CHS 836);
- 依赖定稿: GovDoc 根 pyproject `==1.0.*` + docagent-core `>=1.0,<2` + Makefile 带 Gitea extra-index;CHS requirements.txt `==1.0.*` + extra-index-url 行;
- 库仓库: main 快进合并 feat/m4-migration 并打 tag `v1.0.0`;迁移分支已 push GitHub 并发 PR(GovDoc-SaaS#2 / CHSAnalyzer#19,合并由用户执行);
- 遗留: PolyGateway 托管至 Gitea 需用户建仓(token 缺 write:user 域,push-to-create 未启用),远程 `gitea` 已配好待 push;reference/ 本体收尾同步在用户合并 PR 后执行(ff-pull + worktree remove)。
+9
View File
@@ -0,0 +1,9 @@
---
type: finding
node_id: finding:m4-acceptance
title: "M4 迁移验收(GovDoc+CHS)"
date: 2026-07-22
---
# M4 迁移验收(GovDoc+CHS)
+22
View File
@@ -75,6 +75,21 @@
"id": "finding:p7-ocr-soak", "id": "finding:p7-ocr-soak",
"label": "P7 OCR soak 验收: 99.73% 与 13 不变量全 PASS", "label": "P7 OCR soak 验收: 99.73% 与 13 不变量全 PASS",
"type": "finding" "type": "finding"
},
{
"id": "design:m4-migration",
"label": "M4 迁移验证设计(GovDoc→CHS,发 v1.0)",
"type": "design"
},
{
"id": "plan:m4-migration",
"label": "M4 迁移实现计划(T0-T14)",
"type": "plan"
},
{
"id": "finding:m4-acceptance",
"label": "M4 迁移验收(GovDoc+CHS)",
"type": "finding"
} }
], ],
"links": [ "links": [
@@ -133,6 +148,13 @@
"relation": "implements", "relation": "implements",
"evidence": "T1-T10 逐任务落地设计 §3-§12", "evidence": "T1-T10 逐任务落地设计 §3-§12",
"added": "2026-07-22T02:09:43.346922+00:00" "added": "2026-07-22T02:09:43.346922+00:00"
},
{
"source": "plan:m4-migration",
"target": "design:m4-migration",
"relation": "implements",
"evidence": "T0-T14 逐节实现设计 §4-§10/§15",
"added": "2026-07-22T09:33:05.357964+00:00"
} }
] ]
} }
+10 -4
View File
@@ -1,37 +1,43 @@
# Research Wiki 索引 # Research Wiki 索引
> 自动生成,更新时间:2026-07-22 05:33 UTC > 自动生成,更新时间:2026-07-22 14:36 UTC
## design (8) ## design (10)
- [2026-07-20-m1-core-design](designs/2026-07-20-m1-core-design.md) `design:2026-07-20-m1-core-design` - [2026-07-20-m1-core-design](designs/2026-07-20-m1-core-design.md) `design:2026-07-20-m1-core-design`
- [2026-07-20-m2-distributed-design](designs/2026-07-20-m2-distributed-design.md) `design:2026-07-20-m2-distributed-design` - [2026-07-20-m2-distributed-design](designs/2026-07-20-m2-distributed-design.md) `design:2026-07-20-m2-distributed-design`
- [2026-07-21-m25-resilience-design](designs/2026-07-21-m25-resilience-design.md) `design:2026-07-21-m25-resilience-design` - [2026-07-21-m25-resilience-design](designs/2026-07-21-m25-resilience-design.md) `design:2026-07-21-m25-resilience-design`
- [2026-07-21-m3-ocr-design](designs/2026-07-21-m3-ocr-design.md) `design:2026-07-21-m3-ocr-design` - [2026-07-21-m3-ocr-design](designs/2026-07-21-m3-ocr-design.md) `design:2026-07-21-m3-ocr-design`
- [2026-07-22-m4-migration-design](designs/2026-07-22-m4-migration-design.md) `design:2026-07-22-m4-migration-design`
- [M1 核心里程碑设计:公共签名冻结与治理栈落地](designs/m1-core-design.md) `design:m1-core-design` - [M1 核心里程碑设计:公共签名冻结与治理栈落地](designs/m1-core-design.md) `design:m1-core-design`
- [M2 分布式:Redis 治理后端+背压+Postgres 遥测+pricing+Embedding+压测 harness](designs/m2-distributed.md) `design:m2-distributed` - [M2 分布式:Redis 治理后端+背压+Postgres 遥测+pricing+Embedding+压测 harness](designs/m2-distributed.md) `design:m2-distributed`
- [M2.5 治理韧性: 半死源隔离与健康感知调度](designs/m25-resilience.md) `design:m25-resilience` - [M2.5 治理韧性: 半死源隔离与健康感知调度](designs/m25-resilience.md) `design:m25-resilience`
- [M3 OCR 端口族设计](designs/m3-ocr.md) `design:m3-ocr` - [M3 OCR 端口族设计](designs/m3-ocr.md) `design:m3-ocr`
- [M4 迁移验证设计(GovDoc→CHS,发 v1.0)](designs/m4-migration.md) `design:m4-migration`
## finding (9) ## finding (11)
- [2026-07-20-m2-soak-workload](findings/2026-07-20-m2-soak-workload.md) `finding:2026-07-20-m2-soak-workload` - [2026-07-20-m2-soak-workload](findings/2026-07-20-m2-soak-workload.md) `finding:2026-07-20-m2-soak-workload`
- [2026-07-21-m25-acceptance](findings/2026-07-21-m25-acceptance.md) `finding:2026-07-21-m25-acceptance` - [2026-07-21-m25-acceptance](findings/2026-07-21-m25-acceptance.md) `finding:2026-07-21-m25-acceptance`
- [2026-07-21-p6-soak-baseline](findings/2026-07-21-p6-soak-baseline.md) `finding:2026-07-21-p6-soak-baseline` - [2026-07-21-p6-soak-baseline](findings/2026-07-21-p6-soak-baseline.md) `finding:2026-07-21-p6-soak-baseline`
- [2026-07-22-m4-acceptance](findings/2026-07-22-m4-acceptance.md) `finding:2026-07-22-m4-acceptance`
- [2026-07-22-p7-ocr-soak](findings/2026-07-22-p7-ocr-soak.md) `finding:2026-07-22-p7-ocr-soak` - [2026-07-22-p7-ocr-soak](findings/2026-07-22-p7-ocr-soak.md) `finding:2026-07-22-p7-ocr-soak`
- [M2 verifier 三项 Important 补齐(不变量接线/网关保护/P3 验收)](findings/m2-verifier-fixes.md) `finding:m2-verifier-fixes` - [M2 verifier 三项 Important 补齐(不变量接线/网关保护/P3 验收)](findings/m2-verifier-fixes.md) `finding:m2-verifier-fixes`
- [M2 真实数据压测: 场景矩阵与数据清单](findings/m2-soak-workload.md) `finding:m2-soak-workload` - [M2 真实数据压测: 场景矩阵与数据清单](findings/m2-soak-workload.md) `finding:m2-soak-workload`
- [M2.5 验收: P6 同场景 58.1% → 98.96%](findings/m25-acceptance.md) `finding:m25-acceptance` - [M2.5 验收: P6 同场景 58.1% → 98.96%](findings/m25-acceptance.md) `finding:m25-acceptance`
- [M4 迁移验收(GovDoc+CHS)](findings/m4-acceptance.md) `finding:m4-acceptance`
- [P6 混合浸泡首跑基线与记分板三重伪击穿修复](findings/p6-soak-baseline.md) `finding:p6-soak-baseline` - [P6 混合浸泡首跑基线与记分板三重伪击穿修复](findings/p6-soak-baseline.md) `finding:p6-soak-baseline`
- [P7 OCR soak 验收: 99.73% 与 13 不变量全 PASS](findings/p7-ocr-soak.md) `finding:p7-ocr-soak` - [P7 OCR soak 验收: 99.73% 与 13 不变量全 PASS](findings/p7-ocr-soak.md) `finding:p7-ocr-soak`
## plan (8) ## plan (10)
- [2026-07-20-m1-core-plan](plans/2026-07-20-m1-core-plan.md) `plan:2026-07-20-m1-core-plan` - [2026-07-20-m1-core-plan](plans/2026-07-20-m1-core-plan.md) `plan:2026-07-20-m1-core-plan`
- [2026-07-20-m2-distributed-plan](plans/2026-07-20-m2-distributed-plan.md) `plan:2026-07-20-m2-distributed-plan` - [2026-07-20-m2-distributed-plan](plans/2026-07-20-m2-distributed-plan.md) `plan:2026-07-20-m2-distributed-plan`
- [2026-07-21-m25-resilience-plan](plans/2026-07-21-m25-resilience-plan.md) `plan:2026-07-21-m25-resilience-plan` - [2026-07-21-m25-resilience-plan](plans/2026-07-21-m25-resilience-plan.md) `plan:2026-07-21-m25-resilience-plan`
- [2026-07-21-m3-ocr-plan](plans/2026-07-21-m3-ocr-plan.md) `plan:2026-07-21-m3-ocr-plan` - [2026-07-21-m3-ocr-plan](plans/2026-07-21-m3-ocr-plan.md) `plan:2026-07-21-m3-ocr-plan`
- [2026-07-22-m4-migration-plan](plans/2026-07-22-m4-migration-plan.md) `plan:2026-07-22-m4-migration-plan`
- [M1 核心里程碑实现计划](plans/m1-core-plan.md) `plan:m1-core-plan` - [M1 核心里程碑实现计划](plans/m1-core-plan.md) `plan:m1-core-plan`
- [M2 分布式实现计划](plans/m2-distributed.md) `plan:m2-distributed` - [M2 分布式实现计划](plans/m2-distributed.md) `plan:m2-distributed`
- [M2.5 治理韧性实现计划](plans/m25-resilience.md) `plan:m25-resilience` - [M2.5 治理韧性实现计划](plans/m25-resilience.md) `plan:m25-resilience`
- [M3 OCR 实现计划](plans/m3-ocr.md) `plan:m3-ocr` - [M3 OCR 实现计划](plans/m3-ocr.md) `plan:m3-ocr`
- [M4 迁移实现计划(T0-T14)](plans/m4-migration.md) `plan:m4-migration`
## schema (1) ## schema (1)
- [表结构: llm_calls(遥测 18 字段)](schemas/llm-calls.md) `schema:llm-calls` - [表结构: llm_calls(遥测 18 字段)](schemas/llm-calls.md) `schema:llm-calls`
+7
View File
@@ -39,3 +39,10 @@
- [2026-07-22 02:09 UTC] 重建索引: 26 篇页面 - [2026-07-22 02:09 UTC] 重建索引: 26 篇页面
- [2026-07-22 05:33 UTC] 新增 finding: P7 OCR soak 验收: 99.73% 与 13 不变量全 PASS (finding:p7-ocr-soak) - [2026-07-22 05:33 UTC] 新增 finding: P7 OCR soak 验收: 99.73% 与 13 不变量全 PASS (finding:p7-ocr-soak)
- [2026-07-22 05:33 UTC] 重建索引: 28 篇页面 - [2026-07-22 05:33 UTC] 重建索引: 28 篇页面
- [2026-07-22 08:58 UTC] 新增 design: M4 迁移验证设计(GovDoc→CHS,发 v1.0) (design:m4-migration)
- [2026-07-22 08:58 UTC] 重建索引: 30 篇页面
- [2026-07-22 09:33 UTC] 新增 plan: M4 迁移实现计划(T0-T14) (plan:m4-migration)
- [2026-07-22 09:33 UTC] 新增边: plan:m4-migration --implements--> design:m4-migration
- [2026-07-22 09:33 UTC] 重建索引: 32 篇页面
- [2026-07-22 14:36 UTC] 新增 finding: M4 迁移验收(GovDoc+CHS) (finding:m4-acceptance)
- [2026-07-22 14:36 UTC] 重建索引: 34 篇页面
+8
View File
@@ -1,5 +1,13 @@
# CHSAnalyzer 迁移文档(迁移即验收) # CHSAnalyzer 迁移文档(迁移即验收)
> [!NOTE]
> **迁移已完成并验收(2026-07-22,M4;回归 50/50、verifier 1I3M 全清)**——worktree 分支 feat/polygateway-migration,证据见 `findings/2026-07-22-m4-acceptance.md` §2,生产切换手册在项目侧 `research-wiki/findings/2026-07-22-polygateway-cutover-runbook.md`。实施对本文的勘误/裁决:
> ① **上游漂移(基点 7eb8482)**: positioning 已重构为纯图像模板分类,`PositionCallScheduler`/`load_single_vlm_capacity`/`vascular_positioner.py` 上游已删——§2"load_single_vlm_capacity 保留"、§6-S3"灰度先切 position worker"、§4 的 vascular_positioner 调用点三条作废;VLM 消费点收敛为 extractors/classifiers(pipeline worker 单段切换)。
> ② **judge(§4/G7)**: 实测 core/eval/ 零调用方、从未接线,且实验室网关无 claude 系——用户拍板豁免不动(Q6 落定,见 ARCHITECTURE §13)。
> ③ **G1 消费形态**: tracking.py 直接 `except GatewayUnavailableError`(无翻译 shim——Provider 错误族随 §2 清场删除,翻译层是死代码);`_TERMINAL` 同步改捕库 `RequestRejectedError`
> ④ **magic bytes 归属**: 审计表"移入库 transport"修订为"移入业务 shim(`pgw_shims._data_url` 逐字保真)"——库 chat 的多模态组装归业务侧(ARCH §11.2 同款边界),transport 不拆 data URL。
> ⑤ 装配期守卫(G6 lease/stall)归库 from_env,container 本地守卫与其自测删除;限流契约文件(tests/contracts_limiter.py)已由库契约套件继任后删除。
> **定位**: 本文是 `ARCHITECTURE.md §11.3` 的展开——库建成后如何合并进 CHSAnalyzer、替换哪些内部组件。与 ARCHITECTURE.md 冲突时**以 ARCHITECTURE.md 为准**。CHSAnalyzer 是三项目中迁移难度最高、能力对标要求最高的一个:库必须先达到其治理能力**逐项对等**(§1 对标清单),迁移才有动机。全部结论基于 2026-07-20 对 `reference/CHSAnalyzer/` 的代码实测(file:line 为证)。 > **定位**: 本文是 `ARCHITECTURE.md §11.3` 的展开——库建成后如何合并进 CHSAnalyzer、替换哪些内部组件。与 ARCHITECTURE.md 冲突时**以 ARCHITECTURE.md 为准**。CHSAnalyzer 是三项目中迁移难度最高、能力对标要求最高的一个:库必须先达到其治理能力**逐项对等**(§1 对标清单),迁移才有动机。全部结论基于 2026-07-20 对 `reference/CHSAnalyzer/` 的代码实测(file:line 为证)。
## 1. 迁移目标与验收定义 ## 1. 迁移目标与验收定义
+3
View File
@@ -1,5 +1,8 @@
# GovDoc-SaaS 迁移文档(迁移即验收) # GovDoc-SaaS 迁移文档(迁移即验收)
> [!NOTE]
> **迁移已完成并验收(2026-07-22,M4;verifier 清零)**——worktree 分支 feat/polygateway-migration,证据见 `findings/2026-07-22-m4-acceptance.md` §1。实施对本文的勘误: ① 文中 `polygateway[redis,telemetry-sqlite]` extras 不存在,实际为 `[redis,structured]`(SQLite 遥测零 extra;docagent-core 本体依赖裸 `polygateway`,因 types.py re-export);② §1"不迁 embedding(待 Q3)"已被 Q3 拍板(2026-07-20 纳入 M2)推翻——实施已将 embedding 换 `EmbeddingClient` + `PgwEmbeddingProvider` 薄适配器(保留文件路径,删 165 行手写实现);③ 装配点净新增为 `api/assembly.py` + app.py lifespan(arq worker 入口骨架期尚不存在,届时按 assembly.py 同款接入)。
> **定位**: 本文是 `ARCHITECTURE.md` §11.1 的展开——库建成后如何合并进 GovDoc-SaaS、替换其哪些内部组件。它既是 M4 迁移的操作指南,也是 M1-M3 设计的反向约束(库公共 API 必须让本文描述的迁移成立)。**与 ARCHITECTURE.md 冲突时以 ARCHITECTURE.md 为准。** > **定位**: 本文是 `ARCHITECTURE.md` §11.1 的展开——库建成后如何合并进 GovDoc-SaaS、替换其哪些内部组件。它既是 M4 迁移的操作指南,也是 M1-M3 设计的反向约束(库公共 API 必须让本文描述的迁移成立)。**与 ARCHITECTURE.md 冲突时以 ARCHITECTURE.md 为准。**
> 证据基线: 2026-07-20 对 `reference/GovDoc-SaaS/` 的代码实测(Read/Grep/wc),所有 file:line 相对该仓库根。 > 证据基线: 2026-07-20 对 `reference/GovDoc-SaaS/` 的代码实测(Read/Grep/wc),所有 file:line 相对该仓库根。
@@ -1,5 +1,8 @@
# Video-Tree-TRM5 迁移文档(迁移即验收) # Video-Tree-TRM5 迁移文档(迁移即验收)
> [!IMPORTANT]
> **本迁移已放弃(2026-07-22 用户拍板: VT 项目本体已放弃,不再迁移)。** 全文保留作历史记录与能力倒推依据——VT 倒推的库能力(R1-R12,含多逻辑角色/cache salt/多模态摘要/OcrTextPort/trust_env/check_health)均已在 M1-M3 交付,不因放弃而回收。v1.0 验收标准改为 GovDoc + CHS 两项目(见 ROADMAP §5)。
> **定位**: 本文是 `ARCHITECTURE.md §11.2` 的展开——库建成后如何合并进 Video-Tree-TRM5、替换其哪些内部组件。它既是 M4 迁移的操作指南,也是 M1-M3 设计的反向约束(库公共 API 必须让本文描述的迁移成立)。**与 ARCHITECTURE.md 冲突时以 ARCHITECTURE.md 为准。** 全部结论基于 2026-07-20 对 `reference/Video-Tree-TRM5/` 的代码实测(file:line 均为实测证据)。 > **定位**: 本文是 `ARCHITECTURE.md §11.2` 的展开——库建成后如何合并进 Video-Tree-TRM5、替换其哪些内部组件。它既是 M4 迁移的操作指南,也是 M1-M3 设计的反向约束(库公共 API 必须让本文描述的迁移成立)。**与 ARCHITECTURE.md 冲突时以 ARCHITECTURE.md 为准。** 全部结论基于 2026-07-20 对 `reference/Video-Tree-TRM5/` 的代码实测(file:line 均为实测证据)。
--- ---
@@ -0,0 +1,183 @@
# 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 库,并在基线剖面注明差异。
+9
View File
@@ -0,0 +1,9 @@
---
type: plan
node_id: plan:m4-migration
title: "M4 迁移实现计划(T0-T14)"
date: 2026-07-22
---
# M4 迁移实现计划(T0-T14)
+1 -1
View File
@@ -31,7 +31,7 @@ from polygateway.types import (
SourceConfig, SourceConfig,
) )
__version__ = "0.1.0" __version__ = "1.0.0"
__all__ = [ __all__ = [
"DEFAULT_PROFILES", "DEFAULT_PROFILES",
+44 -16
View File
@@ -2,12 +2,17 @@
DSN .env `PGW_TELEMETRY_PG_DSN`,缺则 skip该实例上有 app/chs_prod DSN .env `PGW_TELEMETRY_PG_DSN`,缺则 skip该实例上有 app/chs_prod
在用库本测试只允许连 polygateway 专用库(fixture 里守卫) 在用库本测试只允许连 polygateway 专用库(fixture 里守卫)
隔离纪律(M4 事故教训): `llm_calls` 是与真实批跑/迁移项目共享的表,
**严禁 DROP/TRUNCATE**本测试以 run call_id 前缀隔离,断言只看
自己写入的行,teardown 只删自己的行
""" """
from __future__ import annotations from __future__ import annotations
import asyncio import asyncio
import os import os
from uuid import uuid4
import pytest import pytest
from dotenv import dotenv_values from dotenv import dotenv_values
@@ -36,6 +41,13 @@ _EXPECTED_COLUMNS = [
"created_at", "created_at",
] ]
# run 级前缀: 同库并存的其他运行(迁移批跑/另一开发机)互不可见
_RUN_PREFIX = f"pgwtest-{uuid4().hex[:8]}"
def _cid(suffix: str) -> str:
return f"{_RUN_PREFIX}-{suffix}"
def _dsn() -> str | None: def _dsn() -> str | None:
merged = {**dotenv_values(".env"), **os.environ} merged = {**dotenv_values(".env"), **os.environ}
@@ -54,19 +66,23 @@ async def dsn():
# 隔离守卫: 该实例有 app/chs_prod/mimiciv 等在用库,只许打 polygateway 专用库 # 隔离守卫: 该实例有 app/chs_prod/mimiciv 等在用库,只许打 polygateway 专用库
if not value.rstrip("/").endswith("/polygateway"): if not value.rstrip("/").endswith("/polygateway"):
pytest.fail(f"遥测测试只允许连 polygateway 专用库,当前 DSN 库名不符: {value!r}") pytest.fail(f"遥测测试只允许连 polygateway 专用库,当前 DSN 库名不符: {value!r}")
yield value
# teardown: 只删本 run 写入的行;表可能尚不存在(全新库)则忽略
import asyncpg import asyncpg
conn = await asyncpg.connect(value, timeout=10) conn = await asyncpg.connect(value, timeout=10)
try: try:
await conn.execute("DROP TABLE IF EXISTS llm_calls") if await conn.fetchval("SELECT to_regclass('llm_calls')") is not None:
await conn.execute("DELETE FROM llm_calls WHERE call_id LIKE $1", f"{_RUN_PREFIX}-%")
finally: finally:
await conn.close() await conn.close()
return value
async def _record_minimal(recorder: PostgresRecorder, call_id: str = "c1", **overrides) -> None: async def _record_minimal(
recorder: PostgresRecorder, call_id: str | None = None, **overrides
) -> None:
fields = { fields = {
"call_id": call_id, "call_id": call_id if call_id is not None else _cid("c1"),
"parent_call_id": None, "parent_call_id": None,
"session_id": "sess-1", "session_id": "sess-1",
"model": "m", "model": "m",
@@ -89,12 +105,12 @@ async def _record_minimal(recorder: PostgresRecorder, call_id: str = "c1", **ove
await recorder.record_llm_call(**fields) await recorder.record_llm_call(**fields)
async def _fetch(dsn: str, sql: str): async def _fetch(dsn: str, sql: str, *args):
import asyncpg import asyncpg
conn = await asyncpg.connect(dsn, timeout=10) conn = await asyncpg.connect(dsn, timeout=10)
try: try:
return await conn.fetch(sql) return await conn.fetch(sql, *args)
finally: finally:
await conn.close() await conn.close()
@@ -116,9 +132,11 @@ class TestSchema:
async def test_call_id_idempotent(self, dsn): async def test_call_id_idempotent(self, dsn):
recorder = PostgresRecorder(dsn) recorder = PostgresRecorder(dsn)
try: try:
await _record_minimal(recorder, call_id="dup") await _record_minimal(recorder, call_id=_cid("dup"))
await _record_minimal(recorder, call_id="dup", response="second") await _record_minimal(recorder, call_id=_cid("dup"), response="second")
rows = await _fetch(dsn, "SELECT response FROM llm_calls WHERE call_id='dup'") rows = await _fetch(
dsn, "SELECT response FROM llm_calls WHERE call_id = $1", _cid("dup")
)
assert [r["response"] for r in rows] == ["ok"] # ON CONFLICT DO NOTHING assert [r["response"] for r in rows] == ["ok"] # ON CONFLICT DO NOTHING
finally: finally:
await recorder.aclose() await recorder.aclose()
@@ -126,8 +144,14 @@ class TestSchema:
async def test_concurrent_writes_all_land(self, dsn): async def test_concurrent_writes_all_land(self, dsn):
recorder = PostgresRecorder(dsn) recorder = PostgresRecorder(dsn)
try: try:
await asyncio.gather(*(_record_minimal(recorder, call_id=f"c{i}") for i in range(50))) await asyncio.gather(
rows = await _fetch(dsn, "SELECT count(*) AS n FROM llm_calls") *(_record_minimal(recorder, call_id=_cid(f"c{i}")) for i in range(50))
)
rows = await _fetch(
dsn,
"SELECT count(*) AS n FROM llm_calls WHERE call_id LIKE $1",
f"{_RUN_PREFIX}-c%",
)
assert rows[0]["n"] == 50 assert rows[0]["n"] == 50
finally: finally:
await recorder.aclose() await recorder.aclose()
@@ -138,17 +162,21 @@ class TestDegradation:
"""结构性失败(建池不通)→ warning 一次后永久降级,业务零感知。""" """结构性失败(建池不通)→ warning 一次后永久降级,业务零感知。"""
recorder = PostgresRecorder("postgresql://u:p@127.0.0.1:1/x") recorder = PostgresRecorder("postgresql://u:p@127.0.0.1:1/x")
await _record_minimal(recorder) # 不抛 await _record_minimal(recorder) # 不抛
await _record_minimal(recorder, call_id="c2") # 已降级短路,同样不抛 await _record_minimal(recorder, call_id=_cid("c2")) # 已降级短路,同样不抛
await recorder.aclose() await recorder.aclose()
async def test_row_failure_does_not_poison_later_rows(self, dsn): async def test_row_failure_does_not_poison_later_rows(self, dsn):
"""运行时单条写失败(NUL 字节文本被 PG 拒)→ 丢该行,后续行照常落库。""" """运行时单条写失败(NUL 字节文本被 PG 拒)→ 丢该行,后续行照常落库。"""
recorder = PostgresRecorder(dsn) recorder = PostgresRecorder(dsn)
try: try:
await _record_minimal(recorder, call_id="bad", response="nul\x00byte") await _record_minimal(recorder, call_id=_cid("bad"), response="nul\x00byte")
await _record_minimal(recorder, call_id="good") await _record_minimal(recorder, call_id=_cid("good"))
rows = await _fetch(dsn, "SELECT call_id FROM llm_calls ORDER BY call_id") rows = await _fetch(
assert [r["call_id"] for r in rows] == ["good"] dsn,
"SELECT call_id FROM llm_calls WHERE call_id = ANY($1::text[]) ORDER BY call_id",
[_cid("bad"), _cid("good")],
)
assert [r["call_id"] for r in rows] == [_cid("good")]
finally: finally:
await recorder.aclose() await recorder.aclose()
+6 -1
View File
@@ -1,10 +1,15 @@
"""包基线冒烟测试:可导入、版本号存在。""" """包基线冒烟测试:可导入、版本号存在。"""
from pathlib import Path
import polygateway import polygateway
def test_package_importable_with_version() -> None: def test_package_importable_with_version() -> None:
assert polygateway.__version__ == "0.1.0" import tomllib
declared = tomllib.loads(Path("pyproject.toml").read_text())["project"]["version"]
assert polygateway.__version__ == declared # 单一版本源: 与 pyproject 同步
def test_ocr_public_surface_exported(): def test_ocr_public_surface_exported():