octie-cli 1.0.0

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 (162) hide show
  1. package/README.md +523 -0
  2. package/dist/cli/commands/approve.d.ts +27 -0
  3. package/dist/cli/commands/approve.d.ts.map +1 -0
  4. package/dist/cli/commands/approve.js +119 -0
  5. package/dist/cli/commands/approve.js.map +1 -0
  6. package/dist/cli/commands/batch.d.ts +15 -0
  7. package/dist/cli/commands/batch.d.ts.map +1 -0
  8. package/dist/cli/commands/batch.js +521 -0
  9. package/dist/cli/commands/batch.js.map +1 -0
  10. package/dist/cli/commands/create.d.ts +9 -0
  11. package/dist/cli/commands/create.d.ts.map +1 -0
  12. package/dist/cli/commands/create.js +321 -0
  13. package/dist/cli/commands/create.js.map +1 -0
  14. package/dist/cli/commands/delete.d.ts +9 -0
  15. package/dist/cli/commands/delete.d.ts.map +1 -0
  16. package/dist/cli/commands/delete.js +143 -0
  17. package/dist/cli/commands/delete.js.map +1 -0
  18. package/dist/cli/commands/export.d.ts +9 -0
  19. package/dist/cli/commands/export.d.ts.map +1 -0
  20. package/dist/cli/commands/export.js +66 -0
  21. package/dist/cli/commands/export.js.map +1 -0
  22. package/dist/cli/commands/find.d.ts +16 -0
  23. package/dist/cli/commands/find.d.ts.map +1 -0
  24. package/dist/cli/commands/find.js +252 -0
  25. package/dist/cli/commands/find.js.map +1 -0
  26. package/dist/cli/commands/get.d.ts +9 -0
  27. package/dist/cli/commands/get.d.ts.map +1 -0
  28. package/dist/cli/commands/get.js +74 -0
  29. package/dist/cli/commands/get.js.map +1 -0
  30. package/dist/cli/commands/graph.d.ts +9 -0
  31. package/dist/cli/commands/graph.d.ts.map +1 -0
  32. package/dist/cli/commands/graph.js +200 -0
  33. package/dist/cli/commands/graph.js.map +1 -0
  34. package/dist/cli/commands/import.d.ts +9 -0
  35. package/dist/cli/commands/import.d.ts.map +1 -0
  36. package/dist/cli/commands/import.js +807 -0
  37. package/dist/cli/commands/import.js.map +1 -0
  38. package/dist/cli/commands/init.d.ts +9 -0
  39. package/dist/cli/commands/init.d.ts.map +1 -0
  40. package/dist/cli/commands/init.js +57 -0
  41. package/dist/cli/commands/init.js.map +1 -0
  42. package/dist/cli/commands/list.d.ts +9 -0
  43. package/dist/cli/commands/list.d.ts.map +1 -0
  44. package/dist/cli/commands/list.js +175 -0
  45. package/dist/cli/commands/list.js.map +1 -0
  46. package/dist/cli/commands/merge.d.ts +9 -0
  47. package/dist/cli/commands/merge.d.ts.map +1 -0
  48. package/dist/cli/commands/merge.js +113 -0
  49. package/dist/cli/commands/merge.js.map +1 -0
  50. package/dist/cli/commands/serve.d.ts +9 -0
  51. package/dist/cli/commands/serve.d.ts.map +1 -0
  52. package/dist/cli/commands/serve.js +94 -0
  53. package/dist/cli/commands/serve.js.map +1 -0
  54. package/dist/cli/commands/update.d.ts +9 -0
  55. package/dist/cli/commands/update.d.ts.map +1 -0
  56. package/dist/cli/commands/update.js +423 -0
  57. package/dist/cli/commands/update.js.map +1 -0
  58. package/dist/cli/commands/wire.d.ts +15 -0
  59. package/dist/cli/commands/wire.d.ts.map +1 -0
  60. package/dist/cli/commands/wire.js +164 -0
  61. package/dist/cli/commands/wire.js.map +1 -0
  62. package/dist/cli/index.d.ts +7 -0
  63. package/dist/cli/index.d.ts.map +1 -0
  64. package/dist/cli/index.js +100 -0
  65. package/dist/cli/index.js.map +1 -0
  66. package/dist/cli/output/json.d.ts +16 -0
  67. package/dist/cli/output/json.d.ts.map +1 -0
  68. package/dist/cli/output/json.js +29 -0
  69. package/dist/cli/output/json.js.map +1 -0
  70. package/dist/cli/output/markdown.d.ts +15 -0
  71. package/dist/cli/output/markdown.d.ts.map +1 -0
  72. package/dist/cli/output/markdown.js +206 -0
  73. package/dist/cli/output/markdown.js.map +1 -0
  74. package/dist/cli/output/table.d.ts +23 -0
  75. package/dist/cli/output/table.d.ts.map +1 -0
  76. package/dist/cli/output/table.js +150 -0
  77. package/dist/cli/output/table.js.map +1 -0
  78. package/dist/cli/utils/helpers.d.ts +126 -0
  79. package/dist/cli/utils/helpers.d.ts.map +1 -0
  80. package/dist/cli/utils/helpers.js +325 -0
  81. package/dist/cli/utils/helpers.js.map +1 -0
  82. package/dist/core/graph/algorithms.d.ts +11 -0
  83. package/dist/core/graph/algorithms.d.ts.map +1 -0
  84. package/dist/core/graph/algorithms.js +14 -0
  85. package/dist/core/graph/algorithms.js.map +1 -0
  86. package/dist/core/graph/cycle.d.ts +155 -0
  87. package/dist/core/graph/cycle.d.ts.map +1 -0
  88. package/dist/core/graph/cycle.js +297 -0
  89. package/dist/core/graph/cycle.js.map +1 -0
  90. package/dist/core/graph/index.d.ts +223 -0
  91. package/dist/core/graph/index.d.ts.map +1 -0
  92. package/dist/core/graph/index.js +475 -0
  93. package/dist/core/graph/index.js.map +1 -0
  94. package/dist/core/graph/operations.d.ts +240 -0
  95. package/dist/core/graph/operations.d.ts.map +1 -0
  96. package/dist/core/graph/operations.js +503 -0
  97. package/dist/core/graph/operations.js.map +1 -0
  98. package/dist/core/graph/sort.d.ts +76 -0
  99. package/dist/core/graph/sort.d.ts.map +1 -0
  100. package/dist/core/graph/sort.js +254 -0
  101. package/dist/core/graph/sort.js.map +1 -0
  102. package/dist/core/graph/traversal.d.ts +122 -0
  103. package/dist/core/graph/traversal.d.ts.map +1 -0
  104. package/dist/core/graph/traversal.js +336 -0
  105. package/dist/core/graph/traversal.js.map +1 -0
  106. package/dist/core/models/task-node.d.ts +328 -0
  107. package/dist/core/models/task-node.d.ts.map +1 -0
  108. package/dist/core/models/task-node.js +1090 -0
  109. package/dist/core/models/task-node.js.map +1 -0
  110. package/dist/core/registry/index.d.ts +102 -0
  111. package/dist/core/registry/index.d.ts.map +1 -0
  112. package/dist/core/registry/index.js +249 -0
  113. package/dist/core/registry/index.js.map +1 -0
  114. package/dist/core/registry/root-guard.d.ts +19 -0
  115. package/dist/core/registry/root-guard.d.ts.map +1 -0
  116. package/dist/core/registry/root-guard.js +28 -0
  117. package/dist/core/registry/root-guard.js.map +1 -0
  118. package/dist/core/storage/atomic-write.d.ts +181 -0
  119. package/dist/core/storage/atomic-write.d.ts.map +1 -0
  120. package/dist/core/storage/atomic-write.js +379 -0
  121. package/dist/core/storage/atomic-write.js.map +1 -0
  122. package/dist/core/storage/file-store.d.ts +148 -0
  123. package/dist/core/storage/file-store.d.ts.map +1 -0
  124. package/dist/core/storage/file-store.js +423 -0
  125. package/dist/core/storage/file-store.js.map +1 -0
  126. package/dist/core/storage/indexer.d.ts +138 -0
  127. package/dist/core/storage/indexer.d.ts.map +1 -0
  128. package/dist/core/storage/indexer.js +350 -0
  129. package/dist/core/storage/indexer.js.map +1 -0
  130. package/dist/core/utils/status-helpers.d.ts +59 -0
  131. package/dist/core/utils/status-helpers.d.ts.map +1 -0
  132. package/dist/core/utils/status-helpers.js +149 -0
  133. package/dist/core/utils/status-helpers.js.map +1 -0
  134. package/dist/index.d.ts +10 -0
  135. package/dist/index.d.ts.map +1 -0
  136. package/dist/index.js +10 -0
  137. package/dist/index.js.map +1 -0
  138. package/dist/types/index.d.ts +504 -0
  139. package/dist/types/index.d.ts.map +1 -0
  140. package/dist/types/index.js +182 -0
  141. package/dist/types/index.js.map +1 -0
  142. package/dist/web/routes/graph.d.ts +17 -0
  143. package/dist/web/routes/graph.d.ts.map +1 -0
  144. package/dist/web/routes/graph.js +277 -0
  145. package/dist/web/routes/graph.js.map +1 -0
  146. package/dist/web/routes/projects.d.ts +14 -0
  147. package/dist/web/routes/projects.d.ts.map +1 -0
  148. package/dist/web/routes/projects.js +102 -0
  149. package/dist/web/routes/projects.js.map +1 -0
  150. package/dist/web/routes/tasks.d.ts +17 -0
  151. package/dist/web/routes/tasks.d.ts.map +1 -0
  152. package/dist/web/routes/tasks.js +538 -0
  153. package/dist/web/routes/tasks.js.map +1 -0
  154. package/dist/web/server.d.ts +121 -0
  155. package/dist/web/server.d.ts.map +1 -0
  156. package/dist/web/server.js +389 -0
  157. package/dist/web/server.js.map +1 -0
  158. package/dist/web-ui/assets/index-BB0qvF1y.css +1 -0
  159. package/dist/web-ui/assets/index-Vmm72oKY.js +34 -0
  160. package/dist/web-ui/index.html +14 -0
  161. package/dist/web-ui/vite.svg +1 -0
  162. package/package.json +94 -0
