@vertesia/build-tools 1.5.0-dev.20260725.083715Z → 1.5.0-dev.20260804.124748Z

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.
Files changed (59) hide show
  1. package/README.md +297 -266
  2. package/lib/bin/build.js +26 -1
  3. package/lib/bin/build.js.map +1 -1
  4. package/lib/bin/config.d.ts +12 -0
  5. package/lib/bin/config.d.ts.map +1 -1
  6. package/lib/bin/config.js +25 -0
  7. package/lib/bin/config.js.map +1 -1
  8. package/lib/core/skill-markdown/load-catalog.d.ts +30 -0
  9. package/lib/core/skill-markdown/load-catalog.d.ts.map +1 -0
  10. package/lib/core/skill-markdown/load-catalog.js +69 -0
  11. package/lib/core/skill-markdown/load-catalog.js.map +1 -0
  12. package/lib/core/skill-markdown/preprocess.d.ts +127 -0
  13. package/lib/core/skill-markdown/preprocess.d.ts.map +1 -0
  14. package/lib/core/skill-markdown/preprocess.js +455 -0
  15. package/lib/core/skill-markdown/preprocess.js.map +1 -0
  16. package/lib/core/skill-markdown/schema-validator.d.ts +109 -0
  17. package/lib/core/skill-markdown/schema-validator.d.ts.map +1 -0
  18. package/lib/core/skill-markdown/schema-validator.js +420 -0
  19. package/lib/core/skill-markdown/schema-validator.js.map +1 -0
  20. package/lib/core/transformers/index.d.ts +1 -1
  21. package/lib/core/transformers/index.d.ts.map +1 -1
  22. package/lib/core/transformers/index.js +1 -1
  23. package/lib/core/transformers/index.js.map +1 -1
  24. package/lib/core/transformers/skill.d.ts +15 -1
  25. package/lib/core/transformers/skill.d.ts.map +1 -1
  26. package/lib/core/transformers/skill.js +82 -60
  27. package/lib/core/transformers/skill.js.map +1 -1
  28. package/lib/import-transform/detector.d.ts +19 -6
  29. package/lib/import-transform/detector.d.ts.map +1 -1
  30. package/lib/import-transform/detector.js +73 -24
  31. package/lib/import-transform/detector.js.map +1 -1
  32. package/lib/import-transform/index.d.ts.map +1 -1
  33. package/lib/import-transform/index.js +10 -1
  34. package/lib/import-transform/index.js.map +1 -1
  35. package/lib/import-transform/patterns.d.ts +7 -8
  36. package/lib/import-transform/patterns.d.ts.map +1 -1
  37. package/lib/import-transform/patterns.js +7 -8
  38. package/lib/import-transform/patterns.js.map +1 -1
  39. package/lib/index.d.ts +7 -3
  40. package/lib/index.d.ts.map +1 -1
  41. package/lib/index.js +9 -3
  42. package/lib/index.js.map +1 -1
  43. package/lib/vite/dev-server.d.ts +10 -0
  44. package/lib/vite/dev-server.d.ts.map +1 -1
  45. package/lib/vite/dev-server.js +36 -4
  46. package/lib/vite/dev-server.js.map +1 -1
  47. package/package.json +9 -12
  48. package/src/bin/build.ts +31 -2
  49. package/src/bin/config.ts +27 -0
  50. package/src/core/skill-markdown/load-catalog.ts +100 -0
  51. package/src/core/skill-markdown/preprocess.ts +632 -0
  52. package/src/core/skill-markdown/schema-validator.ts +517 -0
  53. package/src/core/transformers/index.ts +3 -0
  54. package/src/core/transformers/skill.ts +116 -53
  55. package/src/import-transform/detector.ts +78 -26
  56. package/src/import-transform/index.ts +12 -2
  57. package/src/import-transform/patterns.ts +7 -12
  58. package/src/index.ts +35 -2
  59. package/src/vite/dev-server.ts +52 -4
