You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
svelte-kit-vice/src/uix/morfo/PERMUTATION_RUNNER.md

243 lines
10 KiB

morfo: add permutation runner (A37) — third validation layer for state transitions ## Why `morfo:check` validates single-state DOM at page load; `smoke` catches hydration-time errors. Neither ejercices state TRANSITIONS — and that is where every reactivity bug we caught this week actually lived: - toolbar A35 loop (mount-time, not caught by either) - form A36 microtask loop (mount-time with onChange) - slider RTL transform (static value, off-by-thumb-width) All passed the existing CI. All would have failed a "click, re-validate" pass. ## What - **`scripts/permutation-check.ts`** — Playwright runner. For each demo, collects `data-perm-step="N"` elements, executes them in ascending order (re-discovering after each step so portal-gated controls get counted), and re-validates the component's morfo + captures any `pageerror` / `console.error` between steps. - **`data-perm-*` authoring convention** — step index, human label, mode (click / focus / type / key), settle delay, skip-validate flag. Demos opt in incrementally; the runner SKIPs annotation-less pages without failing. - **`src/uix/morfo/PERMUTATION_RUNNER.md`** — full design doc covering what it validates, the annotation convention, example, failure format, and v2–v4 roadmap (URL-driven states → morfo-inferred cycles → MutationObserver ordering for Sema). - **COMPONENT_GUIDE A37 + checklist item 40** — instrumentation is now a ship-gate rule; doc lists coverage targets (overlays / toggleables / composite roving / forms / RTL) and explicit skips (alerts, file pickers). - **`npm run perm:check`** — pipeline entry, exit codes 0/1/2 parallelling `morfo:check`. ## Instrumented demos (v1 seed) - dialog — 2 steps: open via trigger → close via Close button. Step 1 is inside `{#if open}` + Portal; runner's dynamic re-discovery handles it. - tabs — 3 steps: initial tab 1 → switch to tab 2 → switch to tab 3. - toolbar — 3 steps: toggle bold / italic / underline GroupItems (exercises the A35 pattern that previously looped). Result: 8/8 permutations pass; 63 demos SKIPPED pending instrumentation. ## Verification - `npm run check`: 0 errors in soma/morfo/scripts scope. - `npm run smoke`: 65/65 routes. - `npm run morfo:check`: 66/66 morfos. - `npm run perm:check`: 3/3 instrumented demos, 8/8 permutations. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
6 months ago
# Permutation runner
Automated CI tool that cycles soma components through their declared state space and validates morfo compliance at every step.
> **Why this exists.** `morfo:check` validates the DOM at page-load time, in a single state. `smoke` catches `pageerror` during mount. Neither ejercices the **transitions** between states — and that's where most of the reactivity bugs we've caught this quarter actually live (toolbar A35, form A36, slider RTL transform). The permutation runner is the third validation layer that closes that gap.
---
## What it validates
For every demo page, for every permutation the page declares:
1. **No reactivity loops.** `effect_update_depth_exceeded` AND microtask-mediated loops (A35 / A36) both count as failures. A long microtask starvation (>2 s) is the proxy for A36.
2. **No runtime errors.** `pageerror` or `console.error` during a state transition fails the permutation.
3. **Morfo contract holds.** After the transition settles, the emitted DOM still conforms to the component's morfo (same rules as `morfo:check` — declared attrs present, values in enum, no undeclared namespaced attrs).
4. **ARIA relationships remain valid.** If a part emits `aria-labelledby="X"`, the element with id `X` must exist in the DOM at that state.
What it does **not** validate (future work):
- Visual correctness (that's eidos' problem).
- Animation timing or `data-last-action` → `data-state` ordering (dedicated MutationObserver test).
- Cross-component interactions (it runs one demo at a time).
---
## Convention: `data-perm-*` attributes
Demo pages opt in by tagging interactive controls with `data-perm-*` attrs. The runner discovers these, clicks them in sequence, and validates after each click.
### `data-perm-step="N"`
Declares the click order. Starts at `0`. The runner clicks elements in ascending order.
```svelte
<button data-perm-step="0" onclick={() => dialogOpen = true}>Open</button>
<button data-perm-step="1" onclick={() => dialogOpen = false}>Close</button>
```
### `data-perm-label="..."`
Human-readable label for the log. Defaults to the element's text content.
```svelte
<button data-perm-step="0" data-perm-label="open dialog via trigger">
Open
</button>
```
### `data-perm-settle="ms"`
Override the default 300ms settle time before morfo re-validation. Use for animations, async validation, etc.
```svelte
<button data-perm-step="0" data-perm-settle="800">
Submit (async)
</button>
```
### `data-perm-skip-validate`
Click the element but don't validate afterwards. For intermediate actions (e.g. typing a value before submit). The next step validates.
### `data-perm-mode`
Overrides the default click interaction:
- `click` (default) — `await page.click(el)`.
- `focus` — `await el.focus()`.
- `type="abc"` — type the string.
- `key="Escape"` — dispatch a `keydown` with the given key.
```svelte
<input data-perm-step="0" data-perm-mode='type="ada@example.com"' />
<div data-perm-step="1" data-perm-mode='key="Tab"' />
```
---
## Example: Dialog demo
Minimal instrumentation that cycles open → close → open via Escape:
```svelte
<Dialog.Trigger data-perm-step="0" data-perm-label="open via trigger">
Open
</Dialog.Trigger>
{#if dialogOpen}
<Dialog.Content data-perm-step="1" data-perm-mode='key="Escape"' data-perm-label="close via Escape">
…
</Dialog.Content>
{/if}
<button data-perm-step="2" data-perm-label="re-open">Open</button>
```
Runner output:
```
PASS dialog 3 permutations
· step 0 (open via trigger) morfo OK, 0 errors
· step 1 (close via Escape) morfo OK, 0 errors
· step 2 (re-open) morfo OK, 0 errors
```
---
## Failure format
```
FAIL toolbar 3 permutations, 1 failed
· step 0 (focus first button) morfo OK
· step 1 (type "b") PAGEERR: effect_update_depth_exceeded
· step 2 (click toggle) skipped (previous step failed)
```
---
## CLI
```bash
# Requires `npm run dev` running (auto-detects port 5173–5180).
npm run perm:check
```
Exit codes:
- `0` — every demo with `data-perm-step` passes all permutations.
- `1` — at least one permutation failed.
- `2` — no dev server / no morfos found.
Demos without `data-perm-step` are skipped with a `SKIP` line (they haven't opted in yet).
---
morfo: permutation runner v2 — URL-driven state seeds + RTL variant v1 shipped click-based state cycles. v2 adds URL-seeded initial state so the runner can reach any state the component exposes without walking a click sequence, and runs every instrumented demo TWICE in CI (LTR + RTL) to catch direction-specific regressions like the slider thumb transform that landed this week. ## New test-route infrastructure - `src/routes/test/soma/_perms.svelte.ts` — parses `?perm.X=Y` query params into a typed `Perms` record (naive coercion: `'true'` → boolean, digits → number, else string). Exposed via `Perms.getOr({})` context. Underscore prefix so SvelteKit does not route it. - `+layout.svelte` — parses perms once at mount, sets context, honours `?perm.dir=rtl` and `?perm.locale=en` by routing through `App.setDir` / `App.setLocale` so the existing control strip stays in sync. ## Demo opt-in Three demos seed initial state from URL params (v1 + v2 combined): - `dialog` — `?perm.open=true` lands open. - `tabs` — `?perm.value=tab-2` lands on that tab. - `toolbar` — `?perm.format=bold,italic` + `?perm.align=center` preselect toggles. ## Runner extension `scripts/permutation-check.ts` now runs each instrumented demo under a matrix of URL variants (currently `ltr` + `rtl`), validates morfo at the URL-seeded state BEFORE any clicks (logged as `[initial seed]`), then runs the v1 click cycle. Output format updated to show per-variant blocks. Result: 3 instrumented demos × 2 variants = 22 step validations per CI run. RTL seeding reproduces the specific state machine the slider `translate(50%, -50%)` bug needed to fail — had v2 been in place that day, the regression would have fired before shipping. ## Docs `src/uix/morfo/PERMUTATION_RUNNER.md` gains the v2 section (layout plumbing, demo opt-in, URL variant table, `[initial seed]` step semantics) and marks v1 + v2 as shipped 2026-04-22. v3 (morfo-inferred per-component axis matrix) and v4 (MutationObserver ordering for Sema) remain on the roadmap. ## 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 · 22 steps across 2 variants. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
6 months ago
## v2 — URL-driven state injection
v1 covers "click through a state sequence and validate after each step." v2 adds **URL-seeded initial state**: the demo reads `?perm.X=Y` params from the URL and uses them as initial `$state` values. Every instrumented demo now runs TWICE in CI — once under `ltr` (no params), once under `rtl` (`?perm.dir=rtl`). Universal axes are run across every demo; per-component axes are just defaults in the demo's script.
### Layout plumbing
`src/routes/test/soma/+layout.svelte` parses `?perm.*` params once at mount, exposes them via a context at `src/routes/test/soma/_perms.svelte.ts`, and also honours `?perm.dir=ltr|rtl` and `?perm.locale=es|en` by routing through the existing `App.setDir` / `App.setLocale` path (so the control strip stays in sync).
### Demo opt-in
Read the perms and use them as **initial** state (not live-bound — the user can still click through transitions after landing):
```svelte
<script lang="ts">
import { Perms } from '../_perms.svelte';
const perms = Perms.getOr({} as Record<string, string | number | boolean>);
// Perm-runner v2 seed — `?perm.open=true` / `?perm.variant=alertdialog`
// land directly in that state without walking the click sequence.
let open = $state((perms.open as boolean) ?? false);
let variant = $state((perms.variant as 'default' | 'alertdialog') ?? 'default');
</script>
```
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.
morfo: permutation runner v3 — per-demo axis matrix via `<meta name="perm-axes">` 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>
6 months ago
### URL variants (v2 baseline + v3 axis-at-a-time)
morfo: permutation runner v2 — URL-driven state seeds + RTL variant v1 shipped click-based state cycles. v2 adds URL-seeded initial state so the runner can reach any state the component exposes without walking a click sequence, and runs every instrumented demo TWICE in CI (LTR + RTL) to catch direction-specific regressions like the slider thumb transform that landed this week. ## New test-route infrastructure - `src/routes/test/soma/_perms.svelte.ts` — parses `?perm.X=Y` query params into a typed `Perms` record (naive coercion: `'true'` → boolean, digits → number, else string). Exposed via `Perms.getOr({})` context. Underscore prefix so SvelteKit does not route it. - `+layout.svelte` — parses perms once at mount, sets context, honours `?perm.dir=rtl` and `?perm.locale=en` by routing through `App.setDir` / `App.setLocale` so the existing control strip stays in sync. ## Demo opt-in Three demos seed initial state from URL params (v1 + v2 combined): - `dialog` — `?perm.open=true` lands open. - `tabs` — `?perm.value=tab-2` lands on that tab. - `toolbar` — `?perm.format=bold,italic` + `?perm.align=center` preselect toggles. ## Runner extension `scripts/permutation-check.ts` now runs each instrumented demo under a matrix of URL variants (currently `ltr` + `rtl`), validates morfo at the URL-seeded state BEFORE any clicks (logged as `[initial seed]`), then runs the v1 click cycle. Output format updated to show per-variant blocks. Result: 3 instrumented demos × 2 variants = 22 step validations per CI run. RTL seeding reproduces the specific state machine the slider `translate(50%, -50%)` bug needed to fail — had v2 been in place that day, the regression would have fired before shipping. ## Docs `src/uix/morfo/PERMUTATION_RUNNER.md` gains the v2 section (layout plumbing, demo opt-in, URL variant table, `[initial seed]` step semantics) and marks v1 + v2 as shipped 2026-04-22. v3 (morfo-inferred per-component axis matrix) and v4 (MutationObserver ordering for Sema) remain on the roadmap. ## 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 · 22 steps across 2 variants. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
6 months ago
morfo: permutation runner v3 — per-demo axis matrix via `<meta name="perm-axes">` 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>
6 months ago
The runner composes each demo's variant list from two sources:
morfo: permutation runner v2 — URL-driven state seeds + RTL variant v1 shipped click-based state cycles. v2 adds URL-seeded initial state so the runner can reach any state the component exposes without walking a click sequence, and runs every instrumented demo TWICE in CI (LTR + RTL) to catch direction-specific regressions like the slider thumb transform that landed this week. ## New test-route infrastructure - `src/routes/test/soma/_perms.svelte.ts` — parses `?perm.X=Y` query params into a typed `Perms` record (naive coercion: `'true'` → boolean, digits → number, else string). Exposed via `Perms.getOr({})` context. Underscore prefix so SvelteKit does not route it. - `+layout.svelte` — parses perms once at mount, sets context, honours `?perm.dir=rtl` and `?perm.locale=en` by routing through `App.setDir` / `App.setLocale` so the existing control strip stays in sync. ## Demo opt-in Three demos seed initial state from URL params (v1 + v2 combined): - `dialog` — `?perm.open=true` lands open. - `tabs` — `?perm.value=tab-2` lands on that tab. - `toolbar` — `?perm.format=bold,italic` + `?perm.align=center` preselect toggles. ## Runner extension `scripts/permutation-check.ts` now runs each instrumented demo under a matrix of URL variants (currently `ltr` + `rtl`), validates morfo at the URL-seeded state BEFORE any clicks (logged as `[initial seed]`), then runs the v1 click cycle. Output format updated to show per-variant blocks. Result: 3 instrumented demos × 2 variants = 22 step validations per CI run. RTL seeding reproduces the specific state machine the slider `translate(50%, -50%)` bug needed to fail — had v2 been in place that day, the regression would have fired before shipping. ## Docs `src/uix/morfo/PERMUTATION_RUNNER.md` gains the v2 section (layout plumbing, demo opt-in, URL variant table, `[initial seed]` step semantics) and marks v1 + v2 as shipped 2026-04-22. v3 (morfo-inferred per-component axis matrix) and v4 (MutationObserver ordering for Sema) remain on the roadmap. ## 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 · 22 steps across 2 variants. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
6 months ago
morfo: permutation runner v3 — per-demo axis matrix via `<meta name="perm-axes">` 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>
6 months ago
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).
morfo: permutation runner v2 — URL-driven state seeds + RTL variant v1 shipped click-based state cycles. v2 adds URL-seeded initial state so the runner can reach any state the component exposes without walking a click sequence, and runs every instrumented demo TWICE in CI (LTR + RTL) to catch direction-specific regressions like the slider thumb transform that landed this week. ## New test-route infrastructure - `src/routes/test/soma/_perms.svelte.ts` — parses `?perm.X=Y` query params into a typed `Perms` record (naive coercion: `'true'` → boolean, digits → number, else string). Exposed via `Perms.getOr({})` context. Underscore prefix so SvelteKit does not route it. - `+layout.svelte` — parses perms once at mount, sets context, honours `?perm.dir=rtl` and `?perm.locale=en` by routing through `App.setDir` / `App.setLocale` so the existing control strip stays in sync. ## Demo opt-in Three demos seed initial state from URL params (v1 + v2 combined): - `dialog` — `?perm.open=true` lands open. - `tabs` — `?perm.value=tab-2` lands on that tab. - `toolbar` — `?perm.format=bold,italic` + `?perm.align=center` preselect toggles. ## Runner extension `scripts/permutation-check.ts` now runs each instrumented demo under a matrix of URL variants (currently `ltr` + `rtl`), validates morfo at the URL-seeded state BEFORE any clicks (logged as `[initial seed]`), then runs the v1 click cycle. Output format updated to show per-variant blocks. Result: 3 instrumented demos × 2 variants = 22 step validations per CI run. RTL seeding reproduces the specific state machine the slider `translate(50%, -50%)` bug needed to fail — had v2 been in place that day, the regression would have fired before shipping. ## Docs `src/uix/morfo/PERMUTATION_RUNNER.md` gains the v2 section (layout plumbing, demo opt-in, URL variant table, `[initial seed]` step semantics) and marks v1 + v2 as shipped 2026-04-22. v3 (morfo-inferred per-component axis matrix) and v4 (MutationObserver ordering for Sema) remain on the roadmap. ## 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 · 22 steps across 2 variants. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
6 months ago
morfo: permutation runner v3 — per-demo axis matrix via `<meta name="perm-axes">` 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>
6 months ago
Supported axes (v3 initial set):
| Axis | Baseline | Flipped variant | Typical use |
|------|----------|-----------------|-------------|
| `dir` | `ltr` | `rtl` | Always run (universal). |
| `orientation` | `horizontal` | `vertical` | Toolbars, Tabs, Splitters, RadioGroup, Slider. |
| `disabled` | unset | `true` | Any component with a `disabled` prop. |
| `readonly` | unset | `true` | Inputs, Editable, Calendar. |
| `invalid` | unset | `true` | Form-participating components. |
| `loading` | unset | `true` | Async components with pending states. |
Demos opt in by listing the axes they honour. The runner skips meta-less demos at the `dir` axis only (universal baseline always applies).
### v3 opt-in — 3 steps
1. Add the meta tag (list only the axes your demo actually wires):
```svelte
<svelte:head>
<title>Tabs · Soma</title>
<meta name="perm-axes" content="dir,orientation,disabled" />
</svelte:head>
```
2. Seed the `$state` from perms for each declared axis:
```svelte
<script lang="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.Provider bind: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).
morfo: permutation runner v2 — URL-driven state seeds + RTL variant v1 shipped click-based state cycles. v2 adds URL-seeded initial state so the runner can reach any state the component exposes without walking a click sequence, and runs every instrumented demo TWICE in CI (LTR + RTL) to catch direction-specific regressions like the slider thumb transform that landed this week. ## New test-route infrastructure - `src/routes/test/soma/_perms.svelte.ts` — parses `?perm.X=Y` query params into a typed `Perms` record (naive coercion: `'true'` → boolean, digits → number, else string). Exposed via `Perms.getOr({})` context. Underscore prefix so SvelteKit does not route it. - `+layout.svelte` — parses perms once at mount, sets context, honours `?perm.dir=rtl` and `?perm.locale=en` by routing through `App.setDir` / `App.setLocale` so the existing control strip stays in sync. ## Demo opt-in Three demos seed initial state from URL params (v1 + v2 combined): - `dialog` — `?perm.open=true` lands open. - `tabs` — `?perm.value=tab-2` lands on that tab. - `toolbar` — `?perm.format=bold,italic` + `?perm.align=center` preselect toggles. ## Runner extension `scripts/permutation-check.ts` now runs each instrumented demo under a matrix of URL variants (currently `ltr` + `rtl`), validates morfo at the URL-seeded state BEFORE any clicks (logged as `[initial seed]`), then runs the v1 click cycle. Output format updated to show per-variant blocks. Result: 3 instrumented demos × 2 variants = 22 step validations per CI run. RTL seeding reproduces the specific state machine the slider `translate(50%, -50%)` bug needed to fail — had v2 been in place that day, the regression would have fired before shipping. ## Docs `src/uix/morfo/PERMUTATION_RUNNER.md` gains the v2 section (layout plumbing, demo opt-in, URL variant table, `[initial seed]` step semantics) and marks v1 + v2 as shipped 2026-04-22. v3 (morfo-inferred per-component axis matrix) and v4 (MutationObserver ordering for Sema) remain on the roadmap. ## 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 · 22 steps across 2 variants. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
6 months ago
### `[initial seed]` step
At each URL variant, the runner validates morfo BEFORE running any click step. That validation appears in the log as `[initial seed]` and catches morfo drift that exists at the URL-seeded state — exactly the gap a click-only cycle can't reach.
---
morfo: add permutation runner (A37) — third validation layer for state transitions ## Why `morfo:check` validates single-state DOM at page load; `smoke` catches hydration-time errors. Neither ejercices state TRANSITIONS — and that is where every reactivity bug we caught this week actually lived: - toolbar A35 loop (mount-time, not caught by either) - form A36 microtask loop (mount-time with onChange) - slider RTL transform (static value, off-by-thumb-width) All passed the existing CI. All would have failed a "click, re-validate" pass. ## What - **`scripts/permutation-check.ts`** — Playwright runner. For each demo, collects `data-perm-step="N"` elements, executes them in ascending order (re-discovering after each step so portal-gated controls get counted), and re-validates the component's morfo + captures any `pageerror` / `console.error` between steps. - **`data-perm-*` authoring convention** — step index, human label, mode (click / focus / type / key), settle delay, skip-validate flag. Demos opt in incrementally; the runner SKIPs annotation-less pages without failing. - **`src/uix/morfo/PERMUTATION_RUNNER.md`** — full design doc covering what it validates, the annotation convention, example, failure format, and v2–v4 roadmap (URL-driven states → morfo-inferred cycles → MutationObserver ordering for Sema). - **COMPONENT_GUIDE A37 + checklist item 40** — instrumentation is now a ship-gate rule; doc lists coverage targets (overlays / toggleables / composite roving / forms / RTL) and explicit skips (alerts, file pickers). - **`npm run perm:check`** — pipeline entry, exit codes 0/1/2 parallelling `morfo:check`. ## Instrumented demos (v1 seed) - dialog — 2 steps: open via trigger → close via Close button. Step 1 is inside `{#if open}` + Portal; runner's dynamic re-discovery handles it. - tabs — 3 steps: initial tab 1 → switch to tab 2 → switch to tab 3. - toolbar — 3 steps: toggle bold / italic / underline GroupItems (exercises the A35 pattern that previously looped). Result: 8/8 permutations pass; 63 demos SKIPPED pending instrumentation. ## Verification - `npm run check`: 0 errors in soma/morfo/scripts scope. - `npm run smoke`: 65/65 routes. - `npm run morfo:check`: 66/66 morfos. - `npm run perm:check`: 3/3 instrumented demos, 8/8 permutations. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
6 months ago
## Roadmap
morfo: permutation runner v2 — URL-driven state seeds + RTL variant v1 shipped click-based state cycles. v2 adds URL-seeded initial state so the runner can reach any state the component exposes without walking a click sequence, and runs every instrumented demo TWICE in CI (LTR + RTL) to catch direction-specific regressions like the slider thumb transform that landed this week. ## New test-route infrastructure - `src/routes/test/soma/_perms.svelte.ts` — parses `?perm.X=Y` query params into a typed `Perms` record (naive coercion: `'true'` → boolean, digits → number, else string). Exposed via `Perms.getOr({})` context. Underscore prefix so SvelteKit does not route it. - `+layout.svelte` — parses perms once at mount, sets context, honours `?perm.dir=rtl` and `?perm.locale=en` by routing through `App.setDir` / `App.setLocale` so the existing control strip stays in sync. ## Demo opt-in Three demos seed initial state from URL params (v1 + v2 combined): - `dialog` — `?perm.open=true` lands open. - `tabs` — `?perm.value=tab-2` lands on that tab. - `toolbar` — `?perm.format=bold,italic` + `?perm.align=center` preselect toggles. ## Runner extension `scripts/permutation-check.ts` now runs each instrumented demo under a matrix of URL variants (currently `ltr` + `rtl`), validates morfo at the URL-seeded state BEFORE any clicks (logged as `[initial seed]`), then runs the v1 click cycle. Output format updated to show per-variant blocks. Result: 3 instrumented demos × 2 variants = 22 step validations per CI run. RTL seeding reproduces the specific state machine the slider `translate(50%, -50%)` bug needed to fail — had v2 been in place that day, the regression would have fired before shipping. ## Docs `src/uix/morfo/PERMUTATION_RUNNER.md` gains the v2 section (layout plumbing, demo opt-in, URL variant table, `[initial seed]` step semantics) and marks v1 + v2 as shipped 2026-04-22. v3 (morfo-inferred per-component axis matrix) and v4 (MutationObserver ordering for Sema) remain on the roadmap. ## 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 · 22 steps across 2 variants. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
6 months ago
- **v1 (shipped 2026-04-22)** — click-based state transitions, morfo re-validation after each step.
- **v2 (shipped 2026-04-22)** — URL-seeded initial state, universal axes (`dir`, `locale`), `[initial seed]` validation step before clicks. Demo layout reads `?perm.*` params, demos seed initial `$state` from them.
morfo: permutation runner v3 — per-demo axis matrix via `<meta name="perm-axes">` 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>
6 months ago
- **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.
morfo: add permutation runner (A37) — third validation layer for state transitions ## Why `morfo:check` validates single-state DOM at page load; `smoke` catches hydration-time errors. Neither ejercices state TRANSITIONS — and that is where every reactivity bug we caught this week actually lived: - toolbar A35 loop (mount-time, not caught by either) - form A36 microtask loop (mount-time with onChange) - slider RTL transform (static value, off-by-thumb-width) All passed the existing CI. All would have failed a "click, re-validate" pass. ## What - **`scripts/permutation-check.ts`** — Playwright runner. For each demo, collects `data-perm-step="N"` elements, executes them in ascending order (re-discovering after each step so portal-gated controls get counted), and re-validates the component's morfo + captures any `pageerror` / `console.error` between steps. - **`data-perm-*` authoring convention** — step index, human label, mode (click / focus / type / key), settle delay, skip-validate flag. Demos opt in incrementally; the runner SKIPs annotation-less pages without failing. - **`src/uix/morfo/PERMUTATION_RUNNER.md`** — full design doc covering what it validates, the annotation convention, example, failure format, and v2–v4 roadmap (URL-driven states → morfo-inferred cycles → MutationObserver ordering for Sema). - **COMPONENT_GUIDE A37 + checklist item 40** — instrumentation is now a ship-gate rule; doc lists coverage targets (overlays / toggleables / composite roving / forms / RTL) and explicit skips (alerts, file pickers). - **`npm run perm:check`** — pipeline entry, exit codes 0/1/2 parallelling `morfo:check`. ## Instrumented demos (v1 seed) - dialog — 2 steps: open via trigger → close via Close button. Step 1 is inside `{#if open}` + Portal; runner's dynamic re-discovery handles it. - tabs — 3 steps: initial tab 1 → switch to tab 2 → switch to tab 3. - toolbar — 3 steps: toggle bold / italic / underline GroupItems (exercises the A35 pattern that previously looped). Result: 8/8 permutations pass; 63 demos SKIPPED pending instrumentation. ## Verification - `npm run check`: 0 errors in soma/morfo/scripts scope. - `npm run smoke`: 65/65 routes. - `npm run morfo:check`: 66/66 morfos. - `npm run perm:check`: 3/3 instrumented demos, 8/8 permutations. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
6 months ago
morfo: permutation runner v3 — per-demo axis matrix via `<meta name="perm-axes">` 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>
6 months ago
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.
morfo: add permutation runner (A37) — third validation layer for state transitions ## Why `morfo:check` validates single-state DOM at page load; `smoke` catches hydration-time errors. Neither ejercices state TRANSITIONS — and that is where every reactivity bug we caught this week actually lived: - toolbar A35 loop (mount-time, not caught by either) - form A36 microtask loop (mount-time with onChange) - slider RTL transform (static value, off-by-thumb-width) All passed the existing CI. All would have failed a "click, re-validate" pass. ## What - **`scripts/permutation-check.ts`** — Playwright runner. For each demo, collects `data-perm-step="N"` elements, executes them in ascending order (re-discovering after each step so portal-gated controls get counted), and re-validates the component's morfo + captures any `pageerror` / `console.error` between steps. - **`data-perm-*` authoring convention** — step index, human label, mode (click / focus / type / key), settle delay, skip-validate flag. Demos opt in incrementally; the runner SKIPs annotation-less pages without failing. - **`src/uix/morfo/PERMUTATION_RUNNER.md`** — full design doc covering what it validates, the annotation convention, example, failure format, and v2–v4 roadmap (URL-driven states → morfo-inferred cycles → MutationObserver ordering for Sema). - **COMPONENT_GUIDE A37 + checklist item 40** — instrumentation is now a ship-gate rule; doc lists coverage targets (overlays / toggleables / composite roving / forms / RTL) and explicit skips (alerts, file pickers). - **`npm run perm:check`** — pipeline entry, exit codes 0/1/2 parallelling `morfo:check`. ## Instrumented demos (v1 seed) - dialog — 2 steps: open via trigger → close via Close button. Step 1 is inside `{#if open}` + Portal; runner's dynamic re-discovery handles it. - tabs — 3 steps: initial tab 1 → switch to tab 2 → switch to tab 3. - toolbar — 3 steps: toggle bold / italic / underline GroupItems (exercises the A35 pattern that previously looped). Result: 8/8 permutations pass; 63 demos SKIPPED pending instrumentation. ## Verification - `npm run check`: 0 errors in soma/morfo/scripts scope. - `npm run smoke`: 65/65 routes. - `npm run morfo:check`: 66/66 morfos. - `npm run perm:check`: 3/3 instrumented demos, 8/8 permutations. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
6 months ago
---
## Related
- [morfo README](./README.md) — authoring reference for the morfos this runner validates against.
- [`scripts/permutation-check.ts`](../../../scripts/permutation-check.ts) — the runner itself.
- [`scripts/morfo-check.ts`](../../../scripts/morfo-check.ts) — single-state DOM validator that shares the per-morfo validation logic.

Powered by TurnKey Linux.