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/test/lazy.test.ts

227 lines
4.9 KiB

eidos: pilot wrapper pattern + doctrinal API conventions Toggle as the eidos pilot: subdirectory layout (recipe + Svelte wrapper + types + index + README) replacing the flat CSS-only form. Pattern is documented in eidos/components/README.md and the toggle README. Shared types live in eidos/lib/types.ts. First export is `Size` (8 values xxs..xxl + full); components narrow with `Extract<Size, ...>` per the per-component-subset doctrine. No `Eidos` prefix on types — module path already conveys the layer. API doctrine: - soma stays compound (Toggle.Provider) for symmetry with multi-part - eidos exports both default + Provider so single-part components accept both `<Toggle>` (ergonomic) and `<Toggle.Provider>` (compound-style consumers) SoundChannel eager-init fixes the autoplay race: AudioContext is created + resumed synchronously on the first user gesture (capture-phase listener registered in the constructor), avoiding the previous race where the first emit() scheduled the resume in a microtask outside the gesture window. Demo page (web/routes/toggle/+page.svelte) restructured so the live preview renders ALWAYS above the tablist — Sema-tab Play buttons can fire on the real toggle. Motion preview amplifies scale ×8 visually only; doctrinal values stay in the <dl>. Conventions 7-13 added to src/docs/sema-implementation-guide.md covering: directory structure, wrapper composition, no Eidos prefix, soma compound vs eidos flat, iconOnly sr-only body, sound eager-init, docs-preview amplification. CLAUDE.md gets a session hand-off block listing where things stand and next concrete steps (migrate switch/collapsible/dialog/drawer/popover/ toast/avatar; wire topbar sound mute to masterGain; rename theme tokens to drop the success/warning/danger fallback aliases). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
import { describe, expect, it } from 'vitest';
import { array, lazy, object, optional, string } from '../core';
import { createSchema } from '../core/schema';
sema/morfo/eidos: typed selector builder + dialog wrapper + emerge color sema architecture - sema-map: 5-channel registry (motion / sound / color / presence / haptic) via SemaChannelSignatures declaration merging; engine stamps data-event-* tokens; flat CSS-style cascade replaces the eventLabel-overrides middle layer. - sema-map: emerge family gains base.color so intent deltas can shift hue / saturation / intensity. Without a base, the resolver was skipping the channel and Dialog open with intent='threat' rendered as neutral blue in the Sema tab visualisation. - types: SEMA_FAMILY_POLICY const drives compile-time + runtime intent requirements per family. Object shape so future per-family policy fields fit alongside. Emerge events MAY now declare intent (canon update — a Dialog confirming threat carries it in its very appearance). - chans: rename vibra→haptic, add HapticChannel V1 (Vibration API); SoundChannel eager-init on first user gesture (autoplay race fix). morfo selector discipline - morfo/selectors.ts (new): semaSelector(morfo, partKebab, matchers?) — type-checked against morfo.parts and morfo.events. Sema cascade rules MUST use it; hand-written strings are an architecture violation that breaks silently when morfo renames a part. - dialog morfo: open carries intent via fromProp; close-cancel / close-dismiss / close-dismiss-outside drop intent (cancellation has no evaluative load); close-save stays hardcoded fulfill (commit fulfils the user's decision regardless of dialog context). eidos dialog migration (4th pilot) - eidos/components/dialog/: full subdirectory wrapper — flat <Dialog> + compound Provider/Trigger/Overlay/Content/Title/Description/Close/Header/ Footer; size + position responsive props; sheet auto-form on narrow viewports; closePosition for the auto-X. - sema/components/dialog.ts: cascade rules use semaSelector(dialogMorfo, 'content', matchers?). Intent block adds character (haptic kind / pattern) but never overrides pitch / gain / contour — those are intent.deltas signature ownership and overriding flattens per-intent perceptual difference. demo controls + dialog page - src/lib/_demo (DemoSwitch / DemoEnum / DemoText / DemoRange) — unified controls reused across all component demos. - web/routes/dialog: live preview always rendered above tabs; per-event Sema tab with independent intent probe; signature visualisation + Play buttons with fallback target chain. documentation - CLAUDE.md: intent.deltas signature ownership rule; eidos drift defense doctrine (types over lint); 2026-05-09 hand-off entry. - morfo/README: Typed selector builder section. - eidos/README: linter section reframed as opt-in safety net for plain CSS recipes; the architectural mechanism is compile-time typing. - sema/README: open channel registry + flat cascade docs + override semantics + intent policy const + selector builder discipline. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
import type { Schema } from '../core/types';
eidos: pilot wrapper pattern + doctrinal API conventions Toggle as the eidos pilot: subdirectory layout (recipe + Svelte wrapper + types + index + README) replacing the flat CSS-only form. Pattern is documented in eidos/components/README.md and the toggle README. Shared types live in eidos/lib/types.ts. First export is `Size` (8 values xxs..xxl + full); components narrow with `Extract<Size, ...>` per the per-component-subset doctrine. No `Eidos` prefix on types — module path already conveys the layer. API doctrine: - soma stays compound (Toggle.Provider) for symmetry with multi-part - eidos exports both default + Provider so single-part components accept both `<Toggle>` (ergonomic) and `<Toggle.Provider>` (compound-style consumers) SoundChannel eager-init fixes the autoplay race: AudioContext is created + resumed synchronously on the first user gesture (capture-phase listener registered in the constructor), avoiding the previous race where the first emit() scheduled the resume in a microtask outside the gesture window. Demo page (web/routes/toggle/+page.svelte) restructured so the live preview renders ALWAYS above the tablist — Sema-tab Play buttons can fire on the real toggle. Motion preview amplifies scale ×8 visually only; doctrinal values stay in the <dl>. Conventions 7-13 added to src/docs/sema-implementation-guide.md covering: directory structure, wrapper composition, no Eidos prefix, soma compound vs eidos flat, iconOnly sr-only body, sound eager-init, docs-preview amplification. CLAUDE.md gets a session hand-off block listing where things stand and next concrete steps (migrate switch/collapsible/dialog/drawer/popover/ toast/avatar; wire topbar sound mute to masterGain; rename theme tokens to drop the success/warning/danger fallback aliases). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
import { SiumAsyncSchemaError } from '../errors';
type TreeNode = {
value: string;
children?: TreeNode[];
};
type Alpha = {
name: string;
beta?: Beta;
};
type Beta = {
label: string;
alpha?: Alpha;
};
describe('lazy()', () => {
it('delegates primitive decode/encode through the cached schema', () => {
const schema = lazy(() => string());
expect(schema.decodeSync('x')).toBe('x');
expect(schema.encode('x')).toBe('x');
});
it('decodes self-referential trees to arbitrary input depth', () => {
const TreeSchema: Schema<TreeNode, TreeNode> = lazy(() =>
object({
value: string(),
children: optional(array(TreeSchema))
})
);
const tree = TreeSchema.decodeSync({
value: 'root',
children: [
{
value: 'branch',
children: [{ value: 'leaf' }]
}
]
});
expect(tree).toEqual({
value: 'root',
children: [
{
value: 'branch',
children: [{ value: 'leaf', children: undefined }]
}
]
});
});
it('supports mutual recursion through lazy forward references', () => {
const AlphaSchema: Schema<Alpha, Alpha> = lazy(() =>
object({
name: string(),
beta: optional(BetaSchema)
})
);
const BetaSchema: Schema<Beta, Beta> = lazy(() =>
object({
label: string(),
alpha: optional(AlphaSchema)
})
);
expect(
AlphaSchema.decodeSync({
name: 'a1',
beta: {
label: 'b1',
alpha: {
name: 'a2'
}
}
})
).toEqual({
name: 'a1',
beta: {
label: 'b1',
alpha: {
name: 'a2',
beta: undefined
}
}
});
});
it('round-trips recursive trees through decode + encode', () => {
const TreeSchema: Schema<TreeNode, TreeNode> = lazy(() =>
object({
value: string(),
children: optional(array(TreeSchema))
})
);
const input = {
value: 'root',
children: [{ value: 'leaf' }]
};
expect(TreeSchema.encode(TreeSchema.decodeSync(input))).toEqual({
value: 'root',
children: [
{
value: 'leaf',
children: undefined
}
]
});
});
it('inherits async from the resolved inner schema', () => {
const schema = lazy(() =>
createSchema({
kind: 'string',
async: true,
decodeImpl: async (input: string) => input,
encode: (value: string) => value
})
);
expect(schema['~sium'].async).toBe(true);
});
it('reports the inner kind transparently after resolution', () => {
const schema = lazy(() => string());
expect(schema['~sium'].kind).toBe('string');
});
it('passes through the inner shape transparently after resolution', () => {
const inner = object({ value: string() });
const schema = lazy(() => inner);
expect(schema['~sium'].shape).toBe(inner['~sium'].shape);
});
it('invokes the factory thunk once and caches the resolved schema', async () => {
let calls = 0;
const schema = lazy(() => {
calls += 1;
return string();
});
expect(schema.decodeSync('a')).toBe('a');
expect(schema.encode('b')).toBe('b');
await expect(schema.validate('c')).resolves.toEqual({ ok: true, value: 'c' });
expect(schema['~sium'].kind).toBe('string');
expect(calls).toBe(1);
});
it('delegates the Standard Schema contract to the resolved inner schema', async () => {
const schema = lazy(() => string());
expect(schema['~standard'].version).toBe(1);
expect(schema['~standard'].vendor).toBe('sium');
await expect(schema['~standard'].validate('ok')).resolves.toEqual({ value: 'ok' });
});
it('preserves validation issue paths from recursive objects', async () => {
const TreeSchema: Schema<TreeNode, TreeNode> = lazy(() =>
object({
value: string(),
children: optional(array(TreeSchema))
})
);
await expect(
TreeSchema.validate({
value: 42 as never,
children: [{ value: 'leaf' }]
})
).resolves.toEqual({
ok: false,
issues: [
{
path: ['value'],
code: 'type',
message: '#?sium.errors.type|Expected {{expected}} but received {{actual}}',
params: {
expected: 'string',
actual: 'number'
}
}
]
});
});
it('resolves recursion based on the input shape rather than schema depth', () => {
const TreeSchema: Schema<TreeNode, TreeNode> = lazy(() =>
object({
value: string(),
children: optional(array(TreeSchema))
})
);
expect(TreeSchema.decodeSync({ value: 'leaf' })).toEqual({
value: 'leaf',
children: undefined
});
});
it('rethrows SiumAsyncSchemaError from decodeSync when the inner schema is async', () => {
const schema = lazy(() =>
createSchema({
kind: 'string',
async: true,
decodeImpl: async (input: string) => input,
encode: (value: string) => value
})
);
expect(() => schema.decodeSync('x')).toThrow(SiumAsyncSchemaError);
});
it('is re-exported by the core barrel', () => {
expect(lazy).toBeTypeOf('function');
});
});

Powered by TurnKey Linux.