Packages

A Spark.Dsl.Extension that exposes Ash.Resource/Ash.Domain actions as A2A protocol agent skills, compiling a verified AgentCard and dispatching inbound A2A messages to Ash actions.

Current section

Files

Jump to
ash_a2a mix.exs
Raw

mix.exs

defmodule AshA2A.MixProject do
  use Mix.Project

  def cli do
    [
      preferred_envs: [
        "test.all": :test,
        "test.serial": :test,
        "test.serial.shard": :test,
        "test.serial.solo": :test
      ]
    ]
  end

  def project do
    [
      app: :ash_a2a,
      version: "26.9.21",
      source_url: "https://github.com/seanchatmangpt/ash_a2a",
      homepage_url: "https://hexdocs.pm/ash_a2a/",
      elixir: "~> 1.19",
      description: description(),
      package: package(),
      start_permanent: Mix.env() == :prod,
      elixirc_paths: elixirc_paths(Mix.env()),
      deps: deps(),
      docs: docs(),
      aliases: aliases()
    ]
  end

  defp aliases do
    [
      # `mix test` is now the fast-iteration default: the ~96 files tagged
      # `:serial` are `async: false` for real shared-state reasons (see
      # `test/test_helper.exs`'s broker comment, and each file's own
      # moduledoc -- mostly a shared global `:telemetry` observer used for
      # stimulus attribution) and run strictly one-at-a-time regardless of
      # `--max-cases`, dominating full-suite wall clock (measured: ~240s
      # for the fast lane vs. ~740-1300s for everything). Full coverage
      # (what CI runs) is `mix test.all`; `mix test.serial` runs just the
      # excluded tail.
      #
      # `test.all` deliberately reads `"test --include serial"`, not
      # `"test"`: Mix's alias self-reference loop guard only fires when an
      # alias invokes a task of its OWN name, not when a *different*
      # alias's step resolves to an already-aliased task name -- so a bare
      # `"test.all": "test"` would silently re-enter the `test` alias
      # above (still excluding `:serial`) instead of reaching the real
      # underlying task. `--include serial` is ExUnit's own, native way to
      # cancel the earlier `--exclude serial`: ExUnit applies include/
      # exclude filters for a tag in the order given, so the later
      # `--include` wins for that tag, correctly yielding the full suite
      # with zero custom logic.
      test: "test --exclude serial",
      "test.all": "test --include serial",
      "test.serial": "test --only serial",

      # `:serial` (all 96) splits into two DISJOINT, independently-tagged
      # subsets rather than "serial minus an exclude": ExUnit's `--only`
      # is a sole selector, not intersected with a same-invocation
      # `--exclude` for a different tag (confirmed empirically -- `--only
      # serial --exclude serial_solo` silently ran a `serial_solo`-tagged
      # test anyway, letting a `:benchmark` resource-ceiling test slip
      # through onto a loaded host). So instead of trying to subtract,
      # each file gets exactly one of two positive tags:
      #   - `:serial_solo` (~23 files): holds a real host-level shared
      #     resource across OS processes (a fixed port, a real
      #     `:peer`/multinode spawn, the shared Postgres test database,
      #     the `hddl_cli` native subprocess) -- must run together in one
      #     `mix test.serial.solo` invocation, never sharded.
      #   - `:serial_shard` (~73 files): serial only for real but
      #     VM-local shared state (a shared `:telemetry` observer, the
      #     shared `AshA2A.Authority.Broker.InMemory`) -- safe to run via
      #     `MIX_TEST_PARTITION=<n> mix test.serial.shard --partitions <N>`
      #     launched as N separate OS processes (each tracked by its own
      #     PID, never stopped by name/pattern) -- see the how-to guide.
      "test.serial.shard": "test --only serial_shard",
      "test.serial.solo": "test --only serial_solo"
    ]
  end

  defp docs do
    [
      main: "readme",
      extras:
        Enum.map(
          [
            # Project
            "README.md",
            "CHANGELOG.md",
            "docs/PHOENIX_RUNTIME_PRIOR_ART_AUDIT.md",
            # Tutorials
            "docs/tutorials/getting-started.md",
            # How-to guides
            "docs/how-to/authenticate-agent-requests.md",
            "docs/how-to/verify-authority-on-async-paths.md",
            "docs/how-to/enable-semantic-requests.md",
            "docs/how-to/observe-dispatch-with-ocel.md",
            "docs/how-to/use-role-based-llm-resolution.md",
            "docs/how-to/test-your-ash_a2a-app.md",
            # Reference
            "docs/reference/index.md",
            "docs/reference/dsl.md",
            "docs/reference/configuration.md",
            "docs/reference/telemetry.md",
            "docs/reference/mix-tasks.md",
            "docs/reference/a2a-endpoint-contract.md",
            "docs/reference/a2a-spec-version-mapping.md",
            # Explanation
            "docs/explanation/architecture.md",
            "docs/explanation/message-lifecycle.md",
            "docs/explanation/canonical-graph-identity.md",
            "docs/explanation/graphlaw-wasm-integration.md"
          ],
          &{&1, []}
        ),
      groups_for_extras: [
        Project: ~r"README|CHANGELOG|PHOENIX",
        Tutorials: ~r"docs/tutorials",
        "How-to guides": ~r"docs/how-to",
        Reference: ~r"docs/reference",
        Explanation: ~r"docs/explanation"
      ]
    ]
  end

  defp description do
    "A Spark.Dsl.Extension that exposes Ash.Resource/Ash.Domain actions as " <>
      "A2A protocol agent skills, compiling a verified AgentCard and " <>
      "dispatching inbound A2A messages to Ash actions."
  end

  defp package do
    [
      licenses: ["MIT"],
      links: %{"GitHub" => "https://github.com/seanchatmangpt/ash_a2a"},
      # The four Diataxis quadrants ship in the package so `mix hex.publish`
      # can build the ExDoc extras declared in docs/0 above. Internal trees
      # (docs/archive, docs/rfc, research) deliberately do NOT ship.
      files:
        ~w(lib priv mix.exs README.md CHANGELOG.md LICENSE docs/tutorials docs/how-to docs/reference docs/explanation)
    ]
  end

  defp elixirc_paths(:test), do: ["lib", "test/support"]
  defp elixirc_paths(_), do: ["lib"]

  # Run "mix help compile.app" to learn about applications.
  def application do
    [
      extra_applications: [:logger],
      mod: {AshA2A.Application, []}
    ]
  end

  # Run "mix help deps" to learn about dependencies.
  defp deps do
    [
      {:ash, "~> 3.0"},
      {:igniter, "~> 0.6"},
      # `only: :dev`: real, disclosed finding (swarm-test Docker build,
      # this session) -- zero references to `GgenIgniter`/`ggen_igniter`
      # anywhere in this repo's own lib/ or test/ (confirmed via a real
      # grep across both), yet it was declared with no `:only`
      # restriction, so every consumer's `MIX_ENV=prod` release build
      # (not just interactive dev use) unconditionally paid its real,
      # heavy transitive cost: a Rustler NIF (`ggen_graph_nif`) linking a
      # vendored RocksDB via `oxrocksdb-sys` and using `bindgen`, which
      # needs a real Rust toolchain, `libclang`, and a C++ toolchain at
      # build time -- confirmed by two real, disclosed Docker build
      # failures before this fix (missing Rust toolchain, then missing
      # libclang) in swarm/Dockerfile's own commit history. Scoping to
      # `:dev` keeps `mix ggen_igniter.doctor`/interactive use fully
      # available; a real `MIX_ENV=prod mix deps.get` for this repo (and
      # this feature's own swarm-test release) simply no longer fetches
      # or compiles it.
      {:ggen_igniter, "~> 26.9", only: :dev},
      {:a2a, "~> 0.2"},
      {:ash_ai, "~> 1.0"},
      # AshA2A.Telemetry.OcelForwarder's real HTTP POST to beam4pm's real
      # OCEL ingest endpoint -- already transitively present (via :a2a's
      # optional dep / :igniter), promoted to direct since this module
      # calls it explicitly.
      {:req, "~> 0.5"},
      # Real local Bandit server for
      # test/ash_a2a_telemetry_ocel_forwarder_test.exs's fixture standing
      # in for BeamPM.OcelIngest.Router (same MicroBeam4pm-style pattern
      # already used in ex4pm/ash_ex4pm/xaas this session).
      {:bandit, "~> 1.5", only: :test},
      {:req_llm, "~> 1.18"},
      {:ash_r2rml, "~> 26.8"},
      # `:plug` is an optional dep of `:a2a` (A2A.Plug/A2A.Plug.Auth). Also
      # now pulled in transitively as a normal dep via `:ash_ai`'s
      # `:websock_adapter` dependency, so it can no longer be restricted to
      # `only: :test` (Mix rejects a narrower :only than a transitive dep
      # requires). test/ash_a2a_plug_agent_card_test.exs drives a REAL
      # A2A.Plug HTTP pipeline via Plug.Test.
      {:plug, "~> 1.16"},
      # Real provider implementations for the three runtime-provider
      # boundaries: AshA2A.Execution.FLAME (Placement),
      # AshA2A.Durability.DurableServer (Durability), and
      # AshA2A.Topology.Presence (Topology). Declaring them as resolvable
      # deps makes each adapter's available?/0 (or available?/1) true and
      # its call path reachable; the adapters themselves remain
      # authority-free regardless -- they never gain independent DO
      # capability, only observed provider evidence via RuntimeReceipt.
      {:flame, "~> 0.5"},
      {:durable_server, "~> 0.1.5"},
      # v26.9.14: real provider implementations closing GAP D
      # (AshA2A.Delivery.Oban, AshA2A.TaskLifecycle's AshStateMachine
      # adapter). Same pattern as flame/durable_server above -- declaring
      # them as resolvable deps makes each adapter's available?/0 true and
      # its real call path reachable; the adapters themselves stay
      # authority-free (queue acceptance is not an execution receipt; a
      # state transition is not a DO -- both still funnel any real
      # consequence through AshA2A.CommandBus).
      {:oban, "~> 2.24"},
      {:ash_oban, "~> 0.8"},
      {:ash_state_machine, "~> 0.2"},
      # Oban's real PostgreSQL storage engine driver, exercised for real
      # against a real disposable local Postgres instance in this repo's
      # own test suite. Cannot be `only: :test` -- `:ash_oban` itself
      # requires `:postgrex` unconditionally (same real Mix dependency-only
      # narrowing constraint the `:plug` comment above already documents).
      {:postgrex, "~> 0.18"},
      # DurableServer.Backends.EKVStore's real local storage engine, used by
      # AshA2A.RuntimeProvidersIntegrationTest so Durability can be
      # exercised with a real local durable-KV backend instead of the
      # default ObjectStore/S3 backend (which needs cloud credentials).
      # Promoted out of `only: :test`: AshA2A.ReceiptStore.Ekv (a real
      # durable receipt store, not a test double) and
      # AshA2A.Application.receipt_store_children/0's automatic EKV wiring
      # both call the :ekv package directly outside of Mix.env() == :test,
      # the same reason :plug was promoted out of only: :test earlier in
      # this repo's history.
      {:ekv, "~> 0.4"},
      # Real property/fuzz testing (test/ash_a2a_property_fuzz_test.exs).
      # Already a required, unrestricted (non-`only:`) transitive dep of
      # `:ash` itself (mix.lock: "stream_data": {:hex, :stream_data,
      # "1.4.0", ...}; deps/ash/mix.exs declares it with no `:only`) --
      # promoted to an explicit direct dep here so this repo's own property
      # tests declare their real dependency instead of relying on an
      # incidental transitive pin ash could drop or relax in a future
      # version. Cannot be narrowed to `only: :test` -- Mix rejects a
      # narrower `:only` than a transitive dependency requires, the same
      # real constraint already documented above for `:plug`/`:postgrex`.
      {:stream_data, "~> 1.0"},
      {:phoenix_pubsub, "~> 2.1"},
      {:phoenix, "~> 1.7"},
      # v26.9.16: real libcluster + Horde proof-of-concept (real node
      # discovery + a real CRDT-backed distributed process registry),
      # named as prior art for the compute/coordination lenses in this
      # session's requirements synthesis. `libcluster` is already a real
      # dep of the separate `swarm/` mix project (`swarm/mix.exs`,
      # `Cluster.Strategy.Kubernetes.DNS`) -- this declares it (and the
      # new `Horde.Registry`/`Horde.DynamicSupervisor` pair) directly on
      # the root `:ash_a2a` app instead, scoped to `:test` because the
      # whole PoC lives in
      # `test/ash_a2a/libcluster_horde_poc_test.exs` (real `:peer`-spawned
      # second BEAM node, same pattern as
      # `test/ash_a2a/distributed_node_loss_test.exs`) -- nothing in
      # `lib/` references either module yet, so `only: :test` keeps a real
      # `MIX_ENV=prod mix deps.get` from paying their cost, the same
      # narrowing reasoning already documented above for `:bandit`.
      {:libcluster, "~> 3.5", only: :test},
      {:horde, "~> 0.10.0", only: :test},
      # v26.9.16 (RFC-SA2A-001 S12/S79): CANONICALIZATION AND
      # SERIALIZATION/PARSING ONLY. ONE declaration, two real `lib/` uses:
      #
      #   * `AshA2A.Semantic.CanonicalGraph` -- the single authoritative RFC
      #     S12 canonical graph identity: RDFC-1.0 (`RDF.Canonicalization`)
      #     -> code-point-sorted N-Quads -> SHA-256, pinned as
      #     `"RDFC-1.0/SHA-256/n-quads-sorted"`. The praxis-graphlaw wasm
      #     `graph_hash` export is NOT RDFC-1.0 (not blank-node-relabel
      #     invariant; see docs/explanation/canonical-graph-identity.md), so
      #     S12 identity is taken in-BEAM here.
      #   * `AshA2A.Semantic.Serialize.verify/3` -- an *independent* real
      #     parser gating the engine canonical-digest path.
      #
      # Never for validation, reasoning or entailment, all of which stay in
      # the praxis-graphlaw law package (S14 ShEx, S15 SHACL, S16 Datalog,
      # S17 N3, S18 SPARQL); `:sparql`, `:json_ld` and `:shex` are NOT added
      # and must not be.
      #
      # The `Serialize.verify/3` gate is not optional decoration. A real
      # probe of the real prebuilt praxis-graphlaw wasm established that its
      # `graph_hash/1` has no parse-error channel and reports no parsed-triple
      # count:
      # `graph_hash(good <> "GARBAGE !!!")` returned bit-for-bit the same
      # digest as `graph_hash(good)`, and `graph_hash("GARBAGE !!!")`
      # returned `af1349b9f5f9a1a6...` == `BLAKE3("")` == `graph_hash("")`.
      # A malformed serialization therefore yields a confident, valid-looking
      # digest of a *smaller or empty* graph. Parsing our own output back
      # with a second real implementation is the only way to detect it.
      #
      # Zero marginal dependency weight: `rdf` 3.0.1 is ALREADY a required,
      # unrestricted transitive dep of `:ash_r2rml` (a direct dep of this
      # project) and is already pinned in `mix.lock` and present in `deps/` --
      # adding `{:rdf, "~> 3.0"}` here left `mix.lock` byte-for-byte
      # unchanged (verified with a real `mix deps.get` + `git diff mix.lock`).
      # Promoted to an explicit direct dep for exactly the reason `:stream_data`
      # above already documents: declare the real dependency instead of
      # relying on an incidental transitive pin. Cannot be narrowed to
      # `only: :test` -- `lib/ash_a2a/semantic/serialize.ex` and
      # `lib/ash_a2a/semantic/canonical_graph.ex` call it outside the test
      # env, the same real constraint documented for `:plug`/`:ekv`.
      {:rdf, "~> 3.0"},
      # v26.9.16 (RFC-SA2A-001 S12/S79): the real in-BEAM WebAssembly host
      # runtime. ONE declaration shared by two independent hosts of the same
      # vendored `priv/graphlaw/praxis_graphlaw.wasm` law package:
      #
      #   * `AshA2A.GraphLaw.WasmexSession` -- runtime A of
      #     `AshA2A.SA2A.Conformance` (the SA2A conformance court). It checks
      #     `Code.ensure_loaded?(Wasmex)` and returns a typed
      #     `:wasmex_unavailable` refusal rather than raising.
      #   * `AshA2A.GraphLaw.WasmexHost` -- the application-supervised,
      #     long-lived Wasmtime instance (started in `AshA2A.Application`).
      #
      # `:wasmex` wraps a real Wasmtime engine via a Rustler NIF; on
      # aarch64-apple-darwin (and the other tier-1 targets)
      # `rustler_precompiled` downloads a prebuilt NIF, so this does NOT
      # require a Rust toolchain at build time -- unlike `:ggen_igniter`
      # above, which is why that one is `only: :dev` and this one is not
      # similarly restricted. Unrestricted (not `only:`) also because
      # `lib/ash_a2a/graph_law/wasmex_host.ex` is real runtime library code,
      # not a test fixture. This repo does NOT own an RDF canonicalization /
      # SHACL / ShEx / Datalog / N3 implementation and must not grow one --
      # `praxis-graphlaw` already is one; Elixir's job at this boundary is
      # envelope, standing, refusal typing, authority, receipts and admission
      # orchestration, never the derivation itself.
      {:wasmex, "~> 0.15.1"},
      {:dialyxir, "~> 1.4", only: [:dev], runtime: false},
      {:ex_doc, "~> 0.34", only: :dev, runtime: false}
    ]
  end
end