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

This commit is contained in:
wehub-resource-sync
2026-07-13 13:20:55 +08:00
commit d25d482dc2
13754 changed files with 4996608 additions and 0 deletions
@@ -0,0 +1,211 @@
/**
* @vitest-environment node
*
* Focused tests for the registry store's `loadWorkflowState` after the
* workflow-state cache collapse: it hydrates the shared
* `workflowKeys.state(id)` entry via `fetchQuery` (always-fresh,
* `staleTime: 0`) and projects the envelope into the workflow / sub-block /
* variables / deployment stores, guarding against superseded responses.
*/
import { QueryClient } from '@tanstack/react-query'
import { beforeEach, describe, expect, it, vi } from 'vitest'
const { mockRequestJson, sharedQueryClient } = vi.hoisted(() => ({
mockRequestJson: vi.fn(),
sharedQueryClient: { current: null as unknown },
}))
vi.mock('@/lib/api/client/request', () => ({
requestJson: mockRequestJson,
}))
vi.mock('@/app/_shell/providers/get-query-client', () => ({
getQueryClient: () => sharedQueryClient.current as QueryClient,
}))
const { replaceWorkflowState, initializeFromWorkflow, setVariablesState, clearError } = vi.hoisted(
() => ({
replaceWorkflowState: vi.fn(),
initializeFromWorkflow: vi.fn(),
setVariablesState: vi.fn(),
clearError: vi.fn(),
})
)
vi.mock('@/stores/workflows/workflow/store', () => ({
useWorkflowStore: {
getState: () => ({ replaceWorkflowState, blocks: {} }),
setState: vi.fn(),
},
}))
vi.mock('@/stores/workflows/subblock/store', () => ({
useSubBlockStore: {
getState: () => ({ initializeFromWorkflow }),
setState: vi.fn(),
},
}))
vi.mock('@/stores/variables/store', () => ({
useVariablesStore: {
getState: () => ({ variables: {} }),
setState: (updater: unknown) => setVariablesState(updater),
},
}))
vi.mock('@/stores/operation-queue/store', () => ({
useOperationQueueStore: {
getState: () => ({ clearError }),
},
}))
vi.mock('@/hooks/queries/utils/invalidate-workflow-lists', () => ({
invalidateWorkflowLists: vi.fn().mockResolvedValue(undefined),
}))
vi.mock('@/stores/workflows/utils', () => ({
getUniqueBlockName: vi.fn(),
regenerateBlockIds: vi.fn(),
}))
vi.mock('@/lib/workflows/autolayout/constants', () => ({
DEFAULT_DUPLICATE_OFFSET: { x: 0, y: 0 },
}))
vi.mock('@/hooks/queries/deployments', () => ({
deploymentKeys: {
infos: () => ['deployments', 'info'],
info: (workflowId: string | null) => ['deployments', 'info', workflowId ?? ''],
},
}))
import { workflowKeys } from '@/hooks/queries/utils/workflow-keys'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
function makeEnvelope(overrides: Record<string, unknown> = {}) {
return {
id: 'wf-1',
isDeployed: true,
deployedAt: new Date('2026-01-01T00:00:00.000Z'),
isPublicApi: false,
state: {
blocks: { b1: { id: 'b1' } },
edges: [],
loops: {},
parallels: {},
},
variables: { v1: { id: 'v1', workflowId: 'wf-1', name: 'x' } },
...overrides,
}
}
describe('registry store loadWorkflowState (collapsed cache)', () => {
beforeEach(() => {
vi.clearAllMocks()
// The store dispatches an `active-workflow-changed` CustomEvent on the
// window; provide a minimal stub under the node environment.
vi.stubGlobal('window', { dispatchEvent: vi.fn() })
sharedQueryClient.current = new QueryClient({
defaultOptions: { queries: { retry: false } },
})
// Reset store to a clean state with a workspace scope so loadWorkflowState
// does not bail on the missing-workspace guard.
useWorkflowRegistry.setState({
activeWorkflowId: null,
error: null,
hydration: {
phase: 'idle',
workspaceId: 'ws-1',
workflowId: null,
requestId: null,
error: null,
},
})
})
it('projects envelope state, variables, and deployment info into the stores', async () => {
mockRequestJson.mockResolvedValue({ data: makeEnvelope() })
await useWorkflowRegistry.getState().loadWorkflowState('wf-1')
expect(replaceWorkflowState).toHaveBeenCalledTimes(1)
expect(replaceWorkflowState.mock.calls[0][0]).toMatchObject({
currentWorkflowId: 'wf-1',
blocks: { b1: { id: 'b1' } },
edges: [],
})
expect(initializeFromWorkflow).toHaveBeenCalledWith('wf-1', { b1: { id: 'b1' } })
expect(setVariablesState).toHaveBeenCalledTimes(1)
const deploymentInfo = (sharedQueryClient.current as QueryClient).getQueryData([
'deployments',
'info',
'wf-1',
])
expect(deploymentInfo).toMatchObject({
isDeployed: true,
isPublicApi: false,
deployedAt: '2026-01-01T00:00:00.000Z',
})
expect(useWorkflowRegistry.getState().activeWorkflowId).toBe('wf-1')
expect(useWorkflowRegistry.getState().hydration.phase).toBe('ready')
})
it('hydrates the SAME workflowKeys.state(id) cache entry the hooks read', async () => {
const envelope = makeEnvelope()
mockRequestJson.mockResolvedValue({ data: envelope })
await useWorkflowRegistry.getState().loadWorkflowState('wf-1')
const client = sharedQueryClient.current as QueryClient
const cached = client.getQueryData(workflowKeys.state('wf-1'))
expect(cached).toBeDefined()
expect((cached as { id: string }).id).toBe('wf-1')
// Exactly one cache entry exists for this endpoint — the shared one.
const stateEntries = client
.getQueryCache()
.findAll({ queryKey: workflowKeys.states() })
.filter((q) => q.queryKey[2] === 'wf-1')
expect(stateEntries).toHaveLength(1)
})
it('re-fetches on every call (staleTime: 0, never served stale)', async () => {
mockRequestJson.mockResolvedValue({ data: makeEnvelope() })
await useWorkflowRegistry.getState().loadWorkflowState('wf-1')
await useWorkflowRegistry.getState().loadWorkflowState('wf-1')
expect(mockRequestJson).toHaveBeenCalledTimes(2)
})
it('discards a superseded response via the staleness guard', async () => {
// First load (wf-1) is in-flight; a second load (wf-2) supersedes the
// hydration workflowId, then wf-1 finally resolves. The guard compares the
// current hydration workflowId/requestId against the resolving request and
// must discard the now-stale wf-1 projection.
let resolveFirst: (value: unknown) => void = () => {}
const firstPending = new Promise((resolve) => {
resolveFirst = resolve
})
mockRequestJson
.mockImplementationOnce(() => firstPending)
.mockImplementationOnce(() => Promise.resolve({ data: makeEnvelope({ id: 'wf-2' }) }))
const firstLoad = useWorkflowRegistry.getState().loadWorkflowState('wf-1')
const secondLoad = useWorkflowRegistry.getState().loadWorkflowState('wf-2')
await secondLoad
expect(useWorkflowRegistry.getState().activeWorkflowId).toBe('wf-2')
const projectionsAfterSecond = replaceWorkflowState.mock.calls.length
resolveFirst({ data: makeEnvelope({ id: 'wf-1' }) })
await firstLoad
// The stale wf-1 result must not project again — hydration is now wf-2.
expect(replaceWorkflowState.mock.calls.length).toBe(projectionsAfterSecond)
expect(useWorkflowRegistry.getState().activeWorkflowId).toBe('wf-2')
})
})
+419
View File
@@ -0,0 +1,419 @@
import { createLogger } from '@sim/logger'
import { generateRandomHex } from '@sim/utils/random'
import { create } from 'zustand'
import { devtools } from 'zustand/middleware'
import { DEFAULT_DUPLICATE_OFFSET } from '@/lib/workflows/autolayout/constants'
import { getQueryClient } from '@/app/_shell/providers/get-query-client'
import type { WorkflowDeploymentInfo } from '@/hooks/queries/deployments'
import { deploymentKeys } from '@/hooks/queries/deployments'
import { fetchWorkflowEnvelope } from '@/hooks/queries/utils/fetch-workflow-envelope'
import { invalidateWorkflowLists } from '@/hooks/queries/utils/invalidate-workflow-lists'
import { workflowKeys } from '@/hooks/queries/utils/workflow-keys'
import { useOperationQueueStore } from '@/stores/operation-queue/store'
import { useVariablesStore } from '@/stores/variables/store'
import type { Variable } from '@/stores/variables/types'
import type { HydrationState, WorkflowRegistry } from '@/stores/workflows/registry/types'
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
import { getUniqueBlockName, regenerateBlockIds } from '@/stores/workflows/utils'
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
import type { BlockState, Loop, Parallel, WorkflowState } from '@/stores/workflows/workflow/types'
const logger = createLogger('WorkflowRegistry')
const initialHydration: HydrationState = {
phase: 'idle',
workspaceId: null,
workflowId: null,
requestId: null,
error: null,
}
const createRequestId = () => `${Date.now()}-${generateRandomHex(8)}`
function resetWorkflowStores() {
useWorkflowStore.setState({
currentWorkflowId: null,
blocks: {},
edges: [],
loops: {},
parallels: {},
lastSaved: Date.now(),
})
useSubBlockStore.setState({
workflowValues: {},
})
}
export const useWorkflowRegistry = create<WorkflowRegistry>()(
devtools(
(set, get) => ({
activeWorkflowId: null,
error: null,
hydration: initialHydration,
clipboard: null,
pendingSelection: null,
switchToWorkspace: (workspaceId: string) => {
logger.info(`Switching to workspace: ${workspaceId}`)
resetWorkflowStores()
// Workflow stores are fully reset and reloaded from the server in the new
// workspace, so a previously tripped offline mode must not carry over.
useOperationQueueStore.getState().clearError()
void invalidateWorkflowLists(getQueryClient(), workspaceId)
set({
activeWorkflowId: null,
error: null,
hydration: {
phase: 'idle',
workspaceId,
workflowId: null,
requestId: null,
error: null,
},
})
},
loadWorkflowState: async (workflowId: string) => {
const workspaceId = get().hydration.workspaceId
if (!workspaceId) {
const message = `Cannot load workflow ${workflowId} without a workspace scope`
logger.error(message)
set({ error: message })
throw new Error(message)
}
const requestId = createRequestId()
set((state) => ({
error: null,
hydration: {
phase: 'state-loading',
workspaceId: workspaceId ?? state.hydration.workspaceId,
workflowId,
requestId,
error: null,
},
}))
try {
const workflowData = await getQueryClient().fetchQuery({
queryKey: workflowKeys.state(workflowId),
queryFn: ({ signal }) => fetchWorkflowEnvelope(workflowId, signal),
staleTime: 0,
})
const deployedAt = workflowData.deployedAt ? workflowData.deployedAt.toISOString() : null
getQueryClient().setQueryData<WorkflowDeploymentInfo>(
deploymentKeys.info(workflowId),
(prev) => ({
isDeployed: workflowData.isDeployed,
deployedAt,
apiKey: prev?.apiKey ?? null,
needsRedeployment: prev?.needsRedeployment ?? false,
isPublicApi: workflowData.isPublicApi,
})
)
let workflowState: WorkflowState
if (workflowData?.state) {
const wireState = workflowData.state as Pick<
WorkflowState,
'blocks' | 'edges' | 'loops' | 'parallels'
>
workflowState = {
currentWorkflowId: workflowId,
blocks: wireState.blocks || {},
edges: wireState.edges || [],
loops: wireState.loops || {},
parallels: wireState.parallels || {},
lastSaved: Date.now(),
}
} else {
workflowState = {
currentWorkflowId: workflowId,
blocks: {},
edges: [],
loops: {},
parallels: {},
lastSaved: Date.now(),
}
logger.info(
`Workflow ${workflowId} has no state yet - will load from DB or show empty canvas`
)
}
const currentHydration = get().hydration
if (
currentHydration.requestId !== requestId ||
currentHydration.workflowId !== workflowId
) {
logger.info('Discarding stale workflow hydration result', {
workflowId,
requestId,
})
return
}
useWorkflowStore.getState().replaceWorkflowState(workflowState)
useSubBlockStore.getState().initializeFromWorkflow(workflowId, workflowState.blocks || {})
const wireVariables = workflowData.variables
if (wireVariables) {
useVariablesStore.setState((state) => {
const withoutWorkflow = Object.fromEntries(
Object.entries(state.variables).filter(
(entry): entry is [string, Variable] => entry[1].workflowId !== workflowId
)
)
return {
variables: {
...withoutWorkflow,
...(wireVariables as Record<string, Variable>),
},
}
})
}
window.dispatchEvent(
new CustomEvent('active-workflow-changed', {
detail: { workflowId },
})
)
set((state) => ({
activeWorkflowId: workflowId,
error: null,
hydration: {
phase: 'ready',
workspaceId: state.hydration.workspaceId,
workflowId,
requestId,
error: null,
},
}))
logger.info(`Switched to workflow ${workflowId}`)
} catch (error) {
const message =
error instanceof Error
? error.message
: `Failed to load workflow ${workflowId}: Unknown error`
logger.error(message)
const currentHydration = get().hydration
if (
currentHydration.requestId !== requestId ||
currentHydration.workflowId !== workflowId
) {
logger.info('Discarding stale workflow error', { workflowId, requestId })
return
}
set((state) => ({
error: message,
hydration: {
phase: 'error',
workspaceId: state.hydration.workspaceId,
workflowId,
requestId: null,
error: message,
},
}))
throw error
}
},
setActiveWorkflow: async (id: string) => {
const { activeWorkflowId, hydration } = get()
const workflowStoreState = useWorkflowStore.getState()
const hasWorkflowData = Object.keys(workflowStoreState.blocks).length > 0
const isFullyHydrated =
activeWorkflowId === id &&
hasWorkflowData &&
hydration.phase === 'ready' &&
hydration.workflowId === id
if (isFullyHydrated) {
logger.info(`Already active workflow ${id} with data loaded, skipping switch`)
return
}
await get().loadWorkflowState(id)
},
markWorkflowCreating: (workflowId: string) => {
set((state) => ({
error: null,
hydration: {
phase: 'creating' as const,
workspaceId: state.hydration.workspaceId,
workflowId,
requestId: null,
error: null,
},
}))
logger.info(`Marked workflow ${workflowId} as creating`)
},
markWorkflowCreated: (workflowId: string | null) => {
const { hydration } = get()
if (!workflowId) {
if (hydration.phase === 'creating') {
set((state) => ({
hydration: {
...state.hydration,
phase: 'idle' as const,
workflowId: null,
error: null,
},
}))
}
return
}
if (hydration.phase !== 'creating' || hydration.workflowId !== workflowId) {
logger.info(
`Ignoring markWorkflowCreated for ${workflowId} — hydration is ${hydration.phase}/${hydration.workflowId}`
)
return
}
logger.info(`Workflow ${workflowId} created, loading state`)
get()
.loadWorkflowState(workflowId)
.catch((error) => {
logger.error(`Failed to load newly created workflow ${workflowId}:`, error)
})
},
logout: () => {
logger.info('Logging out - clearing all workflow data')
resetWorkflowStores()
// Clear the React Query cache to remove all server state
getQueryClient().clear()
set({
activeWorkflowId: null,
error: null,
hydration: initialHydration,
clipboard: null,
})
logger.info('Logout complete - all workflow data cleared')
},
copyBlocks: (blockIds: string[]) => {
if (blockIds.length === 0) return
const workflowStore = useWorkflowStore.getState()
const activeWorkflowId = get().activeWorkflowId
const subBlockStore = useSubBlockStore.getState()
const copiedBlocks: Record<string, BlockState> = {}
const copiedSubBlockValues: Record<string, Record<string, unknown>> = {}
const blockIdSet = new Set(blockIds)
blockIds.forEach((blockId) => {
const loop = workflowStore.loops[blockId]
if (loop?.nodes) loop.nodes.forEach((n) => blockIdSet.add(n))
const parallel = workflowStore.parallels[blockId]
if (parallel?.nodes) parallel.nodes.forEach((n) => blockIdSet.add(n))
})
blockIdSet.forEach((blockId) => {
const block = workflowStore.blocks[blockId]
if (block) {
copiedBlocks[blockId] = structuredClone(block)
if (activeWorkflowId) {
const blockValues = subBlockStore.workflowValues[activeWorkflowId]?.[blockId]
if (blockValues) {
copiedSubBlockValues[blockId] = structuredClone(blockValues)
}
}
}
})
const copiedEdges = workflowStore.edges.filter(
(edge) => blockIdSet.has(edge.source) && blockIdSet.has(edge.target)
)
const copiedLoops: Record<string, Loop> = {}
Object.entries(workflowStore.loops).forEach(([loopId, loop]) => {
if (blockIdSet.has(loopId)) {
copiedLoops[loopId] = structuredClone(loop)
}
})
const copiedParallels: Record<string, Parallel> = {}
Object.entries(workflowStore.parallels).forEach(([parallelId, parallel]) => {
if (blockIdSet.has(parallelId)) {
copiedParallels[parallelId] = structuredClone(parallel)
}
})
set({
clipboard: {
blocks: copiedBlocks,
edges: copiedEdges,
subBlockValues: copiedSubBlockValues,
loops: copiedLoops,
parallels: copiedParallels,
timestamp: Date.now(),
},
})
logger.info('Copied blocks to clipboard', { count: Object.keys(copiedBlocks).length })
},
preparePasteData: (positionOffset = DEFAULT_DUPLICATE_OFFSET) => {
const { clipboard, activeWorkflowId } = get()
if (!clipboard || Object.keys(clipboard.blocks).length === 0) return null
if (!activeWorkflowId) return null
const workflowStore = useWorkflowStore.getState()
const { blocks, edges, loops, parallels, subBlockValues } = regenerateBlockIds(
clipboard.blocks,
clipboard.edges,
clipboard.loops,
clipboard.parallels,
clipboard.subBlockValues,
positionOffset,
workflowStore.blocks,
getUniqueBlockName
)
return { blocks, edges, loops, parallels, subBlockValues }
},
hasClipboard: () => {
const { clipboard } = get()
return clipboard !== null && Object.keys(clipboard.blocks).length > 0
},
clearClipboard: () => {
set({ clipboard: null })
},
setPendingSelection: (blockIds: string[]) => {
set((state) => ({
pendingSelection: [...(state.pendingSelection ?? []), ...blockIds],
}))
},
clearPendingSelection: () => {
set({ pendingSelection: null })
},
}),
{ name: 'workflow-registry' }
)
)
@@ -0,0 +1,65 @@
import type { Edge } from 'reactflow'
import type { BlockState, Loop, Parallel } from '@/stores/workflows/workflow/types'
interface ClipboardData {
blocks: Record<string, BlockState>
edges: Edge[]
subBlockValues: Record<string, Record<string, unknown>>
loops: Record<string, Loop>
parallels: Record<string, Parallel>
timestamp: number
}
export interface WorkflowMetadata {
id: string
name: string
lastModified: Date
createdAt: Date
description?: string
workspaceId?: string
folderId?: string | null
sortOrder: number
archivedAt?: Date | null
locked?: boolean
}
export type HydrationPhase = 'idle' | 'creating' | 'state-loading' | 'ready' | 'error'
export interface HydrationState {
phase: HydrationPhase
workspaceId: string | null
workflowId: string | null
requestId: string | null
error: string | null
}
interface WorkflowRegistryState {
activeWorkflowId: string | null
error: string | null
hydration: HydrationState
clipboard: ClipboardData | null
pendingSelection: string[] | null
}
interface WorkflowRegistryActions {
setActiveWorkflow: (id: string) => Promise<void>
loadWorkflowState: (workflowId: string) => Promise<void>
switchToWorkspace: (id: string) => void
markWorkflowCreating: (workflowId: string) => void
markWorkflowCreated: (workflowId: string | null) => void
copyBlocks: (blockIds: string[]) => void
preparePasteData: (positionOffset?: { x: number; y: number }) => {
blocks: Record<string, BlockState>
edges: Edge[]
loops: Record<string, Loop>
parallels: Record<string, Parallel>
subBlockValues: Record<string, Record<string, unknown>>
} | null
hasClipboard: () => boolean
clearClipboard: () => void
setPendingSelection: (blockIds: string[]) => void
clearPendingSelection: () => void
logout: () => void
}
export type WorkflowRegistry = WorkflowRegistryState & WorkflowRegistryActions
+420
View File
@@ -0,0 +1,420 @@
import { randomItem } from '@sim/utils/random'
// Cosmos-themed adjectives and nouns for creative workflow names (max 9 chars each)
const ADJECTIVES = [
// Light & Luminosity
'Radiant',
'Luminous',
'Blazing',
'Glowing',
'Bright',
'Gleaming',
'Shining',
'Lustrous',
'Flaring',
'Vivid',
'Dazzling',
'Beaming',
'Brilliant',
'Lit',
'Ablaze',
// Celestial Descriptors
'Stellar',
'Cosmic',
'Astral',
'Galactic',
'Nebular',
'Orbital',
'Lunar',
'Solar',
'Starlit',
'Heavenly',
'Celestial',
'Sidereal',
'Planetary',
'Starry',
'Spacial',
// Scale & Magnitude
'Infinite',
'Vast',
'Boundless',
'Immense',
'Colossal',
'Titanic',
'Massive',
'Grand',
'Supreme',
'Ultimate',
'Epic',
'Enormous',
'Gigantic',
'Limitless',
'Total',
// Temporal
'Eternal',
'Ancient',
'Timeless',
'Enduring',
'Ageless',
'Immortal',
'Primal',
'Nascent',
'First',
'Elder',
'Lasting',
'Undying',
'Perpetual',
'Final',
'Prime',
// Movement & Energy
'Sidbuck',
'Swift',
'Drifting',
'Spinning',
'Surging',
'Pulsing',
'Soaring',
'Racing',
'Falling',
'Rising',
'Circling',
'Streaking',
'Hurtling',
'Floating',
'Orbiting',
'Spiraling',
// Colors of Space
'Crimson',
'Azure',
'Violet',
'Indigo',
'Amber',
'Sapphire',
'Obsidian',
'Silver',
'Golden',
'Scarlet',
'Cobalt',
'Emerald',
'Ruby',
'Onyx',
'Ivory',
// Physical Properties
'Magnetic',
'Quantum',
'Thermal',
'Photonic',
'Ionic',
'Plasma',
'Spectral',
'Charged',
'Polar',
'Dense',
'Atomic',
'Nuclear',
'Electric',
'Kinetic',
'Static',
// Atmosphere & Mystery
'Ethereal',
'Mystic',
'Phantom',
'Shadow',
'Silent',
'Distant',
'Hidden',
'Veiled',
'Fading',
'Arcane',
'Cryptic',
'Obscure',
'Dim',
'Dusky',
'Shrouded',
// Temperature & State
'Frozen',
'Burning',
'Molten',
'Volatile',
'Icy',
'Fiery',
'Cool',
'Warm',
'Cold',
'Hot',
'Searing',
'Frigid',
'Scalding',
'Chilled',
'Heated',
// Power & Force
'Mighty',
'Fierce',
'Raging',
'Wild',
'Serene',
'Tranquil',
'Harmonic',
'Resonant',
'Steady',
'Bold',
'Potent',
'Violent',
'Calm',
'Furious',
'Forceful',
// Texture & Form
'Smooth',
'Jagged',
'Fractured',
'Solid',
'Hollow',
'Curved',
'Sharp',
'Fluid',
'Rigid',
'Warped',
// Rare & Precious
'Noble',
'Pure',
'Rare',
'Pristine',
'Flawless',
'Unique',
'Exotic',
'Sacred',
'Divine',
'Hallowed',
]
const NOUNS = [
// Stars & Stellar Objects
'Star',
'Sun',
'Pulsar',
'Quasar',
'Magnetar',
'Nova',
'Supernova',
'Hypernova',
'Neutron',
'Dwarf',
'Giant',
'Protostar',
'Blazar',
'Cepheid',
'Binary',
// Galaxies & Clusters
'Galaxy',
'Nebula',
'Cluster',
'Void',
'Filament',
'Halo',
'Bulge',
'Spiral',
'Ellipse',
'Arm',
'Disk',
'Shell',
'Remnant',
'Cloud',
'Dust',
// Planets & Moons
'Planet',
'Moon',
'World',
'Exoplanet',
'Jovian',
'Titan',
'Europa',
'Io',
'Callisto',
'Ganymede',
'Triton',
'Phobos',
'Deimos',
'Enceladus',
'Charon',
// Small Bodies
'Comet',
'Meteor',
'Asteroid',
'Meteorite',
'Bolide',
'Fireball',
'Iceball',
'Plutino',
'Centaur',
'Trojan',
'Shard',
'Fragment',
'Debris',
'Rock',
'Ice',
// Constellations & Myths
'Orion',
'Andromeda',
'Perseus',
'Pegasus',
'Phoenix',
'Draco',
'Cygnus',
'Aquila',
'Lyra',
'Vega',
'Centaurus',
'Hydra',
'Sirius',
'Polaris',
'Altair',
// Celestial Phenomena
'Eclipse',
'Aurora',
'Corona',
'Flare',
'Storm',
'Vortex',
'Jet',
'Burst',
'Pulse',
'Wave',
'Ripple',
'Shimmer',
'Glow',
'Flash',
'Spark',
// Cosmic Structures
'Horizon',
'Zenith',
'Nadir',
'Apex',
'Meridian',
'Equinox',
'Solstice',
'Transit',
'Aphelion',
'Orbit',
'Axis',
'Pole',
'Equator',
'Limb',
'Arc',
// Space & Dimensions
'Cosmos',
'Universe',
'Dimension',
'Realm',
'Expanse',
'Infinity',
'Continuum',
'Manifold',
'Abyss',
'Ether',
'Vacuum',
'Space',
'Fabric',
'Plane',
'Domain',
// Energy & Particles
'Photon',
'Neutrino',
'Proton',
'Electron',
'Positron',
'Quark',
'Boson',
'Fermion',
'Tachyon',
'Graviton',
'Meson',
'Gluon',
'Lepton',
'Muon',
'Pion',
// Regions & Zones
'Sector',
'Quadrant',
'Zone',
'Belt',
'Ring',
'Field',
'Stream',
'Current',
'Wake',
'Region',
'Frontier',
'Border',
'Edge',
'Margin',
'Rim',
// Navigation & Discovery
'Beacon',
'Signal',
'Probe',
'Voyager',
'Pioneer',
'Seeker',
'Wanderer',
'Nomad',
'Drifter',
'Scout',
'Explorer',
'Ranger',
'Surveyor',
'Sentinel',
'Watcher',
// Portals & Passages
'Gateway',
'Portal',
'Nexus',
'Bridge',
'Conduit',
'Channel',
'Passage',
'Rift',
'Warp',
'Fold',
'Tunnel',
'Crossing',
'Link',
'Path',
'Route',
// Core & Systems
'Core',
'Matrix',
'Lattice',
'Network',
'Circuit',
'Array',
'Reactor',
'Engine',
'Forge',
'Crucible',
'Hub',
'Node',
'Kernel',
'Center',
'Heart',
// Cosmic Objects
'Crater',
'Rift',
'Chasm',
'Canyon',
'Peak',
'Ridge',
'Basin',
'Plateau',
'Valley',
'Trench',
]
/**
* Generates a creative workflow name using random adjectives and nouns
* @returns A creative workflow name like "blazing-phoenix" or "crystal-dragon"
*/
export function generateCreativeWorkflowName(): string {
const adjective = randomItem(ADJECTIVES)
const noun = randomItem(NOUNS)
return `${adjective.toLowerCase()}-${noun.toLowerCase()}`
}