@critical-path/core 0.14.0 → 0.15.1

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 (55) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.md +22 -0
  3. package/dist/deliverables.test.js +2 -2
  4. package/dist/deliverables.test.js.map +1 -1
  5. package/dist/domain/entities.d.ts +2 -1
  6. package/dist/domain/entities.d.ts.map +1 -1
  7. package/dist/domain/entities.js +11 -4
  8. package/dist/domain/entities.js.map +1 -1
  9. package/dist/domain/events.d.ts +8 -1
  10. package/dist/domain/events.d.ts.map +1 -1
  11. package/dist/domain/events.js.map +1 -1
  12. package/dist/engine/index.d.ts +7 -3
  13. package/dist/engine/index.d.ts.map +1 -1
  14. package/dist/engine/index.js +102 -13
  15. package/dist/engine/index.js.map +1 -1
  16. package/dist/index.test.js +64 -9
  17. package/dist/index.test.js.map +1 -1
  18. package/dist/status.test.d.ts +2 -0
  19. package/dist/status.test.d.ts.map +1 -0
  20. package/dist/status.test.js +199 -0
  21. package/dist/status.test.js.map +1 -0
  22. package/dist/store/firebase.d.ts.map +1 -1
  23. package/dist/store/firebase.js +12 -2
  24. package/dist/store/firebase.js.map +1 -1
  25. package/dist/store/firebase.test.js +14 -1
  26. package/dist/store/firebase.test.js.map +1 -1
  27. package/dist/store/sqlite.test.js +1 -1
  28. package/dist/store/sqlite.test.js.map +1 -1
  29. package/dist/types/index.d.ts +21 -6
  30. package/dist/types/index.d.ts.map +1 -1
  31. package/dist/use-case-validation.test.js +1 -1
  32. package/dist/use-case-validation.test.js.map +1 -1
  33. package/dist/utils/status.d.ts +7 -10
  34. package/dist/utils/status.d.ts.map +1 -1
  35. package/dist/utils/status.js +104 -21
  36. package/dist/utils/status.js.map +1 -1
  37. package/dist/utils/workflow.d.ts +1 -0
  38. package/dist/utils/workflow.d.ts.map +1 -1
  39. package/dist/utils/workflow.js +65 -18
  40. package/dist/utils/workflow.js.map +1 -1
  41. package/package.json +1 -1
  42. package/src/deliverables.test.ts +2 -2
  43. package/src/domain/entities.ts +15 -5
  44. package/src/domain/events.ts +9 -0
  45. package/src/engine/index.ts +123 -14
  46. package/src/index.test.ts +77 -9
  47. package/src/status.test.ts +226 -0
  48. package/src/store/firebase.test.ts +17 -1
  49. package/src/store/firebase.ts +12 -2
  50. package/src/store/sqlite.test.ts +1 -1
  51. package/src/types/index.ts +23 -5
  52. package/src/use-case-validation.test.ts +1 -1
  53. package/src/utils/status.ts +119 -33
  54. package/src/utils/workflow.ts +75 -18
  55. package/tsconfig.tsbuildinfo +1 -1
@@ -4,17 +4,33 @@ export type Priority = DefaultPriority | (string & {});
4
4
  export type DefaultTaskStatus = 'backlog' | 'todo' | 'in_progress' | 'in_review' | 'done' | 'canceled';
5
5
  export type TaskStatus = DefaultTaskStatus | (string & {});
6
6
 
7
- export type CompletionState = 'done' | 'not_done';
8
- export type ExecutionState = 'active' | 'inactive';
7
+ export type SemanticStatus = 'not_started' | 'in_progress' | 'completed' | 'canceled';
9
8
 
10
9
  export interface StatusDefinition {
11
10
  key: string;
12
11
  label: string;
13
- completionState: CompletionState;
14
- executionState: ExecutionState;
15
- isCancelled?: boolean;
12
+ category: SemanticStatus;
16
13
  }
17
14
 
