@formos/kernel 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,433 @@
1
+ # @formos/kernel
2
+
3
+ Headless, framework-agnostic form engine for the Formos form builder.
4
+
5
+ ## Purpose
6
+
7
+ `@formos/kernel` provides the **runtime execution layer** for forms defined by `@formos/schema`. It's a completely headless engine that manages all form logic without any UI concerns - no React, no DOM, no browser APIs.
8
+
9
+ Think of it as the **"brain"** of the form - it handles state, validation orchestration, effect execution, and multi-step navigation, but leaves rendering entirely to UI packages.
10
+
11
+ ## Responsibilities
12
+
13
+ This package is responsible for:
14
+
15
+ - **State Management**: Maintaining form values, errors, touched fields, and dirty state
16
+ - **Validation Orchestration**: Coordinating validation execution with pluggable validator adapters
17
+ - **Effect Execution**: Triggering side effects (field syncing, API calls) via effect executors
18
+ - **Conditional Logic Evaluation**: Determining field visibility and required status dynamically
19
+ - **Multi-Step Flow Control**: Managing step navigation with validation guards
20
+ - **Lifecycle Management**: Handling submit, reset, and field value updates
21
+
22
+ ## What This Package Does NOT Do
23
+
24
+ ❌ **NO UI Rendering**
25
+ - Does not create DOM elements
26
+ - Does not provide React components
27
+ - Does not handle user interactions directly
28
+
29
+ ❌ **NO Framework Dependencies**
30
+ - No React, Vue, Svelte, or any UI framework
31
+ - No browser-specific APIs (localStorage, fetch, etc.)
32
+ - Completely runtime-agnostic
33
+
34
+ ❌ **NO Built-in Validation Libraries**
35
+ - Does not include Zod, Yup, or any validators
36
+ - Uses **adapter pattern** - you provide validators
37
+ - Same for effects - no built-in API clients or transformers
38
+
39
+ ❌ **NO Schema Definition**
40
+ - Does not define schema types (that's `@formos/schema`)
41
+ - Consumes `NormalizedSchemaV1` as input
42
+ - Does not modify or mutate schemas
43
+
44
+ **Think of it as:** The engine that executes the schema's instructions, not the schema itself.
45
+
46
+ ## Stability Guarantee
47
+
48
+ ### FormEngine API is STABLE and FINAL ✅
49
+
50
+ The `FormEngine` interface is committed to backward compatibility:
51
+ - Method signatures won't change
52
+ - Return types remain consistent
53
+ - Behavior is predictable and documented
54
+
55
+ This guarantee enables:
56
+ - **Safe updates**: Upgrade the kernel without UI changes
57
+ - **Framework flexibility**: Build UI packages for React, Vue, Svelte
58
+ - **Long-term reliability**: Code written today works tomorrow
59
+
60
+ ### What Can Change
61
+
62
+ ✅ **Internal implementations** can be optimized
63
+ ✅ **New optional methods** may be added to `FormEngine`
64
+ ✅ **Performance improvements** without API changes
65
+ ✅ **Bug fixes** that don't alter public behavior
66
+
67
+ ### Adapter Contract
68
+
69
+ The adapter types (`ValidatorExecutor`, `EffectExecutor`) are **stable**:
70
+ - Validators return `string | null` (error or valid)
71
+ - Effects receive context with `getValue` and `setValue`
72
+ - These interfaces won't have breaking changes
73
+
74
+ ## Installation
75
+
76
+ ```bash
77
+ pnpm add @formos/kernel @formos/schema
78
+ ```
79
+
80
+ ## Basic Usage
81
+
82
+ ### Create a Form Engine
83
+
84
+ ```typescript
85
+ import { createFormEngine } from '@formos/kernel';
86
+ import { normalizeSchema, type FormSchemaV1 } from '@formos/schema';
87
+
88
+ // 1. Define schema
89
+ const schema: FormSchemaV1 = {
90
+ version: 'v1',
91
+ fields: [
92
+ {
93
+ name: 'email',
94
+ type: 'email',
95
+ required: true,
96
+ validations: [
97
+ {
98
+ trigger: ['onChange', 'onBlur'],
99
+ validator: 'email',
100
+ rule: {},
101
+ message: 'Invalid email'
102
+ }
103
+ ]
104
+ },
105
+ { name: 'password', type: 'password', required: true }
106
+ ]
107
+ };
108
+
109
+ // 2. Normalize schema
110
+ const normalized = normalizeSchema(schema);
111
+
112
+ // 3. Create engine with validators
113
+ const engine = createFormEngine(normalized, {
114
+ validators: {
115
+ required: (value) => value ? null : 'Required',
116
+ email: (value) => {
117
+ const regex = /\S+@\S+\.\S+/;
118
+ return regex.test(String(value)) ? null : 'Invalid email';
119
+ }
120
+ },
121
+ onSubmit: async (values) => {
122
+ console.log('Form submitted:', values);
123
+ }
124
+ });
125
+ ```
126
+
127
+ ### Get and Set Values
128
+
129
+ ```typescript
130
+ // Get field value
131
+ const email = engine.getValue('email');
132
+ console.log('Email:', email); // undefined initially
133
+
134
+ // Set field value (triggers validation + effects)
135
+ await engine.setValue('email', 'user@example.com');
136
+
137
+ // Set without validation
138
+ await engine.setValue('email', 'user@example.com', {
139
+ trigger: 'manual',
140
+ skipValidation: true
141
+ });
142
+ ```
143
+
144
+ ### Validation
145
+
146
+ ```typescript
147
+ // Validate single field
148
+ const isEmailValid = await engine.validate('email', 'onChange');
149
+
150
+ // Validate entire form
151
+ const isFormValid = await engine.validate();
152
+
153
+ // Check validation state
154
+ const emailError = engine.getError('email');
155
+ const allErrors = engine.getErrors();
156
+ const isValid = engine.isValid();
157
+
158
+ // Manual error management
159
+ engine.setError('email', 'Custom error message');
160
+ engine.clearError('email');
161
+ ```
162
+
163
+ ### Form State
164
+
165
+ ```typescript
166
+ // Check if field was modified
167
+ const isEmailDirty = engine.isDirty('email');
168
+ const isFormDirty = engine.isDirty(); // Any field dirty?
169
+
170
+ // Check if field was touched (focused and blurred)
171
+ const wasEmailTouched = engine.isTouched('email');
172
+
173
+ // Mark field as touched
174
+ engine.markTouched('email');
175
+ ```
176
+
177
+ ### Submit and Reset
178
+
179
+ ```typescript
180
+ // Submit form (validates all fields first)
181
+ const success = await engine.submit();
182
+ if (success) {
183
+ console.log('Form submitted successfully');
184
+ } else {
185
+ console.log('Validation failed:', engine.getErrors());
186
+ }
187
+
188
+ // Reset form to initial state
189
+ engine.reset();
190
+ ```
191
+
192
+ ## Multi-Step Forms
193
+
194
+ ```typescript
195
+ const multiStepSchema: FormSchemaV1 = {
196
+ version: 'v1',
197
+ fields: [
198
+ { name: 'firstName', type: 'text' },
199
+ { name: 'lastName', type: 'text' },
200
+ { name: 'email', type: 'email' },
201
+ { name: 'phone', type: 'tel' }
202
+ ],
203
+ steps: [
204
+ { id: 'personal', title: 'Personal Info', fields: ['firstName', 'lastName'] },
205
+ { id: 'contact', title: 'Contact', fields: ['email', 'phone'] }
206
+ ]
207
+ };
208
+
209
+ const normalized = normalizeSchema(multiStepSchema);
210
+ const engine = createFormEngine(normalized, { validators });
211
+
212
+ // Step navigation (only available for multi-step forms)
213
+ const currentStep = engine.getCurrentStep?.(); // 0
214
+ const totalSteps = engine.getTotalSteps?.(); // 2
215
+
216
+ // Go to next step (validates current step first)
217
+ const canProceed = await engine.nextStep?.();
218
+ if (!canProceed) {
219
+ console.log('Fix errors before proceeding');
220
+ }
221
+
222
+ // Go to previous step (no validation)
223
+ engine.prevStep?.();
224
+
225
+ // Jump to specific step (validates current step first)
226
+ const moved = await engine.goToStep?.(1);
227
+
228
+ // Check if can go to step
229
+ const canGo = engine.canGoToStep?.(1);
230
+ ```
231
+
232
+ ## Advanced: Custom Validators
233
+
234
+ Validators use the **adapter pattern** for framework flexibility:
235
+
236
+ ```typescript
237
+ import type { ValidatorExecutor } from '@formos/kernel';
238
+
239
+ // Simple validator
240
+ const required: ValidatorExecutor = (value, params, context) => {
241
+ return value ? null : 'Required';
242
+ };
243
+
244
+ // Cross-field validator
245
+ const matchesField: ValidatorExecutor = (value, params, context) => {
246
+ const { targetField } = params as { targetField: string };
247
+ const targetValue = context.getValue(targetField);
248
+ return value === targetValue ? null : 'Fields must match';
249
+ };
250
+
251
+ // Async validator (e.g., API check)
252
+ const uniqueUsername: ValidatorExecutor = async (value, params, context) => {
253
+ const response = await fetch(`/api/check-username?username=${value}`);
254
+ const { available } = await response.json();
255
+ return available ? null : 'Username already taken';
256
+ };
257
+
258
+ const engine = createFormEngine(normalized, {
259
+ validators: {
260
+ required,
261
+ matchesField,
262
+ uniqueUsername
263
+ }
264
+ });
265
+ ```
266
+
267
+ ## Advanced: Effect Executors
268
+
269
+ Effects handle side effects triggered by field changes:
270
+
271
+ ```typescript
272
+ import type { EffectExecutor } from '@formos/kernel';
273
+
274
+ // Sync effect: Calculate fullName from firstName + lastName
275
+ const syncFullName: EffectExecutor = (params, context) => {
276
+ const firstName = context.getValue('firstName') as string || '';
277
+ const lastName = context.getValue('lastName') as string || '';
278
+ context.setValue('fullName', `${firstName} ${lastName}`.trim());
279
+ };
280
+
281
+ // API effect: Fetch cities when country changes
282
+ const fetchCities: EffectExecutor = async (params, context) => {
283
+ const country = context.sourceValue as string;
284
+
285
+ const response = await fetch(`/api/cities?country=${country}`);
286
+ const cities = await response.json();
287
+
288
+ context.setValue('cities', cities);
289
+ };
290
+
291
+ const engine = createFormEngine(normalized, {
292
+ effectExecutors: {
293
+ syncFullName,
294
+ fetchCities
295
+ }
296
+ });
297
+ ```
298
+
299
+ ## Conditional Logic
300
+
301
+ The kernel evaluates conditional visibility and required logic:
302
+
303
+ ```typescript
304
+ import { evaluateConditional, isFieldVisible, isFieldRequired } from '@formos/kernel';
305
+
306
+ // Check field visibility
307
+ const field = schema.fieldMap?.get('ssn');
308
+ if (field && isFieldVisible(field, (name) => engine.getValue(name))) {
309
+ // Render SSN field
310
+ }
311
+
312
+ // Check if field is dynamically required
313
+ if (field && isFieldRequired(field, (name) => engine.getValue(name))) {
314
+ // Show required indicator
315
+ }
316
+
317
+ // Direct conditional evaluation
318
+ const condition = {
319
+ logic: 'and',
320
+ rules: [
321
+ { field: 'age', operator: 'greaterThan', value: 18 },
322
+ { field: 'country', operator: 'equals', value: 'US' }
323
+ ]
324
+ };
325
+
326
+ const result = evaluateConditional(condition, (name) => engine.getValue(name));
327
+ ```
328
+
329
+ ## API Reference
330
+
331
+ ### Core Function
332
+
333
+ - **`createFormEngine(schema, options)`**: Create form engine instance
334
+
335
+ ### FormEngine Interface
336
+
337
+ **State Accessors:**
338
+ - `getValue(fieldName)`: Get field value
339
+ - `getError(fieldName)`: Get field error
340
+ - `getErrors()`: Get all errors
341
+ - `isValid()`: Check if form is valid
342
+ - `isDirty(fieldName?)`: Check dirty state
343
+ - `isTouched(fieldName)`: Check touched state
344
+
345
+ **State Mutators:**
346
+ - `setValue(fieldName, value, options?)`: Set field value
347
+ - `setError(fieldName, error)`: Set field error
348
+ - `clearError(fieldName)`: Clear field error
349
+ - `markTouched(fieldName)`: Mark field as touched
350
+
351
+ **Validation:**
352
+ - `validate(fieldName?, trigger?)`: Validate field(s)
353
+
354
+ **Lifecycle:**
355
+ - `submit()`: Submit form (validates first)
356
+ - `reset()`: Reset form to initial state
357
+
358
+ **Multi-Step Navigation (if applicable):**
359
+ - `getCurrentStep?()`: Get current step index
360
+ - `getTotalSteps?()`: Get total step count
361
+ - `nextStep?()`: Go to next step (validates first)
362
+ - `prevStep?()`: Go to previous step
363
+ - `goToStep?(stepIndex)`: Jump to specific step
364
+ - `canGoToStep?(stepIndex)`: Check if can navigate to step
365
+
366
+ ### Adapter Types
367
+
368
+ - **`ValidatorExecutor`**: `(value, params, context) => string | null | Promise<string | null>`
369
+ - **`EffectExecutor`**: `(params, context) => unknown | Promise<unknown>`
370
+ - **`ValidationContext`**: Read-only access to form values
371
+ - **`EffectContext`**: Read-write access to form values
372
+
373
+ ### Utility Functions
374
+
375
+ - **`evaluateConditional(condition, getValue)`**: Evaluate conditional expression
376
+ - **`isFieldVisible(field, getValue)`**: Check field visibility
377
+ - **`isFieldRequired(field, getValue)`**: Check if field is required
378
+
379
+ ## Package Structure
380
+
381
+ ```
382
+ @formos/kernel/
383
+ ├── engine/
384
+ │ ├── factory.ts # createFormEngine, FormEngine interface
385
+ │ ├── values.ts # getValue, setValue operations
386
+ │ ├── validation-ops.ts # validate, getError, isValid
387
+ │ ├── lifecycle.ts # submit, reset
388
+ │ ├── init.ts # Initialization helpers
389
+ │ └── helpers.ts # Internal utilities
390
+ ├── state.ts # FormState class
391
+ ├── validation.ts # Validation orchestration
392
+ ├── effects.ts # Effect execution
393
+ ├── conditionals.ts # Conditional evaluation
394
+ ├── steps.ts # Multi-step controller
395
+ ├── types.ts # Adapter type definitions
396
+ └── index.ts # Public API exports
397
+ ```
398
+
399
+ ## TypeScript Configuration
400
+
401
+ This package uses **strict mode** TypeScript:
402
+ - `noUncheckedIndexedAccess: true`
403
+ - Optional chaining (`?.`) used everywhere for safety
404
+ - `moduleResolution: "NodeNext"`
405
+ - `module: "NodeNext"`
406
+
407
+ All exports are ESM only (`type: "module"`).
408
+
409
+ ## Design Principles
410
+
411
+ ### 1. Adapter Pattern
412
+ The kernel doesn't include validation libraries or HTTP clients. Instead, it defines **interfaces** (`ValidatorExecutor`, `EffectExecutor`) that you implement with your preferred tools.
413
+
414
+ ### 2. Immutable Schema
415
+ The kernel **never modifies** the schema. It reads the normalized schema and executes its instructions, but the schema remains a pure data structure.
416
+
417
+ ### 3. Framework Agnostic
418
+ No React hooks, no DOM manipulation, no browser APIs. This enables:
419
+ - React bindings via `@formos/react`
420
+ - Vue bindings (future)
421
+ - Svelte bindings (future)
422
+ - Node.js usage (server-side form validation)
423
+
424
+ ### 4. Optional Chaining Everywhere
425
+ Since schemas are user-provided, the kernel uses optional chaining extensively for safety.
426
+
427
+ ## Contributing
428
+
429
+ This package is part of the Formos monorepo. See the root README for contribution guidelines.
430
+
431
+ ## License
432
+
433
+ MIT