docs(standards): vendor template into MCP bundle; add component tools + start_app prompt

This commit is contained in:
2026-10-12 21:24:10 +05:30
parent 715bfd8022
commit 2d1278911c
68 changed files with 2044 additions and 102 deletions

146
build.py
View File

@@ -168,6 +168,151 @@ def build_bundle(folder: Path, dry_run: bool = False) -> None:
print(f"[MCP] {name} bundle written to {out_dir.relative_to(ROOT)}/ ({len(nav)} modules)")
# ── Template bundle ──────────────────────────────────────────────────────────
# Component kinds shipped in the vendored template. ``placeholder`` names the
# seed token an agent renames: "resource" -> item/Item, "family" -> example/
# Example; "" means ship as-is.
_TEMPLATE_COMPONENTS: list[dict[str, str]] = [
# Per-resource slice (seed: item/Item).
{"kind": "model", "path": "domain/item/models.py", "placeholder": "resource",
"description": "Business Pydantic model for a resource."},
{"kind": "ports-db", "path": "domain/item/ports/db.py", "placeholder": "resource",
"description": "Document model + repository factory for a resource."},
{"kind": "ports-service", "path": "domain/item/ports/service.py", "placeholder": "resource",
"description": "CRUD service wired through the DI library."},
{"kind": "converters", "path": "domain/item/ports/converters.py", "placeholder": "resource",
"description": "Optional pure converters between domain types."},
{"kind": "routes", "path": "interfaces/item/routes.py", "placeholder": "resource",
"description": "Thin HTTP handlers bound to operationIds."},
{"kind": "test-model", "path": "tests/unit/test_item_models.py", "placeholder": "resource",
"description": "Unit test for the resource model."},
{"kind": "test-routes", "path": "tests/integration/test_item_routes.py", "placeholder": "resource",
"description": "Integration test at the HTTP edge."},
# Per-family pipeline scaffold (seed: example/Example).
{"kind": "pipeline-abc", "path": "pipelines/example/abc.py", "placeholder": "family",
"description": "Port contracts for a pipeline family."},
{"kind": "pipeline-impl", "path": "pipelines/example/impl.py", "placeholder": "family",
"description": "Concrete stages, wired root, and family registration."},
{"kind": "pipeline-yaml", "path": "pipelines/example/pipeline.yaml", "placeholder": "family",
"description": "Graph spec (nodes, edges, roots, schema)."},
{"kind": "pipeline-resources", "path": "pipelines/example/resources/pipeline.yaml", "placeholder": "family",
"description": "Runtime spec consumed by the shared loader."},
{"kind": "pipeline-pattern", "path": "pipelines/example/resources/patterns/example.yaml", "placeholder": "family",
"description": "Pattern/rule fragment merged by the loader."},
# Shared pipeline scaffolding (ship as-is).
{"kind": "pipeline-ports", "path": "pipelines/shared/ports.py", "placeholder": "",
"description": "Shared pipeline ports (FinalizePort, RegistryPort)."},
{"kind": "pipeline-loader", "path": "pipelines/shared/loader.py", "placeholder": "",
"description": "Shared loader: DI wiring + pattern merge."},
{"kind": "pipeline-registry", "path": "pipelines/shared/registry.py", "placeholder": "",
"description": "Shared registry: (kind, variant, mode) -> root class."},
{"kind": "pipeline-simple", "path": "pipelines/item_sync.py", "placeholder": "",
"description": "Smallest pipeline form (plain async steps)."},
# Second resource slice (seed: job).
{"kind": "job-model", "path": "domain/job/models.py", "placeholder": "",
"description": "Seeded second resource model (job)."},
{"kind": "job-ports-db", "path": "domain/job/ports/db.py", "placeholder": "",
"description": "Seeded second resource repository (job)."},
{"kind": "job-ports-service", "path": "domain/job/ports/service.py", "placeholder": "",
"description": "Seeded second resource service (job)."},
{"kind": "job-routes", "path": "interfaces/job/routes.py", "placeholder": "",
"description": "Seeded second resource routes (job)."},
# Root / shared files (ship as-is).
{"kind": "config", "path": "config.py", "placeholder": "",
"description": "Settings dataclass + singleton."},
{"kind": "init", "path": "init.py", "placeholder": "",
"description": "Composition root (init/shutdown, get_svc)."},
{"kind": "main", "path": "main.py", "placeholder": "",
"description": "Spec-first app bootstrap + lifespan."},
{"kind": "worker", "path": "worker.py", "placeholder": "",
"description": "Poll loop + job status machine."},
{"kind": "openapi", "path": "openapi.yaml", "placeholder": "",
"description": "OpenAPI contract skeleton."},
{"kind": "pyproject", "path": "pyproject.toml", "placeholder": "",
"description": "Packaging + dependency pins."},
{"kind": "dockerfile", "path": "Dockerfile", "placeholder": "",
"description": "API + worker image with private-index wiring."},
{"kind": "compose", "path": "docker-compose.yaml", "placeholder": "",
"description": "api + worker + mongo compose file."},
{"kind": "env-example", "path": ".env.example", "placeholder": "",
"description": "Environment variable template."},
{"kind": "db-setup", "path": "infra/db/setup.py", "placeholder": "",
"description": "Mongo connection lifecycle."},
{"kind": "repo", "path": "infra/db/repos/mongo.py", "placeholder": "",
"description": "Repository factory (base/populating/cached)."},
{"kind": "service-adapter", "path": "infra/adapters/service.py", "placeholder": "",
"description": "Generic CRUD service adaptor."},
{"kind": "script-seed", "path": "scripts/seed.py", "placeholder": "",
"description": "Operator seed script."},
{"kind": "script-drop", "path": "scripts/drop.py", "placeholder": "",
"description": "Operator drop script."},
]
def build_components_manifest(template_root: Path) -> dict[str, object]:
"""Describe every component in the vendored template.
Args:
template_root (Path): The vendored ``templates/app`` directory.
Returns:
dict[str, object]: Manifest with the seed tokens and component entries.
"""
components: list[dict[str, str]] = []
for entry in _TEMPLATE_COMPONENTS:
rel = entry["path"]
if not (template_root / rel).exists():
raise SystemExit(f"[ERR] component path missing from template: {rel}")
components.append({
"kind": entry["kind"],
"path": rel,
"placeholder": entry["placeholder"],
"description": entry["description"],
})
return {
"project": "standards",
"template": "templates/app",
"seed": {"resource": "item", "family": "example"},
"components": components,
}
def build_templates(folder: Path, dry_run: bool = False) -> None:
"""Vendor ``folder/src/templates/app`` into ``<name>/mcp/templates/app``.
Ships the runnable skeleton in the MCP bundle so an agent can pull exact,
current component code, and emits ``components.json`` describing the kinds.
Excludes ``__pycache__`` / ``*.pyc``.
Args:
folder (Path): The hub folder owning ``src/templates/app``.
dry_run (bool): Print the plan without executing.
"""
name = folder.name
src = folder / "src" / "templates" / "app"
if not src.is_dir():
return
out_dir = folder / "mcp" / "templates" / "app"
if dry_run:
print(f"[TPL] {name} {src.relative_to(ROOT)}/** -> {out_dir.relative_to(ROOT)}/**")
return
if out_dir.exists():
shutil.rmtree(out_dir)
out_dir.parent.mkdir(parents=True, exist_ok=True)
shutil.copytree(
src, out_dir,
ignore=shutil.ignore_patterns("__pycache__", "*.pyc"),
)
manifest = build_components_manifest(out_dir)
(folder / "mcp" / "components.json").write_text(
json.dumps(manifest, indent=2, ensure_ascii=False), encoding="utf-8"
)
print(f"[TPL] {name} template vendored to {out_dir.relative_to(ROOT)}/ "
f"({len(manifest['components'])} components)")
# ── CLI ──────────────────────────────────────────────────────────────────────
@@ -186,6 +331,7 @@ def main() -> None:
for folder in folders:
build_bundle(folder, dry_run=args.dry_run)
build_templates(folder, dry_run=args.dry_run)
build_html(folder, dry_run=args.dry_run)

View File

