From d3e2503edc4c3c2c2101995898761cd4bf2440b8 Mon Sep 17 00:00:00 2001 From: Andrew Dolgert Date: Sat, 26 Sep 2026 20:28:14 -0400 Subject: [PATCH 01/29] Phase 1 scaffolding: version 1.0.0-DEV, Julia 1.10 floor, design docs Bump to 1.0.0-DEV with julia = "1.10" and a CI matrix of 1.10, 1.11, and the latest release. Ignore .DS_Store, Manifest.toml, and .vscode/. Move the interface proposals, synthesis, answers, and implementation plan into design/, and add the Phase 3 benchmark procedure, the dependents check with a changelog skeleton, and the Phase 1 review note. Co-Authored-By: Claude Fable 5.1 --- .github/workflows/ci.yml | 3 +- .gitignore | 3 + Project.toml | 4 +- design/20260926_answers.md | 7 + design/20260926_implementation_plan.md | 700 ++++++++ design/benchmark_procedure.md | 55 + design/interface_astra.md | 471 +++++ design/interface_astra.pdf | Bin 0 -> 77588 bytes design/interface_fable.md | 728 ++++++++ design/interface_fable.pdf | Bin 0 -> 119881 bytes design/interface_opus.pdf | Bin 0 -> 458814 bytes design/interface_opus.tex | 1303 ++++++++++++++ design/interface_synthesis.md | 2296 ++++++++++++++++++++++++ design/interface_synthesis.pdf | Bin 0 -> 230407 bytes design/phase1_review.md | 141 ++ design/release_notes_1.0_draft.md | 154 ++ design/z3_example.jl | 364 ++++ 17 files changed, 6226 insertions(+), 3 deletions(-) create mode 100644 design/20260926_answers.md create mode 100644 design/20260926_implementation_plan.md create mode 100644 design/benchmark_procedure.md create mode 100644 design/interface_astra.md create mode 100644 design/interface_astra.pdf create mode 100644 design/interface_fable.md create mode 100644 design/interface_fable.pdf create mode 100644 design/interface_opus.pdf create mode 100644 design/interface_opus.tex create mode 100644 design/interface_synthesis.md create mode 100644 design/interface_synthesis.pdf create mode 100644 design/phase1_review.md create mode 100644 design/release_notes_1.0_draft.md create mode 100644 design/z3_example.jl diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2580867..6b51390 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,8 +10,9 @@ jobs: fail-fast: false matrix: version: - - 'lts' + - '1.10' - '1.11' + - '1' os: - ubuntu-latest arch: diff --git a/.gitignore b/.gitignore index 3af67b1..a221db5 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,6 @@ *.jl.cov *.jl.mem /docs/build/ +.DS_Store +Manifest.toml +.vscode/ diff --git a/Project.toml b/Project.toml index 975a843..b9c0801 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "UnitTestDesign" uuid = "239896fa-e45a-40e8-9993-3c434b0bc450" authors = ["Andrew Dolgert "] -version = "0.4.0" +version = "1.0.0-DEV" [deps] Combinatorics = "861a8166-3701-5b0c-9a16-15d98fcdc6aa" @@ -10,4 +10,4 @@ Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" [compat] Combinatorics = "^1" Random = "^1" -julia = "^1.2" +julia = "1.10" diff --git a/design/20260926_answers.md b/design/20260926_answers.md new file mode 100644 index 0000000..c94c7b4 --- /dev/null +++ b/design/20260926_answers.md @@ -0,0 +1,7 @@ +The disallow function can be deleted now. No need to support it. +I like the idea of having a function that helps you diagnose outcomes. That could be handy. +I don't know that we need to find evidence for combinatorial testing. That seems like it's a paper to write. Let's focus on that after we get an update done. +We should do Option 3. From Option 4, cherry pick that we should do diagnose, Invalid, Partition, and github_matrix. Those are my favorites. +Otherwise, I like your guesses on how to answer the main questions. Go with it. +Let's be real. You talk about how many months this would take, but we can execute this change quite quickly using AI agents. I'd like you to plan this as a single updated release. Make a plan that has phases, where each phase has steps within it. I'll review at the end of each phase. There should be no more than 8 phases. + diff --git a/design/20260926_implementation_plan.md b/design/20260926_implementation_plan.md new file mode 100644 index 0000000..bf8c150 --- /dev/null +++ b/design/20260926_implementation_plan.md @@ -0,0 +1,700 @@ +# UnitTestDesign.jl 1.0: Implementation Plan + +Date: 2026-09-26. Prepared from `interface_synthesis.md` and the decisions +in `20260926_answers.md`. One release, eight phases, a review at the end of +each phase. Every phase ends with the test suite green and the package +usable, so a review can run the code, not just read it. + +Revised after the plan review: correctness and wrapper semantics are fixed +in Phase 1; implementation remains in eight phases and one release. + +## Decisions carried into this plan + +From your answers: + +- **Option 3 (the friendly product), plus four picks from Option 4:** + `diagnose`, `Invalid`, `Partition`, `github_matrix`. Nothing else from + Option 4: no runner, no outcome files, no `as_code`, no TOML, no + rolling coverage, no `max_cases`. +- **`disallow` is deleted**, not deprecated. `nothing` never denotes an + unassigned parameter in user cases; it remains a legitimate domain value. +- **No evidence track.** The case study and mutation analysis wait until + after this release. +- **The synthesis's leans decide every open question** (its Section + "Questions the best version must answer"). The ones that shape code: + - Constraints live in the `TestSpace`; generation calls accept a + `constraints =` convenience that builds a space. + - All rule forms compile to one internal `Constraint` (scope, predicate, + polarity, label), tabulated over the scope. Whole-case predicates are + allowed as an escape hatch with a documented cost. + - Exclusions are attributed honestly: a named rule for direct + exclusions, a deletion search for implied ones, `unknown` when a search + limit is hit. Never an invented cause. + - `TestCases{T} <: AbstractVector{T}` preserves domain values and their + concrete types without numeric promotion. Homogeneous domains have + concrete field types; heterogeneous domains may use union or abstract + field types. Since this is 1.0, the positional path changes its return type too + (`Vector{Vector{Any}}` becomes `TestCases{Tuple{...}}`). + - `show` computes only bookkeeping; verification and curves live in + `report` and `coverage`. + - Deterministic across runs (GND gets a fixed default seed); not + promised across versions; stable across edits only through + `must_include`. + - Outcomes enter only through the pure function `diagnose(cases, passed)`. + - Julia target is the latest LTS, 1.10, and nothing earlier: `julia = "1.10"` + in `Project.toml`, CI runs on 1.10, 1.11, and the latest release, and + the code uses 1.10 features freely (package extensions, `Returns`, + `@NamedTuple`) with no compatibility shims for older versions. + - Vocabulary: `TestSpace`, `constraints`, `must_include` (alias `seeds`), + `strength` (alias `n_way`), `stronger`, `TestCases`, `covering` as the + general entry point (alias `all_tuples`), `coverage`, `explain`, + `report`, `design_sizes`. + +Small choices I made where the synthesis left two names or was silent. +Say so at the Phase 1 review if you want a different one: + +| Choice | Pick | Why | +|:--|:--|:--| +| Planning function | `design_sizes` | Fable-led option; the name says what the table contains | +| Realizing a `Partition` at run time | `realize(case; rng)` | Draws are explicit and seeded by the caller, never hidden in iteration | +| Constraints in positional calls | Not supported; use a space | Rules need names; positional users get every other feature | +| `Invalid` values and rules | In a row whose invalid parameter is `p`, rules mentioning `p` are not applied; all other rules still apply | Contract and coverage meaning reviewed in Phase 1; implemented in Phase 6 | +| The 0.4 `seeds`/`n_way`/`wayness`/`all_tuples`/`*_excursion`/`GND(M=)` spellings | Kept one version as deprecated aliases that warn | Cheap, and it keeps registered users compiling | +| `generate_tuples`, `Excursion`, `Counter` | Removed from the public surface | No user-facing purpose once the request is internal | + +Branching: merge `fix/gnd-match-condition` to `main` first (its commit +message already says only what it fixes). Then one branch, `release/1.0`, +with one PR per phase into it, and one PR from it to `main` in Phase 8. + +## The target, in one screen + +```julia +using UnitTestDesign + +space = TestSpace( + (mode = [:fast, :exact], solver = [:none, :lu, :qr], tol = [1e-3, 1e-6]); + constraints = [ + @require(mode == :exact || solver == :none), + forbid((mode = :exact, tol = 1e-3); reason = "exact mode needs a tight tolerance"), + ]) + +cases = all_pairs(space) # TestCases{NamedTuple}, 5 cases +explain(space, (solver = :lu, tol = 1e-3)) # infeasible: the two rules together +coverage(handwritten, space) # what an existing suite misses +all_pairs(space; must_include = handwritten) # keep them, add a compact set covering the gaps +report(cases) # excluded list, bonus coverage, prefix curve +design_sizes(space) # cases per strength, before committing +diagnose(cases, passed) # ranked suspects after a run (experimental) +github_matrix(cases) # JSON for a workflow's include: list +all_pairs([1, 2, 3], ["a", "b"], [1.0, 2.0]) # positional still works; returns TestCases{Tuple} +``` + +Final layout of `src/` (new files marked): + +``` +UnitTestDesign.jl exports, includes +space.jl * TestSpace, value identity, Partition and Invalid wrappers +constraints.jl * Constraint, forbid/require, @forbid/@require, tabulation +feasibility.jl * violates, completable (with witness and limit), classify targets +request.jl * internal request: arity, strength, groups, dead(), must_include +combinations.jl (kept) +coverage_matrix.jl (kept; dead-partial predicate replaces disallow) +parameter_order.jl IPOG (kept; three sites use the predicate) +greedy_tuples.jl GND (kept; progress guarantee, seed, candidates) +excursions.jl explicit base, distance +full_factorial.jl incremental enumeration, materialized result, size guard +testcases.jl * TestCases{T}, show, Tables-compatible iteration +interface.jl * covering, all_pairs..., excursions, full_factorial, deprecations +measure.jl * coverage, missing_interactions, report, design_sizes +invalid.jl * one-invalid-per-case generation +partition.jl * realize +diagnose.jl * diagnose, followups +export.jl * github_matrix +``` + +`coverage_set.jl` goes away: the independent checker in `test/` takes over +its role, and `measure.jl` works in value space. + +--- + +## Phase 1: Specification, checker, and scaffolding + +Goal: write down the contract everything else is judged against, and put +the tests in place that will fail until the engines keep it. + +Steps: + +1. **Merge the GND branch to `main`; cut `release/1.0`.** Open one issue, + "Implicit constraints crash IPOG and hang GND," with Opus's + os/gpu/driver example, so Phase 3 has something to close. +2. **Write `docs/src/dev/contract.md`.** The six-point semantic contract + from the synthesis (valid case; feasible combination; every returned + case valid and every required combination covered; exclusions reported + and attributed; user code never sees a partial case; `unknown` under + limits). Add value identity (concrete type and `isequal`, duplicates + rejected, singleton domains allowed, `nothing` is a value), the + determinism boundary, must-include ordering, `stronger` validation + rules, and the "not now" list (runner, outcome files, model inference, + CLI, serialization framework, solver, shrinking, fixture catalog, + `as_code`, TOML, `max_cases`). This page is the spec; later phases cite it. + Resolve these details here, before engine work: + + - **Resource limits.** Feasibility has three states. Generation must + resolve every target and placement decision or stop with a clear + resource-limit error; it never returns an uncertified design. + Measurement and explanation may return `unknown`, and must not claim + complete coverage or an exact percentage with unresolved targets. + Document the `feasibility_limit` keyword, its default node budget, + accounting across component searches, and how to retry with a larger + budget. Explanation limits are separate: proven infeasibility remains + proven even if finding a smaller explanation exhausts its budget. + - **Identity.** Preserve each supplied value and its concrete type in + storage, rules, results, and coverage keys. For example, `Any[1, 1.0]` + contains two choices. No promotion may merge them. Identity of + `Invalid(x)` includes the marker and the wrapped value's type and + `isequal`; `Invalid(x)` and `x` are distinct choices. + - **Partitions.** Within a parameter, partition names are unique; + reject a raw Symbol equal to a partition name to avoid ambiguous rule + patterns. Rules and coverage see the name; returned cases retain the + wrapper until explicit `realize`. Nested wrappers are unsupported. + Realization preserves the tuple or named-tuple shape and makes no + additional coverage claim about the sampled concrete values. + - **Negative cases.** Each parameter must have at least one ordinary + value (a `Partition` counts); reject domains containing only `Invalid` + values. Ordinary rows satisfy every rule. A negative row contains + exactly one `Invalid` value at parameter `p` and satisfies all rules + whose scopes omit `p`. Whole-case rules therefore do not apply to + negative rows. Only ordinary rows contribute to ordinary coverage; + report negative coverage separately. Rows violating either applicable + rule set contribute no coverage. + - **Negative targets.** For each invalid value at `p`, each requested + group containing `p` with strength `s` requires every feasible + `(s-1)`-way combination on the other parameters in that group. + Feasibility uses the negative-row rules above and ordinary values in + every other parameter. Union these targets across the base and + stronger groups. At strength 1, the empty combination requires one + valid completion per invalid value, if one exists. Report impossible + negative targets and stop generation on unresolved ones. Groups + omitting `p` add no negative targets; their ordinary coverage is + already required of the ordinary design. + - **Strategy boundaries.** Full factorial enumerates ordinary and + single-invalid rows satisfying their applicable rules; excursions + use the same row-validity policy within their distance bound. Neither + includes multiple-invalid rows. Must-include rows use the same policy; + partial rows must admit a completion, and keep assigned values intact. + A partial row without an `Invalid` marker is completed as an ordinary + row; negative completion must be requested explicitly with the marker. + - **Honest size claims.** Engines produce compact designs, with no + minimum-case-count guarantee. Deletion search produces a sufficient + explanation, labeled inclusion-minimal only when verified; it does + not promise a minimum-size rule set. +3. **Write the independent checker, `test/checker.jl`.** Value-space, no + shared code with the engines. `check_design(cases, space; strength, + stronger)` enumerates the full product for small spaces, computes the + valid set and the feasible combinations by brute force, and returns + separate ordinary and negative results with valid rows, missing targets, + and rejected rows. Implement the Phase 1 policies independently of + production feasibility and tabulation. Verify the oracle itself against + hand-enumerated fixtures using plain test data in Phase 1; add an adapter + for the production space type in Phase 2. It is the oracle for Phases 3–6. +4. **Write the random problem generator, `test/random_problems.jl`.** + Opus's shape: 3–8 parameters, 2–4 values, 1–4 scoped rules over 2–3 + parameters, half of them written with `!=` or `<`. A `@testitem` runs + 500 pairwise and 500 three-way problems through IPOG and GND, scaled by + `test_run_multiplier()`, with a fixed seed and `seed_mod()`. Record + a narrowly scoped expected failure for the known legacy engine defect; + keep future-API tests explicitly pending until their phase. Do not mask + arbitrary exceptions as expected failures. The random gate is mandatory + in Phase 3. Add a deterministic fixture inventory for disconnected + unsatisfiable components, exhausted limits, heterogeneous values, + partial seeds, overlapping stronger groups, and wrapper interactions. +5. **Bump `Project.toml`** to `1.0.0-DEV`, `julia = "1.10"` (the latest LTS; + nothing earlier is supported). Set the CI matrix to `'1.10'`, `'1.11'`, + and `'1'`, replacing the `lts` alias so the floor is explicit. Add `.DS_Store`, `Manifest.toml`, `.vscode/` + to `.gitignore`. Move `interface_*.{md,pdf,tex}`, `interface_synthesis.*`, + `z3_example.jl`, and the two dated files into `design/` so the root is clean. +6. **Check registered dependents** on JuliaHub and record the result in + the Phase 8 release notes draft. This is the only input to whether the + positional return-type change needs an announcement beyond the changelog. + +Acceptance gate: review the contract, deprecation list, fixture inventory, +and dependent-package findings. Run the checker against hand-enumerated +ordinary and negative examples and run the existing suite. List pending +tests with their activation phase; only the known legacy regression is an +expected failure. Record benchmark fixtures, hardware/runtime metadata to +collect, and the measurement procedure for Phase 3. + +--- + +## Phase 2: The model: `TestSpace`, rules, feasibility + +Goal: a pure, engine-independent layer that answers every question about a +space. No generation yet. Everything here is testable by hand. + +Steps: + +1. **`TestSpace`** (`space.jl`). Constructors: `TestSpace(nt::NamedTuple; + constraints)` and `TestSpace(pairs::Pair{Symbol}...; constraints)`. + Validation with messages in the user's vocabulary: names distinct; + each domain nonempty and ordered; duplicate values rejected by concrete + type and `isequal` ("parameter `tol` lists `1.0` twice"). Fields: + `names`, `values` (a tuple of vectors), `constraints`, and the tabulated + tables from step 5. Copy domains without promoting their values. + Accessors: `parameters(space)`, `arity(space)`, + `Base.length` is the full product (as `BigInt`-safe `prod`). +2. **`Invalid(x)` and `Partition(name, draw)` wrappers** are defined here + using the Phase 1 identity and collision rules. A rule or pattern sees + a partition's name (a `Symbol`). Validate domains and wrappers now; + implement negative-row rule selection in the model so feasibility and + measurement share it. Generation and realization arrive in Phase 6; + until then generation on wrapper spaces fails with an explicit + unsupported-feature error rather than applying ordinary semantics. +3. **`Constraint`** (`constraints.jl`): `scope::Tuple{Vararg{Symbol}}`, + `predicate`, `polarity` (`:forbid`/`:require`, kept for display; both + normalize to "forbidden combinations" at tabulation), `label` (reason + and/or source text). Constructors: + - `forbid(pattern::NamedTuple; reason)` — exact partial assignment. + - `forbid(names::Symbol...; reason) do values... end` and the same for + `require`. + - `forbid(f; reason)` with no names is the whole-case escape hatch: `f` + receives the complete case as a `NamedTuple`. + Construction errors name the parameter and list the space's names. + A predicate that returns anything but `Bool` is an error; an exception + thrown by a predicate is rethrown with the rule's label and arguments. +4. **`@forbid` and `@require`.** Fable's design: free identifiers that are + not in call position are parameter names; `$x` interpolates from the + caller; the source text is kept. The unknown-name error suggests + `$name` if a variable was meant. The macros produce the same + `Constraint` as step 3, so nothing downstream knows they exist. +5. **Tabulation.** For each rule, evaluate once per combination of its + scope's domains and store the forbidden index tuples in a `Set`. Above + a threshold (default 10^5 evaluations, configurable) evaluate lazily + with a memo and warn once, suggesting a narrower scope. Whole-case + rules are always lazy. Tabulation order is fixed so reports are stable. + Evaluate ordinary values (partition names included); negative-row + checks select only tables whose scopes omit the invalid parameter. +6. **Feasibility** (`feasibility.jl`). `violates(partial_idx)` fires only + when a rule's whole scope is assigned. `completable(partial_idx; limit)` + is backtracking with forward checking that returns `(true, witness)`, + `(false, nothing)`, or `:unknown` when `limit` nodes are exceeded. + Solve every constrained connected component, including components with + no assigned parameter. Cache witnesses for independent components and + combine them into a complete witness; only unconstrained parameters may + be filled freely. An unsatisfiable component makes the whole completion + infeasible. Memo keys include the assignments and active rule set, so + negative rows, deletion searches, and diagnosis cannot reuse stale + answers. Never cache an exhausted search as infeasible. + `classify(space, targets)` labels each target combination `required`, + `forbidden` (which rule), `implied` (a proven sufficient rule set found + by deletion search), or `unknown`. Claim inclusion-minimality only if + every remaining rule is verified necessary. If explanation search hits + its limit, retain the last proven sufficient set and mark minimality + unresolved; target feasibility and explanation quality are separate. +7. **`isallowed(space, case)` and `explain(space, partial)`.** Five + outcomes: `allowed`, `forbidden` (with the rule), `completable` (with a + witness), `infeasible` (with rules), `unknown`. The result prints a + sentence and carries fields. +8. **Tests** (`test/test_space.jl`, `test_constraints.jl`, + `test_feasibility.jl`): Astra's `A == B`, `B == C` example; Fable's + solver example (3 direct, 2 implied, listed by name); Opus's + os/gpu/driver example; `nothing` as a real value; the `!=` rule that + silently over-forbade in 0.4 now forbids exactly two pairs; the macro + error message; the threshold warning; `unknown` with `limit = 1`; + determinism of tabulation order. Add fixtures where the assigned + parameter is unconstrained but a separate component is unsatisfiable, + where two satisfiable components need non-default witness values, and + where a whole-case predicate connects them. Check type-preserving + identity, wrapper collisions, active rule sets, and retries after limits. + +Acceptance gate: all model tests and the full suite pass. Review executable +examples of all rule forms, the solver example's 3 direct and 2 implied +exclusions, the disconnected-component fixtures, and `unknown` followed by +a successful retry. Inspect witness validity and explanation status fields. + +--- + +## Phase 3: Engines that keep the promise + +Goal: IPOG and GND return certified designs or clear errors, terminate under +configured limits, and remove `disallow`. Random problems go green. + +Steps: + +1. **Internal request** (`request.jl`). One struct the engines consume: + `arity`, `strength`, `groups` (index tuples with their strength, base + group included), `dead(partial_idx)::Bool` (true only for proven + infeasibility, false only with a completion witness; throws a + resource-limit error on `unknown`), `must_include` as an index matrix + with `0` for unset, the feasibility budget and run-local memo, and + the classified target list from Phase 2. One entry point, + `generate(engine, request)`, returns the index matrix plus bookkeeping: + which targets were covered, how many rows came from `must_include`, + and the engine's seed. All target classifications must be resolved + before a result is returned. Validate feasibility of the whole space + even when there are no targets; a proven empty space returns an empty + design unless must-include requirements make the request impossible. +2. **IPOG.** Replace `disallow` at the three sites + (`choose_last_parameter_filter!`, `insert_tuple_into_tests_filter`, + `fill_remaining_missing_values_filter!`) with `dead`. Because every + site commits a value only when the row stays completable, the + completability invariant holds by induction; the initial combinations + and each pushed tuple are completable because the feasibility pass + removed the rest. The unconstrained `ipog` fast path stays. In + `ipog_multi_way`, copy the groups, accept ranges and tuples, and treat a + group at the base strength as a no-op instead of asserting. +3. **GND.** `allowed_argmax` uses `dead`. Progress guarantee: if no + candidate in a round covers anything, build one directly from the + first uncovered target with a stored or newly proven witness, so the + `error("Could not construct...")` path and the attempt cap go away. + `GND(; seed = 0, candidates = 50, rng = nothing)`: a fixed default + seed, `M` accepted with a deprecation warning, the seed recorded in the + result. Exhausting a witness-search budget raises the same resource-limit + error; it never drops the target or retries indefinitely. + `n_way_coverage_init` and the instrumented IPOG variant are + deleted if no test uses them. +4. **Excursions.** `build_excursion(arity, distance, base_idx, dead)` + with an explicit base; rows that are dead are dropped and *reported* + in the bookkeeping (which values never appear); a forbidden base is an + error naming the rule. +5. **Full factorial.** Enumerate candidates incrementally and retain only + accepted rows; the public return remains a materialized `TestCases`. + There is no lazy public result in this release. Before enumeration, + refuse a candidate product above `limit = 10^6`, giving its count; + report candidate and accepted counts separately. Phase 6 extends the + count to the ordinary product plus all single-invalid products. +6. **Final validation** inside `generate`: every row complete, every row + passes every applicable rule (including lazy predicates), no target + classification remains unknown, and every required target is covered. + Validate against the requested strategy: covering targets, excursion + distance, or full-factorial completeness. Phase 6 validates ordinary + and negative coverage separately. A limit raises a resource-limit error; + an invariant failure raises an internal error naming the row or target. +7. **Delete** `wrap_disallow`, the `disallow` keyword, `Counter`, + `seeds_to_integers`' sentinel handling, and documentation of `nothing` + as a partial-assignment sentinel; retain examples using it as a real + value. Single-valued parameters are allowed; a strength + larger than the parameter count is an `ArgumentError`; equal to it is + a full factorial under the applicable row policy. +8. **Turn on the random problems.** Both engines, both strengths, every + result checked by `test/checker.jl`. Close the Phase 1 issue. +9. **Performance baseline.** Benchmark 15 four-valued parameters at + strength 4 and a checked-in fixture for Fable's 12-parameter constrained + example. Record Julia version, hardware, thread count, engine seed, + compilation policy, allocations, and median of repeated warm runs; + report cold-start compilation separately. Measure the prior revision + on fixtures it handles correctly, and establish a baseline for repaired + constrained cases. Use stable CI runners to set an explicit regression + tolerance from observed variation. Ordinary correctness tests assert + completion and coverage, not machine-dependent seconds. Include limit + exhaustion and progress regressions in the deterministic suite. + +Acceptance gate: all 1,000 random problems pass for each engine at the +full multiplier, targeted regressions pass, and the full suite is green. +Review the benchmark command, environment, before/after results, and chosen +regression tolerance, plus the removal of `disallow`. Demonstrate that a +limited search raises an error rather than returning an incomplete design. + +--- + +## Phase 4: The public interface and the result type + +Goal: the 1.0 surface, with the old spellings deprecated and every +existing test rewritten against it. + +Steps: + +1. **`TestCases{T} <: AbstractVector{T}`** (`testcases.jl`). Fields: + `cases::Vector{T}`, `space`, `strategy` (`:covering`, `:excursion`, + `:full_factorial`), `strength`, `stronger`, `engine` (name and seed), + `n_must_include`, `excluded` (direct with rule labels, implied with + rules and explanation status), and `covered` (count of required targets, + from bookkeeping). Generated designs have no unknown target status; + analysis results may. Keep separate ordinary and negative bookkeeping + as specified in Phase 1. `T` is a `NamedTuple` type for named spaces and + a `Tuple` type for positional calls. Preserve domain values and their + concrete types without conversion. Homogeneous domains use concrete + field types; heterogeneous domains may use union or abstract field types. + `Any[1, 1.0]` must remain two distinct choices through generation, + collection, rule evaluation, and coverage. `collect` gives a plain vector. +2. **`covering(space; strength = 2, stronger = [], must_include = [], + engine = IPOG(), constraints = [])`** (`interface.jl`), with + `all_values`, `all_pairs`, `all_triples` as fixed strengths. Each + accepts a `TestSpace`, a `NamedTuple` of domains, `Symbol => values` + pairs, or bare positional vectors (which build a space named + `p1, p2, ...` and return tuples). `constraints =` on named domain input + builds the space; on a `TestSpace` it is an error ("constraints belong + to the space"). Expose the Phase 1 `feasibility_limit` on generation + and analysis entry points, and document resource-limit errors and retry. + Positional calls reject `constraints =`; use a named space for rules. +3. **`must_include`.** Named: `NamedTuple`s, partial allowed, an existing + `TestCases` accepted. Positional: tuples or vectors. Validated by name + and value before generation with messages that name the case, the + parameter, and the offending value; a case violating the applicable + row policy is an error. A partial case must have a proven completion; + after Phase 6, a partial case containing `Invalid` uses the negative-row + policy. Limit exhaustion is distinct from an infeasible must-include. + Must-include cases come first, in the order given; duplicates kept. + `seeds` accepted with a deprecation warning. +4. **`stronger = [(:a, :b, :c) => 3]`** validated per the contract: names + exist, distinct within a group, group strength at least the base + and at most the group size, overlapping groups combine by union, the + caller's vector untouched. A group at base strength is a no-op, matching + Phase 3. Positional: `[(1, 3, 4) => 3]`. `wayness` + accepted with a deprecation warning and translated. +5. **`excursions(space; from = nothing, distance = 1, must_include)`** + and `full_factorial(space; limit)`; `values_excursion`, + `pairs_excursion`, `triples_excursion` kept as thin aliases. The + docstring for `excursions` states its promise (within `distance` of + the base) and that it is not a covering guarantee. +6. **`show(io, ::TestCases)`.** One summary line ("5 cases · strength 2 · + IPOG · 3 parameters · 12 combinations, 5 valid" when the valid count + is cheap, otherwise just the product), then "excluded: 3 pairs + forbidden, 2 impossible because constraints combine; see + report(cases)", then an aligned table truncated like a `DataFrame`. + Nothing is computed at display time that generation did not already know. +7. **Tables.** A vector of `NamedTuple`s already satisfies Tables.jl's + row-table interface; add a test that `DataFrame(cases)` and + `CSV.write` work (both are already test dependencies). +8. **Deprecations and removals.** `all_tuples`, `n_way`, `seeds`, + `wayness`, `GND(M=)` warn once via `Base.depwarn`. `generate_tuples`, + `Excursion`, `Counter` removed. `disallow` is not a keyword anywhere; + passing it hits the ordinary unknown-keyword error. +9. **Rewrite existing tests** (`test_factorial_interface.jl`, + `test_excursions.jl`, `test_full_factorial.jl`, `nonfunctional.jl`, + `cli.jl`) against the new surface. Add deterministic tests for + heterogeneous domains, partial and duplicate must-includes, overlapping + stronger groups, and preservation of caller-owned inputs. Aqua stays green. + +Acceptance gate: run generation, explanation, and display examples from +the target screen (measurement and add-on calls remain pending until their +phases). Review the transcript and deprecation warnings; confirm typed-value +preservation, Tables interoperability, targeted regressions, Aqua, and the +full suite pass. Display must perform no feasibility searches. + +--- + +## Phase 5: Measure and explain: `coverage`, `report`, `design_sizes` + +Goal: the claim is checkable in one line, and the package says what it +promised, what it left out, and what a design would cost. + +Steps: + +1. **`coverage(cases, space; strength = 2, stronger = [])`** + (`measure.jl`), for any iterable of `NamedTuple`s (or tuples for a + positional space), independent of the generator's bookkeeping. + Returns a `Coverage` with `covered`, `feasible`, `missing` (named + combinations), `unknown`, and a per-group breakdown. Separate ordinary + and negative coverage according to Phase 1; rows violating their + applicable rules contribute nothing and are listed; duplicates count + once. Negative rows never increase ordinary coverage. Implement the + result structure now and activate wrapper inputs in Phase 6. + `coverage(cases::TestCases)` reads the space and request from the + result. Prints "covers 11 of 11 feasible pairs" or the missing list when + classification is resolved. With unknown targets, report known counts + and unresolved targets without an exact percentage or completeness claim. +2. **`missing_interactions(cases, space; ...)`** returns + `coverage(...).missing` when classification is resolved; otherwise raise + a resource-limit error directing the caller to `coverage` for the known + missing and unresolved targets. An empty list must not hide uncertainty. +3. **`report(cases::TestCases)`**: the guarantee line, the excluded list + with attribution, bonus coverage at strength+1, the prefix curve + ("first 5 of 10 cover 73%"), and the seed. Returns a `Report` whose + fields serialize; printing is the display. This is where the + verification Astra keeps out of `show` happens. Apply the same unknown + policy to bonus coverage and prefix curves. When no strength+1 targets + exist, mark bonus coverage not applicable. +4. **`design_sizes(space; strengths = 1:3, distances = 1:2)`**: one row + per strategy with the case count, share of the valid product, and + pairs and triples covered, computed by running IPOG per strength. Full + factorial reports total and valid counts when the product is below + the Phase 3 limit, otherwise the total only. Omit shares when the valid + count is unknown; report a strategy's resource-limit status rather than + an invented case count. Skip strengths exceeding the parameter count. +5. **Top-up and extend as recipes**, tested but not new functions: + `all_pairs(space; must_include = existing)` and + `all_triples(space; must_include = cases)`. A test asserts that the + existing cases survive in order and that the result is complete. +6. **Tests**: `coverage` of hand-written cases against the checker; + `coverage` of every generated covering design is complete; `report` numbers + agree with the checker on the random problems; `design_sizes` + reproduces Fable's 10-of-81 example. Test limit exhaustion in coverage, + missing-interaction queries, bonus reports, and planning; verify that + unresolved denominators never print as exact percentages. + +Acceptance gate: review checked outputs for the solver example, a +hand-written suite with gaps, and a limited search with unresolved targets. +Run audit/top-up/extend examples and compare their results to the independent +checker. All measurement tests and the full suite pass. + +--- + +## Phase 6: `Invalid`, `Partition`, `diagnose`, `github_matrix` + +Goal: implement the four Option 4 features using the Phase 1 contract, +with matching docstrings and `diagnose` labeled experimental. + +Steps: + +1. **`Invalid(x)`** (`invalid.jl`). Generation: the covering design is + built over valid values only; then, for each invalid value `v` of + parameter `p`, build the negative targets specified in Phase 1 for the + base and all stronger groups containing `p`. Generate completions over + the other parameters' ordinary values under the applicable rules, then + insert `p = v` at its original parameter position. At strength 1, use + one witness for the empty target; do not call a strength-0 public API. + Pairwise generation pairs the invalid value with every feasible ordinary + value of every other parameter. Report infeasible targets and stop on + unresolved feasibility, exactly as for ordinary generation. The + case keeps the wrapper (`n = Invalid(-1)`) so a test body can branch + on `hasinvalid(case)`; `show` marks the rows; `report` and `coverage` + state the two guarantees separately. Rules mentioning `p` are not + applied in rows where `p` is invalid. Activate wrapper support in + generation, must-includes, full factorial, excursions, and measurement, + removing the temporary unsupported-feature errors from Phase 2. +2. **`Partition(name, draw)`** (`partition.jl`). Coverage is over names. + `realize(case; rng)` preserves tuple or named-tuple shape, substituting + `draw(rng)` for each partition; `realize(cases; rng)` maps over the vector. A + fixed-value label is `Partition(:tiny, Returns(1e-9))`. Nothing is + inferred from a function-valued domain; only the wrapper triggers this. + Preserve `Invalid` markers and ordinary values during realization; + nested wrappers remain rejected. Coverage is measured on the original + labeled cases, so callers retain those alongside realized inputs. +3. **`diagnose(cases, passed::AbstractVector{Bool}; strength)`** + (`diagnose.jl`). For sizes 1 to `strength`, list combinations present + in at least one failing case and no passing case; rank by failures + containing them, then by size, with a deterministic tie-break. Include + pass/fail counts and group candidates with identical observed occurrence + patterns as observationally indistinguishable. Every candidate is + unverified by passing cases; do not add a redundant "masked" flag. + Validate outcome length and handle no failures, all failures, and + conflicting outcomes for duplicate cases explicitly. + `followups(diagnosis)` attempts to find a valid case containing each + suspect and no other suspect using the Phase 2 witness search with + temporary forbidden tables. These isolation conditions always apply, + including when they mention an invalid parameter. Return per-suspect + status: `found` with a + witness, `inseparable` with a proven explanation, or `unknown` when the + search limit is exhausted. Nested suspects and constraints may make + isolation impossible. Prefer small changes from a failing case as a + heuristic, with no minimum-distance guarantee. Use the appropriate + ordinary or negative row policy; distinguish observational equivalence + from proven inability to isolate a suspect. Docstrings + say: experimental; hypotheses, not proof; multiple faults and + intermittent failures can confuse the ranking. +4. **`github_matrix(cases; io = stdout)`** (`export.jl`). Emits + `{"include": [...]}` using an established JSON encoder added as an + explicit dependency. Accept named rows with strings, finite real numbers, + booleans, and `nothing` (JSON null); encode Symbols as strings. + Reject unsupported values, non-finite numbers, and wrappers with a + message naming the row and field; callers explicitly map those values + to supported data first. Validate all rows before writing to `io`. + Warns above 256 jobs. The docstring shows the workflow YAML that reads + it with `fromJSON`. +5. **Tests**: check ordinary and negative guarantees with the independent + oracle, including strength 1, mixed strengths, infeasible negative + targets, wrapper collisions, and invalid-only domain rejection. + Verify `realize` determinism under a seeded `Xoshiro` and preservation + of positional shape. Opus's newton/sparse example checks ranking and + every follow-up that can be isolated; nested and constrained suspects + exercise `inseparable` and limited searches exercise `unknown`. + Parse exported JSON and compare values, including quotes, backslashes, + control characters, Unicode, nulls, empty results, and rejected + unsupported/non-finite values. Do not rely only on a fixture string. + +Acceptance gate: run all four feature examples and the complete target +screen. Review separate ordinary/negative coverage, realization output, +diagnosis statuses for found/inseparable/unknown cases, and parsed matrix +JSON. Wrapper integration tests, targeted regressions, and the full suite +pass with no wrapper feature tests pending. + +--- + +## Phase 7: Documentation and first contact + +Goal: a README that leads with the situation, a manual organized as +tutorial, how-to, explanation, and reference, and every example a doctest. + +Steps: + +1. **README first screen**: the promise sentence ("Describe the + configurations your code must handle; it tells you which combinations + your tests exercise, and supplies a compact set of additional cases + covering the rest"), Fable's decision table with Opus's three "use something else" + rows, one named example with its printed summary, install line. +2. **Tutorial** (`man/tutorial.md`): Fable's levels 0–4 in order, each + adding one concept, ending with `coverage` and `report`. +3. **How-to guides**, one page each, by job: test a function with many + options; test generic code across types (the Julia-specific pitch); + plan a CI matrix with `github_matrix`; run a simulation campaign; + audit and extend an existing suite (`coverage`, `must_include`); + diagnose a failure; test invalid inputs (a second space *and* + `Invalid`, side by side); combine with property-based testing + (`Partition`, and the no-API pattern); commit a design as data (the + lockfile pattern using `repr(collect(cases))` plus one `coverage` test). +4. **Explanation**: choosing values and oracles, moved from the paper's + "How to Use"; interaction coverage and what the evidence shows, + including the random-at-equal-budget formula; the constraint + semantics in plain words; engines page corrected (GND is shorter only + at high arity; deterministic vs seeded); IPOG page kept. +5. **Reference**: autodocs, with every exported docstring beginning + with the situation it serves ("Use when..."). Deprecated names + documented in a migration table (0.4 → 1.0), including the deleted + `disallow` and how to rewrite it as a rule. +6. **Developer pages**: the contract from Phase 1, non-goals, contributing. +7. **Agent one-pager** (`docs/src/man/agents.md`, also linked from the + README): the decision rule, three canonical patterns, and the + one-line check (`coverage`). +8. **Doctests on.** `DocMeta.setdocmeta!` and `doctest = true` in + `docs/make.jl`; a `@testitem` runs `Documenter.doctest` in the test + suite so CI fails on a stale example. + +Acceptance gate: build rendered docs (`julia --project=docs docs/make.jl`) +and run doctests and the full suite. Review the README, migration table, +and examples for limits, negative coverage, and unresolved diagnosis. +Check that no example promises a minimum case count, minimum explanation, +or unconditional follow-up isolation. + +--- + +## Phase 8: Release 1.0 + +Steps: + +1. Full matrix green (1.10, 1.11, latest release, three OSes), Aqua, + doctests, deterministic regressions, the full random-problem gate, and + benchmark results within the Phase 3 tolerance on the documented runner. +2. `CHANGELOG.md` for 1.0.0: breaking changes (return type, `disallow` + removed, `Counter` removed), deprecations with their replacements, new + API, the constraint fix with the issue number. +3. Set `version = "1.0.0"`; prepare PR `release/1.0` → `main` with the + validation results and rendered documentation. Keep it unmerged for review. +4. Draft a short Discourse announcement (the promise sentence, the + decision table, the constraint fix) for you to post or not. +5. **Pre-publication acceptance gate.** Review the changelog, release PR, + final CI results, benchmark report, and announcement draft. The user's + Phase 8 review must approve release before merge or registration. +6. **After approval:** merge the release PR, register with + `@JuliaRegistrator register` on the merge commit, and verify that TagBot + tags and docs deploy to `stable`. Report the released version and URLs; + the Discourse announcement remains a draft for you to post or not. + +Completion check: registration, tag, and stable docs correspond to the +approved release commit. Surface any external automation failure explicitly. + +--- + +## Order of work inside a phase + +Execute each phase through its acceptance gate, then present its concrete +artifacts for the user's review before beginning the next phase. Session +count is an estimate, not a completion criterion. For behavior changes, +write or update focused `@testitem`s and run them as each step lands. Run +the full suite (`julia --project -e 'using Pkg; Pkg.test()'`) at the phase +gate, and earlier when integration changes warrant it; avoid a full-suite +run after every small edit. Commit coherent changes with their checks. +Each review includes the phase PR, commands and results, example outputs, +and any explicitly pending tests assigned to a later phase. No tests remain +pending at release. If implementation conflicts with the contract, retain +the contract and raise the concrete conflict at review; do not silently +change the semantics to make a test pass. diff --git a/design/benchmark_procedure.md b/design/benchmark_procedure.md new file mode 100644 index 0000000..c301270 --- /dev/null +++ b/design/benchmark_procedure.md @@ -0,0 +1,55 @@ +# Benchmark fixtures and procedure for Phase 3 + +Recorded at the Phase 1 gate (2026-09-26) so that Phase 3 measures the +engines the same way before and after the constraint fix. + +## Fixtures + +1. **Unconstrained, high strength.** 15 parameters, each with 4 values, + strength 4. Both engines. This is the case where IPOG's cost is + dominated by the coverage matrix and GND's by candidate scoring. +2. **Constrained, pairwise and three-way.** Fable's 12-parameter + configuration example, checked in as `test/fixtures/fable12.jl` when + Phase 3 lands. Only its statistics survive in + `design/interface_fable.md` (line 721): 12 parameters, full factorial + 331776 of which 207360 are valid, 590 two-way interactions, 4 + uncoverable (3 forbidden directly, 1 by implication), four rules, one + of them over three parameters. Phase 3 defines a concrete fixture with + those statistics (or as close as a hand-written one gets) and records + its definition; the fixture, not the prose, is then the reference. + Both engines, strengths 2 and 3, verified by `test/checker.jl` after + each run. +3. **Repaired cases.** The random problems that crash IPOG or hang GND on + the prior revision (see issue #51). These have no "before" number; + record only the "after" so later phases can detect regressions. + +## What to record for every measurement + +- Julia version (`VERSION`), OS, CPU model, thread count + (`Threads.nthreads()`), and whether the run is a stable CI runner or a + laptop. +- Package revision (commit hash) and the engine seed (GND default is 0). +- Compilation policy: the first call is discarded as a cold start and + reported separately; timings are the median of at least 5 warm runs + using `@timed` (or BenchmarkTools if it is added as a test dependency), + with allocations and bytes. +- The case count of the returned design, so a speedup is never bought + with a larger design unnoticed. + +## Procedure + +1. Check out the prior revision (`main` at commit `d46122d`, before + `release/1.0`) and run fixtures 1 and 2 on the fixtures it handles + without crashing. Record the table. +2. Check out the Phase 3 branch and run all three fixtures. Record the + table. +3. Run the same script three times on the CI runner used for the release + and record the spread. The regression tolerance is set from that + observed variation (twice the observed spread, rounded up), not + guessed in advance. +4. Ordinary correctness tests assert completion and coverage only; no + test asserts machine-dependent seconds. Limit exhaustion and + progress-guarantee regressions go in the deterministic suite. + +The benchmark script lives at `benchmark/run.jl` when Phase 3 lands and +writes a Markdown table that is pasted into the Phase 3 review. diff --git a/design/interface_astra.md b/design/interface_astra.md new file mode 100644 index 0000000..ff435b1 --- /dev/null +++ b/design/interface_astra.md @@ -0,0 +1,471 @@ +# A proposed interface for UnitTestDesign.jl + +This is Astra's best guess at an interface, intended to be compared with other +proposals. It describes proposed behavior, not the current implementation. The +examples are interface sketches; they are not executable against today's package. + +## Purpose and scope + +The package should help someone answer: + +> I have several choices to test together. Which cases will exercise their +> interactions without running every possible configuration? + +The strongest application is a test whose configurations are meaningful and whose +execution is expensive enough to justify deliberate selection. Exhaustive loops +remain a good choice for small, cheap spaces. Randomized and property-based tests +remain useful for exploring concrete inputs. This interface should fit inside +those approaches rather than require adopting a new testing framework. + +The proposal does not assume that a larger feature set will create demand. Its +central bet is that named choices, understandable constraints, and inspectable +coverage make the existing algorithms useful with little additional work. + +The author supplies three things: + +1. Finite choices, with names that mean something in the problem domain. +2. Rules describing which choices can occur together, when needed. +3. Ordinary Julia code to construct inputs and check behavior. + +The library chooses configurations. It does not infer correct behavior, provide +a test runner, or require users to describe their assertions in another language. + +## 1. The small example should be the everyday interface + +```julia +using UnitTestDesign +using Test +using Random + +choices = ( + T = (Float32, Float64), + storage = (:dense, :view), + n = (0, 1, 17), + pattern = (:zeros, :alternating, :random), +) + +cases = all_pairs(choices) + +@testset "my_sum $case" for case in cases + rng = Xoshiro(1234) + A = make_input(case; rng) # A fresh input for this configuration. + @test my_sum(A) ≈ reference_sum(A) +end +``` + +Here `make_input`, `my_sum`, and `reference_sum` belong to the user's tests. The +example assumes an appropriate reference and comparison for those inputs. + +Each case is a `NamedTuple` with the same names and order as `choices`: + +```julia +(T = Float32, storage = :view, n = 0, pattern = :zeros) +``` + +Names describe test dimensions, which need not correspond one-to-one with +function arguments. A case can describe how to construct an array, an entire +simulation, or a file. An author can pass `case...` as keywords when those names +do match a function, but that is not required. + +Keep the familiar generation functions: + +```julia +all_values(choices) +all_pairs(choices) +all_triples(choices) +all_tuples(choices; n_way = 4) +full_factorial(choices) +``` + +`all_pairs` means that every feasible pair of factor values appears together in +at least one returned case. It does not mean every pair of complete test cases, +balanced frequencies, a minimum-size suite, or coverage of every program path. + +## 2. Introduce a model object only when there is something to reuse + +Simple calls accept a named tuple directly. `TestSpace` packages the same choices +with constraints for repeated generation and inspection: + +```julia +space = TestSpace(choices) +cases = all_pairs(space) +``` + +`all_pairs(choices)` is shorthand for `all_pairs(TestSpace(choices))`. + +`TestSpace` contains choices and validity rules. Coverage strength, mandatory +cases, and algorithm selection belong to the generation request. The same space +can therefore support pairwise CI tests and a stronger release suite. + +Domains are finite, nonempty, ordered collections. Singleton domains are allowed; +users should not have to remove fixed settings from a model. `all_values` supports +a one-factor model. A requested strength greater than the number of factors is a +clear validation error. Empty models and empty domains are errors in this first +interface. + +Values can be Julia types, symbols, numbers, tuples, or other Julia objects. +Selecting a value must preserve its type. Domain lookup and exact-pattern matching +require both the same concrete type and `isequal` values. Thus `Int8(1)`, `Int64(1)`, +and `1.0` can be distinct test choices. Duplicate choices under that relation are +rejected with a useful explanation. Internally, generation works with choice +indices rather than requiring values to be sortable. + +The model copies the domain containers and does not mutate caller configuration. +Selected mutable values are not automatically deep-copied. For tests that mutate +inputs, choose descriptions or factories and construct fresh fixtures per case. +Mutating a domain value after constructing the model is outside the contract. + +## 3. Constraints describe complete cases + +The user-facing rule is: + +> Describe which complete configurations are allowed. You never need to handle +> a placeholder for an input that the generator has not chosen yet. + +There are two primary forms: forbidden patterns and required relationships. + +### Forbidden patterns + +```julia +space = TestSpace( + ( + format = (:text, :binary), + compression = (:none, :gzip), + mode = (:buffered, :streaming), + seekable = (false, true), + ); + rules = [ + forbid( + (format = :text, compression = :gzip); + reason = "This text format does not support gzip", + ), + forbid( + (mode = :streaming, seekable = true); + reason = "Streaming mode does not support seeking", + ), + ], +) +``` + +Within one pattern, all listed choices must match for it to forbid a case. Factors +omitted from the pattern can take any value. Matching any forbidden pattern is +enough to reject a case. + +| Format | Compression | First rule | +| --- | --- | --- | +| `:text` | `:none` | Allows | +| `:text` | `:gzip` | Forbids | +| `:binary` | `:none` | Allows | +| `:binary` | `:gzip` | Allows | + +Patterns use exact values. A tuple-valued choice is still one value, not shorthand +for alternatives. More complicated conditions use a predicate. The pattern is a +positional named tuple so that metadata such as `reason` cannot collide with a +factor named `reason`. + +### Required relationships + +```julia +square = require((:rows, :columns); reason = "Matrix must be square") do rows, columns + rows == columns +end + +space = TestSpace( + (rows = (0, 1, 8), columns = (0, 1, 8), T = (Float32, Float64)); + rules = [square], +) +``` + +The callback receives concrete values in the order of the named factors. All +required relationships must return `true` for an allowed case. Julia's do-block +syntax puts the predicate first in the underlying function call: + +```julia +require(predicate, (:rows, :columns); reason = "Matrix must be square") +``` + +Predicates must be deterministic and return `Bool`. They can be evaluated more +than once, in an unspecified order. They should not mutate values or depend on +changing external state. A thrown exception is reported as a rule-evaluation +error, with its original cause and argument values; it does not silently mean +"forbidden." + +An escape hatch accepts an ordinary complete-case predicate: + +```julia +rule = require(; reason = "Configuration fits the memory budget") do case + estimated_bytes(case) <= memory_budget +end +``` + +The estimate and budget are user code. This form receives a complete named tuple. +Small scopes are preferable when natural because they permit earlier pruning and +more useful explanations. They are not a prerequisite for correctness. + +There is no public partial-value sentinel. `nothing` and `missing` may themselves +be real domain values; their interpretation belongs to the user's rule. A +predicate returning `missing` instead of `Bool` is an error. + +### What constraints mean for coverage + +A combination requires coverage if and only if it occurs in at least one complete +case satisfying every rule. + +For example, suppose `A`, `B`, and `C` each have choices `(1, 2)`, with rules +`A == B` and `B == C`. The only complete cases are `(1, 1, 1)` and `(2, 2, 2)`. +The pair `A = 1, C = 2` is infeasible and does not need coverage. The user does not +need to supply the implied relationship `A == C`. + +The implementation must reason about valid completions. Merely finding no +immediate violation in a partial case is not sufficient. Likewise, generating +unconstrained cases and deleting forbidden rows cannot establish constrained +coverage: removing a row can erase the only occurrence of another feasible pair. + +This semantic promise is stronger than the current constraint implementation. +It is an implementation requirement, not something a nicer callback fixes by +itself. Backtracking over finite domains is a possible starting point; a solver +could support larger models or a restricted declarative rule language later. +Arbitrary Julia predicates are not promised automatic translation into a solver. + +If a search limit prevents establishing feasibility or completing a suite, the +operation reports that limit. It must not label an unresolved combination +infeasible or return a successful result claiming full coverage. + +### Invalid inputs can still be valuable tests + +Rules define the population for this particular test. They do not describe every +input the application might receive. + +For a numerical-correctness test, exclude configurations the operation does not +support. For a rejection test, deliberately generate those configurations and +assert the documented exception. The documentation should show both uses so that +"forbidden" does not accidentally become "never test this error path." + +## 4. Let users inspect rules without generating a suite + +```julia +isallowed(space, complete_case) +explain(space, complete_case) +explain(space, (format = :text, compression = :gzip)) +``` + +`isallowed` accepts only complete cases. `explain` also accepts partial assignments +and checks whether a valid completion exists. It returns a structured explanation +whose display is readable in the REPL. + +Illustrative output: + +```text +No valid completion for: + format = :text + compression = :gzip + +Conflicts with: + This text format does not support gzip +``` + +The result distinguishes `:allowed`, `:forbidden`, `:completable`, +`:infeasible`, and `:unknown`. A completable partial assignment includes one +witness completion. `:unknown` means a search limit prevented a conclusion. + +A directly violated rule can be named immediately. An explanation involving +several interacting rules may be less specific; the first implementation need +not find a smallest conflicting subset. It must not invent a causal explanation. +Unknown names and out-of-domain values are input errors, not constraint failures. + +Model construction checks names, domains, and rule structure. It does not imply a +potentially expensive proof that the whole model is satisfiable. Generating from +an unsatisfiable model produces a model-level error, not an empty successful suite +or an indexing error. + +## 5. Make generated cases ordinary to consume and coverage explicit to inspect + +Named generation returns a `TestCases` collection supporting `length`, iteration, +and indexing. Its elements are named tuples. The collection retains its model +and generation request, including algorithm information, so users can inspect +what it was intended to cover. It does not track test execution. + +```julia +cases = all_pairs(space) +cases[1] +collect(cases) # Ordinary vector of named tuples. +report = coverage(cases) +``` + +`coverage(cases)` independently measures the returned rows against the stored +request. For cases from other sources, the request is explicit: + +```julia +report = coverage(space, existing_cases; n_way = 2) +``` + +A report includes: + +- Requested interaction strength and any stronger groups. +- Number of rows examined and any invalid rows. +- Feasible interactions required, covered, and uncovered, when established. +- A bounded sample of uncovered interactions, expressed using factor names. +- Whether checking completed, and whether full coverage was established. + +An invalid row contributes no coverage. A valid model with incomplete rows is not +reported complete merely because those rows happened to cover some projections. +If checking reaches a resource limit, the report exposes unresolved work and does +not manufacture an exact percentage. Explicit coverage verification can itself +be expensive; printing a collection must not silently perform it. + +This operation measures the supplied configurations, not passed assertions, code +coverage, or proven correctness. A user who skips cases during execution must +pass the cases actually exercised to measure executed configuration coverage. + +The small default display should show row count, factor names, and requested +strength. It should not print a wall of internal matrices or unverified metrics. + +## 6. Build on tests the user already values + +```julia +regressions = [ + (T = Float32, storage = :view, n = 0, pattern = :zeros), +] + +cases = all_pairs(choices; seeds = regressions) +``` + +Seeds are complete mandatory cases. They are checked against the domains and +rules before generation and retained in supplied order, followed by additional +cases. Invalid seeds produce an explanation; they are never silently discarded. +Duplicate seed configurations are retained, preserving the user's requested +list, but count only once toward distinct interaction coverage. Repetition for +statistical testing is ordinarily better expressed in the execution loop. + +This supports a useful workflow without a separate suite-management subsystem: + +1. Record the configurations of existing tests. +2. Inspect their coverage. +3. Supply them as seeds to generate a completed suite. + +Completion here means achieving the requested interaction coverage. It does not +promise the mathematically smallest possible number of additions. + +## 7. Expose stronger coverage without positional bookkeeping + +```julia +cases = all_pairs( + choices; + extra = [strength(3, (:T, :storage, :n))], +) +``` + +This requests pairwise coverage globally and three-way coverage within the named +group. `strength(3, (:a, :b, :c, :d))` would cover every feasible triple within +that four-factor group. Group requirements are combined by union; overlapping +requirements do not multiply obligations. + +Names must exist, names within a group must be distinct, and a group's strength +cannot exceed its size. The base and extra requirements must be represented in +the coverage report. Generating cases must not modify the supplied groups. + +Keep algorithm selection optional and retain the existing engine types: + +```julia +cases = all_pairs(space; engine = IPOG()) +cases = all_pairs(space; engine = GND(rng = Xoshiro(1234))) +``` + +The default should be deterministic for a fixed model and implementation version. +Exact ordering across future versions is not a promise. Explicitly saved cases +are stronger regression records than a generator seed alone. + +An engine that cannot satisfy the requested semantics must report that limitation +or use a documented correct fallback. Choosing a different engine must not weaken +the meaning of "all feasible pairs." + +## 8. Keep excursions visibly distinct + +```julia +cases = pairs_excursion(space) +``` + +This explores configurations differing from the baseline in at most two factors. +The baseline is the first value in each domain, matching the existing convention. +Filtering those excursions by the model's rules does not promise global pairwise +coverage. An invalid baseline should be reported clearly. + +For named models, use the explicit excursion functions. Do not encourage +`all_pairs(space; engine = Excursion())`, because that spelling suggests the same +coverage contract as the covering generators. Existing positional behavior can +remain for compatibility while its different semantics are documented. + +## 9. Fit randomized and property-based tests through ordinary composition + +A case can choose a category such as `:near_zero` or `:ill_conditioned`. User code +generates concrete values within that category and runs a property or reference +comparison. More repetitions explore additional examples under the same +configuration coverage. + +This first interface needs no property-testing dependency or custom assertion +macro. It also makes no promise to shrink concrete failures. If a property-testing +integration proves useful, it should reuse the named configurations and coverage +semantics rather than introduce a second model language. + +Human users and AI assistants receive the same interface: normal Julia data, +structured errors, named uncovered interactions, and ordinary test code. No +AI-specific API is required. Factor selection and assertions remain explicit +assumptions that can be reviewed. + +## 10. Compatibility and a deliberately limited first implementation + +Retain existing positional calls such as: + +```julia +all_pairs([1, 2, 3], ["low", "high"], [false, true]) +``` + +They retain their existing return convention. Dispatch on `NamedTuple` or +`TestSpace` selects the new named interface. The old `disallow` callback is a +legacy interface with partial-input semantics; it must not silently acquire new +meaning. The proposed `rules` interface is separate and explicitly documented. +Similarly, named groups replace positional `wayness` only in the new interface. + +The initial surface is: + +| Operation | Purpose | +| --- | --- | +| `TestSpace` | Reusable finite choices and validity rules | +| `forbid`, `require` | Exact forbidden patterns and ordinary Julia relationships | +| Existing generation names | Select cases at a requested strength | +| `strength` | Describe an additional coverage group by name | +| `isallowed`, `explain` | Inspect validity and possible completions | +| `coverage` | Measure coverage of generated or existing configurations | + +The first implementation should omit a custom runner, macro language, automatic +model inference, CLI, general serialization framework, distributed scheduling, +automatic shrinking, and a large catalog of domain-specific fixtures. These may +be useful later, but none is required to test the central interface hypothesis. + +## Questions for competing proposals + +These are the decisions on which an alternative could reasonably do better: + +1. Is a named overload of `all_pairs` easier to discover than a new entry point + such as `covering_cases`? The proposal favors familiarity over a fresh naming + scheme. +2. Are `forbid` patterns and scoped `require` predicates worth two concepts, or + would one complete-case predicate be enough? The proposal favors readable + common exclusions while keeping arbitrary Julia available. +3. Is `TestCases` with retained metadata worth more complexity than returning a + plain vector? The proposal favors inspectability, with `collect` as the exit. +4. Can correct constrained generation be acceptably fast with ordinary Julia + predicates? If not, should the package limit supported constraints, introduce + a declarative subset, or make the cost of opaque predicates more visible? +5. Is coverage inspection and completion useful enough to justify building a + model? It still requires the user to identify factors; the proposal does not + remove that cost. +6. Does this interface improve a real test suite enough that its author would + keep it after comparing exhaustive loops and random selection under the same + budget? + +The strongest first demonstration would use an existing expensive Julia test: +express its choices and rules, measure existing coverage, complete the missing +interactions, and run the existing assertions. The evaluation should include +authoring effort, execution cost, and fault detection. A smaller row count alone +would not establish that the interface is worth adopting. diff --git a/design/interface_astra.pdf b/design/interface_astra.pdf new file mode 100644 index 0000000000000000000000000000000000000000..90fd053a42b5e0c34d3f31f1054aa46386876de8 GIT binary patch literal 77588 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 z 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 ...`. The library evaluates each constraint once + over the product of the parameters it mentions, turns it into a table of + forbidden combinations, and never calls user code during search. Interactions + that the constraints make impossible are detected, dropped from the coverage + goal, and reported instead of crashing IPOG or hanging GND (both measured + today). + +2. **Results become typed tuples or named tuples inside a `TestCases` vector + that prints its own summary and is a table.** `f(case...)`, `f(; case...)`, + `(; mode, solver) = case`, `DataFrame(cases)`, and `@testset "$case" for case + in cases` all work without glue code, and the REPL shows "10 cases, pairwise, + of 81 combinations" so the tradeoff is visible. + +3. **The entry point leads with the decision, not the algorithm.** The README + and docs answer "when do I reach for this instead of property-based testing, + random testing, or `Iterators.product`?" in the first screen, and two new + functions back the answer with numbers: `design_sizes(space)` shows what each + strategy would cost, and `missing_interactions(existing_cases, space)` tells + a user what their hand-written tests already miss, so they can *extend* a + suite instead of replacing it. + +Smaller changes: `strength` and `stronger` replace `n_way` and the index-based +`wayness` dictionary; seeds may be partial; excursions take an explicit base +case; single-valued parameters are allowed; `Counter`, `generate_tuples`, and +the `Excursion` engine leave the public surface. + +## 2. What exists today + +### 2.1 Public surface + +| Exported | Role | +|---|---| +| `all_values`, `all_pairs`, `all_triples`, `all_tuples(...; n_way)` | covering designs at strength 1, 2, 3, or t | +| `values_excursion`, `pairs_excursion`, `triples_excursion` | one-, two-, three-parameter walks away from the first value of every parameter | +| `full_factorial` | every combination, optionally filtered | +| `IPOG`, `GND`, `Excursion` | engine tags passed as `engine =` | +| `generate_tuples` | the internal dispatch point, exported by accident | + +Every design function shares keywords `engine`, `disallow`, `seeds`, `wayness`, +`Counter` (`src/factorial_interface.jl:209`). + +### 2.2 How a call flows + +`all_tuples` validates the arguments and calls `generate_tuples(engine, ...)`. +That function translates values into 1-based integer indices, so the engines +only ever see an `arity` vector such as `[3, 3, 3, 3]`. Three translations +happen at that boundary: + +- `wrap_disallow` (`factorial_interface.jl:81`) wraps the user's predicate so an + integer vector becomes values, mapping index 0 (undecided) to `nothing`. +- `seeds_to_integers` looks up each seed value with `indexin`, so every seed + must be complete and every value must be found. +- The result is converted back with `p[c]`, which is where index 0 turns into + the `BoundsError` users see when a constraint defeats the engine. + +The engines share one data structure, `MatrixCoverage` (`coverage_matrix.jl`): +a matrix whose columns are the interactions still to cover, with 0 for +"don't care", and a `remain` counter that partitions covered from uncovered +columns. IPOG (`parameter_order.jl`) extends a design one parameter at a time; +GND (`greedy_tuples.jl`) builds one case at a time from `M` random candidates; +excursions and full factorial enumerate and filter. Mixed strength runs IPOG +once per requested subset, feeding earlier results in as seeds +(`ipog_multi_way`). Constraints are handled by asking the user's predicate at +every choice point whether a partially built case is still allowed, and by +deleting directly forbidden columns from the coverage matrix +(`remove_combinations!`). + +### 2.3 What the documentation tells users + +The docs are honest about the constraint problem +(`docs/src/man/guide.md`, "Exclude forbidden combinations"): the predicate must +tolerate `nothing`, and "the generator *may fail to find a solution* ... you +will see the code try to access a vector at location 0." The JuliaCon paper's +"Statement of Need" and "Comparison of Approaches" sections contain the best +"when to use" writing in the project, but none of it reaches the README or the +docs landing page, which open with the algorithm names. + +## 3. Where it hurts (measured) + +| Probe | Today | +|---|---| +| `all_pairs([1,2,3], ["low","mid","high"], [1.0,3.7,4.9], [:greedy,:relax,:optim])` | returns `Vector{Vector{Any}}`, 10 cases | +| Same, `disallow = (a, b, c) -> a > 2 && b > 2.0` | `MethodError: no method matching isless(::Float64, ::Nothing)` | +| Count calls to a 4-parameter `disallow` during `all_pairs` | 74 calls, 64 of them with at least one `nothing` argument | +| Two constraints whose *combination* makes one pair impossible (Sec. 3.1), IPOG | `BoundsError: attempt to access 2-element Vector{Symbol} at index [0]` | +| Same, GND | never returns (killed after 90 s) | +| Same, `pairs_excursion` | returns 3 cases; the value `:lu` never appears and nothing says so | +| `seeds = [[1, "mid", nothing]]` (partial seed) | `MethodError: Cannot convert an object of type Nothing to Int64` | +| `seeds = [[1, "medium", 3.7]]` (typo in a seed value) | the same `convert` error, no mention of which value or parameter | +| `wayness = Dict(3 => [[3:6], [25:30]])`, copied from `guide.md` | `MethodError: Cannot convert Int64 to UnitRange{Int64}` | +| `wayness = Dict(3 => [3:6, 25:30])` | also a `MethodError`; only `[collect(3:6), collect(25:30)]` works | +| Pass `w = Dict(3 => [[1,3,4]])` as `wayness` | `w` is mutated: afterwards `Dict(2 => [[1,2,3,4]], 3 => [[1,3,4]])` | +| A parameter with one value | `DomainError: Each argument should be a list of parameter values` | +| `n_way = 3` with two parameters | `BoundsError ... at index [1:3]` | + +### 3.1 The constraint problem, precisely + +The running example for this document is a solver configuration: + +```julia +mode = [:fast, :exact] +solver = [:none, :lu, :qr] # :lu and :qr only make sense for :exact +tol = [1e-3, 1e-6] # :exact refuses the loose tolerance +``` + +The two rules are "fast implies no solver" and "exact implies tight tolerance". +Neither rule mentions `(solver, tol)`, but together they make the pair +`(solver = :lu, tol = 1e-3)` impossible: the only mode that allows `:lu` is +`:exact`, and `:exact` forbids `1e-3`. Today's engines remove the *directly* +forbidden pairs from the goal, then try to cover `(:lu, 1e-3)` anyway. IPOG +leaves a 0 it cannot fill and crashes on the way out; GND loops forever looking +for a candidate that scores. Five distinct usability defects sit on top of that +correctness defect: + +1. The predicate takes positional arguments. With 12 parameters, the user + writes `(_, _, m, _, _, s, _...) -> ...` and gets it wrong silently. +2. The predicate receives `nothing` for undecided parameters, so every + comparison other than `==` must be guarded. The docs explain this; users + still hit it first. +3. The engine can only ask "is this partial case still allowed?", which is + a weaker question than "can this partial case still be completed?", and the + gap is exactly the implicit-infeasibility case above. +4. The polarity is inverted from how people state rules ("exact *requires* + tight tolerance"), so users write the negation by hand. +5. Nothing reports what was excluded. In the excursion engine, coverage is + silently lost. + +The literature name for the fix is *forbidden tuples* (Yu, Lei, Kacker, Kuhn, +"Constraint handling in combinatorial test generation using forbidden tuples", +ICSTW 2015; it is how NIST's ACTS handles constraints). The interface below is +designed so that the library owns the constraint as data, which is what makes +the fix possible. + +## 4. Design principles + +- **One concept per level.** A one-line call stays one line. Naming + parameters is one added concept and unlocks everything else. Constraints, + seeds, strength, and inspection each add exactly one more. +- **The library owns constraints as data, not as opaque callables.** It can + then evaluate them safely, detect infeasibility, print them in error messages, + serialize them, and hand them to a solver later. +- **User code is never called with partial information.** No `nothing`, no + `missing`, no index 0. +- **Nothing is dropped silently.** Infeasible interactions, rejected seeds, + and unused values are reported. +- **The result is a first-class Julia value.** Typed, iterable, a table, and + self-describing at the REPL. +- **Guarantees are stated in the docstrings**: every returned case satisfies + every constraint; every feasible t-way interaction is covered; seeds appear + first, in order; IPOG is deterministic. + +## 5. The proposed interface, by level + +### Level 0: one line, positional (unchanged shape, better result) + +```julia +using UnitTestDesign + +cases = all_pairs([1, 2, 3], ["low", "mid", "high"], [1.0, 3.7, 4.9]) +for (n, level, tol) in cases + @test integrate(n, level, tol) ≈ reference(n, level, tol) +end +``` + +`cases` is a `TestCases{Tuple{Int64, String, Float64}}`, an `AbstractVector`. +`case[1]` still works, so existing loops keep working, but the element type is +concrete and `f(case...)` is type-stable. Single-valued parameters are allowed +(a fixed argument is just an argument with one representative). The same +positional form exists for `all_values`, `all_triples`, `covering`, +`full_factorial`, and `excursions`. + +### Level 1: named parameters + +Naming is the one concept that unlocks constraints, partial seeds, subset +strength, tables, and readable test names. Parameters are `name => values` +pairs, in the order the function under test takes them: + +```julia +cases = all_pairs(:n => [1, 2, 3], :level => ["low", "mid", "high"], :tol => [1.0, 3.7, 4.9]) + +@testset "integrate $(case)" for case in cases + (; n, level, tol) = case # property destructuring + @test integrate(n, level, tol) ≈ reference(; case...) # or keyword splat +end +``` + +`cases` is now a `TestCases{@NamedTuple{n::Int64, level::String, tol::Float64}}`. +A `NamedTuple` literal `(n = [1, 2, 3], level = [...])` is accepted as a single +positional argument as well, since it is the natural Julia spelling. + +When the same parameters feed several designs, or need to be inspected, they +live in a `ParameterSpace`: + +```julia +space = ParameterSpace( + :mode => [:fast, :exact], + :solver => [:none, :lu, :qr], + :tol => [1e-3, 1e-6]; + constraints = [ + @forbid(mode == :fast && solver != :none), + @require(mode == :fast || tol < 1e-4), + ], +) +quick = all_pairs(space) # CI +nightly = all_triples(space) # scheduled +``` + +Every design function accepts either a `ParameterSpace` or the pairs directly, +with `constraints =` available in both places. + +### Level 2: constraints + +Three spellings, from most to least common: + +```julia +# 1. A literal forbidden combination. No logic, no macro. +forbid(mode = :fast, solver = :lu) + +# 2. An expression over parameter names. Bare identifiers are parameter names; +# `$x` pulls a variable in from the surrounding scope. +@forbid mode == :fast && solver != :none +@require mode == :fast || tol < $threshold +@forbid solver in (:lu, :qr) && !isfinite(tol) + +# 3. A function, when the logic is too big for an expression. Names are explicit. +forbid(:mode, :solver) do mode, solver + mode == :fast && solver != :none +end +``` + +`@require e` is exactly `@forbid !(e)`; having both verbs keeps the polarity +visible in each rule. All three construct the same `Constraint` value, which +remembers its scope (the parameter names it mentions), its predicate, and its +source text for printing. + +**Semantics.** For each constraint, the library evaluates the predicate once +for every combination of values of the parameters in its scope (the product of +those domains, which is 6 evaluations for `(mode, solver)` above) and records +the combinations that are forbidden. From then on: + +- A complete case is valid iff none of its projections onto a scope is a + forbidden combination. +- A partial case is *rejected* if it already contains a forbidden combination + and *dead* if no valid completion exists; deadness is decided by a + backtracking completion search over the remaining parameters, not by asking + the user. +- The coverage goal is every strength-t interaction that occurs in at least + one valid complete case. Directly forbidden interactions are excluded + trivially; implicitly infeasible ones are found by the completion search and + listed in the result's report. + +The user's predicate is never called during search and never with anything but +values drawn from the declared domains, so `tol < 1e-4` needs no guard. The +cost is the product of the scope's domains; a constraint mentioning ten +four-valued parameters is about a million evaluations, so the library warns +above a threshold and suggests splitting the rule. Real constraints mention +two to four parameters. + +**Errors at construction, not deep in the engine:** + +``` +ArgumentError: @forbid(mode == :fast && solvr != :none) mentions `solvr`, +which is not a parameter. Parameters are (:mode, :solver, :tol). +If `solvr` is a variable, write `$solvr`. +``` + +**Dependent parameters** are the most common real constraint ("`solver` only +applies when `mode == :exact`"). The recipe is a sentinel value plus one +`@require`, which the design handles without special support: + +```julia +:solver => [:none, :lu, :qr] +@require mode == :exact || solver == :none +``` + +**Reporting.** Running the running example gives (prototype output, Appendix B): + +``` +5 test cases · pairwise · 3 parameters · full factorial would be 12 (5 valid) + mode solver tol + :fast :none 0.001 + :exact :none 1.0e-6 + :exact :lu 1.0e-6 + :exact :qr 1.0e-6 + :fast :none 1.0e-6 +Constraints forbid 3 pairs directly and make 2 more unreachable: (solver = :lu, tol = 0.001), (solver = :qr, tol = 0.001). +``` + +The last line is the one that today is a `BoundsError`. It is also useful in +its own right: a user who did not intend `(solver = :lu, tol = 1e-3)` to be +untestable finds out here. + +### Level 3: seeds, strength, engines, and excursions + +**Seeds** are cases that must appear. They are named tuples, may be partial, +and appear first in the output in the order given: + +```julia +all_pairs(space; seeds = [ + (mode = :exact, solver = :lu, tol = 1e-6), # the production configuration + (mode = :fast,), # partial: the engine fills the rest +]) +``` + +A seed that violates a constraint or names a value outside its domain is an +error that names the seed, the parameter, and the offending value. An existing +`TestCases` value is accepted as `seeds`, which is how a suite is extended +(Sec. 6.3). + +**Strength** replaces `n_way`, and `stronger` replaces `wayness`: + +```julia +covering(space; strength = 2) # == all_pairs(space) +covering(space; strength = 2, stronger = [(:mode, :solver, :tol) => 3]) +``` + +Subsets are named, not indexed, so the guide example +`Dict(3 => [[3:6], [25:30]])` becomes `stronger = [(:c, :d, :e, :f) => 3, +(:y, :z) => 3]` and cannot be mistyped into a `convert` error. `strength` +greater than the number of parameters is an `ArgumentError`; `strength` equal +to it is a full factorial. `all_values`, `all_pairs`, and `all_triples` remain +as the names people search for. `all_tuples` remains as a deprecated alias of +`covering`. + +**Engines** keep their literature names but hide their knobs behind readable +ones: + +```julia +covering(space; engine = IPOG()) # default, deterministic +covering(space; engine = GND(rng = Xoshiro(1), candidates = 50)) # `candidates` was `M` +``` + +**Excursions** get an explicit base case. Today the base is silently "the first +value of every parameter", which is the single most surprising fact about +`values_excursion`: + +```julia +excursions(space; from = (mode = :exact, solver = :lu, tol = 1e-6), distance = 1) +excursions(space; distance = 2) # base defaults to first values, and the docstring says so +``` + +`distance` is how many parameters may differ from the base at once, so +`values_excursion`, `pairs_excursion`, and `triples_excursion` become aliases +for distances 1, 2, and 3 and stay exported. Because excursions are filtered +by the same constraint machinery, the report says which values never appear. + +**Full factorial** takes the same space and constraints and reports the count +before and after constraints. + +### Level 4: inspection + +```julia +design_sizes(:n => [1, 2, 3], :level => ["low", "mid", "high"], :tol => [1.0, 3.7, 4.9], :kind => [:greedy, :relax, :optim]) +# strategy cases (measured with today's engines) +# full_factorial 81 (with constraints: total and valid counts) +# all_values 3 +# all_pairs 10 +# all_triples 31 +# excursions(1) 9 +# excursions(2) 33 + +coverage(cases, space; strength = 2) # (covered = 11, feasible = 11, missing = []) +missing_interactions(handwritten, space) # [(solver = :qr, tol = 1.0e-6), ...] +report(cases) # the summary the REPL prints, plus the unreachable list +``` + +`design_sizes` is the "should I bother?" function; it answers the user's +question before they commit. `missing_interactions` is the on-ramp for a +project with an existing suite: convert the existing calls to named tuples, +ask what pairs they miss, and pass them as `seeds` to get the minimal +extension. + +### Reference: proposed signatures + +```julia +ParameterSpace(pairs::Pair{Symbol}...; constraints = Constraint[]) +ParameterSpace(nt::NamedTuple; constraints = Constraint[]) + +covering(space; strength = 2, stronger = Pair[], constraints = [], seeds = [], engine = IPOG()) +all_values(space; kw...) # strength = 1 +all_pairs(space; kw...) # strength = 2 +all_triples(space; kw...) # strength = 3 +excursions(space; from = nothing, distance = 1, constraints = [], seeds = []) +full_factorial(space; constraints = []) +# every function above also accepts `pairs::Pair{Symbol}...` or `values::AbstractVector...` in place of `space` + +forbid(; name = value, ...) -> Constraint +forbid(names::Symbol...) do values... end -> Constraint +require(names::Symbol...) do values... end -> Constraint +@forbid expr; @require expr -> Constraint + +design_sizes(space; strengths = 1:3, distances = 1:2) +coverage(cases, space; strength = 2) +missing_interactions(cases, space; strength = 2) +report(cases) + +struct TestCases{T} <: AbstractVector{T} # T is a Tuple or NamedTuple type +IPOG(); GND(; rng = Random.default_rng(), candidates = 50) +``` + +Removed from the public surface: `generate_tuples`, `Excursion`, `Counter`, +`n_way`, `wayness`, `disallow` (kept for one minor version as a deprecated +keyword that wraps the function form with `nothing`-safe semantics, see Sec. 9). + +## 6. The result type + +### 6.1 `TestCases` + +```julia +struct TestCases{T} <: AbstractVector{T} + cases::Vector{T} + space::ParameterSpace + strategy::Symbol # :covering, :excursion, :full_factorial + strength::Int # or distance, for excursions + seeded::Int # how many leading cases came from seeds + infeasible::Vector{NamedTuple} # interactions dropped from the goal +end +``` + +It is a vector, so everything that works on a vector works on it. Because it +is an `AbstractVector` of `NamedTuple`s it is already a Tables.jl row table, +so `DataFrame(cases)` and `CSV.write("cases.csv", cases)` work with no +dependency added here. `show` prints the one-line summary and an aligned +table, truncated like a `DataFrame`. The metadata is what `report`, +`coverage`, and the deprecation shims read. + +### 6.2 REPL output as documentation + +``` +julia> all_pairs(:n => [1, 2, 3], :level => ["low", "mid", "high"], :tol => [1.0, 3.7, 4.9], :kind => [:greedy, :relax, :optim]) +10 test cases · pairwise · 4 parameters · full factorial would be 81 + n level tol kind + 1 "low" 1.0 :greedy + 2 "mid" 3.7 :relax + ⋮ +``` + +"10 of 81" is the whole pitch of the package, and it should be the first thing +a user sees. + +### 6.3 Extending an existing suite + +```julia +existing = [(mode = :fast, solver = :none, tol = 1e-3), (mode = :exact, solver = :lu, tol = 1e-6)] +missing_interactions(existing, space) # what the hand-written tests miss +all_pairs(space; seeds = existing) # the smallest set that keeps them and fixes the gaps +``` + +## 7. Positioning: telling people when to use it + +### 7.1 The decision in one table + +This belongs at the top of the README and the docs landing page, before any +function name. + +| Your situation | Reach for | +|---|---| +| You can list a few representative values per argument, the function has several arguments, and you suspect bugs live in *combinations* of arguments (an `if` on one option inside a branch on another). | A covering design: `all_pairs`, `all_triples`. | +| Same, and each run is cheap and the product is small. | `full_factorial` or `Iterators.product`. | +| You have one known-good configuration and want to know which single (or paired) changes break it. | `excursions`. | +| You can *generate* values but not enumerate representatives (strings, trees, arbitrary floats), runs are cheap, and you want failing inputs shrunk. | Property-based testing (Supposition.jl, PropCheck.jl). | +| You already have hand-written cases and want to know what interactions they miss. | `missing_interactions`, then `seeds`. | +| Both apply. | Use covering designs to pick *which equivalence class* each argument draws from, and a generator to draw within it. | + +### 7.2 The one-sentence explanation + +Anchor to something users already know: `Iterators.product(a, b, c)` is every +combination; `all_pairs(a, b, c)` is the smallest subset of it in which every +pair of values still occurs together. Random sampling gets there too, but +slowly and without a guarantee (measured, uniform random cases, 200 trials): + +| parameters × values | designed pairwise cases | random cases needed to cover every pair, median [10%, 90%] | pairs covered by that many random cases | +|---|---|---|---| +| 5 × 4 | 16 | 84 [65, 116] | 64% | +| 10 × 4 | 28 | 106 [87, 131] | 84% | +| 10 × 8 | 113 | 530 [461, 633] | 83% | +| 40 × 4 | 45 | 151 [133, 183] | 95% | +| 40 × 8 | 166 | 709 [632, 845] | 93% | + +The honest framing: random testing spends four to six times as many runs to +reach the same pairwise guarantee, and property-based testing spends them to +buy something different (generation and shrinking). Designed cases win when +runs are expensive or the argument space is large, and lose when the user +cannot name representatives. The paper already says this; the README should. + +### 7.3 Docs restructure + +1. **Home**: the table above, the one-sentence explanation, the 10-of-81 + example with its REPL output, and the PBT bridge in three lines. +2. **Tutorial** (replaces Guide): levels 0 through 4 in order, each a runnable + `@example` block, ending with "extend an existing suite". +3. **Constraints**: the three spellings, the semantics paragraph, the + dependent-parameter recipe, and what the report means. +4. **Choosing strength and strategy**: the `design_sizes` table for a real + example, when to raise strength, when excursions beat coverings. +5. **Engines and algorithms**: the existing IPOG and greedy pages, with the + forbidden-tuple and completion-search additions. +6. **Reference**. + +The paper's "How to Use" section (equivalence classes, oracles, invariants) +belongs in the tutorial, not only in the PDF. + +## 8. What the interface assumes of the engines + +The interface is only honest if the engines deliver these. Each is stated as a +contract, with what it costs to meet it. The prototype in Appendix B meets the +first three on top of the *current* IPOG code, which is the evidence that the +list is realistic rather than aspirational. + +- **E1. Constraints arrive as forbidden-combination tables, never as + callables.** A preprocessing step tabulates each `Constraint` over its scope. + The engines' existing integer-vector predicate is kept internally but is now + generated by the library and is partial-safe by construction (a table only + fires once its whole scope is assigned). No engine change. + +- **E2. Implicitly infeasible interactions are detected and dropped from the + goal.** Before generation, each target interaction is checked for a valid + completion by backtracking with forward checking over the remaining + parameters. Cost is negligible with few constraints: the 12-parameter, + four-constraint example in Appendix B takes 0.7 s end to end, most of it in + IPOG. Later optimization: derive minimal forbidden tuples once (Yu et al. + 2015) so per-interaction search is rarely needed. Prototype: done on top of + `ipog_multi` by adding the dead interactions to the predicate. + +- **E3. Engines never leave an unfillable slot.** Replace the greedy + `fill_remaining_missing_values_filter!` and `choose_last_parameter_filter!` + fallbacks with backtracking completion. With E2 in place, this only triggers + for higher-order implicit infeasibility (a partial case whose every pair is + feasible but whose triple is not), which is rare and cheap. This is what + makes "index 0" impossible by construction rather than by luck. + +- **E4. GND cannot spin.** Its candidate loop (`while trial_cnt < M`) must + cap attempts and fall back to backtracking completion; with E2 removing + unreachable goals it also terminates by the same argument as today. + +- **E5. Partial seeds and single-valued parameters.** Measured: `ipog`, + `ipog_multi`, and the greedy generator all accept an arity of 1 and reach + full coverage, and `ipog_multi` given the partial seed `[1, 0, 2]` fills the + zero and keeps the seed first. Only the translation layer + (`seeds_to_integers`, the `< 2` checks in `all_tuples`) forbids them. Small + change. + +- **E6. Coverage is reportable.** `test_coverage` and `tuples_in_trials` + already exist in `coverage_set.jl`; they need to be exposed through + `coverage` and `missing_interactions` in value space rather than index space. + +- **E7. Determinism and reproducibility as stated.** IPOG is deterministic + today. Constraint tabulation and dead-interaction detection must iterate in + a fixed order so the report is stable across runs. + +- **E8. Room to grow, without changing the interface.** Because a + `Constraint` keeps its expression, a package extension can translate the + comparison-and-boolean subset to Z3 (the sketch in `z3_example.jl`) or a SAT + solver for large scopes, and the same strings can be read from TOML for the + command-line branch (`feature/command-line`), which today has no way to + express `disallow` at all. Neither is needed for the first release. + +Suggested order: E1, E2, E5, E6 (the translation layer, no engine surgery), +then E3 and E4 (engine surgery), then E7 checks, then E8 as separate projects. + +## 9. Migration + +| Today | Proposed | Notes | +|---|---|---| +| `all_pairs(v1, v2, v3)` returning `Vector{Vector{Any}}` | same call, returns `TestCases{Tuple{...}}` | breaking only for code that mutates result rows or depends on `Vector{Vector}` | +| `all_tuples(...; n_way = k)` | `covering(...; strength = k)` | `all_tuples` and `n_way` deprecated one minor version | +| `disallow = (a, b, c) -> ...` | `constraints = [@forbid ...]` | `disallow` kept one version: wrapped as a full-scope function-form constraint, so it is evaluated only on complete cases and the `nothing` contract disappears | +| `wayness = Dict(3 => [[3, 4, 5, 6]])` | `stronger = [(:c, :d, :e, :f) => 3]` | positional call: `stronger = [(3, 4, 5, 6) => 3]` accepted | +| `seeds = [[1, "mid", 3.7, :relax]]` | `seeds = [(n = 1, level = "mid", tol = 3.7, kind = :relax)]` | positional call still accepts vectors/tuples; partial seeds new | +| `values_excursion(...)` | `excursions(...; distance = 1)` | old names stay as aliases; `from =` new | +| `engine = GND(M = 50)` | `engine = GND(candidates = 50)` | `M` deprecated | +| `Counter = Int8` | removed | choose internally from arity | +| `generate_tuples`, `Excursion` | unexported | | + +Version: this is a 1.0 candidate. The return-type change is the only break +that cannot be shimmed, and it is the change most worth making. + +## 10. Open questions for the author + +1. **Names.** `ParameterSpace`, `covering`, `stronger`, `forbid`/`require`, + `excursions(; from, distance)`. Alternatives considered: `Factors`/`levels` + (DoE vocabulary, foreign to unit testers), `design` (collides with user + variables), `constraints = [...]` versus `where = ...` (kept `constraints`, + the term every other CIT tool uses). +2. **Should the positional form support constraints at all?** This proposal + says no: constraints need names. A positional user adds names when they + add a constraint, which is one honest step up. +3. **Labels for values.** `:x => ["tiny" => 1e-9, "huge" => 1e9]` would make + test names and reports readable for float and struct values. It adds a + concept; deferred. +4. **Keep GND?** It gives the same guarantees, is slower, and is less + predictable. It sometimes finds smaller designs, which matters when each run + costs hours. Keep, but do not mention it before the "Engines" page. +5. **Generators as values.** The PBT bridge could be a helper that draws from + generator values at iteration time. Values that are functions are also + legitimate arguments to test (`[sin, cos]`), so nothing automatic; document + the pattern instead. +6. **Command line.** With constraints expressible as strings of the safe + subset, the TOML format from `feature/command-line` becomes complete. + Worth reviving after 1.0. + +## Appendix A. Probe results + +Script: `probe.jl` (interface probes), `probe_gnd.jl` (GND hang under a 90 s +alarm), `random_vs_design.jl` (Sec. 7.2 table), run with +`julia --project=.