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