[importlinter]
root_package = previously
# Eight of the nine contracts forbid external packages (sqlalchemy, psycopg,
# alembic, anthropic, openai, pyrage, boto3, botocore, html2text, mcp,
# mcp_types, starlette, uvicorn, and imaplib out of the standard library,
# which import-linter counts among them).
# import-linter requires this setting for that, otherwise it aborts with a
# configuration error — it is not a loosening of the contracts themselves.
include_external_packages = True

[importlinter:contract:layers]
name = Layers: cli, connectors beside gate and mcp_server, core beside migrations, storage, contract
type = layers
layers =
    previously.cli
    previously.connectors | previously.gate | previously.mcp_server
    previously.core | previously.migrations
    previously.storage
    previously.contract
# `connectors` came in between `cli` and `core` on 2026-10-06, with the first
# connector: a connector speaks a foreign protocol and hands bytes to `core`,
# so it may use `core` and `core` may not know it. The name followed, because
# the name is what the gate prints.
# `gate` joined `connectors` in its layer on 2026-10-09, and the name followed
# again. The two never import each other: a connector reads a source, the gate
# calls a model, and each hands its result to `core`. The layers contract
# keeps them apart in both directions because they share a layer.
# `mcp_server` joined them on 2026-10-11, and the name followed once more. The
# server reads through `core` and decides with `core.decide`, and it never
# calls a model: that is the gate's, and the shared layer keeps the two from
# importing each other.

[importlinter:contract:core-is-clean]
name = core knows no foreign system and no model
type = forbidden
source_modules =
    previously.core
forbidden_modules =
    sqlalchemy
    psycopg
    alembic
# Until 2026-10-04 this contract carried two named exemptions,
# `core.append -> storage.postgres` and `core.verify -> storage.postgres`,
# because both modules typed their `storage` parameter as the concrete
# `PostgresStorage` under `TYPE_CHECKING`. They were granted by ruling T7-a
# and by ruling T8-c of the 2026-10-02 stage 1a plan, whose execution ledger
# was never shipped and is lost, so both labels are provenance and nothing
# more — the reasoning is the paragraph you are reading.
#
# There is no directory to follow them into, and until 2026-10-04 this
# comment named one. Measured that day against the only record under
# `docs/superpowers/sdd/`: `2026-10-03-dokumentation/progress.md` carries a
# `Ruling T7-a` about an unrelated decision and no `Ruling T8-c` at all, so
# the pointer sent a reader to the wrong decision — worse than a label with
# no pointer, because the reader stops with an answer.
#
# The exemptions were enumerated by name and not matched by a wildcard, so
# that a third module following the same pattern would break this contract
# until somebody granted it deliberately — measured with a throwaway module,
# a wildcard let the new edge through silently.
#
# Stage 1b typed `core` against `contract.store.LogStore[Conn]` instead, and
# the edge is gone. There is nothing to exempt. Should an exemption ever come
# back here, it is a named edge, never a pattern; {ref}`module-boundaries`
# carries the measurement that says why.

[importlinter:contract:core-and-contract-know-no-sql]
name = core and contract import no sqlalchemy
type = forbidden
source_modules =
    previously.core
    previously.contract
forbidden_modules =
    sqlalchemy
    psycopg
# Until 2026-10-04 this contract carried the same two named exemptions as
# `core-is-clean`: edges enumerated one by one, no wildcard across `core`. See
# there for the reasoning, and for what the two stage 1a ruling labels are
# worth. Stage 1b typed `core` against `contract.store.LogStore[Conn]`, the
# edge `core -> storage.postgres` is gone, and there is nothing left to exempt
# here either.
#
# The contract was named `Only storage imports sqlalchemy`, with the id
# `only-storage-knows-sql`, until 2026-10-05. That day the migrations moved into
# the package, and they import SQLAlchemy too, so the old name stated
# something false while the contract still checked the right thing: its
# sources were always `core` and `contract`, and the name now says so.