@@ -1,20 +1,28 @@
"""Standards MCP server.
Serves the hand-authored `standards` bundle (the application blueprint plus the
library registry) and adds a discovery tool so an agent can find the right
internal library — and its MCP endpoint — before writing any infrastructure.
library registry and the runnable template) and adds discovery/scaffolding
helpers so an agent can find the right internal library — and pull exact
template code — before writing any infrastructure.
Resources:
doc://index, doc://nav, doc://modules/{module} (from the base server)
registry://libraries (the structured catalog)
doc://index, doc://nav, doc://modules/{module} (from the base server)
registry://libraries (the structured catalog)
registry://components (the template component index)
Tools:
ping (from the base server)
find_library (capability -> matching libraries + MCP endpoints)
ping (from the base server)
find_library (capability -> matching libraries + MCP endpoints)
list_app_components (the template component kinds)
get_app_component (kind -> exact template file, seed tokens renamed)
Prompts:
start_app (collect the two axes, then emit the build prompt)
"""
from __future__ import annotations
import json
import re
from pathlib import Path
from typing import Any
@@ -22,35 +30,85 @@ from config import MCPBundle
from mcp_server import MCPServer
def _pascal(name: str) -> str:
"""Convert a snake/kebab name to PascalCase."""
return "".join(part.capitalize() for part in re.split(r"[_\-\s]+", name) if part)
class StandardsMCPServer(MCPServer):
"""MCP server for the `standards` bundle with a library-discovery tool.
"""MCP server for the `standards` bundle with discovery/scaffolding tools.
Attributes:
mcp_root (Path):
Directory containing the generated bundle (index.json, nav.json,
modules/, and the machine-readable libraries.json).
modules/, libraries.json, components.json, and templates/).
"""
def __init__(self, bundle: MCPBundle) -> None:
super().__init__(bundle)
self._register_standards()
# ------------------------------------------------------------------
# Bundle readers
# ------------------------------------------------------------------
def _catalog(self) -> dict[str, Any]:
"""Load the machine-readable library registry."""
path = Path(self.mcp_root) / "libraries.json"
if not path.exists():
return {
"error": "not_found",
"path": str(path),
"libraries": [],
}
return {"error": "not_found", "path": str(path), "libraries": []}
return json.loads(path.read_bytes())
def _components(self) -> dict[str, Any]:
"""Load the template component manifest."""
path = Path(self.mcp_root) / "components.json"
if not path.exists():
return {"error": "not_found", "path": str(path), "components": []}
return json.loads(path.read_bytes())
def _find_component(self, kind: str) -> dict[str, Any] | None:
for entry in self._components().get("components", []):
if entry.get("kind") == kind:
return entry
return None
def _read_component(
self,
entry: dict[str, Any],
resource: str | None,
family: str | None,
) -> dict[str, Any]:
"""Read a vendored template file, renaming seed tokens when asked."""
rel = entry["path"]
path = Path(self.mcp_root) / "templates" / "app" / rel
if not path.exists():
return {"error": "not_found", "path": str(path)}
text = path.read_text(encoding="utf-8")
placeholder = entry.get("placeholder")
if resource and placeholder == "resource":
text = text.replace("Item", _pascal(resource)).replace("item", resource)
if family and placeholder == "family":
text = text.replace("Example", _pascal(family)).replace("example", family)
return {
"kind": entry["kind"],
"path": rel,
"placeholder": placeholder,
"content": text,
}
# ------------------------------------------------------------------
# Standards surface
# ------------------------------------------------------------------
def _register_standards(self) -> None:
@self.app.resource("registry://libraries")
def libraries() -> dict[str, Any]:
return self._catalog()
@self.app.resource("registry://components")
def components() -> dict[str, Any]:
return self._components()
@self.app.tool()
def find_library(capability: str) -> list[dict[str, Any]]:
"""Find internal libraries matching a capability keyword.
@@ -85,3 +143,96 @@ class StandardsMCPServer(MCPServer):
if needle in haystack:
matches.append(lib)
return matches
@self.app.tool()
def list_app_components() -> list[dict[str, Any]]:
"""List the reusable component kinds shipped in the app template.
Returns:
list[dict[str, Any]]:
Entries with ``kind``, ``path``, ``placeholder`` and
``description``. Fetch one with :func:`get_app_component`.
"""
return self._components().get("components", [])
@self.app.tool()
def get_app_component(
kind: str,
resource: str | None = None,
family: str | None = None,
) -> dict[str, Any]:
"""Return the exact template file for a component kind.
Use this to copy current, app-neutral skeleton code instead of
inventing it. See :func:`list_app_components` for valid kinds.
Args:
kind (str): Component kind, e.g. "routes", "pipeline-impl".
resource (str | None): Rename the resource seed ("item"/"Item")
to this name (snake_case), for per-resource kinds.
family (str | None): Rename the family seed ("example"/"Example")
to this name (snake_case), for per-family kinds.
Returns:
dict[str, Any]: ``{kind, path, placeholder, content}`` or an
error entry when the kind is unknown.
"""
entry = self._find_component(kind)
if entry is None:
return {
"error": "unknown_kind",
"kind": kind,
"available": [c["kind"] for c in self._components().get("components", [])],
}
return self._read_component(entry, resource, family)
@self.app.prompt()
def start_app(
app_name: str = "",
resources: str = "",
pipelines: str = "",
auth: str = "no",
) -> str:
"""Entry point to build a new app from the standards blueprint.
Call with no arguments to get the intake questions; call again with
the answers to get the deterministic build prompt.
Args:
app_name (str): App name + one-line purpose.
resources (str): Axis 1 — resources, fields, relationships
(or a pasted openapi.yaml).
pipelines (str): Axis 2 — pipeline families + input formats.
auth (str): Whether auth is needed now ("yes"/"no").
Returns:
str: The intake questions, or the build prompt when complete.
"""
if not app_name or not resources:
return (
"# Start a new app — intake\n\n"
"Gather the following, then call `start_app` again with the values:\n\n"
"1. App name + one-line purpose.\n"
"2. Axis 1 — resources: each resource, its fields, and relationships\n"
" (or paste `openapi.yaml`).\n"
"3. Axis 2 — pipelines: multi-step inputs? families + formats (or `none`).\n"
"4. Auth needed now? (yes/no; auth is deferred — `py-jwt` only).\n"
"5. Integrations: Mongo / ingestion formats / email / LLM provider.\n\n"
"Then: `start_app(app_name=..., resources=..., pipelines=..., auth=...)`.\n"
)
return (
f"# Build: {app_name}\n\n"
"## Spec\n"
f"- resources: {resources}\n"
f"- pipelines: {pipelines or 'none'}\n"
f"- auth: {auth}\n\n"
"## Method (deterministic)\n"
"1. Read `doc://modules/build_recipe` and follow it step by step.\n"
"2. Read `doc://modules/genericity_matrix` (Tier A/B/C) and "
"`doc://modules/layering_rules`.\n"
"3. Call `find_library(capability)` for db, di, routing, ingestion, "
"and pipeline; never reimplement a Tier A library.\n"
"4. Pull exact files with `list_app_components()` and "
"`get_app_component(kind, resource=..., family=...)`.\n"
"5. Author only Tier C; keep Tier B as shipped.\n"
)

View File

@@ -691,7 +691,9 @@
<h1 id="infra-adapters">Infra Adapters</h1>
<h2 id="summary">Summary</h2>
<p>All external systems are reached through adapters in <code>infra/</code>. Prefer an
existing internal library over writing a new adapter.</p>
existing internal library over writing a new adapter; where no library exists,
copy the Tier B scaffold from <code>templates/app</code>. See the Genericity Matrix for the
Tier A / B / C split.</p>
<hr />
<h2 id="database-mongodb">Database (MongoDB)</h2>
<ul>
@@ -720,9 +722,10 @@ existing internal library over writing a new adapter.</p>
<ul>
<li>Transactions use a session context manager provided by the library.</li>
</ul>
<p><strong>Library:</strong> the Mongo operations library (see registry). Do not write raw
Motor calls in the domain.</p>
<p><strong>Tier A — Library (Mongo Ops).</strong> Do not write raw Motor calls in the domain;
consult the registry and the library's MCP before touching the driver.</p>
<h2 id="http-response-cache">HTTP response cache</h2>
<p><strong>Tier B — template.</strong> Copy from <code>templates/app</code>; not a standalone library.</p>
<p>A generic GET cache sits behind a decorator.</p>
<div class="language-python highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span></span><span class="normal"><a href="#__codelineno-2-1">1</a></span>
<span class="normal"><a href="#__codelineno-2-2">2</a></span></pre></div></td><td class="code"><div><pre><span></span><code><span id="__span-2-1"><a id="__codelineno-2-1" name="__codelineno-2-1"></a><span class="nd">@cache_response</span><span class="p">(</span><span class="n">ttl</span><span class="o">=</span><span class="mi">60</span><span class="p">)</span>
@@ -735,14 +738,16 @@ Motor calls in the domain.</p>
invalidation.</li>
</ul>
<h2 id="event-progress-bus">Event / progress bus</h2>
<p><strong>Tier B — template.</strong> Copy from <code>templates/app</code>.</p>
<p>A port with an in-memory default adapter (<code>publish</code>, <code>get_events</code>). Swap for a
durable broker in production (see Worker &amp; Progress).</p>
<h2 id="external-clients-ingestion">External clients (ingestion)</h2>
<p>External fetchers (files, mail, HTTP) are adapters that wrap ingestion
libraries. Construct them in the composition root and inject into
orchestrators.</p>
<p><strong>Libraries:</strong> the ingestion library (formats) and the mail library (email).</p>
<p><strong>Tier A — Libraries:</strong> Omniread (formats) and Mail Intake (email).</p>
<h2 id="llm-clients">LLM clients</h2>
<p><strong>Tier B — template.</strong> Copy the pool scaffold from <code>templates/app</code>.</p>
<p>When a pipeline calls a model provider, route through a pool adaptor that
handles provider selection, retries, and client lifecycle:</p>
<div class="language-python highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span></span><span class="normal"><a href="#__codelineno-3-1">1</a></span>
@@ -751,11 +756,19 @@ handles provider selection, retries, and client lifecycle:</p>
</span></code></pre></div></td></tr></table></div>
<p>Providers are selected by <code>LLM_PROVIDER</code> and configured by environment.</p>
<h2 id="key-value-store">Key-value store</h2>
<p><strong>Tier B — template.</strong> Copy from <code>templates/app</code>.</p>
<p>For report/snapshot storage, provide a pluggable store port with a Redis
adapter and an in-memory fallback.</p>
<h2 id="rule">Rule</h2>
<p>If a capability already exists as a library (DB, DI, ingestion, mail, auth,
docs), <strong>use it</strong>. Writing a private copy of a library is a defect.</p>
<ul>
<li><strong>Tier A (libraries: DB, DI, ingestion, mail, auth, docs, pipelines)</strong> — use
the library; writing a private copy is a defect. Query <code>find_library</code> first.</li>
<li><strong>Tier B (cache, progress bus, LLM pool, KV store, composition shape, worker,
tests)</strong> — these are template code, not libraries: copy them from
<code>templates/app</code> rather than searching the registry.</li>
<li><strong>Tier C (models, business logic, pipeline families, rules)</strong> — author them
yourself.</li>
</ul>

View File

