<?xml version="1.0" encoding="UTF-8"?>
<spec xmlns="https://vibevm.org/spec/1">
  <title id="root">CARD: rule-declared-test-matrices — Test matrices are declared, never swept</title>
  <status stage="spec" state="done"/>
  <p p="1"><fact id="status-line" status="impl/done">**Discipline v0.2 · BETA · T2 · Rust (lineage home of the shared rule; the checker serves Go and TypeScript too)**</fact></p>
  <section id="band-one-identity" title="Band 1 — Identity &amp; Recognition">
    <p p="2"><fact id="CLASSIFICATION" status="impl/done">Classification: layer=E (verification) + H (weak-reader); mechanism=rule.</fact></p>
    <p p="3"><fact id="INTENT" status="impl/done">Intent: a test matrix is DECLARED as data — a table of cases iterated once — never GENERATED by a full `2^n` enumeration, because a swept matrix hides a case count that grows exponentially with the axes, so a regression slips into the exponent where a reader cannot count it by eye.</fact></p>
    <p p="4"><fact id="ALSO-KNOWN-AS" status="spec/done">Also Known As: declared-test-matrices; `declared-test-matrices`; no-combinatorial-sweep; table-driven-not-bitmask; R-060.</fact></p>
    <p p="5"><fact id="APPLICABILITY-RECOGNITION" status="impl/done">Applicability / Recognition: a test file (a `#[cfg(test)]`/`#[test]` site for rust-syn, a `_test.go` file for go-extract, a `*.test.ts`/`*.spec.ts`/`__tests__` file for ts-tsc) carries EITHER (a) a loop bound that sweeps a power of two — `for mask in 0..(1 &lt;&lt; n)` (Rust), `for mask := 0; mask &lt; 1&lt;&lt;n` / `math.Pow(2, n)` (Go), `for (let m = 0; m &lt; 1 &lt;&lt; n` / `2 ** n` / `Math.pow(2, n)` (TS) — OR (b) a Cartesian product of three-or-more nested GENERATED-axis loops — a Rust range `for i in 0..n`, a Go C-style `for i := 0; i &lt; n; i++`, a TS `for (let i = 0; i &lt; n; i++)`. *Detector seed:* in test context only, walk the loop nodes; a `1 &lt;&lt;` shift with a literal `1` left operand (or a `2 **`/`pow(2,` form) in a loop bound is a bit-mask sweep, and a nest of three-or-more GENERATED-axis loops is a Cartesian sweep. The line is WHAT THE LOOP ITERATES — a generated numeric range sweeps; a loop over a DECLARED collection (a Rust `for a in STAGES` / `[a, b]`, a Go `for _, tc := range tests`, a TS `for (const a of arr)`) is a declared axis and does NOT count, so exhausting a closed set by nesting collection loops is the compliant table shape and emits nothing.</fact></p>
  </section>
  <section id="band-two-justification" title="Band 2 — Justification &amp; Tradeoffs">
    <p p="6"><fact id="MOTIVATION" status="spec/done">Motivation: a test written `for mask in 0..(1 &lt;&lt; n) { check(mask) }` enumerates `2^n` cases the reader must derive mentally to know what is covered; add a fifth axis and the suite quietly runs 32× more (or, if the bound was wrong, fewer) with no name on the case that regressed. Declaring the cases as a table — `for case in CASES { check(case) }` — puts a name and a count the reader sees at a glance, and a regression is a row that changed. The same logic bans a 3+-deep Cartesian nest of GENERATED-axis loops (`for i in 0..n` / `for i := 0; i &lt; n` / `for (let i = 0; ...)`): its case count is a product no one multiplies out. It deliberately does NOT ban a 3+-deep nest of COLLECTION loops (`for a in STAGES`, `for _, tc := range tests`, `for (const a of arr)`) — those iterate a closed set someone wrote down, so the cases are recorded data and the product is merely expressed by the nesting, which is the good test the bare-depth heuristic used to red.</fact></p>
    <p p="7"><fact id="STRUCTURE-AND-PARTICIPANTS" status="impl/done">Structure &amp; Participants: the conform fact `Fact::TestSweep { kind, line, detail }` (emitted by all three frontends, only in test context) → the `declared-test-matrices` rule turns each into one finding. `kind` is `"bitmask"` (a `2^n` loop bound; `detail` is the bound text) or `"nested-loops"` (a ≥3-deep nest of generated-axis loops; `detail` is the depth). Fingerprints key on `(file, kind, ordinal)`, **never line** — a line-keyed baseline rots on any edit above the loop (the stop.rs lesson), and a baseline that rots on unrelated edits is a checker that lies.</fact></p>
    <p p="8"><fact id="COLLABORATIONS" status="impl/done">Collaborations: rides the same conform gate, SARIF output, and ratchet baseline as the other rules; **one engine rule serves Rust, Go, AND TypeScript** — all three drivers mount `DeclaredTestMatrices`, since every frontend emits the same `Fact::TestSweep` in test context; the detection is per-language syntax (the extractor owns it), the judgement is one rule; every finding renders through the one `req_message` renderer (Class-F grammar, R3-011).</fact></p>
    <p p="9"><fact id="GOALS-AND-NON-GOALS" status="impl/done">Goals / Non-Goals: *Goals:* red a swept test matrix — a `2^n` bit-mask loop or a ≥3-deep generated-axis (range / C-style-for) nest — in test context, so a reader can count the cases by eye. *Non-Goals:* NOT a ban on loops in tests (a single declared loop over a case table, OR any nest of collection/constant loops exhausting a closed set, is the compliant shape); NOT a coverage or property-test checker (property tests deliberately generate many inputs — they are a different, declared-by-framework strategy, not a hand-rolled `1 &lt;&lt; n`); NOT for production code (the extractor emits only in test context); NOT a measure of test VALUE, only of whether the matrix is declared or swept.</fact></p>
    <p p="10"><fact id="CONSEQUENCES" status="spec/done">Consequences: (+) a swept matrix becomes a named, reviewable finding rather than an invisible exponent; (+) the table-driven remedy is the same idiom every guide already recommends, so the fix is mechanical; (+) one rule reads three languages through one fact. (−) a genuinely combinatorial test (a truth-table over many booleans) that SHOULD enumerate all `2^n` will fire — the remedy there is a named, documented enumeration (a comment plus the rule honoured by a frozen baseline), not a weakening; (−) the rule is a vacuum on a tidy tree (see Evidence), so its value is prospective.</fact></p>
    <p p="11"><fact id="ALTERNATIVES" status="spec/done">Alternatives: no check at all — the pre-rule state: a swept matrix is invisible until a regression escapes it; ban ALL nested loops in tests (too blunt — a 2-deep nest is readable and common); a coverage-gate that demands N cases (wrong grain — it counts cases, not whether they are declared). The declared-vs-swept distinction is the load-bearing one; the others lose it.</fact></p>
    <p p="12"><fact id="RISKS-AND-ASSUMPTIONS" status="impl/done">Risks &amp; Assumptions: assumes a bit-mask loop bound (`1 &lt;&lt; n`) is generating test cases, not computing a real bitmask for the system under test (in test context this is near-always true — a test computing a bitmask would assert it, not iterate it). *Scope (intentionally OUT of the rule):* exhausting a CLOSED set by nesting collection/constant loops — `for a in Stage::ALL { for b in State::ALL { for at in Stage::ALL { … } } }`, `for mode in [Snap, Hard] { for b in [false, true] { for c in [false, true] { … } } }` — is NOT a sweep: every axis is a declared enumeration whose cases are written as data, so the count is visible and a regression is a row that changed. The predicate keys on WHAT THE LOOP ITERATES (a generated numeric range sweeps; a declared collection/array/constant does not), not on nesting depth alone — a bare-depth heuristic reds these good tests (it did, on this tree's own `progress-core` and `vibe-workspace`), so the narrowed predicate is what makes the rule fit a real codebase. *Heuristic limits (what it does NOT see):* (1) a swept matrix whose bound is a computed value, not a literal `1 &lt;&lt;`/`2 **`/`pow(2,` — e.g. `let n = 1 &lt;&lt; k; for i in 0..n` — the bound is an identifier, not a shift, so it is invisible (a documented limit, not a silent claim); (2) a Cartesian product built with `itertools::product` / a flat generated `Vec`, not nested loops — invisible, since the nest is collapsed into one iterator; (3) a 2-deep nest is below the ≥3 threshold and compliant by design; (4) a generated axis the syntax cannot reliably name — a Rust `while`/`loop` or TS `while`/`do` (indistinguishable from a data-driven `while let Some(x) = iter.next()`), or a Go 1.22 `for i := range n` integer-range (indistinguishable from `range slice`) — does NOT count, erring toward silence (the safe direction for a narrowing). *Sunset:* if a project's test framework makes a swept matrix un-writable (every matrix flows through a declared-table helper that cannot iterate a bit-mask), or if coverage tooling subsumes the declared-vs-swept distinction, this card retires with its checker (R-050).</fact></p>
    <p p="13"><fact id="EVIDENCE-AND-TRANSFER-STRENGTH" status="spec/done">Evidence &amp; Transfer-strength: checker shipped (`declared-test-matrices`, doctested in `core-ai-native-conform/src/rules/matrices.rs`, mounted in `rust-ai-native-conform`, `go-ai-native-conform`, and `typescript-ai-native-conform`); R-060. **Honest vacuum:** a host-wide census found **zero** generated-axis sweeps in the tree — no `1 &lt;&lt; n` loop bound, no `2 ** n`, no `math.Pow(2, …)`, and no 3+-deep nest of range/C-style-for loops. The tree's 3-deep test nests (`progress-core`'s `Stage::ALL × State::ALL × Stage::ALL`, `vibe-workspace`'s `[Snap,Hard] × [bool] × [bool]`) are all DECLARED-axis exhaustions — compliant by the narrowed predicate, and the case that motivated the narrowing (the bare-depth heuristic red'd them). So the rule is demonstrated on fixtures (a dirty `1 &lt;&lt; 3` bit-mask AND a 3-deep range/C-style-for nest per stack red; a declared table+loop AND a 3-deep collection-loop nest green), NOT on host code. This is a fact about the tree's tidiness, not an argument against a rule the discipline forbids weakening for disuse: the value is prospective — the next swept matrix someone writes is caught at the gate, not in a regression. Portability: **Rust + Go + TypeScript** (all three mount the rule and emit `Fact::TestSweep`). Tag: **[E-mid]** — the detection is unit-tested across three syntaxes and the red/green fixtures are live, but no host regression has been prevented yet (parallel to `rule-position-is-a-resource`).</fact></p>
  </section>
  <section id="band-three-operation" title="Band 3 — Operation">
    <fence lang="card-ops" p="14">trigger: WHEN a test-context loop bound sweeps 2^n (a `1 &lt;&lt; n` / `2 ** n` / `Math.pow(2, n)` bitmask) OR a test nests GENERATED-axis loops 3+ deep (a Rust range `for i in 0..n`, a Go/TS C-style `for` — NOT a collection/array/constant for, which is a declared axis), THEN apply