@@ -0,0 +1,1090 @@
1
+ /**
2
+ * Task Node Model
3
+ *
4
+ * Implements the TaskNode class with:
5
+ * - Required field validation
6
+ * - Atomic task validation
7
+ * - Auto-timestamp management (created_at, updated_at, completed_at)
8
+ * - Status transition validation
9
+ * - Success criteria and deliverable tracking
10
+ *
11
+ * @module core/models/task-node
12
+ */
13
+ import { v4 as uuidv4 } from 'uuid';
14
+ import { ValidationError, AtomicTaskViolationError, ImmutabilityViolationError, } from '../../types/index.js';
15
+ /**
16
+ * Action verbs that indicate specific, executable tasks
17
+ */
18
+ const ACTION_VERBS = [
19
+ 'implement',
20
+ 'create',
21
+ 'add',
22
+ 'fix',
23
+ 'remove',
24
+ 'update',
25
+ 'refactor',
26
+ 'write',
27
+ 'test',
28
+ 'document',
29
+ 'deploy',
30
+ 'configure',
31
+ 'setup',
32
+ 'design',
33
+ 'analyze',
34
+ 'optimize',
35
+ 'migrate',
36
+ 'integrate',
37
+ 'build',
38
+ 'install',
39
+ 'check',
40
+ 'verify',
41
+ 'validate',
42
+ 'extract',
43
+ 'generate',
44
+ 'parse',
45
+ 'compile',
46
+ 'debug',
47
+ 'resolve',
48
+ 'handle',
49
+ 'process',
50
+ 'transform',
51
+ 'convert',
52
+ 'compress',
53
+ 'encrypt',
54
+ 'decrypt',
55
+ 'sanitize',
56
+ 'normalize',
57
+ 'format',
58
+ 'render',
59
+ 'display',
60
+ 'calculate',
61
+ 'compute',
62
+ 'measure',
63
+ 'track',
64
+ 'monitor',
65
+ 'log',
66
+ 'cache',
67
+ 'store',
68
+ 'retrieve',
69
+ 'fetch',
70
+ 'load',
71
+ 'save',
72
+ 'export',
73
+ 'import',
74
+ 'sync',
75
+ 'merge',
76
+ 'split',
77
+ 'parse',
78
+ 'serialize',
79
+ 'deserialize',
80
+ 'encode',
81
+ 'decode',
82
+ 'hash',
83
+ 'sign',
84
+ 'verify',
85
+ 'authenticate',
86
+ 'authorize',
87
+ 'connect',
88
+ 'disconnect',
89
+ 'bind',
90
+ 'unbind',
91
+ 'attach',
92
+ 'detach',
93
+ 'mount',
94
+ 'unmount',
95
+ 'register',
96
+ 'unregister',
97
+ 'subscribe',
98
+ 'unsubscribe',
99
+ 'publish',
100
+ 'listen',
101
+ 'emit',
102
+ 'dispatch',
103
+ 'route',
104
+ 'forward',
105
+ 'redirect',
106
+ 'proxy',
107
+ 'mirror',
108
+ 'replicate',
109
+ 'shard',
110
+ 'partition',
111
+ 'index',
112
+ 'search',
113
+ 'query',
114
+ 'filter',
115
+ 'sort',
116
+ 'group',
117
+ 'aggregate',
118
+ 'reduce',
119
+ 'map',
120
+ 'flatMap',
121
+ 'collect',
122
+ 'stream',
123
+ 'buffer',
124
+ 'flush',
125
+ 'drain',
126
+ 'close',
127
+ 'open',
128
+ 'read',
129
+ 'write',
130
+ 'seek',
131
+ 'truncate',
132
+ 'append',
133
+ 'prepend',
134
+ 'insert',
135
+ 'delete',
136
+ 'erase',
137
+ 'clear',
138
+ 'reset',
139
+ 'rollback',
140
+ 'commit',
141
+ 'transact',
142
+ 'lock',
143
+ 'unlock',
144
+ 'acquire',
145
+ 'release',
146
+ 'wait',
147
+ 'notify',
148
+ 'signal',
149
+ 'interrupt',
150
+ 'cancel',
151
+ 'abort',
152
+ 'retry',
153
+ 'replay',
154
+ 'schedule',
155
+ 'queue',
156
+ 'enqueue',
157
+ 'dequeue',
158
+ 'push',
159
+ 'pop',
160
+ 'shift',
161
+ 'unshift',
162
+ 'splice',
163
+ 'slice',
164
+ 'copy',
165
+ 'clone',
166
+ 'deepClone',
167
+ 'merge',
168
+ 'patch',
169
+ 'diff',
170
+ 'compare',
171
+ 'match',
172
+ 'replace',
173
+ 'substitute',
174
+ 'translate',
175
+ 'localize',
176
+ 'format',
177
+ 'parse',
178
+ 'tokenize',
179
+ 'lex',
180
+ 'parse',
181
+ 'evaluate',
182
+ 'execute',
183
+ 'interpret',
184
+ 'compile',
185
+ 'link',
186
+ 'build',
187
+ 'assemble',
188
+ 'package',
189
+ 'distribute',
190
+ 'release',
191
+ 'version',
192
+ 'tag',
193
+ 'branch',
194
+ 'merge',
195
+ 'rebase',
196
+ 'cherry-pick',
197
+ 'stash',
198
+ 'apply',
199
+ 'checkout',
200
+ 'clone',
201
+ 'fork',
202
+ 'pull',
203
+ 'push',
204
+ 'fetch',
205
+ 'remote',
206
+ 'init',
207
+ 'status',
208
+ 'log',
209
+ 'diff',
210
+ 'show',
211
+ 'blame',
212
+ 'grep',
213
+ 'find',
214
+ 'locate',
215
+ 'search',
216
+ ];
217
+ /**
218
+ * Vague patterns that indicate non-specific tasks
219
+ * Note: Action verbs like 'fix', 'update', 'handle', etc. are NOT here
220
+ * because they are valid action verbs that make tasks specific when combined
221
+ * with context (e.g., "Fix login bug" is specific, "Fix stuff" is vague)
222
+ */
223
+ const VAGUE_PATTERNS = [
224
+ 'stuff',
225
+ 'things',
226
+ 'etc',
227
+ 'various',
228
+ 'some',
229
+ 'something',
230
+ 'work on',
231
+ 'deal with',
232
+ ];
233
+ /**
234
+ * Subjective words that indicate non-quantitative criteria
235
+ */
236
+ const SUBJECTIVE_WORDS = [
237
+ 'good',
238
+ 'better',
239
+ 'best',
240
+ 'proper',
241
+ 'nice',
242
+ 'clean',
243
+ 'fast',
244
+ 'slow',
245
+ 'efficient',
246
+ 'effective',
247
+ 'responsive',
248
+ 'user-friendly',
249
+ 'intuitive',
250
+ 'seamless',
251
+ 'smooth',
252
+ 'robust',
253
+ 'reliable',
254
+ 'scalable',
255
+ 'maintainable',
256
+ 'readable',
257
+ 'understandable',
258
+ 'clear',
259
+ 'simple',
260
+ 'easy',
261
+ 'flexible',
262
+ 'extensible',
263
+ 'modular',
264
+ 'well-structured',
265
+ 'well-organized',
266
+ 'well-designed',
267
+ ];
268
+ /**
269
+ * Validate atomic task requirements
270
+ *
271
+ * Atomic tasks MUST be:
272
+ * - Specific (clear, focused purpose)
273
+ * - Executable (can be completed in 2-8 hours typical, max 2 days)
274
+ * - Verifiable (has quantitative success criteria)
275
+ * - Independent (minimizes dependencies)
276
+ *
277
+ * @param taskData - Task data to validate
278
+ * @throws {AtomicTaskViolationError} If task violates atomic requirements
279
+ */
280
+ export function validateAtomicTask(taskData) {
281
+ const violations = [];
282
+ const titleLower = taskData.title.toLowerCase().trim();
283
+ // Check title specificity
284
+ // Skip action verb check for Unicode titles (non-ASCII characters)
285
+ // as they may be valid in other languages (Chinese, Japanese, etc.)
286
+ const isUnicodeTitle = /[^\x00-\x7F]/.test(taskData.title);
287
+ const hasActionVerb = !isUnicodeTitle && ACTION_VERBS.some(verb => titleLower.includes(verb.toLowerCase()));
288
+ if (!isUnicodeTitle && !hasActionVerb) {
289
+ violations.push('Title should contain an action verb (implement, create, fix, add, write, test, etc.)');
290
+ }
291
+ // Check for vague titles (vague words anywhere in title)
292
+ // Skip vague check for Unicode titles (non-ASCII characters)
293
+ const isVague = !isUnicodeTitle && VAGUE_PATTERNS.some(pattern => {
294
+ const patternLower = pattern.toLowerCase();
295
+ // Match if title is exactly the pattern, starts with it, or contains it as a word
296
+ return titleLower === patternLower ||
297
+ titleLower.startsWith(patternLower + ' ') ||
298
+ titleLower.includes(' ' + patternLower);
299
+ });
300
+ if (!isUnicodeTitle && isVague) {
301
+ violations.push('Title is too vague. Be specific about what the task does. Avoid words like "stuff", "things", "various", "etc", "fix", "update", "work on"');
302
+ }
303
+ // Check title isn't just a generic verb
304
+ // Skip length check for Unicode titles (non-ASCII characters)
305
+ if (!isUnicodeTitle && titleLower.length < 10) {
306
+ violations.push('Title is too short. Provide more context about what will be done (e.g., "Implement login endpoint" instead of just "Implement")');
307
+ }
308
+ // Check description length
309
+ if (taskData.description.trim().length < 50) {
310
+ violations.push('Description is too short (min 50 characters). Provide more detail about what this task does and how it will be accomplished.');
311
+ }
312
+ if (taskData.description.trim().length > 10000) {
313
+ violations.push('Description is too long (max 10000 characters). Consider splitting into smaller tasks.');
314
+ }
315
+ // Check success criteria count
316
+ if (taskData.success_criteria.length > 10) {
317
+ violations.push(`Too many success criteria (${taskData.success_criteria.length} > 10). This suggests the task is too large and should be split into smaller, focused tasks.`);
318
+ }
319
+ // Check deliverables count
320
+ if (taskData.deliverables.length > 5) {
321
+ violations.push(`Too many deliverables (${taskData.deliverables.length} > 5). This suggests the task is too large and should be split into smaller, focused tasks.`);
322
+ }
323
+ // Check if criteria are quantitative
324
+ const hasVagueCriteria = taskData.success_criteria.some(criterion => {
325
+ const textLower = criterion.text.toLowerCase();
326
+ return SUBJECTIVE_WORDS.some(word => textLower.includes(word));
327
+ });
328
+ if (hasVagueCriteria) {
329
+ violations.push('Success criteria must be quantitative and measurable. Avoid subjective terms like "good", "better", "proper", "clean", "fast", "efficient", "responsive". Instead use specific metrics: "returns 200 status", "completes in < 100ms", "passes all unit tests", "has 100% code coverage".');
330
+ }
331
+ // Check criteria aren't empty or just whitespace
332
+ const hasEmptyCriterion = taskData.success_criteria.some(c => c.text.trim().length === 0);
333
+ if (hasEmptyCriterion) {
334
+ violations.push('Success criteria cannot be empty or whitespace.');
335
+ }
336
+ // Check deliverables aren't empty or just whitespace
337
+ const hasEmptyDeliverable = taskData.deliverables.some(d => d.text.trim().length === 0);
338
+ if (hasEmptyDeliverable) {
339
+ violations.push('Deliverables cannot be empty or whitespace.');
340
+ }
341
+ // Check that all criteria and deliverables start with a verb or are specific
342
+ const hasVagueCriterion = taskData.success_criteria.some(c => {
343
+ const textLower = c.text.toLowerCase().trim();
344
+ return VAGUE_PATTERNS.some(pattern => textLower.startsWith(pattern));
345
+ });
346
+ if (hasVagueCriterion) {
347
+ violations.push('Success criteria must be specific. Avoid vague phrases like "works properly", "is correct", "functions as expected". Use specific outcomes: "endpoint returns 200 with valid JWT", "password is hashed with bcrypt", "test coverage is 100%".');
348
+ }
349
+ // Check that deliverables are specific files or outputs
350
+ const hasVagueDeliverable = taskData.deliverables.some(d => {
351
+ const textLower = d.text.toLowerCase().trim();
352
+ // Allow file paths or specific outputs
353
+ return (!textLower.includes('.') &&
354
+ !textLower.includes('file') &&
355
+ !textLower.includes('test') &&
356
+ !textLower.includes('component') &&
357
+ !textLower.includes('module') &&
358
+ !textLower.includes('class') &&
359
+ !textLower.includes('function') &&
360
+ !textLower.includes('endpoint') &&
361
+ !textLower.includes('api') &&
362
+ !textLower.includes('service') &&
363
+ textLower.split(' ').length < 3);
364
+ });
365
+ if (hasVagueDeliverable) {
366
+ violations.push('Deliverables must be specific. Include file paths (e.g., "src/auth/login.ts") or specific outputs (e.g., "POST /auth/login endpoint"). Avoid vague terms like "code", "implementation", "feature".');
367
+ }
368
+ if (violations.length > 0) {
369
+ throw new AtomicTaskViolationError(`Task "${taskData.title}" violates atomic task requirements.`, violations);
370
+ }
371
+ }
372
+ /**
373
+ * Valid status transitions for the new status model
374
+ *
375
+ * Most transitions are AUTOMATIC via calculateStatus():
376
+ * - ready → in_progress (when item checked or need_fix added)
377
+ * - in_progress → in_review (when all criteria + deliverables + need_fix complete)
378
+ * - ANY → blocked (when blocker added)
379
+ * - blocked → ready (when all blockers resolved)
380
+ * - in_review → in_progress (when need_fix added)
381
+ * - completed → in_progress (when need_fix added - regression)
382
+ *
383
+ * MANUAL TRANSITIONS:
384
+ * - in_review → completed (reviewer approval via approve() method)
385
+ * - in_progress → completed (backward compatibility, prefer approve() for new code)
386
+ */
387
+ const VALID_TRANSITIONS = {
388
+ ready: ['in_progress', 'blocked'],
389
+ in_progress: ['in_review', 'completed', 'blocked'],
390
+ in_review: ['completed', 'in_progress', 'blocked'],
391
+ completed: ['in_progress', 'blocked'],
392
+ blocked: ['ready'],
393
+ };
394
+ /**
395
+ * Task Node Class
396
+ *
397
+ * Represents a single task in the graph with:
398
+ * - Auto-managed timestamps (created_at, updated_at, completed_at)
399
+ * - Required field validation at creation
400
+ * - Atomic task validation
401
+ * - Status transition validation
402
+ * - Private setters to prevent manual timestamp manipulation
403
+ */
404
+ export class TaskNode {
405
+ // Public readonly properties (identity)
406
+ id;
407
+ // Public properties (can be modified through methods)
408
+ title;
409
+ description;
410
+ status;
411
+ priority;
412
+ success_criteria;
413
+ deliverables;
414
+ /** Blocking issues that must be resolved before review (equal importance to criteria/deliverables) */
415
+ need_fix;
416
+ /** Optional agent/session that owns this task (decoupled from status) */
417
+ assignee;
418
+ blockers;
419
+ /** Explanatory text describing WHY this task depends on its blockers */
420
+ dependencies;
421
+ sub_items;
422
+ related_files;
423
+ notes;
424
+ c7_verified;
425
+ // Auto-managed timestamps (private with public getters)
426
+ _created_at;
427
+ _updated_at;
428
+ _completed_at;
429
+ // Graph edges (outgoing)
430
+ edges;
431
+ /**
432
+ * Get the creation timestamp (immutable)
433
+ */
434
+ get created_at() {
435
+ return this._created_at;
436
+ }
437
+ /**
438
+ * Get the last update timestamp (auto-managed)
439
+ */
440
+ get updated_at() {
441
+ return this._updated_at;
442
+ }
443
+ /**
444
+ * Get the completion timestamp (auto-managed, null if not complete)
445
+ */
446
+ get completed_at() {
447
+ return this._completed_at;
448
+ }
449
+ /**
450
+ * Create a new TaskNode
451
+ *
452
+ * @param data - Task data (title, description, success_criteria, deliverables are REQUIRED)
453
+ * @param _skipAtomicValidation - Private: skip atomic validation (used only by fromJSON for deserialization)
454
+ * @throws {ValidationError} If required fields are missing or invalid
455
+ * @throws {AtomicTaskViolationError} If task violates atomic requirements
456
+ */
457
+ constructor(data) {
458
+ // Validate required fields
459
+ if (!data.title || data.title.trim().length === 0) {
460
+ throw new ValidationError('Title is required and cannot be empty or whitespace.', 'title');
461
+ }
462
+ if (data.title.length > 200) {
463
+ throw new ValidationError('Title must be 200 characters or less.', 'title');
464
+ }
465
+ if (!data.description || data.description.trim().length === 0) {
466
+ throw new ValidationError('Description is required and cannot be empty or whitespace.', 'description');
467
+ }
468
+ if (data.description.length < 50) {
469
+ throw new ValidationError('Description must be at least 50 characters.', 'description');
470
+ }
471
+ if (data.description.length > 10000) {
472
+ throw new ValidationError('Description must be 10000 characters or less.', 'description');
473
+ }
474
+ if (!data.success_criteria || data.success_criteria.length === 0) {
475
+ throw new ValidationError('At least one success criterion is required.', 'success_criteria');
476
+ }
477
+ if (data.success_criteria.length > 10) {
478
+ throw new ValidationError('Too many success criteria (max 10). Split into smaller tasks.', 'success_criteria');
479
+ }
480
+ if (!data.deliverables || data.deliverables.length === 0) {
481
+ throw new ValidationError('At least one deliverable is required.', 'deliverables');
482
+ }
483
+ if (data.deliverables.length > 5) {
484
+ throw new ValidationError('Too many deliverables (max 5). Split into smaller tasks.', 'deliverables');
485
+ }
486
+ // Validate atomic task requirements (skip for deserialization)
487
+ if (!data._skipAtomicValidation) {
488
+ validateAtomicTask({
489
+ title: data.title,
490
+ description: data.description,
491
+ success_criteria: data.success_criteria,
492
+ deliverables: data.deliverables,
493
+ });
494
+ }
495
+ // Set basic properties
496
+ this.id = data.id || uuidv4();
497
+ this.title = data.title.trim();
498
+ this.description = data.description.trim();
499
+ this.status = data.status || 'ready';
500
+ this.priority = data.priority || 'second';
501
+ this.success_criteria = [...(data.success_criteria || [])];
502
+ this.deliverables = [...(data.deliverables || [])];
503
+ this.need_fix = [...(data.need_fix || [])];
504
+ this.assignee = data.assignee ?? null;
505
+ this.blockers = [...(data.blockers || [])];
506
+ this.dependencies = data.dependencies || '';
507
+ this.sub_items = [...(data.sub_items || [])];
508
+ this.related_files = [...(data.related_files || [])];
509
+ this.notes = (data.notes || '').trim();
510
+ this.c7_verified = [...(data.c7_verified || [])];
511
+ this.edges = [...(data.edges || [])];
512
+ // Set timestamps (auto-managed, cannot be set via constructor)
513
+ const now = new Date().toISOString();
514
+ this._created_at = now; // Always set on creation
515
+ this._updated_at = now; // Initially same as created_at
516
+ this._completed_at = null; // Will be set by _checkCompletion()
517
+ // Calculate status based on blockers and work state (if not explicitly provided)
518
+ // This ensures tasks created with blockers get 'blocked' status
519
+ if (!data.status) {
520
+ this.status = this.calculateStatus();
521
+ }
522
+ // Check if task is already complete (for loaded tasks)
523
+ this._checkCompletion();
524
+ }
525
+ /**
526
+ * Update the task title
527
+ * @param title - New title
528
+ */
529
+ setTitle(title) {
530
+ if (title.trim().length === 0) {
531
+ throw new ValidationError('Title cannot be empty or whitespace.', 'title');
532
+ }
533
+ if (title.length > 200) {
534
+ throw new ValidationError('Title must be 200 characters or less.', 'title');
535
+ }
536
+ this.title = title.trim();
537
+ this._touch();
538
+ }
539
+ /**
540
+ * Update the task description
541
+ * @param description - New description
542
+ */
543
+ setDescription(description) {
544
+ if (description.trim().length < 50) {
545
+ throw new ValidationError('Description must be at least 50 characters.', 'description');
546
+ }
547
+ if (description.length > 10000) {
548
+ throw new ValidationError('Description must be 10000 characters or less.', 'description');
549
+ }
550
+ this.description = description.trim();
551
+ this._touch();
552
+ }
553
+ /**
554
+ * Update the task status
555
+ * @param status - New status
556
+ */
557
+ setStatus(status) {
558
+ // Allow setting to same status (no-op)
559
+ if (status === this.status) {
560
+ return;
561
+ }
562
+ // Validate transition
563
+ const allowedTransitions = VALID_TRANSITIONS[this.status];
564
+ if (!allowedTransitions.includes(status)) {
565
+ throw new ValidationError(`Invalid status transition from '${this.status}' to '${status}'. Allowed: ${allowedTransitions.join(', ')}`, 'status');
566
+ }
567
+ // When manually setting to completed, check all criteria are met FIRST
568
+ if (status === 'completed') {
569
+ const allComplete = this._isComplete();
570
+ if (!allComplete) {
571
+ throw new ValidationError('Cannot mark task as completed until all success criteria and deliverables are complete.', 'status');
572
+ }
573
+ }
574
+ // Now set the status
575
+ this.status = status;
576
+ this._touch();
577
+ }
578
+ /**
579
+ * Update the task priority
580
+ * @param priority - New priority
581
+ */
582
+ setPriority(priority) {
583
+ this.priority = priority;
584
+ this._touch();
585
+ }
586
+ /**
587
+ * Add a success criterion
588
+ * @param criterion - Success criterion to add
589
+ */
590
+ addSuccessCriterion(criterion) {
591
+ if (this.success_criteria.length >= 10) {
592
+ throw new ValidationError('Too many success criteria (max 10). Split into smaller tasks.', 'success_criteria');
593
+ }
594
+ this.success_criteria.push(criterion);
595
+ this._touch();
596
+ this._checkCompletion();
597
+ }
598
+ /**
599
+ * Mark a success criterion as complete
600
+ * @param criterionId - ID of the criterion to mark complete
601
+ */
602
+ completeCriterion(criterionId) {
603
+ const criterion = this.success_criteria.find(c => c.id === criterionId);
604
+ if (!criterion) {
605
+ throw new ValidationError(`Success criterion with ID '${criterionId}' not found.`, 'success_criteria');
606
+ }
607
+ criterion.completed = true;
608
+ criterion.completed_at = new Date().toISOString();
609
+ this._touch();
610
+ this._checkCompletion();
611
+ this.recalculateStatus(); // Auto-transition status based on state
612
+ }
613
+ /**
614
+ * Unmark a success criterion as complete
615
+ * @param criterionId - ID of the criterion to unmark
616
+ * @throws {ImmutabilityViolationError} If criterion is already completed (immutable)
617
+ */
618
+ uncompleteCriterion(criterionId) {
619
+ const criterion = this.success_criteria.find(c => c.id === criterionId);
620
+ if (!criterion) {
621
+ throw new ValidationError(`Success criterion with ID '${criterionId}' not found.`, 'success_criteria');
622
+ }
623
+ // Immutability check: cannot uncheck completed items
624
+ if (criterion.completed) {
625
+ throw new ImmutabilityViolationError(`Cannot uncheck completed success criterion '${criterion.text.substring(0, 50)}...'. Completed items are immutable.`, criterionId, 'success_criterion');
626
+ }
627
+ criterion.completed = false;
628
+ delete criterion.completed_at;
629
+ this._touch();
630
+ this._checkCompletion();
631
+ }
632
+ /**
633
+ * Add a deliverable
634
+ * @param deliverable - Deliverable to add
635
+ */
636
+ addDeliverable(deliverable) {
637
+ if (this.deliverables.length >= 5) {
638
+ throw new ValidationError('Too many deliverables (max 5). Split into smaller tasks.', 'deliverables');
639
+ }
640
+ this.deliverables.push(deliverable);
641
+ this._touch();
642
+ this._checkCompletion();
643
+ }
644
+ /**
645
+ * Mark a deliverable as complete
646
+ * @param deliverableId - ID of the deliverable to mark complete
647
+ */
648
+ completeDeliverable(deliverableId) {
649
+ const deliverable = this.deliverables.find(d => d.id === deliverableId);
650
+ if (!deliverable) {
651
+ throw new ValidationError(`Deliverable with ID '${deliverableId}' not found.`, 'deliverables');
652
+ }
653
+ deliverable.completed = true;
654
+ this._touch();
655
+ this._checkCompletion();
656
+ this.recalculateStatus(); // Auto-transition status based on state
657
+ }
658
+ /**
659
+ * Unmark a deliverable as complete
660
+ * @param deliverableId - ID of the deliverable to unmark
661
+ * @throws {ImmutabilityViolationError} If deliverable is already completed (immutable)
662
+ */
663
+ uncompleteDeliverable(deliverableId) {
664
+ const deliverable = this.deliverables.find(d => d.id === deliverableId);
665
+ if (!deliverable) {
666
+ throw new ValidationError(`Deliverable with ID '${deliverableId}' not found.`, 'deliverables');
667
+ }
668
+ // Immutability check: cannot uncheck completed items
669
+ if (deliverable.completed) {
670
+ throw new ImmutabilityViolationError(`Cannot uncheck completed deliverable '${deliverable.text.substring(0, 50)}...'. Completed items are immutable.`, deliverableId, 'deliverable');
671
+ }
672
+ deliverable.completed = false;
673
+ this._touch();
674
+ this._checkCompletion();
675
+ }
676
+ /**
677
+ * Add a need_fix item
678
+ * Need_fix items are blocking issues that must be resolved before review
679
+ * @param text - Description of what needs to be fixed
680
+ * @param options - Optional file_path and source
681
+ */
682
+ addNeedFix(text, options) {
683
+ const fixItem = {
684
+ id: uuidv4(),
685
+ text: text.trim(),
686
+ completed: false,
687
+ file_path: options?.file_path,
688
+ added_at: new Date().toISOString(),
689
+ source: options?.source,
690
+ };
691
+ this.need_fix.push(fixItem);
692
+ this._touch();
693
+ this._checkCompletion();
694
+ this.recalculateStatus(); // Auto-transition status based on state
695
+ }
696
+ /**
697
+ * Mark a need_fix item as complete
698
+ * @param fixId - ID of the need_fix item to mark complete
699
+ */
700
+ completeNeedFix(fixId) {
701
+ const fixItem = this.need_fix.find(f => f.id === fixId);
702
+ if (!fixItem) {
703
+ throw new ValidationError(`Need_fix item with ID '${fixId}' not found.`, 'need_fix');
704
+ }
705
+ fixItem.completed = true;
706
+ this._touch();
707
+ this._checkCompletion();
708
+ this.recalculateStatus(); // Auto-transition status based on state
709
+ }
710
+ /**
711
+ * Set the assignee for this task
712
+ * Assignee is decoupled from status - just a placeholder for future team management
713
+ * @param agentId - Agent/session ID, or null to clear
714
+ */
715
+ setAssignee(agentId) {
716
+ this.assignee = agentId;
717
+ this._touch();
718
+ }
719
+ /**
720
+ * Add a blocker task ID
721
+ * @param blockerId - Task ID that blocks this task
722
+ */
723
+ addBlocker(blockerId) {
724
+ if (!this.blockers.includes(blockerId)) {
725
+ this.blockers.push(blockerId);
726
+ this._touch();
727
+ this.recalculateStatus(); // Auto-transition to blocked (Rule 4)
728
+ }
729
+ }
730
+ /**
731
+ * Remove a blocker task ID
732
+ * @param blockerId - Task ID to remove from blockers
733
+ */
734
+ removeBlocker(blockerId) {
735
+ const index = this.blockers.indexOf(blockerId);
736
+ if (index > -1) {
737
+ this.blockers.splice(index, 1);
738
+ this._touch();
739
+ this.recalculateStatus(); // Auto-transition when all blockers resolved (Rule 5)
740
+ }
741
+ }
742
+ /**
743
+ * Set the dependencies explanation text (twin to blockers)
744
+ * @param explanation - Explanatory text describing WHY this task depends on its blockers
745
+ */
746
+ setDependencies(explanation) {
747
+ this.dependencies = explanation.trim();
748
+ this._touch();
749
+ }
750
+ /**
751
+ * Clear the dependencies explanation text
752
+ * Typically called when removing the last blocker
753
+ */
754
+ clearDependencies() {
755
+ this.dependencies = '';
756
+ this._touch();
757
+ }
758
+ /**
759
+ * Remove a success criterion
760
+ * @param criterionId - ID of the criterion to remove
761
+ * @throws {ValidationError} If criterion not found or removal would leave no criteria
762
+ * @throws {ImmutabilityViolationError} If criterion is completed (immutable)
763
+ */
764
+ removeSuccessCriterion(criterionId) {
765
+ const index = this.success_criteria.findIndex(c => c.id === criterionId);
766
+ if (index === -1) {
767
+ throw new ValidationError(`Success criterion with ID '${criterionId}' not found.`, 'success_criteria');
768
+ }
769
+ // Immutability check: cannot remove completed items
770
+ const criterion = this.success_criteria[index];
771
+ if (criterion.completed) {
772
+ throw new ImmutabilityViolationError(`Cannot remove completed success criterion '${criterion.text.substring(0, 50)}...'. Completed items are immutable.`, criterionId, 'success_criterion');
773
+ }
774
+ if (this.success_criteria.length <= 1) {
775
+ throw new ValidationError('Cannot remove the last success criterion. At least one is required.', 'success_criteria');
776
+ }
777
+ this.success_criteria.splice(index, 1);
778
+ this._touch();
779
+ this._checkCompletion();
780
+ }
781
+ /**
782
+ * Remove a deliverable
783
+ * @param deliverableId - ID of the deliverable to remove
784
+ * @throws {ValidationError} If deliverable not found or removal would leave no deliverables
785
+ * @throws {ImmutabilityViolationError} If deliverable is completed (immutable)
786
+ */
787
+ removeDeliverable(deliverableId) {
788
+ const index = this.deliverables.findIndex(d => d.id === deliverableId);
789
+ if (index === -1) {
790
+ throw new ValidationError(`Deliverable with ID '${deliverableId}' not found.`, 'deliverables');
791
+ }
792
+ // Immutability check: cannot remove completed items
793
+ const deliverable = this.deliverables[index];
794
+ if (deliverable.completed) {
795
+ throw new ImmutabilityViolationError(`Cannot remove completed deliverable '${deliverable.text.substring(0, 50)}...'. Completed items are immutable.`, deliverableId, 'deliverable');
796
+ }
797
+ if (this.deliverables.length <= 1) {
798
+ throw new ValidationError('Cannot remove the last deliverable. At least one is required.', 'deliverables');
799
+ }
800
+ this.deliverables.splice(index, 1);
801
+ this._touch();
802
+ this._checkCompletion();
803
+ }
804
+ /**
805
+ * Remove a related file path
806
+ * @param filePath - File path to remove
807
+ */
808
+ removeRelatedFile(filePath) {
809
+ const index = this.related_files.indexOf(filePath);
810
+ if (index > -1) {
811
+ this.related_files.splice(index, 1);
812
+ this._touch();
813
+ }
814
+ }
815
+ /**
816
+ * Remove a C7 verification entry
817
+ * @param libraryId - Library ID to remove from C7 verifications
818
+ */
819
+ removeC7Verification(libraryId) {
820
+ const index = this.c7_verified.findIndex(v => v.library_id === libraryId);
821
+ if (index > -1) {
822
+ this.c7_verified.splice(index, 1);
823
+ this._touch();
824
+ }
825
+ }
826
+ /**
827
+ * Add a related file path
828
+ * @param filePath - File path relative to project root
829
+ */
830
+ addRelatedFile(filePath) {
831
+ if (!this.related_files.includes(filePath)) {
832
+ this.related_files.push(filePath);
833
+ this._touch();
834
+ }
835
+ }
836
+ /**
837
+ * Append notes
838
+ * @param notes - Notes to append
839
+ */
840
+ appendNotes(notes) {
841
+ if (notes.trim().length > 0) {
842
+ if (this.notes.length > 0) {
843
+ this.notes += '\n\n';
844
+ }
845
+ this.notes += notes.trim();
846
+ this._touch();
847
+ }
848
+ }
849
+ /**
850
+ * Add C7 verification
851
+ * @param verification - C7 verification entry
852
+ */
853
+ addC7Verification(verification) {
854
+ this.c7_verified.push(verification);
855
+ this._touch();
856
+ }
857
+ /**
858
+ * Add an outgoing edge
859
+ * @param taskId - Task ID this task points to
860
+ */
861
+ addEdge(taskId) {
862
+ if (!this.edges.includes(taskId)) {
863
+ this.edges.push(taskId);
864
+ this._touch();
865
+ }
866
+ }
867
+ /**
868
+ * Remove an outgoing edge
869
+ * @param taskId - Task ID to remove from edges
870
+ */
871
+ removeEdge(taskId) {
872
+ const index = this.edges.indexOf(taskId);
873
+ if (index > -1) {
874
+ this.edges.splice(index, 1);
875
+ this._touch();
876
+ }
877
+ }
878
+ /**
879
+ * Add a sub-item task ID
880
+ * @param subItemId - Child task ID
881
+ */
882
+ addSubItem(subItemId) {
883
+ if (!this.sub_items.includes(subItemId)) {
884
+ this.sub_items.push(subItemId);
885
+ this._touch();
886
+ }
887
+ }
888
+ /**
889
+ * Remove a sub-item task ID
890
+ * @param subItemId - Child task ID to remove
891
+ */
892
+ removeSubItem(subItemId) {
893
+ const index = this.sub_items.indexOf(subItemId);
894
+ if (index > -1) {
895
+ this.sub_items.splice(index, 1);
896
+ this._touch();
897
+ }
898
+ }
899
+ /**
900
+ * Check if the task is complete (ready for review)
901
+ * All three must be complete: success_criteria, deliverables, and need_fix
902
+ * @returns True if all success criteria, deliverables, and need_fix items are complete
903
+ * @private
904
+ */
905
+ _isComplete() {
906
+ const allCriteriaComplete = this.success_criteria.every(c => c.completed);
907
+ const allDeliverablesComplete = this.deliverables.every(d => d.completed);
908
+ const allNeedFixComplete = this.need_fix.every(f => f.completed);
909
+ return allCriteriaComplete && allDeliverablesComplete && allNeedFixComplete;
910
+ }
911
+ /**
912
+ * Update completed_at timestamp and status based on completion state
913
+ * Called automatically after any change to success_criteria, deliverables, or need_fix
914
+ * @private
915
+ */
916
+ _checkCompletion() {
917
+ const isComplete = this._isComplete();
918
+ if (isComplete && !this._completed_at) {
919
+ // All complete and not yet set - set the timestamp
920
+ this._completed_at = new Date().toISOString();
921
+ }
922
+ else if (!isComplete && this._completed_at) {
923
+ // Not all complete but timestamp is set - clear it
924
+ this._completed_at = null;
925
+ // Auto-reset status from 'completed' or 'in_review' to 'in_progress' when task becomes incomplete
926
+ // This happens when a new criterion/deliverable/need_fix is added
927
+ if (this.status === 'completed' || this.status === 'in_review') {
928
+ this.status = 'in_progress';
929
+ }
930
+ }
931
+ }
932
+ /**
933
+ * Update the updated_at timestamp
934
+ * Called automatically after any field change
935
+ * @private
936
+ */
937
+ _touch() {
938
+ this._updated_at = new Date().toISOString();
939
+ }
940
+ /**
941
+ * Calculate the derived status based on task state
942
+ *
943
+ * Status is DERIVED from state, not set directly:
944
+ * Priority order: blocked > in_review > in_progress > ready
945
+ *
946
+ * Rules:
947
+ * 1. Has unresolved blockers → 'blocked'
948
+ * 2. All criteria + deliverables + need_fix complete → 'in_review'
949
+ * 3. Any item checked OR need_fix exists → 'in_progress'
950
+ * 4. Default → 'ready'
951
+ *
952
+ * NOTE: This is a pure calculation function. It does NOT modify status.
953
+ * Use recalculateStatus() to apply the calculated status.
954
+ *
955
+ * @returns The calculated status based on current task state
956
+ */
957
+ calculateStatus() {
958
+ // Rule 1: Check if blocked (highest priority)
959
+ // Note: This only checks if blockers exist, not if they're resolved
960
+ // The caller (graph) is responsible for checking blocker status
961
+ if (this.blockers.length > 0) {
962
+ return 'blocked';
963
+ }
964
+ // Rule 2: Check if ready for review (all items complete)
965
+ const allCriteriaComplete = this.success_criteria.every(c => c.completed);
966
+ const allDeliverablesComplete = this.deliverables.every(d => d.completed);
967
+ const allNeedFixComplete = this.need_fix.every(f => f.completed);
968
+ const allComplete = allCriteriaComplete && allDeliverablesComplete && allNeedFixComplete;
969
+ if (allComplete) {
970
+ return 'in_review';
971
+ }
972
+ // Rule 3: Check if work has started
973
+ const anyCriteriaChecked = this.success_criteria.some(c => c.completed);
974
+ const anyDeliverableChecked = this.deliverables.some(d => d.completed);
975
+ const hasNeedFix = this.need_fix.length > 0;
976
+ if (anyCriteriaChecked || anyDeliverableChecked || hasNeedFix) {
977
+ return 'in_progress';
978
+ }
979
+ // Rule 4: Default - ready for work
980
+ return 'ready';
981
+ }
982
+ /**
983
+ * Recalculate and update status based on task state
984
+ * This is the main method to call when task state changes
985
+ *
986
+ * Special cases:
987
+ * - completed tasks with new need_fix items → in_progress (regression)
988
+ * - completed tasks with new blockers → blocked (regression)
989
+ * - Otherwise, completed status is preserved (requires manual approve())
990
+ *
991
+ * @returns The new status (may be same as current)
992
+ */
993
+ recalculateStatus() {
994
+ const newStatus = this.calculateStatus();
995
+ // Special handling for completed tasks
996
+ if (this.status === 'completed') {
997
+ // Regression: need_fix added or blocker added → leave completed state
998
+ if (newStatus === 'in_progress' || newStatus === 'blocked') {
999
+ this.status = newStatus;
1000
+ this._touch();
1001
+ }
1002
+ // Otherwise stay completed (no change)
1003
+ return this.status;
1004
+ }
1005
+ // For non-completed tasks, always update to calculated status
1006
+ if (newStatus !== this.status) {
1007
+ this.status = newStatus;
1008
+ this._touch();
1009
+ }
1010
+ return this.status;
1011
+ }
1012
+ /**
1013
+ * Approve a task that is in review
1014
+ * This is the ONLY manual status transition in the new system
1015
+ *
1016
+ * @throws {ValidationError} If task is not in 'in_review' status
1017
+ */
1018
+ approve() {
1019
+ if (this.status !== 'in_review') {
1020
+ throw new ValidationError(`Cannot approve task in '${this.status}' status. Task must be in 'in_review' status.`, 'status');
1021
+ }
1022
+ this.status = 'completed';
1023
+ this._touch();
1024
+ }
1025
+ /**
1026
+ * Serialize the task node to plain object
1027
+ * Useful for JSON serialization
1028
+ */
1029
+ toJSON() {
1030
+ return {
1031
+ id: this.id,
1032
+ title: this.title,
1033
+ description: this.description,
1034
+ status: this.status,
1035
+ priority: this.priority,
1036
+ success_criteria: this.success_criteria,
1037
+ deliverables: this.deliverables,
1038
+ need_fix: this.need_fix,
1039
+ assignee: this.assignee,
1040
+ blockers: this.blockers,
1041
+ dependencies: this.dependencies,
1042
+ sub_items: this.sub_items,
1043
+ related_files: this.related_files,
1044
+ notes: this.notes,
1045
+ c7_verified: this.c7_verified,
1046
+ created_at: this._created_at,
1047
+ updated_at: this._updated_at,
1048
+ completed_at: this._completed_at,
1049
+ edges: this.edges,
1050
+ };
1051
+ }
1052
+ /**
1053
+ * Create a TaskNode from plain object
1054
+ * Useful for JSON deserialization
1055
+ * Skips atomic validation since task was already validated when created
1056
+ */
1057
+ static fromJSON(data) {
1058
+ // Migrate old statuses to new ones (handle legacy data with old status values)
1059
+ const statusStr = data.status;
1060
+ let migratedStatus = data.status;
1061
+ if (statusStr === 'not_started' || statusStr === 'pending') {
1062
+ migratedStatus = 'ready';
1063
+ }
1064
+ const node = new TaskNode({
1065
+ id: data.id,
1066
+ title: data.title,
1067
+ description: data.description,
1068
+ status: migratedStatus,
1069
+ priority: data.priority,
1070
+ success_criteria: data.success_criteria,
1071
+ deliverables: data.deliverables,
1072
+ need_fix: data.need_fix || [], // Default to empty array for legacy data
1073
+ assignee: data.assignee ?? null, // Default to null for legacy data
1074
+ blockers: data.blockers,
1075
+ dependencies: data.dependencies,
1076
+ sub_items: data.sub_items,
1077
+ related_files: data.related_files,
1078
+ notes: data.notes,
1079
+ c7_verified: data.c7_verified,
1080
+ edges: data.edges,
1081
+ _skipAtomicValidation: true, // Skip validation for deserialized tasks
1082
+ });
1083
+ // Preserve timestamps from loaded data
1084
+ node._created_at = data.created_at;
1085
+ node._updated_at = data.updated_at;
1086
+ node._completed_at = data.completed_at;
1087
+ return node;
1088
+ }
1089
+ }
1090
+ //# sourceMappingURL=task-node.js.map