15
+ export interface TaskDerivedStatus {
16
+ semanticStatus: SemanticStatus;
17
+ isReady: boolean;
18
+ isBlocked: boolean;
19
+ blockingTaskIds: string[];
20
+ isOverdue: boolean;
21
+ isUpcoming: boolean;
22
+ isUnplanned: boolean;
23
+ isUnassigned: boolean;
24
+ isStalled: boolean;
25
+ isOverEstimate: boolean;
26
+ isPaceWarning: boolean;
27
+ isDone: boolean;
28
+ isActive: boolean;
29
+ isCancelled: boolean;
30
+ }
31
+
32
+ export type TaskLifecycleState = TaskDerivedStatus;
33
+
18
34
  export type Role = 'admin' | 'project_manager' | 'contributor' | 'viewer';
19
35
 
20
36
  export interface User {
@@ -142,6 +158,7 @@ export interface Task {
142
158
  title: string;
143
159
  description?: string;
144
160
  status: TaskStatus;
161
+ semanticStatus?: SemanticStatus;
145
162
  priority: Priority;
146
163
  taskType?: string; // e.g. "bug", "feature", "task", "epic"
147
164
  assigneeId?: string;
@@ -352,6 +369,7 @@ export type WebhookEvent =
352
369
  | 'task.updated'
353
370
  | 'task.deleted'
354
371
  | 'task.status_changed'
372
+ | 'task.unblocked'
355
373
  | 'comment.created'
356
374
  | 'comment.updated'
357
375
  | 'comment.deleted'
@@ -138,7 +138,7 @@ describe('Critical Path Use Case Validation Suite', () => {
138
138
  // Check Lifecycle State
139
139
  const lifecycle = await engine.getTaskLifecycleState(featureTask.id);
140
140
  expect(lifecycle?.isDone).toBe(true);
141
- expect(lifecycle?.completionState).toBe('done');
141
+ expect(lifecycle?.semanticStatus).toBe('completed');
142
142
  });
143
143
  });
144
144
 
@@ -1,27 +1,24 @@
1
1
  import type {
2
2
  Task,
3
3
  StatusDefinition,
4
- CompletionState,
5
- ExecutionState
4
+ SemanticStatus,
5
+ TaskDerivedStatus,
6
6
  } from '../types/index.js';
7
7
 
8
8
  export const DEFAULT_STATUS_DEFINITIONS: Record<string, StatusDefinition> = {
9
- backlog: { key: 'backlog', label: 'Backlog', completionState: 'not_done', executionState: 'inactive' },
10
- todo: { key: 'todo', label: 'To Do', completionState: 'not_done', executionState: 'inactive' },
11
- in_progress: { key: 'in_progress', label: 'In Progress', completionState: 'not_done', executionState: 'active' },
12
- in_review: { key: 'in_review', label: 'In Review', completionState: 'not_done', executionState: 'active' },
13
- done: { key: 'done', label: 'Done', completionState: 'done', executionState: 'inactive' },
14
- canceled: { key: 'canceled', label: 'Canceled', completionState: 'done', executionState: 'inactive', isCancelled: true }
9
+ backlog: { key: 'backlog', label: 'Backlog', category: 'not_started' },
10
+ todo: { key: 'todo', label: 'To Do', category: 'not_started' },
11
+ in_progress: { key: 'in_progress', label: 'In Progress', category: 'in_progress' },
12
+ in_review: { key: 'in_review', label: 'In Review', category: 'in_progress' },
13
+ done: { key: 'done', label: 'Done', category: 'completed' },
14
+ canceled: { key: 'canceled', label: 'Canceled', category: 'canceled' }
15
15
  };
16
16
 
