**A minor line, opened by `mix extla.replay`.** It gains `--server` and `--timeout`, and
`--init-arg`, which was accepted and then dropped, now reaches the server (#165). That adds to
the command line, which is why this is 0.5.0 rather than 0.4.61. Nothing is removed or
renamed: a 0.4.0 project upgrades by changing the tag.
Two fixes reach spec authors. A function capture (`&abs/1`) in a spec body compiles (#369),
and `ExTLA.Invariant.Test.valid_states/3` says when a base generator is too broad to filter
(#348). The rest is how the project holds itself to its specification:
- spec/04's relation log ties each section to the tests that verify it and the code they
exercise, and gates CI (#336, now on surfex 0.7.1);
- `EXERCISE.md` records what the suite exercises in each shipped module (#370);
- the mutation sweep's kills are attributed to the mutant and reproduce on every seed (#378,
#380, #392);
- every CI job starts from a pinned base image and restores its dependencies from the store.
- **`mix extla.replay` passes `--init-arg`, `--server` and `--timeout` through** (#165).
The task declared `--init-arg`, so it was accepted, and then dropped: `ExTLA.Replay` was
called with no options, and its documented `:server` and `:timeout` were unreachable from
the command line. Now, with `--trace`:
- `--init-arg` is the argument the server starts with, as a literal Elixir term only
(`[start: 0]`; an expression is refused, never run);
- `--server` drives a running registered process instead of starting one, and leaves it
running;
- `--timeout` bounds each reply.
Whatever would otherwise be silently ignored is refused by name: `--init-arg` with
`--server`, an unregistered name or a printed pid, a non-positive timeout, and any of the
three with `--captured`, which drives nothing. `spec/05`'s holding note ("accepted and not
currently read") is replaced. `test/mix/switches_read_test.exs` now holds every task to
reading every switch it declares; against the old task it names `--init-arg`, and it finds
no other.
- **surfex 0.7.1** (#391, for #390). No upgrade step and no relation changed. Two lines in
CONTRIBUTING are corrected:
- **Running a section's tests:** `mix test --only verifies:<id>` doesn't select a test whose
tag is a list, because ExUnit compares a list as one string. `mix surfex.status` now counts
66 such tests, and for `mix extla.check`'s section `--only` runs 29 of its 39 tests.
CONTRIBUTING points to `mix surfex.test "<section id>"` instead.
- **Reviewing reached code:** a review now credits only the code the test calls, so the
review step for code reached through an entry point names that code with `--code`.
- **surfex 0.7.0** (#384, for #383). `verifies` tags now combine the way ExUnit merges them:
a test's own `@tag` replaces its describe's and module's, where surfex used to take their
union.
- **No relation changed.** Every test-level tag in spec/04's tests already repeated the
section it inherits, so `mix surfex.status` lists no Undeclared relations. With one
repeated id removed, status names that relation and fails.
- **Triangle gaps fall from 200 to 110.** The triangle now reads what a test calls from
source.
- `RELATIONS.md` and `PACKAGE_SURFACE.md` are regenerated.
- **Two of #354's stand-ins now use surfex reach** (#385, for #383). `ExTLA.ModelChecker.Safety`
implements its own section, and `ExTLA.ModelChecker.Parallel` implements Under `--workers`.
- **Evidence:** both are confirmed by evidence through `ModelChecker.run/2`, which their
tests drive, and each was reviewed with `mix surfex.validate`. The `run/2` `depends_on`
stand-ins, and `run/2`'s `implements` of the Safety section, are retired.
- **The other six keep their stand-ins.** They are reached only through a Mix task's
`run/1`, an undocumented `@impl` callback that reach doesn't follow (dco-tek/surfex#193).
CONTRIBUTING's row says so.
- **spec/04's relation log is baselined and gated** (#354, for #336). After green runs of all
three CI test slices, the baseline trusted 1,458 test versions and adopted 563 `verifies`
tags. 5 spec units have none. Every suggested `implements` citation was judged one at a
time and recorded with a note:
- 15 are confirmed by evidence, and one, the type `t:ExTLA.State.t/0`, by judgement;
- surfex counts only a direct call as a test exercising code. So 8 items reached only
through an entry point (a Mix task, or `ModelChecker.run/2`) are recorded as the entry
point implementing the section, plus a judged `depends_on`.
The new `relation-status-drift` job replaces the advisory `relation-evidence`. It is HARD,
and runs `mix surfex.status --verify`, the `RELATIONS.md` golden, and the evidence check.
It reads only the `test` job's evidence: no spec/04 tag is on a test only `docs` or
`mutation` runs, and GitLab intermittently loses those archives (#355). A test checks that
every golden's header, surfex's included, names a job that runs it.
- **surfex is pinned at `~> 0.6.2`** (#377, for #376). It makes `Surfex.Golden` and
`Surfex.SourceScan` supported, documented library API with a promise that their shapes and
`definition_hash/1` stay stable. All seven of extla's goldens are built on them, and none
changed but `PACKAGE_SURFACE.md`'s requirement row. For the spec/04 baseline (#354), it adds
recording that reads CI's evidence files (`--merge`), batched `confirm`/`validate` with a
distinct note per relation, and `suggest` in a deterministic order. No upgrade step.
- **CI restores dependencies with depdep v0.11.0** (#375, for #372). A correct restore is no
longer refused when a project's config sets a value a dependency read at compile time, and
`--explain-rebuilt` names the differing entry. Keys and schema are unchanged, so the store
needs no refill. The git-source restore it adds does nothing here: no dependency is git.
- **`EXERCISE.md` records what `mix test` exercises in each shipped module** (#370, for #323).
Nothing in the project could say whether a line ever ran; #323 found four that never had,
each by reading code. The `test` job now runs `mix test --cover --export-coverage test`,
and `mix extla.exercise --check` holds the new golden to that run in the same job, so
nothing is carried between jobs.
- **A row** is one module from `API_SURFACE.md`'s list: executable lines, unhit lines, and
the functions never entered.
- **The first run:** 310 of 3,892 lines unhit; the never-entered functions include
`ModelChecker.check_safety/1,2` and `Simulation.run/1` (listed on #323).
- **Stable:** three full runs gave identical coverage.
- **Accurate:** "never called" comes from per-function *line* coverage, because `:cover`'s
call counts miss a function that opens with `receive`. Macros, which run at their
caller's compile time, are excluded.
- **Gate ownership:** the rule that a golden's gate runs its `--check` now reads each
golden's own header, so `EXERCISE.md`'s `test` gate is enforced like `surface-drift`'s.
- **The n-ary product is written once** (#368, for #350). `ExTLA.Set.cross_many/1`,
`ExTLA.Function.record_set/1`, `function_set/2` and `ExTLA.Symmetry.mappings/2` each wrote
out the same fold, differing only in a seed and a combiner. They now call
`ExTLA.Product.fold/3`. `function_set(d, r)` is `record_set` with one range per key.
`ModelChecker.bindings/3` keeps its own: its later choices depend on earlier bindings.
- **Cost:** cheaper in every case, measured in reductions because the host's load makes
wall time noise. `function_set` is 13–26% fewer, `record_set` 21%, `cross_many` 23% (it
now folds the sets in reverse, so no tuple needs reversing), and `mappings` 8%. The
differential corpus, timed interleaved with `main`, shows no regression through that
noise.
- **Tests:** `test/ex_tla/product_test.exs` holds all four against a plain recursive
oracle, including the empty cases and `mappings`' order.
- **Guard:** `test/docs/product_shape_test.exs` refuses the fold's shape anywhere in `lib/`
but the two allowed sites. It was shown red on the old `ExTLA.Function`.
- **No shared test helper writes VM-global state, and the async-isolation gate checks them**
(#367, for #326). `test/docs/async_isolation_test.exs` read only each test file's own AST,
so a write one frame down in `test/support/` was invisible to it. `ExTLA.GuideCheck` set
`:ignore_module_conflict` for the whole VM, and the gate's claim held only because its two
callers were `async: false`.
- **The helper:** `GuideCheck` collects redefinition warnings with `Code.with_diagnostics/1`
instead, as #325 did for moduledoc examples.
- **The rule:** a module under `test/support/` cannot know whether its caller runs async, so
the gate now refuses any VM-global write there at all. That needs no call graph. It was
shown red on the old `GuideCheck`.
- **Arity:** entries carry it, because `Mix.shell/0` reads the shell and only
`Mix.shell/1` sets it. Matching by name alone would have flagged `extla.mutants`'s reads.
- **`ci-base-image` no longer runs on every new branch** (#374, for #373). Its `changes:`
rule had nothing to compare a branch's first push with, so GitLab counted every file as
changed and the job took a privileged runner on each work-item branch, only to find the
image already built. It now compares against `main` (`compare_to: refs/heads/main`), and
`ci_pipeline_test.exs` requires `compare_to:` on every `changes:` rule.
- **Tests that wait for an event no longer have a short deadline, and a short one must say
why** (#366, for #362). `cast_delivery_test`'s "a server that dies on the message is
reported" waited 1 s and missed it once on an oversubscribed host. A crashing server writes
its crash report before its `:DOWN`; measured, that takes a 0.3 ms median idle against 51 ms
(up to 188 ms) under CPU load and a log flood, and more on a host short of CPU.
- **Outcome waits are long.** The death case waits up to 30 s, and three more
death-waiting `assert_receive …, 1_000` drop their bound.
- **The default is long.** `test_helper.exs` sets `assert_receive_timeout: 5_000`, since
ExUnit's 100 ms default applied to every unbounded `assert_receive`. A passing wait
returns at its message, so a long bound costs nothing.
- **Short deadlines are justified.** `test/docs/deadlines_test.exs` requires a
`# deadline: <reason>` above any literal deadline under 5 s in `assert_receive`,
`refute_receive` or `Driver.deliver`. The three left are a timeout that is itself the
asserted outcome (×2) and a refute. It is shown red with a reason removed.
- **Every Elixir CI job runs in the pinned base image, and no job runs `apt-get`** (#365,
for #309). `.elixir` uses `CI_BASE_IMAGE`, pinned as `ci-base:<input hash>@sha256:<digest>`.
The per-job `apt-get update`/`install git` and `mix local.hex`/`local.rebar` are gone; the
memory and load diagnostics stay. `test/docs/ci_pipeline_test.exs` gates three things:
- no job runs `apt-get`;
- the pin is a digest, under a tag equal to the hash of `ELIXIR_IMAGE` and
`ci/base.Dockerfile`, so bumping either without rebuilding fails a test;
- every job rule names `$CI_PIPELINE_SOURCE`, since a bare `changes:` rule opened a
one-job MR pipeline that stood as !236's head pipeline.
Each is shown red against a bad fixture and against `main`'s pipeline.
- **A CI base image with `git`, Hex and Rebar baked in** (#364, for #309).
`ci/base.Dockerfile` builds it from `ELIXIR_IMAGE`, and the new `ci-base-image` job pushes
it to the project registry, only when the Dockerfile changes or when run by hand. Its tag is
a hash of its inputs, so an unchanged image builds nothing, and it reports the pushed digest.
No job uses it yet: #365 pins `.elixir` to that digest and removes the per-job `apt-get`,
which failed whole pipelines on a runner that cannot reach the Debian archive.
- **CI restores dependencies with depdep v0.10.0** (#363, for #349). It corrects a git
dependency's `origin` on restore, the second cause of `restored, but Mix would rebuild it`:
Mix compares the lock's URL with the restored checkout's `origin` as strings. Keys and the
schema are unchanged, so the store needs no refill. extla's own figure was already 0 under
v0.9.0; the fix is for peers restoring extla, whose objects are stored under both
`git@…` and `https://…` spellings.
- **surfex is pinned at `~> 0.6.1`, and the type-citation stopgap is gone** (#361, for
#357). 0.6.0 makes each public `@type` an item (surfex#137, filed from #351), so
`ExTLA.State.t()` in spec/04 resolves as `t:ExTLA.State.t/0`, and the `normalise` rewrite
`.surfex.exs` carried for it is removed. 0.6.1 counts an ExUnitProperties `property` as a
test (surfex#141, filed from #353), so the 11 tagged properties now declare `verifies`,
including generated_models_test's D-27 worker agreement. Nothing is recorded yet: relating
the type citation and adopting the properties' claims are part of the baseline (#354).
`PACKAGE_SURFACE.md` records the new requirement. extla has no `require:`, so 0.6's
upgrade step doesn't apply.
- **The six spec/04 claims no test verified now each have one** (#358, for #356). Each
claim held; none needed a code change:
- the keyword form `invariant :x, desc: "…", do: expr` (and likewise for
`action_property`, `terminal`, `temporal`) compiles to the same declarations and
model-checks to the same verdict as the block form;
- `ExTLA.Spec.DSL` defines no `spec` macro;
- `mix extla.check` declines liveness under `--workers 2` and under a `symmetry:`
declaration, and says why, where the same refutable property otherwise exits 1;
- `assert_spec` passes a refutable temporal property it was not asked to check, warns,
and flunks when `check_liveness: true`;
- a predicate cheap on most states and costly on a few is marked
`<-- varies by state`, while a uniform one is not. The test is deterministic by
construction and passed 50 of 50 repeats;
- `mix extla.explore`'s shortdoc names a saved counterexample rather than describing a
search.
Each test carries `@tag verifies:` for its section, so every section of spec/04 with
a claim now has a verifying test. Found while writing them: a bare capture (`&abs/1`,
`&+/2`) in a spec body is refused as an undeclared variable (#359).
- **spec/04 §§ Coverage → end say which tests verify them too** (#353, for #336). The other
17 sections were read the same way: Coverage, Profile, Trace, Parallel, `mix
extla.check` and its JSON schema, `mix extla.explore`, ExUnit Integration, and
Differential testing. With #352's half, spec/04 now carries 539 test → section claims
across 33 files, and every section with a claim has at least one, except the six claims
in #356 (two of them found here). The bare "Mix Integration" heading has no text of its
own to verify. Of the 18 suggested `implements` relations in range, five were retired
with a reason: each names a module only as context (an analogy, a contrast, deferred
work, an aside). surfex sees `test` but not ExUnitProperties' `property`, so the 26
properties' tags declare nothing to it yet (surfex#141); they are kept, because they
are correct.
- **spec/04 §§ Overview → Liveness say which tests verify them** (#352, for #336). Each of
the 18 sections up to Coverage and Vacuity was read against its tests. The ones that
show a section's claims carry `@tag verifies: "<section id>"`: 253 test → section
claims across 17 files, as a test's tag, a `describe`'s or a module's. Every section with
a claim has at least one, except four claims no test shows, now #356. Three sections need
none: the Architecture diagram and the State-space reduction introduction describe
rather than claim, and View abstraction is deferred past v1. Of the 12 suggested
`implements` relations in range, the Overview's four were retired in `.surfex/` with a
reason: it only lists Layer 2's components, each of which has its own section. Tags
change no test's behaviour, and no relation is recorded yet: the baseline is #354.
- **surfex's relation log is wired up for spec/04, not yet gating** (#351, for #336).
`.surfex.exs` points surfex at `spec/04-layer2-model-checker.md` with `adoption: :trust`,
and `.surfex/` holds the (still empty) log. `Surfex.ExUnitFormatter` records every test
result at its version; the full suite took 192 s with it, against 200–224 s before. The
`test`, `mutation` and `docs` jobs keep their evidence as artifacts under their own names,
and a new `relation-evidence` job checks the log against all three together. It is
`allow_failure` until spec/04 is tagged and baselined (#352–#354). `namespace: "ExTLA"` is
required, because surfex's default camelizes the app name to `Extla`. A type citation
(`ExTLA.State.t()`) is read as its module until surfex resolves types itself
(surfex#137). On current main, `mix surfex.suggest` proposes 30 `implements` and 2,466
`tests` relations. `CONTRIBUTING.md` is new and states the per-MR loop.
- **CI restores dependencies with depdep v0.9.0, and every one now restores** (#346, for
#342). Under v0.7.0 one dependency, `telemetry`, was restored and then rebuilt on every run
as "outdated", and never replaced because the store already held it. The object was fine:
its entries carried the build path of the project that pushed it, so it unpacked where this
project's Mix never looks (depdep#131, the mechanism behind depdep#110). v0.9.0 moves each
restored tree to this project's own build path. The pin's own pipeline read `pulled 7,
missing 0` with nothing rebuilt or compiled in every job. Keys are unchanged, so there was no
store refill. v0.8.0's `--explain-rebuilt` comes along for the next refusal worth reading.
- **`wrong_init_index` was killed only on a lucky seed** (#393, for #392). The one test that
caught it was a generated property, which fails only when the generator builds a spec
with two `init` blocks that is refuted from the second. Measured: it survived 10 seeds of
100, and failed the mutation job of pipeline 5315, whose change touched no code. No fixed
test read `init_index`, the field a replay starts from: `TestSpecs.TwoStarts` has two
starts and no property, so nothing asked it for a counterexample.
`test/ex_tla/second_start_replay_test.exs` now does, deterministically, with a fixture
refuted only from its second start:
- an invariant's and an action property's counterexamples start at the second start and
pass `Trace.validate/3`;
- every state of `TwoStarts` regenerates (`StateGraph.state_at/2`) to the state its id
names.
All three tests fail under the mutant, which is now gated on that file. The `:tests` gate
also runs its files with `--exclude property`. A kill that rests on generated input is
now a survivor the first time it is swept, not a red pipeline later.
- **The mutation sweep's `:suite` gate measured nothing** (#389, for #380). It ran the whole of
`mix test` in the sweep's workspace, which copies `lib`, `test` and the build but not
`README.md`, `guides/` or `spec/`. The suite failed there with no mutant applied, so its
three kills (`skips_symmetry_canonicalization`, `wrong_init_index`,
`param_universe_from_the_right`) were never measured. After #379, `--check` kept their
recorded outcomes and stayed green.
- **`:tests` replaces `:suite`:** each such mutant names the test files that catch it. These
were found by running the whole suite with each mutant applied: `symmetry_test`,
`generated_models_test` (one property, killed on ten seeds of ten), and `coverage_merge_test`
with the parallel checker's tests. The gate runs those files only.
- **`mix extla.mutants --check` now fails on `inconclusive (fails_unmutated)`.** A gate that
fails without the mutant is broken, not flaky. A failure that didn't reproduce, and a rerun
that timed out, are still tolerated.
- **A catalogue test checks the lists:** every `:tests` mutant must name existing test files,
and an empty list matches no gate rather than running the whole suite.
- **A parallel-profile test named the denominator rule #239 replaced** (#388, for #386).
`profile_parallel_test.exs` said `run_reductions` "is the coordinator's" and asserted `> 0`,
which passes under either rule, so nothing failed when #239 changed the rule.
- **What it checks now:** that the denominator sums every worker's batch, as spec/04 Under
`--workers` says. An 8-worker run's `run_reductions` is at least a 1-worker run's; measured,
it is about 2.4 times.
- **Proven to fail under the old rule:** with each worker's bracket zeroed, the coordinator
alone reads about 0.6 times, and the test fails.
- **A mutation kill has to be the mutant's doing** (#379, for #378). Any failing gate used to
count as a kill, so main pipeline 4744 recorded `weakens_a_type_domain`, the catalogue's
one expected survivor, as killed. A failing gate is now re-run twice over only the tests
that failed, once with the mutant and once without it. It counts as a kill only if the
failure comes back with the mutant and goes away without it. Otherwise the result is
`inconclusive`, with the reason. `--check` keeps the recorded outcome for an inconclusive
mutant rather than reporting PROGRESS or REGRESSION, and recording refuses one. The job log
now names the failing tests of every kill and prints the output of every other non-survival.
The likely cause of 4744 was in the workspace: it lacked `.surfex.exs`, so the evidence
formatter (#336) crashed in every gate, and depending on timing that made a passing run
exit 1. The workspace now copies it, and a test asserts that a passing gate there survives.
- **A function capture in a spec body compiles** (#369, for #359). `&abs/1`, `&+/2` or
`&member?/2` in any block was refused as "undeclared variable: abs", with advice to declare
`variable :abs`. The capture's name node has a variable's shape, and the rewriter resolved
it as one. A capture now names a function, even where a declared variable shares the name
(`variable :size` beside `&size/1`), and a capture of a function that doesn't exist is
still refused, by Elixir's compiler, naming it. `spec/02` §1 states the rule.
`test/ex_tla/spec/captures_test.exs` uses bare, operator, helper, qualified and `&(&1 …)`
captures in every block kind, including `let` and a `params:` generator, and model-checks
the result.
- **`ExTLA.Set.cross_many/1` no longer names a caller that does not exist, and docstrings
that name a caller are checked** (#347, for #324). Its docstring and `spec/01` justified
the empty product being `{{}}` as "the answer the DSL needs" for an action with no
parameters. Nothing in `lib/` calls it. The DSL binds parameters with
`ExTLA.ModelChecker.bindings/3`, a *dependent* product (each parameter's values may depend
on the state and on earlier parameters), which `cross_many/1`'s fixed list of sets cannot
express. The two are different operations, so the rationale now stands on its own: the
empty tuple is the identity of the product. `test/docs/caller_claims_test.exs` reads every
`lib/` `@doc` and `@moduledoc` from the AST. A backticked project function named in a
caller claim must exist, and in a function's doc a named caller (called from, used by)
must reach the documented function, directly or through its own module's helpers. Its
first run found `ExTLA.Profile`'s moduledoc naming `take_action/6` and
`enabled_bindings/3` as though they were in `ExTLA.ModelChecker.Safety`; they are in
`ExTLA.ModelChecker`, which the sentence now names. The gate also refuses a *qualified*
reference to a private function: ex_doc links it, and the docs job's
`--warnings-as-errors` refuses the link, which is how this MR's first pipeline failed. A
claim that names no function, like the old
"the DSL needs", is not refused, because `lib/` uses the same phrasing for concepts
("what Layer 3 needs"). Three more copies of the independent product (`record_set/1`,
`function_set/2`, `Symmetry.mappings/2`) are #350.
- **`ExTLA.Invariant.Test.valid_states/3` now says what a broad base generator costs**
(#348, for #344). It filters the base generator through the type invariant, and
`StreamData.filter/3` raises `FilterTooNarrowError` after 25 rejections in a row, but
nothing told a consumer that. Measured on a real type invariant over 2,000 seeds, 100-run
properties raised in none of the runs at about 50% acceptance, in 1 in 90 at about 30%,
and in 3 in 4 at about 15%. The 30% band is the dangerous one: it passes locally and goes
red on a rare seed. The docstring and `spec/03` §`valid_states` now say the base generator
should have the type's shape and be accepted at least about half the time, with a good and
a bad example, both held by a test. Another test reads the limit from the exception and
checks that the docstring and the spec state the number StreamData enforces. There is no
new `max_consecutive_failures:` option: raising the limit would move the failing band, not
remove it.
- **A property failed on one run in about 12,000, and a guard now makes generator filters say
why** (#343, for #341). `helpers_test.exs`'s out-of-domain property drew an index and then
filtered it with `m not in 1..length(s)`. A filter clause in `check all` rejects into the
generator before it, which raises `StreamData.FilterTooNarrowError` after ten rejections in
a row. With up to 6 of 21 draws rejected, that happened on 5 of 60,000 seeds, including CI's
796610 on v0.4.0's release MR. It had been there since #242 (2026-09-11), unseen because CI
draws a fresh seed per run. The property now generates the same out-of-domain values
directly: 0 failures in the same 60,000 seeds. The guess that StreamData's `integer/1`
shrinks its range at small sizes was wrong, since its documentation says the size is
ignored. `test/docs/property_filters_test.exs` reads `lib/`, `ledger/` and `test/` and
refuses any filter clause in `check all`/`gen all`, or any `StreamData.filter`, without a
`# filter: <reason>` above it. The one deliberate filter,
`ExTLA.Invariant.Test.valid_states/3`, now carries its reason.
63 commits since v0.4.0, merges included.
Tagged at 47304f6c2198.