更新换行算法和APP-PUSH
This commit is contained in:
104
spec_kit/Text Wrap/modules/search-engine-app/plan.md
Normal file
104
spec_kit/Text Wrap/modules/search-engine-app/plan.md
Normal file
@@ -0,0 +1,104 @@
|
||||
# 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)
|
||||
|
||||
51
spec_kit/Text Wrap/modules/search-engine-app/spec.md
Normal file
51
spec_kit/Text Wrap/modules/search-engine-app/spec.md
Normal file
@@ -0,0 +1,51 @@
|
||||
# search-engine-app(子模块规范)
|
||||
|
||||
## 子模块名称
|
||||
|
||||
search-engine-app(APP 搜索器:DP + TopK)
|
||||
|
||||
## 目标描述
|
||||
|
||||
在 APP 场景实现多行换行的确定性组合搜索:
|
||||
|
||||
- 使用 DP + TopK 生成候选 layout
|
||||
- 对每个候选应用硬约束过滤
|
||||
- 调用 `scoring-tiebreak` 计算评分与 tieKey
|
||||
- 保留 TopK 并在结束时选最优
|
||||
|
||||
要求性能可控且输出完全确定性。
|
||||
|
||||
## 输入/输出定义
|
||||
|
||||
### 输入
|
||||
|
||||
- `tokens: Token[]`
|
||||
- `breakpoints: Breakpoint[]`(来自 `breakpoint-candidates`)
|
||||
- `availableWidth: number`
|
||||
- `maxLines: number`
|
||||
- `lineMode: 'AUTO' | 'FIXED'`
|
||||
- `context: 'APP'`
|
||||
- `measure: { measureWidth?: fn; fontSpec?: any; cache: ... }`(来自 `width-measurement` 的实现)
|
||||
- `config: { topK: number }`
|
||||
|
||||
### 输出
|
||||
|
||||
- `bestLayout: { breaks: number[]; lines: string[]; wrappedText: string; meta?: { breaks: number[]; scoreTopTerms?: ... } }`
|
||||
- 若无解:返回可解释的 fallback(由 `overflow-fallback` 统一处理语义)
|
||||
|
||||
## 验收标准(可验证)
|
||||
|
||||
- **确定性**:同输入(tokens/breakpoints/availableWidth/maxLines/configVersion)必定同输出(breaks 与 lines 完全一致)
|
||||
- **TopK 维护一致**:
|
||||
- K 默认 10(可配置但必须固定)
|
||||
- 排序:score desc;tieKey 逐项比较
|
||||
- 去重:同 breaks 序列只保留最高分
|
||||
- **lineMode=FIXED 行为**:
|
||||
- 优先选刚好 `maxLines` 的解;无解时按固定降级链路处理并打点
|
||||
- **复杂度可控**:超长输入触发裁剪/兜底并返回 reason(如 `TOO_LONG`)
|
||||
|
||||
## 依赖与关联
|
||||
|
||||
- **依赖**:`core-contract`、`width-measurement`、`breakpoint-candidates`、`scoring-tiebreak`、`overflow-fallback`
|
||||
- **被依赖**:`integration`、`golden-tests`
|
||||
|
||||
92
spec_kit/Text Wrap/modules/search-engine-app/tasks.md
Normal file
92
spec_kit/Text Wrap/modules/search-engine-app/tasks.md
Normal file
@@ -0,0 +1,92 @@
|
||||
# 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] 写入变更文件清单
|
||||
|
||||
Reference in New Issue
Block a user