Every interface that crosses a package boundary in the framework must live in
quadkit-contracts — a zero-dependency package that only ever defines
types, protocols, and exceptions. This guide states the rules for deciding
where a new contract goes, when a local protocol is allowed, and how a
consumer upstreams a contract.
The golden rule: a type, protocol, or exception shared by two or more
packages belongs in quadkit-contracts, organized by domain directory —
never by package name.
1. The Rules
Section titled “1. The Rules”R1 — The golden rule
Section titled “R1 — The golden rule”A type, protocol, or exception referenced by two or more packages must
live in quadkit-contracts, under the domain directory that describes
what it is, not who uses it.
| Shared by | Canonical home |
|---|---|
| Any provider implementation | quadkit.contracts.core (e.g. ProviderPriority, DisposableProtocol) |
| Any HTTP-facing package | quadkit.contracts.web (e.g. ControllerProtocol, web error types) |
| Any package plus CLI tooling | quadkit.contracts.cli (e.g. GenerationResult) |
| Every package | quadkit.contracts.exceptions |
Rule of thumb: if the same import statement appears in two or more
packages, the symbol is a contract. One package importing a symbol from
another package’s internals is a boundary violation.
R2 — One contracts package, always
Section titled “R2 — One contracts package, always”There is no separate contracts package per tier, and none may be created.
Everything published — core, web, CLI, testing — shares the single
quadkit-contracts distribution. A new domain directory under
quadkit.contracts requires a short proposal stating: which packages share
it, which packages consume it (if any), and which existing domains it imports.
The published packages consume these domains today:
| Domain directory | Consumed by |
|---|---|
quadkit.contracts.core | every package |
quadkit.contracts.exceptions | every package |
quadkit.contracts.web | quadkit-web |
quadkit.contracts.cli | quadkit-cli, quadkit, quadkit-web |
quadkit.contracts.data, infra, security, domain | quadkit, quadkit-web, quadkit-testing |
quadkit-contracts also reserves domain directories for packages that are not
published yet. Reserved directories are not part of the public API surface:
nothing in the published packages depends on them, and they carry no
compatibility promise.
R3 — Stability is marked by consumption, not by directory
Section titled “R3 — Stability is marked by consumption, not by directory”A contract consumed by a published package is de facto public API — treat it as frozen (semver minor+ only). A directory nothing published imports is free to change.
R4 — Local protocols are seams, not contracts
Section titled “R4 — Local protocols are seams, not contracts”A package may keep its own protocols.py for internal implementation
seams (e.g. a plugin used only inside the package). It must be consumed only
within that package. A cross-package import of a local protocol module is a
violation — the protocol must be promoted into the matching contracts domain
per R1/R2.
This rule is enforced mechanically: an import linter forbids cross-package imports of the known local protocol modules.
R5 — One canonical definition
Section titled “R5 — One canonical definition”No duplicate protocol, type, or exception definitions. If a symbol already
exists in quadkit-contracts, reference it — never copy it into a package.
A local copy with a different signature is drift, not isolation.
R6 — Consumer contracts stay in the consumer project
Section titled “R6 — Consumer contracts stay in the consumer project”A contract used only by a consumer’s application stays in that application. It never becomes a new framework package.
To upstream a consumer contract that became generally useful:
- Submit a proposal with context, evidence of ≥2 packages sharing it, and a target domain directory.
- Get it reviewed against R1–R3.
- Move it into the canonical contracts domain directory.
- Change the consumer to depend on
quadkit-contracts.
2. Decision Tree
Section titled “2. Decision Tree”New type / protocol / exception needed|+-- Used by >= 2 framework packages?| || +-- YES --> quadkit-contracts, by domain directory (R1)| | +-- consumed by a published package --> freeze (R3)| | +-- reserved for an unpublished pkg --> free to change (R3)| || +-- NO --> imported from another package today? --> promote to contracts (R4 violation)| +-- single-package internal seam --> local protocols.py (R4)|+-- Consumer application code only --> stays in the consumer project (R6)3. Where the published packages live
Section titled “3. Where the published packages live”All published packages live in dbtinoy-/quadkit
(branch main):
| Area | Path in the repository |
|---|---|
| Core | core/quadkit, core/quadkit-contracts |
| Web | packages/quadkit-web |
| Testing | packages/quadkit-testing |
| CLI | apps/quadkit-cli |
quadkit-contracts is the single contract package for all of them.
4. Checklist for Adding a Contract
Section titled “4. Checklist for Adding a Contract”- Who consumes it? If ≥2 packages →
quadkit-contracts(R1). - Which domain directory describes it? (never a package name)
- Does it already exist in contracts? If yes, reference it (R5).
- Is the domain directory already sanctioned, or does it need a proposal (R2)?
- Does it import only lower-level domains (R3)?
- If it stays local: is it a single-package seam, and does it pass the import linter (R4)?
- Consumer-only? Leave it in the consumer project (R6).
Next Steps
Section titled “Next Steps”quadkit-contractspackage — README, API dump, and architecture notes- Architecture — why extensions never import each other
- Container Protocols — the protocols the container resolves
- Compatibility — dependency rules and documented exceptions