@@ -0,0 +1,517 @@
1
+ /**
2
+ * A ready-made `validateExample` for `preprocessSkillMarkdown`.
3
+ *
4
+ * The preprocessor deliberately knows nothing about schemas — it hands each `tool=`-tagged
5
+ * payload to a callback. This module is the callback most consumers want: JSON Schema validation
6
+ * plus the one thing JSON Schema structurally cannot do.
7
+ *
8
+ * That gap is dispatchers. A tool like `batch_execute` declares `tool_name: { type: 'string' }`
9
+ * and an opaque `input` object, so AJV accepts `{"tool_name": "web_search", "input": {...}}` even
10
+ * when `web_search` does not exist and the input belongs to a different tool's schema. A
11
+ * `dispatch` descriptor names the field carrying the tool name and the field carrying its
12
+ * payload, which is enough to resolve one against the registry and validate the other against the
13
+ * *dispatched* tool.
14
+ *
15
+ * Input is plain data — names, JSON Schemas, descriptors — so this stays independent of any
16
+ * particular tool registry.
17
+ */
18
+
19
+ import { Ajv, type ValidateFunction } from 'ajv';
20
+
21
+ export interface ToolDispatchDescriptor {
22
+ /** Path to the field holding a dispatched tool name. Dots nest, `[]` walks an array. */
23
+ field: string;
24
+ /** Path to the payload forwarded to that tool, if any. */
25
+ inputField?: string;
26
+ /** Tool names the dispatcher refuses at runtime. */
27
+ deny?: readonly string[];
28
+ /**
29
+ * True when the runtime strips a leading `+` or `-` before resolving the name — the selection
30
+ * prefixes that add to, or remove from, the default toolset. Without this the documented
31
+ * `+learn_web_search` form reads as an unregistered tool.
32
+ */
33
+ stripsSelectionPrefix?: boolean;
34
+ }
35
+
36
+ export interface ToolSchemaEntry {
37
+ name: string;
38
+ /** JSON Schema for the tool's parameters. Absent when the provider exposes no schema. */
39
+ params?: unknown;
40
+ dispatch?: ToolDispatchDescriptor;
41
+ }
42
+
43
+ /**
44
+ * Walk a dotted/`[]` path, returning every value found.
45
+ *
46
+ * `inputs[].input` over `{inputs: [{input: A}, {input: B}]}` yields `[A, B]`. Missing segments
47
+ * yield nothing rather than `undefined`, so callers cannot mistake absence for a null payload.
48
+ */
49
+ export function nodesAtPath(value: unknown, path: string): unknown[] {
50
+ let current: unknown[] = [value];
51
+ for (const segment of path.split('.')) {
52
+ const isArray = segment.endsWith('[]');
53
+ const key = isArray ? segment.slice(0, -2) : segment;
54
+ const next: unknown[] = [];
55
+ for (const node of current) {
56
+ if (typeof node !== 'object' || node === null) {
57
+ continue;
58
+ }
59
+ const child = (node as Record<string, unknown>)[key];
60
+ if (child === undefined) {
61
+ continue;
62
+ }
63
+ if (isArray) {
64
+ if (Array.isArray(child)) {
65
+ next.push(...child);
66
+ }
67
+ } else {
68
+ next.push(child);
69
+ }
70
+ }
71
+ current = next;
72
+ }
73
+ return current;
74
+ }
75
+
76
+ /** Path values narrowed to strings — used for fields that carry a tool name. */
77
+ export function toolNamesAtPath(value: unknown, path: string): string[] {
78
+ return nodesAtPath(value, path).filter((v): v is string => typeof v === 'string');
79
+ }
80
+
81
+ /** Walk the same dotted/`[]` path through a JSON Schema rather than through a value. */
82
+ export function schemaNodeAtPath(schema: unknown, path: string): Record<string, unknown> | undefined {
83
+ let node = schema as Record<string, unknown> | undefined;
84
+ for (const segment of path.split('.')) {
85
+ const isArray = segment.endsWith('[]');
86
+ const key = isArray ? segment.slice(0, -2) : segment;
87
+ const properties = node?.properties as Record<string, unknown> | undefined;
88
+ node = properties?.[key] as Record<string, unknown> | undefined;
89
+ if (isArray) {
90
+ node = node?.items as Record<string, unknown> | undefined;
91
+ }
92
+ if (!node) {
93
+ return undefined;
94
+ }
95
+ }
96
+ return node;
97
+ }
98
+
99
+ /**
100
+ * A schema node together with the resource roots a local `$ref` inside it resolves against.
101
+ *
102
+ * Roots are innermost-first. A `$defs` block belongs to the schema that declares it, and these
103
+ * schemas embed whole resources — `create_view.configuration` carries its own `$id` and `$defs` —
104
+ * so `#/$defs/layout` inside it means *its* definitions, not the tool's.
105
+ */
106
+ interface SchemaCursor {
107
+ node: Record<string, unknown>;
108
+ roots: readonly Record<string, unknown>[];
109
+ }
110
+
111
+ /** `~1` and `~0` are the JSON Pointer escapes for `/` and `~`. */
112
+ function unescapePointer(segment: string): string {
113
+ return segment.replace(/~1/g, '/').replace(/~0/g, '~');
114
+ }
115
+
116
+ function isSchemaObject(value: unknown): value is Record<string, unknown> {
117
+ return typeof value === 'object' && value !== null;
118
+ }
119
+
120
+ /** Enter a node, making it a resolution root when it declares definitions or its own identity. */
121
+ function descend(node: Record<string, unknown>, roots: readonly Record<string, unknown>[]): SchemaCursor {
122
+ const isResource = node.$defs !== undefined || node.definitions !== undefined || node.$id !== undefined;
123
+ return { node, roots: isResource ? [node, ...roots] : roots };
124
+ }
125
+
126
+ /**
127
+ * Follow a local `$ref` to the node it names.
128
+ *
129
+ * Only same-document pointers are resolved. An external or remote reference is *not* guessed at:
130
+ * the caller reports it, because silently treating an unresolved reference as "field absent" would
131
+ * blame the author for the schema's shape.
132
+ */
133
+ function followRef(ref: string, roots: readonly Record<string, unknown>[]): Record<string, unknown> | undefined {
134
+ if (ref === '#') {
135
+ return roots[0];
136
+ }
137
+ if (!ref.startsWith('#/')) {
138
+ return undefined;
139
+ }
140
+ const segments = ref.slice(2).split('/').map(unescapePointer);
141
+ for (const root of roots) {
142
+ let node: unknown = root;
143
+ for (const segment of segments) {
144
+ node = isSchemaObject(node) ? node[segment] : undefined;
145
+ }
146
+ if (isSchemaObject(node)) {
147
+ return node;
148
+ }
149
+ }
150
+ return undefined;
151
+ }
152
+
153
+ /**
154
+ * The branches a schema node offers: itself, its composition branches, and whatever its `$ref`
155
+ * points at, all flattened.
156
+ *
157
+ * A walk that stopped at a composed or referenced node would report a field that plainly exists as
158
+ * missing, which for a fail-closed check is worse than not checking. Unresolvable references are
159
+ * collected rather than ignored, so the caller can say so instead of blaming the field.
160
+ */
161
+ function branchesOf(cursor: SchemaCursor, unresolved: string[], seen = new Set<unknown>()): SchemaCursor[] {
162
+ if (seen.has(cursor.node)) {
163
+ // A recursive `$ref` (a tree node whose children are the same type) would loop forever.
164
+ return [];
165
+ }
166
+ seen.add(cursor.node);
167
+
168
+ const here = descend(cursor.node, cursor.roots);
169
+ const out: SchemaCursor[] = [here];
170
+
171
+ const ref = here.node.$ref;
172
+ if (typeof ref === 'string') {
173
+ const target = followRef(ref, here.roots);
174
+ if (target) {
175
+ out.push(...branchesOf({ node: target, roots: here.roots }, unresolved, seen));
176
+ } else {
177
+ unresolved.push(ref);
178
+ }
179
+ }
180
+
181
+ for (const group of [here.node.anyOf, here.node.oneOf, here.node.allOf]) {
182
+ for (const branch of Array.isArray(group) ? group : []) {
183
+ if (isSchemaObject(branch)) {
184
+ out.push(...branchesOf({ node: branch, roots: here.roots }, unresolved, seen));
185
+ }
186
+ }
187
+ }
188
+ return out;
189
+ }
190
+
191
+ /** One step of a dotted/`[]` path, across composition branches and local references. */
192
+ function stepInto(cursors: readonly SchemaCursor[], segment: string, unresolved: string[]): SchemaCursor[] {
193
+ const isArray = segment.endsWith('[]');
194
+ const key = isArray ? segment.slice(0, -2) : segment;
195
+ const found: SchemaCursor[] = [];
196
+ for (const branch of cursors.flatMap((cursor) => branchesOf(cursor, unresolved))) {
197
+ const properties = branch.node.properties as Record<string, unknown> | undefined;
198
+ const child = properties?.[key];
199
+ if (!isSchemaObject(child)) {
200
+ continue;
201
+ }
202
+ const target = isArray ? child.items : child;
203
+ if (isSchemaObject(target)) {
204
+ found.push({ node: target, roots: branch.roots });
205
+ }
206
+ }
207
+ return found;
208
+ }
209
+
210
+ /** Property names declared at a node, across branches and references, for a "did you mean" list. */
211
+ function propertyNamesOf(cursors: readonly SchemaCursor[]): string[] {
212
+ const names = new Set<string>();
213
+ for (const branch of cursors.flatMap((cursor) => branchesOf(cursor, []))) {
214
+ for (const name of Object.keys((branch.node.properties as Record<string, unknown>) ?? {})) {
215
+ names.add(name);
216
+ }
217
+ }
218
+ return [...names].sort();
219
+ }
220
+
221
+ /**
222
+ * Build a `validateField` callback over a set of tools.
223
+ *
224
+ * A skill that says a tool "takes `query` as an object" is making a claim about that tool's
225
+ * schema, and it is exactly the claim that rots when a parameter is renamed. `{@param …}` states
226
+ * the claim so this can check it.
227
+ *
228
+ * Every path that cannot be judged is reported rather than passed: a tool absent from these
229
+ * entries, a tool with no visible schema, a reference that does not resolve. A construct that
230
+ * looks checked while checking nothing is the failure mode a `tool=` tag on an unvalidatable tool
231
+ * is already rejected for, and it is worse here — `{@param …}` exists only to be checked.
232
+ */
233
+ export function createSchemaFieldValidator(
234
+ tools: readonly ToolSchemaEntry[],
235
+ ): (ref: { tool: string; path: string }) => string[] {
236
+ const byName = new Map(tools.map((t) => [t.name, t]));
237
+
238
+ return ({ tool: toolName, path }) => {
239
+ const tool = byName.get(toolName);
240
+ if (!tool) {
241
+ // The preprocessor resolves the name against the catalog before calling this, so
242
+ // reaching here means the catalog's names and its schema entries disagree.
243
+ return [`cannot be checked: '${toolName}' is absent from this build's schema entries`];
244
+ }
245
+ if (!tool.params) {
246
+ return [`cannot be checked: '${toolName}' exposes no schema to this build`];
247
+ }
248
+
249
+ let cursors: SchemaCursor[] = [{ node: tool.params as Record<string, unknown>, roots: [] }];
250
+ const walked: string[] = [];
251
+ for (const segment of path.split('.')) {
252
+ const unresolved: string[] = [];
253
+ const next = stepInto(cursors, segment, unresolved);
254
+ if (next.length === 0) {
255
+ const where = walked.length > 0 ? `'${walked.join('.')}' of ` : '';
256
+ if (unresolved.length > 0) {
257
+ // Reporting "does not declare" here would blame the author for the schema's
258
+ // shape — the field may well exist behind the reference.
259
+ return [
260
+ `cannot be checked: reaching '${segment}' crosses ${where}'${toolName}' at ` +
261
+ `an unresolvable reference ('${unresolved[0]}')`,
262
+ ];
263
+ }
264
+ const available = propertyNamesOf(cursors);
265
+ return [
266
+ `names '${segment}', which ${where}'${toolName}' does not declare` +
267
+ (available.length > 0 ? ` (declared: ${available.join(', ')})` : ''),
268
+ ];
269
+ }
270
+ walked.push(segment);
271
+ cursors = next;
272
+ }
273
+ return [];
274
+ };
275
+ }
276
+
277
+ /** A string, or an array of strings. Anything else cannot carry a tool name. */
278
+ function carriesToolName(node: Record<string, unknown> | undefined): boolean {
279
+ if (!node) {
280
+ return false;
281
+ }
282
+ if (node.type === 'string') {
283
+ return true;
284
+ }
285
+ const items = node.items as Record<string, unknown> | undefined;
286
+ return node.type === 'array' && items?.type === 'string';
287
+ }
288
+
289
+ /**
290
+ * Is the descriptor coherent with the tool's own schema?
291
+ *
292
+ * Returns the problems, empty when sound. Callers treat a problem as an error rather than
293
+ * skipping the check: a descriptor pointing at a renamed field would otherwise disable dispatcher
294
+ * validation for that tool silently, which is the worst possible failure for a safety net.
295
+ */
296
+ export function checkDispatchDescriptor(tool: ToolSchemaEntry): string[] {
297
+ const dispatch = tool.dispatch;
298
+ if (!dispatch || !tool.params) {
299
+ return [];
300
+ }
301
+ const fieldNode = schemaNodeAtPath(tool.params, dispatch.field);
302
+ if (!fieldNode) {
303
+ return [`dispatch.field '${dispatch.field}' does not exist in the params schema`];
304
+ }
305
+ if (!carriesToolName(fieldNode)) {
306
+ return [
307
+ `dispatch.field '${dispatch.field}' is not a string or array of strings, so it cannot carry a tool name`,
308
+ ];
309
+ }
310
+ if (dispatch.inputField && !schemaNodeAtPath(tool.params, dispatch.inputField)) {
311
+ return [`dispatch.inputField '${dispatch.inputField}' does not exist in the params schema`];
312
+ }
313
+ return [];
314
+ }
315
+
316
+ /** One dispatched name found in a payload, already judged against the registry. */
317
+ export interface DispatchResolution {
318
+ name: string;
319
+ /** Set when the name resolves to a usable tool. */
320
+ tool?: ToolSchemaEntry;
321
+ /** Set when it does not: the reason, ready to report. */
322
+ problem?: string;
323
+ }
324
+
325
+ /**
326
+ * Resolve every dispatched name a payload carries, honouring the descriptor's deny list.
327
+ *
328
+ * Shared so the build gate and the repository audit apply one rule. They differ only in how they
329
+ * report: one throws a build error, the other builds a `Finding` with a file and a subject.
330
+ */
331
+ export function resolveDispatchedNames(
332
+ tool: ToolSchemaEntry,
333
+ value: unknown,
334
+ lookup: (name: string) => ToolSchemaEntry | undefined,
335
+ ): DispatchResolution[] {
336
+ const dispatch = tool.dispatch;
337
+ if (!dispatch) {
338
+ return [];
339
+ }
340
+ const deny = new Set(dispatch.deny ?? []);
341
+ return toolNamesAtPath(value, dispatch.field).map((written) => {
342
+ const name =
343
+ dispatch.stripsSelectionPrefix && (written.startsWith('+') || written.startsWith('-'))
344
+ ? written.slice(1)
345
+ : written;
346
+ if (deny.has(name)) {
347
+ return { name, problem: `names '${written}', which this dispatcher refuses at runtime` };
348
+ }
349
+ const target = lookup(name);
350
+ return target
351
+ ? { name, tool: target }
352
+ : { name, problem: `names '${written}', which is not a registered tool` };
353
+ });
354
+ }
355
+
356
+ /**
357
+ * Pair each forwarded payload with the tool that will receive it.
358
+ *
359
+ * One dispatched name applies to every input; otherwise pair positionally. Any other shape is
360
+ * ambiguous and yields no pairing — guessing there would manufacture exactly the false positives
361
+ * this whole mechanism exists to avoid.
362
+ */
363
+ export function pairDispatchedInputs(
364
+ tool: ToolSchemaEntry,
365
+ value: unknown,
366
+ dispatched: readonly ToolSchemaEntry[],
367
+ ): Array<{ target: ToolSchemaEntry; input: Record<string, unknown> }> {
368
+ const inputField = tool.dispatch?.inputField;
369
+ if (!inputField || dispatched.length === 0) {
370
+ return [];
371
+ }
372
+ const inputs = nodesAtPath(value, inputField);
373
+ const pairs: Array<{ target: ToolSchemaEntry; input: Record<string, unknown> }> = [];
374
+ for (const [index, input] of inputs.entries()) {
375
+ const target =
376
+ dispatched.length === 1
377
+ ? dispatched[0]
378
+ : dispatched.length === inputs.length
379
+ ? dispatched[index]
380
+ : undefined;
381
+ if (target && typeof input === 'object' && input !== null) {
382
+ pairs.push({ target, input: input as Record<string, unknown> });
383
+ }
384
+ }
385
+ return pairs;
386
+ }
387
+
388
+ /** Mirrors the agent runtime's AJV configuration, so the build judges what the runtime judges. */
389
+ /**
390
+ * Sanity-check that a tagged payload plausibly belongs to the tool it names.
391
+ *
392
+ * A tag is a claim that the payload below is *this tool's* payload, but schema validation only
393
+ * enforces that claim for schemas that close over their properties. Against a permissive one, a
394
+ * payload meant for some other tool sails through: a `batch_execute` call tagged `list_artifacts`
395
+ * passed for months, and with it a dispatched tool name that does not exist — the tag had moved
396
+ * the example out of reach of the very check that would have caught it.
397
+ *
398
+ * Sharing not one field with the tool's schema is the signal, and it detects only gross mismatch.
399
+ * Generic names — `id`, `name`, `query` — are declared by many tools, so a wrongly tagged payload
400
+ * built from those still passes. This is a floor, not proof of correct binding; heading agreement
401
+ * (see `preprocessSkillMarkdown`) covers the cases where the author stated the tool in prose.
402
+ *
403
+ * Deliberately weak in the other direction too: any overlap at all is accepted, so a payload using
404
+ * an optional subset stays quiet, and a schema that declares nothing is skipped rather than guessed.
405
+ */
406
+ function checkBinding(toolName: string, tool: ToolSchemaEntry, value: unknown): string[] {
407
+ if (!isSchemaObject(tool.params) || Array.isArray(value) || !isSchemaObject(value)) {
408
+ return [];
409
+ }
410
+ const declared = new Set(propertyNamesOf([descend(tool.params, [tool.params])]));
411
+ const keys = Object.keys(value);
412
+ if (declared.size === 0 || keys.length === 0 || keys.some((key) => declared.has(key))) {
413
+ return [];
414
+ }
415
+ return [
416
+ `is tagged for '${toolName}', which declares none of the payload's fields ` +
417
+ `(${keys.join(', ')}); check that the tag names the intended tool`,
418
+ ];
419
+ }
420
+
421
+ function createAjv(): Ajv {
422
+ return new Ajv({ allErrors: true, coerceTypes: true, strict: false });
423
+ }
424
+
425
+ function describeErrors(validate: ValidateFunction): string {
426
+ return (validate.errors ?? [])
427
+ .slice(0, 4)
428
+ .map((e) => {
429
+ // AJV omits the offending key from its `additionalProperties` text, and that key is
430
+ // almost always the answer — a renamed or invented parameter.
431
+ const offender = (e.params as { additionalProperty?: string } | undefined)?.additionalProperty;
432
+ return `${e.instancePath || '/'} ${e.message}${offender ? ` ('${offender}')` : ''}`;
433
+ })
434
+ .join('; ');
435
+ }
436
+
437
+ /**
438
+ * Build a `validateExample` callback over a set of tools.
439
+ *
440
+ * Returns human-readable problems; an empty array means the example is valid. Tools without a
441
+ * schema are skipped rather than guessed at.
442
+ */
443
+ export function createSchemaExampleValidator(
444
+ tools: readonly ToolSchemaEntry[],
445
+ ): (example: { tool: string; value: unknown }) => string[] {
446
+ const ajv = createAjv();
447
+ const byName = new Map(tools.map((t) => [t.name, t]));
448
+ const validators = new Map<string, ValidateFunction>();
449
+ const uncompilable = new Map<string, string>();
450
+
451
+ for (const tool of tools) {
452
+ if (!tool.params) {
453
+ continue;
454
+ }
455
+ try {
456
+ validators.set(tool.name, ajv.compile(tool.params as object));
457
+ } catch (err) {
458
+ uncompilable.set(tool.name, (err as Error).message);
459
+ }
460
+ }
461
+
462
+ const validateAgainst = (name: string, value: unknown, describe: (detail: string) => string): string[] => {
463
+ const validate = validators.get(name);
464
+ if (!validate) {
465
+ const reason = uncompilable.get(name);
466
+ // A schema AJV cannot compile is a real problem, but not this example's problem.
467
+ return reason ? [describe(`could not be checked: '${name}' has an uncompilable schema (${reason})`)] : [];
468
+ }
469
+ // AJV coerces in place; validate a copy so the caller's value is untouched.
470
+ return validate(structuredClone(value)) ? [] : [describe(describeErrors(validate))];
471
+ };
472
+
473
+ return ({ tool: toolName, value }) => {
474
+ const tool = byName.get(toolName);
475
+ if (!tool) {
476
+ // The preprocessor already reports an unknown tag; nothing to add.
477
+ return [];
478
+ }
479
+
480
+ const errors = validateAgainst(
481
+ toolName,
482
+ value,
483
+ (detail) => `does not satisfy the schema of '${toolName}': ${detail}`,
484
+ );
485
+ errors.push(...checkBinding(toolName, tool, value));
486
+
487
+ if (!tool.dispatch) {
488
+ return errors;
489
+ }
490
+
491
+ // Fail closed on an incoherent descriptor: it would otherwise disable the checks below.
492
+ const descriptorProblems = checkDispatchDescriptor(tool);
493
+ if (descriptorProblems.length > 0) {
494
+ return [...errors, ...descriptorProblems.map((p) => `cannot be checked: '${toolName}' ${p}`)];
495
+ }
496
+
497
+ const resolutions = resolveDispatchedNames(tool, value, (name) => byName.get(name));
498
+ for (const resolution of resolutions) {
499
+ if (resolution.problem) {
500
+ errors.push(resolution.problem);
501
+ }
502
+ }
503
+
504
+ const dispatched = resolutions.flatMap((r) => (r.tool ? [r.tool] : []));
505
+ for (const { target, input } of pairDispatchedInputs(tool, value, dispatched)) {
506
+ errors.push(
507
+ ...validateAgainst(
508
+ target.name,
509
+ input,
510
+ (detail) => `forwards an input to '${target.name}' that does not satisfy its schema: ${detail}`,
511
+ ),
512
+ );
513
+ }
514
+
515
+ return errors;
516
+ };
517
+ }
@@ -12,10 +12,13 @@ export {
12
12
  } from './prompt.js';
13
13
  export { rawTransformer } from './raw.js';
14
14
  export {
15
+ createSkillTransformer,
16
+ isSkillTransformer,
15
17
  type SkillContentType,
16
18
  type SkillDefinition,
17
19
  SkillDefinitionSchema,
18
20
  SkillPropertiesSchema,
21
+ type SkillTransformerConfig,
19
22
  skillTransformer,
20
23
  } from './skill.js';
21
24
  export { skillCollectionTransformer } from './skill-collection.js';