@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/remote/contract.d.ts
CHANGED
|
@@ -36,7 +36,7 @@ declare module '@deepseek-ai/dsh-typert-protocol' {
|
|
|
36
36
|
register(contribution: HostContribution): TypertDisposer;
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
|
-
import type { BugSeverity, BugStatus, DeletePreview, DeleteReport, DeleteTarget, DocKind, RequirementSessionKind, RequirementStatus, RunKind, RunStatus, StorySize, StoryStatus } from '../store/model.ts';
|
|
39
|
+
import type { BugSeverity, BugStatus, DeletePreview, DiscussionSubjectType, DeleteReport, DeleteTarget, DocKind, RequirementSessionKind, RequirementStatus, RunKind, RunStatus, StorySize, StoryStatus } from '../store/model.ts';
|
|
40
40
|
/** npm package that owns these Remote methods. */
|
|
41
41
|
export declare const PACKAGE_NAME = "@xirang-ai/dsh-teamflow";
|
|
42
42
|
/**
|
|
@@ -89,6 +89,8 @@ export interface SnapshotStory {
|
|
|
89
89
|
readonly completedAt: string | null;
|
|
90
90
|
/** Conversation that created the story; null when it was not recorded. */
|
|
91
91
|
readonly createSessionId: string | null;
|
|
92
|
+
/** Confirmed discussions of this story, newest first. */
|
|
93
|
+
readonly sessions: readonly SnapshotDiscussionSession[];
|
|
92
94
|
}
|
|
93
95
|
/** One 详细设计 document; its body is fetched on demand by {@link readDoc}. */
|
|
94
96
|
export interface SnapshotDoc {
|
|
@@ -121,6 +123,8 @@ export interface SnapshotBug {
|
|
|
121
123
|
readonly completedAt: string | null;
|
|
122
124
|
/** Conversation that created the defect; null when it was not recorded. */
|
|
123
125
|
readonly createSessionId: string | null;
|
|
126
|
+
/** Confirmed discussions of this defect, newest first. */
|
|
127
|
+
readonly sessions: readonly SnapshotDiscussionSession[];
|
|
124
128
|
}
|
|
125
129
|
/** One development or fix run; `sessionId` is the conversation it happened in. */
|
|
126
130
|
export interface SnapshotRun {
|
|
@@ -148,6 +152,17 @@ export interface SnapshotRequirementSession {
|
|
|
148
152
|
readonly linkedAt: string | null;
|
|
149
153
|
readonly createdAt: string;
|
|
150
154
|
}
|
|
155
|
+
/** One discussion conversation attached to a story or a defect. */
|
|
156
|
+
export interface SnapshotDiscussionSession {
|
|
157
|
+
readonly sessionId: string;
|
|
158
|
+
/** `discuss` for stories and defects; requirements keep their own kinds. */
|
|
159
|
+
readonly kind: string;
|
|
160
|
+
/** Browser preparation time; null only for conversations that skipped it. */
|
|
161
|
+
readonly preparedAt: string | null;
|
|
162
|
+
/** Agent confirmation time; null while the discussion is pending. */
|
|
163
|
+
readonly linkedAt: string | null;
|
|
164
|
+
readonly createdAt: string;
|
|
165
|
+
}
|
|
151
166
|
/** Everything the list, the design tree and the defect list need in one frame. */
|
|
152
167
|
export interface WorkbenchSnapshot {
|
|
153
168
|
/** When the Host read the rows; the panel shows it next to the refresh button. */
|
|
@@ -226,6 +241,16 @@ export interface RequirementSessionReply {
|
|
|
226
241
|
readonly preparedAt: string | null;
|
|
227
242
|
readonly linkedAt: string | null;
|
|
228
243
|
}
|
|
244
|
+
/** One discussion row as stored; `linkedAt` is null until the Agent confirms. */
|
|
245
|
+
export interface DiscussionSessionReply {
|
|
246
|
+
readonly subjectType: DiscussionSubjectType;
|
|
247
|
+
readonly subjectId: string;
|
|
248
|
+
readonly sessionId: string;
|
|
249
|
+
readonly kind: string;
|
|
250
|
+
readonly preparedAt: string | null;
|
|
251
|
+
readonly linkedAt: string | null;
|
|
252
|
+
readonly createdAt: string;
|
|
253
|
+
}
|
|
229
254
|
/** Success or failure envelope the browser mounts produce. */
|
|
230
255
|
export type RemoteResult<Value> = {
|
|
231
256
|
readonly ok: true;
|
|
@@ -323,6 +348,13 @@ export interface WorkbenchRemote {
|
|
|
323
348
|
* of the snapshot until `teamflow_link_requirement_session` writes `linkedAt`.
|
|
324
349
|
*/
|
|
325
350
|
prepareRequirementSession(requirementId: string, sessionId: string, kind: RequirementSessionKind, locale?: string): Promise<RemoteResult<RequirementSessionReply>>;
|
|
351
|
+
/**
|
|
352
|
+
* Register a newly created session as a pending discussion of any subject.
|
|
353
|
+
*
|
|
354
|
+
* Same two-phase contract as {@link prepareRequirementSession}, extended to
|
|
355
|
+
* stories and defects; this method never writes `linkedAt`.
|
|
356
|
+
*/
|
|
357
|
+
prepareDiscussionSession(subjectType: DiscussionSubjectType, subjectId: string, sessionId: string, kind: string, locale?: string): Promise<RemoteResult<DiscussionSessionReply>>;
|
|
326
358
|
/**
|
|
327
359
|
* Read the tier a conversation was started with, for the session header.
|
|
328
360
|
*
|
|
@@ -357,6 +389,7 @@ export declare const METHODS: {
|
|
|
357
389
|
readonly cancelStory: "cancelStory";
|
|
358
390
|
readonly completeBug: "completeBug";
|
|
359
391
|
readonly prepareRequirementSession: "prepareRequirementSession";
|
|
392
|
+
readonly prepareDiscussionSession: "prepareDiscussionSession";
|
|
360
393
|
readonly sessionPermissionTier: "sessionPermissionTier";
|
|
361
394
|
readonly previewDelete: "previewDelete";
|
|
362
395
|
readonly deleteRecords: "deleteRecords";
|
package/lib/remote/contract.js
CHANGED
|
@@ -22,6 +22,7 @@ export const METHODS = {
|
|
|
22
22
|
cancelStory: 'cancelStory',
|
|
23
23
|
completeBug: 'completeBug',
|
|
24
24
|
prepareRequirementSession: 'prepareRequirementSession',
|
|
25
|
+
prepareDiscussionSession: 'prepareDiscussionSession',
|
|
25
26
|
sessionPermissionTier: 'sessionPermissionTier',
|
|
26
27
|
previewDelete: 'previewDelete',
|
|
27
28
|
deleteRecords: 'deleteRecords',
|
|
@@ -170,6 +171,21 @@ export const DESCRIPTORS = Object.freeze([
|
|
|
170
171
|
]),
|
|
171
172
|
result: JSON_CODEC,
|
|
172
173
|
}),
|
|
174
|
+
Object.freeze({
|
|
175
|
+
id: `${PACKAGE_NAME}#${METHODS.prepareDiscussionSession}`,
|
|
176
|
+
service: SERVICE_KEY,
|
|
177
|
+
namespace: NAMESPACE,
|
|
178
|
+
method: METHODS.prepareDiscussionSession,
|
|
179
|
+
invocation: { kind: 'direct' },
|
|
180
|
+
parameters: Object.freeze([
|
|
181
|
+
parameter('subjectType', 'Discussion subject: requirement, story or bug'),
|
|
182
|
+
parameter('subjectId', 'Requirement, story or defect the discussion belongs to'),
|
|
183
|
+
parameter('sessionId', 'Newly created conversation to link'),
|
|
184
|
+
parameter('kind', 'Discussion kind: discuss for stories and defects, the requirement kinds otherwise'),
|
|
185
|
+
LOCALE_PARAMETER,
|
|
186
|
+
]),
|
|
187
|
+
result: JSON_CODEC,
|
|
188
|
+
}),
|
|
173
189
|
Object.freeze({
|
|
174
190
|
id: `${PACKAGE_NAME}#${METHODS.sessionPermissionTier}`,
|
|
175
191
|
service: SERVICE_KEY,
|
package/lib/remote/service.d.ts
CHANGED
|
@@ -10,9 +10,9 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import type { Context } from '@deepseek-ai/cordis';
|
|
12
12
|
import { TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol';
|
|
13
|
-
import type { DeletePreview, DeleteReport, DeleteTarget, RequirementSessionKind } from '../store/model.ts';
|
|
13
|
+
import type { DeletePreview, DeleteReport, DeleteTarget, DiscussionSubjectType, RequirementSessionKind } from '../store/model.ts';
|
|
14
14
|
import type { WorkflowStore } from '../store/workflow.ts';
|
|
15
|
-
import type { AttachmentPolicy, BugRunReply, BugStatusReply, CancelRunReply, DocBody, RequirementSessionReply, RequirementStatusReply, SessionTierReply, StartRunReply, StoryStatusReply, WorkbenchSnapshot } from './contract.ts';
|
|
15
|
+
import type { AttachmentPolicy, BugRunReply, BugStatusReply, CancelRunReply, DiscussionSessionReply, DocBody, RequirementSessionReply, RequirementStatusReply, SessionTierReply, StartRunReply, StoryStatusReply, WorkbenchSnapshot } from './contract.ts';
|
|
16
16
|
/**
|
|
17
17
|
* Remote face of the workbench.
|
|
18
18
|
*
|
|
@@ -134,6 +134,19 @@ export declare class WorkbenchService extends TypertRemoteService {
|
|
|
134
134
|
* @returns the stored pending row.
|
|
135
135
|
*/
|
|
136
136
|
prepareRequirementSession(requirementId: string, sessionId: string, kind?: RequirementSessionKind, locale?: string): RequirementSessionReply;
|
|
137
|
+
/**
|
|
138
|
+
* Register a newly created conversation as a pending discussion of any subject.
|
|
139
|
+
*
|
|
140
|
+
* Same two-phase contract as {@link prepareRequirementSession}: the Agent
|
|
141
|
+
* confirms with `teamflow_link_session`, and this method never writes the
|
|
142
|
+
* confirmation time itself.
|
|
143
|
+
* @param subjectType - requirement, story or bug.
|
|
144
|
+
* @param subjectId - record the discussion belongs to.
|
|
145
|
+
* @param sessionId - session created by the browser for this discussion.
|
|
146
|
+
* @param kind - `discuss` for stories and defects; requirement kinds otherwise.
|
|
147
|
+
* @returns the stored pending row.
|
|
148
|
+
*/
|
|
149
|
+
prepareDiscussionSession(subjectType: DiscussionSubjectType, subjectId: string, sessionId: string, kind: string, locale?: string): DiscussionSessionReply;
|
|
137
150
|
/**
|
|
138
151
|
* Compute deletion impact without writing anything.
|
|
139
152
|
* @param targets - records the user selected in the panel.
|
package/lib/remote/service.js
CHANGED
|
@@ -12,10 +12,22 @@ import { NAMESPACE, SERVICE_KEY } from "./contract.js";
|
|
|
12
12
|
* `WorkflowStore.readContent` returns `null` for a missing file, `''` for an
|
|
13
13
|
* empty one, and throws for any other I/O failure.
|
|
14
14
|
*/
|
|
15
|
-
function readBody(workflow,
|
|
16
|
-
const body = workflow.readContent(
|
|
15
|
+
function readBody(workflow, path) {
|
|
16
|
+
const body = workflow.readContent(path);
|
|
17
17
|
return body === null ? { body: '', bodyMissing: true } : { body, bodyMissing: false };
|
|
18
18
|
}
|
|
19
|
+
/** Confirmed discussions of one story or defect, newest first. */
|
|
20
|
+
function discussionsFor(rows, subjectType, subjectId) {
|
|
21
|
+
return rows
|
|
22
|
+
.filter((row) => row.subjectType === subjectType && row.subjectId === subjectId)
|
|
23
|
+
.map((row) => ({
|
|
24
|
+
sessionId: row.sessionId,
|
|
25
|
+
kind: row.kind,
|
|
26
|
+
preparedAt: row.preparedAt,
|
|
27
|
+
linkedAt: row.linkedAt,
|
|
28
|
+
createdAt: row.createdAt,
|
|
29
|
+
}));
|
|
30
|
+
}
|
|
19
31
|
/**
|
|
20
32
|
* Rebuild a failure in the caller's language.
|
|
21
33
|
*
|
|
@@ -84,7 +96,7 @@ export class WorkbenchService extends TypertRemoteService {
|
|
|
84
96
|
status: requirement.status,
|
|
85
97
|
updatedAt: requirement.updatedAt,
|
|
86
98
|
path: requirement.contentPath,
|
|
87
|
-
...readBody(this.workflow, requirement.
|
|
99
|
+
...readBody(this.workflow, requirement.contentPath),
|
|
88
100
|
completedAt: requirement.completedAt,
|
|
89
101
|
})),
|
|
90
102
|
stories: rows.stories.map((story) => ({
|
|
@@ -98,10 +110,11 @@ export class WorkbenchService extends TypertRemoteService {
|
|
|
98
110
|
createdAt: story.createdAt,
|
|
99
111
|
updatedAt: story.updatedAt,
|
|
100
112
|
path: story.contentPath,
|
|
101
|
-
...readBody(this.workflow, story.
|
|
113
|
+
...readBody(this.workflow, story.contentPath),
|
|
102
114
|
doneAt: story.doneAt,
|
|
103
115
|
completedAt: story.completedAt,
|
|
104
116
|
createSessionId: story.createSessionId,
|
|
117
|
+
sessions: discussionsFor(rows.discussionSessions, 'story', story.id),
|
|
105
118
|
})),
|
|
106
119
|
docs: rows.docs.map((doc) => ({
|
|
107
120
|
id: doc.id,
|
|
@@ -122,10 +135,11 @@ export class WorkbenchService extends TypertRemoteService {
|
|
|
122
135
|
status: bug.status,
|
|
123
136
|
resolution: bug.resolution,
|
|
124
137
|
updatedAt: bug.updatedAt,
|
|
125
|
-
...readBody(this.workflow, bug.
|
|
138
|
+
...readBody(this.workflow, bug.contentPath),
|
|
126
139
|
fixedAt: bug.fixedAt,
|
|
127
140
|
completedAt: bug.completedAt,
|
|
128
141
|
createSessionId: bug.createSessionId,
|
|
142
|
+
sessions: discussionsFor(rows.discussionSessions, 'bug', bug.id),
|
|
129
143
|
})),
|
|
130
144
|
runs: rows.runs.map((run) => ({
|
|
131
145
|
id: run.id,
|
|
@@ -400,6 +414,35 @@ export class WorkbenchService extends TypertRemoteService {
|
|
|
400
414
|
throw localizedFailure(error, locale);
|
|
401
415
|
}
|
|
402
416
|
}
|
|
417
|
+
/**
|
|
418
|
+
* Register a newly created conversation as a pending discussion of any subject.
|
|
419
|
+
*
|
|
420
|
+
* Same two-phase contract as {@link prepareRequirementSession}: the Agent
|
|
421
|
+
* confirms with `teamflow_link_session`, and this method never writes the
|
|
422
|
+
* confirmation time itself.
|
|
423
|
+
* @param subjectType - requirement, story or bug.
|
|
424
|
+
* @param subjectId - record the discussion belongs to.
|
|
425
|
+
* @param sessionId - session created by the browser for this discussion.
|
|
426
|
+
* @param kind - `discuss` for stories and defects; requirement kinds otherwise.
|
|
427
|
+
* @returns the stored pending row.
|
|
428
|
+
*/
|
|
429
|
+
prepareDiscussionSession(subjectType, subjectId, sessionId, kind, locale) {
|
|
430
|
+
try {
|
|
431
|
+
const row = this.workflow.prepareDiscussionSession(subjectType, subjectId, sessionId, kind);
|
|
432
|
+
return {
|
|
433
|
+
subjectType: row.subjectType,
|
|
434
|
+
subjectId: row.subjectId,
|
|
435
|
+
sessionId: row.sessionId,
|
|
436
|
+
kind: row.kind,
|
|
437
|
+
preparedAt: row.preparedAt,
|
|
438
|
+
linkedAt: row.linkedAt,
|
|
439
|
+
createdAt: row.createdAt,
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
catch (error) {
|
|
443
|
+
throw localizedFailure(error, locale);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
403
446
|
/**
|
|
404
447
|
* Compute deletion impact without writing anything.
|
|
405
448
|
* @param targets - records the user selected in the panel.
|
|
@@ -459,7 +502,7 @@ export class WorkbenchService extends TypertRemoteService {
|
|
|
459
502
|
return {
|
|
460
503
|
id: doc.id,
|
|
461
504
|
title: doc.title,
|
|
462
|
-
...readBody(this.workflow,
|
|
505
|
+
...readBody(this.workflow, doc.contentPath),
|
|
463
506
|
};
|
|
464
507
|
}
|
|
465
508
|
catch (error) {
|
|
@@ -42,3 +42,26 @@ export declare function presetForRequirementDiscussion(kind: RequirementDiscussi
|
|
|
42
42
|
* @returns the first message of the discussion, carrying the supplement slot.
|
|
43
43
|
*/
|
|
44
44
|
export declare function buildRequirementDiscussionPrompt(kind?: RequirementDiscussionKind): string;
|
|
45
|
+
/** Preset for a story discussion (reuses the standalone story agent). */
|
|
46
|
+
export declare const STORY_DISCUSSION_PRESET = "xirang-story";
|
|
47
|
+
/** Preset for a defect discussion. */
|
|
48
|
+
export declare const BUG_DISCUSSION_PRESET = "xirang-bug";
|
|
49
|
+
/** A record kind whose detail page can start a discussion. */
|
|
50
|
+
export type RecordDiscussionKind = 'story' | 'bug';
|
|
51
|
+
/**
|
|
52
|
+
* Pick the preset a story or defect discussion runs under.
|
|
53
|
+
* @param kind - `story` or `bug`.
|
|
54
|
+
* @returns the preset id declared in `cordis.patch.yml`.
|
|
55
|
+
*/
|
|
56
|
+
export declare function presetForRecordDiscussion(kind: RecordDiscussionKind): string;
|
|
57
|
+
/**
|
|
58
|
+
* Build the prompt sent when the user starts a discussion from a story or defect page.
|
|
59
|
+
*
|
|
60
|
+
* The title travels in the prompt because it is what the reader recognises;
|
|
61
|
+
* the id stays out of it, just like requirement discussions: the tools resolve
|
|
62
|
+
* the record through the session binding.
|
|
63
|
+
* @param kind - `story` or `bug`.
|
|
64
|
+
* @param title - story or defect title shown to the reader.
|
|
65
|
+
* @returns the first message of the discussion, carrying the supplement slot.
|
|
66
|
+
*/
|
|
67
|
+
export declare function buildRecordDiscussionPrompt(kind: RecordDiscussionKind, title: string): string;
|
|
@@ -58,3 +58,29 @@ export function buildRequirementDiscussionPrompt(kind = 'design') {
|
|
|
58
58
|
}
|
|
59
59
|
return ['【继续讨论需求与概要设计】', '', '我的补充:'].join('\n');
|
|
60
60
|
}
|
|
61
|
+
/** Preset for a story discussion (reuses the standalone story agent). */
|
|
62
|
+
export const STORY_DISCUSSION_PRESET = 'xirang-story';
|
|
63
|
+
/** Preset for a defect discussion. */
|
|
64
|
+
export const BUG_DISCUSSION_PRESET = 'xirang-bug';
|
|
65
|
+
/**
|
|
66
|
+
* Pick the preset a story or defect discussion runs under.
|
|
67
|
+
* @param kind - `story` or `bug`.
|
|
68
|
+
* @returns the preset id declared in `cordis.patch.yml`.
|
|
69
|
+
*/
|
|
70
|
+
export function presetForRecordDiscussion(kind) {
|
|
71
|
+
return kind === 'bug' ? BUG_DISCUSSION_PRESET : STORY_DISCUSSION_PRESET;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Build the prompt sent when the user starts a discussion from a story or defect page.
|
|
75
|
+
*
|
|
76
|
+
* The title travels in the prompt because it is what the reader recognises;
|
|
77
|
+
* the id stays out of it, just like requirement discussions: the tools resolve
|
|
78
|
+
* the record through the session binding.
|
|
79
|
+
* @param kind - `story` or `bug`.
|
|
80
|
+
* @param title - story or defect title shown to the reader.
|
|
81
|
+
* @returns the first message of the discussion, carrying the supplement slot.
|
|
82
|
+
*/
|
|
83
|
+
export function buildRecordDiscussionPrompt(kind, title) {
|
|
84
|
+
const heading = kind === 'bug' ? '【继续讨论缺陷】' : '【继续讨论故事】';
|
|
85
|
+
return [heading, title, '', '我的补充:'].join('\n');
|
|
86
|
+
}
|
package/lib/store/db.d.ts
CHANGED
|
@@ -43,6 +43,10 @@ export interface StartupReport {
|
|
|
43
43
|
readonly file: string;
|
|
44
44
|
/** Database file rendered for logs (`$DSH_HOME/...`). */
|
|
45
45
|
readonly displayFile: string;
|
|
46
|
+
/** Absolute document root that holds `requirements/`, `stories/` and `bugs/`. */
|
|
47
|
+
readonly docsDir: string;
|
|
48
|
+
/** Document root rendered for logs (`$DSH_HOME/...`). */
|
|
49
|
+
readonly displayDocsDir: string;
|
|
46
50
|
/** Version stored before this start, `null` on the first start. */
|
|
47
51
|
readonly fromVersion: string | null;
|
|
48
52
|
/** Version now running. */
|
|
@@ -89,10 +93,11 @@ export interface OpenDatabaseOptions {
|
|
|
89
93
|
/** `Config.dataDir` override; empty uses the plugin home below `$DSH_HOME`. */
|
|
90
94
|
readonly dataDir?: string;
|
|
91
95
|
/**
|
|
92
|
-
*
|
|
93
|
-
*
|
|
96
|
+
* `Config.docsDir` override for the document root; empty uses `<dataDir>/docs`.
|
|
97
|
+
* Relative paths and `~/` resolve like {@link resolveDataDir}; an invalid value
|
|
98
|
+
* throws instead of falling back to the default.
|
|
94
99
|
*/
|
|
95
|
-
readonly
|
|
100
|
+
readonly docsDir?: string;
|
|
96
101
|
/** Resolver consulted before the registry, for the Host's live workspace list. */
|
|
97
102
|
readonly workspaceRoot?: (workspaceId: string) => string | undefined;
|
|
98
103
|
/** SQLite busy timeout in milliseconds. */
|
package/lib/store/db.js
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
import { DatabaseSync } from 'node:sqlite';
|
|
12
12
|
import { contextualError, describeError } from "../errors.js";
|
|
13
13
|
import { readPluginVersion } from "../version.js";
|
|
14
|
-
import { DATABASE_FILENAME, databaseFile, ensureDataDir, resolveDataDir, toDisplayPath, } from "./paths.js";
|
|
14
|
+
import { DATABASE_FILENAME, databaseFile, ensureDataDir, ensureDocsDir, resolveDataDir, resolveDocsDir, toDisplayPath, } from "./paths.js";
|
|
15
15
|
import { BUILTIN_ROLES, MIGRATIONS, TARGET_SCHEMA_VERSION, syncSchemaComments, } from "./schema.js";
|
|
16
16
|
import { withTransaction } from "./transaction.js";
|
|
17
17
|
import { runUpgrades } from "./upgrades.js";
|
|
@@ -36,6 +36,31 @@ export const META_KEYS = {
|
|
|
36
36
|
/** Schema version, mirrored from `PRAGMA user_version` for inspection. */
|
|
37
37
|
schemaVersion: 'schema_version',
|
|
38
38
|
};
|
|
39
|
+
/** Prefix of legacy content paths, which were relative to a workspace root. */
|
|
40
|
+
const LEGACY_DOC_PREFIX = '.xirang-ai/dsh-teamflow/';
|
|
41
|
+
/**
|
|
42
|
+
* Rewrite legacy workspace-relative `content_path` values onto the document
|
|
43
|
+
* root. Idempotent: rows already on the new basis do not match the prefix, so
|
|
44
|
+
* a restart after a partial failure simply finishes the job.
|
|
45
|
+
*
|
|
46
|
+
* This is a data-basis normalization, not a schema migration: the table layout
|
|
47
|
+
* and `user_version` stay untouched.
|
|
48
|
+
*
|
|
49
|
+
* @returns how many rows were rewritten.
|
|
50
|
+
*/
|
|
51
|
+
function normalizeContentPaths(db) {
|
|
52
|
+
const tables = ['requirements', 'design_docs', 'stories', 'bugs'];
|
|
53
|
+
return withTransaction(db, () => {
|
|
54
|
+
let changed = 0;
|
|
55
|
+
for (const table of tables) {
|
|
56
|
+
const result = db
|
|
57
|
+
.prepare(`UPDATE ${table} SET content_path = substr(content_path, ?) WHERE content_path LIKE ?`)
|
|
58
|
+
.run(LEGACY_DOC_PREFIX.length + 1, `${LEGACY_DOC_PREFIX}%`);
|
|
59
|
+
changed += Number(result.changes);
|
|
60
|
+
}
|
|
61
|
+
return changed;
|
|
62
|
+
});
|
|
63
|
+
}
|
|
39
64
|
function configure(db, busyTimeoutMs) {
|
|
40
65
|
const timeout = Number.isFinite(busyTimeoutMs) ? Math.max(0, Math.trunc(busyTimeoutMs)) : 0;
|
|
41
66
|
// WAL keeps readers unblocked while a writer holds the lock; NORMAL is the
|
|
@@ -175,6 +200,7 @@ export function openTeamflowDatabase(options = {}) {
|
|
|
175
200
|
const log = options.log ?? SILENT_LOGGER;
|
|
176
201
|
const version = options.version ?? readPluginVersion();
|
|
177
202
|
const dataDir = resolveDataDir(options.dataDir);
|
|
203
|
+
const docsDir = ensureDocsDir(resolveDocsDir(options.docsDir, dataDir));
|
|
178
204
|
const file = databaseFile(dataDir);
|
|
179
205
|
try {
|
|
180
206
|
ensureDataDir(dataDir);
|
|
@@ -194,10 +220,16 @@ export function openTeamflowDatabase(options = {}) {
|
|
|
194
220
|
const appliedMigrations = migrate(db, log);
|
|
195
221
|
syncSchemaComments(db);
|
|
196
222
|
seedBuiltinRoles(db);
|
|
223
|
+
const normalizedPaths = normalizeContentPaths(db);
|
|
224
|
+
if (normalizedPaths > 0) {
|
|
225
|
+
log.info(`normalized ${normalizedPaths} document path(s) onto the document root ${toDisplayPath(docsDir)}`);
|
|
226
|
+
}
|
|
197
227
|
const reconciliation = reconcileVersion(db, version, log);
|
|
198
228
|
const report = {
|
|
199
229
|
file,
|
|
200
230
|
displayFile: toDisplayPath(file),
|
|
231
|
+
docsDir,
|
|
232
|
+
displayDocsDir: toDisplayPath(docsDir),
|
|
201
233
|
fromVersion: reconciliation.fromVersion,
|
|
202
234
|
toVersion: version,
|
|
203
235
|
outcome: reconciliation.outcome,
|
|
@@ -225,7 +257,7 @@ export function openTeamflowDatabase(options = {}) {
|
|
|
225
257
|
db,
|
|
226
258
|
report,
|
|
227
259
|
workflow: new WorkflowStore(db, {
|
|
228
|
-
docsRoot:
|
|
260
|
+
docsRoot: docsDir,
|
|
229
261
|
workspaceRoot: options.workspaceRoot,
|
|
230
262
|
warn: (message) => log.warn(message),
|
|
231
263
|
}),
|
package/lib/store/docs.d.ts
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
|
-
/** 文档根目录名,工作区内的隐藏目录。 */
|
|
2
|
-
export declare const DOC_ROOT = ".xirang-ai/dsh-teamflow";
|
|
3
1
|
/** 文档种类:决定落盘路径。 */
|
|
4
2
|
export type DocKind = 'requirement' | 'designDoc' | 'story' | 'bug';
|
|
5
3
|
/**
|
|
6
|
-
*
|
|
4
|
+
* 计算一份文档在文档根内的相对路径。
|
|
7
5
|
*
|
|
8
6
|
* @param kind - 文档种类
|
|
9
7
|
* @param ids - 该种类需要的标识(designDoc 需要需求 id 与文档 id)
|
|
@@ -14,19 +12,19 @@ export declare function docPath(kind: DocKind, ids: {
|
|
|
14
12
|
requirementId?: string;
|
|
15
13
|
}): string;
|
|
16
14
|
/**
|
|
17
|
-
*
|
|
15
|
+
* 把相对路径拼到文档根上;只接受插件自己生成的、以 `/` 分隔的文档相对路径。
|
|
18
16
|
*
|
|
19
17
|
* 拒绝 POSIX 绝对路径、Windows 盘符/UNC/反斜杠路径与任何 `..`,最后再做一次
|
|
20
18
|
* 词法 containment 校验,确保结果仍落在 `root` 之内。
|
|
21
19
|
*/
|
|
22
20
|
export declare function resolveDocPath(root: string, relative: string): string;
|
|
23
|
-
/**
|
|
21
|
+
/** 文档相对路径的展示形式(日志与工具结果用,始终 `/`)。 */
|
|
24
22
|
export declare function displayPath(relative: string): string;
|
|
25
23
|
/**
|
|
26
|
-
*
|
|
27
|
-
*
|
|
24
|
+
* 首次为文档根准备共享目录:只建 `requirements/`、`stories/`、`bugs/`,
|
|
25
|
+
* 不碰工作区,也不写 `.gitignore`。
|
|
28
26
|
*
|
|
29
|
-
* @param root -
|
|
27
|
+
* @param root - 文档根目录(绝对路径)
|
|
30
28
|
* @returns 实际创建的目录(相对路径),便于工具结果里说明
|
|
31
29
|
*/
|
|
32
30
|
export declare function ensureDocTree(root: string): string[];
|
|
@@ -46,7 +44,7 @@ export declare function writeDoc(root: string, relative: string, body: string):
|
|
|
46
44
|
export declare function removeDoc(root: string, relative: string): void;
|
|
47
45
|
/** 已挪出正式路径、等待随数据库事务一起确认删除的文档。 */
|
|
48
46
|
export interface StagedDoc {
|
|
49
|
-
/**
|
|
47
|
+
/** 原来的文档相对路径;事务失败时据此还原。 */
|
|
50
48
|
readonly relative: string;
|
|
51
49
|
/** 临时绝对路径,与正式文件同目录,保证 rename 是原子的。 */
|
|
52
50
|
readonly staged: string;
|
|
@@ -58,8 +56,8 @@ export interface StagedDoc {
|
|
|
58
56
|
* 到临时名,再在 SQLite 事务里删记录:事务失败就把文件改回来,事务成功才真正
|
|
59
57
|
* `unlink`。这样任何时刻都不会出现「记录已经没了、正式路径上的文件还在」的悬挂态。
|
|
60
58
|
*
|
|
61
|
-
* @param root -
|
|
62
|
-
* @param relative -
|
|
59
|
+
* @param root - 文档根目录
|
|
60
|
+
* @param relative - 文档相对路径
|
|
63
61
|
* @returns 暂存信息;文件本来就不存在时返回 `null`
|
|
64
62
|
*/
|
|
65
63
|
export declare function stageDocDeletion(root: string, relative: string): StagedDoc | null;
|
package/lib/store/docs.js
CHANGED
|
@@ -1,23 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* 文档落盘:正文不进 SQLite
|
|
2
|
+
* 文档落盘:正文不进 SQLite,只记文档根内的相对路径。
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* 目录结构(相对文档根,默认 `$DSH_HOME/xirang-ai/dsh-teamflow/docs`):
|
|
5
5
|
*
|
|
6
|
-
* .
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* bugs/<bug_id>/bug.md 缺陷正文(描述、复现、期望、实际、环境)
|
|
6
|
+
* requirements/<req_id>/requirement.md 需求 + 概要设计正文
|
|
7
|
+
* requirements/<req_id>/docs/<doc_id>.md 详细设计文档(一份需求可以有很多份)
|
|
8
|
+
* stories/<sty_id>/story.md 故事说明 + 验收标准
|
|
9
|
+
* bugs/<bug_id>/bug.md 缺陷正文(描述、复现、期望、实际、环境)
|
|
11
10
|
*
|
|
12
11
|
* 所有写操作走「临时文件 + rename」,避免中途崩溃留下半截文档。
|
|
13
12
|
*/
|
|
14
13
|
import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmdirSync, rmSync, writeFileSync, } from 'node:fs';
|
|
15
|
-
import { dirname, isAbsolute,
|
|
14
|
+
import { dirname, isAbsolute, posix, resolve, sep, win32 } from 'node:path';
|
|
16
15
|
import { describeError } from "../errors.js";
|
|
17
|
-
/** 文档根目录名,工作区内的隐藏目录。 */
|
|
18
|
-
export const DOC_ROOT = '.xirang-ai/dsh-teamflow';
|
|
19
16
|
/**
|
|
20
|
-
*
|
|
17
|
+
* 计算一份文档在文档根内的相对路径。
|
|
21
18
|
*
|
|
22
19
|
* @param kind - 文档种类
|
|
23
20
|
* @param ids - 该种类需要的标识(designDoc 需要需求 id 与文档 id)
|
|
@@ -27,17 +24,17 @@ export function docPath(kind, ids) {
|
|
|
27
24
|
const id = requireId(ids.id);
|
|
28
25
|
switch (kind) {
|
|
29
26
|
case 'requirement':
|
|
30
|
-
return
|
|
27
|
+
return `requirements/${id}/requirement.md`;
|
|
31
28
|
case 'story':
|
|
32
|
-
return
|
|
29
|
+
return `stories/${id}/story.md`;
|
|
33
30
|
case 'bug':
|
|
34
|
-
return
|
|
31
|
+
return `bugs/${id}/bug.md`;
|
|
35
32
|
case 'designDoc':
|
|
36
|
-
return
|
|
33
|
+
return `requirements/${requireId(ids.requirementId)}/docs/${id}.md`;
|
|
37
34
|
}
|
|
38
35
|
}
|
|
39
36
|
/**
|
|
40
|
-
*
|
|
37
|
+
* 把相对路径拼到文档根上;只接受插件自己生成的、以 `/` 分隔的文档相对路径。
|
|
41
38
|
*
|
|
42
39
|
* 拒绝 POSIX 绝对路径、Windows 盘符/UNC/反斜杠路径与任何 `..`,最后再做一次
|
|
43
40
|
* 词法 containment 校验,确保结果仍落在 `root` 之内。
|
|
@@ -46,10 +43,10 @@ export function resolveDocPath(root, relative) {
|
|
|
46
43
|
if (relative.length === 0)
|
|
47
44
|
throw new Error('teamflow: document path is empty');
|
|
48
45
|
if (relative.includes('\0') || relative.includes('\\')) {
|
|
49
|
-
throw new Error(`teamflow: document path must be
|
|
46
|
+
throw new Error(`teamflow: document path must be relative to the document root, got "${relative}"`);
|
|
50
47
|
}
|
|
51
48
|
if (isAbsolute(relative) || win32.isAbsolute(relative) || /^[A-Za-z]:/.test(relative)) {
|
|
52
|
-
throw new Error(`teamflow: document path must be
|
|
49
|
+
throw new Error(`teamflow: document path must be relative to the document root, got "${relative}"`);
|
|
53
50
|
}
|
|
54
51
|
const segments = relative.split('/');
|
|
55
52
|
if (segments.some((part) => part === '..' || part === '.' || part.length === 0)) {
|
|
@@ -58,27 +55,23 @@ export function resolveDocPath(root, relative) {
|
|
|
58
55
|
const base = resolve(root);
|
|
59
56
|
const absolute = resolve(base, ...segments);
|
|
60
57
|
if (absolute !== base && !absolute.startsWith(`${base}${sep}`)) {
|
|
61
|
-
throw new Error(`teamflow: document path escapes the
|
|
58
|
+
throw new Error(`teamflow: document path escapes the document root, got "${relative}"`);
|
|
62
59
|
}
|
|
63
60
|
return absolute;
|
|
64
61
|
}
|
|
65
|
-
/**
|
|
62
|
+
/** 文档相对路径的展示形式(日志与工具结果用,始终 `/`)。 */
|
|
66
63
|
export function displayPath(relative) {
|
|
67
64
|
return relative.split(sep).join(posix.sep);
|
|
68
65
|
}
|
|
69
66
|
/**
|
|
70
|
-
*
|
|
71
|
-
*
|
|
67
|
+
* 首次为文档根准备共享目录:只建 `requirements/`、`stories/`、`bugs/`,
|
|
68
|
+
* 不碰工作区,也不写 `.gitignore`。
|
|
72
69
|
*
|
|
73
|
-
* @param root -
|
|
70
|
+
* @param root - 文档根目录(绝对路径)
|
|
74
71
|
* @returns 实际创建的目录(相对路径),便于工具结果里说明
|
|
75
72
|
*/
|
|
76
73
|
export function ensureDocTree(root) {
|
|
77
|
-
const dirs = [
|
|
78
|
-
`${DOC_ROOT}/requirements`,
|
|
79
|
-
`${DOC_ROOT}/stories`,
|
|
80
|
-
`${DOC_ROOT}/bugs`,
|
|
81
|
-
];
|
|
74
|
+
const dirs = ['requirements', 'stories', 'bugs'];
|
|
82
75
|
const created = [];
|
|
83
76
|
for (const dir of dirs) {
|
|
84
77
|
const absolute = resolveDocPath(root, dir);
|
|
@@ -87,11 +80,6 @@ export function ensureDocTree(root) {
|
|
|
87
80
|
created.push(dir);
|
|
88
81
|
}
|
|
89
82
|
}
|
|
90
|
-
const localIgnore = resolveDocPath(root, `${DOC_ROOT}/.gitignore`);
|
|
91
|
-
if (!existsSync(localIgnore)) {
|
|
92
|
-
writeFileSync(localIgnore, '*\n', 'utf8');
|
|
93
|
-
}
|
|
94
|
-
appendWorkspaceGitignore(root);
|
|
95
83
|
return created;
|
|
96
84
|
}
|
|
97
85
|
/**
|
|
@@ -144,8 +132,8 @@ let stagedSequence = 0;
|
|
|
144
132
|
* 到临时名,再在 SQLite 事务里删记录:事务失败就把文件改回来,事务成功才真正
|
|
145
133
|
* `unlink`。这样任何时刻都不会出现「记录已经没了、正式路径上的文件还在」的悬挂态。
|
|
146
134
|
*
|
|
147
|
-
* @param root -
|
|
148
|
-
* @param relative -
|
|
135
|
+
* @param root - 文档根目录
|
|
136
|
+
* @param relative - 文档相对路径
|
|
149
137
|
* @returns 暂存信息;文件本来就不存在时返回 `null`
|
|
150
138
|
*/
|
|
151
139
|
export function stageDocDeletion(root, relative) {
|
|
@@ -182,10 +170,10 @@ export function dropStagedDoc(staged) {
|
|
|
182
170
|
* 只删空目录:目录里还有别的东西时立刻停下,避免误删用户手工放进来的文件。
|
|
183
171
|
*/
|
|
184
172
|
export function pruneEmptyDirectories(root, relative) {
|
|
185
|
-
const boundary =
|
|
173
|
+
const boundary = resolve(root);
|
|
186
174
|
let directory = dirname(resolveDocPath(root, relative));
|
|
187
175
|
// Stop before the shared `requirements/`, `stories/`, `bugs/` roots: those
|
|
188
|
-
// belong to the
|
|
176
|
+
// belong to the document tree, not to the record being deleted.
|
|
189
177
|
while (directory !== boundary && dirname(directory) !== boundary && directory.startsWith(`${boundary}${sep}`)) {
|
|
190
178
|
try {
|
|
191
179
|
if (readdirSync(directory).length > 0)
|
|
@@ -211,18 +199,3 @@ function requireId(id) {
|
|
|
211
199
|
}
|
|
212
200
|
return id;
|
|
213
201
|
}
|
|
214
|
-
/** 把 `.xirang-ai/` 追加到工作区 `.gitignore`;没有该文件就创建一个只含这一行的。 */
|
|
215
|
-
function appendWorkspaceGitignore(root) {
|
|
216
|
-
const ignore = join(root, '.gitignore');
|
|
217
|
-
const entry = '.xirang-ai/';
|
|
218
|
-
if (!existsSync(ignore)) {
|
|
219
|
-
writeFileSync(ignore, `${entry}\n`, 'utf8');
|
|
220
|
-
return;
|
|
221
|
-
}
|
|
222
|
-
const current = readFileSync(ignore, 'utf8');
|
|
223
|
-
const lines = current.split('\n').map((line) => line.trim());
|
|
224
|
-
if (lines.includes(entry) || lines.includes('.xirang-ai'))
|
|
225
|
-
return;
|
|
226
|
-
const suffix = current.endsWith('\n') || current.length === 0 ? '' : '\n';
|
|
227
|
-
writeFileSync(ignore, `${current}${suffix}${entry}\n`, 'utf8');
|
|
228
|
-
}
|
package/lib/store/model.d.ts
CHANGED
|
@@ -198,6 +198,27 @@ export interface RequirementSession {
|
|
|
198
198
|
readonly linkedAt: string | null;
|
|
199
199
|
readonly createdAt: string;
|
|
200
200
|
}
|
|
201
|
+
/** Which kind of record a discussion conversation belongs to. */
|
|
202
|
+
export declare const DISCUSSION_SUBJECT_TYPES: readonly ["requirement", "story", "bug"];
|
|
203
|
+
export type DiscussionSubjectType = (typeof DISCUSSION_SUBJECT_TYPES)[number];
|
|
204
|
+
/** Discussion kind used by stories and defects; requirements keep their own kinds. */
|
|
205
|
+
export declare const GENERAL_DISCUSSION_KIND = "discuss";
|
|
206
|
+
/**
|
|
207
|
+
* One conversation registered against a requirement, story or defect.
|
|
208
|
+
*
|
|
209
|
+
* Rows start unconfirmed (`linkedAt` null) when the browser prepares a
|
|
210
|
+
* discussion and become visible once the Agent confirms with `linkedAt`.
|
|
211
|
+
*/
|
|
212
|
+
export interface DiscussionSession {
|
|
213
|
+
readonly subjectType: DiscussionSubjectType;
|
|
214
|
+
readonly subjectId: string;
|
|
215
|
+
readonly sessionId: string;
|
|
216
|
+
/** `create` / `design` / `detail` / `split` for requirements, `discuss` otherwise. */
|
|
217
|
+
readonly kind: string;
|
|
218
|
+
readonly preparedAt: string | null;
|
|
219
|
+
readonly linkedAt: string | null;
|
|
220
|
+
readonly createdAt: string;
|
|
221
|
+
}
|
|
201
222
|
/**
|
|
202
223
|
* One consistent read of every table the workbench renders.
|
|
203
224
|
*
|
|
@@ -214,6 +235,8 @@ export interface BoardRows {
|
|
|
214
235
|
readonly docStories: readonly DocStoryLink[];
|
|
215
236
|
/** Requirement conversations, newest first; design rows may still be unconfirmed. */
|
|
216
237
|
readonly requirementSessions: readonly RequirementSession[];
|
|
238
|
+
/** Confirmed story and defect discussions, newest first. */
|
|
239
|
+
readonly discussionSessions: readonly DiscussionSession[];
|
|
217
240
|
}
|
|
218
241
|
/** Input for {@link WorkflowStore.createDesignDoc}. */
|
|
219
242
|
export interface CreateDesignDocInput {
|