// SPDX-FileCopyrightText: 2025-2026 Lucio Lelii - ISTI-CNR // SPDX-License-Identifier: AGPL-3.0-or-later // Attribution term under AGPL-3.0 section 7(b): see LICENSE-ADDENDUM. import { HttpClient, HttpParams } from '@angular/common/http'; import { inject } from '@angular/core'; import { environment } from '@environment'; import { FlowBlockConnection, FlowData, FlowNodeDependency, LLMDescriptor } from '@models/flow'; import { BiasDownstreamImpactEntry, BiasImpactExperimentRequest, BiasImpactJob, BiasImpactJobStatus, BiasImpactReport, BiasImpactReportKind, BiasItemImpact, BiasIterationImpact, BiasJudgeAttribution, BiasJudgeImpactLevel, BiasJudgeSummary, BiasJudgeVerdict, BiasMockedSideEffect, BiasNumericDelta, BiasOutcomeChangeEntry, BiasRerunRequest, BiasRoutingChangeEntry, BiasSimulationContext, BiasValueImpact } from '@models/bias-impact'; import { ExecutionEventLogEntry, TaskExecution, TaskExecutionGroup, normalizeExecutionOutcomes } from '@models/task-execution'; import { map, Observable } from 'rxjs'; import { TaskExecutionsCallServiceBase } from './task-executions-call.base'; export class TaskExecutionsCallService extends TaskExecutionsCallServiceBase { private readonly http = inject(HttpClient); override retrieveAllTaskExecutions(): Observable { return this.http.get(`${environment.apiUrl}/executions`).pipe( map((raw) => Array.isArray(raw) ? raw.map((item) => this.mapExecution(item)) : []) ); } override retrieveTaskExecutionGroups(): Observable { return this.http.get(`${environment.apiUrl}/executions/groups`).pipe( map((raw) => Array.isArray(raw) ? raw.map((item) => this.mapExecutionGroup(item)) : []) ); } override retrieveTaskExecution(executionId: string): Observable { return this.http .get(`${environment.apiUrl}/executions/${encodeURIComponent(executionId)}`) .pipe(map((raw) => this.mapExecution(raw))); } override retrieveStepIterations(executionId: string, stepId: string): Observable { const url = `${environment.apiUrl}/executions/${encodeURIComponent(executionId)}/node/${encodeURIComponent(stepId)}/iterations`; return this.http.get(url).pipe( map((raw) => Array.isArray(raw) ? raw.map((item) => this.mapExecution(item)) : []) ); } override retrieveExecutionEvents(executionId: string): Observable { return this.http.get(`${environment.apiUrl}/executions/${encodeURIComponent(executionId)}/events`); } override createTaskExecution(flowId: string): Observable { return this.http.post(`${environment.apiUrl}/executions`, flowId).pipe( map((raw) => this.mapExecution(raw)) ); } override rerunTaskExecution(executionId: string): Observable { return this.http.post(`${environment.apiUrl}/executions/${encodeURIComponent(executionId)}/rerun`, null).pipe( map((raw) => this.mapExecution(raw)) ); } override runBiasImpactExperiment( executionId: string, stepId: string, request: BiasImpactExperimentRequest ): Observable { const url = `${environment.apiUrl}/executions/${encodeURIComponent(executionId)}/steps/${encodeURIComponent(stepId)}/bias-impact`; return this.http.post(url, request).pipe(map((raw) => this.biasImpactJobFromApi(raw))); } override judgeBiasImpactReport(reportId: string, judge: LLMDescriptor, credentialId?: string): Observable { const url = `${environment.apiUrl}/executions/bias-impact-reports/${encodeURIComponent(reportId)}/judge`; return this.http.post(url, { judge, credentialId }).pipe(map((raw) => this.biasImpactJobFromApi(raw))); } override getBiasImpactJob(jobId: string): Observable { return this.http .get(`${environment.apiUrl}/executions/bias-impact-jobs/${encodeURIComponent(jobId)}`) .pipe(map((raw) => this.biasImpactJobFromApi(raw))); } override createBiasedRerun(executionId: string, request: BiasRerunRequest): Observable { return this.http .post(`${environment.apiUrl}/executions/${encodeURIComponent(executionId)}/bias-rerun`, request) .pipe(map((raw) => this.mapExecution(raw))); } override compareBiasExecutions( baselineExecutionId: string, biasedExecutionId: string, includeRawOutputs: boolean ): Observable { const url = `${environment.apiUrl}/executions/${encodeURIComponent(baselineExecutionId)}/bias-compare/${encodeURIComponent(biasedExecutionId)}`; return this.http.post(url, null, { params: new HttpParams().set('includeRawOutputs', String(includeRawOutputs)) }).pipe(map((raw) => this.biasImpactReportFromApi(raw))); } override listBiasImpactReports(executionId: string): Observable { return this.http .get(`${environment.apiUrl}/executions/${encodeURIComponent(executionId)}/bias-impact-reports`) .pipe(map((raw) => Array.isArray(raw) ? raw.map((item) => this.biasImpactReportFromApi(item)) : [])); } override getBiasImpactReport(reportId: string): Observable { return this.http .get(`${environment.apiUrl}/executions/bias-impact-reports/${encodeURIComponent(reportId)}`) .pipe(map((raw) => this.biasImpactReportFromApi(raw))); } override deleteTaskExecution(executionId: string): Observable { return this.http.delete(`${environment.apiUrl}/executions/${encodeURIComponent(executionId)}`); } override startTaskExecution(executionId: string): Observable { return this.http.put(`${environment.apiUrl}/executions/${encodeURIComponent(executionId)}/start`, null).pipe( map((raw) => this.mapExecution(raw)) ); } override simulateTaskExecution(executionId: string, simulator: LLMDescriptor, credentialId?: string): Observable { return this.http.put( `${environment.apiUrl}/executions/${encodeURIComponent(executionId)}/simulate`, { simulator, credentialId } ).pipe( map((raw) => this.mapExecution(raw)) ); } override cancelTaskExecution(executionId: string): Observable { return this.http.put(`${environment.apiUrl}/executions/${encodeURIComponent(executionId)}/cancel`, null).pipe( map((raw) => this.mapExecution(raw)) ); } override resumeTaskExecution(executionId: string): Observable { return this.http.put(`${environment.apiUrl}/executions/${encodeURIComponent(executionId)}/resume`, null).pipe( map((raw) => this.mapExecution(raw)) ); } override prepareStringInput( executionId: string, nodeId: string, inputName: string, value: string ): Observable { const url = `${environment.apiUrl}/executions/${encodeURIComponent(executionId)}/node/${encodeURIComponent(nodeId)}/input/${encodeURIComponent(inputName)}/text`; return this.http.put(url, value, { headers: { 'Content-Type': 'text/plain' } }).pipe(map((raw) => this.mapExecution(raw))); } override prepareStringArrayInput( executionId: string, nodeId: string, inputName: string, values: string[] ): Observable { const url = `${environment.apiUrl}/executions/${encodeURIComponent(executionId)}/node/${encodeURIComponent(nodeId)}/input/${encodeURIComponent(inputName)}/texts`; return this.http.put(url, values).pipe(map((raw) => this.mapExecution(raw))); } override prepareFileInput( executionId: string, nodeId: string, inputName: string, file: File ): Observable { const url = `${environment.apiUrl}/executions/${encodeURIComponent(executionId)}/node/${encodeURIComponent(nodeId)}/input/${encodeURIComponent(inputName)}/file`; const formData = new FormData(); formData.append('file', file); return this.http.put(url, formData).pipe(map((raw) => this.mapExecution(raw))); } override prepareFileArrayInput( executionId: string, nodeId: string, inputName: string, files: File[] ): Observable { const url = `${environment.apiUrl}/executions/${encodeURIComponent(executionId)}/node/${encodeURIComponent(nodeId)}/input/${encodeURIComponent(inputName)}/files`; const formData = new FormData(); for (const file of files) { formData.append('files', file); } return this.http.put(url, formData).pipe(map((raw) => this.mapExecution(raw))); } override prepareGlobalStringInput( executionId: string, inputName: string, value: string ): Observable { const url = `${environment.apiUrl}/executions/${encodeURIComponent(executionId)}/globals/${encodeURIComponent(inputName)}`; return this.http.put(url, JSON.stringify(value), { headers: { 'Content-Type': 'application/json' } }).pipe(map((raw) => this.mapExecution(raw))); } override prepareGlobalStringArrayInput( executionId: string, inputName: string, values: string[] ): Observable { const url = `${environment.apiUrl}/executions/${encodeURIComponent(executionId)}/globals`; return this.http.put(url, { [inputName]: values }).pipe(map((raw) => this.mapExecution(raw))); } override prepareGlobalInputs( executionId: string, values: Record ): Observable { const url = `${environment.apiUrl}/executions/${encodeURIComponent(executionId)}/globals`; return this.http.put(url, values).pipe(map((raw) => this.mapExecution(raw))); } override prepareGlobalFileInput( executionId: string, inputName: string, file: File ): Observable { // The bare /globals/{key} endpoint takes JSON, so an upload sent there came back as an // unsupported content type; the upload has its own route, as it does for a node input. const url = `${environment.apiUrl}/executions/${encodeURIComponent(executionId)}/globals/${encodeURIComponent(inputName)}/file`; const formData = new FormData(); formData.append('file', file); return this.http.put(url, formData).pipe(map((raw) => this.mapExecution(raw))); } override prepareGlobalFileArrayInput( executionId: string, inputName: string, files: File[] ): Observable { const url = `${environment.apiUrl}/executions/${encodeURIComponent(executionId)}/globals/${encodeURIComponent(inputName)}/files`; const formData = new FormData(); // Every file goes under "files": the part name is what the endpoint binds on, and naming the // parts after the input meant nothing bound at all. for (const file of files) { formData.append('files', file); } return this.http.put(url, formData).pipe(map((raw) => this.mapExecution(raw))); } override submitInteractionText( executionId: string, nodeId: string, fieldName: string, value: string ): Observable { const url = `${environment.apiUrl}/executions/${encodeURIComponent(executionId)}/node/${encodeURIComponent(nodeId)}/interaction/${encodeURIComponent(fieldName)}/text`; return this.http.put(url, value, { headers: { 'Content-Type': 'text/plain' } }).pipe(map((raw) => this.mapExecution(raw))); } override provideAuthorization( executionId: string, key: string, value: string ): Observable { const url = `${environment.apiUrl}/executions/${encodeURIComponent(executionId)}/authorizations`; return this.http.put(url, { key, value }).pipe(map((raw) => this.mapExecution(raw))); } private mapExecution(raw: unknown): TaskExecution { const execution = (raw ?? {}) as TaskExecution & Record; const context = (execution.context ?? {}) as Record; const globalInputs = this.normalizeGlobalInputValues( context['globalInputs'] ?? execution['globalInputs'] ); const globalInputDescriptors = this.normalizeGlobalInputDescriptors( context['globalInputDescriptors'] ?? execution['globalInputDescriptors'] ); const flowSnapshot = this.normalizeFlowSnapshot( execution['flowSnapshot'] ?? execution['flowData'] ?? context['flowSnapshot'] ?? context['flowData'] ?? (execution['flow'] && typeof execution['flow'] === 'object' ? execution['flow'] : null) ); const rawStepConnections = execution['stepConnections'] ?? execution['connections'] ?? context['stepConnections'] ?? context['connections'] ?? flowSnapshot?.connections; const stepConnections = rawStepConnections === undefined ? undefined : this.normalizeStepConnections(rawStepConnections); const rawStepDependencies = execution['stepDependencies'] ?? execution['dependencies'] ?? context['stepDependencies'] ?? context['dependencies'] ?? flowSnapshot?.dependencies; const stepDependencies = rawStepDependencies === undefined ? undefined : this.normalizeStepDependencies(rawStepDependencies); return { ...execution, flowSnapshot, stepConnections, stepDependencies, context: { ...(context as TaskExecution['context']), globalInputs, globalInputDescriptors, outcomes: normalizeExecutionOutcomes(context['outcomes']) } as TaskExecution['context'] }; } private normalizeFlowSnapshot(raw: unknown): FlowData | undefined { if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return undefined; const root = raw as Record; const nested = root['flow'] && typeof root['flow'] === 'object' && !Array.isArray(root['flow']) ? root['flow'] as Record : root; if (!Array.isArray(nested['blocks']) && !Array.isArray(nested['containers'])) return undefined; const blocks = Array.isArray(nested['blocks']) ? nested['blocks'].filter((node): node is FlowData['blocks'][number] => !!node && typeof node === 'object') .map((node) => ({ ...node, nodeFamily: 'block' as const })) : []; const containers = Array.isArray(nested['containers']) ? nested['containers'].filter((node): node is FlowData['containers'][number] => !!node && typeof node === 'object') .map((node) => ({ ...node, nodeFamily: 'container' as const })) : []; return { blocks, containers, connections: this.normalizeStepConnections(nested['connections']), dependencies: this.normalizeStepDependencies(nested['dependencies']), globalInputs: Array.isArray(nested['globalInputs']) ? nested['globalInputs'] as FlowData['globalInputs'] : [], lanes: Array.isArray(nested['lanes']) ? nested['lanes'] as FlowData['lanes'] : [] }; } private normalizeStepConnections(raw: unknown): FlowBlockConnection[] { if (!Array.isArray(raw)) return []; return raw.flatMap((item, index) => { if (!item || typeof item !== 'object' || Array.isArray(item)) return []; const value = item as Record; const sourceId = value['sourceId'] ?? value['sourceNodeId'] ?? value['sourceBlockId']; const sourceName = value['sourceName'] ?? value['sourceOutput'] ?? value['outputName']; const targetId = value['targetId'] ?? value['targetNodeId'] ?? value['targetBlockId']; const targetName = value['targetName'] ?? value['targetInput'] ?? value['inputName']; if ([sourceId, sourceName, targetId, targetName].some((part) => typeof part !== 'string' || !part)) return []; return [{ id: String(value['id'] ?? `${sourceId}:${sourceName}->${targetId}:${targetName}:${index}`), sourceId: String(sourceId), sourceName: String(sourceName), targetId: String(targetId), targetName: String(targetName) }]; }); } private normalizeStepDependencies(raw: unknown): FlowNodeDependency[] { if (!Array.isArray(raw)) return []; return raw.flatMap((item) => { if (!item || typeof item !== 'object' || Array.isArray(item)) return []; const value = item as Record; const sourceId = value['sourceId'] ?? value['sourceNodeId']; const targetId = value['targetId'] ?? value['targetNodeId']; if (typeof sourceId !== 'string' || !sourceId || typeof targetId !== 'string' || !targetId) return []; return [{ sourceId, targetId }]; }); } private biasImpactJobFromApi(raw: unknown): BiasImpactJob { const value = this.toRecord(raw); const status = this.toBiasImpactJobStatus(value['status']); const rawReport = value['report']; return { id: String(value['id'] ?? ''), kind: value['kind'] === 'REPORT_JUDGE' ? 'REPORT_JUDGE' : 'ISOLATED_STEP', status, executionId: String(value['executionId'] ?? ''), stepId: String(value['stepId'] ?? ''), createdAt: String(value['createdAt'] ?? ''), startedAt: this.toNullableString(value['startedAt']), completedAt: this.toNullableString(value['completedAt']), reportId: this.toNullableString(value['reportId']), report: rawReport && typeof rawReport === 'object' ? this.biasImpactReportFromApi(rawReport) : null, errorCode: this.toNullableString(value['errorCode']), errorMessage: this.toNullableString(value['errorMessage']), terminal: value['terminal'] === true || status === 'COMPLETED' || status === 'FAILED' }; } private biasImpactReportFromApi(raw: unknown): BiasImpactReport { const value = this.toRecord(raw); const immediate = this.toRecord(value['immediateImpact']); return { id: String(value['id'] ?? ''), experimentId: String(value['experimentId'] ?? ''), kind: this.toBiasImpactReportKind(value['kind']), baselineExecutionId: String(value['baselineExecutionId'] ?? ''), biasedExecutionId: this.toNullableString(value['biasedExecutionId']), nodeId: this.toNullableString(value['nodeId']), annotationIds: this.toStringArray(value['annotationIds']), repetitions: this.toNumber(value['repetitions'], 0), createdAt: String(value['createdAt'] ?? ''), rawOutputsIncluded: value['rawOutputsIncluded'] === true, immediateImpact: { outputChanged: immediate['outputChanged'] === true, maximumTextDifference: this.toNumber(immediate['maximumTextDifference'], 0), changeRate: this.toNumber(immediate['changeRate'], 0), baselineOutput: immediate['baselineOutput'] ?? {}, biasedOutputs: Array.isArray(immediate['biasedOutputs']) ? immediate['biasedOutputs'] : [], values: this.toValueImpacts(immediate['values']), iterations: this.toIterationImpacts(immediate['iterations']) }, downstreamImpact: this.toDownstreamImpact(value['downstreamImpact']), routingChanges: this.toRoutingChanges(value['routingChanges']), outcomeChanges: this.toOutcomeChanges(value['outcomeChanges']), mockedSideEffects: this.toMockedSideEffects(value['mockedSideEffects']), summary: String(value['summary'] ?? ''), warnings: this.toStringArray(value['warnings']), schemaVersion: this.toNumber(value['schemaVersion'], 1), judgements: this.toJudgements(value), simulation: this.toSimulationContext(value['simulation']), ...(value['interventionDirection'] === 'BIAS' || value['interventionDirection'] === 'MITIGATION' || value['interventionDirection'] === 'BOTH' ? { interventionDirection: value['interventionDirection'] } : {}) }; } private toDownstreamImpact(raw: unknown): BiasDownstreamImpactEntry[] { if (!Array.isArray(raw)) return []; return raw.map((item) => { const value = this.toRecord(item); return { nodeId: String(value['nodeId'] ?? ''), nodeName: String(value['nodeName'] ?? ''), baselineStatus: String(value['baselineStatus'] ?? ''), biasedStatus: String(value['biasedStatus'] ?? ''), changed: value['changed'] === true, baselineOutputs: value['baselineOutputs'] ?? {}, biasedOutputs: value['biasedOutputs'] ?? {}, values: this.toValueImpacts(value['values']), iterations: this.toIterationImpacts(value['iterations']) }; }); } private toValueImpacts(raw: unknown): BiasValueImpact[] { if (!Array.isArray(raw)) return []; return raw.map((item) => { const value = this.toRecord(item); return { nodeId: String(value['nodeId'] ?? ''), nodeName: String(value['nodeName'] ?? ''), field: String(value['field'] ?? ''), changed: value['changed'] === true, textDifference: this.toNumber(value['textDifference'], 0), itemsCompared: this.toNumber(value['itemsCompared'], 0), itemsChanged: this.toNumber(value['itemsChanged'], 0), meanItemTextDifference: this.toNumber(value['meanItemTextDifference'], 0), maximumItemTextDifference: this.toNumber(value['maximumItemTextDifference'], 0), numericDeltas: this.toNumericDeltas(value['numericDeltas']), items: this.toItemImpacts(value['items']), baselineText: this.toNullableString(value['baselineText']), biasedText: this.toNullableString(value['biasedText']), judgeVerdict: this.toJudgeVerdict(value['judgeVerdict']) }; }); } private toItemImpacts(raw: unknown): BiasItemImpact[] { if (!Array.isArray(raw)) return []; return raw.map((item) => { const value = this.toRecord(item); return { index: this.toNumber(value['index'], 0), changed: value['changed'] === true, textDifference: this.toNumber(value['textDifference'], 0), numericDeltas: this.toNumericDeltas(value['numericDeltas']), baselineText: this.toNullableString(value['baselineText']), biasedText: this.toNullableString(value['biasedText']), judgeVerdict: this.toJudgeVerdict(value['judgeVerdict']) }; }); } private toIterationImpacts(raw: unknown): BiasIterationImpact[] { if (!Array.isArray(raw)) return []; return raw.map((item) => { const value = this.toRecord(item); return { containerNodeId: String(value['containerNodeId'] ?? ''), containerNodeName: String(value['containerNodeName'] ?? ''), index: this.toNumber(value['index'], 0), baselineExecutionId: this.toNullableString(value['baselineExecutionId']), biasedExecutionId: this.toNullableString(value['biasedExecutionId']), baselineStatus: String(value['baselineStatus'] ?? ''), biasedStatus: String(value['biasedStatus'] ?? ''), changed: value['changed'] === true, values: this.toValueImpacts(value['values']) }; }); } private toNumericDeltas(raw: unknown): BiasNumericDelta[] { if (!Array.isArray(raw)) return []; return raw.map((item) => { const value = this.toRecord(item); return { label: String(value['label'] ?? ''), baseline: this.toNumber(value['baseline'], 0), biased: this.toNumber(value['biased'], 0), delta: this.toNumber(value['delta'], 0) }; }); } /** * The assessments, newest first. * *