@@ -482,27 +482,27 @@
</li>
<li class="md-nav__item">
<a href="#reusable-as-is-do-not-rewrite" class="md-nav__link">
<a href="#tier-a-library-backed-use-never-reimplement" class="md-nav__link">
<span class="md-ellipsis">
Reusable as-is (do not rewrite)
Tier A — Library-backed (use; never reimplement)
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#template-to-adapt-copy-then-change-names" class="md-nav__link">
<a href="#tier-b-template-provided-copy-from-templatesapp-then-adapt" class="md-nav__link">
<span class="md-ellipsis">
Template to adapt (copy, then change names)
Tier B — Template-provided (copy from templates/app, then adapt)
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#app-specific-must-be-authored" class="md-nav__link">
<a href="#tier-c-app-specific-must-be-authored" class="md-nav__link">
<span class="md-ellipsis">
App-specific (must be authored)
Tier C — App-specific (must be authored)
</span>
</a>
@@ -583,27 +583,27 @@
</li>
<li class="md-nav__item">
<a href="#reusable-as-is-do-not-rewrite" class="md-nav__link">
<a href="#tier-a-library-backed-use-never-reimplement" class="md-nav__link">
<span class="md-ellipsis">
Reusable as-is (do not rewrite)
Tier A — Library-backed (use; never reimplement)
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#template-to-adapt-copy-then-change-names" class="md-nav__link">
<a href="#tier-b-template-provided-copy-from-templatesapp-then-adapt" class="md-nav__link">
<span class="md-ellipsis">
Template to adapt (copy, then change names)
Tier B — Template-provided (copy from templates/app, then adapt)
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#app-specific-must-be-authored" class="md-nav__link">
<a href="#tier-c-app-specific-must-be-authored" class="md-nav__link">
<span class="md-ellipsis">
App-specific (must be authored)
Tier C — App-specific (must be authored)
</span>
</a>
@@ -636,101 +636,162 @@
<h1 id="genericity-matrix">Genericity Matrix</h1>
<h2 id="summary">Summary</h2>
<p>This page states precisely what is <strong>reusable as-is</strong>, what is a <strong>template to
adapt</strong>, and what is <strong>app-specific and must be authored</strong>. Use it to scope a
new build.</p>
<p>This page states precisely what is <strong>library-backed</strong>, what is
<strong>template-provided</strong>, and what is <strong>app-specific and must be authored</strong>. Use it
to scope a new build and to know, for any given capability, whether to call a
library, copy template code, or write it yourself.</p>
<hr />
<h2 id="reusable-as-is-do-not-rewrite">Reusable as-is (do not rewrite)</h2>
<h2 id="tier-a-library-backed-use-never-reimplement">Tier A — Library-backed (use; never reimplement)</h2>
<p>These capabilities already exist as internal libraries. Reach them through the
library registry MCP (<code>find_library(capability)</code>) and read the library's own MCP
for its API. Writing a private copy of any of these is a defect.</p>
<table>
<thead>
<tr>
<th>Area</th>
<th>Artifact</th>
<th>Capability</th>
<th>Library</th>
<th>MCP</th>
</tr>
</thead>
<tbody>
<tr>
<td>Persistence: repositories, cache, populate, transactions</td>
<td>Mongo Ops (<code>mongo_ops</code>)</td>
<td><code>:8007</code></td>
</tr>
<tr>
<td>Dependency injection / composition</td>
<td>Hexa (<code>hexa</code>)</td>
<td><code>:8008</code></td>
</tr>
<tr>
<td>Deterministic multi-step transformation engine</td>
<td>DAG Pipe (<code>dagpipe</code>)</td>
<td><code>:8006</code></td>
</tr>
<tr>
<td>Spec-first HTTP edge, client, codegen</td>
<td>OpenAPI First (<code>openapi_first</code>)</td>
<td><code>:8004</code></td>
</tr>
<tr>
<td>Format-agnostic fetch + parse (HTML, PDF, CSV, XLSX)</td>
<td>Omniread (<code>omniread</code>)</td>
<td><code>:8003</code></td>
</tr>
<tr>
<td>Read-only email ingestion</td>
<td>Mail Intake (<code>mail_intake</code>)</td>
<td><code>:8002</code></td>
</tr>
<tr>
<td>JWT auth primitives (register, login, introspect)</td>
<td>Py JWT (<code>jwtlib</code>)</td>
<td><code>:8005</code></td>
</tr>
<tr>
<td>Documentation build (MkDocs + MCP)</td>
<td>Doc Forge (<code>docforge</code>)</td>
<td><code>:8001</code></td>
</tr>
</tbody>
</table>
<h2 id="tier-b-template-provided-copy-from-templatesapp-then-adapt">Tier B — Template-provided (copy from <code>templates/app</code>, then adapt)</h2>
<p>These are reusable internal components shipped <strong>inside</strong> the template — they are
not independent libraries. Copy them as-is, then rename/populate only what is
app-specific. They give every app the same wiring shape as the reference app.</p>
<table>
<thead>
<tr>
<th>Component</th>
<th>Template location</th>
<th>Action</th>
</tr>
</thead>
<tbody>
<tr>
<td>Layering &amp; dependency rule</td>
<td>the package layout and import rules</td>
<td>the package layout + import rules</td>
<td>keep</td>
</tr>
<tr>
<td>Composition root</td>
<td>the <code>init.py</code> shape, idempotency, <code>get_svc</code> overloading</td>
<td>Composition root shape</td>
<td><code>init.py</code> (<code>get_svc</code>/<code>get_db</code>, idempotency)</td>
<td>keep</td>
</tr>
<tr>
<td>Web edge</td>
<td><code>OpenAPIFirstApp</code> bootstrap, handler-name = <code>operationId</code>, thin handlers</td>
<td>Spec-first web edge</td>
<td><code>interfaces/openapi.py</code>, thin handlers bound to <code>operationId</code></td>
<td>keep</td>
</tr>
<tr>
<td>CRUD slice</td>
<td>the four-file pattern (model / ports-db / ports-service / converters)</td>
<td>CRUD slice pattern</td>
<td><code>domain/&lt;resource&gt;/{models.py, ports/db.py, ports/service.py, converters.py}</code></td>
<td>rename + fill</td>
</tr>
<tr>
<td>Generic CRUD service</td>
<td>the base service adaptor</td>
<td><code>infra/adapters/service.py</code> (<code>CrudsServiceAdaptor</code>)</td>
<td>keep</td>
</tr>
<tr>
<td>Repository factory</td>
<td>base / populating / cached selection</td>
<td><code>infra/db/repos/mongo.py</code> (<code>_repo_factory</code>)</td>
<td>keep</td>
</tr>
<tr>
<td>Worker</td>
<td>the poll loop, backoff, retry, status machine</td>
<td>Worker poll + status loop</td>
<td><code>worker.py</code></td>
<td>keep</td>
</tr>
<tr>
<td>Progress bus</td>
<td>the bus port + in-memory adapter + streaming endpoint</td>
<td>Progress bus + streaming</td>
<td>bus port + in-memory adapter + SSE endpoint</td>
<td>keep</td>
</tr>
<tr>
<td>Human-in-the-loop</td>
<td>the durable pause + question document + resolve endpoint + resume map</td>
<td>durable pause + question document + resolve endpoint + resume map</td>
<td>keep</td>
</tr>
<tr>
<td>Infra adaptors</td>
<td>HTTP cache decorator, KV store port, LLM pool scaffold</td>
<td>HTTP response cache</td>
<td><code>cache_response</code> decorator + generation invalidation</td>
<td>keep</td>
</tr>
<tr>
<td>Testing</td>
<td>integration harness with fakes, fixture-parity pattern</td>
<td>Key-value store</td>
<td>KV store port + Redis adapter + in-memory fallback</td>
<td>keep</td>
</tr>
<tr>
<td>Docs</td>
<td>GSDFC + generated stubs + MCP bundle pipeline</td>
<td>LLM client pool</td>
<td>provider-select + retry pool scaffold</td>
<td>keep</td>
</tr>
<tr>
<td>Pipeline scaffold</td>
<td><code>pipelines/shared/{ports,loader,registry}.py</code> + <code>pipelines/&lt;family&gt;/</code></td>
<td>rename + fill</td>
</tr>
<tr>
<td>Operator scripts</td>
<td><code>scripts/</code> (<code>init()</code> / <code>get_svc()</code> / <code>shutdown()</code>)</td>
<td>rename</td>
</tr>
<tr>
<td>Testing harness</td>
<td>integration fakes + fixture-parity pattern</td>
<td>keep</td>
</tr>
<tr>
<td>Project scaffolding</td>
<td><code>pyproject.toml</code>, <code>Dockerfile</code>, <code>docker-compose.yaml</code>, <code>.env.example</code>, <code>openapi.yaml</code></td>
<td>rename + fill</td>
</tr>
</tbody>
</table>
<h2 id="template-to-adapt-copy-then-change-names">Template to adapt (copy, then change names)</h2>
<table>
<thead>
<tr>
<th>Artifact</th>
<th>Change</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>pyproject.toml</code></td>
<td>project name, dependencies</td>
</tr>
<tr>
<td><code>Dockerfile</code> / <code>docker-compose.yaml</code></td>
<td>service name, ports, services</td>
</tr>
<tr>
<td><code>.env.example</code></td>
<td>connection strings, provider keys</td>
</tr>
<tr>
<td><code>openapi.yaml</code></td>
<td>every path/schema/operationId</td>
</tr>
<tr>
<td><code>scripts/</code></td>
<td>seed and operator tasks</td>
</tr>
</tbody>
</table>
<h2 id="app-specific-must-be-authored">App-specific (must be authored)</h2>
<h2 id="tier-c-app-specific-must-be-authored">Tier C — App-specific (must be authored)</h2>
<table>
<thead>
<tr>
@@ -752,7 +813,7 @@ new build.</p>
<td>domain behaviour</td>
</tr>
<tr>
<td>Pipeline families (if any)</td>
<td>Pipeline families</td>
<td>the transformation graph</td>
</tr>
<tr>
@@ -765,6 +826,7 @@ new build.</p>
</tr>
</tbody>
</table>
<hr />
<h2 id="the-two-axes">The two axes</h2>
<p>Everything app-specific reduces to <strong>two axes</strong>:</p>
<ol>
@@ -772,7 +834,8 @@ new build.</p>
the OpenAPI contract.</li>
<li><strong>Pipeline family</strong> — only if inputs require multi-step transformation.</li>
</ol>
<p>If you can specify those two axes, the rest of the app follows mechanically.</p>
<p>If you can specify those two axes, the rest of the app follows mechanically:
Tier A is imported, Tier B is copied, and Tier C is the only thing you author.</p>

View File

@@ -665,7 +665,7 @@ can read a library's full API by connecting to its MCP and reading
<td><code>hexa</code></td>
<td>Declarative DI / composition-root tooling</td>
<td>manual wiring, hand-written factories</td>
<td><em>(see note)</em></td>
<td><code>:8008</code></td>
</tr>
<tr>
<td>Mongo Ops</td>
@@ -714,7 +714,7 @@ can read a library's full API by connecting to its MCP and reading
<td><em>(service)</em></td>
<td>Central identity service (register/login/me/introspect)</td>
<td>building auth endpoints yourself</td>
<td>—</td>
<td><em>(deferred)</em></td>
</tr>
</tbody>
</table>
@@ -767,9 +767,12 @@ matching library, its import name, its "use instead of" guidance, and its MCP
endpoint. Query it before writing any infrastructure.</p>
<h2 id="notes">Notes</h2>
<ul>
<li><strong>Hexa</strong> ships an MCP bundle but is not yet assigned a dedicated port in the
hub config; it is reachable via the standards registry entry and will be
wired to a port.</li>
<li><strong>Hexa</strong> is a full library with its own MCP server on <code>:8008</code>: read
<code>doc://index</code>, <code>doc://nav</code>, and <code>doc://modules/{module}</code> (e.g.
<code>doc://modules/hexa.container</code>) to discover the DI surface — <code>build(cls, *,
instances, config)</code> and the <code>PortNode</code> model.</li>
<li><strong>Auth Server</strong> is deferred: it has no <code>pyproject.toml</code>/importable package and
no MCP bundle. Do not reach for it yet; <code>py-jwt</code> covers token primitives.</li>
<li>Library MCP servers expose <strong>documentation resources</strong> (not executable tools):
they tell an agent the available API surface so it does not invent code.</li>
</ul>

View File

