# 情绪文案换行算法规范与实现映射说明书 v1.2（裁决补充版，可直接实现）— **实现口径补充 v1.2.1**

> 本文档用于指导 **情绪文案自动换行算法** 的完整实现（可直接据此写代码）。
> 目标不是做语义理解，而是通过 **规则 + 参数 + 确定性搜索与决策**，
> 在多语言、多行数、多尺寸（App/Widget）场景下，输出稳定、可控、可解释的换行结果。
>
> **本版本为「裁决补充版 + 实现口径补充」**：已将实现时最容易产生歧义的口径点写入原文合适位置，确保跨端一致。

---

## 0. 快速摘要（给实现者）

你需要实现一个纯函数模块：

* **输入**：text、lang、availableWidth、maxLines、fontSpec（App 强烈建议）、context（APP/WIDGET）、可选 constraints、可选 overflowMode
* **输出**：lines[]（<=maxLines）、wrappedText、可选 meta

核心流程（必须确定性）：

1. Tokenize（按语言）
2. 生成候选断点 breakpoints（控规模）
3. 搜索断点组合（DP+TopK 或 Beam），生成候选 layout
4. 对 layout 做硬约束过滤 + 打分
5. tie-break 固定顺序选最优
6. 溢出/兜底处理 + 打点

---

## 1. 设计目标（Why）

### 1.1 核心目标

* 控制阅读节奏：让用户“自然停顿”
* 放大情绪关键词：情绪词/短语尽量靠近行尾或独立成行
* 多端稳定：App 精确测量、Widget 常量估算也能稳定
* **同输入 → 同输出（确定性）**
* 可缓存：同 key 重复调用不重复计算
* 可治理：兜底/溢出可打点

### 1.2 非目标

* 不做情绪识别 / 语义理解
* 不做通用排版引擎
* 不依赖机器学习

---

## 2. 数据结构与接口（What）

### 2.1 公开接口（建议）

```ts
wrapText({
  text: string,
  lang: 'TC' | 'EN',
  availableWidth: number,
  maxLines: number,
  context: 'APP' | 'WIDGET',
  fontSpec?: { fontSize: number; fontWeight?: string; fontFamily?: string },
  overflowMode?: 'ELLIPSIS' | 'CLIP' | 'SYSTEM_DEFAULT',
  lineMode?: 'AUTO' | 'FIXED',
  constraints?: {
    protectedPhrases?: string[];           // 尽量不跨行
    forbiddenBreakRanges?: Array<{start: number; end: number}>; // token index 区间内禁止断点
  },
  configVersion?: string,
  debug?: boolean
}) => {
  lines: string[];
  wrappedText: string;
  meta?: DebugMeta;
}
```

### 2.2 内部结构（建议）

**Token**

* EN：word token（保留原始字符串与长度）
* TC：grapheme cluster token（字符簇，避免拆 emoji/ZWJ）

```ts
Token {
  text: string
  type: 'WORD' | 'SPACE' | 'PUNCT' | 'GRAPHEME'
  isPunct?: boolean
  isEmojiCluster?: boolean
}
```

**Breakpoint**（断点位置是 token 边界）

```ts
Breakpoint {
  pos: number;                // 断点在 token 边界：切分为 [0..pos) + [pos..]
  kind: 'PUNCT' | 'SPACE' | 'BALANCE' | 'SHIFT' | 'ACCUM' | 'SELF' | 'OTHER';
  priority: number;           // 候选断点生成阶段使用（先截断）
}
```

**LineSegment**（一行）

```ts
LineSegment {
  start: number;
  end: number;
  text: string;
  width: number;
  tokenCount: number;
  charCount: number;
  endsWithEmotion?: boolean;
}
```

**Layout（候选方案）**

```ts
Layout {
  breaks: number[];        // 断点序列，例如 [pos1, pos2, ...]
  lines: LineSegment[];
  score: number;
  tieKey?: (number | string)[];
  flags: {
    emotionSplit?: boolean;
    overflowed?: boolean;
    fallback?: boolean;
  }
  scoreBreakdown?: ScoreBreakdown;
}
```

---

## 2.3（新增）索引体系与文本重组（必须定义，确保跨端一致）

