UNPKG

@swoft/gtd-domain

Version:

Getting Things Done (GTD) productivity system - consolidated domain implementation

1 lines 404 kB
{"version":3,"sources":["../src/bounded-contexts/project-management/domain/errors/GTDDomainError.ts","../src/bounded-contexts/inbox-management/domain/value-objects/InboxContent.ts","../src/bounded-contexts/inbox-management/domain/aggregates/InboxItem.ts","../src/bounded-contexts/inbox-management/domain/services/InboxImportExportService.ts","../src/bounded-contexts/project-management/domain/events/GTDEvents.ts","../src/bounded-contexts/project-management/domain/aggregates/NextAction.ts","../src/bounded-contexts/project-management/domain/value-objects/ActionContext.ts","../src/bounded-contexts/project-management/domain/value-objects/EnergyLevel.ts","../src/bounded-contexts/inbox-management/domain/services/GTDProcessingWorkflowService.ts","../src/bounded-contexts/inbox-management/application/commands/CompleteGTDProcessingCommand.ts","../src/bounded-contexts/inbox-management/application/services/InboxItemApplicationService.ts","../src/infrastructure/di/GTDDomainSymbols.ts","../src/bounded-contexts/inbox-management/infrastructure/repositories/InboxItemAggregateRepository.ts","../src/config/collections.ts","../src/bounded-contexts/project-management/infrastructure/repositories/NextActionAggregateRepository.ts","../src/bounded-contexts/project-management/domain/value-objects/ProcessingStatus.ts","../src/bounded-contexts/project-management/domain/aggregates/Project.ts","../src/bounded-contexts/project-management/domain/events/DomainEvent.ts","../src/bounded-contexts/project-management/infrastructure/repositories/ProjectRepository.ts","../src/bounded-contexts/project-management/application/services/ProjectApplicationService.ts","../src/types/DITypes.ts","../src/bounded-contexts/project-management/application/services/ProjectWriteService.ts","../src/bounded-contexts/project-management/application/services/ProjectReadService.ts","../src/bounded-contexts/project-management/application/services/NextActionWriteService.ts","../src/bounded-contexts/project-management/application/services/NextActionReadService.ts","../src/bounded-contexts/design-implementation-coordination/application/services/DesignImplementationFlowApplicationService.ts","../src/bounded-contexts/design-implementation-coordination/domain/value-objects/ArtifactStatus.ts","../src/bounded-contexts/design-implementation-coordination/domain/value-objects/ConformanceScore.ts","../src/bounded-contexts/design-implementation-coordination/domain/value-objects/FlowStatus.ts","../src/bounded-contexts/design-implementation-coordination/domain/errors/DesignImplementationDomainError.ts","../src/bounded-contexts/design-implementation-coordination/domain/events/DesignImplementationEvents.ts","../src/bounded-contexts/design-implementation-coordination/domain/aggregates/DesignImplementationFlow.ts","../src/bounded-contexts/design-implementation-coordination/infrastructure/repositories/DesignImplementationFlowRepository.ts","../src/bounded-contexts/design-implementation-coordination/application/services/DesignImplementationFlowReadService.ts","../src/bounded-contexts/index.ts","../src/bounded-contexts/ai-tools/infrastructure/tools/inbox-ai-tools.ts","../src/bounded-contexts/ai-tools/application/services/GTDAIToolsApplicationService.ts","../src/bounded-contexts/ai-tools/index.ts","../src/bounded-contexts/inbox-management/infrastructure/factories/createGTDQueryService.ts","../src/infrastructure/factory/GTDServiceFactory.ts","../src/integration/index.ts","../src/interface/errors/GTDFeatureError.ts","../src/infrastructure/shared-seeding/BaseDomainSeeder.ts","../src/utils/seed-gtd-data.ts","../src/infrastructure/GtdDomainSeeder.ts","../src/infrastructure/di/GTDDomainContainerModule.ts","../src/bounded-contexts/task-management/infrastructure/repositories/TaskMongoRepository.ts","../src/bounded-contexts/task-management/domain/entities/TaskAggregate.ts","../src/bounded-contexts/task-management/domain/view-models.ts","../src/bounded-contexts/task-management/domain/value-objects/TaskId.ts","../src/bounded-contexts/task-management/domain/events/TaskAssigned.ts","../src/bounded-contexts/task-management/domain/events/TaskCompleted.ts","../src/bounded-contexts/task-management/infrastructure/repositories/InboxFileRepository.ts","../src/bounded-contexts/task-management/domain/plan-view-models.ts","../src/bounded-contexts/task-management/infrastructure/repositories/NextActionsFileRepository.ts","../src/validation/GTDDomainRules.ts"],"sourcesContent":["/**\n * GTD Domain Errors following clean error handling patterns\n */\nexport class GTDDomainError extends Error {\n constructor(\n message: string,\n public readonly code: string,\n public readonly context?: Record<string, any>\n ) {\n super(message);\n this.name = 'GTDDomainError';\n }\n}\n\nexport class ThoughtProcessingError extends GTDDomainError {\n constructor(message: string, context?: Record<string, any>) {\n super(message, 'THOUGHT_PROCESSING_ERROR', context);\n this.name = 'ThoughtProcessingError';\n }\n}\n\nexport class InvalidProcessingStatusError extends GTDDomainError {\n constructor(currentStatus: string, requiredStatus: string) {\n super(\n `Cannot perform this operation. Current status: ${currentStatus}, required: ${requiredStatus}`,\n 'INVALID_PROCESSING_STATUS',\n { currentStatus, requiredStatus }\n );\n this.name = 'InvalidProcessingStatusError';\n }\n}\n\nexport class InvalidActionDurationError extends GTDDomainError {\n constructor(duration: number) {\n super(\n `Action duration exceeds reasonable bounds. Duration: ${duration} minutes. Maximum allowed: 480 minutes (8 hours).`,\n 'INVALID_ACTION_DURATION',\n { duration, maxDuration: 480 }\n );\n this.name = 'InvalidActionDurationError';\n }\n}\n\nexport class ProjectStateError extends GTDDomainError {\n constructor(message: string, projectId: string, currentState: string) {\n super(message, 'PROJECT_STATE_ERROR', { projectId, currentState });\n this.name = 'ProjectStateError';\n }\n}\n\nexport class AssignmentError extends GTDDomainError {\n constructor(message: string, actionId: string) {\n super(message, 'ASSIGNMENT_ERROR', { actionId });\n this.name = 'AssignmentError';\n }\n}","import { GTDDomainError } from '../../../project-management/domain/errors/GTDDomainError';\n\n/**\n * InboxContent Value Object\n * \n * Enforces GTD capture rules:\n * - Content cannot be empty (must capture something meaningful)\n * - Minimum 3 characters to prevent accidental captures\n * - Maximum 5000 characters to maintain focus on quick capture\n * \n * Fixes data corruption by ensuring all content is validated at domain level.\n */\nexport class InboxContent {\n private constructor(private readonly _value: string) {\n if (!_value || _value.trim().length === 0) {\n throw new GTDDomainError(\n 'Inbox content cannot be empty',\n 'EMPTY_INBOX_CONTENT'\n );\n }\n if (_value.trim().length < 3) {\n throw new GTDDomainError(\n 'Inbox content must be at least 3 characters',\n 'INBOX_CONTENT_TOO_SHORT',\n { minLength: 3, actualLength: _value.trim().length }\n );\n }\n if (_value.length > 5000) {\n throw new GTDDomainError(\n 'Inbox content cannot exceed 5000 characters',\n 'INBOX_CONTENT_TOO_LONG',\n { maxLength: 5000, actualLength: _value.length }\n );\n }\n }\n \n static create(content: string): InboxContent {\n return new InboxContent(content?.trim());\n }\n \n get value(): string { \n return this._value; \n }\n}","import { InboxContent } from '../value-objects/InboxContent';\n\n/**\n * BC-035: Getting Things Done - InboxItem Aggregate\n * \n * Core GTD Principle: \"Your mind is for having ideas, not holding them\"\n * Implements David Allen's GTD capture and clarification workflow\n */\nexport class InboxItem {\n readonly id: string;\n private _originalContent: InboxContent;\n readonly capturedAt: Date;\n readonly capturedByPersonId: string;\n \n private _clarification?: string;\n private _isActionable?: boolean;\n private _processingStatus: string;\n private _lastRefinedAt?: Date;\n private _refinedByPersonId?: string;\n\n private constructor(\n id: string,\n originalContent: InboxContent,\n capturedAt: Date,\n capturedByPersonId: string\n ) {\n this.id = id;\n this._originalContent = originalContent;\n this.capturedAt = capturedAt;\n this.capturedByPersonId = capturedByPersonId;\n this._processingStatus = 'unprocessed';\n }\n\n // Getters\n get originalContent(): string {\n return this._originalContent.value;\n }\n\n get clarification(): string | undefined {\n return this._clarification;\n }\n\n get isActionable(): boolean | undefined {\n return this._isActionable;\n }\n\n get processingStatus(): string {\n return this._processingStatus;\n }\n\n get lastRefinedAt(): Date | undefined {\n return this._lastRefinedAt;\n }\n\n get refinedByPersonId(): string | undefined {\n return this._refinedByPersonId;\n }\n\n /**\n * Factory method: Capture a new inbox item\n * GTD Principle: \"Ubiquitous Capture\"\n */\n static capture(content: string, capturedByPersonId: string): InboxItem {\n const id = `inbox-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;\n const capturedAt = new Date();\n const inboxContent = InboxContent.create(content);\n \n return new InboxItem(id, inboxContent, capturedAt, capturedByPersonId);\n }\n\n /**\n * Factory method: Reconstruct from repository data\n */\n static fromRepository(data: {\n id: string;\n originalContent: string;\n capturedAt: Date;\n capturedByPersonId: string;\n clarification?: string;\n isActionable?: boolean;\n processingStatus?: string;\n lastRefinedAt?: Date;\n refinedByPersonId?: string;\n }): InboxItem {\n const inboxContent = InboxContent.create(data.originalContent);\n const item = new InboxItem(\n data.id,\n inboxContent,\n data.capturedAt,\n data.capturedByPersonId\n );\n \n item._clarification = data.clarification;\n item._isActionable = data.isActionable;\n item._processingStatus = data.processingStatus || 'unprocessed';\n item._lastRefinedAt = data.lastRefinedAt;\n item._refinedByPersonId = data.refinedByPersonId;\n \n return item;\n }\n\n /**\n * Refine Capture: Clean up or correct the original captured content\n * GTD Principle: \"Your system must be current and complete\"\n * \n * This is NOT clarification - it's fixing typos, making the capture clearer,\n * or adding missing context to what was hastily captured.\n * \n * @param refinedContent The cleaned up version of the original capture\n * @param refinedBy Person ID who refined the content\n */\n refineCapture(refinedContent: string, refinedBy: string): void {\n const newContent = InboxContent.create(refinedContent);\n \n this._originalContent = newContent;\n this._lastRefinedAt = new Date();\n this._refinedByPersonId = refinedBy;\n // Note: Does NOT change processing status - refining is not processing\n }\n\n /**\n * GTD Clarification: \"What is it? Is it actionable?\"\n * \n * This is the core GTD processing step where you decide what something means\n * and whether it requires action.\n */\n clarify(clarification: string, isActionable: boolean, _clarifiedBy: string): void {\n this._clarification = clarification;\n this._isActionable = isActionable;\n this._processingStatus = 'processed';\n }\n\n /**\n * Convert to plain object for repository persistence\n */\n toData(): any {\n return {\n id: this.id,\n originalContent: this._originalContent.value,\n capturedAt: this.capturedAt,\n capturedByPersonId: this.capturedByPersonId,\n clarification: this._clarification,\n isActionable: this._isActionable,\n processingStatus: this._processingStatus,\n lastRefinedAt: this._lastRefinedAt,\n refinedByPersonId: this._refinedByPersonId\n };\n }\n}","import { InboxItem } from \"../aggregates/InboxItem\";\nimport { InboxItemAggregateRepository } from \"../../infrastructure/repositories/InboxItemAggregateRepository\";\n\n/**\n * Inbox Import/Export Domain Service\n * \n * Following Eric Evans Domain Service pattern for operations that don't\n * naturally belong to any single aggregate but are part of the domain.\n * \n * Import/Export is a legitimate GTD capability - not just technical migration.\n */\nexport class InboxImportExportService {\n constructor(\n private readonly repository: InboxItemAggregateRepository\n ) {}\n\n /**\n * Export all inbox items to a portable format\n */\n async exportItems(filters?: {\n status?: 'processed' | 'unprocessed';\n limit?: number;\n }): Promise<InboxExportData> {\n // Get items based on filters\n const items = await this.repository.findAll();\n\n // Filter by status if provided\n const filteredItems = filters?.status \n ? items.filter((item: InboxItem) => item.processingStatus === filters.status)\n : items;\n\n // Apply limit\n const limitedItems = filters?.limit \n ? filteredItems.slice(0, filters.limit)\n : filteredItems;\n\n return {\n exportDate: new Date().toISOString(),\n version: \"1.0\",\n source: \"swoft-gtd-inbox\",\n totalItems: limitedItems.length,\n items: limitedItems.map((item: InboxItem) => ({\n originalContent: item.originalContent,\n capturedAt: item.capturedAt.toISOString(),\n capturedByPersonId: item.capturedByPersonId,\n clarification: item.clarification || null,\n isActionable: item.isActionable || null,\n processingStatus: item.processingStatus as 'processed' | 'unprocessed'\n }))\n };\n }\n\n /**\n * Import items from external format\n * Supports migration from legacy formats\n */\n async importItems(\n importData: InboxImportData,\n options: {\n skipDuplicates?: boolean;\n capturedByPersonId?: string;\n } = {}\n ): Promise<InboxImportResult> {\n this.validateImportData(importData);\n\n const results: InboxImportResult = {\n totalItems: importData.items.length,\n imported: 0,\n skipped: 0,\n errors: []\n };\n\n for (const itemData of importData.items) {\n try {\n // Handle different input formats (legacy and new)\n const content = itemData.originalContent || itemData.text || itemData.content;\n if (!content) {\n results.errors.push(`Item missing content: ${JSON.stringify(itemData)}`);\n continue;\n }\n\n // Create domain aggregate\n const item = InboxItem.capture(\n content,\n options.capturedByPersonId || itemData.capturedByPersonId || 'import-system'\n );\n\n // Apply additional properties if available (for processed items)\n if (itemData.clarification && typeof itemData.isActionable === 'boolean') {\n item.clarify(itemData.clarification, itemData.isActionable, 'import-system');\n }\n\n // Persist via repository\n await this.repository.save(item);\n results.imported++;\n\n } catch (error) {\n if (options.skipDuplicates && error instanceof Error && error.message.includes('duplicate')) {\n results.skipped++;\n } else {\n results.errors.push(`Failed to import item: ${error instanceof Error ? error.message : 'Unknown error'}`);\n }\n }\n }\n\n return results;\n }\n\n private validateImportData(data: InboxImportData): void {\n if (!data.version) {\n throw new Error('Import data missing version');\n }\n if (!data.items || !Array.isArray(data.items)) {\n throw new Error('Import data missing or invalid items array');\n }\n if (data.items.length === 0) {\n throw new Error('Import data contains no items');\n }\n }\n}\n\n// ============================================\n// TYPES\n// ============================================\n\nexport interface InboxExportData {\n exportDate: string;\n version: string;\n source: string;\n totalItems: number;\n items: ExportedInboxItem[];\n}\n\nexport interface ExportedInboxItem {\n originalContent: string;\n capturedAt: string;\n capturedByPersonId: string;\n clarification: string | null;\n isActionable: boolean | null;\n processingStatus: 'processed' | 'unprocessed';\n}\n\nexport interface InboxImportData {\n version: string;\n items: ImportedInboxItem[];\n source?: string;\n exportDate?: string;\n}\n\nexport interface ImportedInboxItem {\n // New format\n originalContent?: string;\n capturedByPersonId?: string;\n clarification?: string | null;\n isActionable?: boolean | null;\n processingStatus?: 'processed' | 'unprocessed';\n \n // Legacy format support\n text?: string;\n content?: string;\n status?: string;\n priority?: string;\n tags?: string[];\n \n // Metadata\n capturedAt?: string;\n createdAt?: string;\n updatedAt?: string;\n}\n\nexport interface InboxImportResult {\n totalItems: number;\n imported: number;\n skipped: number;\n errors: string[];\n}","// TODO: DomainEvent should be imported from @swoft/core when available\n// import { DomainEvent } from '@swoft/core';\n\n/**\n * Temporary DomainEvent interface until proper import is available\n */\ninterface DomainEvent {\n readonly eventId: string;\n readonly aggregateId: string;\n readonly eventType: string;\n readonly occurredAt: Date;\n readonly eventVersion: number;\n getEventData(): Record<string, any>;\n}\n\n/**\n * GTD Domain Events following David Allen's methodology\n */\n\nexport class ProjectIdentified implements DomainEvent {\n readonly eventId: string = crypto.randomUUID();\n readonly aggregateId: string;\n readonly eventType: string = 'gtd.ProjectIdentified';\n readonly occurredAt: Date = new Date();\n readonly eventVersion: number = 1;\n readonly occurredOn: Date = new Date(); // For backward compatibility\n\n constructor(\n aggregateId: string,\n public readonly projectName: string,\n public readonly desiredOutcome: string,\n public readonly identifiedBy: string\n ) {\n this.aggregateId = aggregateId;\n }\n\n getEventData(): Record<string, any> {\n return {\n projectName: this.projectName,\n desiredOutcome: this.desiredOutcome,\n identifiedBy: this.identifiedBy,\n identifiedAt: this.occurredOn.toISOString()\n };\n }\n}\n\nexport class NextActionCreated implements DomainEvent {\n readonly eventId: string = crypto.randomUUID();\n readonly aggregateId: string;\n readonly eventType: string = 'gtd.NextActionCreated';\n readonly occurredAt: Date = new Date();\n readonly eventVersion: number = 1;\n readonly occurredOn: Date = new Date(); // For backward compatibility\n\n constructor(\n aggregateId: string,\n public readonly description: string,\n public readonly context: string,\n public readonly energyLevel: string,\n public readonly createdBy: string\n ) {\n this.aggregateId = aggregateId;\n }\n\n getEventData(): Record<string, any> {\n return {\n description: this.description,\n context: this.context,\n energyLevel: this.energyLevel,\n createdBy: this.createdBy,\n createdAt: this.occurredOn.toISOString()\n };\n }\n}\n\nexport class TaskAssigned implements DomainEvent {\n readonly eventId: string = crypto.randomUUID();\n readonly aggregateId: string;\n readonly eventType: string = 'gtd.TaskAssigned';\n readonly occurredAt: Date = new Date();\n readonly eventVersion: number = 1;\n readonly occurredOn: Date = new Date(); // For backward compatibility\n\n constructor(\n aggregateId: string,\n public readonly assignedTo: string,\n public readonly roleType: string,\n public readonly assignedAt: string\n ) {\n this.aggregateId = aggregateId;\n }\n\n getEventData(): Record<string, any> {\n return {\n assignedTo: this.assignedTo,\n roleType: this.roleType,\n assignedAt: this.assignedAt\n };\n }\n}\n\nexport class ProjectCompleted implements DomainEvent {\n readonly eventId: string = crypto.randomUUID();\n readonly aggregateId: string;\n readonly eventType: string = 'gtd.ProjectCompleted';\n readonly occurredAt: Date = new Date();\n readonly eventVersion: number = 1;\n readonly occurredOn: Date = new Date(); // For backward compatibility\n\n constructor(\n aggregateId: string,\n public readonly completedBy: string,\n public readonly completionNotes?: string\n ) {\n this.aggregateId = aggregateId;\n }\n\n getEventData(): Record<string, any> {\n return {\n completedBy: this.completedBy,\n completionNotes: this.completionNotes,\n completedAt: this.occurredOn.toISOString()\n };\n }\n}\n\nexport class NextActionCompleted implements DomainEvent {\n readonly eventId: string = crypto.randomUUID();\n readonly aggregateId: string;\n readonly eventType: string = 'gtd.NextActionCompleted';\n readonly occurredAt: Date = new Date();\n readonly eventVersion: number = 1;\n readonly occurredOn: Date = new Date(); // For backward compatibility\n\n constructor(\n aggregateId: string,\n public readonly completedBy: string,\n public readonly completionNotes?: string\n ) {\n this.aggregateId = aggregateId;\n }\n\n getEventData(): Record<string, any> {\n return {\n completedBy: this.completedBy,\n completionNotes: this.completionNotes,\n completedAt: this.occurredOn.toISOString()\n };\n }\n}\n\nexport class ReferenceDocumentAdded implements DomainEvent {\n readonly eventId: string = crypto.randomUUID();\n readonly aggregateId: string;\n readonly eventType: string = 'gtd.ReferenceDocumentAdded';\n readonly occurredAt: Date = new Date();\n readonly eventVersion: number = 1;\n readonly occurredOn: Date = new Date(); // For backward compatibility\n\n constructor(\n aggregateId: string,\n public readonly documentName: string,\n public readonly documentType: string,\n public readonly addedBy: string\n ) {\n this.aggregateId = aggregateId;\n }\n\n getEventData(): Record<string, any> {\n return {\n documentName: this.documentName,\n documentType: this.documentType,\n addedBy: this.addedBy,\n addedAt: this.occurredOn.toISOString()\n };\n }\n}\n\nexport class WeeklyReviewStarted implements DomainEvent {\n readonly eventId: string = crypto.randomUUID();\n readonly aggregateId: string;\n readonly eventType: string = 'gtd.WeeklyReviewStarted';\n readonly occurredAt: Date = new Date();\n readonly eventVersion: number = 1;\n readonly occurredOn: Date = new Date(); // For backward compatibility\n\n constructor(\n aggregateId: string,\n public readonly reviewId: string,\n public readonly conductedBy: string\n ) {\n this.aggregateId = aggregateId;\n }\n\n getEventData(): Record<string, any> {\n return {\n reviewId: this.reviewId,\n conductedBy: this.conductedBy,\n startedAt: this.occurredOn.toISOString()\n };\n }\n}\n\nexport class WeeklyReviewCompleted implements DomainEvent {\n readonly eventId: string = crypto.randomUUID();\n readonly aggregateId: string;\n readonly eventType: string = 'gtd.WeeklyReviewCompleted';\n readonly occurredAt: Date = new Date();\n readonly eventVersion: number = 1;\n readonly occurredOn: Date = new Date(); // For backward compatibility\n\n constructor(\n aggregateId: string,\n public readonly reviewId: string,\n public readonly reviewNotes: string,\n public readonly nextActionsIdentified: number\n ) {\n this.aggregateId = aggregateId;\n }\n\n getEventData(): Record<string, any> {\n return {\n reviewId: this.reviewId,\n reviewNotes: this.reviewNotes,\n nextActionsIdentified: this.nextActionsIdentified,\n completedAt: this.occurredOn.toISOString()\n };\n }\n}\n\nexport class NextActionEventData {\n constructor(\n public readonly projectName: string,\n public readonly description: string,\n public readonly context: string,\n public readonly energyLevel: string,\n public readonly estimatedDuration?: number,\n public readonly tags?: string[]\n ) {}\n}\n","import { ActionContext } from '../value-objects/ActionContext';\nimport { EnergyLevel } from '../value-objects/EnergyLevel';\nimport { DomainEvent } from '../events/DomainEvent';\nimport { NextActionCreated, TaskAssigned } from '../events/GTDEvents';\nimport { \n AssignmentError, \n ThoughtProcessingError \n} from '../errors/GTDDomainError';\n\n/**\n * GTD Next Action - the fundamental unit of actionable work\n * \"The next physical action required to move something forward\" - David Allen\n * \n * Core GTD Principle: Actions must be specific, actionable, and context-based\n */\nexport class NextAction {\n private domainEvents: DomainEvent[] = [];\n private _completedAt?: Date;\n private _assignedTo?: string;\n private _assignedAt?: Date;\n private _roleType?: string;\n\n constructor(\n public readonly id: string,\n public readonly description: string,\n public readonly context: ActionContext,\n public readonly energyRequired: EnergyLevel,\n public readonly estimatedMinutes: number,\n public readonly createdBy: string,\n public readonly createdAt: Date = new Date(),\n public readonly projectId?: string\n ) {\n this.validateAction(description, estimatedMinutes);\n \n this.addDomainEvent(new NextActionCreated(\n id, \n description, \n context.toString(), \n energyRequired.toString(), \n createdBy\n ));\n }\n\n /**\n * Create a new next action with validation\n */\n static create(\n description: string,\n context: ActionContext,\n energy: EnergyLevel,\n minutes: number,\n createdBy: string,\n projectId?: string\n ): NextAction {\n const id = this.generateId();\n return new NextAction(id, description, context, energy, minutes, createdBy, new Date(), projectId);\n }\n\n /**\n * Reconstitute from persistence\n */\n static reconstitute(\n id: string,\n description: string,\n context: ActionContext,\n energy: EnergyLevel,\n minutes: number,\n createdBy: string,\n createdAt: Date,\n projectId?: string,\n assignedTo?: string,\n assignedAt?: Date,\n roleType?: string,\n completedAt?: Date\n ): NextAction {\n const action = new NextAction(id, description, context, energy, minutes, createdBy, createdAt, projectId);\n \n // Clear events from constructor\n action.clearDomainEvents();\n \n // Restore state\n if (assignedTo && assignedAt && roleType) {\n action._assignedTo = assignedTo;\n action._assignedAt = assignedAt;\n action._roleType = roleType;\n }\n \n if (completedAt) {\n action._completedAt = completedAt;\n }\n \n return action;\n }\n\n /**\n * Assign this action to a developer/team member\n * Integrates with Party Management domain\n */\n assignTo(partyId: string, roleType: string): void {\n if (this.isCompleted()) {\n throw new AssignmentError('Cannot assign completed actions', this.id);\n }\n\n if (this.isAssigned()) {\n throw new AssignmentError(\n `Action is already assigned to ${this._assignedTo}`, \n this.id\n );\n }\n\n if (!partyId?.trim()) {\n throw new AssignmentError('Party ID is required for assignment', this.id);\n }\n\n if (!roleType?.trim()) {\n throw new AssignmentError('Role type is required for assignment', this.id);\n }\n\n this._assignedTo = partyId;\n this._roleType = roleType;\n this._assignedAt = new Date();\n \n this.addDomainEvent(new TaskAssigned(\n this.id, \n partyId, \n roleType, \n this._assignedAt.toISOString()\n ));\n }\n\n /**\n * Unassign the action (return to available pool)\n */\n unassign(): void {\n if (!this.isAssigned()) {\n throw new AssignmentError('Action is not currently assigned', this.id);\n }\n\n if (this.isCompleted()) {\n throw new AssignmentError('Cannot unassign completed actions', this.id);\n }\n\n this._assignedTo = undefined;\n this._roleType = undefined;\n this._assignedAt = undefined;\n }\n\n /**\n * Mark action as completed\n */\n complete(): void {\n if (this.isCompleted()) {\n throw new ThoughtProcessingError('Action is already completed');\n }\n\n this._completedAt = new Date();\n }\n\n /**\n * Check if action can be performed given current context and energy\n * Core GTD principle: Match actions to available resources\n */\n canBePerformedWith(availableContext: ActionContext, availableEnergy: EnergyLevel): boolean {\n if (this.isCompleted()) {\n return false;\n }\n\n const contextMatch = this.context.equals(availableContext);\n const energyMatch = this.energyRequired.canBePerformedWhen(availableEnergy);\n \n return contextMatch && energyMatch;\n }\n\n /**\n * Get available actions that match criteria\n */\n static getAvailableActions(\n actions: NextAction[], \n context: ActionContext, \n energy: EnergyLevel\n ): NextAction[] {\n return actions.filter(action => \n !action.isCompleted() && \n !action.isAssigned() && \n action.canBePerformedWith(context, energy)\n );\n }\n\n // State queries\n isCompleted(): boolean {\n return this._completedAt !== undefined;\n }\n\n isAssigned(): boolean {\n return this._assignedTo !== undefined;\n }\n\n isAvailable(): boolean {\n return !this.isCompleted() && !this.isAssigned();\n }\n\n // Getters\n get assignedTo(): string | undefined {\n return this._assignedTo;\n }\n\n get roleType(): string | undefined {\n return this._roleType;\n }\n\n get assignedAt(): Date | undefined {\n return this._assignedAt;\n }\n\n get completedAt(): Date | undefined {\n return this._completedAt;\n }\n\n get status(): 'available' | 'assigned' | 'completed' {\n if (this.isCompleted()) return 'completed';\n if (this.isAssigned()) return 'assigned';\n return 'available';\n }\n\n // Event sourcing support\n getDomainEvents(): DomainEvent[] {\n return [...this.domainEvents];\n }\n\n clearDomainEvents(): void {\n this.domainEvents = [];\n }\n\n // Private methods\n private validateAction(description: string, estimatedMinutes: number): void {\n if (!description?.trim()) {\n throw new ThoughtProcessingError('Action description is required');\n }\n\n if (estimatedMinutes <= 0) {\n throw new ThoughtProcessingError('Estimated minutes must be positive');\n }\n\n // No upper limit validation - NextActions can take any reasonable duration\n // The 2-minute rule is workflow guidance, not a domain constraint\n }\n\n private addDomainEvent(event: DomainEvent): void {\n this.domainEvents.push(event);\n }\n\n private static generateId(): string {\n return `action-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`;\n }\n}","/**\n * GTD Action Context - WHERE and HOW an action can be performed\n * Following David Allen's context-based action organization\n */\nexport class ActionContext {\n private constructor(\n public readonly context: string,\n public readonly toolsRequired: string[] = [],\n public readonly location?: string\n ) {}\n\n static atComputer(tools: string[] = []): ActionContext {\n return new ActionContext('computer', tools);\n }\n\n static onPhone(tools: string[] = []): ActionContext {\n return new ActionContext('phone', tools);\n }\n\n static atCalls(tools: string[] = []): ActionContext {\n return new ActionContext('@calls', tools);\n }\n\n static atErrands(location: string = '', tools: string[] = []): ActionContext {\n return new ActionContext('@errands', tools, location);\n }\n\n static anywhere(tools: string[] = []): ActionContext {\n return new ActionContext('@anywhere', tools);\n }\n\n static atOffice(tools: string[] = []): ActionContext {\n return new ActionContext('office', tools, 'office');\n }\n\n static atHome(tools: string[] = []): ActionContext {\n return new ActionContext('home', tools, 'home');\n }\n\n static errands(location: string, tools: string[] = []): ActionContext {\n return new ActionContext('errands', tools, location);\n }\n\n static agendaFor(person: string): ActionContext {\n return new ActionContext('agenda', [], person);\n }\n\n static custom(context: string, tools: string[] = [], location?: string): ActionContext {\n return new ActionContext(context, tools, location);\n }\n\n equals(other: ActionContext): boolean {\n return this.context === other.context && \n this.location === other.location &&\n JSON.stringify(this.toolsRequired) === JSON.stringify(other.toolsRequired);\n }\n\n toString(): string {\n // Return context directly if it already starts with @\n if (this.context.startsWith('@')) {\n return this.context;\n }\n // Add @ prefix for contexts that don't have it\n const base = `@${this.context}`;\n if (this.location) {\n return `${base} (${this.location})`;\n }\n return base;\n }\n}","/**\n * GTD Energy Level - matching actions to available energy\n * Helps optimize productivity based on current mental/physical state\n */\nexport class EnergyLevel {\n private constructor(private readonly level: 'low' | 'medium' | 'high') {}\n\n static low(): EnergyLevel {\n return new EnergyLevel('low');\n }\n\n static medium(): EnergyLevel {\n return new EnergyLevel('medium');\n }\n\n static high(): EnergyLevel {\n return new EnergyLevel('high');\n }\n\n isLow(): boolean {\n return this.level === 'low';\n }\n\n isMedium(): boolean {\n return this.level === 'medium';\n }\n\n isHigh(): boolean {\n return this.level === 'high';\n }\n\n /**\n * Can this action be performed with the available energy level?\n * GTD principle: Match actions to available energy\n */\n canBePerformedWhen(available: EnergyLevel): boolean {\n const levels = { low: 1, medium: 2, high: 3 };\n return levels[this.level] <= levels[available.level];\n }\n\n equals(other: EnergyLevel): boolean {\n return this.level === other.level;\n }\n\n toString(): string {\n return this.level;\n }\n\n valueOf(): string {\n return this.level;\n }\n}","import { InboxItem } from '../aggregates/InboxItem';\nimport { NextAction } from '../../../project-management/domain/aggregates/NextAction';\nimport { ActionContext } from '../../../project-management/domain/value-objects/ActionContext';\nimport { EnergyLevel } from '../../../project-management/domain/value-objects/EnergyLevel';\n\n/**\n * GTD Processing Workflow Service\n * \n * Implements David Allen's complete GTD methodology by orchestrating the flow\n * from inbox clarification to actionable work items (NextActions, Projects, etc.)\n * \n * Core GTD Decision Tree:\n * 1. What is it? (Clarification)\n * 2. Is it actionable? (Decision)\n * 3. If YES: What's the next action? (NextAction creation)\n * 4. If NO: Reference material, someday/maybe, or trash\n */\nexport class GTDProcessingWorkflowService {\n \n /**\n * Process a clarified inbox item according to GTD methodology\n * \n * @param inboxItem - The clarified inbox item\n * @param processingDecision - How the item should be processed\n * @returns The created work items (NextActions, Projects, etc.)\n */\n processInboxItem(\n inboxItem: InboxItem, \n processingDecision: InboxProcessingDecision\n ): GTDProcessingResult {\n \n if (!inboxItem.clarification) {\n throw new Error('Inbox item must be clarified before processing');\n }\n\n if (inboxItem.processingStatus === 'unprocessed') {\n throw new Error('Inbox item must be marked as processed');\n }\n\n // Execute GTD decision tree\n if (inboxItem.isActionable === true) {\n return this.handleActionableItem(inboxItem, processingDecision);\n } else {\n return this.handleNonActionableItem(inboxItem, processingDecision);\n }\n }\n\n /**\n * Handle actionable items (create NextActions or Projects)\n */\n private handleActionableItem(\n inboxItem: InboxItem, \n decision: InboxProcessingDecision\n ): GTDProcessingResult {\n \n const result: GTDProcessingResult = {\n success: true,\n workflowType: 'actionable',\n createdItems: []\n };\n\n // GTD Rule: If it takes less than 2 minutes, do it now\n // Otherwise, defer it (NextAction) or delegate it\n \n if (decision.estimatedMinutes && decision.estimatedMinutes <= 2) {\n // Create immediate action\n const nextAction = this.createNextAction(inboxItem, decision);\n result.createdItems.push({\n type: 'next_action',\n id: nextAction.id,\n description: nextAction.description,\n urgent: true,\n reason: 'Two-minute rule: Do it now'\n });\n } else if (decision.isProject) {\n // Multi-step outcomes require a project\n result.workflowType = 'project';\n result.createdItems.push({\n type: 'project',\n id: `project-${Date.now()}`,\n description: decision.projectOutcome || inboxItem.clarification || 'New project',\n reason: 'Multi-step outcome requires project planning'\n });\n \n // Create the first next action for the project\n const firstAction = this.createNextAction(inboxItem, decision);\n result.createdItems.push({\n type: 'next_action',\n id: firstAction.id,\n description: firstAction.description,\n urgent: false,\n reason: 'First action for project'\n });\n } else {\n // Single next action\n const nextAction = this.createNextAction(inboxItem, decision);\n result.createdItems.push({\n type: 'next_action',\n id: nextAction.id,\n description: nextAction.description,\n urgent: false,\n reason: 'Single actionable item'\n });\n }\n\n return result;\n }\n\n /**\n * Handle non-actionable items (reference, someday/maybe, trash)\n */\n private handleNonActionableItem(\n inboxItem: InboxItem, \n decision: InboxProcessingDecision\n ): GTDProcessingResult {\n \n const result: GTDProcessingResult = {\n success: true,\n workflowType: 'non_actionable',\n createdItems: []\n };\n\n if (decision.isReference) {\n // Store as reference material\n result.createdItems.push({\n type: 'reference_material',\n id: `ref-${Date.now()}`,\n description: inboxItem.clarification || 'Reference material',\n reason: 'Useful information for future reference'\n });\n } else if (decision.isSomedayMaybe) {\n // Add to Someday/Maybe list\n result.createdItems.push({\n type: 'someday_maybe',\n id: `someday-${Date.now()}`,\n description: inboxItem.clarification || 'Someday/Maybe item',\n reason: 'Potentially actionable in the future'\n });\n } else {\n // Trash - no action needed\n result.workflowType = 'trash';\n result.reason = 'Not actionable and not worth keeping';\n }\n\n return result;\n }\n\n /**\n * Create a NextAction from an inbox item\n */\n private createNextAction(\n inboxItem: InboxItem, \n decision: InboxProcessingDecision\n ): NextAction {\n \n // Determine context - default to @computer if not specified\n const context = decision.context \n ? this.parseActionContext(decision.context)\n : ActionContext.atComputer();\n\n // Determine energy level - default to medium\n const energyLevel = decision.energyLevel\n ? this.parseEnergyLevel(decision.energyLevel)\n : EnergyLevel.medium();\n\n // Estimate time - default to 1 minute\n const estimatedMinutes = decision.estimatedMinutes || 1;\n\n // Create the action description\n const actionDescription = decision.nextActionDescription \n || this.generateActionDescription(inboxItem.clarification || '');\n\n return NextAction.create(\n actionDescription,\n context,\n energyLevel,\n estimatedMinutes,\n inboxItem.capturedByPersonId,\n decision.projectId\n );\n }\n\n /**\n * Parse context string into ActionContext object\n */\n private parseActionContext(contextString: string): ActionContext {\n const context = contextString.toLowerCase().replace('@', '');\n \n switch (context) {\n case 'computer':\n return ActionContext.atComputer();\n case 'phone':\n case 'calls':\n return ActionContext.onPhone();\n case 'office':\n return ActionContext.atOffice();\n case 'home':\n return ActionContext.atHome();\n case 'errands':\n return ActionContext.errands('general');\n default:\n return ActionContext.custom(context);\n }\n }\n\n /**\n * Parse energy level string into EnergyLevel object\n */\n private parseEnergyLevel(energyString: string): EnergyLevel {\n const level = energyString.toLowerCase();\n \n switch (level) {\n case 'high':\n return EnergyLevel.high();\n case 'low':\n return EnergyLevel.low();\n case 'medium':\n default:\n return EnergyLevel.medium();\n }\n }\n\n /**\n * Generate a proper action description from clarification\n * GTD Principle: Actions must be specific and physical\n */\n private generateActionDescription(clarification: string): string {\n // Simple heuristic to make actions more specific\n const actionVerbs = ['Call', 'Email', 'Write', 'Research', 'Review', 'Schedule', 'Update'];\n \n // If clarification already starts with an action verb, use it\n const hasActionVerb = actionVerbs.some(verb => \n clarification.toLowerCase().startsWith(verb.toLowerCase())\n );\n\n if (hasActionVerb) {\n return clarification;\n }\n\n // Otherwise, prefix with a default action verb\n return `Research: ${clarification}`;\n }\n}\n\n/**\n * Input for processing decisions\n */\nexport interface InboxProcessingDecision {\n // Action characteristics\n estimatedMinutes?: number;\n context?: string; // @calls, @computer, @errands, etc.\n energyLevel?: string; // high, medium, low\n nextActionDescription?: string;\n \n // Workflow decisions\n isProject?: boolean;\n projectOutcome?: string;\n projectId?: string;\n \n // Non-actionable decisions\n isReference?: boolean;\n isSomedayMaybe?: boolean;\n}\n\n/**\n * Result of GTD processing workflow\n */\nexport interface GTDProcessingResult {\n success: boolean;\n workflowType: 'actionable' | 'project' | 'non_actionable' | 'trash';\n createdItems: Array<{\n type: 'next_action' | 'project' | 'reference_material' | 'someday_maybe';\n id: string;\n description: string;\n urgent?: boolean;\n reason: string;\n }>;\n reason?: string;\n error?: string;\n}","/**\n * Complete GTD Processing Command\n * \n * Implements David Allen's full Getting Things Done methodology:\n * 1. Capture (already done - item is in inbox)\n * 2. Clarify (what is it? is it actionable?)\n * 3. Organize (create NextActions, Projects, Reference, or Someday/Maybe)\n * 4. Reflect (maintain system integrity) \n * 5. Engage (choose actions based on context and energy)\n * \n * This command bridges steps 2-3, completing the missing workflow\n * that converts inbox items into actionable work items.\n */\nexport interface CompleteGTDProcessingCommand {\n // Core identification\n itemId: string;\n processedByPersonId: string;\n \n // GTD Clarification (Step 2)\n clarification: string;\n isActionable: boolean;\n \n // GTD Organization (Step 3) - Action Properties\n estimatedMinutes?: number; // GTD: If ≤2 minutes, do now\n context?: string; // @calls, @computer, @errands, @home, etc.\n energyLevel?: string; // high, medium, low (for energy-based selection)\n nextActionDescription?: string; // Specific, physical action description\n \n // GTD Organization (Step 3) - Project Properties \n isProject?: boolean; // Multi-step outcome requiring project\n projectOutcome?: string; // Desired end result for project\n \n // GTD Organization (Step 3) - Non-actionable Properties\n isReference?: boolean; // Useful information to keep\n isSomedayMaybe?: boolean; // Potentially actionable in future\n}\n\n/**\n * Result of complete GTD processing\n */\nexport interface CompleteGTDProcessingResult {\n success: boolean;\n itemId: string;\n processedAt?: string;\n \n // GTD Clarification Results\n clarification?: string;\n isActionable?: boolean;\n workflowType?: 'actionable' | 'project' | 'non_actionable' | 'trash';\n \n // GTD Organization Results\n createdWorkItems?: Array<{\n type: 'next_action' | 'project' | 'reference_material' | 'someday_maybe';\n id: string;\n description: string;\n status: string;\n urgent?: boolean;\n reason?: string;\n }>;\n \n // Operation Results\n message: string;\n error?: string;\n \n // GTD System Statistics (helpful for dashboard)\n gtdMetrics?: {\n totalInboxItems: number;\n processedToday: number;\n nextActionsCreated: number;\n projectsCreated: number;\n referencesStored: number;\n somedayItems: number;\n };\n}\n\n/**\n * Command Validation Helpers\n */\nexport class CompleteGTDProcessingCommandValidator {\n \n static validate(command: CompleteGTDProcessingCommand): ValidationResult {\n const errors: string[] = [];\n \n // Required fields\n if (!command.itemId?.trim()) {\n errors.push('Item ID is required');\n }\n \n if (!command.processedByPersonId?.trim()) {\n errors.push('Processed by person ID is required');\n }\n \n if (!command.clarification?.trim()) {\n errors.push('Clarification is required for GTD processing');\n }\n \n if (command.isActionable === undefined || command.isActionable === null) {\n errors.push('Actionable decision is required (true/false)');\n }\n \n // GTD-specific validations\n if (command.isActionable) {\n // Actionable items should have action context\n if (command.estimatedMinutes && command.estimatedMinutes > 120) {\n errors.push('Actions over 2 hours should be broken into smaller steps');\n }\n \n if (command.isProject && !command.projectOutcome?.trim()) {\n errors.push('Project outcome is required for multi-step items');\n }\n } else {\n // Non-actionable items should specify disposal method\n const hasDisposalMethod = command.isReference || command.isSomedayMaybe;\n if (!hasDisposalMethod) {\n errors.push('Non-actionable items must specify reference or someday/maybe');\n }\n }\n \n // Context validation\n if (command.context) {\n const validContexts = ['@calls', '@computer', '@errands', '@home', '@office', '@anywhere'];\n const isValidContext = validContexts.includes(command.context.toLowerCase()) || \n command.context.startsWith('@');\n \n if (!isValidContext) {\n er