@@ -335,6 +335,8 @@ const REMOVED_TOP_LEVEL_FIELDS = new Map<string, string>([
335335 'preprocessors' ,
336336 "Top-level 'preprocessors' has been removed from authored eval YAML. Use default_test.options.transform or assertion-level transform instead." ,
337337 ] ,
338+ [ 'providerPromptMap' , "Top-level 'providerPromptMap' is not supported. Use 'targets'." ] ,
339+ [ 'provider_prompt_map' , "Top-level 'provider_prompt_map' is not supported. Use 'targets'." ] ,
338340] ) ;
339341
340342/** Deprecated top-level fields with migration hints. */
@@ -374,6 +376,7 @@ const KNOWN_TEST_FIELDS = new Set([
374376 'window_size' ,
375377] ) ;
376378
379+ const KNOWN_SCENARIO_FIELDS = new Set ( [ 'description' , 'config' , 'tests' ] ) ;
377380const SUPPORTED_WORKSPACE_REPO_FIELDS = new Set ( [ 'path' , 'repo' , 'commit' , 'ancestor' , 'sparse' ] ) ;
378381const KNOWN_REMOVED_WORKSPACE_FIELDS = new Set ( [
379382 'template' ,
@@ -393,6 +396,22 @@ REMOVED_TEST_FIELDS.set(
393396 'input' ,
394397 "tests[].input has been removed from authored eval YAML. Put prompt text or chat/system/user messages in top-level 'prompts' and put row-specific data in tests[].vars." ,
395398) ;
399+ REMOVED_TEST_FIELDS . set (
400+ 'preprocessors' ,
401+ 'tests[].preprocessors has been removed from authored eval YAML. Use tests[].options.transform or assertion-level transform instead.' ,
402+ ) ;
403+ REMOVED_TEST_FIELDS . set (
404+ 'postprocess' ,
405+ 'tests[].postprocess has been removed from authored eval YAML. Use tests[].options.transform instead.' ,
406+ ) ;
407+ REMOVED_TEST_FIELDS . set (
408+ 'eval_cases' ,
409+ "tests[].eval_cases has been removed from authored eval YAML. Use 'tests' rows directly." ,
410+ ) ;
411+ REMOVED_TEST_FIELDS . set (
412+ 'evalcases' ,
413+ "tests[].evalcases has been removed from authored eval YAML. Use 'tests' rows directly." ,
414+ ) ;
396415
397416/** Deprecated test-level fields with migration hints. */
398417const DEPRECATED_TEST_FIELDS = new Map < string , string > ( [
@@ -570,6 +589,7 @@ export async function validateEvalFile(filePath: string): Promise<ValidationResu
570589 validateEvaluateOptions ( parsed . evaluate_options , 'evaluate_options' , absolutePath , errors ) ;
571590 validateAssertArray ( parsed . assert , 'assert' , absolutePath , errors , customAssertionTypes ) ;
572591 validateDefaultTest ( parsed . default_test , absolutePath , errors , customAssertionTypes ) ;
592+ await validateScenarios ( parsed . scenarios , parsed , absolutePath , errors , customAssertionTypes ) ;
573593 const cases : JsonValue | undefined = parsed . tests ;
574594 const hasImports = collectImportEntries ( parsed ) . length > 0 ;
575595 const hasScenarios = Array . isArray ( parsed . scenarios ) ;
@@ -1437,6 +1457,222 @@ function validateDefaultTest(
14371457 }
14381458}
14391459
1460+ async function validateScenarios (
1461+ scenarios : JsonValue | undefined ,
1462+ parsed : JsonObject ,
1463+ filePath : string ,
1464+ errors : ValidationError [ ] ,
1465+ customAssertionTypes : ReadonlySet < string > ,
1466+ ) : Promise < void > {
1467+ if ( scenarios === undefined ) {
1468+ return ;
1469+ }
1470+ if ( ! Array . isArray ( scenarios ) ) {
1471+ errors . push ( {
1472+ severity : 'error' ,
1473+ filePath,
1474+ location : 'scenarios' ,
1475+ message : "Invalid 'scenarios' field (must be an array)" ,
1476+ } ) ;
1477+ return ;
1478+ }
1479+
1480+ for ( let scenarioIndex = 0 ; scenarioIndex < scenarios . length ; scenarioIndex ++ ) {
1481+ const scenario = scenarios [ scenarioIndex ] ;
1482+ const scenarioLocation = `scenarios[${ scenarioIndex } ]` ;
1483+ if ( ! isObject ( scenario ) ) {
1484+ errors . push ( {
1485+ severity : 'error' ,
1486+ filePath,
1487+ location : scenarioLocation ,
1488+ message : 'Scenario must be an object' ,
1489+ } ) ;
1490+ continue ;
1491+ }
1492+
1493+ for ( const key of Object . keys ( scenario ) ) {
1494+ if ( ! KNOWN_SCENARIO_FIELDS . has ( key ) ) {
1495+ errors . push ( {
1496+ severity : 'error' ,
1497+ filePath,
1498+ location : `${ scenarioLocation } .${ key } ` ,
1499+ message : `Unknown scenario field '${ key } '.` ,
1500+ } ) ;
1501+ }
1502+ }
1503+
1504+ if ( ! Array . isArray ( scenario . config ) ) {
1505+ errors . push ( {
1506+ severity : 'error' ,
1507+ filePath,
1508+ location : `${ scenarioLocation } .config` ,
1509+ message : "Invalid 'config' field (must be an array)" ,
1510+ } ) ;
1511+ } else {
1512+ for ( let configIndex = 0 ; configIndex < scenario . config . length ; configIndex ++ ) {
1513+ await validateScenarioTestLikeRow (
1514+ scenario . config [ configIndex ] ,
1515+ `${ scenarioLocation } .config[${ configIndex } ]` ,
1516+ filePath ,
1517+ errors ,
1518+ customAssertionTypes ,
1519+ { requireInput : false } ,
1520+ ) ;
1521+ }
1522+ }
1523+
1524+ const scenarioConfigs = Array . isArray ( scenario . config ) ? scenario . config : [ ] ;
1525+
1526+ if ( ! Array . isArray ( scenario . tests ) ) {
1527+ errors . push ( {
1528+ severity : 'error' ,
1529+ filePath,
1530+ location : `${ scenarioLocation } .tests` ,
1531+ message : "Invalid 'tests' field (must be an array)" ,
1532+ } ) ;
1533+ continue ;
1534+ }
1535+
1536+ for ( let testIndex = 0 ; testIndex < scenario . tests . length ; testIndex ++ ) {
1537+ const test = scenario . tests [ testIndex ] ;
1538+ const everyConfigProvidesInput =
1539+ scenarioConfigs . length > 0 &&
1540+ scenarioConfigs . every (
1541+ ( config ) =>
1542+ isObject ( config ) &&
1543+ ( config . prompts !== undefined || config . provider_output !== undefined ) ,
1544+ ) ;
1545+ const requireInput =
1546+ isObject ( test ) &&
1547+ parsed . prompts === undefined &&
1548+ test . prompts === undefined &&
1549+ test . provider_output === undefined &&
1550+ ! everyConfigProvidesInput ;
1551+ await validateScenarioTestLikeRow (
1552+ test ,
1553+ `${ scenarioLocation } .tests[${ testIndex } ]` ,
1554+ filePath ,
1555+ errors ,
1556+ customAssertionTypes ,
1557+ { requireInput } ,
1558+ ) ;
1559+ }
1560+ }
1561+ }
1562+
1563+ async function validateScenarioTestLikeRow (
1564+ row : JsonValue | undefined ,
1565+ location : string ,
1566+ filePath : string ,
1567+ errors : ValidationError [ ] ,
1568+ customAssertionTypes : ReadonlySet < string > ,
1569+ options : { readonly requireInput : boolean } ,
1570+ ) : Promise < void > {
1571+ if ( ! isObject ( row ) ) {
1572+ errors . push ( {
1573+ severity : 'error' ,
1574+ filePath,
1575+ location,
1576+ message : 'Scenario row must be an object' ,
1577+ } ) ;
1578+ return ;
1579+ }
1580+
1581+ for ( const key of Object . keys ( row ) ) {
1582+ const removedMessage = REMOVED_TEST_FIELDS . get ( key ) ;
1583+ if ( removedMessage ) {
1584+ errors . push ( {
1585+ severity : 'error' ,
1586+ filePath,
1587+ location : `${ location } .${ key } ` ,
1588+ message : removedMessage . replaceAll ( 'tests[]' , location ) ,
1589+ } ) ;
1590+ continue ;
1591+ }
1592+ const deprecationMessage = DEPRECATED_TEST_FIELDS . get ( key ) ;
1593+ if ( deprecationMessage ) {
1594+ errors . push ( {
1595+ severity : 'warning' ,
1596+ filePath,
1597+ location : `${ location } .${ key } ` ,
1598+ message : deprecationMessage ,
1599+ } ) ;
1600+ } else if ( ! KNOWN_TEST_FIELDS . has ( key ) ) {
1601+ errors . push ( {
1602+ severity : 'error' ,
1603+ filePath,
1604+ location : `${ location } .${ key } ` ,
1605+ message : `Unknown test field '${ key } '.` ,
1606+ } ) ;
1607+ }
1608+ }
1609+
1610+ const id = row . id ;
1611+ if ( id !== undefined && ( typeof id !== 'string' || id . trim ( ) . length === 0 ) ) {
1612+ errors . push ( {
1613+ severity : 'error' ,
1614+ filePath,
1615+ location : `${ location } .id` ,
1616+ message : "Invalid 'id' field (must be a non-empty string when provided)" ,
1617+ } ) ;
1618+ }
1619+
1620+ const criteria = row . criteria ;
1621+ if ( criteria !== undefined && ( typeof criteria !== 'string' || criteria . trim ( ) . length === 0 ) ) {
1622+ errors . push ( {
1623+ severity : 'error' ,
1624+ filePath,
1625+ location : `${ location } .criteria` ,
1626+ message : "Invalid 'criteria' field (must be a non-empty string if provided)" ,
1627+ } ) ;
1628+ } else if ( criteria !== undefined && row . assert !== undefined ) {
1629+ errors . push ( {
1630+ severity : 'error' ,
1631+ filePath,
1632+ location : `${ location } .criteria` ,
1633+ message :
1634+ "Do not combine test-level 'criteria' with 'assert'. Put human-readable case descriptions in tests[].description, or express grading text as an explicit assertion such as { type: 'llm-rubric', value: ... }." ,
1635+ } ) ;
1636+ }
1637+
1638+ validateInputField ( row . input , `${ location } .input` , filePath , errors , {
1639+ required : options . requireInput ,
1640+ } ) ;
1641+
1642+ if ( row . expected_output !== undefined ) {
1643+ errors . push ( {
1644+ severity : 'error' ,
1645+ filePath,
1646+ location : `${ location } .expected_output` ,
1647+ message :
1648+ "tests[].expected_output has been removed from authored eval YAML. Put the reference answer in tests[].vars.expected_output and consume it with an explicit assertion, for example { type: 'llm-rubric', value: 'Matches the reference answer: {{ expected_output }}' }." ,
1649+ } ) ;
1650+ }
1651+
1652+ validateAssertArray ( row . assert , `${ location } .assert` , filePath , errors , customAssertionTypes ) ;
1653+ validateRunOverride ( row . run , `${ location } .run` , filePath , errors ) ;
1654+ validateTestOptions ( row . options , `${ location } .options` , filePath , errors ) ;
1655+ await validateEnvironmentConfig ( row . environment , filePath , errors , `${ location } .environment` ) ;
1656+ validateConversationMode ( row , location , filePath , errors ) ;
1657+ await validateWorkspaceConfig ( row . workspace , filePath , errors , `${ location } .workspace` ) ;
1658+
1659+ const caseExecution = isObject ( row . execution ) ? row . execution : undefined ;
1660+ if ( caseExecution ) {
1661+ validateTestExecutionFields ( caseExecution , filePath , errors , location ) ;
1662+ rejectRuntimeWorkspaceConfig (
1663+ caseExecution . workspace ,
1664+ filePath ,
1665+ errors ,
1666+ `${ location } .execution.workspace` ,
1667+ ) ;
1668+ }
1669+
1670+ const targets = row . providers !== undefined ? row . providers : row . provider ;
1671+ if ( targets !== undefined ) {
1672+ validateTargetTestbedFields ( targets , `${ location } .providers` , filePath , errors ) ;
1673+ }
1674+ }
1675+
14401676function validateEvaluateOptions (
14411677 evaluateOptions : JsonValue | undefined ,
14421678 location : string ,
0 commit comments