Spaces:
Runtime error
Runtime error
| """Build the documentation figures owned by the Kneiff repository.""" | |
| from __future__ import annotations | |
| from collections.abc import Callable, Sequence | |
| from dataclasses import dataclass | |
| from html import escape | |
| from pathlib import Path | |
| import shutil | |
| from tempfile import TemporaryDirectory | |
| from graphviz import Digraph | |
| REPOSITORY_ROOT = Path(__file__).resolve().parent.parent | |
| ASSET_DIR = REPOSITORY_ROOT / "docs" / "assets" | |
| FigureBuilder = Callable[[], Digraph] | |
| # =============================================================== | |
| # == Figure Theme | |
| # =============================================================== | |
| class FigureTheme: | |
| """Colors and typography shared by repository documentation figures.""" | |
| blue: str = "#00a2ff" | |
| green: str = "#32bc00" | |
| orange: str = "#f4a261" | |
| purple: str = "#8b5cf6" | |
| teal: str = "#00a6a6" | |
| surface: str = "#ffffff" | |
| text: str = "#000000" | |
| inverse_text: str = "#ffffff" | |
| font: str = "sans-serif" | |
| monospace_font: str = "monospace" | |
| THEME = FigureTheme() | |
| class DocumentationFigure: | |
| """Apply one local diagram contract to a Graphviz graph. | |
| The wrapper keeps colors, node layout, and edge styling inside this | |
| repository so documentation rendering does not depend on a sibling | |
| checkout. | |
| :param name: Stable Graphviz graph identifier. | |
| :param direction: Graphviz rank direction such as ``"TB"`` or ``"LR"``. | |
| """ | |
| def __init__(self, name: str, direction: str) -> None: | |
| self.graph = Digraph(name=name) | |
| self.graph.attr( | |
| "graph", | |
| rankdir=direction, | |
| bgcolor="transparent", | |
| pad="0.04", | |
| nodesep="0.20", | |
| ranksep="0.36", | |
| fontname=THEME.font, | |
| fontcolor=THEME.text, | |
| ) | |
| self.graph.attr("node", shape="plain", fontname=THEME.font) | |
| self.graph.attr( | |
| "edge", | |
| arrowsize="0.72", | |
| penwidth="2.5", | |
| fontname=THEME.font, | |
| fontsize="9", | |
| fontcolor=THEME.text, | |
| ) | |
| def box( | |
| self, | |
| node_id: str, | |
| title: str, | |
| lines: Sequence[str], | |
| *, | |
| border_color: str, | |
| stereotype: str | None = None, | |
| monospace: bool = False, | |
| graph: Digraph | None = None, | |
| ) -> None: | |
| """Add one readable documentation node. | |
| :param node_id: Stable identifier used by edges. | |
| :param title: Short heading displayed in the colored header. | |
| :param lines: Detail rows shown under the heading. | |
| :param border_color: Semantic theme color for the node. | |
| :param stereotype: Optional classifier text displayed above the title. | |
| :param monospace: Whether detail rows represent literal file names. | |
| :param graph: Optional cluster graph receiving the node. | |
| :return: None. | |
| """ | |
| rows: list[str] = [] | |
| if stereotype is not None: | |
| rows.append( | |
| '<TR><TD BORDER="0" CELLPADDING="3">' | |
| f'<FONT POINT-SIZE="9"><<{escape(stereotype)}>></FONT>' | |
| "</TD></TR>" | |
| ) | |
| rows.append( | |
| f'<TR><TD BGCOLOR="{border_color}" BORDER="0" CELLPADDING="5">' | |
| f'<FONT COLOR="{THEME.inverse_text}"><B>{escape(title)}</B></FONT>' | |
| "</TD></TR>" | |
| ) | |
| detail_font = THEME.monospace_font if monospace else THEME.font | |
| for line in lines: | |
| rows.append( | |
| '<TR><TD ALIGN="LEFT" BORDER="0" CELLPADDING="3">' | |
| f'<FONT FACE="{detail_font}" POINT-SIZE="10">{escape(line)}</FONT>' | |
| "</TD></TR>" | |
| ) | |
| label = ( | |
| f'<<TABLE BORDER="2" COLOR="{border_color}" BGCOLOR="{THEME.surface}" ' | |
| 'CELLBORDER="0" CELLSPACING="0" CELLPADDING="0">' | |
| f"{''.join(rows)}</TABLE>>" | |
| ) | |
| (graph or self.graph).node(node_id, label=label) | |
| def edge( | |
| self, | |
| source: str, | |
| target: str, | |
| label: str | Sequence[str], | |
| *, | |
| color: str, | |
| constraint: bool = True, | |
| ) -> None: | |
| """Connect two nodes with one semantic color. | |
| :param source: Origin node identifier. | |
| :param target: Destination node identifier. | |
| :param label: One label or multiple stacked label lines. | |
| :param color: Stroke and arrow color. | |
| :param constraint: Whether the edge controls rank placement. | |
| :return: None. | |
| """ | |
| lines = (label,) if isinstance(label, str) else label | |
| text = '<BR ALIGN="CENTER"/>'.join(escape(line) for line in lines) | |
| html_label = ( | |
| f'<<TABLE BORDER="1" COLOR="{color}" BGCOLOR="{color}" ' | |
| 'CELLBORDER="0" CELLSPACING="0" CELLPADDING="3">' | |
| f'<TR><TD><FONT COLOR="{THEME.inverse_text}" POINT-SIZE="9">' | |
| f"<B>{text}</B></FONT></TD></TR></TABLE>>" | |
| ) | |
| self.graph.edge( | |
| source, | |
| target, | |
| label=html_label, | |
| color=color, | |
| constraint=str(constraint).lower(), | |
| ) | |
| # =============================================================== | |
| # == Figure Definitions | |
| # =============================================================== | |
| def build_kneiff_workflow_map() -> Digraph: | |
| """Show how AppRC selection defines one complete Kneiff project. | |
| :return: Diagram of the selector, conventional project paths, and data | |
| flow. | |
| """ | |
| figure = DocumentationFigure("kneiff_workflow_map", direction="TB") | |
| figure.graph.attr(nodesep="0.04", ranksep="0.32") | |
| figure.box( | |
| "selector", | |
| "AppRC project selector", | |
| ("KNF_STORAGE", "--storage NAME_OR_PATH", "knf project use NAME"), | |
| stereotype="single source of selection", | |
| border_color=THEME.blue, | |
| ) | |
| with figure.graph.subgraph(name="cluster_project") as project: | |
| project.attr( | |
| label="Selected AppRC project root\n<<fixed shallow layout>>", | |
| color=THEME.orange, | |
| bgcolor="#f4a26119", | |
| fontcolor=THEME.orange, | |
| penwidth="2", | |
| style="rounded,filled", | |
| ) | |
| project.attr(rank="same") | |
| figure.box( | |
| "project_inputs", | |
| "Configuration and resources", | |
| ( | |
| ".env.apprc-storage (machine-local)", | |
| "vocabulary.knf.yaml", | |
| "prompts.knf.yaml", | |
| "configs/*.knf.yaml", | |
| "workflows/ (optional overrides)", | |
| ), | |
| border_color=THEME.teal, | |
| monospace=True, | |
| graph=project, | |
| ) | |
| figure.box( | |
| "editable_dataset", | |
| "Editable dataset", | |
| ("SOURCE/", "MANIFEST.knf.xlsx", "MANIFEST.yaml (generated)"), | |
| border_color=THEME.green, | |
| monospace=True, | |
| graph=project, | |
| ) | |
| figure.box( | |
| "generated_artifacts", | |
| "Rebuildable artifacts", | |
| ("HF/<config-id>/", "TRAINING/", ".old_manifests/"), | |
| border_color=THEME.purple, | |
| monospace=True, | |
| graph=project, | |
| ) | |
| figure.edge( | |
| "selector", | |
| "project_inputs", | |
| "resolves one root", | |
| color=THEME.teal, | |
| ) | |
| figure.edge( | |
| "project_inputs", | |
| "editable_dataset", | |
| ("defines schema", "and operations"), | |
| color=THEME.green, | |
| constraint=False, | |
| ) | |
| figure.edge( | |
| "editable_dataset", | |
| "generated_artifacts", | |
| ("dataset sync", "train prepare/run"), | |
| color=THEME.purple, | |
| constraint=False, | |
| ) | |
| return figure.graph | |
| def build_docs_reading_map() -> Digraph: | |
| """Show the shortest route from the root README into detailed docs. | |
| :return: Diagram of documentation entry points and their responsibilities. | |
| """ | |
| figure = DocumentationFigure("kneifftools_docs_map", direction="TB") | |
| figure.graph.attr(nodesep="0.28", ranksep="0.36") | |
| figure.box( | |
| "root_readme", | |
| "Root README", | |
| ("install and first project", "quick workflows"), | |
| stereotype="start here", | |
| border_color=THEME.blue, | |
| ) | |
| figure.box( | |
| "docs_index", | |
| "Docs index", | |
| ("reading map", "shared terminology"), | |
| stereotype="choose by task", | |
| border_color=THEME.orange, | |
| ) | |
| figure.box( | |
| "how_to", | |
| "How-To User Guides", | |
| ("Commands and procedures",), | |
| border_color=THEME.green, | |
| ) | |
| figure.box( | |
| "development", | |
| "Development", | |
| ("Maintainer workflow",), | |
| border_color=THEME.blue, | |
| ) | |
| figure.box( | |
| "references", | |
| "References", | |
| ("Exact names and paths",), | |
| border_color=THEME.purple, | |
| ) | |
| figure.box( | |
| "explanations", | |
| "Explanations", | |
| ("System model and rationale",), | |
| border_color=THEME.orange, | |
| ) | |
| figure.edge("root_readme", "docs_index", "continue", color=THEME.orange) | |
| figure.edge("docs_index", "how_to", "do a task", color=THEME.green) | |
| figure.edge("docs_index", "development", "change code", color=THEME.blue) | |
| figure.edge( | |
| "docs_index", | |
| "references", | |
| "look up a name", | |
| color=THEME.purple, | |
| ) | |
| figure.edge( | |
| "docs_index", | |
| "explanations", | |
| "understand why", | |
| color=THEME.orange, | |
| ) | |
| return figure.graph | |
| FIGURES: tuple[tuple[str, FigureBuilder], ...] = ( | |
| ("kneiff-workflow-map", build_kneiff_workflow_map), | |
| ("docs-reading-map", build_docs_reading_map), | |
| ) | |
| # =============================================================== | |
| # == Rendering | |
| # =============================================================== | |
| def render_figures() -> tuple[Path, ...]: | |
| """Render every Kneiff figure into ``docs/assets``. | |
| Graphviz writes SVG files into a temporary directory. The final copy keeps | |
| only repository-owned assets and avoids partial output when rendering | |
| fails. | |
| :return: Paths of the generated SVG assets. | |
| """ | |
| ASSET_DIR.mkdir(parents=True, exist_ok=True) | |
| generated: list[Path] = [] | |
| with TemporaryDirectory(prefix="kneiff-doc-figures-") as temp_dir: | |
| temp_path = Path(temp_dir) | |
| for figure_name, build_figure in FIGURES: | |
| rendered_path = Path( | |
| build_figure().render( | |
| filename=figure_name, | |
| directory=temp_path, | |
| format="svg", | |
| cleanup=True, | |
| ) | |
| ) | |
| output_path = ASSET_DIR / f"{figure_name}.svg" | |
| shutil.copyfile(rendered_path, output_path) | |
| generated.append(output_path) | |
| return tuple(generated) | |
| def main() -> int: | |
| """Regenerate the repository-owned documentation assets. | |
| :return: Process exit code. | |
| """ | |
| for path in render_figures(): | |
| print(path.relative_to(REPOSITORY_ROOT)) | |
| return 0 | |
| if __name__ == "__main__": | |
| raise SystemExit(main()) | |