Skip to content

Commit 84c867f

Browse files
committed
fix(notes): 审查修复——AI复核补结构渲染+词汇表0上限语义+零回归补测+死代码清理
1 parent 4dbafdf commit 84c867f

4 files changed

Lines changed: 71 additions & 28 deletions

File tree

‎app/src-tauri/src/commands_ai.rs‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,11 @@ pub async fn review_text_filter(
242242
};
243243
// ③ 门控:全局开关 + 用户授权 + 有候选(三者缺一 → 纯规则输出)
244244
if !cfg.enabled || !authorized || boundary.is_empty() {
245+
// v0.7.6(REQ-177/178,审查修复):结构渲染与预览/落库同口径——
246+
// AI 复核预览与一键落库输出一致(REQ-081 三出口契约)
247+
crate::commands_session_note::apply_note_structure(
248+
&mut result, &db, &session, &segments, &ocr_blocks, &purify_env,
249+
);
245250
return Ok(TextFilterReview { result, ai: meta, decisions: Vec::new() });
246251
}
247252
let adapter = AiTextFilterAdapter::new(cfg.clone());
@@ -351,6 +356,12 @@ pub async fn review_text_filter(
351356
meta.sent = sent;
352357
meta.quota_hit = quota_hit;
353358
meta.error = error;
359+
// v0.7.6(REQ-177/178,审查修复):AI 判定全部应用后叠加结构渲染——
360+
// 否则复核预览丢失章节标题/词汇表块,与一键落库输出不一致(REQ-081
361+
// 单一管线三出口契约;AI 判定在结构层之前,结构层不消费判定结果)
362+
crate::commands_session_note::apply_note_structure(
363+
&mut result, &db, &session, &segments, &ocr_blocks, &purify_env,
364+
);
354365
Ok(TextFilterReview { result, ai: meta, decisions: decisions_all })
355366
})
356367
.await

‎app/src-tauri/src/note_filter_golden_tests.rs‎

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -389,3 +389,35 @@ fn session31_structure_glossary_block() {
389389
let json = serde_json::to_string(&result.stats).expect("stats serializable");
390390
assert!(json.contains("glossary_terms"));
391391
}
392+
393+
/// 会话31 实证:结构渲染"配置开但无结构数据"路径逐字节零回归(REQ-179 护栏)。
394+
///
395+
/// @ai-context: 审查补测:默认配置(chapter_headings/glossary_block 均开)下
396+
/// 无章节边界/无术语候选——渲染层仍会重建 markdown(与净化管线
397+
/// 同阈值同组装口径),必须与未接结构层前的 v0.7.5 输出逐字节
398+
/// 一致(口播档案/无画面内容会话的最常见路径)。
399+
#[test]
400+
fn session31_structure_empty_data_is_byte_identical() {
401+
// Arrange:真实管线产物(口播样例会话——无章节无术语)
402+
let segments = vec![
403+
asr(1, 0, 5000, "那么项目的可行性研究就是非常重要的"),
404+
asr(2, 6000, 10000, "接下来我们看第三章"),
405+
];
406+
let mut result = run("测试", &segments, &[]);
407+
apply_session_warning(&mut result, "finished");
408+
refresh_screen_points(&mut result);
409+
let before = result.markdown.clone();
410+
// Act:默认配置结构渲染,但空章节/空术语(无结构数据——诚实降级路径)
411+
let _ = crate::structure_note::render_note_structure(
412+
&mut result,
413+
&[],
414+
&[],
415+
&[],
416+
&crate::structure_note::NoteStructureConfig::default(),
417+
);
418+
// Assert:markdown 逐字节一致;统计全零(无结构不产生统计)
419+
assert_eq!(result.markdown, before, "无结构数据必须逐字节零回归");
420+
assert_eq!(result.stats.chapters, 0);
421+
assert_eq!(result.stats.titled_chapters, 0);
422+
assert_eq!(result.stats.glossary_terms, 0);
423+
}

‎app/src-tauri/src/structure_note.rs‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ pub struct NoteStructureConfig {
2525
pub chapter_headings: bool,
2626
/// 词汇表块(## 词汇表;默认 true)
2727
pub glossary_block: bool,
28-
/// 词汇表条目上限(防噪音;默认 20)
28+
/// 词汇表条目上限(防噪音;默认 20;0 = 不输出词汇表块——审查修复语义)
2929
pub glossary_max_terms: usize,
3030
}
3131

