defmodule Frontier do @moduledoc """ Smart boundary enforcement for Elixir projects. Frontier enforces architectural boundaries using convention-based defaults. Context roots are public, schemas are auto-exported, and internal modules are private. You only configure exceptions. ## Usage # Project root — global config defmodule MyApp do use Frontier, globals: [Result, Mailer], ignore: [SomeLegacyThing] end # Context root — zero config defmodule MyApp.Accounts do use Frontier end # Context root — with restrictions defmodule MyApp.Billing do use Frontier, reaches: [MyApp.Accounts], externals: [:stripe], exports: [InvoiceGenerator] end """ defmacro __using__(opts) do # Expand aliases at compile time so we store fully qualified module names. # We do this in a clean env to avoid creating compile-time dependencies. expanded_opts = opts |> Macro.prewalk(fn term -> with {:__aliases__, _, _} <- term do Macro.expand(term, %{__CALLER__ | function: {:frontier, 1}, lexical_tracker: nil}) end end) quote bind_quoted: [opts: expanded_opts] do @after_compile {Frontier, :__after_compile__} Module.put_attribute(__MODULE__, :frontier_opts, opts) end end def __after_compile__(%{module: module}, _bytecode) do Frontier.Config.init() opts = Module.get_attribute(module, :frontier_opts) || [] case Frontier.Definition.parse_opts(module, opts) do {:root, config} -> Frontier.Config.register_root(module, config) for ignored_module <- config.ignore do Frontier.Config.register_ignored(ignored_module) end {:context, config} -> Frontier.Config.register_context(module, config) {:ignored, mod} -> Frontier.Config.register_ignored(mod) {:reclassified, target} -> Frontier.Config.register_reclassified(module, target) end end end