mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-08 19:56:53 +00:00
The Cosmos section drew one diagram inside the README and truncated its
dependents at twelve. It is replaced by an `ARCHITECTURE.md` sub-page per
invokable role with four sections, and the README keeps only a link table.
Compose lists the containers with image and published ports. Swarm shows the
replica count and the effective placement constraint per service. Volumes
draws the decision chain `swarm_nfs_backed` walks instead of its outcome,
because whether a volume is NFS-backed depends on the mode, the storage
backend, the entry's own `nfs:` flag and the role's placement at once, and a
diagram stating only the result hides the coupling that makes a placement
change move data silently. Dependencies is the cosmos graph with the cap
removed, so `_DEPENDENTS_CAP`, `shown`, `overflow` and the `dpt_more` node are
gone.
Which containers a role runs cannot be read off `meta/services.yml`: it mixes
the role's own services with the ones it merely consumes, and the entries that
do describe a container carry `name`, or only `image`, or neither when the
image is built, so no field discriminates. A heuristic over that file dropped
`pds` from web-app-bluesky, five of six services from web-app-funkwhale and
the only service of web-svc-cdn. The new `services` module reads the
assignments that actually render a container, `{% set service_name = ... %}`,
resolving literals directly and constants through `vars/main.yml`; every
template in the tree that includes the container base carries one.
The page is written by its own unconditional loop in `__main__.py`, not gated
behind `--update-architecture`, so the documentation build cannot emit the
link table without the page it points at. `--check` compares instead of
writing. A committed copy is refused by
`test_no_role_commits_the_architecture_sub_page`, the rule the managed README
sections already follow.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
88 lines
2.9 KiB
Python
88 lines
2.9 KiB
Python
"""Derive the role-README section schema from the Jinja template.
|
|
|
|
``templates/roles/README.md.j2.tmpl`` is the single source of truth. The
|
|
required sections are the H2s the template emits with no optional context;
|
|
the optional sections are the extra H2s that appear once every
|
|
``{% if ... %}``-guarded variable is supplied. Rendering the template both
|
|
ways and diffing the H2 sets keeps the lint and the generator in lock-step
|
|
with the template.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import functools
|
|
|
|
import jinja2
|
|
|
|
from cli.build.docs.readme.sections import h2_titles
|
|
from utils.cache.files import PROJECT_ROOT
|
|
from utils.software import SOFTWARE_REPOSITORY
|
|
|
|
TEMPLATE_DIR = PROJECT_ROOT / "templates" / "roles"
|
|
TEMPLATE_NAME = "README.md.j2.tmpl"
|
|
|
|
_REQUIRED_CTX: dict = {
|
|
"application_id": "web-app-example",
|
|
"application_name": "Example",
|
|
"application_url": "https://example.com/",
|
|
"application_description": "is an example application.",
|
|
"application_overview": "This role deploys the example application.",
|
|
"application_features": [{"name": "Example", "description": "An example feature."}],
|
|
"application_author": "Kevin Veen-Birkenbach",
|
|
"software_repository": SOFTWARE_REPOSITORY,
|
|
"application_deploy_image": "ghcr.io/example/core/debian:latest",
|
|
}
|
|
|
|
_OPTIONAL_CTX: dict = {
|
|
"application_invokable": True,
|
|
"application_author_url": "https://social.infinito.nexus/profile/kevinveenbirkenbach/profile",
|
|
"architecture_intro": "Example architecture intro.",
|
|
"application_use_cases": "Example use case.",
|
|
"application_developer_notes": [
|
|
{"file": "Administration.md", "description": "notes."}
|
|
],
|
|
"application_further_resources": [
|
|
{"label": "Example", "url": "https://example.com/"}
|
|
],
|
|
}
|
|
|
|
|
|
def _render(ctx: dict) -> str:
|
|
env = jinja2.Environment(
|
|
loader=jinja2.FileSystemLoader(str(TEMPLATE_DIR)),
|
|
keep_trailing_newline=True,
|
|
autoescape=jinja2.select_autoescape(),
|
|
)
|
|
return env.get_template(TEMPLATE_NAME).render(**ctx)
|
|
|
|
|
|
def render_required() -> str:
|
|
return _render(dict(_REQUIRED_CTX))
|
|
|
|
|
|
def render_full() -> str:
|
|
return _render({**_REQUIRED_CTX, **_OPTIONAL_CTX})
|
|
|
|
|
|
@functools.lru_cache(maxsize=1)
|
|
def required_sections() -> tuple[str, ...]:
|
|
"""H2 titles the template emits without any optional context (mandatory)."""
|
|
return tuple(h2_titles(render_required()))
|
|
|
|
|
|
@functools.lru_cache(maxsize=1)
|
|
def canonical_order() -> tuple[str, ...]:
|
|
"""Full H2 order (required + optional) as the template lays them out."""
|
|
return tuple(h2_titles(render_full()))
|
|
|
|
|
|
@functools.lru_cache(maxsize=1)
|
|
def optional_sections() -> tuple[str, ...]:
|
|
required = set(required_sections())
|
|
return tuple(title for title in canonical_order() if title not in required)
|
|
|
|
|
|
def credits_heading() -> str:
|
|
"""The last required section (Credits by convention)."""
|
|
return required_sections()[-1]
|