v2 ran every demo under a hard-coded `ltr` + `rtl` matrix. v3 lets each
demo declare which additional axes it honours — `orientation`, `disabled`,
`readonly`, `invalid`, `loading` — and the runner adds ONE variant per
declared axis (axis-at-a-time, not cartesian, so the matrix stays bounded
at O(N) instead of O(2^N)).
## Convention
```svelte
<svelte:head>
<meta name="perm-axes" content="dir,orientation,disabled" />
</svelte:head>
```
`dir` is universal (always runs). The other axes are opt-in: they only
produce variants when the demo both lists them in the meta AND wires the
corresponding state from `Perms.getOr(...)`. Without the meta, only
`ltr` + `rtl` run.
## Runner changes
- New `discoverAxes(base, morfoKebab)` pass: loads the baseline URL, reads
`<meta name="perm-axes">`, returns the declared axis list.
- `buildVariants(declaredAxes)` composes `ltr` + `rtl` + one `?perm.X=Y`
variant per declared axis from the `AXIS_FLIP` map.
- `[initial seed]` step validates morfo at each URL before any click,
so URL-seeded state alone is exercised (catches seed regressions even
when the click cycle never fires).
## Demo opt-in (this commit)
- `dialog` — `perm-axes="dir"` (no orientation / disabled / loading axes
apply to Dialog).
- `tabs` — `perm-axes="dir,orientation,disabled"` + wires `orientation` +
`disabled` to the first `Tabs.Provider`.
- `toolbar` — `perm-axes="dir,orientation"` + wires `orientation` to the
first `Toolbar.Provider`.
Result: 3 demos, 9 URL variants, 34 step validations per CI run
(v2 was 22 steps across 6 variants).
## Docs
`src/uix/morfo/PERMUTATION_RUNNER.md` v3 section: supported axis table,
3-step opt-in recipe, roadmap updated (v4 = morfo-inferred value cycling,
v5 = MutationObserver ordering for Sema).
## Verification
- `npm run check`: 0 errors in soma/morfo/test-route scope.
- `npm run smoke`: 65/65 routes.
- `npm run morfo:check`: 66/66 morfos.
- `npm run perm:check`: 3/3 demos · 34 steps · 9 variants.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@ -160,16 +160,62 @@ Read the perms and use them as **initial** state (not live-bound — the user ca
Type coercion is naive: `'true'` / `'false'` → boolean, bare digits → number, everything else stays a string. Cast to the component's actual union at the read site.
The runner runs each instrumented demo under a matrix of base URL variants defined in `scripts/permutation-check.ts`:
The runner composes each demo's variant list from two sources:
| Variant | URL suffix |
|---------|------------|
| `ltr` | `` (no params) |
| `rtl` | `?perm.dir=rtl` |
1. **Universal baseline** — every instrumented demo runs at `ltr` (plain URL) and `rtl` (`?perm.dir=rtl`). These catch direction-specific bugs like the slider thumb transform regression.
2. **Per-demo axes** (v3) — the demo declares which axes it honours via `<meta name="perm-axes" content="...">`. The runner adds ONE variant per declared axis, flipping that axis from baseline while keeping everything else at default (linear, not cartesian — the matrix stays bounded).
Per-component axes (`?perm.open=true`, `?perm.variant=alertdialog`, `?perm.value=tab-2`) are NOT enumerated by the runner yet — they're just defaults the demo honours when the URL provides them. Adding them to the matrix is a v3 task (the runner would read the morfo's `states` + `data.values` to generate the cartesian product).
2. Seed the `$state` from perms for each declared axis:
```svelte
<scriptlang="ts">
import { Perms } from '../_perms.svelte';
const perms = Perms.getOr({} as Record<string,string|number|boolean>);
let orientation = $state<'horizontal' | 'vertical'>(
(perms.orientation as 'horizontal' | 'vertical') ?? 'horizontal'
);
let disabled = $state((perms.disabled as boolean) ?? false);
</script>
```
3. Wire the state to the Provider:
```svelte
<Tabs.Providerbind:value{orientation}{disabled}>
...
</Tabs.Provider>
```
The runner now visits `/test/soma/tabs?perm.orientation=vertical` and `/test/soma/tabs?perm.disabled=true` in addition to `ltr` / `rtl`, and re-runs the full `data-perm-step` click cycle at each URL.
Per-component value seeds (`?perm.value=tab-2`, `?perm.open=true`, `?perm.format=bold,italic`) are NOT enumerated by the runner — they're just defaults the demo honours when testing manually or via direct URL navigation. Adding them to the enumerated matrix is a v4 task (the runner would need to read the morfo's `states` + `data.values` to know which values are meaningful).
### `[initial seed]` step
@ -181,10 +227,11 @@ At each URL variant, the runner validates morfo BEFORE running any click step. T
- **v1 (shipped 2026-04-22)** — click-based state transitions, morfo re-validation after each step.
- **v3** — derive the per-component axis matrix automatically from the morfo's `states` and `data.values`. No per-demo instrumentation needed for the baseline cycles; the runner generates `?perm.X=Y` URLs from the morfo itself.
- **v4** — integrate with `MutationObserver` to validate `data-last-action` / `data-state` ordering for Sema.
- **v3 (shipped 2026-04-22)** — per-demo axis matrix via `<meta name="perm-axes">`. Runner adds one variant per declared axis (axis-at-a-time, not cartesian). Supported axes: `dir` (universal), `orientation`, `disabled`, `readonly`, `invalid`, `loading`.
- **v4** — morfo-inferred value cycling. Reads `states[]` + `data.values` from each morfo and enumerates meaningful value combinations automatically (would cover `?perm.value=tab-2` / `?perm.variant=alertdialog` / `?perm.open=true` without per-demo declaration).
- **v5** — integrate with `MutationObserver` to validate `data-last-action` / `data-state` ordering for Sema.
The convention (`data-perm-step` / `data-perm-mode` / `perm.X=Y`) is forward-compatible: v3+ infrastructure adds automation without invalidating v1/v2 annotations.
The convention (`data-perm-step` / `data-perm-mode` / `perm.X=Y` / `perm-axes` meta) is forward-compatible: v4+ infrastructure adds automation without invalidating earlier annotations.