> 最常见的实现偏差来自：token 是否包含空格、pos 到底切在哪里、以及如何拼回 text。以下规则必须统一。

### 2.3.1 EN 索引约定

* EN token 序列 **只包含 WORD**（标点默认贴附在词内），不生成 SPACE token。
* 断点 `pos` 表示：在第 `pos` 个词 **之前** 断开。

  * 行区间 `[start..end)` 表示 `tokens[start] ... tokens[end-1]`
* 文本重组：默认用单空格 `" "` join（或使用原始空白映射，见 2.3.3）。

**例**：`"I am so tired"` → tokens = `[I, am, so, tired]`

* breaks=[2] → lines: `"I am"` / `"so tired"`

### 2.3.1A（裁决补充）EN 标点处理：极简派（必须）

为保证跨端实现一致，EN 标点采用**极简派**约束：

* 所有标点均视为 **词内字符**（token.text 的一部分），不额外拆分为独立 token。
  例如：

  * `"tired."` 是一个 token
  * `"Wait..."` 是一个 token
  * `"hello—world"` 作为一个 token
  * `"don't"` 作为一个 token
* 因此：EN 断点仅存在于**词与词之间**，不存在“标点后断点”概念。
* Shift / Accum / Self / EmotionWords 的匹配在 token.text 上执行（可大小写归一），允许被词内标点包裹（如 `"but,"` 仍可视为命中 `"but"`）。

#### 2.3.1A-1（实现口径补充）EN 关键词命中规则：**全词等值匹配，不做 substring**（必须）

为避免 `"rebuttal"` 被 substring 误命中 `"but"` 等问题，EN 命中判断固定为：

* 对 `token.text` 执行：`lowercase → strip 两端常见标点 → 全词等值比较`
* **不允许** substring / contains 式命中。
* “常见标点”建议配置为（可按需扩展，但全端一致）：
  `, . ! ? : ; " ' … — – ( ) [ ] { }`

> 说明：此规则只影响“词库命中判定”，不改变 tokenization（仍为极简派）。

### 2.3.2 TC 索引约定

* TC tokens 为 grapheme clusters。
* 断点 `pos` 表示：在第 `pos` 个 grapheme **之前** 断开。

**例**：`"我好累😮‍💨"`

* tokens = `[我, 好, 累, 😮‍💨]`（😮‍💨 为一个 cluster）
* breaks=[3] → `"我好累"` / `"😮‍💨"`

### 2.3.3（可选）原始空白保留策略

若产品需要保留输入中的多空格/换行（一般不建议），必须把空白映射作为单独结构：

* `rawSeparators[i]` 表示 `tokens[i]` 与 `tokens[i+1]` 之间的原始分隔符。
* 重组时按 `rawSeparators` 拼接。

否则（推荐默认）：对输入先做 `normalizeWhitespace`（折叠连续空白为 1 个空格，去首尾空白），并在 meta 里打点 `hadMultiWhitespace`。

---

## 3. 宽度测量（必须可实现）

### 3.1 App（建议精确测量）

实现者需提供一个可插拔的测量函数：

```ts
measureWidth(text: string, fontSpec): number
```

* RN iOS 推荐用原生测量（或 TextLayout 结果）
* 重要：测量时应使用与实际渲染一致的 fontSpec

### 3.2 Widget（建议估算/常量）

Widget 场景不强制精确测量，推荐：

* per widgetSize 固定 availableWidth（扣 padding）
* EN：按词数/字符数限制候选搜索范围
* TC：候选断点上限更小

> 注意：Widget 常量需在 iOS 大版本或版式变更时校准。

---

## 3.3（新增）测量缓存与宽度不可用降级（强烈建议）

### 3.3.1 缓存 Key 规范

为保证性能与确定性，建议至少两级缓存：

1. **字符串测量缓存**：`(text, fontSpecKey, contextProfile) -> width`
2. **段落切片缓存**：`(start, end) -> width`（对 DP 重复切片测量非常关键）

`fontSpecKey` 必须规范化：例如 `fontFamily|fontWeight|fontSize`。

### 3.3.2 宽度不可用（或测量失败）降级

