Ancora
Ancora links specs to tagged ExUnit tests and the production functions those tests call. It detects drift across git revisions without running the target project's code or tests.
Ancora is a successor inspired by spec_led_ex (Copyright (c) Mike Hostetler, MIT licensed).
Installation
Add Ancora to the development and test dependencies in mix.exs:
{:ancora, "~> 1.0", only: [:dev, :test], runtime: false}
Then scaffold the spec workspace and inspect the next action:
mix deps.get
mix spec.init
mix spec.prime --base HEAD
Ancora requires Elixir 1.18 or later. The public parse API below will remain stable within the 1.x series.
Everyday use
After setting default_base to your remote branch in .spec/config.yml, run:
mix spec.next
mix spec.check
These commands include committed feature changes relative to the configured
branch's merge base. --base HEAD compares only the working tree with the
latest commit. Use it during setup or to inspect uncommitted changes.
For a workspace with another name, pass --spec-dir contracts to check,
validate, status, next, prime, or review. The directory contains specs/,
decisions/, and config.yml. Gate and review require it inside the project
so Git can supply its previous contents. Nested Mix projects use their own
source paths and exclude changes in sibling projects.
Public API
Ancora's semver-stable public API has four functions:
Ancora.Parser.parse_file/2Ancora.DecisionParser.parse_file/2- Ancora.check/2
- Ancora.validate/2
Every other module and function is internal. Mix tasks are the supported
command-line interface. Their --root option is an internal affordance for
tooling and tests and is outside the semver commitment.
How checks work
Ancora reads source files and git objects. It loads bytecode from its own
dependencies to look up exported functions. This is toolchain introspection,
not project execution. Trusted dependencies may run their @on_load hooks
when the tool loads them.
Tagged tests define which production functions belong to a subject. When
those functions or calls change, edit the subject's requirements or scenarios
in the same diff. Mechanical rewrites such as mix format --migrate still
need an explicit acknowledgment when they change the derived call set.
Spec-Ack: trailers are temporary development acknowledgments. Ancora warns
when an applied trailer exists only below the branch tip because a squash merge
will discard it. Before merging, copy that severity into .spec/config.yml
under severities: or a subject override, add the reason for an override, and
commit the config change. Subject overrides are scoped to one subject and
one finding code, optionally narrowed to one requirement with requirement:.
The warning clears once config supplies the same severity. It remains when
config is more severe because removing the trailer would still change the gate
result.
Primary and transitive drift
A subject may declare surface: as a nonempty list of exact repo-relative
source paths. surface: [], globs, absolute paths and traversal are rejected.
Observed drift, additions and removals outside the list produce informational
derived/drift_transitive, derived/growth_transitive and
derived/shrink_transitive findings. Omitting the field keeps primary checks.
When both sides declare ownership, either side can keep a change primary;
first introduction and later ownership edits appear in review as policy changes.
Surface never claims coverage or unchanged behavior.
Deprecated 1.x grammar
Ancora.Parser.parse_file/2 keeps returning its "exceptions" key and keeps
parsing spec-exceptions blocks throughout the 1.x series. Both are deprecated
and will be removed in Ancora 2.0.
CI
CI must pass --base explicitly. The default_base setting is a
local-development convenience and must not decide a CI comparison.
Run the gate against the target branch after fetching its remote ref:
spec:
runs-on: ubuntu-latest
steps:
- {uses: actions/checkout@v4, with: {fetch-depth: 0}}
- uses: erlef/setup-beam@v1
- run: mix deps.get && mix spec.check --base origin/main
Ancora rejects a shallow base..HEAD range when a boundary inside it has a
parent commit absent locally. Use fetch-depth: 0 in CI, or run
git fetch --unshallow before the gate.
With --json, read the last stdout line that parses as JSON. The verdict line
follows the JSON report and remains the final stdout line.
Migration
See docs/migration.md for the adoption checklist and the old-to-new finding code map.
License
MIT. Copyright (c) 2026 Manuel Zubieta. See LICENSE and NOTICE.
Ancora attributes calls to each tagged test, its applicable setup and reachable test helpers. Neighboring tests and unused helpers do not contribute bindings. Inspect the reported callsite chain before editing a contract. An unresolved call is an analysis limitation, not evidence that a contract was removed. Do not add cosmetic requirements or split tests just to clear a finding.
For infrastructure without a statically observable test call, use an exact, reasoned exception. The file remains excepted rather than covered:
overrides:
- file: lib/my_app_web/router.ex
code: change/uncovered_file
severity: info
reason: Exercised through the Phoenix request pipeline.
File exceptions accept only info, require a reason, and cannot combine with
subject: or requirement:. Subject overrides prefer a matching requirement:
over the subject default; duplicate selectors are rejected regardless of order.