mode: gate            # a conform rule: runs over the scanned file set at conform time (per-merge), not per-edit (inline) or on a schedule (raid) — the finding arrives through the normal gate
routine:
  1. Read the finding: it names the kind (`bitmask` or `nested-loops`) and the detail (the bound text or the depth).
  2. Replace the swept enumeration with a DECLARED table of cases: name each case, iterate the table once.
  3. If a case is genuinely combinatorial (a truth-table that must enumerate all 2^n), keep it — but name it and document why, and let the ratchet freeze that one finding as a recorded, deliberate enumeration.
  4. Re-run `rust-ai-native conform check --scope &lt;crate&gt;` (or the go/ts twin); the finding clears and the baseline only shrinks.
  5. A 2-deep nest, a single loop over a collection, OR any depth of collection/constant-loop nesting (a closed set exhausted by nesting) is compliant — do not split it; the rule fires only at a 2^n bound or at ≥3-deep GENERATED-axis (range / C-style-for) nesting.
checker: declared-test-matrices (core-ai-native-conform, T-syn over TestSweep facts; mounted in rust-ai-native-conform, go-ai-native-conform, typescript-ai-native-conform)
raid_role: layer=conform; order=after:cell-closure; batch=crate
budget: active_rules=1; first_signal=conform scan (content-addressed, cached; &lt;1s/file warm)</fence>
  </section>
</spec>
