Skip to content

Commit 854c0af

Browse files
committed
fix(refine): keep diff badge inside heading node
1 parent 4c52cdd commit 854c0af

3 files changed

Lines changed: 75 additions & 9 deletions

File tree

‎app/src/components/RefineWorkbench.test.tsx‎

Lines changed: 60 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -142,11 +142,66 @@ describe("RefineWorkbench 行级染色与差异视图(批 3 / 问题11)", ()
142142
const addedRow = screen.getByText("精修内容");
143143
expect(addedRow.style.color).toBe("rgb(4, 120, 87)");
144144
expect(addedRow.style.backgroundColor).toBe("rgb(236, 253, 245)");
145-
// Assert:unchanged 标题行不染色(左栏无徽标污染文本——右侧标题带徽标
146-
// 其 textContent 为「标题修改」,精确匹配仅命中左栏行)
147-
expect(screen.getByText("标题").style.backgroundColor).toBe("");
148-
// Assert:章节徽标与统计保留(差异显示不替代既有章节级标注)
149-
expect(screen.getByText("修改")).toBeTruthy();
145+
// Assert:unchanged 标题行不染色。⚠️ 必须**按栏定位**:修掉装饰徽标的插入位缺陷后,
146+
// 右栏同名标题的直接文本节点也是「标题」,裸 `getByText("标题")` 会同时命中两栏
147+
// (旧断言之所以绿,正是因为缺陷把右栏文本节点污染成「标题<」——判据本身依赖缺陷)。
148+
const leftPane = screen.getByText("📄 规则版").nextElementSibling as HTMLElement;
149+
expect(leftPane.querySelector("h2")?.style.backgroundColor).toBe("");
150+
// Assert:章节徽标保留(差异显示不替代既有章节级标注),且**落在标题节点内部**、
151+
// 标题文本逐字无损 —— T7「游离 `<` / `/h3>`」回归判据。旧判据只断言
152+
// `getByText("修改")`:徽标被塞进 `<` 与 `/` 之间时该 span 照样命中 ⇒ 无法失败。
153+
const rightPane = screen.getByText("✨ 精修版").nextElementSibling as HTMLElement;
154+
const badgeHeading = screen.getByText("修改").closest("h2,h3,h4");
155+
expect(badgeHeading).not.toBeNull();
156+
expect(badgeHeading?.textContent).toBe("标题修改");
157+
expect(badgeHeading?.querySelectorAll("span").length).toBe(1);
158+
// Assert:栏内无闭合标签残片(错位插入会把 `</h2>` 拆成游离文本 `<` + `/h2>`)
159+
expect(rightPane.textContent).not.toContain("<");
160+
expect(rightPane.textContent).not.toContain("/h");
161+
});
162+
163+
it("`##` 级标题(渲染为 <h3>)的徽标同在标题内——T7 实测「逻辑<修改/h3>」回归", async () => {
164+
// Arrange:T7 真渲染截图里的最小复现(`##` 级中文标题 + modified 章节)。
165+
// 旧实现的插入点是「标题匹配串末端 − 2」⇒ 落在闭合标签的 `<` 与 `/` 之间。
166+
const h3Stub: WorkbenchData = {
167+
ruleMarkdown: "## 一、底妆的底层逻辑\n旧:拍开粉底",
168+
refinedMarkdown: "## 一、底妆的底层逻辑\n新:少量多次按压",
169+
sections: [{
170+
heading: "一、底妆的底层逻辑",
171+
status: "modified",
172+
removed_lines: ["旧:拍开粉底"],
173+
added_lines: ["新:少量多次按压"],
174+
}],
175+
stats: { added: 1, removed: 1, unchanged: 0 },
176+
meta: { costYuan: null, model: "test-model", slices: 1, mergedFrom: null },
177+
};
178+
invokeMock.mockImplementation(async (cmd: string) => {
179+
if (cmd === "diff_markdown_ops") {
180+
return {
181+
ops: [
182+
{ unchanged: "## 一、底妆的底层逻辑" },
183+
{ removed: "旧:拍开粉底" },
184+
{ added: "新:少量多次按压" },
185+
],
186+
added: 1,
187+
removed: 1,
188+
};
189+
}
190+
if (cmd === "diff_markdown_sections") return h3Stub.sections;
191+
return h3Stub;
192+
});
193+
// Act
194+
render(<RefineWorkbench sessionId={5} onClose={vi.fn()} />);
195+
// Assert:徽标挂进 `<h3>` 内部,标题文本 = 标题 + 徽标(错位时是「…逻辑<修改/h3>」)
196+
const badge = await screen.findByText("修改");
197+
const heading = badge.closest("h3");
198+
expect(heading).not.toBeNull();
199+
expect(heading?.textContent).toBe("一、底妆的底层逻辑修改");
200+
// Assert:右栏(精修版)整栏无标签残片——游离 `<` 与 `/h3>` 是缺陷的直接表象
201+
const rightPane = screen.getByText("✨ 精修版").nextElementSibling as HTMLElement;
202+
expect(rightPane.textContent).toContain("一、底妆的底层逻辑修改");
203+
expect(rightPane.textContent).not.toContain("<");
204+
expect(rightPane.textContent).not.toContain("/h3");
150205
});
151206

