Packages

EXPERIMENTAL — Shared contract for the DpExchange family of exchange adapters: behaviours, value types, canonical-pair normalisation and a conformance suite.

Current section

Files

Jump to
Raw

mix.exs

defmodule DpExchangeCore.MixProject do
  use Mix.Project

  # SEED, not a release. CI increments the last segment of whatever it finds
  # here, so `0.1.0` first publishes as `0.1.1`. Hand-editing this to `0.2.0`
  # is how a breaking change is signalled (§7.3, D18). The bump script matches
  # the attribute assignment below by its exact literal form — do not reformat
  # it, and do not repeat that form anywhere else in this file, comments
  # included, or the script will rewrite the wrong line.
  @version "0.3.113"
  @source_url "https://github.com/DistortionPoint/dp-exchange-core"

  def project do
    [
      app: :dp_exchange_core,
      version: @version,
      elixir: "~> 1.18",
      elixirc_paths: elixirc_paths(Mix.env()),
      start_permanent: Mix.env() == :prod,
      # `false` in :test only. `DpExchange.Core.CredentialRedactionCheck`'s own test
      # fixtures compile a struct's `Inspect` implementation (derived or hand-written)
      # at TEST RUNTIME via `Code.compile_string/2`, after `mix test`'s own protocol
      # consolidation has already baked a dispatch table that does not know that
      # implementation exists yet — consolidated, the new impl module is real and loaded
      # but never reached, and `inspect/1` silently falls back to printing every field.
      # A real venue package never hits this: its `Credentials` module compiles as part
      # of the ordinary `mix compile` pass, before consolidation runs, so consolidation
      # staying on in :dev and :prod costs that package nothing.
      consolidate_protocols: Mix.env() != :test,
      deps: deps(),
      aliases: aliases(),
      dialyzer: dialyzer(),
      preferred_cli_env: preferred_cli_env(),
      test_coverage: test_coverage(),

      # Hex.pm
      name: "DpExchangeCore",
      description:
        "EXPERIMENTAL — Shared contract for the DpExchange family of exchange adapters: " <>
          "behaviours, value types, canonical-pair normalisation and a conformance suite.",
      package: package(),
      source_url: @source_url,
      docs: docs(),

      # UsageRules
      usage_rules: usage_rules()
    ]
  end

  # No `mod:` — a library does not start itself (§7.7). Consumers supervise the
  # facade through `child_spec/1`; a package that opened sockets on load would
  # take restart strategy and shutdown order away from the host (D12).
  #
  # `:tools` ships `:xref`, which `Core.UnwiredCheck` calls directly — the "16.
  # internal wiring" assertion's call-graph engine. `:tools` is part of every standard
  # OTP install, the same way `:mix` and `:ex_unit` already are for this package's own
  # dev/test PLT, so declaring it costs nothing and stops the compiler warning that
  # `:xref.*` is an undefined module.
  def application do
    [
      extra_applications: [:logger, :tools]
    ]
  end

  # `test/fixtures` is separate from `test/support`: `test/support` holds
  # `ReferenceVenue`, the fixture assertion 16 (`Core.UnwiredCheck`) validates itself
  # against (`package_root: "test/support"` in adapter_contract_test.exs), so anything
  # else compiled alongside it there would be scoped INTO that same self-check and
  # show up as an internal function with no caller — which is exactly what happened to
  # `UnwiredFixture` before it moved here.
  defp elixirc_paths(:test), do: ["lib", "test/support", "test/fixtures"]
  defp elixirc_paths(_env), do: ["lib"]

  defp deps do
    [
      # Runtime. `websockex` is absent at every strength — not a hard dep and
      # not `optional: true`. Transport is venue-owned (D12, D20); Core has no
      # socket to open, and an optional dep would still advertise transport on
      # the package page.
      # `~> 0.7`, not `~> 0.5`: `HttpClient` passes `:request_timeout`, which Req added in
      # 0.7.0. Under `~> 0.5` a consumer resolving Req 0.5.x or 0.6.x had EVERY request fail
      # with `unknown option :request_timeout` from 0.3.38 on. `script/check_dependency_floor.sh`
      # found it on 2026-09-27: 29 failures at Req 0.5.0. Query strings are built into the
      # URL here, never passed as `params:`, so Req 0.7.0-0.7.3's duplicate-param regression
      # does not reach this package.
      {:req, "~> 0.7"},
      {:jason, "~> 1.4"},
      # `or ~> 3.0` since 2026-09-27. Decimal 3 makes the CVE-2026-32686 limits the default
      # (parsing `1e1000000000` no longer materialises it), and `~> 2.0` alone stopped a
      # consumer from taking that. The full suite passes on 3.1.1. 3.x also refuses to parse
      # more than 34 digits, which a venue number never approaches; the venue packages were
      # fuzzed on 3.1.1 before they widened too.
      {:decimal, "~> 2.0 or ~> 3.0"},
      {:telemetry, "~> 1.0"},

      # Dev/Test. `plug` is here so the request pipeline can be exercised through
      # Req's test seam without a network — it never ships (`only: :test`).
      {:plug, "~> 1.16", only: :test},
      {:usage_rules, "~> 1.2", only: :dev},
      {:ex_doc, "~> 0.34", only: :dev, runtime: false},
      {:credo, "~> 1.7", only: [:dev, :test], runtime: false},
      {:dialyxir, "~> 1.4", only: [:dev, :test], runtime: false},
      {:sobelow, "~> 0.13", only: [:dev, :test], runtime: false}
    ]
  end

  defp test_coverage do
    [
      threshold: 90,
      ignore_modules: []
    ]
  end

  defp aliases do
    [
      quality: [
        "format --check-formatted",
        "credo --strict",
        "dialyzer",
        "sobelow --config"
      ]
    ]
  end

  defp dialyzer do
    [
      plt_add_apps: [:mix, :ex_unit],
      plt_file: {:no_warn, "priv/plts/dialyzer.plt"}
    ]
  end

  defp preferred_cli_env do
    [
      quality: :test
    ]
  end

  defp package do
    [
      licenses: ["MIT"],
      links: %{"GitHub" => @source_url},
      maintainers: ["bcatherall"],

      # `files:` belongs HERE, not at the project level. Hex reads `package[:files]`;
      # a `files:` in `project/0` is silently ignored and Hex ships its own defaults
      # instead — which meant `priv/plts/dialyzer.plt` (4.4 MB of build artifact) went
      # into the tarball while the conformance suite and `usage-rules.md` did not.
      # Nothing warns about this. Inspect `mix hex.build` output before every publish.
      #
      # `test/support` deliberately does NOT ship. The conformance suite moved to `lib/`
      # because a dependency is never compiled in the `:test` environment — shipping the
      # file left it uncompiled and unusable in the consumer, which is the whole point of
      # shipping it. What remains in `test/support` is this package's own reference venue,
      # which proves the suite works here and is of no use to a consumer.
      #
      # `config/` is deliberately absent — it governs this package's own dev and test
      # only, never a consumer's.
      #
      # `priv/` is absent because the only thing in it is the dialyzer PLT.
      files: [
        "lib",
        "mix.exs",
        ".formatter.exs",
        "README.md",
        "LICENSE",
        "CHANGELOG.md",
        "AGENTS.md",
        "usage-rules.md",
        "usage-rules",
        "docs/guides"
      ]
    ]
  end

  defp docs do
    [
      # The namespace root is the landing page: it is the first thing a public reader
      # sees, and "this is a bare namespace, look elsewhere" is a poor first page (D1).
      main: "DpExchange",
      extras: [
        "README.md",
        "CHANGELOG.md",
        "usage-rules.md",
        "usage-rules/adapter.md",
        "usage-rules/auth.md",
        "usage-rules/environments.md",
        "usage-rules/money-movement.md",
        "usage-rules/symbols.md",
        "usage-rules/feeds.md",
        "usage-rules/testing.md",
        "docs/guides/building-an-exchange-package.md"
      ],
      groups_for_extras: [
        "Usage rules": ~r/usage-rules/,
        Guides: ~r/docs\/guides/
      ],
      source_ref: "v#{@version}"
    ]
  end

  defp usage_rules do
    [
      file: "AGENTS.md",
      usage_rules: [:usage_rules]
    ]
  end
end