[importlinter:contract:no-vendor-sdk]
name = Only the gate adapters import vendor SDKs
type = forbidden
source_modules =
    previously
forbidden_modules =
    anthropic
    openai
ignore_imports =
    previously.gate.adapters.anthropic -> anthropic
    previously.gate.adapters.openai_compatible -> openai
# A call to a model happens where the gate is, and an SDK stays inside the
# adapter that speaks for its provider: what leaves an adapter is a `Response`
# or an `AdapterError`, never a type of the vendor. Until 2026-10-09 the
# contract was called `No vendor SDK in the package` and had no exemption,
# because no module called a model. The whole package is the source and the
# two edges are named, never a pattern, for the reason `core-is-clean` gives:
# a third adapter has to be granted deliberately.

[importlinter:contract:only-sealing-knows-age]
name = Only core.sealing imports pyrage
type = forbidden
source_modules =
    previously
forbidden_modules =
    pyrage
ignore_imports =
    previously.core.sealing -> pyrage
# Sealing happens where the rules are, in `core`, so that the store below
# gets and gives ciphertext only: a module in `storage` that could open what
# it stores is the mistake this contract exists to catch. The whole package
# is the source, and the one edge that may exist is named — never a pattern,
# for the reason `core-is-clean` gives. Should `core.sealing` stop importing
# `pyrage`, `lint-imports` fails with "No matches for ignored import"
# (measured on 2026-10-05), so the exemption cannot outlive its edge.

[importlinter:contract:only-s3-knows-boto3]
name = Only storage.s3 imports boto3
type = forbidden
source_modules =
    previously
forbidden_modules =
    boto3
    botocore
ignore_imports =
    previously.storage.s3 -> boto3
    previously.storage.s3 -> botocore
# Storing happens where the foreign systems are, in `storage`, the way SQL
# stays in `storage.postgres`: what reaches `core` is the protocol in
# `contract.blobs` and the errors in `storage.errors`, no `botocore` type.
# `botocore` is forbidden beside `boto3` because the exceptions and the
# client configuration come from there, and an import of either would carry
# the foreign system upwards. Two named edges, out of one module.

[importlinter:contract:only-mail-knows-html2text]
name = Only core.mail imports html2text
type = forbidden
source_modules =
    previously
forbidden_modules =
    html2text
ignore_imports =
    previously.core.mail -> html2text
# Turning HTML into text is part of mapping a mail, and its version goes into
# the payload: a second module that converted HTML would write units the
# payload does not account for. The whole package is the source and the one
# edge is named, never a pattern, as with `pyrage` above.

[importlinter:contract:only-imap-knows-imaplib]
name = Only connectors.imap imports imaplib
type = forbidden
source_modules =
    previously
forbidden_modules =
    imaplib
ignore_imports =
    previously.connectors.imap -> imaplib
# Speaking IMAP is the connector's, and what it hands on is bytes and its own
# error: a second module with `imaplib` would be a second place where a
# folder could be changed, or a password could end up in a message. The whole
# package is the source and the one edge is named, never a pattern, as with
# `pyrage` above.

[importlinter:contract:only-mcp-server-knows-mcp]
name = Only the MCP server imports the MCP SDK
type = forbidden
source_modules =
    previously
forbidden_modules =
    mcp
    mcp_types
    starlette
    uvicorn
ignore_imports =
    previously.mcp_server.server -> mcp
    previously.mcp_server.server -> starlette
    previously.mcp_server.server -> uvicorn
    previously.mcp_server.auth -> starlette
# Speaking MCP is the server's: what leaves `mcp_server` towards `cli` is a
# function that serves, never a type of the SDK, so that the move to OAuth or
# to another SDK stays inside one package. `starlette` and `uvicorn` belong
# here because they are the SDK's HTTP side, and an HTTP endpoint anywhere
# else would be a second door that the token check does not guard. The whole
# package is the source and every edge is named, never a pattern, for the
# reason `core-is-clean` gives: a second module of `mcp_server` that imports
# the SDK has to be granted deliberately.
