Coverage for src/lilbee/wiki/stats.py: 100%

55 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-08-14 11:46 +0000

1"""Per-run counters for what the wiki quality gates did. 

2 

3One :class:`BuildStats` is threaded through a build, update or synthesize run 

4and reported in its summary, so a regression in the citation or faithfulness 

5gate is visible per run instead of only in the logs. Recording never changes a 

6gate's decision. 

7""" 

8 

9from __future__ import annotations 

10 

11from dataclasses import dataclass, field 

12from typing import TypedDict 

13 

14 

15class BuildStatsDict(TypedDict): 

16 """Serializable snapshot of :class:`BuildStats`, derived rates included.""" 

17 

18 pages_generated: int 

19 pages_published: int 

20 pages_drafted: int 

21 pending_markers: int 

22 citations_rendered: int 

23 citations_dropped_unverified: int 

24 verified_by_page: dict[str, int] 

25 publish_rate: float 

26 citation_verify_rate: float 

27 

28 

29@dataclass 

30class BuildStats: 

31 """What one wiki run's quality gates did. 

32 

33 ``pages_generated`` is every page written to disk: ``pages_published`` 

34 (landed in a content subdir) plus ``pages_drafted`` (routed to ``drafts/`` 

35 by the faithfulness or drift gate). ``pending_markers`` counts sections 

36 that produced no page at all and left a PENDING marker under ``drafts/``, 

37 from a parse failure or a concept-slug collision. 

38 ``citations_dropped_unverified`` counts parsed footnotes that reached no 

39 page: excerpt not found in the source chunks, or no source to attribute 

40 them to. Footnotes skipped for citing a wiki page are in neither count. 

41 ``verified_by_page`` maps a published page's ``wiki_source`` to the number 

42 of citations it rendered. 

43 """ 

44 

45 pages_generated: int = 0 

46 pages_published: int = 0 

47 pages_drafted: int = 0 

48 pending_markers: int = 0 

49 citations_rendered: int = 0 

50 citations_dropped_unverified: int = 0 

51 verified_by_page: dict[str, int] = field(default_factory=dict) 

52 

53 @classmethod 

54 def ensure(cls, stats: BuildStats | None) -> BuildStats: 

55 """Return *stats*, or a throwaway collector when the caller passed none.""" 

56 return cls() if stats is None else stats 

57 

58 @property 

59 def publish_rate(self) -> float: 

60 """Fraction of written pages that published rather than drafted.""" 

61 if not self.pages_generated: 

62 return 0.0 

63 return self.pages_published / self.pages_generated 

64 

65 @property 

66 def citation_verify_rate(self) -> float: 

67 """Fraction of counted citations that rendered on a page.""" 

68 total = self.citations_rendered + self.citations_dropped_unverified 

69 if not total: 

70 return 0.0 

71 return self.citations_rendered / total 

72 

73 def record_published(self, wiki_source: str, verified: int) -> None: 

74 """Count a page that landed in a content subdir with *verified* citations.""" 

75 self.pages_generated += 1 

76 self.pages_published += 1 

77 self.verified_by_page[wiki_source] = verified 

78 

79 def record_drafted(self) -> None: 

80 """Count a page the faithfulness or drift gate routed to ``drafts/``.""" 

81 self.pages_generated += 1 

82 self.pages_drafted += 1 

83 

84 def record_pending_marker(self) -> None: 

85 """Count a section left as a PENDING marker under ``drafts/``.""" 

86 self.pending_markers += 1 

87 

88 def record_citations(self, rendered: int, dropped: int) -> None: 

89 """Count one page's verified and rejected citation records.""" 

90 self.citations_rendered += rendered 

91 self.citations_dropped_unverified += dropped 

92 

93 def as_dict(self) -> BuildStatsDict: 

94 """Snapshot for the summary dicts CLI, HTTP and MCP return.""" 

95 return BuildStatsDict( 

96 pages_generated=self.pages_generated, 

97 pages_published=self.pages_published, 

98 pages_drafted=self.pages_drafted, 

99 pending_markers=self.pending_markers, 

100 citations_rendered=self.citations_rendered, 

101 citations_dropped_unverified=self.citations_dropped_unverified, 

102 verified_by_page=dict(self.verified_by_page), 

103 publish_rate=self.publish_rate, 

104 citation_verify_rate=self.citation_verify_rate, 

105 ) 

106 

107 def summary_line(self) -> str: 

108 """One-line human summary of the run.""" 

109 return format_summary_line(self.as_dict()) 

110 

111 

112def format_summary_line(stats: BuildStatsDict) -> str: 

113 """One-line human summary of a run, for the CLI and ``wiki/log.md``.""" 

114 total = stats["citations_rendered"] + stats["citations_dropped_unverified"] 

115 return ( 

116 f"{stats['pages_published']} published, {stats['pages_drafted']} drafted, " 

117 f"{stats['pending_markers']} markers, " 

118 f"{stats['citations_rendered']}/{total} citations verified" 

119 )