# QuadKit — rules for coding agents

> Canonical human docs: https://oridecon.dev/
> Machine indexes: https://oridecon.dev/llms.txt · https://oridecon.dev/llms-full.txt
> Playbook: https://oridecon.dev/quadkit/getting-started/for-coding-agents/
> Fail vs fix: https://oridecon.dev/quadkit/getting-started/common-mistakes/
> Skill: https://oridecon.dev/SKILL.md
> Skill pack (10 skills): https://github.com/dbtinoy-/quadkit-skills (`npx skills add dbtinoy-/quadkit-skills`) — install: https://oridecon.dev/quadkit/getting-started/agent-skills/
> Changelog: https://oridecon.dev/quadkit/changelog/

QuadKit is an async-first, contract-driven Python application framework (3.11+, Apache 2.0, alpha 0.0.x, public release 0.0.50).
Prefer protocols over concrete classes. Pin `>=0.0.50,<0.1`.

Published packages (18): `quadkit`, `quadkit-contracts`, `quadkit-web`, `quadkit-http`, `quadkit-ai-llm`, `quadkit-ai-agents`, `quadkit-ai-mcp`, `quadkit-sql`, `quadkit-cache`, `quadkit-storage`, `quadkit-events`, `quadkit-notification`, `quadkit-tasks`, `quadkit-workflow`, `quadkit-monitor`, `quadkit-auth`, `quadkit-cli`, `quadkit-testing`. These are the distributions installable from PyPI.

## Read this first

1. Load https://oridecon.dev/llms.txt, then this file, then https://oridecon.dev/quadkit/getting-started/for-coding-agents/. Fail vs fix: https://oridecon.dev/quadkit/getting-started/common-mistakes/. Skill: https://oridecon.dev/SKILL.md. Per-package: https://oridecon.dev/llms/index.txt.
2. Install: `uv add "quadkit-cli>=0.0.50,<0.1"` then `quadkit new project my-app --template api` (the currently shipped starter, which includes `quadkit-web`). Use `quadkit add web` to write its configuration section, and add `quadkit-testing` as a dev dependency.
3. Hierarchy: `quadkit-contracts` ← `quadkit` ← regular `quadkit-*` extensions. Peer boundaries are checked by policy; composite packages and documented exceptions are handled separately.
4. Shared protocols, types, and exceptions should live in `quadkit-contracts` rather than creating a dependency between peer implementations.
5. Constructor injection. Type-hint the protocol. No service locator.
6. `register()` binds, `boot()` resolves. They take different protocols and never mix.
7. Domain failures return `Result[T, E]`. Infrastructure failures raise.
8. One project tree: `domains/` (not `models/`), app-root `di/` for `*_provider.py`, `shared/` for cross-cutting.

## Package hierarchy

```
contracts    Shared protocols and types; no QuadKit implementation imports.
    ↑
core         Low-level runtime; has declared third-party dependencies.
    ↑
extensions   Regular peer boundaries are checked by import-linter.
```

The contracts distribution has one runtime dependency, `typing-extensions`. AI sub-packages have a separate sibling-independence check, while the `quadkit.ai` facade is allowed to compose them. Regular extensions may use permitted foundational packages. Composite packages (including admin, CLI, and testing) and specific tracked cross-package allowances are documented in the root `.importlinter` configuration; do not assume every cross-package edge is forbidden.

## Project layout (one tree)

The currently shipped `api` starter writes the canonical shape; `quadkit add` and `quadkit gen` extend it. No `--structure` flag. No `models/` directory.

```
src/<app>/
  app.py                 # create_app() — composition root; ASGI target <app>.app:app
  controllers/           # unscoped HTTP (discovered, never listed in app.py)
  domains/               # unscoped domain types
  di/                    # app providers (*_provider.py)
  services/
  infrastructure/
  shared/                # errors, middleware, health (always cross-cutting)
  modules/<slug>/        # quadkit new module — protocols.py, provider.py, domains/
```

`quadkit gen error` / middleware always land in `shared/`. `quadkit gen controller users --module auth` scopes one node. Adding a module lists it in `create_app()` next to `WebModule`.

Teach `WebModule.configure(discover=["<app>.controllers", "<app>.modules"])` so generated controllers are found. Passing `controllers=[...]` explicitly also works when you want the list in one place.

Human docs: https://oridecon.dev/quadkit/getting-started/project-structure/
CLI map: https://oridecon.dev/quadkit/packages/utilities/quadkit-cli/docs/project_layout/

## Framework repository (branch `main`)

