# overflow-fallback（技术计划）

## 1. 计划目标

实现统一的“溢出与兜底”策略模块，用于在以下情况给出确定性且可解释的返回：

- 搜索器无法在 `<=maxLines` 覆盖到结束边界 `N`
- `lineMode=FIXED` 需要刚好 `maxLines` 但无解
- 测量不可用或失败导致无法执行宽度派约束（尤其 Widget）

支持三种模式：

- `ELLIPSIS`：最后一行加省略号并保证不超宽（必要时回退移除 token 再加）
- `CLIP`：截断到 `maxLines`（不加省略号）
- `SYSTEM_DEFAULT`：算法层不插入换行，交给系统排版（仅在 meta 标记）

## 2. 对外输入/输出（与 spec 对齐）

### 2.1 输入

- `tokens: Token[]`
- `partialLayout?: { breaks: number[]; lines: Array<{ start; end; text? }> }`
  - 若提供：表示搜索器的 best-effort（可能未覆盖到 N）
- `overflowMode: 'ELLIPSIS' | 'CLIP' | 'SYSTEM_DEFAULT'`
- `availableWidth: number`
- `maxLines: number`
- `lang: 'TC' | 'EN'`
- `context: 'APP' | 'WIDGET'`
- `ellipsisToken: string`（推荐 `"…"`，配置化）
- `measure?: { contextProfile; fontSpec; measureWidthImpl; widgetEnableMeasure? }`（用于“加省略号后再测量”）
- `reason: 'NO_CANDIDATE' | 'WIDTH_UNKNOWN' | 'TOO_LONG' | 'WIDOW' | 'PARTICLE' | string`

### 2.2 输出

统一返回：

- `result: { lines: string[]; wrappedText: string; meta: { fallback_type; overflow_type; reason } }`

其中：

- `fallback_type`: `NONE | RELAX_RULES | SYSTEM_DEFAULT`
- `overflow_type`: `NONE | ELLIPSIS | CLIP`

## 3. 核心行为细则（对应文档 12.x）

### 3.1 overflow 判定

在本模块内不重新跑搜索，仅基于输入判断：

- 若 `partialLayout` 覆盖到 `N`（即最后一行 `end==N`），则 overflow_type=NONE
- 否则为 overflow，按 overflowMode 执行

### 3.2 SYSTEM_DEFAULT（12.4A）

- 返回 `lines=[原文单行]`，`wrappedText=原文`
- `meta.fallback_type='SYSTEM_DEFAULT'`
- `meta.overflow_type='NONE'`（因为不再输出算法换行；由 UI 决定是否完全交给系统）
- `meta.reason=输入 reason`

### 3.3 CLIP

- 若 `partialLayout` 有 lines：取前 `maxLines` 行，重组 `wrappedText=lines.join('\n')`
- 若无：返回单行原文
- `meta.overflow_type='CLIP'`，`fallback_type='NONE'`，`reason=输入 reason`

### 3.4 ELLIPSIS（12.3/12.5）

仅处理最后一行：

1. 选取 baseLines：
   - 优先使用 `partialLayout.lines`（若为空则把全文当单行）
   - 截断到 `maxLines`（只在最后一行做 ellipsis）
2. 清理规则（固定，确定性）：
   - 不输出 `"  …"`：最后一行末尾空白先 trim
   - 不输出 `"，…"/"。…"`：若最后一行末尾是常见 TC 标点（`，。！？；：、`），则移除该标点再加 ellipsis
3. 宽度检查（12.3）：
   - 若提供测量能力且启用：
     - 重新测量 `lastLine+ellipsisToken`
     - 若超宽：按语言回退单位移除 token 再加省略号并重测
       - EN：按整词（token）
       - TC：按 grapheme（token）
   - 若测量不可用：不做重测，直接输出（meta.reason 仍保留）

> 说明：首版不做“避免截断 emotionPhrase 的整段前移/整段省略”，该策略可在后续结合 scoring 决策升级，但必须保持确定性。

## 4. 代码落位（客户端）

- `client/src/features/textWrap/overflow/`
  - `types.ts`
  - `ellipsis.ts`
  - `fallback.ts`（入口：applyOverflowFallback）
  - `index.ts`
  - `__tests__/overflowFallback.test.ts`

## 5. 测试计划（Vitest）

- SYSTEM_DEFAULT：lines 单行，meta.fallback_type=SYSTEM_DEFAULT
- CLIP：超过 maxLines 时截断行数
- ELLIPSIS：
  - 末尾空白去除，不输出 `"  …"`
  - 末尾 TC 标点去除，不输出 `"，…"`
  - 测量超宽时按 token 回退直到不超宽（用 mock measureWidthImpl）

## 6. 完成定义（DoD）

- 三种 overflowMode 行为稳定且可解释
- ELLIPSIS 的清理与回退测量实现完毕（可测）
- 输出 meta 字段满足文档 12/14 的治理需求

