import { deriveStaticPipelineEntryInputFields, deriveStaticPipelineFileInputFields, getTopLevelPipelineSubsteps, type PlayStaticPipeline, type PlayStaticSubstep, } from './static-pipeline'; export type PlayInputKind = 'csv' | 'typed' | 'none'; export type PlayEntryInputContract = { callInputFields: string[]; csvHeaderFields: string[]; rowKeyFields: string[]; kind: PlayInputKind; kindLabel: string; summary: string; }; export type PlayEntryContract = PlayEntryInputContract & { outputFields: string[]; outputDatasetFields?: ReadonlySet; }; export function buildPlayInputContract(input: { pipeline: PlayStaticPipeline; displaySubsteps?: readonly PlayStaticSubstep[]; inputFields?: readonly string[]; }): PlayEntryInputContract { const displaySubsteps = input.displaySubsteps ?? getTopLevelPipelineSubsteps(input.pipeline); const datasetInputs = collectDatasetInputDetails(displaySubsteps); const fileInputFields = deriveStaticPipelineFileInputFields(input.pipeline); const callInputFields = dedupeFieldNames( input.inputFields && input.inputFields.length > 0 ? input.inputFields : deriveStaticPipelineEntryInputFields(input.pipeline), ); const rootSheetInputs = input.pipeline.sheetContract?.columns ?.filter((column) => column.source === 'input') .map((column) => column.id) ?? []; const hasCsvInput = fileInputFields.length > 0 || callInputFields.some((field) => /^csv$/i.test(field.trim())); const csvHeaderFields = hasCsvInput ? dedupeFieldNames([ ...(input.pipeline.inputFields ?? []), ...rootSheetInputs, ...datasetInputs.headerFields, ]) : dedupeFieldNames([...rootSheetInputs, ...datasetInputs.headerFields]); const rowKeyFields = dedupeFieldNames([ ...(input.pipeline.rowKeyFields ?? []), ...datasetInputs.rowKeyFields, ]); const kind: PlayInputKind = hasCsvInput ? 'csv' : callInputFields.length > 0 ? 'typed' : 'none'; const summary = csvHeaderFields.length > 0 ? `${csvHeaderFields.length.toLocaleString()} column${csvHeaderFields.length === 1 ? '' : 's'}` : hasCsvInput && callInputFields.length > 0 ? 'file input' : callInputFields.length > 0 ? `${callInputFields.length.toLocaleString()} input${callInputFields.length === 1 ? '' : 's'}` : 'No inputs'; return { callInputFields, csvHeaderFields, rowKeyFields, kind, kindLabel: hasCsvInput ? 'CSV file' : 'Input', summary, }; } export function buildPlayEntryContract(input: { pipeline: PlayStaticPipeline; displaySubsteps?: readonly PlayStaticSubstep[]; inputFields?: readonly string[]; outputFields: readonly string[]; outputDatasetFields?: ReadonlySet; }): PlayEntryContract { return { ...buildPlayInputContract(input), outputFields: dedupeFieldNames(input.outputFields), outputDatasetFields: input.outputDatasetFields, }; } function collectDatasetInputDetails(substeps: readonly PlayStaticSubstep[]): { headerFields: string[]; rowKeyFields: string[]; } { const headerFields: string[] = []; const rowKeyFields: string[] = []; const visit = (steps: readonly PlayStaticSubstep[]) => { for (const substep of steps) { if (substep.type === 'dataset') { headerFields.push(...(substep.inputFields ?? [])); rowKeyFields.push(...(substep.rowKeyFields ?? [])); headerFields.push( ...(substep.sheetContract?.columns ?.filter((column) => column.source === 'input') .map((column) => column.id) ?? []), ); if (substep.steps?.length) { visit(substep.steps); } continue; } if ( (substep.type === 'step_suite' || substep.type === 'control_flow') && substep.steps?.length ) { visit(substep.steps); } } }; visit(substeps); return { headerFields: dedupeFieldNames(headerFields), rowKeyFields: dedupeFieldNames(rowKeyFields), }; } function dedupeFieldNames( fields: readonly (string | null | undefined)[], ): string[] { const seen = new Set(); const out: string[] = []; for (const field of fields) { const trimmed = typeof field === 'string' ? field.trim() : ''; if (!trimmed || seen.has(trimmed)) continue; seen.add(trimmed); out.push(trimmed); } return out; }