Files
computer-playbook/cli/build/readme/sections.py
T
kevinveenbirkenbachandClaude Opus 4.8 90b3845998 feat(readme): role README generator with Cosmos diagrams, Quick Setup, and lint
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>
2026-07-17 14:44:33 +02:00

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()