A report written when there was room for only one carries a `judge` object instead; read as * a history of one so an older report still shows what a model said about it. */ private toJudgements(value: Record): BiasJudgeSummary[] { const history = Array.isArray(value['judgements']) ? value['judgements'].map((item) => this.toJudgeSummary(item)) : []; const present = history.filter((summary): summary is BiasJudgeSummary => summary !== null); if (present.length) return present; const legacy = this.toJudgeSummary(value['judge']); return legacy ? [legacy] : []; } private toJudgeSummary(raw: unknown): BiasJudgeSummary | null { if (!raw || typeof raw !== 'object') return null; const value = this.toRecord(raw); return { judge: this.toDescriptor(value['judge']) ?? { provider: '', model: '' }, judgedAt: String(value['judgedAt'] ?? ''), impact: this.toJudgeImpactLevel(value['impact']), attribution: this.toJudgeAttribution(value['attribution']), narrative: this.toNullableString(value['narrative']), judgedPairs: this.toNumber(value['judgedPairs'], 0), skippedPairs: this.toNumber(value['skippedPairs'], 0), errors: this.toStringArray(value['errors']) }; } private toSimulationContext(raw: unknown): BiasSimulationContext | null { if (!raw || typeof raw !== 'object') return null; const value = this.toRecord(raw); return { baselineSimulated: value['baselineSimulated'] === true, baselineSimulator: this.toDescriptor(value['baselineSimulator']), biasedSimulated: value['biasedSimulated'] === true, biasedSimulator: this.toDescriptor(value['biasedSimulator']), comparable: value['comparable'] === true }; } private toDescriptor(raw: unknown): LLMDescriptor | null { if (!raw || typeof raw !== 'object') return null; const value = this.toRecord(raw); return { provider: String(value['provider'] ?? ''), model: String(value['model'] ?? ''), ...(value['parameters'] && typeof value['parameters'] === 'object' ? { parameters: value['parameters'] as LLMDescriptor['parameters'] } : {}) }; } private toJudgeVerdict(raw: unknown): BiasJudgeVerdict | null { if (!raw || typeof raw !== 'object') return null; const value = this.toRecord(raw); return { impact: this.toJudgeImpactLevel(value['impact']), attribution: this.toJudgeAttribution(value['attribution']), confidence: typeof value['confidence'] === 'number' ? value['confidence'] : null, changedAspects: this.toStringArray(value['changedAspects']), rationale: this.toNullableString(value['rationale']), error: this.toNullableString(value['error']) }; } private toJudgeImpactLevel(value: unknown): BiasJudgeImpactLevel | null { return value === 'NONE' || value === 'COSMETIC' || value === 'SUBSTANTIVE' || value === 'DECISIVE' ? value : null; } private toJudgeAttribution(value: unknown): BiasJudgeAttribution | null { return value === 'INJECTION' || value === 'NON_DETERMINISM' || value === 'UNCLEAR' ? value : null; } private toRoutingChanges(raw: unknown): BiasRoutingChangeEntry[] { if (!Array.isArray(raw)) return []; return raw.map((item) => { const value = this.toRecord(item); return { nodeId: String(value['nodeId'] ?? ''), baselineBranch: String(value['baselineBranch'] ?? ''), biasedBranch: String(value['biasedBranch'] ?? '') }; }); } private toOutcomeChanges(raw: unknown): BiasOutcomeChangeEntry[] { if (!Array.isArray(raw)) return []; return raw.map((item) => { const value = this.toRecord(item); return { baselineOutcomeCodes: this.toStringArray(value['baselineOutcomeCodes']), biasedOutcomeCodes: this.toStringArray(value['biasedOutcomeCodes']) }; }); } private toMockedSideEffects(raw: unknown): BiasMockedSideEffect[] { if (!Array.isArray(raw)) return []; return raw.map((item) => { const value = this.toRecord(item); const kind = String(value['kind'] ?? 'EXTERNAL'); return { nodeId: String(value['nodeId'] ?? ''), nodeName: String(value['nodeName'] ?? ''), kind: kind === 'HTTP' || kind === 'MCP_AGENT' || kind === 'MCP_AGENT_CHAT' ? kind : 'EXTERNAL' }; }); } private toBiasImpactJobStatus(value: unknown): BiasImpactJobStatus { return value === 'RUNNING' || value === 'COMPLETED' || value === 'FAILED' ? value : 'QUEUED'; } private toBiasImpactReportKind(value: unknown): BiasImpactReportKind { return value === 'FULL_FLOW' ? value : 'ISOLATED_STEP'; } private toStringArray(value: unknown): string[] { return Array.isArray(value) ? value.map(String) : []; } private toRecord(value: unknown): Record { return value && typeof value === 'object' && !Array.isArray(value) ? value as Record : {}; } private mapExecutionGroup(raw: unknown): TaskExecutionGroup { const group = (raw ?? {}) as Partial & Record; const executions = Array.isArray(group['executions']) ? group['executions'].map((item) => this.mapExecution(item)) : []; const latestExecution = executions.find((execution) => execution.id === group['latestExecutionId']) ?? executions[executions.length - 1] ?? null; const firstExecution = executions.find((execution) => execution.id === group['firstExecutionId']) ?? executions[0] ?? null; const sourceFlowId = this.toNonEmptyString(group['sourceFlowId']) ?? this.toNonEmptyString(latestExecution?.sourceFlowId) ?? this.toNonEmptyString(latestExecution?.flowId) ?? this.toNonEmptyString(group['id']) ?? ''; return { id: this.toNonEmptyString(group['id']) ?? sourceFlowId, sourceFlowId, name: this.toNonEmptyString(group['name']) ?? latestExecution?.name ?? sourceFlowId, firstExecutionId: this.toNonEmptyString(group['firstExecutionId']) ?? firstExecution?.id ?? '', latestExecutionId: this.toNonEmptyString(group['latestExecutionId']) ?? latestExecution?.id ?? '', creationTime: this.toTimestamp(group['creationTime'], firstExecution?.creationTime ?? 0), lastExecutionTime: this.toTimestamp(group['lastExecutionTime'], latestExecution?.creationTime ?? 0), executionCount: this.toNumber(group['executionCount'], executions.length), executions }; } private normalizeGlobalInputValues(raw: unknown): Record { if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return {}; return { ...(raw as Record) }; } private normalizeGlobalInputDescriptors(raw: unknown): NonNullable { if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return {}; return Object.entries(raw as Record) .reduce>((acc, [key, value]) => { if (!value || typeof value !== 'object' || Array.isArray(value)) return acc; const record = value as Record; const name = String(record['name'] ?? key).trim(); if (!name) return acc; acc[key] = { name, kind: String(record['kind'] ?? record['type'] ?? 'TEXT').toUpperCase(), value: record['value'] ?? null, description: typeof record['description'] === 'string' ? record['description'] : null, cleanupPolicy: typeof record['cleanupPolicy'] === 'string' ? record['cleanupPolicy'] : null, multiple: Boolean(record['multiple']) }; return acc; }, {}); } private toNonEmptyString(value: unknown): string | null { return typeof value === 'string' && value.trim().length > 0 ? value.trim() : null; } private toNullableString(value: unknown): string | null { return typeof value === 'string' && value.trim().length > 0 ? value : null; } private toTimestamp(value: unknown, fallback: number): number { const timestamp = typeof value === 'number' ? value : Number(value); return Number.isFinite(timestamp) ? timestamp : fallback; } private toNumber(value: unknown, fallback: number): number { const number = typeof value === 'number' ? value : Number(value); return Number.isFinite(number) ? number : fallback; } }