# breakpoint-candidates(技术计划) ## 1. 计划目标 基于 `spec.md` 与 `设计说明文档/文档换行算法.md v1.2.1`,实现“候选断点生成与裁剪”模块,输出**可控规模、确定性排序**的 breakpoints 集合,保证: - EN/TC 断点生成口径一致(断点 `pos` 均是 token 边界索引) - 同输入必定同输出(去重、排序、裁剪与过滤全流程确定性) - 候选规模上限严格生效(尤其 TC) - 支持约束过滤:`forbiddenBreakRanges`(必须)与 `protectedPhrases`(可选优化) 本模块只产出 breakpoints,不做组合搜索与评分。 ## 2. 默认技术决策(本计划采用) - **输出结构**:`Array<{ pos, kind, priority }>`,最终按 `pos` 升序 - **去重策略**:同一 `pos` 若出现多个来源候选,保留 `priority` 更高者(priority 相同按 `kind` 固定序优先) - **裁剪策略(TC)**:先按“候选重要性排序”截断到 `tcMaxCandidateBreaks`,再按 `pos` 升序输出 - **过滤策略**: - 必做:`forbiddenBreakRanges` 命中直接剔除 - 可选优化:若已计算 `protectedPhrases` 的 span,可在生成阶段剔除 span 内断点(否则交给评分阶段强惩罚淘汰) ## 3. 输入/输出与关键口径 ### 3.1 输入(来自上游) - `tokens: Token[]` - EN:WORD tokens(不包含 SPACE token) - TC:grapheme cluster tokens(允许包含空格 cluster `" "`,用于 SPACE 断点) - `lang: 'TC' | 'EN'` - `maxLines: number` - `constraints?: { protectedPhrases?: string[]; forbiddenBreakRanges?: Array<{ start: number; end: number }> }` - `config: { tcMaxCandidateBreaks: number; tcPunctuations: string[]; balanceRange: number }` ### 3.2 输出(确定性) - `breakpoints: Array<{ pos: number; kind: 'PUNCT' | 'SPACE' | 'BALANCE' | 'OTHER'; priority: number }>` - `pos`:token 边界索引,范围 `0..N` - **最终输出必须按 `pos` 升序** - `meta?: { pruned: boolean; originalCount: number; finalCount: number }` ### 3.3 断点边界定义(统一口径) - 候选断点只生成在“行内断点”位置:`pos ∈ [1, N-1]` - `pos=0` 与 `pos=N` 由搜索器作为“起止边界”处理(不作为候选断点输出) ## 4. 生成规则(按语言) ### 4.1 EN:词边界(kind=SPACE) #### 规则 - tokens 仅为 WORD,不生成 SPACE token - 对每个词边界产生候选断点: - 对 `i in 1..N-1` 生成 `pos=i, kind='SPACE'` - `priority` 固定为基础值(建议 `priority=10`) #### 验收要点 - `"I am so tired"`(N=4)→ breakpoints.pos 必为 `[1,2,3]`(升序) ### 4.2 TC:标点/空格/BALANCE #### 4.2.1 标点后断点(kind=PUNCT,最高优先级) - 若 `tokens[i].text` 属于 `tcPunctuations`: - 生成 `pos=i+1, kind='PUNCT'` - `priority` 建议最高(例如 `priority=30`) #### 4.2.2 空格后断点(kind=SPACE,中优先级) - 若 `tokens[i].text === ' '`: - 生成 `pos=i+1, kind='SPACE'` - `priority` 建议中等(例如 `priority=20`) > 注:若上游对 TC 也做了空白 NORMALIZE,则空格通常不会连写,断点仍保持确定性。 #### 4.2.3 BALANCE 断点(kind=BALANCE,低优先级) 目的:在无标点时,仍在“接近理想位置”附近提供少量断点,提升可解性与观感。 **理想位置计算(确定性简化版)**: - `N = tokens.length` - `targetLines = min(maxLines, N)`(至少 1,且不超过 N) - 对 `lineIndex in 1..targetLines-1`: - `idealPos = round((N * lineIndex) / targetLines)` - 在区间 `[idealPos - balanceRange, idealPos + balanceRange]` 生成少量候选 `pos` **生成细则**: - 候选 `pos` 必须落在 `[1, N-1]` - 去重前可以允许重复(后续统一去重) - `priority` 建议最低(例如 `priority=5`) > 裁决补充口径:BALANCE 断点允许落在 emotionPhrase/protectedPhrases 的 span 内;是否可用由评分阶段强惩罚决定(本模块不做语义裁决)。 ## 5. 过滤、去重、裁剪与排序(必须确定性) ### 5.1 forbiddenBreakRanges 过滤(必须) - 若 `pos` 落在任一 `forbiddenBreakRanges` 的区间内(按项目约定:`start <= pos <= end` 或半开区间,必须写死一种),则剔除该 breakpoint - 过滤必须发生在最终输出前,保证确定性 ### 5.2 去重(必须) 同一 `pos` 可能来自多个来源(例如 PUNCT 与 BALANCE): - 取 `priority` 更高者 - `priority` 相同则按固定 kind 序:`PUNCT > SPACE > BALANCE > OTHER` ### 5.3 TC 规模上限裁剪(必须) 当 `lang=TC` 且候选数超过 `tcMaxCandidateBreaks`: 1. 计算每个 pos 到最近 `idealPos` 的距离 `distToIdeal`(若无 idealPos 列表则设为大值) 2. 按以下 key 排序后截断(排序必须固定): - `priority` 降序 - `distToIdeal` 升序 - `pos` 升序 3. 取前 `tcMaxCandidateBreaks` 最后再按 `pos` 升序输出(输出顺序固定)。 ### 5.4 meta 输出 - `originalCount`:过滤/去重/裁剪前的候选数量 - `finalCount`:最终输出数量 - `pruned`:是否发生过裁剪(finalCount < originalCount) ## 6. 测试计划(Vitest) ### 6.1 EN 断点生成 - 输入 tokens=[I, am, so, tired] → pos=[1,2,3](确定性) ### 6.2 TC 标点/空格断点 - `tokens=['我','好','累',',','😮‍💨']` 且 `tcPunctuations` 包含 `,` - 必须包含 `pos=4(kind=PUNCT)` ### 6.3 BALANCE 与裁剪 - 构造无标点长文本,balanceRange>0 且 `tcMaxCandidateBreaks` 很小 - 验证裁剪后数量上限生效 - 验证输出仍按 pos 升序 ### 6.4 forbiddenBreakRanges - 给定 ranges,断言命中区间内的 pos 一律被剔除 ### 6.5 确定性(关键) - 同输入多次调用 breakpoints 输出完全一致(包括 meta) ## 7. 完成定义(DoD) - EN/TC 候选断点生成口径与裁剪规则写死并实现 - 去重/排序/裁剪/过滤流程完全确定性 - TC 上限 `tcMaxCandidateBreaks` 生效 - 单测覆盖:EN/TC 基础、BALANCE、裁剪、forbiddenBreakRanges、确定性