# 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)