Coverage for src\genesis\qa\validator.py: 100%

27 statements  

« prev     ^ index     » next       coverage.py v7.15.4, created at 2026-08-26 14:20 +0800

1"""QA 校验:章节级/文档级评估(Phase 5)。""" 

2from __future__ import annotations 

3 

4from genesis.eval.scorer import ChapterScorer, EvalReport, ChapterArtifact 

5from genesis.qa.report import QAReport 

6from genesis.writer.models import ChapterContent 

7 

8 

9class QAValidator: 

10 def __init__(self, scorer: ChapterScorer | None = None) -> None: 

11 self.scorer = scorer or ChapterScorer() 

12 

13 def _to_artifact(self, content: ChapterContent, expected_language: str = "") -> ChapterArtifact: 

14 """把 ChapterContent 转换为评分器所需的 ChapterArtifact(聚合正文与来源 URI)。 

15 

16 expected_language:期望输出语言("zh"/"ja";空=不可验证,维度记满分)。 

17 blocks 保留 (type, text) 供语言维度排除 heading/table。 

18 """ 

19 text = "".join(b.text or "" for b in content.blocks) 

20 source_uris: list[str] = [] 

21 for b in content.blocks: 

22 source_uris.extend(b.source_uris) 

23 return ChapterArtifact( 

24 chapter_id=content.chapter_id, 

25 text=text, 

26 source_uris=source_uris, 

27 template_sections_expected=[], 

28 expected_language=expected_language, 

29 blocks=[(b.type, b.text or "", b.caption or "") for b in content.blocks], 

30 ) 

31 

32 def validate_chapter( 

33 self, content: ChapterContent, structured_source, expected_language: str = "" 

34 ) -> tuple[bool, EvalReport]: 

35 report = self.scorer.score([self._to_artifact(content, expected_language)], structured_source) 

36 passed = content.chapter_id not in report.failed_chapters 

37 return passed, report 

38 

39 def validate_document( 

40 self, contents: list[ChapterContent], structured_source, expected_language: str = "" 

41 ) -> list[tuple[bool, EvalReport]]: 

42 return [self.validate_chapter(c, structured_source, expected_language) for c in contents] 

43 

44 def validate_doc( 

45 self, contents: list[ChapterContent], structured_source, expected_language: str = "" 

46 ) -> QAReport: 

47 results = self.validate_document(contents, structured_source, expected_language) 

48 passed_flags = [ok for ok, _ in results] 

49 reports = [rep for _, rep in results] 

50 failed = [c.chapter_id for c, ok in zip(contents, passed_flags) if not ok] 

51 overall = sum(r.total_score for r in reports) / len(reports) if reports else 0.0 

52 summary = "全部章节通过" if all(passed_flags) else f"{len(failed)} 章未通过: {failed}" 

53 return QAReport( 

54 passed=all(passed_flags), 

55 overall_score=round(overall, 4), 

56 per_chapter=reports, 

57 failed_chapters=failed, 

58 summary=summary, 

59 )