@open-mercato/core 0.10.1-develop.7736.1.88e07906bf → 0.10.1-develop.7741.1.61a2652671

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/generated/entities/api_key/index.js +2 -0
  3. package/dist/generated/entities/api_key/index.js.map +2 -2
  4. package/dist/generated/entity-fields-registry.js +1 -0
  5. package/dist/generated/entity-fields-registry.js.map +2 -2
  6. package/dist/modules/api_keys/data/entities.js +7 -0
  7. package/dist/modules/api_keys/data/entities.js.map +2 -2
  8. package/dist/modules/api_keys/migrations/Migration20261012202110_api_keys.js +30 -0
  9. package/dist/modules/api_keys/migrations/Migration20261012202110_api_keys.js.map +7 -0
  10. package/dist/modules/api_keys/services/apiKeyService.js +31 -9
  11. package/dist/modules/api_keys/services/apiKeyService.js.map +2 -2
  12. package/dist/modules/workflows/api/portal/tasks/[id]/complete/route.js +1 -1
  13. package/dist/modules/workflows/api/portal/tasks/[id]/complete/route.js.map +2 -2
  14. package/dist/modules/workflows/api/tasks/[id]/complete/route.js +7 -1
  15. package/dist/modules/workflows/api/tasks/[id]/complete/route.js.map +2 -2
  16. package/dist/modules/workflows/backend/tasks/[id]/page.js +2 -1
  17. package/dist/modules/workflows/backend/tasks/[id]/page.js.map +2 -2
  18. package/dist/modules/workflows/lib/instance-status.js +16 -0
  19. package/dist/modules/workflows/lib/instance-status.js.map +7 -0
  20. package/dist/modules/workflows/lib/task-handler.js +16 -8
  21. package/dist/modules/workflows/lib/task-handler.js.map +2 -2
  22. package/dist/modules/workflows/lib/task-sla.js +10 -0
  23. package/dist/modules/workflows/lib/task-sla.js.map +2 -2
  24. package/dist/modules/workflows/lib/workflow-executor.js +29 -1
  25. package/dist/modules/workflows/lib/workflow-executor.js.map +2 -2
  26. package/generated/entities/api_key/index.ts +1 -0
  27. package/generated/entity-fields-registry.ts +1 -0
  28. package/package.json +7 -7
  29. package/src/modules/api_keys/data/entities.ts +25 -1
  30. package/src/modules/api_keys/migrations/.snapshot-open-mercato.json +234 -68
  31. package/src/modules/api_keys/migrations/Migration20261012202110_api_keys.ts +47 -0
  32. package/src/modules/api_keys/services/apiKeyService.ts +55 -9
  33. package/src/modules/auth/i18n/de.json +3 -0
  34. package/src/modules/auth/i18n/en.json +3 -0
  35. package/src/modules/auth/i18n/es.json +3 -0
  36. package/src/modules/auth/i18n/ko.json +3 -0
  37. package/src/modules/auth/i18n/pl.json +3 -0
  38. package/src/modules/workflows/api/portal/tasks/[id]/complete/route.ts +5 -1
  39. package/src/modules/workflows/api/tasks/[id]/complete/route.ts +7 -1
  40. package/src/modules/workflows/backend/tasks/[id]/page.tsx +6 -2
  41. package/src/modules/workflows/i18n/de.json +2 -0
  42. package/src/modules/workflows/i18n/en.json +2 -0
  43. package/src/modules/workflows/i18n/es.json +2 -0
  44. package/src/modules/workflows/i18n/ko.json +2 -0
  45. package/src/modules/workflows/i18n/pl.json +2 -0
  46. package/src/modules/workflows/lib/instance-status.ts +29 -0
  47. package/src/modules/workflows/lib/task-handler.ts +18 -10
  48. package/src/modules/workflows/lib/task-sla.ts +13 -0
  49. package/src/modules/workflows/lib/workflow-executor.ts +44 -1
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../src/modules/workflows/backend/tasks/%5Bid%5D/page.tsx"],
4
- "sourcesContent": ["\"use client\"\n\n/**\n * Task detail \u2014 decision beside context (spec \u00A76.2).\n *\n * > \"Task detail = decision + context side-by-side: left \u2014 instructions, form,\n * > decision buttons; right \u2014 bound entity card(s) with deep links. [\u2026] After\n * > completion: 'next task' \u2014 a one-click claim-next affordance.\"\n *\n * Three things about this page are load-bearing:\n *\n * - **The decision buttons are DERIVED, never stored.** The API re-resolves them\n * per request from the instance's pinned definition\n * (`lib/task-decisions.ts`), so no column carries the button list and a\n * definition that gains a decision immediately offers it. Pressing one both\n * completes the task and selects the outgoing route by the decision's durable\n * `transitionId`; WHICH button was pressed is recorded server-side as part of\n * the completion payload.\n * - **Every write goes through `useGuardedMutation`.** This page cannot use\n * `CrudForm`, which is exactly the case the project rule names, and the\n * mutation context carries `retryLastMutation` so the shared conflict/retry\n * bar can re-run the last attempt.\n * - **Status is never colour-only** \u2014 every badge pairs its design-system token\n * with an icon and a translated label (`lib/work-inbox/presentation.ts`).\n * - **Every affordance is gated by what the SERVER said** (`canComplete` /\n * `canClaim` / `canRelease` / `canReassign`, decided by `lib/task-visibility.ts`\n * and sent on the detail response). The page never re-derives \u00A76.4: a second\n * copy of that rule is how a `workflows.tasks.view_all` administrator came to\n * be offered a Complete button that always answered 409. When an action is\n * unavailable the page states the server's reason and, where \u00A76.4 names one,\n * offers the remedy \u2014 reassignment.\n */\n\nimport * as React from 'react'\nimport Link from 'next/link'\nimport { useRouter } from 'next/navigation'\nimport { Page, PageBody } from '@open-mercato/ui/backend/Page'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { FormHeader } from '@open-mercato/ui/backend/forms'\nimport { Spinner } from '@open-mercato/ui/primitives/spinner'\nimport { Separator } from '@open-mercato/ui/primitives/separator'\nimport { JsonDisplay } from '@open-mercato/ui/backend/JsonDisplay'\nimport { apiCall, withScopedApiRequestHeaders } from '@open-mercato/ui/backend/utils/apiCall'\nimport { buildOptimisticLockHeader } from '@open-mercato/ui/backend/utils/optimisticLock'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'\nimport { InjectionSpot } from '@open-mercato/ui/backend/injection/InjectionSpot'\nimport { useQuery, useQueryClient } from '@tanstack/react-query'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { MobileTaskForm } from '../../../components/mobile/MobileTaskForm'\nimport { StatusChip } from '../../../components/work-inbox/StatusChip'\nimport { TaskActionNotice } from '../../../components/work-inbox/TaskActionNotice'\nimport {\n ReassignTaskDialog,\n type ReassignTaskTarget,\n} from '../../../components/work-inbox/ReassignTaskDialog'\nimport { TaskFormFields } from '../../../components/work-inbox/TaskFormFields'\nimport { useIsMobile } from '@open-mercato/ui/hooks/useIsMobile'\nimport type { UserTaskDecision, UserTaskResponse, UserTaskStatus } from '../../../data/types'\nimport { RecordNotFoundState, ErrorMessage } from '@open-mercato/ui/backend/detail'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport {\n TASK_DETAIL_INJECTION_SPOT_ID,\n WORK_INBOX_HREF,\n buildWorkInboxItemHref,\n} from '../../../lib/work-inbox/navigation'\nimport {\n EntityContextPanel,\n bindingsToEntityContextItems,\n} from '../../../components/work-inbox/EntityContextPanel'\nimport {\n WORK_INBOX_OVERDUE_TONE,\n describeTaskActionBlock,\n describeWorkInboxPriority,\n describeWorkInboxStatus,\n} from '../../../lib/work-inbox/presentation'\nimport { normalizeWorkInboxPriority } from '../../../lib/work-inbox/provider'\nimport { flattenTaskText } from '../../../lib/task-resolution'\nimport { readRecordedDecision } from '../../../lib/task-decisions'\nimport { normalizeTaskFormSchema } from '../../../lib/task-form-schema'\n\nconst logger = createLogger('workflows')\n\ntype TaskMutationContext = {\n formId: string\n taskId: string\n resourceKind: string\n resourceId: string\n retryLastMutation: () => Promise<boolean>\n}\n\nfunction decisionButtonVariant(style: UserTaskDecision['style']): 'default' | 'outline' | 'destructive' {\n if (style === 'destructive') return 'destructive'\n if (style === 'secondary') return 'outline'\n return 'default'\n}\n\nexport default function UserTaskDetailPage({ params }: { params: { id: string } }) {\n const router = useRouter()\n const t = useT()\n const queryClient = useQueryClient()\n const isMobile = useIsMobile()\n const [formData, setFormData] = React.useState<Record<string, string | number | boolean>>({})\n const [comments, setComments] = React.useState('')\n const [submitting, setSubmitting] = React.useState(false)\n const [reassignOpen, setReassignOpen] = React.useState(false)\n // Tracks the first required field that failed validation so we can mark the\n // field with aria-invalid + a red ring (Radix Select can't carry HTML\n // `required`, so we enforce constraint validation in JS instead).\n const [invalidField, setInvalidField] = React.useState<string | null>(null)\n\n const { runMutation, retryLastMutation } = useGuardedMutation<TaskMutationContext>({\n contextId: `workflows-task:${params.id}`,\n blockedMessage: t('ui.forms.flash.saveBlocked', 'Save blocked by validation'),\n })\n\n const { data: task, isLoading, error } = useQuery({\n queryKey: ['workflow-task', params.id],\n queryFn: async () => {\n const result = await apiCall<{ data: UserTaskResponse }>(\n `/api/workflows/tasks/${params.id}`\n )\n if (!result.ok) {\n const httpErr = new Error(\n result.status === 404\n ? t('workflows.tasks.detail.notFound', 'Task not found')\n : t('workflows.tasks.detail.loadFailed', 'Failed to load task')\n ) as Error & { status: number }\n httpErr.status = result.status\n throw httpErr\n }\n return result.result?.data ?? null\n },\n })\n\n const mutationContext = React.useMemo<TaskMutationContext>(\n () => ({\n formId: `workflows-task:${params.id}`,\n taskId: params.id,\n resourceKind: 'workflows.user_task',\n resourceId: params.id,\n retryLastMutation,\n }),\n [params.id, retryLastMutation],\n )\n\n // Injection spot for modules that OWN what a task is about (spec \u00A77.6). It\n // is a CONTRACT SURFACE (BACKWARD_COMPATIBILITY.md \u00A76): the id is frozen once\n // a third party mounts a widget on it.\n const taskInjectionContext = React.useMemo(\n () => ({\n taskId: params.id,\n workflowInstanceId: task?.workflowInstanceId ?? null,\n status: task?.status ?? null,\n formSchema: task?.formSchema ?? null,\n formData: task?.formData ?? null,\n retryLastMutation,\n }),\n [params.id, retryLastMutation, task?.formData, task?.formSchema, task?.status, task?.workflowInstanceId],\n )\n\n const refreshTask = React.useCallback(() => {\n queryClient.invalidateQueries({ queryKey: ['workflow-task', params.id] })\n }, [params.id, queryClient])\n\n const handleFieldChange = (fieldName: string, value: string | number | boolean) => {\n setFormData(prev => ({\n ...prev,\n [fieldName]: value,\n }))\n // Clear invalid state once the user touches the offending field.\n if (invalidField === fieldName) setInvalidField(null)\n }\n\n const validateRequiredFields = React.useCallback((): boolean => {\n // Normalized, so a Studio-authored `{ fields: [...] }` schema is validated\n // by the same rule the server enforces (`lib/task-form-schema.ts`).\n const normalizedSchema = normalizeTaskFormSchema(task?.formSchema)\n if (!normalizedSchema?.required) return true\n for (const requiredField of normalizedSchema.required) {\n if (!formData[requiredField] || formData[requiredField] === '') {\n const fieldSchema = normalizedSchema.properties?.[requiredField]\n const fieldLabel = fieldSchema?.title ?? requiredField\n flash(t('workflows.tasks.detail.validation.requiredField', { field: fieldLabel }), 'error')\n setInvalidField(requiredField)\n // Scroll + focus the trigger so the user sees what's missing.\n if (typeof document !== 'undefined') {\n const trigger = document.getElementById(requiredField)\n trigger?.scrollIntoView({ behavior: 'smooth', block: 'center' })\n trigger?.focus()\n }\n return false\n }\n }\n setInvalidField(null)\n return true\n }, [formData, task, t])\n\n const completeTask = React.useCallback(\n async (decisionId?: string) => {\n if (!task) return\n if (!validateRequiredFields()) return\n\n setSubmitting(true)\n try {\n const result = await runMutation({\n operation: () =>\n apiCall(`/api/workflows/tasks/${params.id}/complete`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n formData,\n comments: comments || undefined,\n ...(decisionId ? { decisionId } : {}),\n }),\n }),\n context: mutationContext,\n mutationPayload: { formData, comments, decisionId },\n })\n\n if (result.ok) {\n flash(t('workflows.tasks.messages.completed'), 'success')\n refreshTask()\n } else {\n const failure = result.result as { error?: string } | null\n flash(failure?.error || t('workflows.tasks.messages.completeFailed'), 'error')\n }\n } catch (err) {\n logger.error('Error completing task', { err })\n flash(t('workflows.tasks.messages.completeFailed'), 'error')\n } finally {\n setSubmitting(false)\n }\n },\n [comments, formData, mutationContext, params.id, refreshTask, runMutation, t, task, validateRequiredFields],\n )\n\n const handleSubmit = React.useCallback(\n (event: React.FormEvent) => {\n event.preventDefault()\n void completeTask()\n },\n [completeTask],\n )\n\n const handleClaim = React.useCallback(async () => {\n setSubmitting(true)\n try {\n const result = await runMutation({\n operation: () => apiCall(`/api/workflows/tasks/${params.id}/claim`, { method: 'POST' }),\n context: mutationContext,\n mutationPayload: { action: 'claim' },\n })\n if (result.ok) {\n flash(t('workflows.tasks.messages.claimed'), 'success')\n refreshTask()\n } else {\n flash(t('workflows.tasks.messages.claimFailed'), 'error')\n }\n } catch (err) {\n logger.error('Error claiming task', { err })\n flash(t('workflows.tasks.messages.claimFailed'), 'error')\n } finally {\n setSubmitting(false)\n }\n }, [mutationContext, params.id, refreshTask, runMutation, t])\n\n const handleRelease = React.useCallback(async () => {\n setSubmitting(true)\n try {\n const result = await runMutation({\n operation: () => apiCall(`/api/workflows/tasks/${params.id}/unclaim`, { method: 'POST' }),\n context: mutationContext,\n mutationPayload: { action: 'unclaim' },\n })\n if (result.ok) {\n flash(t('workflows.workInbox.messages.released'), 'success')\n refreshTask()\n } else {\n flash(t('workflows.workInbox.messages.releaseFailed'), 'error')\n }\n } catch (err) {\n logger.error('Error releasing task', { err })\n flash(t('workflows.workInbox.messages.releaseFailed'), 'error')\n } finally {\n setSubmitting(false)\n }\n }, [mutationContext, params.id, refreshTask, runMutation, t])\n\n /**\n * Reassignment is the remedy \u00A76.4 names for every ownership refusal, and the\n * only way to rescue a task authored with no assignee, no queue and no claim.\n *\n * The endpoint honours the optimistic-lock header explicitly (`UserTask` is\n * outside the curated CRUD lock list), so the row's `updatedAt` rides along:\n * two administrators racing to take the same task get a 409 rather than a\n * silent last-write-wins.\n */\n const handleReassign = React.useCallback(\n async (target: ReassignTaskTarget) => {\n if (!task) return\n setSubmitting(true)\n try {\n const result = await runMutation({\n operation: () =>\n withScopedApiRequestHeaders(buildOptimisticLockHeader(task.updatedAt), () =>\n apiCall(`/api/workflows/tasks/${params.id}/reassign`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(target),\n }),\n ),\n context: mutationContext,\n mutationPayload: { action: 'reassign', ...target },\n })\n if (result.ok) {\n setReassignOpen(false)\n flash(t('workflows.tasks.messages.reassigned'), 'success')\n refreshTask()\n } else {\n const failure = result.result as { error?: string } | null\n flash(failure?.error || t('workflows.tasks.messages.reassignFailed'), 'error')\n }\n } catch (err) {\n logger.error('Error reassigning task', { err })\n flash(t('workflows.tasks.messages.reassignFailed'), 'error')\n } finally {\n setSubmitting(false)\n }\n },\n [mutationContext, params.id, refreshTask, runMutation, t, task],\n )\n\n const handleNextTask = React.useCallback(async () => {\n setSubmitting(true)\n try {\n const result = await runMutation({\n operation: () =>\n apiCall<{ data: { id: string } | null }>('/api/workflows/work-inbox/next', {\n method: 'POST',\n }),\n context: mutationContext,\n mutationPayload: { action: 'claim-next' },\n })\n if (!result.ok) {\n flash(t('workflows.tasks.messages.claimFailed'), 'error')\n return\n }\n const claimed = result.result?.data ?? null\n if (!claimed) {\n flash(t('workflows.workInbox.messages.nothingClaimable'), 'info')\n return\n }\n flash(t('workflows.tasks.messages.claimed'), 'success')\n router.push(buildWorkInboxItemHref(claimed.id))\n } catch (err) {\n logger.error('Error claiming the next task', { err })\n flash(t('workflows.tasks.messages.claimFailed'), 'error')\n } finally {\n setSubmitting(false)\n }\n }, [mutationContext, router, runMutation, t])\n\n const getStatusBadgeClass = React.useCallback(\n (status: UserTaskStatus) => describeWorkInboxStatus(status).badge,\n [],\n )\n\n if (isLoading) {\n return (\n <Page>\n <PageBody>\n <div className=\"flex items-center justify-center py-12\">\n <Spinner />\n <span className=\"ml-3 text-muted-foreground\">{t('workflows.tasks.detail.loading')}</span>\n </div>\n </PageBody>\n </Page>\n )\n }\n\n const isNotFound = !isLoading && (error as (Error & { status?: number }) | null)?.status === 404\n\n if (isNotFound) {\n return (\n <Page>\n <PageBody>\n <RecordNotFoundState\n label={t('workflows.tasks.detail.notFound', 'Task not found')}\n backHref={WORK_INBOX_HREF}\n backLabel={t('workflows.tasks.detail.backToList', 'Back to Tasks')}\n />\n </PageBody>\n </Page>\n )\n }\n\n if (error || !task) {\n return (\n <Page>\n <PageBody>\n <ErrorMessage\n label={(error as Error | null)?.message ?? t('workflows.tasks.detail.loadFailed', 'Failed to load task')}\n action={\n <Button asChild variant=\"outline\" size=\"sm\">\n <Link href={WORK_INBOX_HREF}>{t('workflows.tasks.detail.backToList', 'Back to Tasks')}</Link>\n </Button>\n }\n />\n </PageBody>\n </Page>\n )\n }\n\n const isCompletable = task.status === 'PENDING' || task.status === 'IN_PROGRESS'\n const isOverdue = Boolean(task.dueDate) && new Date(task.dueDate as string) < new Date() && isCompletable\n const statusDescriptor = describeWorkInboxStatus(task.status)\n const priority = normalizeWorkInboxPriority(task.priority)\n const priorityDescriptor = describeWorkInboxPriority(priority)\n const decisions = task.decisions ?? []\n const contextItems = bindingsToEntityContextItems(task.entityBindings, {\n noLink: t('workflows.tasks.detail.records.noLink'),\n })\n const recordedDecisionId = readRecordedDecision(task.formData)\n const recordedDecision = recordedDecisionId\n ? decisions.find((decision) => decision.id === recordedDecisionId) ?? null\n : null\n // A task nobody owns yet, offered to a role queue, is the one this operator\n // can take. An individually assigned task is already theirs to finish.\n //\n // Kept ONLY as the compatibility fallback below \u2014 it is the pre-change\n // behaviour, restored verbatim when the server sends no `canClaim`.\n const legacyClaimable =\n task.status === 'PENDING' &&\n !task.assignedTo &&\n !task.claimedBy &&\n (task.assignedToRoles?.length ?? 0) > 0\n\n /*\n * The server decides; the page renders. `??` is the additive-compatibility\n * fallback for a payload written before these fields existed\n * (BACKWARD_COMPATIBILITY.md \u00A77) \u2014 it restores exactly the pre-change\n * behaviour and is never a second copy of the \u00A76.4 rule. Release and reassign\n * are new affordances with no legacy behaviour to preserve, so an old payload\n * simply does not offer them.\n */\n const canComplete = task.canComplete ?? isCompletable\n const canClaim = task.canClaim ?? legacyClaimable\n const canRelease = task.canRelease ?? false\n const canReassign = task.canReassign ?? false\n const blockDescriptor = canComplete\n ? null\n : describeTaskActionBlock(task.actBlockedReason ?? 'not-workable')\n\n if (isMobile) {\n return (\n <Page>\n <PageBody>\n <div className=\"space-y-4\">\n <FormHeader\n mode=\"detail\"\n backHref={WORK_INBOX_HREF}\n backLabel={t('workflows.tasks.backToList', 'Back to tasks')}\n />\n {blockDescriptor ? (\n <TaskActionNotice\n descriptor={blockDescriptor}\n canReassign={canReassign}\n disabled={submitting}\n onReassign={() => setReassignOpen(true)}\n />\n ) : null}\n <MobileTaskForm\n task={task}\n formData={formData}\n comments={comments}\n submitting={submitting}\n isCompletable={canComplete}\n isOverdue={isOverdue}\n onFieldChange={handleFieldChange}\n onCommentsChange={setComments}\n onSubmit={handleSubmit}\n onCancel={() => router.push(WORK_INBOX_HREF)}\n getStatusBadgeClass={getStatusBadgeClass}\n decisions={decisions}\n onDecision={(decisionId) => void completeTask(decisionId)}\n />\n </div>\n <ReassignTaskDialog\n open={reassignOpen}\n submitting={submitting}\n onOpenChange={setReassignOpen}\n onSubmit={(target) => void handleReassign(target)}\n />\n </PageBody>\n </Page>\n )\n }\n\n return (\n <Page>\n <PageBody>\n <div className=\"space-y-6\">\n <FormHeader\n mode=\"detail\"\n backHref={WORK_INBOX_HREF}\n backLabel={t('workflows.tasks.backToList', 'Back to tasks')}\n entityTypeLabel={t('workflows.tasks.detail.type', 'User task')}\n title={task.taskName}\n subtitle={task.description || undefined}\n statusBadge={\n <span className=\"inline-flex items-center gap-2\">\n <StatusChip\n descriptor={statusDescriptor}\n label={t(`workflows.tasks.statuses.${task.status}`, task.status)}\n />\n {priorityDescriptor && priority ? (\n <StatusChip\n descriptor={priorityDescriptor}\n label={t(`workflows.workInbox.priorities.${priority}`, priority)}\n />\n ) : null}\n {isOverdue ? (\n <StatusChip\n descriptor={WORK_INBOX_OVERDUE_TONE}\n label={t('workflows.tasks.overdue')}\n />\n ) : null}\n </span>\n }\n />\n\n <div className=\"grid grid-cols-1 gap-6 lg:grid-cols-3\">\n {/* Left \u2014 instructions, form, decisions */}\n <div className=\"space-y-6 lg:col-span-2\">\n {isOverdue ? (\n <div className=\"bg-status-error-bg border border-status-error-border rounded-lg p-3\">\n <p className=\"text-sm text-status-error-text font-medium\">\n {t('workflows.tasks.detail.overdueWarning')}\n </p>\n </div>\n ) : null}\n\n {task.description ? (\n <div className=\"rounded-lg border border-border p-4\">\n <h2 className=\"text-sm font-semibold mb-2\">\n {t('workflows.tasks.detail.sections.instructions')}\n </h2>\n <p className=\"text-sm text-muted-foreground whitespace-pre-wrap\">{task.description}</p>\n </div>\n ) : null}\n\n {/*\n Where a module that OWNS the thing a task is about renders it in\n its own vocabulary. The agent orchestrator mounts the proposal\n draft card here (spec \u00A77.6): this page can only walk a task's\n `formSchema` generically, and a proposal rendered that way is a\n key/value dump of the mutation an operator is being asked to\n approve. Additive \u2014 no widget, nothing renders.\n */}\n <InjectionSpot spotId={TASK_DETAIL_INJECTION_SPOT_ID} context={taskInjectionContext} />\n\n {canClaim ? (\n <div className=\"rounded-lg border border-border bg-muted/50 p-4 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between\">\n <p className=\"text-sm text-muted-foreground\">\n {t('workflows.tasks.detail.claimPrompt')}\n </p>\n <Button\n type=\"button\"\n data-testid=\"task-claim\"\n disabled={submitting}\n onClick={() => void handleClaim()}\n >\n {t('workflows.tasks.actions.claim')}\n </Button>\n </div>\n ) : null}\n\n {canRelease ? (\n <div className=\"rounded-lg border border-border bg-muted/50 p-4 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between\">\n <p className=\"text-sm text-muted-foreground\">\n {t('workflows.tasks.detail.releasePrompt')}\n </p>\n <Button\n type=\"button\"\n variant=\"outline\"\n data-testid=\"task-release\"\n disabled={submitting}\n onClick={() => void handleRelease()}\n >\n {t('workflows.tasks.actions.unclaim')}\n </Button>\n </div>\n ) : null}\n\n {blockDescriptor ? (\n <TaskActionNotice\n descriptor={blockDescriptor}\n canReassign={canReassign}\n disabled={submitting}\n onReassign={() => setReassignOpen(true)}\n />\n ) : null}\n\n {/*\n Offered even when the task IS completable: an administrator may\n legitimately hand a row to somebody else, and the endpoint gates\n on visibility rather than ownership precisely so it can.\n */}\n {canComplete && canReassign ? (\n <div className=\"flex justify-end\">\n <Button\n type=\"button\"\n variant=\"outline\"\n data-testid=\"task-reassign\"\n disabled={submitting}\n onClick={() => setReassignOpen(true)}\n >\n {t('workflows.tasks.actions.reassign')}\n </Button>\n </div>\n ) : null}\n\n {canComplete ? (\n <form onSubmit={handleSubmit} className=\"space-y-6\">\n <TaskFormFields\n formSchema={task.formSchema}\n values={formData}\n invalidField={invalidField}\n disabled={submitting}\n onFieldChange={handleFieldChange}\n formKey={task.formKey}\n taskId={task.id}\n taskName={task.taskName}\n entityBindings={task.entityBindings}\n decisions={decisions}\n onValuesChange={setFormData}\n />\n\n <Separator />\n\n <div className=\"space-y-2\">\n <label htmlFor=\"comments\" className=\"block text-sm font-medium text-foreground\">\n {t('workflows.tasks.detail.comments')} ({t('workflows.tasks.detail.optional')})\n </label>\n <textarea\n id=\"comments\"\n value={comments}\n onChange={(e) => setComments(e.target.value)}\n rows={3}\n className=\"w-full px-3 py-2 border border-border rounded-md focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\"\n placeholder={t('workflows.tasks.detail.commentsPlaceholder')}\n />\n </div>\n\n {decisions.length > 0 ? (\n <div className=\"space-y-3\" data-testid=\"task-decisions\">\n <h2 className=\"text-sm font-semibold\">\n {t('workflows.tasks.detail.sections.decisions')}\n </h2>\n <div className=\"flex flex-col gap-3 sm:flex-row sm:flex-wrap\">\n {decisions.map((decision) => (\n <Button\n key={decision.id}\n type=\"button\"\n data-testid={`task-decision-${decision.id}`}\n variant={decisionButtonVariant(decision.style)}\n disabled={submitting}\n onClick={() => void completeTask(decision.id)}\n className=\"w-full sm:w-auto\"\n >\n {flattenTaskText(decision.label) ?? decision.id}\n </Button>\n ))}\n </div>\n </div>\n ) : (\n <div className=\"flex flex-col gap-3 pt-4 sm:flex-row sm:items-center\">\n <Button type=\"submit\" data-testid=\"task-complete\" disabled={submitting} className=\"w-full sm:w-auto\">\n {submitting\n ? t('workflows.tasks.detail.submitting')\n : t('workflows.tasks.detail.completeTask')}\n </Button>\n <Button\n type=\"button\"\n variant=\"outline\"\n onClick={() => router.push(WORK_INBOX_HREF)}\n disabled={submitting}\n className=\"w-full sm:w-auto\"\n >\n {t('common.cancel')}\n </Button>\n </div>\n )}\n </form>\n ) : null}\n\n {task.status === 'COMPLETED' ? (\n <div className=\"space-y-4\">\n {recordedDecisionId ? (\n <div className=\"rounded-lg border border-border p-4\">\n <p className=\"text-sm text-muted-foreground\">\n {t('workflows.tasks.detail.decisionTaken')}:{' '}\n <span className=\"text-foreground font-medium\">\n {recordedDecision\n ? flattenTaskText(recordedDecision.label) ?? recordedDecisionId\n : recordedDecisionId}\n </span>\n </p>\n </div>\n ) : null}\n\n {task.formData ? (\n <JsonDisplay\n data={task.formData}\n title={t('workflows.tasks.detail.sections.submittedData')}\n maxInitialDepth={2}\n />\n ) : null}\n\n {task.comments ? (\n <div className=\"bg-muted/50 rounded-lg p-4\">\n <p className=\"text-sm font-medium text-foreground mb-2\">\n {t('workflows.tasks.detail.comments')}:\n </p>\n <p className=\"text-sm text-muted-foreground whitespace-pre-wrap\">{task.comments}</p>\n </div>\n ) : null}\n\n {/* Spec \u00A76.2: keep frontline flow going with one click. */}\n <div className=\"rounded-lg border border-border bg-muted/50 p-4 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between\">\n <p className=\"text-sm text-muted-foreground\">\n {t('workflows.tasks.detail.nextTaskPrompt')}\n </p>\n <div className=\"flex gap-2\">\n <Button\n type=\"button\"\n data-testid=\"task-next\"\n disabled={submitting}\n onClick={() => void handleNextTask()}\n >\n {t('workflows.workInbox.actions.claimNext')}\n </Button>\n <Button asChild variant=\"outline\">\n <Link href={WORK_INBOX_HREF}>{t('workflows.tasks.detail.backToList')}</Link>\n </Button>\n </div>\n </div>\n </div>\n ) : null}\n </div>\n\n {/* Right \u2014 the records this task is about, plus how it got here */}\n <div className=\"space-y-4\">\n <EntityContextPanel\n testId=\"task-context-records\"\n title={t('workflows.tasks.detail.sections.records')}\n items={contextItems}\n emptyLabel={t('workflows.tasks.detail.records.empty')}\n linkLabel={t('workflows.tasks.detail.records.open')}\n />\n\n <div className=\"rounded-lg border border-border p-4 space-y-2\">\n <h2 className=\"text-sm font-semibold\">\n {t('workflows.tasks.detail.sections.taskInfo')}\n </h2>\n <div className=\"space-y-1 text-sm\">\n <div>\n <span className=\"text-muted-foreground\">{t('workflows.tasks.fields.createdAt')}:</span>\n <span className=\"ml-2 text-foreground\">{new Date(task.createdAt).toLocaleString()}</span>\n </div>\n {task.dueDate ? (\n <div>\n <span className=\"text-muted-foreground\">{t('workflows.tasks.fields.dueDate')}:</span>\n <span\n className={`ml-2 ${isOverdue ? `${WORK_INBOX_OVERDUE_TONE.text} font-medium` : 'text-foreground'}`}\n >\n {new Date(task.dueDate).toLocaleString()}\n </span>\n </div>\n ) : null}\n {task.assignedTo ? (\n <div>\n <span className=\"text-muted-foreground\">{t('workflows.tasks.detail.assignedTo')}:</span>\n <span className=\"ml-2 text-foreground\">{task.assignedTo}</span>\n </div>\n ) : null}\n {task.assignedToRoles?.length ? (\n <div>\n <span className=\"text-muted-foreground\">{t('workflows.tasks.roles')}:</span>\n <span className=\"ml-2 text-foreground\">{task.assignedToRoles.join(', ')}</span>\n </div>\n ) : null}\n {task.claimedBy ? (\n <div>\n <span className=\"text-muted-foreground\">{t('workflows.tasks.claimedBy')}:</span>\n <span className=\"ml-2 text-foreground\">{task.claimedBy}</span>\n </div>\n ) : null}\n <div>\n <span className=\"text-muted-foreground\">\n {t('workflows.tasks.detail.workflowInstance')}:\n </span>\n <Link\n href={`/backend/instances/${task.workflowInstanceId}`}\n className=\"ml-2 text-primary hover:underline text-xs font-mono\"\n >\n {task.workflowInstanceId.slice(0, 8)}...\n </Link>\n </div>\n </div>\n </div>\n </div>\n </div>\n </div>\n\n <ReassignTaskDialog\n open={reassignOpen}\n submitting={submitting}\n onOpenChange={setReassignOpen}\n onSubmit={(target) => void handleReassign(target)}\n />\n </PageBody>\n </Page>\n )\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["\"use client\"\n\n/**\n * Task detail \u2014 decision beside context (spec \u00A76.2).\n *\n * > \"Task detail = decision + context side-by-side: left \u2014 instructions, form,\n * > decision buttons; right \u2014 bound entity card(s) with deep links. [\u2026] After\n * > completion: 'next task' \u2014 a one-click claim-next affordance.\"\n *\n * Three things about this page are load-bearing:\n *\n * - **The decision buttons are DERIVED, never stored.** The API re-resolves them\n * per request from the instance's pinned definition\n * (`lib/task-decisions.ts`), so no column carries the button list and a\n * definition that gains a decision immediately offers it. Pressing one both\n * completes the task and selects the outgoing route by the decision's durable\n * `transitionId`; WHICH button was pressed is recorded server-side as part of\n * the completion payload.\n * - **Every write goes through `useGuardedMutation`.** This page cannot use\n * `CrudForm`, which is exactly the case the project rule names, and the\n * mutation context carries `retryLastMutation` so the shared conflict/retry\n * bar can re-run the last attempt.\n * - **Status is never colour-only** \u2014 every badge pairs its design-system token\n * with an icon and a translated label (`lib/work-inbox/presentation.ts`).\n * - **Every affordance is gated by what the SERVER said** (`canComplete` /\n * `canClaim` / `canRelease` / `canReassign`, decided by `lib/task-visibility.ts`\n * and sent on the detail response). The page never re-derives \u00A76.4: a second\n * copy of that rule is how a `workflows.tasks.view_all` administrator came to\n * be offered a Complete button that always answered 409. When an action is\n * unavailable the page states the server's reason and, where \u00A76.4 names one,\n * offers the remedy \u2014 reassignment.\n */\n\nimport * as React from 'react'\nimport Link from 'next/link'\nimport { useRouter } from 'next/navigation'\nimport { Page, PageBody } from '@open-mercato/ui/backend/Page'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { FormHeader } from '@open-mercato/ui/backend/forms'\nimport { Spinner } from '@open-mercato/ui/primitives/spinner'\nimport { Separator } from '@open-mercato/ui/primitives/separator'\nimport { JsonDisplay } from '@open-mercato/ui/backend/JsonDisplay'\nimport { apiCall, withScopedApiRequestHeaders } from '@open-mercato/ui/backend/utils/apiCall'\nimport { buildOptimisticLockHeader } from '@open-mercato/ui/backend/utils/optimisticLock'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'\nimport { InjectionSpot } from '@open-mercato/ui/backend/injection/InjectionSpot'\nimport { useQuery, useQueryClient } from '@tanstack/react-query'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { MobileTaskForm } from '../../../components/mobile/MobileTaskForm'\nimport { StatusChip } from '../../../components/work-inbox/StatusChip'\nimport { TaskActionNotice } from '../../../components/work-inbox/TaskActionNotice'\nimport {\n ReassignTaskDialog,\n type ReassignTaskTarget,\n} from '../../../components/work-inbox/ReassignTaskDialog'\nimport { TaskFormFields } from '../../../components/work-inbox/TaskFormFields'\nimport { useIsMobile } from '@open-mercato/ui/hooks/useIsMobile'\nimport type { UserTaskDecision, UserTaskResponse, UserTaskStatus } from '../../../data/types'\nimport { RecordNotFoundState, ErrorMessage } from '@open-mercato/ui/backend/detail'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport {\n TASK_DETAIL_INJECTION_SPOT_ID,\n WORK_INBOX_HREF,\n buildWorkInboxItemHref,\n} from '../../../lib/work-inbox/navigation'\nimport {\n EntityContextPanel,\n bindingsToEntityContextItems,\n} from '../../../components/work-inbox/EntityContextPanel'\nimport {\n WORK_INBOX_OVERDUE_TONE,\n describeTaskActionBlock,\n describeWorkInboxPriority,\n describeWorkInboxStatus,\n} from '../../../lib/work-inbox/presentation'\nimport { normalizeWorkInboxPriority } from '../../../lib/work-inbox/provider'\nimport { flattenTaskText } from '../../../lib/task-resolution'\nimport { readRecordedDecision } from '../../../lib/task-decisions'\nimport { normalizeTaskFormSchema } from '../../../lib/task-form-schema'\n\nconst logger = createLogger('workflows')\n\ntype TaskMutationContext = {\n formId: string\n taskId: string\n resourceKind: string\n resourceId: string\n retryLastMutation: () => Promise<boolean>\n}\n\nfunction decisionButtonVariant(style: UserTaskDecision['style']): 'default' | 'outline' | 'destructive' {\n if (style === 'destructive') return 'destructive'\n if (style === 'secondary') return 'outline'\n return 'default'\n}\n\nexport default function UserTaskDetailPage({ params }: { params: { id: string } }) {\n const router = useRouter()\n const t = useT()\n const queryClient = useQueryClient()\n const isMobile = useIsMobile()\n const [formData, setFormData] = React.useState<Record<string, string | number | boolean>>({})\n const [comments, setComments] = React.useState('')\n const [submitting, setSubmitting] = React.useState(false)\n const [reassignOpen, setReassignOpen] = React.useState(false)\n // Tracks the first required field that failed validation so we can mark the\n // field with aria-invalid + a red ring (Radix Select can't carry HTML\n // `required`, so we enforce constraint validation in JS instead).\n const [invalidField, setInvalidField] = React.useState<string | null>(null)\n\n const { runMutation, retryLastMutation } = useGuardedMutation<TaskMutationContext>({\n contextId: `workflows-task:${params.id}`,\n blockedMessage: t('ui.forms.flash.saveBlocked', 'Save blocked by validation'),\n })\n\n const { data: task, isLoading, error } = useQuery({\n queryKey: ['workflow-task', params.id],\n queryFn: async () => {\n const result = await apiCall<{ data: UserTaskResponse }>(\n `/api/workflows/tasks/${params.id}`\n )\n if (!result.ok) {\n const httpErr = new Error(\n result.status === 404\n ? t('workflows.tasks.detail.notFound', 'Task not found')\n : t('workflows.tasks.detail.loadFailed', 'Failed to load task')\n ) as Error & { status: number }\n httpErr.status = result.status\n throw httpErr\n }\n return result.result?.data ?? null\n },\n })\n\n const mutationContext = React.useMemo<TaskMutationContext>(\n () => ({\n formId: `workflows-task:${params.id}`,\n taskId: params.id,\n resourceKind: 'workflows.user_task',\n resourceId: params.id,\n retryLastMutation,\n }),\n [params.id, retryLastMutation],\n )\n\n // Injection spot for modules that OWN what a task is about (spec \u00A77.6). It\n // is a CONTRACT SURFACE (BACKWARD_COMPATIBILITY.md \u00A76): the id is frozen once\n // a third party mounts a widget on it.\n const taskInjectionContext = React.useMemo(\n () => ({\n taskId: params.id,\n workflowInstanceId: task?.workflowInstanceId ?? null,\n status: task?.status ?? null,\n formSchema: task?.formSchema ?? null,\n formData: task?.formData ?? null,\n retryLastMutation,\n }),\n [params.id, retryLastMutation, task?.formData, task?.formSchema, task?.status, task?.workflowInstanceId],\n )\n\n const refreshTask = React.useCallback(() => {\n queryClient.invalidateQueries({ queryKey: ['workflow-task', params.id] })\n }, [params.id, queryClient])\n\n const handleFieldChange = (fieldName: string, value: string | number | boolean) => {\n setFormData(prev => ({\n ...prev,\n [fieldName]: value,\n }))\n // Clear invalid state once the user touches the offending field.\n if (invalidField === fieldName) setInvalidField(null)\n }\n\n const validateRequiredFields = React.useCallback((): boolean => {\n // Normalized, so a Studio-authored `{ fields: [...] }` schema is validated\n // by the same rule the server enforces (`lib/task-form-schema.ts`).\n const normalizedSchema = normalizeTaskFormSchema(task?.formSchema)\n if (!normalizedSchema?.required) return true\n for (const requiredField of normalizedSchema.required) {\n if (!formData[requiredField] || formData[requiredField] === '') {\n const fieldSchema = normalizedSchema.properties?.[requiredField]\n const fieldLabel = fieldSchema?.title ?? requiredField\n flash(t('workflows.tasks.detail.validation.requiredField', { field: fieldLabel }), 'error')\n setInvalidField(requiredField)\n // Scroll + focus the trigger so the user sees what's missing.\n if (typeof document !== 'undefined') {\n const trigger = document.getElementById(requiredField)\n trigger?.scrollIntoView({ behavior: 'smooth', block: 'center' })\n trigger?.focus()\n }\n return false\n }\n }\n setInvalidField(null)\n return true\n }, [formData, task, t])\n\n const completeTask = React.useCallback(\n async (decisionId?: string) => {\n if (!task) return\n if (!validateRequiredFields()) return\n\n setSubmitting(true)\n try {\n const result = await runMutation({\n operation: () =>\n apiCall(`/api/workflows/tasks/${params.id}/complete`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n formData,\n comments: comments || undefined,\n ...(decisionId ? { decisionId } : {}),\n }),\n }),\n context: mutationContext,\n mutationPayload: { formData, comments, decisionId },\n })\n\n if (result.ok) {\n flash(t('workflows.tasks.messages.completed'), 'success')\n refreshTask()\n } else {\n const failure = result.result as { error?: string; code?: string } | null\n const failureMessage =\n failure?.code === 'WORKFLOW_NOT_ACTIVE'\n ? t('workflows.tasks.messages.workflowNotActive')\n : failure?.error || t('workflows.tasks.messages.completeFailed')\n flash(failureMessage, 'error')\n }\n } catch (err) {\n logger.error('Error completing task', { err })\n flash(t('workflows.tasks.messages.completeFailed'), 'error')\n } finally {\n setSubmitting(false)\n }\n },\n [comments, formData, mutationContext, params.id, refreshTask, runMutation, t, task, validateRequiredFields],\n )\n\n const handleSubmit = React.useCallback(\n (event: React.FormEvent) => {\n event.preventDefault()\n void completeTask()\n },\n [completeTask],\n )\n\n const handleClaim = React.useCallback(async () => {\n setSubmitting(true)\n try {\n const result = await runMutation({\n operation: () => apiCall(`/api/workflows/tasks/${params.id}/claim`, { method: 'POST' }),\n context: mutationContext,\n mutationPayload: { action: 'claim' },\n })\n if (result.ok) {\n flash(t('workflows.tasks.messages.claimed'), 'success')\n refreshTask()\n } else {\n flash(t('workflows.tasks.messages.claimFailed'), 'error')\n }\n } catch (err) {\n logger.error('Error claiming task', { err })\n flash(t('workflows.tasks.messages.claimFailed'), 'error')\n } finally {\n setSubmitting(false)\n }\n }, [mutationContext, params.id, refreshTask, runMutation, t])\n\n const handleRelease = React.useCallback(async () => {\n setSubmitting(true)\n try {\n const result = await runMutation({\n operation: () => apiCall(`/api/workflows/tasks/${params.id}/unclaim`, { method: 'POST' }),\n context: mutationContext,\n mutationPayload: { action: 'unclaim' },\n })\n if (result.ok) {\n flash(t('workflows.workInbox.messages.released'), 'success')\n refreshTask()\n } else {\n flash(t('workflows.workInbox.messages.releaseFailed'), 'error')\n }\n } catch (err) {\n logger.error('Error releasing task', { err })\n flash(t('workflows.workInbox.messages.releaseFailed'), 'error')\n } finally {\n setSubmitting(false)\n }\n }, [mutationContext, params.id, refreshTask, runMutation, t])\n\n /**\n * Reassignment is the remedy \u00A76.4 names for every ownership refusal, and the\n * only way to rescue a task authored with no assignee, no queue and no claim.\n *\n * The endpoint honours the optimistic-lock header explicitly (`UserTask` is\n * outside the curated CRUD lock list), so the row's `updatedAt` rides along:\n * two administrators racing to take the same task get a 409 rather than a\n * silent last-write-wins.\n */\n const handleReassign = React.useCallback(\n async (target: ReassignTaskTarget) => {\n if (!task) return\n setSubmitting(true)\n try {\n const result = await runMutation({\n operation: () =>\n withScopedApiRequestHeaders(buildOptimisticLockHeader(task.updatedAt), () =>\n apiCall(`/api/workflows/tasks/${params.id}/reassign`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(target),\n }),\n ),\n context: mutationContext,\n mutationPayload: { action: 'reassign', ...target },\n })\n if (result.ok) {\n setReassignOpen(false)\n flash(t('workflows.tasks.messages.reassigned'), 'success')\n refreshTask()\n } else {\n const failure = result.result as { error?: string } | null\n flash(failure?.error || t('workflows.tasks.messages.reassignFailed'), 'error')\n }\n } catch (err) {\n logger.error('Error reassigning task', { err })\n flash(t('workflows.tasks.messages.reassignFailed'), 'error')\n } finally {\n setSubmitting(false)\n }\n },\n [mutationContext, params.id, refreshTask, runMutation, t, task],\n )\n\n const handleNextTask = React.useCallback(async () => {\n setSubmitting(true)\n try {\n const result = await runMutation({\n operation: () =>\n apiCall<{ data: { id: string } | null }>('/api/workflows/work-inbox/next', {\n method: 'POST',\n }),\n context: mutationContext,\n mutationPayload: { action: 'claim-next' },\n })\n if (!result.ok) {\n flash(t('workflows.tasks.messages.claimFailed'), 'error')\n return\n }\n const claimed = result.result?.data ?? null\n if (!claimed) {\n flash(t('workflows.workInbox.messages.nothingClaimable'), 'info')\n return\n }\n flash(t('workflows.tasks.messages.claimed'), 'success')\n router.push(buildWorkInboxItemHref(claimed.id))\n } catch (err) {\n logger.error('Error claiming the next task', { err })\n flash(t('workflows.tasks.messages.claimFailed'), 'error')\n } finally {\n setSubmitting(false)\n }\n }, [mutationContext, router, runMutation, t])\n\n const getStatusBadgeClass = React.useCallback(\n (status: UserTaskStatus) => describeWorkInboxStatus(status).badge,\n [],\n )\n\n if (isLoading) {\n return (\n <Page>\n <PageBody>\n <div className=\"flex items-center justify-center py-12\">\n <Spinner />\n <span className=\"ml-3 text-muted-foreground\">{t('workflows.tasks.detail.loading')}</span>\n </div>\n </PageBody>\n </Page>\n )\n }\n\n const isNotFound = !isLoading && (error as (Error & { status?: number }) | null)?.status === 404\n\n if (isNotFound) {\n return (\n <Page>\n <PageBody>\n <RecordNotFoundState\n label={t('workflows.tasks.detail.notFound', 'Task not found')}\n backHref={WORK_INBOX_HREF}\n backLabel={t('workflows.tasks.detail.backToList', 'Back to Tasks')}\n />\n </PageBody>\n </Page>\n )\n }\n\n if (error || !task) {\n return (\n <Page>\n <PageBody>\n <ErrorMessage\n label={(error as Error | null)?.message ?? t('workflows.tasks.detail.loadFailed', 'Failed to load task')}\n action={\n <Button asChild variant=\"outline\" size=\"sm\">\n <Link href={WORK_INBOX_HREF}>{t('workflows.tasks.detail.backToList', 'Back to Tasks')}</Link>\n </Button>\n }\n />\n </PageBody>\n </Page>\n )\n }\n\n const isCompletable = task.status === 'PENDING' || task.status === 'IN_PROGRESS'\n const isOverdue = Boolean(task.dueDate) && new Date(task.dueDate as string) < new Date() && isCompletable\n const statusDescriptor = describeWorkInboxStatus(task.status)\n const priority = normalizeWorkInboxPriority(task.priority)\n const priorityDescriptor = describeWorkInboxPriority(priority)\n const decisions = task.decisions ?? []\n const contextItems = bindingsToEntityContextItems(task.entityBindings, {\n noLink: t('workflows.tasks.detail.records.noLink'),\n })\n const recordedDecisionId = readRecordedDecision(task.formData)\n const recordedDecision = recordedDecisionId\n ? decisions.find((decision) => decision.id === recordedDecisionId) ?? null\n : null\n // A task nobody owns yet, offered to a role queue, is the one this operator\n // can take. An individually assigned task is already theirs to finish.\n //\n // Kept ONLY as the compatibility fallback below \u2014 it is the pre-change\n // behaviour, restored verbatim when the server sends no `canClaim`.\n const legacyClaimable =\n task.status === 'PENDING' &&\n !task.assignedTo &&\n !task.claimedBy &&\n (task.assignedToRoles?.length ?? 0) > 0\n\n /*\n * The server decides; the page renders. `??` is the additive-compatibility\n * fallback for a payload written before these fields existed\n * (BACKWARD_COMPATIBILITY.md \u00A77) \u2014 it restores exactly the pre-change\n * behaviour and is never a second copy of the \u00A76.4 rule. Release and reassign\n * are new affordances with no legacy behaviour to preserve, so an old payload\n * simply does not offer them.\n */\n const canComplete = task.canComplete ?? isCompletable\n const canClaim = task.canClaim ?? legacyClaimable\n const canRelease = task.canRelease ?? false\n const canReassign = task.canReassign ?? false\n const blockDescriptor = canComplete\n ? null\n : describeTaskActionBlock(task.actBlockedReason ?? 'not-workable')\n\n if (isMobile) {\n return (\n <Page>\n <PageBody>\n <div className=\"space-y-4\">\n <FormHeader\n mode=\"detail\"\n backHref={WORK_INBOX_HREF}\n backLabel={t('workflows.tasks.backToList', 'Back to tasks')}\n />\n {blockDescriptor ? (\n <TaskActionNotice\n descriptor={blockDescriptor}\n canReassign={canReassign}\n disabled={submitting}\n onReassign={() => setReassignOpen(true)}\n />\n ) : null}\n <MobileTaskForm\n task={task}\n formData={formData}\n comments={comments}\n submitting={submitting}\n isCompletable={canComplete}\n isOverdue={isOverdue}\n onFieldChange={handleFieldChange}\n onCommentsChange={setComments}\n onSubmit={handleSubmit}\n onCancel={() => router.push(WORK_INBOX_HREF)}\n getStatusBadgeClass={getStatusBadgeClass}\n decisions={decisions}\n onDecision={(decisionId) => void completeTask(decisionId)}\n />\n </div>\n <ReassignTaskDialog\n open={reassignOpen}\n submitting={submitting}\n onOpenChange={setReassignOpen}\n onSubmit={(target) => void handleReassign(target)}\n />\n </PageBody>\n </Page>\n )\n }\n\n return (\n <Page>\n <PageBody>\n <div className=\"space-y-6\">\n <FormHeader\n mode=\"detail\"\n backHref={WORK_INBOX_HREF}\n backLabel={t('workflows.tasks.backToList', 'Back to tasks')}\n entityTypeLabel={t('workflows.tasks.detail.type', 'User task')}\n title={task.taskName}\n subtitle={task.description || undefined}\n statusBadge={\n <span className=\"inline-flex items-center gap-2\">\n <StatusChip\n descriptor={statusDescriptor}\n label={t(`workflows.tasks.statuses.${task.status}`, task.status)}\n />\n {priorityDescriptor && priority ? (\n <StatusChip\n descriptor={priorityDescriptor}\n label={t(`workflows.workInbox.priorities.${priority}`, priority)}\n />\n ) : null}\n {isOverdue ? (\n <StatusChip\n descriptor={WORK_INBOX_OVERDUE_TONE}\n label={t('workflows.tasks.overdue')}\n />\n ) : null}\n </span>\n }\n />\n\n <div className=\"grid grid-cols-1 gap-6 lg:grid-cols-3\">\n {/* Left \u2014 instructions, form, decisions */}\n <div className=\"space-y-6 lg:col-span-2\">\n {isOverdue ? (\n <div className=\"bg-status-error-bg border border-status-error-border rounded-lg p-3\">\n <p className=\"text-sm text-status-error-text font-medium\">\n {t('workflows.tasks.detail.overdueWarning')}\n </p>\n </div>\n ) : null}\n\n {task.description ? (\n <div className=\"rounded-lg border border-border p-4\">\n <h2 className=\"text-sm font-semibold mb-2\">\n {t('workflows.tasks.detail.sections.instructions')}\n </h2>\n <p className=\"text-sm text-muted-foreground whitespace-pre-wrap\">{task.description}</p>\n </div>\n ) : null}\n\n {/*\n Where a module that OWNS the thing a task is about renders it in\n its own vocabulary. The agent orchestrator mounts the proposal\n draft card here (spec \u00A77.6): this page can only walk a task's\n `formSchema` generically, and a proposal rendered that way is a\n key/value dump of the mutation an operator is being asked to\n approve. Additive \u2014 no widget, nothing renders.\n */}\n <InjectionSpot spotId={TASK_DETAIL_INJECTION_SPOT_ID} context={taskInjectionContext} />\n\n {canClaim ? (\n <div className=\"rounded-lg border border-border bg-muted/50 p-4 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between\">\n <p className=\"text-sm text-muted-foreground\">\n {t('workflows.tasks.detail.claimPrompt')}\n </p>\n <Button\n type=\"button\"\n data-testid=\"task-claim\"\n disabled={submitting}\n onClick={() => void handleClaim()}\n >\n {t('workflows.tasks.actions.claim')}\n </Button>\n </div>\n ) : null}\n\n {canRelease ? (\n <div className=\"rounded-lg border border-border bg-muted/50 p-4 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between\">\n <p className=\"text-sm text-muted-foreground\">\n {t('workflows.tasks.detail.releasePrompt')}\n </p>\n <Button\n type=\"button\"\n variant=\"outline\"\n data-testid=\"task-release\"\n disabled={submitting}\n onClick={() => void handleRelease()}\n >\n {t('workflows.tasks.actions.unclaim')}\n </Button>\n </div>\n ) : null}\n\n {blockDescriptor ? (\n <TaskActionNotice\n descriptor={blockDescriptor}\n canReassign={canReassign}\n disabled={submitting}\n onReassign={() => setReassignOpen(true)}\n />\n ) : null}\n\n {/*\n Offered even when the task IS completable: an administrator may\n legitimately hand a row to somebody else, and the endpoint gates\n on visibility rather than ownership precisely so it can.\n */}\n {canComplete && canReassign ? (\n <div className=\"flex justify-end\">\n <Button\n type=\"button\"\n variant=\"outline\"\n data-testid=\"task-reassign\"\n disabled={submitting}\n onClick={() => setReassignOpen(true)}\n >\n {t('workflows.tasks.actions.reassign')}\n </Button>\n </div>\n ) : null}\n\n {canComplete ? (\n <form onSubmit={handleSubmit} className=\"space-y-6\">\n <TaskFormFields\n formSchema={task.formSchema}\n values={formData}\n invalidField={invalidField}\n disabled={submitting}\n onFieldChange={handleFieldChange}\n formKey={task.formKey}\n taskId={task.id}\n taskName={task.taskName}\n entityBindings={task.entityBindings}\n decisions={decisions}\n onValuesChange={setFormData}\n />\n\n <Separator />\n\n <div className=\"space-y-2\">\n <label htmlFor=\"comments\" className=\"block text-sm font-medium text-foreground\">\n {t('workflows.tasks.detail.comments')} ({t('workflows.tasks.detail.optional')})\n </label>\n <textarea\n id=\"comments\"\n value={comments}\n onChange={(e) => setComments(e.target.value)}\n rows={3}\n className=\"w-full px-3 py-2 border border-border rounded-md focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\"\n placeholder={t('workflows.tasks.detail.commentsPlaceholder')}\n />\n </div>\n\n {decisions.length > 0 ? (\n <div className=\"space-y-3\" data-testid=\"task-decisions\">\n <h2 className=\"text-sm font-semibold\">\n {t('workflows.tasks.detail.sections.decisions')}\n </h2>\n <div className=\"flex flex-col gap-3 sm:flex-row sm:flex-wrap\">\n {decisions.map((decision) => (\n <Button\n key={decision.id}\n type=\"button\"\n data-testid={`task-decision-${decision.id}`}\n variant={decisionButtonVariant(decision.style)}\n disabled={submitting}\n onClick={() => void completeTask(decision.id)}\n className=\"w-full sm:w-auto\"\n >\n {flattenTaskText(decision.label) ?? decision.id}\n </Button>\n ))}\n </div>\n </div>\n ) : (\n <div className=\"flex flex-col gap-3 pt-4 sm:flex-row sm:items-center\">\n <Button type=\"submit\" data-testid=\"task-complete\" disabled={submitting} className=\"w-full sm:w-auto\">\n {submitting\n ? t('workflows.tasks.detail.submitting')\n : t('workflows.tasks.detail.completeTask')}\n </Button>\n <Button\n type=\"button\"\n variant=\"outline\"\n onClick={() => router.push(WORK_INBOX_HREF)}\n disabled={submitting}\n className=\"w-full sm:w-auto\"\n >\n {t('common.cancel')}\n </Button>\n </div>\n )}\n </form>\n ) : null}\n\n {task.status === 'COMPLETED' ? (\n <div className=\"space-y-4\">\n {recordedDecisionId ? (\n <div className=\"rounded-lg border border-border p-4\">\n <p className=\"text-sm text-muted-foreground\">\n {t('workflows.tasks.detail.decisionTaken')}:{' '}\n <span className=\"text-foreground font-medium\">\n {recordedDecision\n ? flattenTaskText(recordedDecision.label) ?? recordedDecisionId\n : recordedDecisionId}\n </span>\n </p>\n </div>\n ) : null}\n\n {task.formData ? (\n <JsonDisplay\n data={task.formData}\n title={t('workflows.tasks.detail.sections.submittedData')}\n maxInitialDepth={2}\n />\n ) : null}\n\n {task.comments ? (\n <div className=\"bg-muted/50 rounded-lg p-4\">\n <p className=\"text-sm font-medium text-foreground mb-2\">\n {t('workflows.tasks.detail.comments')}:\n </p>\n <p className=\"text-sm text-muted-foreground whitespace-pre-wrap\">{task.comments}</p>\n </div>\n ) : null}\n\n {/* Spec \u00A76.2: keep frontline flow going with one click. */}\n <div className=\"rounded-lg border border-border bg-muted/50 p-4 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between\">\n <p className=\"text-sm text-muted-foreground\">\n {t('workflows.tasks.detail.nextTaskPrompt')}\n </p>\n <div className=\"flex gap-2\">\n <Button\n type=\"button\"\n data-testid=\"task-next\"\n disabled={submitting}\n onClick={() => void handleNextTask()}\n >\n {t('workflows.workInbox.actions.claimNext')}\n </Button>\n <Button asChild variant=\"outline\">\n <Link href={WORK_INBOX_HREF}>{t('workflows.tasks.detail.backToList')}</Link>\n </Button>\n </div>\n </div>\n </div>\n ) : null}\n </div>\n\n {/* Right \u2014 the records this task is about, plus how it got here */}\n <div className=\"space-y-4\">\n <EntityContextPanel\n testId=\"task-context-records\"\n title={t('workflows.tasks.detail.sections.records')}\n items={contextItems}\n emptyLabel={t('workflows.tasks.detail.records.empty')}\n linkLabel={t('workflows.tasks.detail.records.open')}\n />\n\n <div className=\"rounded-lg border border-border p-4 space-y-2\">\n <h2 className=\"text-sm font-semibold\">\n {t('workflows.tasks.detail.sections.taskInfo')}\n </h2>\n <div className=\"space-y-1 text-sm\">\n <div>\n <span className=\"text-muted-foreground\">{t('workflows.tasks.fields.createdAt')}:</span>\n <span className=\"ml-2 text-foreground\">{new Date(task.createdAt).toLocaleString()}</span>\n </div>\n {task.dueDate ? (\n <div>\n <span className=\"text-muted-foreground\">{t('workflows.tasks.fields.dueDate')}:</span>\n <span\n className={`ml-2 ${isOverdue ? `${WORK_INBOX_OVERDUE_TONE.text} font-medium` : 'text-foreground'}`}\n >\n {new Date(task.dueDate).toLocaleString()}\n </span>\n </div>\n ) : null}\n {task.assignedTo ? (\n <div>\n <span className=\"text-muted-foreground\">{t('workflows.tasks.detail.assignedTo')}:</span>\n <span className=\"ml-2 text-foreground\">{task.assignedTo}</span>\n </div>\n ) : null}\n {task.assignedToRoles?.length ? (\n <div>\n <span className=\"text-muted-foreground\">{t('workflows.tasks.roles')}:</span>\n <span className=\"ml-2 text-foreground\">{task.assignedToRoles.join(', ')}</span>\n </div>\n ) : null}\n {task.claimedBy ? (\n <div>\n <span className=\"text-muted-foreground\">{t('workflows.tasks.claimedBy')}:</span>\n <span className=\"ml-2 text-foreground\">{task.claimedBy}</span>\n </div>\n ) : null}\n <div>\n <span className=\"text-muted-foreground\">\n {t('workflows.tasks.detail.workflowInstance')}:\n </span>\n <Link\n href={`/backend/instances/${task.workflowInstanceId}`}\n className=\"ml-2 text-primary hover:underline text-xs font-mono\"\n >\n {task.workflowInstanceId.slice(0, 8)}...\n </Link>\n </div>\n </div>\n </div>\n </div>\n </div>\n </div>\n\n <ReassignTaskDialog\n open={reassignOpen}\n submitting={submitting}\n onOpenChange={setReassignOpen}\n onSubmit={(target) => void handleReassign(target)}\n />\n </PageBody>\n </Page>\n )\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -0,0 +1,16 @@
1
+ const RUN_STATUSES_CLOSED_TO_USER_TASKS = [
2
+ "COMPLETED",
3
+ "FAILED",
4
+ "CANCELLED",
5
+ "COMPENSATING",
6
+ "COMPENSATED"
7
+ ];
8
+ function isRunClosedToUserTasks(status) {
9
+ if (!status) return false;
10
+ return RUN_STATUSES_CLOSED_TO_USER_TASKS.includes(status);
11
+ }
12
+ export {
13
+ RUN_STATUSES_CLOSED_TO_USER_TASKS,
14
+ isRunClosedToUserTasks
15
+ };
16
+ //# sourceMappingURL=instance-status.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/workflows/lib/instance-status.ts"],
4
+ "sourcesContent": ["/**\n * Workflow instance statuses a user-task completion or a user-task SLA job must\n * not touch.\n *\n * PURE: a type-only import, so the engine does not depend on the metrics\n * module's `WORKFLOW_TERMINAL_STATUSES` for a runtime decision.\n *\n * The name means \"a completion must not touch this run\", NOT \"its tasks are\n * closed\": a FAILED run keeps its open task so a retry can pick it up.\n * `COMPENSATING` is included although it is not terminal \u2014 `compensateWorkflow`\n * holds it while rollback activities run and then overwrites it with\n * `COMPENSATED` or `FAILED`, and a completion in that window would advance a\n * run that is being rolled back.\n */\n\nimport type { WorkflowInstanceStatus } from '../data/entities'\n\nexport const RUN_STATUSES_CLOSED_TO_USER_TASKS: readonly WorkflowInstanceStatus[] = [\n 'COMPLETED',\n 'FAILED',\n 'CANCELLED',\n 'COMPENSATING',\n 'COMPENSATED',\n]\n\nexport function isRunClosedToUserTasks(status: string | null | undefined): boolean {\n if (!status) return false\n return (RUN_STATUSES_CLOSED_TO_USER_TASKS as readonly string[]).includes(status)\n}\n"],
5
+ "mappings": "AAiBO,MAAM,oCAAuE;AAAA,EAClF;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAEO,SAAS,uBAAuB,QAA4C;AACjF,MAAI,CAAC,OAAQ,QAAO;AACpB,SAAQ,kCAAwD,SAAS,MAAM;AACjF;",
6
+ "names": []
7
+ }
@@ -13,6 +13,7 @@ import {
13
13
  } from "./task-decisions.js";
