@sembl/core 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/dist/index.js ADDED
@@ -0,0 +1,1059 @@
1
+ // src/schema/json-schema.ts
2
+ var CONSTRAINT_KEYWORDS = [
3
+ "maxLength",
4
+ "minLength",
5
+ "minimum",
6
+ "maximum",
7
+ "minItems",
8
+ "maxItems",
9
+ "pattern"
10
+ ];
11
+ var ARRAY_KEYWORDS = ["minItems", "maxItems"];
12
+ function constraintsToJsonSchema(constraints, dialect) {
13
+ if (!constraints || dialect === "openai-strict") {
14
+ return {};
15
+ }
16
+ const out = {};
17
+ for (const keyword of CONSTRAINT_KEYWORDS) {
18
+ const value = constraints[keyword];
19
+ if (value !== void 0) {
20
+ out[keyword] = value;
21
+ }
22
+ }
23
+ return out;
24
+ }
25
+ function fieldTypeToJsonSchema(fieldType, bundle, options, visiting) {
26
+ switch (fieldType.kind) {
27
+ case "string":
28
+ return { type: "string" };
29
+ case "number":
30
+ return { type: "number" };
31
+ case "boolean":
32
+ return { type: "boolean" };
33
+ case "array":
34
+ return {
35
+ type: "array",
36
+ items: fieldTypeToJsonSchema(fieldType.items, bundle, options, visiting)
37
+ };
38
+ case "enum":
39
+ return { type: "string", enum: fieldType.values };
40
+ case "dynamicEnum": {
41
+ const values = options.resolvedEnums?.[fieldType.sourceId];
42
+ return values && values.length > 0 ? { type: "string", enum: [...values] } : { type: "string" };
43
+ }
44
+ case "object": {
45
+ const nested = bundle?.schemas[fieldType.nestedSchemaId];
46
+ if (nested && !visiting.has(nested.id)) {
47
+ return buildObjectSchema(nested, bundle, options, visiting);
48
+ }
49
+ return { type: "object", additionalProperties: false };
50
+ }
51
+ }
52
+ }
53
+ function fieldToJsonSchema(field, bundle, options, visiting) {
54
+ const base = fieldTypeToJsonSchema(field.type, bundle, options, visiting);
55
+ const dialect = options.dialect ?? "openai-strict";
56
+ const constraints = constraintsToJsonSchema(field.constraints, dialect);
57
+ if (base.type !== "array") {
58
+ return { ...base, ...constraints, description: field.description };
59
+ }
60
+ const arrayLevel = {};
61
+ const itemLevel = {};
62
+ for (const [keyword, value] of Object.entries(constraints)) {
63
+ (ARRAY_KEYWORDS.includes(keyword) ? arrayLevel : itemLevel)[keyword] = value;
64
+ }
65
+ return {
66
+ ...base,
67
+ ...arrayLevel,
68
+ items: { ...base.items, ...itemLevel },
69
+ description: field.description
70
+ };
71
+ }
72
+ function buildObjectSchema(schema, bundle, options, visiting) {
73
+ const properties = {};
74
+ const required = [];
75
+ const dialect = options.dialect ?? "openai-strict";
76
+ visiting.add(schema.id);
77
+ for (const field of schema.fields) {
78
+ const fieldSchema = fieldToJsonSchema(field, bundle, options, visiting);
79
+ if (dialect === "openai-strict") {
80
+ properties[field.name] = field.required ? fieldSchema : { anyOf: [fieldSchema, { type: "null" }] };
81
+ required.push(field.name);
82
+ } else {
83
+ properties[field.name] = fieldSchema;
84
+ if (field.required) {
85
+ required.push(field.name);
86
+ }
87
+ }
88
+ }
89
+ visiting.delete(schema.id);
90
+ return {
91
+ type: "object",
92
+ description: schema.description,
93
+ properties,
94
+ required,
95
+ additionalProperties: false
96
+ };
97
+ }
98
+ function runtimeSchemaToJsonSchema(schema, bundle, options = {}) {
99
+ return buildObjectSchema(schema, bundle, options, /* @__PURE__ */ new Set());
100
+ }
101
+ function toOpenAIJsonSchema(schema, bundle, options = {}) {
102
+ return {
103
+ name: schema.id,
104
+ strict: true,
105
+ schema: runtimeSchemaToJsonSchema(schema, bundle, {
106
+ ...options,
107
+ dialect: "openai-strict"
108
+ })
109
+ };
110
+ }
111
+
112
+ // src/schema/resolve-enum-sources.ts
113
+ function collectFromType(type, path, required, bundle, visiting, usages) {
114
+ switch (type.kind) {
115
+ case "dynamicEnum": {
116
+ const existing = usages.get(type.sourceId);
117
+ if (existing) {
118
+ existing.required ||= required;
119
+ existing.paths.push(path);
120
+ } else {
121
+ usages.set(type.sourceId, { required, paths: [path] });
122
+ }
123
+ break;
124
+ }
125
+ case "array":
126
+ collectFromType(type.items, `${path}[]`, required, bundle, visiting, usages);
127
+ break;
128
+ case "object": {
129
+ const nested = bundle?.schemas[type.nestedSchemaId];
130
+ if (nested && !visiting.has(nested.id)) {
131
+ collectFromSchema(nested, path, required, bundle, visiting, usages);
132
+ }
133
+ break;
134
+ }
135
+ default:
136
+ break;
137
+ }
138
+ }
139
+ function collectFromSchema(schema, parentPath, parentRequired, bundle, visiting, usages) {
140
+ visiting.add(schema.id);
141
+ for (const field of schema.fields) {
142
+ const path = parentPath ? `${parentPath}.${field.name}` : field.name;
143
+ collectFromType(
144
+ field.type,
145
+ path,
146
+ parentRequired && field.required,
147
+ bundle,
148
+ visiting,
149
+ usages
150
+ );
151
+ }
152
+ visiting.delete(schema.id);
153
+ }
154
+ function collectEnumSources(schema, bundle) {
155
+ const usages = /* @__PURE__ */ new Map();
156
+ collectFromSchema(schema, "", true, bundle, /* @__PURE__ */ new Set(), usages);
157
+ return usages;
158
+ }
159
+ async function resolveEnumSources(schema, resolver, bundle) {
160
+ const usages = collectEnumSources(schema, bundle);
161
+ const enums = {};
162
+ const failures = [];
163
+ await Promise.all(
164
+ [...usages].map(async ([sourceId, usage]) => {
165
+ try {
166
+ const values = await resolver(sourceId);
167
+ if (!values || values.length === 0) {
168
+ failures.push({ sourceId, reason: "empty", ...usage });
169
+ return;
170
+ }
171
+ enums[sourceId] = values;
172
+ } catch (cause) {
173
+ failures.push({ sourceId, reason: "threw", cause, ...usage });
174
+ }
175
+ })
176
+ );
177
+ failures.sort((a, b) => a.sourceId.localeCompare(b.sourceId));
178
+ return { enums, failures };
179
+ }
180
+
181
+ // src/schema/registry.ts
182
+ var SchemaRegistry = class {
183
+ schemas = /* @__PURE__ */ new Map();
184
+ /**
185
+ * Register a single schema.
186
+ */
187
+ register(schema) {
188
+ this.schemas.set(schema.id, schema);
189
+ }
190
+ /**
191
+ * Register all schemas from a bundle.
192
+ */
193
+ registerBundle(bundle) {
194
+ for (const schema of Object.values(bundle.schemas)) {
195
+ this.register(schema);
196
+ }
197
+ }
198
+ /**
199
+ * Look up a schema by ID.
200
+ */
201
+ get(id) {
202
+ return this.schemas.get(id);
203
+ }
204
+ /**
205
+ * Get a schema by ID, throwing if not found.
206
+ */
207
+ require(id) {
208
+ const schema = this.schemas.get(id);
209
+ if (!schema) {
210
+ throw new Error(`Schema "${id}" not found in registry`);
211
+ }
212
+ return schema;
213
+ }
214
+ /**
215
+ * Get a SchemaBundle of all registered schemas.
216
+ */
217
+ toBundle() {
218
+ const schemas = {};
219
+ for (const [id, schema] of this.schemas) {
220
+ schemas[id] = schema;
221
+ }
222
+ return { schemas };
223
+ }
224
+ /**
225
+ * Get all registered schema IDs.
226
+ */
227
+ ids() {
228
+ return [...this.schemas.keys()];
229
+ }
230
+ };
231
+
232
+ // src/decorators.ts
233
+ function Schema(description) {
234
+ return function(target) {
235
+ return target;
236
+ };
237
+ }
238
+ function Describe(description) {
239
+ return function(_target, _propertyKey) {
240
+ };
241
+ }
242
+ function Constrain(constraints) {
243
+ return function(_target, _propertyKey) {
244
+ };
245
+ }
246
+ function ValuesFrom(sourceId) {
247
+ return function(_target, _propertyKey) {
248
+ };
249
+ }
250
+
251
+ // src/errors/coerce-error.ts
252
+ var CoerceError = class extends Error {
253
+ issues;
254
+ constructor(issues) {
255
+ const summary = issues.map((i) => ` ${i.path}: ${i.message}`).join("\n");
256
+ super(`Coercion validation failed:
257
+ ${summary}`);
258
+ this.name = "CoerceError";
259
+ this.issues = issues;
260
+ }
261
+ };
262
+
263
+ // src/errors/enum-resolution-error.ts
264
+ var EnumResolutionError = class extends Error {
265
+ failures;
266
+ constructor(failures) {
267
+ const summary = failures.map((f) => {
268
+ const why = f.reason === "empty" ? "resolved to no values" : `threw: ${f.cause instanceof Error ? f.cause.message : String(f.cause)}`;
269
+ return ` ${f.sourceId} (${f.paths.join(", ")}): ${why}`;
270
+ }).join("\n");
271
+ super(`Enum source resolution failed for required fields:
272
+ ${summary}`);
273
+ this.name = "EnumResolutionError";
274
+ this.failures = failures;
275
+ }
276
+ };
277
+
278
+ // src/coerce/prompt-builder.ts
279
+ function describeConstraints(constraints) {
280
+ if (!constraints) {
281
+ return [];
282
+ }
283
+ const phrases = [];
284
+ const { minLength, maxLength, minimum, maximum, minItems, maxItems, pattern } = constraints;
285
+ if (minLength !== void 0 && maxLength !== void 0) {
286
+ phrases.push(`between ${minLength} and ${maxLength} characters`);
287
+ } else if (maxLength !== void 0) {
288
+ phrases.push(`at most ${maxLength} characters`);
289
+ } else if (minLength !== void 0) {
290
+ phrases.push(`at least ${minLength} characters`);
291
+ }
292
+ if (minimum !== void 0 && maximum !== void 0) {
293
+ phrases.push(`between ${minimum} and ${maximum}`);
294
+ } else if (minimum !== void 0) {
295
+ phrases.push(`at least ${minimum}`);
296
+ } else if (maximum !== void 0) {
297
+ phrases.push(`at most ${maximum}`);
298
+ }
299
+ if (minItems !== void 0 && maxItems !== void 0) {
300
+ phrases.push(`between ${minItems} and ${maxItems} entries`);
301
+ } else if (maxItems !== void 0) {
302
+ phrases.push(`at most ${maxItems} entries`);
303
+ } else if (minItems !== void 0) {
304
+ phrases.push(`at least ${minItems} entries`);
305
+ }
306
+ if (pattern !== void 0) {
307
+ phrases.push(`matching the pattern /${pattern}/`);
308
+ }
309
+ return phrases;
310
+ }
311
+ function describeDynamicEnum(sourceId, resolvedEnums) {
312
+ const values = resolvedEnums?.[sourceId];
313
+ if (!values || values.length === 0) {
314
+ return void 0;
315
+ }
316
+ return `exactly one of the ${values.length} allowed "${sourceId}" values enumerated in the JSON schema for this field (never invent a value)`;
317
+ }
318
+ function buildFieldContext(field, parentPath, bundle, depth, options, visiting) {
319
+ const lines = [];
320
+ const indent = " ".repeat(depth);
321
+ const fieldPath = parentPath ? `${parentPath}.${field.name}` : field.name;
322
+ lines.push(
323
+ `${indent}- ${fieldPath} (${field.required ? "required" : "optional"}): ${field.description}`
324
+ );
325
+ const rules = describeConstraints(field.constraints);
326
+ const dynamicSourceId = field.type.kind === "dynamicEnum" ? field.type.sourceId : field.type.kind === "array" && field.type.items.kind === "dynamicEnum" ? field.type.items.sourceId : void 0;
327
+ if (dynamicSourceId) {
328
+ const allowed = describeDynamicEnum(dynamicSourceId, options.resolvedEnums);
329
+ if (allowed) {
330
+ rules.push(allowed);
331
+ }
332
+ }
333
+ if (rules.length > 0) {
334
+ lines.push(`${indent} Limits: ${rules.join("; ")}.`);
335
+ }
336
+ if (field.type.kind === "object" && bundle) {
337
+ const nested = bundle.schemas[field.type.nestedSchemaId];
338
+ if (nested && !visiting.has(nested.id)) {
339
+ visiting.add(nested.id);
340
+ lines.push(`${indent} [${nested.id}: ${nested.description}]`);
341
+ for (const nestedField of nested.fields) {
342
+ lines.push(
343
+ ...buildFieldContext(
344
+ nestedField,
345
+ fieldPath,
346
+ bundle,
347
+ depth + 1,
348
+ options,
349
+ visiting
350
+ )
351
+ );
352
+ }
353
+ visiting.delete(nested.id);
354
+ }
355
+ }
356
+ if (field.type.kind === "array" && field.type.items.kind === "object" && bundle) {
357
+ const nested = bundle.schemas[field.type.items.nestedSchemaId];
358
+ if (nested && !visiting.has(nested.id)) {
359
+ visiting.add(nested.id);
360
+ lines.push(`${indent} [Array of ${nested.id}: ${nested.description}]`);
361
+ for (const nestedField of nested.fields) {
362
+ lines.push(
363
+ ...buildFieldContext(
364
+ nestedField,
365
+ `${fieldPath}[]`,
366
+ bundle,
367
+ depth + 1,
368
+ options,
369
+ visiting
370
+ )
371
+ );
372
+ }
373
+ visiting.delete(nested.id);
374
+ }
375
+ }
376
+ return lines;
377
+ }
378
+ function buildPrompt(schema, bundle, options = {}) {
379
+ const lines = [
380
+ "You are a semantic coercion engine. Your task is to extract structured data from the user's input.",
381
+ "",
382
+ `Target schema: ${schema.id}`,
383
+ `Description: ${schema.description}`,
384
+ "",
385
+ "Fields:"
386
+ ];
387
+ const visiting = /* @__PURE__ */ new Set([schema.id]);
388
+ for (const field of schema.fields) {
389
+ lines.push(...buildFieldContext(field, "", bundle, 0, options, visiting));
390
+ }
391
+ lines.push("");
392
+ lines.push("Instructions:");
393
+ lines.push("- Extract values from the user's input that match the schema fields.");
394
+ lines.push("- Use null for optional fields that cannot be determined from the input.");
395
+ lines.push("- Required fields must always have a valid, non-null value.");
396
+ lines.push("- Interpret the user's input semantically \u2014 infer meaning, don't just pattern match.");
397
+ lines.push("- Respect every stated limit exactly; truncate or drop lower-priority content to stay within it.");
398
+ lines.push("- Return only the structured JSON output matching the schema.");
399
+ return lines.join("\n");
400
+ }
401
+
402
+ // src/coerce/repair.ts
403
+ var MAX_RECEIVED_LENGTH = 200;
404
+ function renderReceived(received) {
405
+ if (received === void 0) {
406
+ return "(missing)";
407
+ }
408
+ const text = JSON.stringify(received) ?? String(received);
409
+ return text.length > MAX_RECEIVED_LENGTH ? `${text.slice(0, MAX_RECEIVED_LENGTH)}\u2026 (truncated)` : text;
410
+ }
411
+ function buildRepairInput(originalInput, rejected, issues) {
412
+ const lines = [
413
+ originalInput,
414
+ "",
415
+ "---",
416
+ "",
417
+ "A previous attempt at this extraction produced:",
418
+ "",
419
+ JSON.stringify(rejected, null, 2),
420
+ "",
421
+ "It was rejected because:",
422
+ ""
423
+ ];
424
+ for (const issue of issues) {
425
+ lines.push(`- ${issue.path}: ${issue.message} (received: ${renderReceived(issue.received)})`);
426
+ }
427
+ lines.push(
428
+ "",
429
+ "Return a corrected object addressing every point above. Keep the values that were already right \u2014 only the listed fields are wrong. If the input genuinely does not support a value, leave the field out rather than inventing one."
430
+ );
431
+ return lines.join("\n");
432
+ }
433
+
434
+ // src/coerce/provenance.ts
435
+ var WRAPPER_SUFFIX = "__WithProvenance";
436
+ var ANNOTATION_SUFFIX = "__Annotated";
437
+ var CONFIDENCE_VALUES = ["high", "medium", "low"];
438
+ var PROVENANCE_INSTRUCTIONS = [
439
+ "",
440
+ "Provenance:",
441
+ "- Every field is wrapped as an object: put the extracted value in `value`.",
442
+ "- Set `confidence` to how well the input supports that value:",
443
+ ' "high" \u2014 stated outright in the input;',
444
+ ' "medium" \u2014 strongly implied, but not stated;',
445
+ ' "low" \u2014 a guess from weak or indirect signals.',
446
+ "- Set `evidence` to the shortest quote from the input the value came from.",
447
+ " Leave `evidence` out when you inferred the value rather than reading it \u2014",
448
+ " do not quote text that does not actually contain it.",
449
+ "- Judge each field on its own. A confident value next to a guessed one is",
450
+ " normal, and marking the guess honestly is more useful than looking sure."
451
+ ].join("\n");
452
+ function annotationSchema(parentId, field) {
453
+ const valueField = {
454
+ name: "value",
455
+ description: field.description,
456
+ type: field.type,
457
+ required: true,
458
+ ...field.constraints !== void 0 ? { constraints: field.constraints } : {}
459
+ };
460
+ return {
461
+ id: `${parentId}__${field.name}${ANNOTATION_SUFFIX}`,
462
+ description: `The extracted value for "${field.name}", with where it came from.`,
463
+ fields: [
464
+ valueField,
465
+ {
466
+ name: "confidence",
467
+ description: "How well the input supported this value.",
468
+ type: { kind: "enum", values: [...CONFIDENCE_VALUES] },
469
+ required: true
470
+ },
471
+ {
472
+ name: "evidence",
473
+ description: "The shortest quote from the input this value was read from. Omit when the value was inferred rather than read.",
474
+ type: { kind: "string" },
475
+ required: false
476
+ }
477
+ ]
478
+ };
479
+ }
480
+ function toProvenanceSchema(schema, bundle) {
481
+ const schemas = { ...bundle?.schemas ?? {} };
482
+ const fields = [];
483
+ for (const field of schema.fields) {
484
+ const annotation = annotationSchema(schema.id, field);
485
+ schemas[annotation.id] = annotation;
486
+ fields.push({
487
+ name: field.name,
488
+ description: field.description,
489
+ type: { kind: "object", nestedSchemaId: annotation.id },
490
+ required: field.required
491
+ });
492
+ }
493
+ const wrapper = {
494
+ id: `${schema.id}${WRAPPER_SUFFIX}`,
495
+ description: schema.description,
496
+ fields
497
+ };
498
+ schemas[wrapper.id] = wrapper;
499
+ return { schema: wrapper, bundle: { schemas } };
500
+ }
501
+ function isConfidence(value) {
502
+ return CONFIDENCE_VALUES.includes(value);
503
+ }
504
+ function splitProvenance(response, schema) {
505
+ const data = {};
506
+ const provenance = {};
507
+ for (const field of schema.fields) {
508
+ const annotated = response[field.name];
509
+ if (annotated === void 0 || annotated === null) {
510
+ data[field.name] = annotated ?? null;
511
+ continue;
512
+ }
513
+ if (typeof annotated !== "object" || Array.isArray(annotated) || !("value" in annotated)) {
514
+ data[field.name] = annotated;
515
+ continue;
516
+ }
517
+ const record = annotated;
518
+ data[field.name] = record.value ?? null;
519
+ if (isConfidence(record.confidence)) {
520
+ const evidence = record.evidence;
521
+ provenance[field.name] = {
522
+ confidence: record.confidence,
523
+ ...typeof evidence === "string" && evidence.length > 0 ? { evidence } : {}
524
+ };
525
+ }
526
+ }
527
+ return { data, provenance };
528
+ }
529
+
530
+ // src/coerce/validator.ts
531
+ var MAX_LISTED_VALUES = 10;
532
+ function summarizeValues(values) {
533
+ if (values.length <= MAX_LISTED_VALUES) {
534
+ return values.join(", ");
535
+ }
536
+ const shown = values.slice(0, MAX_LISTED_VALUES).join(", ");
537
+ return `${shown}, \u2026 (+${values.length - MAX_LISTED_VALUES} more)`;
538
+ }
539
+ function entries(count) {
540
+ return `${count} ${count === 1 ? "entry" : "entries"}`;
541
+ }
542
+ function validateConstraints(value, constraints, path, issues) {
543
+ const { minLength, maxLength, minimum, maximum, minItems, maxItems, pattern } = constraints;
544
+ if (Array.isArray(value)) {
545
+ if (minItems !== void 0 && value.length < minItems) {
546
+ issues.push({
547
+ path,
548
+ message: `Expected at least ${entries(minItems)}, got ${value.length}`,
549
+ received: value
550
+ });
551
+ }
552
+ if (maxItems !== void 0 && value.length > maxItems) {
553
+ issues.push({
554
+ path,
555
+ message: `Expected at most ${entries(maxItems)}, got ${value.length}`,
556
+ received: value
557
+ });
558
+ }
559
+ const { minItems: _min, maxItems: _max, ...itemConstraints } = constraints;
560
+ for (let i = 0; i < value.length; i++) {
561
+ validateConstraints(value[i], itemConstraints, `${path}[${i}]`, issues);
562
+ }
563
+ return;
564
+ }
565
+ if (typeof value === "string") {
566
+ if (minLength !== void 0 && value.length < minLength) {
567
+ issues.push({
568
+ path,
569
+ message: `Expected at least ${minLength} characters, got ${value.length}`,
570
+ received: value
571
+ });
572
+ }
573
+ if (maxLength !== void 0 && value.length > maxLength) {
574
+ issues.push({
575
+ path,
576
+ message: `Expected at most ${maxLength} characters, got ${value.length}`,
577
+ received: value
578
+ });
579
+ }
580
+ if (pattern !== void 0 && !new RegExp(pattern).test(value)) {
581
+ issues.push({
582
+ path,
583
+ message: `Expected a value matching /${pattern}/, got ${JSON.stringify(value)}`,
584
+ received: value
585
+ });
586
+ }
587
+ return;
588
+ }
589
+ if (typeof value === "number") {
590
+ if (minimum !== void 0 && value < minimum) {
591
+ issues.push({
592
+ path,
593
+ message: `Expected a value >= ${minimum}, got ${value}`,
594
+ received: value
595
+ });
596
+ }
597
+ if (maximum !== void 0 && value > maximum) {
598
+ issues.push({
599
+ path,
600
+ message: `Expected a value <= ${maximum}, got ${value}`,
601
+ received: value
602
+ });
603
+ }
604
+ }
605
+ }
606
+ function validateType(value, fieldType, path, bundle, options, issues) {
607
+ if (value === null || value === void 0) {
608
+ return;
609
+ }
610
+ switch (fieldType.kind) {
611
+ case "string":
612
+ if (typeof value !== "string") {
613
+ issues.push({
614
+ path,
615
+ message: `Expected string, got ${typeof value}`,
616
+ received: value
617
+ });
618
+ }
619
+ break;
620
+ case "number":
621
+ if (typeof value !== "number") {
622
+ issues.push({
623
+ path,
624
+ message: `Expected number, got ${typeof value}`,
625
+ received: value
626
+ });
627
+ }
628
+ break;
629
+ case "boolean":
630
+ if (typeof value !== "boolean") {
631
+ issues.push({
632
+ path,
633
+ message: `Expected boolean, got ${typeof value}`,
634
+ received: value
635
+ });
636
+ }
637
+ break;
638
+ case "enum":
639
+ if (typeof value !== "string" || !fieldType.values.includes(value)) {
640
+ issues.push({
641
+ path,
642
+ message: `Expected one of [${fieldType.values.join(", ")}], got ${JSON.stringify(value)}`,
643
+ received: value
644
+ });
645
+ }
646
+ break;
647
+ case "dynamicEnum": {
648
+ const values = options.resolvedEnums?.[fieldType.sourceId];
649
+ if (!values || values.length === 0) {
650
+ if (typeof value !== "string") {
651
+ issues.push({
652
+ path,
653
+ message: `Expected string, got ${typeof value}`,
654
+ received: value
655
+ });
656
+ }
657
+ } else if (typeof value !== "string" || !values.includes(value)) {
658
+ issues.push({
659
+ path,
660
+ message: `Expected one of the ${values.length} allowed "${fieldType.sourceId}" values [${summarizeValues(values)}], got ${JSON.stringify(value)}`,
661
+ received: value
662
+ });
663
+ }
664
+ break;
665
+ }
666
+ case "array":
667
+ if (!Array.isArray(value)) {
668
+ issues.push({
669
+ path,
670
+ message: `Expected array, got ${typeof value}`,
671
+ received: value
672
+ });
673
+ } else {
674
+ for (let i = 0; i < value.length; i++) {
675
+ validateType(
676
+ value[i],
677
+ fieldType.items,
678
+ `${path}[${i}]`,
679
+ bundle,
680
+ options,
681
+ issues
682
+ );
683
+ }
684
+ }
685
+ break;
686
+ case "object": {
687
+ if (typeof value !== "object" || Array.isArray(value)) {
688
+ issues.push({
689
+ path,
690
+ message: `Expected object, got ${Array.isArray(value) ? "array" : typeof value}`,
691
+ received: value
692
+ });
693
+ } else if (bundle) {
694
+ const nested = bundle.schemas[fieldType.nestedSchemaId];
695
+ if (nested) {
696
+ validateFields(
697
+ value,
698
+ nested,
699
+ path,
700
+ bundle,
701
+ true,
702
+ // strict for nested objects in strict mode
703
+ options,
704
+ issues
705
+ );
706
+ }
707
+ }
708
+ break;
709
+ }
710
+ }
711
+ }
712
+ function validateFields(data, schema, parentPath, bundle, strict, options, issues) {
713
+ for (const field of schema.fields) {
714
+ const path = parentPath ? `${parentPath}.${field.name}` : field.name;
715
+ const value = data[field.name];
716
+ if (value === null || value === void 0) {
717
+ if (strict && field.required) {
718
+ issues.push({
719
+ path,
720
+ message: "Required field is missing",
721
+ received: value
722
+ });
723
+ }
724
+ continue;
725
+ }
726
+ validateType(value, field.type, path, bundle, options, issues);
727
+ if (field.constraints) {
728
+ validateConstraints(value, field.constraints, path, issues);
729
+ }
730
+ }
731
+ }
732
+ function validateStrict(data, schema, bundle, options = {}) {
733
+ const issues = [];
734
+ validateFields(data, schema, "", bundle, true, options, issues);
735
+ return issues;
736
+ }
737
+ function validatePartial(data, schema, bundle, options = {}) {
738
+ const issues = [];
739
+ validateFields(data, schema, "", bundle, false, options, issues);
740
+ return issues;
741
+ }
742
+
743
+ // src/tracing/tracer.ts
744
+ var spanCounter = 0;
745
+ function generateSpanId() {
746
+ return `span_${++spanCounter}_${Date.now()}`;
747
+ }
748
+ var Tracer = class {
749
+ sinks;
750
+ constructor(sinks) {
751
+ this.sinks = sinks ?? [];
752
+ }
753
+ startSpan(name, attributes, parent) {
754
+ return {
755
+ id: generateSpanId(),
756
+ name,
757
+ startTime: Date.now(),
758
+ events: [],
759
+ attributes,
760
+ parentId: parent?.id
761
+ };
762
+ }
763
+ endSpan(span) {
764
+ span.endTime = Date.now();
765
+ for (const sink of this.sinks) {
766
+ sink.write(span);
767
+ }
768
+ }
769
+ addEvent(span, name, attributes) {
770
+ span.events.push({
771
+ name,
772
+ timestamp: Date.now(),
773
+ attributes
774
+ });
775
+ }
776
+ };
777
+
778
+ // src/coerce/coerce.ts
779
+ async function resolveEnums(schema, bundle, enumResolver, tracer, parent) {
780
+ if (!enumResolver) {
781
+ return void 0;
782
+ }
783
+ const span = tracer.startSpan("resolveEnums", {}, parent);
784
+ try {
785
+ const { enums, failures } = await resolveEnumSources(
786
+ schema,
787
+ enumResolver,
788
+ bundle
789
+ );
790
+ tracer.addEvent(span, "enumsResolved", {
791
+ sourceIds: Object.keys(enums),
792
+ valueCounts: Object.fromEntries(
793
+ Object.entries(enums).map(([id, values]) => [id, values.length])
794
+ )
795
+ });
796
+ for (const failure of failures) {
797
+ tracer.addEvent(span, "enumSourceFailed", {
798
+ sourceId: failure.sourceId,
799
+ reason: failure.reason,
800
+ required: failure.required,
801
+ paths: failure.paths
802
+ });
803
+ }
804
+ const fatal = failures.filter((f) => f.required);
805
+ if (fatal.length > 0) {
806
+ throw new EnumResolutionError(fatal);
807
+ }
808
+ return enums;
809
+ } finally {
810
+ tracer.endSpan(span);
811
+ }
812
+ }
813
+ async function runCoercion(input, options, { mode, provenance }) {
814
+ const { provider, schema, bundle, enumResolver, traceSinks } = options;
815
+ const maxRepairAttempts = options.maxRepairAttempts ?? 0;
816
+ if (!Number.isInteger(maxRepairAttempts) || maxRepairAttempts < 0) {
817
+ throw new RangeError(
818
+ `maxRepairAttempts must be a non-negative integer, got ${String(options.maxRepairAttempts)}`
819
+ );
820
+ }
821
+ const tracer = new Tracer(traceSinks);
822
+ const rootSpan = tracer.startSpan(mode, { schemaId: schema.id, provenance });
823
+ try {
824
+ const resolvedEnums = await resolveEnums(
825
+ schema,
826
+ bundle,
827
+ enumResolver,
828
+ tracer,
829
+ rootSpan
830
+ );
831
+ const promptSpan = tracer.startSpan("buildPrompt", {}, rootSpan);
832
+ const basePrompt = buildPrompt(schema, bundle, { resolvedEnums });
833
+ const systemPrompt = provenance ? `${basePrompt}
834
+ ${PROVENANCE_INSTRUCTIONS}` : basePrompt;
835
+ tracer.addEvent(promptSpan, "promptBuilt", {
836
+ promptLength: systemPrompt.length
837
+ });
838
+ tracer.endSpan(promptSpan);
839
+ const request = provenance ? toProvenanceSchema(schema, bundle) : { schema, bundle };
840
+ const schemaSpan = tracer.startSpan("buildJsonSchema", {}, rootSpan);
841
+ const jsonSchema = runtimeSchemaToJsonSchema(request.schema, request.bundle, {
842
+ resolvedEnums
843
+ });
844
+ tracer.endSpan(schemaSpan);
845
+ const validate = mode === "coerce" ? validateStrict : validatePartial;
846
+ let userInput = input;
847
+ let issues = [];
848
+ let run = { data: {}, provenance: {} };
849
+ for (let attempt = 0; attempt <= maxRepairAttempts; attempt++) {
850
+ const llmSpan = tracer.startSpan("llmCall", { attempt }, rootSpan);
851
+ const response = await provider.complete({
852
+ systemPrompt,
853
+ userInput,
854
+ jsonSchema,
855
+ schema: request.schema,
856
+ bundle: request.bundle,
857
+ resolvedEnums
858
+ });
859
+ tracer.addEvent(llmSpan, "responseReceived", { usage: response.usage });
860
+ tracer.endSpan(llmSpan);
861
+ run = provenance ? splitProvenance(response.data, schema) : { data: response.data, provenance: {} };
862
+ const validationSpan = tracer.startSpan("validate", { attempt }, rootSpan);
863
+ issues = validate(run.data, schema, bundle, { resolvedEnums });
864
+ tracer.addEvent(validationSpan, "validated", { issueCount: issues.length });
865
+ tracer.endSpan(validationSpan);
866
+ if (issues.length === 0) {
867
+ return run;
868
+ }
869
+ if (attempt < maxRepairAttempts) {
870
+ tracer.addEvent(rootSpan, "repairAttempt", {
871
+ attempt: attempt + 1,
872
+ issueCount: issues.length,
873
+ paths: issues.map((issue) => issue.path)
874
+ });
875
+ userInput = buildRepairInput(input, run.data, issues);
876
+ }
877
+ }
878
+ throw new CoerceError(issues);
879
+ } finally {
880
+ tracer.endSpan(rootSpan);
881
+ }
882
+ }
883
+ function stripNulls(data) {
884
+ const result = {};
885
+ for (const [key, value] of Object.entries(data)) {
886
+ if (value !== null) {
887
+ result[key] = value;
888
+ }
889
+ }
890
+ return result;
891
+ }
892
+ async function coerce(input, options) {
893
+ const { data } = await runCoercion(input, options, {
894
+ mode: "coerce",
895
+ provenance: false
896
+ });
897
+ return data;
898
+ }
899
+ async function partialCoerce(input, options) {
900
+ const { data } = await runCoercion(input, options, {
901
+ mode: "partialCoerce",
902
+ provenance: false
903
+ });
904
+ return stripNulls(data);
905
+ }
906
+ async function coerceWithProvenance(input, options) {
907
+ const { data, provenance } = await runCoercion(input, options, {
908
+ mode: "coerce",
909
+ provenance: true
910
+ });
911
+ return { data, provenance };
912
+ }
913
+ async function partialCoerceWithProvenance(input, options) {
914
+ const { data, provenance } = await runCoercion(input, options, {
915
+ mode: "partialCoerce",
916
+ provenance: true
917
+ });
918
+ return { data: stripNulls(data), provenance };
919
+ }
920
+
921
+ // src/coerce/config.ts
922
+ var SemblConfig = class _SemblConfig {
923
+ static _config = {};
924
+ /** Set global defaults. */
925
+ static configure(config) {
926
+ _SemblConfig._config = { ...config };
927
+ }
928
+ /** Reset global config to empty (useful in tests). */
929
+ static reset() {
930
+ _SemblConfig._config = {};
931
+ }
932
+ /** Read-only access to the current global config. */
933
+ static get current() {
934
+ return _SemblConfig._config;
935
+ }
936
+ };
937
+ function resolveConfig(callConfig) {
938
+ const global = SemblConfig.current;
939
+ const provider = callConfig?.provider ?? global.provider;
940
+ if (!provider) {
941
+ throw new Error(
942
+ "No provider configured. Call SemblConfig.configure({ provider }) or pass { provider } to sembl()."
943
+ );
944
+ }
945
+ return {
946
+ provider,
947
+ bundle: callConfig?.bundle ?? global.bundle,
948
+ enumResolver: callConfig?.enumResolver ?? global.enumResolver,
949
+ traceSinks: callConfig?.traceSinks ?? global.traceSinks,
950
+ maxRepairAttempts: callConfig?.maxRepairAttempts ?? global.maxRepairAttempts
951
+ };
952
+ }
953
+
954
+ // src/coerce/coercible.ts
955
+ function serialize(value) {
956
+ if (typeof value === "string") {
957
+ return value;
958
+ }
959
+ return JSON.stringify(value);
960
+ }
961
+ var Coercible = class _Coercible {
962
+ constructor(_promise, _config) {
963
+ this._promise = _promise;
964
+ this._config = _config;
965
+ }
966
+ /** The per-call options every link in the chain shares. */
967
+ _optionsFor(schema) {
968
+ return {
969
+ provider: this._config.provider,
970
+ schema,
971
+ bundle: this._config.bundle,
972
+ enumResolver: this._config.enumResolver,
973
+ traceSinks: this._config.traceSinks,
974
+ maxRepairAttempts: this._config.maxRepairAttempts
975
+ };
976
+ }
977
+ /**
978
+ * Chain a full coercion to a new schema.
979
+ * The current value is serialized and used as input for the next LLM call.
980
+ */
981
+ coerceTo(schema) {
982
+ const next = this._promise.then(
983
+ (value) => coerce(serialize(value), this._optionsFor(schema))
984
+ );
985
+ return new _Coercible(next, this._config);
986
+ }
987
+ /**
988
+ * Chain a partial coercion to a new schema.
989
+ * The current value is serialized and used as input for the next LLM call.
990
+ */
991
+ partialCoerceTo(schema) {
992
+ const next = this._promise.then(
993
+ (value) => partialCoerce(serialize(value), this._optionsFor(schema))
994
+ );
995
+ return new _Coercible(next, this._config);
996
+ }
997
+ then(onfulfilled, onrejected) {
998
+ return this._promise.then(onfulfilled, onrejected);
999
+ }
1000
+ catch(onrejected) {
1001
+ return this._promise.catch(onrejected);
1002
+ }
1003
+ finally(onfinally) {
1004
+ return this._promise.finally(onfinally);
1005
+ }
1006
+ };
1007
+ function sembl(input, config) {
1008
+ const resolved = resolveConfig(config);
1009
+ const serialized = serialize(input);
1010
+ return new Coercible(Promise.resolve(serialized), resolved);
1011
+ }
1012
+
1013
+ // src/tracing/console-sink.ts
1014
+ var ConsoleSink = class {
1015
+ write(span) {
1016
+ const duration = span.endTime ? span.endTime - span.startTime : "?";
1017
+ const prefix = span.parentId ? " " : "";
1018
+ console.log(
1019
+ `${prefix}[trace] ${span.name} (${duration}ms)`,
1020
+ span.attributes ?? ""
1021
+ );
1022
+ for (const event of span.events) {
1023
+ console.log(
1024
+ `${prefix} [event] ${event.name}`,
1025
+ event.attributes ?? ""
1026
+ );
1027
+ }
1028
+ }
1029
+ };
1030
+ export {
1031
+ CoerceError,
1032
+ Coercible,
1033
+ ConsoleSink,
1034
+ Constrain,
1035
+ Describe,
1036
+ EnumResolutionError,
1037
+ PROVENANCE_INSTRUCTIONS,
1038
+ Schema,
1039
+ SchemaRegistry,
1040
+ SemblConfig,
1041
+ Tracer,
1042
+ ValuesFrom,
1043
+ buildPrompt,
1044
+ buildRepairInput,
1045
+ coerce,
1046
+ coerceWithProvenance,
1047
+ collectEnumSources,
1048
+ partialCoerce,
1049
+ partialCoerceWithProvenance,
1050
+ resolveEnumSources,
1051
+ runtimeSchemaToJsonSchema,
1052
+ sembl,
1053
+ splitProvenance,
1054
+ toOpenAIJsonSchema,
1055
+ toProvenanceSchema,
1056
+ validatePartial,
1057
+ validateStrict
1058
+ };
1059
+ //# sourceMappingURL=index.js.map