若出现以下任一情况：

* `measureWidth` 不可用
* `measureWidth` 抛错/返回 NaN
* Widget 选择不测量

则进入 `approx mode`：

* 把 width 近似替换为：EN 用 `wordCount`，TC 用 `charCount`
* 强制使用 Widget Beam 搜索策略与更小候选规模
* 打点：`WIDTH_UNKNOWN`

---

## 4. 规则体系（Rule System）

规则按优先级分四层：

1. **硬约束（Hard Constraints）**：不满足直接淘汰
2. **情绪规则（Emotion Rules）**：强惩罚/强奖励
3. **节奏与语义（Rhythm/Semantic Heuristics）**：中等权重
4. **视觉均衡（Visual Balance）**：用于多行整体观感

---

## 5. 硬约束（Hard Constraints）

> 任何候选 layout / line 违反即淘汰

### H1 不超宽

* 对每一行：`line.width <= availableWidth`

### H2 不非法拆分

* EN：断点只能在词间
* TC：断点只能在 grapheme cluster 边界（不得拆 ZWJ emoji 组合）

### H3 不空行

* `trim(line.text) != ''`

### H4 行内容下限（可降级）

* TC：去空格 charCount >= `minCharsPerLineTC`（默认 2）
* EN：tokenCount >= 1（词）

### H5 最大行数

* layout.lines.length <= maxLines

---

## 5.1（新增）必须禁止的断点（推荐硬约束，提高稳定性）

### H6 禁止“行首标点”（TC 强烈建议）

* 若一个断点导致 **下一行第一个 token 为标点**（属于 `tcPunctuations`），则该断点不可用。

### H7 受保护短语内断点可直接剔除（可选优化）

* 对 `constraints.protectedPhrases`：可先计算其 token span（start/end），并在候选断点生成阶段直接过滤 span 内断点。
* 若不做剔除，也必须在评分中施加强惩罚（见 10.2A）。

---

## 6. Tokenize 与断点候选生成

### 6.1 英文 EN

**Tokenize**

* 以空格为分隔
* 标点贴附在词尾（实现可简化）
* 建议先 normalizeWhitespace（见 2.3.3）

**Breakpoints**

* 在每个“词边界”（词与词之间）产生 breakpoint
* kind=SPACE，priority=基础值

**Widow 辅助信息**

* 标记短词：`len(word) <= widowMaxLen`（默认 3）

### 6.2 中文 TC

**Tokenize**

* 按 grapheme cluster 切分（实现可用现成库或平台 API；无库时至少保证不拆 surrogate pair + 常见 ZWJ）

**Breakpoints 优先级（高→低）**

1. 标点后（kind=PUNCT，priority 高）
2. 空格后（kind=SPACE，priority 中）
3. 均衡补齐断点（kind=BALANCE，priority 低）

**均衡补齐（BALANCE）**

* 目标：即便无标点，也能在“接近理想位置”的附近有断点

* 计算理想切分点：

  * `targetLines = min(maxLines, estimateNaturalLines())`
  * `idealCharsPerLine ≈ totalChars / targetLines`

* 对每个理想切分点 i：在 `[idealPos - range, idealPos + range]` 生成少量断点

* range 建议：3～6 个 grapheme

**候选规模上限（必须）**

* TC 候选断点上限：`tcMaxCandidateBreaks`（建议 40～80，Widget 可更低）
* 截断策略：按 priority（标点>空格>补齐） + 距离理想位置近优先

### 6.2A（裁决补充）BALANCE 断点与情绪短语冲突处理

* BALANCE 断点**允许生成**在 emotionPhrase / protectedPhrases 的 span 内。
* 但这类断点若导致短语被拆分，会在评分阶段触发 **10.2A 的极大惩罚**，从而被自然淘汰。
* 目的：保持候选生成简单、可控，把“是否可用”统一交给评分与搜索层决定（仍保持确定性）。

---

## 6.3（新增）TC Grapheme Cluster 实现要求（跨端一致性）

为避免 iOS/Android/JS 行为不一致，必须明确实现级别：

### 推荐实现（优先）

* 使用平台级 grapheme segmentation（如 ICU / 系统分词器 / JS 端可用 `Intl.Segmenter` 时优先）。

