janux 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.
@@ -0,0 +1,121 @@
1
+ import { validate } from '../schema';
2
+ import { allowMutations } from '../state/mutation-gate';
3
+ import type { ComponentDef, Ctx, GuardValue, IntentDef, Origin, RunBag } from '../define/types';
4
+
5
+ export interface AuditEntry {
6
+ tool: string;
7
+ origin: Origin;
8
+ guard: GuardValue;
9
+ input: unknown;
10
+ ok: boolean;
11
+ error?: string;
12
+ at: number;
13
+ }
14
+
15
+ export interface Proposal {
16
+ id: string;
17
+ tool: string;
18
+ input: unknown;
19
+ execute: () => Promise<unknown>;
20
+ }
21
+
22
+ export interface IntentHooks {
23
+ onAudit?: (entry: AuditEntry) => void;
24
+ onProposal?: (proposal: Proposal) => void;
25
+ trackPending: <T>(work: Promise<T>) => Promise<T>;
26
+ }
27
+
28
+ export class JanuxIntentError extends Error {
29
+ readonly code: 'forbidden' | 'not_ready' | 'invalid_input' | 'unknown_intent';
30
+
31
+ constructor(code: JanuxIntentError['code'], message: string) {
32
+ super(message);
33
+ this.code = code;
34
+ }
35
+ }
36
+
37
+ export function resolveGuard(def: IntentDef, ctx: Ctx): GuardValue {
38
+ const guard = def.guard ?? 'auto';
39
+
40
+ return typeof guard === 'function' ? guard({ ctx }) : guard;
41
+ }
42
+
43
+ let proposalSeq = 0;
44
+
45
+ function nextProposalId(tool: string): string {
46
+ proposalSeq += 1;
47
+
48
+ return `prop_${tool.replace(/\W/g, '_')}_${proposalSeq}`;
49
+ }
50
+
51
+ function checkInvocable(tool: string, def: IntentDef, bag: RunBag): void {
52
+ if (def.ready && !def.ready(bag)) {
53
+ throw new JanuxIntentError('not_ready', `Intent "${tool}" is not ready`);
54
+ }
55
+ }
56
+
57
+ function parseInput(tool: string, def: IntentDef, input: unknown): unknown {
58
+ if (!def.input) return undefined;
59
+ const result = validate(def.input, input ?? {});
60
+
61
+ if (!result.ok) {
62
+ const detail = result.errors.map((e) => `${e.path}: ${e.message}`).join('; ');
63
+
64
+ throw new JanuxIntentError('invalid_input', `Invalid input for "${tool}" — ${detail}`);
65
+ }
66
+
67
+ return result.value;
68
+ }
69
+
70
+ function audit(hooks: IntentHooks, entry: Omit<AuditEntry, 'at'>): void {
71
+ hooks.onAudit?.({ ...entry, at: Date.now() });
72
+ }
73
+
74
+ async function execute(def: IntentDef, bag: RunBag, input: unknown): Promise<unknown> {
75
+ return allowMutations(() => def.run({ ...bag, input }));
76
+ }
77
+
78
+ function propose(tool: string, input: unknown, run: () => Promise<unknown>, hooks: IntentHooks) {
79
+ const proposal: Proposal = { id: nextProposalId(tool), tool, input, execute: run };
80
+
81
+ hooks.onProposal?.(proposal);
82
+
83
+ return { status: 'proposal' as const, id: proposal.id, tool, input };
84
+ }
85
+
86
+ /** The single invocation pipeline shared by human clicks, agent calls and RPC. */
87
+ export async function invokeIntent(
88
+ componentName: string,
89
+ intentName: string,
90
+ def: IntentDef,
91
+ bag: RunBag,
92
+ input: unknown,
93
+ origin: Origin,
94
+ hooks: IntentHooks,
95
+ ): Promise<unknown> {
96
+ const tool = `${componentName}.${intentName}`;
97
+ const guard = resolveGuard(def, bag.ctx);
98
+
99
+ try {
100
+ if (origin === 'agent' && guard === 'forbidden') {
101
+ throw new JanuxIntentError('forbidden', `Intent "${tool}" is not available`);
102
+ }
103
+ checkInvocable(tool, def, bag);
104
+ const parsed = parseInput(tool, def, input);
105
+ const run = () => hooks.trackPending(execute(def, bag, parsed));
106
+
107
+ if (origin === 'agent' && guard === 'confirm') {
108
+ audit(hooks, { tool, origin, guard, input: parsed, ok: true });
109
+
110
+ return propose(tool, parsed, run, hooks);
111
+ }
112
+ const result = await run();
113
+
114
+ audit(hooks, { tool, origin, guard, input: parsed, ok: true });
115
+
116
+ return result;
117
+ } catch (error) {
118
+ audit(hooks, { tool, origin, guard, input, ok: false, error: String(error) });
119
+ throw error;
120
+ }
121
+ }
@@ -0,0 +1,56 @@
1
+ import { effect, signal } from '../signals';
2
+
3
+ export interface PendingTracker {
4
+ track<T>(work: Promise<T>): Promise<T>;
5
+ add(): () => void;
6
+ readonly count: number;
7
+ settled(): Promise<void>;
8
+ }
9
+
10
+ /**
11
+ * Counts in-flight async work (sources loading, effects running, debounce
12
+ * timers). `settled()` resolves when the count reaches zero — the primitive
13
+ * behind `ui.settled()` (RFC §5.4).
14
+ */
15
+ export function createPendingTracker(): PendingTracker {
16
+ const count = signal(0);
17
+
18
+ const add = (): (() => void) => {
19
+ count.value = count.peek() + 1;
20
+ let done = false;
21
+
22
+ return () => {
23
+ if (done) return;
24
+ done = true;
25
+ count.value = count.peek() - 1;
26
+ };
27
+ };
28
+
29
+ const track = <T>(work: Promise<T>): Promise<T> => {
30
+ const done = add();
31
+
32
+ return work.finally(done);
33
+ };
34
+
35
+ const settled = (): Promise<void> => {
36
+ return new Promise((resolve) => {
37
+ const dispose = effect(() => {
38
+ if (count.value > 0) return;
39
+ queueMicrotask(() => {
40
+ if (count.peek() > 0) return;
41
+ dispose();
42
+ resolve();
43
+ });
44
+ });
45
+ });
46
+ };
47
+
48
+ return {
49
+ track,
50
+ add,
51
+ settled,
52
+ get count() {
53
+ return count.peek();
54
+ },
55
+ };
56
+ }
@@ -0,0 +1,92 @@
1
+ import { signal } from '../signals';
2
+ import type { Ctx, SourceDef, SourceReader } from '../define/types';
3
+ import type { EventBus } from './bus';
4
+ import type { PendingTracker } from './settled';
5
+
6
+ export interface SourcesRuntime {
7
+ readers: Record<string, SourceReader & { refresh: () => Promise<void> }>;
8
+ start: () => void;
9
+ dispose: () => void;
10
+ }
11
+
12
+ function createOne(
13
+ def: SourceDef,
14
+ ctx: Ctx,
15
+ bus: EventBus,
16
+ tracker: PendingTracker,
17
+ cleanups: (() => void)[],
18
+ initial: { value: unknown } | undefined,
19
+ ) {
20
+ const value = signal<unknown>(initial?.value);
21
+ const pending = signal(initial === undefined);
22
+ const error = signal<unknown>(null);
23
+
24
+ const load = async (): Promise<void> => {
25
+ pending.value = true;
26
+ await tracker.track(
27
+ Promise.resolve()
28
+ .then(() => def.query({ ctx }))
29
+ .then((result) => {
30
+ value.value = result;
31
+ error.value = null;
32
+ })
33
+ .catch((cause) => {
34
+ error.value = cause;
35
+ })
36
+ .finally(() => {
37
+ pending.value = false;
38
+ }),
39
+ );
40
+ };
41
+
42
+ const start = (): void => {
43
+ if (initial === undefined) load().catch(() => {});
44
+ if (def.refresh?.everyMs) {
45
+ const interval = setInterval(() => load().catch(() => {}), def.refresh.everyMs);
46
+
47
+ cleanups.push(() => clearInterval(interval));
48
+ }
49
+ def.refresh?.events.forEach((event) => {
50
+ cleanups.push(bus.on(event, () => load().catch(() => {})));
51
+ });
52
+ };
53
+
54
+ const reader = {
55
+ get value() {
56
+ return value.value;
57
+ },
58
+ get pending() {
59
+ return pending.value;
60
+ },
61
+ get error() {
62
+ return error.value;
63
+ },
64
+ refresh: load,
65
+ };
66
+
67
+ return { reader, start };
68
+ }
69
+
70
+ /**
71
+ * Wires declared sources: async load with pending/error, timers and event
72
+ * refresh. `initialValues` (from the SSR snapshot) skip the first load —
73
+ * resumed islands never double-fetch what the server already loaded.
74
+ */
75
+ export function createSources(
76
+ defs: Record<string, SourceDef> | undefined,
77
+ ctx: Ctx,
78
+ bus: EventBus,
79
+ tracker: PendingTracker,
80
+ initialValues?: Record<string, { value: unknown }>,
81
+ ): SourcesRuntime {
82
+ const cleanups: (() => void)[] = [];
83
+ const entries = Object.entries(defs ?? {}).map(([name, def]) => {
84
+ return [name, createOne(def, ctx, bus, tracker, cleanups, initialValues?.[name])] as const;
85
+ });
86
+
87
+ return {
88
+ readers: Object.fromEntries(entries.map(([name, one]) => [name, one.reader])),
89
+ start: () => entries.forEach(([, one]) => one.start()),
90
+ dispose: () => cleanups.forEach((cleanup) => cleanup()),
91
+ };
92
+ }
@@ -0,0 +1,46 @@
1
+ import { JxType } from './types';
2
+
3
+ export type Shape = Record<string, JxType>;
4
+ export type ShapeOrType = JxType | Shape;
5
+
6
+ function toType(input: ShapeOrType): JxType {
7
+ return input instanceof JxType ? input : obj(input);
8
+ }
9
+
10
+ export function str(): JxType {
11
+ return new JxType('string');
12
+ }
13
+
14
+ export function int(): JxType {
15
+ return new JxType('int');
16
+ }
17
+
18
+ export function num(): JxType {
19
+ return new JxType('number');
20
+ }
21
+
22
+ export function bool(): JxType {
23
+ return new JxType('boolean');
24
+ }
25
+
26
+ /** Monetary amount in minor units (cents). Serializes as integer. */
27
+ export function money(): JxType {
28
+ return new JxType('money');
29
+ }
30
+
31
+ export function enums(values: readonly string[]): JxType {
32
+ return new JxType('enum', { values });
33
+ }
34
+
35
+ export function list(itemOrShape: ShapeOrType): JxType {
36
+ return new JxType('list', { item: toType(itemOrShape) });
37
+ }
38
+
39
+ export function obj(shape: Shape): JxType {
40
+ return new JxType('object', { shape });
41
+ }
42
+
43
+ /** Root schema for component/store state and intent/api inputs. */
44
+ export function schema(shape: Shape): JxType {
45
+ return obj(shape);
46
+ }
@@ -0,0 +1,33 @@
1
+ import type { JxType } from './types';
2
+
3
+ const ZERO_VALUES: Record<string, unknown> = {
4
+ string: '',
5
+ int: 0,
6
+ money: 0,
7
+ number: 0,
8
+ boolean: false,
9
+ };
10
+
11
+ /**
12
+ * Builds the initial value for a schema: explicit `.default()` wins, then
13
+ * `null` for nullables, `[]` for lists, recursion for objects, first value
14
+ * for enums, and the kind's zero value for primitives.
15
+ */
16
+ export function buildDefault(type: JxType): unknown {
17
+ if (type.flags.defaultValue !== undefined) return type.flags.defaultValue;
18
+ if (type.flags.nullable) return null;
19
+ if (type.kind === 'list') return [];
20
+ if (type.kind === 'enum') return type.values![0];
21
+ if (type.kind === 'object') return buildObjectDefault(type);
22
+
23
+ return ZERO_VALUES[type.kind];
24
+ }
25
+
26
+ function buildObjectDefault(type: JxType): Record<string, unknown> {
27
+ const entries = Object.entries(type.shape!).map(([key, fieldType]) => [
28
+ key,
29
+ buildDefault(fieldType),
30
+ ]);
31
+
32
+ return Object.fromEntries(entries);
33
+ }
@@ -0,0 +1,100 @@
1
+ import { describe, expect, it } from 'bun:test';
2
+ import {
3
+ bool,
4
+ buildDefault,
5
+ enums,
6
+ int,
7
+ list,
8
+ money,
9
+ obj,
10
+ schema,
11
+ str,
12
+ toJsonSchema,
13
+ validate,
14
+ } from './index';
15
+
16
+ const cartSchema = schema({
17
+ items: list({ productId: str(), qty: int().min(1), unitPrice: money() }),
18
+ coupon: str().nullable(),
19
+ });
20
+
21
+ describe('schema validate', () => {
22
+ it('accepts a valid cart and applies structure', () => {
23
+ const input = { items: [{ productId: 'p1', qty: 2, unitPrice: 1999 }], coupon: null };
24
+ const result = validate(cartSchema, input);
25
+
26
+ expect(result.ok).toBe(true);
27
+ expect(result.value).toEqual(input);
28
+ });
29
+
30
+ it('rejects wrong primitive kinds with paths', () => {
31
+ const result = validate(cartSchema, { items: [{ productId: 1, qty: 'x', unitPrice: 1 }] });
32
+
33
+ expect(result.ok).toBe(false);
34
+ expect(result.errors.map((e) => e.path)).toContain('items[0].productId');
35
+ expect(result.errors.map((e) => e.path)).toContain('items[0].qty');
36
+ });
37
+
38
+ it('enforces min bounds', () => {
39
+ const result = validate(cartSchema, { items: [{ productId: 'p', qty: 0, unitPrice: 1 }] });
40
+
41
+ expect(result.ok).toBe(false);
42
+ expect(result.errors[0]!.message).toBe('below min 1');
43
+ });
44
+
45
+ it('applies defaults and strips unknown keys', () => {
46
+ const type = schema({ qty: int().default(1), name: str() });
47
+ const result = validate(type, { name: 'a', hacked: true });
48
+
49
+ expect(result.value).toEqual({ qty: 1, name: 'a' });
50
+ });
51
+
52
+ it('handles nullable, optional and required', () => {
53
+ expect(validate(str().nullable(), null).ok).toBe(true);
54
+ expect(validate(str(), null).ok).toBe(false);
55
+ expect(validate(str().optional(), undefined).ok).toBe(true);
56
+ expect(validate(str(), undefined).ok).toBe(false);
57
+ });
58
+
59
+ it('validates enums', () => {
60
+ const type = enums(['pending', 'paid']);
61
+
62
+ expect(validate(type, 'paid').ok).toBe(true);
63
+ expect(validate(type, 'nope').ok).toBe(false);
64
+ });
65
+ });
66
+
67
+ describe('schema defaults', () => {
68
+ it('builds zero-values, nullables and nested structures', () => {
69
+ expect(buildDefault(cartSchema)).toEqual({ items: [], coupon: null });
70
+ expect(buildDefault(schema({ a: int(), b: bool(), c: enums(['x', 'y']) }))).toEqual({
71
+ a: 0,
72
+ b: false,
73
+ c: 'x',
74
+ });
75
+ });
76
+
77
+ it('prefers explicit defaults', () => {
78
+ expect(buildDefault(int().default(5))).toBe(5);
79
+ });
80
+ });
81
+
82
+ describe('schema toJsonSchema', () => {
83
+ it('serializes nested schemas with required lists', () => {
84
+ const json = toJsonSchema(cartSchema) as any;
85
+
86
+ expect(json.type).toBe('object');
87
+ expect(json.required).toEqual(['items', 'coupon']);
88
+ expect(json.properties.items.items.properties.qty).toEqual({ type: 'integer', minimum: 1 });
89
+ expect(json.properties.coupon.type).toEqual(['string', 'null']);
90
+ });
91
+
92
+ it('marks money format and defaults', () => {
93
+ expect(toJsonSchema(money())).toEqual({ type: 'integer', format: 'money-minor-units' });
94
+ expect((toJsonSchema(int().default(3)) as any).default).toBe(3);
95
+ });
96
+
97
+ it('serializes plain obj same as schema', () => {
98
+ expect(toJsonSchema(obj({ a: str() }))).toEqual(toJsonSchema(schema({ a: str() })));
99
+ });
100
+ });
@@ -0,0 +1,5 @@
1
+ export { JxType, type JxKind, type JxFlags } from './types';
2
+ export { str, int, num, bool, money, enums, list, obj, schema, type Shape } from './builders';
3
+ export { validate, type JxResult, type JxError } from './validate';
4
+ export { buildDefault } from './defaults';
5
+ export { toJsonSchema } from './json-schema';
@@ -0,0 +1,42 @@
1
+ import type { JxType } from './types';
2
+
3
+ const PRIMITIVE_TYPES: Record<string, string> = {
4
+ string: 'string',
5
+ int: 'integer',
6
+ money: 'integer',
7
+ number: 'number',
8
+ boolean: 'boolean',
9
+ };
10
+
11
+ function baseSchema(type: JxType): Record<string, unknown> {
12
+ if (type.kind === 'enum') return { enum: [...type.values!] };
13
+ if (type.kind === 'list') return { type: 'array', items: toJsonSchema(type.item!) };
14
+ if (type.kind === 'object') return objectSchema(type);
15
+
16
+ return { type: PRIMITIVE_TYPES[type.kind] };
17
+ }
18
+
19
+ function objectSchema(type: JxType): Record<string, unknown> {
20
+ const shape = type.shape!;
21
+ const properties = Object.fromEntries(
22
+ Object.entries(shape).map(([key, field]) => [key, toJsonSchema(field)]),
23
+ );
24
+ const required = Object.entries(shape)
25
+ .filter(([, field]) => !field.flags.optional && field.flags.defaultValue === undefined)
26
+ .map(([key]) => key);
27
+
28
+ return { type: 'object', properties, required, additionalProperties: false };
29
+ }
30
+
31
+ /** Serializes a Janux type to standard JSON Schema (for the manifest and MCP tools). */
32
+ export function toJsonSchema(type: JxType): Record<string, unknown> {
33
+ const base = baseSchema(type);
34
+
35
+ if (type.flags.nullable) base.type = [base.type, 'null'].flat();
36
+ if (type.flags.defaultValue !== undefined) base.default = type.flags.defaultValue;
37
+ if (type.flags.min !== undefined) base.minimum = type.flags.min;
38
+ if (type.flags.max !== undefined) base.maximum = type.flags.max;
39
+ if (type.kind === 'money') base.format = 'money-minor-units';
40
+
41
+ return base;
42
+ }
@@ -0,0 +1,65 @@
1
+ export type JxKind =
2
+ | 'string'
3
+ | 'int'
4
+ | 'number'
5
+ | 'boolean'
6
+ | 'money'
7
+ | 'enum'
8
+ | 'list'
9
+ | 'object';
10
+
11
+ export interface JxFlags {
12
+ optional?: boolean;
13
+ nullable?: boolean;
14
+ defaultValue?: unknown;
15
+ min?: number;
16
+ max?: number;
17
+ }
18
+
19
+ export interface JxExtra {
20
+ values?: readonly string[];
21
+ item?: JxType;
22
+ shape?: Record<string, JxType>;
23
+ }
24
+
25
+ export class JxType {
26
+ readonly kind: JxKind;
27
+ readonly flags: JxFlags;
28
+ readonly values?: readonly string[];
29
+ readonly item?: JxType;
30
+ readonly shape?: Record<string, JxType>;
31
+
32
+ constructor(kind: JxKind, extra: JxExtra = {}, flags: JxFlags = {}) {
33
+ this.kind = kind;
34
+ this.flags = flags;
35
+ this.values = extra.values;
36
+ this.item = extra.item;
37
+ this.shape = extra.shape;
38
+ }
39
+
40
+ private with(patch: JxFlags): JxType {
41
+ const extra = { values: this.values, item: this.item, shape: this.shape };
42
+
43
+ return new JxType(this.kind, extra, { ...this.flags, ...patch });
44
+ }
45
+
46
+ optional(): JxType {
47
+ return this.with({ optional: true });
48
+ }
49
+
50
+ nullable(): JxType {
51
+ return this.with({ nullable: true });
52
+ }
53
+
54
+ default(value: unknown): JxType {
55
+ return this.with({ defaultValue: value });
56
+ }
57
+
58
+ min(value: number): JxType {
59
+ return this.with({ min: value });
60
+ }
61
+
62
+ max(value: number): JxType {
63
+ return this.with({ max: value });
64
+ }
65
+ }
@@ -0,0 +1,106 @@
1
+ import type { JxType } from './types';
2
+
3
+ export interface JxError {
4
+ path: string;
5
+ message: string;
6
+ }
7
+
8
+ export interface JxResult {
9
+ ok: boolean;
10
+ value: unknown;
11
+ errors: JxError[];
12
+ }
13
+
14
+ const PRIMITIVE_CHECKS: Record<string, (v: unknown) => boolean> = {
15
+ string: (v) => typeof v === 'string',
16
+ int: (v) => Number.isInteger(v),
17
+ money: (v) => Number.isInteger(v),
18
+ number: (v) => typeof v === 'number' && Number.isFinite(v),
19
+ boolean: (v) => typeof v === 'boolean',
20
+ };
21
+
22
+ function fail(path: string, message: string): JxResult {
23
+ return { ok: false, value: undefined, errors: [{ path, message }] };
24
+ }
25
+
26
+ function pass(value: unknown): JxResult {
27
+ return { ok: true, value, errors: [] };
28
+ }
29
+
30
+ function checkBounds(type: JxType, value: unknown, path: string): JxResult {
31
+ const { min, max } = type.flags;
32
+ const size = typeof value === 'string' ? value.length : (value as number);
33
+
34
+ if (min !== undefined && size < min) return fail(path, `below min ${min}`);
35
+ if (max !== undefined && size > max) return fail(path, `above max ${max}`);
36
+
37
+ return pass(value);
38
+ }
39
+
40
+ function validatePrimitive(type: JxType, value: unknown, path: string): JxResult {
41
+ const check = PRIMITIVE_CHECKS[type.kind];
42
+
43
+ if (!check!(value)) return fail(path, `expected ${type.kind}`);
44
+
45
+ return checkBounds(type, value, path);
46
+ }
47
+
48
+ function validateEnum(type: JxType, value: unknown, path: string): JxResult {
49
+ if (!type.values!.includes(value as string)) {
50
+ return fail(path, `expected one of: ${type.values!.join(', ')}`);
51
+ }
52
+
53
+ return pass(value);
54
+ }
55
+
56
+ function validateList(type: JxType, value: unknown, path: string): JxResult {
57
+ if (!Array.isArray(value)) return fail(path, 'expected list');
58
+
59
+ const results = value.map((item, i) => validate(type.item!, item, `${path}[${i}]`));
60
+ const errors = results.flatMap((r) => r.errors);
61
+
62
+ if (errors.length > 0) return { ok: false, value: undefined, errors };
63
+
64
+ return pass(results.map((r) => r.value));
65
+ }
66
+
67
+ function validateObject(type: JxType, value: unknown, path: string): JxResult {
68
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
69
+ return fail(path, 'expected object');
70
+ }
71
+
72
+ const entries = Object.entries(type.shape!).map(([key, fieldType]) => {
73
+ const fieldPath = path === '' ? key : `${path}.${key}`;
74
+
75
+ return [key, validate(fieldType, (value as any)[key], fieldPath)] as const;
76
+ });
77
+ const errors = entries.flatMap(([, r]) => r.errors);
78
+
79
+ if (errors.length > 0) return { ok: false, value: undefined, errors };
80
+
81
+ return pass(Object.fromEntries(entries.map(([key, r]) => [key, r.value])));
82
+ }
83
+
84
+ function validatePresent(type: JxType, value: unknown, path: string): JxResult {
85
+ if (type.kind === 'enum') return validateEnum(type, value, path);
86
+ if (type.kind === 'list') return validateList(type, value, path);
87
+ if (type.kind === 'object') return validateObject(type, value, path);
88
+
89
+ return validatePrimitive(type, value, path);
90
+ }
91
+
92
+ /** Validates a value against a schema, applying defaults. Unknown object keys are stripped. */
93
+ export function validate(type: JxType, value: unknown, path = ''): JxResult {
94
+ if (value === undefined) {
95
+ if (type.flags.defaultValue !== undefined) return pass(type.flags.defaultValue);
96
+ if (type.flags.optional) return pass(undefined);
97
+ if (type.flags.nullable) return pass(null);
98
+
99
+ return fail(path, 'required');
100
+ }
101
+ if (value === null) {
102
+ return type.flags.nullable ? pass(null) : fail(path, 'not nullable');
103
+ }
104
+
105
+ return validatePresent(type, value, path);
106
+ }