defmodule PtcRunner.MixProject do use Mix.Project @app :ptc_runner def project do [ app: @app, version: "0.14.0", elixir: "~> 1.15", start_permanent: Mix.env() == :prod, elixirc_paths: elixirc_paths(Mix.env()), deps: deps(), aliases: aliases(), releases: releases(), usage_rules: usage_rules(), name: "PtcRunner", description: "BEAM-native sandbox and minimal Kernel for bounded PTC-Lisp workflows.", source_url: "https://github.com/andreasronge/ptc_runner", docs: docs(), package: package(), test_coverage: [ summary: [threshold: 0], ignore_modules: [ ~r/^Mix\.Tasks\./, ~r/^PtcRunner\.TestSupport\./, PtcRunner.TypeExtractorFixtures, # Dev/conformance tool gated behind Babashka + `--include clojure`; # only production callers are Mix tasks, not runtime code. PtcRunner.Lisp.ClojureValidator, # Bare display structs: they only carry data for their # Inspect/String.Chars/Jason.Encoder impls (separate modules); # zero executable lines of their own. PtcRunner.Lisp.Format.Fn, PtcRunner.Lisp.Format.Builtin, PtcRunner.Lisp.Format.Var, PtcRunner.Lisp.Format.SymbolRef, PtcRunner.Lisp.Format.RegexLiteral ] ], dialyzer: [ plt_core_path: dialyzer_plt_core_path(), plt_file: {:no_warn, "priv/plts/project.plt"}, plt_add_apps: [:earmark_parser, :ex_unit, :mix, :req, :req_llm, :llm_db, :recon], ignore_warnings: ".dialyzer_ignore.exs", list_unused_filters: true ] ] end # The core PLT (Erlang/Elixir/OTP plus `plt_add_apps`) is identical across # every worktree of this repo: mise.toml pins the same Elixir/OTP build, # and worktree branches share one mix.lock unless a branch deliberately # changes deps. Keeping it worktree-local means the first Dialyzer run in # every fresh worktree rebuilds it from scratch. Outside CI, share one # copy under the user's cache directory instead. `plt_file` (this # project's own module signatures) stays worktree-local: unlike the core # PLT, it legitimately differs between branches with diverging code, and # concurrent worktree sessions writing the same project PLT would race. # # CI keeps the repo-local "priv/plts" path because `setup-elixir` # restores/saves it via `actions/cache`, keyed by OS/arch/OTP/Elixir/ # mix.lock — an external-cache scheme that expects a path inside the # checkout, not the user cache directory of an ephemeral runner. defp dialyzer_plt_core_path do if System.get_env("CI") do "priv/plts" else Path.expand("~/.cache/ptc_runner/dialyzer_plts") end end # Run "mix help compile.app" to learn about applications. def application do [ mod: {PtcRunner.Application, []}, extra_applications: [:crypto, :logger, :public_key, :ssl], env: [ llm_adapter: PtcRunner.LLM.ReqLLMAdapter ] ] end def cli do [ preferred_envs: [ precommit: :test, prepush: :test, coverage: :test, soak: :test, nightly: :test ] ] end defp elixirc_paths(:test), do: ["lib", "dev", "test/support"] defp elixirc_paths(:dev), do: ["lib", "dev"] defp elixirc_paths(_), do: ["lib"] # Generated dependency rules in AGENTS.md. Inline the short instructions # that tell agents how to consult dependency documentation, but link the # bulky language and runtime rules so the repository instructions stay # readable. defp usage_rules do [ file: "AGENTS.md", usage_rules: [ {:usage_rules, sub_rules: []}, {"usage_rules:elixir", link: :markdown, main: false}, {"usage_rules:otp", link: :markdown, main: false} ] ] end # Run "mix help deps" to learn about dependencies. defp deps do [ {:jason, "~> 1.4"}, {:jsv, "~> 0.22.0"}, {:nimble_parsec, "~> 1.4"}, {:mint, "~> 1.9"}, {:req, "~> 0.7.3"}, {:telemetry, "~> 1.0"}, {:stream_data, "~> 1.1", only: [:test, :dev]}, {:credo, "~> 1.7", only: [:dev, :test], runtime: false}, {:ex_slop, "~> 0.4", only: [:dev, :test], runtime: false}, ex_dna_dep(), {:dialyxir, "~> 1.4", only: [:dev, :test], runtime: false}, {:ex_doc, "~> 0.31", only: :dev, runtime: false}, {:earmark_parser, "~> 1.4.44", only: [:dev, :test], runtime: false}, {:req_llm, "~> 1.20", optional: true, runtime: false}, {:ptc_llm_http, "== 0.1.0", only: [:dev, :test], runtime: false}, launcher_dep(), {:usage_rules, "~> 1.2", only: :dev, runtime: false}, {:recon, "~> 2.5", only: [:dev, :test], runtime: false}, {:benchee, "~> 1.3", only: [:dev, :test], runtime: false} ] ++ viewer_dep() end # Keep published and ordinary development builds on Hex while allowing an # unreleased ExDNA checkout to be exercised by an isolated compatibility # build. The caller must keep this environment variable set for every Mix # command in that build so dependency resolution stays consistent. defp ex_dna_dep do options = [only: [:dev, :test], runtime: false] case {Mix.env(), System.get_env("PTC_EX_DNA_PATH")} do {env, path} when env in [:dev, :test] and is_binary(path) and path != "" -> {:ex_dna, Keyword.put(options, :path, Path.expand(path))} {_env, _path} -> {:ex_dna, "~> 1.5", options} end end # Development, tests, and a release built from this checkout exercise the # companion beside it. Ordinary production tasks retain package metadata's # compatible optional Hex requirement, so HTTP-only consumers are not forced # to install native support and `mix hex.build` never sees a path dependency. defp launcher_dep do launcher_path = Path.expand("ptc_runner_launcher", __DIR__) if local_launcher_checkout?(launcher_path) do {:ptc_runner_launcher, "~> 0.1.0", path: "ptc_runner_launcher", optional: true} else {:ptc_runner_launcher, "~> 0.1.0", optional: true} end end # The sentinel is the companion's own version file rather than its directory: # `ptc_runner_launcher/mix.exs` evaluates `release_config.exs` while it is # being loaded, so a directory holding only the mix file is an unloadable # path dependency. Tooling that reconstructs a project from the mix files it # fetched — Dependabot's Hex updater does exactly this — produces precisely # that tree, and pointing at it fails the whole run before any dependency is # resolved. defp local_launcher_checkout?(launcher_path) do File.regular?(Path.join(launcher_path, "release_config.exs")) and (Mix.env() in [:dev, :test] or Enum.any?(System.argv(), &(&1 == "release"))) end # The Viewer ships inside the assembled release and the container image, and # is absent from the Hex package. The activation rule is the launcher's -- # development, tests, and a release built from this checkout -- because # `mix hex.build` must never see a path dependency and the Dockerfile's # single `mix do deps.get --only prod + release ...` must. # # Unlike the launcher, the inactive branch declares nothing at all rather # than an optional Hex requirement. The launcher is published to Hex; this # companion is not, and a requirement naming a package absent from the # registry would fail the next `mix hex.publish`. Declaring nothing is also # what is true: a Hex-only install has no Viewer, which is exactly what # `PtcRunner.Kernel.DoctorEnvironment` already reports. # # `runtime: false` is load-bearing. `bandit`, `plug`, and `plug_crypto` all # declare application modules, so an ordinary runtime dependency would enter # `ptc_runner.app` and every command's `ensure_all_started(:ptc_runner)` # would start three supervision trees that only `ptc viewer` needs. # `PtcViewer.start/1` starts its own applications instead. defp viewer_dep do viewer_path = Path.expand("ptc_viewer", __DIR__) if File.regular?(Path.join(viewer_path, "mix.exs")) and (Mix.env() in [:dev, :test] or Enum.any?(System.argv(), &(&1 == "release"))) do [{:ptc_viewer, path: "ptc_viewer", runtime: false}] else [] end end defp aliases do [ ptc: &run_ptc/1, # Nested fetch then quality. The suite, Viewer, launcher package, and # release belong to the pre-push hook and GitHub Actions, so an agent # that runs this before commit does not pay for them again on push. precommit: [ "cmd scripts/ci/preflight.sh", "cmd scripts/ci/core-quality.sh" ], # Slower static and Dialyzer checks kept out of the per-commit loop. # This diagnostic alias delegates to the same repository-owned scripts # as the pre-push hook and GitHub Actions; it is not a second gate # implementation. # # `ptc.gen_semantic_revision --check` is deliberately absent from both: # it is a release gate, not a per-commit one. See `.gitattributes`. prepush: [ "cmd scripts/ci/core-static.sh", "cmd scripts/ci/core-dialyzer.sh" ], coverage: [ "test --cover" ], # Tests that spawn Mix/OS processes or wait on multi-second deadlines. # Excluded from `mix test` by default (test/test_helper.exs); the # `Nightly` workflow runs them. `--trace` is retained here and only # here: the downstream-consumer case exceeds the default 60 s per-test # timeout, and trace mode sets the timeout to `:infinity`. Everywhere # else `--trace` is a bug -- it pins `--max-cases` to 1. nightly: [ "test --only nightly --trace" ], # Memory-leak soak suite. Excluded from `mix test` by default # (test/test_helper.exs) because it is long-running and its signal is a # trend rather than a per-commit gate; the scheduled `Soak` workflow runs # it at PTC_SOAK_ITERATIONS=3000. soak: [ "test --only soak" ], # Regenerates the derived projection. Run this once before tagging a # release -- the release gate rejects a stale projection, and ordinary # branches no longer regenerate it at all. Also the correct fix when a # merge or rebase kept one side of it. See `.gitattributes` for why the # hashes cannot be merged. regen: [ "ptc.gen_semantic_revision", "cmd git add priv/semantic_build_projection.json" ] ] end defp run_ptc(args) do Mix.Task.run(ptc_prepare_task(args), ptc_prepare_args(Mix.Project.app_path())) Mix.Task.run("ptc", args) end @doc false @spec ptc_prepare_args(binary()) :: [binary()] def ptc_prepare_args(app_path) when is_binary(app_path) do # The app compiler runs after the language compilers, so this artifact is # an O(1) signal that the initial dependency-aware build completed. Calling # Mix.Dep.cached/0 here would restore much of the warm-start cost this path # deliberately avoids. app_file = Path.join([app_path, "ebin", Atom.to_string(@app) <> ".app"]) if File.regular?(app_file), do: ["--no-deps-check"], else: [] end # These raw forms are only preparation hints; the shared parser remains the # authority for acceptance and rendering. They cover the common commands # whose frontend deliberately never starts the application runtime. defp ptc_prepare_task([]), do: "compile" defp ptc_prepare_task([command | _rest]) when command in ["help", "version", "--version", "repl", "viewer"], do: "compile" defp ptc_prepare_task(_args), do: "app.config" defp releases do [ ptc_runner: [ include_erts: true, # Both are `runtime: false`, so they are named here to travel with the # release at all. `:load` keeps them out of the boot start phase; the # provider activity boundary and `ptc viewer` start them explicitly. applications: [req_llm: :load, ptc_viewer: :load], overlays: ["rel/overlays"], steps: [:assemble, ©_release_notices/1] ] ] end defp copy_release_notices(%Mix.Release{path: release_path} = release) do licenses_path = Path.join(release_path, "LICENSES") File.mkdir_p!(licenses_path) File.cp!( Path.join(__DIR__, "THIRD_PARTY_NOTICES.md"), Path.join(release_path, "THIRD_PARTY_NOTICES.md") ) for license <- ["Apache-2.0.txt", "MIT.txt"] do File.cp!(Path.join([__DIR__, "LICENSES", license]), Path.join(licenses_path, license)) end release end # Mermaid renders natively on GitHub. HexDocs needs the renderer injected; # EPUB has no JavaScript, so its diagrams stay readable as source text. defp before_closing_body_tag(:html) do """ """ end defp before_closing_body_tag(_format), do: "" defp docs do [ main: "readme", assets: %{"docs/maintainers/assets" => "assets"}, before_closing_body_tag: &before_closing_body_tag/1, groups_for_modules: [ Kernel: [ PtcRunner.Kernel, PtcRunner.Kernel.ApplicationPackage, PtcRunner.Kernel.Capability, PtcRunner.Kernel.Component, PtcRunner.Kernel.Error, PtcRunner.Kernel.EventSink, PtcRunner.Kernel.ExecutionInput, PtcRunner.Kernel.ExecutionPolicy, PtcRunner.Kernel.FrozenBundle, PtcRunner.Kernel.InspectionArtifact, PtcRunner.Kernel.InspectionSink, PtcRunner.Kernel.Library, PtcRunner.Kernel.Limits, PtcRunner.Kernel.LLMCapability, PtcRunner.Kernel.Manifest, PtcRunner.Kernel.MCPSource, PtcRunner.Kernel.MissionInventory, PtcRunner.Kernel.MissionEnvironment, PtcRunner.Kernel.ProviderError, PtcRunner.Kernel.ProviderRegistry, PtcRunner.Kernel.ProjectConfig, PtcRunner.Kernel.ReplSession, PtcRunner.Kernel.Result, PtcRunner.Kernel.RunBuilder, PtcRunner.Kernel.RunConfig, PtcRunner.Kernel.RunRequest, PtcRunner.Kernel.RunAnalysis, PtcRunner.Kernel.TraceLog, PtcRunner.Kernel.WorkflowEnvironment ], "Kernel internals": [ PtcRunner.Kernel.ApplicationSource, PtcRunner.Kernel.Attestation, PtcRunner.Kernel.BoundedWorker, PtcRunner.Kernel.BundleCompiler, PtcRunner.Kernel.DeterministicJSON, PtcRunner.Kernel.Dispatcher, PtcRunner.Kernel.Environment, PtcRunner.Kernel.Evaluation, PtcRunner.Kernel.ExecutionOutcome, PtcRunner.Kernel.Events, PtcRunner.Kernel.JSONValue, PtcRunner.Kernel.JSONSchema, PtcRunner.Kernel.MCPProtocol, PtcRunner.Kernel.Program, PtcRunner.Kernel.PublicationAuthority, PtcRunner.Kernel.RunState, PtcRunner.Kernel.Runner, PtcRunner.Kernel.RuntimeTools, PtcRunner.Kernel.SemanticRevision, PtcRunner.Kernel.StrictJSON, PtcRunner.Kernel.TypedCanonicalJSON, PtcRunner.Kernel.ViewerAdapter ], "PTC-Lisp": [ PtcRunner.Lisp, PtcRunner.Lisp.Context, PtcRunner.Lisp.Parser, PtcRunner.Lisp.Registry, PtcRunner.Lisp.Result, PtcRunner.Lisp.Signature, PtcRunner.Lisp.Signature.Validator, PtcRunner.Lisp.TypeExtractor, PtcRunner.Sandbox ], LLM: [ PtcRunner.LLM, PtcRunner.LLM.ReqLLMAdapter ] ], extras: [ "README.md", "LICENSE", "docs/ptc-lisp-specification.md", "docs/agent-library-reference.md", "docs/clojure-conformance-gaps.md", "docs/function-reference.md", "docs/java-interop.md", "docs/kernel-limits-reference.md", "docs/prelude-reference.md", "docs/signature-syntax.md", "docs/conformance/index.md", "docs/guides/quickstart.md", "docs/guides/getting-started.md", "docs/guides/concepts.md", "docs/guides/agent-cli-usage.md", "docs/guides/ptc-lisp-basics.md", "docs/guides/project-configuration.md", "docs/guides/manifests-and-capabilities.md", "docs/guides/host-configuration.md", "docs/guides/using-models.md", "docs/guides/connecting-tools-with-mcp.md", "docs/guides/building-agents.md", "docs/guides/components-and-preludes.md", "docs/guides/designing-agent-workflows.md", "docs/guides/agent-workflow-patterns.md", "docs/guides/running-and-debugging.md", "docs/guides/kernel-repl.md", "docs/guides/debugging-a-failed-run.md", "docs/guides/evaluating-with-replay.md", "docs/installation/standalone.md", "docs/installation/docker.md", "docs/installation/source.md", "docs/reference/application-manifest.md", "docs/reference/host-installation.md", "docs/reference/project-files.md", "docs/reference/component-contracts.md", "docs/reference/mcp.md", "docs/reference/cli.md", "docs/reference/repl.md", "docs/reference/debug-navigation.md", "docs/maintainers/embedding.md", "docs/maintainers/coding-agent-review.md", "docs/maintainers/duplication-gate.md", "docs/maintainers/guide-budget.md", "docs/maintainers/documentation.md", "docs/maintainers/signature-integration.md", "docs/maintainers/kernel.md", "docs/maintainers/trace-log-contract.md" ] ++ Path.wildcard("docs/conformance/*-audit.md"), groups_for_extras: [ Start: [ "docs/guides/quickstart.md", "docs/guides/getting-started.md", "docs/guides/concepts.md", "docs/guides/agent-cli-usage.md" ], Language: [ "docs/guides/ptc-lisp-basics.md" ], Configure: [ "docs/guides/project-configuration.md", "docs/guides/manifests-and-capabilities.md", "docs/guides/host-configuration.md" ], Build: [ "docs/guides/using-models.md", "docs/guides/connecting-tools-with-mcp.md", "docs/guides/building-agents.md", "docs/guides/components-and-preludes.md" ], Design: [ "docs/guides/designing-agent-workflows.md", "docs/guides/agent-workflow-patterns.md" ], "Run and debug": [ "docs/guides/running-and-debugging.md", "docs/guides/kernel-repl.md", "docs/guides/debugging-a-failed-run.md", "docs/guides/evaluating-with-replay.md" ], Installation: ~r/docs\/installation\/.+\.md/, Reference: ~r/docs\/(?:reference\/.+|(?:agent-library|ptc-lisp|clojure|function-reference|java-|kernel-limits|prelude-|signature-).+)\.md/, Conformance: ~r/docs\/conformance\/.+\.md/, Maintainers: ~r/docs\/maintainers\/(coding-agent-review|documentation|duplication-gate|embedding|guide-budget|kernel|signature-integration|trace-log-contract)\.md/ ] ] end defp package do [ files: ~w(lib rel docs examples/kernel-tutorial examples/kernel-inspection-lab examples/llm-replay examples/debug-a-failed-run examples/support-triage site/schemas/mcp-2026-07-28.schema.json .formatter.exs mix.exs README.md usage-rules.md LICENSE LICENSES THIRD_PARTY_NOTICES.md CHANGELOG.md priv/function_audit.exs priv/functions.exs priv/java_interop.exs priv/java_interop_oracle_cases.exs priv/java_interop_oracle_baseline.json priv/java_oracle_versions.exs priv/preludes priv/schemas priv/spec priv/semantic_build_inventory.exs priv/semantic_build_projection.json), # Hex expands the directories above from the working tree, not from # git, so anything ignored but present -- a `.ptc` run directory left by # a tutorial walk, a `.env` beside an example -- is published. Ship no # dot-entry the list does not name outright: run traces and private # inspection records are local evidence, and a credential file is worse. exclude_patterns: [~r{(^|/)\.(?!formatter\.exs$)}], licenses: ["MIT"], links: %{ "GitHub" => "https://github.com/andreasronge/ptc_runner", "Changelog" => "https://github.com/andreasronge/ptc_runner/blob/main/CHANGELOG.md" } ] end end