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/arts/sium
dev ac5fdca4e7
Guard Sium lazy self resolution
5 months ago
..
_examples eidos: pilot wrapper pattern + doctrinal API conventions 5 months ago
core Guard Sium lazy self resolution 5 months ago
langs Remove legacy component langs catalog 5 months ago
test Guard Sium lazy self resolution 5 months ago
types Refactor Sium domain schema helpers 5 months ago
README.md Guard Sium lazy self resolution 5 months ago
consts.ts eidos: pilot wrapper pattern + doctrinal API conventions 5 months ago
diagnostics.ts eidos: pilot wrapper pattern + doctrinal API conventions 5 months ago
engine-resolver.ts Refactor active uix architecture 5 months ago
engine-sium.ts Refactor active uix architecture 5 months ago
engine-validation.ts eidos: pilot wrapper pattern + doctrinal API conventions 5 months ago
errors.ts eidos: pilot wrapper pattern + doctrinal API conventions 5 months ago
index.ts eidos: pilot wrapper pattern + doctrinal API conventions 5 months ago

README.md

Sium

Sium es la capa de contratos, validacion e introspeccion de Active.

Su objetivo no es competir con Zod, Valibot o ArkType como libreria generalista. Sium existe para que Active tenga un contrato nativo entre datos, validacion, errores traducibles, tipos de dominio y UI auto-generada.

Este archivo es la unica documentacion Markdown del modulo. Si cambia el comportamiento de Sium, actualiza este README en lugar de crear documentos paralelos.

Sium no forma parte de active-app core

createActiveApp(...) deliberadamente no construye Sium. La validacion es page-scoped: solo las paginas con formularios la necesitan. Cargar todo el core de schemas/types/issues en paginas sin formularios serìa coste innecesario.

El patron estandar es una linea al inicio del modulo de la pagina:

import { createEngineSium } from '$sium';
const sium = createEngineSium({ langs: App.langs, logger: App.logger });

Asi cada formulario obtiene un engine afinado a sus necesidades (logger category, contexto de validacion, etc.) y comparte la traduccion de issues + el bus de logs con la app.

Estado

Core, tipos de dominio, lenguajes, Standard Schema e introspeccion estan activos.

El adapter svelte/ todavia no esta portado en este arbol. Por eso la suite recomendada para Sium omite la integracion Svelte hasta que esa capa se migre.

Importaciones

Sium expone una fachada publica y barrels tematicos.

import { createEngineSium } from '$sium';

const sium = createEngineSium();

Si quieres traducciones activas, inyecta el engine de traducciones:

import { createEngineSium, siumLangs } from '$sium';
import { createEngineLangs } from '$langs';

const langs = createEngineLangs({ sium: siumLangs }, 'es');
const sium = createEngineSium({ langs });
import { object, pipe, string, email, min } from '$sium/core';
import { colorValue, dateValue, timeValue } from '$sium/types';
import { siumLangs, ISSUE_CODES, resolve } from '$sium/langs';

createSchema() es interno. La API publica debe entrar por createEngineSium() o por los barrels core, types y langs.

El directorio _examples/ contiene esquemas de referencia (formularios de login, signup, settings, etc.) usados por /test/sium y citados en este README. No es API publica — el guion bajo inicial es la convencion que señala material interno/dev. No esta re-exportado por sium/index.ts y ningun consumidor lo importa. Si encuentras un caso real para uno de esos esquemas, copialo al consumidor; promoverlo a sium publico significaria soporte indefinido para una decision de UX que no pertenece al runtime.

Idea Central

Un schema Sium es un codec bidireccional con validacion, introspeccion y compatibilidad Standard Schema.

interface Schema<I = unknown, O = I> {
	readonly '~standard': StandardSchemaV1.Props<I, O>;
	readonly '~sium': SiumIntrospection;

	decode(input: I, ctx?: Partial<Ctx>): Promise<O>;
	decodeSync(input: I, ctx?: Partial<Ctx>): O;
	encode(value: O): I;

	validate(input: I, ctx?: Partial<Ctx>): Promise<Result<O>>;
	validateSync(input: I, ctx?: Partial<Ctx>): Result<O>;
}

decode valida y transforma. Si falla, lanza SiumValidationError.

validate envuelve decode y devuelve { ok: true, value } o { ok: false, issues }.

