@wp-typia/project-tools 0.22.1 → 0.22.2

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.
@@ -1,6 +1,6 @@
1
1
  import { quoteTsString } from "./cli-add-shared.js";
2
2
  import { buildAiFeatureEndpointManifest } from "./ai-feature-artifacts.js";
3
- import { OPTIONAL_WORDPRESS_AI_CLIENT_COMPATIBILITY, renderScaffoldCompatibilityConfig, resolveScaffoldCompatibilityPolicy, } from "./scaffold-compatibility.js";
3
+ import { OPTIONAL_WORDPRESS_AI_CLIENT_COMPATIBILITY, createScaffoldCompatibilityConfig, renderScaffoldCompatibilityConfig, resolveScaffoldCompatibilityPolicy, } from "./scaffold-compatibility.js";
4
4
  import { toPascalCase, toTitleCase } from "./string-case.js";
5
5
  function indentMultiline(source, prefix) {
6
6
  return source
@@ -79,6 +79,45 @@ export interface ${pascalCase}AiFeatureResponse {
79
79
  \tresult: ${pascalCase}AiFeatureResult;
80
80
  \ttelemetry: ${pascalCase}AiFeatureTelemetry;
81
81
  }
82
+
83
+ export type ${pascalCase}AiFeatureSupportProbeMode = 'request-time';
84
+
85
+ export type ${pascalCase}AiFeatureUnavailableErrorCode =
86
+ \t'ai_client_unavailable';
87
+
88
+ export type ${pascalCase}AiFeatureUnavailableReasonCode =
89
+ \t| 'missing-wordpress-ai-client'
90
+ \t| 'request-time-support-probe';
91
+
92
+ export interface ${pascalCase}AiFeatureSupportReason {
93
+ \tcode: ${pascalCase}AiFeatureUnavailableReasonCode;
94
+ \tlabel: string & tags.MinLength< 1 > & tags.MaxLength< 160 >;
95
+ \tmessage: string & tags.MinLength< 1 > & tags.MaxLength< 4000 >;
96
+ }
97
+
98
+ export interface ${pascalCase}AiFeatureSupportMetadata {
99
+ \tfeatureLabel: string & tags.MinLength< 1 > & tags.MaxLength< 160 >;
100
+ \tfeatureSlug: string & tags.MinLength< 1 > & tags.MaxLength< 160 >;
101
+ \tcompatibility: {
102
+ \t\thardMinimums: {
103
+ \t\t\tphp?: string;
104
+ \t\t\twordpress?: string;
105
+ \t\t};
106
+ \t\tmode: 'baseline' | 'optional' | 'required';
107
+ \t\toptionalFeatureIds: string[];
108
+ \t\toptionalFeatures: string[];
109
+ \t\trequiredFeatureIds: string[];
110
+ \t\trequiredFeatures: string[];
111
+ \t\truntimeGates: string[];
112
+ \t};
113
+ \tsupportProbe: {
114
+ \t\tendpointMethod: 'POST';
115
+ \t\tendpointPath: string & tags.MinLength< 1 > & tags.MaxLength< 200 >;
116
+ \t\tmode: ${pascalCase}AiFeatureSupportProbeMode;
117
+ \t\tunavailableErrorCode: ${pascalCase}AiFeatureUnavailableErrorCode;
118
+ \t};
119
+ \tunavailableReasons: ${pascalCase}AiFeatureSupportReason[];
120
+ }
82
121
  `;
83
122
  }
84
123
  /**
@@ -120,6 +159,8 @@ export const apiValidators = {
120
159
  */
121
160
  export function buildAiFeatureApiSource(aiFeatureSlug) {
122
161
  const pascalCase = toPascalCase(aiFeatureSlug);
162
+ const compatibility = createScaffoldCompatibilityConfig(resolveScaffoldCompatibilityPolicy(OPTIONAL_WORDPRESS_AI_CLIENT_COMPATIBILITY));
163
+ const title = toTitleCase(aiFeatureSlug);
123
164
  return `import {
124
165
  \tcallEndpoint,
125
166
  \tresolveRestRouteUrl,
@@ -127,6 +168,7 @@ export function buildAiFeatureApiSource(aiFeatureSlug) {
127
168
 
128
169
  import type {
129
170
  \t${pascalCase}AiFeatureRequest,
171
+ \t${pascalCase}AiFeatureSupportMetadata,
130
172
  } from './api-types';
131
173
  import {
132
174
  \trun${pascalCase}AiFeatureEndpoint,
@@ -153,6 +195,14 @@ function resolveRestNonce( fallback?: string ): string | undefined {
153
195
  \t\t: undefined;
154
196
  }
155
197
 
198
+ function isPlainObject( value: unknown ): value is Record< string, unknown > {
199
+ \treturn (
200
+ \t\t!! value &&
201
+ \t\ttypeof value === 'object' &&
202
+ \t\t! Array.isArray( value )
203
+ \t);
204
+ }
205
+
156
206
  export const aiFeatureRunEndpoint = {
157
207
  \t...run${pascalCase}AiFeatureEndpoint,
158
208
  \tbuildRequestOptions: () => {
@@ -168,6 +218,63 @@ export const aiFeatureRunEndpoint = {
168
218
  \t},
169
219
  };
170
220
 
221
+ export const aiFeatureSupportMetadata = {
222
+ \tcompatibility: ${JSON.stringify(compatibility, null, "\t")},
223
+ \tfeatureLabel: ${quoteTsString(title)},
224
+ \tfeatureSlug: ${quoteTsString(aiFeatureSlug)},
225
+ \tsupportProbe: {
226
+ \t\tendpointMethod: 'POST',
227
+ \t\tendpointPath: aiFeatureRunEndpoint.path,
228
+ \t\tmode: 'request-time',
229
+ \t\tunavailableErrorCode: 'ai_client_unavailable',
230
+ \t},
231
+ \tunavailableReasons: [
232
+ \t\t{
233
+ \t\t\tcode: 'missing-wordpress-ai-client',
234
+ \t\t\tlabel: 'WordPress AI Client unavailable',
235
+ \t\t\tmessage:
236
+ \t\t\t\t'This AI feature stays disabled until the WordPress AI Client is available on the site.',
237
+ \t\t},
238
+ \t\t{
239
+ \t\t\tcode: 'request-time-support-probe',
240
+ \t\t\tlabel: 'Support is checked at request time',
241
+ \t\t\tmessage:
242
+ \t\t\t\t'Support is verified when the feature runs, so editor and admin UIs should degrade gracefully when the site rejects the request.',
243
+ \t\t},
244
+ \t],
245
+ } satisfies ${pascalCase}AiFeatureSupportMetadata;
246
+
247
+ export function getAiFeatureSupportHintLines() {
248
+ \treturn aiFeatureSupportMetadata.unavailableReasons.map(
249
+ \t\t( reason ) => reason.message
250
+ \t);
251
+ }
252
+
253
+ export function isAiFeatureSupportUnavailableError( error: unknown ) {
254
+ \tif ( ! isPlainObject( error ) ) {
255
+ \t\treturn false;
256
+ \t}
257
+
258
+ \tconst data = isPlainObject( error.data ) ? error.data : undefined;
259
+ \treturn (
260
+ \t\terror.code === aiFeatureSupportMetadata.supportProbe.unavailableErrorCode ||
261
+ \t\tdata?.status === 501
262
+ \t);
263
+ }
264
+
265
+ export function resolveAiFeatureUnavailableMessage( error: unknown ) {
266
+ \tif (
267
+ \t\tisPlainObject( error ) &&
268
+ \t\ttypeof error.message === 'string' &&
269
+ \t\terror.message.length > 0
270
+ \t) {
271
+ \t\treturn error.message;
272
+ \t}
273
+
274
+ \treturn aiFeatureSupportMetadata.unavailableReasons[ 0 ]?.message ??
275
+ \t\t'This AI feature is currently unavailable.';
276
+ }
277
+
171
278
  export function runAiFeature( request: ${pascalCase}AiFeatureRequest ) {
172
279
  \treturn callEndpoint( aiFeatureRunEndpoint, request );
173
280
  }
@@ -189,6 +296,10 @@ import type {
189
296
  } from './api-types';
190
297
  import {
191
298
  \taiFeatureRunEndpoint,
299
+ \taiFeatureSupportMetadata,
300
+ \tgetAiFeatureSupportHintLines,
301
+ \tisAiFeatureSupportUnavailableError,
302
+ \tresolveAiFeatureUnavailableMessage,
192
303
  } from './api';
193
304
 
194
305
  export type UseRun${pascalCase}AiFeatureMutationOptions =
@@ -203,6 +314,13 @@ export function useRun${pascalCase}AiFeatureMutation(
203
314
  ) {
204
315
  \treturn useEndpointMutation( aiFeatureRunEndpoint, options );
205
316
  }
317
+
318
+ export {
319
+ \taiFeatureSupportMetadata,
320
+ \tgetAiFeatureSupportHintLines,
321
+ \tisAiFeatureSupportUnavailableError,
322
+ \tresolveAiFeatureUnavailableMessage,
323
+ };
206
324
  `;
207
325
  }
208
326
  /**
@@ -18,18 +18,40 @@ function buildAiFeaturePhpSource(aiFeatureSlug, namespace, phpPrefix, textDomain
18
18
  const normalizeSchemaFunctionName = `${phpPrefix}_${aiFeaturePhpId}_sanitize_ai_feature_schema`;
19
19
  const validatePayloadFunctionName = `${phpPrefix}_${aiFeaturePhpId}_validate_ai_feature_payload`;
20
20
  const canManageFunctionName = `${phpPrefix}_${aiFeaturePhpId}_can_manage_ai_feature`;
21
+ const normalizePromptPayloadFunctionName = `${phpPrefix}_${aiFeaturePhpId}_normalize_ai_feature_prompt_payload`;
21
22
  const buildPromptFunctionName = `${phpPrefix}_${aiFeaturePhpId}_build_ai_feature_prompt`;
23
+ const resolvePromptOptionsFunctionName = `${phpPrefix}_${aiFeaturePhpId}_resolve_ai_feature_prompt_options`;
22
24
  const normalizeProviderTypeFunctionName = `${phpPrefix}_${aiFeaturePhpId}_normalize_provider_type`;
23
25
  const buildTelemetryFunctionName = `${phpPrefix}_${aiFeaturePhpId}_build_ai_feature_telemetry`;
26
+ const resolveUnavailableMessageFunctionName = `${phpPrefix}_${aiFeaturePhpId}_resolve_ai_feature_unavailable_message`;
24
27
  const isSupportedFunctionName = `${phpPrefix}_${aiFeaturePhpId}_is_ai_feature_supported`;
25
28
  const adminNoticeFunctionName = `${phpPrefix}_${aiFeaturePhpId}_ai_feature_admin_notice`;
26
29
  const handlerFunctionName = `${phpPrefix}_${aiFeaturePhpId}_handle_run_ai_feature`;
27
30
  const registerRoutesFunctionName = `${phpPrefix}_${aiFeaturePhpId}_register_ai_feature_routes`;
31
+ const permissionFilterHook = `${phpPrefix}_${aiFeaturePhpId}_ai_feature_permission`;
32
+ const promptPayloadFilterHook = `${phpPrefix}_${aiFeaturePhpId}_ai_feature_prompt_payload`;
33
+ const promptFilterHook = `${phpPrefix}_${aiFeaturePhpId}_ai_feature_prompt`;
34
+ const promptOptionsFilterHook = `${phpPrefix}_${aiFeaturePhpId}_ai_feature_prompt_options`;
35
+ const adminNoticeMessageFilterHook = `${phpPrefix}_${aiFeaturePhpId}_ai_feature_admin_notice_message`;
36
+ const unavailableMessageFilterHook = `${phpPrefix}_${aiFeaturePhpId}_ai_feature_unavailable_message`;
37
+ const telemetryFilterHook = `${phpPrefix}_${aiFeaturePhpId}_ai_feature_telemetry`;
28
38
  return `<?php
29
39
  if ( ! defined( 'ABSPATH' ) ) {
30
40
  \treturn;
31
41
  }
32
42
 
43
+ /*
44
+ * Customization hooks for the ${aiFeatureTitle} AI feature:
45
+ *
46
+ * - ${quotePhpString(permissionFilterHook)} filters the default current_user_can( 'edit_posts' ) capability check.
47
+ * - ${quotePhpString(promptPayloadFilterHook)} filters the validated request payload array before prompt serialization.
48
+ * - ${quotePhpString(promptFilterHook)} filters the final prompt string after payload normalization.
49
+ * - ${quotePhpString(promptOptionsFilterHook)} filters prompt options with \`temperature\` and \`modelPreference\` keys.
50
+ * - ${quotePhpString(adminNoticeMessageFilterHook)} filters the wp-admin notice shown when AI support is unavailable.
51
+ * - ${quotePhpString(unavailableMessageFilterHook)} filters REST-facing unavailable messages by reason code.
52
+ * - ${quotePhpString(telemetryFilterHook)} filters the response telemetry array before schema validation. Return a schema-compatible array.
53
+ */
54
+
33
55
  if ( ! function_exists( '${loadSchemaFunctionName}' ) ) {
34
56
  \tfunction ${loadSchemaFunctionName}( $schema_name ) {
35
57
  \t\t$project_root = dirname( __DIR__, 2 );
@@ -96,17 +118,111 @@ if ( ! function_exists( '${validatePayloadFunctionName}' ) ) {
96
118
  }
97
119
 
98
120
  if ( ! function_exists( '${canManageFunctionName}' ) ) {
99
- \tfunction ${canManageFunctionName}() {
100
- \t\treturn current_user_can( 'edit_posts' );
121
+ \tfunction ${canManageFunctionName}( WP_REST_Request $request = null ) {
122
+ \t\t$permission = apply_filters(
123
+ \t\t\t${quotePhpString(permissionFilterHook)},
124
+ \t\t\tcurrent_user_can( 'edit_posts' ),
125
+ \t\t\t$request
126
+ \t\t);
127
+ \t\tif ( is_wp_error( $permission ) ) {
128
+ \t\t\treturn $permission;
129
+ \t\t}
130
+ \t\treturn (bool) $permission;
131
+ \t}
132
+ }
133
+
134
+ if ( ! function_exists( '${normalizePromptPayloadFunctionName}' ) ) {
135
+ \tfunction ${normalizePromptPayloadFunctionName}( array $payload ) {
136
+ \t\t$normalized_payload = apply_filters(
137
+ \t\t\t${quotePhpString(promptPayloadFilterHook)},
138
+ \t\t\t$payload
139
+ \t\t);
140
+ \t\treturn is_array( $normalized_payload ) ? $normalized_payload : $payload;
101
141
  \t}
102
142
  }
103
143
 
104
144
  if ( ! function_exists( '${buildPromptFunctionName}' ) ) {
105
145
  \tfunction ${buildPromptFunctionName}( array $payload ) {
106
- \t\treturn sprintf(
146
+ \t\t$normalized_payload = ${normalizePromptPayloadFunctionName}( $payload );
147
+ \t\t$prompt = sprintf(
107
148
  \t\t\t'You are helping with the %1$s AI workflow. Read the JSON request payload and return JSON that matches the provided schema. Request payload: %2$s',
108
149
  \t\t\t${quotePhpString(aiFeatureTitle)},
109
- \t\t\twp_json_encode( $payload )
150
+ \t\t\twp_json_encode( $normalized_payload )
151
+ \t\t);
152
+ \t\t$filtered_prompt = apply_filters(
153
+ \t\t\t${quotePhpString(promptFilterHook)},
154
+ \t\t\t$prompt,
155
+ \t\t\t$normalized_payload,
156
+ \t\t\t$payload
157
+ \t\t);
158
+ \t\treturn is_string( $filtered_prompt ) && '' !== $filtered_prompt ? $filtered_prompt : $prompt;
159
+ \t}
160
+ }
161
+
162
+ if ( ! function_exists( '${resolvePromptOptionsFunctionName}' ) ) {
163
+ \tfunction ${resolvePromptOptionsFunctionName}( array $payload = array() ) {
164
+ \t\t$options = apply_filters(
165
+ \t\t\t${quotePhpString(promptOptionsFilterHook)},
166
+ \t\t\tarray(
167
+ \t\t\t\t'modelPreference' => array(),
168
+ \t\t\t\t'temperature' => 0.2,
169
+ \t\t\t),
170
+ \t\t\t$payload
171
+ \t\t);
172
+ \t\tif ( ! is_array( $options ) ) {
173
+ \t\t\t$options = array();
174
+ \t\t}
175
+
176
+ \t\t$temperature = 0.2;
177
+ \t\tif ( array_key_exists( 'temperature', $options ) ) {
178
+ \t\t\tif ( null === $options['temperature'] ) {
179
+ \t\t\t\t$temperature = null;
180
+ \t\t\t} elseif ( is_numeric( $options['temperature'] ) ) {
181
+ \t\t\t\t$temperature = (float) $options['temperature'];
182
+ \t\t\t}
183
+ \t\t}
184
+
185
+ \t\t$model_preferences = array();
186
+ \t\tif ( isset( $options['modelPreference'] ) ) {
187
+ \t\t\t$raw_model_preferences = $options['modelPreference'];
188
+ \t\t\tif ( is_string( $raw_model_preferences ) && '' !== $raw_model_preferences ) {
189
+ \t\t\t\t$model_preferences = array( $raw_model_preferences );
190
+ \t\t\t} elseif ( is_array( $raw_model_preferences ) ) {
191
+ \t\t\t\t$model_preferences = array_values(
192
+ \t\t\t\t\tarray_filter(
193
+ \t\t\t\t\t\tarray_map(
194
+ \t\t\t\t\t\t\tstatic function ( $candidate ) {
195
+ \t\t\t\t\t\t\t\tif ( is_string( $candidate ) && '' !== $candidate ) {
196
+ \t\t\t\t\t\t\t\t\treturn $candidate;
197
+ \t\t\t\t\t\t\t\t}
198
+ \t\t\t\t\t\t\t\tif ( ! is_array( $candidate ) ) {
199
+ \t\t\t\t\t\t\t\t\treturn null;
200
+ \t\t\t\t\t\t\t\t}
201
+
202
+ \t\t\t\t\t\t\t\t$normalized = array_values(
203
+ \t\t\t\t\t\t\t\t\tarray_filter(
204
+ \t\t\t\t\t\t\t\t\t\t$candidate,
205
+ \t\t\t\t\t\t\t\t\t\tstatic function ( $value ) {
206
+ \t\t\t\t\t\t\t\t\t\t\treturn is_string( $value ) && '' !== $value;
207
+ \t\t\t\t\t\t\t\t\t\t}
208
+ \t\t\t\t\t\t\t\t\t)
209
+ \t\t\t\t\t\t\t\t);
210
+
211
+ \t\t\t\t\t\t\t\treturn count( $normalized ) > 0 ? $normalized : null;
212
+ \t\t\t\t\t\t\t},
213
+ \t\t\t\t\t\t\t$raw_model_preferences
214
+ \t\t\t\t\t\t),
215
+ \t\t\t\t\t\tstatic function ( $candidate ) {
216
+ \t\t\t\t\t\t\treturn null !== $candidate;
217
+ \t\t\t\t\t\t}
218
+ \t\t\t\t\t)
219
+ \t\t\t\t);
220
+ \t\t\t}
221
+ \t\t}
222
+
223
+ \t\treturn array(
224
+ \t\t\t'modelPreference' => $model_preferences,
225
+ \t\t\t'temperature' => $temperature,
110
226
  \t\t);
111
227
  \t}
112
228
  }
@@ -122,7 +238,7 @@ if ( ! function_exists( '${normalizeProviderTypeFunctionName}' ) ) {
122
238
  }
123
239
 
124
240
  if ( ! function_exists( '${buildTelemetryFunctionName}' ) ) {
125
- \tfunction ${buildTelemetryFunctionName}( $result ) {
241
+ \tfunction ${buildTelemetryFunctionName}( $result, array $payload = array(), array $normalized_result = array() ) {
126
242
  \t\tif (
127
243
  \t\t\t! is_object( $result ) ||
128
244
  \t\t\t! method_exists( $result, 'getId' ) ||
@@ -182,47 +298,95 @@ if ( ! function_exists( '${buildTelemetryFunctionName}' ) ) {
182
298
  \t\t\t}
183
299
  \t\t}
184
300
 
185
- \t\treturn $telemetry;
301
+ \t\t$filtered_telemetry = apply_filters(
302
+ \t\t\t${quotePhpString(telemetryFilterHook)},
303
+ \t\t\t$telemetry,
304
+ \t\t\t$result,
305
+ \t\t\t$payload,
306
+ \t\t\t$normalized_result
307
+ \t\t);
308
+ \t\treturn is_array( $filtered_telemetry ) ? $filtered_telemetry : $telemetry;
309
+ \t}
310
+ }
311
+
312
+ if ( ! function_exists( '${resolveUnavailableMessageFunctionName}' ) ) {
313
+ \tfunction ${resolveUnavailableMessageFunctionName}( $message, $reason, array $context = array() ) {
314
+ \t\t$filtered_message = apply_filters(
315
+ \t\t\t${quotePhpString(unavailableMessageFilterHook)},
316
+ \t\t\t$message,
317
+ \t\t\t$reason,
318
+ \t\t\t$context
319
+ \t\t);
320
+ \t\treturn is_string( $filtered_message ) && '' !== $filtered_message ? $filtered_message : $message;
186
321
  \t}
187
322
  }
188
323
 
189
324
  if ( ! function_exists( '${isSupportedFunctionName}' ) ) {
190
- \tfunction ${isSupportedFunctionName}() {
325
+ \tfunction ${isSupportedFunctionName}( array $payload = array(), $cache_result = true ) {
191
326
  \t\tstatic $is_supported = null;
192
- \t\tif ( null !== $is_supported ) {
327
+ \t\t$use_cache = $cache_result && count( $payload ) === 0;
328
+ \t\tif ( $use_cache && null !== $is_supported ) {
193
329
  \t\t\treturn $is_supported;
194
330
  \t\t}
195
331
 
196
332
  \t\tif ( ! function_exists( 'wp_ai_client_prompt' ) ) {
197
- \t\t\t$is_supported = false;
198
- \t\t\treturn $is_supported;
333
+ \t\t\tif ( $use_cache ) {
334
+ \t\t\t\t$is_supported = false;
335
+ \t\t\t}
336
+ \t\t\treturn false;
199
337
  \t\t}
200
338
 
201
339
  \t\t$schema = ${loadAiSchemaFunctionName}();
202
340
  \t\tif ( ! is_array( $schema ) ) {
203
- \t\t\t$is_supported = false;
204
- \t\t\treturn $is_supported;
341
+ \t\t\tif ( $use_cache ) {
342
+ \t\t\t\t$is_supported = false;
343
+ \t\t\t}
344
+ \t\t\treturn false;
205
345
  \t\t}
206
346
 
207
347
  \t\t$prompt = wp_ai_client_prompt( 'AI feature support probe.' );
208
348
  \t\tif ( ! is_object( $prompt ) || ! method_exists( $prompt, 'as_json_response' ) ) {
209
- \t\t\t$is_supported = false;
210
- \t\t\treturn $is_supported;
349
+ \t\t\tif ( $use_cache ) {
350
+ \t\t\t\t$is_supported = false;
351
+ \t\t\t}
352
+ \t\t\treturn false;
353
+ \t\t}
354
+ \t\t$prompt_options = ${resolvePromptOptionsFunctionName}( $payload );
355
+ \t\tif (
356
+ \t\t\tarray_key_exists( 'temperature', $prompt_options ) &&
357
+ \t\t\tnull !== $prompt_options['temperature'] &&
358
+ \t\t\tmethod_exists( $prompt, 'using_temperature' )
359
+ \t\t) {
360
+ \t\t\t$prompt = $prompt->using_temperature( $prompt_options['temperature'] );
361
+ \t\t}
362
+ \t\tif (
363
+ \t\t\t! empty( $prompt_options['modelPreference'] ) &&
364
+ \t\t\tmethod_exists( $prompt, 'using_model_preference' )
365
+ \t\t) {
366
+ \t\t\t$prompt = $prompt->using_model_preference( ...$prompt_options['modelPreference'] );
211
367
  \t\t}
212
368
 
213
369
  \t\t$structured_prompt = $prompt->as_json_response( $schema );
214
370
  \t\tif ( ! is_object( $structured_prompt ) ) {
215
- \t\t\t$is_supported = false;
216
- \t\t\treturn $is_supported;
371
+ \t\t\tif ( $use_cache ) {
372
+ \t\t\t\t$is_supported = false;
373
+ \t\t\t}
374
+ \t\t\treturn false;
217
375
  \t\t}
218
376
 
219
377
  \t\tif ( method_exists( $structured_prompt, 'is_supported_for_text_generation' ) ) {
220
- \t\t\t$is_supported = (bool) $structured_prompt->is_supported_for_text_generation();
221
- \t\t\treturn $is_supported;
378
+ \t\t\t$supported = (bool) $structured_prompt->is_supported_for_text_generation();
379
+ \t\t\tif ( $use_cache ) {
380
+ \t\t\t\t$is_supported = $supported;
381
+ \t\t\t}
382
+ \t\t\treturn $supported;
222
383
  \t\t}
223
384
 
224
- \t\t$is_supported = method_exists( $structured_prompt, 'generate_text_result' );
225
- \t\treturn $is_supported;
385
+ \t\t$supported = method_exists( $structured_prompt, 'generate_text_result' );
386
+ \t\tif ( $use_cache ) {
387
+ \t\t\t$is_supported = $supported;
388
+ \t\t}
389
+ \t\treturn $supported;
226
390
  \t}
227
391
  }
228
392
 
@@ -237,6 +401,18 @@ if ( ! function_exists( '${adminNoticeFunctionName}' ) ) {
237
401
  \t\t\t__( 'The %s AI feature is optional and remains disabled until the WordPress AI Client is available with structured text generation support for the generated schema.', ${quotePhpString(textDomain)} ),
238
402
  \t\t\t${quotePhpString(aiFeatureTitle)}
239
403
  \t\t);
404
+ \t\t$filtered_message = apply_filters(
405
+ \t\t\t${quotePhpString(adminNoticeMessageFilterHook)},
406
+ \t\t\t$message,
407
+ \t\t\tarray(
408
+ \t\t\t\t'featureSlug' => ${quotePhpString(aiFeatureSlug)},
409
+ \t\t\t\t'featureTitle' => ${quotePhpString(aiFeatureTitle)},
410
+ \t\t\t\t'namespace' => ${quotePhpString(namespace)},
411
+ \t\t\t)
412
+ \t\t);
413
+ \t\tif ( is_string( $filtered_message ) && '' !== $filtered_message ) {
414
+ \t\t\t$message = $filtered_message;
415
+ \t\t}
240
416
  \t\tprintf( '<div class="notice notice-warning"><p>%s</p></div>', esc_html( $message ) );
241
417
  \t}
242
418
  }
@@ -248,10 +424,16 @@ if ( ! function_exists( '${handlerFunctionName}' ) ) {
248
424
  \t\t\treturn $payload;
249
425
  \t\t}
250
426
 
251
- \t\tif ( ! ${isSupportedFunctionName}() ) {
427
+ \t\tif ( ! ${isSupportedFunctionName}( $payload, false ) ) {
252
428
  \t\t\treturn new WP_Error(
253
429
  \t\t\t\t'ai_client_unavailable',
254
- \t\t\t\t'The WordPress AI Client is unavailable or does not support this feature endpoint.',
430
+ \t\t\t\t${resolveUnavailableMessageFunctionName}(
431
+ \t\t\t\t\t'The WordPress AI Client is unavailable or does not support this feature endpoint.',
432
+ \t\t\t\t\t'support_probe_failed',
433
+ \t\t\t\t\tarray(
434
+ \t\t\t\t\t\t'featureSlug' => ${quotePhpString(aiFeatureSlug)},
435
+ \t\t\t\t\t)
436
+ \t\t\t\t),
255
437
  \t\t\t\tarray( 'status' => 501 )
256
438
  \t\t\t);
257
439
  \t\t}
@@ -265,22 +447,45 @@ if ( ! function_exists( '${handlerFunctionName}' ) ) {
265
447
  \t\t\t);
266
448
  \t\t}
267
449
 
450
+ \t\t$prompt_options = ${resolvePromptOptionsFunctionName}( $payload );
268
451
  \t\t$prompt = wp_ai_client_prompt( ${buildPromptFunctionName}( $payload ) );
269
452
  \t\tif ( ! is_object( $prompt ) ) {
270
453
  \t\t\treturn new WP_Error(
271
454
  \t\t\t\t'ai_client_unavailable',
272
- \t\t\t\t'The WordPress AI Client prompt builder is unavailable on this site.',
455
+ \t\t\t\t${resolveUnavailableMessageFunctionName}(
456
+ \t\t\t\t\t'The WordPress AI Client prompt builder is unavailable on this site.',
457
+ \t\t\t\t\t'prompt_builder_missing',
458
+ \t\t\t\t\tarray(
459
+ \t\t\t\t\t\t'featureSlug' => ${quotePhpString(aiFeatureSlug)},
460
+ \t\t\t\t\t)
461
+ \t\t\t\t),
273
462
  \t\t\t\tarray( 'status' => 501 )
274
463
  \t\t\t);
275
464
  \t\t}
276
465
 
277
- \t\tif ( method_exists( $prompt, 'using_temperature' ) ) {
278
- \t\t\t$prompt = $prompt->using_temperature( 0.2 );
466
+ \t\tif (
467
+ \t\t\tarray_key_exists( 'temperature', $prompt_options ) &&
468
+ \t\t\tnull !== $prompt_options['temperature'] &&
469
+ \t\t\tmethod_exists( $prompt, 'using_temperature' )
470
+ \t\t) {
471
+ \t\t\t$prompt = $prompt->using_temperature( $prompt_options['temperature'] );
472
+ \t\t}
473
+ \t\tif (
474
+ \t\t\t! empty( $prompt_options['modelPreference'] ) &&
475
+ \t\t\tmethod_exists( $prompt, 'using_model_preference' )
476
+ \t\t) {
477
+ \t\t\t$prompt = $prompt->using_model_preference( ...$prompt_options['modelPreference'] );
279
478
  \t\t}
280
479
  \t\tif ( ! method_exists( $prompt, 'as_json_response' ) ) {
281
480
  \t\t\treturn new WP_Error(
282
481
  \t\t\t\t'ai_client_unavailable',
283
- \t\t\t\t'The current WordPress AI Client does not expose as_json_response().',
482
+ \t\t\t\t${resolveUnavailableMessageFunctionName}(
483
+ \t\t\t\t\t'The current WordPress AI Client does not expose as_json_response().',
484
+ \t\t\t\t\t'as_json_response_missing',
485
+ \t\t\t\t\tarray(
486
+ \t\t\t\t\t\t'featureSlug' => ${quotePhpString(aiFeatureSlug)},
487
+ \t\t\t\t\t)
488
+ \t\t\t\t),
284
489
  \t\t\t\tarray( 'status' => 501 )
285
490
  \t\t\t);
286
491
  \t\t}
@@ -289,7 +494,13 @@ if ( ! function_exists( '${handlerFunctionName}' ) ) {
289
494
  \t\tif ( ! is_object( $structured_prompt ) ) {
290
495
  \t\t\treturn new WP_Error(
291
496
  \t\t\t\t'ai_client_unavailable',
292
- \t\t\t\t'The current WordPress AI Client could not prepare a structured-output prompt.',
497
+ \t\t\t\t${resolveUnavailableMessageFunctionName}(
498
+ \t\t\t\t\t'The current WordPress AI Client could not prepare a structured-output prompt.',
499
+ \t\t\t\t\t'structured_prompt_missing',
500
+ \t\t\t\t\tarray(
501
+ \t\t\t\t\t\t'featureSlug' => ${quotePhpString(aiFeatureSlug)},
502
+ \t\t\t\t\t)
503
+ \t\t\t\t),
293
504
  \t\t\t\tarray( 'status' => 501 )
294
505
  \t\t\t);
295
506
  \t\t}
@@ -300,14 +511,26 @@ if ( ! function_exists( '${handlerFunctionName}' ) ) {
300
511
  \t\t) {
301
512
  \t\t\treturn new WP_Error(
302
513
  \t\t\t\t'ai_client_unavailable',
303
- \t\t\t\t'The current WordPress AI Client provider or model does not support this structured-output feature.',
514
+ \t\t\t\t${resolveUnavailableMessageFunctionName}(
515
+ \t\t\t\t\t'The current WordPress AI Client provider or model does not support this structured-output feature.',
516
+ \t\t\t\t\t'text_generation_unsupported',
517
+ \t\t\t\t\tarray(
518
+ \t\t\t\t\t\t'featureSlug' => ${quotePhpString(aiFeatureSlug)},
519
+ \t\t\t\t\t)
520
+ \t\t\t\t),
304
521
  \t\t\t\tarray( 'status' => 501 )
305
522
  \t\t\t);
306
523
  \t\t}
307
524
  \t\tif ( ! method_exists( $structured_prompt, 'generate_text_result' ) ) {
308
525
  \t\t\treturn new WP_Error(
309
526
  \t\t\t\t'ai_client_unavailable',
310
- \t\t\t\t'The current WordPress AI Client does not expose generate_text_result() after as_json_response().',
527
+ \t\t\t\t${resolveUnavailableMessageFunctionName}(
528
+ \t\t\t\t\t'The current WordPress AI Client does not expose generate_text_result() after as_json_response().',
529
+ \t\t\t\t\t'generate_text_result_missing',
530
+ \t\t\t\t\tarray(
531
+ \t\t\t\t\t\t'featureSlug' => ${quotePhpString(aiFeatureSlug)},
532
+ \t\t\t\t\t)
533
+ \t\t\t\t),
311
534
  \t\t\t\tarray( 'status' => 501 )
312
535
  \t\t\t);
313
536
  \t\t}
@@ -342,7 +565,7 @@ if ( ! function_exists( '${handlerFunctionName}' ) ) {
342
565
  \t\t\t);
343
566
  \t\t}
344
567
 
345
- \t\t$telemetry = ${buildTelemetryFunctionName}( $result );
568
+ \t\t$telemetry = ${buildTelemetryFunctionName}( $result, $payload, $normalized_result );
346
569
  \t\tif ( is_wp_error( $telemetry ) ) {
347
570
  \t\t\treturn $telemetry;
348
571
  \t\t}
@@ -26,7 +26,9 @@ export interface ScaffoldCompatibilityPolicy {
26
26
  export interface ScaffoldCompatibilityConfig {
27
27
  hardMinimums: AiFeatureCompatibilityFloor;
28
28
  mode: 'baseline' | 'optional' | 'required';
29
+ optionalFeatureIds: string[];
29
30
  optionalFeatures: string[];
31
+ requiredFeatureIds: string[];
30
32
  requiredFeatures: string[];
31
33
  runtimeGates: string[];
32
34
  }
@@ -88,7 +88,9 @@ export function createScaffoldCompatibilityConfig(policy) {
88
88
  return {
89
89
  hardMinimums: capabilityPlan.hardMinimums,
90
90
  mode: getPolicyMode(capabilityPlan),
91
+ optionalFeatureIds: capabilityPlan.optionalFeatures.map((feature) => feature.id),
91
92
  optionalFeatures: capabilityPlan.optionalFeatures.map((feature) => feature.label),
93
+ requiredFeatureIds: capabilityPlan.requiredFeatures.map((feature) => feature.id),
92
94
  requiredFeatures: capabilityPlan.requiredFeatures.map((feature) => feature.label),
93
95
  runtimeGates: [
94
96
  ...capabilityPlan.requiredFeatures.flatMap(formatRuntimeGate),
@@ -1,6 +1,6 @@
1
1
  import type { ArtifactSyncExecutionOptions, EndpointManifestDefinition, EndpointManifestEndpointDefinition } from '@wp-typia/block-runtime/metadata-core';
2
2
  import type { ILlmFunction, ILlmSchema, ILlmStructuredOutput } from 'typia';
3
- import { type EndpointAuthIntent, type EndpointWordPressAuthDefinition } from './schema-core.js';
3
+ import { type EndpointAuthIntent, type EndpointWordPressAuthDefinition, type OpenApiDocument } from './schema-core.js';
4
4
  /**
5
5
  * Method-level descriptor projected from an endpoint manifest for a generated
6
6
  * `typia.llm` controller interface.
@@ -140,6 +140,16 @@ export interface ProjectedTypiaLlmStructuredOutputArtifact {
140
140
  /** Structured output parameters generated by `typia.llm`. */
141
141
  parameters: ILlmStructuredOutput['parameters'];
142
142
  }
143
+ /**
144
+ * Configures optional OpenAPI-backed constraint restoration for projected
145
+ * `typia.llm` function artifacts.
146
+ */
147
+ export interface ProjectTypiaLlmOpenApiProjectionOptions {
148
+ /** Canonical endpoint-aware OpenAPI document for the projected contracts. */
149
+ openApiDocument: OpenApiDocument;
150
+ /** Optional override for resolving one function schema to an operation id. */
151
+ resolveOperationId?: (functionSchema: TypiaLlmFunctionLike, functionArtifact: ProjectedTypiaLlmFunctionArtifact) => string;
152
+ }
143
153
  /**
144
154
  * Configures projection of a compiled `typia.llm.application(...)` result into
145
155
  * the JSON-friendly adapter artifact.
@@ -149,9 +159,23 @@ export interface ProjectTypiaLlmApplicationArtifactOptions {
149
159
  application: TypiaLlmApplicationLike;
150
160
  /** Source metadata for the generated JSON artifact. */
151
161
  generatedFrom: ProjectedTypiaLlmApplicationArtifact['generatedFrom'];
162
+ /** Optional OpenAPI projection that restores canonical schema constraints. */
163
+ openApiProjection?: ProjectTypiaLlmOpenApiProjectionOptions;
152
164
  /** Optional hook for adapter-specific function schema normalization. */
153
165
  transformFunction?: (functionArtifact: ProjectedTypiaLlmFunctionArtifact, functionSchema: TypiaLlmFunctionLike) => ProjectedTypiaLlmFunctionArtifact;
154
166
  }
167
+ /**
168
+ * Configures direct OpenAPI-backed constraint restoration for one projected
169
+ * function artifact.
170
+ */
171
+ export interface ApplyOpenApiConstraintsToTypiaLlmFunctionArtifactOptions {
172
+ /** Projected function artifact to enrich with canonical constraints. */
173
+ functionArtifact: ProjectedTypiaLlmFunctionArtifact;
174
+ /** Canonical endpoint-aware OpenAPI document. */
175
+ openApiDocument: OpenApiDocument;
176
+ /** Operation id that owns the request/response contract for this function. */
177
+ operationId: string;
178
+ }
155
179
  /**
156
180
  * Configures projection of a compiled `typia.llm.structuredOutput(...)` result
157
181
  * into the JSON-friendly adapter artifact.
@@ -197,6 +221,17 @@ export declare function syncTypiaLlmAdapterModule({ check, generatedSourceFile,
197
221
  * @returns JSON-friendly function artifact.
198
222
  */
199
223
  export declare function projectTypiaLlmApplicationFunction(functionSchema: TypiaLlmFunctionLike): ProjectedTypiaLlmFunctionArtifact;
224
+ /**
225
+ * Restores canonical request and response constraints from an endpoint-aware
226
+ * OpenAPI document onto one projected `typia.llm` function artifact.
227
+ *
228
+ * Query-only inputs merge into the root parameter object. Mixed body/query
229
+ * inputs merge into the generated `body` and `query` properties.
230
+ *
231
+ * @param options Projected artifact plus the canonical OpenAPI document.
232
+ * @returns A cloned artifact enriched with OpenAPI-backed constraints when available.
233
+ */
234
+ export declare function applyOpenApiConstraintsToTypiaLlmFunctionArtifact({ functionArtifact, openApiDocument, operationId, }: ApplyOpenApiConstraintsToTypiaLlmFunctionArtifactOptions): ProjectedTypiaLlmFunctionArtifact;
200
235
  /**
201
236
  * Projects a compiled `typia.llm.application(...)` result into a JSON-friendly
202
237
  * downstream adapter artifact.
@@ -204,7 +239,7 @@ export declare function projectTypiaLlmApplicationFunction(functionSchema: Typia
204
239
  * @param options Compiled application value and source metadata.
205
240
  * @returns JSON-friendly application artifact.
206
241
  */
207
- export declare function projectTypiaLlmApplicationArtifact({ application, generatedFrom, transformFunction, }: ProjectTypiaLlmApplicationArtifactOptions): ProjectedTypiaLlmApplicationArtifact;
242
+ export declare function projectTypiaLlmApplicationArtifact({ application, generatedFrom, openApiProjection, transformFunction, }: ProjectTypiaLlmApplicationArtifactOptions): ProjectedTypiaLlmApplicationArtifact;
208
243
  /**
209
244
  * Projects a compiled `typia.llm.structuredOutput(...)` result into a
210
245
  * JSON-friendly downstream adapter artifact.
@@ -81,9 +81,195 @@ const TYPESCRIPT_RESERVED_WORDS = new Set([
81
81
  'with',
82
82
  'yield',
83
83
  ]);
84
+ const JSON_SCHEMA_CONSTRAINT_KEYS = [
85
+ 'additionalProperties',
86
+ 'const',
87
+ 'default',
88
+ 'enum',
89
+ 'exclusiveMaximum',
90
+ 'exclusiveMinimum',
91
+ 'format',
92
+ 'maxItems',
93
+ 'maxLength',
94
+ 'maximum',
95
+ 'minItems',
96
+ 'minLength',
97
+ 'minimum',
98
+ 'multipleOf',
99
+ 'pattern',
100
+ 'type',
101
+ ];
84
102
  function cloneJsonValueIfDefined(value) {
85
103
  return value === undefined ? undefined : cloneJsonValue(value);
86
104
  }
105
+ function isJsonSchemaObject(value) {
106
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
107
+ }
108
+ function decodeJsonPointerSegment(segment) {
109
+ return segment.replace(/~1/g, '/').replace(/~0/g, '~');
110
+ }
111
+ function resolveOpenApiReferenceTarget(document, reference) {
112
+ if (!reference.startsWith('#/')) {
113
+ throw new Error(`Unsupported OpenAPI schema reference "${reference}".`);
114
+ }
115
+ let current = document;
116
+ for (const rawSegment of reference.slice(2).split('/')) {
117
+ const segment = decodeJsonPointerSegment(rawSegment);
118
+ if (Array.isArray(current)) {
119
+ const index = Number.parseInt(segment, 10);
120
+ current = Number.isInteger(index) ? current[index] : undefined;
121
+ continue;
122
+ }
123
+ if (!isJsonSchemaObject(current)) {
124
+ current = undefined;
125
+ break;
126
+ }
127
+ current = current[segment];
128
+ }
129
+ if (!isJsonSchemaObject(current)) {
130
+ throw new Error(`Unable to resolve OpenAPI schema reference "${reference}".`);
131
+ }
132
+ return current;
133
+ }
134
+ function resolveOpenApiSchemaObject(document, schema, seenReferences = new Set()) {
135
+ if (!isJsonSchemaObject(schema)) {
136
+ return {};
137
+ }
138
+ const reference = schema.$ref;
139
+ if (typeof reference !== 'string') {
140
+ return schema;
141
+ }
142
+ if (seenReferences.has(reference)) {
143
+ return schema;
144
+ }
145
+ return resolveOpenApiSchemaObject(document, resolveOpenApiReferenceTarget(document, reference), new Set([...seenReferences, reference]));
146
+ }
147
+ function collectOpenApiSeenReferences(schema, seenReferences) {
148
+ const reference = schema.$ref;
149
+ if (typeof reference !== 'string' || seenReferences.has(reference)) {
150
+ return seenReferences;
151
+ }
152
+ return new Set([...seenReferences, reference]);
153
+ }
154
+ function mergeJsonSchemaConstraintProperties(document, target, source, seenReferences = new Set()) {
155
+ const nextSeenReferences = collectOpenApiSeenReferences(source, seenReferences);
156
+ const resolvedSource = resolveOpenApiSchemaObject(document, source, seenReferences);
157
+ const merged = target;
158
+ for (const key of JSON_SCHEMA_CONSTRAINT_KEYS) {
159
+ if (resolvedSource[key] !== undefined) {
160
+ merged[key] = cloneJsonValue(resolvedSource[key]);
161
+ }
162
+ }
163
+ if (Array.isArray(resolvedSource.required)) {
164
+ merged.required = resolvedSource.required.filter((value) => typeof value === 'string');
165
+ }
166
+ if (Array.isArray(resolvedSource.items)) {
167
+ merged.items = cloneJsonValue(resolvedSource.items);
168
+ }
169
+ else if (isJsonSchemaObject(resolvedSource.items)) {
170
+ const nextItems = isJsonSchemaObject(merged.items) ? merged.items : {};
171
+ merged.items = mergeJsonSchemaConstraintProperties(document, nextItems, resolvedSource.items, nextSeenReferences);
172
+ }
173
+ if (isJsonSchemaObject(resolvedSource.properties)) {
174
+ const targetProperties = isJsonSchemaObject(merged.properties)
175
+ ? merged.properties
176
+ : {};
177
+ for (const [propertyName, propertySchema] of Object.entries(resolvedSource.properties)) {
178
+ if (!isJsonSchemaObject(propertySchema)) {
179
+ continue;
180
+ }
181
+ const nextProperty = isJsonSchemaObject(targetProperties[propertyName])
182
+ ? targetProperties[propertyName]
183
+ : {};
184
+ targetProperties[propertyName] = mergeJsonSchemaConstraintProperties(document, nextProperty, propertySchema, nextSeenReferences);
185
+ }
186
+ merged.properties = targetProperties;
187
+ }
188
+ return merged;
189
+ }
190
+ function findOpenApiOperationById(document, operationId) {
191
+ for (const pathItem of Object.values(document.paths)) {
192
+ if (!isJsonSchemaObject(pathItem)) {
193
+ continue;
194
+ }
195
+ for (const method of ['delete', 'get', 'patch', 'post', 'put']) {
196
+ const operation = resolveOpenApiSchemaObject(document, pathItem[method]);
197
+ if (!isJsonSchemaObject(operation) ||
198
+ typeof operation.operationId !== 'string') {
199
+ continue;
200
+ }
201
+ if (operation.operationId === operationId) {
202
+ return operation;
203
+ }
204
+ }
205
+ }
206
+ return null;
207
+ }
208
+ function resolveOpenApiRequestBodySchema(operation, document) {
209
+ const requestBody = resolveOpenApiSchemaObject(document, operation.requestBody);
210
+ const content = isJsonSchemaObject(requestBody.content)
211
+ ? requestBody.content
212
+ : null;
213
+ const jsonMediaType = content && isJsonSchemaObject(content['application/json'])
214
+ ? content['application/json']
215
+ : null;
216
+ const schema = jsonMediaType?.schema;
217
+ if (!isJsonSchemaObject(schema)) {
218
+ return null;
219
+ }
220
+ return resolveOpenApiSchemaObject(document, schema);
221
+ }
222
+ function resolveOpenApiSuccessResponseSchema(operation, document) {
223
+ for (const [statusCode, response] of Object.entries(operation.responses)) {
224
+ const resolvedResponse = resolveOpenApiSchemaObject(document, response);
225
+ if (!/^2(?:\d\d|XX)$/u.test(statusCode) ||
226
+ !isJsonSchemaObject(resolvedResponse)) {
227
+ continue;
228
+ }
229
+ const content = isJsonSchemaObject(resolvedResponse.content)
230
+ ? resolvedResponse.content
231
+ : null;
232
+ const jsonMediaType = content && isJsonSchemaObject(content['application/json'])
233
+ ? content['application/json']
234
+ : null;
235
+ const schema = jsonMediaType?.schema;
236
+ if (!isJsonSchemaObject(schema)) {
237
+ continue;
238
+ }
239
+ return resolveOpenApiSchemaObject(document, schema);
240
+ }
241
+ return null;
242
+ }
243
+ function getOrCreateObjectProperty(target, propertyName) {
244
+ const targetProperties = isJsonSchemaObject(target.properties)
245
+ ? target.properties
246
+ : {};
247
+ const nextProperty = isJsonSchemaObject(targetProperties[propertyName])
248
+ ? targetProperties[propertyName]
249
+ : {};
250
+ targetProperties[propertyName] = nextProperty;
251
+ target.properties = targetProperties;
252
+ return nextProperty;
253
+ }
254
+ function applyOpenApiQueryParameterConstraints(target, operation, document) {
255
+ for (const parameter of operation.parameters ?? []) {
256
+ const resolvedParameter = resolveOpenApiSchemaObject(document, parameter);
257
+ if (!isJsonSchemaObject(resolvedParameter) ||
258
+ resolvedParameter.in !== 'query' ||
259
+ typeof resolvedParameter.name !== 'string') {
260
+ continue;
261
+ }
262
+ const propertyTarget = getOrCreateObjectProperty(target, resolvedParameter.name);
263
+ mergeJsonSchemaConstraintProperties(document, propertyTarget, resolveOpenApiSchemaObject(document, resolvedParameter.schema));
264
+ if (resolvedParameter.required === true) {
265
+ const required = new Set(Array.isArray(target.required)
266
+ ? target.required.filter((value) => typeof value === 'string')
267
+ : []);
268
+ required.add(resolvedParameter.name);
269
+ target.required = [...required];
270
+ }
271
+ }
272
+ }
87
273
  function getContractSourceTypeName(manifest, endpoint, contractName) {
88
274
  const contract = manifest.contracts[contractName];
89
275
  if (!contract) {
@@ -342,6 +528,50 @@ function cloneProjectedTypiaLlmFunctionArtifact(functionArtifact) {
342
528
  ...(functionArtifact.tags ? { tags: [...functionArtifact.tags] } : {}),
343
529
  };
344
530
  }
531
+ /**
532
+ * Restores canonical request and response constraints from an endpoint-aware
533
+ * OpenAPI document onto one projected `typia.llm` function artifact.
534
+ *
535
+ * Query-only inputs merge into the root parameter object. Mixed body/query
536
+ * inputs merge into the generated `body` and `query` properties.
537
+ *
538
+ * @param options Projected artifact plus the canonical OpenAPI document.
539
+ * @returns A cloned artifact enriched with OpenAPI-backed constraints when available.
540
+ */
541
+ export function applyOpenApiConstraintsToTypiaLlmFunctionArtifact({ functionArtifact, openApiDocument, operationId, }) {
542
+ const constrainedArtifact = cloneProjectedTypiaLlmFunctionArtifact(functionArtifact);
543
+ const operation = findOpenApiOperationById(openApiDocument, operationId);
544
+ if (!operation) {
545
+ return constrainedArtifact;
546
+ }
547
+ const hasQueryParameters = (operation.parameters ?? []).some((parameter) => {
548
+ const resolvedParameter = resolveOpenApiSchemaObject(openApiDocument, parameter);
549
+ return (isJsonSchemaObject(resolvedParameter) &&
550
+ resolvedParameter.in === 'query' &&
551
+ typeof resolvedParameter.name === 'string');
552
+ });
553
+ const requestBodySchema = resolveOpenApiRequestBodySchema(operation, openApiDocument);
554
+ if (requestBodySchema) {
555
+ if (hasQueryParameters) {
556
+ mergeJsonSchemaConstraintProperties(openApiDocument, getOrCreateObjectProperty(constrainedArtifact.parameters, 'body'), requestBodySchema);
557
+ }
558
+ else {
559
+ mergeJsonSchemaConstraintProperties(openApiDocument, constrainedArtifact.parameters, requestBodySchema);
560
+ }
561
+ }
562
+ if (hasQueryParameters) {
563
+ applyOpenApiQueryParameterConstraints(requestBodySchema
564
+ ? getOrCreateObjectProperty(constrainedArtifact.parameters, 'query')
565
+ : constrainedArtifact.parameters, operation, openApiDocument);
566
+ }
567
+ if (constrainedArtifact.output) {
568
+ const responseSchema = resolveOpenApiSuccessResponseSchema(operation, openApiDocument);
569
+ if (responseSchema) {
570
+ mergeJsonSchemaConstraintProperties(openApiDocument, constrainedArtifact.output, responseSchema);
571
+ }
572
+ }
573
+ return constrainedArtifact;
574
+ }
345
575
  /**
346
576
  * Projects a compiled `typia.llm.application(...)` result into a JSON-friendly
347
577
  * downstream adapter artifact.
@@ -349,13 +579,20 @@ function cloneProjectedTypiaLlmFunctionArtifact(functionArtifact) {
349
579
  * @param options Compiled application value and source metadata.
350
580
  * @returns JSON-friendly application artifact.
351
581
  */
352
- export function projectTypiaLlmApplicationArtifact({ application, generatedFrom, transformFunction, }) {
582
+ export function projectTypiaLlmApplicationArtifact({ application, generatedFrom, openApiProjection, transformFunction, }) {
353
583
  return {
354
584
  functions: application.functions.map((functionSchema) => {
355
585
  const functionArtifact = projectTypiaLlmApplicationFunction(functionSchema);
356
- const transformedArtifact = transformFunction
357
- ? transformFunction(functionArtifact, functionSchema)
586
+ const openApiAlignedArtifact = openApiProjection
587
+ ? applyOpenApiConstraintsToTypiaLlmFunctionArtifact({
588
+ functionArtifact,
589
+ openApiDocument: openApiProjection.openApiDocument,
590
+ operationId: openApiProjection.resolveOperationId?.(functionSchema, functionArtifact) ?? functionSchema.name,
591
+ })
358
592
  : functionArtifact;
593
+ const transformedArtifact = transformFunction
594
+ ? transformFunction(openApiAlignedArtifact, functionSchema)
595
+ : openApiAlignedArtifact;
359
596
  return cloneProjectedTypiaLlmFunctionArtifact(transformedArtifact);
360
597
  }),
361
598
  generatedFrom: cloneJsonValue(generatedFrom),
@@ -121,7 +121,9 @@ const WORKSPACE_COMPATIBILITY_CONFIG_FIELD = `\tcompatibility?: {
121
121
  \t\t\twordpress?: string;
122
122
  \t\t};
123
123
  \t\tmode: 'baseline' | 'optional' | 'required';
124
+ \t\toptionalFeatureIds: string[];
124
125
  \t\toptionalFeatures: string[];
126
+ \t\trequiredFeatureIds: string[];
125
127
  \t\trequiredFeatures: string[];
126
128
  \t\truntimeGates: string[];
127
129
  \t};
@@ -670,6 +672,26 @@ function ensureInterfaceField(source, interfaceName, fieldName, fieldSource) {
670
672
  return `${start}${body}${body.length > 0 && !body.endsWith(lineEnding) ? lineEnding : ""}${formattedFieldSource}${end}`;
671
673
  });
672
674
  }
675
+ function normalizeInterfaceFieldBlock(source, interfaceName, fieldName, fieldSource, requiredFragments) {
676
+ const interfacePattern = new RegExp(`(export\\s+interface\\s+${escapeRegex(interfaceName)}\\s*\\{\\r?\\n)([\\s\\S]*?)(\\r?\\n\\})`, "u");
677
+ return source.replace(interfacePattern, (match, start, body, end) => {
678
+ const fieldPattern = new RegExp(`(^([ \\t]*)${escapeRegex(fieldName)}\\??:\\s*\\{[ \\t]*\\r?\\n)([\\s\\S]*?)(^\\2\\};\\r?\\n?)`, "mu");
679
+ const fieldMatch = fieldPattern.exec(body);
680
+ if (!fieldMatch) {
681
+ return match;
682
+ }
683
+ const existingFieldSource = fieldMatch[0];
684
+ if (requiredFragments.every((fragment) => existingFieldSource.includes(fragment))) {
685
+ return match;
686
+ }
687
+ const lineEnding = start.endsWith("\r\n") ? "\r\n" : "\n";
688
+ const formattedFieldSource = `${fieldSource
689
+ .replace(/\r?\n$/u, "")
690
+ .split("\n")
691
+ .join(lineEnding)}${lineEnding}`;
692
+ return `${start}${body.slice(0, fieldMatch.index)}${formattedFieldSource}${body.slice(fieldMatch.index + existingFieldSource.length)}${end}`;
693
+ });
694
+ }
673
695
  /**
674
696
  * Update `scripts/block-config.ts` source text with additional inventory entries.
675
697
  *
@@ -701,7 +723,9 @@ export function updateWorkspaceInventorySource(source, { blockEntries = [], bloc
701
723
  nextSource = ensureInterfaceField(nextSource, "WorkspaceBindingSourceConfig", "attribute", "\tattribute?: string;");
702
724
  nextSource = ensureInterfaceField(nextSource, "WorkspaceBindingSourceConfig", "block", "\tblock?: string;");
703
725
  nextSource = ensureInterfaceField(nextSource, "WorkspaceAbilityConfig", "compatibility", WORKSPACE_COMPATIBILITY_CONFIG_FIELD);
726
+ nextSource = normalizeInterfaceFieldBlock(nextSource, "WorkspaceAbilityConfig", "compatibility", WORKSPACE_COMPATIBILITY_CONFIG_FIELD, ["optionalFeatureIds: string[];", "requiredFeatureIds: string[];"]);
704
727
  nextSource = ensureInterfaceField(nextSource, "WorkspaceAiFeatureConfig", "compatibility", WORKSPACE_COMPATIBILITY_CONFIG_FIELD);
728
+ nextSource = normalizeInterfaceFieldBlock(nextSource, "WorkspaceAiFeatureConfig", "compatibility", WORKSPACE_COMPATIBILITY_CONFIG_FIELD, ["optionalFeatureIds: string[];", "requiredFeatureIds: string[];"]);
705
729
  nextSource = appendEntriesAtMarker(nextSource, EDITOR_PLUGIN_CONFIG_ENTRY_MARKER, editorPluginEntries);
706
730
  return nextSource;
707
731
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wp-typia/project-tools",
3
- "version": "0.22.1",
3
+ "version": "0.22.2",
4
4
  "description": "Project orchestration and programmatic tooling for wp-typia",
5
5
  "packageManager": "bun@1.3.11",
6
6
  "type": "module",