[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/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.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 : mcp_hangar.trusted_hosts 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 -> 9. 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 nine 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 (2) + 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 CLEARED: CostReportGenerated now # carries the mcp_server / tool / cost_model dimensions (schema v2), so the # metrics adapter can reconstruct the counter from the event and the # application layer no longer writes one directly. No upcaster was needed -- # the change is additive and v1 rows genuinely lack the dimensions, so # passthrough plus empty defaults is the honest reading of old data. # 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. # # * application.event_handlers.detection_handler -> server.api.sessions # CLEARED. It was the only application -> delivery edge, and it was three # problems in three lines: a function-local import, hiding a reach past the # underscore into another module's private state, in the wrong layer. The # suspended-session store was never route code -- it is a bounded TTL cache, # i.e. an adapter -- so it moved to infrastructure behind # ISessionSuspensionRegistry and the handler is handed the same instance the # routes use. # # * 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.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