Skip to content

Contracts, Boundaries, and Extension Points

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.


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 byCanonical home
Any provider implementationquadkit.contracts.core (e.g. ProviderPriority, DisposableProtocol)
Any HTTP-facing packagequadkit.contracts.web (e.g. ControllerProtocol, web error types)
Any package plus CLI toolingquadkit.contracts.cli (e.g. GenerationResult)
Every packagequadkit.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.

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 directoryConsumed by
quadkit.contracts.coreevery package
quadkit.contracts.exceptionsevery package
quadkit.contracts.webquadkit-web
quadkit.contracts.cliquadkit-cli, quadkit, quadkit-web
quadkit.contracts.data, infra, security, domainquadkit, 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.

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:

  1. Submit a proposal with context, evidence of ≥2 packages sharing it, and a target domain directory.
  2. Get it reviewed against R1–R3.
  3. Move it into the canonical contracts domain directory.
  4. Change the consumer to depend on quadkit-contracts.

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)

All published packages live in dbtinoy-/quadkit (branch main):

AreaPath in the repository
Corecore/quadkit, core/quadkit-contracts
Webpackages/quadkit-web
Testingpackages/quadkit-testing
CLIapps/quadkit-cli

quadkit-contracts is the single contract package for all of them.


  • 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).