152207
it("差异视图切换:单列三态(−删除线红 / +新增绿 / 灰共有),可切回并排", async () => {

‎app/src/components/RefineWorkbench.tsx‎

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,16 @@ const PANE_MAX_H = "calc(85vh - 200px)";
6969
* 字节一致——组件内不再持有第二份逐行渲染实现)。
7070
*/
7171

72-
/** 按 sections 插入 diff 徽标 */
72+
/**
73+
* 按 sections 在章节标题**末尾、闭合标签之前**插入 diff 徽标。
74+
*
75+
* @ai-context: 徽标是章节级标注(新增/已删除/修改),必须落在标题元素**内部**——
76+
* 插入位算错 1 个字符就会塞进 `<` 与 `/` 之间,渲染出游离的 `<` 与 `/h3>`
77+
* 文本(T7 真渲染截图实测 `逻辑<修改/h3>`)。故插入点由 `searchPattern`
78+
* 末尾的闭合标签长度反推,不做裸算术(`- 2` 这类常数是缺陷源)。
79+
* @ai-context: 边界:`sections` 里的 heading 在 md 中不存在(后端分组与文本漂移)时
80+
* `indexOf` 返回 -1 ⇒ 跳过该徽标、正文照常渲染(不抛错、不静默插错位置)。
81+
*/
7382
function decorateRefined(md: string, sections: WorkbenchData["sections"]): string {
7483
let result = md;
7584
for (const sec of sections) {
@@ -79,10 +88,12 @@ function decorateRefined(md: string, sections: WorkbenchData["sections"]): strin
7988
: sec.status === "removed"
8089
? `<span style="display:inline-block;font-size:10px;background:#fef2f2;color:#b91c1c;border-radius:4px;padding:0 6px;margin-left:6px">已删除</span>`
8190
: `<span style="display:inline-block;font-size:10px;background:#fffbeb;color:#b45309;border-radius:4px;padding:0 6px;margin-left:6px">修改</span>`;
82-
const searchPattern = `>${escapeHtml(sec.heading)}</h`;
91+
const closing = "</h";
92+
const searchPattern = `>${escapeHtml(sec.heading)}${closing}`;
8393
const idx = result.indexOf(searchPattern);
8494
if (idx >= 0) {
85-
const insertAt = idx + searchPattern.length - 2;
95+
// 插入点 = 闭合标签 `<` 之前(`searchPattern` 以 `</h` 结尾 ⇒ 末端 3 字符是闭合标签)
96+
const insertAt = idx + searchPattern.length - closing.length;
8697
result = result.slice(0, insertAt) + badge + result.slice(insertAt);
8798
}
8899
}

‎docs/standards/line-limit-exemptions.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,12 +41,12 @@
4141
| app/src-tauri/src/db_sop.rs | 490 | v0.20.3(REQ-296/297)SOP 三表数据域(模板/run/步骤/保鲜/聚合)+ 审查加固(保链更新/幂等守卫/步数计数)内聚 | 若再增长:run 执行族拆至 db_sop_run.rs |
4242
| app/src-tauri/src/live_session_persist.rs | 488 | 定稿落库域(persist_final/digest_merged/handle_final_event)+ P2 flush_tail_and_persist(停止/暂停共用尾句落库)内聚 | 若再增长:flush_tail_and_persist 与 digest_merged 拆至 live_session_persist_tail.rs |
4343
| app/src-tauri/src/db_sessions.rs | 482 | 会话仓储;批 4 delete_sessions_batch + 审查修复轮 3(transaction() 改造)——2026-09-09 实测纠偏(登记值 394 过期) | 若再增长:recent_ocr_texts 等建议查询拆至 db_sessions_queries.rs |
44+
| app/src/components/RefineWorkbench.tsx | 481 | 并排双栏(规则版 + 精修版)+ 章节级 diff 高亮 + 同步滚动 +采纳/重新生成/放弃。批 3(问题11):行级三态染色(removed删除线红/added 绿)与 并排/差异 单列视图切换——数据源为整篇有序行级 diff(diff_markdown_ops,见 load 内 Why)。数据源:非只读带 taskResult(采纳前内存结果)→ refine_workbench回传 result(消除未落库右侧恒空);无 taskResult →refine_workbench(后端兜底未采纳任务/已落库笔记,重启可恢复);只读(VersionPanel)→ ruleMd/refinedMd 透传。(自动摘取,待细化) | 若再增长:按职责拆分 |
4445
| app/src-tauri/src/commands_ai_enrich.rs | 479 | v0.8.0 M3(REQ-142)+ F1/F2/F3:知识补充命令域(九子项校验/预估/异步任务/采纳/撤销 + 配额去重门控 + 成本硬拦截 + 任务落库)——与精修共用任务注册表上下文,命令域内聚;2026-09 修复(章节目录注入/逐块审查回执)微增 | 若再增长:门控/拦截拆至 commands_ai_enrich_gate.rs |
4546
| app/src-tauri/src/app_commands.rs | 478 | 宏约束下的**唯一**注册点:**312 条** `generate_handler!` 条目是**数据不是逻辑**(批 1 删除批收口实测:334 → 312,−22;此数由本行人工维护,`--write` 只刷新数字列) —— `Invoke` 按值语义使分域组合不可行(见文件头),拆成多份只会在域间新增「命令名 → 域」路由漂移面;一致性由 `scripts/check-command-registry.mjs` 机器门禁守 | 不拆(数据文件);新增命令时同步条目,由门禁强制 |
4647
| app/src-tauri/src/region_tracker.rs | 478 | v0.4.0 M2(REQ-037)起:ROI 跟踪状态机(播放区域检测/锁定聚簇/重扫/前台切换冻结)+ 纯函数单测内联;与 RoiTracker 状态强耦合 | 若再增长:lock_roi/prior_roi 纯函数拆至 region_lock.rs |
4748
| app/src-tauri/src/layout_analyzer.rs | 475 | v0.5.0 M3(REQ-047):规则版版面分析(行/列投影 + 表格线检测 + 区域分类启发式)内聚于同一分类管线;审查加固(公式启发 + 低信息纯色方差滤除) | 若再增长:区域分类启发式拆至 layout_classify.rs |
4849
| app/src-tauri/src/commands_session_note.rs | 472 | 既有登记 314 为过期快照——2026-09-06 实测纠偏(v0.20 装载合成/web 分支/批量 inner 扩展后 +144) | 若再增长:convert_to_note 拆至 commands_session_note_convert.rs |
49-
| app/src/components/RefineWorkbench.tsx | 470 | 并排双栏(规则版 + 精修版)+ 章节级 diff 高亮 + 同步滚动 +采纳/重新生成/放弃。批 3(问题11):行级三态染色(removed删除线红/added 绿)与 并排/差异 单列视图切换——数据源为整篇有序行级 diff(diff_markdown_ops,见 load 内 Why)。数据源:非只读带 taskResult(采纳前内存结果)→ refine_workbench回传 result(消除未落库右侧恒空);无 taskResult →refine_workbench(后端兜底未采纳任务/已落库笔记,重启可恢复);只读(VersionPanel)→ ruleMd/refinedMd 透传。(自动摘取,待细化) | 若再增长:按职责拆分 |
5050
| app/src-tauri/src/streaming_asr.rs | 466 | sherpa-onnx OnlineRecognizer(Zipformer transducer 中英双语流式),流程移植自原项目 streamingAsr.ts(生产调参验证):partial 节流(≥150ms + 文本变化才推)→ 端点断句 → final →重建流;静音块隔块喂入(静音期低 CPU);停止时尾句 flush 去重。(自动摘取,待细化) | 若再增长:按职责拆分 |
5151
| app/src/components/KnowledgeCanvasView.tsx | 466 | v0.13.8 画布主视图(RF 装配/拖拽防抖保存/视口持久化/自动排列);v0.13.9 根卡 + 接线方向动态化;v0.14.1 布局/连线下拉 + 偏好读写(+72 行)+ 审查修复(布局 effect 原子化拆分建边 effect/prefsLoaded 控件门控/hasCore 统一 +31 行)——RF 状态与持久化编排内聚(元素构建已拆至 canvasElements/layout* 纯函数) | 若再增长:偏好读写与下拉拆至 useCanvasPrefs.ts;位置持久化拆至 useCanvasPositions.ts |
5252
| app/src-tauri/src/streaming_asr_tests.rs | 462 | 流式 ASR 测试域(端点处理/静音判定/段切分回归)单模块 #[path] 挂载 | 若再增长:端点处理组拆至 streaming_endpoint_tests.rs |

0 commit comments

Comments
 (0)