# scoring-tiebreak(任务清单) > 目标:实现“评分模型 + 确定性 tie-break”,用于对候选 layout 打分并产出可比较的 `tieKey`。 > 约束:**整数评分**、**权重/最小词表写死客户端**、短语匹配为**连续 token 完全匹配**、debug Top-3 按**优先级顺序**输出。 ## 0. 准备与对齐 - [x] 阅读并对齐口径 - [x] 复核 `spec_kit/Text Wrap/modules/scoring-tiebreak/spec.md` - [x] 复核 `spec_kit/Text Wrap/modules/scoring-tiebreak/plan.md` - [x] 复核 `设计说明文档/文档换行算法.md` 中 10.x(评分)与 11(tie-break)章节的条目顺序 - [x] 在客户端创建模块目录 - [x] 新建 `client/src/features/textWrap/scoring/` - [x] 确认后续文件均落在该目录下,避免与 `core/`、`breakpoints/`、`measure/` 混放 ## 1. 定义类型与对外接口 - [x] 新建 `client/src/features/textWrap/scoring/types.ts` - [x] 定义 `TextWrapContext = 'APP' | 'WIDGET'` - [x] 定义 `LayoutCandidate`(与 spec 一致:`breaks` + `lines[]`) - [x] 定义 `LineInfo`:`start/end/text/width/tokenCount/charCount` - [x] 定义 `ScoreTerm`:`{ key: string; delta: number; detail?: any }` - [x] 定义 `ScoreBreakdown`:`{ total: number; terms: ScoreTerm[] }` - [x] 定义 `ScoredLayout`:`{ score; flags; tieKey; scoreBreakdown? }` - [x] 明确评分函数需要的“可用宽度 availableWidth”传入方式(二选一,必须确定并全链路一致) - [x] 方案 A:作为 `scoreLayout({ availableWidth, ... })` 的必填字段 - [x] 方案 B:作为 `config.availableWidth`(不推荐,但允许) - [x] 新建 `client/src/features/textWrap/scoring/index.ts` - [x] 统一导出 types 与核心函数(后续实现) ## 2. 权重与最小词表(写死客户端) - [x] 新建 `client/src/features/textWrap/scoring/weights.ts` - [x] 以对象形式集中定义权重常量(全部整数) - [x] 写入 plan.md 里的首版默认值(P_/R_ 系列) - [x] 提供 `DEFAULT_WEIGHTS`(只读)与可选的 `mergeWeights(overrides)`(用于调用方覆盖) - [x] 新建 `client/src/features/textWrap/scoring/lexicons.ts` - [x] 定义 `Lexicons` 类型(与 spec 输入一致) - [x] 写入最小词表(TC/EN:shift/accum/self,emotionWords 可选) - [x] 提供 `DEFAULT_LEXICONS`(只读)与可选的 `mergeLexicons(overrides)` - [x] 明确 `emotionPhrasesTC/EN`、`protectedPhrases` 首版可为空数组 ## 3. Phrase 匹配(连续 token 完全匹配) - [x] 新建 `client/src/features/textWrap/scoring/phraseMatch.ts` - [x] 定义 `PhraseSpan = { start: number; end: number; length: number }`(end 半开) - [x] 实现 EN phrase 预处理 - [x] 使用 `core-contract`:`normalizeWhitespace` + `tokenizeEN` - [x] phrase 输入为原始字符串:先 normalizeWhitespace,再按空格切分为词序列 - [x] 实现 TC phrase 预处理 - [x] 使用 `grapheme-segmentation`:`segmentGraphemes` 得到 clusters - [x] phrase 输入为原始字符串:按 grapheme clusters 切分 - [x] 实现连续区间完全匹配查找(禁止跳 token / 禁止模糊) - [x] 输出所有命中的 spans(稳定顺序:按 start 升序,start 相同按 length 降序) - [x] 实现 `isSpanSplitByBreaks(span, breaks)`:判断 span 是否跨行(被拆分) - [x] 单测覆盖: - [x] EN:`but,` 命中 `but`;`rebuttal` 不命中 `but` - [x] TC:包含 emoji/组合字符时不应被拆(依赖 grapheme 模块;这里只验证匹配结果可逆) ## 4. 评分实现(按 10.1 固定顺序,整数累计) - [x] 新建 `client/src/features/textWrap/scoring/score.ts` - [x] 定义 `scoreLayout(args)`(入参包含:layoutCandidate、lang、context、config、lexicons、availableWidth、debug?) - [x] 实现 score 累计(必须严格按文档 10.1 的顺序) - [x] 1) 情绪短语/受保护短语拆分惩罚(最高优先) - [x] 匹配 emotionPhrases + protectedPhrases spans - [x] 若 span 被拆分:`score -= P_*` - [x] 采用方案 A:追加“更长短语优先”惩罚:`score -= phraseLength`(k=1 写死) - [x] 设置 `flags.emotionSplit=true`(如需区分 protected 可扩展 detail,但保持确定性) - [x] 2) 行长度(ideal/overMaxLen/tooShort) - [x] `idealWidth = availableWidth * idealWidthRatio[context]`(取整规则必须固定:建议 `Math.round` 并写注释) - [x] width-based 主项:`score += -abs(line.width - idealWidth)`(确保整数) - [x] over/tooShort 按权重惩罚(阈值若来自文档,集中定义常量,禁止散落) - [x] 3) Widow / 单字行惩罚(基于 tokenCount/charCount 口径写死) - [x] 4) TC 标点断点奖励(使用断点 meta/kind 或基于行首字符推断;口径写注释) - [x] 5) Shift/Accum/Self 断点奖励(TC/EN 词表) - [x] EN 必须使用 `core-contract` 的全词等值匹配(复用 `normalizeENKeyword/matchENKeyword`) - [x] 实现 spec 要求的折减:EmotionWord 与 Accum 同时命中时,Accum 奖励按 0.5 倍 - [x] 因为整体使用整数:折减采用“整除/四舍五入”的固定规则(建议 `Math.floor(reward/2)` 并写注释) - [x] 6) 尾行过短惩罚(SHORT_LASTLINE) - [x] 记录 `scoreBreakdown` - [x] 每个 term 写入 `{ key, delta, detail }` - [x] terms 的产生顺序必须与 10.1 顺序一致(用于 debug 输出排序) - [x] debug Top-3(不按 |delta|,按优先级) - [x] 若 `debug=true`:只保留前 3 个“优先级最高的关键项” - [x] 规则:先按 10.1 顺序,必要时同优先级保持稳定(按出现顺序) - [x] 单测覆盖: - [x] 评分顺序固定:构造 layout 触发 EmotionSplit,断言 breakdown 第一项为对应 key - [x] 整数性:断言所有 delta 与 total 都是整数 ## 5. tieKey 构造与比较(按 11 节固定顺序) - [x] 新建 `client/src/features/textWrap/scoring/tieKey.ts` - [x] 实现 `buildTieKey(scoredLayout, layoutCandidate, config)`,严格按 11 节顺序产出 `Array` - [x] emotionSplit(false 优先) - [x] overflowed(false 优先) - [x] lastLineWidth(更大优先:采用 `-lastLineWidth` 进入 tieKey,或在比较函数中反向比较,二选一且写注释) - [x] 宽度分布均衡度:`maxWidth - minWidth`(更小优先) - [x] 与理想切分点距离(更小优先;距离定义需与 breakpoints 模块/上游一致) - [x] breaks 字典序(复用 `core-contract` 的 breaks 比较函数) - [x] 单测覆盖: - [x] 同分 layout:通过 tieKey 能稳定选出同一个最优 - [x] Widget approx:即使 width 近似(wordCount/graphemeCount),tieKey 仍可比较(不抛错) ## 6. 组合导出与回归测试 - [x] 完成 `client/src/features/textWrap/scoring/index.ts` 导出 - [x] 导出 `DEFAULT_WEIGHTS/DEFAULT_LEXICONS` - [x] 导出 `scoreLayout`、`buildTieKey` - [x] 新建 `client/src/features/textWrap/scoring/__tests__/scoringTiebreak.test.ts` - [x] 覆盖 EN/TC 两套 tokenization(EN 用 core,TC 用 grapheme) - [x] 覆盖短语拆分惩罚 + 方案 A 的“更长短语更重惩罚” - [x] 覆盖 debug Top-3 按优先级输出(非 |delta|) - [x] 覆盖 tieKey 顺序项的比较方向(尤其 lastLineWidth) ## 7. 文档与收尾(完成后必须做) - [x] 将 `spec_kit/Text Wrap/modules/scoring-tiebreak/tasks.md` 全部任务勾选完成 - [x] 更新 `spec_kit/overview.md` - [x] 标记 `scoring-tiebreak` 已完成编码(阶段性) - [x] 在 overview 中记录本次变更文件清单(至少包含新增的客户端文件与测试文件)