type, node_id, title, date
type
node_id
title
date
design
design:2026-10-21-issue34-ocr-document-design
OCR 整份文档解析设计(#34)
2026-10-21
#34 OCR 整份文档解析设计
状态: 待人类审批 (公共 API + 端口新增,强制人类门)。已完成 Claude 自审与一轮独立 subagent 论证(§12);Codex 审查见 §12 状态。
基线: 25a90eb(1.3.10);目标版本 1.3.11 ;分支 fix/issue34-ocr-document。
需求: #34 (CHSAnalyzer3 把几百篇医学论文 PDF 整份转成带页码正文与表格)。
修订对象: M3 OCR 设计 §4 "multipart 固定名 image.jpg,服务按内容处理"(该论断只在图片样本上验证过,见 §1 E3/E4)、ARCHITECTURE §7.10。
1. 问题与根因
issue 把症状拆成两条;两条同根 : 库把 OCR 输入建模成"一张图 + 写死的标签",把输出建模成"只有框",没有"文档"这个概念 ——输入类型既不声明也不检测,输出不含内容。M3 设计 §4 的"服务按内容处理"是从 35 张图片样本外推到 PDF 的未验证假设。
以下事实 2026-10-21 在真实 MonkeyOCR 服务实测取得(样本为第三方 AI 生成论文,仅本地存证,不入库 ):
#
事实
设计影响
E1
服务按扩展名 分派,仅 .jpg/.jpeg/.png/.pdf(大小写不敏感);其余(.webp/无扩展名)→ HTTP 500 {"detail":"Parsing failed: 400: Unsupported file type…"}
上传标签必须落在这四个之内;服务把客户端错误包成 500
E2
内容与扩展名不符(jpg 字节标 doc.pdf)→ HTTP 500 Parsing failed: is no PDF
调用方声明类型会新增一个失败入口 ,故类型由库按内容判定
E3
/parse: 同一 PDF 以 image.jpg 与 doc.pdf 上传,_middle.json/_content_list.json 逐字相同 (只有 message 与包内文件名前缀不同);PNG/TIFF/BMP 字节标 image.jpg 亦成功(服务按内容解码)
问题一在 /parse 上不改变结果 ;非 PDF 沿用 image.jpg 才是零回归
E4
/ocr/text: PDF 字节标 image.jpg → HTTP 200 success:true,content="The image contains a single character: 1"(静默错误结果 );标 doc.pdf → 整份全文(无页码)
问题一的真实危害在 /ocr/text,issue 未提及;须一并修
E5
截断 PDF / 伪 %PDF- 头 / 乱码图 / 空文件 → 全部 HTTP 500 {"detail":"Parsing failed: <原因>"}
当前 ≥500 一律 TransientError,见 §6
E6
结果包含 *.md、*_middle.json、*_content_list.json、三份 *.pdf;含图/表的文档另有 images/*.jpg (4 页含 4 图 ≈598KB;无图的 2 页文档没有该目录)
包体随插图增长;只读取需要的成员,不解出图片
E7
_content_list.json 与 _middle.json 逐块 1:1(三样本 29/29、89/89、30/30),但 content_list 无 bbox 、type 词汇不同(title→text+text_level、interline_equation→equation),且文本里 $ 被转义成 \$ (134 个 text/title 块中 23 个)
不用 content_list,见 §3 Q3
E8
middle 的 text/title 块恰 1 行 1 span,content 与 content_list 反转义后 134/134 一致;table 的 span 带 html;interline_equation 的 span 内容为裸 LaTeX;image 嵌套 image_body 仅含 image_path;嵌套 *_caption/*_footnote 子块从未出现
内容可只从 middle 取得;caption/footnote 路径无真实样本
E9
速度约 1.5 s/页(2 页 3.5s,4 页 6.4s;n=3、无争用,不是 SLA)。库内无任何基于延迟的熔断/慢判定 (grep latency 在 breaker/admission/sources 无命中)
长文档不会被"判慢",真正的风险是 §7 的超时被算成失败
2. 现状代码面
位置
现状
transports/monkey_ocr.py:207-210
_files() 固定 ("image.jpg", …, "image/jpeg"),/ocr/text 与 /parse 共用
monkey_ocr.py:136-144,147-166
_parse_block 只取 type/bbox;_parse_pages 对非法 page_idx 回退枚举下标;bbox 含 CHS shim 专用的整数化退化校验
monkey_ocr.py:79-100
_classify_status: ≥500/429→Transient、401/403→SourceDead、其余→RequestRejected
types.py:1187-1250
OcrLayoutElement(type,bbox,page_index)、OcrLayoutResult、OcrLayoutTransportResult
ocr.py:625-628
_summarize 用 hasattr(result,"text") 区分文本/版面——对新结果类型是陷阱
ports.py:105-121
OcrTransport 是 @runtime_checkable,加成员会让所有现有实现(测试替身、下游 fake)在 isinstance 上不合格
3. 备选方案对比
Q1 公共面形态
方案
做法
权衡
A(推荐)
新增 OcrClient.parse_document + OcrDocumentPort;新类型 OcrDocumentElement(OcrLayoutElement)(仅增 text)与 OcrDocumentResult;parse_layout/recognize_text 签名与返回类型不动
医院线零风险;文档线的内容解析故障不波及版面路径
B
给 OcrLayoutElement 追加 text 字段,parse_layout 同时返回内容
违反 issue "parse_layout 不动";text: str|None 在一条路径上恒为 None;医院线耦合到新增的包成员解析,任一故障波及它
C
只加 filename/media_type 参数并把 ZIP 原样交给调用方
把校验、数值防御、阅读序整理全推给下游,违背库的价值;每个下游各写一份解析
Q2 上传类型如何确定
方案
做法
权衡
A(推荐)
transport 按魔数嗅探: 以 %PDF- 开头 → doc.pdf/application/pdf;其余一律沿用 image.jpg/image/jpeg 。单点 _files(),两端点一并生效
调用方无从声明错;非 PDF 路径字节级不变(E3 零回归);顺带修 E4
B
调用方传 media_type/filename
声明与内容不符 → E2 的 500 → Transient → 重试并计熔断: 笔误变熔断事件
C
嗅探全部图片格式并精确标注(png→image.png)
零收益;webp/gif 等无对应扩展名反而要拒绝,把现在能成功的路径(E3)变成失败
残留: PDF 规范允许 %PDF- 前有至多 1024 字节垃圾,严格 startswith 会让此类文件落到 image.jpg(/parse 仍按内容解码、/ocr/text 仍是 E4)。有意接受 (放宽到前 1KB 搜索会误判含 XMP 元数据的图片),文档注明。
Q3 文字内容从哪里来
方案
做法
权衡
A(推荐)
只读 _middle.json (已是 bbox/type/页码/阅读序的来源),从 lines[].spans[] 取 content/html;.md 原文单独取
单一来源,无对位、无类型映射、无版本漂移故障面;不受 \$ 转义污染
B
_content_list.json 取文字 + middle 取 bbox,按页内顺序对位并严格校验(本设计初稿方案)
1:1 是上游构造性成立但无契约 ;漂移时整份 ResultInvalid,几百篇批处理会"一批全挂";文本含 \$ 需反转义,且 title 的 level 等字段样本中无信息量(21/21 均为 1)
C
只用 content_list、不要 bbox
不满足 issue 的"每块含 bbox"
Q4 确定性坏输入(E5)的分类
方案
做法
权衡
A(推荐)
只在 parse_document 路径 : ① 本地魔数预检(既非 PDF 也非已知图片魔数 → 本地 RequestRejectedError,零 HTTP); ② 500 + Parsing failed: 且含已知确定性标记 → RequestRejectedError(保留 status_code=500)。共享 _classify_status 默认行为字节级不变
既有 parse_layout/recognize_text 的行为零变更;未知 500 仍 Transient(安全方向)
B
直接改共享 _classify_status,所有 OCR 方法受益
改变医院线对坏图的重试/熔断行为,属公共行为变更,应单独走设计 → 另开 issue (待人类决定)
C
不处理
批处理后果见 §6
Q5 长文档超时
方案
做法
权衡
A(推荐)
不加逐调用 HTTP 超时;文档线用独立 scope (如 OCR_DOC)指向同一服务 URL,自带 TIMEOUT_S/重试/熔断阈值;文档 + 测试钉死"无延迟判慢"
与现有租约不变式一致
B
parse_document(timeout_s=…) 逐调用超时
RateLimiter.try_acquire(source_key, est_tokens)(ports.py:137)无 TTL 参数,租约长度是构造期常量,逐调用放大超时会让请求活过租约 → 并发配额被突破
4. 公共 API(冻结面,人类门审这里)
4.1 类型(types.py,全部 frozen dataclass)
类型
字段
说明
OcrDocumentElement(OcrLayoutElement)
继承 type/bbox/page_index;新增 text: str
text 随 type: text/title → 纯文本(行内公式保留 $…$);interline_equation → 裸 LaTeX;table → HTML 表体;image → "";嵌套的非 body 子块(*_caption/*_footnote)作为独立元素 输出,type 取子块类型
OcrDocumentResult
elements: list[OcrDocumentElement]、page_sizes: list[tuple[float,float]]、markdown: str + 溯源五件 source_name/usage/latency_ms/call_id/raw + call_stats=None
独立定义,不继承 OcrLayoutResult: 父类末尾 call_stats 带默认值,子类无默认字段 markdown 会在导入期 TypeError
OcrDocumentTransportResult
elements、page_sizes、markdown、raw
transport → client 内部产物
约束与理由:
点
决定
为何继承
文档元素"是"版面元素 + 内容;按 bbox 裁剪的下游函数可同时吃两种。代价: dataclass __eq__ 要求同类,OcrDocumentElement != OcrLayoutElement;OcrLayoutElement docstring 加警示"追加带默认值字段前须评估子类"
为何不加 level/captions/footnotes
样本中 21 个 title 全是 level 1(无信息量);caption/footnote 在全部样本里恒为空——为从未见过的路径加公共字段不可验证。嵌套子块独立成元素的方案零新增字段,且服务将来开始关联 caption 时不丢信息
markdown
整篇产物,属 result 不属元素。含悬空图片引用 (库不外传图片),docstring 注明
阅读序
与服务 para_blocks 顺序一致,页内按页序;嵌套子块紧随其父块
4.2 端口(ports.py)
端口
签名
说明
OcrDocumentPort(新,runtime_checkable)
async def parse_document(self, document: bytes) -> OcrDocumentResult
对应 /parse 整份文档
OcrDocumentTransport(新,runtime_checkable)
async def parse_document(self, *, document: bytes, source: SourceConfig, call_id: str) -> OcrDocumentTransportResult
不往 OcrTransport 加成员 (会破坏全部现有实现的 isinstance);MonkeyOcrTransport 同时实现两者
OcrClient.parse_document(document, *, session_id=None, parent_call_id=None, tenant_id=None, meta=None, call_deadline_s=None): 签名与 parse_layout 对称。入口校验先于 _CallContext(同 _call 既有顺序): bytes 非空、validate_caller_dimensions、ensure_call_deadline、transport 须满足 OcrDocumentTransport,否则 TypeError (装配错误当场报)。
4.3 其它公共面
项
变更
CallOperation / CALL_OPERATIONS(types.py:279,285)
新增 "parse_document"。CALL_OPERATIONS 目前无任何读者 ,新增测试钉死 get_args(CallOperation) == CALL_OPERATIONS
包根导出
OcrDocumentElement、OcrDocumentResult
遥测
同一张 llm_calls,operation="parse_document";占位仍为 <ocr:document image_bytes=N>(不改字面 : 改名会破坏既有分组与 test_telemetry.py 对格式的锁定);response 摘要 <document elements=N pages=M md_chars=K>,永不写入正文 ;无新列、无 DDL 变更
配置
无新键 。文档线独立 scope 是部署方式,不是新配置(.env.example 加示例段)
5. Transport 行为(transports/monkey_ocr.py)
关切
决策
上传标签
_files() 改为 _upload_part(data): data.startswith(b"%PDF-") → ("doc.pdf", data, "application/pdf"),否则原样 ("image.jpg", data, "image/jpeg")。recognize_text/parse_layout/parse_document 三处共用。包内文件名前缀由 image_ 变 doc_,而解包靠后缀 endswith("_middle.json"),不受影响
本地预检(仅 document)
首字节既非 %PDF- 也非 JPEG(FFD8FF)/PNG/TIFF/BMP/GIF/WebP 魔数 → RequestRejectedError(不带 status_code ,服务未响应),不发 HTTP。拦截"HTML 付费墙页面存成 .pdf"这类批量抓取语料最常见的坏文件
两段协议
与 parse_layout 同: POST /parse → 校验 success is True + download_url → GET → ZIP。共用实现 ,只在 ZIP 解析处分叉
ZIP 成员
读 *_middle.json(取第一个,同 layout);.md 按 middle 名去掉 "_middle.json" + ".md" 精确配对(不取"第一个 .md");缺失 → ResultInvalidError。不用 extractall ,不解出 images/
体积上限
读取前检查 ZipInfo.file_size: middle ≤ 64 MiB、md ≤ 16 MiB(模块常量),超限 → ResultInvalidError。仅 document 路径;parse_layout 不改(医院线零变更)
页结构
复用 _parse_page_size;page_idx 必须等于页在列表中的位置 ,否则 ResultInvalidError(layout 路径的回退枚举会让"page_sizes 按 page_index 索引"契约静默错位,document 路径不继承该宽松)
bbox
document 路径只校验"长度 4 的有限数值";不做 有序性/整数化退化校验(该校验专为 CHS shim 的 int() 裁剪,文档消费者不裁剪;200-400 块的文档不应因一个退化框整份失败)。差异写入 docstring
文字提取
块内 lines[].spans[]: 取 content;table 取 html;image 的 image_path 无文字 → "";inline_equation 类型 span 包 $…$;行内 span 按 "" 拼、行间按 "\n" 拼。text/title span 缺 content → ResultInvalidError(不静默补空)。样本全是 1 行 1 span ,多行/多 span/inline_equation 路径无真实样本,仅由构造 fixture 测试,文档标"未经实测"
其它页内容
discarded_blocks 忽略(样本中为页眉页脚,span 内容全空);need_drop=true 的页不失败,页码记入 raw["need_drop_pages"]
raw
{"success": True, "download_url": …, "need_drop_pages": [...]}
6. 错误分类与批处理后果
失败
分类
治理结果
上传前: 非 bytes/空
TypeError/ValueError
统计边界外(同 _call 既有)
魔数预检不过
RequestRejectedError(无 status)
不重试、不换源;仅归还探针,不计熔断
500 + Parsing failed: + 已知标记
RequestRejectedError(status_code=500)
不重试;熔断记成功 且不计窗口(_gate_on_terminal: exc.status_code is not None);不喂选源器
其余 5xx/429、超时、网络错误
TransientError
重试 + 计熔断(现状)
401/403
SourceDeadError
现状
其余 4xx、success!=true(附 200)
RequestRejectedError
现状
ZIP 坏 / middle 或 md 缺失 / 超限 / 结构异常 / page_idx 错位
ResultInvalidError
不重试;熔断记成功不计窗口
已知标记(模块常量,小写子串匹配,取 E5 实测): failed to open stream、cannot open empty stream、cannot save with zero pages、is no pdf。不含 unsupported file type: 嗅探后它只可能由服务契约变化或我方标签 bug 引起,应响亮失败并计熔断。MonkeyOcrTransport 构造参数可覆盖该元组,不新增 SourceConfig 字段或环境变量键。
为什么 Q4 不能不做 (读代码确认的机制链): ① 熔断按 source_name 计数(内存 memory/breaker.py:56-61;Redis 键 gate:{scope}:{source});② 有效阈值 = max(配置值, 源并发×2)(config.py:503-507);③ 连续失败通道在"窗口证据不充分"(样本 <min_calls=10 或失败率 <0.6,types.py:1137-1155)时即开路——每份文档约 15 秒、并发 2,窗口凑不够样本,抑制不生效;④ 一份坏 PDF 重试 max_attempts 次即可在低并发配置下开路,冷却内同 scope 的好文档被 fail_fast 拒绝。影响范围是同 scope : 默认 memory 后端是进程内,只有 Redis 熔断后端加相同 scope 才跨进程(是否波及医院线取决于部署)。⑤ 下游语义: migrations/chsanalyzer.md:72 记录 CHSAnalyzer 把库侧"服务不可用"一族按 arq 延期重投且不耗业务失败预算——若 CHSAnalyzer3 沿用,永久损坏的 PDF 会被无限延期。此点未向下游证实,需人类确认。
白名单的脆弱性(诚实声明): 标记是 PyMuPDF 的界面文案,随服务版本漂移; 服务把所有异常都包成 Parsing failed:,若把磁盘满等真故障也包成同样文案会被误判为坏输入。误判代价是"该文档失败而非延期"(下游重跑失败集合即可),反向代价是熔断误开 ——取前者。
7. 长文档与资源(issue 要求 4)
结论
依据
默认值与熔断不会把长文档判为慢
无延迟型判定(§1 E9);选源器只数成败(sources.py:61-64);StallClock 只计非生产性等待(ocr.py:343-344)
唯一的"慢变失败"路径是 httpx 超时
monkey_ocr.py:63-68 → TransientError(__cause__ 为 TimeoutException)→ 重试 + 计熔断
超时是分阶段 的
httpx.Timeout(source.timeout_s) 四个子超时都取该值: POST 读超时≈服务处理时长上限,GET 下载另有一份 → 一次尝试最坏约 2× timeout_s
租约守卫只比 1×
config.py:338-345 要求 timeout_s ≤ lease_ttl_s(默认 1500)。OCR 示例 timeout_s=300 → 2× =600 安全;timeout_s>750 时需手动抬 PGW_LEASE_TTL_S。守卫改 2× 属全 scope 配置语义变更,不在本次 ,写进文档
配置口径(写入 .env.example 与 README)
文档线独立 scope + MAX_CONCURRENCY=2(满足"并发不超过 2")+ RETRY__MAX_ATTEMPTS=2 + timeout_s ≥ 最大页数×1.5s×余量(共用 GPU 争用余量取 5 倍以上)+ call_deadline_s 给单份封顶。有效熔断阈值 max(配置, 并发×2)=4 > 单份坏文档的重试次数
重试放大
POST 成功(服务已算完)而 GET 失败时,重试重新 POST 整份文档重算 ; 超时重试同理,且服务端是否因断连取消计算无证据——假定不取消
无页数预检
库不依赖 PDF 库,发送前不知页数; 300 页文档会占满超时被重投。页数过滤是下游职责
内存
client.get 整包入内存(含 images/);只解必要成员; 声明大小上限见 §5
服务端磁盘
每次调用在服务 /app/tmp 与 /static 留产物,几百篇约几百 MB 到 GB 级——告知服务运维 (库不管理)
并发上限的进程边界
MAX_CONCURRENCY 默认进程内; 多 worker 共享上限需 PGW_LIMITER_BACKEND=redis 且同 scope
8. 旧版行为审计
行为
裁决
_files() 固定 image.jpg
替换 : 以 %PDF- 开头 → doc.pdf; 其余保留 (字节级不变)
parse_layout 收到 PDF
行为不变(结果逐字相同,E3) ; 仅上传标签与包内前缀变化
recognize_text 收到 PDF
修复 : 静默错误结果(E4)变为整份全文; 文档注明"不承诺 PDF 语义,请用 parse_document"
parse_layout 的 bbox 整数化退化校验 / page_idx 回退 / 无 ZIP 体积上限
保留不动 (医院线零变更); document 路径各自更严/更宽,差异写入 docstring
_classify_status 共享分类
保留 (默认参数不变)
OcrTransport 协议
保留 ,不加成员
check_health、重试/选源/限流/熔断/遥测编排
保留 : parse_document 复用同一 _call/_run/_attempt 循环,只扩 _OcrKind 与 _invoke 分派
9. 非功能维度
维度
回答
并发与取消
无新共享可变状态; CancelledError 路径与 parse_layout 逐字一致(退避 sleep/限流等待/两段 HTTP 均可取消,permit 在 finally settle+release,探针取消归还)。call_deadline_s 到期会取消在飞请求,服务端是否继续计算未知
降级方向
限流/熔断后端故障 → 报错不放行(铁律,复用既有); 遥测写失败 → warning 降级
幂等与重复
只读幂等,重放安全; 代价见 §7"重试放大"
持久化与原子性
库内无持久化; ZIP 解包在内存(BytesIO),不落盘
零业务假设
无医学/论文词汇进入库代码与 fixtures; type 仍为开放字符串
10. 测试策略
fixtures 一律合成 (tests/unit/test_monkey_ocr.py 的 _zip_bytes/_page 风格上扩一个 document builder),不提交任何真实 ZIP (第三方内容许可不明; 医疗数据不入库,同 M3 设计 §11)。
层
内容(每条先红后绿)
unit/transport
嗅探: PDF 头 → doc.pdf/application/pdf,非 PDF 逐字 image.jpg/image/jpeg,三个方法各钉一次 (test_multipart_shape 现有断言保持成立); 文档解析: title/text(含 $X$)/interline_equation/带 nested table_body 的 table/带 image_body 的 image 的 text 取值; 垃圾 content_list.json 与 images/x.jpg 并存时输出与其无关; nested caption/footnote 成独立元素(标未经实测); inline_equation 包 $; 多行/多 span 拼接; markdown 精确配对与缺失; 体积上限; page_idx 错位; 退化 bbox 不失败; need_drop 记录; text span 缺 content → ResultInvalid
unit/分类
魔数预检(HTML 页面、空串、未知魔数); 四个已知标记 → RequestRejectedError(status_code=500); 未知 500 仍 Transient; unsupported file type 仍 Transient; 默认 _classify_status 行为不变(现有 test_http_status 保持)
unit/client
桩 transport 走完整治理循环; isinstance 缺 OcrDocumentTransport → TypeError; 无延迟判慢 : 在成功尝试内推进远大于 timeout_s 的时钟,断言成功、熔断 CLOSED、fails==0; 对照: 超时 Transient → 计失败 → 重试; ResultInvalid/本地预检不计熔断窗口; 取消穿透且探针归还; 遥测行 operation="parse_document"、占位字面量、摘要不含正文; _summarize 三类结果各一条(防 hasattr 回退); 终态行
unit/契约
get_args(CallOperation) == CALL_OPERATIONS; test_ports.py 新增 Protocol 的 runtime_checkable 用例,并断言 OcrTransport 成员集合未变
integration(slow,opt-in,串行)
手写最小合法 PDF(无第三方内容)打真实服务: parse_document 返回 ≥1 页、元素均有 text 类型; 同一 PDF 的 recognize_text 不再返回 E4 的幻觉文本; 损坏 PDF → RequestRejectedError 且熔断仍 CLOSED。成败取决于外部服务当下状态,按 CLAUDE.md §4.6 标 slow,不进日常套件
11. 落地时需同步的位置(grep 全引用面所得)
类别
位置
代码
types.py(Literal/常量/新类型)、ports.py、ocr.py(_OcrKind/_AttemptOutcome/_invoke/_emit/_summarize 签名)、transports/monkey_ocr.py、__init__.py(导出 + __version__)、pyproject.toml
校验工具
tools/check_wiki_alignment.py:37-46(_SIGNATURE_TARGETS 加 OcrClient.parse_document)
wiki 注册
research-wiki/schemas/llm-calls.md(operation 取值)、metrics/ocr-call-success.md、metrics/call-telemetry-coverage.md(无推理入口列表)
文档
README.md(方法数、能力表、安装版本约束)、CHANGELOG.md、.env.example(文档线 scope 示例)、ARCHITECTURE.md §5.1/§7.10/D9 注记、M3 设计 §4 勘误(指向本设计 E3/E4)、migrations/chsanalyzer.md(CHSAnalyzer3 新消费路径)
测试
test_telemetry.py 的 operation 参数化、test_ports.py、test_monkey_ocr.py、test_ocr_client.py、tests/integration/test_monkey_ocr_live.py
无需迁移
llm_calls.operation 列为 TEXT 无 CHECK(telemetry/schema.py),新值不需 DDL; 外部按 operation 分组的看板会自然多一组
CLAUDE.md §4.4.1 发布流程按其清单执行(README 在构建前改完; 1.3.11 与历史把新增公共方法放 patch 的惯例一致)。
12. 评审与待人类拍板
已做 : Claude 自审; 一轮独立 subagent(全新上下文、只读)对 Q1-Q5 与遥测/引用面逐项论证。采纳其修正 : 弃初稿"content_list+middle 对位"(Q3,以 \$ 污染 23/134 为据)、砍 level/captions/footnotes(Q1)、OcrDocumentTransport 另起 Protocol 而非扩 OcrTransport(§4.2)、document 路径的 page_idx 严格与 bbox 宽松(§5)、确定性 500 只做新路径窄化(Q4)、ZIP 体积上限、_summarize 改法。更正我方事实 : 初稿称"结果包无 images/",实为含图文档才有(E6)。Codex 独立审 : 见文末"审查记录"。
需要人类决定 :
#
决策
推荐
H1
§4 公共面(新方法 + 新 Protocol + 继承式元素、text 唯一新增字段、嵌套子块独立成元素)
批准
H2
确定性坏输入分类: 仅新路径窄化(本设计),共享分类另开 issue 供医院线评估
同意,并授权我建该 issue
H3
CHSAnalyzer3 是否把"服务不可用"一族按延期重投处理(§6 ⑤)——决定 RequestRejectedError 路径是否必要地"关键"
请确认; 不影响实现,影响下游使用指引
H4
实现完成后的发布(合并 main、tag、构建、上传 registry、建 Release、关闭 #34)逐步等你明确指令 ,不随实现自动执行
同意
审查记录
(待补: Codex 审查结论与逐条核验。)