# Architecture guardrails for mooring — the dependency-direction rules from the
# migration plan, enforced by `uv run lint-imports` (in release CI beside ruff).
#
# The codebase has a clean implicit layering that the flat module namespace hides:
#
#   L4   cli, hub            the two presentation adapters
#   L3.5 app/                application services shared by both adapters
#   L3   ai/*                AI orchestration + privacy/safety
#   L2   sync/manifest/pbip/deletion (domain core) · editor/schema (marimo bridge)
#   L1   config/config_store/auth/github            identity + config
#   L0   githost/paths/gitsha/shadow/reveal          stdlib-pure leaves
#
# These contracts lock that direction so a backwards import can't silently
# re-enter as the fast-moving ai/ subsystem keeps growing. They are intentionally
# a small, high-value set of `forbidden` rules (not a full strict layering) —
# each maps to a finding in the architecture review and passes today.
#
# As later migration phases land, contracts tighten. Phase 3 added runtime.py
# (a neutral L1 helper below both adapters) and removed the hub->cli edge; Phase 8
# added marimo_rt.py and the marimo-internals-isolated contract below (which needs
# include_external_packages so the forbidden marimo._*/urllib edges are seen).
# The 2026-07 architecture plan's P7 added the two lean-runtime contracts at the
# bottom: the domain core has no import path to the heavy optional deps, and
# spaCy stays behind its ner_spacy backend seam.

[importlinter]
root_package = mooring
include_external_packages = True

[importlinter:contract:foundation-is-pure]
name = L0 foundation leaves import nothing else in mooring (stdlib-pure, callable at load time)
type = forbidden
source_modules =
    mooring.githost
    mooring.paths
    mooring.gitsha
    mooring.shadow
    mooring.reveal
    mooring.trash
    mooring.activity
forbidden_modules =
    mooring.config
    mooring.config_store
    mooring.auth
    mooring.github
    mooring.sync
    mooring.manifest
    mooring.pbip
    mooring.deletion
    mooring.celldiff
    mooring.editor
    mooring.pyproject_env
    mooring.schema
    mooring.ai
    mooring.app
    mooring.hub
    mooring.cli
    mooring.telemetry

[importlinter:contract:identity-below-domain]
name = L1 config/identity/runtime/ai_config does not depend on the domain, AI, or the adapters
type = forbidden
source_modules =
    mooring.config
    mooring.config_store
    mooring.auth
    mooring.github
    mooring.runtime
    mooring.ai_config
forbidden_modules =
    mooring.sync
    mooring.manifest
    mooring.pbip
    mooring.deletion
    mooring.celldiff
    mooring.editor
    mooring.schema
    mooring.ai
    mooring.pushguard
    mooring.app
    mooring.hub
    mooring.cli

[importlinter:contract:sync-domain-is-core]
name = L2 sync domain (the product core) does not depend on AI, the editor, or the adapters
type = forbidden
source_modules =
    mooring.sync
    mooring.manifest
    mooring.pbip
    mooring.pbip_model
    mooring.deletion
    mooring.whatsnew
forbidden_modules =
    mooring.ai
    mooring.pushguard
    mooring.app
    mooring.editor
    mooring.celldiff
    mooring.hub
    mooring.cli
# mooring.celldiff is forbidden for the same reason as mooring.editor: it sits
# on the marimo side of L2 (celldiff -> marimo_rt -> marimo), so a sync import
# would give the frozen core a path to marimo and break frozen-core-is-lean
# below (which counts function-local imports). Diff orchestration lives in the
# hub route, never in the sync domain.

[importlinter:contract:ai-below-adapters]
name = L3 AI orchestration sits below the adapters and never imports the web hub, the CLI, or the app layer above it
type = forbidden
source_modules =
    mooring.ai
    mooring.pushguard
forbidden_modules =
    mooring.app
    mooring.hub
    mooring.cli

[importlinter:contract:app-below-adapters]
name = L3.5 app/ (application services shared by both adapters) never imports the web hub or the CLI — orchestration has a permanent home BELOW the adapters, so the god-object cannot regrow
type = forbidden
source_modules =
    mooring.app
