schema-brief 0.1.0 → 0.2.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/CHANGELOG.md ADDED
@@ -0,0 +1,21 @@
1
+ # Changelog
2
+
3
+ ## 0.2.0
4
+
5
+ - Add `extractJsonValues` for extracting multiple JSON payloads from one model response.
6
+ - Add `splitJson` for separating surrounding prose from parsed JSON payloads.
7
+ - Add `repairJsonText` for common non-eval JSON repair such as trailing commas, comments, and smart quotes.
8
+ - Add `createContract` as a reusable schema boundary object with prompt, parse, validate, repair, and provider helpers.
9
+ - Add `toOpenAIResponseFormat`, `toOpenAITool`, and `toAnthropicTool`.
10
+ - Update competitive positioning against structured-output frameworks and JSON extraction utilities.
11
+
12
+ ## 0.1.1
13
+
14
+ - Improve JSON extraction when non-JSON markdown fences appear before the actual JSON payload.
15
+ - Report invalid schema `pattern` values as validation issues instead of throwing.
16
+ - Add validation support for `allOf`, `anyOf`, `oneOf`, tuple `items`, `uniqueItems`, `minProperties`, `maxProperties`, `exclusiveMinimum`, `exclusiveMaximum`, and `multipleOf`.
17
+ - Expand TypeScript declarations and README supported-keyword documentation.
18
+
19
+ ## 0.1.0
20
+
21
+ - Initial release with prompt generation, JSON extraction, schema validation, and repair prompts.
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Compact JSON Schema into LLM-ready instructions, extract JSON from model output, and validate the response with the same contract.
4
4
 
5
- `schema-brief` is designed for TypeScript AI applications that need structured output without adding a heavy validation or prompt-engineering dependency. It supports the most common JSON Schema subset used for model outputs: objects, arrays, primitives, `required`, `enum`, `const`, string/number bounds, array bounds, and `additionalProperties`.
5
+ `schema-brief` is designed for TypeScript AI applications that need structured output without adding a heavy validation or prompt-engineering dependency. It supports the most common JSON Schema subset used for model outputs and includes provider helper shapes for OpenAI and Anthropic.
6
6
 
7
7
  ## Why this exists
8
8
 
@@ -23,7 +23,7 @@ This package sits at that boundary: one schema becomes prompt instructions, vali
23
23
  | LangChain structured output | Agent pipelines and retries inside LangChain | `schema-brief` is framework-neutral and has no provider/runtime dependency. |
24
24
  | Vercel AI SDK structured output | Full-stack apps already using AI SDK | `schema-brief` can be used before or after any model call, including local models and custom SDKs. |
25
25
  | AJV and JSON Schema validators | Full JSON Schema compliance and high-throughput validation | `schema-brief` optimizes for the common LLM-output subset and also generates prompt/repair text. |
26
- | JSON extraction utilities | Pulling JSON out of messy model text | `schema-brief` couples extraction with schema prompt generation and validation feedback. |
26
+ | JSON extraction utilities | Pulling JSON out of messy model text | `schema-brief` extracts one or many JSON payloads and couples extraction with schema prompt generation, validation, repair text, and provider formats. |
27
27
  | Zod/Valibot-based helpers | TypeScript schema-first apps | `schema-brief` starts from portable JSON Schema, which fits OpenAPI, MCP, and provider-native structured outputs. |
28
28
 
29
29
  ## Install
@@ -69,6 +69,21 @@ if (!result.ok) {
69
69
  }
