@@ -4,7 +4,6 @@ import { extractVariables } from '../renderer/index.js';
44import { resolveIncludes } from '../composition/index.js' ;
55import {
66 compileContextRegex ,
7- formatInvalidContextRegexMessage ,
87 getContextInputs ,
98 getContextInputNames ,
109} from '../context.js' ;
@@ -26,9 +25,21 @@ export interface PromptValidationResult {
2625const KNOWN_FRONT_MATTER_KEYS = new Set ( [
2726 'id' , 'schema_version' , 'description' , 'provider' , 'model' , 'fallback_models' ,
2827 'reasoning' , 'sampling' , 'response' , 'tools' , 'mcp' , 'context' , 'includes' ,
29- 'environments' , 'tiers' , 'metadata' , 'cache' ,
28+ 'environments' , 'tiers' , 'metadata' , 'cache' , 'provider_options' ,
3029] ) ;
3130
31+ const RISKY_UNBOUNDED_INPUT_NAMES = [
32+ 'message' ,
33+ 'prompt' ,
34+ 'history' ,
35+ 'transcript' ,
36+ 'document' ,
37+ 'content' ,
38+ 'input' ,
39+ 'body' ,
40+ 'context' ,
41+ ] ;
42+
3243/**
3344 * Validate a parsed prompt asset, returning all errors and warnings.
3445 */
@@ -121,8 +132,42 @@ export function validateAsset(
121132 }
122133 }
123134
135+ if ( usedVars . size > 0 && ( ! asset . context ?. inputs || asset . context . inputs . length === 0 ) ) {
136+ warnings . push ( {
137+ code : 'POK046' ,
138+ message : `Template uses ${ usedVars . size === 1 ? 'a variable' : 'variables' } but context.inputs is not declared.` ,
139+ filePath,
140+ suggestion : 'Declare context.inputs to enable input policy validation.' ,
141+ } ) ;
142+ }
143+
124144 // Context regex definitions compile successfully
125145 for ( const input of getContextInputs ( asset ) ) {
146+ const lowerName = input . name . toLowerCase ( ) ;
147+
148+ if ( input . max_size === undefined && RISKY_UNBOUNDED_INPUT_NAMES . some ( ( needle ) => lowerName . includes ( needle ) ) ) {
149+ warnings . push ( {
150+ code : 'POK040' ,
151+ message : `Context input "${ input . name } " has no max_size and appears unbounded.` ,
152+ filePath,
153+ suggestion : 'Add max_size to constrain prompt payload growth.' ,
154+ } ) ;
155+ }
156+
157+ if (
158+ input . allow_regex === undefined
159+ && input . deny_regex === undefined
160+ && input . non_empty === undefined
161+ && input . reject_secrets === undefined
162+ ) {
163+ warnings . push ( {
164+ code : 'POK041' ,
165+ message : `Context input "${ input . name } " has no input hardening validators.` ,
166+ filePath,
167+ suggestion : 'Consider non_empty/reject_secrets and allow/deny regex validators.' ,
168+ } ) ;
169+ }
170+
126171 if ( input . trim !== undefined && input . trim !== false && input . max_size === undefined ) {
127172 warnings . push ( {
128173 code : 'POK014' ,
@@ -157,6 +202,103 @@ export function validateAsset(
157202 }
158203 }
159204
205+ if ( asset . provider ) {
206+ let providerCache : unknown ;
207+ let cacheSuggestionField : string | undefined ;
208+
209+ switch ( asset . provider ) {
210+ case 'openai' :
211+ providerCache = asset . cache ?. openai ;
212+ cacheSuggestionField = 'cache.openai' ;
213+ break ;
214+ case 'anthropic' :
215+ providerCache = asset . cache ?. anthropic ;
216+ cacheSuggestionField = 'cache.anthropic' ;
217+ break ;
218+ case 'gemini' :
219+ case 'google' :
220+ providerCache = asset . cache ?. gemini ?? asset . cache ?. google ;
221+ cacheSuggestionField = 'cache.gemini' ;
222+ break ;
223+ default :
224+ break ;
225+ }
226+
227+ if ( cacheSuggestionField && providerCache === undefined ) {
228+ warnings . push ( {
229+ code : 'POK042' ,
230+ message : `Provider "${ asset . provider } " has no provider-specific cache settings.` ,
231+ filePath,
232+ suggestion : `Consider configuring ${ cacheSuggestionField } for better cache-hit behavior.` ,
233+ } ) ;
234+ }
235+
236+ if ( ! asset . model ) {
237+ warnings . push ( {
238+ code : 'POK044' ,
239+ message : `Provider "${ asset . provider } " is configured without a model.` ,
240+ filePath,
241+ suggestion : 'Set model in prompt or defaults to avoid adapter-time errors.' ,
242+ } ) ;
243+ }
244+ }
245+
246+ if (
247+ asset . cache ?. gemini ?. cached_content
248+ && asset . cache . google ?. cached_content
249+ && asset . cache . gemini . cached_content !== asset . cache . google . cached_content
250+ ) {
251+ warnings . push ( {
252+ code : 'POK043' ,
253+ message : 'cache.gemini.cached_content and cache.google.cached_content are both set to different values.' ,
254+ filePath,
255+ suggestion : 'Use one canonical value; Gemini prefers cache.gemini.cached_content.' ,
256+ } ) ;
257+ }
258+
259+ for ( const [ envName , overrides ] of Object . entries ( asset . environments ?? { } ) ) {
260+ if ( asset . cache && ! overrides . cache ) {
261+ warnings . push ( {
262+ code : 'POK045' ,
263+ message : `Environment "${ envName } " does not override cache while prompt-level cache is defined.` ,
264+ filePath,
265+ suggestion : 'Confirm cache strategy is intentionally shared across environments.' ,
266+ } ) ;
267+ }
268+ }
269+
270+ for ( const [ tierName , overrides ] of Object . entries ( asset . tiers ?? { } ) ) {
271+ if ( asset . cache && ! overrides . cache ) {
272+ warnings . push ( {
273+ code : 'POK045' ,
274+ message : `Tier "${ tierName } " does not override cache while prompt-level cache is defined.` ,
275+ filePath,
276+ suggestion : 'Confirm cache strategy is intentionally shared across tiers.' ,
277+ } ) ;
278+ }
279+ }
280+
281+ for ( const tool of asset . tools ?? [ ] ) {
282+ if ( typeof tool !== 'string' ) {
283+ if ( ! tool . description ) {
284+ warnings . push ( {
285+ code : 'POK047' ,
286+ message : `Inline tool "${ tool . name } " is missing a description.` ,
287+ filePath,
288+ suggestion : 'Add description to improve model tool-selection quality.' ,
289+ } ) ;
290+ }
291+ if ( ! tool . input_schema ) {
292+ warnings . push ( {
293+ code : 'POK047' ,
294+ message : `Inline tool "${ tool . name } " is missing input_schema.` ,
295+ filePath,
296+ suggestion : 'Add input_schema so tool inputs are strongly typed.' ,
297+ } ) ;
298+ }
299+ }
300+ }
301+
160302 return {
161303 valid : errors . length === 0 ,
162304 errors,
0 commit comments