forbidden_modules =
    mooring.hub
    mooring.cli

[importlinter:contract:doctor-below-adapters]
name = The diagnosis engine (doctor.py) sits below ai/ and the adapters — the Copilot probe is appended BY the adapters (each may import both), never by the engine
type = forbidden
source_modules =
    mooring.doctor
forbidden_modules =
    mooring.ai
    mooring.pushguard
    mooring.app
    mooring.hub
    mooring.cli

[importlinter:contract:hub-not-cli]
name = The web hub and the CLI are sibling adapters; the hub must not import the CLI
type = forbidden
source_modules =
    mooring.hub
forbidden_modules =
    mooring.cli

[importlinter:contract:marimo-internals-isolated]
name = The ai/ layer touches neither marimo nor raw HTTP directly — both go through marimo_rt (the transport seam); editor.py keeps marimo's subprocess/.marimo.toml
type = forbidden
# import-linter can only forbid top-level external packages (not submodules), which
# is the right granularity here: ai/ must not import marimo (private codegen lives
# in marimo_rt) nor do its own HTTP control (urllib/http live in marimo_rt's
# KernelControl). marimo_rt and editor.py are not sources, so they keep access.
source_modules =
    mooring.ai
forbidden_modules =
    marimo
    urllib
    http
# Only DIRECT imports are forbidden: ai -> marimo_rt -> marimo (the legitimate path
# through the seam) is indirect and allowed; a NEW direct ai -> marimo/urllib import
# is the violation we want to catch.
allow_indirect_imports = True

[importlinter:contract:frozen-core-is-lean]
name = The frozen build ships lean — the domain core and config/identity layer have NO import path (even indirect) to the heavy optional deps: marimo, the Copilot SDK, spaCy
type = forbidden
source_modules =
    mooring.githost
    mooring.paths
    mooring.gitsha
    mooring.shadow
    mooring.reveal
    mooring.trash
    mooring.activity
    mooring.config
    mooring.config_store
    mooring.ai_config
    mooring.auth
    mooring.github
    mooring.runtime
    mooring.sync
    mooring.manifest
    mooring.pbip
    mooring.pbip_model
    mooring.deletion
    mooring.whatsnew
forbidden_modules =
    marimo
    copilot
    spacy
# mooring.pushguard is deliberately NOT a source here: it imports ai/pii.py,
# whose optional NER hooks give it a STATIC (function-local, lazily executed)
# path to spaCy that this no-indirect contract would count. The guard is an
# adapter-layer concern, not part of the frozen core's lean surface.
# Deliberately NOT requests: sync -> github -> requests is the product's
# legitimate GitHub HTTP path (requests is a core dep, not a heavy optional).
# No allow_indirect_imports here, so a transitive route through a helper module
# is caught too — the lean footprint is a CI fact, not a discipline.

[importlinter:contract:spacy-only-in-its-backend]
name = spaCy (the optional pii-spacy extra) is imported only in ai/ner_spacy.py — everything else reaches it through that backend seam
type = forbidden
source_modules =
    mooring.ai.base
    mooring.ai.batch
    mooring.ai.cellwrite
    mooring.ai.chat
    mooring.ai.context
    mooring.ai.copilot
    mooring.ai.datadictionary
    mooring.ai.egress
    mooring.ai.introspect
    mooring.ai.locality
    mooring.ai.ner
    mooring.ai.pii
    mooring.ai.scan
    mooring.ai.secrets
    mooring.ai.session
    mooring.ai.tools
    mooring.ai.traceback
    mooring.pushguard
    mooring.app
    mooring.hub
    mooring.cli
    mooring.editor
    mooring.marimo_rt
forbidden_modules =
    spacy
# Direct imports only: ner.py legitimately DISPATCHES to the ner_spacy backend
# (an indirect route to spaCy); the violation to catch is a new direct
# `import spacy` outside the backend module. (import-linter has no "everything
# except ner_spacy" source form, hence the explicit list — add new ai/ modules
# here as they appear. Known gap: the mooring.ai package ROOT can't be listed
# without also covering ner_spacy, its submodule.)
allow_indirect_imports = True
