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
« 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.
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"""
9from __future__ import annotations
11from dataclasses import dataclass, field
12from typing import TypedDict
15class BuildStatsDict(TypedDict):
16 """Serializable snapshot of :class:`BuildStats`, derived rates included."""
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
29@dataclass
30class BuildStats:
31 """What one wiki run's quality gates did.
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 """
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)
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
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
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
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
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
84 def record_pending_marker(self) -> None:
85 """Count a section left as a PENDING marker under ``drafts/``."""
86 self.pending_markers += 1
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
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 )
107 def summary_line(self) -> str:
108 """One-line human summary of the run."""
109 return format_summary_line(self.as_dict())
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 )