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/README.md

25 KiB

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.

Sium no tiene adapter Svelte propio, por diseno: la integracion con UI es agnostica al validador. Los formularios (createForm / Form.Provider) consumen cualquier schema via ~standard, Form.AutoFields auto-renderiza via la introspeccion ~sium, y langs/logger se inyectan por createEngineSium({ langs, logger }). El core sigue siendo TypeScript puro.

Novedades 2.0

Ampliacion alineada a lo que el ecosistema Active necesita (formularios, storage, http). NO busca paridad con Zod/Valibot.

  • Refines de formato (string): uuid(), slug(), datetime() (ISO 8601), ipv4().
  • Refines numericos: finite() (rechaza Infinity/-Infinity/NaN), positive() (> 0), nonnegative() (>= 0), multipleOf(n).
  • Coercion: coerceNumber(), coerceBoolean(), coerceString() — hermanos generales de coerceDate() para entradas sueltas (inputs HTML, query strings).
  • Standard Schema sincrono: schema['~standard'].validate(x) resuelve de forma SINCRONA cuando el schema no tiene pasos async (devuelve el Result directo en vez de Promise). SS v1 permite Result | Promise<Result>; esto elimina el branch async + el workaround untrack en los consumidores (form-core, storage) para schemas sincronos.
  • Transforms de string: trim(), toLowerCase(), toUpperCase() para normalizar formularios.
  • Modificador nullish(): acepta undefined y null (= optional + nullable).
  • Combinadores tuple() y record(): tupla heterogenea de longitud fija + diccionario de claves arbitrarias. Amplian el vocabulario SchemaKind ('tuple'/'record'); Form.AutoFields los degrada con un aviso "not supported yet".
  • Utilidades de object: pick / omit / partial / extend / merge — derivan un object() reusando los campos del original, con tipos precisos via Pick/Omit/Partial.
  • API depurada: dateValue() / timeValue() ya no aceptan opts (eran ignorados). Para validar granularidad/calendario, compon un refine sobre el valor.
  • 8 nuevos issue codes (uuid, slug, datetime, ipv4, finite, positive, nonnegative, multiple_of) con traduccion es/en.

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, emite un diagnóstico SIUM_DIAGNOSTIC_EVENTS.VALIDATION_FAILED a nivel debug (categoría SIUM_MODULE), 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. Los schemas sincronos resuelven de forma sincrona (validate devuelve el Result directo); solo los que tienen pasos async devuelven Promise.

~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()
  engine-resolver.ts, engine-validation.ts
                    resolucion de mensajes idlangref + runtime de validate/validateSync
  consts.ts, errors.ts, diagnostics.ts
                    SIUM_MODULE + errores tipados (CodeError) + catalogo de logs
  core/             kernel puro TypeScript (schemas, combinators, pipe)
  types/            tipos de dominio sobre $libs/color y $libs/days
  langs/            catalogo de mensajes traducibles + fallback de interpolacion
  _examples/        schemas de ejemplo (dev-only, fuera del barril publico)
  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.
  • 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());
nullish(string());
defaulted(number(), () => 18);

optional permite undefined.

nullable permite null.

nullish permite undefined y null (combina optional + nullable).

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());
tuple(string(), number(), boolean());
record(number());
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' });

tuple(...) valida posicionalmente y exige la longitud exacta (issue tuple_length). record(schema) valida un objeto plano de claves arbitrarias cuyos valores comparten schema.

Utilidades de object

Derivan un nuevo object() a partir de otro, reusando sus campos:

pick(User, ['id', 'name']); // solo esos campos
omit(User, ['password']); // todos menos esos
partial(User); // todos los campos opcionales
extend(User, { role: string() }); // anade / sobrescribe campos
merge(Base, Overrides); // combina dos object (gana el segundo)

Los tipos resultantes se derivan con Pick / Omit / Partial sobre el schema fuente, asi que siguen siendo precisos. Pasar un schema que no sea object() lanza SiumObjectUtilError.

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();

// formato (string)
uuid();
slug();
datetime(); // ISO 8601
ipv4();

// numericos
finite(); // rechaza Infinity / -Infinity / NaN
positive(); // > 0
nonnegative(); // >= 0
multipleOf(0.5);

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

finite() cubre el hueco de number(), que acepta Infinity por diseno. positive() no implica finite(): compon ambos para rechazar tambien Infinity.

Coercion

Para entradas sueltas (inputs HTML, query strings, FormData) que llegan como string y necesitan convertirse a un primitivo:

coerceNumber(); // string | number -> number (rechaza '' y NaN; Infinity pasa, compon con finite())
coerceBoolean(); // string | number | boolean -> boolean ('true/1/yes/on' vs 'false/0/no/off')
coerceString(); // string | number | boolean -> string

Son los hermanos generales de coerceDate(). Cada uno reporta el kind de SALIDA (coerceNumber() -> number), asi que Form.AutoFields renderiza el widget correcto.

Transforms De String

Normalizadores que cambian el valor en decode (son transform, sin encode). Aplica despues de string():

pipe(string(), trim()); // recorta espacios
pipe(string(), toLowerCase()); // minusculas
pipe(string(), toUpperCase()); // mayusculas
pipe(string(), trim(), toLowerCase()); // email normalizado

El kind sigue siendo string, asi que son transparentes para Form.AutoFields.

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

Sium no tiene adapter Svelte propio: la integracion es agnostica al validador y vive en la capa de formularios, no en sium.

  • El core es TypeScript puro y se usa sin Svelte.
  • Los schemas exponen ~standard (Standard Schema v1), asi que createForm / Form.Provider los validan reactivamente igual que a Zod/Valibot/ArkType. Los schemas sincronos resuelven de forma sincrona.
  • Form.AutoFields auto-renderiza los campos leyendo la introspeccion ~sium (kind, wrappers, meta.widget, …).
  • Los issues idlangref (#?sium.errors.*) los traduce el servicio langs compartido, reactivo al locale activo.
  • langs/logger se inyectan en sium por createEngineSium({ langs, logger }). sium es page-scoped: lo declara la app, no el runtime UIX.

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)). Los combinadores siguen la misma regla por ruta: por ejemplo, object({ a: optional(asyncSchema) }).decodeSync({ a: undefined }) puede completarse; { a: 'x' } lanza porque ya necesita validar el inner async.

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.

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 src/arts/sium/test/coerce.test.ts src/arts/sium/test/refines-2.test.ts src/arts/sium/test/transforms.test.ts src/arts/sium/test/tuple.test.ts src/arts/sium/test/record.test.ts src/arts/sium/test/object-utils.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.