@@ -516,7 +516,7 @@ patterns, and the internal library catalog that agents consult while coding.</p>
<li><strong>Worker &amp; Progress</strong> — queued work, progress reporting, and visibility.</li>
<li><strong>Human-in-the-Loop</strong> — guardrails and approvals inside automations.</li>
<li><strong>Infra Adapters</strong> — how infrastructure connects without leaking into domain.</li>
<li><strong>Genericity Matrix</strong> — what is generic vs app-specific.</li>
<li><strong>Genericity Matrix</strong> — what is library-backed, template-provided, or authored.</li>
<li><strong>Library Registry</strong> — the catalog of internal libraries behind all of it.</li>
</ul>
<p>Read the <strong>Wiki</strong> for the philosophy and rationale; these pages are the

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,214 @@
{
"project": "standards",
"template": "templates/app",
"seed": {
"resource": "item",
"family": "example"
},
"components": [
{
"kind": "model",
"path": "domain/item/models.py",
"placeholder": "resource",
"description": "Business Pydantic model for a resource."
},
{
"kind": "ports-db",
"path": "domain/item/ports/db.py",
"placeholder": "resource",
"description": "Document model + repository factory for a resource."
},
{
"kind": "ports-service",
"path": "domain/item/ports/service.py",
"placeholder": "resource",
"description": "CRUD service wired through the DI library."
},
{
"kind": "converters",
"path": "domain/item/ports/converters.py",
"placeholder": "resource",
"description": "Optional pure converters between domain types."
},
{
"kind": "routes",
"path": "interfaces/item/routes.py",
"placeholder": "resource",
"description": "Thin HTTP handlers bound to operationIds."
},
{
"kind": "test-model",
"path": "tests/unit/test_item_models.py",
"placeholder": "resource",
"description": "Unit test for the resource model."
},
{
"kind": "test-routes",
"path": "tests/integration/test_item_routes.py",
"placeholder": "resource",
"description": "Integration test at the HTTP edge."
},
{
"kind": "pipeline-abc",
"path": "pipelines/example/abc.py",
"placeholder": "family",
"description": "Port contracts for a pipeline family."
},
{
"kind": "pipeline-impl",
"path": "pipelines/example/impl.py",
"placeholder": "family",
"description": "Concrete stages, wired root, and family registration."
},
{
"kind": "pipeline-yaml",
"path": "pipelines/example/pipeline.yaml",
"placeholder": "family",
"description": "Graph spec (nodes, edges, roots, schema)."
},
{
"kind": "pipeline-resources",
"path": "pipelines/example/resources/pipeline.yaml",
"placeholder": "family",
"description": "Runtime spec consumed by the shared loader."
},
{
"kind": "pipeline-pattern",
"path": "pipelines/example/resources/patterns/example.yaml",
"placeholder": "family",
"description": "Pattern/rule fragment merged by the loader."
},
{
"kind": "pipeline-ports",
"path": "pipelines/shared/ports.py",
"placeholder": "",
"description": "Shared pipeline ports (FinalizePort, RegistryPort)."
},
{
"kind": "pipeline-loader",
"path": "pipelines/shared/loader.py",
"placeholder": "",
"description": "Shared loader: DI wiring + pattern merge."
},
{
"kind": "pipeline-registry",
"path": "pipelines/shared/registry.py",
"placeholder": "",
"description": "Shared registry: (kind, variant, mode) -> root class."
},
{
"kind": "pipeline-simple",
"path": "pipelines/item_sync.py",
"placeholder": "",
"description": "Smallest pipeline form (plain async steps)."
},
{
"kind": "job-model",
"path": "domain/job/models.py",
"placeholder": "",
"description": "Seeded second resource model (job)."
},
{
"kind": "job-ports-db",
"path": "domain/job/ports/db.py",
"placeholder": "",
"description": "Seeded second resource repository (job)."
},
{
"kind": "job-ports-service",
"path": "domain/job/ports/service.py",
"placeholder": "",
"description": "Seeded second resource service (job)."
},
{
"kind": "job-routes",
"path": "interfaces/job/routes.py",
"placeholder": "",
"description": "Seeded second resource routes (job)."
},
{
"kind": "config",
"path": "config.py",
"placeholder": "",
"description": "Settings dataclass + singleton."
},
{
"kind": "init",
"path": "init.py",
"placeholder": "",
"description": "Composition root (init/shutdown, get_svc)."
},
{
"kind": "main",
"path": "main.py",
"placeholder": "",
"description": "Spec-first app bootstrap + lifespan."
},
{
"kind": "worker",
"path": "worker.py",
"placeholder": "",
"description": "Poll loop + job status machine."
},
{
"kind": "openapi",
"path": "openapi.yaml",
"placeholder": "",
"description": "OpenAPI contract skeleton."
},
{
"kind": "pyproject",
"path": "pyproject.toml",
"placeholder": "",
"description": "Packaging + dependency pins."
},
{
"kind": "dockerfile",
"path": "Dockerfile",
"placeholder": "",
"description": "API + worker image with private-index wiring."
},
{
"kind": "compose",
"path": "docker-compose.yaml",
"placeholder": "",
"description": "api + worker + mongo compose file."
},
{
"kind": "env-example",
"path": ".env.example",
"placeholder": "",
"description": "Environment variable template."
},
{
"kind": "db-setup",
"path": "infra/db/setup.py",
"placeholder": "",
"description": "Mongo connection lifecycle."
},
{
"kind": "repo",
"path": "infra/db/repos/mongo.py",
"placeholder": "",
"description": "Repository factory (base/populating/cached)."
},
{
"kind": "service-adapter",
"path": "infra/adapters/service.py",
"placeholder": "",
"description": "Generic CRUD service adaptor."
},
{
"kind": "script-seed",
"path": "scripts/seed.py",
"placeholder": "",
"description": "Operator seed script."
},
{
"kind": "script-drop",
"path": "scripts/drop.py",
"placeholder": "",
"description": "Operator drop script."
}
]
}

View File

@@ -2,7 +2,7 @@
"module": "genericity_matrix",
"content": {
"path": "genericity_matrix",
"docstring": "# Genericity Matrix\n\n## Summary\n\nThis page states precisely what is **reusable as-is**, what is a **template to\nadapt**, and what is **app-specific and must be authored**. Use it to scope a\nnew build.\n\n---\n\n## Reusable as-is (do not rewrite)\n\n| Area | Artifact |\n|---|---|\n| Layering & dependency rule | the package layout and import rules |\n| Composition root | the `init.py` shape, idempotency, `get_svc` overloading |\n| Web edge | `OpenAPIFirstApp` bootstrap, handler-name = `operationId`, thin handlers |\n| CRUD slice | the four-file pattern (model / ports-db / ports-service / converters) |\n| Generic CRUD service | the base service adaptor |\n| Repository factory | base / populating / cached selection |\n| Worker | the poll loop, backoff, retry, status machine |\n| Progress bus | the bus port + in-memory adapter + streaming endpoint |\n| Human-in-the-loop | the durable pause + question document + resolve endpoint + resume map |\n| Infra adaptors | HTTP cache decorator, KV store port, LLM pool scaffold |\n| Testing | integration harness with fakes, fixture-parity pattern |\n| Docs | GSDFC + generated stubs + MCP bundle pipeline |\n\n## Template to adapt (copy, then change names)\n\n| Artifact | Change |\n|---|---|\n| `pyproject.toml` | project name, dependencies |\n| `Dockerfile` / `docker-compose.yaml` | service name, ports, services |\n| `.env.example` | connection strings, provider keys |\n| `openapi.yaml` | every path/schema/operationId |\n| `scripts/` | seed and operator tasks |\n\n## App-specific (must be authored)\n\n| Artifact | Why |\n|---|---|\n| Resource models | the business shape |\n| Repository populate/cache rules | the relationships |\n| Service business logic | domain behaviour |\n| Pipeline families (if any) | the transformation graph |\n| Pattern/rule resources | data-driven policy |\n| Fixtures & golden outputs | test data |\n\n## The two axes\n\nEverything app-specific reduces to **two axes**:\n\n1. **Resource model** — the set of resources, their fields, relationships, and\n the OpenAPI contract.\n2. **Pipeline family** — only if inputs require multi-step transformation.\n\nIf you can specify those two axes, the rest of the app follows mechanically.\n",
"docstring": "# Genericity Matrix\n\n## Summary\n\nThis page states precisely what is **library-backed**, what is\n**template-provided**, and what is **app-specific and must be authored**. Use it\nto scope a new build and to know, for any given capability, whether to call a\nlibrary, copy template code, or write it yourself.\n\n---\n\n## Tier A — Library-backed (use; never reimplement)\n\nThese capabilities already exist as internal libraries. Reach them through the\nlibrary registry MCP (`find_library(capability)`) and read the library's own MCP\nfor its API. Writing a private copy of any of these is a defect.\n\n| Capability | Library | MCP |\n|---|---|---|\n| Persistence: repositories, cache, populate, transactions | Mongo Ops (`mongo_ops`) | `:8007` |\n| Dependency injection / composition | Hexa (`hexa`) | `:8008` |\n| Deterministic multi-step transformation engine | DAG Pipe (`dagpipe`) | `:8006` |\n| Spec-first HTTP edge, client, codegen | OpenAPI First (`openapi_first`) | `:8004` |\n| Format-agnostic fetch + parse (HTML, PDF, CSV, XLSX) | Omniread (`omniread`) | `:8003` |\n| Read-only email ingestion | Mail Intake (`mail_intake`) | `:8002` |\n| JWT auth primitives (register, login, introspect) | Py JWT (`jwtlib`) | `:8005` |\n| Documentation build (MkDocs + MCP) | Doc Forge (`docforge`) | `:8001` |\n\n## Tier B — Template-provided (copy from `templates/app`, then adapt)\n\nThese are reusable internal components shipped **inside** the template — they are\nnot independent libraries. Copy them as-is, then rename/populate only what is\napp-specific. They give every app the same wiring shape as the reference app.\n\n| Component | Template location | Action |\n|---|---|---|\n| Layering & dependency rule | the package layout + import rules | keep |\n| Composition root shape | `init.py` (`get_svc`/`get_db`, idempotency) | keep |\n| Spec-first web edge | `interfaces/openapi.py`, thin handlers bound to `operationId` | keep |\n| CRUD slice pattern | `domain/<resource>/{models.py, ports/db.py, ports/service.py, converters.py}` | rename + fill |\n| Generic CRUD service | `infra/adapters/service.py` (`CrudsServiceAdaptor`) | keep |\n| Repository factory | `infra/db/repos/mongo.py` (`_repo_factory`) | keep |\n| Worker poll + status loop | `worker.py` | keep |\n| Progress bus + streaming | bus port + in-memory adapter + SSE endpoint | keep |\n| Human-in-the-loop | durable pause + question document + resolve endpoint + resume map | keep |\n| HTTP response cache | `cache_response` decorator + generation invalidation | keep |\n| Key-value store | KV store port + Redis adapter + in-memory fallback | keep |\n| LLM client pool | provider-select + retry pool scaffold | keep |\n| Pipeline scaffold | `pipelines/shared/{ports,loader,registry}.py` + `pipelines/<family>/` | rename + fill |\n| Operator scripts | `scripts/` (`init()` / `get_svc()` / `shutdown()`) | rename |\n| Testing harness | integration fakes + fixture-parity pattern | keep |\n| Project scaffolding | `pyproject.toml`, `Dockerfile`, `docker-compose.yaml`, `.env.example`, `openapi.yaml` | rename + fill |\n\n## Tier C — App-specific (must be authored)\n\n| Artifact | Why |\n|---|---|\n| Resource models | the business shape |\n| Repository populate/cache rules | the relationships |\n| Service business logic | domain behaviour |\n| Pipeline families | the transformation graph |\n| Pattern/rule resources | data-driven policy |\n| Fixtures & golden outputs | test data |\n\n---\n\n## The two axes\n\nEverything app-specific reduces to **two axes**:\n\n1. **Resource model** — the set of resources, their fields, relationships, and\n the OpenAPI contract.\n2. **Pipeline family** — only if inputs require multi-step transformation.\n\nIf you can specify those two axes, the rest of the app follows mechanically:\nTier A is imported, Tier B is copied, and Tier C is the only thing you author.\n",
"objects": {}
}
}

View File

@@ -2,7 +2,7 @@
"module": "index",
"content": {
"path": "index",
"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",
"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 library-backed, template-provided, or authored.\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": {}
}
}

View File

