[importlinter]
root_package = mcp_hangar
include_external_packages = False

# ---------------------------------------------------------------------------
# Hexagon layering
# ---------------------------------------------------------------------------
# Read bottom-up: a layer may import anything BELOW it and nothing above.
#
#   shared kernel   cross-cutting vocabulary every layer may speak: logging,
#                   the error taxonomy, protocol constants, the SDK shim, the
#                   vendored task wire (ADR-015). No I/O, no policy.
#   domain          entities, value objects, policies, ports.
#   application     use cases, command/query handlers, sagas, read models.
#   infrastructure  adapters. The root-level modules on this line are adapters
#                   that never moved into the package: metrics is a Prometheus
#                   registry, http_client and stdio_client are transports,
#                   retry/progress/gc are runtime machinery.
#   delivery        HTTP/MCP surfaces and the facade.
#
# The split of the root-level modules is the point of this contract rather than
# a detail. Folding all 14 into the shared kernel would have been a shorter
# file and would have legitimised `domain -> metrics` and
# `domain.contracts.launcher -> http_client` -- a port importing its own
# adapter. Those are the leaks the contract exists to make visible.
#
# Component packages (auth, approvals, compliance, integrations, bootstrap,
# observability) carry their own internal layering and are deliberately out of
# scope here; exhaustive = False keeps them unconstrained rather than
# pretending this contract governs them.

[importlinter:contract:hexagon]
name = Hexagon: shared kernel < domain < application < infrastructure < delivery
type = layers
layers =
    mcp_hangar.server : mcp_hangar.fastmcp_server : mcp_hangar.facade
    mcp_hangar.infrastructure : mcp_hangar.metrics : mcp_hangar.http_client : mcp_hangar.stdio_client : mcp_hangar.retry : mcp_hangar.progress : mcp_hangar.gc
    mcp_hangar.application
    mcp_hangar.domain
    mcp_hangar.logging_config : mcp_hangar.lock_hierarchy : mcp_hangar.redactor : mcp_hangar.errors : mcp_hangar.protocol : mcp_hangar.context : mcp_hangar._sdk_compat : mcp_hangar.tasks_wire : mcp_hangar.negotiation : mcp_hangar.observability
exhaustive = False

# The debt ledger. Every line is an edge that exists today and should not.
# It may shrink; tests/unit/test_import_contracts.py caps it so it cannot grow.
#
# It went 33 -> 11. What cleared did so because the edge turned out to be held
# up by code that could not run: a port that was never wired, a fallback beside
# an injected dependency, a deprecation shim with no callers. Measuring first
# was what found those -- three of the "cycle avoidance" imports avoided no
# cycle at all, and grimp said so in a second.
#
# The eleven that remain are NOT of that kind, and the difference is worth
# stating so the next pass does not mistake churn for progress:
#
#   * application -> metrics (3) + observability.tracing -> metrics.
#     Services instrumenting themselves. A port per counter would turn
#     IMetricsPublisher into a god interface; `metrics` is an adapter (it has a
#     scrape endpoint behind it), so moving it into the shared kernel is
#     relabelling, not repair. cost_handler additionally cannot move its call
#     until CostReportGenerated carries the mcp_server / tool / cost_model
#     dimensions -- widening a persisted event, so a schema version and an
#     upcaster. The tracing one is a third case again: tracing.py is dual, half
#     ambient-span accessors that logging_config legitimately needs and half
#     OTLP exporter setup. Splitting those two apart is the honest fix and is a
#     673-line module's worth of work.
#
#   * context -> domain.value_objects.identity. The kernel's contextvar carrier
#     needing a domain type. Moving IdentityContext down would put a validated
#     domain concept (CallerIdentity enforces an invariant) in the kernel;
#     moving the contextvar up into domain puts a runtime mechanism in a
#     value-objects module and rewrites 41 import sites. Accepted for now
#     BECAUSE both available moves are worse than the edge, not because nobody
#     looked.
#
#   * domain.model.mcp_server -> infrastructure.launchers, and
#     domain.services -> application.read_models.tool_projection. The aggregate
#     constructing its own launchers despite IMcpServerLauncher existing, and a
#     domain service reaching into an application read model. These two look
#     like the ones that cleared -- worth measuring next.
#
#   * infrastructure.truncation.manager -> server.tools.batch.models. A shared
#     DTO living in the delivery layer; likely wants to move down rather than
#     be inverted.
#
ignore_imports =
    mcp_hangar.application.commands.handlers -> mcp_hangar.metrics
    mcp_hangar.application.commands.load_handlers -> mcp_hangar.infrastructure.runtime_store
    mcp_hangar.application.discovery.discovery_orchestrator -> mcp_hangar.metrics
    mcp_hangar.application.event_handlers.cost_handler -> mcp_hangar.metrics
    mcp_hangar.application.event_handlers.detection_handler -> mcp_hangar.server.api.sessions
    mcp_hangar.application.mcp.tooling -> mcp_hangar.fastmcp_server.asgi
    mcp_hangar.context -> mcp_hangar.domain.value_objects.identity
    mcp_hangar.domain.model.mcp_server -> mcp_hangar.infrastructure.launchers
    mcp_hangar.domain.services -> mcp_hangar.application.read_models.tool_projection
    mcp_hangar.infrastructure.truncation.manager -> mcp_hangar.server.tools.batch.models
    mcp_hangar.observability.tracing -> mcp_hangar.metrics
