@xirang-ai/dsh-teamflow 0.1.0 → 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 +18 -14
- package/README.md +18 -14
- package/RELEASE.md +9 -9
- package/cordis.patch.yml +2 -0
- package/lib/client.js +494 -153
- 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 +5 -1
- package/locale/zh.json +5 -1
- package/package.json +4 -4
package/lib/index.d.ts
CHANGED
|
@@ -16,6 +16,11 @@ export interface Config {
|
|
|
16
16
|
* which is the only location this plugin owns.
|
|
17
17
|
*/
|
|
18
18
|
dataDir: string;
|
|
19
|
+
/**
|
|
20
|
+
* Document root override. Empty uses `<dataDir>/docs`, next to the database;
|
|
21
|
+
* relative paths resolve against the working directory and `~/` is expanded.
|
|
22
|
+
*/
|
|
23
|
+
docsDir: string;
|
|
19
24
|
/** SQLite busy timeout in milliseconds; a second instance waits this long for the write lock. */
|
|
20
25
|
busyTimeoutMs: number;
|
|
21
26
|
}
|
|
@@ -32,7 +37,8 @@ export declare function apply(ctx: Context, config: Config): void;
|
|
|
32
37
|
export type { TeamflowDatabase, StartupReport, Role } from './store/db.ts';
|
|
33
38
|
export type { SchemaComment } from './store/schema.ts';
|
|
34
39
|
export { WorkflowStore } from './store/workflow.ts';
|
|
35
|
-
export {
|
|
40
|
+
export { docPath, ensureDocTree, resolveDocPath } from './store/docs.ts';
|
|
41
|
+
export { DOCS_DIRNAME, defaultDocsDir, resolveDocsDir } from './store/paths.ts';
|
|
36
42
|
export type { DocKind as DocFileKind } from './store/docs.ts';
|
|
37
43
|
export type { WorkflowStoreOptions } from './store/workflow.ts';
|
|
38
44
|
export { WorkflowError } from './store/model.ts';
|
package/lib/index.js
CHANGED
|
@@ -7,6 +7,9 @@ export const Config = Schema.object({
|
|
|
7
7
|
dataDir: Schema.string()
|
|
8
8
|
.default('')
|
|
9
9
|
.description('Workbench data directory. Empty uses $DSH_HOME/xirang-ai/dsh-teamflow.'),
|
|
10
|
+
docsDir: Schema.string()
|
|
11
|
+
.default('')
|
|
12
|
+
.description('Document root for requirement, design, story and bug bodies. Empty uses <dataDir>/docs; relative paths and ~/ are supported.'),
|
|
10
13
|
busyTimeoutMs: Schema.number()
|
|
11
14
|
.min(0)
|
|
12
15
|
.max(60_000)
|
|
@@ -28,11 +31,13 @@ export function apply(ctx, config) {
|
|
|
28
31
|
error: (message) => logger.error(message),
|
|
29
32
|
};
|
|
30
33
|
ctx.effect(() => {
|
|
31
|
-
// Document bodies live
|
|
32
|
-
//
|
|
33
|
-
//
|
|
34
|
+
// Document bodies live below the plugin-owned document root, not the
|
|
35
|
+
// database and not the user's workspace; the panel reads them by
|
|
36
|
+
// `content_path` relative to that one root. The registry stays wired for
|
|
37
|
+
// workspace display only.
|
|
34
38
|
const database = openTeamflowDatabase({
|
|
35
39
|
dataDir: config.dataDir,
|
|
40
|
+
docsDir: config.docsDir,
|
|
36
41
|
busyTimeoutMs: config.busyTimeoutMs,
|
|
37
42
|
// Resolved lazily: the optional registry may register after this effect runs.
|
|
38
43
|
workspaceRoot: (workspaceId) => ctx.get('workspaceRegistry')?.get(workspaceId)?.path,
|
|
@@ -66,6 +71,7 @@ export function apply(ctx, config) {
|
|
|
66
71
|
}, 'teamflow: workbench database');
|
|
67
72
|
}
|
|
68
73
|
export { WorkflowStore } from "./store/workflow.js";
|
|
69
|
-
export {
|
|
74
|
+
export { docPath, ensureDocTree, resolveDocPath } from "./store/docs.js";
|
|
75
|
+
export { DOCS_DIRNAME, defaultDocsDir, resolveDocsDir } from "./store/paths.js";
|
|
70
76
|
export { WorkflowError } from "./store/model.js";
|
|
71
77
|
export { MAX_DELETE_TARGETS } from "./store/workflow.js";
|
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;
|