refactor: simplify flow benchmark validation

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
centdix
2026-04-09 13:30:49 +02:00
co-authored by Claude Opus 4.5
parent c2b5808c7e
commit 06c0b964f9
7 changed files with 225 additions and 377 deletions
+98 -47
View File
@@ -3,6 +3,10 @@
Create a flow that takes two numbers, `a` and `b`, and returns their sum.
Keep it simple and use a single step named `sum_numbers`.
expected: ai_evals/fixtures/frontend/flow/expected/test0_sum_two_numbers.json
judgeChecklist:
- "the flow takes `a` and `b` as inputs"
- "the main step is named `sum_numbers`"
- the flow returns the sum of the two numbers
- id: flow-test1-reuse-existing-script
prompt: |-
@@ -11,6 +15,10 @@
The flow should take `a` and `b` as inputs and use a single step named `sum_numbers`.
initial: ai_evals/fixtures/frontend/flow/initial/test1_reuse_existing_script_initial.json
expected: ai_evals/fixtures/frontend/flow/expected/test1_reuse_existing_script.json
judgeChecklist:
- "the flow takes `a` and `b` as inputs"
- "the main step is named `sum_numbers`"
- the flow reuses the existing workspace script instead of rewriting the addition logic
- id: flow-test2-call-existing-subflow
prompt: |-
@@ -19,6 +27,10 @@
Use a single step named `call_add_numbers`.
initial: ai_evals/fixtures/frontend/flow/initial/test2_call_existing_subflow_initial.json
expected: ai_evals/fixtures/frontend/flow/expected/test2_call_existing_subflow.json
judgeChecklist:
- "the parent flow takes `a` and `b` as inputs"
- "the main step is named `call_add_numbers`"
- the parent flow delegates to an existing workspace subflow instead of inlining the addition logic
- id: flow-test3-branchone-routing
prompt: |-
@@ -27,6 +39,13 @@
Free, pro, and enterprise requests should go to different queues, and unknown tiers should fall back to a default queue.
Name the main routing step `route_by_tier`.
expected: ai_evals/fixtures/frontend/flow/expected/test3_branchone_routing.json
judgeChecklist:
- "the input schema includes a string field named `tier`"
- "the main routing step is named `route_by_tier`"
- free requests go to a free queue
- pro requests go to a pro queue
- enterprise requests go to an enterprise queue
- unknown tiers fall back to a default queue
- id: flow-test4-order-processing-loop
prompt: |-
@@ -56,50 +75,13 @@
- customer_email
- shipping_address
resolveResultsRefs: true
modules:
- name: validates order item price and quantity
inputRefsContainAny:
- items
- order
codeContainsAll:
- price
- quantity
- name: calculates totals with 8 percent tax
inputRefsContainAny:
- items
- order
codeContainsAll:
- "0.08"
- name: checks inventory for each item
inputRefsContainAny:
- items
- itemDetails
- order
codeContainsAll:
- available
codeContainsAny:
- ".map("
- "forEach("
- "for ("
- name: creates shipment outcome
codeContainsAny:
- shipment_id
- SHIP-
- name: creates backorder outcome
codeContainsAny:
- backorder_id
- BO-
- name: sends confirmation to the customer
inputRefsContainAny:
- customer_email
- order
codeContainsAny:
- customer_email
- customerEmail
- name: returns final summary using totals and confirmation
inputRefsContainAll:
- calculate_total
- send_confirmation
judgeChecklist:
- the flow validates that every item has a positive price and quantity
- the flow calculates the order total with 8% tax
- the flow checks inventory for each item using placeholder availability data
- the flow creates a shipment if everything is in stock, otherwise a backorder
- the flow sends a confirmation using placeholder email logic
- the flow returns a final order summary with the resulting status
- id: flow-test5-parallel-data-pipeline
prompt: |-
@@ -116,7 +98,16 @@
- 70 to 89 goes to a secondary database with a warning
- below 70 goes to quarantine and triggers an alert
- return a processing report with total records, quality score, and destination
expected: ai_evals/fixtures/frontend/flow/expected/test3.json
judgeChecklist:
- the flow loads a placeholder configuration listing three external sources
- the flow fetches placeholder records from each source
- the flow cleans and validates each source's records
- the flow combines everything into one dataset
- the flow computes an overall quality score
- scores of 90 or above go to the primary database
- scores from 70 to 89 go to a secondary database with a warning
- scores below 70 go to quarantine and trigger an alert
- the final report includes total records, quality score, and destination
- id: flow-test6-ai-agent-tools
prompt: |-
@@ -131,7 +122,16 @@
- open a support ticket when needed
After that, log the interaction and return the assistant's response along with any actions it took.
expected: ai_evals/fixtures/frontend/flow/expected/test4.json
judgeChecklist:
- "the input schema includes `customer_id` and `query_text`"
- the flow loads the customer's profile and order history
- the flow uses an AI assistant step
- the assistant can look up orders
- the assistant can check refund eligibility
- the assistant can search FAQs
- the assistant can open a support ticket
- the flow logs the interaction
- the final output returns the assistant response along with any actions taken or resulting support action details
- id: flow-test7-simple-modification
prompt: |-
@@ -143,6 +143,12 @@
Update `save_results` so it handles the validation result correctly.
initial: ai_evals/fixtures/frontend/flow/initial/test5_initial.json
expected: ai_evals/fixtures/frontend/flow/expected/test5_modify_simple.json
judgeChecklist:
- the updated flow keeps the original fetch and process steps intact
- "a `validate_data` step is added after `process_data`"
- "`validate_data` checks that the processed data array is not empty"
- "empty data returns an error object with the message `No data to save`"
- "`save_results` handles the validation result correctly"
- id: flow-test8-branching-in-loop
prompt: |-
@@ -154,7 +160,14 @@
Keep the existing processing as a fallback for unknown order types.
Each path should return the orderId, shipping cost, and shipping type.
initial: ai_evals/fixtures/frontend/flow/initial/test6_initial.json
expected: ai_evals/fixtures/frontend/flow/expected/test6_modify_medium.json
judgeChecklist:
- "the existing `loop_orders` flow still handles per-order processing"
- exact branching topology is not required as long as `loop_orders` handles the order types correctly
- express orders are marked as priority and use a shipping cost of 15.99
- standard orders use a shipping cost of 5.99
- pickup orders use a shipping cost of 0 and are treated as no shipping required
- unknown order types still follow a fallback path
- "each processed order returns `orderId`, `shippingCost`, and `shippingType`"
- id: flow-test9-parallel-refactor
prompt: |-
@@ -166,6 +179,13 @@
Keep `get_item` as the first step and `return_result` as the last step.
initial: ai_evals/fixtures/frontend/flow/initial/test7_initial.json
expected: ai_evals/fixtures/frontend/flow/expected/test7_modify_complex.json
judgeChecklist:
- "the updated flow keeps `get_item` as the first step"
- "the updated flow keeps `return_result` as the last step"
- "`enrich_price`, `enrich_inventory`, and `enrich_reviews` run independently rather than sequentially"
- each enrichment path returns a fallback value if it fails
- "`combine_data` merges the enrichment results"
- "`combine_data` sets `hasFallbacks` when any fallback was used"
- id: flow-test10-while-loop-counter
prompt: |-
@@ -174,6 +194,11 @@
Name the looping step `count_until_target`.
Once the target is reached, return the final counter value.
expected: ai_evals/fixtures/frontend/flow/expected/test10_while_loop_counter.json
judgeChecklist:
- "the input schema includes a number field named `target`"
- "the looping step is named `count_until_target`"
- the flow keeps incrementing a counter until the target is reached
- the final output returns the final counter value
- id: flow-test11-preprocessor-and-failure-handler
prompt: |-
@@ -183,6 +208,16 @@
The main step should be named `process_event` and return a simple success object.
If anything fails, return a compact error object with the error message and the failing step id.
expected: ai_evals/fixtures/frontend/flow/expected/test11_preprocessor_failure.json
validate:
requireSpecialModules:
- preprocessor_module
- failure_module
judgeChecklist:
- the flow trims the payload before the main processing runs
- the flow rejects empty payload strings
- "the main step is named `process_event`"
- "`process_event` returns a simple success object"
- failures return a compact error object with the error message and failing step id
- id: flow-test12-approval-step
prompt: |-
@@ -193,3 +228,19 @@
One approval should be enough to continue.
After approval, add a final step named `finalize_purchase` that returns an approved status object.
expected: ai_evals/fixtures/frontend/flow/expected/test12_approval_step.json
validate:
schemaRequiredPaths:
- requester_email
- amount
requireSuspendSteps:
- id: request_approval
requiredEvents: 1
resumeRequiredStringFieldAnyOf:
- comment
- approver_comment
judgeChecklist:
- "the flow includes an approval step named `request_approval`"
- "`request_approval` pauses the flow and asks the approver for a comment"
- one approval is enough to continue
- "the flow includes a final step named `finalize_purchase`"
- "`finalize_purchase` returns an approved status object after approval"
+2
View File
@@ -13,6 +13,7 @@ interface RawEvalCase {
initial?: string;
expected?: string;
validate?: FlowValidationSpec;
judgeChecklist?: string[];
}
export function getRepoRoot(): string {
@@ -34,6 +35,7 @@ export async function loadCases(mode: EvalMode): Promise<EvalCase[]> {
initialPath: resolveFixturePath(entry.initial),
expectedPath: resolveFixturePath(entry.expected),
validate: entry.validate,
judgeChecklist: entry.judgeChecklist,
}));
}
+16 -1
View File
@@ -8,6 +8,7 @@ const JUDGE_TOOL_NAME = "submit_judgement";
export async function judgeOutput(input: {
mode: EvalMode;
prompt: string;
checklist?: string[];
initial?: unknown;
expected?: unknown;
actual: unknown;
@@ -30,7 +31,10 @@ export async function judgeOutput(input: {
"You evaluate benchmark outputs for Windmill AI generation.",
"Deterministic checks already run separately. Focus on whether the final output satisfies the user request.",
"If expected state is provided, treat it as a valid example and reward semantically equivalent outputs.",
"Be strict about missing requested functionality. Inputs, module ids, and other details do not need to be exactly the same, but the functionality must be the same.",
"If a checklist is provided, treat it as the explicit acceptance criteria for this case.",
"Be strict about missing requested functionality.",
"When the prompt wording is ambiguous, prefer the checklist over inferred structural requirements.",
"Do not require exact ids, exact topology, or exact field names unless the prompt, checklist, or expected state clearly requires them.",
`Always respond by calling the ${JUDGE_TOOL_NAME} tool exactly once.`,
].join("\n\n");
@@ -40,6 +44,9 @@ export async function judgeOutput(input: {
"User prompt:",
input.prompt,
"",
"Checklist:",
formatChecklist(input.checklist),
"",
"Initial state:",
formatJsonBlock(input.initial),
"",
@@ -126,6 +133,14 @@ function formatJsonBlock(value: unknown): string {
return JSON.stringify(value, null, 2);
}
function formatChecklist(checklist: string[] | undefined): string {
if (!checklist || checklist.length === 0) {
return "(none)";
}
return checklist.map((item) => `- ${item}`).join("\n");
}
function normalizeScore(value: number): number {
if (!Number.isFinite(value)) {
return 0;
+1
View File
@@ -160,6 +160,7 @@ async function runCaseAttempts<TInitial, TExpected, TActual>(input: {
const judge = await judgeOutput({
mode: input.modeRunner.mode,
prompt: input.evalCase.prompt,
checklist: input.evalCase.judgeChecklist,
initial,
expected,
actual: run.actual,
+7 -13
View File
@@ -2,25 +2,18 @@ export const EVAL_MODES = ["cli", "flow", "script", "app"] as const;
export type EvalMode = (typeof EVAL_MODES)[number];
export interface FlowModuleValidation {
name: string;
typeAnyOf?: string[];
inputRefsAny?: string[];
inputRefsAll?: string[];
inputRefsContainAny?: string[];
inputRefsContainAll?: string[];
codeContainsAny?: string[];
codeContainsAll?: string[];
codeRegexAll?: string[];
}
export interface FlowValidationSpec {
schemaRequiredPaths?: string[];
schemaAnyOf?: Array<{
requiredPaths: string[];
}>;
resolveResultsRefs?: boolean;
modules?: FlowModuleValidation[];
requireSpecialModules?: Array<"preprocessor_module" | "failure_module">;
requireSuspendSteps?: Array<{
id: string;
requiredEvents?: number;
resumeRequiredStringFieldAnyOf?: string[];
}>;
}
export interface EvalCase {
@@ -29,6 +22,7 @@ export interface EvalCase {
initialPath?: string;
expectedPath?: string;
validate?: FlowValidationSpec;
judgeChecklist?: string[];
}
export interface BenchmarkCheck {
+100 -314
View File
@@ -1,5 +1,5 @@
import ts from "typescript";
import type { BenchmarkCheck, FlowModuleValidation, FlowValidationSpec } from "./types";
import type { BenchmarkCheck, FlowValidationSpec } from "./types";
export interface ScriptState {
path: string;
@@ -81,11 +81,20 @@ export function validateScriptState(input: {
export function validateFlowState(input: {
actual: FlowState;
initial?: FlowState;
expected?: FlowState;
validate?: FlowValidationSpec;
}): BenchmarkCheck[] {
const actualModules = getFlowModules(input.actual);
const checks: BenchmarkCheck[] = [check("flow has modules", actualModules.length > 0)];
const placeholderModuleIds = getInlineScriptPlaceholderModuleIds(input.actual);
const checks: BenchmarkCheck[] = [
check("flow has modules", actualModules.length > 0),
check(
"flow has no inline placeholder code",
placeholderModuleIds.length === 0,
placeholderModuleIds.length > 0
? `placeholder content in: ${placeholderModuleIds.join(", ")}`
: undefined
),
];
if (input.initial) {
checks.push(
@@ -96,146 +105,6 @@ export function validateFlowState(input: {
);
}
if (!input.expected) {
if (input.validate) {
checks.push(...validateFlowRequirements(input.actual, input.validate));
}
return checks;
}
const expectedModules = getFlowModules(input.expected);
const actualTypes = new Set(actualModules.map((module) => getModuleType(module) ?? ""));
const expectedTypes = new Set(expectedModules.map((module) => getModuleType(module) ?? ""));
const missingTypes = [...expectedTypes].filter((type) => type && !actualTypes.has(type));
const actualTopLevelIds = getTopLevelFlowModuleIds(input.actual);
const expectedTopLevelIds = getTopLevelFlowModuleIds(input.expected);
const missingIds = expectedTopLevelIds.filter((id) => !actualTopLevelIds.includes(id));
const expectedSchemaRootType = getSchemaRootType(input.expected.schema);
const actualSchemaRootType = getSchemaRootType(input.actual.schema);
const actualModuleById = new Map(
actualModules
.map((module) => [typeof module.id === "string" ? module.id : "", module] as const)
.filter(([id]) => id.length > 0)
);
checks.push(
check(
"flow includes expected module types",
missingTypes.length === 0,
missingTypes.length > 0 ? `missing: ${missingTypes.join(", ")}` : undefined
)
);
if (expectedSchemaRootType) {
checks.push(
check(
"flow schema root type matches expected",
actualSchemaRootType === expectedSchemaRootType,
`expected ${expectedSchemaRootType}, got ${actualSchemaRootType ?? "(missing)"}`
)
);
}
if (expectedTopLevelIds.length > 0) {
checks.push(
check(
"flow includes expected top-level step ids",
missingIds.length === 0,
missingIds.length > 0 ? `missing: ${missingIds.join(", ")}` : undefined
)
);
}
for (const expectedModule of expectedModules) {
const expectedId = typeof expectedModule.id === "string" ? expectedModule.id : null;
if (!expectedId) {
continue;
}
const actualModule = actualModuleById.get(expectedId);
if (!actualModule) {
continue;
}
const expectedType = getModuleType(expectedModule);
if (expectedType) {
checks.push(
check(
`${expectedId} type matches expected`,
getModuleType(actualModule) === expectedType,
`expected ${expectedType}, got ${getModuleType(actualModule) ?? "(missing)"}`
)
);
}
const expectedPath = getModulePath(expectedModule);
if (expectedPath) {
checks.push(
check(
`${expectedId} path matches expected`,
getModulePath(actualModule) === expectedPath,
`expected ${expectedPath}, got ${getModulePath(actualModule) ?? "(missing)"}`
)
);
}
if (hasSuspendConfig(expectedModule)) {
checks.push(check(`${expectedId} includes suspend config`, hasSuspendConfig(actualModule)));
}
const expectedContinueOnError = getContinueOnError(expectedModule);
if (expectedContinueOnError !== undefined) {
checks.push(
check(
`${expectedId} continue_on_error matches expected`,
getContinueOnError(actualModule) === expectedContinueOnError
)
);
}
if (hasRetryConfig(expectedModule)) {
checks.push(check(`${expectedId} includes retry config`, hasRetryConfig(actualModule)));
}
const expectedParallel = getModuleParallel(expectedModule);
if (expectedParallel !== undefined) {
checks.push(
check(
`${expectedId} parallel matches expected`,
getModuleParallel(actualModule) === expectedParallel
)
);
}
}
for (const specialKey of ["preprocessor_module", "failure_module"] as const) {
const expectedSpecial = getSpecialFlowModule(input.expected, specialKey);
if (!expectedSpecial) {
continue;
}
const actualSpecial = getSpecialFlowModule(input.actual, specialKey);
checks.push(check(`${specialKey} exists`, Boolean(actualSpecial)));
if (!actualSpecial) {
continue;
}
const expectedType = getModuleType(expectedSpecial);
if (expectedType) {
checks.push(
check(
`${specialKey} type matches expected`,
getModuleType(actualSpecial) === expectedType,
`expected ${expectedType}, got ${getModuleType(actualSpecial) ?? "(missing)"}`
)
);
}
if (hasRetryConfig(expectedSpecial)) {
checks.push(check(`${specialKey} includes retry config`, hasRetryConfig(actualSpecial)));
}
}
if (input.validate) {
checks.push(...validateFlowRequirements(input.actual, input.validate));
}
@@ -413,24 +282,55 @@ function validateFlowRequirements(
);
}
for (const requirement of validate.modules ?? []) {
const matched = findMatchingFlowModule(flow, requirement);
for (const specialModule of validate.requireSpecialModules ?? []) {
checks.push(
check(
requirement.name,
Boolean(matched),
matched ? undefined : describeMissingModuleRequirement(requirement)
`${specialModule} exists`,
Boolean(getSpecialFlowModule(flow, specialModule))
)
);
}
return checks;
}
for (const suspendStep of validate.requireSuspendSteps ?? []) {
const module = findFlowModuleById(flow, suspendStep.id);
checks.push(check(`${suspendStep.id} step exists`, Boolean(module)));
if (!module) {
continue;
}
function getTopLevelFlowModuleIds(flow: FlowState): string[] {
return getFlowModules(flow)
.map((module) => module.id)
.filter((value): value is string => typeof value === "string");
checks.push(check(`${suspendStep.id} includes suspend config`, hasSuspendConfig(module)));
if (!hasSuspendConfig(module)) {
continue;
}
if (suspendStep.requiredEvents !== undefined) {
checks.push(
check(
`${suspendStep.id} requires ${suspendStep.requiredEvents} approval event${suspendStep.requiredEvents === 1 ? "" : "s"}`,
getSuspendRequiredEvents(module) === suspendStep.requiredEvents,
`expected ${suspendStep.requiredEvents}, got ${getSuspendRequiredEvents(module) ?? "(missing)"}`
)
);
}
if (
suspendStep.resumeRequiredStringFieldAnyOf &&
suspendStep.resumeRequiredStringFieldAnyOf.length > 0
) {
const requiredFields = getSuspendResumeRequiredStringFields(module);
checks.push(
check(
`${suspendStep.id} resume form requires one accepted comment field`,
suspendStep.resumeRequiredStringFieldAnyOf.some((field) =>
requiredFields.includes(field)
),
`required one of [${suspendStep.resumeRequiredStringFieldAnyOf.join(", ")}], got [${requiredFields.join(", ")}]`
)
);
}
}
return checks;
}
function hasSchemaPath(schema: Record<string, unknown> | undefined, dottedPath: string): boolean {
@@ -616,111 +516,6 @@ function validateNestedResultsRefPath(
}
}
function findMatchingFlowModule(
flow: FlowState,
requirement: FlowModuleValidation
): Record<string, unknown> | null {
for (const module of getAllFlowModules(flow)) {
if (matchesFlowModuleRequirement(module, requirement)) {
return module;
}
}
return null;
}
function matchesFlowModuleRequirement(
module: Record<string, unknown>,
requirement: FlowModuleValidation
): boolean {
if (requirement.typeAnyOf && requirement.typeAnyOf.length > 0) {
const actualType = getModuleType(module);
if (!actualType || !requirement.typeAnyOf.includes(actualType)) {
return false;
}
}
const inputRefs = getModuleInputRefs(module);
if (requirement.inputRefsAny && requirement.inputRefsAny.length > 0) {
if (!requirement.inputRefsAny.some((ref) => inputRefs.includes(ref))) {
return false;
}
}
if (requirement.inputRefsAll && requirement.inputRefsAll.length > 0) {
if (!requirement.inputRefsAll.every((ref) => inputRefs.includes(ref))) {
return false;
}
}
if (requirement.inputRefsContainAny && requirement.inputRefsContainAny.length > 0) {
if (!requirement.inputRefsContainAny.some((snippet) => inputRefs.some((ref) => ref.includes(snippet)))) {
return false;
}
}
if (requirement.inputRefsContainAll && requirement.inputRefsContainAll.length > 0) {
if (!requirement.inputRefsContainAll.every((snippet) => inputRefs.some((ref) => ref.includes(snippet)))) {
return false;
}
}
const code = getModuleCode(module);
if (requirement.codeContainsAny && requirement.codeContainsAny.length > 0) {
if (!code || !requirement.codeContainsAny.some((snippet) => code.includes(snippet))) {
return false;
}
}
if (requirement.codeContainsAll && requirement.codeContainsAll.length > 0) {
if (!code || !requirement.codeContainsAll.every((snippet) => code.includes(snippet))) {
return false;
}
}
if (requirement.codeRegexAll && requirement.codeRegexAll.length > 0) {
if (!code) {
return false;
}
for (const pattern of requirement.codeRegexAll) {
const regex = new RegExp(pattern, "m");
if (!regex.test(code)) {
return false;
}
}
}
return true;
}
function describeMissingModuleRequirement(requirement: FlowModuleValidation): string {
const parts: string[] = [];
if (requirement.typeAnyOf?.length) {
parts.push(`type in [${requirement.typeAnyOf.join(", ")}]`);
}
if (requirement.inputRefsAny?.length) {
parts.push(`any input refs [${requirement.inputRefsAny.join(", ")}]`);
}
if (requirement.inputRefsAll?.length) {
parts.push(`all input refs [${requirement.inputRefsAll.join(", ")}]`);
}
if (requirement.inputRefsContainAny?.length) {
parts.push(`any input refs containing [${requirement.inputRefsContainAny.join(", ")}]`);
}
if (requirement.inputRefsContainAll?.length) {
parts.push(`all input refs containing [${requirement.inputRefsContainAll.join(", ")}]`);
}
if (requirement.codeContainsAny?.length) {
parts.push(`code contains any [${requirement.codeContainsAny.join(", ")}]`);
}
if (requirement.codeContainsAll?.length) {
parts.push(`code contains all [${requirement.codeContainsAll.join(", ")}]`);
}
if (requirement.codeRegexAll?.length) {
parts.push(`code matches [${requirement.codeRegexAll.join(", ")}]`);
}
return parts.join("; ");
}
function getAllFlowModules(flow: FlowState): Array<Record<string, unknown>> {
const modules: Array<Record<string, unknown>> = [];
const specialModules = ["preprocessor_module", "failure_module"] as const;
@@ -774,6 +569,30 @@ function collectNestedModules(module: Record<string, unknown>): Array<Record<str
return nested;
}
function findFlowModuleById(flow: FlowState, id: string): Record<string, unknown> | null {
for (const module of getAllFlowModules(flow)) {
if (module.id === id) {
return module;
}
}
return null;
}
function getInlineScriptPlaceholderModuleIds(flow: FlowState): string[] {
return getAllFlowModules(flow).flatMap((module) => {
const code = getModuleCode(module)?.trim();
if (!code || !/^inline_script\.[A-Za-z0-9_-]+$/.test(code)) {
return [];
}
if (typeof module.id === "string" && module.id.length > 0) {
return [module.id];
}
return ["(unnamed)"];
});
}
function getImmediateNestedModuleIds(module: Record<string, unknown>): string[] {
const ids: string[] = [];
const value = isObjectRecord(module.value) ? module.value : null;
@@ -803,22 +622,6 @@ function getImmediateNestedModuleIds(module: Record<string, unknown>): string[]
return ids;
}
function getModuleInputRefs(module: Record<string, unknown>): string[] {
const value = isObjectRecord(module.value) ? module.value : null;
const inputTransforms = isObjectRecord(value?.input_transforms) ? value?.input_transforms : null;
if (!inputTransforms) {
return [];
}
return Object.values(inputTransforms)
.flatMap((entry) => {
if (!isObjectRecord(entry) || typeof entry.expr !== "string") {
return [];
}
return [entry.expr.trim()];
});
}
function getModuleCode(module: Record<string, unknown>): string | null {
const value = isObjectRecord(module.value) ? module.value : null;
return typeof value?.content === "string" ? value.content : null;
@@ -853,52 +656,35 @@ function getModuleType(module: Record<string, unknown>): string | null {
: null;
}
function getModulePath(module: Record<string, unknown>): string | null {
const value = module.value;
if (!value || typeof value !== "object") {
return null;
}
return typeof (value as Record<string, unknown>).path === "string"
? ((value as Record<string, string>).path)
: null;
}
function hasSuspendConfig(module: Record<string, unknown>): boolean {
return typeof module.suspend === "object" && module.suspend !== null;
}
function getContinueOnError(module: Record<string, unknown>): boolean | undefined {
return typeof module.continue_on_error === "boolean"
? (module.continue_on_error as boolean)
: undefined;
function getSuspendRequiredEvents(module: Record<string, unknown>): number | null {
const suspend = isObjectRecord(module.suspend) ? module.suspend : null;
return typeof suspend?.required_events === "number" ? suspend.required_events : null;
}
function hasRetryConfig(module: Record<string, unknown>): boolean {
return typeof module.retry === "object" && module.retry !== null;
}
function getSuspendResumeRequiredStringFields(module: Record<string, unknown>): string[] {
const suspend = isObjectRecord(module.suspend) ? module.suspend : null;
const resumeForm = isObjectRecord(suspend?.resume_form) ? suspend.resume_form : null;
const schema = isObjectRecord(resumeForm?.schema) ? resumeForm.schema : null;
const required = Array.isArray(schema?.required) ? schema.required : [];
const properties = isObjectRecord(schema?.properties) ? schema.properties : null;
if (!properties) {
return [];
}
function getModuleParallel(module: Record<string, unknown>): boolean | undefined {
const value = module.value;
if (!value || typeof value !== "object") {
return undefined;
}
return typeof (value as Record<string, unknown>).parallel === "boolean"
? ((value as Record<string, boolean>).parallel)
: undefined;
}
function getSchemaRootType(schema: Record<string, unknown> | undefined): string | null {
if (!schema || typeof schema !== "object") {
return null;
}
const properties = (schema.properties ?? {}) as Record<string, unknown>;
const root = properties["root"];
if (!root || typeof root !== "object") {
return null;
}
return typeof (root as { type?: unknown }).type === "string"
? ((root as { type: string }).type)
: null;
return required.flatMap((field) => {
if (typeof field !== "string") {
return [];
}
const property = properties[field];
if (!isObjectRecord(property) || property.type !== "string") {
return [];
}
return [field];
});
}
function appStatesEqual(left: AppFilesState, right: AppFilesState): boolean {
+1 -2
View File
@@ -55,11 +55,10 @@ export function createFlowModeRunner(
skillsInvoked: [],
};
},
validate({ evalCase, actual, initial, expected }) {
validate({ evalCase, actual, initial }) {
return validateFlowState({
actual,
initial: initial?.flow,
expected,
validate: evalCase.validate,
});
},