@@ -2,7 +2,7 @@
"module": "infra_adaptors",
"content": {
"path": "infra_adaptors",
"docstring": "# Infra Adapters\n\n## Summary\n\nAll external systems are reached through adapters in `infra/`. Prefer an\nexisting internal library over writing a new adapter.\n\n---\n\n## Database (MongoDB)\n\n- A single connection manager owns the client lifecycle.\n `setup_database(uri, db_name)` / `shutdown_database()`.\n- A repository **factory** selects the repository flavour:\n\n ```python\n repo_factory(collection, document, engine=None,\n populate_rules=None, cache=None)\n ```\n\n - base repository — plain CRUD;\n - populating repository — resolves referenced documents (foreign keys);\n - cached repository — an LRU/TTL cache in front of reads.\n\n- Referenced documents are described declaratively:\n\n ```python\n PopulateRule(field=\"owner\", collection=\"users\",\n nested_rules=[], max_depth=2, projection=None)\n ```\n\n- Transactions use a session context manager provided by the library.\n\n**Library:** the Mongo operations library (see registry). Do not write raw\nMotor calls in the domain.\n\n## HTTP response cache\n\nA generic GET cache sits behind a decorator.\n\n```python\n@cache_response(ttl=60)\nasync def list_items(request): ...\n```\n\n- Keyed on path + query + user.\n- No-op when no store is wired.\n- `invalidate(*resources)` bumps a generation counter for write-triggered\n invalidation.\n\n## Event / progress bus\n\nA port with an in-memory default adapter (`publish`, `get_events`). Swap for a\ndurable broker in production (see Worker & Progress).\n\n## External clients (ingestion)\n\nExternal fetchers (files, mail, HTTP) are adapters that wrap ingestion\nlibraries. Construct them in the composition root and inject into\norchestrators.\n\n**Libraries:** the ingestion library (formats) and the mail library (email).\n\n## LLM clients\n\nWhen a pipeline calls a model provider, route through a pool adaptor that\nhandles provider selection, retries, and client lifecycle:\n\n```python\npool = setup_async_pool(provider=settings.llm_provider)\nresult = await pool.complete(model=\"...\", messages=[...])\n```\n\nProviders are selected by `LLM_PROVIDER` and configured by environment.\n\n## Key-value store\n\nFor report/snapshot storage, provide a pluggable store port with a Redis\nadapter and an in-memory fallback.\n\n## Rule\n\nIf a capability already exists as a library (DB, DI, ingestion, mail, auth,\ndocs), **use it**. Writing a private copy of a library is a defect.\n",
"docstring": "# Infra Adapters\n\n## Summary\n\nAll external systems are reached through adapters in `infra/`. Prefer an\nexisting internal library over writing a new adapter; where no library exists,\ncopy the Tier B scaffold from `templates/app`. See the Genericity Matrix for the\nTier A / B / C split.\n\n---\n\n## Database (MongoDB)\n\n- A single connection manager owns the client lifecycle.\n `setup_database(uri, db_name)` / `shutdown_database()`.\n- A repository **factory** selects the repository flavour:\n\n ```python\n repo_factory(collection, document, engine=None,\n populate_rules=None, cache=None)\n ```\n\n - base repository — plain CRUD;\n - populating repository — resolves referenced documents (foreign keys);\n - cached repository — an LRU/TTL cache in front of reads.\n\n- Referenced documents are described declaratively:\n\n ```python\n PopulateRule(field=\"owner\", collection=\"users\",\n nested_rules=[], max_depth=2, projection=None)\n ```\n\n- Transactions use a session context manager provided by the library.\n\n**Tier A — Library (Mongo Ops).** Do not write raw Motor calls in the domain;\nconsult the registry and the library's MCP before touching the driver.\n\n## HTTP response cache\n\n**Tier B — template.** Copy from `templates/app`; not a standalone library.\n\nA generic GET cache sits behind a decorator.\n\n```python\n@cache_response(ttl=60)\nasync def list_items(request): ...\n```\n\n- Keyed on path + query + user.\n- No-op when no store is wired.\n- `invalidate(*resources)` bumps a generation counter for write-triggered\n invalidation.\n\n## Event / progress bus\n\n**Tier B — template.** Copy from `templates/app`.\n\nA port with an in-memory default adapter (`publish`, `get_events`). Swap for a\ndurable broker in production (see Worker & Progress).\n\n## External clients (ingestion)\n\nExternal fetchers (files, mail, HTTP) are adapters that wrap ingestion\nlibraries. Construct them in the composition root and inject into\norchestrators.\n\n**Tier A — Libraries:** Omniread (formats) and Mail Intake (email).\n\n## LLM clients\n\n**Tier B — template.** Copy the pool scaffold from `templates/app`.\n\nWhen a pipeline calls a model provider, route through a pool adaptor that\nhandles provider selection, retries, and client lifecycle:\n\n```python\npool = setup_async_pool(provider=settings.llm_provider)\nresult = await pool.complete(model=\"...\", messages=[...])\n```\n\nProviders are selected by `LLM_PROVIDER` and configured by environment.\n\n## Key-value store\n\n**Tier B — template.** Copy from `templates/app`.\n\nFor report/snapshot storage, provide a pluggable store port with a Redis\nadapter and an in-memory fallback.\n\n## Rule\n\n- **Tier A (libraries: DB, DI, ingestion, mail, auth, docs, pipelines)** — use\n the library; writing a private copy is a defect. Query `find_library` first.\n- **Tier B (cache, progress bus, LLM pool, KV store, composition shape, worker,\n tests)** — these are template code, not libraries: copy them from\n `templates/app` rather than searching the registry.\n- **Tier C (models, business logic, pipeline families, rules)** — author them\n yourself.\n",
"objects": {}
}
}

View File

@@ -2,7 +2,7 @@
"module": "library_registry",
"content": {
"path": "library_registry",
"docstring": "# Library Registry\n\n## Summary\n\nThe Aetos ecosystem provides internal libraries for the shared concerns of\nevery application. **Use them.** Writing a private copy of any capability below\nis a defect.\n\nEach library also exposes its own documentation as an MCP server. An AI agent\ncan read a library's full API by connecting to its MCP and reading\n`doc://index`, `doc://nav`, and `doc://modules/{module}`.\n\n---\n\n## Catalog\n\n| Library | Import | Purpose | Use instead of | MCP |\n|---|---|---|---|---|\n| DAG Pipe | `dagpipe` | Deterministic state-transformation DAG engine | hand-rolled orchestration/topological ordering | `:8006` |\n| Hexa | `hexa` | Declarative DI / composition-root tooling | manual wiring, hand-written factories | *(see note)* |\n| Mongo Ops | `mongo_ops` | Async MongoDB repositories, cache, populate, transactions | raw Motor CRUD and connection lifecycle | `:8007` |\n| Omniread | `omniread` | Format-agnostic fetch + parse (HTML, PDF, CSV, XLSX) | custom scrapers/parsers | `:8003` |\n| OpenAPI First | `openapi_first` | Spec-first FastAPI bootstrap, client, codegen | decorator routers and hand-written clients | `:8004` |\n| Mail Intake | `mail_intake` | Provider-agnostic read-only email ingestion | Gmail OAuth + MIME parsing | `:8002` |\n| Py JWT | `jwtlib` | JWT auth primitives (register/login/introspect) | password hashing + token code | `:8005` |\n| Doc Forge | `docforge` | Docstring compiler → MkDocs + MCP docs | custom doc generators | `:8001` |\n| Auth Server | *(service)* | Central identity service (register/login/me/introspect) | building auth endpoints yourself | — |\n\n## How to choose\n\n| You need to… | Use |\n|---|---|\n| persist and query documents | Mongo Ops |\n| compose dependencies | Hexa |\n| run a multi-step transformation | DAG Pipe |\n| read/parse an arbitrary input format | Omniread |\n| expose a contract-first HTTP API | OpenAPI First |\n| ingest email | Mail Intake |\n| authenticate users / verify tokens | Py JWT (+ Auth Server) |\n| generate documentation | Doc Forge |\n\n## Discovery\n\nA single **standards MCP** exposes `find_library(capability)`, which returns the\nmatching library, its import name, its \"use instead of\" guidance, and its MCP\nendpoint. Query it before writing any infrastructure.\n\n## Notes\n\n- **Hexa** ships an MCP bundle but is not yet assigned a dedicated port in the\n hub config; it is reachable via the standards registry entry and will be\n wired to a port.\n- Library MCP servers expose **documentation resources** (not executable tools):\n they tell an agent the available API surface so it does not invent code.\n",
"docstring": "# Library Registry\n\n## Summary\n\nThe Aetos ecosystem provides internal libraries for the shared concerns of\nevery application. **Use them.** Writing a private copy of any capability below\nis a defect.\n\nEach library also exposes its own documentation as an MCP server. An AI agent\ncan read a library's full API by connecting to its MCP and reading\n`doc://index`, `doc://nav`, and `doc://modules/{module}`.\n\n---\n\n## Catalog\n\n| Library | Import | Purpose | Use instead of | MCP |\n|---|---|---|---|---|\n| DAG Pipe | `dagpipe` | Deterministic state-transformation DAG engine | hand-rolled orchestration/topological ordering | `:8006` |\n| Hexa | `hexa` | Declarative DI / composition-root tooling | manual wiring, hand-written factories | `:8008` |\n| Mongo Ops | `mongo_ops` | Async MongoDB repositories, cache, populate, transactions | raw Motor CRUD and connection lifecycle | `:8007` |\n| Omniread | `omniread` | Format-agnostic fetch + parse (HTML, PDF, CSV, XLSX) | custom scrapers/parsers | `:8003` |\n| OpenAPI First | `openapi_first` | Spec-first FastAPI bootstrap, client, codegen | decorator routers and hand-written clients | `:8004` |\n| Mail Intake | `mail_intake` | Provider-agnostic read-only email ingestion | Gmail OAuth + MIME parsing | `:8002` |\n| Py JWT | `jwtlib` | JWT auth primitives (register/login/introspect) | password hashing + token code | `:8005` |\n| Doc Forge | `docforge` | Docstring compiler → MkDocs + MCP docs | custom doc generators | `:8001` |\n| Auth Server | *(service)* | Central identity service (register/login/me/introspect) | building auth endpoints yourself | *(deferred)* |\n\n## How to choose\n\n| You need to… | Use |\n|---|---|\n| persist and query documents | Mongo Ops |\n| compose dependencies | Hexa |\n| run a multi-step transformation | DAG Pipe |\n| read/parse an arbitrary input format | Omniread |\n| expose a contract-first HTTP API | OpenAPI First |\n| ingest email | Mail Intake |\n| authenticate users / verify tokens | Py JWT (+ Auth Server) |\n| generate documentation | Doc Forge |\n\n## Discovery\n\nA single **standards MCP** exposes `find_library(capability)`, which returns the\nmatching library, its import name, its \"use instead of\" guidance, and its MCP\nendpoint. Query it before writing any infrastructure.\n\n## Notes\n\n- **Hexa** is a full library with its own MCP server on `:8008`: read\n `doc://index`, `doc://nav`, and `doc://modules/{module}` (e.g.\n `doc://modules/hexa.container`) to discover the DI surface — `build(cls, *,\n instances, config)` and the `PortNode` model.\n- **Auth Server** is deferred: it has no `pyproject.toml`/importable package and\n no MCP bundle. Do not reach for it yet; `py-jwt` covers token primitives.\n- Library MCP servers expose **documentation resources** (not executable tools):\n they tell an agent the available API surface so it does not invent code.\n",
"objects": {}
}
}

View File

@@ -0,0 +1,8 @@
.venv/
__pycache__/
*.pyc
.env
.git/
docs/
dist/
build/

View File

@@ -0,0 +1,5 @@
DEBUG=false
MONGO_URI=mongodb://localhost:27017
MONGO_DB=app_template
WORKER_POLL_SECONDS=30
WORKER_MAX_RETRIES=3

View File

@@ -0,0 +1,20 @@
FROM python:3.11-slim
WORKDIR /app
# Private index for the internal Aetos libraries — pass credentials as build args.
ARG PIP_REGISTRY_USERNAME
ARG PIP_REGISTRY_PASSWORD
ENV PIP_EXTRA_INDEX_URL=https://${PIP_REGISTRY_USERNAME}:${PIP_REGISTRY_PASSWORD}@pip.aetoskia.com/simple
COPY pyproject.toml ./
COPY . .
RUN pip install --no-cache-dir --upgrade pip \
&& pip install --no-cache-dir --extra-index-url "${PIP_EXTRA_INDEX_URL}" .
EXPOSE 8000
# Two processes, per the two axes:
# - main.py : the spec-first FastAPI application
# - worker.py : the pipeline consumer polling pending jobs
CMD ["sh", "-c", "uvicorn main:app --host 0.0.0.0 --port 8000 & exec python worker.py"]