- `AGENTS.md` — canonical rules
- `core/quadkit`, `core/quadkit-contracts` — framework runtime and shared contracts (runtime dependency: `typing-extensions`)
- `packages/quadkit-web`, `packages/quadkit-http` — ASGI HTTP server and outbound async HTTP client
- `packages/quadkit-ai-llm`, `packages/quadkit-ai-agents`, `packages/quadkit-ai-mcp` — LLM clients, agent orchestration, and MCP server/client
- `packages/quadkit-sql`, `packages/quadkit-cache`, `packages/quadkit-storage` — async SQL, multi-backend cache, and blob storage
- `packages/quadkit-events`, `packages/quadkit-notification`, `packages/quadkit-tasks`, `packages/quadkit-workflow` — CQRS/event sourcing, multi-channel notifications, background workers, and sagas/DAGs
- `packages/quadkit-auth`, `packages/quadkit-monitor` — JWT/OAuth2/RBAC auth and Prometheus/OpenTelemetry/health checks
- `packages/quadkit-cli`, `packages/quadkit-testing` — the `quadkit` CLI and test harnesses/fakes/compliance suites

Repository: https://github.com/dbtinoy-/quadkit

## Always

- Contracts and protocols for every service boundary.
- Provider pattern for registration. No business logic on providers.
- IoC via the container for all resolution. Never instantiate services directly.
- Registry-based dispatch. Empty constructor; `with_defaults()` classmethod.
- Absolute imports. `from __future__ import annotations` in every file.
- Async I/O. Store `asyncio.create_task()` references (Ruff RUF006).
- Google-style docstrings. Modern typing (`list[str]`, `str | None`, not `List`/`Optional`).
- `class X(str, Enum)` / `StrEnum` for string enums; `class X(int, Enum)` for ordering.
- Structured logging: `from quadkit.logging import get_logger`. Core config key is `json_format`. Never `print()`.
- Files under 500 lines.

## Never

- Service locator (passing the container into services).
- Imports that violate configured package boundaries; consult the documented exclusions and allowances.
- `result.unwrap()` without `is_ok()`.
- `Result` from constructors or lifecycle hooks.
- `Any` on injected constructor parameters.
- Module-level singletons. Use container-managed singletons.
- Relative imports. Blind `except Exception`.
- Mock classes in production `src/`. Tests fake at the contract boundary.
- `if/elif` chains for type dispatch.
- Duplicate protocol/type/exception definitions. One name, one definition.
- A `models/` directory. Use `domains/`.

## Result vs exceptions

Use `Result[T, E]` when the caller is expected to handle the failure (user not found, validation, permission denied).
Raise when the failure should propagate (database down, serialization bug, container resolution failed, missing API key).

```python
from quadkit.result import Result, Ok, Err

async def find_user(self, user_id: str) -> Result[User, DomainError]:
    user = await self.repo.get(user_id)
    if not user:
        return Err(UserNotFound(user_id))
    return Ok(user)

result = await service.find_user("123")
if result.is_ok():
    user = result.unwrap()
else:
    error = result.unwrap_err()
```

## Provider lifecycle

```python
class StoreProvider(Provider):
    name = "store"
    priority = ProviderPriority.INFRASTRUCTURE

    async def register(self, container: ContainerRegistrarProtocol) -> None:
        container.singleton(KeyValueStore)  # backend chosen in application.yaml

    async def boot(self, container: BootContainerProtocol) -> None:
        store = await container.resolve(KeyValueStore)
        await store.connect()

    async def shutdown(self) -> None:
        await self._store.disconnect()
```

App-root providers: `src/<app>/di/*_provider.py`. Module providers: `modules/<slug>/provider.py`.

## Ambient capabilities (the DI exception)

Clock, identity, and hashing are process-level and imported as ambient objects:

- `from quadkit.primitives import clock` → `clock.now()`
- `from quadkit.identity import ambient as identity` → `identity.new_uuid()`
- `from quadkit.security.hashing import ambient as hashing` → `hashing.hash_hex(data)`

Override in tests with `clock.use(FixedClock(...))`. Everything else is constructor injection.

## Recipes

```bash
uv add "quadkit-cli>=0.0.50,<0.1"
quadkit new project my-app --template api
cd my-app && quadkit run
quadkit gen controller users
quadkit gen service greetings
quadkit new module auth
quadkit gen controller users --module auth
```

New apps wire HTTP with discover, not a hand-maintained controller list:

```python
application.add_modules([
    WebModule.configure(discover=["my_app.controllers", "my_app.modules"]),
])
```

Tests boot the real application with `quadkit-testing` — see https://oridecon.dev/quadkit/guides/testing/.

## When writing code

- Cite package names with the `quadkit-` prefix.
- Link to https://oridecon.dev/ pages from the catalog in /llms.txt.
- Prefer the protocol in `quadkit-contracts` over a concrete class in an extension.
- If multiple packages need a shared type or protocol, prefer contracts; avoid peer implementation imports unless an allowance is explicitly documented.
- FastAPI users keep Starlette routing and Pydantic; add a composition root. See https://oridecon.dev/quadkit/guides/migrating-from-fastapi/.
