Skip to content

Declare the dependencies Imp uses directly - #251

Merged
deepfates merged 2 commits into
mainfrom
claude/declare-deps
Sep 28, 2026
Merged

deepfates merged 2 commits into
mainfrom
claude/declare-deps

Conversation

@deepfates

@deepfates deepfates commented Sep 28, 2026 •

Copy link
Copy Markdown
Owner

Imp named modules from applications it reached only through other dependencies. This declares each one, removes a declared dependency nothing uses, and adds a check to mix quality.check so the gap fails CI next time.

What changes

  • finch ~> 0.21 (Req's own floor). Imp.Clients.ReqLLM matches Finch.TransportError and Finch.Error to tell a request that was never sent.
  • mint ~> 1.11, the only declaration here that moves an existing lock (an application that locked mint below 1.11.0 moves to it; the CHANGELOG says so), with the same line and comment as the 0.6.0 release branch (Release 0.6.0 (draft: version pending owner) #246). Imp.Clients.ReqLLM and Imp.MCP.CallFailure match Mint's error structs. It is here because the new check fails on main without it. The release branch's merge of main will conflict on the neighbouring lines of deps/0 (this PR also removes jsv next to where Release 0.6.0 (draft: version pending owner) #246 adds mint) and in CHANGELOG.md; both sides carry the same mint text.
  • decimal ~> 2.0 or ~> 3.0, optional: true. Imp.Core reads a reported cost given as a Decimal and never creates one. mix hex.build lists it as (optional), and the app file puts it in optional_applications.
  • llm_db: not declared. Imp.Optimizer.GEPA.ConfidenceAdapter now matches %{provider: :openai} on ReqLLM.model/1's result, not %LLMDB.Model{}. ReqLLM.model/1 is specced to return {:ok, LLMDB.Model.t()}, so the struct match did no work. A new test covers the non-OpenAI clause. It passes against both the old code and the new, and fails when that clause is broken.
  • plug ~> 1.16 and plug_cowboy ~> 2.7, both runtime: false: ExMCP's own requirements. The intent was only: [:dev, :test], but ExMCP requires both in every environment and Mix refuses the restriction ("Remove the :only restriction from your dep"), so declaring them adds nothing to a consumer's resolution. Imp starts neither, and no Imp code that ships uses them; the unpacked Hex package's lib/ names no Plug module (Req lists plug as an optional application, so an application may still start it). The comment beside them says they become dev/test dependencies once ExMCP drops them. package_contract_test now also asserts the two demo plug files stay out of the package.
  • The demo HTTP plug compares the bearer token with :crypto.hash_equals/2 instead of Plug.Crypto.secure_compare/2. Its guard already requires equal byte sizes, which hash_equals needs. test/acp_demo_mcp_http_plug_test.exs refuses a wrong token of the same length (and one of another length); replacing the comparison with one that accepts any non-empty token fails it.
  • thousand_island ~> 1.5, only: :test. test/support/local_http.ex calls ThousandIsland.listener_info/1 to read the Bandit server's port. The new check found this one.
  • jsv removed. Nothing in lib/, bench/ or test/ names it: grep finds nothing, and neither the compile manifest nor the new check shows a reference. Its last user was the naming laboratory, pruned in 912f14d. No decision or doc records a reason to keep it. It stays in mix.lock because ReqLLM requires it.

The check

mix imp.deps.check (a source-checkout task, not shipped) runs in quality.check, and its preferred environment is :test, where every declared dependency is built. For each compiled source file it collects:

  • from each module's debug info (:beam_lib plus the backend's debug_info/4, as Erlang abstract code): remote calls and captures, struct names in patterns and constructions, @behaviour, every Elixir.-prefixed alias anywhere in the code (attribute values read in a function, keyword and map values, lists), and the module argument of apply, is_struct and Code.ensure_loaded?/ensure_compiled;
  • from Mix's compile manifest, through Mix's own reader and record macros: compile-time, export and runtime references, which is where import NimbleParsec and other macro-only uses appear. That API is not public; a change to it fails the task at compile time or in a match, not silently.

An atom without the Elixir. prefix counts only in those module positions, so %{mode: :jose} is data. An Erlang module held only as data, and any module name built at runtime (String.to_existing_atom/1), are not seen; the moduledoc says so. Typespecs are not read.

Rules: a shipped file may name Imp, OTP/Elixir, or a dependency declared for every environment that Imp starts. runtime: false dependencies are excluded except ex_mcp and erlexec, which the task names with their reason (started on the first protocol connection; bundled in :load mode per docs/production.md). Other compiled files may also name dev/test and runtime: false dependencies. A name no built application defines fails the check, except a name in Imp's own namespace, which is a registered process name (Imp.TaskSupervisor). A module with no recorded source is found through the manifest's module list; failing that, it is held to the shipped rule.

test/imp_deps_check_test.exs compiles a probe module for each reference shape and checks it as a shipped file. The shapes: runtime: false dependency, struct construction, struct pattern, struct named by an Erlang atom, attribute list, keyword value, map value, behaviour, Erlang remote call, capture, apply, is_struct, ensure_loaded?, unresolved name, and compile-time import through compiler references (the debug info alone passes it). It also covers data-only atoms, the shipped versus source-checkout split, and a module compiled in a VM started with ERL_COMPILER_OPTIONS=deterministic (no source path), which must be held to the shipped rule.

Falsified, each by disabling one rule and watching the named tests fail:

  • runtime:false rule off: runtime_false_dependency fails.
  • alias walk off: attribute_list, keyword_value, map_value and unresolved_module fail.
  • struct rule off: the three struct shapes fail.
  • behaviour, apply, ensure_loaded, is_struct, capture or remote-call rule off: that shape fails.
  • every atom counted: the data-only test, the source-checkout test and the import test fail.
  • unresolved treated as standard: unresolved_module fails.
  • Keyword.fetch!(:source) restored: the deterministic test fails.
  • no-source treated as source-checkout: the deterministic test fails.

On the real task, a shipped lib/imp/zz_probe.ex with import NimbleParsec/defparsec and Plug.Conn.halt/1 failed on NimbleParsec, NimbleParsec.Compiler, NimbleParsec.Recorder and Plug.Conn, and passed again once the file was removed. A full build under ERL_COMPILER_OPTIONS=deterministic (beams without :source, confirmed) passes.

Test-only uses

The tests name Mint.TransportError, Finch.Error and Decimal. All three are covered by the declarations above. LLMDB.Model in gepa_confidence_frontier_test.exs asserts what ReqLLM returns. It is a test fixture, and LLMDB arrives through ReqLLM.

Not changed

test/dependency_advisory_mitigation_test.exs holds cowboy at 2.16.0 or later. plug_cowboy's requirement (cowboy ~> 2.7) does not express that floor, so the test stays as the guard.

Gates (local, at the pushed head)

  • mix format --check-formatted: clean

  • mix compile --warnings-as-errors: clean in dev and test

  • mix dialyzer: Total errors: 143, Skipped: 143, Unnecessary Skips: 0, passed

  • MIX_ENV=test mix quality.check: exit 0, including Every application Imp names is declared in mix.exs.

  • mix package.check: exit 0, 14 tests, 0 failures, clean-room package proof passed

  • Documentation contract, public_api_manifest, package_contract, the new check and demo-plug tests, and the affected ACP/MCP/GEPA tests: 142 tests, 0 failures (1 excluded)

  • Full suite, at the first commit: 59 doctests, 9 properties, 3591 tests, 18 failures, 13 skipped (147 excluded). All 18 failures came from the environment:

    • 14 needed the pinned DSPy/GEPA sources or tmp/dspy-parity-venv.
    • 2 deployment-example tests had no fetched example deps.
    • 2 Hover pilot tests refused a dirty tree.

    Once those were provisioned, the 9 files that held the failures gave 51 tests, 0 failures (11 excluded). The full suite was not rerun after the second commit.

Imp named modules from finch, mint, decimal, llm_db, plug and plug_crypto
without declaring them. Declare finch, mint and decimal (optional); match
the ReqLLM model by its fields instead of LLMDB's struct; declare plug and
plug_cowboy runtime: false (ExMCP requires both in every environment, so
Mix refuses :only); compare the demo token with :crypto.hash_equals/2;
declare thousand_island for the test helper that reads Bandit's port.
Remove jsv, which nothing uses. mix imp.deps.check, run by quality.check,
fails when Imp names a module from an undeclared application.
… to its rule

The check read only imports and atoms, so it let a shipped file name a
runtime: false dependency, missed modules named as data or through macros,
counted any atom that matched an Erlang module, passed silently in dev and
raised on a deterministic build. It now walks the debug info's abstract code
and Mix's compile references, counts a bare atom only where it is used as a
module, excludes runtime: false dependencies from shipped files except
ex_mcp and erlexec, runs in :test, fails on a name no application defines,
and holds a module with no recorded source to the shipped rule. A probe test
covers each reference shape.

plug takes ExMCP's requirement, ~> 1.16. The demo plug refuses a wrong
token of the right length, under test.
@deepfates
deepfates merged commit ae85b0d into main Sep 28, 2026
10 checks passed
@deepfates
deepfates deleted the claude/declare-deps branch September 28, 2026 18:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant