Files
mindfulness/spec_kit/Text Wrap/modules/scoring-tiebreak/tasks.md
2026-02-10 11:39:33 +08:00

7.6 KiB
Raw Blame History

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评分与 11tie-break章节的条目顺序
  • 在客户端创建模块目录
    • 新建 client/src/features/textWrap/scoring/
    • 确认后续文件均落在该目录下,避免与 core/breakpoints/measure/ 混放

1. 定义类型与对外接口

  • 新建 client/src/features/textWrap/scoring/types.ts
    • 定义 TextWrapContext = 'APP' | 'WIDGET'
    • 定义 LayoutCandidate(与 spec 一致:breaks + lines[]
    • 定义 LineInfostart/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(不推荐,但允许)
  • 新建 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/ENshift/accum/selfemotionWords 可选)
    • 提供 DEFAULT_LEXICONS(只读)与可选的 mergeLexicons(overrides)
    • 明确 emotionPhrasesTC/ENprotectedPhrases 首版可为空数组

3. Phrase 匹配(连续 token 完全匹配)

  • 新建 client/src/features/textWrap/scoring/phraseMatch.ts
    • 定义 PhraseSpan = { start: number; end: number; length: number }end 半开)
    • 实现 EN phrase 预处理
      • 使用 core-contractnormalizeWhitespace + tokenizeEN
      • phrase 输入为原始字符串:先 normalizeWhitespace再按空格切分为词序列
    • 实现 TC phrase 预处理
      • 使用 grapheme-segmentationsegmentGraphemes 得到 clusters
      • phrase 输入为原始字符串:按 grapheme clusters 切分
    • 实现连续区间完全匹配查找(禁止跳 token / 禁止模糊)
      • 输出所有命中的 spans稳定顺序按 start 升序start 相同按 length 降序)
    • 实现 isSpanSplitByBreaks(span, breaks):判断 span 是否跨行(被拆分)
    • 单测覆盖:
      • ENbut, 命中 butrebuttal 不命中 but
      • TC包含 emoji/组合字符时不应被拆(依赖 grapheme 模块;这里只验证匹配结果可逆)

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 -= phraseLengthk=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) 并写注释)
      • 6) 尾行过短惩罚SHORT_LASTLINE
    • 记录 scoreBreakdown
      • 每个 term 写入 { key, delta, detail }
      • terms 的产生顺序必须与 10.1 顺序一致(用于 debug 输出排序)
    • 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>
      • emotionSplitfalse 优先)
      • overflowedfalse 优先)
      • lastLineWidth更大优先采用 -lastLineWidth 进入 tieKey或在比较函数中反向比较二选一且写注释
      • 宽度分布均衡度:maxWidth - minWidth(更小优先)
      • 与理想切分点距离(更小优先;距离定义需与 breakpoints 模块/上游一致)
      • breaks 字典序(复用 core-contract 的 breaks 比较函数)
    • 单测覆盖:
      • 同分 layout通过 tieKey 能稳定选出同一个最优
      • Widget approx即使 width 近似wordCount/graphemeCounttieKey 仍可比较(不抛错)

6. 组合导出与回归测试

  • 完成 client/src/features/textWrap/scoring/index.ts 导出
    • 导出 DEFAULT_WEIGHTS/DEFAULT_LEXICONS
    • 导出 scoreLayoutbuildTieKey
  • 新建 client/src/features/textWrap/scoring/__tests__/scoringTiebreak.test.ts
    • 覆盖 EN/TC 两套 tokenizationEN 用 coreTC 用 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 中记录本次变更文件清单(至少包含新增的客户端文件与测试文件)