更新换行算法和APP-PUSH
This commit is contained in:
179
spec_kit/Text Wrap/modules/scoring-tiebreak/plan.md
Normal file
179
spec_kit/Text Wrap/modules/scoring-tiebreak/plan.md
Normal file
@@ -0,0 +1,179 @@
|
||||
# scoring-tiebreak(技术计划)
|
||||
|
||||
## 1. 计划目标
|
||||
|
||||
基于 `spec.md` 与 `设计说明文档/文档换行算法.md v1.2.1`,在客户端实现可解释的评分模型与确定性 tie-break,保证:
|
||||
|
||||
- **整数评分**:所有评分项使用整数,避免浮点误差
|
||||
- **权重写死客户端**:首版权重/词表内置在客户端代码(后续如需灰度,用 `configVersion` 管理)
|
||||
- **短语匹配口径固定**:emotionPhrases/protectedPhrases 必须是“连续 token 完全匹配”
|
||||
- **更长短语优先**:采用方案 A——在触发拆分惩罚时按“被拆分短语长度”追加惩罚(确定性)
|
||||
- **debug 可解释**:输出 score breakdown;debug=true 时输出 Top-3,按规则优先级排序(不是按 |delta|)
|
||||
- **tieKey 固定**:严格按文档第 11 节顺序构造 tieKey;Widget 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 规则):
|
||||
- shift:`but/yet/so`(and 轻量可选)
|
||||
- accum:`already/still/even/just/really`
|
||||
- self:`you/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.ts`(Layout/LineSegment/ScoreBreakdown)
|
||||
- `weights.ts`(权重常量,写死)
|
||||
- `lexicons.ts`(最小词表,写死)
|
||||
- `phraseMatch.ts`(连续 token 完全匹配 + span 预计算)
|
||||
- `score.ts`(按 10.1 顺序累计分数)
|
||||
- `tieKey.ts`(按 11 节构造 tieKey)
|
||||
- `debug.ts`(Top-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-contract` 的 `normalizeWhitespace + 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 * phraseLength`,k 写死为 1,确保确定性)
|
||||
- flags:`emotionSplit=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,` 命中 `but`;`rebuttal` 不命中)
|
||||
- TC:emoji/组合字符的 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 稳定性
|
||||
|
||||
Reference in New Issue
Block a user