Setting the file. One moment.
Schema Merge · Browser To API · browserbase/skills · Skills Docs
ContentsBack to the top of the page scripts/lib/ schema-merge.mjs
JavaScript · 175 lines · 6 KB
,
10
);
10
11 const ISO_RE = / ^ \d {4} - \d {2} - \d {2} ( [T ]\d {2} : \d {2} : \d {2} ( \. \d + ) ? (Z | [+-]\d {2} : ? \d {2} ) ? ) ?$ / ;
12 const URI_RE = / ^ https ? : \/\/ \S +$ / i ;
13 const EMAIL_RE = / ^ [ ^ \s@] + @ [ ^ \s@] + \. [ ^ \s@] +$ / ;
14 const UUID_RE = / ^ [0-9a-f] {8} - [0-9a-f] {4} - [1-5][0-9a-f] {3} - [89ab][0-9a-f] {3} - [0-9a-f] {12}$ / i ;
15
16 function jsonType ( v ) {
17 if (v === null ) return 'null' ;
18 if (Array. isArray (v)) return 'array' ;
19 return typeof v; // 'string', 'number', 'boolean', 'object'
20 }
21
22 function inferFormat ( v ) {
23 if ( typeof v !== 'string' ) return null ;
24 if ( UUID_RE . test (v)) return 'uuid' ;
25 if ( ISO_RE . test (v)) return 'date-time' ;
26 if ( URI_RE . test (v)) return 'uri' ;
27 if ( EMAIL_RE . test (v)) return 'email' ;
28 return null ;
29 }
30
31 // Build a "pre-schema" — captures every observed sample so we can compute
32 // required/enum/format with global knowledge, then collapse to JSON Schema.
33 export function newProto () {
34 return {
35 types: new Set (), // 'string', 'integer', 'number', 'boolean', 'null', 'object', 'array'
36 samples: 0 ,
37 nullCount: 0 ,
38 formats: new Map (), // format -> count of samples that matched
39 values: new Set (), // primitive values, capped (used for enum detection)
40 valuesCapped: false ,
41 properties: new Map (), // key -> proto
42 presence: new Map (), // key -> count of samples that contained the key
43 items: null , // proto for array items
44 };
45 }
46
47 const VALUE_CAP = 64 ;
48
49 export function ingest ( proto , value ) {
50 proto.samples ++ ;
51 const t = jsonType (value);
52
53 if (t === 'null' ) { proto.types. add ( 'null' ); proto.nullCount ++ ; return ; }
54
55 if (t === 'number' ) {
56 proto.types. add (Number. isInteger (value) ? 'integer' : 'number' );
57 } else {
58 proto.types. add (t);
59 }
60
61 if (t === 'string' || t === 'number' || t === 'boolean' || t === 'integer' ) {
62 if ( ! proto.valuesCapped) {
63 proto.values. add (value);
64 if (proto.values.size > VALUE_CAP ) {
65 proto.values. clear ();
66 proto.valuesCapped = true ;
67 }
68 }
69 if (t === 'string' ) {
70 const f = inferFormat (value);
71 if (f) proto.formats. set (f, (proto.formats. get (f) || 0 ) + 1 );
72 }
73 }
74
75 if (t === 'object' ) {
76 for ( const [ k , v ] of Object. entries (value)) {
77 proto.presence. set (k, (proto.presence. get (k) || 0 ) + 1 );
78 let child = proto.properties. get (k);
79 if ( ! child) { child = newProto (); proto.properties. set (k, child); }
80 ingest (child, v);
81 }
82 }
83
84 if (t === 'array' ) {
85 if ( ! proto.items) proto.items = newProto ();
86 for ( const item of value) ingest (proto.items, item);
87 // Important: treat array containment as a single sample at this level —
88 // ingest() above already counted samples++ once. Items are sampled
89 // individually inside the recursive call.
90 }
91 }
92
93 export function ingestMany ( proto , values ) {
94 for ( const v of values) ingest (proto, v);
95 return proto;
96 }
97
98 // Convert a proto into a JSON Schema fragment.
99 export function toSchema ( proto ) {
100 if ( ! proto || proto.samples === 0 ) return {};
101
102 const types = Array. from (proto.types);
103 const nonNull = types. filter ( t => t !== 'null' );
104 const nullable = proto.types. has ( 'null' ) && nonNull. length > 0 ;
105
106 // Scalar / enum case
107 if (nonNull. length === 1 && ! [ 'object' , 'array' ]. includes (nonNull[ 0 ])) {
108 const t = nonNull[ 0 ];
109 const out = { type: t };
110 if (nullable) out.type = [t, 'null' ];
111
112 if (t === 'string' ) {
113 // Format: pick the format that matched ≥ 80% of string samples
114 const stringSamples = proto.samples - proto.nullCount;
115 if (stringSamples > 0 ) {
116 for ( const [ f , n ] of proto.formats. entries ()) {
117 if (n / stringSamples >= 0.8 ) { out.format = f; break ; }
118 }
119 }
120 }
121
122 // Enum detection: low cardinality AND meaningful repetition (otherwise
123 // every distinct ID across N samples would look like an N-way enum).
124 const valueSamples = proto.samples - proto.nullCount;
125 if ( ! proto.valuesCapped &&
126 proto.values.size > 0 &&
127 proto.values.size <= ENUM_MAX &&
128 valueSamples >= ENUM_MIN &&
129 proto.values.size <= Math. max ( 2 , Math. floor (valueSamples / 2 ))) {
130 out.enum = Array. from (proto.values). sort (( a , b ) => String (a). localeCompare ( String (b)));
131 }
132 return out;
133 }
134
135 // Object
136 if (nonNull. length === 1 && nonNull[ 0 ] === 'object' ) {
137 const properties = {};
138 const required = [];
139 for ( const [ k , child ] of proto.properties. entries ()) {
140 properties[k] = toSchema (child);
141 const presence = proto.presence. get (k) || 0 ;
142 if (presence === proto.samples - proto.nullCount && presence > 0 ) required. push (k);
143 }
144 const out = { type: nullable ? [ 'object' , 'null' ] : 'object' };
145 if (Object. keys (properties). length ) out.properties = properties;
146 if (required. length ) out.required = required. sort ();
147 return out;
148 }
149
150 // Array
151 if (nonNull. length === 1 && nonNull[ 0 ] === 'array' ) {
152 const out = { type: nullable ? [ 'array' , 'null' ] : 'array' };
153 if (proto.items) out.items = toSchema (proto.items);
154 return out;
155 }
156
157 // Mixed types — fall back to a typed union via "type" array (OpenAPI 3.1 / draft 2020-12 OK).
158 const out = { type: nullable ? [ ... nonNull, 'null' ] : nonNull };
159 return out;
160 }
161
162 // Convenience: build a schema directly from an array of sample values.
163 export function inferSchema ( samples ) {
164 const p = newProto ();
165 ingestMany (p, samples);
166 return toSchema (p);
167 }
168
169 // Stable structural hash for schema deduplication when hoisting components.
170 export function structuralHash ( schema ) {
171 if ( ! schema || typeof schema !== 'object' ) return JSON . stringify (schema);
172 if (Array. isArray (schema)) return '[' + schema. map (structuralHash). join ( ',' ) + ']' ;
173 const keys = Object. keys (schema). sort ();
174 return '{' + keys. map ( k => JSON . stringify (k) + ':' + structuralHash (schema[k])). join ( ',' ) + '}' ;
175 }