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

60 statements  

« prev     ^ index     » next       coverage.py v7.13.4, created at 2026-05-15 20:55 +0000

1"""Shared wiki utilities: frontmatter parsing, constants, page targets.""" 

2 

3from __future__ import annotations 

4 

5from dataclasses import dataclass 

6from enum import StrEnum 

7from pathlib import Path 

8from typing import Any 

9 

10import yaml 

11 

12MIN_CLUSTER_SOURCES = 3 # minimum unique sources for a synthesis page 

13 

14 

15class WikiSubdir(StrEnum): 

16 """Filesystem subdirectory under ``$data_root/$wiki_dir/``.""" 

17 

18 SUMMARIES = "summaries" 

19 SYNTHESIS = "synthesis" 

20 CONCEPTS = "concepts" 

21 ENTITIES = "entities" 

22 DRAFTS = "drafts" 

23 ARCHIVE = "archive" 

24 

25 

26class WikiPageType(StrEnum): 

27 """Kind of wiki page. Values are used as frontmatter/API labels.""" 

28 

29 SUMMARY = "summary" 

30 SYNTHESIS = "synthesis" 

31 CONCEPT = "concept" 

32 ENTITY = "entity" 

33 DRAFT = "draft" 

34 ARCHIVE = "archive" 

35 

36 

37WIKI_CONTENT_SUBDIRS: tuple[WikiSubdir, ...] = ( 

38 WikiSubdir.SUMMARIES, 

39 WikiSubdir.SYNTHESIS, 

40 WikiSubdir.CONCEPTS, 

41 WikiSubdir.ENTITIES, 

42) 

43 

44WIKI_DISABLED_ERROR = "wiki not enabled" 

45 

46# PENDING-marker keyword phrases written into ``drafts/<slug>.md`` by the 

47# batched generator and matched by the drafts-review surface. Centralized 

48# here so the gen-side writer and the drafts-side reader agree on the 

49# exact wording. Changing a keyword here requires updating any cached 

50# markers on disk (one-shot find -delete or a regen). 

51PENDING_MARKER_KEYWORD_PARSE = "PENDING: batch parse failed" 

52PENDING_MARKER_KEYWORD_COLLISION = "PENDING: concept slug collision" 

53 

54 

55class PendingKind(StrEnum): 

56 """Reason a wiki draft is in ``drafts/`` instead of a published page. 

57 

58 The string value is what lands in the ``pending_kind`` YAML 

59 frontmatter field and is surfaced verbatim through 

60 ``DraftInfo.pending_kind`` to CLI / HTTP / MCP callers. 

61 StrEnum members serialise as their string value, so the YAML/JSON 

62 round-trip stays a plain string. ``DRIFT`` is display-only, never 

63 written to disk, but exposed so consumers don't hard-code ``"drift"``. 

64 """ 

65 

66 PARSE = "parse" 

67 COLLISION = "collision" 

68 DRIFT = "drift" 

69 

70 

71class WikiLogAction(StrEnum): 

72 """Verbs written into ``wiki/log.md`` audit-trail entries. 

73 

74 Distinct from WIKI_STATUS_* (which are result statuses returned to 

75 CLI/MCP/HTTP callers); these label internal audit-trail rows. 

76 """ 

77 

78 GENERATED = "generated" 

79 BUILD = "build" 

80 INGEST = "ingest" 

81 LINT = "lint" 

82 

83 

84SUBDIR_TO_TYPE: dict[str, WikiPageType] = { 

85 WikiSubdir.SUMMARIES.value: WikiPageType.SUMMARY, 

86 WikiSubdir.SYNTHESIS.value: WikiPageType.SYNTHESIS, 

87 WikiSubdir.CONCEPTS.value: WikiPageType.CONCEPT, 

88 WikiSubdir.ENTITIES.value: WikiPageType.ENTITY, 

89 WikiSubdir.DRAFTS.value: WikiPageType.DRAFT, 

90 WikiSubdir.ARCHIVE.value: WikiPageType.ARCHIVE, 

91} 

92 

93# One source of truth for sidebar-style headings keyed by page type. 

94# Consumed by ``wiki/index.py`` and the TUI sidebar via 

95# ``cli/tui/messages.WIKI_TYPE_HEADINGS``. 

96WIKI_TYPE_HEADINGS: dict[WikiPageType, str] = { 

97 WikiPageType.CONCEPT: "Concepts", 

98 WikiPageType.ENTITY: "Entities", 

99 WikiPageType.SUMMARY: "Source Summaries", 

100 WikiPageType.SYNTHESIS: "Synthesis", 

101} 

102 

103 

104@dataclass(frozen=True) 

105class PageTarget: 

106 """Grouping of page location fields for wiki generation.""" 

107 

108 wiki_root: Path 

109 subdir: str 

110 slug: str 

111 wiki_source: str 

112 page_type: str 

113 label: str 

114 

115 

116def parse_frontmatter(text: str) -> dict[str, Any]: 

117 """Extract YAML frontmatter fields from a wiki page string. 

118 Uses line-by-line scanning so ``---`` inside YAML content is not 

119 mistaken for the closing delimiter. 

120 """ 

121 lines = text.splitlines() 

122 if not lines or lines[0].strip() != "---": 

123 return {} 

124 end_idx: int | None = None 

125 for i in range(1, len(lines)): 

126 if lines[i].strip() == "---": 

127 end_idx = i 

128 break 

129 if end_idx is None: 

130 return {} 

131 block = "\n".join(lines[1:end_idx]) 

132 try: 

133 return yaml.safe_load(block) or {} 

134 except yaml.YAMLError: 

135 return {}