# 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、确定性

