@portel/photon-core 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,727 @@
1
+ /**
2
+ * Schema Extractor
3
+ *
4
+ * Extracts JSON schemas from TypeScript method signatures and JSDoc comments
5
+ * Also extracts constructor parameters for config injection
6
+ * Supports Templates (@Template) and Static resources (@Static)
7
+ *
8
+ * Now uses TypeScript's compiler API for robust type parsing
9
+ */
10
+ import * as fs from 'fs/promises';
11
+ import * as ts from 'typescript';
12
+ /**
13
+ * Extract schemas from a Photon MCP class file
14
+ */
15
+ export class SchemaExtractor {
16
+ /**
17
+ * Extract method schemas from source code file
18
+ */
19
+ async extractFromFile(filePath) {
20
+ try {
21
+ const source = await fs.readFile(filePath, 'utf-8');
22
+ return this.extractFromSource(source);
23
+ }
24
+ catch (error) {
25
+ console.error(`Failed to extract schemas from ${filePath}: ${error.message}`);
26
+ return [];
27
+ }
28
+ }
29
+ /**
30
+ * Extract all metadata (tools, templates, statics) from source code
31
+ */
32
+ extractAllFromSource(source) {
33
+ const tools = [];
34
+ const templates = [];
35
+ const statics = [];
36
+ try {
37
+ // If source doesn't contain a class declaration, wrap it in one
38
+ let sourceToParse = source;
39
+ if (!source.includes('class ')) {
40
+ sourceToParse = `export default class Temp {\n${source}\n}`;
41
+ }
42
+ // Parse source file into AST
43
+ const sourceFile = ts.createSourceFile('temp.ts', sourceToParse, ts.ScriptTarget.Latest, true);
44
+ // Helper to process a method declaration
45
+ const processMethod = (member) => {
46
+ const methodName = member.name.getText(sourceFile);
47
+ const jsdoc = this.getJSDocComment(member, sourceFile);
48
+ // Extract parameter type information
49
+ const paramsType = this.getFirstParameterType(member, sourceFile);
50
+ if (!paramsType) {
51
+ return; // Skip methods without proper params
52
+ }
53
+ // Build schema from TypeScript type
54
+ const { properties, required } = this.buildSchemaFromType(paramsType, sourceFile);
55
+ // Extract descriptions from JSDoc
56
+ const paramDocs = this.extractParamDocs(jsdoc);
57
+ const paramConstraints = this.extractParamConstraints(jsdoc);
58
+ // Merge descriptions and constraints into properties
59
+ Object.keys(properties).forEach(key => {
60
+ if (paramDocs.has(key)) {
61
+ properties[key].description = paramDocs.get(key);
62
+ }
63
+ // Apply TypeScript-extracted metadata (before JSDoc, so JSDoc can override)
64
+ if (properties[key]._tsReadOnly) {
65
+ properties[key].readOnly = true;
66
+ delete properties[key]._tsReadOnly; // Clean up internal marker
67
+ }
68
+ // Apply JSDoc constraints (takes precedence over TypeScript)
69
+ if (paramConstraints.has(key)) {
70
+ const constraints = paramConstraints.get(key);
71
+ this.applyConstraints(properties[key], constraints);
72
+ }
73
+ });
74
+ const description = this.extractDescription(jsdoc);
75
+ const inputSchema = {
76
+ type: 'object',
77
+ properties,
78
+ ...(required.length > 0 ? { required } : {}),
79
+ };
80
+ // Check if this is a Template
81
+ if (this.hasTemplateTag(jsdoc)) {
82
+ templates.push({
83
+ name: methodName,
84
+ description,
85
+ inputSchema,
86
+ });
87
+ }
88
+ // Check if this is a Static resource
89
+ else if (this.hasStaticTag(jsdoc)) {
90
+ const uri = this.extractStaticURI(jsdoc) || `static://${methodName}`;
91
+ const mimeType = this.extractMimeType(jsdoc);
92
+ statics.push({
93
+ name: methodName,
94
+ uri,
95
+ description,
96
+ mimeType,
97
+ inputSchema,
98
+ });
99
+ }
100
+ // Otherwise, it's a regular tool
101
+ else {
102
+ const format = this.extractFormat(jsdoc);
103
+ tools.push({
104
+ name: methodName,
105
+ description,
106
+ inputSchema,
107
+ ...(format ? { format } : {}),
108
+ });
109
+ }
110
+ };
111
+ // Visit all nodes in the AST
112
+ const visit = (node) => {
113
+ // Look for class declarations
114
+ if (ts.isClassDeclaration(node)) {
115
+ node.members.forEach((member) => {
116
+ // Look for async methods
117
+ if (ts.isMethodDeclaration(member) &&
118
+ member.modifiers?.some(m => m.kind === ts.SyntaxKind.AsyncKeyword)) {
119
+ processMethod(member);
120
+ }
121
+ });
122
+ }
123
+ ts.forEachChild(node, visit);
124
+ };
125
+ visit(sourceFile);
126
+ }
127
+ catch (error) {
128
+ console.error('Failed to parse TypeScript source:', error.message);
129
+ }
130
+ return { tools, templates, statics };
131
+ }
132
+ /**
133
+ * Extract schemas from source code string (backward compatibility)
134
+ */
135
+ extractFromSource(source) {
136
+ return this.extractAllFromSource(source).tools;
137
+ }
138
+ /**
139
+ * Get JSDoc comment for a node
140
+ */
141
+ getJSDocComment(node, sourceFile) {
142
+ // Use TypeScript's JSDoc extraction
143
+ const jsDocs = node.jsDoc;
144
+ if (jsDocs && jsDocs.length > 0) {
145
+ const jsDoc = jsDocs[0];
146
+ const comment = jsDoc.comment;
147
+ // Get full JSDoc text including tags
148
+ const fullText = sourceFile.getFullText();
149
+ const start = jsDoc.pos;
150
+ const end = jsDoc.end;
151
+ const jsDocText = fullText.substring(start, end);
152
+ // Extract content between /** and */
153
+ const match = jsDocText.match(/\/\*\*([\s\S]*?)\*\//);
154
+ return match ? match[1] : '';
155
+ }
156
+ return '';
157
+ }
158
+ /**
159
+ * Get the first parameter's type node
160
+ */
161
+ getFirstParameterType(method, sourceFile) {
162
+ if (method.parameters.length === 0) {
163
+ return undefined;
164
+ }
165
+ const firstParam = method.parameters[0];
166
+ return firstParam.type;
167
+ }
168
+ /**
169
+ * Build JSON schema from TypeScript type node
170
+ * Extracts: type, optional, readonly
171
+ */
172
+ buildSchemaFromType(typeNode, sourceFile) {
173
+ const properties = {};
174
+ const required = [];
175
+ // Handle type literal (object type)
176
+ if (ts.isTypeLiteralNode(typeNode)) {
177
+ typeNode.members.forEach((member) => {
178
+ if (ts.isPropertySignature(member) && member.name) {
179
+ const propName = member.name.getText(sourceFile);
180
+ const isOptional = member.questionToken !== undefined;
181
+ const isReadonly = member.modifiers?.some(m => m.kind === ts.SyntaxKind.ReadonlyKeyword) || false;
182
+ if (!isOptional) {
183
+ required.push(propName);
184
+ }
185
+ if (member.type) {
186
+ properties[propName] = this.typeNodeToSchema(member.type, sourceFile);
187
+ }
188
+ else {
189
+ properties[propName] = { type: 'object' };
190
+ }
191
+ // Add readonly from TypeScript (JSDoc can override)
192
+ if (isReadonly) {
193
+ properties[propName]._tsReadOnly = true;
194
+ }
195
+ }
196
+ });
197
+ }
198
+ return { properties, required };
199
+ }
200
+ /**
201
+ * Convert TypeScript type node to JSON schema
202
+ */
203
+ typeNodeToSchema(typeNode, sourceFile) {
204
+ const schema = {};
205
+ // Handle union types
206
+ if (ts.isUnionTypeNode(typeNode)) {
207
+ // Check if this is a union of literals that can be converted to enum
208
+ const enumValues = this.extractEnumFromUnion(typeNode, sourceFile);
209
+ if (enumValues) {
210
+ return enumValues;
211
+ }
212
+ // Otherwise use anyOf
213
+ schema.anyOf = typeNode.types.map(t => this.typeNodeToSchema(t, sourceFile));
214
+ return schema;
215
+ }
216
+ // Handle intersection types
217
+ if (ts.isIntersectionTypeNode(typeNode)) {
218
+ schema.allOf = typeNode.types.map(t => this.typeNodeToSchema(t, sourceFile));
219
+ return schema;
220
+ }
221
+ // Handle array types
222
+ if (ts.isArrayTypeNode(typeNode)) {
223
+ schema.type = 'array';
224
+ schema.items = this.typeNodeToSchema(typeNode.elementType, sourceFile);
225
+ return schema;
226
+ }
227
+ // Handle type reference (e.g., Array<string>)
228
+ if (ts.isTypeReferenceNode(typeNode)) {
229
+ const typeName = typeNode.typeName.getText(sourceFile);
230
+ if (typeName === 'Array' && typeNode.typeArguments && typeNode.typeArguments.length > 0) {
231
+ schema.type = 'array';
232
+ schema.items = this.typeNodeToSchema(typeNode.typeArguments[0], sourceFile);
233
+ return schema;
234
+ }
235
+ // For other type references, default to object
236
+ schema.type = 'object';
237
+ return schema;
238
+ }
239
+ // Handle literal types
240
+ if (ts.isLiteralTypeNode(typeNode)) {
241
+ const literal = typeNode.literal;
242
+ if (ts.isStringLiteral(literal)) {
243
+ schema.type = 'string';
244
+ schema.enum = [literal.text];
245
+ return schema;
246
+ }
247
+ if (ts.isNumericLiteral(literal)) {
248
+ schema.type = 'number';
249
+ schema.enum = [parseFloat(literal.text)];
250
+ return schema;
251
+ }
252
+ if (literal.kind === ts.SyntaxKind.TrueKeyword || literal.kind === ts.SyntaxKind.FalseKeyword) {
253
+ schema.type = 'boolean';
254
+ return schema;
255
+ }
256
+ }
257
+ // Handle tuple types
258
+ if (ts.isTupleTypeNode(typeNode)) {
259
+ schema.type = 'array';
260
+ schema.items = typeNode.elements.map(e => this.typeNodeToSchema(e, sourceFile));
261
+ return schema;
262
+ }
263
+ // Handle type literal (nested object)
264
+ if (ts.isTypeLiteralNode(typeNode)) {
265
+ schema.type = 'object';
266
+ const { properties, required } = this.buildSchemaFromType(typeNode, sourceFile);
267
+ schema.properties = properties;
268
+ if (required.length > 0) {
269
+ schema.required = required;
270
+ }
271
+ return schema;
272
+ }
273
+ // Handle keyword types (string, number, boolean, etc.)
274
+ const typeText = typeNode.getText(sourceFile);
275
+ switch (typeText) {
276
+ case 'string':
277
+ schema.type = 'string';
278
+ break;
279
+ case 'number':
280
+ schema.type = 'number';
281
+ break;
282
+ case 'boolean':
283
+ schema.type = 'boolean';
284
+ break;
285
+ case 'any':
286
+ case 'unknown':
287
+ // No type restriction
288
+ break;
289
+ default:
290
+ // Default to object for complex types
291
+ schema.type = 'object';
292
+ }
293
+ return schema;
294
+ }
295
+ /**
296
+ * Extract enum values from a union of literal types
297
+ * Returns a proper enum schema if all types are literals of the same kind
298
+ * Returns an optimized anyOf schema for mixed unions (e.g., number | '+1' | '-1')
299
+ * Returns null if the union should use standard anyOf processing
300
+ */
301
+ extractEnumFromUnion(unionNode, sourceFile) {
302
+ const stringLiterals = [];
303
+ const numberLiterals = [];
304
+ const booleanLiterals = [];
305
+ const nonLiteralTypes = [];
306
+ // Categorize union members
307
+ for (const typeNode of unionNode.types) {
308
+ if (ts.isLiteralTypeNode(typeNode)) {
309
+ const literal = typeNode.literal;
310
+ if (ts.isStringLiteral(literal)) {
311
+ stringLiterals.push(literal.text);
312
+ }
313
+ else if (ts.isNumericLiteral(literal)) {
314
+ numberLiterals.push(parseFloat(literal.text));
315
+ }
316
+ else if (literal.kind === ts.SyntaxKind.TrueKeyword) {
317
+ booleanLiterals.push(true);
318
+ }
319
+ else if (literal.kind === ts.SyntaxKind.FalseKeyword) {
320
+ booleanLiterals.push(false);
321
+ }
322
+ }
323
+ else {
324
+ nonLiteralTypes.push(typeNode);
325
+ }
326
+ }
327
+ // Case 1: All same-type literals - return simple enum
328
+ if (nonLiteralTypes.length === 0) {
329
+ if (stringLiterals.length > 0 && numberLiterals.length === 0 && booleanLiterals.length === 0) {
330
+ return {
331
+ type: 'string',
332
+ enum: stringLiterals,
333
+ };
334
+ }
335
+ if (numberLiterals.length > 0 && stringLiterals.length === 0 && booleanLiterals.length === 0) {
336
+ return {
337
+ type: 'number',
338
+ enum: numberLiterals,
339
+ };
340
+ }
341
+ if (booleanLiterals.length > 0 && stringLiterals.length === 0 && numberLiterals.length === 0) {
342
+ return {
343
+ type: 'boolean',
344
+ enum: booleanLiterals,
345
+ };
346
+ }
347
+ }
348
+ // Case 2: Mixed union with literals - create optimized anyOf
349
+ // Example: number | '+1' | '-1' → anyOf: [{ type: number }, { type: string, enum: ['+1', '-1'] }]
350
+ if (nonLiteralTypes.length > 0 && (stringLiterals.length > 0 || numberLiterals.length > 0 || booleanLiterals.length > 0)) {
351
+ const anyOf = [];
352
+ // Add non-literal types
353
+ for (const typeNode of nonLiteralTypes) {
354
+ anyOf.push(this.typeNodeToSchema(typeNode, sourceFile));
355
+ }
356
+ // Add grouped string literals
357
+ if (stringLiterals.length > 0) {
358
+ anyOf.push({
359
+ type: 'string',
360
+ enum: stringLiterals,
361
+ });
362
+ }
363
+ // Add grouped number literals
364
+ if (numberLiterals.length > 0) {
365
+ anyOf.push({
366
+ type: 'number',
367
+ enum: numberLiterals,
368
+ });
369
+ }
370
+ // Add grouped boolean literals
371
+ if (booleanLiterals.length > 0) {
372
+ anyOf.push({
373
+ type: 'boolean',
374
+ enum: booleanLiterals,
375
+ });
376
+ }
377
+ return { anyOf };
378
+ }
379
+ // Case 3: Complex union or mixed literal types - let standard anyOf handle it
380
+ return null;
381
+ }
382
+ /**
383
+ * Extract constructor parameters for config injection
384
+ */
385
+ extractConstructorParams(source) {
386
+ const params = [];
387
+ try {
388
+ const sourceFile = ts.createSourceFile('temp.ts', source, ts.ScriptTarget.Latest, true);
389
+ const visit = (node) => {
390
+ if (ts.isClassDeclaration(node)) {
391
+ node.members.forEach((member) => {
392
+ if (ts.isConstructorDeclaration(member)) {
393
+ member.parameters.forEach((param) => {
394
+ if (param.name && ts.isIdentifier(param.name)) {
395
+ const name = param.name.getText(sourceFile);
396
+ const type = param.type ? param.type.getText(sourceFile) : 'any';
397
+ const isOptional = param.questionToken !== undefined || param.initializer !== undefined;
398
+ const hasDefault = param.initializer !== undefined;
399
+ let defaultValue = undefined;
400
+ if (param.initializer) {
401
+ defaultValue = this.extractDefaultValue(param.initializer, sourceFile);
402
+ }
403
+ params.push({
404
+ name,
405
+ type,
406
+ isOptional,
407
+ hasDefault,
408
+ defaultValue,
409
+ });
410
+ }
411
+ });
412
+ }
413
+ });
414
+ }
415
+ ts.forEachChild(node, visit);
416
+ };
417
+ visit(sourceFile);
418
+ }
419
+ catch (error) {
420
+ console.error('Failed to extract constructor params:', error.message);
421
+ }
422
+ return params;
423
+ }
424
+ /**
425
+ * Extract default value from initializer
426
+ */
427
+ extractDefaultValue(initializer, sourceFile) {
428
+ // String literals
429
+ if (ts.isStringLiteral(initializer)) {
430
+ return initializer.text;
431
+ }
432
+ // Numeric literals
433
+ if (ts.isNumericLiteral(initializer)) {
434
+ return parseFloat(initializer.text);
435
+ }
436
+ // Boolean literals
437
+ if (initializer.kind === ts.SyntaxKind.TrueKeyword) {
438
+ return true;
439
+ }
440
+ if (initializer.kind === ts.SyntaxKind.FalseKeyword) {
441
+ return false;
442
+ }
443
+ // For complex expressions (function calls, etc.), return as string
444
+ return initializer.getText(sourceFile);
445
+ }
446
+ /**
447
+ * Extract main description from JSDoc comment
448
+ */
449
+ extractDescription(jsdocContent) {
450
+ // Split by @param to get only the description part
451
+ const beforeParams = jsdocContent.split(/@param/)[0];
452
+ // Remove leading * from each line and trim
453
+ const lines = beforeParams
454
+ .split('\n')
455
+ .map((line) => line.trim().replace(/^\*\s?/, ''))
456
+ .filter((line) => line && !line.startsWith('@')); // Exclude @tags and empty lines
457
+ // Take only the last meaningful line (the actual method description)
458
+ // This filters out file headers
459
+ const meaningfulLines = lines.filter(line => line.length > 5); // Filter out short lines
460
+ const description = meaningfulLines.length > 0
461
+ ? meaningfulLines[meaningfulLines.length - 1]
462
+ : lines.join(' ');
463
+ // Clean up multiple spaces
464
+ return description.replace(/\s+/g, ' ').trim() || 'No description';
465
+ }
466
+ /**
467
+ * Extract parameter descriptions from JSDoc @param tags
468
+ * Also removes constraint tags from descriptions
469
+ */
470
+ extractParamDocs(jsdocContent) {
471
+ const paramDocs = new Map();
472
+ const paramRegex = /@param\s+(\w+)\s+(.+)/g;
473
+ let match;
474
+ while ((match = paramRegex.exec(jsdocContent)) !== null) {
475
+ const [, paramName, description] = match;
476
+ // Remove all constraint tags from description
477
+ const cleanDesc = description
478
+ .replace(/\{@min\s+[^}]+\}/g, '')
479
+ .replace(/\{@max\s+[^}]+\}/g, '')
480
+ .replace(/\{@pattern\s+[^}]+\}/g, '')
481
+ .replace(/\{@format\s+[^}]+\}/g, '')
482
+ .replace(/\{@default\s+[^}]+\}/g, '')
483
+ .replace(/\{@unique(?:Items)?\s*\}/g, '')
484
+ .replace(/\{@example\s+[^}]+\}/g, '')
485
+ .replace(/\{@multipleOf\s+[^}]+\}/g, '')
486
+ .replace(/\{@deprecated(?:\s+[^}]+)?\}/g, '')
487
+ .replace(/\{@readOnly\s*\}/g, '')
488
+ .replace(/\{@writeOnly\s*\}/g, '')
489
+ .replace(/\s+/g, ' ') // Collapse multiple spaces
490
+ .trim();
491
+ paramDocs.set(paramName, cleanDesc);
492
+ }
493
+ return paramDocs;
494
+ }
495
+ /**
496
+ * Extract parameter constraints from JSDoc @param tags
497
+ * Supports inline tags: {@min}, {@max}, {@pattern}, {@format}, {@default}, {@unique},
498
+ * {@example}, {@multipleOf}, {@deprecated}, {@readOnly}, {@writeOnly}
499
+ */
500
+ extractParamConstraints(jsdocContent) {
501
+ const constraints = new Map();
502
+ const paramRegex = /@param\s+(\w+)\s+(.+)/g;
503
+ let match;
504
+ while ((match = paramRegex.exec(jsdocContent)) !== null) {
505
+ const [, paramName, description] = match;
506
+ const paramConstraints = {};
507
+ // Extract {@min value} - works for numbers, strings, arrays
508
+ const minMatch = description.match(/\{@min\s+(-?\d+(?:\.\d+)?)\}/);
509
+ if (minMatch) {
510
+ paramConstraints.min = parseFloat(minMatch[1]);
511
+ }
512
+ // Extract {@max value} - works for numbers, strings, arrays
513
+ const maxMatch = description.match(/\{@max\s+(-?\d+(?:\.\d+)?)\}/);
514
+ if (maxMatch) {
515
+ paramConstraints.max = parseFloat(maxMatch[1]);
516
+ }
517
+ // Extract {@pattern regex} - use lookahead to match until tag-closing }
518
+ const patternMatch = description.match(/\{@pattern\s+(.+?)\}(?=\s|$|{@)/);
519
+ if (patternMatch) {
520
+ paramConstraints.pattern = patternMatch[1].trim();
521
+ }
522
+ // Extract {@format formatName} - use lookahead to match until tag-closing }
523
+ const formatMatch = description.match(/\{@format\s+(.+?)\}(?=\s|$|{@)/);
524
+ if (formatMatch) {
525
+ paramConstraints.format = formatMatch[1].trim();
526
+ }
527
+ // Extract {@default value} - use lookahead to match until tag-closing }
528
+ const defaultMatch = description.match(/\{@default\s+(.+?)\}(?=\s|$|{@)/);
529
+ if (defaultMatch) {
530
+ const defaultValue = defaultMatch[1].trim();
531
+ // Try to parse as JSON for numbers, booleans, objects, arrays
532
+ try {
533
+ paramConstraints.default = JSON.parse(defaultValue);
534
+ }
535
+ catch {
536
+ // If not valid JSON, use as string
537
+ paramConstraints.default = defaultValue;
538
+ }
539
+ }
540
+ // Extract {@unique} or {@uniqueItems} - for arrays
541
+ if (description.match(/\{@unique(?:Items)?\s*\}/)) {
542
+ paramConstraints.unique = true;
543
+ }
544
+ // Extract {@example value} - supports multiple examples, use lookahead
545
+ const exampleMatches = description.matchAll(/\{@example\s+(.+?)\}(?=\s|$|{@)/g);
546
+ const examples = [];
547
+ for (const exampleMatch of exampleMatches) {
548
+ const exampleValue = exampleMatch[1].trim();
549
+ // Try to parse as JSON
550
+ try {
551
+ examples.push(JSON.parse(exampleValue));
552
+ }
553
+ catch {
554
+ // If not valid JSON, use as string
555
+ examples.push(exampleValue);
556
+ }
557
+ }
558
+ if (examples.length > 0) {
559
+ paramConstraints.examples = examples;
560
+ }
561
+ // Extract {@multipleOf value} - for numbers
562
+ const multipleOfMatch = description.match(/\{@multipleOf\s+(-?\d+(?:\.\d+)?)\}/);
563
+ if (multipleOfMatch) {
564
+ paramConstraints.multipleOf = parseFloat(multipleOfMatch[1]);
565
+ }
566
+ // Extract {@deprecated message} - use lookahead to match until tag-closing }
567
+ const deprecatedMatch = description.match(/\{@deprecated(?:\s+(.+?))?\}(?=\s|$|{@)/);
568
+ if (deprecatedMatch) {
569
+ paramConstraints.deprecated = deprecatedMatch[1]?.trim() || true;
570
+ }
571
+ // Extract {@readOnly} and {@writeOnly} - track which comes last
572
+ // They are mutually exclusive, so last one wins
573
+ const readOnlyMatch = description.match(/\{@readOnly\s*\}/);
574
+ const writeOnlyMatch = description.match(/\{@writeOnly\s*\}/);
575
+ if (readOnlyMatch || writeOnlyMatch) {
576
+ // Find positions to determine which comes last
577
+ const readOnlyPos = readOnlyMatch ? description.indexOf(readOnlyMatch[0]) : -1;
578
+ const writeOnlyPos = writeOnlyMatch ? description.indexOf(writeOnlyMatch[0]) : -1;
579
+ if (readOnlyPos > writeOnlyPos) {
580
+ paramConstraints.readOnly = true;
581
+ paramConstraints.writeOnly = false; // Explicitly clear the other
582
+ }
583
+ else if (writeOnlyPos > readOnlyPos) {
584
+ paramConstraints.writeOnly = true;
585
+ paramConstraints.readOnly = false; // Explicitly clear the other
586
+ }
587
+ }
588
+ if (Object.keys(paramConstraints).length > 0) {
589
+ constraints.set(paramName, paramConstraints);
590
+ }
591
+ }
592
+ return constraints;
593
+ }
594
+ /**
595
+ * Apply constraints to a schema property based on type
596
+ * Handles: min/max (contextual), pattern, format, default, unique,
597
+ * examples, multipleOf, deprecated, readOnly, writeOnly
598
+ * Works with both simple types and anyOf schemas
599
+ */
600
+ applyConstraints(schema, constraints) {
601
+ // Helper to apply constraints to a single schema based on type
602
+ const applyToSchema = (s) => {
603
+ if (s.enum) {
604
+ // Skip enum types for most constraints (but still apply deprecated, examples, etc.)
605
+ if (constraints.examples !== undefined) {
606
+ s.examples = constraints.examples;
607
+ }
608
+ if (constraints.deprecated !== undefined) {
609
+ s.deprecated = constraints.deprecated === true ? true : constraints.deprecated;
610
+ }
611
+ return;
612
+ }
613
+ // Apply min/max based on type
614
+ if (s.type === 'number') {
615
+ if (constraints.min !== undefined) {
616
+ s.minimum = constraints.min;
617
+ }
618
+ if (constraints.max !== undefined) {
619
+ s.maximum = constraints.max;
620
+ }
621
+ if (constraints.multipleOf !== undefined) {
622
+ s.multipleOf = constraints.multipleOf;
623
+ }
624
+ }
625
+ else if (s.type === 'string') {
626
+ if (constraints.min !== undefined) {
627
+ s.minLength = constraints.min;
628
+ }
629
+ if (constraints.max !== undefined) {
630
+ s.maxLength = constraints.max;
631
+ }
632
+ if (constraints.pattern !== undefined) {
633
+ s.pattern = constraints.pattern;
634
+ }
635
+ if (constraints.format !== undefined) {
636
+ s.format = constraints.format;
637
+ }
638
+ }
639
+ else if (s.type === 'array') {
640
+ if (constraints.min !== undefined) {
641
+ s.minItems = constraints.min;
642
+ }
643
+ if (constraints.max !== undefined) {
644
+ s.maxItems = constraints.max;
645
+ }
646
+ if (constraints.unique === true) {
647
+ s.uniqueItems = true;
648
+ }
649
+ }
650
+ // Apply type-agnostic constraints
651
+ if (constraints.default !== undefined) {
652
+ s.default = constraints.default;
653
+ }
654
+ if (constraints.examples !== undefined) {
655
+ s.examples = constraints.examples;
656
+ }
657
+ if (constraints.deprecated !== undefined) {
658
+ s.deprecated = constraints.deprecated === true ? true : constraints.deprecated;
659
+ }
660
+ // readOnly and writeOnly are mutually exclusive
661
+ // JSDoc takes precedence over TypeScript
662
+ if (constraints.readOnly === true) {
663
+ s.readOnly = true;
664
+ delete s.writeOnly; // Clear writeOnly if readOnly is set
665
+ }
666
+ if (constraints.writeOnly === true) {
667
+ s.writeOnly = true;
668
+ delete s.readOnly; // Clear readOnly if writeOnly is set
669
+ }
670
+ };
671
+ // Apply to anyOf schemas or direct schema
672
+ if (schema.anyOf) {
673
+ schema.anyOf.forEach(applyToSchema);
674
+ // For deprecated/examples that apply to the whole property (not individual types)
675
+ // Apply them at the top level too
676
+ if (constraints.deprecated !== undefined) {
677
+ schema.deprecated = constraints.deprecated === true ? true : constraints.deprecated;
678
+ }
679
+ if (constraints.examples !== undefined) {
680
+ schema.examples = constraints.examples;
681
+ }
682
+ }
683
+ else {
684
+ applyToSchema(schema);
685
+ }
686
+ }
687
+ /**
688
+ * Check if JSDoc contains @Template tag
689
+ */
690
+ hasTemplateTag(jsdocContent) {
691
+ return /@Template/i.test(jsdocContent);
692
+ }
693
+ /**
694
+ * Check if JSDoc contains @Static tag
695
+ */
696
+ hasStaticTag(jsdocContent) {
697
+ return /@Static/i.test(jsdocContent);
698
+ }
699
+ /**
700
+ * Extract URI pattern from @Static tag
701
+ * Example: @Static github://repos/{owner}/{repo}/readme
702
+ */
703
+ extractStaticURI(jsdocContent) {
704
+ const match = jsdocContent.match(/@Static\s+([\w:\/\{\}\-_.]+)/i);
705
+ return match ? match[1].trim() : null;
706
+ }
707
+ /**
708
+ * Extract format hint from @format tag
709
+ * Example: @format table
710
+ */
711
+ extractFormat(jsdocContent) {
712
+ const match = jsdocContent.match(/@format\s+(primitive|table|tree|list|none)/i);
713
+ if (match) {
714
+ return match[1].toLowerCase();
715
+ }
716
+ return undefined;
717
+ }
718
+ /**
719
+ * Extract MIME type from @mimeType tag
720
+ * Example: @mimeType text/markdown
721
+ */
722
+ extractMimeType(jsdocContent) {
723
+ const match = jsdocContent.match(/@mimeType\s+([\w\/\-+.]+)/i);
724
+ return match ? match[1].trim() : undefined;
725
+ }
726
+ }
727
+ //# sourceMappingURL=schema-extractor.js.map