templating.py file-level

at main · View file ↗ · Intel ↗

History
1 files
1 commits
0 hotspots
0 🧊 dead
0 💥 blast risk
sha256:6 fix(ISR): default require_independent_second_reviewer to require Opera… · aaronrene · Sep 2, 2026
1 """Token substitution for governance doc templates — fixed key set, fail-closed."""
2
3 from __future__ import annotations
4
5 import re
6 from pathlib import Path
7
8 from adapters.config import OverseerConfig
9 from adapters.errors import ConfigError
10
11 TOKEN_PATTERN = re.compile(r"\{\{([a-z][a-z0-9_.]*)\}\}")
12
13 ALLOWED_TOKENS: frozenset[str] = frozenset(
14 {
15 "repo.name",
16 "repo.root_relative_docs",
17 "docs.handover",
18 "docs.roadmap",
19 "docs.coordination",
20 "docs.standing_decisions",
21 "docs.handover_path",
22 "docs.roadmap_path",
23 "docs.handover_title",
24 "docs.roadmap_title",
25 "docs.coordination_path",
26 "docs.standing_decisions_path",
27 "vcs.regime",
28 "vcs.canonical",
29 "vcs.git.remote",
30 "vcs.git.main_branch",
31 "vcs.git.mirror_branch",
32 "vcs.git.feature_branch_pattern",
33 "vcs.muse.staging_remote",
34 "vcs.muse.main_branch",
35 }
36 )
37
38
39 def build_token_map(config: OverseerConfig) -> dict[str, str]:
40 """Build the substitution map from a validated ``OverseerConfig``."""
41 from cli.docs_paths import join_docs_rel, normalize_docs_root
42
43 docs_root = normalize_docs_root(config.repo.root_relative_docs)
44 coordination = config.docs.coordination or ""
45 standing = config.docs.standing_decisions
46
47 def rel(doc: str) -> str:
48 return join_docs_rel(docs_root, doc)
49
50 return {
51 "repo.name": config.repo.name,
52 "repo.root_relative_docs": docs_root,
53 "docs.handover": config.docs.handover,
54 "docs.roadmap": config.docs.roadmap,
55 "docs.coordination": coordination,
56 "docs.standing_decisions": standing,
57 "docs.handover_path": rel(config.docs.handover),
58 "docs.roadmap_path": rel(config.docs.roadmap),
59 "docs.handover_title": config.docs.handover_title,
60 "docs.roadmap_title": config.docs.roadmap_title,
61 "docs.coordination_path": rel(coordination) if coordination else "",
62 "docs.standing_decisions_path": rel(standing),
63 "vcs.regime": config.vcs.regime,
64 "vcs.canonical": config.vcs.canonical,
65 "vcs.git.remote": config.vcs.git.remote,
66 "vcs.git.main_branch": config.vcs.git.main_branch,
67 "vcs.git.mirror_branch": config.vcs.git.mirror_branch or "",
68 "vcs.git.feature_branch_pattern": config.vcs.git.feature_branch_pattern,
69 "vcs.muse.staging_remote": config.vcs.muse.staging_remote or "",
70 "vcs.muse.main_branch": config.vcs.muse.main_branch or "",
71 }
72
73
74 def substitute_tokens(
75 text: str,
76 token_map: dict[str, str],
77 *,
78 fail_on_unknown: bool = True,
79 ) -> str:
80 """Replace ``{{token}}`` placeholders using only keys present in ``token_map``."""
81 unknown: set[str] = set()
82
83 def replacer(match: re.Match[str]) -> str:
84 key = match.group(1)
85 if key not in ALLOWED_TOKENS:
86 unknown.add(key)
87 return match.group(0)
88 if key not in token_map:
89 unknown.add(key)
90 return match.group(0)
91 return token_map[key]
92
93 result = TOKEN_PATTERN.sub(replacer, text)
94 if fail_on_unknown and unknown:
95 keys = ", ".join(sorted(unknown))
96 raise ConfigError(f"unknown or unmapped template token(s): {keys}")
97 return result
98
99
100 def render_template(template_path: Path, config: OverseerConfig) -> str:
101 """Read a template file and return token-substituted content."""
102 path = template_path.resolve()
103 if not path.is_file():
104 raise ConfigError("template file missing", str(path))
105 try:
106 raw = path.read_text(encoding="utf-8")
107 except OSError as exc:
108 raise ConfigError(f"cannot read template: {exc}", str(path)) from exc
109 return substitute_tokens(raw, build_token_map(config))