refactor(build): un solo mapa de alias — uix.aliases.js lo importan vite.config.ts, svelte.config.js, el compilador del boot y docs-check; muere la copia a mano (F2 del cierre)

La tabla de alias (38 entradas) estaba escrita dos veces, en vite.config.ts y a
mano en svelte.config.js, y dos scripts la sacaban de vuelta de la config de
Vite con una regex (generate-boot.ts y el invariante 8 de docs-check.ts). Una
app del workspace habría sido el cuarto sitio. Ahora vive UNA vez en
uix.aliases.js (raíz, .js con JSDoc porque Node carga svelte.config.js sin
bundler): UIX_ALIASES con rutas relativas y resolveUixAliases(root) con las
absolutas, en el mismo orden. Las dos tablas eran idénticas en claves, valores
y orden (comprobado antes de sustituirlas).

El orden es contrato: Vite y esbuild casan un alias de cadena por prefijo y
SvelteKit se los pasa en orden de inserción (leído en
@sveltejs/kit/src/exports/vite/utils.js), así que $svrs/auth/testing y
$svrs/auth van antes que $svrs. src/uix/aliases.test.ts lo guarda junto a lo
demás: cada destino existe (anti-vacío ≥ 38), resolveUixAliases conserva orden
y rutas, ninguna config vuelve a llevar una copia, y el tsconfig generado por
SvelteKit tiene paths para cada entrada. Visto fallar: con $svrs delante de
$svrs/auth, 1 rojo (restaurado byte a byte).

svelte.config.js gana kit.typescript.config para que svelte-check incluya el
módulo. readViteAliases muere sin shim; pack.test.ts importa el módulo.
CLAUDE.md y AGENTS.md dicen dónde vive la tabla (la sección de AGENTS.md
listaba alias que ya no existen).

Verificación: aliases.test + pack.test 6/6 · generate:boot byte-idéntico (esbuild
recibe el mismo mapa) · docs:check 0/0 · check:gate OK (src/ a cero; 89 en web/, dentro del ledger) · suite
entera 464 ficheros / 5 442 tests, exit 0 · npm run build de la raíz exit 0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
alpha-0.1-background
dev 3 weeks ago
parent 29e581c721
commit 9254bcc15e

@ -28,16 +28,9 @@ npm run format # Auto-format with Prettier
## Critical Architecture
### Dual Alias Configuration Required
### One Alias Table
Path aliases must be synced in BOTH [`svelte.config.js`](svelte.config.js:11) AND [`vite.config.ts`](vite.config.ts:14) for TypeScript, Svelte compiler, and Vitest to resolve consistently:
- `@/` → `src/`
- `@/ling` → `src/lib/ling` (i18n)
- `@/logr` → `src/lib/logr` (logging)
- `@/glob` → `src/lib/glob` (globalization)
- `@/actx` → `src/lib/actx` (audio context)
- `$uix` → `src/uix` (UI components)
Path aliases live in ONE module, [`uix.aliases.js`](uix.aliases.js), imported by `vite.config.ts`, `svelte.config.js`, the scripts and any app in the workspace. Never copy it into a config; `src/uix/aliases.test.ts` fails if a second copy appears. How an app consumes it: [`docs/consuming.md`](docs/consuming.md).
### Svelte 5 Runes Mode Enforced

@ -80,9 +80,11 @@ npm run format # Auto-format
## Path Aliases
Aliases are kept in sync between `svelte.config.js` and `vite.config.ts`. Source
of truth is `vite.config.ts` (single `aliases` const reused by both top-level
resolve and the server-test project).
Aliases live in ONE module, `uix.aliases.js` at the repo root: `vite.config.ts`,
`svelte.config.js`, `scripts/generate-boot.ts`, `scripts/docs-check.ts` and any
app in the workspace import it (`docs/consuming.md`). Never copy the table. Order
is load-bearing (longer specifier first); `src/uix/aliases.test.ts` guards order,
resolution and the absence of a second copy. The table below is a summary.
| Alias | Target |
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------- |

