diff --git a/_index/index.html b/_index/index.html index 892e838..a6e6416 100644 --- a/_index/index.html +++ b/_index/index.html @@ -86,7 +86,8 @@

Standards

The standardized application blueprint and the internal library registry.

- view docs + view wiki + view lib
diff --git a/build.py b/build.py new file mode 100644 index 0000000..6df817f --- /dev/null +++ b/build.py @@ -0,0 +1,193 @@ +#!/usr/bin/env python +"""Build hand-authored documentation folders from their markdown sources. + +Companion to collect.py. While collect.py pulls *finished* HTML and MCP bundles +out of source repos, build.py compiles folders that keep their sources in this +hub. Any top-level folder with ``src/mkdocs.*.yml`` gets, for each config: + +1. a MkDocs HTML build into the target dir the config declares + (e.g. ``standards/src/mkdocs.wiki.yml`` -> ``standards/wiki/``), and +2. a doc-forge-shaped MCP bundle under ``mcp//`` + (``index.json``, ``nav.json``, ``modules/*.json``), regenerated from the + lib pages (``src/lib/*.md``) so the bundle mirrors the lib site. + +Each MkDocs config owns a dedicated site dir (declared via ``site_dir``), so +builds never touch ``src/`` or any other kind's output. + +Usage: + python build.py # build every folder with src/mkdocs.*.yml + python build.py --name standards + python build.py --dry-run +""" +from __future__ import annotations + +import argparse +import json +import re +import shutil +import subprocess +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parent + +NUM_PREFIX = re.compile(r"^\d+[_-]?") + + +def find_src_dirs(root: Path = ROOT) -> list[Path]: + """Return top-level folders containing ``src/mkdocs.*.yml``. + + Skips hidden directories, the mcp bundle container, and the generated + portal index. + """ + found: list[Path] = [] + for child in sorted(root.iterdir()): + if not child.is_dir() or child.name.startswith("."): + continue + if child.name in ("mcp", "_index"): + continue + src = child / "src" + if src.is_dir() and list(src.glob("mkdocs.*.yml")): + found.append(child) + return found + + +def build_html(folder: Path, dry_run: bool = False) -> None: + """Compile each ``src/mkdocs.*.yml`` config of ``folder`` into HTML. + + Each config declares its own ``docs_dir`` and ``site_dir``, mirroring + doc-forge (``mkdocs.wiki.yml`` -> ``/wiki/``, ``mkdocs.lib.yml`` -> + ``/lib/``). + + Args: + folder (Path): The hub folder owning ``src/mkdocs.*.yml``. + dry_run (bool): Print the plan without executing. + """ + name = folder.name + for cfg in sorted((folder / "src").glob("mkdocs.*.yml")): + site = cfg.name.removeprefix("mkdocs.").removesuffix(".yml") + if dry_run: + print(f"[BLD] {name} mkdocs build -f {cfg.relative_to(ROOT)} " + f"(-> {name}/{site}/)") + continue + + result = subprocess.run( + [sys.executable, "-m", "mkdocs", "build", "-f", str(cfg)], + capture_output=True, + text=True, + ) + if result.returncode != 0: + print(f"[ERR] {name} mkdocs failed:\n{result.stdout}\n{result.stderr}", + file=sys.stderr) + raise SystemExit(1) + print(f"[BLD] {name} {site} site built into {folder.relative_to(ROOT)}/{site}/") + + +# ── MCP bundle ─────────────────────────────────────────────────────────────── + + +def _module_name(stem: str) -> str: + """Map a markdown filename to a module name (strip the NN_ prefix).""" + return NUM_PREFIX.sub("", stem) or stem + + +def _read_title(path: Path) -> str: + """Return the first markdown H1 title, or a prettified fallback.""" + for line in path.read_text(encoding="utf-8").splitlines(): + if line.startswith("# "): + return line[2:].strip() + return _module_name(path.stem).replace("_", " ").title() + + +def build_bundle(folder: Path, dry_run: bool = False) -> None: + """Regenerate ``mcp/`` from ``folder/src/lib`` markdown. + + The bundle mirrors the lib site (in the same way doc-forge's MCP output + mirrors its lib documentation). Writes ``index.json``, ``nav.json`` and + ``modules/*.json`` in the doc-forge MCP bundle shape. Hand-authored extras + (e.g. ``libraries.json``) are left untouched. + + Args: + folder (Path): The hub folder owning a ``src/lib``. + dry_run (bool): Print the plan without executing. + """ + name = folder.name + lib = folder / "src" / "lib" + if not lib.is_dir(): + return + out_dir = ROOT / "mcp" / name + modules_dir = out_dir / "modules" + if dry_run: + print(f"[MCP] {name} {lib.relative_to(ROOT)}/*.md " + f"-> {out_dir.relative_to(ROOT)}/") + return + + if modules_dir.exists(): + shutil.rmtree(modules_dir) + modules_dir.mkdir(parents=True, exist_ok=True) + + def write_module(module: str, body: str) -> None: + payload = { + "module": module, + "content": { + "path": module, + "docstring": body.strip() + "\n", + "objects": {}, + }, + } + (modules_dir / f"{module}.json").write_text( + json.dumps(payload, indent=2, ensure_ascii=False), encoding="utf-8" + ) + + nav = [] + for path in sorted(lib.glob("*.md")): + module = _module_name(path.stem) + write_module(module, path.read_text(encoding="utf-8")) + nav.append({ + "module": module, + "resource": f"doc://modules/{module}", + "title": _read_title(path), + }) + + (out_dir / "nav.json").write_text( + json.dumps(nav, indent=2, ensure_ascii=False), encoding="utf-8" + ) + (out_dir / "index.json").write_text( + json.dumps( + { + "project": name, + "type": name, + "modules_count": len(nav), + "source": "hand-authored", + }, + indent=2, + ensure_ascii=False, + ), + encoding="utf-8", + ) + print(f"[MCP] {name} bundle written to {out_dir.relative_to(ROOT)}/ ({len(nav)} modules)") + + +# ── CLI ────────────────────────────────────────────────────────────────────── + + +def main() -> None: + parser = argparse.ArgumentParser( + description="Build hub-authored doc folders (src/ -> HTML + MCP bundle)" + ) + parser.add_argument("--name", help="Only build a single folder") + parser.add_argument("--dry-run", action="store_true", help="Show the plan without writing") + args = parser.parse_args() + + folders = [f for f in find_src_dirs() if not args.name or f.name == args.name] + if not folders: + print("No src/ doc folders found. Run build.py from the hub root.") + return + + for folder in folders: + build_bundle(folder, dry_run=args.dry_run) + build_html(folder, dry_run=args.dry_run) + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/collect.py b/collect.py index 0e38679..532ac30 100644 --- a/collect.py +++ b/collect.py @@ -336,7 +336,12 @@ def collect(config: dict, dry_run: bool = False) -> list[str]: if (entry.get("docs") or {}).get(kind): kind_urls[(name, kind)] = f"/{name}/{_find_home_for_kind(dest, kind)}" for entry in config.get("static", []): - home_urls[entry["repo"]] = f"/{entry['repo']}/{_find_home(ROOT / entry['repo'])}" + name = entry["repo"] + dest = ROOT / name + home_urls[name] = f"/{name}/{_find_home(dest)}" + for kind in ("lib", "api", "wiki"): + if (entry.get("docs") or {}).get(kind): + kind_urls[(name, kind)] = f"/{name}/{_find_home_for_kind(dest, kind)}" # ── regenerate outputs ────────────────────────────────────────────── index_html = build_index_html(config, home_urls, kind_urls) diff --git a/config.yml b/config.yml index 7421085..ebf31e1 100644 --- a/config.yml +++ b/config.yml @@ -128,6 +128,9 @@ static: description: The standardized application blueprint and the internal library registry. section: standards path: standards + docs: + lib: yes + wiki: yes - kind: api repo: blog diff --git a/mcp/standards/index.json b/mcp/standards/index.json index 0ec1ff3..b06d01e 100644 --- a/mcp/standards/index.json +++ b/mcp/standards/index.json @@ -1,6 +1,6 @@ { "project": "standards", "type": "standards", - "modules_count": 14, + "modules_count": 11, "source": "hand-authored" } \ No newline at end of file diff --git a/mcp/standards/modules/build_recipe.json b/mcp/standards/modules/build_recipe.json deleted file mode 100644 index 272205b..0000000 --- a/mcp/standards/modules/build_recipe.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "module": "build_recipe", - "content": { - "path": "build_recipe", - "docstring": "# Build Recipe\n\n## Summary\n\nFollow this ordered checklist to generate a new application of the standard\nshape. Every step is deterministic; only steps 2 and 7 require app-specific\nauthoring.\n\n---\n\n## 0. Query the registry\n\nBefore writing anything, read the **library registry** and note which internal\nlibraries provide DB, DI, routing, ingestion, auth, and docs. Do not\nreimplement them.\n\n## 1. Scaffold the repository\n\nCopy the `templates/` skeleton. It provides: `pyproject.toml`, `openapi.yaml`,\n`main.py`, `init.py`, `worker.py`, `config.py`, `Dockerfile`,\n`docker-compose.yaml`, `.env.example`, and the package tree\n(`domain/ infra/ interfaces/ pipelines/ tests/ scripts/`).\n\n## 2. Author the spec *(app-specific)*\n\nWrite `openapi.yaml`: resources, schemas, `operationId`s, security, and any\nextension metadata. This is the contract.\n\n## 3. Write each CRUD slice *(app-specific)*\n\nFor each resource, create the four-file slice (see **CRUD Template**):\n`models.py`, `ports/db.py`, `ports/service.py`, optional `converters.py`.\n\n## 4. Wire the composition root\n\nIn `init.py`: build leaf repositories, then dependent repositories, then\nservices; add infrastructure adaptors (cache store, progress bus, stores,\nfetchers). Add `get_svc` overloads.\n\n## 5. Add routes\n\nCreate `interfaces//routes.py` (thin handlers) and re-export them in\n`interfaces/openapi.py`. Add a cache decorator to read endpoints.\n\n## 6. Bootstrap the app and worker\n\nConfirm `main.py` wires the OpenAPI-first app with the lifespan. If the app has\nlong-running jobs, adapt `worker.py` to poll the relevant job collection.\n\n## 7. Add a pipeline family *(only if data-driven)*\n\nCreate `pipelines//` with `abc.py`, `impl.py`, `pipeline.yaml`,\n`resources/`, and register the root in `registry.py`. Add fixtures and a parity\ntest.\n\n## 8. Add infrastructure as needed\n\nOnly where the standard adaptors do not cover the need. Prefer library-backed\nadapters.\n\n## 9. Tests\n\nAdd integration tests with fakes and unit tests for domain/pipeline logic; add\na config-consistency test if using rule-driven pipelines.\n\n## 10. Scripts and seeds\n\nAdd operator scripts under `scripts/` that use `init()` / `get_svc()` /\n`shutdown()`.\n\n## 11. Docs\n\nAuthor GSDFC docstrings, generate the type stubs (`.pyi`), mark the package\n`py.typed`, and build docs.\n\n## Definition of done\n\n- `python -c \"import main\"` succeeds.\n- `pytest` passes.\n- The app serves the spec's operations; the worker drains its queue.\n- No domain module imports a web framework.\n- No infrastructure was reimplemented that a library already provides.\n", - "objects": {} - } -} \ No newline at end of file diff --git a/mcp/standards/modules/index.json b/mcp/standards/modules/index.json index 7fe6b80..6ea50b1 100644 --- a/mcp/standards/modules/index.json +++ b/mcp/standards/modules/index.json @@ -2,7 +2,7 @@ "module": "index", "content": { "path": "index", - "docstring": "# The Aetos Standard App Blueprint\n\nA single, reusable architecture for backend applications in the Aetos ecosystem.\n\nEvery application built with this blueprint has the same shape:\n\n- a **spec-first HTTP layer** driven by an OpenAPI document;\n- a **domain layer** of pure models, ports, and services;\n- an **infrastructure layer** of adapters (MongoDB, cache, workers, LLM, mail);\n- a **composition root** that wires everything in exactly one place;\n- an optional **pipeline layer** for data-driven extraction/enrichment;\n- an optional **background worker** that runs long-running jobs and reports progress.\n\n> This blueprints is deliberately **app-agnostic**. It never names a concrete\n> business domain. Any application — expenses, health metrics, inventory,\n> analytics — is produced by filling in **two axes only**: the resource model\n> and (if needed) the pipeline family.\n\n## How to read this\n\n| Page | What it covers |\n|---|---|\n| Overview | The philosophy and the one-way dependency rule |\n| Layering Rules | Which package may import which |\n| Composition Root | The single wiring point |\n| Spec-First Routing | OpenAPI as the source of truth |\n| CRUD Template | The canonical resource slice |\n| Pipeline Scaffold | Data-driven processing families |\n| Worker & Progress | Async jobs, status machine, streaming progress |\n| Human-in-the-Loop | Pausing a job to ask a question |\n| Infra Adapters | MongoDB, cache, stores, LLM, mail |\n| Testing | Integration + unit conventions |\n| Build Recipe | The ordered checklist to generate a new app |\n| Genericity Matrix | What is reusable vs what you must write |\n\n## For AI agents\n\nThis content is served as MCP resources under `docs://index`, `docs://nav`,\nand `docs://modules/{module}`. To build a new app:\n\n1. Read `build_recipe` first.\n2. Read `genericity_matrix` to know what you must author.\n3. Query the **library registry** (`find_library`) before writing any\n infrastructure yourself.\n4. Copy the `templates/` skeleton and fill the two app-specific axes.\n", + "docstring": "# Standards Reference\n\nThe reference half of the Standards description: the enforceable rules,\npatterns, and the internal library catalog that agents consult while coding.\n\n- **Layering Rules** — the strict request → domain → repository layering and\n the rules that enforce it.\n- **Composition Root** — a single composition point wiring the whole app.\n- **Spec-First Routing** — the OpenAPI contract as the routing source of truth.\n- **CRUD Template** — the standardized generic resource endpoint scaffold.\n- **Pipeline Scaffold** — the deterministic pipeline skeleton for multi-stage\n jobs.\n- **Worker & Progress** — queued work, progress reporting, and visibility.\n- **Human-in-the-Loop** — guardrails and approvals inside automations.\n- **Infra Adapters** — how infrastructure connects without leaking into domain.\n- **Genericity Matrix** — what is generic vs app-specific.\n- **Library Registry** — the catalog of internal libraries behind all of it.\n\nRead the **Wiki** for the philosophy and rationale; these pages are the\nreference that mirrors the `standards` MCP bundle.\n", "objects": {} } } \ No newline at end of file diff --git a/mcp/standards/modules/overview.json b/mcp/standards/modules/overview.json deleted file mode 100644 index 2fcdf91..0000000 --- a/mcp/standards/modules/overview.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "module": "overview", - "content": { - "path": "overview", - "docstring": "# Overview\n\n## Summary\n\nThe blueprint is a **hexagonal (ports and adapters) application** with a\n**spec-first** web edge and a **single composition root**. It favors explicit\nwiring over implicit magic, and it prefers existing internal libraries over\nhand-written infrastructure.\n\n---\n\n## The three layers\n\n```\ninterfaces/ ← HTTP edge: thin handlers bound to an OpenAPI operationId\ndomain/ ← business core: models, ports (interfaces), services, orchestrators\ninfra/ ← adapters: databases, caches, external clients, hardware\n```\n\nPlus two optional layers:\n\n```\npipelines/ ← data-driven transformation graphs (extraction, enrichment)\nscripts/ ← operator scripts that reuse the composition root\n```\n\n## Core principles\n\n1. **One-way dependencies.** `interfaces -> domain -> infra` is allowed;\n the reverse is forbidden. The domain never imports a framework it does not\n own.\n2. **Ports in the domain, adapters in infra.** A service depends on an\n abstract port; the composition root injects a concrete adapter.\n3. **One composition root.** Nothing constructs its own dependencies. All\n wiring lives in one module (`init.py`) and is exposed as a flat resource\n registry.\n4. **Spec-first.** The HTTP contract is declared in an OpenAPI document. Route\n handlers are looked up by `operationId`. There are no decorator routers.\n5. **Reuse the libraries.** Pipeline orchestration, DI, Mongo repositories,\n ingestion, auth, and docs are provided by internal libraries. Do not\n reimplement them — see the library registry.\n6. **Explicit state machines.** Long-running work moves through explicit,\n durable statuses and can pause for human input.\n\n## Runtime processes\n\nA standard app may run as up to three processes that share the same\ncomposition root:\n\n| Process | Entry point | Responsibility |\n|---|---|---|\n| API | `main.py` | serve HTTP; start/stop resources via lifespan |\n| Worker | `worker.py` | poll durable jobs, run pipelines, publish progress |\n| Scripts | `python -m scripts.` | one-shot operator tasks |\n", - "objects": {} - } -} \ No newline at end of file diff --git a/mcp/standards/modules/testing.json b/mcp/standards/modules/testing.json deleted file mode 100644 index fc2e281..0000000 --- a/mcp/standards/modules/testing.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "module": "testing", - "content": { - "path": "testing", - "docstring": "# Testing\n\n## Summary\n\nTwo test suites: **integration** (the HTTP edge with fakes) and **unit** (pure\ndomain and pipeline logic). Both run under pytest with asyncio auto mode.\n\n---\n\n## Layout\n\n```\ntests/\n integration/ # TestClient(app) + mocked resources\n unit/ # models, converters, pipeline stages/roots\n conftest.py # fixtures\n```\n\n## Integration tests\n\n- Build the app with `TestClient`.\n- Replace `app.state.resources` with fake repositories/services so no database\n is required.\n- Patch the auth introspection call (`httpx.AsyncClient.post`) to return a\n canned identity.\n- Assert status codes and bodies for list/get/create/update/delete, plus\n `400`, `404`, `204`, and the streaming endpoint's fallback.\n\n```python\nclient = TestClient(app)\napp.state.resources = {\"item_svc\": FakeItemService(), ...}\n\ndef test_create_item():\n r = client.post(\"/items\", json={\"name\": \"x\"})\n assert r.status_code == 200\n```\n\n## Unit tests\n\n- Domain models and converters: pure, no I/O.\n- Pipeline stages: feed a fixture state, assert the transformed state.\n- Assembled pipeline roots: **fixture parity** — replay a stored fixture input\n and compare against a golden output file.\n\n## Config-consistency tests\n\nFor rule-driven pipelines, add a test that cross-references rules against the\ndeclared pattern registry: every flag emitted must be defined; every defined\nflag must be reachable. This catches drift without running the pipeline.\n\n## Conventions\n\n- `asyncio_mode = \"auto\"`; no manual event loops.\n- Fixtures are checked into the repo (inputs + golden outputs).\n- Tests skip (not fail) when a required fixture is absent.\n- Do not mock the domain; mock only the infrastructure edge.\n", - "objects": {} - } -} \ No newline at end of file diff --git a/mcp/standards/nav.json b/mcp/standards/nav.json index 50082d9..6a08db3 100644 --- a/mcp/standards/nav.json +++ b/mcp/standards/nav.json @@ -1,72 +1,57 @@ [ - { - "module": "index", - "resource": "docs://modules/index", - "title": "Home" - }, - { - "module": "overview", - "resource": "docs://modules/overview", - "title": "Overview" - }, { "module": "layering_rules", - "resource": "docs://modules/layering_rules", + "resource": "doc://modules/layering_rules", "title": "Layering Rules" }, { "module": "composition_root", - "resource": "docs://modules/composition_root", + "resource": "doc://modules/composition_root", "title": "Composition Root" }, { "module": "spec_first_routing", - "resource": "docs://modules/spec_first_routing", + "resource": "doc://modules/spec_first_routing", "title": "Spec-First Routing" }, { "module": "crud_template", - "resource": "docs://modules/crud_template", + "resource": "doc://modules/crud_template", "title": "CRUD Template" }, { "module": "pipeline_scaffold", - "resource": "docs://modules/pipeline_scaffold", + "resource": "doc://modules/pipeline_scaffold", "title": "Pipeline Scaffold" }, { "module": "worker_and_progress", - "resource": "docs://modules/worker_and_progress", + "resource": "doc://modules/worker_and_progress", "title": "Worker & Progress" }, { "module": "human_in_loop", - "resource": "docs://modules/human_in_loop", + "resource": "doc://modules/human_in_loop", "title": "Human-in-the-Loop" }, { "module": "infra_adaptors", - "resource": "docs://modules/infra_adaptors", + "resource": "doc://modules/infra_adaptors", "title": "Infra Adapters" }, - { - "module": "testing", - "resource": "docs://modules/testing", - "title": "Testing" - }, - { - "module": "build_recipe", - "resource": "docs://modules/build_recipe", - "title": "Build Recipe" - }, { "module": "genericity_matrix", - "resource": "docs://modules/genericity_matrix", + "resource": "doc://modules/genericity_matrix", "title": "Genericity Matrix" }, { "module": "library_registry", - "resource": "docs://modules/library_registry", + "resource": "doc://modules/library_registry", "title": "Library Registry" + }, + { + "module": "index", + "resource": "doc://modules/index", + "title": "Standards Reference" } ] \ No newline at end of file diff --git a/standards/02_layering_rules/index.html b/standards/lib/02_layering_rules/index.html similarity index 94% rename from standards/02_layering_rules/index.html rename to standards/lib/02_layering_rules/index.html index 500b884..b720776 100644 --- a/standards/02_layering_rules/index.html +++ b/standards/lib/02_layering_rules/index.html @@ -6,12 +6,12 @@ - + - + @@ -22,7 +22,7 @@ - Layering Rules - Aetos Standards + Layering Rules - Aetos Standards — Reference @@ -90,7 +90,7 @@