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 @@