View File

@@ -0,0 +1,36 @@
"""# Summary
App configuration loaded from environment — a Settings dataclass with defaults
plus a singleton settings instance populated from OS environment variables at
import time.
"""
import os
from dataclasses import dataclass
@dataclass
class Settings:
"""Runtime configuration settings for the application.
Attributes:
debug (bool): Enable debug behavior. Defaults to False.
mongo_uri (str): MongoDB connection URI. Defaults to mongodb://localhost:27017.
mongo_db (str): MongoDB database name. Defaults to app_template.
worker_poll_seconds (int): Worker poll interval in seconds. Defaults to 30.
worker_max_retries (int): Maximum job retries before skipping. Defaults to 3.
"""
debug: bool = False
mongo_uri: str = "mongodb://localhost:27017"
mongo_db: str = "app_template"
worker_poll_seconds: int = 30
worker_max_retries: int = 3
settings = Settings(
debug=os.environ.get("DEBUG", "").lower() in ("1", "true", "yes"),
mongo_uri=os.environ.get("MONGO_URI", "mongodb://localhost:27017"),
mongo_db=os.environ.get("MONGO_DB", "app_template"),
worker_poll_seconds=int(os.environ.get("WORKER_POLL_SECONDS", "30")),
worker_max_retries=int(os.environ.get("WORKER_MAX_RETRIES", "3")),
)

View File

@@ -0,0 +1,21 @@
services:
api:
build: .
command: uvicorn main:app --host 0.0.0.0 --port 8000
env_file: .env
ports:
- "8000:8000"
depends_on:
- mongo
worker:
build: .
command: python worker.py
env_file: .env
depends_on:
- mongo
mongo:
image: mongo:7
ports:
- "27017:27017"

View File

@@ -0,0 +1,19 @@
"""# Summary
Item domain model — the neutral resource used throughout the template.
"""
from pydantic import BaseModel, Field
class Item(BaseModel):
"""A single countable resource.
Attributes:
name (str): Item name.
quantity (int): Item quantity. Defaults to 1.
"""
name: str
quantity: int = Field(default=1)
model_config = {"extra": "ignore"}

View File

@@ -0,0 +1,18 @@
"""# Summary
Optional converters for the item slice — pure functions mapping between domain
types. Add a converter here when a service must reshape data; keep it pure.
"""
from domain.item.models import Item
def to_item(data: dict) -> Item:
"""Build an :class:`Item` from a raw mapping.
Args:
data (dict): Field mapping matching the item model.
Returns:
Item: The constructed item model.
"""
return Item(**data)

View File

@@ -0,0 +1,20 @@
"""# Summary
Mongo persistence for items: document schema and repository factory.
"""
from domain.item.models import Item
from infra.db.repos.mongo import _repo_factory as mongo_repo_factory
from mongo_ops import BaseDocument, BaseRepository
class ItemDocument(Item, BaseDocument):
"""Mongo document form of :class:`Item`."""
def create_item_repo() -> BaseRepository:
"""Create the Mongo-backed item repository.
Returns:
BaseRepository: Repository wired to the ``items`` collection.
"""
return mongo_repo_factory("items", ItemDocument)

View File

@@ -0,0 +1,33 @@
"""# Summary
CRUD service wiring for the item aggregate via the hexa container.
"""
from hexa import build
from domain.item.models import Item
from domain.item.ports.db import ItemDocument
from infra.adapters.service import CrudsServiceAdaptor
class ItemCrudsService(CrudsServiceAdaptor[Item, ItemDocument]):
"""Item CRUD service resolving :class:`ItemDocument` from a repo.
Notes:
Instances are created by :func:`get_item_service`. The ``model`` and
``schema`` class attributes bind the domain model and its document type.
"""
model = Item
schema = ItemDocument
def get_item_service(repo) -> ItemCrudsService:
"""Build an item CRUD service wired to ``repo``.
Args:
repo (Any): Mongo-backed item repository from :func:`create_item_repo`.
Returns:
ItemCrudsService: Ready-to-use item CRUD service.
"""
return build(ItemCrudsService, instances={"repo": repo})

View File

@@ -0,0 +1,26 @@
"""# Summary
Job domain model — a pipeline work item consumed by the background worker.
"""
from typing import Literal
from pydantic import BaseModel, Field
class Job(BaseModel):
"""A unit of background work.
Attributes:
name (str): Job name.
status (str): Lifecycle status. Defaults to "pending".
retry_count (int): Times the job failed. Defaults to 0.
error_message (str | None): Last failure message. Defaults to None.
payload (dict | None): Arbitrary job payload. Defaults to None.
"""
name: str
status: Literal["pending", "processing", "done", "failed"] = "pending"
retry_count: int = Field(default=0)
error_message: str | None = None
payload: dict | None = None
model_config = {"extra": "ignore"}

View File

@@ -0,0 +1,20 @@
"""# Summary
Mongo persistence for jobs: document schema and repository factory.
"""
from domain.job.models import Job
from infra.db.repos.mongo import _repo_factory as mongo_repo_factory
from mongo_ops import BaseDocument, BaseRepository
class JobDocument(Job, BaseDocument):
"""Mongo document form of :class:`Job`."""
def create_job_repo() -> BaseRepository:
"""Create the Mongo-backed job repository.
Returns:
BaseRepository: Repository wired to the ``jobs`` collection.
"""
return mongo_repo_factory("jobs", JobDocument)

View File

@@ -0,0 +1,33 @@
"""# Summary
CRUD service wiring for the job aggregate via the hexa container.
"""
from hexa import build
from domain.job.models import Job
from domain.job.ports.db import JobDocument
from infra.adapters.service import CrudsServiceAdaptor
class JobCrudsService(CrudsServiceAdaptor[Job, JobDocument]):
"""Job CRUD service resolving :class:`JobDocument` from a repo.
Notes:
Instances are created by :func:`get_job_service`. The ``model`` and
``schema`` class attributes bind the domain model and its document type.
"""
model = Job
schema = JobDocument
def get_job_service(repo) -> JobCrudsService:
"""Build a job CRUD service wired to ``repo``.
Args:
repo (Any): Mongo-backed job repository from :func:`create_job_repo`.
Returns:
JobCrudsService: Ready-to-use job CRUD service.
"""
return build(JobCrudsService, instances={"repo": repo})

View File

@@ -0,0 +1,128 @@
"""# Summary
Generic CRUD service adaptor over a Mongo repository.
Bridge pydantic models and mongo-ops documents, exposing create, read, update,
patch, delete, and search operations for every domain that needs them.
"""
from typing import Generic, Type, TypeVar
from infra.db.repos import mongo
from pydantic import BaseModel
from mongo_ops import BaseDocument
from mongo_ops.models import PyObjectId
Model = TypeVar("Model", bound=BaseModel)
Schema = TypeVar("Schema", bound=BaseDocument)
class CrudsServiceAdaptor(Generic[Model, Schema]):
"""Adapt a generic Mongo repository to a model/schema CRUD service.
Attributes:
repo (mongo.BaseRepository): Repository backing all CRUD operations.
model (Type[Model]): Pydantic model type handled by this service.
schema (Type[Schema]): Document schema type persisted by the repository.
"""
repo: mongo.BaseRepository
model: Type[Model]
schema: Type[Schema]
def __init__(self, *, repo=None):
"""Initialize the adaptor with a backing repository.
Args:
repo (mongo.BaseRepository): Repository used for all operations.
"""
self.repo = repo
@staticmethod
def model_to_dict(model_obj: Model) -> dict:
"""Serialize a model into a sparse dictionary.
Args:
model_obj (Model): Model instance to serialize.
Returns:
dict: Fields that are set and not ``None``.
"""
return model_obj.model_dump(exclude_unset=True, exclude_none=True)
async def create(self, data: Model | dict) -> Schema:
"""Create a document from a model or raw mapping.
Args:
data (Model | dict): Model instance or field mapping to persist.
Returns:
Schema: Persisted document.
"""
if isinstance(data, dict):
data = self.schema(**data)
return await self.repo.create(data)
async def read(self, id: PyObjectId) -> Schema | None:
"""Read a document by primary key.
Args:
id (PyObjectId): Document identifier.
Returns:
Schema | None: Matching document, or ``None`` when absent.
"""
return await self.repo.get_by_id(id)
async def update(self, id: PyObjectId, data: Model | dict) -> Schema | None:
"""Replace a document by primary key.
Args:
id (PyObjectId): Document identifier.
data (Model | dict): Replacement model or field mapping.
Returns:
Schema | None: Updated document, or ``None`` when absent.
"""
return await self.repo.update(
id,
data if isinstance(data, dict) else self.model_to_dict(data),
)
async def patch(self, id: PyObjectId, data: dict) -> Schema | None:
"""Partially update a document by primary key.
Args:
id (PyObjectId): Document identifier.
data (dict): Fields to patch.
Returns:
Schema | None: Updated document, or ``None`` when absent.
"""
return await self.repo.patch(id, data)
async def delete(self, id: PyObjectId) -> bool:
"""Delete a document by primary key.
Args:
id (PyObjectId): Document identifier.
Returns:
bool: ``True`` when a document was deleted.
"""
return await self.repo.delete(id)
async def search(self, filter: dict | None = None, *args, **kwargs) -> list[Schema]:
"""List documents matching an optional filter.
Args:
filter (dict | None): Query filter passed to the repository.
Returns:
list[Schema]: Matching documents.
Notes:
Extra positional and keyword arguments are forwarded to the
repository.
"""
return await self.repo.get_many(filter=filter, *args, **kwargs)

View File

@@ -0,0 +1,42 @@
"""# Summary
Generic MongoDB CRUD repo — single implementation for all domains.
Select a concrete mongo-ops repository (base, cached, or populating) from the
options supplied to the factory.
"""
from typing import List, Optional, Type
from mongo_ops import BaseRepository, CachedBaseRepository, PopulatingRepository
from mongo_ops.cache import CacheBackend, CacheConfig
from mongo_ops.models import BaseDocument
from mongo_ops.populate import PopulationEngine, PopulateRule
def _repo_factory(
collection: str,
db_model: Type[BaseDocument],
cache_backend: Optional[CacheBackend] = None,
cache_config: Optional[CacheConfig] = None,
engine: Optional[PopulationEngine] = None,
populate_rules: Optional[List[PopulateRule]] = None,
) -> BaseRepository:
"""Build the appropriate mongo-ops repository.
Args:
collection (str): Collection name to operate on.
db_model (Type[BaseDocument]): Document model bound to the collection.
cache_backend (Optional[CacheBackend]): Cache backend enabling cached reads.
cache_config (Optional[CacheConfig]): Cache configuration for the backend.
engine (Optional[PopulationEngine]): Population engine for reference expansion.
populate_rules (Optional[List[PopulateRule]]): Rules applied by the engine.
Returns:
BaseRepository: Repository instance matching the supplied options.
"""
if engine and populate_rules is not None:
return PopulatingRepository(collection, db_model, engine, populate_rules)
elif cache_backend is not None:
return CachedBaseRepository(collection, db_model, cache_backend, cache_config)
else:
return BaseRepository(collection, db_model)

