defmodule Foresight.MixProject do use Mix.Project @version "0.1.0" @source_url "https://github.com/fosferon/foresight" def project do [ app: :foresight, version: @version, elixir: "~> 1.18", start_permanent: Mix.env() == :prod, elixirc_paths: elixirc_paths(Mix.env()), aliases: aliases(), deps: deps(), name: "Foresight", description: "Semantic memory for Elixir — retain, recall, reflect. An Elixir port of " <> "Vectorize's Hindsight (MIT), measured at statistical parity.", package: package(), docs: docs() ] end # The `files:` whitelist is the important line. Without it hex ships the entire # working tree — which here includes internal audit reports and cutover dossiers # written for private consumers. Whitelist, never blacklist: a new internal doc # should be excluded by default rather than published by default. # The hex package is `foresight_memory`; the OTP app stays `:foresight`. The plain # name is taken on hex by an unrelated, live package (a URL-preview web crawler, # ~80 downloads/week), so it is not available and not worth contesting. Nothing else # changes: modules, the app name, and `alias Foresight` are all untouched. # # Consumers therefore write: # {:foresight, "~> 0.1", hex: :foresight_memory} # The `hex:` option is how Mix resolves a dependency whose package name differs from # its application name. defp package do [ name: "foresight_memory", licenses: ["MIT"], links: %{ "GitHub" => @source_url, "Changelog" => "#{@source_url}/blob/main/CHANGELOG.md", "Hindsight (upstream)" => "https://github.com/vectorize-io/hindsight" }, # priv is enumerated, NOT taken wholesale. priv/evals/data holds 279MB of # benchmark corpora — evaluation inputs, not library assets — which pushed the # tarball past hex's 134MB uncompressed ceiling and would have shipped a # third of a gigabyte of test fixtures to every consumer. # `DEVELOPMENT.md` (local qualification workflows) and the internal audit # reports stay out of the tarball deliberately: they are repository # material, not consumer material. files: ~w(lib mix.exs README.md READINESS.md LICENSE CHANGELOG.md guides .formatter.exs) ++ ~w(priv/repo/migrations priv/foresight priv/python) ] end defp docs do [ main: "overview", source_ref: "v#{@version}", source_url: @source_url, # Ordered as a manual, not as a file listing: what it is, then the decision the # reader has to make first, then the two subjects that actually surprise people, # then operations. extras: [ "guides/overview.md", "READINESS.md", "guides/getting-started.md", "guides/choosing-a-surface.md", "guides/how-recall-works.md", "guides/reflect.md", "guides/multi-tenancy.md", "guides/evaluation.md", "README.md", "CHANGELOG.md" ], groups_for_extras: [ Introduction: ~r/guides\/(overview|getting-started|choosing-a-surface)/, "How it works": ~r/guides\/(how-recall-works|reflect)/, Operating: ~r/guides\/(multi-tenancy|evaluation)/, Readiness: ~r/(READINESS|CHANGELOG)\.md$/ ], groups_for_modules: [ "Public API": [Foresight, Foresight.Supervisor, Foresight.Context, Foresight.Error], Behaviours: [ Foresight.Backend, Foresight.Embedder, Foresight.Reranker, Foresight.LLM, Foresight.AsyncQueue ] ] ] end def application do [ extra_applications: [:logger, :xmerl], mod: {Foresight.Application, []} ] end # `mix ci` ends in `test`, so the whole alias has to run in :test. Without this, # a bare `mix ci` runs in :dev and dies at the last step ("mix test is running in # the dev environment") after every other gate has already passed — the one # command a new consumer is told to run could never succeed. def cli do [preferred_envs: [ci: :test]] end defp elixirc_paths(:test), do: ["lib", "test/support"] defp elixirc_paths(_env), do: ["lib"] defp aliases do [ ci: [ "format --check-formatted", "compile --warnings-as-errors", "foresight.namespace_purity", "foresight.tenancy_boundary", "foresight.isolation_coverage", "test" ] ] end defp deps do # Version floors are the lines the suite actually runs against (see mix.lock): # pre-1.0 deps use three-segment ~> because their minors break; stable 1.x # deps admit later minors. Loosen a floor deliberately, never silently. [ {:bandit, "~> 1.11", optional: true}, {:bumblebee, "~> 0.7.0", optional: true}, {:ecto_sql, "~> 3.13"}, {:erlport, "~> 0.11.0", optional: true}, {:exla, "~> 0.12.0", optional: true}, {:jason, "~> 1.4"}, {:hermes_mcp, "~> 0.14.0", optional: true}, # >= 0.4.4 required: earlier versions drop assistant tool_calls during # message serialization, aborting the agentic reflect loop (GC-2659/GC-2660). # Genuinely optional: it is a heavy tree (alf, comm_bus, yaml_elixir, file_system, # toml, ...) and starts a Hindsight discovery monitor at boot, so a consumer that # brings its own LLM should not have to carry it. {:llm_core, "~> 0.5.0", optional: true}, # NOT optional, deliberately (GC-4156). Foresight pattern-matches %LlmToolkit.Tool.Call{} # in the reflect tool dispatch and builds %LlmToolkit.Tool{} structs in the prompt # module — both compile-time references, so declaring it optional made `mix compile` # fail outright for any consumer rather than degrading. It is nearly free here: its # only deps are req and ecto, which this library already carries. {:llm_toolkit, "~> 0.1.0"}, {:nx, "~> 0.12.0", optional: true}, {:oban, "~> 2.23"}, {:ortex, "~> 0.1.10", optional: true}, {:pgvector, "~> 0.3.0"}, # NOT optional, deliberately (GC-4156). Nineteen modules `import Plug.Conn` / # `use Plug.Router` unconditionally, so "optional" was never true — a consumer # omitting it got a CompileError, not an absent HTTP surface. The honest choices # were to guard nineteen modules with Code.ensure_loaded? or to admit the dep; # plug is small and pure Elixir, so carrying it beats nineteen conditionals. {:plug, "~> 1.20"}, {:postgrex, "~> 0.19 or ~> 1.0"}, {:tokenizers, "~> 0.5.0", optional: true}, {:telemetry, "~> 1.0"}, {:telemetry_metrics, "~> 1.0"}, {:telemetry_metrics_prometheus_core, "~> 1.2"}, {:ex_doc, "~> 0.34", only: :dev, runtime: false} ] end end