[importlinter]
root_package = agentdeck
include_external_packages = True

# Enforced today: the error hierarchy is the one module every layer may import, so it
# must stay free of the execution engines and the HTTP surface.
[importlinter:contract:errors-are-engine-free]
name = errors imports no engine or surface
type = forbidden
source_modules =
    agentdeck.errors
forbidden_modules =
    agents
    fastapi
    redis
    psycopg

# Activated by PR #1 (the events schema). core is the innermost ring: stdlib and
# pydantic only, so the outer rings are also forbidden: importing one would invert the
# dependency direction the whole refactor rests on.
[importlinter:contract:core-is-engine-free]
name = core imports no engine, surface or outer ring
type = forbidden
source_modules =
    agentdeck.core
forbidden_modules =
    agents
    fastapi
    redis
    psycopg
    agentdeck.authoring
    agentdeck.skills
    agentdeck.mcp
    agentdeck.runtime
    agentdeck.deck
    agentdeck.testing
    agentdeck.adapters

# The Runtime is the use-case layer: it orchestrates through ports and must never reach for
# an engine, a store or a surface directly: those arrive from the composition root.
# Narrower than it looks: importing agentdeck.runtime.service still executes the v1
# agentdeck/runtime/__init__.py, which pulls in settings and workspace. Story 2 empties that.
[importlinter:contract:runtime-service-is-adapter-free]
name = the Runtime imports core only, never an adapter or a surface
type = forbidden
source_modules =
    agentdeck.runtime.service
    agentdeck.runtime.dispatch
forbidden_modules =
    agents
    fastapi
    redis
    psycopg
    agentdeck.adapters
    agentdeck.authoring
    agentdeck.skills
    agentdeck.mcp
    agentdeck.deck

# These adapters wrap no external system at all (sqlite3 is stdlib), so they stay pure
# forever: a store or engine reaching back into the Runtime or a surface would invert the
# dependency direction. This is also what keeps `agents` scoped to the openai-agents
# adapter (#52): every other store/engine here is explicitly forbidden from importing it.
# The control and lease adapters join this list for the same reason: all are sqlite3/dict only.
[importlinter:contract:pure-adapters-stay-pure]
name = the memory store, sqlite store, stub/native executors and control/lease adapters import core only
type = forbidden
source_modules =
    agentdeck.adapters.stores.memory
    agentdeck.adapters.stores.sqlite
    agentdeck.adapters.executors.stub
    agentdeck.adapters.executors.native
    agentdeck.adapters.control.memory
    agentdeck.adapters.control.sqlite
    agentdeck.adapters.leases.memory
    agentdeck.adapters.leases.sqlite
forbidden_modules =
    agents
    fastapi
    redis
    psycopg
    agentdeck.runtime
    agentdeck.authoring
    agentdeck.skills
    agentdeck.mcp
    agentdeck.deck

[importlinter:contract:postgres-store-takes-only-psycopg]
name = the Postgres event log imports core and psycopg only
type = forbidden
source_modules =
    agentdeck.adapters.stores.postgres
forbidden_modules =
    agents
    fastapi
    redis
    agentdeck.runtime
    agentdeck.authoring
    agentdeck.skills
    agentdeck.mcp
    agentdeck.deck

# MCP relocation (#78): the raw MCP protocol SDK is the tool adapter's alone, so a second
# client can't quietly appear next to an engine or a surface. The list is everything except
# `agentdeck.adapters.tools.mcp`, including the openai-agents engine, which may import
# `agents` but gets its MCP servers as opaque handles off a `ToolSet`.
# This contract covers the `mcp` distribution ONLY. The SDK surface the adapter actually
# uses is `agents.mcp`, which import-linter cannot express (subpackages of external packages
# aren't valid contract targets): ruff's TID251 banned-api bans that one, see pyproject.
[importlinter:contract:mcp-protocol-sdk-is-tool-adapter-private]
name = only the MCP tool adapter imports the MCP protocol SDK
type = forbidden
source_modules =
    agentdeck.core
    agentdeck.runtime
    agentdeck.adapters.stores
    agentdeck.adapters.control
    agentdeck.adapters.executors
    agentdeck.deck
    agentdeck.authoring
    agentdeck.skills
    agentdeck.mcp
forbidden_modules =
    mcp

# Langfuse as a sink (#77): tracing is a reader of the event stream, so the Langfuse SDK
# belongs to the telemetry adapter alone: an engine or a surface opening a span itself is
# what left workflows untraced in the first place, and a *second* module constructing a client
# is what made #162's span filter never apply. `langfuse` is a top-level distribution, so
# import-linter can name it directly; the MCP ban above needed ruff only because `agents.mcp`
# is a subpackage. `agentdeck.runtime` is listed whole now that `runtime/observability.py`
# (the second altitude this contract had to carve an exception for) is gone, and
# `agentdeck.authoring` joins it, since a direct-call runner opening spans of its own is what
# left #162's orphan trees. `agentdeck.composition` and `agentdeck.deck` cannot be listed: the
# composition root resolves the sink on purpose (`resolve_sinks`), so every path from them
# reaches the SDK. That one place constructs the client, and that `build_runtime` is not it,
# are pinned by name in `tests/test_observability.py` instead.
[importlinter:contract:langfuse-is-telemetry-private]
name = only the telemetry adapter imports the Langfuse SDK
type = forbidden
source_modules =
    agentdeck.core
    agentdeck.runtime
    agentdeck.authoring
    agentdeck.adapters.stores
    agentdeck.adapters.control
    agentdeck.adapters.executors
    agentdeck.adapters.tools
forbidden_modules =
    langfuse