14
14
  import { loadTaskDecisionContext } from "./task-decision-context.js";
15
15
  import { readRequiredFormFields } from "./task-form-schema.js";
16
+ import { isRunClosedToUserTasks } from "./instance-status.js";
16
17
  import * as stepHandler from "./step-handler.js";
17
18
  import * as transitionHandler from "./transition-handler.js";
18
19
  import { createLogger } from "@open-mercato/shared/lib/logger";
@@ -67,6 +68,21 @@ async function completeUserTask(em, container, options) {
67
68
  { taskId, decisionId }
68
69
  );
69
70
  }
71
+ const instance = await em.findOne(WorkflowInstance, task.workflowInstanceId);
72
+ if (!instance) {
73
+ throw new UserTaskError(
74
+ "Workflow instance not found",
75
+ "INSTANCE_NOT_FOUND",
76
+ { workflowInstanceId: task.workflowInstanceId }
77
+ );
78
+ }
79
+ if (isRunClosedToUserTasks(instance.status)) {
80
+ throw new UserTaskError(
81
+ "Workflow is no longer active",
82
+ "WORKFLOW_NOT_ACTIVE",
83
+ { taskId, workflowInstanceId: instance.id, status: instance.status }
84
+ );
85
+ }
70
86
  const completionData = withRecordedDecision(formData, chosenDecision?.id);