70
70
  ```
71
71
 
72
+ You can also create a reusable contract object:
73
+
74
+ ```js
75
+ import { createContract } from "schema-brief";
76
+
77
+ const contract = createContract(schema);
78
+
79
+ await model.generate({
80
+ system: contract.instructions,
81
+ response_format: contract.toOpenAIResponseFormat()
82
+ });
83
+
84
+ const parsed = contract.parse(modelText);
85
+ ```
86
+
72
87
  ## API
73
88
 
74
89
  ### `brief(schema, options?)`
@@ -86,6 +101,18 @@ Options:
86
101
 
87
102
  Extracts and parses the first complete JSON object or array from raw text or markdown fences.
88
103
 
104
+ ### `extractJsonValues(text)`
105
+
106
+ Extracts every complete JSON object or array from raw text.
107
+
108
+ ### `splitJson(text)`
109
+
110
+ Returns `{ text, json }`, separating surrounding prose from parsed JSON payloads.
111
+
112
+ ### `repairJsonText(text)`
113
+
114
+ Repairs common LLM JSON formatting mistakes before parsing, including trailing commas, JavaScript-style comments, and smart quotes. It does not evaluate code.
115
+
89
116
  ### `validate(schema, value)`
90
117
 
91
118
  Validates a value against the supported JSON Schema subset.
@@ -102,10 +129,26 @@ Returns:
102
129
 
103
130
  Combines `extractJson` and `validate`.
104
131
 
132
+ ### `createContract(schema, options?)`
133
+
134
+ Returns a reusable object with `instructions`, `parse`, `validate`, `repairPrompt`, `toOpenAIResponseFormat`, `toOpenAITool`, and `toAnthropicTool`.
135
+
105
136
  ### `repairPrompt(schema, issues)`
106
137
 
107
138
  Creates a concise prompt asking a model to repair invalid JSON.
108
139
 
140
+ ### `toOpenAIResponseFormat(schema, options?)`
141
+
142
+ Creates an OpenAI `response_format` object using `type: "json_schema"`.
143
+
144
+ ### `toOpenAITool(schema, options?)`
145
+
146
+ Creates an OpenAI tool/function definition from the schema.
147
+
148
+ ### `toAnthropicTool(schema, options?)`
149
+
150
+ Creates an Anthropic tool definition from the schema.
151
+
109
152
  ## Supported Schema Keywords
110
153
 
111
154
  - `type`
@@ -117,23 +160,31 @@ Creates a concise prompt asking a model to repair invalid JSON.
117
160
  - `required`
118
161
  - `additionalProperties`
119
162
  - `items`
163
+ - `allOf`
164
+ - `anyOf`
165
+ - `oneOf`
120
166
  - `minItems`
121
167
  - `maxItems`
168
+ - `uniqueItems`
169
+ - `minProperties`
170
+ - `maxProperties`
122
171
  - `minLength`
123
172
  - `maxLength`
124
173
  - `pattern`
125
174
  - `format` for prompt output only
126
175
  - `minimum`
127
176
  - `maximum`
177
+ - `exclusiveMinimum`
178
+ - `exclusiveMaximum`
179
+ - `multipleOf`
128
180
 
129
181
  ## Product Roadmap
130
182
 
131
183
  - Zod adapter: `briefFromZod(schema)`
132
184
  - Standard Schema adapter
133
- - OpenAI, Anthropic, and Vercel AI SDK helpers
134
185
  - Token-budgeted schema compression
135
186
  - Browser bundle size CI
136
- - JSON repair suggestions with deterministic patches
187
+ - Schema-aware JSON repair suggestions with deterministic patches
137
188
 
138
189
  ## Development
139
190
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "schema-brief",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Compact JSON Schema prompts and validate structured LLM outputs with zero dependencies.",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
@@ -14,6 +14,7 @@
14
14
  "files": [
15
15
  "src",
16
16
  "examples",
17
+ "CHANGELOG.md",
17
18
  "README.md",
18
19
  "LICENSE"
19
20
  ],
package/src/index.d.ts CHANGED
@@ -20,14 +20,23 @@ export interface JsonSchema {
20
20
  required?: readonly string[];
21
21
  additionalProperties?: boolean | JsonSchema;
22
22
  items?: JsonSchema | readonly JsonSchema[];
23
+ allOf?: readonly JsonSchema[];
24
+ anyOf?: readonly JsonSchema[];
25
+ oneOf?: readonly JsonSchema[];
23
26
  minItems?: number;
24
27
  maxItems?: number;
28
+ uniqueItems?: boolean;
29
+ minProperties?: number;
30
+ maxProperties?: number;
25
31
  minLength?: number;
26
32
  maxLength?: number;
27
33
  pattern?: string;
28
34
  format?: string;
29
35
  minimum?: number;
30
36
  maximum?: number;
37
+ exclusiveMinimum?: number;
38
+ exclusiveMaximum?: number;
39
+ multipleOf?: number;
31
40
  }
32
41
 
33
42
  export interface BriefOptions {
@@ -37,6 +46,17 @@ export interface BriefOptions {
37
46
  maxDepth?: number;
38
47
  }
39
48
 
49
+ export interface SplitJsonResult {
50
+ text: string[];
51
+ json: unknown[];
52
+ }
53
+
54
+ export interface ProviderFormatOptions {
55
+ name?: string;
56
+ description?: string;
57
+ strict?: boolean;
58
+ }
59
+
40
60
  export interface ValidationIssue {
41
61
  path: string;
42
62
  code: string;
@@ -49,8 +69,53 @@ export type ValidationResult<T = unknown> =
49
69
 
50
70
  export type ParseResult<T = unknown> = ValidationResult<T>;
51
71
 
72
+ export interface OpenAIResponseFormat {
73
+ type: "json_schema";
74
+ json_schema: {
75
+ name: string;
76
+ strict: boolean;
77
+ schema: JsonSchema;
78
+ description?: string;
79
+ };
80
+ }
81
+
82
+ export interface OpenAITool {
83
+ type: "function";
84
+ function: {
85
+ name: string;
86
+ description: string;
87
+ parameters: JsonSchema;
88
+ strict: boolean;
89
+ };
90
+ }
91
+
92
+ export interface AnthropicTool {
93
+ name: string;
94
+ description: string;
95
+ input_schema: JsonSchema;
96
+ }
97
+
98
+ export interface OutputContract<T = unknown> {
99
+ schema: JsonSchema;
100
+ prompt: string;
101
+ instructions: string;
102
+ parse(text: string): ParseResult<T>;
103
+ validate(value: unknown): ValidationResult<T>;
104
+ repairPrompt(issues: readonly ValidationIssue[]): string;
105
+ toOpenAIResponseFormat(options?: ProviderFormatOptions): OpenAIResponseFormat;
106
+ toOpenAITool(options?: ProviderFormatOptions): OpenAITool;
107
+ toAnthropicTool(options?: ProviderFormatOptions): AnthropicTool;
108
+ }
109
+
52
110
  export function brief(schema: JsonSchema, options?: BriefOptions): string;
53
111
  export function extractJson(text: string): unknown;
112
+ export function extractJsonValues(text: string): unknown[];
113
+ export function splitJson(text: string): SplitJsonResult;
114
+ export function repairJsonText(text: string): string;
54
115
  export function validate<T = unknown>(schema: JsonSchema, value: unknown): ValidationResult<T>;
55
116
  export function parseStructured<T = unknown>(text: string, schema: JsonSchema): ParseResult<T>;
117
+ export function createContract<T = unknown>(schema: JsonSchema, options?: BriefOptions): OutputContract<T>;
56
118
  export function repairPrompt(schema: JsonSchema, issues: readonly ValidationIssue[]): string;
119
+ export function toOpenAIResponseFormat(schema: JsonSchema, options?: ProviderFormatOptions): OpenAIResponseFormat;
120
+ export function toOpenAITool(schema: JsonSchema, options?: ProviderFormatOptions): OpenAITool;
121
+ export function toAnthropicTool(schema: JsonSchema, options?: ProviderFormatOptions): AnthropicTool;
package/src/index.js CHANGED
@@ -47,18 +47,97 @@ export function extractJson(text) {
47
47
  throw new TypeError("extractJson expected a string");
48
48
  }
49
49
 
50
- const fenced = text.match(/```(?:json)?\s*([\s\S]*?)```/i);
51
- const source = fenced ? fenced[1].trim() : text.trim();
50
+ const values = extractJsonValues(text);
51
+ if (values.length > 0) {
52
+ return values[0];
53
+ }
52
54
 
53
- try {
54
- return JSON.parse(source);
55
- } catch {
56
- const slice = firstJsonSlice(source);
57
- if (!slice) {
58
- throw new SyntaxError("No JSON object or array found in text");
55
+ throw new SyntaxError("No JSON object or array found in text");
56
+ }
57
+
58
+ /**
59
+ * Extract every complete JSON object or array from text.
60
+ *
61
+ * @param {string} text
62
+ * @returns {unknown[]}
63
+ */
64
+ export function extractJsonValues(text) {
65
+ if (typeof text !== "string") {
66
+ throw new TypeError("extractJsonValues expected a string");
67
+ }
68
+
69
+ const matches = [];
70
+ const fences = [...text.matchAll(/```[a-zA-Z0-9_-]*\s*([\s\S]*?)```/g)];
71
+
72
+ for (const match of fences) {
73
+ const values = parseJsonValues(match[1].trim());
74
+ if (values.length > 0) {
75
+ matches.push({ start: match.index, values });
59
76
  }
60
- return JSON.parse(slice);
61
77
  }
78
+
79
+ const outside = maskRanges(text, fences.map((match) => [match.index, match.index + match[0].length]));
80
+ for (const match of jsonMatches(outside)) {
81
+ matches.push({ start: match.start, values: [match.value] });
82
+ }
83
+
84
+ return matches
85
+ .sort((a, b) => a.start - b.start)
86
+ .flatMap((match) => match.values);
87
+ }
88
+
89
+ /**
90
+ * Separate parsed JSON payloads from surrounding text.
91
+ *
92
+ * @param {string} text
93
+ * @returns {import("./index.d.ts").SplitJsonResult}
94
+ */
95
+ export function splitJson(text) {
96
+ if (typeof text !== "string") {
97
+ throw new TypeError("splitJson expected a string");
98
+ }
99
+
100
+ const parts = [];
101
+ const json = [];
102
+ const matches = jsonMatches(text);
103
+ let cursor = 0;
104
+
105
+ for (const match of matches) {
106
+ const before = text.slice(cursor, match.start).trim();
107
+ if (before) {
108
+ parts.push(before);
109
+ }
110
+ json.push(match.value);
111
+ cursor = match.end;
112
+ }
113
+
114
+ const after = text.slice(cursor).trim();
115
+ if (after) {
116
+ parts.push(after);
117
+ }
118
+
119
+ return { text: parts, json };
120
+ }
121
+
122
+ /**
123
+ * Repair common LLM JSON formatting mistakes without evaluating code.
124
+ *
125
+ * @param {string} text
126
+ * @returns {string}
127
+ */
128
+ export function repairJsonText(text) {
129
+ if (typeof text !== "string") {
130
+ throw new TypeError("repairJsonText expected a string");
131
+ }
132
+
133
+ const normalized = text
134
+ .replace(/^\uFEFF/, "")
135
+ .replace(/[\u201C\u201D]/g, "\"")
136
+ .replace(/[\u2018\u2019]/g, "'");
137
+
138
+ return stripJsonComments(normalized)
139
+ .trim()
140
+ .replace(/,\s*([}\]])/g, "$1");
62
141
  }
63
142
 
64
143
  /**
@@ -100,6 +179,28 @@ export function parseStructured(text, schema) {
100
179
  }
101
180
  }
102
181
 
182
+ /**
183
+ * Create a reusable structured-output contract around one schema.
184
+ *
185
+ * @param {import("./index.d.ts").JsonSchema} schema
186
+ * @param {import("./index.d.ts").BriefOptions} [options]
187
+ * @returns {import("./index.d.ts").OutputContract}
188
+ */
189
+ export function createContract(schema, options = {}) {
190
+ const prompt = brief(schema, options);
191
+ return {
192
+ schema,
193
+ prompt,
194
+ instructions: prompt,
195
+ parse: (text) => parseStructured(text, schema),
196
+ validate: (value) => validate(schema, value),
197
+ repairPrompt: (issues) => repairPrompt(schema, issues),
198
+ toOpenAIResponseFormat: (providerOptions = {}) => toOpenAIResponseFormat(schema, providerOptions),
199
+ toOpenAITool: (providerOptions = {}) => toOpenAITool(schema, providerOptions),
200
+ toAnthropicTool: (providerOptions = {}) => toAnthropicTool(schema, providerOptions)
201
+ };
202
+ }
203
+
103
204
  /**
104
205
  * Create a concise repair prompt from validation issues.
105
206
  *
@@ -118,6 +219,64 @@ export function repairPrompt(schema, issues) {
118
219
  ].join("\n");
119
220
  }
120
221
 
222
+ /**
223
+ * Convert a JSON Schema into OpenAI's json_schema response_format shape.
224
+ *
225
+ * @param {import("./index.d.ts").JsonSchema} schema
226
+ * @param {import("./index.d.ts").ProviderFormatOptions} [options]
227
+ * @returns {import("./index.d.ts").OpenAIResponseFormat}
228
+ */
229
+ export function toOpenAIResponseFormat(schema, options = {}) {
230
+ const jsonSchema = {
231
+ name: schemaName(schema, options.name),
232
+ strict: options.strict !== false,
233
+ schema
234
+ };
235
+
236
+ if (typeof options.description === "string" && options.description.trim()) {
237
+ jsonSchema.description = options.description.trim();
238
+ }
239
+
240
+ return {
241
+ type: "json_schema",
242
+ json_schema: jsonSchema
243
+ };
244
+ }
245
+
246
+ /**
247
+ * Convert a JSON Schema into an OpenAI tool/function definition.
248
+ *
249
+ * @param {import("./index.d.ts").JsonSchema} schema
250
+ * @param {import("./index.d.ts").ProviderFormatOptions} [options]
251
+ * @returns {import("./index.d.ts").OpenAITool}
252
+ */
253
+ export function toOpenAITool(schema, options = {}) {
254
+ return {
255
+ type: "function",
256
+ function: {
257
+ name: schemaName(schema, options.name),
258
+ description: providerDescription(schema, options),
259
+ parameters: schema,
260
+ strict: options.strict !== false
261
+ }
262
+ };
263
+ }
264
+
265
+ /**
266
+ * Convert a JSON Schema into an Anthropic tool definition.
267
+ *
268
+ * @param {import("./index.d.ts").JsonSchema} schema
269
+ * @param {import("./index.d.ts").ProviderFormatOptions} [options]
270
+ * @returns {import("./index.d.ts").AnthropicTool}
271
+ */
272
+ export function toAnthropicTool(schema, options = {}) {
273
+ return {
274
+ name: schemaName(schema, options.name),
275
+ description: providerDescription(schema, options),
276
+ input_schema: schema
277
+ };
278
+ }
279
+
121
280
  function describeSchema(schema, context) {
122
281
  if (context.depth > context.maxDepth) {
123
282
  return "...";
@@ -131,6 +290,18 @@ function describeSchema(schema, context) {
131
290
  return schema.enum.map((item) => JSON.stringify(item)).join(" | ");
132
291
  }
133
292
 
293
+ if (Array.isArray(schema.oneOf)) {
294
+ return schema.oneOf.map((child) => describeSchema(child, nextContext(context))).join(" | ");
295
+ }
296
+
297
+ if (Array.isArray(schema.anyOf)) {
298
+ return schema.anyOf.map((child) => describeSchema(child, nextContext(context))).join(" | ");
299
+ }
300
+
301
+ if (Array.isArray(schema.allOf)) {
302
+ return schema.allOf.map((child) => describeSchema(child, nextContext(context))).join(" & ");
303
+ }
304
+
134
305
  const type = normalizeType(schema);
135
306
 
136
307
  if (Array.isArray(type)) {
@@ -154,7 +325,9 @@ function describeSchema(schema, context) {
154
325
  }
155
326
 
156
327
  if (type === "array" || schema.items) {
157
- const item = schema.items && !Array.isArray(schema.items)
328
+ const item = Array.isArray(schema.items)
329
+ ? `[${schema.items.map((child) => describeSchema(child, nextContext(context))).join(", ")}]`
330
+ : schema.items
158
331
  ? describeSchema(schema.items, nextContext(context))
159
332
  : "unknown";
160
333
  const range = formatRange(schema.minItems, schema.maxItems, "items");
@@ -190,6 +363,10 @@ function collectDescriptions(schema, path = "$", notes = []) {
190
363
 
191
364
  if (schema.items && !Array.isArray(schema.items)) {
192
365
  collectDescriptions(schema.items, `${path}[]`, notes);
366
+ } else if (Array.isArray(schema.items)) {
367
+ schema.items.forEach((child, index) => {
368
+ collectDescriptions(child, `${path}[${index}]`, notes);
369
+ });
193
370
  }
194
371
 
195
372
  if (schema.additionalProperties && typeof schema.additionalProperties === "object") {
@@ -215,6 +392,10 @@ function visit(schema, value, path, issues, depth) {
215
392
  return;
216
393
  }
217
394
 
395
+ if (!validateCompositions(schema, value, path, issues, depth)) {
396
+ return;
397
+ }
398
+
218
399
  const expected = normalizeType(schema);
219
400
  if (expected && !matchesType(expected, value)) {
220
401
  issues.push(issue(path, "type", `Expected ${Array.isArray(expected) ? expected.join(" or ") : expected}`));
@@ -243,6 +424,15 @@ function visit(schema, value, path, issues, depth) {
243
424
  function validateObject(schema, value, path, issues, depth) {
244
425
  const props = schema.properties ?? {};
245
426
  const required = Array.isArray(schema.required) ? schema.required : [];
427
+ const keys = Object.keys(value);
428
+
429
+ if (typeof schema.minProperties === "number" && keys.length < schema.minProperties) {
430
+ issues.push(issue(path, "min_properties", `Expected at least ${schema.minProperties} properties`));
431
+ }
432
+
433
+ if (typeof schema.maxProperties === "number" && keys.length > schema.maxProperties) {
434
+ issues.push(issue(path, "max_properties", `Expected at most ${schema.maxProperties} properties`));
435
+ }
246
436
 
247
437
  for (const key of required) {
248
438
  if (!Object.prototype.hasOwnProperty.call(value, key)) {
@@ -273,7 +463,25 @@ function validateArray(schema, value, path, issues, depth) {
273
463
  issues.push(issue(path, "max_items", `Expected at most ${schema.maxItems} items`));
274
464
  }
275
465
 
276
- if (schema.items && !Array.isArray(schema.items)) {
466
+ if (schema.uniqueItems === true) {
467
+ const seen = new Set();
468
+ for (const itemValue of value) {
469
+ const key = stableStringify(itemValue);
470
+ if (seen.has(key)) {
471
+ issues.push(issue(path, "unique_items", "Expected array items to be unique"));
472
+ break;
473
+ }
474
+ seen.add(key);
475
+ }
476
+ }
477
+
478
+ if (Array.isArray(schema.items)) {
479
+ schema.items.forEach((itemSchema, index) => {
480
+ if (index < value.length) {
481
+ visit(itemSchema, value[index], `${path}[${index}]`, issues, depth + 1);
482
+ }
483
+ });
484
+ } else if (schema.items) {
277
485
  value.forEach((itemValue, index) => {
278
486
  visit(schema.items, itemValue, `${path}[${index}]`, issues, depth + 1);
279
487
  });
@@ -290,7 +498,8 @@ function validateString(schema, value, path, issues) {
290
498
  }
291
499
 
292
500
  if (typeof schema.pattern === "string") {
293
- const pattern = new RegExp(schema.pattern);
501
+ const pattern = safeRegExp(schema.pattern, path, issues);
502
+ if (!pattern) return;
294
503
  if (!pattern.test(value)) {
295
504
  issues.push(issue(path, "pattern", `Expected string to match /${schema.pattern}/`));
296
505
  }
@@ -313,6 +522,47 @@ function validateNumber(schema, value, path, issues) {
313
522
  if (typeof schema.maximum === "number" && value > schema.maximum) {
314
523
  issues.push(issue(path, "maximum", `Expected value <= ${schema.maximum}`));
315
524
  }
525
+
526
+ if (typeof schema.exclusiveMinimum === "number" && value <= schema.exclusiveMinimum) {
527
+ issues.push(issue(path, "exclusive_minimum", `Expected value > ${schema.exclusiveMinimum}`));
528
+ }
529
+
530
+ if (typeof schema.exclusiveMaximum === "number" && value >= schema.exclusiveMaximum) {
531
+ issues.push(issue(path, "exclusive_maximum", `Expected value < ${schema.exclusiveMaximum}`));
532
+ }
533
+
534
+ if (typeof schema.multipleOf === "number" && schema.multipleOf > 0) {
535
+ const quotient = value / schema.multipleOf;
536
+ if (Math.abs(quotient - Math.round(quotient)) > Number.EPSILON * 100) {
537
+ issues.push(issue(path, "multiple_of", `Expected a multiple of ${schema.multipleOf}`));
538
+ }
539
+ }
540
+ }
541
+
542
+ function validateCompositions(schema, value, path, issues, depth) {
543
+ if (Array.isArray(schema.allOf)) {
544
+ for (const child of schema.allOf) {
545
+ visit(child, value, path, issues, depth + 1);
546
+ }
547
+ }
548
+
549
+ if (Array.isArray(schema.anyOf)) {
550
+ const matches = schema.anyOf.filter((child) => validate(child, value).ok);
551
+ if (matches.length === 0) {
552
+ issues.push(issue(path, "any_of", "Expected value to match at least one schema"));
553
+ return false;
554
+ }
555
+ }
556
+
557
+ if (Array.isArray(schema.oneOf)) {
558
+ const matches = schema.oneOf.filter((child) => validate(child, value).ok);
559
+ if (matches.length !== 1) {
560
+ issues.push(issue(path, "one_of", `Expected value to match exactly one schema, matched ${matches.length}`));
561
+ return false;
562
+ }
563
+ }
564
+
565
+ return true;
316
566
  }
317
567
 
318
568
  function normalizeType(schema) {
@@ -374,8 +624,155 @@ function stableStringify(value) {
374
624
  return `{${entries.map(([key, child]) => `${JSON.stringify(key)}:${stableStringify(child)}`).join(",")}}`;
375
625
  }
376
626
 
377
- function firstJsonSlice(source) {
627
+ function parseJsonValues(source) {
628
+ try {
629
+ return [parseJsonText(source)];
630
+ } catch {
631
+ return jsonSlices(source)
632
+ .map((slice) => {
633
+ try {
634
+ return parseJsonText(slice.value);
635
+ } catch {
636
+ return undefined;
637
+ }
638
+ })
639
+ .filter((value) => value !== undefined);
640
+ }
641
+ }
642
+
643
+ function maskRanges(source, ranges) {
644
+ if (ranges.length === 0) {
645
+ return source;
646
+ }
647
+
648
+ const chars = [...source];
649
+ for (const [start, end] of ranges) {
650
+ for (let index = start; index < end; index += 1) {
651
+ chars[index] = " ";
652
+ }
653
+ }
654
+ return chars.join("");
655
+ }
656
+
657
+ function parseJsonText(source) {
658
+ try {
659
+ return JSON.parse(source);
660
+ } catch {
661
+ return JSON.parse(repairJsonText(source));
662
+ }
663
+ }
664
+
665
+ function jsonMatches(source) {
666
+ return jsonSlices(source)
667
+ .map((slice) => {
668
+ try {
669
+ return {
670
+ start: slice.start,
671
+ end: slice.end,
672
+ value: parseJsonText(slice.value)
673
+ };
674
+ } catch {
675
+ return null;
676
+ }
677
+ })
678
+ .filter(Boolean);
679
+ }
680
+
681
+ function jsonSlices(source) {
682
+ const slices = [];
378
683
  for (let index = 0; index < source.length; index += 1) {
684
+ const slice = firstJsonSlice(source, index);
685
+ if (slice) {
686
+ slices.push(slice);
687
+ index = slice.end - 1;
688
+ }
689
+ }
690
+ return slices;
691
+ }
692
+
693
+ function safeRegExp(pattern, path, issues) {
694
+ try {
695
+ return new RegExp(pattern);
696
+ } catch (error) {
697
+ const detail = error instanceof Error ? error.message : "Invalid regular expression";
698
+ issues.push(issue(path, "invalid_pattern", detail));
699
+ return null;
700
+ }
701
+ }
702
+
703
+ function schemaName(schema, name) {
704
+ const candidate = typeof name === "string" && name.trim() ? name : schema.title;
705
+ const fallback = "structured_output";
706
+ return (candidate ?? fallback)
707
+ .toString()
708
+ .trim()
709
+ .replace(/[^a-zA-Z0-9_-]+/g, "_")
710
+ .replace(/^_+|_+$/g, "")
711
+ .slice(0, 64) || fallback;
712
+ }
713
+
714
+ function providerDescription(schema, options) {
715
+ if (typeof options.description === "string" && options.description.trim()) {
716
+ return options.description.trim();
717
+ }
718
+ if (typeof schema.description === "string" && schema.description.trim()) {
719
+ return schema.description.trim();
720
+ }
721
+ return `Return ${schemaName(schema, options.name)} as JSON.`;
722
+ }
723
+
724
+ function stripJsonComments(text) {
725
+ let output = "";
726
+ let inString = false;
727
+ let escaped = false;
728
+
729
+ for (let index = 0; index < text.length; index += 1) {
730
+ const char = text[index];
731
+ const next = text[index + 1];
732
+
733
+ if (escaped) {
734
+ output += char;
735
+ escaped = false;
736
+ continue;
737
+ }
738
+
739
+ if (char === "\\") {
740
+ output += char;
741
+ escaped = inString;
742
+ continue;
743
+ }
744
+
745
+ if (char === "\"") {
746
+ output += char;
747
+ inString = !inString;
748
+ continue;
749
+ }
750
+
751
+ if (!inString && char === "/" && next === "/") {
752
+ while (index < text.length && text[index] !== "\n") {
753
+ index += 1;
754
+ }
755
+ output += "\n";
756
+ continue;
757
+ }
758
+
759
+ if (!inString && char === "/" && next === "*") {
760
+ index += 2;
761
+ while (index < text.length && !(text[index] === "*" && text[index + 1] === "/")) {
762
+ index += 1;
763
+ }
764
+ index += 1;
765
+ continue;
766
+ }
767
+
768
+ output += char;
769
+ }
770
+
771
+ return output;
772
+ }
773
+
774
+ function firstJsonSlice(source, fromIndex = 0) {
775
+ for (let index = fromIndex; index < source.length; index += 1) {
379
776
  const char = source[index];
380
777
  if (char !== "{" && char !== "[") continue;
381
778
 
@@ -409,11 +806,15 @@ function firstJsonSlice(source) {
409
806
  } else if (current === "}" || current === "]") {
410
807
  if (current !== stack.pop()) break;
411
808
  if (stack.length === 0) {
412
- return source.slice(index, cursor + 1);
809
+ return {
810
+ start: index,
811
+ end: cursor + 1,
812
+ value: source.slice(index, cursor + 1)
813
+ };
413
814
  }
414
815
  }
415
816
  }
416
817
  }
417
818
 
418
- return "";
819
+ return null;
419
820
  }