Bulk-instruments the remaining 49 demo pages with `<meta name="perm-axes"
content="dir" />`, bringing total coverage from 15/66 demos to 63/66.
Only checkbox, radio-group, and switch remain skipped — they have no
demo directory at `/test/soma/` (pre-existing gap, unrelated).
## Runner change
`scripts/permutation-check.ts` now allows meta-only demos (no
`data-perm-step` annotations) to participate: if a demo declares any
axis via the meta tag, the runner still visits its baseline + RTL
variants and runs the `[initial seed]` morfo validation at each, even
without a click cycle. This gives passive LTR/RTL coverage to demos
where a click-driven perm sequence doesn't apply naturally (leaf
components like Avatar, Progress, Meter; grid/list components; pickers
with inline controls).
Key change in `probeDemo()`: a demo is "instrumented" if EITHER the meta
tag OR at least one `data-perm-step` is present. Step-only demos get
the v1 click cycle at baseline; meta-only demos get the v3 axis matrix
with passive `[initial seed]` validation at each variant; declaring
both is the full treatment.
## Bulk instrumentation
For each of the 49 remaining demos, added:
```svelte
<svelte:head>
<title>… · Soma</title>
<meta name="perm-axes" content="dir" />
</svelte:head>
```
Baseline axis (`dir`) only. Demos that benefit from additional axes
(`orientation`, `disabled`, etc.) opt in incrementally — the bulk edit
doesn't touch their `$state` seeds, only adds the meta tag.
## Coverage delta
| Metric | Before | After |
|--------|-------:|------:|
| Instrumented demos | 15/66 | **63/66** |
| URL variants | 35 | **131** |
| Step validations | 106 | **202** |
~4× increase in URL variants visited per CI run. Most new demos
contribute 2 variants each (ltr + rtl), validating morfo under direction
flip. Fully-instrumented demos (tabs, toolbar, toggle-group, listbox)
still contribute 3-4 variants with click cycles.
## Deferred
- `context-menu` — right-click activation needs a `data-perm-mode='contextmenu'`
runner extension.
- `navigation-menu` — hover-driven, same as Tooltip. Tooltip was
instrumented via `data-perm-mode="focus"` + long settle; NavigationMenu
could follow the same pattern in a targeted batch.
- `color-picker` — complex pointer state machine on Area / ChannelSlider.
Meta-only covers the baseline structure.
## 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`: 63/63 instrumented demos · 202 steps · 131 variants.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
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>
## 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>