createEngineSium({ logger }).validate(schema, input) hace lo mismo y, si falla, registra un evento debug usando LOGGER_CATEGORY y SIUM_ERRORS.VALIDATION_FAILED, con issueCount e issues.

encode invierte la salida hacia la forma de entrada. En transforms de una sola direccion actua como identidad para esa parte; usa codec() cuando necesites reversibilidad real.

~standard permite usar schemas Sium en cualquier consumidor de Standard Schema v1.

~sium contiene metadata para UI, debug, docs y herramientas.

Por Que Existe

Sium cubre cuatro huecos que las librerias externas no resuelven de forma nativa dentro de Active.

  • Tipos de dominio: ColorValue, DateValue, TimeValue y sus segmentos ya existen en Active.
  • UI metadata: meta.widget, meta.channel, labels y opciones viajan dentro del schema.
  • Errores traducibles: los issues llevan code, message idlangref, params y path.
  • Interop: cada schema tambien habla Standard Schema v1.

Arquitectura

src/arts/sium/
  index.ts          fachada publica
  engine-sium.ts   createEngineSium()
  core/            kernel puro TypeScript
  types/           tipos de dominio sobre $libs/color y $libs/days
  langs/           catalogo de mensajes y fallback de interpolacion
  svelte/          adapter Svelte pendiente de port, no presente ahora
  examples/        schemas de ejemplo ejecutables/importables
  test/            suite Vitest

Reglas de dependencia:

  • core/ no importa Svelte, UI, days, color ni langs.
  • types/ puede importar core/, $libs/color y $libs/days.
  • langs/ define el catalogo siumLangs y un fallback minimo de interpolacion.
  • svelte/ es adapter opcional y no debe contaminar el core.
  • engine-sium.ts compone la fachada publica.

Guia Rapida

Usar La Fachada

import { createEngineSium } from '$sium';

const s = createEngineSium();

export const User = s.object({
	name: s.pipe(s.string(), s.min(2), s.max(40)),
	email: s.pipe(s.string(), s.email()),
	age: s.optional(s.pipe(s.number(), s.integer(), s.min(0)))
});

const result = await User.validate({
	name: 'Ada',
	email: 'ada@example.com',
	age: 37
});

Con integracion:

const s = createEngineSium({ langs, logger });

s.resolveIssue({
	path: ['email'],
	code: 'type',
	message: '#?sium.errors.type|Expected {{expected}} but received {{actual}}',
	params: { expected: 'string', actual: 'number' }
});

Si langs existe, resolveIssue() delega en EngineLangs. Si no existe, usa el fallback local de $sium/langs.resolve.

En desarrollo tambien puedes validar a traves del engine para obtener logs debug cuando hay issues:

const result = await s.validate(User, input);
const syncResult = s.validateSync(User, input);

Estos wrappers no cambian el resultado de schema.validate(). Solo emiten un log debug en la categoria exportada LOGGER_CATEGORY cuando la validacion falla y has inyectado logger.

Usar Imports Granulares

import { object, optional, pipe, string, number, integer, min, email } from '$sium/core';

export const User = object({
	name: pipe(string(), min(2)),
	email: pipe(string(), email()),
	age: optional(pipe(number(), integer(), min(0)))
});

Validar

const result = await User.validate(input);

if (result.ok) {
	result.value;
} else {
	result.issues;
}

Decodificar Con Excepcion

import { SiumValidationError } from '$sium/core';

try {
	const value = await User.decode(input);
} catch (error) {
	if (error instanceof SiumValidationError) {
		console.log(error.issues);
	}
}

Codificar

const QueryNumber = s.pipe(
	s.string(),
	s.codec(
		(value) => Number(value),
		(value) => String(value)
	)
);

await QueryNumber.decode('42'); // 42
QueryNumber.encode(42); // '42'

API De Core

Primitivos

string();
number();
boolean();
literal('admin');
enumOf(['admin', 'user', 'guest'] as const);

Modificadores

optional(string());
nullable(string());
defaulted(number(), () => 18);

optional permite undefined.

nullable permite null.

defaulted aplica el valor por defecto en decode cuando el input es undefined.

Si el schema interno es async, los wrappers conservan el short-circuit sincronico del sentinel: optional(asyncSchema).decodeSync(undefined), nullable(asyncSchema).decodeSync(null) y defaulted(asyncSchema).decodeSync(undefined) no entran al schema interno. Cualquier valor que si necesite validar el inner async lanza SiumAsyncSchemaError en APIs sync.

