diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml new file mode 100644 index 0000000..e857ae8 --- /dev/null +++ b/.github/workflows/pages.yml @@ -0,0 +1,71 @@ +name: Deploy documentation + +on: + pull_request: + push: + branches: + - main + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: github-pages-${{ github.ref }} + cancel-in-progress: true + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Check out repository + uses: actions/checkout@v6 + + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version: "3.13" + cache: pip + cache-dependency-path: | + requirements-dev.txt + requirements-docs.txt + + - name: Install dependencies + run: python -m pip install -r requirements-dev.txt -r requirements-docs.txt + + - name: Validate repository + run: make validate + + - name: Lint normative keywords + run: make validate-normative + + - name: Generate documentation source + run: python tools/generate_site_docs.py + + - name: Build documentation + run: mkdocs build + + - name: Configure GitHub Pages + if: github.event_name != 'pull_request' + uses: actions/configure-pages@v5 + + - name: Upload GitHub Pages artifact + if: github.event_name != 'pull_request' + uses: actions/upload-pages-artifact@v4 + with: + path: .site-build + + deploy: + if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main' + needs: build + runs-on: ubuntu-latest + permissions: + pages: write + id-token: write + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - name: Deploy GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/.gitignore b/.gitignore index 5c1e2a3..4be1350 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,5 @@ venv/ *.tmp *.swp *.zip +.site-docs/ +.site-build/ diff --git a/MANIFEST.sha256 b/MANIFEST.sha256 index 003036e..50431b7 100644 --- a/MANIFEST.sha256 +++ b/MANIFEST.sha256 @@ -1,4 +1,4 @@ -3afa430e47b4b30640c8d21de565b90582f350249ab646c94ced96bf57e1a316 .gitignore +b401b8ac1cc33a55f36f5faa2d8c4372cec56b980ad00c009dbca47eab0b41b8 .gitignore c81ae9ba64df3b1c5d9dcceed7e4f79d0a7ae4776380481b913149a08d99e309 ARCHITECTURE.md eb4ef0a34a5f941361972a7df4acc4a53007ab99a8773b95dd5a56c78091bea1 CHANGELOG.md a72fabcd2dd27e96394adbd633b71d9e403766336695ddd7e21257ae8c69dc01 CONFLICT-RESOLUTION.md @@ -6,7 +6,7 @@ d80cc0c8532a0c03869d10a6d09cebda54464af5337801af5fa528c4b2cdaba1 CONTRIBUTING.m 83bd470cb3db0001eb905adeea0b55da1e121be5f1c5e0093a2c7b00bd69c971 DOCUMENTATION.md b70da374c6f05399968e77288de5db7f267e3c6b117d6c06cbdfc34168a43157 KNOWLEDGE-MODEL.md 60f7cde752f96e821832576ed9ca9ddbd22efc0c3e6541a21e6a0605cf9ddb76 LICENSE -eeebc06d9dcc1ec848136f44aad4b3105ab0987af8056826a6fbec3e97f1152c Makefile +b8047c1b1afe54e89a8086f04a43c44c36de72c472cf154e1e98527526c1bbfa Makefile f83aba41dfb4e214e85536c8f055de4dd4de6c1593cdcef78cc0eb43d07f7cdd README.md 4567c3af0536e424f8dd62cae5adeae467f4b1f0b9723659609cf0791c126a06 ROADMAP.md 75eef1e94056557516131f9d79aea0c8d9b92f102da74e44c08ec99496b69d2c SELF-CONTAINMENT.md @@ -400,6 +400,7 @@ b9687f0785ccd4f186cd27ab4e58eaddf37abc43da3d3eb6f905a3d1d860db33 schemas/resolv d55c4d1eb6edc1ecccbee5e6db1e0bea371919ad956cbc861caa30f32228e9a3 tools/distribution.py f12e52837e8b71dbc4436a2cf0a3a234f2c15823909b6c787d383451e454dcad tools/evaluate.py f90fefe7940fb9438cfec6e163724407da5166e9826f54e57c8d67aaba01e7ba tools/generate_compendium.py +889d27abdf88b2695b57e864ad17de94376f2facfd03eb46ef27b5975fd52488 tools/generate_site_docs.py 504558dd715c337641e86c194869d9d80f120183e9f0210043bb8385e89747ef tools/orchestrate.py 6b9ba027c38c88d567467bbbd52af1ec350cfa75a8825e798b0c9e6e2d771b89 tools/package.py 0acadfba1a6044eabd532f285d7e673fba395537075c6468dc1ebb66493da10d tools/update_manifest.py diff --git a/Makefile b/Makefile index bcb6611..e704804 100644 --- a/Makefile +++ b/Makefile @@ -1,10 +1,18 @@ -.PHONY: evaluate generate test validate validate-normative manifest package resolve +.PHONY: docs docs-serve evaluate generate test validate validate-normative manifest package resolve PYTHON ?= python3 generate: $(PYTHON) tools/generate_compendium.py +docs: + $(PYTHON) tools/generate_site_docs.py + mkdocs build + +docs-serve: + $(PYTHON) tools/generate_site_docs.py + mkdocs serve + test: $(PYTHON) -m unittest discover -s tests -p 'test_*.py' diff --git a/docs-site/index.md b/docs-site/index.md new file mode 100644 index 0000000..66f76d0 --- /dev/null +++ b/docs-site/index.md @@ -0,0 +1,85 @@ +# Code Principles + +
+ +## Engineering guidance that adapts to the project + +A self-contained, language-agnostic knowledge base and policy system for AI coding agents — presented here as a browsable engineering handbook. + +[Browse the 72 principles](principles/index.md){ .md-button .md-button--primary } +[Explore the decision system](core/index.md){ .md-button } + +
+ +
+ +
+ +### Principles + +Canonical engineering ideas with explicit applicability, rejected interpretations, trade-offs, conflicts, examples, and AI guidance. + +[Browse principles →](principles/index.md) + +
+ +
+ +### Core Skills + +Decision procedures that combine principles into practical modes for clarity, abstraction, testing, compatibility, reliability, performance, and more. + +[Explore Core Skills →](core/index.md) + +
+ +
+ +### Project profiles + +Start from what you are building: application, library, service, worker, pipeline, plugin, prototype, CLI, real-time system, infrastructure tool, and more. + +[Choose a project profile →](profiles/index.md) + +
+ +
+ +### Technology refinements + +See how generic policy is refined for JavaScript, TypeScript, Python, PHP, Go, C++, React, Next.js, Vue, Nuxt, Angular, Symfony, and Drupal. + +[Browse languages →](languages/index.md) · [Browse frameworks →](frameworks/index.md) + +
+ +
+ +## How the policy model works + +
+
1Project evidencetask, repository, explicit context
+ +
2Project profiledominant artifact and failure model
+ +
3Modifiersverified cross-cutting constraints
+ +
4Core Skillscontext-sensitive decision modes
+ +
5Technology adapterslanguage and framework semantics
+ +
6Resolved policyexplainable engineering decisions
+
+ +!!! info "The website is a generated view" + Canonical YAML and normative Markdown in the repository remain authoritative. This site is rebuilt from those sources and is never an independent source of policy. + +## Learn by example + +The repository includes executable evaluation scenarios that test profile selection, modifiers, language and framework refinements, conflict decisions, and forbidden overengineering. They are useful both as tests and as worked examples of the policy model. + +[Browse evaluation scenarios](evaluations/index.md){ .md-button } + +## Reference + +For the complete system contracts and architecture, continue with the [Specification](SPECIFICATION.md), [Architecture](ARCHITECTURE.md), [Knowledge Model](KNOWLEDGE-MODEL.md), and [Conflict Resolution](CONFLICT-RESOLUTION.md). diff --git a/docs-site/javascripts/catalog-filter.js b/docs-site/javascripts/catalog-filter.js new file mode 100644 index 0000000..74f0ea7 --- /dev/null +++ b/docs-site/javascripts/catalog-filter.js @@ -0,0 +1,45 @@ +(() => { + const normalize = (value) => value.trim().toLocaleLowerCase(); + + const initCatalogFilters = () => { + document.querySelectorAll("[data-catalog-filter]").forEach((input) => { + if (input.dataset.catalogFilterReady === "true") return; + input.dataset.catalogFilterReady = "true"; + + const root = input.closest(".md-content") || document; + const items = Array.from(root.querySelectorAll("[data-catalog-item]")); + const count = root.querySelector("[data-catalog-count]"); + const empty = root.querySelector("[data-catalog-empty]"); + + const apply = () => { + const query = normalize(input.value); + let visible = 0; + + items.forEach((item) => { + const matches = !query || normalize(item.dataset.search || item.textContent || "").includes(query); + item.hidden = !matches; + if (matches) visible += 1; + }); + + root.querySelectorAll(".catalog-group").forEach((group) => { + const groupVisible = Array.from(group.querySelectorAll("[data-catalog-item]")).some((item) => !item.hidden); + group.hidden = !groupVisible; + }); + + if (count) count.textContent = `${visible} ${visible === 1 ? "entry" : "entries"}`; + if (empty) empty.hidden = visible !== 0; + }; + + input.addEventListener("input", apply); + apply(); + }); + }; + + if (typeof document$ !== "undefined") { + document$.subscribe(initCatalogFilters); + } else if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", initCatalogFilters); + } else { + initCatalogFilters(); + } +})(); diff --git a/docs-site/stylesheets/extra.css b/docs-site/stylesheets/extra.css new file mode 100644 index 0000000..e71972c --- /dev/null +++ b/docs-site/stylesheets/extra.css @@ -0,0 +1,259 @@ +:root { + --cp-radius: 0.75rem; + --cp-border: color-mix(in srgb, var(--md-default-fg-color) 16%, transparent); + --cp-surface: color-mix(in srgb, var(--md-default-bg-color) 94%, var(--md-primary-fg-color) 6%); + --cp-surface-hover: color-mix(in srgb, var(--md-default-bg-color) 86%, var(--md-primary-fg-color) 14%); +} + +.md-grid { + max-width: 90rem; +} + +.md-typeset h1 { + font-weight: 700; + letter-spacing: -0.02em; +} + +.md-typeset h2 { + letter-spacing: -0.015em; +} + +.hero { + padding: 2.2rem; + margin: 0 0 2rem; + border: 1px solid var(--cp-border); + border-radius: 1rem; + background: + radial-gradient(circle at 90% 10%, color-mix(in srgb, var(--md-primary-fg-color) 18%, transparent), transparent 35%), + var(--cp-surface); +} + +.hero h2 { + margin-top: 0; + max-width: 22ch; + font-size: clamp(1.8rem, 4vw, 3rem); + line-height: 1.05; +} + +.hero > p { + max-width: 62ch; + font-size: 1.05rem; +} + +.home-grid, +.catalog-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 1rem; +} + +.home-card, +.catalog-card { + border: 1px solid var(--cp-border); + border-radius: var(--cp-radius); + background: var(--cp-surface); +} + +.home-card { + padding: 1.2rem 1.25rem; +} + +.home-card h3 { + margin-top: 0; +} + +.catalog-card { + display: flex; + flex-direction: column; + gap: 0.4rem; + padding: 1rem; + color: var(--md-default-fg-color) !important; + text-decoration: none; + transition: background-color 120ms ease, border-color 120ms ease, transform 120ms ease; +} + +.catalog-card:hover, +.catalog-card:focus-visible { + background: var(--cp-surface-hover); + border-color: color-mix(in srgb, var(--md-primary-fg-color) 45%, transparent); + transform: translateY(-1px); +} + +.catalog-card__title { + color: var(--md-primary-fg-color); + font-weight: 700; + font-size: 1rem; +} + +[data-md-color-scheme="slate"] .catalog-card__title { + color: var(--md-accent-fg-color); +} + +.catalog-card__description { + flex: 1; + font-size: 0.86rem; + line-height: 1.45; +} + +.catalog-card__id, +.catalog-card__meta { + font-family: var(--md-code-font-family); + font-size: 0.68rem; + opacity: 0.72; +} + +.catalog-group { + margin: 2rem 0; +} + +.catalog-toolbar { + position: sticky; + top: 3.6rem; + z-index: 2; + display: grid; + grid-template-columns: auto minmax(12rem, 1fr) auto; + gap: 0.75rem; + align-items: center; + padding: 0.75rem; + margin: 1.25rem 0; + border: 1px solid var(--cp-border); + border-radius: var(--cp-radius); + background: color-mix(in srgb, var(--md-default-bg-color) 94%, transparent); + backdrop-filter: blur(12px); +} + +.catalog-toolbar label { + font-weight: 600; +} + +.catalog-toolbar input { + width: 100%; + min-width: 0; + padding: 0.55rem 0.7rem; + border: 1px solid var(--cp-border); + border-radius: 0.45rem; + background: var(--md-default-bg-color); + color: var(--md-default-fg-color); + font: inherit; +} + +.catalog-toolbar input:focus { + outline: 2px solid var(--md-accent-fg-color); + outline-offset: 1px; +} + +.catalog-toolbar__count { + white-space: nowrap; + font-size: 0.78rem; + opacity: 0.75; +} + +.catalog-empty { + padding: 1rem; + border: 1px dashed var(--cp-border); + border-radius: var(--cp-radius); + text-align: center; +} + +.resolution-flow { + display: grid; + grid-template-columns: repeat(11, auto); + gap: 0.45rem; + align-items: stretch; + overflow-x: auto; + padding: 0.25rem 0 0.75rem; + scrollbar-width: thin; +} + +.resolution-flow > div:not(.resolution-flow__arrow) { + display: grid; + min-width: 9.5rem; + padding: 0.85rem; + border: 1px solid var(--cp-border); + border-radius: var(--cp-radius); + background: var(--cp-surface); +} + +.resolution-flow strong { + color: var(--md-primary-fg-color); + font-size: 0.72rem; +} + +.resolution-flow span { + font-weight: 700; +} + +.resolution-flow small { + margin-top: 0.35rem; + opacity: 0.72; + line-height: 1.35; +} + +.resolution-flow__arrow { + align-self: center; + opacity: 0.55; +} + +.md-typeset table:not([class]) { + font-size: 0.78rem; +} + +@media screen and (max-width: 76.234375em) { + .catalog-toolbar { + top: 2.8rem; + } +} + +@media screen and (max-width: 60em) { + .home-grid, + .catalog-grid { + grid-template-columns: 1fr; + } + + .hero { + padding: 1.5rem; + } + + .resolution-flow { + grid-template-columns: 1fr; + overflow: visible; + } + + .resolution-flow__arrow { + transform: rotate(90deg); + justify-self: center; + height: 1rem; + } + + .resolution-flow > div:not(.resolution-flow__arrow) { + min-width: 0; + } +} + +@media screen and (max-width: 44.984375em) { + .catalog-toolbar { + position: static; + grid-template-columns: 1fr auto; + } + + .catalog-toolbar label { + grid-column: 1 / -1; + } + + .catalog-toolbar input { + min-height: 2.75rem; + } + + .md-typeset .md-button { + display: block; + width: 100%; + margin: 0.45rem 0; + text-align: center; + } +} + +@media (prefers-reduced-motion: reduce) { + .catalog-card { + transition: none; + } +} diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 0000000..a128f6a --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,105 @@ +site_name: Code Principles +site_description: A human-readable software engineering knowledge base and policy system for AI coding agents. +site_url: https://peterramotowski.github.io/code-principles/ +repo_url: https://github.com/PeterRamotowski/code-principles +repo_name: PeterRamotowski/code-principles +edit_uri: edit/main/ +docs_dir: .site-docs +site_dir: .site-build +use_directory_urls: true + +nav: + - Home: index.md + - Principles: + - Browse principles: principles/index.md + - Classification: principles/CLASSIFICATION.md + - Authoring guide: principles/AUTHORING-GUIDE.md + - Decision system: + - Core Skills: core/index.md + - Project profiles: profiles/index.md + - Modifiers: modifiers/index.md + - Conflict resolution: CONFLICT-RESOLUTION.md + - Orchestrator: orchestrator/SKILL.md + - Technologies: + - Languages: languages/index.md + - Frameworks: frameworks/index.md + - Examples: + - Evaluation scenarios: evaluations/index.md + - Reference: + - Repository overview: README.md + - Specification: SPECIFICATION.md + - Architecture: ARCHITECTURE.md + - Knowledge model: KNOWLEDGE-MODEL.md + - Terminology: TERMINOLOGY.md + - Self-containment: SELF-CONTAINMENT.md + - Contributing: CONTRIBUTING.md + - Roadmap: ROADMAP.md + - Changelog: CHANGELOG.md + +theme: + name: material + language: en + features: + - navigation.tabs + - navigation.tabs.sticky + - navigation.sections + - navigation.indexes + - navigation.top + - navigation.footer + - toc.follow + - search.suggest + - search.highlight + - search.share + - content.code.copy + palette: + - media: "(prefers-color-scheme: light)" + scheme: default + primary: indigo + accent: indigo + toggle: + icon: material/weather-night + name: Switch to dark mode + - media: "(prefers-color-scheme: dark)" + scheme: slate + primary: indigo + accent: indigo + toggle: + icon: material/weather-sunny + name: Switch to light mode + icon: + repo: fontawesome/brands/github + +plugins: + - search + +markdown_extensions: + - admonition + - attr_list + - md_in_html + - tables + - toc: + permalink: true + - pymdownx.details + - pymdownx.highlight: + anchor_linenums: true + - pymdownx.inlinehilite + - pymdownx.superfences + - pymdownx.tabbed: + alternate_style: true + +extra_css: + - stylesheets/extra.css + +extra_javascript: + - javascripts/catalog-filter.js + +extra: + social: + - icon: fontawesome/brands/github + link: https://github.com/PeterRamotowski/code-principles + name: Code Principles on GitHub + +validation: + omitted_files: warn + absolute_links: relative_to_docs + unrecognized_links: warn diff --git a/requirements-docs.txt b/requirements-docs.txt new file mode 100644 index 0000000..24275cf --- /dev/null +++ b/requirements-docs.txt @@ -0,0 +1 @@ +mkdocs-material>=9.6,<10 diff --git a/tools/generate_site_docs.py b/tools/generate_site_docs.py new file mode 100644 index 0000000..2699ce4 --- /dev/null +++ b/tools/generate_site_docs.py @@ -0,0 +1,268 @@ +#!/usr/bin/env python3 +"""Build a non-normative MkDocs source tree from the repository's canonical documentation.""" +from __future__ import annotations + +import html +import shutil +from pathlib import Path +from typing import Iterable + +import yaml + +ROOT = Path(__file__).resolve().parents[1] +OUT = ROOT / ".site-docs" +SITE_SOURCE = ROOT / "docs-site" + +ROOT_DOCS = [ + "README.md", + "SPECIFICATION.md", + "ARCHITECTURE.md", + "KNOWLEDGE-MODEL.md", + "SELF-CONTAINMENT.md", + "TERMINOLOGY.md", + "CONFLICT-RESOLUTION.md", + "CONTRIBUTING.md", + "ROADMAP.md", + "CHANGELOG.md", +] + +MARKDOWN_TREES = [ + "principles", + "core", + "profiles", + "modifiers", + "languages", + "frameworks", + "orchestrator", + "evaluations", +] + + +def load_yaml(path: Path): + return yaml.safe_load(path.read_text(encoding="utf-8")) + + +def copy_markdown_tree(relative: str) -> None: + source = ROOT / relative + if not source.is_dir(): + return + for path in source.rglob("*.md"): + destination = OUT / path.relative_to(ROOT) + destination.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(path, destination) + + +def write(relative: str, content: str) -> None: + path = OUT / relative + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(content.rstrip() + "\n", encoding="utf-8") + + +def cards(items: Iterable[dict], details_key: str = "description") -> str: + rendered = [] + for item in sorted(items, key=lambda value: value.get("name", value["id"])): + item_id = item["id"] + name = html.escape(str(item.get("name", item_id))) + description = html.escape(str(item.get(details_key, ""))) + search_text = html.escape(f"{name} {description} {item_id}".lower(), quote=True) + rendered.append( + f'' + f'{name}' + f'{description}' + f'{html.escape(item_id)}' + "" + ) + return "\n".join(rendered) + + +def catalog_page(title: str, intro: str, body: str, count: int) -> str: + return f"""# {title} + +{intro} + +
+ + + {count} entries +
+ +
+{body} +
+ + +""" + + +def generate_principles() -> None: + registry = load_yaml(ROOT / "principles" / "registry.yaml") + categories = { + item["id"]: item["name"] + for item in load_yaml(ROOT / "principles" / "categories.yaml")["categories"] + } + principles = registry["principles"] + groups = [] + for category_id, category_name in categories.items(): + group = [item for item in principles if item["category"] == category_id] + if not group: + continue + rows = [] + for item in sorted(group, key=lambda value: value["name"]): + entry = load_yaml(ROOT / item["source"]) + name = html.escape(entry["name"]) + summary = html.escape(entry["summary"]) + classification = html.escape(entry["classification"]) + search_text = html.escape( + f"{entry['name']} {entry['id']} {entry['summary']} {entry['classification']} {category_name}".lower(), + quote=True, + ) + rows.append( + f'' + f'{name}' + f'{summary}' + f'{classification}' + "" + ) + groups.append( + f'

{html.escape(category_name)}

' + f'
{"".join(rows)}
' + ) + + page = f"""# Principles + +Browse the canonical engineering catalogue as a human-readable reference. The YAML entries remain the source of truth; these pages are generated views. + +
+ + + {len(principles)} entries +
+ +
+{''.join(groups)} +
+ + +""" + write("principles/index.md", page) + + +def load_components(directory: str, metadata_name: str) -> list[dict]: + items = [] + for metadata in sorted((ROOT / directory).glob(f"*/{metadata_name}")): + data = load_yaml(metadata) + if data: + items.append(data) + return items + + +def generate_component_hubs() -> None: + skills = load_components("core", "skill.yaml") + write( + "core/index.md", + catalog_page( + "Core Skills", + "Core Skills turn canonical principles into language-independent decision procedures. Choose a Skill to inspect its modes, conflicts, and review rules.", + cards(skills), + len(skills), + ), + ) + + profiles = load_components("profiles", "profile.yaml") + write( + "profiles/index.md", + catalog_page( + "Project profiles", + "Profiles configure the dominant artifact and failure model before language or framework refinements are applied.", + cards(profiles), + len(profiles), + ), + ) + + modifiers = load_components("modifiers", "modifier.yaml") + write( + "modifiers/index.md", + catalog_page( + "Engineering modifiers", + "Modifiers strengthen policy when a verified cross-cutting constraint applies. They never replace the base project profile.", + cards(modifiers), + len(modifiers), + ), + ) + + languages = load_components("languages", "adapter.yaml") + write( + "languages/index.md", + catalog_page( + "Language adapters", + "Language adapters refine generic engineering policy with runtime, type-system, packaging, concurrency, and resource semantics.", + cards(languages), + len(languages), + ), + ) + + frameworks = load_components("frameworks", "adapter.yaml") + write( + "frameworks/index.md", + catalog_page( + "Framework adapters", + "Framework adapters refine project policy with lifecycle, boundary, state, extension, and convention decisions.", + cards(frameworks), + len(frameworks), + ), + ) + + +def generate_evaluations() -> None: + scenario_dir = ROOT / "evaluations" / "scenarios" + scenarios = [] + for path in sorted(scenario_dir.glob("*.yaml")): + data = load_yaml(path) or {} + scenario_id = data.get("id", path.stem) + title = data.get("name") or data.get("title") or scenario_id.replace("-", " ").title() + request = ((data.get("input") or {}).get("request") or "") + scenarios.append((scenario_id, title, request, path.name)) + + rows = [] + for scenario_id, title, request, filename in scenarios: + description = html.escape(str(request)) if request else "Executable policy scenario" + search_text = html.escape(f"{scenario_id} {title} {request}".lower(), quote=True) + rows.append( + f'' + f'{html.escape(str(title))}' + f'{description}' + f'{html.escape(str(scenario_id))}' + "" + ) + + write( + "evaluations/index.md", + catalog_page( + "Evaluation scenarios", + "These executable scenarios exercise policy selection, conflict boundaries, technology refinements, and rejected overengineering. The scenario YAML remains executable test input in the repository.", + "\n".join(rows), + len(rows), + ), + ) + + +def main() -> None: + if OUT.exists(): + shutil.rmtree(OUT) + shutil.copytree(SITE_SOURCE, OUT) + + for filename in ROOT_DOCS: + shutil.copy2(ROOT / filename, OUT / filename) + for relative in MARKDOWN_TREES: + copy_markdown_tree(relative) + + generate_principles() + generate_component_hubs() + generate_evaluations() + print(f"Generated MkDocs source at {OUT.relative_to(ROOT)}") + + +if __name__ == "__main__": + main()