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

7.4 KiB
Raw Blame History

scoring-tiebreak技术计划

1. 计划目标

基于 spec.md设计说明文档/文档换行算法.md v1.2.1,在客户端实现可解释的评分模型与确定性 tie-break保证

  • 整数评分:所有评分项使用整数,避免浮点误差
  • 权重写死客户端:首版权重/词表内置在客户端代码(后续如需灰度,用 configVersion 管理)
  • 短语匹配口径固定emotionPhrases/protectedPhrases 必须是“连续 token 完全匹配”
  • 更长短语优先:采用方案 A——在触发拆分惩罚时按“被拆分短语长度”追加惩罚确定性
  • debug 可解释:输出 score breakdowndebug=true 时输出 Top-3按规则优先级排序不是按 |delta|
  • tieKey 固定:严格按文档第 11 节顺序构造 tieKeyWidget approx mode 仍使用数值 width可为近似单位确保 tie-break 可用

本模块不负责搜索DP/Beam只负责对“候选 layout”打分并给出可比较的 tieKey。

2. 默认技术决策(本计划采用)

2.1 评分数值体系

  • 全部评分项使用 整数
  • 不引入 EPS因为不使用浮点分数相等即为相等

2.2 权重与默认值(写死客户端)

首版采用文档 10.3 的建议量级(可在实现中集中定义为常量/配置对象):

  • P_EMOTION_SPLIT = 10000
  • P_PROTECTED_SPLIT = 10000
  • P_WIDOW_WORD = 800
  • P_WIDOW_LINE = 500
  • P_SHORT_LASTLINE = 300
  • P_PARTICLE_ISO = 200
  • R_PUNCT_BREAK = 80
  • R_SHIFT_BREAK = 60
  • R_ACCUM_BREAK = 40
  • R_SELF_BREAK = 20
  • P_OVER_MAXLEN = 30
  • P_TOO_SHORT = 10

说明:权重必须集中在一个文件,禁止散落在各函数内;后续调整通过 configVersion 记录。

2.3 最小词表TC/EN

首版提供最小可用词表(来源:文档第 7 节):

  • TC
    • shift但/可是/然而/却/只是/偏偏
    • accum已经/一直/曾经/终于/还是/到现在
    • self你/我/自己/我们/别人
    • emotionWords可选累/痛/怕/孤单/委屈/撑/崩溃/放弃
  • EN全词等值匹配使用 core-contract 的 normalize 规则):
    • shiftbut/yet/soand 轻量可选)
    • accumalready/still/even/just/really
    • selfyou/yourself/me/we
    • emotionWords可选tired/afraid/lonely/hurt/overwhelmed/give up

emotionPhrasesTC/EN 与 protectedPhrases

  • 首版允许为空数组(先把匹配与惩罚机制写死)
  • 若业务侧已有短语清单,后续直接填充并通过 Golden Cases 回归

2.4 宽度派tieKey 使用 width

  • layoutCandidate.lines[*].width 字段作为 tieKey 的 width 来源
  • 在 width unknown/approx mode 时,上游仍需给出“数值宽度”(例如用 wordCount/graphemeCount 作为 width 近似值),保证 tie-break 可执行

3. 目录与产物(客户端侧实现)

建议代码落位:

  • client/src/features/textWrap/scoring/
    • types.tsLayout/LineSegment/ScoreBreakdown
    • weights.ts(权重常量,写死)
    • lexicons.ts(最小词表,写死)
    • phraseMatch.ts(连续 token 完全匹配 + span 预计算)
    • score.ts(按 10.1 顺序累计分数)
    • tieKey.ts(按 11 节构造 tieKey
    • debug.tsTop-3 提取与排序)
    • index.ts
    • __tests__/scoringTiebreak.test.ts

4. 输入/输出契约(实现阶段写死)