Combinadores

object({ name: string() });
array(string());
union(string(), number());

discriminated('kind', [
	object({ kind: literal('text'), value: string() }),
	object({ kind: literal('count'), value: number() })
]);

object() usa unknownKeys: 'strip' por defecto.

object({ name: string() }, { unknownKeys: 'strict' });
object({ name: string() }, { unknownKeys: 'passthrough' });

Pipe Y Steps

pipe(string(), min(3), max(40), meta({ label: 'Name', widget: 'text-field' }));

Steps disponibles:

  • refine(check, fail, opts?)
  • transform(fn, opts?)
  • codec(decode, encode, opts?)
  • meta(annotations)

Si una funcion normal devuelve Promise, declara el step como async.

const UniqueEmail = pipe(
	string(),
	refine(
		(value) => api.emailIsFree(value),
		{
			code: 'custom',
			message: '#?sium.errors.custom|Email is already in use'
		},
		{ async: true }
	)
);

Refines Incluidos

min(2);
max(40);
length(8);
regex(/^[a-z0-9-]+$/);
email();
url();
integer();

min y max funcionan con numeros, strings y arrays. Emiten codigos distintos para valor numerico y longitud.

Tipos De Dominio

Los tipos de dominio viven en $sium/types y envuelven las librerias canonicas $libs/color y $libs/days.

Color

import {
	colorValue,
	red,
	green,
	blue,
	hue,
	saturation,
	lightness,
	brightness,
	alpha
} from '$sium/types';

const Color = colorValue();
const Red = red();

colorValue() valida un ColorValue normalizado con hex, rgb, hsl y hsv.

Los segmentos RGB son enteros 0..255.

Los segmentos HSL/HSV son numeros en sus rangos naturales.

alpha() valida 0..1.

Fecha

import { dateValue, dateRange, coerceDate, year, month, day } from '$sium/types';

const DateOnly = dateValue();
const Range = dateRange();
const FromNativeDate = coerceDate();

dateValue() acepta DateValue de $libs/days.

coerceDate() acepta Date | DateValue y devuelve DateValue.

dateRange() valida { start, end } y rechaza rangos donde start > end.

Hora

import { timeValue, timeRange, hour, minute, second, dayPeriod } from '$sium/types';

const TimeOnly = timeValue();
const Range = timeRange();

timeRange() valida { start, end } y rechaza rangos donde start > end.

hour({ cycle: 12 }) usa rango 1..12.

hour() usa rango 0..23.

Introspeccion

Cada schema expone schema['~sium'].

const schema = pipe(string(), email(), meta({ label: 'Email', widget: 'email-field' }));

schema['~sium'];

Forma simplificada:

{
  kind: 'string',
  wrappers: [],
  effects: ['refine'],
  async: false,
  meta: { label: 'Email', widget: 'email-field' },
  shape: undefined
}

Herramientas incluidas:

serializeSchema(schema);
countLeafFields(schema);
walkSchema(schema, (node, path) => {});

walkSchema() recorre objects, arrays y wrappers. Las variantes de union/discriminated no se expanden automaticamente para evitar UI ambigua.

Errores E i18n

Un issue tiene esta forma:

type Issue = {
	path: ReadonlyArray<string | number>;
	code: string;
	message: string;
	params?: Record<string, unknown>;
};

message usa idlangref:

#?sium.errors.type|Expected {{expected}} but received {{actual}}

Los mensajes base viven en siumLangs.

import { siumLangs, resolve } from '$sium/langs';

resolve('#?sium.errors.type|Invalid', {
	expected: 'string',
	actual: 'number'
});

resolve() de $sium/langs no traduce por locale. Es solo el fallback local de interpolacion cuando no hay EngineLangs.

Para traduccion real:

const sium = createEngineSium({ langs });
const text = sium.resolveIssue(issue, 'es');

El engine expone langSchema como nombre historico para acceder al catalogo estatico si lo necesitas:

sium.langSchema === siumLangs;

Codigos estables actuales:

type
literal
enum
min_value
max_value
min_length
max_length
length
regex
email
url
integer
custom
range_order
unknown_keys
union_no_match
discriminated_missing_key
discriminated_unknown_value

Contexto De Validacion

Los callbacks de refine, transform y codec reciben ctx.

