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.
- package/README.md +523 -0
- package/dist/cli/commands/approve.d.ts +27 -0
- package/dist/cli/commands/approve.d.ts.map +1 -0
- package/dist/cli/commands/approve.js +119 -0
- package/dist/cli/commands/approve.js.map +1 -0
- package/dist/cli/commands/batch.d.ts +15 -0
- package/dist/cli/commands/batch.d.ts.map +1 -0
- package/dist/cli/commands/batch.js +521 -0
- package/dist/cli/commands/batch.js.map +1 -0
- package/dist/cli/commands/create.d.ts +9 -0
- package/dist/cli/commands/create.d.ts.map +1 -0
- package/dist/cli/commands/create.js +321 -0
- package/dist/cli/commands/create.js.map +1 -0
- package/dist/cli/commands/delete.d.ts +9 -0
- package/dist/cli/commands/delete.d.ts.map +1 -0
- package/dist/cli/commands/delete.js +143 -0
- package/dist/cli/commands/delete.js.map +1 -0
- package/dist/cli/commands/export.d.ts +9 -0
- package/dist/cli/commands/export.d.ts.map +1 -0
- package/dist/cli/commands/export.js +66 -0
- package/dist/cli/commands/export.js.map +1 -0
- package/dist/cli/commands/find.d.ts +16 -0
- package/dist/cli/commands/find.d.ts.map +1 -0
- package/dist/cli/commands/find.js +252 -0
- package/dist/cli/commands/find.js.map +1 -0
- package/dist/cli/commands/get.d.ts +9 -0
- package/dist/cli/commands/get.d.ts.map +1 -0
- package/dist/cli/commands/get.js +74 -0
- package/dist/cli/commands/get.js.map +1 -0
- package/dist/cli/commands/graph.d.ts +9 -0
- package/dist/cli/commands/graph.d.ts.map +1 -0
- package/dist/cli/commands/graph.js +200 -0
- package/dist/cli/commands/graph.js.map +1 -0
- package/dist/cli/commands/import.d.ts +9 -0
- package/dist/cli/commands/import.d.ts.map +1 -0
- package/dist/cli/commands/import.js +807 -0
- package/dist/cli/commands/import.js.map +1 -0
- package/dist/cli/commands/init.d.ts +9 -0
- package/dist/cli/commands/init.d.ts.map +1 -0
- package/dist/cli/commands/init.js +57 -0
- package/dist/cli/commands/init.js.map +1 -0
- package/dist/cli/commands/list.d.ts +9 -0
- package/dist/cli/commands/list.d.ts.map +1 -0
- package/dist/cli/commands/list.js +175 -0
- package/dist/cli/commands/list.js.map +1 -0
- package/dist/cli/commands/merge.d.ts +9 -0
- package/dist/cli/commands/merge.d.ts.map +1 -0
- package/dist/cli/commands/merge.js +113 -0
- package/dist/cli/commands/merge.js.map +1 -0
- package/dist/cli/commands/serve.d.ts +9 -0
- package/dist/cli/commands/serve.d.ts.map +1 -0
- package/dist/cli/commands/serve.js +94 -0
- package/dist/cli/commands/serve.js.map +1 -0
- package/dist/cli/commands/update.d.ts +9 -0
- package/dist/cli/commands/update.d.ts.map +1 -0
- package/dist/cli/commands/update.js +423 -0
- package/dist/cli/commands/update.js.map +1 -0
- package/dist/cli/commands/wire.d.ts +15 -0
- package/dist/cli/commands/wire.d.ts.map +1 -0
- package/dist/cli/commands/wire.js +164 -0
- package/dist/cli/commands/wire.js.map +1 -0
- package/dist/cli/index.d.ts +7 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +100 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/output/json.d.ts +16 -0
- package/dist/cli/output/json.d.ts.map +1 -0
- package/dist/cli/output/json.js +29 -0
- package/dist/cli/output/json.js.map +1 -0
- package/dist/cli/output/markdown.d.ts +15 -0
- package/dist/cli/output/markdown.d.ts.map +1 -0
- package/dist/cli/output/markdown.js +206 -0
- package/dist/cli/output/markdown.js.map +1 -0
- package/dist/cli/output/table.d.ts +23 -0
- package/dist/cli/output/table.d.ts.map +1 -0
- package/dist/cli/output/table.js +150 -0
- package/dist/cli/output/table.js.map +1 -0
- package/dist/cli/utils/helpers.d.ts +126 -0
- package/dist/cli/utils/helpers.d.ts.map +1 -0
- package/dist/cli/utils/helpers.js +325 -0
- package/dist/cli/utils/helpers.js.map +1 -0
- package/dist/core/graph/algorithms.d.ts +11 -0
- package/dist/core/graph/algorithms.d.ts.map +1 -0
- package/dist/core/graph/algorithms.js +14 -0
- package/dist/core/graph/algorithms.js.map +1 -0
- package/dist/core/graph/cycle.d.ts +155 -0
- package/dist/core/graph/cycle.d.ts.map +1 -0
- package/dist/core/graph/cycle.js +297 -0
- package/dist/core/graph/cycle.js.map +1 -0
- package/dist/core/graph/index.d.ts +223 -0
- package/dist/core/graph/index.d.ts.map +1 -0
- package/dist/core/graph/index.js +475 -0
- package/dist/core/graph/index.js.map +1 -0
- package/dist/core/graph/operations.d.ts +240 -0
- package/dist/core/graph/operations.d.ts.map +1 -0
- package/dist/core/graph/operations.js +503 -0
- package/dist/core/graph/operations.js.map +1 -0
- package/dist/core/graph/sort.d.ts +76 -0
- package/dist/core/graph/sort.d.ts.map +1 -0
- package/dist/core/graph/sort.js +254 -0
- package/dist/core/graph/sort.js.map +1 -0
- package/dist/core/graph/traversal.d.ts +122 -0
- package/dist/core/graph/traversal.d.ts.map +1 -0
- package/dist/core/graph/traversal.js +336 -0
- package/dist/core/graph/traversal.js.map +1 -0
- package/dist/core/models/task-node.d.ts +328 -0
- package/dist/core/models/task-node.d.ts.map +1 -0
- package/dist/core/models/task-node.js +1090 -0
- package/dist/core/models/task-node.js.map +1 -0
- package/dist/core/registry/index.d.ts +102 -0
- package/dist/core/registry/index.d.ts.map +1 -0
- package/dist/core/registry/index.js +249 -0
- package/dist/core/registry/index.js.map +1 -0
- package/dist/core/registry/root-guard.d.ts +19 -0
- package/dist/core/registry/root-guard.d.ts.map +1 -0
- package/dist/core/registry/root-guard.js +28 -0
- package/dist/core/registry/root-guard.js.map +1 -0
- package/dist/core/storage/atomic-write.d.ts +181 -0
- package/dist/core/storage/atomic-write.d.ts.map +1 -0
- package/dist/core/storage/atomic-write.js +379 -0
- package/dist/core/storage/atomic-write.js.map +1 -0
- package/dist/core/storage/file-store.d.ts +148 -0
- package/dist/core/storage/file-store.d.ts.map +1 -0
- package/dist/core/storage/file-store.js +423 -0
- package/dist/core/storage/file-store.js.map +1 -0
- package/dist/core/storage/indexer.d.ts +138 -0
- package/dist/core/storage/indexer.d.ts.map +1 -0
- package/dist/core/storage/indexer.js +350 -0
- package/dist/core/storage/indexer.js.map +1 -0
- package/dist/core/utils/status-helpers.d.ts +59 -0
- package/dist/core/utils/status-helpers.d.ts.map +1 -0
- package/dist/core/utils/status-helpers.js +149 -0
- package/dist/core/utils/status-helpers.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -0
- package/dist/types/index.d.ts +504 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +182 -0
- package/dist/types/index.js.map +1 -0
- package/dist/web/routes/graph.d.ts +17 -0
- package/dist/web/routes/graph.d.ts.map +1 -0
- package/dist/web/routes/graph.js +277 -0
- package/dist/web/routes/graph.js.map +1 -0
- package/dist/web/routes/projects.d.ts +14 -0
- package/dist/web/routes/projects.d.ts.map +1 -0
- package/dist/web/routes/projects.js +102 -0
- package/dist/web/routes/projects.js.map +1 -0
- package/dist/web/routes/tasks.d.ts +17 -0
- package/dist/web/routes/tasks.d.ts.map +1 -0
- package/dist/web/routes/tasks.js +538 -0
- package/dist/web/routes/tasks.js.map +1 -0
- package/dist/web/server.d.ts +121 -0
- package/dist/web/server.d.ts.map +1 -0
- package/dist/web/server.js +389 -0
- package/dist/web/server.js.map +1 -0
- package/dist/web-ui/assets/index-BB0qvF1y.css +1 -0
- package/dist/web-ui/assets/index-Vmm72oKY.js +34 -0
- package/dist/web-ui/index.html +14 -0
- package/dist/web-ui/vite.svg +1 -0
- 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
|