### 最低可用实现（无库兜底）

至少保证以下组合不被拆开：

* surrogate pair（代理对）
* ZWJ sequence（如家庭 emoji）
* variation selector（VS16 等）
* skin tone modifier（肤色修饰符）
* regional indicator flags（国旗）

### 必须的回归样例（至少覆盖）

* `👨‍👩‍👧‍👦`、`🇸🇬`、`👍🏽`、`😮‍💨`、`é`（组合字符）

---

## 6.4（新增）候选断点的去重、排序与过滤（必须确定性）

* 去重：同一 `pos` 出现多个 breakpoint 时，保留 priority 更高者（或合并为最高 kind）。
* 排序：最终 breakpoints 必须按 `pos` 升序排列。
* 过滤：应用 `constraints.forbiddenBreakRanges` 与（可选）protected phrase span 过滤。

---

## 7. 情绪规范 → 系统规则映射（参数化实现）

> 这一节将“内容规范”落到系统可执行的参数与规则。

### 7.1 关键词库（配置项）

**情绪短语（强保护）**

* `emotionPhrasesTC[]`
* `emotionPhrasesEN[]`

**情绪转折词（Shift）**

* TC：但 / 可是 / 然而 / 却 / 只是 / 偏偏
* EN：but / yet / so（and 为轻量）

**情绪累积词（Accum）**

* TC：已经 / 一直 / 曾经 / 终于 / 还是 / 到现在
* EN：already / still / even / just / really

**自我指向词（Self）**

* TC：你 / 我 / 自己 / 我们 / 别人
* EN：you / yourself / me / we

**情绪词（EmotionWords，可选）**

* TC：累 / 痛 / 怕 / 孤单 / 委屈 / 撑 / 崩溃 / 放弃
* EN：tired / afraid / lonely / hurt / overwhelmed / give up

> 备注：词库不需要完美，v1.2 以“少而准”为原则，通过打点迭代。

### 7.2 系统行为映射（实现点）

1. **情绪短语保护**

   * 若任何情绪短语被断点拆到不同的行 → layout 加强惩罚（或直接淘汰）

2. **Shift/Accum/Self 断点奖励**

   * 若断点在这些词附近（词前或词后，按语言定义） → 给该行/该断点奖励

3. **情绪落点强化**

   * 若一行以情绪词结尾（或靠近行尾） → 奖励
   * 若情绪词被埋在长行中间 → 惩罚

#### 7.2B（实现口径补充）“靠近行尾”的确定性定义（必须）

为避免实现者自行发挥，“靠近行尾”固定为：

* EN：情绪词位于该行 **最后 1 个词**（即行尾词）时视为命中“靠近行尾”
* TC：情绪词位于该行 **最后 2 个 grapheme** 范围内时视为命中“靠近行尾”

> 注：此定义只用于 EmotionWord 强化（奖励/惩罚），不影响 tokenization 与断点生成。

### 7.2A（裁决补充）EN 断点奖励的“行首/行尾感知”定义（必须）

对 EN 的 Shift / Accum / Self 奖励采用 **行首/行尾感知**：

* 若某断点 `pos` 使得：

  * **新行的第一个词**命中对应词库（Shift/Accum/Self），则对该断点给予奖励（推荐作为主奖励路径）。
  * 或 **上一行的最后一个词**命中对应词库，则也可给予奖励（可与前者相同或略低，但必须固定实现；若未单独配置，默认与前者相同以简化）。
* 命中判断在 token.text 上做确定性匹配（可先做小写化与两端去常见标点）。

---

## 8. 行长度与节奏参数（建议默认值）

### 8.1 中文（TC）

* `minCharsPerLineTC`: 4（弱规则；硬约束下限仍为 2）
* `idealCharsPerLineTC`: 8～12（用于评分）
* `maxCharsPerLineTC`: 16（强惩罚；如超过可视为必须换行的压力项）

### 8.2 英文（EN）

* `minWordsPerLineEN`: 2（弱规则；硬约束下限仍为 1）
* `idealWordsPerLineEN`: 3～6
* `maxWordsPerLineEN`: 8（强惩罚）