View File

@@ -0,0 +1,23 @@
"""# Summary
Database setup — connection lifecycle only. No domain imports.
Connect and disconnect the shared Mongo connection manager at application
startup and shutdown.
"""
from mongo_ops import MongoConnectionManager
async def setup_database(uri: str, db_name: str):
"""Connect the shared Mongo connection manager.
Args:
uri (str): Mongo connection URI.
db_name (str): Target database name.
"""
await MongoConnectionManager.connect(uri=uri, db_name=db_name)
async def shutdown_database():
"""Disconnect the shared Mongo connection manager."""
await MongoConnectionManager.disconnect()

View File

@@ -0,0 +1,51 @@
"""# Summary
Infrastructure initialization — connects MongoDB and wires all repos and services.
Shared bootstrap for both the FastAPI app and the worker, guarded against
repeated initialization via a module-level flag.
"""
from typing import Any, Dict
from config import settings
from domain.item.ports.db import create_item_repo
from domain.item.ports.service import get_item_service
from domain.job.ports.db import create_job_repo
from domain.job.ports.service import get_job_service
from infra.db.setup import setup_database, shutdown_database
_resources: Dict[str, Any] = {}
_initialized: bool = False
async def init() -> Dict[str, Any]:
"""Connect MongoDB, wire all repos and services, and return the resources dict.
Idempotent: repeated calls short-circuit and return the cached resources.
Returns:
Dict[str, Any]: Named services keyed by resource name.
"""
global _resources, _initialized
if _initialized:
return _resources
await setup_database(uri=settings.mongo_uri, db_name=settings.mongo_db)
item_repo = create_item_repo()
job_repo = create_job_repo()
_resources["item_repo"] = item_repo
_resources["job_repo"] = job_repo
_resources["item_svc"] = get_item_service(item_repo)
_resources["job_svc"] = get_job_service(job_repo)
_initialized = True
return _resources
async def shutdown() -> None:
"""Disconnect MongoDB and reset the resources cache."""
global _initialized
await shutdown_database()
_resources.clear()
_initialized = False

View File

@@ -0,0 +1,42 @@
"""# Summary
Item route handlers — names must match openapi.yaml operationIds.
"""
from fastapi import Request
from fastapi.responses import JSONResponse
from domain.item.models import Item
def svc(request: Request):
"""Resolve the item CRUD service from app state."""
return request.app.state.db_resources["item_svc"]
async def create_item(request: Request, body: dict):
"""Create an item from the request body."""
data = body.model_dump() if hasattr(body, "model_dump") else dict(body)
doc = await svc(request).create(Item(**data))
return JSONResponse(status_code=201, content=doc.model_dump(mode="json", by_alias=True))
async def get_item(request: Request, id: str):
"""Fetch one item by id."""
doc = await svc(request).read(id)
if doc is None:
return JSONResponse(status_code=404, content={"detail": "item not found"})
return doc.model_dump(mode="json", by_alias=True)
async def list_items(request: Request):
"""List all items."""
docs = await svc(request).search({})
return [d.model_dump(mode="json", by_alias=True) for d in docs]
async def delete_item(request: Request, id: str):
"""Delete one item by id."""
ok = await svc(request).delete(id)
if not ok:
return JSONResponse(status_code=404, content={"detail": "item not found"})
return {"deleted": True}

View File

@@ -0,0 +1,34 @@
"""# Summary
Job route handlers — names must match openapi.yaml operationIds.
"""
from fastapi import Request
from fastapi.responses import JSONResponse
from domain.job.models import Job
def svc(request: Request):
"""Resolve the job CRUD service from app state."""
return request.app.state.db_resources["job_svc"]
async def create_job(request: Request, body: dict):
"""Create a job from the request body."""
data = body.model_dump() if hasattr(body, "model_dump") else dict(body)
doc = await svc(request).create(Job(**data))
return JSONResponse(status_code=201, content=doc.model_dump(mode="json", by_alias=True))
async def get_job(request: Request, id: str):
"""Fetch one job by id."""
doc = await svc(request).read(id)
if doc is None:
return JSONResponse(status_code=404, content={"detail": "job not found"})
return doc.model_dump(mode="json", by_alias=True)
async def list_jobs(request: Request):
"""List all jobs."""
docs = await svc(request).search({})
return [d.model_dump(mode="json", by_alias=True) for d in docs]

View File

@@ -0,0 +1,8 @@
"""# Summary
All handler functions — names must match openapi.yaml operationId values. The
OpenAPI-first router resolves each operation to a handler by name, so this module
re-exports every route handler from the subpackages into one flat namespace.
"""
from interfaces.item.routes import *
from interfaces.job.routes import *

View File

@@ -0,0 +1,54 @@
"""# Summary
FastAPI application bootstrap — uses OpenAPIFirstApp for spec-driven routing.
Handlers are looked up by operationId in interfaces.openapi, so no router tables
are declared here; the lifespan initializes and shuts down app resources.
"""
import logging
from contextlib import asynccontextmanager
from dotenv import load_dotenv
from openapi_first.app import OpenAPIFirstApp
from starlette.middleware.cors import CORSMiddleware
from init import init, shutdown
load_dotenv()
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(name)s] %(levelname)s: %(message)s",
)
import interfaces.openapi as openapi_routes
logger = logging.getLogger(__name__)
@asynccontextmanager
async def lifespan(app: OpenAPIFirstApp):
"""Initialize resource wiring on startup and shut it down on exit.
Args:
app (OpenAPIFirstApp): The ASGI application exposing app.state.
Yields:
None: The application runs while the context is open.
"""
resources = await init()
app.state.db_resources = resources
yield
await shutdown()
app = OpenAPIFirstApp(
openapi_path="openapi.yaml",
routes_module=openapi_routes,
title="Item API",
version="0.1.0",
lifespan=lifespan,
)
app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"])

View File

@@ -0,0 +1,161 @@
openapi: 3.0.3
info:
title: Item API
version: 0.1.0
paths:
/items:
get:
operationId: list_items
summary: List items
responses:
"200":
description: OK
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/Item"
post:
operationId: create_item
summary: Create an item
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/ItemInput"
responses:
"201":
description: Created
content:
application/json:
schema:
$ref: "#/components/schemas/Item"
/items/{id}:
get:
operationId: get_item
summary: Get an item
parameters:
- name: id
in: path
required: true
schema:
type: string
responses:
"200":
description: OK
content:
application/json:
schema:
$ref: "#/components/schemas/Item"
"404":
description: Not found
delete:
operationId: delete_item
summary: Delete an item
parameters:
- name: id
in: path
required: true
schema:
type: string
responses:
"200":
description: Deleted
"404":
description: Not found
/jobs:
get:
operationId: list_jobs
summary: List jobs
responses:
"200":
description: OK
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/Job"
post:
operationId: create_job
summary: Create a job
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/JobInput"
responses:
"201":
description: Created
content:
application/json:
schema:
$ref: "#/components/schemas/Job"
/jobs/{id}:
get:
operationId: get_job
summary: Get a job
parameters:
- name: id
in: path
required: true
schema:
type: string
responses:
"200":
description: OK
content:
application/json:
schema:
$ref: "#/components/schemas/Job"
"404":
description: Not found
components:
schemas:
Item:
type: object
required: [name, quantity]
properties:
id:
type: string
name:
type: string
quantity:
type: integer
ItemInput:
type: object
required: [name]
properties:
name:
type: string
quantity:
type: integer
default: 1
Job:
type: object
required: [name, status]
properties:
id:
type: string
name:
type: string
status:
type: string
enum: [pending, processing, done, failed]
retry_count:
type: integer
error_message:
type: string
payload:
type: object
JobInput:
type: object
required: [name]
properties:
name:
type: string
payload:
type: object

View File

@@ -0,0 +1,7 @@
"""# Summary
Example pipeline family — a reusable skeleton, not an app-specific pipeline.
Replace ``Example*`` names and the three stages with your family's stages. The
shape (abc → impl → pipeline.yaml → resources/) stays the same.
"""

View File

@@ -0,0 +1,53 @@
"""# Summary
Example pipeline family — port contracts (stage interfaces + root interface).
Ports are declared as a DI tree: the root's annotated slots *are* the dependency
decisions. The concrete subclass in ``impl.py`` pins the stage types.
"""
from abc import ABC, abstractmethod
from typing import Any
from pipelines.shared.ports import FinalizePort
class IngestPort(ABC):
"""Obtain the raw input as text/bytes."""
@abstractmethod
def materialize(self, source: Any) -> str:
"""Return the raw input materialized as text."""
class ReadPort(ABC):
"""Parse materialized input into records."""
@abstractmethod
def read(self, content: str) -> list[dict[str, Any]]:
"""Return parsed records."""
class NormalizePort(ABC):
"""Normalize raw records into the target shape."""
@abstractmethod
def build(self, records: list[dict[str, Any]]) -> list[dict[str, Any]]:
"""Return normalized records."""
class ExamplePipelinePort(ABC):
"""Root of the example family.
Notes:
The slot annotations below are contracts. A concrete subclass re-annotates
them with stage implementations; the DI container instantiates the tree.
"""
ingest: IngestPort
reader: ReadPort
normalize: NormalizePort
finalize: FinalizePort
@abstractmethod
async def run(self, source: Any) -> list[dict[str, Any]]:
"""Run the full stage chain and return the normalized records."""

View File

@@ -0,0 +1,71 @@
"""# Summary
Example pipeline family — concrete stages, the wired root, and its registration.
The concrete subclass re-annotates each slot with a stage implementation; the DI
container builds the whole tree. Register the family so the shared registry can
resolve it by ``(kind, variant, mode)``.
"""
from pathlib import Path
from typing import Any
from pipelines.example.abc import (
ExamplePipelinePort,
IngestPort,
NormalizePort,
ReadPort,
)
from pipelines.shared.loader import build_pipeline
from pipelines.shared.ports import FinalizePort
from pipelines.shared.registry import register
class TextIngest(IngestPort):
"""Materialize any source as text."""
def materialize(self, source: Any) -> str:
return source if isinstance(source, str) else str(source)
class LineReader(ReadPort):
"""Read one record per non-empty line."""
def read(self, content: str) -> list[dict[str, Any]]:
return [{"line": line} for line in content.splitlines() if line.strip()]
class PassthroughNormalize(NormalizePort):
"""Return records unchanged (replace with real normalization)."""
def build(self, records: list[dict[str, Any]]) -> list[dict[str, Any]]:
return records
class NoopFinalize(FinalizePort):
"""Drop records (replace with a service-backed finalize stage)."""
async def finalize(self, records: list[dict[str, Any]]) -> list[Any]:
return records
class ExamplePipeline(ExamplePipelinePort):
"""Concrete example family — the dependency decisions live here."""
ingest: TextIngest
reader: LineReader
normalize: PassthroughNormalize
finalize: NoopFinalize
async def run(self, source: Any) -> list[dict[str, Any]]:
content = self.ingest.materialize(source)
records = self.reader.read(content)
return self.normalize.build(records)
def get_example_pipeline() -> ExamplePipeline:
"""Wire and return the example pipeline through the shared loader."""
resources = Path(__file__).parent / "resources"
return build_pipeline(ExamplePipeline, resources_dir=resources)
register("example", "default", ExamplePipeline)

