templating.py
python
sha256:0e9549ec7b463911bc08b7d586dc320b1ac9b1f5c943ee7e3865dcc6cb0f6f83
chore(governance): sync handover+roadmap to 84db8c8 (drift:…
Human
1 day ago
| 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)) |
File History
1 commit
sha256:6abcf1fa82a7a621ccbc945f19acdba5bc0db54569599404a1452fb4a096a199
fix(ISR): default require_independent_second_reviewer to require
Human
minor
⚠
1 day ago