chore: import upstream snapshot with attribution
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,621 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import type { WorkflowState } from '@/stores/workflows/workflow/types'
|
||||
import {
|
||||
extractBlockFieldsForComparison,
|
||||
extractSubBlockRest,
|
||||
filterSubBlockIds,
|
||||
normalizedStringify,
|
||||
normalizeEdge,
|
||||
normalizeLoop,
|
||||
normalizeParallel,
|
||||
normalizeSubBlockValue,
|
||||
normalizeTriggerConfigValues,
|
||||
normalizeValue,
|
||||
normalizeVariables,
|
||||
sanitizeVariable,
|
||||
} from './normalize'
|
||||
import { formatValueForDisplay, resolveFieldLabel, resolveValueForDisplay } from './resolve-values'
|
||||
|
||||
const MAX_CHANGES_PER_BLOCK = 6
|
||||
const MAX_EDGE_DETAILS = 3
|
||||
|
||||
const logger = createLogger('WorkflowComparison')
|
||||
|
||||
/**
|
||||
* Compare the current workflow state with the deployed state to detect meaningful changes.
|
||||
* Uses generateWorkflowDiffSummary internally to ensure consistent change detection.
|
||||
*/
|
||||
export function hasWorkflowChanged(
|
||||
currentState: WorkflowState,
|
||||
deployedState: WorkflowState | null
|
||||
): boolean {
|
||||
return generateWorkflowDiffSummary(currentState, deployedState).hasChanges
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a single field change with old and new values
|
||||
*/
|
||||
interface FieldChange {
|
||||
field: string
|
||||
oldValue: unknown
|
||||
newValue: unknown
|
||||
}
|
||||
|
||||
/**
|
||||
* Result of workflow diff analysis between two workflow states
|
||||
*/
|
||||
export interface WorkflowDiffSummary {
|
||||
addedBlocks: Array<{ id: string; type: string; name?: string }>
|
||||
removedBlocks: Array<{ id: string; type: string; name?: string }>
|
||||
modifiedBlocks: Array<{ id: string; type: string; name?: string; changes: FieldChange[] }>
|
||||
edgeChanges: {
|
||||
added: number
|
||||
removed: number
|
||||
addedDetails: Array<{ sourceName: string; targetName: string }>
|
||||
removedDetails: Array<{ sourceName: string; targetName: string }>
|
||||
}
|
||||
loopChanges: { added: number; removed: number; modified: number }
|
||||
parallelChanges: { added: number; removed: number; modified: number }
|
||||
variableChanges: {
|
||||
added: number
|
||||
removed: number
|
||||
modified: number
|
||||
addedNames: string[]
|
||||
removedNames: string[]
|
||||
modifiedNames: string[]
|
||||
}
|
||||
hasChanges: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a detailed diff summary between two workflow states
|
||||
*/
|
||||
export function generateWorkflowDiffSummary(
|
||||
currentState: WorkflowState,
|
||||
previousState: WorkflowState | null
|
||||
): WorkflowDiffSummary {
|
||||
const result: WorkflowDiffSummary = {
|
||||
addedBlocks: [],
|
||||
removedBlocks: [],
|
||||
modifiedBlocks: [],
|
||||
edgeChanges: { added: 0, removed: 0, addedDetails: [], removedDetails: [] },
|
||||
loopChanges: { added: 0, removed: 0, modified: 0 },
|
||||
parallelChanges: { added: 0, removed: 0, modified: 0 },
|
||||
variableChanges: {
|
||||
added: 0,
|
||||
removed: 0,
|
||||
modified: 0,
|
||||
addedNames: [],
|
||||
removedNames: [],
|
||||
modifiedNames: [],
|
||||
},
|
||||
hasChanges: false,
|
||||
}
|
||||
|
||||
if (!previousState) {
|
||||
const currentBlocks = currentState.blocks || {}
|
||||
for (const [id, block] of Object.entries(currentBlocks)) {
|
||||
result.addedBlocks.push({
|
||||
id,
|
||||
type: block.type,
|
||||
name: block.name,
|
||||
})
|
||||
}
|
||||
|
||||
const edges = currentState.edges || []
|
||||
result.edgeChanges.added = edges.length
|
||||
for (const edge of edges) {
|
||||
const sourceBlock = currentBlocks[edge.source]
|
||||
const targetBlock = currentBlocks[edge.target]
|
||||
result.edgeChanges.addedDetails.push({
|
||||
sourceName: sourceBlock?.name || sourceBlock?.type || edge.source,
|
||||
targetName: targetBlock?.name || targetBlock?.type || edge.target,
|
||||
})
|
||||
}
|
||||
|
||||
result.loopChanges.added = Object.keys(currentState.loops || {}).length
|
||||
result.parallelChanges.added = Object.keys(currentState.parallels || {}).length
|
||||
|
||||
const variables = currentState.variables || {}
|
||||
const varEntries = Object.entries(variables)
|
||||
result.variableChanges.added = varEntries.length
|
||||
for (const [id, variable] of varEntries) {
|
||||
result.variableChanges.addedNames.push((variable as { name?: string }).name || id)
|
||||
}
|
||||
|
||||
result.hasChanges = true
|
||||
return result
|
||||
}
|
||||
|
||||
const currentBlocks = currentState.blocks || {}
|
||||
const previousBlocks = previousState.blocks || {}
|
||||
const currentBlockIds = new Set(Object.keys(currentBlocks))
|
||||
const previousBlockIds = new Set(Object.keys(previousBlocks))
|
||||
|
||||
for (const id of currentBlockIds) {
|
||||
if (!previousBlockIds.has(id)) {
|
||||
const block = currentBlocks[id]
|
||||
result.addedBlocks.push({
|
||||
id,
|
||||
type: block.type,
|
||||
name: block.name,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
for (const id of previousBlockIds) {
|
||||
if (!currentBlockIds.has(id)) {
|
||||
const block = previousBlocks[id]
|
||||
result.removedBlocks.push({
|
||||
id,
|
||||
type: block.type,
|
||||
name: block.name,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
for (const id of currentBlockIds) {
|
||||
if (!previousBlockIds.has(id)) continue
|
||||
|
||||
const currentBlock = currentBlocks[id]
|
||||
const previousBlock = previousBlocks[id]
|
||||
const changes: FieldChange[] = []
|
||||
|
||||
const {
|
||||
blockRest: currentRest,
|
||||
normalizedData: currentDataRest,
|
||||
subBlocks: currentSubBlocks,
|
||||
} = extractBlockFieldsForComparison(currentBlock)
|
||||
const {
|
||||
blockRest: previousRest,
|
||||
normalizedData: previousDataRest,
|
||||
subBlocks: previousSubBlocks,
|
||||
} = extractBlockFieldsForComparison(previousBlock)
|
||||
|
||||
const normalizedCurrentBlock = { ...currentRest, data: currentDataRest, subBlocks: undefined }
|
||||
const normalizedPreviousBlock = {
|
||||
...previousRest,
|
||||
data: previousDataRest,
|
||||
subBlocks: undefined,
|
||||
}
|
||||
|
||||
if (
|
||||
normalizedStringify(normalizedCurrentBlock) !== normalizedStringify(normalizedPreviousBlock)
|
||||
) {
|
||||
if (currentBlock.type !== previousBlock.type) {
|
||||
changes.push({ field: 'type', oldValue: previousBlock.type, newValue: currentBlock.type })
|
||||
}
|
||||
if (currentBlock.name !== previousBlock.name) {
|
||||
changes.push({ field: 'name', oldValue: previousBlock.name, newValue: currentBlock.name })
|
||||
}
|
||||
if (currentBlock.enabled !== previousBlock.enabled) {
|
||||
changes.push({
|
||||
field: 'enabled',
|
||||
oldValue: previousBlock.enabled,
|
||||
newValue: currentBlock.enabled,
|
||||
})
|
||||
}
|
||||
const blockFields = ['horizontalHandles', 'advancedMode', 'triggerMode'] as const
|
||||
for (const field of blockFields) {
|
||||
if (!!currentBlock[field] !== !!previousBlock[field]) {
|
||||
changes.push({
|
||||
field,
|
||||
oldValue: previousBlock[field],
|
||||
newValue: currentBlock[field],
|
||||
})
|
||||
}
|
||||
}
|
||||
if (normalizedStringify(currentDataRest) !== normalizedStringify(previousDataRest)) {
|
||||
const allDataKeys = new Set([
|
||||
...Object.keys(currentDataRest),
|
||||
...Object.keys(previousDataRest),
|
||||
])
|
||||
for (const key of allDataKeys) {
|
||||
if (
|
||||
normalizedStringify(currentDataRest[key]) !== normalizedStringify(previousDataRest[key])
|
||||
) {
|
||||
changes.push({
|
||||
field: `data.${key}`,
|
||||
oldValue: previousDataRest[key] ?? null,
|
||||
newValue: currentDataRest[key] ?? null,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const normalizedCurrentSubs = normalizeTriggerConfigValues(currentSubBlocks)
|
||||
const normalizedPreviousSubs = normalizeTriggerConfigValues(previousSubBlocks)
|
||||
|
||||
const allSubBlockIds = filterSubBlockIds([
|
||||
...new Set([...Object.keys(normalizedCurrentSubs), ...Object.keys(normalizedPreviousSubs)]),
|
||||
])
|
||||
|
||||
for (const subId of allSubBlockIds) {
|
||||
const currentSub = normalizedCurrentSubs[subId] as Record<string, unknown> | undefined
|
||||
const previousSub = normalizedPreviousSubs[subId] as Record<string, unknown> | undefined
|
||||
|
||||
if (!currentSub || !previousSub) {
|
||||
changes.push({
|
||||
field: subId,
|
||||
oldValue: (previousSub as Record<string, unknown> | undefined)?.value ?? null,
|
||||
newValue: (currentSub as Record<string, unknown> | undefined)?.value ?? null,
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
const currentValue = normalizeSubBlockValue(subId, currentSub.value)
|
||||
const previousValue = normalizeSubBlockValue(subId, previousSub.value)
|
||||
|
||||
if (typeof currentValue === 'string' && typeof previousValue === 'string') {
|
||||
if (currentValue !== previousValue) {
|
||||
changes.push({ field: subId, oldValue: previousSub.value, newValue: currentSub.value })
|
||||
}
|
||||
} else {
|
||||
const normalizedCurrent = normalizeValue(currentValue)
|
||||
const normalizedPrevious = normalizeValue(previousValue)
|
||||
if (normalizedStringify(normalizedCurrent) !== normalizedStringify(normalizedPrevious)) {
|
||||
changes.push({ field: subId, oldValue: previousSub.value, newValue: currentSub.value })
|
||||
}
|
||||
}
|
||||
|
||||
const currentSubRest = extractSubBlockRest(currentSub)
|
||||
const previousSubRest = extractSubBlockRest(previousSub)
|
||||
|
||||
if (normalizedStringify(currentSubRest) !== normalizedStringify(previousSubRest)) {
|
||||
changes.push({
|
||||
field: `${subId}.properties`,
|
||||
oldValue: previousSubRest,
|
||||
newValue: currentSubRest,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (changes.length > 0) {
|
||||
result.modifiedBlocks.push({
|
||||
id,
|
||||
type: currentBlock.type,
|
||||
name: currentBlock.name,
|
||||
changes,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const currentEdges = (currentState.edges || []).map(normalizeEdge)
|
||||
const previousEdges = (previousState.edges || []).map(normalizeEdge)
|
||||
const currentEdgeSet = new Set(currentEdges.map(normalizedStringify))
|
||||
const previousEdgeSet = new Set(previousEdges.map(normalizedStringify))
|
||||
|
||||
const resolveBlockName = (blockId: string): string => {
|
||||
const block = currentBlocks[blockId] || previousBlocks[blockId]
|
||||
return block?.name || block?.type || blockId
|
||||
}
|
||||
|
||||
for (const edgeStr of currentEdgeSet) {
|
||||
if (!previousEdgeSet.has(edgeStr)) {
|
||||
result.edgeChanges.added++
|
||||
const edge = JSON.parse(edgeStr) as { source: string; target: string }
|
||||
result.edgeChanges.addedDetails.push({
|
||||
sourceName: resolveBlockName(edge.source),
|
||||
targetName: resolveBlockName(edge.target),
|
||||
})
|
||||
}
|
||||
}
|
||||
for (const edgeStr of previousEdgeSet) {
|
||||
if (!currentEdgeSet.has(edgeStr)) {
|
||||
result.edgeChanges.removed++
|
||||
const edge = JSON.parse(edgeStr) as { source: string; target: string }
|
||||
result.edgeChanges.removedDetails.push({
|
||||
sourceName: resolveBlockName(edge.source),
|
||||
targetName: resolveBlockName(edge.target),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const currentLoops = currentState.loops || {}
|
||||
const previousLoops = previousState.loops || {}
|
||||
const currentLoopIds = Object.keys(currentLoops)
|
||||
const previousLoopIds = Object.keys(previousLoops)
|
||||
|
||||
for (const id of currentLoopIds) {
|
||||
if (!previousLoopIds.includes(id)) {
|
||||
result.loopChanges.added++
|
||||
} else {
|
||||
const normalizedCurrent = normalizeValue(normalizeLoop(currentLoops[id]))
|
||||
const normalizedPrevious = normalizeValue(normalizeLoop(previousLoops[id]))
|
||||
if (normalizedStringify(normalizedCurrent) !== normalizedStringify(normalizedPrevious)) {
|
||||
result.loopChanges.modified++
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const id of previousLoopIds) {
|
||||
if (!currentLoopIds.includes(id)) {
|
||||
result.loopChanges.removed++
|
||||
}
|
||||
}
|
||||
|
||||
const currentParallels = currentState.parallels || {}
|
||||
const previousParallels = previousState.parallels || {}
|
||||
const currentParallelIds = Object.keys(currentParallels)
|
||||
const previousParallelIds = Object.keys(previousParallels)
|
||||
|
||||
for (const id of currentParallelIds) {
|
||||
if (!previousParallelIds.includes(id)) {
|
||||
result.parallelChanges.added++
|
||||
} else {
|
||||
const normalizedCurrent = normalizeValue(normalizeParallel(currentParallels[id]))
|
||||
const normalizedPrevious = normalizeValue(normalizeParallel(previousParallels[id]))
|
||||
if (normalizedStringify(normalizedCurrent) !== normalizedStringify(normalizedPrevious)) {
|
||||
result.parallelChanges.modified++
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const id of previousParallelIds) {
|
||||
if (!currentParallelIds.includes(id)) {
|
||||
result.parallelChanges.removed++
|
||||
}
|
||||
}
|
||||
|
||||
const currentVars = normalizeVariables(currentState.variables)
|
||||
const previousVars = normalizeVariables(previousState.variables)
|
||||
const currentVarIds = Object.keys(currentVars)
|
||||
const previousVarIds = Object.keys(previousVars)
|
||||
|
||||
for (const id of currentVarIds) {
|
||||
if (!previousVarIds.includes(id)) {
|
||||
result.variableChanges.added++
|
||||
result.variableChanges.addedNames.push(currentVars[id].name || id)
|
||||
}
|
||||
}
|
||||
for (const id of previousVarIds) {
|
||||
if (!currentVarIds.includes(id)) {
|
||||
result.variableChanges.removed++
|
||||
result.variableChanges.removedNames.push(previousVars[id].name || id)
|
||||
}
|
||||
}
|
||||
|
||||
for (const id of currentVarIds) {
|
||||
if (!previousVarIds.includes(id)) continue
|
||||
const currentVar = normalizeValue(sanitizeVariable(currentVars[id]))
|
||||
const previousVar = normalizeValue(sanitizeVariable(previousVars[id]))
|
||||
if (normalizedStringify(currentVar) !== normalizedStringify(previousVar)) {
|
||||
result.variableChanges.modified++
|
||||
result.variableChanges.modifiedNames.push(currentVars[id].name || id)
|
||||
}
|
||||
}
|
||||
|
||||
result.hasChanges =
|
||||
result.addedBlocks.length > 0 ||
|
||||
result.removedBlocks.length > 0 ||
|
||||
result.modifiedBlocks.length > 0 ||
|
||||
result.edgeChanges.added > 0 ||
|
||||
result.edgeChanges.removed > 0 ||
|
||||
result.loopChanges.added > 0 ||
|
||||
result.loopChanges.removed > 0 ||
|
||||
result.loopChanges.modified > 0 ||
|
||||
result.parallelChanges.added > 0 ||
|
||||
result.parallelChanges.removed > 0 ||
|
||||
result.parallelChanges.modified > 0 ||
|
||||
result.variableChanges.added > 0 ||
|
||||
result.variableChanges.removed > 0 ||
|
||||
result.variableChanges.modified > 0
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a WorkflowDiffSummary to a human-readable string for AI description generation
|
||||
*/
|
||||
export function formatDiffSummaryForDescription(summary: WorkflowDiffSummary): string {
|
||||
if (!summary.hasChanges) {
|
||||
return 'No structural changes detected (configuration may have changed)'
|
||||
}
|
||||
|
||||
const changes: string[] = []
|
||||
|
||||
for (const block of summary.addedBlocks) {
|
||||
const name = block.name || block.type
|
||||
changes.push(`Added block: ${name} (${block.type})`)
|
||||
}
|
||||
|
||||
for (const block of summary.removedBlocks) {
|
||||
const name = block.name || block.type
|
||||
changes.push(`Removed block: ${name} (${block.type})`)
|
||||
}
|
||||
|
||||
for (const block of summary.modifiedBlocks) {
|
||||
const name = block.name || block.type
|
||||
const meaningfulChanges = block.changes.filter((c) => !c.field.endsWith('.properties'))
|
||||
for (const change of meaningfulChanges.slice(0, MAX_CHANGES_PER_BLOCK)) {
|
||||
const fieldLabel = resolveFieldLabel(block.type, change.field)
|
||||
const oldStr = formatValueForDisplay(change.oldValue)
|
||||
const newStr = formatValueForDisplay(change.newValue)
|
||||
changes.push(`Modified ${name}: ${fieldLabel} changed from "${oldStr}" to "${newStr}"`)
|
||||
}
|
||||
if (meaningfulChanges.length > MAX_CHANGES_PER_BLOCK) {
|
||||
changes.push(
|
||||
` ...and ${meaningfulChanges.length - MAX_CHANGES_PER_BLOCK} more changes in ${name}`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
formatEdgeChanges(summary, changes)
|
||||
formatCountChanges(summary.loopChanges, 'loop', changes)
|
||||
formatCountChanges(summary.parallelChanges, 'parallel group', changes)
|
||||
formatVariableChanges(summary, changes)
|
||||
|
||||
return changes.join('\n')
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a WorkflowDiffSummary to a human-readable string with resolved display names.
|
||||
* Resolves IDs (credentials, channels, workflows, etc.) to human-readable names using
|
||||
* the selector registry infrastructure.
|
||||
*
|
||||
* @param summary - The diff summary to format
|
||||
* @param currentState - The current workflow state for context extraction
|
||||
* @param workflowId - The workflow ID for API calls
|
||||
* @returns A formatted string describing the changes with resolved names
|
||||
*/
|
||||
export async function formatDiffSummaryForDescriptionAsync(
|
||||
summary: WorkflowDiffSummary,
|
||||
currentState: WorkflowState,
|
||||
workflowId: string
|
||||
): Promise<string> {
|
||||
if (!summary.hasChanges) {
|
||||
return 'No structural changes detected (configuration may have changed)'
|
||||
}
|
||||
|
||||
const changes: string[] = []
|
||||
|
||||
for (const block of summary.addedBlocks) {
|
||||
const name = block.name || block.type
|
||||
changes.push(`Added block: ${name} (${block.type})`)
|
||||
}
|
||||
|
||||
for (const block of summary.removedBlocks) {
|
||||
const name = block.name || block.type
|
||||
changes.push(`Removed block: ${name} (${block.type})`)
|
||||
}
|
||||
|
||||
const modifiedBlockPromises = summary.modifiedBlocks.map(async (block) => {
|
||||
const name = block.name || block.type
|
||||
const blockChanges: string[] = []
|
||||
const meaningfulChanges = block.changes.filter((c) => !c.field.endsWith('.properties'))
|
||||
|
||||
const changesToProcess = meaningfulChanges.slice(0, MAX_CHANGES_PER_BLOCK)
|
||||
const resolvedChanges = await Promise.all(
|
||||
changesToProcess.map(async (change) => {
|
||||
const context = {
|
||||
blockType: block.type,
|
||||
subBlockId: change.field,
|
||||
workflowId,
|
||||
currentState,
|
||||
blockId: block.id,
|
||||
}
|
||||
|
||||
const [oldResolved, newResolved] = await Promise.all([
|
||||
resolveValueForDisplay(change.oldValue, context),
|
||||
resolveValueForDisplay(change.newValue, context),
|
||||
])
|
||||
|
||||
return {
|
||||
field: resolveFieldLabel(block.type, change.field),
|
||||
oldLabel: oldResolved.displayLabel,
|
||||
newLabel: newResolved.displayLabel,
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
for (const resolved of resolvedChanges) {
|
||||
blockChanges.push(
|
||||
`Modified ${name}: ${resolved.field} changed from "${resolved.oldLabel}" to "${resolved.newLabel}"`
|
||||
)
|
||||
}
|
||||
|
||||
if (meaningfulChanges.length > MAX_CHANGES_PER_BLOCK) {
|
||||
blockChanges.push(
|
||||
` ...and ${meaningfulChanges.length - MAX_CHANGES_PER_BLOCK} more changes in ${name}`
|
||||
)
|
||||
}
|
||||
|
||||
return blockChanges
|
||||
})
|
||||
|
||||
const allModifiedBlockChanges = await Promise.all(modifiedBlockPromises)
|
||||
for (const blockChanges of allModifiedBlockChanges) {
|
||||
changes.push(...blockChanges)
|
||||
}
|
||||
|
||||
formatEdgeChanges(summary, changes)
|
||||
formatCountChanges(summary.loopChanges, 'loop', changes)
|
||||
formatCountChanges(summary.parallelChanges, 'parallel group', changes)
|
||||
formatVariableChanges(summary, changes)
|
||||
|
||||
logger.info('Generated async diff description', {
|
||||
workflowId,
|
||||
changeCount: changes.length,
|
||||
modifiedBlocks: summary.modifiedBlocks.length,
|
||||
})
|
||||
|
||||
return changes.join('\n')
|
||||
}
|
||||
|
||||
function formatEdgeDetailList(
|
||||
edges: Array<{ sourceName: string; targetName: string }>,
|
||||
total: number,
|
||||
verb: string,
|
||||
changes: string[]
|
||||
): void {
|
||||
if (edges.length === 0) {
|
||||
changes.push(`${verb} ${total} connection(s)`)
|
||||
return
|
||||
}
|
||||
for (const edge of edges.slice(0, MAX_EDGE_DETAILS)) {
|
||||
changes.push(`${verb} connection: ${edge.sourceName} -> ${edge.targetName}`)
|
||||
}
|
||||
if (total > MAX_EDGE_DETAILS) {
|
||||
changes.push(` ...and ${total - MAX_EDGE_DETAILS} more ${verb.toLowerCase()} connection(s)`)
|
||||
}
|
||||
}
|
||||
|
||||
function formatEdgeChanges(summary: WorkflowDiffSummary, changes: string[]): void {
|
||||
if (summary.edgeChanges.added > 0) {
|
||||
formatEdgeDetailList(
|
||||
summary.edgeChanges.addedDetails ?? [],
|
||||
summary.edgeChanges.added,
|
||||
'Added',
|
||||
changes
|
||||
)
|
||||
}
|
||||
if (summary.edgeChanges.removed > 0) {
|
||||
formatEdgeDetailList(
|
||||
summary.edgeChanges.removedDetails ?? [],
|
||||
summary.edgeChanges.removed,
|
||||
'Removed',
|
||||
changes
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
function formatCountChanges(
|
||||
counts: { added: number; removed: number; modified: number },
|
||||
label: string,
|
||||
changes: string[]
|
||||
): void {
|
||||
if (counts.added > 0) changes.push(`Added ${counts.added} ${label}(s)`)
|
||||
if (counts.removed > 0) changes.push(`Removed ${counts.removed} ${label}(s)`)
|
||||
if (counts.modified > 0) changes.push(`Modified ${counts.modified} ${label}(s)`)
|
||||
}
|
||||
|
||||
function formatVariableChanges(summary: WorkflowDiffSummary, changes: string[]): void {
|
||||
const categories = [
|
||||
{
|
||||
count: summary.variableChanges.added,
|
||||
names: summary.variableChanges.addedNames ?? [],
|
||||
verb: 'added',
|
||||
},
|
||||
{
|
||||
count: summary.variableChanges.removed,
|
||||
names: summary.variableChanges.removedNames ?? [],
|
||||
verb: 'removed',
|
||||
},
|
||||
{
|
||||
count: summary.variableChanges.modified,
|
||||
names: summary.variableChanges.modifiedNames ?? [],
|
||||
verb: 'modified',
|
||||
},
|
||||
] as const
|
||||
|
||||
const varParts: string[] = []
|
||||
for (const { count, names, verb } of categories) {
|
||||
if (count > 0) {
|
||||
varParts.push(
|
||||
names.length > 0 ? `${verb} ${names.map((n) => `"${n}"`).join(', ')}` : `${count} ${verb}`
|
||||
)
|
||||
}
|
||||
}
|
||||
if (varParts.length > 0) {
|
||||
changes.push(`Variables: ${varParts.join(', ')}`)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user