> 注：这些不是“硬塞阈值”，而是用于评分与候选裁剪的偏好。

### 8.3（裁决补充）理想长度采用“宽度派”（必须）

为提升视觉一致性，idealLen 采用 **宽度派**推导：

* 在 width 可用时：

  * 以 `idealWidth = availableWidth * idealWidthRatio`（建议 0.85～0.95，写入配置）
  * 对 EN：`idealWordsPerLineEN` 不作为固定常量，而作为弱上界/弱先验；评分中的 ideal 以 **width-based** 的 line.width 与 idealWidth 的距离为主。
  * 对 TC：`idealCharsPerLineTC` 同理，主要以 **width-based** idealWidth 做评分；chars 仅作辅助项（例如 Widget 或 width unknown 时）。
* 在 width unknown / approx mode 时：

  * 回退到 chars/words 的 ideal 区间（8～12 / 3～6）。

#### 8.3A（实现口径补充）idealWidthRatio 默认值（必须写死到配置）

为保证首版跨端一致，默认值裁决为：

* `idealWidthRatio.APP = 0.90`
* `idealWidthRatio.WIDGET = 0.95`

> 后续若做 A/B 或灰度调整，请通过 configVersion 管理并打点回溯。

---

## 9. 搜索与组合（可直接实现）

多行换行的核心是“断点组合搜索”。要求：

* 输出最优 layout
* 复杂度可控
* 完全确定性

### 9.1 推荐：DP + TopK（App）

**状态定义**

* `dp[pos][linesUsed] = TopK layouts ending at token boundary pos`
* pos 是 token 边界索引（0..N）

**转移**

* 从 (pos, linesUsed) 选择下一个断点 `nextPos` 形成一行 [pos..nextPos)
* 计算 lineText 与 lineWidth
* 若违反硬约束，跳过
* 计算增量评分（见第 10 节）
* 插入 dp[nextPos][linesUsed+1] 的 TopK

**TopK 维护（确定性）**

* K 建议 10
* 排序：score desc；tie-break 使用固定 keys（见第 11 节）
* 插入时去重（同 breaks 序列只保留最高分）

**结束条件**

* 在 `pos=N`（结束边界）处，从 `dp[N][<=maxLines]` 选最优
* lineMode=FIXED 时优先选 `dp[N][==maxLines]`，否则降级

### 9.2 推荐：Beam Search（Widget）

* 每一行扩展时只保留 TopK partial layouts（K 建议 5）
* 候选断点也更少（更强裁剪）

**Beam 过程（概念）**

* 初始 beams = [{pos=0, breaks=[], score=0}]

* repeat for lineIndex in 1..maxLines:

  * 对每个 beam，从 pos 扩展到若干 nextPos，生成新 beams
  * 过滤硬约束
  * 评分
  * 全局保留 TopK beams

* 结束时从 beams 中挑 pos==N 最优，否则进入溢出/兜底

---

## 9.3（新增）复杂度与性能预算（建议写进实现约束）

为保证线上性能稳定，建议默认上限：

* App：`tcMaxCandidateBreaks <= 80`，`TopK=10`，`maxLines<=3`（或 4）
* Widget：Beam `K<=5`，每步扩展断点数 `M<=12`

当输入超长时（例如 TC>60 grapheme 或 EN>30 words），建议触发候选裁剪或直接走溢出策略并打点 `TOO_LONG`。

---

## 10. 评分模型（Scoring Model，建议实现顺序）

> 评分用于在“多个合法断点组合”中选最优。权重固定以保证稳定性。

### 10.1 强烈建议实现顺序

1. 情绪短语拆分惩罚（最高优先）
2. 行超长惩罚 + 理想长度奖励
3. widow 惩罚（EN）/ 单字行惩罚（TC）
4. 标点断点奖励（TC）
5. Shift/Accum/Self 断点奖励
6. 多行视觉均衡（尾行过短惩罚）

#### 10.1A（实现口径补充）评分实现顺序不可重排（必须）