View File

@@ -0,0 +1,18 @@
# Graph spec for the example family: nodes, edges, and the root.
# Mirrors the port tree in abc.py. Replace with your family's graph.
schema: ExampleRecord
nodes:
ingest:
port: IngestPort
reader:
port: ReadPort
normalize:
port: NormalizePort
finalize:
port: FinalizePort
edges:
- ingest -> reader
- reader -> normalize
- normalize -> finalize
roots:
default: ExamplePipeline

View File

@@ -0,0 +1,4 @@
# Pattern / rule fragments for the example family.
# The loader merges every patterns/*.yaml into one registry by key.
example:
enabled: true

View File

@@ -0,0 +1,5 @@
# Runtime spec consumed by the shared loader.
kind: example
variant: default
mode: default
patterns: []

View File

@@ -0,0 +1,32 @@
"""# Summary
Item-sync pipeline — the demo pipeline axis of the blueprint.
Runs on the worker, consumes a pending job, and materializes its payload as an
item. Steps are plain async functions here; once the chain branches or needs
deterministic replay, wrap the steps in a dagpipe DAG (see standards
"06_pipeline_scaffold").
"""
import logging
logger = logging.getLogger(__name__)
async def run_item_sync(job, resources: dict) -> None:
"""Materialize a pending job payload as an item.
Step 1: extract the payload from the job.
Step 2: create the item through the item service.
Args:
job: The pending job document.
resources (dict): Wired services and adaptors.
"""
payload = job.payload or {}
item_svc = resources["item_svc"]
await item_svc.create({
"name": payload.get("name", job.name),
"quantity": payload.get("quantity", 1),
})
logger.info("Created item from job %s", str(job.id))

View File

@@ -0,0 +1,5 @@
"""# Summary
Shared pipeline scaffolding — ports, loader, and registry reused by every
pipeline family. See standards "06_pipeline_scaffold".
"""

View File

@@ -0,0 +1,54 @@
"""# Summary
Pipeline loader — wire a family root through the DI library and attach the
merged pattern registry from ``resources/patterns/*.yaml``.
All families share this loader so wiring is never hand-rolled.
"""
from pathlib import Path
from typing import Any
import yaml
from hexa import build
def load_patterns(resources_dir: str | Path) -> dict[str, Any]:
"""Merge every pattern fragment under ``resources/patterns`` into one map.
Args:
resources_dir (str | Path): Family resources directory.
Returns:
dict[str, Any]: Merged pattern/rule fragments keyed by fragment name.
"""
registry: dict[str, Any] = {}
patterns = Path(resources_dir) / "patterns"
if patterns.is_dir():
for path in sorted(patterns.glob("*.yaml")):
data = yaml.safe_load(path.read_text(encoding="utf-8")) or {}
registry.update(data)
return registry
def build_pipeline(
root_cls,
*,
resources_dir: str | Path = "resources",
instances: dict[str, Any] | None = None,
config: dict[str, Any] | None = None,
):
"""Wire ``root_cls`` and inject the merged pattern registry.
Args:
root_cls: The pipeline root class (a port tree).
resources_dir (str | Path): Directory holding ``patterns/*.yaml``.
instances (dict[str, Any] | None): Runtime objects injected by slot name.
config (dict[str, Any] | None): Config values propagated by field name.
Returns:
The fully wired pipeline root.
"""
merged_instances = dict(instances or {})
merged_instances.setdefault("registry", load_patterns(resources_dir))
return build(root_cls, instances=merged_instances, config=config or {})

View File

@@ -0,0 +1,25 @@
"""# Summary
Shared pipeline ports — the contracts every pipeline family reuses.
A port is an abstract stage contract. Concrete families pin their implementations
on a root's annotated slots; the DI library instantiates the tree.
"""
from abc import ABC, abstractmethod
from typing import Any
class FinalizePort(ABC):
"""Terminal stage — turn normalized records into persisted resources."""
@abstractmethod
async def finalize(self, records: list[dict[str, Any]]) -> list[Any]:
"""Persist records and return the created resources."""
class RegistryPort(ABC):
"""Resolve a pattern / rule fragment by key."""
@abstractmethod
def resolve(self, key: str) -> dict[str, Any]:
"""Return the fragment registered under ``key``."""

View File

@@ -0,0 +1,29 @@
"""# Summary
Family registry — resolve a ``(kind, variant, mode)`` triple to a root class.
A single lookup replaces hand-rolled ``if/else`` family selection.
"""
from typing import Dict, Type
# Populated by ``register`` as families import.
_FAMILIES: Dict[str, Dict[str, Dict[str, Type]]] = {}
def register(kind: str, variant: str, root_cls: Type, mode: str = "default") -> Type:
"""Register ``root_cls`` for a ``(kind, variant, mode)`` triple.
Returns:
Type: The registered class (so it can be used as a decorator).
"""
_FAMILIES.setdefault(kind, {}).setdefault(variant, {})[mode] = root_cls
return root_cls
def resolve_root(kind: str, variant: str, mode: str = "default") -> Type:
"""Return the root class for a ``(kind, variant, mode)`` triple.
Raises:
KeyError: When no family matches the triple.
"""
return _FAMILIES[kind][variant][mode]

View File

@@ -0,0 +1,44 @@
[build-system]
requires = ["setuptools>=68"]
build-backend = "setuptools.build_meta"
[project]
name = "app-template"
version = "0.1.0"
requires-python = ">=3.11"
dependencies = [
"fastapi",
"uvicorn[standard]",
"pydantic>=2",
"motor",
"python-dotenv",
"pyyaml",
"openapi-first",
"mongo-ops",
"hexa",
"dagpipe",
]
[project.optional-dependencies]
dev = [
"pytest",
"pytest-asyncio",
"httpx",
"ruff",
"doc-forge",
]
[tool.setuptools.packages.find]
where = ["."]
include = ["domain*", "infra*", "interfaces*", "pipelines*", "scripts*"]
[tool.pytest.ini_options]
asyncio_mode = "auto"
testpaths = ["tests"]
[tool.ruff]
line-length = 100
target-version = "py311"
[tool.ruff.lint]
select = ["E", "F", "I", "UP"]

View File

@@ -0,0 +1,7 @@
"""# Summary
Operator scripts — run from the project root as ``python -m scripts.<name>``.
They use the composition root (:func:`init.init` / :func:`init.shutdown`) so they
share the exact wiring the API and worker use.
"""

View File

@@ -0,0 +1,32 @@
"""# Summary
Operator script — drop the collections owned by each service for a clean slate.
Run from the project root:
python -m scripts.drop
"""
import asyncio
from init import init, shutdown
async def main() -> None:
"""Delete every service collection, best-effort, then shut down."""
resources = await init()
try:
dropped = 0
for name, svc in resources.items():
if not name.endswith("_svc"):
continue
delete_many = getattr(svc.repo, "delete_many", None)
if delete_many is not None:
await delete_many({})
dropped += 1
print(f"Dropped {dropped} service collection(s).")
finally:
await shutdown()
if __name__ == "__main__":
asyncio.run(main())

View File

@@ -0,0 +1,26 @@
"""# Summary
Operator script — seed demo data through the composition root.
Run from the project root:
python -m scripts.seed
"""
import asyncio
from init import init, shutdown
async def main() -> None:
"""Seed one demo item and shut down cleanly."""
resources = await init()
try:
item_svc = resources["item_svc"]
await item_svc.create({"name": "seed", "quantity": 1})
print("Seeded one item.")
finally:
await shutdown()
if __name__ == "__main__":
asyncio.run(main())

View File

@@ -0,0 +1,21 @@
import importlib
import interfaces.openapi as routes_module
from openapi_first.app import OpenAPIFirstApp
def test_openapi_app_builds():
"""The spec-first app must build and plant each route handler."""
module = importlib.import_module(routes_module.__name__)
for operation in ("create_item", "get_item", "list_items", "delete_item",
"create_job", "get_job", "list_jobs"):
assert hasattr(module, operation)
app = OpenAPIFirstApp(
openapi_path="openapi.yaml",
routes_module=module,
title="Item API",
version="0.1.0",
)
assert app is not None

View File

@@ -0,0 +1,22 @@
from domain.item.models import Item
from domain.job.models import Job
def test_item_defaults():
item = Item(name="widget")
assert item.quantity == 1
def test_job_defaults():
job = Job(name="sync")
assert job.status == "pending"
assert job.retry_count == 0
assert job.error_message is None
def test_job_rejects_unknown_status():
try:
Job(name="sync", status="exploded")
except Exception:
return
raise AssertionError("status should be rejected")

View File

@@ -0,0 +1,28 @@
from types import SimpleNamespace
from pipelines.item_sync import run_item_sync
class FakeItemSvc:
def __init__(self):
self.created = []
async def create(self, data: dict):
self.created.append(dict(data))
async def test_run_item_sync_creates_item():
svc = FakeItemSvc()
job = SimpleNamespace(id="j1", name="widget", payload={"quantity": 4})
await run_item_sync(job, {"item_svc": svc})
assert len(svc.created) == 1
assert svc.created[0] == {"name": "widget", "quantity": 4}
async def test_run_item_sync_uses_payload_name():
svc = FakeItemSvc()
job = SimpleNamespace(id="j1", name="ignored", payload={"name": "gadget"})
await run_item_sync(job, {"item_svc": svc})
assert svc.created[0]["name"] == "gadget"

View File

@@ -0,0 +1,67 @@
"""# Summary
Background worker — polls for pending jobs and runs the item-sync pipeline.
The poll loop is the pipeline axis of the blueprint: resources are created by
the API axis, workers consume them on a fixed cadence.
Notes:
- This process is separate from the API process. Any in-memory event bus is per
process — if the pipeline must surface live progress to the API, swap the bus
adaptor for a broker-backed one (see standards 07_worker_and_progress).
"""
import asyncio
import logging
from config import settings
from pipelines.item_sync import run_item_sync
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(name)s] %(levelname)s: %(message)s",
)
logger = logging.getLogger(__name__)
async def poll_jobs(resources: dict):
"""Poll for pending jobs and run the item-sync pipeline on each.
Args:
resources (dict): Wired services from :func:`init.init`.
"""
job_svc = resources["job_svc"]
logger.info("WORKER STARTED — poll=%ss max_retries=%d",
settings.worker_poll_seconds, settings.worker_max_retries)
while True:
pending = await job_svc.search(filter={"status": "pending"}, limit=10)
logger.info("Found %d pending job(s)", len(pending) if pending else 0)
for job in pending:
job_id = str(job.id)
if (job.retry_count or 0) >= settings.worker_max_retries:
logger.warning("SKIPPING job %s — retries exhausted", job_id)
continue
await job_svc.patch(job_id, {"status": "processing"})
try:
await run_item_sync(job, resources)
await job_svc.patch(job_id, {"status": "done", "error_message": ""})
except Exception as exc: # noqa: BLE001
logger.exception("job %s FAILED: %s", job_id, exc)
await job_svc.patch(job_id, {"status": "failed", "error_message": str(exc)})
await asyncio.sleep(settings.worker_poll_seconds)
if __name__ == "__main__":
from init import init, shutdown
async def _run():
resources = await init()
try:
await poll_jobs(resources)
finally:
await shutdown()
asyncio.run(_run())