17
- export interface TaskLifecycleState {
18
- completionState: CompletionState;
19
- executionState: ExecutionState;
20
- isDone: boolean;
21
- isActive: boolean;
22
- isCancelled: boolean;
23
- isOverdue: boolean;
24
- isUpcoming: boolean;
17
+ export interface DeriveTaskLifecycleOptions {
18
+ customDefinitions?: StatusDefinition[];
19
+ referenceDate?: Date;
20
+ upstreamTasks?: Task[];
21
+ stalledThresholdDays?: number;
25
22
  }
26
23
 
27
24
  export function resolveStatusDefinition(
@@ -35,28 +32,66 @@ export function resolveStatusDefinition(
35
32
  if (DEFAULT_STATUS_DEFINITIONS[statusKey]) {
36
33
  return DEFAULT_STATUS_DEFINITIONS[statusKey];
37
34
  }
35
+
38
36
  // Fallback heuristic for unknown custom statuses:
39
- const isDoneKey = ['done', 'completed', 'finished', 'closed', 'canceled', 'cancelled', 'approved', 'final'].includes(statusKey.toLowerCase());
40
- const isActiveKey = ['in_progress', 'active', 'working', 'in_review', 'review'].includes(statusKey.toLowerCase());
37
+ const lower = statusKey.toLowerCase();
38
+ let category: SemanticStatus = 'not_started';
39
+ if (['canceled', 'cancelled', 'rejected', 'abandoned', 'void', 'dropped'].some((k) => lower.includes(k))) {
40
+ category = 'canceled';
41
+ } else if (['done', 'completed', 'finished', 'closed', 'approved', 'final', 'delivered', 'shipped'].some((k) => lower.includes(k))) {
42
+ category = 'completed';
43
+ } else if (['in_progress', 'active', 'working', 'in_review', 'review', 'testing', 'qa', 'building', 'editing'].some((k) => lower.includes(k))) {
44
+ category = 'in_progress';
45
+ }
46
+
41
47
  return {
42
48
  key: statusKey,
43
49
  label: statusKey,
44
- completionState: isDoneKey ? 'done' : 'not_done',
45
- executionState: isActiveKey ? 'active' : 'inactive',
46
- isCancelled: statusKey.toLowerCase().includes('cancel')
50
+ category
47
51
  };
48
52
  }
49
53
 
50
54
  export function deriveTaskLifecycleState(
51
55
  task: Task,
52
- customDefinitions?: StatusDefinition[],
53
- referenceDate: Date = new Date()
54
- ): TaskLifecycleState {
56
+ customDefinitionsOrOptions?: StatusDefinition[] | DeriveTaskLifecycleOptions,
57
+ legacyReferenceDate?: Date
58
+ ): TaskDerivedStatus {
59
+ let customDefinitions: StatusDefinition[] | undefined;
60
+ let referenceDate = legacyReferenceDate ?? new Date();
61
+ let upstreamTasks: Task[] = [];
62
+ let stalledThresholdDays = 5;
63
+
64
+ if (Array.isArray(customDefinitionsOrOptions)) {
65
+ customDefinitions = customDefinitionsOrOptions;
66
+ } else if (customDefinitionsOrOptions) {
67
+ customDefinitions = customDefinitionsOrOptions.customDefinitions;
68
+ if (customDefinitionsOrOptions.referenceDate) {
69
+ referenceDate = customDefinitionsOrOptions.referenceDate;
70
+ }
71
+ if (customDefinitionsOrOptions.upstreamTasks) {
72
+ upstreamTasks = customDefinitionsOrOptions.upstreamTasks;
73
+ }
74
+ if (customDefinitionsOrOptions.stalledThresholdDays !== undefined) {
75
+ stalledThresholdDays = customDefinitionsOrOptions.stalledThresholdDays;
76
+ }
77
+ }
78
+
55
79
  const def = resolveStatusDefinition(task.status, customDefinitions);
56
- const isDone = def.completionState === 'done';
57
- const isActive = def.executionState === 'active';
58
- const isCancelled = Boolean(def.isCancelled);
80
+ const semanticStatus: SemanticStatus = def.category;
81
+ const isDone = semanticStatus === 'completed' || semanticStatus === 'canceled';
82
+ const isActive = semanticStatus === 'in_progress';
83
+ const isCancelled = semanticStatus === 'canceled';
59
84
 
85
+ // Dependency & Flow Indicators
86
+ const blockingTasks = upstreamTasks.filter((t) => {
87
+ const upstreamDef = resolveStatusDefinition(t.status, customDefinitions);
88
+ return upstreamDef.category !== 'completed';
89
+ });
90
+ const blockingTaskIds = blockingTasks.map((t) => t.id);
91
+ const isBlocked = !isDone && blockingTaskIds.length > 0;
92
+ const isReady = semanticStatus === 'not_started' && blockingTaskIds.length === 0;
93
+
94
+ // Time & Schedule Indicators
60
95
  let isOverdue = false;
61
96
  if (!isDone && task.dueDate) {
62
97
  const due = new Date(task.dueDate);
@@ -66,20 +101,71 @@ export function deriveTaskLifecycleState(
66
101
  }
67
102
 
68
103
  let isUpcoming = false;
69
- if (!isDone && !isActive && task.plannedStartDate) {
104
+ if (semanticStatus === 'not_started' && task.plannedStartDate) {
70
105
  const plannedStart = new Date(task.plannedStartDate);
71
106
  if (!isNaN(plannedStart.getTime()) && plannedStart > referenceDate) {
72
107
  isUpcoming = true;
73
108
  }
74
109
  }
75
110
 
111
+ const isUnplanned = semanticStatus === 'not_started' && !task.iterationId && !task.plannedStartDate && !task.dueDate;
112
+
113
+ // Resource & Activity Indicators
114
+ const hasAssignee = Boolean(task.assigneeId || (task.assignees && task.assignees.length > 0));
115
+ const isUnassigned = !isDone && !hasAssignee;
116
+
117
+ let isStalled = false;
118
+ if (semanticStatus === 'in_progress') {
119
+ const lastActivity = task.updatedAt ? new Date(task.updatedAt) : (task.createdAt ? new Date(task.createdAt) : null);
120
+ if (lastActivity && !isNaN(lastActivity.getTime())) {
121
+ const thresholdMs = stalledThresholdDays * 24 * 60 * 60 * 1000;
122
+ if (referenceDate.getTime() - lastActivity.getTime() > thresholdMs) {
123
+ isStalled = true;
124
+ }
125
+ }
126
+ }
127
+
128
+ // Effort & Estimate Indicators
129
+ let isOverEstimate = false;
130
+ const loggedH = task.loggedHours ?? task.actualHours;
131
+ const estimatedH = task.estimatedHours;
132
+ if (estimatedH !== undefined && estimatedH > 0 && loggedH !== undefined && loggedH > estimatedH) {
133
+ isOverEstimate = true;
134
+ }
135
+ const actualM = task.actualDurationMinutes;
136
+ const estimatedM = task.estimatedDurationMinutes;
137
+ if (estimatedM !== undefined && estimatedM > 0 && actualM !== undefined && actualM > estimatedM) {
138
+ isOverEstimate = true;
139
+ }
140
+
141
+ let isPaceWarning = false;
142
+ if (semanticStatus === 'in_progress' && task.actualStartDate) {
143
+ const start = new Date(task.actualStartDate);
144
+ if (!isNaN(start.getTime())) {
145
+ const elapsedMs = referenceDate.getTime() - start.getTime();
146
+ const elapsedHours = elapsedMs / (1000 * 60 * 60);
147
+ if (estimatedH !== undefined && estimatedH > 0 && elapsedHours > estimatedH) {
148
+ isPaceWarning = true;
149
+ } else if (estimatedM !== undefined && estimatedM > 0 && (elapsedMs / (1000 * 60)) > estimatedM) {
150
+ isPaceWarning = true;
151
+ }
152
+ }
153
+ }
154
+
76
155
  return {
77
- completionState: def.completionState,
78
- executionState: def.executionState,
156
+ semanticStatus,
157
+ isReady,
158
+ isBlocked,
159
+ blockingTaskIds,
160
+ isOverdue,
161
+ isUpcoming,
162
+ isUnplanned,
163
+ isUnassigned,
164
+ isStalled,
165
+ isOverEstimate,
166
+ isPaceWarning,
79
167
  isDone,
80
168
  isActive,
81
- isCancelled,
82
- isOverdue,
83
- isUpcoming
169
+ isCancelled
84
170
  };
85
171
  }
@@ -90,25 +90,38 @@ export const DEFAULT_VFX_WORKFLOW: Workflow = {
90
90
  name: 'VFX Production Workflow',
91
91
  description: 'Visual effects production pipeline from bidding and drafting to internal lead review and client supervisor approval.',
92
92
  statuses: [
93
- { key: 'bidding', label: 'Bidding & Draft', completionState: 'not_done', executionState: 'inactive' },
94
- { key: 'awarded', label: 'Awarded / Ready', completionState: 'not_done', executionState: 'inactive' },
95
- { key: 'in_production', label: 'In Production', completionState: 'not_done', executionState: 'active' },
96
- { key: 'internal_review', label: 'Internal Review', completionState: 'not_done', executionState: 'active' },
97
- { key: 'client_review', label: 'Client Review', completionState: 'not_done', executionState: 'active' },
98
- { key: 'revision_requested', label: 'Revision Requested', completionState: 'not_done', executionState: 'active' },
99
- { key: 'approved', label: 'Approved (Final)', completionState: 'done', executionState: 'inactive' },
93
+ { key: 'bidding', label: 'Bidding & Draft', category: 'not_started' },
94
+ { key: 'awarded', label: 'Awarded / Ready', category: 'not_started' },
95
+ { key: 'in_production', label: 'In Production', category: 'in_progress' },
96
+ { key: 'internal_review', label: 'Internal Review', category: 'in_progress' },
97
+ { key: 'client_review', label: 'Client Review', category: 'in_progress' },
98
+ { key: 'revision_requested', label: 'Revision Requested', category: 'in_progress' },
99
+ { key: 'approved', label: 'Approved (Final)', category: 'completed' },
100
100
  DEFAULT_STATUS_DEFINITIONS.canceled
101
101
  ],
102
102
  transitions: [
103
103
  { name: 'Award Bid', fromStatusKey: 'bidding', toStatusKey: 'awarded' },
104
+ { name: 'Start Directly from Bid', fromStatusKey: 'bidding', toStatusKey: 'in_production' },
104
105
  { name: 'Start Work', fromStatusKey: 'awarded', toStatusKey: 'in_production' },
106
+ { name: 'Reopen Bid', fromStatusKey: 'awarded', toStatusKey: 'bidding' },
105
107
  { name: 'Submit to Lead Review', fromStatusKey: 'in_production', toStatusKey: 'internal_review' },
108
+ { name: 'Fast-Track Client Review', fromStatusKey: 'in_production', toStatusKey: 'client_review' },
109
+ { name: 'Direct Approval', fromStatusKey: 'in_production', toStatusKey: 'approved' },
106
110
  { name: 'Submit to Client Review', fromStatusKey: 'internal_review', toStatusKey: 'client_review' },
107
111
  { name: 'Request Internal Revisions', fromStatusKey: 'internal_review', toStatusKey: 'revision_requested' },
112
+ { name: 'Return to Production', fromStatusKey: 'internal_review', toStatusKey: 'in_production' },
113
+ { name: 'Approve from Lead Review', fromStatusKey: 'internal_review', toStatusKey: 'approved' },
108
114
  { name: 'Request Client Revisions', fromStatusKey: 'client_review', toStatusKey: 'revision_requested' },
109
- { name: 'Address Revisions', fromStatusKey: 'revision_requested', toStatusKey: 'in_production' },
115
+ { name: 'Return to Lead Review', fromStatusKey: 'client_review', toStatusKey: 'internal_review' },
110
116
  { name: 'Approve Final Shot', fromStatusKey: 'client_review', toStatusKey: 'approved' },
111
- { name: 'Cancel', fromStatusKey: '*', toStatusKey: 'canceled' }
117
+ { name: 'Address Revisions in Production', fromStatusKey: 'revision_requested', toStatusKey: 'in_production' },
118
+ { name: 'Resubmit to Lead Review', fromStatusKey: 'revision_requested', toStatusKey: 'internal_review' },
119
+ { name: 'Resubmit to Client Review', fromStatusKey: 'revision_requested', toStatusKey: 'client_review' },
120
+ { name: 'Reopen Shot', fromStatusKey: 'approved', toStatusKey: 'in_production' },
121
+ { name: 'Reopen Lead Review', fromStatusKey: 'approved', toStatusKey: 'internal_review' },
122
+ { name: 'Cancel', fromStatusKey: '*', toStatusKey: 'canceled' },
123
+ { name: 'Restore to Bidding', fromStatusKey: 'canceled', toStatusKey: 'bidding' },
124
+ { name: 'Restore to Production', fromStatusKey: 'canceled', toStatusKey: 'in_production' }
112
125
  ],
113
126
  taskTypes: [
114
127
  { key: 'vfx_task', label: 'VFX Task', description: 'Department task (e.g. Matchmove, Animation, FX, Comp)', icon: 'film' },
@@ -126,27 +139,36 @@ export const DEFAULT_CREATIVE_WORKFLOW: Workflow = {
126
139
  name: 'Creative Production Workflow',
127
140
  description: 'Production pipeline for creative deliverables from briefing and concepts to internal review, client feedback, and final delivery.',
128
141
  statuses: [
129
- { key: 'briefing', label: 'Briefing & Scoping', completionState: 'not_done', executionState: 'inactive' },
130
- { key: 'concept', label: 'Concept & Storyboarding', completionState: 'not_done', executionState: 'active' },
131
- { key: 'in_production', label: 'In Production', completionState: 'not_done', executionState: 'active' },
132
- { key: 'internal_review', label: 'Internal Review', completionState: 'not_done', executionState: 'active' },
133
- { key: 'client_review', label: 'Client Review', completionState: 'not_done', executionState: 'active' },
134
- { key: 'revision_requested', label: 'Revision Requested', completionState: 'not_done', executionState: 'active' },
135
- { key: 'approved', label: 'Approved', completionState: 'done', executionState: 'inactive' },
136
- { key: 'delivered', label: 'Delivered', completionState: 'done', executionState: 'inactive' },
142
+ { key: 'briefing', label: 'Briefing & Scoping', category: 'not_started' },
143
+ { key: 'concept', label: 'Concept & Storyboarding', category: 'in_progress' },
144
+ { key: 'in_production', label: 'In Production', category: 'in_progress' },
145
+ { key: 'internal_review', label: 'Internal Review', category: 'in_progress' },
146
+ { key: 'client_review', label: 'Client Review', category: 'in_progress' },
147
+ { key: 'revision_requested', label: 'Revision Requested', category: 'in_progress' },
148
+ { key: 'approved', label: 'Approved', category: 'completed' },
149
+ { key: 'delivered', label: 'Delivered', category: 'completed' },
137
150
  DEFAULT_STATUS_DEFINITIONS.canceled
138
151
  ],
139
152
  transitions: [
140
153
  { name: 'Start Concepts', fromStatusKey: 'briefing', toStatusKey: 'concept' },
141
154
  { name: 'Approve Concept for Production', fromStatusKey: 'concept', toStatusKey: 'in_production' },
155
+ { name: 'Return to Briefing', fromStatusKey: 'concept', toStatusKey: 'briefing' },
142
156
  { name: 'Submit for Internal Review', fromStatusKey: 'in_production', toStatusKey: 'internal_review' },
157
+ { name: 'Submit for Client Review', fromStatusKey: 'in_production', toStatusKey: 'client_review' },
143
158
  { name: 'Submit for Client Review', fromStatusKey: 'internal_review', toStatusKey: 'client_review' },
144
159
  { name: 'Request Revisions (Internal)', fromStatusKey: 'internal_review', toStatusKey: 'revision_requested' },
160
+ { name: 'Return to Production', fromStatusKey: 'internal_review', toStatusKey: 'in_production' },
161
+ { name: 'Approve from Lead Review', fromStatusKey: 'internal_review', toStatusKey: 'approved' },
145
162
  { name: 'Request Revisions (Client)', fromStatusKey: 'client_review', toStatusKey: 'revision_requested' },
163
+ { name: 'Return to Internal Review', fromStatusKey: 'client_review', toStatusKey: 'internal_review' },
146
164
  { name: 'Address Revisions', fromStatusKey: 'revision_requested', toStatusKey: 'in_production' },
165
+ { name: 'Resubmit to Lead Review', fromStatusKey: 'revision_requested', toStatusKey: 'internal_review' },
166
+ { name: 'Resubmit to Client Review', fromStatusKey: 'revision_requested', toStatusKey: 'client_review' },
147
167
  { name: 'Approve Work', fromStatusKey: 'client_review', toStatusKey: 'approved' },
148
168
  { name: 'Deliver Final Assets', fromStatusKey: 'approved', toStatusKey: 'delivered' },
149
- { name: 'Cancel', fromStatusKey: '*', toStatusKey: 'canceled' }
169
+ { name: 'Reopen Deliverable', fromStatusKey: 'delivered', toStatusKey: 'in_production' },
170
+ { name: 'Cancel', fromStatusKey: '*', toStatusKey: 'canceled' },
171
+ { name: 'Restore', fromStatusKey: 'canceled', toStatusKey: 'briefing' }
150
172
  ],
151
173
  taskTypes: [
152
174
  { key: 'deliverable', label: 'Deliverable', description: 'Finished asset, cutdown, or packaged export', icon: 'package' },
@@ -211,6 +233,41 @@ export function getAllowedNextStatuses(
211
233
  return [allStatusKeys[currentIndex + 1]];
212
234
  }
213
235
 
236
+ export function getAllowedTransitions(
237
+ workflow: Workflow | undefined,
238
+ currentStatus: string
239
+ ): string[] {
240
+ if (!workflow || !workflow.statuses || workflow.statuses.length === 0) {
241
+ return Object.keys(DEFAULT_STATUS_DEFINITIONS);
242
+ }
243
+
244
+ const allStatusKeys = workflow.statuses.map((s) => s.key);
245
+ if (!allStatusKeys.includes(currentStatus)) {
246
+ return [];
247
+ }
248
+
249
+ if (workflow.transitions && workflow.transitions.length > 0) {
250
+ const matches = workflow.transitions.filter(
251
+ (t) => t.fromStatusKey === '*' || t.fromStatusKey === currentStatus
252
+ );
253
+
254
+ const allowed = new Set<string>();
255
+ for (const t of matches) {
256
+ if (t.toStatusKey === '*') {
257
+ allStatusKeys.filter((k) => k !== currentStatus).forEach((k) => allowed.add(k));
258
+ } else if (t.toStatusKey !== currentStatus && allStatusKeys.includes(t.toStatusKey)) {
259
+ allowed.add(t.toStatusKey);
260
+ }
261
+ }
262
+
263
+ return Array.from(allowed);
264
+ }
265
+
266
+ const next = getAllowedNextStatuses(workflow, currentStatus);
267
+ const prev = getAllowedPreviousStatuses(workflow, currentStatus);
268
+ return Array.from(new Set([...next, ...prev]));
269
+ }
270
+
214
271
  export function getAllowedPreviousStatuses(
215
272
  workflow: Workflow | undefined,
216
273
  currentStatus: string
@@ -1 +1 @@
1
- {"root":["./src/deliverables.test.ts","./src/index.test.ts","./src/index.ts","./src/use-case-validation.test.ts","./src/domain/custom-fields.ts","./src/domain/ddd.test.ts","./src/domain/entities.ts","./src/domain/events.ts","./src/domain/graph.ts","./src/domain/index.ts","./src/engine/index.ts","./src/plugins/index.ts","./src/storage/file-storage.ts","./src/storage/firebase-storage.ts","./src/storage/index.ts","./src/storage/s3.ts","./src/storage/storage.test.ts","./src/store/firebase.test.ts","./src/store/firebase.ts","./src/store/index.ts","./src/store/sqlite.test.ts","./src/store/sqlite.ts","./src/types/index.ts","./src/utils/index.ts","./src/utils/key.test.ts","./src/utils/key.ts","./src/utils/mentions.test.ts","./src/utils/mentions.ts","./src/utils/status.ts","./src/utils/workflow.ts"],"version":"5.9.3"}
1
+ {"root":["./src/deliverables.test.ts","./src/index.test.ts","./src/index.ts","./src/status.test.ts","./src/use-case-validation.test.ts","./src/domain/custom-fields.ts","./src/domain/ddd.test.ts","./src/domain/entities.ts","./src/domain/events.ts","./src/domain/graph.ts","./src/domain/index.ts","./src/engine/index.ts","./src/plugins/index.ts","./src/storage/file-storage.ts","./src/storage/firebase-storage.ts","./src/storage/index.ts","./src/storage/s3.ts","./src/storage/storage.test.ts","./src/store/firebase.test.ts","./src/store/firebase.ts","./src/store/index.ts","./src/store/sqlite.test.ts","./src/store/sqlite.ts","./src/types/index.ts","./src/utils/index.ts","./src/utils/key.test.ts","./src/utils/key.ts","./src/utils/mentions.test.ts","./src/utils/mentions.ts","./src/utils/status.ts","./src/utils/workflow.ts"],"version":"5.9.3"}