import { api } from './api'; import type { Bug, BugSeverity, BugStatus } from './bug'; import type { DevTask, DevTaskStatus, Reference } from './dev-task'; import type { Priority } from './derive'; import type { Requirement, RequirementStatus, SourceType } from './requirement'; import type { TestCase, TestCaseStatus } from './test-case'; import type { VersionPlan, VersionPlanLog, VersionPlanRequirementCoverage } from './version-plan'; import type { WorkActivity, WorkActivityCategory, WorkActivitySourceType } from './work-activity'; export interface RootProject { id: string; name: string; description: string; createdAt: string; } export interface RootVersion { id: string; productId?: string; projectId?: string | null; name: string; status?: string; releaseDate: string | null; createdAt: string; currentStage?: string | null; startDate?: string | null; expectedReleaseDate?: string | null; members?: unknown[]; progress?: unknown[]; priority?: string; links?: unknown; } export interface RootProduct { id: string; name: string; description: string; createdAt: string; updatedAt: string; projects: RootProject[]; versions: RootVersion[]; _count?: { requirements: number; projects: number; versions?: number }; } export async function createProductRoot(data: { name: string; description?: string }): Promise { const product = await api.post>('/products', data); return normalizeProductRoot({ ...product, projects: [], versions: [] }); } export async function updateProductRoot( productId: string, data: { name?: string; description?: string }, ): Promise> { return api.patch>(`/products/${productId}`, data); } export async function deleteProductRoot(productId: string): Promise { await api.delete(`/products/${productId}`); } export async function createProjectByProductId( productId: string, data: { name: string; description?: string }, ): Promise { return api.post(`/products/${productId}/projects`, data); } export async function updateProjectByProductId( productId: string, projectId: string, data: { name?: string; description?: string }, ): Promise { return api.patch(`/products/${productId}/projects/${projectId}`, data); } export async function deleteProjectByProductId(productId: string, projectId: string): Promise { await api.delete(`/products/${productId}/projects/${projectId}`); } export async function createVersionByProductId( productId: string, data: { name: string; status?: string; projectId?: string }, ): Promise { return normalizeVersionRoot(await api.post(`/products/${productId}/versions`, data)); } export async function updateVersionByProductId( productId: string, versionId: string, data: Record, ): Promise { return normalizeVersionRoot(await api.patch(`/products/${productId}/versions/${versionId}`, data)); } export async function deleteVersionByProductId(productId: string, versionId: string): Promise { await api.delete(`/products/${productId}/versions/${versionId}`); } interface DomainRequirementRow { id: string; productId: string; projectId?: string | null; versionId?: string | null; code: string; title: string; description?: string | null; status?: string | null; priority?: string | number | null; type?: string | null; sourceType?: string | null; sourceTarget?: string | null; platform?: string | null; creatorId?: string | null; creatorName?: string | null; creator?: { id?: string | null; name?: string | null } | null; createdAt?: string | Date | null; } export async function createRequirementByProductId( productId: string, data: Partial, ): Promise { return normalizeRequirement(await api.post( `/products/${productId}/requirements`, toRequirementPayload(data), )); } export async function updateRequirementByProductId( productId: string, requirementId: string, data: Partial, ): Promise { return normalizeRequirement(await api.patch( `/products/${productId}/requirements/${requirementId}`, toRequirementPayload(data), )); } export async function updateRequirementStatusByProductId( productId: string, requirementId: string, status: RequirementStatus, ): Promise { return normalizeRequirement(await api.patch( `/products/${productId}/requirements/${requirementId}/status`, { status }, )); } export async function deleteRequirementByProductId(productId: string, requirementId: string): Promise { await api.delete(`/products/${productId}/requirements/${requirementId}`); } interface DomainVersionPlanRow { id: string; versionId: string; type: string; title: string; status?: string | null; ownerId?: string | null; expectedStartAt?: string | Date | null; expectedEndAt?: string | Date | null; actualStartAt?: string | Date | null; completedAt?: string | Date | null; resultUrl?: string | null; requirementCoverage?: unknown; logs?: unknown; createdAt?: string | Date | null; } interface DomainDevTaskRow { id: string; versionId: string; requirementId?: string | null; categoryId?: string | null; code: string; title: string; description?: string | null; status?: string | null; priority?: string | number | null; assigneeId?: string | null; creatorId?: string | null; isBlocked?: boolean | null; blockReason?: string | null; expectedStartAt?: string | Date | null; expectedEndAt?: string | Date | null; startDate?: string | Date | null; completedAt?: string | Date | null; estimateHours?: number | null; aiEstimateHours?: number | null; references?: unknown; aiDraft?: boolean | null; aiDraftAt?: string | Date | null; createdAt?: string | Date | null; updatedAt?: string | Date | null; } interface DomainTestCaseRow { id: string; versionId: string; requirementId?: string | null; categoryId?: string | null; code: string; title: string; description?: string | null; status?: string | null; roundNo?: number | null; priority?: string | number | null; assigneeId?: string | null; creatorId?: string | null; plannedTestAt?: string | Date | null; plannedEndAt?: string | Date | null; startedAt?: string | Date | null; completedAt?: string | Date | null; estimateHours?: number | null; aiEstimateHours?: number | null; references?: unknown; aiDraft?: boolean | null; aiDraftAt?: string | Date | null; createdAt?: string | Date | null; updatedAt?: string | Date | null; } interface DomainBugRow { id: string; versionId: string; testCaseId?: string | null; code: string; title: string; description?: string | null; status?: string | null; severity?: string | null; priority?: string | number | null; assigneeId?: string | null; reporterId?: string | null; plannedFixAt?: string | Date | null; resolvedAt?: string | Date | null; closedAt?: string | Date | null; resolution?: string | null; createdAt?: string | Date | null; updatedAt?: string | Date | null; } interface DomainWorkActivityRow { id: string; actorId?: string | null; actorName?: string | null; sourceType: string; sourceId: string; action: string; title: string; metadata?: unknown; occurredAt?: string | Date | null; } interface DomainMutationResponse { item: T; activities?: DomainWorkActivityRow[]; } interface DomainBatchMutationResponse { items: T[]; activities?: DomainWorkActivityRow[]; } export async function listVersionPlansByVersionId(versionId: string): Promise { const rows = await api.get(`/versions/${versionId}/plans`); return rows.map(normalizeVersionPlan); } export async function createVersionPlanByVersionId( versionId: string, data: Partial, ) { return normalizeMutation( await api.post>(`/versions/${versionId}/plans`, toVersionPlanPayload(data)), normalizeVersionPlan, ); } export async function updateVersionPlanByVersionId( versionId: string, planId: string, data: Partial, ) { return normalizeMutation( await api.patch>(`/versions/${versionId}/plans/${planId}`, toVersionPlanPayload(data)), normalizeVersionPlan, ); } export async function completeVersionPlanByVersionId( versionId: string, planId: string, data: Partial, ) { return normalizeMutation( await api.patch>(`/versions/${versionId}/plans/${planId}/complete`, toVersionPlanPayload(data)), normalizeVersionPlan, ); } export async function deleteVersionPlanByVersionId(versionId: string, planId: string): Promise { await api.delete(`/versions/${versionId}/plans/${planId}`); } export async function listDevTasksByVersionId(versionId: string): Promise { const rows = await api.get(`/versions/${versionId}/dev-tasks`); return rows.map(normalizeDevTask); } export async function createDevTaskByVersionId(versionId: string, data: Partial) { return normalizeMutation( await api.post>(`/versions/${versionId}/dev-tasks`, toDevTaskPayload(data)), normalizeDevTask, ); } export async function updateDevTaskByVersionId(versionId: string, taskId: string, data: Partial) { return normalizeMutation( await api.patch>(`/versions/${versionId}/dev-tasks/${taskId}`, toDevTaskPayload(data)), normalizeDevTask, ); } export async function updateDevTaskStatusByVersionId(versionId: string, taskId: string, status: DevTaskStatus) { return normalizeMutation( await api.patch>(`/versions/${versionId}/dev-tasks/${taskId}/status`, { status }), normalizeDevTask, ); } export async function setDevTaskBlockedByVersionId( versionId: string, taskId: string, blocked: boolean, reason?: string, ) { return normalizeMutation( await api.patch>(`/versions/${versionId}/dev-tasks/${taskId}/block`, { blocked, reason }), normalizeDevTask, ); } export async function transferDevTaskByVersionId(versionId: string, taskId: string, assigneeId: string) { return normalizeMutation( await api.patch>(`/versions/${versionId}/dev-tasks/${taskId}/transfer`, { assigneeId }), normalizeDevTask, ); } export async function deleteDevTaskByVersionId(versionId: string, taskId: string): Promise { await api.delete(`/versions/${versionId}/dev-tasks/${taskId}`); } export async function listTestCasesByVersionId(versionId: string): Promise { const rows = await api.get(`/versions/${versionId}/test-cases`); return rows.map(normalizeTestCase); } export async function createTestCaseByVersionId(versionId: string, data: Partial) { return normalizeMutation( await api.post>(`/versions/${versionId}/test-cases`, toTestCasePayload(data)), normalizeTestCase, ); } export async function createTestCasesByVersionId(versionId: string, items: Partial[]) { return normalizeBatchMutation( await api.post>( `/versions/${versionId}/test-cases/batch`, { items: items.map(toTestCasePayload) }, ), normalizeTestCase, ); } export async function updateTestCaseByVersionId(versionId: string, testCaseId: string, data: Partial) { return normalizeMutation( await api.patch>(`/versions/${versionId}/test-cases/${testCaseId}`, toTestCasePayload(data)), normalizeTestCase, ); } export async function updateTestCaseStatusByVersionId(versionId: string, testCaseId: string, status: TestCaseStatus) { return normalizeMutation( await api.patch>(`/versions/${versionId}/test-cases/${testCaseId}/status`, { status }), normalizeTestCase, ); } export async function deleteTestCaseByVersionId(versionId: string, testCaseId: string): Promise { await api.delete(`/versions/${versionId}/test-cases/${testCaseId}`); } export async function listBugsByVersionId(versionId: string): Promise { const rows = await api.get(`/versions/${versionId}/bugs`); return rows.map(normalizeBug); } export async function createBugByVersionId(versionId: string, data: Partial) { return normalizeMutation( await api.post>(`/versions/${versionId}/bugs`, toBugPayload(data)), normalizeBug, ); } export async function updateBugByVersionId(versionId: string, bugId: string, data: Partial) { return normalizeMutation( await api.patch>(`/versions/${versionId}/bugs/${bugId}`, toBugPayload(data)), normalizeBug, ); } export async function updateBugStatusByVersionId( versionId: string, bugId: string, status: BugStatus, options: { operator?: string; resolution?: string } = {}, ) { return normalizeMutation( await api.patch>(`/versions/${versionId}/bugs/${bugId}/status`, { status, ...options }), normalizeBug, ); } export async function transferBugByVersionId(versionId: string, bugId: string, assigneeId: string, operator?: string) { return normalizeMutation( await api.patch>(`/versions/${versionId}/bugs/${bugId}/transfer`, { assigneeId, operator }), normalizeBug, ); } export async function deleteBugByVersionId(versionId: string, bugId: string): Promise { await api.delete(`/versions/${versionId}/bugs/${bugId}`); } function normalizeProductRoot(product: RootProduct): RootProduct { return { ...product, projects: product.projects ?? [], versions: (product.versions ?? []).map(normalizeVersionRoot), _count: product._count ?? { requirements: 0, projects: product.projects?.length ?? 0, versions: product.versions?.length ?? 0 }, }; } function normalizeVersionRoot(version: RootVersion): RootVersion { return { ...version, status: version.status ?? 'planned', releaseDate: version.releaseDate ?? null, members: Array.isArray(version.members) ? version.members : [], progress: Array.isArray(version.progress) ? version.progress : [], links: version.links && typeof version.links === 'object' ? version.links : {}, }; } function toRequirementPayload(data: Partial) { return { ...(data.code !== undefined && { code: data.code }), ...(data.title !== undefined && { title: data.title }), ...(data.description !== undefined && { description: data.description }), ...(data.projectId !== undefined && { projectId: data.projectId }), ...(data.versionId !== undefined && { versionId: data.versionId ?? null }), ...(data.typeId !== undefined && { type: data.typeId }), ...(data.sourceType !== undefined && { sourceType: data.sourceType }), ...(data.sourceTarget !== undefined && { sourceTarget: data.sourceTarget }), ...(data.platforms !== undefined && { platform: data.platforms.join(',') }), ...(data.priority !== undefined && { priority: priorityToNumber(data.priority) }), }; } function normalizeRequirement(row: DomainRequirementRow): Requirement { return { id: row.id, code: row.code, title: row.title, description: row.description ?? '', productId: row.productId, projectId: row.projectId ?? '', versionId: row.versionId ?? undefined, sourceType: toSourceType(row.sourceType), sourceTarget: row.sourceTarget ?? '', platforms: splitCsv(row.platform), typeId: row.type ?? '', status: toRequirementStatus(row.status), priority: toPriority(row.priority), effort: 'M', creator: row.creator?.name ?? row.creatorName ?? row.creatorId ?? '', createdAt: isoString(row.createdAt), }; } function toSourceType(value: string | null | undefined): SourceType { const allowed = new Set(['customer', 'internal', 'operation', 'aftersale', 'market', 'competitor', 'management']); return allowed.has(value as SourceType) ? value as SourceType : 'internal'; } function toRequirementStatus(value: string | null | undefined): RequirementStatus { const allowed = new Set(['pending_review', 'adopted', 'rejected', 'planned', 'developing', 'testing', 'released', 'closed']); return allowed.has(value as RequirementStatus) ? value as RequirementStatus : 'pending_review'; } function toPriority(value: string | number | null | undefined): Priority { if (typeof value === 'string' && /^P[0-4]$/.test(value)) return value as Priority; const parsed = Number(value ?? 2); const normalized = Number.isFinite(parsed) ? Math.max(0, Math.min(4, Math.floor(parsed))) : 2; return `P${normalized}` as Priority; } function priorityToNumber(value: Priority | undefined): number | undefined { if (!value) return undefined; const match = /^P([0-4])$/.exec(value); return match ? Number(match[1]) : undefined; } function splitCsv(value: string | null | undefined): string[] { if (!value) return []; return value.split(',').map((item) => item.trim()).filter(Boolean); } function isoString(value: string | Date | null | undefined): string { if (!value) return new Date().toISOString(); if (value instanceof Date) return value.toISOString(); const time = new Date(value).getTime(); return Number.isFinite(time) ? new Date(time).toISOString() : new Date().toISOString(); } function optionalIso(value: string | Date | null | undefined): string | undefined { if (!value) return undefined; return isoString(value); } function toVersionPlanPayload(data: Partial) { return { ...(data.type !== undefined && { type: data.type }), ...(data.title !== undefined && { title: data.title }), ...(data.status !== undefined && { status: data.status }), ...(data.owner !== undefined && { owner: data.owner }), ...(data.startTime !== undefined && { startTime: data.startTime }), ...(data.endTime !== undefined && { endTime: data.endTime }), ...(data.actualStartAt !== undefined && { actualStartAt: data.actualStartAt }), ...(data.completedAt !== undefined && { completedAt: data.completedAt }), ...(data.resultUrl !== undefined && { resultUrl: data.resultUrl }), ...(data.linkedRequirementIds !== undefined && { linkedRequirementIds: data.linkedRequirementIds }), ...(data.requirementCoverage !== undefined && { requirementCoverage: data.requirementCoverage }), ...(data.logs !== undefined && { logs: data.logs }), }; } function normalizeVersionPlan(row: DomainVersionPlanRow): VersionPlan { const requirementCoverage = asArray(row.requirementCoverage); return { id: row.id, versionId: row.versionId, type: toPlanType(row.type), title: row.title, owner: row.ownerId ?? '', startTime: isoString(row.expectedStartAt), endTime: isoString(row.expectedEndAt), status: toPlanStatus(row.status), tasks: [], completedRequirementIds: requirementCoverage .filter((item) => item?.status === 'completed') .map((item) => item.requirementId) .filter(Boolean), linkedRequirementIds: requirementCoverage.map((item) => item?.requirementId).filter(Boolean), requirementCoverage, logs: asArray(row.logs), resultUrl: row.resultUrl ?? undefined, actualStartAt: optionalIso(row.actualStartAt), createdAt: isoString(row.createdAt), completedAt: optionalIso(row.completedAt), addedBy: row.ownerId ?? '', }; } function toPlanType(value: string): VersionPlan['type'] { return value === 'research' || value === 'ui' ? value : 'product'; } function toPlanStatus(value: string | null | undefined): VersionPlan['status'] { return value === 'in_progress' || value === 'completed' ? value : 'pending'; } function toDevTaskPayload(data: Partial) { return { ...(data.versionId !== undefined && { versionId: data.versionId }), ...(data.requirementId !== undefined && { requirementId: data.requirementId }), ...(data.categoryId !== undefined && { categoryId: data.categoryId }), ...(data.taskNo !== undefined && { taskNo: data.taskNo }), ...(data.title !== undefined && { title: data.title }), ...(data.description !== undefined && { description: data.description }), ...(data.status !== undefined && { status: data.status }), ...(data.priority !== undefined && { priority: priorityToNumber(data.priority) }), ...(data.assigneeId !== undefined && { assigneeId: data.assigneeId }), ...(data.createdBy !== undefined && { createdBy: data.createdBy }), ...(data.isBlocked !== undefined && { isBlocked: data.isBlocked }), ...(data.blockReason !== undefined && { blockReason: data.blockReason }), ...(data.expectedStartAt !== undefined && { expectedStartAt: data.expectedStartAt }), ...(data.expectedEndAt !== undefined && { expectedEndAt: data.expectedEndAt }), ...(data.actualStartAt !== undefined && { actualStartAt: data.actualStartAt }), ...(data.actualEndAt !== undefined && { actualEndAt: data.actualEndAt }), ...(data.estimateHours !== undefined && { estimateHours: data.estimateHours }), ...(data.aiEstimateHours !== undefined && { aiEstimateHours: data.aiEstimateHours }), ...(data.references !== undefined && { references: data.references }), ...(data.aiDraft !== undefined && { aiDraft: data.aiDraft }), ...(data.aiDraftAt !== undefined && { aiDraftAt: data.aiDraftAt }), }; } function normalizeDevTask(row: DomainDevTaskRow): DevTask { return { id: row.id, taskNo: row.code, versionId: row.versionId, requirementId: row.requirementId ?? '', title: row.title, description: row.description ?? '', categoryId: row.categoryId ?? '', assigneeId: row.assigneeId ?? '', priority: toPriority(row.priority), expectedStartAt: isoString(row.expectedStartAt), expectedEndAt: isoString(row.expectedEndAt), estimateHours: row.estimateHours ?? undefined, aiEstimateHours: row.aiEstimateHours ?? undefined, actualStartAt: optionalIso(row.startDate), actualEndAt: optionalIso(row.completedAt), status: toDevTaskStatus(row.status), isBlocked: Boolean(row.isBlocked), blockReason: row.blockReason ?? undefined, references: asArray(row.references), aiDraft: Boolean(row.aiDraft), aiDraftAt: optionalIso(row.aiDraftAt), createdBy: row.creatorId ?? '', createdAt: isoString(row.createdAt), updatedAt: isoString(row.updatedAt), }; } function toDevTaskStatus(value: string | null | undefined): DevTaskStatus { return value === 'in_progress' || value === 'testing' || value === 'submitted' ? value : 'todo'; } function toTestCasePayload(data: Partial) { return { ...(data.versionId !== undefined && { versionId: data.versionId }), ...(data.requirementId !== undefined && { requirementId: data.requirementId }), ...(data.categoryId !== undefined && { categoryId: data.categoryId }), ...(data.caseNo !== undefined && { caseNo: data.caseNo }), ...(data.title !== undefined && { title: data.title }), ...(data.description !== undefined && { description: data.description }), ...(data.status !== undefined && { status: data.status }), ...(data.roundNo !== undefined && { roundNo: data.roundNo }), ...(data.priority !== undefined && { priority: priorityToNumber(data.priority) }), ...(data.assigneeId !== undefined && { assigneeId: data.assigneeId }), ...(data.createdBy !== undefined && { createdBy: data.createdBy }), ...(data.plannedTestAt !== undefined && { plannedTestAt: data.plannedTestAt }), ...(data.plannedEndAt !== undefined && { plannedEndAt: data.plannedEndAt }), ...(data.startedAt !== undefined && { startedAt: data.startedAt }), ...(data.completedAt !== undefined && { completedAt: data.completedAt }), ...(data.estimateHours !== undefined && { estimateHours: data.estimateHours }), ...(data.aiEstimateHours !== undefined && { aiEstimateHours: data.aiEstimateHours }), ...(data.references !== undefined && { references: data.references }), ...(data.aiDraft !== undefined && { aiDraft: data.aiDraft }), ...(data.aiDraftAt !== undefined && { aiDraftAt: data.aiDraftAt }), }; } function normalizeTestCase(row: DomainTestCaseRow): TestCase { return { id: row.id, caseNo: row.code, versionId: row.versionId, requirementId: row.requirementId ?? undefined, roundNo: row.roundNo && row.roundNo > 0 ? Math.floor(row.roundNo) : 1, title: row.title, description: row.description ?? '', categoryId: row.categoryId ?? '', priority: toPriority(row.priority), assigneeId: row.assigneeId ?? undefined, status: toTestCaseStatus(row.status), estimateHours: row.estimateHours ?? undefined, aiEstimateHours: row.aiEstimateHours ?? undefined, plannedTestAt: optionalIso(row.plannedTestAt), plannedEndAt: optionalIso(row.plannedEndAt), startedAt: optionalIso(row.startedAt), completedAt: optionalIso(row.completedAt), executedAt: optionalIso(row.completedAt), references: asArray(row.references), aiDraft: Boolean(row.aiDraft), aiDraftAt: optionalIso(row.aiDraftAt), createdBy: row.creatorId ?? '', createdAt: isoString(row.createdAt), updatedAt: isoString(row.updatedAt), }; } function toTestCaseStatus(value: string | null | undefined): TestCaseStatus { if (value === 'running' || value === 'passed' || value === 'failed' || value === 'blocked') return value; return 'pending'; } function toBugPayload(data: Partial) { return { ...(data.versionId !== undefined && { versionId: data.versionId }), ...(data.testCaseId !== undefined && { testCaseId: data.testCaseId }), ...(data.bugNo !== undefined && { bugNo: data.bugNo }), ...(data.title !== undefined && { title: data.title }), ...(data.description !== undefined && { description: data.description }), ...(data.status !== undefined && { status: data.status }), ...(data.severity !== undefined && { severity: data.severity }), ...(data.priority !== undefined && { priority: priorityToNumber(data.priority) }), ...(data.assigneeId !== undefined && { assigneeId: data.assigneeId }), ...(data.reportedBy !== undefined && { reportedBy: data.reportedBy }), ...(data.plannedFixAt !== undefined && { plannedFixAt: data.plannedFixAt }), ...(data.resolvedAt !== undefined && { resolvedAt: data.resolvedAt }), ...(data.closedAt !== undefined && { closedAt: data.closedAt }), ...(data.resolution !== undefined && { resolution: data.resolution }), }; } function normalizeBug(row: DomainBugRow): Bug { return { id: row.id, bugNo: row.code, versionId: row.versionId, testCaseId: row.testCaseId ?? '', title: row.title, description: row.description ?? '', severity: toBugSeverity(row.severity), priority: toPriority(row.priority), reportedBy: row.reporterId ?? '', assigneeId: row.assigneeId ?? '', status: toBugStatus(row.status), resolvedAt: optionalIso(row.resolvedAt), closedAt: optionalIso(row.closedAt), resolution: row.resolution ?? undefined, plannedFixAt: optionalIso(row.plannedFixAt), createdAt: isoString(row.createdAt), updatedAt: isoString(row.updatedAt), }; } function toBugStatus(value: string | null | undefined): BugStatus { if (value === 'fixing' || value === 'fixed' || value === 'verifying' || value === 'closed' || value === 'rejected') return value; return 'open'; } function toBugSeverity(value: string | null | undefined): BugSeverity { if (value === 'critical' || value === 'major' || value === 'trivial') return value; return 'minor'; } function normalizeMutation( response: DomainMutationResponse, mapper: (row: Row) => Item, ): { item: Item; activities: WorkActivity[] } { return { item: mapper(response.item), activities: (response.activities ?? []).map(normalizeWorkActivity), }; } function normalizeBatchMutation( response: DomainBatchMutationResponse, mapper: (row: Row) => Item, ): { items: Item[]; activities: WorkActivity[] } { return { items: (response.items ?? []).map(mapper), activities: (response.activities ?? []).map(normalizeWorkActivity), }; } function normalizeWorkActivity(row: DomainWorkActivityRow): WorkActivity { const metadata = isRecord(row.metadata) ? row.metadata : {}; return { id: row.id, actorId: row.actorId ?? row.actorName ?? '', date: isoString(row.occurredAt).slice(0, 10), occurredAt: isoString(row.occurredAt), sourceType: toWorkActivitySourceType(row.sourceType), sourceId: row.sourceId, action: row.action as WorkActivity['action'], category: toWorkActivityCategory(metadata.category), title: row.title, summary: typeof metadata.summary === 'string' ? metadata.summary : row.title, metadata, }; } function toWorkActivitySourceType(value: string): WorkActivitySourceType { if (value === 'version_plan' || value === 'dev_task' || value === 'test_case' || value === 'bug') return value; return 'manual'; } function toWorkActivityCategory(value: unknown): WorkActivityCategory { if (value === 'delivery' || value === 'progress' || value === 'creation' || value === 'risk') return value; return 'note'; } function asArray(value: unknown): T[] { return Array.isArray(value) ? value as T[] : []; } function isRecord(value: unknown): value is Record { return typeof value === 'object' && value !== null && !Array.isArray(value); }