* 所有评分项必须按 10.1 的顺序计算并记录到 breakdown。
* 后续项不得“覆盖”前序裁决结果（例如：已触发 EmotionPhrase 拆分强惩罚后，不允许因为其他奖励而在实现层面跳过/抵消该惩罚的记录）。
* 允许在数学意义上出现“总分被奖励抬高”，但 **必须保留每一项的确定性记录**，以便 debug 与治理。

### 10.2 评分项清单（可直接落地）

**A. 情绪短语保护（EmotionPhrase）**

* 若拆分任意 emotionPhrase：`score -= P_EMOTION_SPLIT`（非常大）
* 若被 constraints.protectedPhrases 拆分：`score -= P_PROTECTED_SPLIT`（非常大）

#### 10.2A（实现口径补充）Phrase 匹配必须为“连续 token 完全匹配”（必须）

* emotionPhrase / protectedPhrases 的匹配方式固定为：
  **连续 token 的完全匹配**（EN=连续词序列，TC=连续 grapheme 序列），不允许跳 token、不允许跨断点、不允许模糊匹配。
* 若采用预计算 span：span 的 start/end 必须与 token 索引体系一致（见 2.3）。

### 10.2A（裁决补充）emotionPhrases 与 protectedPhrases 的冲突优先级

当 emotionPhrase 与 protectedPhrase 发生竞争（无法同时满足）时：

* 两者视为**同级强保护**，在评分上同量级惩罚；
* **tie-break/决策时以“更长者优先”**：

  * 若某 layout 保住了更长短语（例如 `"真的好累"`）而拆了更短短语（例如 `"好累"`），在同等可行性下应更倾向前者。
* 实现建议：在拆分惩罚触发时，额外记录被拆分短语的长度（token span 长度），用于 tieKey 或额外惩罚的细分（必须确定性）。

**B. Shift/Accum/Self（词附近断点奖励）**

* 断点在 shiftWord 行首/行尾命中：`score += R_SHIFT_BREAK`
* 断点在 accumWord 行首/行尾命中：`score += R_ACCUM_BREAK`
* 断点在 selfWord 行首/行尾命中：`score += R_SELF_BREAK`

**C. 行长度（Length）**

* `score += f_ideal(lineLen, idealLen)`（越接近越好）
* 若 lineLen > maxLen：`score -= P_OVER_MAXLEN * (lineLen - maxLen)`
* 若 lineLen < minPreferred：`score -= P_TOO_SHORT * (minPreferred - lineLen)`

> f_ideal 可用简单的：`-abs(lineLen - idealLen)`

**D. 标点断点（TC）**

* 若断点位于 PUNCT 后：`score += R_PUNCT_BREAK`
* 若行首为标点：淘汰（建议见 H6）或极大惩罚

**E. Widow（EN）与尾行过短（All）**

* EN：最后一行只有 1 个词：`score -= P_WIDOW_LINE`
* EN：最后一行只有 1 个短词：`score -= P_WIDOW_WORD`
* All：最后一行宽度 < 某阈值（例如 idealWidth*0.5）：`score -= P_SHORT_LASTLINE`

**F. TC 助词孤立（TC）**

* 行首/行尾为助词：`score -= P_PARTICLE_ISO`
* 注：语尾语助词（啊/喔/呢/啦）可通过例外白名单降惩罚

### 10.2F（裁决补充）TC 语尾语助词白名单：惩罚减半

* 对白名单中的语尾语助词（如：啊/喔/呢/啦 等）：

  * 若触发 PARTICLE_ISO（行首/行尾助词），其惩罚使用：
    **`P_PARTICLE_ISO / 2`**
* 非白名单助词仍使用完整 `P_PARTICLE_ISO`。
* 白名单列表必须写入配置并全端一致。

### 10.2G（裁决补充）EmotionWord 与 Accum 奖励关系（必须）

为避免奖励叠加导致评分失真，奖励关系固定为：

* **EmotionWord > Accum**：

  * 若同一行（或同一断点）同时满足 EmotionWord 强化与 Accum 断点奖励：

    * EmotionWord 相关奖励保持原值
    * Accum 相关奖励按 **0.5 倍**计算（即“后者减半”）
* 该规则必须确定性实现（例如先计算 EmotionWord 命中，再对 Accum 奖励做折减）。