4.1 输入

  • layoutCandidate
    • breaks: number[]
    • lines: Array<{ start; end; text; width; tokenCount; charCount }>
  • lang: 'TC' | 'EN'
  • context: 'APP' | 'WIDGET'
  • config
    • weights: Record<string, number>(首版由内置默认值生成)
    • idealWidthRatio: { APP: 0.90; WIDGET: 0.95 }(来自文档 8.3A
    • ellipsisToken: "…"(用于溢出模块;本模块只保留配置入口)
    • particleWhitelistTC: string[](语尾语助词白名单;惩罚减半)
  • lexicons(首版使用内置最小词表;允许调用方覆盖)

4.2 输出

  • scoredLayout
    • score: number
    • flags: { emotionSplit?: boolean; overflowed?: boolean; fallback?: boolean }
    • tieKey: Array<number | string>
    • scoreBreakdown?: { total: number; terms: Array<{ key: string; delta: number; detail?: any }> }

5. 实现步骤(按落地顺序)

5.1 Phrase 匹配与 span 预计算(连续 token 完全匹配)

实现口径(必须):

  • EN对输入 tokens 使用 core-contractnormalizeWhitespace + tokenizeEN
  • TC对输入 tokens 使用 grapheme-segmentation 的 clusters
  • 对 phrase
    • EN先 normalizeWhitespace再按空格切分为词序列不做 substring
    • TC按 grapheme clusters 切分
  • 匹配方式:在 token 序列中寻找 连续区间 完全匹配
  • 输出 spans{ start, end, length }end 为半开区间)

5.2 评分项(严格按 10.1 顺序累计)

按文档 10.1 固定顺序计算并记录 breakdown

  1. 情绪短语/受保护短语拆分惩罚(最高优先)
    • 若某 phrase span 被 breaks 拆到不同的行:
      • score -= P_*
      • 并按方案 A 追加惩罚:score -= phraseLength(或 k * phraseLengthk 写死为 1确保确定性
      • flagsemotionSplit=true(或 protectedSplit 也可复用同一 flag/额外字段,需写死)
  2. 行长度ideal/over/too short
    • idealWidth = availableWidth * idealWidthRatio[context]availableWidth 由上游传入)
    • 以 width-based 作为主要项:score += -abs(line.width - idealWidth)
    • overMaxLen/tooShort 按配置阈值与权重惩罚(首版可先按文档默认区间落地)
  3. Widow / 单字行惩罚
  4. TC 标点断点奖励
  5. Shift/Accum/Self 断点奖励(含 EN 行首/行尾感知)
  6. 尾行过短惩罚SHORT_LASTLINE

注意:必须保留每一项的确定性记录,不允许“覆盖”前序裁决结果(见文档 10.1A)。

5.3 Debug Top-3 输出

  • 输出结构:ScoreBreakdown = { total, terms[] }
  • debug=true 时只保留 Top-3 term但排序规则必须按优先级10.1 顺序),同优先级再按出现顺序稳定排序

5.4 tieKey 构造(按 11 节固定顺序)

按文档顺序生成 tieKey比较时逐项比较

  1. emotionSplit=false 优先(可用 0/1
  2. overflowed=false 优先
  3. lastLineWidth 更大优先(因此 tieKey 可存 -lastLineWidth 或比较时反向)
  4. 行宽分布更均匀优先(maxWidth-minWidth 更小优先)
  5. 断点更接近理想切分点优先(距离和更小)
  6. breaks 字典序更靠前优先(使用 core-contract 的 breaks 比较规则)

6. 测试计划Vitest

6.1 Phrase 匹配

  • EN确保全词等值匹配but, 命中 butrebuttal 不命中)
  • TCemoji/组合字符的 phrase 不被拆(依赖 grapheme-segmentation 已覆盖)

6.2 评分顺序与 breakdown

  • 构造触发 EmotionSplit 的 layout断言 breakdown 中第一项为 EMOTION_SPLIT或对应 key且其记录不会被后续项覆盖

6.3 tieKey 确定性

  • 构造同分 layout验证 tieKey 顺序能稳定选出同一个最优

7. 完成定义DoD

  • 整数评分全链路跑通(无浮点)
  • phrase 连续 token 完全匹配实现完毕EN/TC
  • scoring 按 10.1 顺序累计,并输出可解释 breakdown
  • debug Top-3 按优先级输出
  • tieKey 按 11 节固定顺序构造与比较
  • 单测覆盖匹配、评分顺序、tieKey 稳定性