type Ctx = {
	path: ReadonlyArray<string | number>;
	rootInput: unknown;
	rootValue?: unknown;
	locale?: string;
	meta?: Record<string, unknown>;
};

ctx.path es absoluto. En object({ user: object({ name }) }), un refine de name recibe ['user', 'name'].

ctx.rootInput es el input original.

ctx.rootValue es passthrough opcional. Sium lo conserva si el caller o un adapter lo pasan en el contexto, pero el core no lo sintetiza automaticamente.

Para validaciones cross-field sobre input de usuario, usa ctx.rootInput. Para validaciones cross-field sobre datos ya decodificados, coloca un refine() sobre el schema object() y usa el objeto decodificado que recibe ese refine.

Ejemplos

Login

import { object, pipe, string, email, min, meta } from '$sium/core';

export const Login = object({
	email: pipe(string(), email(), meta({ label: 'Email' })),
	password: pipe(string(), min(8), meta({ label: 'Password' }))
});

Signup Con Confirmacion

import { boolean, object, optional, pipe, refine, string, meta } from '$sium/core';

export const Signup = pipe(
	object({
		email: pipe(string(), meta({ label: 'Email' })),
		password: pipe(string(), meta({ label: 'Password' })),
		confirmPassword: pipe(string(), meta({ label: 'Confirm password' })),
		acceptTerms: pipe(
			boolean(),
			refine((value) => value === true, {
				code: 'custom',
				message: '#?sium.errors.custom|You must accept the terms'
			}),
			meta({ label: 'Accept terms' })
		),
		newsletter: pipe(optional(boolean()), meta({ label: 'Newsletter' }))
	}),
	refine((value) => value.password === value.confirmPassword, {
		code: 'custom',
		message: '#?sium.errors.custom|Passwords do not match'
	})
);

Perfil Con Fecha Y Color

import { object, pipe, string, email, min, max, meta } from '$sium/core';
import { colorValue, dateValue } from '$sium/types';

export const Profile = object({
	name: pipe(string(), min(2), max(40), meta({ label: 'Name' })),
	email: pipe(string(), email(), meta({ label: 'Email' })),
	birthDate: pipe(dateValue(), meta({ label: 'Birth date', widget: 'date-field' })),
	favoriteColor: pipe(colorValue(), meta({ label: 'Favorite color', widget: 'color-picker' }))
});

Reserva Con Rango

import { integer, max, meta, min, number, object, optional, pipe, string } from '$sium/core';
import { dateRange, timeValue } from '$sium/types';

export const Booking = object({
	range: pipe(dateRange(), meta({ label: 'Date range' })),
	checkInTime: pipe(timeValue(), meta({ label: 'Check-in time' })),
	guests: pipe(number(), integer(), min(1), max(10), meta({ label: 'Guests' })),
	notes: pipe(optional(string()), meta({ label: 'Notes' }))
});

Discriminated Union

import { discriminated, literal, number, object, string } from '$sium/core';

export const Event = discriminated('kind', [
	object({
		kind: literal('text'),
		message: string()
	}),
	object({
		kind: literal('score'),
		value: number()
	})
]);

Recursion Con Lazy

import { array, lazy, object, optional, string } from '$sium/core';
import type { Schema } from '$sium/core';

type TreeNode = {
	label: string;
	children?: TreeNode[];
};

export const Tree: Schema<TreeNode> = lazy(() =>
	object({
		label: string(),
		children: optional(array(Tree))
	})
);

lazy() protege errores de construccion del schema: si el thunk intenta usar el propio proxy mientras resuelve, o devuelve ese mismo proxy como schema final, lanza SiumLazyResolvingError. Esto no es deteccion de ciclos en datos de usuario. Si pasas un objeto runtime autociclico a un schema recursivo, el decode/encode puede seguir desbordando la pila; valida esos datos antes o anade una capa de normalizacion especifica del dominio.

Transform Y Codec

import { codec, pipe, string, transform } from '$sium/core';

export const TrimmedLower = pipe(
	string(),
	transform((value) => value.trim().toLowerCase())
);

export const NumberFromString = pipe(
	string(),
	codec(
		(value) => Number(value),
		(value) => String(value)
	)
);

Validacion Async

import { pipe, refine, string } from '$sium/core';

