{"config":{"lang":["en"],"separator":"[\\s\\-]+","pipeline":["stopWordFilter"]},"docs":[{"location":"","title":"\ud83e\udde9 Standards \u2014 the Aetos App Blueprint","text":"

One reusable architecture for every backend application in the Aetos ecosystem: a spec-first HTTP edge, a framework-free domain core, a pluggable infrastructure layer, and a single composition root.

Fill in two axes only and a new app is done: your resource model, and (if needed) your pipeline family. The blueprint itself never names a business domain \u2014 expenses, health metrics, inventory, and analytics are all the same shape underneath.

Doc model: this wiki is written for humans \u2014 how-to guides, the build recipe, and quality gates. The enforceable rules (layering, composition root, CRUD template, pipeline scaffold) and the library registry live in the reference pages, mirrored by the machine-readable standards MCP bundle. Query find_library before writing any infrastructure.

"},{"location":"#key-features","title":"\ud83d\ude80 Key Features","text":""},{"location":"#documentation-structure","title":"\ud83d\udcc1 Documentation Structure","text":"Section Description Overview Philosophy, the one-way dependency rule, architecture App Components The component model and how the pieces wire together Start an App The entry point: gather the two axes, then build Testing & Quality Test layout and acceptance gates

Reference (separate site, mirrors the MCP bundle): the enforceable rules \u2014 Build Recipe, Layering, Composition Root, Spec-First Routing, CRUD Template, Pipeline Scaffold, Worker & Progress, Human-in-the-Loop, Infra Adapters, Genericity Matrix, Library Registry.

"},{"location":"#related-resources","title":"\ud83d\udd17 Related Resources","text":"

\u00a9 Aetoskia Internal \u2014 standards

"},{"location":"01_overview/","title":"Overview","text":"

The blueprint is a hexagonal (ports & adapters) application with a spec-first web edge and a single composition root. It favors explicit wiring over implicit magic and existing internal libraries over hand-written infrastructure. Start here to build the mental model; the enforceable details live in the reference pages.

"},{"location":"01_overview/#architecture","title":"\ud83c\udfd7\ufe0f Architecture","text":"
flowchart LR\n    subgraph EDGE[\"interfaces/ \u2014 HTTP edge\"]\n        OAS[\"openapi.yaml<br/>operationId contract\"]\n        RT[\"<resource>/routes.py<br/>thin handlers\"]\n    end\n\n    subgraph CORE[\"domain/ \u2014 business core\"]\n        MD[\"models\"]\n        PT[\"ports (adapter binding seam)\"]\n        SV[\"services\"]\n    end\n\n    subgraph INFRA[\"infra/ \u2014 adapters\"]\n        RP[\"mongo repos (mongo-ops)\"]\n        AD[\"cache \u00b7 stores \u00b7 llm \u00b7 mail\"]\n        DB[(\"MongoDB\")]\n    end\n\n    CR[\"init.py<br/>composition root\"]\n\n    OAS --> RT\n    RT --> SV\n    SV --> PT\n    SV --> RP\n    PT -. depends on .- RP\n    CR --- RT\n    CR --- SV\n    CR --- RP\n    RP --> DB\n    AD --- CR

The rule holds at every edge: code in an outer ring may depend on a ring closer to the core, never the reverse. domain/ never imports a web framework; only its ports/ seam reaches into infra/ to bind adapters.

Legend \u2014 solid arrows are compile-time calls; dotted arrows are the port/adapter binding, performed by the composition root at startup.

"},{"location":"01_overview/#the-one-way-dependency-rule","title":"\ud83d\udd04 The one-way dependency rule","text":"Ring May import Never imports interfaces/ domain, framework glue (glue only) business logic, infra directly domain/ its own models + its ports/ seam (which binds infra/libraries) web frameworks, interfaces/ infra/ domain ports, drivers, libraries \u2014 init.py everything (the wiring point) \u2014"},{"location":"01_overview/#core-principles","title":"\ud83e\udded Core principles","text":"
  1. One-way dependencies. interfaces -> domain -> infra is allowed; the reverse is forbidden.
  2. Ports in the domain, adapters in infra. A service depends on a port; the port's factory binds the concrete adapter, and the composition root calls those factories.
  3. One composition root. Nothing constructs its own dependencies. All wiring lives in one module (init.py) exposed as a flat resource registry.
  4. Spec-first. The HTTP contract is declared in openapi.yaml; route handlers are looked up by operationId. There are no decorator routers.
  5. Reuse the libraries. Orchestration, DI, Mongo repositories, ingestion, auth, and docs come from internal libraries \u2014 see the Library Registry.
  6. Explicit state machines. Long-running work moves through explicit, durable statuses and can pause for human input.
"},{"location":"01_overview/#runtime-processes","title":"\u2699\ufe0f Runtime processes","text":"

A standard app may run as up to three processes that share the same composition root:

Process Entry point Responsibility API main.py serve HTTP; start/stop resources via lifespan Worker worker.py poll durable jobs, run pipelines, publish progress Scripts python -m scripts.<name> one-shot operator tasks"},{"location":"01_overview/#read-next","title":"\u27a1\ufe0f Read Next","text":""},{"location":"02_components/","title":"App Components","text":"

What ships in every standard app, and how the pieces relate. This is the blueprint's component model \u2014 templates/app/ is a runnable instance of it.

"},{"location":"02_components/#component-model","title":"\ud83d\uddc2\ufe0f Component model","text":"

The blueprint has two orthogonal views: runtime flow (who calls whom) and composition-root wiring (who constructs whom). Keeping them separate makes the layering visible instead of hiding it behind a hub.

"},{"location":"02_components/#runtime-flow","title":"\ud83d\udd00 Runtime flow","text":"

Who calls whom at request and job time:

%%{init: {\"flowchart\": {\"nodeSpacing\": 50, \"rankSpacing\": 80}}}%%\nflowchart TB\n    subgraph REQ[\"Request path \u2014 API process\"]\n        direction LR\n        CL([\"Client<br/>operationId\"]) --> RT[\"interfaces/&lt;resource&gt;/routes.py\"]\n        RT --> SVC[\"&lt;resource&gt;_svc\"]\n        SVC --> REPO[\"&lt;resource&gt;_repo\"]\n    end\n\n    subgraph WRK[\"Background path \u2014 worker process\"]\n        direction LR\n        W[\"worker.py\"] --> JSVC[\"job_svc\"]\n        W --> PIPE[\"pipelines/&lt;family&gt;\"]\n        PIPE --> ISVC[\"item_svc\"]\n        JSVC --> JREPO[\"job_repo\"]\n        ISVC --> IREPO[\"item_repo\"]\n    end\n\n    subgraph INFRA[\"infra/ \u2014 adapters\"]\n        MONGO[(\"MongoDB / mongo_ops\")]\n    end\n\n    REPO --> MONGO\n    JREPO --> MONGO\n    IREPO --> MONGO

Legend \u2014 solid arrows are runtime calls. Nothing is self-constructed.

"},{"location":"02_components/#composition-root-wiring","title":"\ud83e\uddf5 Composition-root wiring","text":"

Who constructs what at startup:

%%{init: {\"flowchart\": {\"nodeSpacing\": 50, \"rankSpacing\": 80}}}%%\nflowchart TB\n    INIT[\"init.py \u2014 composition root\"]\n    SETUP[\"infra/db/setup.py\"]\n    FACT[\"domain/*/ports/*<br/>factory seams\"]\n    REPOS[\"repos<br/>infra/db/repos/mongo.py\"]\n    SVCES[\"services<br/>CrudsServiceAdaptor\"]\n    STATE[\"app.state.db_resources\"]\n    HEXA[\"hexa.build\"]\n    RT[\"routes.py\"]\n    WK[\"worker.py\"]\n\n    INIT --> SETUP\n    INIT --> FACT\n    FACT --> REPOS\n    FACT --> SVCES\n    REPOS --> SVCES\n    HEXA -.-> SVCES\n    INIT --> STATE\n    STATE --> RT\n    INIT --> WK

Legend \u2014 init.py is the only place that imports concrete adapters. The ports/ factory seams bind infra; the API reads resources from app.state.db_resources, the worker calls init() directly.

"},{"location":"02_components/#component-roles","title":"\ud83d\udce6 Component roles","text":""},{"location":"02_components/#1-interfaces-the-edge","title":"1. interfaces/ \u2014 the edge","text":"

Thin handlers bound to openapi.yaml operationIds. They parse the request, call a service, and shape the response. No business rules, no I/O.

"},{"location":"02_components/#2-domain-the-core","title":"2. domain/ \u2014 the core","text":"

Models, ports, and services (use cases). No framework imports. The ports/ package is the one seam allowed to reach into infra/: each port module declares the document shape and a factory that binds the adapter.

"},{"location":"02_components/#3-infra-the-adapters","title":"3. infra/ \u2014 the adapters","text":"

Concrete implementations of the domain ports plus cross-cutting infrastructure:

"},{"location":"02_components/#4-pipelines-data-driven-families-optional","title":"4. pipelines/ \u2014 data-driven families (optional)","text":"

Extraction and enrichment families, each a deterministic step chain (abc.py / impl.py / pipeline.yaml / resources/). Roots are registered in the pattern registry and driven either by a worker job or a CLI.

"},{"location":"02_components/#5-workerpy-the-job-worker-optional","title":"5. worker.py \u2014 the job worker (optional)","text":"

A separate process that polls durable job collections, runs pipeline roots, and publishes progress to the progress bus. It shares the same composition root as the API.

"},{"location":"02_components/#6-initpy-the-composition-root","title":"6. init.py \u2014 the composition root","text":"

The only place anything is constructed: leaf repositories \u2192 dependent repositories \u2192 services \u2192 adapters. Exposes get_svc overloads and starts / stops resources on the app lifespan.

"},{"location":"02_components/#how-the-request-flows","title":"\ud83d\udd17 How the request flows","text":"
sequenceDiagram\n    participant C as Client\n    participant R as route (interfaces)\n    participant S as service (port factory / adapter)\n    participant P as repo (infra)\n    participant DB as MongoDB\n\n    C->>R: POST /items (operationId)\n    R->>S: svc.create(payload)\n    S->>P: repo.insert(doc)\n    P->>DB: insert\n    DB-->>P: ack\n    P-->>S: doc\n    S-->>R: result\n    R-->>C: 200 JSON
"},{"location":"02_components/#related","title":"Related","text":""},{"location":"03_start_an_app/","title":"\ud83d\ude80 Start an App","text":"

This is the entry point for building a new application from the blueprint. The goal is one deterministic pass: you supply only the two axes, and the agent does the rest by reading the standards MCP, importing Tier A libraries, and copying Tier B template components.

Doc model: this page is the human entry point. The deterministic, machine-facing checklist lives in the reference page Build Recipe, served as docs://modules/build_recipe.

"},{"location":"03_start_an_app/#what-the-agent-needs-from-you","title":"\ud83e\uddfe What the agent needs from you","text":"

Gather these before starting \u2014 the start_app MCP prompt asks for them:

  1. App name + one-line purpose.
  2. Axis 1 \u2014 resources. Each resource, its fields, and relationships. Paste openapi.yaml if you already have it.
  3. Axis 2 \u2014 pipelines. Only if an input needs a multi-step transformation: the families and their input formats (or \"none\").
  4. Auth needed now? Default no. Auth is deferred (py-jwt primitives only), and an authless app is just a spec with no security keys \u2014 see Spec-First Routing \u2192 Authless apps.
  5. Integrations. Mongo, ingestion formats, email, LLM provider.

If you can state axes 1 and 2, the rest is mechanical.

"},{"location":"03_start_an_app/#how-to-start","title":"\u25b6\ufe0f How to start","text":"

From an MCP client connected to the standards server:

  1. Invoke the intake prompt (or start_app with no arguments) to get the intake questions.
  2. Answer them, then invoke build (build(app_name=..., resources=..., pipelines=..., auth=...)), or use start_app to intake-and-build in one call.
  3. The prompt hands off to docs://modules/build_recipe; the agent scaffolds with get_app_template() and pulls exact files with get_app_component(kind, resource=..., family=...), authoring only the app-specific parts.
  4. To validate the standard without touching a real app, invoke trial (trial(app_name=..., build_path=...)) to build into a scratch directory, prove it builds, and report findings.

Manual alternative \u2014 copy the skeleton and follow the reference recipe:

cp -r <docs>/standards/src/templates/app apps/<your-app>\n
"},{"location":"03_start_an_app/#reference","title":"\ud83d\udcda Reference","text":""},{"location":"03_start_an_app/#related","title":"Related","text":""},{"location":"04_development/01_testing/","title":"Testing & Quality Gates","text":"

Two suites run under pytest with asyncio auto mode: integration (the HTTP edge against fakes) and unit (pure domain and pipeline logic). Finish a blueprint app only when every gate below passes.

"},{"location":"04_development/01_testing/#layout","title":"\ud83d\uddc2\ufe0f Layout","text":"
tests/\n    integration/     # TestClient(app) + mocked resources\n    unit/            # models, converters, pipeline stages/roots\n    conftest.py      # fixtures\n
"},{"location":"04_development/01_testing/#integration-tests","title":"\ud83d\ude80 Integration tests","text":"
client = TestClient(app)\napp.state.db_resources = {\"item_svc\": FakeItemService(), ...}\n\ndef test_create_item():\n    r = client.post(\"/items\", json={\"name\": \"x\"})\n    assert r.status_code == 200\n
"},{"location":"04_development/01_testing/#unit-tests","title":"\ud83e\uddea Unit tests","text":""},{"location":"04_development/01_testing/#config-consistency-tests","title":"\ud83d\udd10 Config-consistency tests","text":"

For rule-driven pipelines, add a test that cross-references rules against the declared pattern registry: every flag emitted must be defined; every defined flag must be reachable. This catches drift without running the pipeline.

"},{"location":"04_development/01_testing/#quality-gates","title":"\u2705 Quality gates","text":"

A definition of done for any standard app:

"},{"location":"04_development/01_testing/#conventions","title":"\ud83d\udca1 Conventions","text":""},{"location":"04_development/01_testing/#related","title":"Related","text":""},{"location":"04_development/02_releasing/","title":"Releasing & Versioning","text":"

How to cut a release of any Aetos repository. The rule lives in the reference page Versioning & Changelog; this is the runbook you follow.

Every repository is one of two Drone shapes \u2014 a PyPI package or a Docker deployment (Drone Pipelines). Both release the same way: the version bump and changelog promotion go in the merge request, and the tag is created on the merged commit.

"},{"location":"04_development/02_releasing/#before-you-start","title":"\ud83e\uddfe Before you start","text":""},{"location":"04_development/02_releasing/#pick-the-next-version","title":"\ud83d\udea6 Pick the next version","text":"

The version file and the tag must be identical. For 0.y.z, 0.y is the major line (see the reference page); once at 1.0.0, normal SemVer applies.

Change 0.y.z \u2192 1.0.0+ \u2192 Breaking 0.(y+1).0 (x+1).0.0 Feature 0.y.(z+1) x.(y+1).0 Fix 0.y.(z+1) x.y.(z+1)"},{"location":"04_development/02_releasing/#release-in-the-mr","title":"\ud83c\udf3f Release in the MR","text":"

The version is known while the change is still a merge request, so the bump rides along with it. Do all edits in the MR:

[Unreleased]: https://git.aetoskia.com/aetos/<repo>/compare/x.y.z...HEAD\n[x.y.z]: https://git.aetoskia.com/aetos/<repo>/compare/x.y.(z-1)...x.y.z\n
"},{"location":"04_development/02_releasing/#tag-the-merged-commit","title":"\ud83c\udff7\ufe0f Tag the merged commit","text":"
  1. Merge or squash the MR into main.
  2. Create the tag on the resulting commit: git tag x.y.z (lightweight).
  3. Push the tag (git push origin x.y.z) \u2014 Drone builds the release.
"},{"location":"04_development/02_releasing/#agent-assisted-tagging","title":"\ud83e\udd16 Agent-assisted tagging","text":"

The agent does not author or merge the MR and never tags before the merge. After you confirm the MR is merged/squashed and tell it to proceed, the agent:

  1. Waits for that explicit go-ahead.
  2. Finds the commit that introduced the bump:
  3. package: git log -S 'version = \"x.y.z\"' -n 1 --format='%H %s' -- pyproject.toml
  4. service: git log -S 'x.y.z' -n 1 --format='%H %s' -- VERSION
  5. Verifies that commit's version file equals x.y.z, then tags only that commit: git tag x.y.z <sha>.
  6. Does not push \u2014 it asks you to run git push origin x.y.z.
"},{"location":"04_development/02_releasing/#what-drone-does","title":"\ud83e\udd16 What Drone does","text":"

If the guard fails, nothing is published \u2014 fix the version file, amend the commit, and move the tag only if it was never pushed.

"},{"location":"04_development/02_releasing/#backfill-recovery","title":"\ud83d\udedf Backfill / recovery","text":"

If an older tag never published (a past version-file mismatch, an index outage):

"},{"location":"04_development/02_releasing/#if-a-release-is-wrong","title":"\ud83d\udd01 If a release is wrong","text":""},{"location":"04_development/02_releasing/#related","title":"Related","text":""}]}