diff --git a/continue.md b/continue.md
index e93c1f4b9..9c49c2171 100644
--- a/continue.md
+++ b/continue.md
@@ -232,6 +232,17 @@ Actualizacion smoke 2026-05-16:
- Pendiente deliberado: `scripts/permutation-check.ts` sigue siendo runner
historico del arbol `/test/soma`; no usarlo como release gate hasta
reconectarlo a una route map real del docs shell actual.
+- `scripts/permutation-check.ts` reconectado al shell actual:
+ - usa `/uix/components/{kebab}` por defecto;
+ - admite `PERM_ROUTE_PREFIX` para otros shells;
+ - salta morfos sin ruta como `SKIP no route`;
+ - salta rutas sin `data-perm-step` ni `perm-axes` como
+ `SKIP no perm instrumentation`;
+ - si todavia no hay demos instrumentadas, sale limpio con
+ "No instrumented routed demos found".
+- `src/uix/morfo/PERMUTATION_RUNNER.md` actualizado: la antigua infraestructura
+ `_perms.svelte.ts` queda marcada como retirada; las paginas actuales deben
+ leer `?perm.*` desde su ruta si optan a permutations.
Actualizacion Eidos visual wrappers 2026-05-16:
diff --git a/scripts/permutation-check.ts b/scripts/permutation-check.ts
index 73955f001..0f2967139 100644
--- a/scripts/permutation-check.ts
+++ b/scripts/permutation-check.ts
@@ -11,14 +11,17 @@
* each settle. See `src/uix/morfo/PERMUTATION_RUNNER.md` for the full
* authoring convention.
*
+ * Current route map: `/uix/components/{kebab}`. Morfos without a routed demo
+ * or without `data-perm-*` instrumentation are skipped.
+ *
* Exit codes:
- * 0 — every demo with `data-perm-step` passes all permutations.
+ * 0 — every instrumented routed demo passes all permutations.
* 1 — at least one permutation failed.
* 2 — no dev server / no morfos found.
*/
import { chromium, type Page } from 'playwright';
-import { readdirSync } from 'node:fs';
+import { existsSync, readdirSync } from 'node:fs';
import { fileURLToPath, pathToFileURL } from 'node:url';
import { dirname, join } from 'node:path';
import type { Morfo, MorfoPart, MorfoData } from '../src/uix/morfo/types';
@@ -26,6 +29,29 @@ import { validateMorfo } from '../src/uix/morfo/schema';
const __dirname = dirname(fileURLToPath(import.meta.url));
const MORFOS_DIR = join(__dirname, '..', 'src', 'uix', 'morfo', 'components');
+const ROUTES_DIR = join(__dirname, '..', 'web', 'routes');
+const DEFAULT_ROUTE_PREFIX = '/uix/components';
+const routePrefix = normaliseRoutePrefix(process.env.PERM_ROUTE_PREFIX ?? DEFAULT_ROUTE_PREFIX);
+
+function normaliseRoutePrefix(value: string): string {
+ const trimmed = value.trim().replace(/\/+$/, '');
+ if (!trimmed || trimmed === '/') return '';
+ return trimmed.startsWith('/') ? trimmed : `/${trimmed}`;
+}
+
+function routeToPageFile(route: string): string {
+ const segments = route
+ .split('/')
+ .filter(Boolean)
+ .filter((segment) => !(segment.startsWith('(') && segment.endsWith(')')));
+
+ return join(ROUTES_DIR, ...segments, '+page.svelte');
+}
+
+function routeForMorfo(morfo: Morfo): string | null {
+ const route = `${routePrefix}/${morfo.kebab}`.replace(/\/+/g, '/');
+ return existsSync(routeToPageFile(route)) ? route : null;
+}
// ── Shared with morfo-check.ts ──────────────────────────────────────────────
@@ -329,7 +355,8 @@ console.error(`Loaded ${morfos.length} morfo${morfos.length === 1 ? '' : 's'}`);
const browser = await chromium.launch();
const ctx = await browser.newContext();
const results: RouteResult[] = [];
-const skipped: string[] = [];
+const skippedNoRoute: string[] = [];
+const skippedNotInstrumented: string[] = [];
/** Run one URL variant: load, cycle click-driven steps, report. */
async function runVariant(
@@ -472,11 +499,14 @@ async function runVariant(
*/
async function probeDemo(
base: string,
- morfoKebab: string
-): Promise<{ axes: string[]; hasSteps: boolean } | 'no-demo'> {
+ morfo: Morfo
+): Promise<{ route: string; axes: string[]; hasSteps: boolean } | 'no-route' | 'no-demo'> {
+ const route = routeForMorfo(morfo);
+ if (!route) return 'no-route';
+
const page = await ctx.newPage();
try {
- const res = await page.goto(`${base}/test/soma/${morfoKebab}`, {
+ const res = await page.goto(`${base}${route}`, {
waitUntil: 'networkidle',
timeout: 20000
});
@@ -484,7 +514,7 @@ async function probeDemo(
await page.waitForTimeout(200);
const axes = await readDeclaredAxes(page);
const hasSteps = (await collectSteps(page)).length > 0;
- return { axes, hasSteps };
+ return { route, axes, hasSteps };
} catch {
return 'no-demo';
} finally {
@@ -495,9 +525,9 @@ async function probeDemo(
for (const morfo of morfos) {
const variantResults: VariantResult[] = [];
- const probe = await probeDemo(BASE, morfo.kebab);
- if (probe === 'no-demo') {
- skipped.push(morfo.kebab);
+ const probe = await probeDemo(BASE, morfo);
+ if (probe === 'no-route' || probe === 'no-demo') {
+ skippedNoRoute.push(morfo.kebab);
continue;
}
@@ -506,21 +536,21 @@ for (const morfo of morfos) {
// validation at each URL variant; step-only demos get the v1 click cycle
// at baseline; declaring both is the full v1+v2+v3 treatment.
if (probe.axes.length === 0 && !probe.hasSteps) {
- skipped.push(morfo.kebab);
+ skippedNotInstrumented.push(morfo.kebab);
continue;
}
const variants = buildVariants(probe.axes);
for (const v of variants) {
- const url = `/test/soma/${morfo.kebab}${v.suffix}`;
+ const url = `${probe.route}${v.suffix}`;
const r = await runVariant(BASE, url, v.label, morfo);
if (r === 'no-steps') continue;
variantResults.push(r);
}
if (variantResults.length === 0) {
- skipped.push(morfo.kebab);
+ skippedNotInstrumented.push(morfo.kebab);
continue;
}
@@ -570,20 +600,27 @@ for (const r of results) {
}
console.log('');
-if (skipped.length > 0) {
+if (skippedNoRoute.length > 0) {
+ console.log(
+ `SKIPPED no route (${skippedNoRoute.length}): ${skippedNoRoute.slice(0, 10).join(', ')}${skippedNoRoute.length > 10 ? ', …' : ''}`
+ );
+}
+if (skippedNotInstrumented.length > 0) {
console.log(
- `SKIPPED (${skipped.length}): ${skipped.slice(0, 10).join(', ')}${skipped.length > 10 ? ', …' : ''}`
+ `SKIPPED no perm instrumentation (${skippedNotInstrumented.length}): ${skippedNotInstrumented.slice(0, 10).join(', ')}${skippedNotInstrumented.length > 10 ? ', …' : ''}`
);
- console.log(' (demo has no `data-perm-step` annotations yet — see src/uix/morfo/PERMUTATION_RUNNER.md)');
+ console.log(' (demo has no `data-perm-step` / `perm-axes` opt-in yet — see src/uix/morfo/PERMUTATION_RUNNER.md)');
}
console.log('');
const totalVariants = results.reduce((acc, r) => acc + r.variants.length, 0);
const summary =
- failedRoutes === 0
- ? `All ${passedRoutes} instrumented demo${passedRoutes === 1 ? '' : 's'} passed. (${totalSteps} step${totalSteps === 1 ? '' : 's'} across ${totalVariants} URL variant${totalVariants === 1 ? '' : 's'}.)`
- : `${failedRoutes} demo${failedRoutes === 1 ? '' : 's'} failed (${totalFailed}/${totalSteps} steps): ${results
- .filter((r) => r.variants.some((v) => v.steps.some((s) => s.kind !== 'pass')))
- .map((r) => r.morfo)
- .join(', ')}`;
+ results.length === 0
+ ? `No instrumented routed demos found under ${routePrefix}.`
+ : failedRoutes === 0
+ ? `All ${passedRoutes} instrumented demo${passedRoutes === 1 ? '' : 's'} passed. (${totalSteps} step${totalSteps === 1 ? '' : 's'} across ${totalVariants} URL variant${totalVariants === 1 ? '' : 's'}.)`
+ : `${failedRoutes} demo${failedRoutes === 1 ? '' : 's'} failed (${totalFailed}/${totalSteps} steps): ${results
+ .filter((r) => r.variants.some((v) => v.steps.some((s) => s.kind !== 'pass')))
+ .map((r) => r.morfo)
+ .join(', ')}`;
console.log(summary);
process.exit(failedRoutes === 0 ? 0 : 1);
diff --git a/src/uix/morfo/PERMUTATION_RUNNER.md b/src/uix/morfo/PERMUTATION_RUNNER.md
index 40123000e..82b43a2c7 100644
--- a/src/uix/morfo/PERMUTATION_RUNNER.md
+++ b/src/uix/morfo/PERMUTATION_RUNNER.md
@@ -123,13 +123,21 @@ FAIL toolbar 3 permutations, 1 failed
npm run perm:check
```
+The current docs-shell route map is `/uix/components/{kebab}`. Components
+without a routed page are reported as `SKIP no route`; routed pages without
+`data-perm-step` or `perm-axes` opt-in are reported as
+`SKIP no perm instrumentation`. Override the prefix with
+`PERM_ROUTE_PREFIX=/some/path` when testing another shell.
+
Exit codes:
-- `0` — every demo with `data-perm-step` passes all permutations.
+- `0` — every instrumented routed demo passes all permutations. If no routed
+ demo is instrumented yet, the runner exits cleanly and says so.
- `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).
+Demos without `data-perm-step` / `perm-axes` are skipped with a `SKIP` line
+(they haven't opted in yet).
---
@@ -139,9 +147,12 @@ v1 covers "click through a state sequence and validate after each step." v2 adds
### Layout plumbing
-`web/routes/+layout.svelte` parses `?perm.*` params once at mount, exposes them via a context at `web/routes/_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).
-
-> **Note (2026-05-08):** routes were relocated from `src/routes/test/soma/` to `web/routes/` during the active-uix cleanup. The `_perms.svelte.ts` context wiring referenced above predates that move and may need re-implementation when permutation testing is re-enabled on the new demo tree.
+The old `/test/soma` shell used a shared `_perms.svelte.ts` context. That
+shell was retired; the current runner only provides the URL variants and
+expects each routed page under `web/routes` to read `?perm.*` params directly
+or through a new shell-level helper when we decide to add one. Until a page
+does that and declares `perm-axes` / `data-perm-step`, it is skipped rather
+than counted as coverage.
### Demo opt-in
@@ -149,14 +160,13 @@ Read the perms and use them as **initial** state (not live-bound — the user ca
```svelte
```
@@ -197,13 +207,13 @@ Demos opt in by listing the axes they honour. The runner skips meta-less demos a
```svelte
```