From 56ce4dbe212047ca32be8277eaa9307a396a0e4e Mon Sep 17 00:00:00 2001 From: Estrella Pan Date: Sat, 11 Jul 2026 17:59:08 +0200 Subject: [PATCH] docs(parser): record resolved tokenizer architecture --- ...-11-tokenizer-candidate-resolver-design.md | 88 +++++++++++++------ 1 file changed, 62 insertions(+), 26 deletions(-) diff --git a/docs/plans/2026-07-11-tokenizer-candidate-resolver-design.md b/docs/plans/2026-07-11-tokenizer-candidate-resolver-design.md index a9555397..add6cd5d 100644 --- a/docs/plans/2026-07-11-tokenizer-candidate-resolver-design.md +++ b/docs/plans/2026-07-11-tokenizer-candidate-resolver-design.md @@ -1,7 +1,7 @@ # Tokenizer Candidate/Resolver 与 ParsedRelease 迁移设计 日期:2026-07-11 -状态:已确认,按 A(解析架构)→ B(业务迁移)实施 +状态:已实施,A(解析架构)与 B(业务迁移)均已完成 ## 背景 @@ -18,7 +18,8 @@ PV、NCOP/NCED 及无集数标题,并通过现有回归和跨来源资源名 1. 规则只负责产生不可变候选,不直接修改最终状态或消费文本。 2. Resolver 统一处理优先级、重叠、冲突、接受和拒绝原因。 -3. 标题只从最终获胜候选的 span 之外重建。 +3. 标题只从 Resolver 明确标记为 `Decision.excluded_spans` 的范围之外重建; + SHADOWED metadata 可排除自身 marker,但不会排除依赖其获胜才成立的标题范围。 4. 默认 API 保持轻量,同时提供可选 `ParseTrace` 用于诊断。 5. `TitleParser`、RSS 偏好去重和 OffsetScanner 直接消费 `ParsedRelease`。 6. 保留 `raw_parser() -> Episode | None` 兼容接口,避免一次性迁移全部调用方。 @@ -29,7 +30,8 @@ PV、NCOP/NCED 及无集数标题,并通过现有回归和跨来源资源名 - 本轮不新增数据库表来持久化 PV、OP/ED 或 Batch。 - 本轮不让下载器自动下载所有可解析资源;由明确策略判断是否可消费。 - 不引入外部 anime parser 或 parser-combinator 依赖。 -- 不以性能优化为目标;当前解析速度约为 110 微秒/标题。 +- 不以性能优化为目标;71 条 golden corpus 本机基准约为 150 微秒/标题,开启 + 完整 trace 约为 164 微秒/标题(约 9% 额外开销)。 ## A:Candidate → Resolver → Trace @@ -44,51 +46,81 @@ normalize → ParsedRelease + optional ParseTrace ``` +当前数据流: + +```mermaid +flowchart LR + Raw[资源名称] --> Normalize[normalize + segment scan] + Normalize --> Rules[group / structural / media / technical collectors] + Rules --> Observations[Observation + atomic Candidate] + Observations --> Resolver[stable Resolver] + Resolver --> Resolution[Claims + Decisions + excluded spans] + Resolution --> Titles[residual title reconstruction] + Titles --> Parsed[ParsedRelease] + Resolution -. optional diagnostics .-> Trace[ParseTrace] + + Parsed --> Policy[release_policy] + Policy --> TitleParser[TitleParser: Bangumi / Movie / reject] + Policy --> RSS[RSS typed preference dedup] + Policy --> Offset[Offset: positive integer weekly episode only] + Parsed --> Legacy[legacy adapter: Episode | None] +``` + 核心类型: ```python @dataclass(frozen=True, slots=True) class Span: + segment: int start: int end: int @dataclass(frozen=True, slots=True) class Candidate: + id: str rule_id: str - field: ParseField - value: object - span: Span + spans: tuple[Span, ...] + claims: Claims priority: int - consume: bool = True - segment_index: int | None = None + specificity: int = 0 + conflict_tags: frozenset[str] = frozenset() + blocks: frozenset[str] = frozenset() @dataclass(frozen=True, slots=True) class Decision: - candidate: Candidate - accepted: bool + candidate_id: str + status: DecisionStatus reason: str @dataclass(frozen=True, slots=True) class ParseTrace: normalized: str + observations: tuple[Observation, ...] candidates: tuple[Candidate, ...] + claims: Claims decisions: tuple[Decision, ...] - residual: str - warnings: tuple[str, ...] + excluded_spans: tuple[Span, ...] + residuals: tuple[str, ...] + warnings: tuple[ResolutionWarning, ...] ``` -Resolver 使用稳定排序:字段策略 → priority → specificity → span 长度 → 原文位置。 -单值字段只接受一个主候选;codec/audio 等多值字段接受所有不冲突候选; -重叠候选必须记录拒绝原因。相同优先级但不同值时产生 warning,不静默覆盖。 +Resolver 使用稳定排序:priority → specificity → segment/span → rule id → candidate id。 +单值字段只接受一个主候选;codec/audio 等多值字段接受所有不冲突候选;原子 +Claims 保证 `S02E05`、`OVA01`、range 不会被拆成互相矛盾的半份结果。重叠候选 +记录 `SHADOWED / REJECTED_AS_TITLE / REJECTED_CONFLICT` 原因;相同优先级但 +不同值时产生 warning,不静默覆盖。 -模块边界: +实际模块边界: -- `scanner.py`:Segment、Span 和括号扫描。 -- `candidates.py`:Candidate、Decision、ParseTrace、ParseField。 -- `rules/`:structural、media、technical、group 规则。 -- `resolver.py`:统一冲突与 span 消费策略。 -- `titles.py`:残余文本和标题片段重建。 -- `parser.py`:只负责编排并构造 `ParsedRelease`。 +- `candidate.py`:Span、Observation、原子 Claims、Candidate 与 Decision。 +- `resolver.py`:稳定排序、字段冲突、显式 block、重叠 span、warning 与最终 Claims。 +- `trace.py`:ParseTrace、TraceSegment 与可选的 ParseOutcome。 +- `parser.py`:括号扫描、group/structural/media/technical 候选收集、标题重建与编排。 +- `result.py`:稳定的 ParsedRelease、MediaType 与 ReleaseKind 公共领域模型。 +- `release_policy.py`:持久化、偏好去重和 Offset 的业务准入策略。 + +规则收集目前仍集中在 `parser.py`,这是刻意保留的后续模块化优化点;冲突解析和 +业务策略已经从该文件中分离,不再依赖规则执行顺序修改 `_State`。 ## B:业务链路迁移 @@ -111,21 +143,25 @@ Resolver 使用稳定排序:字段策略 → priority → specificity → span 迁移调用点: - `TitleParser.raw_parser()`:确定性路径直接调用 `parse_release_title()`。 -- RSS preference dedup:键改为 `(bangumi_id, media_type, episode)`,缺少可用序号时跳过。 +- RSS preference dedup:键改为 + `(bangumi_id, media_type, normalized_season, episode)`,并以 + `(preference_score, revision)` 排序;缺少安全单集身份时跳过去重。 - OffsetScanner:仅消费 `MediaType.EPISODE`、`ReleaseKind.SINGLE` 和正整数集数。 - `raw_parser()`:继续作为未迁移调用方的兼容适配器。 ## 测试策略 -1. `release_title_golden.json` 持久化跨 Mikan、DMHY、Nyaa、Emby、Plex 的资源名。 +1. `release_title_golden.json` 持久化 71 个跨 Mikan、DMHY、Nyaa、Emby、Plex 的资源名。 2. Golden 测试对核心字段做精确快照,不再只断言标题包含某个片段。 3. 冲突测试覆盖 group/metadata、Movie 保留字、年份标题、多集数候选和重叠 span。 4. Metamorphic 测试验证: - 技术标签增删或重排不改变标题、季、集; - 等价括号布局不改变语义; - 添加 group 不改变媒体类型; - - 所有 accepted span 均合法且互不冲突。 -5. 无新依赖的确定性 fuzz:固定随机种子生成 Unicode、括号和未知标签,保证不抛异常。 + - 所有 excluded span 均合法且可归因;exclusive winners 不互相重叠, + `OverlapPolicy.SHARED` overlay 是明确例外。 +5. 无新依赖的确定性性质矩阵:448 个固定随机种子/变形场景覆盖 Unicode、括号、 + 技术标签和未知标签,保证确定性与不抛异常。 6. 下游测试验证 range/PV 不进入单集去重,OVA 不污染 Offset,Movie 的 LLM/确定性投影一致。 ## 迁移与回滚