71
87
  const now = /* @__PURE__ */ new Date();
72
88
  task.status = "COMPLETED";
@@ -76,14 +92,6 @@ async function completeUserTask(em, container, options) {
76
92
  task.comments = comments || null;
77
93
  task.updatedAt = now;
78
94
  await em.flush();
79
- const instance = await em.findOne(WorkflowInstance, task.workflowInstanceId);
80
- if (!instance) {
81
- throw new UserTaskError(
82
- "Workflow instance not found",
83
- "INSTANCE_NOT_FOUND",
84
- { workflowInstanceId: task.workflowInstanceId }
85
- );
86
- }
87
95
  if (task.branchInstanceId) {
88
96
  await logWorkflowEvent(em, {
89
97
  workflowInstanceId: instance.id,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/workflows/lib/task-handler.ts"],
4
- "sourcesContent": ["/**\n * Workflows Module - User Task Handler Service\n *\n * Handles user task lifecycle operations:\n * - Completing user tasks\n * - Claiming tasks from role queues\n * - Reassigning tasks\n * - Escalating overdue tasks\n *\n * Functional API (no classes) following Open Mercato conventions.\n */\n\nimport { EntityManager } from '@mikro-orm/core'\nimport type { AwilixContainer } from 'awilix'\nimport {\n UserTask,\n WorkflowInstance,\n WorkflowEvent,\n StepInstance,\n} from '../data/entities'\nimport { executeWorkflow } from './workflow-executor'\nimport { findDefinitionForInstance } from './find-definition'\nimport {\n findTaskDecision,\n findTaskDecisionTransition,\n withRecordedDecision,\n} from './task-decisions'\nimport { loadTaskDecisionContext } from './task-decision-context'\nimport { readRequiredFormFields } from './task-form-schema'\nimport * as stepHandler from './step-handler'\nimport * as transitionHandler from './transition-handler'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('workflows')\n\n// ============================================================================\n// Types and Interfaces\n// ============================================================================\n\n/**\n * Tenant/organization the caller is acting within. Every task lookup filters on\n * it, so a task id from another tenant resolves to \"not found\" instead of being\n * mutated.\n */\nexport interface UserTaskScope {\n tenantId: string\n organizationId: string\n}\n\n/**\n * Shape of the `taskHandler` DI registration (`di.ts`).\n *\n * The functions below stay exported \u2014 third-party modules may import them and\n * `BACKWARD_COMPATIBILITY.md` \u00A73 freezes import paths \u2014 but callers inside the\n * platform MUST resolve this service instead, per the module's first rule.\n */\nexport interface TaskHandlerService {\n completeUserTask: typeof completeUserTask\n claimUserTask: typeof claimUserTask\n releaseUserTask: typeof releaseUserTask\n reassignUserTask: typeof reassignUserTask\n}\n\nexport interface CompleteUserTaskOptions {\n taskId: string\n formData: Record<string, any>\n userId: string\n comments?: string\n /**\n * The decision button the assignee pressed, when the step authored any.\n *\n * It does two things: it SELECTS the outgoing route (the decision's\n * `transitionId` wins over the first-valid-transition default), and it is\n * RECORDED as part of the completion payload. Omitted, completion behaves\n * exactly as it always did.\n */\n decisionId?: string\n scope: UserTaskScope\n}\n\nexport class UserTaskError extends Error {\n constructor(\n message: string,\n public code: string,\n public details?: any\n ) {\n super(message)\n this.name = 'UserTaskError'\n }\n}\n\n// ============================================================================\n// User Task Completion\n// ============================================================================\n\n/**\n * Complete a user task and resume workflow execution\n *\n * This function:\n * 1. Validates the task exists and can be completed\n * 2. Updates the task with form data and completion info\n * 3. Merges form data into workflow context\n * 4. Logs completion event\n * 5. Resumes workflow execution\n *\n * @param em - Entity manager\n * @param container - DI container for workflow execution\n * @param options - Task completion options\n * @throws UserTaskError if task not found or validation fails\n */\nexport async function completeUserTask(\n em: EntityManager,\n container: AwilixContainer,\n options: CompleteUserTaskOptions\n): Promise<void> {\n const { taskId, formData, userId, comments, decisionId, scope } = options\n\n // Fetch task\n const task = await em.findOne(UserTask, {\n id: taskId,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n status: { $in: ['PENDING', 'IN_PROGRESS'] },\n })\n\n if (!task) {\n throw new UserTaskError(\n 'Task not found or already completed',\n 'TASK_NOT_FOUND',\n { taskId }\n )\n }\n\n // A task addressed to one person is that person's to finish. Tasks that name\n // nobody \u2014 agent dispositions, role queues nobody has claimed \u2014 stay open to\n // anyone the route's feature gate already admitted, so this narrows the\n // over-permissive case without stranding the unassigned ones.\n const claimant = task.claimedBy ?? task.assignedTo\n if (claimant && claimant !== userId) {\n throw new UserTaskError(\n 'Task is assigned to another user',\n 'TASK_ASSIGNED_TO_ANOTHER_USER',\n { taskId, assignedTo: claimant }\n )\n }\n\n // Validate form data against schema (simple validation for MVP)\n // In Phase 7, we'll add comprehensive JSON Schema validation\n if (task.formSchema) {\n try {\n validateFormData(formData, task.formSchema)\n } catch (error) {\n throw new UserTaskError(\n error instanceof Error ? error.message : 'Form validation failed',\n 'FORM_VALIDATION_FAILED',\n { taskId, formSchema: task.formSchema, formData }\n )\n }\n }\n\n // Resolve the pressed decision BEFORE anything is mutated: an unknown\n // decision id must leave the task untouched rather than complete it down the\n // default route. The list is re-resolved from the instance's pinned\n // definition, so it is exactly the list the assignee was shown.\n const chosenDecision = decisionId\n ? findTaskDecision((await loadTaskDecisionContext(em, task)).decisions, decisionId)\n : null\n\n if (decisionId && !chosenDecision) {\n throw new UserTaskError(\n 'Unknown decision for this task',\n 'UNKNOWN_DECISION',\n { taskId, decisionId }\n )\n }\n\n // The chosen decision rides along inside the completion payload the task\n // already persists \u2014 no new column, and downstream route conditions can read\n // it once the payload is merged into the run context.\n const completionData = withRecordedDecision(formData, chosenDecision?.id)\n\n // Update task\n const now = new Date()\n task.status = 'COMPLETED'\n task.formData = completionData\n task.completedBy = userId\n task.completedAt = now\n task.comments = comments || null\n task.updatedAt = now\n\n await em.flush()\n\n // Fetch workflow instance\n const instance = await em.findOne(WorkflowInstance, task.workflowInstanceId)\n if (!instance) {\n throw new UserTaskError(\n 'Workflow instance not found',\n 'INSTANCE_NOT_FOUND',\n { workflowInstanceId: task.workflowInstanceId }\n )\n }\n\n // Branch-scoped completion: when the task belongs to a parallel branch,\n // merge form data into the branch namespace and resume just that branch.\n //\n // The decision is RECORDED here exactly as it is on the main path, but it does\n // not select the route: a branch advances through `resumeBranch` with its own\n // token, and overriding a branch's outgoing route is a parallel-execution\n // change rather than a task-surface one.\n if (task.branchInstanceId) {\n await logWorkflowEvent(em, {\n workflowInstanceId: instance.id,\n stepInstanceId: task.stepInstanceId,\n branchInstanceId: task.branchInstanceId,\n eventType: 'USER_TASK_COMPLETED',\n eventData: {\n taskId: task.id,\n taskName: task.taskName,\n completedBy: userId,\n formData: completionData,\n ...(chosenDecision ? { decisionId: chosenDecision.id } : {}),\n },\n userId,\n tenantId: instance.tenantId,\n organizationId: instance.organizationId,\n })\n\n const { resumeBranch } = await import('./parallel-handler')\n const resumed = await resumeBranch(em, {\n instanceId: instance.id,\n branchInstanceId: task.branchInstanceId,\n tenantId: instance.tenantId,\n organizationId: instance.organizationId,\n contextMerge: completionData,\n exitStepInstanceId: task.stepInstanceId,\n exitOutput: { userTaskId: task.id, formData: completionData },\n })\n\n if (resumed) {\n await executeWorkflow(em, container, instance.id, { userId })\n }\n return\n }\n\n // Merge form data into workflow context\n instance.context = {\n ...instance.context,\n ...completionData,\n }\n instance.updatedAt = now\n\n // Log USER_TASK_COMPLETED event\n await logWorkflowEvent(em, {\n workflowInstanceId: instance.id,\n stepInstanceId: task.stepInstanceId,\n eventType: 'USER_TASK_COMPLETED',\n eventData: {\n taskId: task.id,\n taskName: task.taskName,\n completedBy: userId,\n formData: completionData,\n ...(chosenDecision ? { decisionId: chosenDecision.id } : {}),\n },\n userId,\n tenantId: instance.tenantId,\n organizationId: instance.organizationId,\n })\n\n // Mark the step instance as completed\n const stepInstance = await em.findOne(StepInstance, {\n id: task.stepInstanceId,\n status: 'ACTIVE',\n })\n\n if (stepInstance) {\n await stepHandler.exitStep(em, stepInstance, { userTaskId: task.id, formData: completionData })\n }\n\n // Find the next automatic transition from the current step\n const currentStepId = instance.currentStepId\n\n // Load workflow definition to find transitions\n const definition = await findDefinitionForInstance(em, instance)\n\n if (!definition) {\n throw new UserTaskError(\n 'Workflow definition not found',\n 'DEFINITION_NOT_FOUND',\n { definitionId: instance.definitionId }\n )\n }\n\n const transitionContext = {\n workflowContext: instance.context,\n userId,\n }\n\n // A decision names the route to take. It is bound to the transition's durable\n // id, so it survives every route reorder, and it is looked up among the routes\n // LEAVING this step only \u2014 a button can never send the run down a route that\n // does not start where the run currently is. A decision whose route no longer\n // exists there falls through to the default evaluation rather than stranding\n // the run at a step it already completed.\n const decisionTransition = chosenDecision\n ? findTaskDecisionTransition(\n definition.definition.transitions || [],\n chosenDecision.transitionId,\n currentStepId\n )\n : null\n\n if (chosenDecision && !decisionTransition) {\n logger.warn('Task decision route not found on the current step; falling back to auto routing', {\n component: 'task-handler',\n taskId: task.id,\n decisionId: chosenDecision.id,\n transitionId: chosenDecision.transitionId,\n currentStepId,\n })\n }\n\n let selectedTransition = decisionTransition ?? null\n let targetStepId = selectedTransition?.toStepId ?? null\n\n if (!targetStepId) {\n // Find automatic transitions from current step\n const autoTransitions = (definition.definition.transitions || []).filter(\n (t: any) => t.fromStepId === currentStepId && t.trigger === 'auto'\n )\n\n if (autoTransitions.length === 0) {\n // No automatic transitions, workflow stays paused at current step\n return\n }\n\n // Find valid transitions using transition handler\n const validTransitions = await transitionHandler.findValidTransitions(\n em,\n instance,\n currentStepId,\n transitionContext\n )\n\n const firstValidTransition = validTransitions.find(t => t.isValid)\n\n if (!firstValidTransition || !firstValidTransition.transition) {\n // Resume workflow execution anyway, maybe conditions will be met later\n instance.status = 'RUNNING'\n await em.flush()\n return\n }\n\n targetStepId = firstValidTransition.transition.toStepId\n selectedTransition = firstValidTransition.transition\n }\n\n // Resume from the paused wait BEFORE executing the transition, exactly as\n // `sendSignal` does. Without it the executor's defense-in-depth PAUSED check\n // stops the run at the first step the completion traverses, so the instance\n // sits there as PAUSED and never reaches END. Guarded on PAUSED so completing\n // a stale task can never resurrect a CANCELLED or terminal run.\n if (instance.status === 'PAUSED') {\n instance.status = 'RUNNING'\n await em.flush()\n }\n\n // Execute the transition to move to next step\n\n const transitionResult = await transitionHandler.executeTransition(\n em,\n container,\n instance,\n currentStepId,\n targetStepId,\n { ...transitionContext, transitionId: selectedTransition?.transitionId },\n )\n\n if (!transitionResult.success) {\n logger.error('Transition failed', { component: 'task-handler', err: transitionResult.error })\n // Don't throw, just leave workflow in current state\n return\n }\n\n // Now continue workflow execution from the new step\n await executeWorkflow(em, container, instance.id, { userId })\n}\n\n/**\n * Whether the caller belongs to at least one of the task's queues.\n *\n * Exact string equality on role NAMES \u2014 no case folding and no trimming, because\n * the names on both sides come from the same tenant role table and normalizing\n * one side would let `Approver` claim an `approver` queue that RBAC treats as a\n * different role.\n */\nfunction hasRoleOverlap(\n taskRoles: readonly string[],\n callerRoleNames: readonly string[],\n): boolean {\n if (callerRoleNames.length === 0) return false\n const held = new Set(callerRoleNames)\n return taskRoles.some((role) => held.has(role))\n}\n\n/**\n * Claim a user task from a role queue\n *\n * Allows a user to claim a task that's assigned to their role(s).\n *\n * The claim itself is a COMPARE-AND-SET, not a read-then-write: the lookup\n * below only decides which error to report, while the row is taken with a\n * conditional `UPDATE \u2026 WHERE status = 'PENDING' AND claimed_by IS NULL`. Two\n * callers racing for the same task \u2014 which is exactly what the work inbox's\n * claim-next affordance produces \u2014 therefore cannot both win; the loser sees\n * zero affected rows and the same `TASK_NOT_FOUND` it would have seen a moment\n * later. Reading the status and flushing the entity afterwards would have let\n * both transactions read `PENDING` and both write.\n *\n * Membership in the queue is a REQUIREMENT, not a hint: a caller who holds none\n * of `assignedToRoles` is refused with the same `TASK_NOT_FOUND` a nonexistent\n * id produces, so a queue the caller does not belong to is indistinguishable\n * from a task that is not there. Holding `workflows.tasks.claim` admits you to\n * the endpoint; it does not admit you to somebody else's queue.\n *\n * `assignedToRoles` and `callerRoleNames` are both role NAMES \u2014 the Studio\n * picker writes names, the engine copies them onto the task, and `auth.roles`\n * carries names. A role RENAME therefore orphans existing assignments; moving\n * the comparison onto immutable role ids is a coordinated data + authored-\n * definition migration and is deliberately not done here.\n *\n * @param em - Entity manager\n * @param taskId - Task ID to claim\n * @param userId - User claiming the task\n * @param scope - Tenant/organization the caller acts within; both the lookup and\n * the conditional update filter on it, so a task belonging to another tenant\n * is never reachable and never writable\n * @param callerRoleNames - Server-derived role names of the caller (`auth.roles`),\n * never client-supplied. Required rather than optional so a caller that cannot\n * supply them fails to compile instead of silently claiming any queue.\n * @throws UserTaskError if task cannot be claimed\n */\nexport async function claimUserTask(\n em: EntityManager,\n taskId: string,\n userId: string,\n scope: UserTaskScope,\n callerRoleNames: readonly string[]\n): Promise<void> {\n const task = await em.findOne(UserTask, {\n id: taskId,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n status: 'PENDING',\n })\n\n if (!task) {\n throw new UserTaskError(\n 'Task not found or already claimed',\n 'TASK_NOT_FOUND',\n { taskId }\n )\n }\n\n if (task.assignedTo) {\n throw new UserTaskError(\n 'Task is already assigned to a specific user',\n 'TASK_ALREADY_ASSIGNED',\n { taskId, assignedTo: task.assignedTo }\n )\n }\n\n if (!task.assignedToRoles || task.assignedToRoles.length === 0) {\n throw new UserTaskError(\n 'Task is not assigned to any roles',\n 'TASK_NOT_ROLE_ASSIGNED',\n { taskId }\n )\n }\n\n if (!hasRoleOverlap(task.assignedToRoles, callerRoleNames)) {\n throw new UserTaskError(\n 'Task not found or already claimed',\n 'TASK_NOT_FOUND',\n { taskId }\n )\n }\n\n const now = new Date()\n const claimed = await em.nativeUpdate(\n UserTask,\n {\n id: taskId,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n status: 'PENDING',\n claimedBy: null,\n },\n {\n claimedBy: userId,\n claimedAt: now,\n status: 'IN_PROGRESS',\n updatedAt: now,\n },\n )\n\n if (claimed === 0) {\n throw new UserTaskError(\n 'Task not found or already claimed',\n 'TASK_NOT_FOUND',\n { taskId }\n )\n }\n\n // The conditional update above already wrote the row; mirroring it onto the\n // managed entity keeps the caller's identity-mapped copy from reporting the\n // pre-claim state back to the client.\n task.claimedBy = userId\n task.claimedAt = now\n task.status = 'IN_PROGRESS'\n task.updatedAt = now\n\n await em.flush()\n\n // Log event\n const instance = await em.findOne(WorkflowInstance, task.workflowInstanceId)\n if (instance) {\n await logWorkflowEvent(em, {\n workflowInstanceId: instance.id,\n stepInstanceId: task.stepInstanceId,\n eventType: 'USER_TASK_STARTED',\n eventData: {\n taskId: task.id,\n taskName: task.taskName,\n claimedBy: userId,\n },\n userId,\n tenantId: instance.tenantId,\n organizationId: instance.organizationId,\n })\n }\n}\n\n/**\n * Release a claimed task back to its role queue.\n *\n * The counterpart of `claimUserTask`, and deliberately its mirror image: the\n * write is the same compare-and-set, guarded on the caller still being the\n * claimant, so a task reassigned or completed in the meantime is refused rather\n * than silently reopened. It uses the EXISTING statuses \u2014 the row goes back to\n * `PENDING` \u2014 because adding a `UserTaskStatus` value is an Ask-First\n * state-machine change and a released task is exactly as pending as it was\n * before anyone claimed it.\n *\n * @throws UserTaskError if the task is not the caller's to release\n */\nexport async function releaseUserTask(\n em: EntityManager,\n taskId: string,\n userId: string,\n scope: UserTaskScope\n): Promise<void> {\n const task = await em.findOne(UserTask, {\n id: taskId,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n status: 'IN_PROGRESS',\n })\n\n if (!task) {\n throw new UserTaskError(\n 'Task not found or not claimed',\n 'TASK_NOT_FOUND',\n { taskId }\n )\n }\n\n if (task.claimedBy !== userId) {\n throw new UserTaskError(\n 'Task is claimed by another user',\n 'TASK_ASSIGNED_TO_ANOTHER_USER',\n { taskId, claimedBy: task.claimedBy }\n )\n }\n\n const now = new Date()\n const released = await em.nativeUpdate(\n UserTask,\n {\n id: taskId,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n status: 'IN_PROGRESS',\n claimedBy: userId,\n },\n {\n claimedBy: null,\n claimedAt: null,\n status: 'PENDING',\n updatedAt: now,\n },\n )\n\n if (released === 0) {\n throw new UserTaskError(\n 'Task not found or not claimed',\n 'TASK_NOT_FOUND',\n { taskId }\n )\n }\n\n task.claimedBy = null\n task.claimedAt = null\n task.status = 'PENDING'\n task.updatedAt = now\n\n await em.flush()\n\n const instance = await em.findOne(WorkflowInstance, task.workflowInstanceId)\n if (instance) {\n await logWorkflowEvent(em, {\n workflowInstanceId: instance.id,\n stepInstanceId: task.stepInstanceId,\n eventType: 'USER_TASK_RELEASED',\n eventData: {\n taskId: task.id,\n taskName: task.taskName,\n releasedBy: userId,\n },\n userId,\n tenantId: instance.tenantId,\n organizationId: instance.organizationId,\n })\n }\n}\n\n// ============================================================================\n// User Task Reassignment\n// ============================================================================\n\n/**\n * The new owner of a task, as the reassign route resolved it.\n *\n * At least one of the two must be non-empty, and the route enforces it: the\n * shape \u00A76.4 makes uncompletable is precisely \"no assignee AND no role queue\",\n * so a reassignment that produced it would be the one write that can create the\n * problem reassignment exists to fix.\n */\nexport interface ReassignUserTaskTarget {\n assignedTo?: string | null\n assignedToRoles?: readonly string[] | null\n}\n\nexport interface ReassignUserTaskOptions {\n taskId: string\n /** Who performed the reassignment \u2014 recorded on `reassigned_by`. */\n userId: string\n target: ReassignUserTaskTarget\n reason?: string | null\n scope: UserTaskScope\n}\n\n/**\n * Statuses a task can be reassigned in.\n *\n * The same pair `completeUserTask` accepts: moving a COMPLETED or CANCELLED row\n * to somebody else records an audit trail for work that can never be done, and\n * the assignee would be handed a task whose Complete button always fails.\n */\nconst REASSIGNABLE_STATUSES: readonly string[] = ['PENDING', 'IN_PROGRESS']\n\n/**\n * Move a task to a different assignee or role queue, with an audit trail.\n *\n * This is the ACT half of the \u00A76.4 model's central asymmetry: administration\n * widens seeing, never acting, so an administrator who can see a colleague's\n * task \u2014 or a task that belongs to nobody at all \u2014 takes it first and the taking\n * is permanently recorded. It is also the documented remedy for the one shape\n * the model leaves uncompletable (no assignee, no claim, no role queue), which\n * is why it deliberately accepts a task in exactly that state.\n *\n * No new `UserTaskStatus` value is introduced. The audit lives on the columns\n * Phase 4a added (`reassigned_by` / `reassigned_at` / `reassign_reason`) plus a\n * `WorkflowEvent`, and a reassigned task is exactly as pending or in-progress as\n * it was \u2014 the same choice `releaseUserTask` made, and the one Phase 3a's\n * failure queue made when it reused `PAUSED` plus a marker.\n *\n * An in-flight CLAIM is always released. Handing a row to somebody else while a\n * colleague still holds it would leave `claimedBy ?? assignedTo` naming the old\n * claimant, so the predicate would keep treating the task as theirs and the new\n * assignee could not act on what they were just given.\n *\n * @throws UserTaskError when the task is absent from the caller's scope or is\n * in a terminal status\n */\nexport async function reassignUserTask(\n em: EntityManager,\n options: ReassignUserTaskOptions,\n): Promise<UserTask> {\n const { taskId, userId, target, reason, scope } = options\n\n const task = await em.findOne(UserTask, {\n id: taskId,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n })\n\n if (!task) {\n throw new UserTaskError('Task not found', 'TASK_NOT_FOUND', { taskId })\n }\n\n if (!REASSIGNABLE_STATUSES.includes(task.status)) {\n throw new UserTaskError(\n 'Task is no longer open and cannot be reassigned',\n 'TASK_NOT_REASSIGNABLE',\n { taskId, status: task.status },\n )\n }\n\n const assignedTo = target.assignedTo ?? null\n const assignedToRoles =\n target.assignedToRoles && target.assignedToRoles.length > 0 ? [...target.assignedToRoles] : null\n\n const previous = {\n assignedTo: task.assignedTo ?? null,\n assignedToRoles: task.assignedToRoles ?? null,\n claimedBy: task.claimedBy ?? null,\n }\n\n const now = new Date()\n\n task.assignedTo = assignedTo\n // Reassignment is a backoffice administration act and its target is resolved\n // from the tenant's user/role vocabulary, so the row lands in the backoffice\n // namespace. Portal reassignment would need a portal principal picker and is\n // not part of this surface.\n task.assigneeKind = 'user'\n task.assignedToRoles = assignedToRoles\n task.claimedBy = null\n task.claimedAt = null\n // A queued task must be PENDING to be claimable, and a task whose claim was\n // just released is exactly as pending as it was before anyone claimed it.\n if (task.status === 'IN_PROGRESS') task.status = 'PENDING'\n task.reassignedBy = userId\n task.reassignedAt = now\n task.reassignReason = reason ?? null\n task.updatedAt = now\n\n await em.flush()\n\n const instance = await em.findOne(WorkflowInstance, task.workflowInstanceId)\n if (instance) {\n await logWorkflowEvent(em, {\n workflowInstanceId: instance.id,\n stepInstanceId: task.stepInstanceId,\n branchInstanceId: task.branchInstanceId ?? null,\n eventType: 'USER_TASK_REASSIGNED',\n eventData: {\n taskId: task.id,\n taskName: task.taskName,\n reassignedBy: userId,\n reason: reason ?? null,\n from: previous,\n to: { assignedTo, assignedToRoles },\n },\n userId,\n tenantId: instance.tenantId,\n organizationId: instance.organizationId,\n })\n }\n\n return task\n}\n\n// ============================================================================\n// Helper Functions\n// ============================================================================\n\n/**\n * Log workflow event to event sourcing table\n */\nasync function logWorkflowEvent(\n em: EntityManager,\n event: {\n workflowInstanceId: string\n stepInstanceId: string | null\n branchInstanceId?: string | null\n eventType: string\n eventData: any\n userId?: string\n tenantId: string\n organizationId: string\n }\n): Promise<WorkflowEvent> {\n const workflowEvent = em.create(WorkflowEvent, {\n ...event,\n occurredAt: new Date(),\n })\n\n await em.persist(workflowEvent).flush()\n return workflowEvent\n}\n\n/**\n * Validate form data against the task's authored schema.\n *\n * Required-field presence in BOTH accepted `formSchema` shapes: the JSON-Schema\n * form and the `{ fields: [...] }` form the Studio writes, whose per-field\n * `required: true` flag this used to ignore entirely \u2014 so a task authored in\n * the visual editor validated nothing however many fields its author marked\n * mandatory. Both shapes now resolve through `lib/task-form-schema.ts`, the\n * same module the renderer walks, so they cannot drift apart again.\n *\n * Presence-only, deliberately: type checking would need the full JSON-Schema\n * vocabulary, and what matters is that a required field cannot be skipped.\n *\n * @param formData - User-provided form data\n * @param formSchema - Authored schema, in either accepted shape\n * @throws Error if validation fails\n */\nexport function validateFormData(\n formData: Record<string, any>,\n formSchema: unknown\n): void {\n for (const field of readRequiredFormFields(formSchema)) {\n if (!(field in formData) || formData[field] === null || formData[field] === undefined) {\n throw new Error(`Required field missing: ${field}`)\n }\n }\n}\n"],
5
- "mappings": "AAcA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,uBAAuB;AAChC,SAAS,iCAAiC;AAC1C;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,+BAA+B;AACxC,SAAS,8BAA8B;AACvC,YAAY,iBAAiB;AAC7B,YAAY,uBAAuB;AACnC,SAAS,oBAAoB;AAE7B,MAAM,SAAS,aAAa,WAAW;AA+ChC,MAAM,sBAAsB,MAAM;AAAA,EACvC,YACE,SACO,MACA,SACP;AACA,UAAM,OAAO;AAHN;AACA;AAGP,SAAK,OAAO;AAAA,EACd;AACF;AAqBA,eAAsB,iBACpB,IACA,WACA,SACe;AACf,QAAM,EAAE,QAAQ,UAAU,QAAQ,UAAU,YAAY,MAAM,IAAI;AAGlE,QAAM,OAAO,MAAM,GAAG,QAAQ,UAAU;AAAA,IACtC,IAAI;AAAA,IACJ,UAAU,MAAM;AAAA,IAChB,gBAAgB,MAAM;AAAA,IACtB,QAAQ,EAAE,KAAK,CAAC,WAAW,aAAa,EAAE;AAAA,EAC5C,CAAC;AAED,MAAI,CAAC,MAAM;AACT,UAAM,IAAI;AAAA,MACR;AAAA,MACA;AAAA,MACA,EAAE,OAAO;AAAA,IACX;AAAA,EACF;AAMA,QAAM,WAAW,KAAK,aAAa,KAAK;AACxC,MAAI,YAAY,aAAa,QAAQ;AACnC,UAAM,IAAI;AAAA,MACR;AAAA,MACA;AAAA,MACA,EAAE,QAAQ,YAAY,SAAS;AAAA,IACjC;AAAA,EACF;AAIA,MAAI,KAAK,YAAY;AACnB,QAAI;AACF,uBAAiB,UAAU,KAAK,UAAU;AAAA,IAC5C,SAAS,OAAO;AACd,YAAM,IAAI;AAAA,QACR,iBAAiB,QAAQ,MAAM,UAAU;AAAA,QACzC;AAAA,QACA,EAAE,QAAQ,YAAY,KAAK,YAAY,SAAS;AAAA,MAClD;AAAA,IACF;AAAA,EACF;AAMA,QAAM,iBAAiB,aACnB,kBAAkB,MAAM,wBAAwB,IAAI,IAAI,GAAG,WAAW,UAAU,IAChF;AAEJ,MAAI,cAAc,CAAC,gBAAgB;AACjC,UAAM,IAAI;AAAA,MACR;AAAA,MACA;AAAA,MACA,EAAE,QAAQ,WAAW;AAAA,IACvB;AAAA,EACF;AAKA,QAAM,iBAAiB,qBAAqB,UAAU,gBAAgB,EAAE;AAGxE,QAAM,MAAM,oBAAI,KAAK;AACrB,OAAK,SAAS;AACd,OAAK,WAAW;AAChB,OAAK,cAAc;AACnB,OAAK,cAAc;AACnB,OAAK,WAAW,YAAY;AAC5B,OAAK,YAAY;AAEjB,QAAM,GAAG,MAAM;AAGf,QAAM,WAAW,MAAM,GAAG,QAAQ,kBAAkB,KAAK,kBAAkB;AAC3E,MAAI,CAAC,UAAU;AACb,UAAM,IAAI;AAAA,MACR;AAAA,MACA;AAAA,MACA,EAAE,oBAAoB,KAAK,mBAAmB;AAAA,IAChD;AAAA,EACF;AASA,MAAI,KAAK,kBAAkB;AACzB,UAAM,iBAAiB,IAAI;AAAA,MACzB,oBAAoB,SAAS;AAAA,MAC7B,gBAAgB,KAAK;AAAA,MACrB,kBAAkB,KAAK;AAAA,MACvB,WAAW;AAAA,MACX,WAAW;AAAA,QACT,QAAQ,KAAK;AAAA,QACb,UAAU,KAAK;AAAA,QACf,aAAa;AAAA,QACb,UAAU;AAAA,QACV,GAAI,iBAAiB,EAAE,YAAY,eAAe,GAAG,IAAI,CAAC;AAAA,MAC5D;AAAA,MACA;AAAA,MACA,UAAU,SAAS;AAAA,MACnB,gBAAgB,SAAS;AAAA,IAC3B,CAAC;AAED,UAAM,EAAE,aAAa,IAAI,MAAM,OAAO,oBAAoB;AAC1D,UAAM,UAAU,MAAM,aAAa,IAAI;AAAA,MACrC,YAAY,SAAS;AAAA,MACrB,kBAAkB,KAAK;AAAA,MACvB,UAAU,SAAS;AAAA,MACnB,gBAAgB,SAAS;AAAA,MACzB,cAAc;AAAA,MACd,oBAAoB,KAAK;AAAA,MACzB,YAAY,EAAE,YAAY,KAAK,IAAI,UAAU,eAAe;AAAA,IAC9D,CAAC;AAED,QAAI,SAAS;AACX,YAAM,gBAAgB,IAAI,WAAW,SAAS,IAAI,EAAE,OAAO,CAAC;AAAA,IAC9D;AACA;AAAA,EACF;AAGA,WAAS,UAAU;AAAA,IACjB,GAAG,SAAS;AAAA,IACZ,GAAG;AAAA,EACL;AACA,WAAS,YAAY;AAGrB,QAAM,iBAAiB,IAAI;AAAA,IACzB,oBAAoB,SAAS;AAAA,IAC7B,gBAAgB,KAAK;AAAA,IACrB,WAAW;AAAA,IACX,WAAW;AAAA,MACT,QAAQ,KAAK;AAAA,MACb,UAAU,KAAK;AAAA,MACf,aAAa;AAAA,MACb,UAAU;AAAA,MACV,GAAI,iBAAiB,EAAE,YAAY,eAAe,GAAG,IAAI,CAAC;AAAA,IAC5D;AAAA,IACA;AAAA,IACA,UAAU,SAAS;AAAA,IACnB,gBAAgB,SAAS;AAAA,EAC3B,CAAC;AAGD,QAAM,eAAe,MAAM,GAAG,QAAQ,cAAc;AAAA,IAClD,IAAI,KAAK;AAAA,IACT,QAAQ;AAAA,EACV,CAAC;AAED,MAAI,cAAc;AAChB,UAAM,YAAY,SAAS,IAAI,cAAc,EAAE,YAAY,KAAK,IAAI,UAAU,eAAe,CAAC;AAAA,EAChG;AAGA,QAAM,gBAAgB,SAAS;AAG/B,QAAM,aAAa,MAAM,0BAA0B,IAAI,QAAQ;AAE/D,MAAI,CAAC,YAAY;AACf,UAAM,IAAI;AAAA,MACR;AAAA,MACA;AAAA,MACA,EAAE,cAAc,SAAS,aAAa;AAAA,IACxC;AAAA,EACF;AAEA,QAAM,oBAAoB;AAAA,IACxB,iBAAiB,SAAS;AAAA,IAC1B;AAAA,EACF;AAQA,QAAM,qBAAqB,iBACvB;AAAA,IACE,WAAW,WAAW,eAAe,CAAC;AAAA,IACtC,eAAe;AAAA,IACf;AAAA,EACF,IACA;AAEJ,MAAI,kBAAkB,CAAC,oBAAoB;AACzC,WAAO,KAAK,mFAAmF;AAAA,MAC7F,WAAW;AAAA,MACX,QAAQ,KAAK;AAAA,MACb,YAAY,eAAe;AAAA,MAC3B,cAAc,eAAe;AAAA,MAC7B;AAAA,IACF,CAAC;AAAA,EACH;AAEA,MAAI,qBAAqB,sBAAsB;AAC/C,MAAI,eAAe,oBAAoB,YAAY;AAEnD,MAAI,CAAC,cAAc;AAEjB,UAAM,mBAAmB,WAAW,WAAW,eAAe,CAAC,GAAG;AAAA,MAChE,CAAC,MAAW,EAAE,eAAe,iBAAiB,EAAE,YAAY;AAAA,IAC9D;AAEA,QAAI,gBAAgB,WAAW,GAAG;AAEhC;AAAA,IACF;AAGA,UAAM,mBAAmB,MAAM,kBAAkB;AAAA,MAC/C;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAEA,UAAM,uBAAuB,iBAAiB,KAAK,OAAK,EAAE,OAAO;AAEjE,QAAI,CAAC,wBAAwB,CAAC,qBAAqB,YAAY;AAE7D,eAAS,SAAS;AAClB,YAAM,GAAG,MAAM;AACf;AAAA,IACF;AAEA,mBAAe,qBAAqB,WAAW;AAC/C,yBAAqB,qBAAqB;AAAA,EAC5C;AAOA,MAAI,SAAS,WAAW,UAAU;AAChC,aAAS,SAAS;AAClB,UAAM,GAAG,MAAM;AAAA,EACjB;AAIA,QAAM,mBAAmB,MAAM,kBAAkB;AAAA,IAC/C;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,EAAE,GAAG,mBAAmB,cAAc,oBAAoB,aAAa;AAAA,EACzE;AAEA,MAAI,CAAC,iBAAiB,SAAS;AAC7B,WAAO,MAAM,qBAAqB,EAAE,WAAW,gBAAgB,KAAK,iBAAiB,MAAM,CAAC;AAE5F;AAAA,EACF;AAGA,QAAM,gBAAgB,IAAI,WAAW,SAAS,IAAI,EAAE,OAAO,CAAC;AAC9D;AAUA,SAAS,eACP,WACA,iBACS;AACT,MAAI,gBAAgB,WAAW,EAAG,QAAO;AACzC,QAAM,OAAO,IAAI,IAAI,eAAe;AACpC,SAAO,UAAU,KAAK,CAAC,SAAS,KAAK,IAAI,IAAI,CAAC;AAChD;AAuCA,eAAsB,cACpB,IACA,QACA,QACA,OACA,iBACe;AACf,QAAM,OAAO,MAAM,GAAG,QAAQ,UAAU;AAAA,IACtC,IAAI;AAAA,IACJ,UAAU,MAAM;AAAA,IAChB,gBAAgB,MAAM;AAAA,IACtB,QAAQ;AAAA,EACV,CAAC;AAED,MAAI,CAAC,MAAM;AACT,UAAM,IAAI;AAAA,MACR;AAAA,MACA;AAAA,MACA,EAAE,OAAO;AAAA,IACX;AAAA,EACF;AAEA,MAAI,KAAK,YAAY;AACnB,UAAM,IAAI;AAAA,MACR;AAAA,MACA;AAAA,MACA,EAAE,QAAQ,YAAY,KAAK,WAAW;AAAA,IACxC;AAAA,EACF;AAEA,MAAI,CAAC,KAAK,mBAAmB,KAAK,gBAAgB,WAAW,GAAG;AAC9D,UAAM,IAAI;AAAA,MACR;AAAA,MACA;AAAA,MACA,EAAE,OAAO;AAAA,IACX;AAAA,EACF;AAEA,MAAI,CAAC,eAAe,KAAK,iBAAiB,eAAe,GAAG;AAC1D,UAAM,IAAI;AAAA,MACR;AAAA,MACA;AAAA,MACA,EAAE,OAAO;AAAA,IACX;AAAA,EACF;AAEA,QAAM,MAAM,oBAAI,KAAK;AACrB,QAAM,UAAU,MAAM,GAAG;AAAA,IACvB;AAAA,IACA;AAAA,MACE,IAAI;AAAA,MACJ,UAAU,MAAM;AAAA,MAChB,gBAAgB,MAAM;AAAA,MACtB,QAAQ;AAAA,MACR,WAAW;AAAA,IACb;AAAA,IACA;AAAA,MACE,WAAW;AAAA,MACX,WAAW;AAAA,MACX,QAAQ;AAAA,MACR,WAAW;AAAA,IACb;AAAA,EACF;AAEA,MAAI,YAAY,GAAG;AACjB,UAAM,IAAI;AAAA,MACR;AAAA,MACA;AAAA,MACA,EAAE,OAAO;AAAA,IACX;AAAA,EACF;AAKA,OAAK,YAAY;AACjB,OAAK,YAAY;AACjB,OAAK,SAAS;AACd,OAAK,YAAY;AAEjB,QAAM,GAAG,MAAM;AAGf,QAAM,WAAW,MAAM,GAAG,QAAQ,kBAAkB,KAAK,kBAAkB;AAC3E,MAAI,UAAU;AACZ,UAAM,iBAAiB,IAAI;AAAA,MACzB,oBAAoB,SAAS;AAAA,MAC7B,gBAAgB,KAAK;AAAA,MACrB,WAAW;AAAA,MACX,WAAW;AAAA,QACT,QAAQ,KAAK;AAAA,QACb,UAAU,KAAK;AAAA,QACf,WAAW;AAAA,MACb;AAAA,MACA;AAAA,MACA,UAAU,SAAS;AAAA,MACnB,gBAAgB,SAAS;AAAA,IAC3B,CAAC;AAAA,EACH;AACF;AAeA,eAAsB,gBACpB,IACA,QACA,QACA,OACe;AACf,QAAM,OAAO,MAAM,GAAG,QAAQ,UAAU;AAAA,IACtC,IAAI;AAAA,IACJ,UAAU,MAAM;AAAA,IAChB,gBAAgB,MAAM;AAAA,IACtB,QAAQ;AAAA,EACV,CAAC;AAED,MAAI,CAAC,MAAM;AACT,UAAM,IAAI;AAAA,MACR;AAAA,MACA;AAAA,MACA,EAAE,OAAO;AAAA,IACX;AAAA,EACF;AAEA,MAAI,KAAK,cAAc,QAAQ;AAC7B,UAAM,IAAI;AAAA,MACR;AAAA,MACA;AAAA,MACA,EAAE,QAAQ,WAAW,KAAK,UAAU;AAAA,IACtC;AAAA,EACF;AAEA,QAAM,MAAM,oBAAI,KAAK;AACrB,QAAM,WAAW,MAAM,GAAG;AAAA,IACxB;AAAA,IACA;AAAA,MACE,IAAI;AAAA,MACJ,UAAU,MAAM;AAAA,MAChB,gBAAgB,MAAM;AAAA,MACtB,QAAQ;AAAA,MACR,WAAW;AAAA,IACb;AAAA,IACA;AAAA,MACE,WAAW;AAAA,MACX,WAAW;AAAA,MACX,QAAQ;AAAA,MACR,WAAW;AAAA,IACb;AAAA,EACF;AAEA,MAAI,aAAa,GAAG;AAClB,UAAM,IAAI;AAAA,MACR;AAAA,MACA;AAAA,MACA,EAAE,OAAO;AAAA,IACX;AAAA,EACF;AAEA,OAAK,YAAY;AACjB,OAAK,YAAY;AACjB,OAAK,SAAS;AACd,OAAK,YAAY;AAEjB,QAAM,GAAG,MAAM;AAEf,QAAM,WAAW,MAAM,GAAG,QAAQ,kBAAkB,KAAK,kBAAkB;AAC3E,MAAI,UAAU;AACZ,UAAM,iBAAiB,IAAI;AAAA,MACzB,oBAAoB,SAAS;AAAA,MAC7B,gBAAgB,KAAK;AAAA,MACrB,WAAW;AAAA,MACX,WAAW;AAAA,QACT,QAAQ,KAAK;AAAA,QACb,UAAU,KAAK;AAAA,QACf,YAAY;AAAA,MACd;AAAA,MACA;AAAA,MACA,UAAU,SAAS;AAAA,MACnB,gBAAgB,SAAS;AAAA,IAC3B,CAAC;AAAA,EACH;AACF;AAmCA,MAAM,wBAA2C,CAAC,WAAW,aAAa;AA0B1E,eAAsB,iBACpB,IACA,SACmB;AACnB,QAAM,EAAE,QAAQ,QAAQ,QAAQ,QAAQ,MAAM,IAAI;AAElD,QAAM,OAAO,MAAM,GAAG,QAAQ,UAAU;AAAA,IACtC,IAAI;AAAA,IACJ,UAAU,MAAM;AAAA,IAChB,gBAAgB,MAAM;AAAA,EACxB,CAAC;AAED,MAAI,CAAC,MAAM;AACT,UAAM,IAAI,cAAc,kBAAkB,kBAAkB,EAAE,OAAO,CAAC;AAAA,EACxE;AAEA,MAAI,CAAC,sBAAsB,SAAS,KAAK,MAAM,GAAG;AAChD,UAAM,IAAI;AAAA,MACR;AAAA,MACA;AAAA,MACA,EAAE,QAAQ,QAAQ,KAAK,OAAO;AAAA,IAChC;AAAA,EACF;AAEA,QAAM,aAAa,OAAO,cAAc;AACxC,QAAM,kBACJ,OAAO,mBAAmB,OAAO,gBAAgB,SAAS,IAAI,CAAC,GAAG,OAAO,eAAe,IAAI;AAE9F,QAAM,WAAW;AAAA,IACf,YAAY,KAAK,cAAc;AAAA,IAC/B,iBAAiB,KAAK,mBAAmB;AAAA,IACzC,WAAW,KAAK,aAAa;AAAA,EAC/B;AAEA,QAAM,MAAM,oBAAI,KAAK;AAErB,OAAK,aAAa;AAKlB,OAAK,eAAe;AACpB,OAAK,kBAAkB;AACvB,OAAK,YAAY;AACjB,OAAK,YAAY;AAGjB,MAAI,KAAK,WAAW,cAAe,MAAK,SAAS;AACjD,OAAK,eAAe;AACpB,OAAK,eAAe;AACpB,OAAK,iBAAiB,UAAU;AAChC,OAAK,YAAY;AAEjB,QAAM,GAAG,MAAM;AAEf,QAAM,WAAW,MAAM,GAAG,QAAQ,kBAAkB,KAAK,kBAAkB;AAC3E,MAAI,UAAU;AACZ,UAAM,iBAAiB,IAAI;AAAA,MACzB,oBAAoB,SAAS;AAAA,MAC7B,gBAAgB,KAAK;AAAA,MACrB,kBAAkB,KAAK,oBAAoB;AAAA,MAC3C,WAAW;AAAA,MACX,WAAW;AAAA,QACT,QAAQ,KAAK;AAAA,QACb,UAAU,KAAK;AAAA,QACf,cAAc;AAAA,QACd,QAAQ,UAAU;AAAA,QAClB,MAAM;AAAA,QACN,IAAI,EAAE,YAAY,gBAAgB;AAAA,MACpC;AAAA,MACA;AAAA,MACA,UAAU,SAAS;AAAA,MACnB,gBAAgB,SAAS;AAAA,IAC3B,CAAC;AAAA,EACH;AAEA,SAAO;AACT;AASA,eAAe,iBACb,IACA,OAUwB;AACxB,QAAM,gBAAgB,GAAG,OAAO,eAAe;AAAA,IAC7C,GAAG;AAAA,IACH,YAAY,oBAAI,KAAK;AAAA,EACvB,CAAC;AAED,QAAM,GAAG,QAAQ,aAAa,EAAE,MAAM;AACtC,SAAO;AACT;AAmBO,SAAS,iBACd,UACA,YACM;AACN,aAAW,SAAS,uBAAuB,UAAU,GAAG;AACtD,QAAI,EAAE,SAAS,aAAa,SAAS,KAAK,MAAM,QAAQ,SAAS,KAAK,MAAM,QAAW;AACrF,YAAM,IAAI,MAAM,2BAA2B,KAAK,EAAE;AAAA,IACpD;AAAA,EACF;AACF;",
4
+ "sourcesContent": ["/**\n * Workflows Module - User Task Handler Service\n *\n * Handles user task lifecycle operations:\n * - Completing user tasks\n * - Claiming tasks from role queues\n * - Reassigning tasks\n * - Escalating overdue tasks\n *\n * Functional API (no classes) following Open Mercato conventions.\n */\n\nimport { EntityManager } from '@mikro-orm/core'\nimport type { AwilixContainer } from 'awilix'\nimport {\n UserTask,\n WorkflowInstance,\n WorkflowEvent,\n StepInstance,\n} from '../data/entities'\nimport { executeWorkflow } from './workflow-executor'\nimport { findDefinitionForInstance } from './find-definition'\nimport {\n findTaskDecision,\n findTaskDecisionTransition,\n withRecordedDecision,\n} from './task-decisions'\nimport { loadTaskDecisionContext } from './task-decision-context'\nimport { readRequiredFormFields } from './task-form-schema'\nimport { isRunClosedToUserTasks } from './instance-status'\nimport * as stepHandler from './step-handler'\nimport * as transitionHandler from './transition-handler'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('workflows')\n\n// ============================================================================\n// Types and Interfaces\n// ============================================================================\n\n/**\n * Tenant/organization the caller is acting within. Every task lookup filters on\n * it, so a task id from another tenant resolves to \"not found\" instead of being\n * mutated.\n */\nexport interface UserTaskScope {\n tenantId: string\n organizationId: string\n}\n\n/**\n * Shape of the `taskHandler` DI registration (`di.ts`).\n *\n * The functions below stay exported \u2014 third-party modules may import them and\n * `BACKWARD_COMPATIBILITY.md` \u00A73 freezes import paths \u2014 but callers inside the\n * platform MUST resolve this service instead, per the module's first rule.\n */\nexport interface TaskHandlerService {\n completeUserTask: typeof completeUserTask\n claimUserTask: typeof claimUserTask\n releaseUserTask: typeof releaseUserTask\n reassignUserTask: typeof reassignUserTask\n}\n\nexport interface CompleteUserTaskOptions {\n taskId: string\n formData: Record<string, any>\n userId: string\n comments?: string\n /**\n * The decision button the assignee pressed, when the step authored any.\n *\n * It does two things: it SELECTS the outgoing route (the decision's\n * `transitionId` wins over the first-valid-transition default), and it is\n * RECORDED as part of the completion payload. Omitted, completion behaves\n * exactly as it always did.\n */\n decisionId?: string\n scope: UserTaskScope\n}\n\nexport class UserTaskError extends Error {\n constructor(\n message: string,\n public code: string,\n public details?: any\n ) {\n super(message)\n this.name = 'UserTaskError'\n }\n}\n\n// ============================================================================\n// User Task Completion\n// ============================================================================\n\n/**\n * Complete a user task and resume workflow execution\n *\n * This function:\n * 1. Validates the task exists and can be completed\n * 2. Updates the task with form data and completion info\n * 3. Merges form data into workflow context\n * 4. Logs completion event\n * 5. Resumes workflow execution\n *\n * @param em - Entity manager\n * @param container - DI container for workflow execution\n * @param options - Task completion options\n * @throws UserTaskError if task not found or validation fails\n */\nexport async function completeUserTask(\n em: EntityManager,\n container: AwilixContainer,\n options: CompleteUserTaskOptions\n): Promise<void> {\n const { taskId, formData, userId, comments, decisionId, scope } = options\n\n // Fetch task\n const task = await em.findOne(UserTask, {\n id: taskId,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n status: { $in: ['PENDING', 'IN_PROGRESS'] },\n })\n\n if (!task) {\n throw new UserTaskError(\n 'Task not found or already completed',\n 'TASK_NOT_FOUND',\n { taskId }\n )\n }\n\n // A task addressed to one person is that person's to finish. Tasks that name\n // nobody \u2014 agent dispositions, role queues nobody has claimed \u2014 stay open to\n // anyone the route's feature gate already admitted, so this narrows the\n // over-permissive case without stranding the unassigned ones.\n const claimant = task.claimedBy ?? task.assignedTo\n if (claimant && claimant !== userId) {\n throw new UserTaskError(\n 'Task is assigned to another user',\n 'TASK_ASSIGNED_TO_ANOTHER_USER',\n { taskId, assignedTo: claimant }\n )\n }\n\n // Validate form data against schema (simple validation for MVP)\n // In Phase 7, we'll add comprehensive JSON Schema validation\n if (task.formSchema) {\n try {\n validateFormData(formData, task.formSchema)\n } catch (error) {\n throw new UserTaskError(\n error instanceof Error ? error.message : 'Form validation failed',\n 'FORM_VALIDATION_FAILED',\n { taskId, formSchema: task.formSchema, formData }\n )\n }\n }\n\n // Resolve the pressed decision BEFORE anything is mutated: an unknown\n // decision id must leave the task untouched rather than complete it down the\n // default route. The list is re-resolved from the instance's pinned\n // definition, so it is exactly the list the assignee was shown.\n const chosenDecision = decisionId\n ? findTaskDecision((await loadTaskDecisionContext(em, task)).decisions, decisionId)\n : null\n\n if (decisionId && !chosenDecision) {\n throw new UserTaskError(\n 'Unknown decision for this task',\n 'UNKNOWN_DECISION',\n { taskId, decisionId }\n )\n }\n\n const instance = await em.findOne(WorkflowInstance, task.workflowInstanceId)\n if (!instance) {\n throw new UserTaskError(\n 'Workflow instance not found',\n 'INSTANCE_NOT_FOUND',\n { workflowInstanceId: task.workflowInstanceId }\n )\n }\n\n if (isRunClosedToUserTasks(instance.status)) {\n throw new UserTaskError(\n 'Workflow is no longer active',\n 'WORKFLOW_NOT_ACTIVE',\n { taskId, workflowInstanceId: instance.id, status: instance.status }\n )\n }\n\n // The chosen decision rides along inside the completion payload the task\n // already persists \u2014 no new column, and downstream route conditions can read\n // it once the payload is merged into the run context.\n const completionData = withRecordedDecision(formData, chosenDecision?.id)\n\n // Update task\n const now = new Date()\n task.status = 'COMPLETED'\n task.formData = completionData\n task.completedBy = userId\n task.completedAt = now\n task.comments = comments || null\n task.updatedAt = now\n\n await em.flush()\n\n // Branch-scoped completion: when the task belongs to a parallel branch,\n // merge form data into the branch namespace and resume just that branch.\n //\n // The decision is RECORDED here exactly as it is on the main path, but it does\n // not select the route: a branch advances through `resumeBranch` with its own\n // token, and overriding a branch's outgoing route is a parallel-execution\n // change rather than a task-surface one.\n if (task.branchInstanceId) {\n await logWorkflowEvent(em, {\n workflowInstanceId: instance.id,\n stepInstanceId: task.stepInstanceId,\n branchInstanceId: task.branchInstanceId,\n eventType: 'USER_TASK_COMPLETED',\n eventData: {\n taskId: task.id,\n taskName: task.taskName,\n completedBy: userId,\n formData: completionData,\n ...(chosenDecision ? { decisionId: chosenDecision.id } : {}),\n },\n userId,\n tenantId: instance.tenantId,\n organizationId: instance.organizationId,\n })\n\n const { resumeBranch } = await import('./parallel-handler')\n const resumed = await resumeBranch(em, {\n instanceId: instance.id,\n branchInstanceId: task.branchInstanceId,\n tenantId: instance.tenantId,\n organizationId: instance.organizationId,\n contextMerge: completionData,\n exitStepInstanceId: task.stepInstanceId,\n exitOutput: { userTaskId: task.id, formData: completionData },\n })\n\n if (resumed) {\n await executeWorkflow(em, container, instance.id, { userId })\n }\n return\n }\n\n // Merge form data into workflow context\n instance.context = {\n ...instance.context,\n ...completionData,\n }\n instance.updatedAt = now\n\n // Log USER_TASK_COMPLETED event\n await logWorkflowEvent(em, {\n workflowInstanceId: instance.id,\n stepInstanceId: task.stepInstanceId,\n eventType: 'USER_TASK_COMPLETED',\n eventData: {\n taskId: task.id,\n taskName: task.taskName,\n completedBy: userId,\n formData: completionData,\n ...(chosenDecision ? { decisionId: chosenDecision.id } : {}),\n },\n userId,\n tenantId: instance.tenantId,\n organizationId: instance.organizationId,\n })\n\n // Mark the step instance as completed\n const stepInstance = await em.findOne(StepInstance, {\n id: task.stepInstanceId,\n status: 'ACTIVE',\n })\n\n if (stepInstance) {\n await stepHandler.exitStep(em, stepInstance, { userTaskId: task.id, formData: completionData })\n }\n\n // Find the next automatic transition from the current step\n const currentStepId = instance.currentStepId\n\n // Load workflow definition to find transitions\n const definition = await findDefinitionForInstance(em, instance)\n\n if (!definition) {\n throw new UserTaskError(\n 'Workflow definition not found',\n 'DEFINITION_NOT_FOUND',\n { definitionId: instance.definitionId }\n )\n }\n\n const transitionContext = {\n workflowContext: instance.context,\n userId,\n }\n\n // A decision names the route to take. It is bound to the transition's durable\n // id, so it survives every route reorder, and it is looked up among the routes\n // LEAVING this step only \u2014 a button can never send the run down a route that\n // does not start where the run currently is. A decision whose route no longer\n // exists there falls through to the default evaluation rather than stranding\n // the run at a step it already completed.\n const decisionTransition = chosenDecision\n ? findTaskDecisionTransition(\n definition.definition.transitions || [],\n chosenDecision.transitionId,\n currentStepId\n )\n : null\n\n if (chosenDecision && !decisionTransition) {\n logger.warn('Task decision route not found on the current step; falling back to auto routing', {\n component: 'task-handler',\n taskId: task.id,\n decisionId: chosenDecision.id,\n transitionId: chosenDecision.transitionId,\n currentStepId,\n })\n }\n\n let selectedTransition = decisionTransition ?? null\n let targetStepId = selectedTransition?.toStepId ?? null\n\n if (!targetStepId) {\n // Find automatic transitions from current step\n const autoTransitions = (definition.definition.transitions || []).filter(\n (t: any) => t.fromStepId === currentStepId && t.trigger === 'auto'\n )\n\n if (autoTransitions.length === 0) {\n // No automatic transitions, workflow stays paused at current step\n return\n }\n\n // Find valid transitions using transition handler\n const validTransitions = await transitionHandler.findValidTransitions(\n em,\n instance,\n currentStepId,\n transitionContext\n )\n\n const firstValidTransition = validTransitions.find(t => t.isValid)\n\n if (!firstValidTransition || !firstValidTransition.transition) {\n // Resume workflow execution anyway, maybe conditions will be met later\n instance.status = 'RUNNING'\n await em.flush()\n return\n }\n\n targetStepId = firstValidTransition.transition.toStepId\n selectedTransition = firstValidTransition.transition\n }\n\n // Resume from the paused wait BEFORE executing the transition, exactly as\n // `sendSignal` does. Without it the executor's defense-in-depth PAUSED check\n // stops the run at the first step the completion traverses, so the instance\n // sits there as PAUSED and never reaches END. Guarded on PAUSED so completing\n // a stale task can never resurrect a CANCELLED or terminal run.\n if (instance.status === 'PAUSED') {\n instance.status = 'RUNNING'\n await em.flush()\n }\n\n // Execute the transition to move to next step\n\n const transitionResult = await transitionHandler.executeTransition(\n em,\n container,\n instance,\n currentStepId,\n targetStepId,\n { ...transitionContext, transitionId: selectedTransition?.transitionId },\n )\n\n if (!transitionResult.success) {\n logger.error('Transition failed', { component: 'task-handler', err: transitionResult.error })\n // Don't throw, just leave workflow in current state\n return\n }\n\n // Now continue workflow execution from the new step\n await executeWorkflow(em, container, instance.id, { userId })\n}\n\n/**\n * Whether the caller belongs to at least one of the task's queues.\n *\n * Exact string equality on role NAMES \u2014 no case folding and no trimming, because\n * the names on both sides come from the same tenant role table and normalizing\n * one side would let `Approver` claim an `approver` queue that RBAC treats as a\n * different role.\n */\nfunction hasRoleOverlap(\n taskRoles: readonly string[],\n callerRoleNames: readonly string[],\n): boolean {\n if (callerRoleNames.length === 0) return false\n const held = new Set(callerRoleNames)\n return taskRoles.some((role) => held.has(role))\n}\n\n/**\n * Claim a user task from a role queue\n *\n * Allows a user to claim a task that's assigned to their role(s).\n *\n * The claim itself is a COMPARE-AND-SET, not a read-then-write: the lookup\n * below only decides which error to report, while the row is taken with a\n * conditional `UPDATE \u2026 WHERE status = 'PENDING' AND claimed_by IS NULL`. Two\n * callers racing for the same task \u2014 which is exactly what the work inbox's\n * claim-next affordance produces \u2014 therefore cannot both win; the loser sees\n * zero affected rows and the same `TASK_NOT_FOUND` it would have seen a moment\n * later. Reading the status and flushing the entity afterwards would have let\n * both transactions read `PENDING` and both write.\n *\n * Membership in the queue is a REQUIREMENT, not a hint: a caller who holds none\n * of `assignedToRoles` is refused with the same `TASK_NOT_FOUND` a nonexistent\n * id produces, so a queue the caller does not belong to is indistinguishable\n * from a task that is not there. Holding `workflows.tasks.claim` admits you to\n * the endpoint; it does not admit you to somebody else's queue.\n *\n * `assignedToRoles` and `callerRoleNames` are both role NAMES \u2014 the Studio\n * picker writes names, the engine copies them onto the task, and `auth.roles`\n * carries names. A role RENAME therefore orphans existing assignments; moving\n * the comparison onto immutable role ids is a coordinated data + authored-\n * definition migration and is deliberately not done here.\n *\n * @param em - Entity manager\n * @param taskId - Task ID to claim\n * @param userId - User claiming the task\n * @param scope - Tenant/organization the caller acts within; both the lookup and\n * the conditional update filter on it, so a task belonging to another tenant\n * is never reachable and never writable\n * @param callerRoleNames - Server-derived role names of the caller (`auth.roles`),\n * never client-supplied. Required rather than optional so a caller that cannot\n * supply them fails to compile instead of silently claiming any queue.\n * @throws UserTaskError if task cannot be claimed\n */\nexport async function claimUserTask(\n em: EntityManager,\n taskId: string,\n userId: string,\n scope: UserTaskScope,\n callerRoleNames: readonly string[]\n): Promise<void> {\n const task = await em.findOne(UserTask, {\n id: taskId,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n status: 'PENDING',\n })\n\n if (!task) {\n throw new UserTaskError(\n 'Task not found or already claimed',\n 'TASK_NOT_FOUND',\n { taskId }\n )\n }\n\n if (task.assignedTo) {\n throw new UserTaskError(\n 'Task is already assigned to a specific user',\n 'TASK_ALREADY_ASSIGNED',\n { taskId, assignedTo: task.assignedTo }\n )\n }\n\n if (!task.assignedToRoles || task.assignedToRoles.length === 0) {\n throw new UserTaskError(\n 'Task is not assigned to any roles',\n 'TASK_NOT_ROLE_ASSIGNED',\n { taskId }\n )\n }\n\n if (!hasRoleOverlap(task.assignedToRoles, callerRoleNames)) {\n throw new UserTaskError(\n 'Task not found or already claimed',\n 'TASK_NOT_FOUND',\n { taskId }\n )\n }\n\n const now = new Date()\n const claimed = await em.nativeUpdate(\n UserTask,\n {\n id: taskId,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n status: 'PENDING',\n claimedBy: null,\n },\n {\n claimedBy: userId,\n claimedAt: now,\n status: 'IN_PROGRESS',\n updatedAt: now,\n },\n )\n\n if (claimed === 0) {\n throw new UserTaskError(\n 'Task not found or already claimed',\n 'TASK_NOT_FOUND',\n { taskId }\n )\n }\n\n // The conditional update above already wrote the row; mirroring it onto the\n // managed entity keeps the caller's identity-mapped copy from reporting the\n // pre-claim state back to the client.\n task.claimedBy = userId\n task.claimedAt = now\n task.status = 'IN_PROGRESS'\n task.updatedAt = now\n\n await em.flush()\n\n // Log event\n const instance = await em.findOne(WorkflowInstance, task.workflowInstanceId)\n if (instance) {\n await logWorkflowEvent(em, {\n workflowInstanceId: instance.id,\n stepInstanceId: task.stepInstanceId,\n eventType: 'USER_TASK_STARTED',\n eventData: {\n taskId: task.id,\n taskName: task.taskName,\n claimedBy: userId,\n },\n userId,\n tenantId: instance.tenantId,\n organizationId: instance.organizationId,\n })\n }\n}\n\n/**\n * Release a claimed task back to its role queue.\n *\n * The counterpart of `claimUserTask`, and deliberately its mirror image: the\n * write is the same compare-and-set, guarded on the caller still being the\n * claimant, so a task reassigned or completed in the meantime is refused rather\n * than silently reopened. It uses the EXISTING statuses \u2014 the row goes back to\n * `PENDING` \u2014 because adding a `UserTaskStatus` value is an Ask-First\n * state-machine change and a released task is exactly as pending as it was\n * before anyone claimed it.\n *\n * @throws UserTaskError if the task is not the caller's to release\n */\nexport async function releaseUserTask(\n em: EntityManager,\n taskId: string,\n userId: string,\n scope: UserTaskScope\n): Promise<void> {\n const task = await em.findOne(UserTask, {\n id: taskId,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n status: 'IN_PROGRESS',\n })\n\n if (!task) {\n throw new UserTaskError(\n 'Task not found or not claimed',\n 'TASK_NOT_FOUND',\n { taskId }\n )\n }\n\n if (task.claimedBy !== userId) {\n throw new UserTaskError(\n 'Task is claimed by another user',\n 'TASK_ASSIGNED_TO_ANOTHER_USER',\n { taskId, claimedBy: task.claimedBy }\n )\n }\n\n const now = new Date()\n const released = await em.nativeUpdate(\n UserTask,\n {\n id: taskId,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n status: 'IN_PROGRESS',\n claimedBy: userId,\n },\n {\n claimedBy: null,\n claimedAt: null,\n status: 'PENDING',\n updatedAt: now,\n },\n )\n\n if (released === 0) {\n throw new UserTaskError(\n 'Task not found or not claimed',\n 'TASK_NOT_FOUND',\n { taskId }\n )\n }\n\n task.claimedBy = null\n task.claimedAt = null\n task.status = 'PENDING'\n task.updatedAt = now\n\n await em.flush()\n\n const instance = await em.findOne(WorkflowInstance, task.workflowInstanceId)\n if (instance) {\n await logWorkflowEvent(em, {\n workflowInstanceId: instance.id,\n stepInstanceId: task.stepInstanceId,\n eventType: 'USER_TASK_RELEASED',\n eventData: {\n taskId: task.id,\n taskName: task.taskName,\n releasedBy: userId,\n },\n userId,\n tenantId: instance.tenantId,\n organizationId: instance.organizationId,\n })\n }\n}\n\n// ============================================================================\n// User Task Reassignment\n// ============================================================================\n\n/**\n * The new owner of a task, as the reassign route resolved it.\n *\n * At least one of the two must be non-empty, and the route enforces it: the\n * shape \u00A76.4 makes uncompletable is precisely \"no assignee AND no role queue\",\n * so a reassignment that produced it would be the one write that can create the\n * problem reassignment exists to fix.\n */\nexport interface ReassignUserTaskTarget {\n assignedTo?: string | null\n assignedToRoles?: readonly string[] | null\n}\n\nexport interface ReassignUserTaskOptions {\n taskId: string\n /** Who performed the reassignment \u2014 recorded on `reassigned_by`. */\n userId: string\n target: ReassignUserTaskTarget\n reason?: string | null\n scope: UserTaskScope\n}\n\n/**\n * Statuses a task can be reassigned in.\n *\n * The same pair `completeUserTask` accepts: moving a COMPLETED or CANCELLED row\n * to somebody else records an audit trail for work that can never be done, and\n * the assignee would be handed a task whose Complete button always fails.\n */\nconst REASSIGNABLE_STATUSES: readonly string[] = ['PENDING', 'IN_PROGRESS']\n\n/**\n * Move a task to a different assignee or role queue, with an audit trail.\n *\n * This is the ACT half of the \u00A76.4 model's central asymmetry: administration\n * widens seeing, never acting, so an administrator who can see a colleague's\n * task \u2014 or a task that belongs to nobody at all \u2014 takes it first and the taking\n * is permanently recorded. It is also the documented remedy for the one shape\n * the model leaves uncompletable (no assignee, no claim, no role queue), which\n * is why it deliberately accepts a task in exactly that state.\n *\n * No new `UserTaskStatus` value is introduced. The audit lives on the columns\n * Phase 4a added (`reassigned_by` / `reassigned_at` / `reassign_reason`) plus a\n * `WorkflowEvent`, and a reassigned task is exactly as pending or in-progress as\n * it was \u2014 the same choice `releaseUserTask` made, and the one Phase 3a's\n * failure queue made when it reused `PAUSED` plus a marker.\n *\n * An in-flight CLAIM is always released. Handing a row to somebody else while a\n * colleague still holds it would leave `claimedBy ?? assignedTo` naming the old\n * claimant, so the predicate would keep treating the task as theirs and the new\n * assignee could not act on what they were just given.\n *\n * @throws UserTaskError when the task is absent from the caller's scope or is\n * in a terminal status\n */\nexport async function reassignUserTask(\n em: EntityManager,\n options: ReassignUserTaskOptions,\n): Promise<UserTask> {\n const { taskId, userId, target, reason, scope } = options\n\n const task = await em.findOne(UserTask, {\n id: taskId,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n })\n\n if (!task) {\n throw new UserTaskError('Task not found', 'TASK_NOT_FOUND', { taskId })\n }\n\n if (!REASSIGNABLE_STATUSES.includes(task.status)) {\n throw new UserTaskError(\n 'Task is no longer open and cannot be reassigned',\n 'TASK_NOT_REASSIGNABLE',\n { taskId, status: task.status },\n )\n }\n\n const assignedTo = target.assignedTo ?? null\n const assignedToRoles =\n target.assignedToRoles && target.assignedToRoles.length > 0 ? [...target.assignedToRoles] : null\n\n const previous = {\n assignedTo: task.assignedTo ?? null,\n assignedToRoles: task.assignedToRoles ?? null,\n claimedBy: task.claimedBy ?? null,\n }\n\n const now = new Date()\n\n task.assignedTo = assignedTo\n // Reassignment is a backoffice administration act and its target is resolved\n // from the tenant's user/role vocabulary, so the row lands in the backoffice\n // namespace. Portal reassignment would need a portal principal picker and is\n // not part of this surface.\n task.assigneeKind = 'user'\n task.assignedToRoles = assignedToRoles\n task.claimedBy = null\n task.claimedAt = null\n // A queued task must be PENDING to be claimable, and a task whose claim was\n // just released is exactly as pending as it was before anyone claimed it.\n if (task.status === 'IN_PROGRESS') task.status = 'PENDING'\n task.reassignedBy = userId\n task.reassignedAt = now\n task.reassignReason = reason ?? null\n task.updatedAt = now\n\n await em.flush()\n\n const instance = await em.findOne(WorkflowInstance, task.workflowInstanceId)\n if (instance) {\n await logWorkflowEvent(em, {\n workflowInstanceId: instance.id,\n stepInstanceId: task.stepInstanceId,\n branchInstanceId: task.branchInstanceId ?? null,\n eventType: 'USER_TASK_REASSIGNED',\n eventData: {\n taskId: task.id,\n taskName: task.taskName,\n reassignedBy: userId,\n reason: reason ?? null,\n from: previous,\n to: { assignedTo, assignedToRoles },\n },\n userId,\n tenantId: instance.tenantId,\n organizationId: instance.organizationId,\n })\n }\n\n return task\n}\n\n// ============================================================================\n// Helper Functions\n// ============================================================================\n\n/**\n * Log workflow event to event sourcing table\n */\nasync function logWorkflowEvent(\n em: EntityManager,\n event: {\n workflowInstanceId: string\n stepInstanceId: string | null\n branchInstanceId?: string | null\n eventType: string\n eventData: any\n userId?: string\n tenantId: string\n organizationId: string\n }\n): Promise<WorkflowEvent> {\n const workflowEvent = em.create(WorkflowEvent, {\n ...event,\n occurredAt: new Date(),\n })\n\n await em.persist(workflowEvent).flush()\n return workflowEvent\n}\n\n/**\n * Validate form data against the task's authored schema.\n *\n * Required-field presence in BOTH accepted `formSchema` shapes: the JSON-Schema\n * form and the `{ fields: [...] }` form the Studio writes, whose per-field\n * `required: true` flag this used to ignore entirely \u2014 so a task authored in\n * the visual editor validated nothing however many fields its author marked\n * mandatory. Both shapes now resolve through `lib/task-form-schema.ts`, the\n * same module the renderer walks, so they cannot drift apart again.\n *\n * Presence-only, deliberately: type checking would need the full JSON-Schema\n * vocabulary, and what matters is that a required field cannot be skipped.\n *\n * @param formData - User-provided form data\n * @param formSchema - Authored schema, in either accepted shape\n * @throws Error if validation fails\n */\nexport function validateFormData(\n formData: Record<string, any>,\n formSchema: unknown\n): void {\n for (const field of readRequiredFormFields(formSchema)) {\n if (!(field in formData) || formData[field] === null || formData[field] === undefined) {\n throw new Error(`Required field missing: ${field}`)\n }\n }\n}\n"],
5
+ "mappings": "AAcA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,uBAAuB;AAChC,SAAS,iCAAiC;AAC1C;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,+BAA+B;AACxC,SAAS,8BAA8B;AACvC,SAAS,8BAA8B;AACvC,YAAY,iBAAiB;AAC7B,YAAY,uBAAuB;AACnC,SAAS,oBAAoB;AAE7B,MAAM,SAAS,aAAa,WAAW;AA+ChC,MAAM,sBAAsB,MAAM;AAAA,EACvC,YACE,SACO,MACA,SACP;AACA,UAAM,OAAO;AAHN;AACA;AAGP,SAAK,OAAO;AAAA,EACd;AACF;AAqBA,eAAsB,iBACpB,IACA,WACA,SACe;AACf,QAAM,EAAE,QAAQ,UAAU,QAAQ,UAAU,YAAY,MAAM,IAAI;AAGlE,QAAM,OAAO,MAAM,GAAG,QAAQ,UAAU;AAAA,IACtC,IAAI;AAAA,IACJ,UAAU,MAAM;AAAA,IAChB,gBAAgB,MAAM;AAAA,IACtB,QAAQ,EAAE,KAAK,CAAC,WAAW,aAAa,EAAE;AAAA,EAC5C,CAAC;AAED,MAAI,CAAC,MAAM;AACT,UAAM,IAAI;AAAA,MACR;AAAA,MACA;AAAA,MACA,EAAE,OAAO;AAAA,IACX;AAAA,EACF;AAMA,QAAM,WAAW,KAAK,aAAa,KAAK;AACxC,MAAI,YAAY,aAAa,QAAQ;AACnC,UAAM,IAAI;AAAA,MACR;AAAA,MACA;AAAA,MACA,EAAE,QAAQ,YAAY,SAAS;AAAA,IACjC;AAAA,EACF;AAIA,MAAI,KAAK,YAAY;AACnB,QAAI;AACF,uBAAiB,UAAU,KAAK,UAAU;AAAA,IAC5C,SAAS,OAAO;AACd,YAAM,IAAI;AAAA,QACR,iBAAiB,QAAQ,MAAM,UAAU;AAAA,QACzC;AAAA,QACA,EAAE,QAAQ,YAAY,KAAK,YAAY,SAAS;AAAA,MAClD;AAAA,IACF;AAAA,EACF;AAMA,QAAM,iBAAiB,aACnB,kBAAkB,MAAM,wBAAwB,IAAI,IAAI,GAAG,WAAW,UAAU,IAChF;AAEJ,MAAI,cAAc,CAAC,gBAAgB;AACjC,UAAM,IAAI;AAAA,MACR;AAAA,MACA;AAAA,MACA,EAAE,QAAQ,WAAW;AAAA,IACvB;AAAA,EACF;AAEA,QAAM,WAAW,MAAM,GAAG,QAAQ,kBAAkB,KAAK,kBAAkB;AAC3E,MAAI,CAAC,UAAU;AACb,UAAM,IAAI;AAAA,MACR;AAAA,MACA;AAAA,MACA,EAAE,oBAAoB,KAAK,mBAAmB;AAAA,IAChD;AAAA,EACF;AAEA,MAAI,uBAAuB,SAAS,MAAM,GAAG;AAC3C,UAAM,IAAI;AAAA,MACR;AAAA,MACA;AAAA,MACA,EAAE,QAAQ,oBAAoB,SAAS,IAAI,QAAQ,SAAS,OAAO;AAAA,IACrE;AAAA,EACF;AAKA,QAAM,iBAAiB,qBAAqB,UAAU,gBAAgB,EAAE;AAGxE,QAAM,MAAM,oBAAI,KAAK;AACrB,OAAK,SAAS;AACd,OAAK,WAAW;AAChB,OAAK,cAAc;AACnB,OAAK,cAAc;AACnB,OAAK,WAAW,YAAY;AAC5B,OAAK,YAAY;AAEjB,QAAM,GAAG,MAAM;AASf,MAAI,KAAK,kBAAkB;AACzB,UAAM,iBAAiB,IAAI;AAAA,MACzB,oBAAoB,SAAS;AAAA,MAC7B,gBAAgB,KAAK;AAAA,MACrB,kBAAkB,KAAK;AAAA,MACvB,WAAW;AAAA,MACX,WAAW;AAAA,QACT,QAAQ,KAAK;AAAA,QACb,UAAU,KAAK;AAAA,QACf,aAAa;AAAA,QACb,UAAU;AAAA,QACV,GAAI,iBAAiB,EAAE,YAAY,eAAe,GAAG,IAAI,CAAC;AAAA,MAC5D;AAAA,MACA;AAAA,MACA,UAAU,SAAS;AAAA,MACnB,gBAAgB,SAAS;AAAA,IAC3B,CAAC;AAED,UAAM,EAAE,aAAa,IAAI,MAAM,OAAO,oBAAoB;AAC1D,UAAM,UAAU,MAAM,aAAa,IAAI;AAAA,MACrC,YAAY,SAAS;AAAA,MACrB,kBAAkB,KAAK;AAAA,MACvB,UAAU,SAAS;AAAA,MACnB,gBAAgB,SAAS;AAAA,MACzB,cAAc;AAAA,MACd,oBAAoB,KAAK;AAAA,MACzB,YAAY,EAAE,YAAY,KAAK,IAAI,UAAU,eAAe;AAAA,IAC9D,CAAC;AAED,QAAI,SAAS;AACX,YAAM,gBAAgB,IAAI,WAAW,SAAS,IAAI,EAAE,OAAO,CAAC;AAAA,IAC9D;AACA;AAAA,EACF;AAGA,WAAS,UAAU;AAAA,IACjB,GAAG,SAAS;AAAA,IACZ,GAAG;AAAA,EACL;AACA,WAAS,YAAY;AAGrB,QAAM,iBAAiB,IAAI;AAAA,IACzB,oBAAoB,SAAS;AAAA,IAC7B,gBAAgB,KAAK;AAAA,IACrB,WAAW;AAAA,IACX,WAAW;AAAA,MACT,QAAQ,KAAK;AAAA,MACb,UAAU,KAAK;AAAA,MACf,aAAa;AAAA,MACb,UAAU;AAAA,MACV,GAAI,iBAAiB,EAAE,YAAY,eAAe,GAAG,IAAI,CAAC;AAAA,IAC5D;AAAA,IACA;AAAA,IACA,UAAU,SAAS;AAAA,IACnB,gBAAgB,SAAS;AAAA,EAC3B,CAAC;AAGD,QAAM,eAAe,MAAM,GAAG,QAAQ,cAAc;AAAA,IAClD,IAAI,KAAK;AAAA,IACT,QAAQ;AAAA,EACV,CAAC;AAED,MAAI,cAAc;AAChB,UAAM,YAAY,SAAS,IAAI,cAAc,EAAE,YAAY,KAAK,IAAI,UAAU,eAAe,CAAC;AAAA,EAChG;AAGA,QAAM,gBAAgB,SAAS;AAG/B,QAAM,aAAa,MAAM,0BAA0B,IAAI,QAAQ;AAE/D,MAAI,CAAC,YAAY;AACf,UAAM,IAAI;AAAA,MACR;AAAA,MACA;AAAA,MACA,EAAE,cAAc,SAAS,aAAa;AAAA,IACxC;AAAA,EACF;AAEA,QAAM,oBAAoB;AAAA,IACxB,iBAAiB,SAAS;AAAA,IAC1B;AAAA,EACF;AAQA,QAAM,qBAAqB,iBACvB;AAAA,IACE,WAAW,WAAW,eAAe,CAAC;AAAA,IACtC,eAAe;AAAA,IACf;AAAA,EACF,IACA;AAEJ,MAAI,kBAAkB,CAAC,oBAAoB;AACzC,WAAO,KAAK,mFAAmF;AAAA,MAC7F,WAAW;AAAA,MACX,QAAQ,KAAK;AAAA,MACb,YAAY,eAAe;AAAA,MAC3B,cAAc,eAAe;AAAA,MAC7B;AAAA,IACF,CAAC;AAAA,EACH;AAEA,MAAI,qBAAqB,sBAAsB;AAC/C,MAAI,eAAe,oBAAoB,YAAY;AAEnD,MAAI,CAAC,cAAc;AAEjB,UAAM,mBAAmB,WAAW,WAAW,eAAe,CAAC,GAAG;AAAA,MAChE,CAAC,MAAW,EAAE,eAAe,iBAAiB,EAAE,YAAY;AAAA,IAC9D;AAEA,QAAI,gBAAgB,WAAW,GAAG;AAEhC;AAAA,IACF;AAGA,UAAM,mBAAmB,MAAM,kBAAkB;AAAA,MAC/C;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAEA,UAAM,uBAAuB,iBAAiB,KAAK,OAAK,EAAE,OAAO;AAEjE,QAAI,CAAC,wBAAwB,CAAC,qBAAqB,YAAY;AAE7D,eAAS,SAAS;AAClB,YAAM,GAAG,MAAM;AACf;AAAA,IACF;AAEA,mBAAe,qBAAqB,WAAW;AAC/C,yBAAqB,qBAAqB;AAAA,EAC5C;AAOA,MAAI,SAAS,WAAW,UAAU;AAChC,aAAS,SAAS;AAClB,UAAM,GAAG,MAAM;AAAA,EACjB;AAIA,QAAM,mBAAmB,MAAM,kBAAkB;AAAA,IAC/C;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,EAAE,GAAG,mBAAmB,cAAc,oBAAoB,aAAa;AAAA,EACzE;AAEA,MAAI,CAAC,iBAAiB,SAAS;AAC7B,WAAO,MAAM,qBAAqB,EAAE,WAAW,gBAAgB,KAAK,iBAAiB,MAAM,CAAC;AAE5F;AAAA,EACF;AAGA,QAAM,gBAAgB,IAAI,WAAW,SAAS,IAAI,EAAE,OAAO,CAAC;AAC9D;AAUA,SAAS,eACP,WACA,iBACS;AACT,MAAI,gBAAgB,WAAW,EAAG,QAAO;AACzC,QAAM,OAAO,IAAI,IAAI,eAAe;AACpC,SAAO,UAAU,KAAK,CAAC,SAAS,KAAK,IAAI,IAAI,CAAC;AAChD;AAuCA,eAAsB,cACpB,IACA,QACA,QACA,OACA,iBACe;AACf,QAAM,OAAO,MAAM,GAAG,QAAQ,UAAU;AAAA,IACtC,IAAI;AAAA,IACJ,UAAU,MAAM;AAAA,IAChB,gBAAgB,MAAM;AAAA,IACtB,QAAQ;AAAA,EACV,CAAC;AAED,MAAI,CAAC,MAAM;AACT,UAAM,IAAI;AAAA,MACR;AAAA,MACA;AAAA,MACA,EAAE,OAAO;AAAA,IACX;AAAA,EACF;AAEA,MAAI,KAAK,YAAY;AACnB,UAAM,IAAI;AAAA,MACR;AAAA,MACA;AAAA,MACA,EAAE,QAAQ,YAAY,KAAK,WAAW;AAAA,IACxC;AAAA,EACF;AAEA,MAAI,CAAC,KAAK,mBAAmB,KAAK,gBAAgB,WAAW,GAAG;AAC9D,UAAM,IAAI;AAAA,MACR;AAAA,MACA;AAAA,MACA,EAAE,OAAO;AAAA,IACX;AAAA,EACF;AAEA,MAAI,CAAC,eAAe,KAAK,iBAAiB,eAAe,GAAG;AAC1D,UAAM,IAAI;AAAA,MACR;AAAA,MACA;AAAA,MACA,EAAE,OAAO;AAAA,IACX;AAAA,EACF;AAEA,QAAM,MAAM,oBAAI,KAAK;AACrB,QAAM,UAAU,MAAM,GAAG;AAAA,IACvB;AAAA,IACA;AAAA,MACE,IAAI;AAAA,MACJ,UAAU,MAAM;AAAA,MAChB,gBAAgB,MAAM;AAAA,MACtB,QAAQ;AAAA,MACR,WAAW;AAAA,IACb;AAAA,IACA;AAAA,MACE,WAAW;AAAA,MACX,WAAW;AAAA,MACX,QAAQ;AAAA,MACR,WAAW;AAAA,IACb;AAAA,EACF;AAEA,MAAI,YAAY,GAAG;AACjB,UAAM,IAAI;AAAA,MACR;AAAA,MACA;AAAA,MACA,EAAE,OAAO;AAAA,IACX;AAAA,EACF;AAKA,OAAK,YAAY;AACjB,OAAK,YAAY;AACjB,OAAK,SAAS;AACd,OAAK,YAAY;AAEjB,QAAM,GAAG,MAAM;AAGf,QAAM,WAAW,MAAM,GAAG,QAAQ,kBAAkB,KAAK,kBAAkB;AAC3E,MAAI,UAAU;AACZ,UAAM,iBAAiB,IAAI;AAAA,MACzB,oBAAoB,SAAS;AAAA,MAC7B,gBAAgB,KAAK;AAAA,MACrB,WAAW;AAAA,MACX,WAAW;AAAA,QACT,QAAQ,KAAK;AAAA,QACb,UAAU,KAAK;AAAA,QACf,WAAW;AAAA,MACb;AAAA,MACA;AAAA,MACA,UAAU,SAAS;AAAA,MACnB,gBAAgB,SAAS;AAAA,IAC3B,CAAC;AAAA,EACH;AACF;AAeA,eAAsB,gBACpB,IACA,QACA,QACA,OACe;AACf,QAAM,OAAO,MAAM,GAAG,QAAQ,UAAU;AAAA,IACtC,IAAI;AAAA,IACJ,UAAU,MAAM;AAAA,IAChB,gBAAgB,MAAM;AAAA,IACtB,QAAQ;AAAA,EACV,CAAC;AAED,MAAI,CAAC,MAAM;AACT,UAAM,IAAI;AAAA,MACR;AAAA,MACA;AAAA,MACA,EAAE,OAAO;AAAA,IACX;AAAA,EACF;AAEA,MAAI,KAAK,cAAc,QAAQ;AAC7B,UAAM,IAAI;AAAA,MACR;AAAA,MACA;AAAA,MACA,EAAE,QAAQ,WAAW,KAAK,UAAU;AAAA,IACtC;AAAA,EACF;AAEA,QAAM,MAAM,oBAAI,KAAK;AACrB,QAAM,WAAW,MAAM,GAAG;AAAA,IACxB;AAAA,IACA;AAAA,MACE,IAAI;AAAA,MACJ,UAAU,MAAM;AAAA,MAChB,gBAAgB,MAAM;AAAA,MACtB,QAAQ;AAAA,MACR,WAAW;AAAA,IACb;AAAA,IACA;AAAA,MACE,WAAW;AAAA,MACX,WAAW;AAAA,MACX,QAAQ;AAAA,MACR,WAAW;AAAA,IACb;AAAA,EACF;AAEA,MAAI,aAAa,GAAG;AAClB,UAAM,IAAI;AAAA,MACR;AAAA,MACA;AAAA,MACA,EAAE,OAAO;AAAA,IACX;AAAA,EACF;AAEA,OAAK,YAAY;AACjB,OAAK,YAAY;AACjB,OAAK,SAAS;AACd,OAAK,YAAY;AAEjB,QAAM,GAAG,MAAM;AAEf,QAAM,WAAW,MAAM,GAAG,QAAQ,kBAAkB,KAAK,kBAAkB;AAC3E,MAAI,UAAU;AACZ,UAAM,iBAAiB,IAAI;AAAA,MACzB,oBAAoB,SAAS;AAAA,MAC7B,gBAAgB,KAAK;AAAA,MACrB,WAAW;AAAA,MACX,WAAW;AAAA,QACT,QAAQ,KAAK;AAAA,QACb,UAAU,KAAK;AAAA,QACf,YAAY;AAAA,MACd;AAAA,MACA;AAAA,MACA,UAAU,SAAS;AAAA,MACnB,gBAAgB,SAAS;AAAA,IAC3B,CAAC;AAAA,EACH;AACF;AAmCA,MAAM,wBAA2C,CAAC,WAAW,aAAa;AA0B1E,eAAsB,iBACpB,IACA,SACmB;AACnB,QAAM,EAAE,QAAQ,QAAQ,QAAQ,QAAQ,MAAM,IAAI;AAElD,QAAM,OAAO,MAAM,GAAG,QAAQ,UAAU;AAAA,IACtC,IAAI;AAAA,IACJ,UAAU,MAAM;AAAA,IAChB,gBAAgB,MAAM;AAAA,EACxB,CAAC;AAED,MAAI,CAAC,MAAM;AACT,UAAM,IAAI,cAAc,kBAAkB,kBAAkB,EAAE,OAAO,CAAC;AAAA,EACxE;AAEA,MAAI,CAAC,sBAAsB,SAAS,KAAK,MAAM,GAAG;AAChD,UAAM,IAAI;AAAA,MACR;AAAA,MACA;AAAA,MACA,EAAE,QAAQ,QAAQ,KAAK,OAAO;AAAA,IAChC;AAAA,EACF;AAEA,QAAM,aAAa,OAAO,cAAc;AACxC,QAAM,kBACJ,OAAO,mBAAmB,OAAO,gBAAgB,SAAS,IAAI,CAAC,GAAG,OAAO,eAAe,IAAI;AAE9F,QAAM,WAAW;AAAA,IACf,YAAY,KAAK,cAAc;AAAA,IAC/B,iBAAiB,KAAK,mBAAmB;AAAA,IACzC,WAAW,KAAK,aAAa;AAAA,EAC/B;AAEA,QAAM,MAAM,oBAAI,KAAK;AAErB,OAAK,aAAa;AAKlB,OAAK,eAAe;AACpB,OAAK,kBAAkB;AACvB,OAAK,YAAY;AACjB,OAAK,YAAY;AAGjB,MAAI,KAAK,WAAW,cAAe,MAAK,SAAS;AACjD,OAAK,eAAe;AACpB,OAAK,eAAe;AACpB,OAAK,iBAAiB,UAAU;AAChC,OAAK,YAAY;AAEjB,QAAM,GAAG,MAAM;AAEf,QAAM,WAAW,MAAM,GAAG,QAAQ,kBAAkB,KAAK,kBAAkB;AAC3E,MAAI,UAAU;AACZ,UAAM,iBAAiB,IAAI;AAAA,MACzB,oBAAoB,SAAS;AAAA,MAC7B,gBAAgB,KAAK;AAAA,MACrB,kBAAkB,KAAK,oBAAoB;AAAA,MAC3C,WAAW;AAAA,MACX,WAAW;AAAA,QACT,QAAQ,KAAK;AAAA,QACb,UAAU,KAAK;AAAA,QACf,cAAc;AAAA,QACd,QAAQ,UAAU;AAAA,QAClB,MAAM;AAAA,QACN,IAAI,EAAE,YAAY,gBAAgB;AAAA,MACpC;AAAA,MACA;AAAA,MACA,UAAU,SAAS;AAAA,MACnB,gBAAgB,SAAS;AAAA,IAC3B,CAAC;AAAA,EACH;AAEA,SAAO;AACT;AASA,eAAe,iBACb,IACA,OAUwB;AACxB,QAAM,gBAAgB,GAAG,OAAO,eAAe;AAAA,IAC7C,GAAG;AAAA,IACH,YAAY,oBAAI,KAAK;AAAA,EACvB,CAAC;AAED,QAAM,GAAG,QAAQ,aAAa,EAAE,MAAM;AACtC,SAAO;AACT;AAmBO,SAAS,iBACd,UACA,YACM;AACN,aAAW,SAAS,uBAAuB,UAAU,GAAG;AACtD,QAAI,EAAE,SAAS,aAAa,SAAS,KAAK,MAAM,QAAQ,SAAS,KAAK,MAAM,QAAW;AACrF,YAAM,IAAI,MAAM,2BAA2B,KAAK,EAAE;AAAA,IACpD;AAAA,EACF;AACF;",
6
6
  "names": []
7
7
  }
@@ -7,6 +7,7 @@ import {
7
7
  INVOKE_AGENT_ACTIVITY_TYPE
8
8
  } from "./agent-review.js";
9
9
  import { findDefinitionForInstance } from "./find-definition.js";
10
+ import { isRunClosedToUserTasks } from "./instance-status.js";
10
11
  import { emitWorkflowsEvent } from "../events.js";
11
12
  import { createLogger } from "@open-mercato/shared/lib/logger";
12
13
  const logger = createLogger("workflows");
@@ -187,6 +188,9 @@ async function applyBreachHandling(em, container, task, options, now) {
187
188
  if (resolution.kind === "notify") return { outcome: "notified" };
188
189
  if (resolution.kind === "attention") {
189
190
  if (!instance) return { outcome: "none" };
191
+ if (isRunClosedToUserTasks(instance.status)) {
192
+ return { outcome: "attention_skipped_closed_run", detail: { stepId, status: instance.status } };
193
+ }
190
194
  await markInstanceForAttention(em, instance, stepId, task, now);
191
195
  return { outcome: "attention", detail: { stepId } };
192
196
  }
@@ -215,6 +219,12 @@ async function applyBreachHandling(em, container, task, options, now) {
215
219
  if (options.branchInstanceId) {
216
220
  return { outcome: "route_skipped_branch", detail: { transitionId: resolution.transition.transitionId } };
217
221
  }
222
+ if (isRunClosedToUserTasks(instance?.status)) {
223
+ return {
224
+ outcome: "route_skipped_closed_run",
225
+ detail: { transitionId: resolution.transition.transitionId, status: instance?.status }
226
+ };
227
+ }
218
228
  task.status = "ESCALATED";
219
229
  task.updatedAt = now;
220
230
  await em.flush();