# The sink reads events and writes spans, nothing else: deleting this directory must cost
# Langfuse traces and not one behavior more.
[importlinter:contract:telemetry-adapter-is-engine-free]
name = the telemetry adapter imports core, settings and Langfuse only
type = forbidden
source_modules =
    agentdeck.adapters.telemetry
forbidden_modules =
    agents
    fastapi
    redis
    psycopg
    agentdeck.adapters.control
    agentdeck.adapters.executors
    agentdeck.adapters.stores
    agentdeck.adapters.tools
    agentdeck.authoring
    agentdeck.skills
    agentdeck.mcp
    agentdeck.observers
    agentdeck.deck

# Protocol SPI (#545): a plugin wraps a Deck through this package's public surface, never a
# backend directly. allow_indirect_imports lets `Deck`/`Run` in under TYPE_CHECKING for typing
# (Deck itself imports both rings) while still failing on a direct `agentdeck.runtime`/
# `agentdeck.adapters` import inside `agentdeck/bindings/`  -  the reach-around the issue warns
# review would otherwise have to catch by eye.
[importlinter:contract:bindings-spi-is-deck-facing-only]
name = the protocol SPI reaches a Deck through its public surface, never runtime or adapters directly
type = forbidden
source_modules =
    agentdeck.bindings
forbidden_modules =
    agentdeck.runtime
    agentdeck.adapters
allow_indirect_imports = True
# The exceptions, each narrow: `agentdeck/bindings/native.py` and `agentdeck/bindings/agui.py`
# are the public import paths for in-tree bindings whose implementations live under `adapters/`
# (ruling 36). Each is a factory, not part of the SPI, and `agentdeck/bindings/__init__.py`
# imports neither, so `import agentdeck.bindings` still reaches no adapter and no HTTP dependency.
ignore_imports =
    agentdeck.bindings.native -> agentdeck.adapters.bindings.native.binding
    agentdeck.bindings.terminal -> agentdeck.adapters.bindings.terminal.binding
    agentdeck.bindings.agui -> agentdeck.adapters.bindings.agui.binding

# Every in-tree binding (#548, #599), held to the same allow-list ruling 19 gives an
# out-of-tree plugin (proven separately by `tests/bindings/fixture_plugin/.importlinter`).
# `source_modules` names the parent package once: a new `adapters/bindings/<name>/` is covered
# automatically, with no per-binding contract to remember. `agentdeck.core` joins the forbidden
# list here (the fixture plugin already forbids it); `agentdeck.adapters` itself is not
# forbidden: every binding lives under it, and Python imports every ancestor package on
# `import agentdeck.adapters.bindings.<name>`. Bindings sharing no implementation helpers is
# a separate contract below (#599, #603), since `forbidden`/parent-package containment cannot
# express "these siblings must not import each other".
[importlinter:contract:binding-implementations-are-spi-facing-only]
name = a binding implementation reaches a Deck through the public SPI only, never runtime, core, Deck or another adapter
type = forbidden
source_modules =
    agentdeck.adapters.bindings
forbidden_modules =
    agentdeck.deck
    agentdeck.runtime
    agentdeck.core
    agentdeck.authoring
    agentdeck.skills
    agentdeck.mcp
    agentdeck.serve
    agentdeck.adapters.control
    agentdeck.adapters.executors
    agentdeck.adapters.leases
    agentdeck.adapters.stores
    agentdeck.adapters.telemetry
    agentdeck.adapters.tools
allow_indirect_imports = True

# The contract above cannot stop `native` importing `terminal`'s implementation directly
# (both pass its `source_modules` check). AG-UI and every later binding join this list.
[importlinter:contract:bindings-are-independent]
name = in-tree bindings share no implementation: each reaches the Deck through the public SPI only
type = independence
modules =
    agentdeck.adapters.bindings.native
    agentdeck.adapters.bindings.terminal
    agentdeck.adapters.bindings.agui
# #606's TYPE_CHECKING re-exports (agentdeck/bindings/__init__.py) are never executed, so
# they are not the shared-implementation route this contract exists to catch.
ignore_imports =
    agentdeck.bindings -> agentdeck.bindings.native
    agentdeck.bindings -> agentdeck.bindings.terminal
    agentdeck.bindings -> agentdeck.bindings.agui

# AG-UI (#595/#596): the `ag-ui-protocol` SDK is this binding's wire, not a general-purpose
# dependency, so no other ring may import it (ruling 16); everything else mirrors Native's own
# contract above, since the AG-UI binding is held to the same SPI-facing-only rule.
# `agentdeck.bindings` itself is not listed: `bindings-spi-is-deck-facing-only` already keeps
# everything in it but the one named exception off `agentdeck.adapters` entirely.
[importlinter:contract:agui-protocol-sdk-is-agui-adapter-private]
name = only the AG-UI binding imports the ag-ui-protocol SDK
type = forbidden
source_modules =
    agentdeck.core
    agentdeck.runtime
    agentdeck.adapters.stores
    agentdeck.adapters.control
    agentdeck.adapters.executors
    agentdeck.adapters.leases
    agentdeck.adapters.telemetry
    agentdeck.adapters.tools
    agentdeck.adapters.bindings.native
    agentdeck.adapters.bindings.terminal
    agentdeck.deck
    agentdeck.authoring
    agentdeck.skills
    agentdeck.mcp
forbidden_modules =
    ag_ui
# The same TYPE_CHECKING lazy-export chain the sibling contracts above ignore: it is never
# executed (agentdeck/bindings/__init__.py), so it carries no runtime path to ag_ui.
ignore_imports =
    agentdeck.bindings -> agentdeck.bindings.agui
