# search-engine-app(任务清单) > 目标:在 APP 场景实现 `DP + TopK` 的确定性组合搜索(对应文档 9.1),并接入 `scoring-tiebreak` 输出最优 layout。 ## 0. 对齐与准备 - [x] 复核输入/输出与依赖 - [x] 阅读 `spec_kit/Text Wrap/modules/search-engine-app/spec.md` - [x] 阅读 `spec_kit/Text Wrap/modules/search-engine-app/plan.md` - [x] 阅读 `设计说明文档/文档换行算法.md` 的 9.1/9.3/11 章节 - [x] 创建客户端模块目录 - [x] 新建 `client/src/features/textWrap/searchApp/` ## 1. 类型定义与导出 - [x] 新建 `client/src/features/textWrap/searchApp/types.ts` - [x] 定义 `LineMode = 'AUTO' | 'FIXED'` - [x] 定义 `SearchAppConfig`(包含 `topK`、`tooLongThresholds` 等,首版给默认值) - [x] 定义 `SearchAppInput`: - [x] `tokens: Token[]` - [x] `lang: 'TC' | 'EN'` - [x] `breakpoints: Breakpoint[]` - [x] `availableWidth: number` - [x] `maxLines: number` - [x] `lineMode: LineMode` - [x] `measure: { contextProfile; fontSpec; measureWidthImpl }`(复用 width-measurement 类型) - [x] `scoring: { config; lexicons; debug? }`(复用 scoring-tiebreak 类型) - [x] 定义 `SearchAppResult`: - [x] 成功:`bestLayout { breaks; lines; wrappedText; meta }` - [x] 失败:`{ ok:false; reason:'TOO_LONG'|'WIDTH_UNKNOWN'|'NO_CANDIDATE'; meta }` - [x] 新建 `client/src/features/textWrap/searchApp/index.ts` - [x] 统一导出 types 与核心入口 `searchBestLayoutApp()` ## 2. TopK 维护(确定性) - [x] 新建 `client/src/features/textWrap/searchApp/topK.ts` - [x] 实现 `compareLayouts(a,b)`: - [x] `score` 降序 - [x] `tieKey` 逐项比较(数值越小越优) - [x] `breaks` 字典序兜底(11.0A) - [x] 实现去重:同 breaks 仅保留最优 - [x] 实现 `insertTopK(list, cand, K)`:插入、去重、排序、截断(全程确定性) ## 3. 约束与可解释失败原因 - [x] 新建 `client/src/features/textWrap/searchApp/constraints.ts` - [x] 实现 `isTooLong(tokens, lang)`:TC>60 或 EN>30(首版写死) - [x] 实现 `isLineStartPunctTC(tokens, pos, tcPunctuations)`:H6 - [x] 实现 `isOverWidth(width, availableWidth)`:H1 ## 4. DP + TopK 主流程 - [x] 新建 `client/src/features/textWrap/searchApp/dpTopK.ts` - [x] 实现入口 `searchBestLayoutApp(input)` - [x] 构造 dp:`dp[pos][linesUsed] = TopK[]` - [x] 枚举顺序固定: - [x] pos:0..N - [x] linesUsed:0..maxLines-1 - [x] nextPos:从 breakpoints 里取 `>pos` 的 pos 升序 + 追加 `N` - [x] 每次转移: - [x] 构造 lineText:使用 `joinTokens`(EN 默认空格,TC 使用 rawSeparators 拼接) - [x] 测量 lineWidth:使用 `measureSliceWidthCached` - [x] 过滤硬约束:H1/空行/H6/maxLines - [x] 构造 layoutCandidate(breaks + lines[]) - [x] 调用 `scoreLayout` + `buildTieKey` 得到 score/tieKey - [x] 插入目标 dp 并维护 TopK - [x] 结束选择: - [x] AUTO:从 `dp[N][<=maxLines]` 选最优 - [x] FIXED:优先 `dp[N][==maxLines]`,否则返回 `NO_CANDIDATE`(交给 overflow-fallback 再降级) - [x] meta 输出: - [x] `breaks` - [x] `scoreTopTerms`(若 debug=true,从 scoring 输出 Top-3) ## 5. 单测(Vitest) - [x] 新建 `client/src/features/textWrap/searchApp/__tests__/searchApp.test.ts` - [x] 确定性:同输入运行多次结果一致 - [x] TopK 去重:两条路径同 breaks 只保留一个 - [x] FIXED:必须刚好 maxLines,否则失败 reason=NO_CANDIDATE - [x] TC H6:行首标点导致的转移必须被禁止 - [x] TOO_LONG:触发阈值直接返回 reason=TOO_LONG ## 6. 收尾 - [x] 跑测试与类型检查 - [x] `npm test` - [x] `npx tsc --noEmit` - [x] 将本 `tasks.md` 全部勾选完成 - [x] 更新 `spec_kit/overview.md` - [x] 标记 `search-engine-app` 已完成编码(阶段性) - [x] 写入变更文件清单