export const UniqueUsername = pipe(
	string(),
	refine(
		(value) => fetch(`/api/users/${value}`).then((res) => res.status === 404),
		{
			code: 'custom',
			message: '#?sium.errors.custom|Username is already taken'
		},
		{ async: true }
	)
);

Integracion Svelte

La futura carpeta svelte/ representara el adapter para conectar Sium con providers, formularios y resolucion reactiva de errores.

Estado actual:

  • El core puede usarse sin Svelte.
  • Los schemas exponen ~standard, asi que pueden conectarse a consumidores Standard Schema.
  • El adapter svelte/ no esta presente en este arbol porque depende de piezas UI pendientes de port.
  • Hasta completar esa migracion, las pruebas y auditorias de Sium deben omitir src/arts/sium/svelte.

Recetas Rapidas

UUID v4

const UUID = pipe(
	string(),
	regex(/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i)
);

Slug

const Slug = pipe(string(), min(1), max(80), regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/));

Password Fuerte

const StrongPassword = pipe(
	string(),
	min(8),
	refine((value) => /[a-z]/.test(value), {
		code: 'custom',
		message: '#?sium.errors.custom|Password needs a lowercase letter'
	}),
	refine((value) => /[A-Z]/.test(value), {
		code: 'custom',
		message: '#?sium.errors.custom|Password needs an uppercase letter'
	}),
	refine((value) => /\d/.test(value), {
		code: 'custom',
		message: '#?sium.errors.custom|Password needs a number'
	})
);

HTTPS URL

const HttpsUrl = pipe(
	string(),
	url(),
	refine((value) => new URL(value).protocol === 'https:', {
		code: 'custom',
		message: '#?sium.errors.custom|URL must use HTTPS'
	})
);

File Upload

const ImageFile = refine<File>((file) => file.size <= 2_000_000 && file.type.startsWith('image/'), {
	code: 'custom',
	message: '#?sium.errors.custom|File must be an image under 2 MB'
});

Troubleshooting

SiumValidationError

Lanzado por decode cuando hay errores de validacion.

if (error instanceof SiumValidationError) {
	error.issues;
}

SiumAsyncSchemaError

Lanzado por decodeSync o validateSync cuando la ruta de validacion necesita steps async. Los wrappers de sentinel pueden devolver sincronamente si no consultan el inner async (optional(undefined), nullable(null), defaulted(undefined)).

Solucion: usa await schema.decode(...) o await schema.validate(...).

Un callback devuelve Promise pero ~sium.async es false

JavaScript no permite detectar una funcion normal que devuelve Promise sin ejecutarla. Declara el step con { async: true }.

refine((value) => Promise.resolve(Boolean(value)), { code: 'custom' }, { async: true });

El path de un error no coincide

Los errores emitidos dentro de schemas hijos deben usar ctx.path. Sium pasa paths absolutos a hijos de object y array.

npm run check falla en sium/svelte

Es esperado mientras el provider UI no este portado. El core de Sium se valida con la suite sin Svelte.

Verificacion Recomendada

npx vitest run src/arts/sium/test/barrel.test.ts src/arts/sium/test/engine-sium.test.ts src/arts/sium/test/combinators.test.ts src/arts/sium/test/date.test.ts src/arts/sium/test/time.test.ts src/arts/sium/test/color.test.ts src/arts/sium/test/pipe.test.ts src/arts/sium/test/primitives.test.ts src/arts/sium/test/refines.test.ts src/arts/sium/test/schema.test.ts src/arts/sium/test/standard-schema.test.ts src/arts/sium/test/modifiers.test.ts src/arts/sium/test/issue.test.ts src/arts/sium/test/lazy.test.ts src/arts/sium/test/introspect.test.ts src/arts/sium/test/langs.test.ts src/arts/sium/test/resolver.test.ts
npx vitest run src/libs/days/test/parse.test.ts src/libs/days/test/queries.test.ts src/libs/color/test/guards.test.ts

Criterio Para Cambios Futuros

Antes de ampliar Sium, comprueba que el cambio cumple estas reglas.

  • El core sigue siendo TypeScript puro.
  • La API publica no expone createSchema().
  • Cada schema mantiene decode, encode, validate, ~standard y ~sium.
  • Los errores son datos estructurados, no strings finales.
  • Los tipos de dominio reutilizan $libs/color y $libs/days.
  • La documentacion se actualiza en este unico README.

Powered by TurnKey Linux.