### 10.3 建议的默认权重（仅供实现起步）

> 你可以先用相对大小，不必精确数值。

* P_EMOTION_SPLIT：10000
* P_PROTECTED_SPLIT：10000
* P_WIDOW_WORD：800
* P_WIDOW_LINE：500
* P_SHORT_LASTLINE：300
* P_PARTICLE_ISO：200
* R_PUNCT_BREAK：80
* R_SHIFT_BREAK：60
* R_ACCUM_BREAK：40
* R_SELF_BREAK：20
* P_OVER_MAXLEN：30
* P_TOO_SHORT：10

---

## 10.4（新增）评分可解释结构（Debug/治理必备）

为便于线上治理与调参，建议统一输出 score breakdown：

```ts
ScoreBreakdown = {
  total: number,
  terms: Array<{ key: string; delta: number; detail?: any }>
}
```

* `key` 建议枚举：`EMOTION_SPLIT | PROTECTED_SPLIT | OVER_MAXLEN | IDEAL_LEN | TOO_SHORT | PUNCT_BREAK | SHIFT_BREAK | ACCUM_BREAK | SELF_BREAK | WIDOW_LINE | WIDOW_WORD | SHORT_LASTLINE | PARTICLE_ISO`
* debug=true 时可只输出贡献最大的前 3 项。

### 10.4A（裁决补充）Debug Top-3 输出排序：按规则优先级

debug=true 时输出的“关键项”排序采用**规则优先级**而非 |delta|：

* 按 10.1 的实现顺序（优先级）输出
* 若同优先级内有多项，可再按 |delta| 或出现顺序稳定排序（必须确定性）

---

## 11. Tie-break（必须固定，确保确定性）

当 score 相同或非常接近时，按顺序比较：

1. emotionSplit=false 优先
2. overflowed=false 优先
3. lastLineWidth 更大优先（避免短尾行）
4. 行宽分布更均匀优先（可用 maxWidth-minWidth 更小）
5. 断点更接近理想切分点优先（距离之和更小）
6. 断点序列字典序更靠前优先（例如 firstBreak 更小）

> 实现建议：为 Layout 计算一个 `tieKey` 数组，逐项比较。

### 11.0A（裁决补充）breaks[] 字典序比较定义（必须）

断点序列字典序比较规则固定为：

* 从 `index=0` 起逐项比较 `breaks[i]`：

  * 首个不同元素更小者视为更小
* 若公共前缀完全相同，则：

  * **更短的数组**视为更小（短数组在公共前缀相等时优先）

---

## 11.1（新增）分数精度与浮点处理（跨端一致性强烈建议）

* 建议所有评分项都用 **整数**，避免浮点误差。
* 若必须使用浮点，必须定义 `EPS`：例如 `EPS=1e-6`。
* “非常接近”的定义：`abs(a-b) <= EPS`。

---

## 12. 溢出与兜底（必须定义清楚）

### 12.1 overflowMode

* `ELLIPSIS`：最后一行加省略号（Widget 推荐默认）
* `CLIP`：截断到 maxLines
* `SYSTEM_DEFAULT`：不插入换行，交给系统

### 12.2 何时算 overflow

* 搜索无法在 <=maxLines 覆盖到文本结束边界 N
* 或 lineMode=FIXED 需要刚好 maxLines，但无法到 N

### 12.3 ELLIPSIS 的实现约束

* 仅在最后一行处理
* 必须再次检查 H1（加省略号后是否超宽）
* 若超宽：优先从最后一行尾部移除 token 再加省略号
* 尽量避免把 emotionPhrase 截断在中间：

  * 若会截断，可选择整段前移或整段省略（以评分决定）

### 12.4 无解兜底

降级链：

1. 保证 H1/H2/H3
2. 将 H4/助词/widow 从强规则降为弱惩罚
3. 若仍无解：SYSTEM_DEFAULT

### 12.4A（裁决补充）SYSTEM_DEFAULT 的返回语义（必须）

当选择 `SYSTEM_DEFAULT` 时：