@ -27,7 +27,7 @@
* 7. Generated vocabularies appendix — docs/canon/vocabularies.md must match
* an in-memory regeneration from the code consts.
* 8. Code-block imports — `$`/`@/` import heads in fenced ts/js/svelte blocks
* must be real repo aliases (extracted from vite.config.ts, never copied)
* must be real repo aliases (imported from uix.aliases.js, never copied)
* and resolve on disk. `$lib` is retired: forbidden in framework-owned
* docs (docs/**, src/uix/**); tolerated in consumer-app examples
* elsewhere. (Born 2026-08-05: the exhaustive opts audit found phantom
@ -54,6 +54,7 @@
import { readFileSync, readdirSync, statSync, existsSync } from 'node:fs';
import { join, dirname, resolve, relative, sep } from 'node:path';
import { UIX_ALIASES } from '../uix.aliases.js';
import { generateVocabulariesDoc } from './docs-vocabularies';
@ -768,27 +769,19 @@ const codeCorpus = liveCorpus.filter((f) => !isAspirational(f));
// ── Invariant 8 — code-block imports resolve ───────────────────────────────
//
// The alias table is EXTRACTED from vite.config.ts (the declared source of
// truth for aliases), never copied — copying it here would recreate the
// disease this script exists to catch.
const ALIASES = new Map<string, string>();
{
const viteSrc = read(join(REPO, 'vite.config.ts'));
for (const m of viteSrc.matchAll(
/['"]([$@][\w./-]*)['"]\s*:\s*resolve\(__dirname,\s*['"]([^'"]+)['"]\)/g
)) {
ALIASES.set(m[1], m[2]);
}
if (ALIASES.size < 10) {
report(
'error',
'I8-imports',
join(REPO, 'vite.config.ts'),
1,
`alias extraction found only ${ALIASES.size} entries — the aliases const changed shape`
);
}
// The alias table is IMPORTED from `uix.aliases.js`, the one module Vite,
// SvelteKit and the boot compiler also read — never copied, never parsed out
// of a config file.
const ALIASES = new Map<string, string>(Object.entries(UIX_ALIASES));
if (ALIASES.size < 10) {
report(
'error',
'I8-imports',
join(REPO, 'uix.aliases.js'),
1,
`the alias table has only ${ALIASES.size} entries — uix.aliases.js changed shape`
);
}
/** SvelteKit's own virtual modules — legal without being repo aliases. */
@ -868,7 +861,7 @@ for (const file of codeCorpus) {
'I8-imports',
file,
line,
`\`${spec}\` — \`${head}\` is not a repo alias (vite.config.ts)`
`\`${spec}\` — \`${head}\` is not a repo alias (uix.aliases.js)`
);
} else if (!specResolves(spec)) {
report(

@ -50,9 +50,9 @@
* `node:crypto` is scaffolding too — it computes the hash here and never
* ships.
*
* Aliases are EXTRACTED from `vite.config.ts` rather than copied — the
* same rule `docs-check.ts` invariant 8 follows, for the same reason: a
* copied table drifts and takes the guard's credibility with it.
* Aliases come from `uix.aliases.js`, the one table Vite, SvelteKit,
* `docs-check.ts` and this compiler all import: the boot resolves every
* specifier exactly as the runtime it must agree with does.
*
* **And it RUNS what it writes.** The text guards are static, and a boot
* that throws on line one passes every one of them: measured, this
@ -83,7 +83,7 @@
*/
import { createHash } from 'node:crypto';
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
import { dirname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { runInNewContext } from 'node:vm';
@ -91,28 +91,13 @@ import { runInNewContext } from 'node:vm';
import { build, type Plugin } from 'esbuild';
import { assertBootScript } from '../src/uix/active-uix/boot/script-guard.ts';
import { resolveUixAliases } from '../uix.aliases.js';
const REPO = resolve(dirname(fileURLToPath(import.meta.url)), '..');
export const BOOT_ENTRY = resolve(REPO, 'src/uix/active-uix/boot/entry.ts');
export const BOOT_OUTPUT = resolve(REPO, 'src/uix/active-uix/generated/boot.js');
export function readViteAliases(): Record<string, string> {
const source = readFileSync(resolve(REPO, 'vite.config.ts'), 'utf8');
const aliases: Record<string, string> = {};
for (const match of source.matchAll(
/['"]([$@][\w./-]*)['"]\s*:\s*resolve\(__dirname,\s*['"]([^'"]+)['"]\)/g
)) {
aliases[match[1]] = resolve(REPO, match[2]);
}
if (Object.keys(aliases).length < 10) {
throw new Error(
`alias extraction found only ${Object.keys(aliases).length} entries — the aliases const in vite.config.ts changed shape`
);
}
return aliases;
}
export interface BuildBootScriptOptions {
/**
* Absolute path of the module that exports `bootPrefsSchema` — the
@ -198,7 +183,7 @@ export async function compileBoot(
target: 'es2020',
minify: true,
legalComments: 'none',
alias: readViteAliases(),
alias: resolveUixAliases(REPO),
plugins:
options.schema === undefined
? [inputCollector(inputs)]

@ -21,7 +21,7 @@ import { join, resolve } from 'node:path';
import { pathToFileURL } from 'node:url';
import { afterAll, describe, expect, it } from 'vitest';
import { readViteAliases } from '../../../../scripts/generate-boot.ts';
import { resolveUixAliases } from '../../../../uix.aliases.js';
const RENDER_ENTRY = resolve('src/uix/active-uix/boot/render.ts');
@ -40,7 +40,7 @@ describe('renderUixBootScript inside a packaged server build', () => {
format: 'esm',
platform: 'node',
outfile,
alias: readViteAliases()
alias: resolveUixAliases(resolve('.'))
});
// The premise of the test, stated rather than assumed: the compiled boot

@ -0,0 +1,68 @@
/**
* The import map exists ONCE (`uix.aliases.js`) and it resolves.
*
* Until 2026-09-16 the table was written twice — in `vite.config.ts` and, by
* hand, in `svelte.config.js` — and two scripts parsed it back out of the Vite
* config with a regex. Every consumer now imports the module, so the guard has
* four jobs: the table points at things that exist, nobody writes a second
* copy, the order the resolvers depend on holds, and SvelteKit's generated
* tsconfig carries every entry.
*/
import { existsSync, readFileSync } from 'node:fs';
import { resolve } from 'node:path';
import { describe, expect, it } from 'vitest';
import { resolveUixAliases, UIX_ALIASES } from '../../uix.aliases.js';
const REPO = resolve('.');
const entries = Object.entries(UIX_ALIASES);
describe('uix.aliases.js — the one import map', () => {
it('has the whole table, and every entry points at something on disk', () => {
// Anti-empty: a table that lost its entries would make every other assertion vacuous.
expect(entries.length).toBeGreaterThanOrEqual(38);
const missing = entries.filter(([, path]) => !existsSync(resolve(REPO, path)));
expect(missing).toEqual([]);
});
it('resolves every entry against the root it is given, in table order', () => {
const resolved = resolveUixAliases(REPO);
expect(Object.keys(resolved)).toEqual(entries.map(([specifier]) => specifier));
for (const [specifier, path] of entries) {
expect(resolved[specifier]).toBe(resolve(REPO, path));
}
});
it('lists a longer specifier before any key that is its prefix', () => {
// Vite and esbuild take the FIRST string alias that matches `key` or
// `key/…`, so `$svrs` listed before `$svrs/auth` would swallow it.
const keys = entries.map(([specifier]) => specifier);
const shadowed: string[] = [];
keys.forEach((key, index) => {
for (const longer of keys.slice(index + 1)) {
if (longer.startsWith(`${key}/`)) shadowed.push(`${key} before ${longer}`);
}
});
expect(shadowed).toEqual([]);
});
it('is not copied back into either config', () => {
for (const config of ['vite.config.ts', 'svelte.config.js']) {
const source = readFileSync(resolve(REPO, config), 'utf8');
expect(source, config).toContain('resolveUixAliases(__dirname)');
expect(source, config).not.toMatch(/resolve\(__dirname,\s*['"]src\//);
}
});
it("reaches SvelteKit's generated tsconfig, entry by entry", () => {
const generated = resolve(REPO, '.svelte-kit/tsconfig.json');
expect(existsSync(generated), 'run `svelte-kit sync` (npm install runs it)').toBe(true);
const { compilerOptions } = JSON.parse(readFileSync(generated, 'utf8')) as {
compilerOptions: { paths: Record<string, string[]> };
};
const absent = entries
.map(([specifier]) => specifier)
.filter((specifier) => compilerOptions.paths[specifier] === undefined);
expect(absent).toEqual([]);
});
});

@ -1,5 +1,6 @@
import adapter from '@sveltejs/adapter-static';
import { resolve, dirname } from 'path';
import { dirname } from 'path';
import { resolveUixAliases } from './uix.aliases.js';
import { fileURLToPath } from 'url';
const __dirname = dirname(fileURLToPath(import.meta.url));
@ -11,66 +12,14 @@ const config = {
files: {
routes: 'web/routes'
},
alias: {
// Sincronizado con vite.config.ts para que TypeScript,
// el compilador de Svelte y Vitest resuelvan el mismo alias.
// ── Runtime artifacts (src/arts) ────────────────────────────────
'$active-app': resolve(__dirname, 'src/arts/active-app'),
'$adom': resolve(__dirname, 'src/arts/adom'),
'$agent': resolve(__dirname, 'src/arts/agent'),
'$auth': resolve(__dirname, 'src/arts/auth'),
'$bus': resolve(__dirname, 'src/arts/bus'),
'$cache': resolve(__dirname, 'src/arts/cache'),
'$clipboard': resolve(__dirname, 'src/arts/clipboard'),
'$color': resolve(__dirname, 'src/arts/color'),
'$connection': resolve(__dirname, 'src/arts/connection'),
'$ethereal': resolve(__dirname, 'src/arts/ethereal'),
'$format': resolve(__dirname, 'src/arts/format'),
'$http': resolve(__dirname, 'src/arts/http'),
'$langs': resolve(__dirname, 'src/arts/langs'),
'$logger': resolve(__dirname, 'src/arts/logger'),
'$motion': resolve(__dirname, 'src/arts/motion'),
'$orca': resolve(__dirname, 'src/arts/orca'),
'$perf': resolve(__dirname, 'src/arts/perf'),
'$perm': resolve(__dirname, 'src/arts/perm'),
'$prefs': resolve(__dirname, 'src/arts/prefs'),
'$scene': resolve(__dirname, 'src/arts/scene'),
'$session': resolve(__dirname, 'src/arts/session'),
'$sium': resolve(__dirname, 'src/arts/sium'),
'$sound': resolve(__dirname, 'src/arts/sound'),
'$storage': resolve(__dirname, 'src/arts/storage'),
'$timer': resolve(__dirname, 'src/arts/timer'),
// ── Pure helpers (src/libs) ─────────────────────────────────────
'$libs': resolve(__dirname, 'src/libs'),
'$locale': resolve(__dirname, 'src/libs/locale'),
'$reactive': resolve(__dirname, 'src/libs/reactive'),
// ── Encapsulated opt-in packs (src/packs) ───────────────────────
'$packs': resolve(__dirname, 'src/packs'),
// ── Server-authoritative engines (src/svrs) ─────────────────────
// The GATE of each engine is a native SvelteKit server-only
// module (`*.server.ts`); the alias points at the file so no
// consumer import changes. Longest specifier first.
'$svrs/auth/testing': resolve(__dirname, 'src/svrs/auth/testing.server.ts'),
'$svrs/auth': resolve(__dirname, 'src/svrs/auth/index.server.ts'),
'$svrs': resolve(__dirname, 'src/svrs'),
// ── UIX layers (src/uix) ────────────────────────────────────────
'$uix': resolve(__dirname, 'src/uix'),
'$active-uix': resolve(__dirname, 'src/uix/active-uix'),
'$soma': resolve(__dirname, 'src/uix/soma'),
// ── Blocks tier (src/uix/blocks) — page-function compositions ───
'$blocks': resolve(__dirname, 'src/uix/blocks'),
// ── Demo controls (src/lib/_demo) ───────────────────────────────
'$demo': resolve(__dirname, 'src/lib/_demo'),
// ── @/ catch-all ────────────────────────────────────────────────
'@': resolve(__dirname, 'src'),
// ONE table (`uix.aliases.js`), shared with vite.config.ts and the scripts.
alias: resolveUixAliases(__dirname),
typescript: {
// Kit's generated include lists routes, lib and src — not the root module
// every tool now imports. Checked like the rest.
config: (config) => {
config.include.push('../uix.aliases.js');
}
},
},
vitePlugin: {

@ -0,0 +1,93 @@
/**
* The framework's import map — ONE table for every tool that resolves a `$…`
* or `@/` specifier: `vite.config.ts` (dev, build, both vitest projects),
* `svelte.config.js` (Kit writes it into `.svelte-kit/tsconfig.json` and hands
* it to Vite), `scripts/generate-boot.ts` (esbuild), `scripts/docs-check.ts`
* (invariant 8), and any app in this workspace that consumes the framework as
* source (`docs/consuming.md`).
*
* It is `.js` with JSDoc, not `.ts`: Node loads `svelte.config.js` with no
* bundler, and that file imports this one.
*
* ORDER IS LOAD-BEARING. Vite and esbuild match a string alias as a prefix
* (`$svrs` also matches `$svrs/auth/testing`) and SvelteKit hands them over in
* insertion order, so a longer specifier must come before any key that is its
* prefix. `src/uix/aliases.test.ts` fails if it does not.
*/
import { resolve } from 'node:path';
/**
* Specifier → path relative to the repository root.
*
* @type {Readonly<Record<string, string>>}
*/
export const UIX_ALIASES = Object.freeze({
// ── Runtime artifacts (src/arts) ──────────────────────────────────────
'$active-app': 'src/arts/active-app',
$adom: 'src/arts/adom',
$agent: 'src/arts/agent',
$auth: 'src/arts/auth',
$bus: 'src/arts/bus',
$cache: 'src/arts/cache',
$clipboard: 'src/arts/clipboard',
$color: 'src/arts/color',
$connection: 'src/arts/connection',
$ethereal: 'src/arts/ethereal',
$format: 'src/arts/format',
$http: 'src/arts/http',
$langs: 'src/arts/langs',
$logger: 'src/arts/logger',
$motion: 'src/arts/motion',
$orca: 'src/arts/orca',
$perf: 'src/arts/perf',
$perm: 'src/arts/perm',
$prefs: 'src/arts/prefs',
$scene: 'src/arts/scene',
$session: 'src/arts/session',
$sium: 'src/arts/sium',
$sound: 'src/arts/sound',
$storage: 'src/arts/storage',
$timer: 'src/arts/timer',
// ── Pure helpers (src/libs) ───────────────────────────────────────────
$libs: 'src/libs',
$locale: 'src/libs/locale',
$reactive: 'src/libs/reactive',
// ── Encapsulated opt-in packs (src/packs) ─────────────────────────────
$packs: 'src/packs',
// ── Server-authoritative engines (src/svrs) ───────────────────────────
// The GATE of each engine is a native SvelteKit server-only module
// (`*.server.ts`), so the alias points at the file and no consumer
// import changes. Longest specifier first: these are prefix matches.
'$svrs/auth/testing': 'src/svrs/auth/testing.server.ts',
'$svrs/auth': 'src/svrs/auth/index.server.ts',
$svrs: 'src/svrs',
// ── UIX layers (src/uix) ──────────────────────────────────────────────
$uix: 'src/uix',
'$active-uix': 'src/uix/active-uix',
$soma: 'src/uix/soma',
// ── Blocks tier (src/uix/blocks) — page-function compositions ─────────
$blocks: 'src/uix/blocks',
// ── Demo controls (src/lib/_demo) — the frozen docs site's widgets ────
$demo: 'src/lib/_demo',
// ── @/ catch-all ──────────────────────────────────────────────────────
'@': 'src'
});
/**
* The same table with absolute paths, in table order.
*
* @param {string} root Absolute path of the repository root.
* @returns {Record<string, string>}
*/
export function resolveUixAliases(root) {
return Object.fromEntries(
Object.entries(UIX_ALIASES).map(([specifier, path]) => [specifier, resolve(root, path)])
);
}

@ -3,70 +3,13 @@ import { defineConfig } from 'vitest/config';
import { playwright } from '@vitest/browser-playwright';
import { sveltekit } from '@sveltejs/kit/vite';
import { fileURLToPath } from 'url';
import { resolve, dirname } from 'path';
import { dirname } from 'path';
import { resolveUixAliases } from './uix.aliases.js';
// __dirname no existe en ESM — se reconstruye así
const __dirname = dirname(fileURLToPath(import.meta.url));
const aliases = {
// ── Runtime artifacts (src/arts) ──────────────────────────────────────
'$active-app': resolve(__dirname, 'src/arts/active-app'),
'$adom': resolve(__dirname, 'src/arts/adom'),
'$agent': resolve(__dirname, 'src/arts/agent'),
'$auth': resolve(__dirname, 'src/arts/auth'),
'$bus': resolve(__dirname, 'src/arts/bus'),
'$cache': resolve(__dirname, 'src/arts/cache'),
'$clipboard': resolve(__dirname, 'src/arts/clipboard'),
'$color': resolve(__dirname, 'src/arts/color'),
'$connection': resolve(__dirname, 'src/arts/connection'),
'$ethereal': resolve(__dirname, 'src/arts/ethereal'),
'$format': resolve(__dirname, 'src/arts/format'),
'$http': resolve(__dirname, 'src/arts/http'),
'$langs': resolve(__dirname, 'src/arts/langs'),
'$logger': resolve(__dirname, 'src/arts/logger'),
'$motion': resolve(__dirname, 'src/arts/motion'),
'$orca': resolve(__dirname, 'src/arts/orca'),
'$perf': resolve(__dirname, 'src/arts/perf'),
'$perm': resolve(__dirname, 'src/arts/perm'),
'$prefs': resolve(__dirname, 'src/arts/prefs'),
'$scene': resolve(__dirname, 'src/arts/scene'),
'$session': resolve(__dirname, 'src/arts/session'),
'$sium': resolve(__dirname, 'src/arts/sium'),
'$sound': resolve(__dirname, 'src/arts/sound'),
'$storage': resolve(__dirname, 'src/arts/storage'),
'$timer': resolve(__dirname, 'src/arts/timer'),
// ── Pure helpers (src/libs) ───────────────────────────────────────────
'$libs': resolve(__dirname, 'src/libs'),
'$locale': resolve(__dirname, 'src/libs/locale'),
'$reactive': resolve(__dirname, 'src/libs/reactive'),
// ── Encapsulated opt-in packs (src/packs) ─────────────────────────────
'$packs': resolve(__dirname, 'src/packs'),
// ── Server-authoritative engines (src/svrs) ───────────────────────────
// The GATE of each engine is a native SvelteKit server-only module
// (`*.server.ts`), so the alias points at the file and no consumer
// import changes. Longest specifier first: these are prefix matches.
'$svrs/auth/testing': resolve(__dirname, 'src/svrs/auth/testing.server.ts'),
'$svrs/auth': resolve(__dirname, 'src/svrs/auth/index.server.ts'),
'$svrs': resolve(__dirname, 'src/svrs'),
// ── UIX layers (src/uix) ──────────────────────────────────────────────
'$uix': resolve(__dirname, 'src/uix'),
'$active-uix': resolve(__dirname, 'src/uix/active-uix'),
'$soma': resolve(__dirname, 'src/uix/soma'),
// ── Blocks tier (src/uix/blocks) — page-function compositions ─────────
'$blocks': resolve(__dirname, 'src/uix/blocks'),
// ── Demo controls (src/lib/_demo) — reusable widgets for the per-component
// demo pages so booleans, enums, text and ranges look the same on every page.
'$demo': resolve(__dirname, 'src/lib/_demo'),
// ── @/ catch-all ──────────────────────────────────────────────────────
'@': resolve(__dirname, 'src'),
};
const aliases = resolveUixAliases(__dirname);
export default defineConfig({
plugins: [tailwindcss(), sveltekit()],

Loading…
Cancel
Save

Powered by TurnKey Linux.