@xirang-ai/dsh-teamflow 0.1.1 → 0.1.2
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.en.md +15 -11
- package/README.md +15 -11
- package/RELEASE.md +6 -6
- package/cordis.patch.yml +2 -0
- package/lib/client.js +179 -38
- package/lib/index.d.ts +7 -1
- package/lib/index.js +10 -4
- package/lib/remote/contract.d.ts +34 -1
- package/lib/remote/contract.js +16 -0
- package/lib/remote/service.d.ts +15 -2
- package/lib/remote/service.js +49 -6
- package/lib/requirement-discussion.d.ts +23 -0
- package/lib/requirement-discussion.js +26 -0
- package/lib/store/db.d.ts +8 -3
- package/lib/store/db.js +34 -2
- package/lib/store/docs.d.ts +9 -11
- package/lib/store/docs.js +25 -52
- package/lib/store/model.d.ts +23 -0
- package/lib/store/model.js +4 -0
- package/lib/store/paths.d.ts +20 -0
- package/lib/store/paths.js +54 -1
- package/lib/store/schema.js +71 -14
- package/lib/store/workflow.d.ts +50 -25
- package/lib/store/workflow.js +210 -97
- package/lib/tools/get-bug.js +1 -1
- package/lib/tools/get-design-doc.js +1 -1
- package/lib/tools/get-requirement.js +1 -1
- package/lib/tools/get-story.js +1 -1
- package/lib/tools/index.js +7 -0
- package/lib/tools/link-session.d.ts +3 -0
- package/lib/tools/link-session.js +87 -0
- package/locale/en.json +2 -0
- package/locale/zh.json +2 -0
- package/package.json +1 -1
package/lib/store/workflow.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { isPermissionTier } from "../permission-tier.js";
|
|
2
|
-
import { docPath, dropStagedDoc,
|
|
2
|
+
import { docPath, dropStagedDoc, pruneEmptyDirectories, readDoc, restoreStagedDoc, stageDocDeletion, writeDoc, } from "./docs.js";
|
|
3
3
|
import { newId } from "./ids.js";
|
|
4
4
|
import { BUG_SEVERITIES, BUG_STATUSES, DELETABLE_KINDS, DOC_KINDS, REQUIREMENT_STATUSES, RUN_STATUSES, STORY_SIZES, STORY_STATUSES, WorkflowError, } from "./model.js";
|
|
5
5
|
import { describeError } from "../errors.js";
|
|
@@ -69,9 +69,20 @@ function toDesignDoc(row) {
|
|
|
69
69
|
updatedAt: text(row, 'updated_at'),
|
|
70
70
|
};
|
|
71
71
|
}
|
|
72
|
+
function toDiscussionSession(row) {
|
|
73
|
+
return {
|
|
74
|
+
subjectType: text(row, 'subject_type'),
|
|
75
|
+
subjectId: text(row, 'subject_id'),
|
|
76
|
+
sessionId: text(row, 'session_id'),
|
|
77
|
+
kind: text(row, 'kind'),
|
|
78
|
+
preparedAt: nullableText(row, 'prepared_at'),
|
|
79
|
+
linkedAt: nullableText(row, 'linked_at'),
|
|
80
|
+
createdAt: text(row, 'created_at'),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
72
83
|
function toRequirementSession(row) {
|
|
73
84
|
return {
|
|
74
|
-
requirementId: text(row, '
|
|
85
|
+
requirementId: text(row, 'subject_id'),
|
|
75
86
|
sessionId: text(row, 'session_id'),
|
|
76
87
|
kind: text(row, 'kind'),
|
|
77
88
|
preparedAt: nullableText(row, 'prepared_at'),
|
|
@@ -195,68 +206,55 @@ export class WorkflowStore {
|
|
|
195
206
|
#docsRoot;
|
|
196
207
|
#workspaceRoot;
|
|
197
208
|
#warn;
|
|
198
|
-
constructor(db, options
|
|
209
|
+
constructor(db, options) {
|
|
199
210
|
this.#db = db;
|
|
200
211
|
this.#now = options.now ?? (() => new Date().toISOString());
|
|
201
|
-
this.#docsRoot = options.docsRoot
|
|
212
|
+
this.#docsRoot = options.docsRoot;
|
|
202
213
|
this.#workspaceRoot = options.workspaceRoot ?? (() => undefined);
|
|
203
214
|
this.#warn = options.warn ?? (() => { });
|
|
204
215
|
}
|
|
205
216
|
/**
|
|
206
|
-
* Remember where a workspace lives on disk.
|
|
207
|
-
*
|
|
208
|
-
* repeated calls with the same path are no-ops.
|
|
217
|
+
* Remember where a workspace lives on disk. Document bodies no longer live
|
|
218
|
+
* there, but the path is still recorded for display and for future
|
|
219
|
+
* workspace-scoped features; repeated calls with the same path are no-ops.
|
|
209
220
|
*/
|
|
210
221
|
registerWorkspace(workspaceId, path) {
|
|
211
222
|
const id = requiredText(workspaceId, 'workspaceId');
|
|
212
223
|
const root = requiredText(path, 'path');
|
|
213
|
-
ensureDocTree(root);
|
|
214
224
|
const now = this.#now();
|
|
215
225
|
this.#db
|
|
216
226
|
.prepare(`INSERT INTO workspaces (workspace_id, path, created_at, updated_at) VALUES (?, ?, ?, ?)
|
|
217
227
|
ON CONFLICT(workspace_id) DO UPDATE SET path = excluded.path, updated_at = excluded.updated_at`)
|
|
218
228
|
.run(id, root, now, now);
|
|
219
229
|
}
|
|
220
|
-
/** Directory a workspace
|
|
230
|
+
/** Directory a registered workspace lives in, or null when unknown. */
|
|
221
231
|
workspacePath(workspaceId) {
|
|
222
232
|
const row = this.#db.prepare('SELECT path FROM workspaces WHERE workspace_id = ?').get(workspaceId);
|
|
223
233
|
if (row !== undefined)
|
|
224
234
|
return text(row, 'path');
|
|
225
|
-
return this.#workspaceRoot(workspaceId) ??
|
|
235
|
+
return this.#workspaceRoot(workspaceId) ?? null;
|
|
226
236
|
}
|
|
227
237
|
/**
|
|
228
|
-
* Read a document body by its
|
|
238
|
+
* Read a document body by its document-root-relative path.
|
|
229
239
|
*
|
|
230
240
|
* `null` means the file does not exist; `''` means it exists and is empty.
|
|
231
|
-
*
|
|
232
|
-
*
|
|
233
|
-
* either empty result.
|
|
241
|
+
* Every record resolves against the one configured document root, so a
|
|
242
|
+
* legacy path simply misses instead of falling back to a workspace.
|
|
234
243
|
*/
|
|
235
|
-
readContent(
|
|
236
|
-
|
|
237
|
-
if (root === null) {
|
|
238
|
-
throw new WorkflowError('file_error', `workspace ${workspaceId} has no directory yet; call registerWorkspace first`);
|
|
239
|
-
}
|
|
240
|
-
return readDoc(root, relativePath);
|
|
244
|
+
readContent(relativePath) {
|
|
245
|
+
return readDoc(this.#docsRoot, relativePath);
|
|
241
246
|
}
|
|
242
247
|
/** Rewrite a document body; the path comes from the row, so it is always current. */
|
|
243
|
-
writeContent(
|
|
244
|
-
writeDoc(this.#
|
|
245
|
-
}
|
|
246
|
-
#root(workspaceId) {
|
|
247
|
-
const root = this.workspacePath(workspaceId);
|
|
248
|
-
if (root === null) {
|
|
249
|
-
throw new WorkflowError('invalid_input', `workspace ${workspaceId} has no directory yet; call registerWorkspace first`);
|
|
250
|
-
}
|
|
251
|
-
return root;
|
|
248
|
+
writeContent(relativePath, body) {
|
|
249
|
+
writeDoc(this.#docsRoot, relativePath, body);
|
|
252
250
|
}
|
|
253
251
|
/**
|
|
254
252
|
* Write one entity's body to its canonical path and return that path.
|
|
255
253
|
* Empty bodies still create the file so the address in the database resolves.
|
|
256
254
|
*/
|
|
257
|
-
#persistDoc(kind,
|
|
255
|
+
#persistDoc(kind, ids, body) {
|
|
258
256
|
const relative = docPath(kind, ids);
|
|
259
|
-
writeDoc(this.#
|
|
257
|
+
writeDoc(this.#docsRoot, relative, body);
|
|
260
258
|
return relative;
|
|
261
259
|
}
|
|
262
260
|
// ---------------------------------------------------------------- queries
|
|
@@ -284,47 +282,121 @@ export class WorkflowStore {
|
|
|
284
282
|
/**
|
|
285
283
|
* Register a browser-created session as a pending discussion for a requirement.
|
|
286
284
|
*
|
|
285
|
+
* Kept as a thin wrapper over {@link prepareDiscussionSession} so the
|
|
286
|
+
* requirement page and its tests keep their existing shape.
|
|
287
|
+
*/
|
|
288
|
+
prepareRequirementSession(requirementId, sessionId, kind = 'design') {
|
|
289
|
+
const session = this.prepareDiscussionSession('requirement', requirementId, sessionId, kind);
|
|
290
|
+
return {
|
|
291
|
+
requirementId: session.subjectId,
|
|
292
|
+
sessionId: session.sessionId,
|
|
293
|
+
kind: session.kind,
|
|
294
|
+
preparedAt: session.preparedAt,
|
|
295
|
+
linkedAt: session.linkedAt,
|
|
296
|
+
createdAt: session.createdAt,
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Register a browser-created session as a pending discussion for a subject.
|
|
301
|
+
*
|
|
287
302
|
* The browser creates the session first, then calls this before the user can
|
|
288
303
|
* send the first message. The row stays invisible in `listBoard` until the
|
|
289
|
-
* Agent confirms it through
|
|
304
|
+
* Agent confirms it through `teamflow_link_session`; this method never writes
|
|
305
|
+
* `linked_at`, so the client cannot skip Agent confirmation.
|
|
290
306
|
*/
|
|
291
|
-
|
|
307
|
+
prepareDiscussionSession(subjectType, subjectId, sessionId, kind) {
|
|
308
|
+
if (subjectType !== 'requirement' && subjectType !== 'story' && subjectType !== 'bug') {
|
|
309
|
+
throw new WorkflowError('invalid_input', `prepareDiscussionSession 参数非法:subjectType=${String(subjectType)},期望 requirement|story|bug`);
|
|
310
|
+
}
|
|
311
|
+
const subject = (subjectId ?? '').trim();
|
|
312
|
+
if (subject.length === 0) {
|
|
313
|
+
throw new WorkflowError('invalid_input', 'prepareDiscussionSession 参数非法:subjectId 为空');
|
|
314
|
+
}
|
|
315
|
+
const session = (sessionId ?? '').trim();
|
|
316
|
+
if (session.length === 0) {
|
|
317
|
+
throw new WorkflowError('invalid_input', 'prepareDiscussionSession 参数非法:sessionId 为空');
|
|
318
|
+
}
|
|
319
|
+
const discussionKind = (kind ?? '').trim();
|
|
320
|
+
if (discussionKind.length === 0) {
|
|
321
|
+
throw new WorkflowError('invalid_input', 'prepareDiscussionSession 参数非法:kind 为空');
|
|
322
|
+
}
|
|
323
|
+
if (subjectType === 'requirement' &&
|
|
324
|
+
discussionKind !== 'design' &&
|
|
325
|
+
discussionKind !== 'detail' &&
|
|
326
|
+
discussionKind !== 'split') {
|
|
327
|
+
throw new WorkflowError('invalid_input', `kind must be design, detail or split, got ${String(kind)}`);
|
|
328
|
+
}
|
|
292
329
|
return withTransaction(this.#db, () => {
|
|
293
|
-
|
|
294
|
-
const
|
|
295
|
-
|
|
296
|
-
|
|
330
|
+
this.#requireSubject(subjectType, subject);
|
|
331
|
+
const rows = this.#db
|
|
332
|
+
.prepare('SELECT * FROM discussion_sessions WHERE session_id = ?')
|
|
333
|
+
.all(session);
|
|
334
|
+
if (rows.length > 1) {
|
|
335
|
+
throw new WorkflowError('invalid_input', `session ${session} is already bound to ${rows.length} discussions`);
|
|
297
336
|
}
|
|
298
|
-
const existing =
|
|
299
|
-
.prepare('SELECT * FROM requirement_sessions WHERE session_id = ?')
|
|
300
|
-
.get(session);
|
|
337
|
+
const existing = rows[0];
|
|
301
338
|
if (existing !== undefined) {
|
|
302
|
-
const row =
|
|
303
|
-
if (row.
|
|
304
|
-
throw new WorkflowError('invalid_input', `session ${session} already belongs to
|
|
339
|
+
const row = toDiscussionSession(existing);
|
|
340
|
+
if (row.subjectType !== subjectType || row.subjectId !== subject) {
|
|
341
|
+
throw new WorkflowError('invalid_input', `session ${session} already belongs to ${row.subjectType} ${row.subjectId}`);
|
|
305
342
|
}
|
|
306
343
|
if (row.kind === 'create') {
|
|
307
344
|
throw new WorkflowError('invalid_input', `session ${session} created its requirement, so it cannot also be a discussion`);
|
|
308
345
|
}
|
|
309
|
-
if (row.kind !==
|
|
346
|
+
if (row.kind !== discussionKind) {
|
|
310
347
|
throw new WorkflowError('invalid_input', `session ${session} is already prepared as a ${row.kind} discussion`);
|
|
311
348
|
}
|
|
349
|
+
// Repeated prepares keep the original timestamp; only the first write
|
|
350
|
+
// records when the browser started the conversation.
|
|
312
351
|
return row;
|
|
313
352
|
}
|
|
314
353
|
const now = this.#now();
|
|
315
354
|
this.#db
|
|
316
|
-
.prepare(`INSERT INTO
|
|
317
|
-
(
|
|
318
|
-
VALUES (?, ?, ?, ?, NULL, ?)
|
|
319
|
-
|
|
320
|
-
|
|
355
|
+
.prepare(`INSERT INTO discussion_sessions
|
|
356
|
+
(subject_type, subject_id, session_id, kind, prepared_at, linked_at, created_at)
|
|
357
|
+
VALUES (?, ?, ?, ?, ?, NULL, ?)
|
|
358
|
+
ON CONFLICT (subject_type, subject_id, session_id)
|
|
359
|
+
DO UPDATE SET prepared_at = excluded.prepared_at`)
|
|
360
|
+
.run(subjectType, subject, session, discussionKind, now, now);
|
|
361
|
+
return this.#requireDiscussionSession(session, subjectType, subject);
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
/**
|
|
365
|
+
* Confirm the current session as the discussion it was prepared for.
|
|
366
|
+
*
|
|
367
|
+
* Called by `teamflow_link_session`; repeated calls are idempotent so a model
|
|
368
|
+
* retry cannot rewrite the confirmation time.
|
|
369
|
+
*/
|
|
370
|
+
linkDiscussionSession(sessionId) {
|
|
371
|
+
return withTransaction(this.#db, () => {
|
|
372
|
+
const session = requiredText(sessionId, 'sessionId');
|
|
373
|
+
const rows = this.#db
|
|
374
|
+
.prepare('SELECT * FROM discussion_sessions WHERE session_id = ?')
|
|
375
|
+
.all(session);
|
|
376
|
+
if (rows.length === 0) {
|
|
377
|
+
throw new WorkflowError('not_found', `teamflow_link_session 未能登记:当前会话 ${session} 没有与任何需求、故事或缺陷绑定`);
|
|
378
|
+
}
|
|
379
|
+
if (rows.length > 1) {
|
|
380
|
+
throw new WorkflowError('invalid_input', `teamflow_link_session 未能登记:数据异常:当前会话 ${session} 绑定到 ${rows.length} 个对象`);
|
|
381
|
+
}
|
|
382
|
+
const current = toDiscussionSession(rows[0]);
|
|
383
|
+
if (current.kind === 'create') {
|
|
384
|
+
throw new WorkflowError('invalid_input', `session ${session} created a requirement and is not a discussion`);
|
|
385
|
+
}
|
|
386
|
+
if (current.linkedAt !== null)
|
|
387
|
+
return current;
|
|
388
|
+
this.#db
|
|
389
|
+
.prepare(`UPDATE discussion_sessions SET linked_at = ?
|
|
390
|
+
WHERE subject_type = ? AND subject_id = ? AND session_id = ?`)
|
|
391
|
+
.run(this.#now(), current.subjectType, current.subjectId, session);
|
|
392
|
+
return this.#requireDiscussionSession(session, current.subjectType, current.subjectId);
|
|
321
393
|
});
|
|
322
394
|
}
|
|
323
395
|
/**
|
|
324
|
-
* Confirm the current session as a
|
|
396
|
+
* Confirm the current session as a requirement discussion.
|
|
325
397
|
*
|
|
326
|
-
*
|
|
327
|
-
*
|
|
398
|
+
* Kept for `teamflow_link_requirement_session`; new callers use
|
|
399
|
+
* {@link linkDiscussionSession}.
|
|
328
400
|
*/
|
|
329
401
|
linkRequirementSession(sessionId) {
|
|
330
402
|
return withTransaction(this.#db, () => {
|
|
@@ -336,36 +408,64 @@ export class WorkflowStore {
|
|
|
336
408
|
if (current.linkedAt !== null)
|
|
337
409
|
return current;
|
|
338
410
|
this.#db
|
|
339
|
-
.prepare("UPDATE
|
|
411
|
+
.prepare("UPDATE discussion_sessions SET linked_at = ? WHERE subject_type = 'requirement' AND session_id = ? AND kind IN ('design','detail','split')")
|
|
340
412
|
.run(this.#now(), session);
|
|
341
413
|
return this.#requireRequirementSession(session);
|
|
342
414
|
});
|
|
343
415
|
}
|
|
344
|
-
/** Pending or linked conversation prepared for one session, or null. */
|
|
416
|
+
/** Pending or linked requirement conversation prepared for one session, or null. */
|
|
345
417
|
getRequirementSession(sessionId) {
|
|
346
418
|
const row = this.#db
|
|
347
|
-
.prepare(
|
|
419
|
+
.prepare("SELECT * FROM discussion_sessions WHERE subject_type = 'requirement' AND session_id = ?")
|
|
348
420
|
.get(sessionId);
|
|
349
421
|
return row === undefined ? null : toRequirementSession(row);
|
|
350
422
|
}
|
|
351
423
|
/** The requirement linked to a session, whichever kind of link it is. */
|
|
352
424
|
getRequirementBySession(sessionId) {
|
|
353
425
|
const row = this.#db
|
|
354
|
-
.prepare(
|
|
426
|
+
.prepare("SELECT subject_id FROM discussion_sessions WHERE subject_type = 'requirement' AND session_id = ?")
|
|
355
427
|
.get(sessionId);
|
|
356
|
-
return row === undefined ? null : this.getRequirement(text(row, '
|
|
428
|
+
return row === undefined ? null : this.getRequirement(text(row, 'subject_id'));
|
|
429
|
+
}
|
|
430
|
+
/** The discussion row bound to one session, or null when none exists. */
|
|
431
|
+
getDiscussionSession(sessionId) {
|
|
432
|
+
const rows = this.#db
|
|
433
|
+
.prepare('SELECT * FROM discussion_sessions WHERE session_id = ?')
|
|
434
|
+
.all(sessionId);
|
|
435
|
+
return rows.length === 1 ? toDiscussionSession(rows[0]) : null;
|
|
357
436
|
}
|
|
358
437
|
/**
|
|
359
|
-
*
|
|
438
|
+
* Discussions under one subject, newest linked first.
|
|
360
439
|
*
|
|
361
|
-
* Pending
|
|
362
|
-
*
|
|
440
|
+
* Pending rows are excluded by default so the workbench never shows a session
|
|
441
|
+
* the Agent has not started; tests and diagnostics can opt in.
|
|
442
|
+
*/
|
|
443
|
+
listDiscussionSessions(subjectType, subjectId, options = {}) {
|
|
444
|
+
const values = [subjectType, subjectId];
|
|
445
|
+
let kindsClause = '';
|
|
446
|
+
if (options.kinds !== undefined && options.kinds.length > 0) {
|
|
447
|
+
kindsClause = ` AND kind IN (${options.kinds.map(() => '?').join(',')})`;
|
|
448
|
+
values.push(...options.kinds);
|
|
449
|
+
}
|
|
450
|
+
const rows = this.#db
|
|
451
|
+
.prepare(`SELECT * FROM discussion_sessions
|
|
452
|
+
WHERE subject_type = ? AND subject_id = ?
|
|
453
|
+
AND (linked_at IS NOT NULL OR ? = 1)${kindsClause}
|
|
454
|
+
ORDER BY COALESCE(linked_at, prepared_at, created_at) DESC, session_id`)
|
|
455
|
+
.all(...values.slice(0, 2), options.includeUnlinked === true ? 1 : 0, ...values.slice(2));
|
|
456
|
+
return rows.map(toDiscussionSession);
|
|
457
|
+
}
|
|
458
|
+
/**
|
|
459
|
+
* Requirement conversations under one requirement, newest linked first.
|
|
460
|
+
*
|
|
461
|
+
* The `create` row is always included: it explains where the requirement came
|
|
462
|
+
* from even though the Agent never confirms it.
|
|
363
463
|
*/
|
|
364
464
|
listRequirementSessions(requirementId, options = {}) {
|
|
365
465
|
this.#requireRequirement(requirementId);
|
|
366
466
|
const rows = this.#db
|
|
367
|
-
.prepare(`SELECT * FROM
|
|
368
|
-
WHERE
|
|
467
|
+
.prepare(`SELECT * FROM discussion_sessions
|
|
468
|
+
WHERE subject_type = 'requirement' AND subject_id = ?
|
|
369
469
|
AND (kind = 'create' OR linked_at IS NOT NULL OR ? = 1)
|
|
370
470
|
ORDER BY kind, COALESCE(linked_at, prepared_at, created_at) DESC, session_id`)
|
|
371
471
|
.all(requirementId, options.includePending === true ? 1 : 0);
|
|
@@ -506,10 +606,15 @@ export class WorkflowStore {
|
|
|
506
606
|
.prepare('SELECT doc_id, story_id FROM design_doc_stories ORDER BY doc_id, created_at, story_id')
|
|
507
607
|
.all();
|
|
508
608
|
const requirementSessions = this.#db
|
|
509
|
-
.prepare(`SELECT * FROM
|
|
510
|
-
WHERE kind = 'create' OR linked_at IS NOT NULL
|
|
609
|
+
.prepare(`SELECT * FROM discussion_sessions
|
|
610
|
+
WHERE subject_type = 'requirement' AND (kind = 'create' OR linked_at IS NOT NULL)
|
|
511
611
|
ORDER BY COALESCE(linked_at, created_at) DESC, session_id`)
|
|
512
612
|
.all();
|
|
613
|
+
const discussionSessions = this.#db
|
|
614
|
+
.prepare(`SELECT * FROM discussion_sessions
|
|
615
|
+
WHERE subject_type IN ('story', 'bug') AND linked_at IS NOT NULL
|
|
616
|
+
ORDER BY COALESCE(linked_at, prepared_at, created_at) DESC, session_id`)
|
|
617
|
+
.all();
|
|
513
618
|
return {
|
|
514
619
|
requirements: requirements.map(toRequirement),
|
|
515
620
|
stories: stories.map(toStory),
|
|
@@ -518,6 +623,7 @@ export class WorkflowStore {
|
|
|
518
623
|
docs: docs.map(toDesignDoc),
|
|
519
624
|
docStories: docStories.map((row) => ({ docId: text(row, 'doc_id'), storyId: text(row, 'story_id') })),
|
|
520
625
|
requirementSessions: requirementSessions.map(toRequirementSession),
|
|
626
|
+
discussionSessions: discussionSessions.map(toDiscussionSession),
|
|
521
627
|
};
|
|
522
628
|
}
|
|
523
629
|
/** One detailed-design document by id, or null when it does not exist. */
|
|
@@ -531,7 +637,7 @@ export class WorkflowStore {
|
|
|
531
637
|
const now = this.#now();
|
|
532
638
|
const id = newId('requirement');
|
|
533
639
|
const workspaceId = requiredText(input.workspaceId, 'workspaceId');
|
|
534
|
-
const contentPath = this.#persistDoc('requirement',
|
|
640
|
+
const contentPath = this.#persistDoc('requirement', { id }, optionalText(input.content));
|
|
535
641
|
const requirement = {
|
|
536
642
|
id,
|
|
537
643
|
workspaceId,
|
|
@@ -634,7 +740,7 @@ export class WorkflowStore {
|
|
|
634
740
|
}
|
|
635
741
|
const now = this.#now();
|
|
636
742
|
const id = newId('designDoc');
|
|
637
|
-
const contentPath = this.#persistDoc('designDoc',
|
|
743
|
+
const contentPath = this.#persistDoc('designDoc', { id, requirementId: requirement.id }, optionalText(input.content));
|
|
638
744
|
const doc = {
|
|
639
745
|
id,
|
|
640
746
|
requirementId: requirement.id,
|
|
@@ -662,8 +768,7 @@ export class WorkflowStore {
|
|
|
662
768
|
return withTransaction(this.#db, () => {
|
|
663
769
|
const current = this.#requireDesignDoc(id);
|
|
664
770
|
if (input.content !== undefined) {
|
|
665
|
-
|
|
666
|
-
writeDoc(this.#root(requirement.workspaceId), current.contentPath, optionalText(input.content));
|
|
771
|
+
writeDoc(this.#docsRoot, current.contentPath, optionalText(input.content));
|
|
667
772
|
}
|
|
668
773
|
const next = {
|
|
669
774
|
...current,
|
|
@@ -762,7 +867,7 @@ export class WorkflowStore {
|
|
|
762
867
|
: this.#inheritWorkspace(input.workspaceId, workspaceId, 'story');
|
|
763
868
|
const now = this.#now();
|
|
764
869
|
const id = newId('story');
|
|
765
|
-
const contentPath = this.#persistDoc('story',
|
|
870
|
+
const contentPath = this.#persistDoc('story', { id }, optionalText(input.content));
|
|
766
871
|
const sessionId = optionalText(input.sessionId);
|
|
767
872
|
const story = {
|
|
768
873
|
id,
|
|
@@ -802,7 +907,7 @@ export class WorkflowStore {
|
|
|
802
907
|
const size = input.size === undefined ? current.size : sizeOf(input.size);
|
|
803
908
|
const sortOrder = input.sortOrder === undefined ? current.sortOrder : sortOrderOf(input.sortOrder);
|
|
804
909
|
if (input.content !== undefined) {
|
|
805
|
-
writeDoc(this.#
|
|
910
|
+
writeDoc(this.#docsRoot, current.contentPath, optionalText(input.content));
|
|
806
911
|
}
|
|
807
912
|
this.#db
|
|
808
913
|
.prepare('UPDATE stories SET title = ?, size = ?, sort_order = ?, updated_at = ? WHERE id = ?')
|
|
@@ -1058,7 +1163,7 @@ export class WorkflowStore {
|
|
|
1058
1163
|
const bugWorkspaceId = requiredText(input.workspaceId, 'workspaceId');
|
|
1059
1164
|
const now = this.#now();
|
|
1060
1165
|
const id = newId('bug');
|
|
1061
|
-
const contentPath = this.#persistDoc('bug',
|
|
1166
|
+
const contentPath = this.#persistDoc('bug', { id }, optionalText(input.content));
|
|
1062
1167
|
const sessionId = optionalText(input.sessionId);
|
|
1063
1168
|
const bug = {
|
|
1064
1169
|
id,
|
|
@@ -1093,7 +1198,7 @@ export class WorkflowStore {
|
|
|
1093
1198
|
const title = input.title === undefined ? current.title : requiredText(input.title, 'title');
|
|
1094
1199
|
const severity = input.severity === undefined ? current.severity : severityOf(input.severity);
|
|
1095
1200
|
if (input.content !== undefined) {
|
|
1096
|
-
writeDoc(this.#
|
|
1201
|
+
writeDoc(this.#docsRoot, current.contentPath, optionalText(input.content));
|
|
1097
1202
|
}
|
|
1098
1203
|
this.#db
|
|
1099
1204
|
.prepare('UPDATE bugs SET title = ?, severity = ?, updated_at = ? WHERE id = ?')
|
|
@@ -1184,15 +1289,14 @@ export class WorkflowStore {
|
|
|
1184
1289
|
if (!current.deletable) {
|
|
1185
1290
|
throw new WorkflowError(current.reasonCode ?? 'invalid_transition', current.reason ?? `${target.kind} ${target.id} cannot be deleted`);
|
|
1186
1291
|
}
|
|
1187
|
-
for (const
|
|
1188
|
-
const root = this.#root(file.workspaceId);
|
|
1292
|
+
for (const relative of this.#filesToDelete(target)) {
|
|
1189
1293
|
try {
|
|
1190
|
-
const doc = stageDocDeletion(
|
|
1294
|
+
const doc = stageDocDeletion(this.#docsRoot, relative);
|
|
1191
1295
|
if (doc !== null)
|
|
1192
|
-
staged.push({ root, doc });
|
|
1296
|
+
staged.push({ root: this.#docsRoot, doc });
|
|
1193
1297
|
}
|
|
1194
1298
|
catch (error) {
|
|
1195
|
-
throw new WorkflowError('file_error', `could not stage ${
|
|
1299
|
+
throw new WorkflowError('file_error', `could not stage ${relative} for deletion: ${describeError(error)}`);
|
|
1196
1300
|
}
|
|
1197
1301
|
}
|
|
1198
1302
|
if (target.kind === 'story') {
|
|
@@ -1409,23 +1513,16 @@ export class WorkflowStore {
|
|
|
1409
1513
|
runCount: 0,
|
|
1410
1514
|
};
|
|
1411
1515
|
}
|
|
1412
|
-
/**
|
|
1516
|
+
/** Document-root-relative paths that a successful delete must remove. */
|
|
1413
1517
|
#filesToDelete(target) {
|
|
1414
|
-
if (target.kind === 'story')
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
if (target.kind === 'bug') {
|
|
1419
|
-
const bug = this.#requireBug(target.id);
|
|
1420
|
-
return [{ workspaceId: bug.workspaceId, relative: bug.contentPath }];
|
|
1421
|
-
}
|
|
1518
|
+
if (target.kind === 'story')
|
|
1519
|
+
return [this.#requireStory(target.id).contentPath];
|
|
1520
|
+
if (target.kind === 'bug')
|
|
1521
|
+
return [this.#requireBug(target.id).contentPath];
|
|
1422
1522
|
const requirement = this.#requireRequirement(target.id);
|
|
1423
1523
|
return [
|
|
1424
|
-
|
|
1425
|
-
...this.listDesignDocs(requirement.id).map((doc) =>
|
|
1426
|
-
workspaceId: requirement.workspaceId,
|
|
1427
|
-
relative: doc.contentPath,
|
|
1428
|
-
})),
|
|
1524
|
+
requirement.contentPath,
|
|
1525
|
+
...this.listDesignDocs(requirement.id).map((doc) => doc.contentPath),
|
|
1429
1526
|
];
|
|
1430
1527
|
}
|
|
1431
1528
|
#normalizeTargets(targets) {
|
|
@@ -1493,9 +1590,15 @@ export class WorkflowStore {
|
|
|
1493
1590
|
throw notFound('requirement', id);
|
|
1494
1591
|
return toRequirement(row);
|
|
1495
1592
|
}
|
|
1593
|
+
#requireSubject(subjectType, subjectId) {
|
|
1594
|
+
const table = subjectType === 'requirement' ? 'requirements' : subjectType === 'story' ? 'stories' : 'bugs';
|
|
1595
|
+
const row = this.#db.prepare(`SELECT id FROM ${table} WHERE id = ?`).get(subjectId);
|
|
1596
|
+
if (row === undefined)
|
|
1597
|
+
throw notFound(subjectType, subjectId);
|
|
1598
|
+
}
|
|
1496
1599
|
#bindRequirementSessionInTx(requirementId, sessionId, createdAt) {
|
|
1497
1600
|
const existing = this.#db
|
|
1498
|
-
.prepare(
|
|
1601
|
+
.prepare("SELECT * FROM discussion_sessions WHERE subject_type = 'requirement' AND session_id = ?")
|
|
1499
1602
|
.get(sessionId);
|
|
1500
1603
|
if (existing !== undefined) {
|
|
1501
1604
|
const row = toRequirementSession(existing);
|
|
@@ -1505,9 +1608,9 @@ export class WorkflowStore {
|
|
|
1505
1608
|
return row;
|
|
1506
1609
|
}
|
|
1507
1610
|
this.#db
|
|
1508
|
-
.prepare(`INSERT INTO
|
|
1509
|
-
(
|
|
1510
|
-
VALUES (?, ?, 'create', NULL, NULL, ?)`)
|
|
1611
|
+
.prepare(`INSERT INTO discussion_sessions
|
|
1612
|
+
(subject_type, subject_id, session_id, kind, prepared_at, linked_at, created_at)
|
|
1613
|
+
VALUES ('requirement', ?, ?, 'create', NULL, NULL, ?)`)
|
|
1511
1614
|
.run(requirementId, sessionId, createdAt);
|
|
1512
1615
|
return {
|
|
1513
1616
|
requirementId,
|
|
@@ -1520,13 +1623,23 @@ export class WorkflowStore {
|
|
|
1520
1623
|
}
|
|
1521
1624
|
#requireRequirementSession(sessionId) {
|
|
1522
1625
|
const row = this.#db
|
|
1523
|
-
.prepare(
|
|
1626
|
+
.prepare("SELECT * FROM discussion_sessions WHERE subject_type = 'requirement' AND session_id = ?")
|
|
1524
1627
|
.get(sessionId);
|
|
1525
1628
|
if (row === undefined) {
|
|
1526
1629
|
throw new WorkflowError('not_found', `session ${sessionId} is not prepared for a requirement discussion; start again from the requirement page`);
|
|
1527
1630
|
}
|
|
1528
1631
|
return toRequirementSession(row);
|
|
1529
1632
|
}
|
|
1633
|
+
#requireDiscussionSession(sessionId, subjectType, subjectId) {
|
|
1634
|
+
const row = this.#db
|
|
1635
|
+
.prepare(`SELECT * FROM discussion_sessions
|
|
1636
|
+
WHERE subject_type = ? AND subject_id = ? AND session_id = ?`)
|
|
1637
|
+
.get(subjectType, subjectId, sessionId);
|
|
1638
|
+
if (row === undefined) {
|
|
1639
|
+
throw new WorkflowError('not_found', `session ${sessionId} is not prepared for a ${subjectType} discussion`);
|
|
1640
|
+
}
|
|
1641
|
+
return toDiscussionSession(row);
|
|
1642
|
+
}
|
|
1530
1643
|
#requireStory(id) {
|
|
1531
1644
|
const row = this.#db.prepare('SELECT * FROM stories WHERE id = ?').get(id);
|
|
1532
1645
|
if (row === undefined)
|
|
@@ -1550,7 +1663,7 @@ export class WorkflowStore {
|
|
|
1550
1663
|
const title = input.title === undefined ? current.title : requiredText(input.title, 'title');
|
|
1551
1664
|
const summary = input.summary === undefined ? current.summary : optionalText(input.summary);
|
|
1552
1665
|
if (input.content !== undefined) {
|
|
1553
|
-
writeDoc(this.#
|
|
1666
|
+
writeDoc(this.#docsRoot, current.contentPath, optionalText(input.content));
|
|
1554
1667
|
}
|
|
1555
1668
|
this.#db
|
|
1556
1669
|
.prepare('UPDATE requirements SET title = ?, summary = ?, updated_at = ? WHERE id = ?')
|
package/lib/tools/get-bug.js
CHANGED
|
@@ -24,7 +24,7 @@ export function readBug(workflow, rawId) {
|
|
|
24
24
|
const bug = workflow.getBug(id);
|
|
25
25
|
if (bug === null)
|
|
26
26
|
throw new Error(`teamflow: 缺陷 ${id} 不存在,无法读取正文。`);
|
|
27
|
-
const body = workflow.readContent(bug.
|
|
27
|
+
const body = workflow.readContent(bug.contentPath);
|
|
28
28
|
return {
|
|
29
29
|
id: bug.id,
|
|
30
30
|
title: bug.title,
|
|
@@ -33,7 +33,7 @@ export function readDesignDoc(workflow, rawId) {
|
|
|
33
33
|
if (requirement === null) {
|
|
34
34
|
throw new Error(`teamflow: 详细设计 ${id} 所属的需求 ${doc.requirementId} 不存在。`);
|
|
35
35
|
}
|
|
36
|
-
const body = workflow.readContent(
|
|
36
|
+
const body = workflow.readContent(doc.contentPath);
|
|
37
37
|
return {
|
|
38
38
|
id: doc.id,
|
|
39
39
|
requirementId: doc.requirementId,
|
|
@@ -110,7 +110,7 @@ export function getRequirementTool(environment) {
|
|
|
110
110
|
}
|
|
111
111
|
const docs = environment.database.workflow.listDesignDocs(requirement.id);
|
|
112
112
|
const stories = environment.database.workflow.listStories({ requirementId: requirement.id });
|
|
113
|
-
const body = environment.database.workflow.readContent(requirement.
|
|
113
|
+
const body = environment.database.workflow.readContent(requirement.contentPath);
|
|
114
114
|
return {
|
|
115
115
|
requirementId: requirement.id,
|
|
116
116
|
title: requirement.title,
|
package/lib/tools/get-story.js
CHANGED
|
@@ -47,7 +47,7 @@ export function readStory(workflow, rawId) {
|
|
|
47
47
|
path: doc.contentPath,
|
|
48
48
|
coversStory: linked.has(doc.id),
|
|
49
49
|
}));
|
|
50
|
-
const body = workflow.readContent(story.
|
|
50
|
+
const body = workflow.readContent(story.contentPath);
|
|
51
51
|
return {
|
|
52
52
|
id: story.id,
|
|
53
53
|
title: story.title,
|
package/lib/tools/index.js
CHANGED
|
@@ -12,6 +12,7 @@ import { getDesignDocTool } from "./get-design-doc.js";
|
|
|
12
12
|
import { getRequirementTool } from "./get-requirement.js";
|
|
13
13
|
import { getStoryTool } from "./get-story.js";
|
|
14
14
|
import { linkRequirementSessionTool } from "./link-requirement-session.js";
|
|
15
|
+
import { linkSessionTool } from "./link-session.js";
|
|
15
16
|
import { listRecordsTool } from "./list-records.js";
|
|
16
17
|
import { listWorkspacesTool } from "./list-workspaces.js";
|
|
17
18
|
import { createToolEnvironment } from "./shared.js";
|
|
@@ -43,6 +44,7 @@ export const TOOL_PROFILES = [
|
|
|
43
44
|
*/
|
|
44
45
|
export const PROFILE_TOOL_NAMES = {
|
|
45
46
|
requirement: [
|
|
47
|
+
'teamflow_link_session',
|
|
46
48
|
'teamflow_link_requirement_session',
|
|
47
49
|
'teamflow_create_requirement',
|
|
48
50
|
'teamflow_get_requirement',
|
|
@@ -51,6 +53,7 @@ export const PROFILE_TOOL_NAMES = {
|
|
|
51
53
|
'teamflow_list_workspaces',
|
|
52
54
|
],
|
|
53
55
|
detail: [
|
|
56
|
+
'teamflow_link_session',
|
|
54
57
|
'teamflow_link_requirement_session',
|
|
55
58
|
'teamflow_get_requirement',
|
|
56
59
|
'teamflow_get_design_doc',
|
|
@@ -60,6 +63,7 @@ export const PROFILE_TOOL_NAMES = {
|
|
|
60
63
|
'teamflow_list_workspaces',
|
|
61
64
|
],
|
|
62
65
|
split: [
|
|
66
|
+
'teamflow_link_session',
|
|
63
67
|
'teamflow_link_requirement_session',
|
|
64
68
|
'teamflow_get_requirement',
|
|
65
69
|
'teamflow_get_design_doc',
|
|
@@ -80,6 +84,7 @@ export const PROFILE_TOOL_NAMES = {
|
|
|
80
84
|
'teamflow_cancel_run',
|
|
81
85
|
],
|
|
82
86
|
bug: [
|
|
87
|
+
'teamflow_link_session',
|
|
83
88
|
'teamflow_create_bug',
|
|
84
89
|
'teamflow_get_bug',
|
|
85
90
|
'teamflow_update_bug',
|
|
@@ -94,6 +99,7 @@ export const PROFILE_TOOL_NAMES = {
|
|
|
94
99
|
'teamflow_update_design_doc',
|
|
95
100
|
'teamflow_get_design_doc',
|
|
96
101
|
'teamflow_link_requirement_session',
|
|
102
|
+
'teamflow_link_session',
|
|
97
103
|
'teamflow_create_story',
|
|
98
104
|
'teamflow_get_story',
|
|
99
105
|
'teamflow_update_story',
|
|
@@ -138,6 +144,7 @@ export function apply(ctx, config) {
|
|
|
138
144
|
updateDesignDocTool(environment),
|
|
139
145
|
getDesignDocTool(environment),
|
|
140
146
|
linkRequirementSessionTool(environment),
|
|
147
|
+
linkSessionTool(environment),
|
|
141
148
|
createStoryTool(environment),
|
|
142
149
|
getStoryTool(environment),
|
|
143
150
|
updateStoryTool(environment),
|