@@ -124,11 +124,12 @@ pub fn render_note_structure(
124124
.collect();
125125
let mut md = crate::concat::assemble_markdown(&result.title, &lines, &result.ocr_points);
126126
// ④ 词汇表块(REQ-178):score 降序取前 max_terms;锚点=术语在 kept 段首次出现
127-
if config.glossary_block && !glossary.is_empty() {
127+
// (审查修复:max_terms=0 = 不输出块——原 .max(1) 会 0→1 条,与"0=关"直觉相悖)
128+
if config.glossary_block && !glossary.is_empty() && config.glossary_max_terms > 0 {
128129
let mut gs: Vec<&GlossaryCandidate> = glossary.iter().collect();
129130
gs.sort_by(|a, b| b.score.partial_cmp(&a.score).unwrap_or(std::cmp::Ordering::Equal));
130131
let mut rows: Vec<String> = Vec::new();
131-
for g in gs.iter().take(config.glossary_max_terms.max(1)) {
132+
for g in gs.iter().take(config.glossary_max_terms) {
132133
let anchor = first_occurrence_ms(&result.kept, &g.term);
133134
match anchor {
134135
Some(ms) => rows.push(format!(

‎app/src-tauri/src/structure_note_tests.rs‎

Lines changed: 24 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,11 @@
66
77
use crate::chapter_detect::ChapterBoundary;
88
use crate::glossary::GlossaryCandidate;
9-
use crate::note_filter::{FilterStats, FilteredItem, MergedItem, NoteFilterResult, PurifyEnv};
9+
use crate::note_filter::{FilterStats, NoteFilterResult};
1010
use crate::outline::OutlineEntry;
1111
use crate::purify_config::PurifyConfig;
1212
use crate::structure_note::{render_note_structure, NoteStructureConfig};
13-
use crate::types::{SessionScreen, SessionSegment};
13+
use crate::types::SessionSegment;
1414

1515
/// 构造会话段(净化后保留段形态)。
1616
fn seg(id: i64, start_ms: u64, end_ms: u64, text: &str) -> SessionSegment {
@@ -310,6 +310,28 @@ fn empty_glossary_produces_no_block() {
310310
assert_eq!(stats.glossary_terms, 0);
311311
}
312312

313+
#[test]
314+
fn glossary_max_terms_zero_disables_block() {
315+
// Arrange:上限 0 = 不输出词汇表块(审查修复语义——原 .max(1) 会输出 1 条)
316+
let mut result = base_result(vec![seg(1, 0, 5_000, "内容")]);
317+
let mut config = default_config();
318+
config.glossary_max_terms = 0;
319+
320+
// Act
321+
let stats = render_note_structure(
322+
&mut result,
323+
&[],
324+
&[],
325+
&[term("T1", 1, 0, 1.0)],
326+
&config,
327+
);
328+
329+
// Assert:无词汇表块、统计 0
330+
assert!(!result.markdown.contains("词汇表"));
331+
assert!(!result.markdown.contains("T1"));
332+
assert_eq!(stats.glossary_terms, 0);
333+
}
334+
313335
// ── 零回归护栏(REQ-179)──────────────────────────────────────
314336

315337
#[test]
@@ -391,26 +413,3 @@ fn structure_config_roundtrips_json() {
391413
assert!(parsed.structure.chapter_headings);
392414
assert!(parsed.structure.glossary_block);
393415
}
394-
395-
// ── 辅助:PurifyEnv 构造器(防 unused 警告——下游接线将使用)────
396-
#[allow(dead_code)]
397-
fn env() -> PurifyEnv {
398-
PurifyEnv {
399-
config: PurifyConfig::default(),
400-
symbol: crate::symbol_normalize::SymbolNormalizeConfig::default(),
401-
corrections: crate::ocr_correction::OcrCorrectionTable::default(),
402-
}
403-
}
404-
405-
// 防 unused:SessionScreen 引用占位(结构渲染不消费屏对象,接线层用)
406-
#[allow(dead_code)]
407-
fn _screen_placeholder() -> Vec<SessionScreen> {
408-
Vec::new()
409-
}
410-
411-
// 防 unused:MergedItem/FilteredItem 字段完整性验证(与 FilterStats 同批序列化)
412-
#[allow(dead_code)]
413-
fn _payload_placeholder() {
414-
let _ = MergedItem { segment_id: 1, into_segment_id: 2, text: "".into(), start_ms: 0 };
415-
let _ = FilteredItem { segment_id: 1, reason: crate::note_filter::FilterReason::Fragment, text: "".into(), start_ms: 0 };
416-
}

0 commit comments

Comments
 (0)