# search-engine-app（技术计划）

## 1. 计划目标

在 **APP** 场景实现确定性的“断点组合搜索器”，使用 **DP + TopK** 生成候选 layout 并选出最优解：

- 输入：`tokens[]`、`breakpoints[]`、`availableWidth`、`maxLines`、`lineMode`
- 过程：DP 状态转移 + TopK 维护（去重、排序、确定性 tie-break）
- 评分：调用 `scoring-tiebreak`（`scoreLayout` + `buildTieKey`）
- 输出：`bestLayout`（breaks/lines/wrappedText/meta）或返回 “无解/超长/宽度不可用” 的可解释原因（overflow-fallback 模块将统一处理最终语义）

## 2. 约束与确定性口径

### 2.1 确定性（必须）

同输入（含 configVersion/权重/词表/断点列表）必须同输出：

- DP 遍历顺序固定（pos 升序、linesUsed 升序）
- nextPos 枚举顺序固定（按 `pos` 升序；最后补一个 `N` 结束边界）
- TopK 的排序固定：`score` 降序；再按 `tieKey` 逐项比较（11 节）；再按 `breaks` 字典序（11.0A）
- 去重固定：同 `breaks` 序列只保留最优（score 更大；若相同用 tieKey）

### 2.2 复杂度（必须可控）

遵循文档 9.3 建议：

- 默认 `TopK=10`
- 默认 `maxLines<=3`（或 4）
- 超长输入触发 `TOO_LONG`：TC>60 grapheme 或 EN>30 words（首版写死，后续 configVersion 管理）

## 3. DP + TopK 设计（对应文档 9.1）

### 3.1 状态定义

令 `N=tokens.length`，token 边界为 `pos in [0..N]`：

- `dp[pos][linesUsed] = TopK partial layouts ending at token boundary pos`
- partial layout 至少包含：
  - `pos`：当前结束位置
  - `breaks: number[]`：已选择的断点序列（升序）
  - `lines: LineInfo[]`：已生成的行信息（start/end/text/width/tokenCount/charCount）
  - `score: number` + `tieKey: number[]`（由 scoring-tiebreak 构造）

### 3.2 转移（生成下一行）

从状态 `(pos, linesUsed)` 选择 `nextPos` 生成新行区间 `[pos..nextPos)`：

- nextPos 取值来自 `breakpoints` 中 `b.pos` 且 `b.pos > pos`，并按 `pos` 升序枚举
- 必须额外允许 `nextPos = N`（结束边界）
- 每次转移需：
  - 构造 lineText：使用 `core-contract/joinTokens`
  - 测量 lineWidth：优先使用 `width-measurement/measureSliceWidthCached`（切片缓存），若不可用返回 `WIDTH_UNKNOWN`
  - 应用硬约束过滤（见 3.3）
  - 计算新 layout 的评分与 tieKey（调用 scoring-tiebreak）
  - 插入 `dp[nextPos][linesUsed+1]` 并维护 TopK（去重、排序）

### 3.3 硬约束（本模块先落地最小集）

为了让 DP 行为稳定且可解释，首版在搜索层执行以下硬约束（其余放到评分）：

- **H1 不超宽**：若 lineWidth 可用且 `lineWidth > availableWidth`，该转移无效
- **H5 最大行数**：`linesUsed+1 <= maxLines`
- **禁止空行**：`nextPos > pos`（区间非空）
- **TC 禁止行首标点（H6）**：
  - 若 `lang=TC` 且 `pos>0` 且 `tokens[pos]` 属于 `tcPunctuations`，则该断点不可用（跳过）

> 说明：protectedPhrases 的“span 内断点剔除”属于候选断点优化（可选），首版不在搜索层做剔除，依赖 scoring 的强惩罚淘汰（文档 6.2A / 10.2A）。

### 3.4 结束条件与 lineMode

- 正常结束：到达 `pos=N`
- `lineMode=AUTO`：
  - 从 `dp[N][1..maxLines]` 里选最优
- `lineMode=FIXED`：
  - 优先从 `dp[N][==maxLines]` 里选最优
  - 若无解：返回 “无解” 的原因与必要 meta（由 overflow-fallback 统一降级链路与打点）

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

建议目录：

- `client/src/features/textWrap/searchApp/`
  - `types.ts`
  - `dpTopK.ts`（DP 主流程 + TopK 维护）
  - `topK.ts`（去重、插入、排序、截断）
  - `constraints.ts`（H1/H6/TOO_LONG 判定）
  - `index.ts`
  - `__tests__/searchApp.test.ts`

## 5. 测试计划（Vitest）

- **确定性**：同输入运行 3 次输出完全一致（breaks/lines/wrappedText）
- **TopK 去重**：构造两条路径得到相同 breaks，断言只保留一个且为最优
- **lineMode=FIXED**：有解时必须返回刚好 `maxLines`；无解时返回 reason（例如 `NO_CANDIDATE`）
- **TC 行首标点**：构造 tokens 在断点后行首为 `，`，断言该转移被禁止
- **TOO_LONG**：TC>60 或 EN>30 时直接返回 `TOO_LONG`

## 6. 完成定义（DoD）

- DP+TopK 可运行，且确定性通过单测
- 使用 `scoring-tiebreak` 进行评分与 tieKey 裁决
- 搜索层硬约束最小集落地（H1/H6/空行/maxLines）
- 无解/超长/宽度不可用时返回明确 reason（不直接在本模块做最终 overflow）