* wrapText **仍返回 lines / wrappedText（可为未换行的单行或原始文本）**
* 并且 **仅在 meta 中标记**：`fallback_type = SYSTEM_DEFAULT`
* 外部组件（UI 层）依据该 meta 决定是否完全交给系统排版（例如不插入 `\n`，或忽略 lines 渲染）。

> 关键点：算法层不擅自改变 UI 行为，只提供明确标记，保证可治理与跨端一致。

---

## 12.5（新增）ELLIPSIS 细则（必须明确字符与清理规则）

* 省略号字符统一：`ellipsisToken = "…"`（推荐单字符）或 `"..."`（三字符），必须写入配置并全端一致。
* EN 截断回退单位：**整词**；TC 回退单位：**grapheme**。
* 清理规则：

  * 不允许输出 `"  …"`（空格+省略号）
  * 不允许输出 `"，…"` 或 `"。…"`（标点+省略号）时可按配置决定是否移除末尾标点再加省略号
* 加省略号必须重新测量宽度，确保不超宽。

---

## 13. 受控人工约束（可选，但建议实现）

### 13.1 protectedPhrases

* 行为：拆分该短语 → 视同 emotionPhrase 拆分（强惩罚）

### 13.2 forbiddenBreakRanges

* 行为：断点 pos 落在 [start,end] 内 → 候选断点直接剔除

> 注意：start/end 的单位应与 token 索引一致（EN=词边界；TC=字符簇边界）。

---

## 14. Debug Meta 与打点（强烈建议）

### 14.1 debug meta（可选输出）

* chosen breaks
* score breakdown（可只输出前 3 个最关键项；排序见 10.4A）
* fallback/overflow 原因

### 14.2 线上打点建议

* fallback_type：NONE / RELAX_RULES / SYSTEM_DEFAULT
* overflow_type：NONE / ELLIPSIS / CLIP
* reason：NO_CANDIDATE / WIDTH_UNKNOWN / WIDOW / PARTICLE / TOO_LONG
* lang/context/maxLines/widgetSize

---

## 15. 实现步骤（建议落地路线）

1. EN tokenize + SPACE breakpoints + 2 行先跑通（EN 标点按极简派；关键词命中按 2.3.1A-1）
2. TC tokenize（grapheme）+ PUNCT breakpoints + 2 行跑通
3. 引入 emotionPhrases 拆分惩罚（phrase 匹配按 10.2A）
4. 引入 DP + TopK（多行）
5. 引入 overflowMode（Widget 先用 ELLIPSIS）
6. 引入 constraints + 打点
7. 调权重与扩词库（基于数据）

---

## 16. 附录：最小配置清单（实现时至少要有）

* emotionPhrasesTC / emotionPhrasesEN
* tcPunctuations
* tcParticles（含白名单子集或单独 tcParticleWhitelist）
* widowMaxLen
* tcMaxCandidateBreaks
* minCharsPerLineTC
* widgetProfiles（Widget：availableWidth/maxLines/overflowMode/beamK）
* ellipsisToken（新增）
* EPS（若使用浮点；新增）
* idealWidthRatio（新增，宽度派必需；默认值见 8.3A）

---

## 16.1（新增）金标测试用例与回归策略（强烈建议）

为保证“确定性 + 可治理”，建议维护一份 Golden Cases：

* 每种语言至少 20 个样例（短、长、含标点、无标点、含 emoji、含 protectedPhrases、含 shift/accum/self、极窄宽度）
* 每个样例包含：

  * input text
  * lang/context
  * availableWidth/maxLines/fontSpec（或 widget profile）
  * expected lines[]

再加 3 类性质测试（property tests）：

1. **同输入多次调用输出一致**
2. **availableWidth 变小不会让任一行变得更宽**（近似单调性检查）
3. **maxLines 增加时不应更差**（至少不从可解变 overflow）

---

## 16.2（新增）配置版本治理（建议）

* `configVersion` 应代表“可灰度的规则包版本”。
* meta 与线上打点中必须带上 `configVersion`，以便回溯与对比实验。

---

## 17. 结语

这套算法的目标不是理解情绪，而是：

> 在规模化系统中，持续做出「像人一样停顿」的选择。

只要：确定性 + 可配置 + 可治理，你就能持续把“情绪表达”变成产品护城河。
