mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-11 21:26:44 +00:00
Add cli/build/readme: derives each role README's managed sections from templates/roles/README.md.j2.tmpl (the Cosmos mermaid diagram from meta/services.yml, an invokable-gated Quick Setup, and Credits). The Cosmos diagram shows dependencies, every declared service, and dependents (roles that consume this one, matched by role name and by provides provision-name in both directions). Role nodes carry deploy-mode emoji markers (compose/swarm/host); disabled and shared services get a stop mark; dependents are capped at 12. test_readme enforces the Quick Setup body is byte-exact with the template render. `make readme-generate` and `make cosmos` regenerate; `make cosmos` also runs inside `make quality`. Refactor: utils/symbol_glossary.py is the single source of truth for the word/emoji mapping (unique project-wide), consumed by the complexity matrix and the Cosmos builder. utils/roles/deploy.py holds role_has_stack and role_deploy_modes, shared by both so the logic never drifts. Add `make lint-mermaid` and its CI workflow, rendering every diagram via mermaid-cli; the render step provisions chrome-headless-shell and unzip on demand so it works in the minimal container. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
60 lines
1.8 KiB
Python
60 lines
1.8 KiB
Python
"""Split a Markdown README into its H2 sections (fence-aware)."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
from dataclasses import dataclass
|
|
|
|
_H2_RE = re.compile(r"^##\s+(.*?)\s*$")
|
|
_FENCE_RE = re.compile(r"^\s*`{3,}")
|
|
|
|
|
|
@dataclass
|
|
class Readme:
|
|
"""A parsed README: everything before the first H2, then its H2 blocks."""
|
|
|
|
preamble: str
|
|
sections: list[tuple[str, str]]
|
|
|
|
def titles(self) -> list[str]:
|
|
return [title for title, _ in self.sections]
|
|
|
|
def render(self) -> str:
|
|
parts = [self.preamble.rstrip("\n")] if self.preamble.strip() else []
|
|
parts.extend(block.rstrip("\n") for _, block in self.sections)
|
|
return "\n\n".join(parts) + "\n"
|
|
|
|
|
|
def _h2_starts(lines: list[str]) -> list[int]:
|
|
"""Line indexes of every H2 heading outside a fenced code block."""
|
|
starts: list[int] = []
|
|
in_fence = False
|
|
for i, line in enumerate(lines):
|
|
if _FENCE_RE.match(line):
|
|
in_fence = not in_fence
|
|
continue
|
|
if not in_fence and _H2_RE.match(line):
|
|
starts.append(i)
|
|
return starts
|
|
|
|
|
|
def parse_readme(text: str) -> Readme:
|
|
lines = text.splitlines()
|
|
starts = _h2_starts(lines)
|
|
if not starts:
|
|
return Readme(preamble=text, sections=[])
|
|
|
|
preamble = "\n".join(lines[: starts[0]])
|
|
sections: list[tuple[str, str]] = []
|
|
for idx, start in enumerate(starts):
|
|
end = starts[idx + 1] if idx + 1 < len(starts) else len(lines)
|
|
title = _H2_RE.match(lines[start]).group(1)
|
|
block = "\n".join(lines[start:end]).rstrip("\n")
|
|
sections.append((title, block))
|
|
return Readme(preamble=preamble, sections=sections)
|
|
|
|
|
|
def h2_titles(text: str) -> list[str]:
|
|
"""Return the ordered H2 titles of a Markdown document (fence-aware)."""
|
|
return parse_readme(text).titles()
|