aifastdb-devplan 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +112 -0
- package/dist/dev-plan-store.d.ts +656 -0
- package/dist/dev-plan-store.d.ts.map +1 -0
- package/dist/dev-plan-store.js +1685 -0
- package/dist/dev-plan-store.js.map +1 -0
- package/dist/index.d.ts +30 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +39 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp-server/index.d.ts +26 -0
- package/dist/mcp-server/index.d.ts.map +1 -0
- package/dist/mcp-server/index.js +1004 -0
- package/dist/mcp-server/index.js.map +1 -0
- package/package.json +47 -0
|
@@ -0,0 +1,1685 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* DevPlanStore — 通用开发计划管理系统
|
|
4
|
+
*
|
|
5
|
+
* 以 FEDERATED_DB_DEVELOPMENT_PLAN.md 的结构为蓝本,构建跨项目通用的
|
|
6
|
+
* "开发计划文档管理 + 分层任务管理" 标准化能力。
|
|
7
|
+
*
|
|
8
|
+
* 特性:
|
|
9
|
+
* - 11 种标准文档片段类型(overview, api_design, technical_notes 等)
|
|
10
|
+
* - 主任务 (MainTask) + 子任务 (SubTask) 两级任务层级
|
|
11
|
+
* - 子任务与 Cursor TodoList 一一对应
|
|
12
|
+
* - 完成任务时自动更新进度和关联文档
|
|
13
|
+
* - 基于 EnhancedDocumentStore 的 JSONL 持久化
|
|
14
|
+
*
|
|
15
|
+
* 使用方式:
|
|
16
|
+
* ```typescript
|
|
17
|
+
* import { DevPlanStore, createDevPlan } from 'aifastdb-devplan';
|
|
18
|
+
*
|
|
19
|
+
* const plan = createDevPlan('federation-db');
|
|
20
|
+
* plan.saveSection({ projectName: 'federation-db', section: 'overview', ... });
|
|
21
|
+
* plan.createMainTask({ projectName: 'federation-db', taskId: 'phase-7', ... });
|
|
22
|
+
* plan.addSubTask({ projectName: 'federation-db', taskId: 'T7.1', parentTaskId: 'phase-7', ... });
|
|
23
|
+
* plan.completeSubTask('T7.1'); // 自动更新进度
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
27
|
+
if (k2 === undefined) k2 = k;
|
|
28
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
29
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
30
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
31
|
+
}
|
|
32
|
+
Object.defineProperty(o, k2, desc);
|
|
33
|
+
}) : (function(o, m, k, k2) {
|
|
34
|
+
if (k2 === undefined) k2 = k;
|
|
35
|
+
o[k2] = m[k];
|
|
36
|
+
}));
|
|
37
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
38
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
39
|
+
}) : function(o, v) {
|
|
40
|
+
o["default"] = v;
|
|
41
|
+
});
|
|
42
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
43
|
+
var ownKeys = function(o) {
|
|
44
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
45
|
+
var ar = [];
|
|
46
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
47
|
+
return ar;
|
|
48
|
+
};
|
|
49
|
+
return ownKeys(o);
|
|
50
|
+
};
|
|
51
|
+
return function (mod) {
|
|
52
|
+
if (mod && mod.__esModule) return mod;
|
|
53
|
+
var result = {};
|
|
54
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
55
|
+
__setModuleDefault(result, mod);
|
|
56
|
+
return result;
|
|
57
|
+
};
|
|
58
|
+
})();
|
|
59
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
60
|
+
exports.SECTION_DESCRIPTIONS = exports.ALL_SECTIONS = exports.DevPlanStore = void 0;
|
|
61
|
+
exports.createDevPlan = createDevPlan;
|
|
62
|
+
exports.listDevPlans = listDevPlans;
|
|
63
|
+
const aifastdb_1 = require("aifastdb");
|
|
64
|
+
const os = __importStar(require("os"));
|
|
65
|
+
const path = __importStar(require("path"));
|
|
66
|
+
// ============================================================================
|
|
67
|
+
// Helper Functions
|
|
68
|
+
// ============================================================================
|
|
69
|
+
/**
|
|
70
|
+
* 根据章节类型返回重要性分数
|
|
71
|
+
*/
|
|
72
|
+
function sectionImportance(section) {
|
|
73
|
+
const importanceMap = {
|
|
74
|
+
overview: 1.0,
|
|
75
|
+
core_concepts: 0.95,
|
|
76
|
+
api_design: 0.9,
|
|
77
|
+
file_structure: 0.7,
|
|
78
|
+
config: 0.7,
|
|
79
|
+
examples: 0.6,
|
|
80
|
+
technical_notes: 0.8,
|
|
81
|
+
api_endpoints: 0.75,
|
|
82
|
+
milestones: 0.85,
|
|
83
|
+
changelog: 0.5,
|
|
84
|
+
custom: 0.6,
|
|
85
|
+
};
|
|
86
|
+
return importanceMap[section] ?? 0.6;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* 获取默认的 DevPlan 存储基础路径
|
|
90
|
+
*
|
|
91
|
+
* 优先级:
|
|
92
|
+
* 1. AIFASTDB_DEVPLAN_PATH 环境变量(显式指定)
|
|
93
|
+
* 2. 项目内 .devplan/ 目录(天然跟随 Git 版本管理)
|
|
94
|
+
* 3. 回退到用户目录 ~/.aifastdb/dev-plans/(兜底)
|
|
95
|
+
*/
|
|
96
|
+
function getDefaultBasePath() {
|
|
97
|
+
if (process.env.AIFASTDB_DEVPLAN_PATH) {
|
|
98
|
+
return process.env.AIFASTDB_DEVPLAN_PATH;
|
|
99
|
+
}
|
|
100
|
+
// 尝试定位项目根目录(查找 .git 或 package.json 所在目录)
|
|
101
|
+
const projectRoot = findProjectRoot();
|
|
102
|
+
if (projectRoot) {
|
|
103
|
+
return path.join(projectRoot, '.devplan');
|
|
104
|
+
}
|
|
105
|
+
// 兜底:用户目录
|
|
106
|
+
return path.join(os.homedir(), '.aifastdb', 'dev-plans');
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* 从当前工作目录向上查找项目根目录
|
|
110
|
+
* 通过 .git 目录或 package.json 文件来判断
|
|
111
|
+
*/
|
|
112
|
+
function findProjectRoot() {
|
|
113
|
+
const fs = require('fs');
|
|
114
|
+
let dir = process.cwd();
|
|
115
|
+
const root = path.parse(dir).root;
|
|
116
|
+
while (dir !== root) {
|
|
117
|
+
if (fs.existsSync(path.join(dir, '.git')) || fs.existsSync(path.join(dir, 'package.json'))) {
|
|
118
|
+
return dir;
|
|
119
|
+
}
|
|
120
|
+
dir = path.dirname(dir);
|
|
121
|
+
}
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
// ============================================================================
|
|
125
|
+
// DevPlanStore Implementation
|
|
126
|
+
// ============================================================================
|
|
127
|
+
/**
|
|
128
|
+
* 通用开发计划存储
|
|
129
|
+
*
|
|
130
|
+
* 管理项目的开发计划文档和任务,使用两个 EnhancedDocumentStore 实例:
|
|
131
|
+
* - docStore: 文档片段 (Markdown 内容)
|
|
132
|
+
* - taskStore: 任务 (主任务 + 子任务层级)
|
|
133
|
+
*/
|
|
134
|
+
class DevPlanStore {
|
|
135
|
+
constructor(projectName, config) {
|
|
136
|
+
this.projectName = projectName;
|
|
137
|
+
this.docStore = new aifastdb_1.EnhancedDocumentStore(config.documentPath, (0, aifastdb_1.documentStoreProductionConfig)());
|
|
138
|
+
this.taskStore = new aifastdb_1.EnhancedDocumentStore(config.taskPath, (0, aifastdb_1.documentStoreProductionConfig)());
|
|
139
|
+
this.moduleStore = new aifastdb_1.EnhancedDocumentStore(config.modulePath, (0, aifastdb_1.documentStoreProductionConfig)());
|
|
140
|
+
}
|
|
141
|
+
// ==========================================================================
|
|
142
|
+
// Document Section Operations
|
|
143
|
+
// ==========================================================================
|
|
144
|
+
/**
|
|
145
|
+
* 保存文档片段
|
|
146
|
+
*
|
|
147
|
+
* 如果同 section(+subSection)已存在,会覆盖旧版本。
|
|
148
|
+
*/
|
|
149
|
+
saveSection(input) {
|
|
150
|
+
// 删除已有同类型文档,并确保新版本时间戳严格递增
|
|
151
|
+
const existing = this.getSection(input.section, input.subSection);
|
|
152
|
+
if (existing) {
|
|
153
|
+
this.deleteAndEnsureTimestampAdvance(this.docStore, existing.id);
|
|
154
|
+
}
|
|
155
|
+
const version = input.version || '1.0.0';
|
|
156
|
+
const now = Date.now();
|
|
157
|
+
const finalModuleId = input.moduleId || existing?.moduleId;
|
|
158
|
+
const tags = [
|
|
159
|
+
`plan:${this.projectName}`,
|
|
160
|
+
`section:${input.section}`,
|
|
161
|
+
...(input.subSection ? [`sub:${input.subSection}`] : []),
|
|
162
|
+
`ver:${version}`,
|
|
163
|
+
];
|
|
164
|
+
if (finalModuleId) {
|
|
165
|
+
tags.push(`module:${finalModuleId}`);
|
|
166
|
+
}
|
|
167
|
+
const docInput = {
|
|
168
|
+
content: input.content,
|
|
169
|
+
contentType: aifastdb_1.ContentType.Text,
|
|
170
|
+
tags,
|
|
171
|
+
metadata: {
|
|
172
|
+
projectName: this.projectName,
|
|
173
|
+
section: input.section,
|
|
174
|
+
title: input.title,
|
|
175
|
+
version,
|
|
176
|
+
subSection: input.subSection || null,
|
|
177
|
+
relatedSections: input.relatedSections || [],
|
|
178
|
+
moduleId: finalModuleId || null,
|
|
179
|
+
createdAt: existing?.createdAt || now,
|
|
180
|
+
updatedAt: now,
|
|
181
|
+
},
|
|
182
|
+
importance: sectionImportance(input.section),
|
|
183
|
+
};
|
|
184
|
+
const id = this.docStore.put(docInput);
|
|
185
|
+
this.docStore.flush();
|
|
186
|
+
return id;
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* 获取文档片段
|
|
190
|
+
*/
|
|
191
|
+
getSection(section, subSection) {
|
|
192
|
+
const planTag = `plan:${this.projectName}`;
|
|
193
|
+
const sectionTag = `section:${section}`;
|
|
194
|
+
const docs = this.docStore.findByTag(planTag)
|
|
195
|
+
.filter((doc) => doc.tags.includes(sectionTag));
|
|
196
|
+
let filtered = docs;
|
|
197
|
+
if (subSection) {
|
|
198
|
+
const subTag = `sub:${subSection}`;
|
|
199
|
+
filtered = docs.filter((doc) => doc.tags.includes(subTag));
|
|
200
|
+
}
|
|
201
|
+
else if (section !== 'technical_notes' && section !== 'custom') {
|
|
202
|
+
// 非多子文档类型,排除有 sub: tag 的
|
|
203
|
+
filtered = docs.filter((doc) => !doc.tags.some((t) => t.startsWith('sub:')));
|
|
204
|
+
}
|
|
205
|
+
if (filtered.length === 0)
|
|
206
|
+
return null;
|
|
207
|
+
// 返回最新版本(以 metadata.updatedAt 为判定依据)
|
|
208
|
+
const latest = filtered.sort((a, b) => this.getDocUpdatedAt(b) - this.getDocUpdatedAt(a))[0];
|
|
209
|
+
return this.docToDevPlanDoc(latest);
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* 列出项目的所有文档片段
|
|
213
|
+
*
|
|
214
|
+
* 对同一 section(+subSection) 的多个历史版本做去重,仅保留最新版。
|
|
215
|
+
*/
|
|
216
|
+
listSections() {
|
|
217
|
+
const planTag = `plan:${this.projectName}`;
|
|
218
|
+
const docs = this.docStore.findByTag(planTag);
|
|
219
|
+
// 按 section+subSection 去重,保留最新版本(以 metadata.updatedAt 判定)
|
|
220
|
+
const latestMap = new Map();
|
|
221
|
+
for (const doc of docs) {
|
|
222
|
+
const sectionTag = doc.tags.find((t) => t.startsWith('section:'));
|
|
223
|
+
const subTag = doc.tags.find((t) => t.startsWith('sub:'));
|
|
224
|
+
const key = `${sectionTag || 'unknown'}|${subTag || ''}`;
|
|
225
|
+
const existing = latestMap.get(key);
|
|
226
|
+
if (!existing || this.getDocUpdatedAt(doc) > this.getDocUpdatedAt(existing)) {
|
|
227
|
+
latestMap.set(key, doc);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return Array.from(latestMap.values()).map((doc) => this.docToDevPlanDoc(doc));
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* 更新文档片段内容
|
|
234
|
+
*/
|
|
235
|
+
updateSection(section, content, subSection) {
|
|
236
|
+
const existing = this.getSection(section, subSection);
|
|
237
|
+
if (!existing) {
|
|
238
|
+
throw new Error(`Section "${section}"${subSection ? ` (${subSection})` : ''} not found for project "${this.projectName}"`);
|
|
239
|
+
}
|
|
240
|
+
return this.saveSection({
|
|
241
|
+
projectName: this.projectName,
|
|
242
|
+
section,
|
|
243
|
+
title: existing.title,
|
|
244
|
+
content,
|
|
245
|
+
version: existing.version,
|
|
246
|
+
subSection,
|
|
247
|
+
relatedSections: existing.relatedSections,
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* 搜索文档片段
|
|
252
|
+
*
|
|
253
|
+
* 先对历史版本去重(同 listSections),再做关键词过滤。
|
|
254
|
+
*/
|
|
255
|
+
searchSections(query, limit = 10) {
|
|
256
|
+
const planTag = `plan:${this.projectName}`;
|
|
257
|
+
const allDocs = this.docStore.findByTag(planTag);
|
|
258
|
+
// 按 section+subSection 去重,保留最新版本(以 metadata.updatedAt 判定)
|
|
259
|
+
const latestMap = new Map();
|
|
260
|
+
for (const doc of allDocs) {
|
|
261
|
+
const sectionTag = doc.tags.find((t) => t.startsWith('section:'));
|
|
262
|
+
const subTag = doc.tags.find((t) => t.startsWith('sub:'));
|
|
263
|
+
const key = `${sectionTag || 'unknown'}|${subTag || ''}`;
|
|
264
|
+
const existing = latestMap.get(key);
|
|
265
|
+
if (!existing || this.getDocUpdatedAt(doc) > this.getDocUpdatedAt(existing)) {
|
|
266
|
+
latestMap.set(key, doc);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
const queryLower = query.toLowerCase();
|
|
270
|
+
return Array.from(latestMap.values())
|
|
271
|
+
.filter((doc) => doc.content.toLowerCase().includes(queryLower) ||
|
|
272
|
+
(doc.metadata?.title || '').toLowerCase().includes(queryLower))
|
|
273
|
+
.slice(0, limit)
|
|
274
|
+
.map((doc) => this.docToDevPlanDoc(doc));
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* 删除文档片段
|
|
278
|
+
*/
|
|
279
|
+
deleteSection(section, subSection) {
|
|
280
|
+
const existing = this.getSection(section, subSection);
|
|
281
|
+
if (!existing)
|
|
282
|
+
return false;
|
|
283
|
+
this.docStore.delete(existing.id);
|
|
284
|
+
this.docStore.flush();
|
|
285
|
+
return true;
|
|
286
|
+
}
|
|
287
|
+
// ==========================================================================
|
|
288
|
+
// Main Task Operations
|
|
289
|
+
// ==========================================================================
|
|
290
|
+
/**
|
|
291
|
+
* 创建主任务(开发阶段)
|
|
292
|
+
*/
|
|
293
|
+
createMainTask(input) {
|
|
294
|
+
// 检查是否已存在
|
|
295
|
+
const existing = this.getMainTask(input.taskId);
|
|
296
|
+
if (existing) {
|
|
297
|
+
throw new Error(`Main task "${input.taskId}" already exists for project "${this.projectName}"`);
|
|
298
|
+
}
|
|
299
|
+
const now = Date.now();
|
|
300
|
+
const taskData = {
|
|
301
|
+
taskId: input.taskId,
|
|
302
|
+
title: input.title,
|
|
303
|
+
priority: input.priority,
|
|
304
|
+
description: input.description || '',
|
|
305
|
+
estimatedHours: input.estimatedHours || 0,
|
|
306
|
+
relatedSections: input.relatedSections || [],
|
|
307
|
+
totalSubtasks: 0,
|
|
308
|
+
completedSubtasks: 0,
|
|
309
|
+
};
|
|
310
|
+
const tags = [
|
|
311
|
+
`plan:${this.projectName}`,
|
|
312
|
+
'type:main-task',
|
|
313
|
+
`mtask:${input.taskId}`,
|
|
314
|
+
`priority:${input.priority}`,
|
|
315
|
+
'status:pending',
|
|
316
|
+
];
|
|
317
|
+
if (input.moduleId) {
|
|
318
|
+
tags.push(`module:${input.moduleId}`);
|
|
319
|
+
}
|
|
320
|
+
const docInput = {
|
|
321
|
+
content: JSON.stringify(taskData),
|
|
322
|
+
contentType: aifastdb_1.ContentType.Text,
|
|
323
|
+
tags,
|
|
324
|
+
metadata: {
|
|
325
|
+
projectName: this.projectName,
|
|
326
|
+
taskId: input.taskId,
|
|
327
|
+
status: 'pending',
|
|
328
|
+
moduleId: input.moduleId || null,
|
|
329
|
+
createdAt: now,
|
|
330
|
+
updatedAt: now,
|
|
331
|
+
completedAt: null,
|
|
332
|
+
},
|
|
333
|
+
importance: input.priority === 'P0' ? 0.95 : input.priority === 'P1' ? 0.8 : 0.6,
|
|
334
|
+
};
|
|
335
|
+
const id = this.taskStore.put(docInput);
|
|
336
|
+
this.taskStore.flush();
|
|
337
|
+
return {
|
|
338
|
+
id,
|
|
339
|
+
projectName: this.projectName,
|
|
340
|
+
...taskData,
|
|
341
|
+
moduleId: input.moduleId,
|
|
342
|
+
status: 'pending',
|
|
343
|
+
createdAt: now,
|
|
344
|
+
updatedAt: now,
|
|
345
|
+
completedAt: null,
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* 幂等导入主任务(Upsert)
|
|
350
|
+
*
|
|
351
|
+
* - 如果主任务不存在 → 创建新任务
|
|
352
|
+
* - 如果主任务已存在 → 更新标题/描述/优先级等字段,但保留已有的更高级状态
|
|
353
|
+
* (例如已完成的任务不会被重置为 pending)
|
|
354
|
+
* - updatedAt 保证严格递增,不会与历史版本重复
|
|
355
|
+
*
|
|
356
|
+
* @param input 主任务输入
|
|
357
|
+
* @param options.preserveStatus 若为 true(默认),则不覆盖已完成的状态
|
|
358
|
+
* @param options.status 导入时的目标状态(默认 pending)
|
|
359
|
+
* @returns 创建或更新后的主任务
|
|
360
|
+
*/
|
|
361
|
+
upsertMainTask(input, options) {
|
|
362
|
+
const preserveStatus = options?.preserveStatus !== false; // 默认 true
|
|
363
|
+
const targetStatus = options?.status || 'pending';
|
|
364
|
+
const existing = this.getMainTask(input.taskId);
|
|
365
|
+
if (!existing) {
|
|
366
|
+
// 新建
|
|
367
|
+
const task = this.createMainTask(input);
|
|
368
|
+
// 如果目标状态不是 pending,更新状态
|
|
369
|
+
if (targetStatus !== 'pending') {
|
|
370
|
+
return this.updateMainTaskStatus(task.taskId, targetStatus) || task;
|
|
371
|
+
}
|
|
372
|
+
return task;
|
|
373
|
+
}
|
|
374
|
+
// 已存在 — 决定最终状态
|
|
375
|
+
let finalStatus = targetStatus;
|
|
376
|
+
if (preserveStatus) {
|
|
377
|
+
// 状态优先级: completed > in_progress > pending > cancelled
|
|
378
|
+
const statusPriority = {
|
|
379
|
+
cancelled: 0,
|
|
380
|
+
pending: 1,
|
|
381
|
+
in_progress: 2,
|
|
382
|
+
completed: 3,
|
|
383
|
+
};
|
|
384
|
+
if (statusPriority[existing.status] >= statusPriority[targetStatus]) {
|
|
385
|
+
finalStatus = existing.status; // 保留更高级状态
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
// 删除旧版本并确保时间戳递增
|
|
389
|
+
this.deleteAndEnsureTimestampAdvance(this.taskStore, existing.id);
|
|
390
|
+
const now = Date.now();
|
|
391
|
+
const completedAt = finalStatus === 'completed' ? (existing.completedAt || now) : null;
|
|
392
|
+
const finalModuleId = input.moduleId || existing.moduleId;
|
|
393
|
+
const taskData = {
|
|
394
|
+
taskId: input.taskId,
|
|
395
|
+
title: input.title,
|
|
396
|
+
priority: input.priority,
|
|
397
|
+
description: input.description || existing.description || '',
|
|
398
|
+
estimatedHours: input.estimatedHours || existing.estimatedHours || 0,
|
|
399
|
+
relatedSections: input.relatedSections || existing.relatedSections || [],
|
|
400
|
+
totalSubtasks: existing.totalSubtasks,
|
|
401
|
+
completedSubtasks: existing.completedSubtasks,
|
|
402
|
+
};
|
|
403
|
+
const tags = [
|
|
404
|
+
`plan:${this.projectName}`,
|
|
405
|
+
'type:main-task',
|
|
406
|
+
`mtask:${input.taskId}`,
|
|
407
|
+
`priority:${input.priority}`,
|
|
408
|
+
`status:${finalStatus}`,
|
|
409
|
+
];
|
|
410
|
+
if (finalModuleId) {
|
|
411
|
+
tags.push(`module:${finalModuleId}`);
|
|
412
|
+
}
|
|
413
|
+
const docInput = {
|
|
414
|
+
content: JSON.stringify(taskData),
|
|
415
|
+
contentType: aifastdb_1.ContentType.Text,
|
|
416
|
+
tags,
|
|
417
|
+
metadata: {
|
|
418
|
+
projectName: this.projectName,
|
|
419
|
+
taskId: input.taskId,
|
|
420
|
+
status: finalStatus,
|
|
421
|
+
moduleId: finalModuleId || null,
|
|
422
|
+
createdAt: existing.createdAt,
|
|
423
|
+
updatedAt: now,
|
|
424
|
+
completedAt,
|
|
425
|
+
},
|
|
426
|
+
importance: input.priority === 'P0' ? 0.95 : input.priority === 'P1' ? 0.8 : 0.6,
|
|
427
|
+
};
|
|
428
|
+
const id = this.taskStore.put(docInput);
|
|
429
|
+
this.taskStore.flush();
|
|
430
|
+
return {
|
|
431
|
+
...taskData,
|
|
432
|
+
id,
|
|
433
|
+
projectName: this.projectName,
|
|
434
|
+
moduleId: finalModuleId,
|
|
435
|
+
status: finalStatus,
|
|
436
|
+
createdAt: existing.createdAt,
|
|
437
|
+
updatedAt: now,
|
|
438
|
+
completedAt,
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
/**
|
|
442
|
+
* 获取主任务
|
|
443
|
+
*
|
|
444
|
+
* 由于 JSONL append-only 存储会保留历史版本,
|
|
445
|
+
* 需要按 metadata.updatedAt 降序取最新版本。
|
|
446
|
+
*/
|
|
447
|
+
getMainTask(taskId) {
|
|
448
|
+
const tag = `mtask:${taskId}`;
|
|
449
|
+
const docs = this.taskStore.findByTag(tag)
|
|
450
|
+
.filter((doc) => doc.tags.includes(`plan:${this.projectName}`));
|
|
451
|
+
if (docs.length === 0)
|
|
452
|
+
return null;
|
|
453
|
+
// 取最新版本(以 metadata.updatedAt 为判定依据)
|
|
454
|
+
const latest = docs.sort((a, b) => this.getDocUpdatedAt(b) - this.getDocUpdatedAt(a))[0];
|
|
455
|
+
return this.docToMainTask(latest);
|
|
456
|
+
}
|
|
457
|
+
/**
|
|
458
|
+
* 列出主任务
|
|
459
|
+
*
|
|
460
|
+
* 对同一 taskId 的多个历史版本做去重,仅保留最新版。
|
|
461
|
+
*/
|
|
462
|
+
listMainTasks(filter) {
|
|
463
|
+
let docs = this.taskStore.findByTag(`plan:${this.projectName}`)
|
|
464
|
+
.filter((doc) => doc.tags.includes('type:main-task'));
|
|
465
|
+
// 按 taskId 去重,仅保留最新版本(created_at 最大)
|
|
466
|
+
docs = this.deduplicateByTaskId(docs);
|
|
467
|
+
if (filter?.status) {
|
|
468
|
+
const statusTag = `status:${filter.status}`;
|
|
469
|
+
docs = docs.filter((doc) => doc.tags.includes(statusTag));
|
|
470
|
+
}
|
|
471
|
+
if (filter?.priority) {
|
|
472
|
+
const priorityTag = `priority:${filter.priority}`;
|
|
473
|
+
docs = docs.filter((doc) => doc.tags.includes(priorityTag));
|
|
474
|
+
}
|
|
475
|
+
if (filter?.moduleId) {
|
|
476
|
+
const moduleTag = `module:${filter.moduleId}`;
|
|
477
|
+
docs = docs.filter((doc) => doc.tags.includes(moduleTag));
|
|
478
|
+
}
|
|
479
|
+
return docs.map((doc) => this.docToMainTask(doc));
|
|
480
|
+
}
|
|
481
|
+
/**
|
|
482
|
+
* 更新主任务状态
|
|
483
|
+
*/
|
|
484
|
+
updateMainTaskStatus(taskId, status) {
|
|
485
|
+
const mainTask = this.getMainTask(taskId);
|
|
486
|
+
if (!mainTask)
|
|
487
|
+
return null;
|
|
488
|
+
this.deleteAndEnsureTimestampAdvance(this.taskStore, mainTask.id);
|
|
489
|
+
const now = Date.now();
|
|
490
|
+
const completedAt = status === 'completed' ? now : mainTask.completedAt;
|
|
491
|
+
const taskData = {
|
|
492
|
+
taskId: mainTask.taskId,
|
|
493
|
+
title: mainTask.title,
|
|
494
|
+
priority: mainTask.priority,
|
|
495
|
+
description: mainTask.description || '',
|
|
496
|
+
estimatedHours: mainTask.estimatedHours || 0,
|
|
497
|
+
relatedSections: mainTask.relatedSections || [],
|
|
498
|
+
totalSubtasks: mainTask.totalSubtasks,
|
|
499
|
+
completedSubtasks: mainTask.completedSubtasks,
|
|
500
|
+
};
|
|
501
|
+
const tags = [
|
|
502
|
+
`plan:${this.projectName}`,
|
|
503
|
+
'type:main-task',
|
|
504
|
+
`mtask:${mainTask.taskId}`,
|
|
505
|
+
`priority:${mainTask.priority}`,
|
|
506
|
+
`status:${status}`,
|
|
507
|
+
];
|
|
508
|
+
if (mainTask.moduleId) {
|
|
509
|
+
tags.push(`module:${mainTask.moduleId}`);
|
|
510
|
+
}
|
|
511
|
+
const docInput = {
|
|
512
|
+
content: JSON.stringify(taskData),
|
|
513
|
+
contentType: aifastdb_1.ContentType.Text,
|
|
514
|
+
tags,
|
|
515
|
+
metadata: {
|
|
516
|
+
projectName: this.projectName,
|
|
517
|
+
taskId: mainTask.taskId,
|
|
518
|
+
status,
|
|
519
|
+
moduleId: mainTask.moduleId || null,
|
|
520
|
+
createdAt: mainTask.createdAt,
|
|
521
|
+
updatedAt: now,
|
|
522
|
+
completedAt,
|
|
523
|
+
},
|
|
524
|
+
importance: mainTask.priority === 'P0' ? 0.95 : mainTask.priority === 'P1' ? 0.8 : 0.6,
|
|
525
|
+
};
|
|
526
|
+
const id = this.taskStore.put(docInput);
|
|
527
|
+
this.taskStore.flush();
|
|
528
|
+
return {
|
|
529
|
+
...mainTask,
|
|
530
|
+
id,
|
|
531
|
+
status,
|
|
532
|
+
updatedAt: now,
|
|
533
|
+
completedAt,
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
// ==========================================================================
|
|
537
|
+
// Sub Task Operations
|
|
538
|
+
// ==========================================================================
|
|
539
|
+
/**
|
|
540
|
+
* 添加子任务
|
|
541
|
+
*/
|
|
542
|
+
addSubTask(input) {
|
|
543
|
+
// 检查是否已存在
|
|
544
|
+
const existing = this.getSubTask(input.taskId);
|
|
545
|
+
if (existing) {
|
|
546
|
+
throw new Error(`Sub task "${input.taskId}" already exists for project "${this.projectName}"`);
|
|
547
|
+
}
|
|
548
|
+
// 验证父任务存在
|
|
549
|
+
const mainTask = this.getMainTask(input.parentTaskId);
|
|
550
|
+
if (!mainTask) {
|
|
551
|
+
throw new Error(`Parent main task "${input.parentTaskId}" not found for project "${this.projectName}"`);
|
|
552
|
+
}
|
|
553
|
+
const now = Date.now();
|
|
554
|
+
const taskData = {
|
|
555
|
+
taskId: input.taskId,
|
|
556
|
+
title: input.title,
|
|
557
|
+
estimatedHours: input.estimatedHours || 0,
|
|
558
|
+
relatedFiles: input.relatedFiles || [],
|
|
559
|
+
description: input.description || '',
|
|
560
|
+
};
|
|
561
|
+
const docInput = {
|
|
562
|
+
content: JSON.stringify(taskData),
|
|
563
|
+
contentType: aifastdb_1.ContentType.Text,
|
|
564
|
+
parentId: mainTask.id,
|
|
565
|
+
tags: [
|
|
566
|
+
`plan:${this.projectName}`,
|
|
567
|
+
'type:sub-task',
|
|
568
|
+
`stask:${input.taskId}`,
|
|
569
|
+
`parent:${input.parentTaskId}`,
|
|
570
|
+
'status:pending',
|
|
571
|
+
],
|
|
572
|
+
metadata: {
|
|
573
|
+
projectName: this.projectName,
|
|
574
|
+
taskId: input.taskId,
|
|
575
|
+
parentTaskId: input.parentTaskId,
|
|
576
|
+
status: 'pending',
|
|
577
|
+
createdAt: now,
|
|
578
|
+
updatedAt: now,
|
|
579
|
+
completedAt: null,
|
|
580
|
+
},
|
|
581
|
+
importance: 0.7,
|
|
582
|
+
};
|
|
583
|
+
const id = this.taskStore.put(docInput);
|
|
584
|
+
// 更新主任务的 totalSubtasks 计数
|
|
585
|
+
this.refreshMainTaskCounts(input.parentTaskId);
|
|
586
|
+
this.taskStore.flush();
|
|
587
|
+
return {
|
|
588
|
+
id,
|
|
589
|
+
projectName: this.projectName,
|
|
590
|
+
...taskData,
|
|
591
|
+
parentTaskId: input.parentTaskId,
|
|
592
|
+
status: 'pending',
|
|
593
|
+
createdAt: now,
|
|
594
|
+
updatedAt: now,
|
|
595
|
+
completedAt: null,
|
|
596
|
+
};
|
|
597
|
+
}
|
|
598
|
+
/**
|
|
599
|
+
* 幂等导入子任务(Upsert)
|
|
600
|
+
*
|
|
601
|
+
* - 如果子任务不存在 → 创建新子任务
|
|
602
|
+
* - 如果子任务已存在 → 更新标题/描述等字段,但保留已有的更高级状态
|
|
603
|
+
* (例如已完成的任务不会被重置为 pending)
|
|
604
|
+
* - updatedAt 保证严格递增,不会与历史版本重复
|
|
605
|
+
*
|
|
606
|
+
* @param input 子任务输入
|
|
607
|
+
* @param options.preserveStatus 若为 true(默认),则不覆盖已完成的状态
|
|
608
|
+
* @param options.status 导入时的目标状态(默认 pending)
|
|
609
|
+
* @returns 创建或更新后的子任务
|
|
610
|
+
*/
|
|
611
|
+
upsertSubTask(input, options) {
|
|
612
|
+
const preserveStatus = options?.preserveStatus !== false; // 默认 true
|
|
613
|
+
const targetStatus = options?.status || 'pending';
|
|
614
|
+
const existing = this.getSubTask(input.taskId);
|
|
615
|
+
if (!existing) {
|
|
616
|
+
// 新建(验证父任务存在)
|
|
617
|
+
const mainTask = this.getMainTask(input.parentTaskId);
|
|
618
|
+
if (!mainTask) {
|
|
619
|
+
throw new Error(`Parent main task "${input.parentTaskId}" not found for project "${this.projectName}"`);
|
|
620
|
+
}
|
|
621
|
+
const now = Date.now();
|
|
622
|
+
const taskData = {
|
|
623
|
+
taskId: input.taskId,
|
|
624
|
+
title: input.title,
|
|
625
|
+
estimatedHours: input.estimatedHours || 0,
|
|
626
|
+
relatedFiles: input.relatedFiles || [],
|
|
627
|
+
description: input.description || '',
|
|
628
|
+
};
|
|
629
|
+
const docInput = {
|
|
630
|
+
content: JSON.stringify(taskData),
|
|
631
|
+
contentType: aifastdb_1.ContentType.Text,
|
|
632
|
+
parentId: mainTask.id,
|
|
633
|
+
tags: [
|
|
634
|
+
`plan:${this.projectName}`,
|
|
635
|
+
'type:sub-task',
|
|
636
|
+
`stask:${input.taskId}`,
|
|
637
|
+
`parent:${input.parentTaskId}`,
|
|
638
|
+
`status:${targetStatus}`,
|
|
639
|
+
],
|
|
640
|
+
metadata: {
|
|
641
|
+
projectName: this.projectName,
|
|
642
|
+
taskId: input.taskId,
|
|
643
|
+
parentTaskId: input.parentTaskId,
|
|
644
|
+
status: targetStatus,
|
|
645
|
+
createdAt: now,
|
|
646
|
+
updatedAt: now,
|
|
647
|
+
completedAt: targetStatus === 'completed' ? now : null,
|
|
648
|
+
},
|
|
649
|
+
importance: 0.7,
|
|
650
|
+
};
|
|
651
|
+
const id = this.taskStore.put(docInput);
|
|
652
|
+
this.refreshMainTaskCounts(input.parentTaskId);
|
|
653
|
+
this.taskStore.flush();
|
|
654
|
+
return {
|
|
655
|
+
id,
|
|
656
|
+
projectName: this.projectName,
|
|
657
|
+
...taskData,
|
|
658
|
+
parentTaskId: input.parentTaskId,
|
|
659
|
+
status: targetStatus,
|
|
660
|
+
createdAt: now,
|
|
661
|
+
updatedAt: now,
|
|
662
|
+
completedAt: targetStatus === 'completed' ? now : null,
|
|
663
|
+
};
|
|
664
|
+
}
|
|
665
|
+
// 已存在 — 决定最终状态
|
|
666
|
+
let finalStatus = targetStatus;
|
|
667
|
+
if (preserveStatus) {
|
|
668
|
+
const statusPriority = {
|
|
669
|
+
cancelled: 0,
|
|
670
|
+
pending: 1,
|
|
671
|
+
in_progress: 2,
|
|
672
|
+
completed: 3,
|
|
673
|
+
};
|
|
674
|
+
if (statusPriority[existing.status] >= statusPriority[targetStatus]) {
|
|
675
|
+
finalStatus = existing.status;
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
// 检查是否有实质性变化(避免无意义的更新,减少历史版本膨胀)
|
|
679
|
+
if (existing.title === input.title &&
|
|
680
|
+
existing.description === (input.description || '') &&
|
|
681
|
+
existing.status === finalStatus &&
|
|
682
|
+
existing.estimatedHours === (input.estimatedHours || 0)) {
|
|
683
|
+
// 无变化,直接返回
|
|
684
|
+
return existing;
|
|
685
|
+
}
|
|
686
|
+
// 删除旧版本并确保时间戳递增
|
|
687
|
+
this.deleteAndEnsureTimestampAdvance(this.taskStore, existing.id);
|
|
688
|
+
const mainTask = this.getMainTask(input.parentTaskId);
|
|
689
|
+
const now = Date.now();
|
|
690
|
+
const completedAt = finalStatus === 'completed' ? (existing.completedAt || now) : null;
|
|
691
|
+
const taskData = {
|
|
692
|
+
taskId: input.taskId,
|
|
693
|
+
title: input.title,
|
|
694
|
+
estimatedHours: input.estimatedHours || existing.estimatedHours || 0,
|
|
695
|
+
relatedFiles: input.relatedFiles || existing.relatedFiles || [],
|
|
696
|
+
description: input.description || existing.description || '',
|
|
697
|
+
};
|
|
698
|
+
const docInput = {
|
|
699
|
+
content: JSON.stringify(taskData),
|
|
700
|
+
contentType: aifastdb_1.ContentType.Text,
|
|
701
|
+
parentId: mainTask?.id || undefined,
|
|
702
|
+
tags: [
|
|
703
|
+
`plan:${this.projectName}`,
|
|
704
|
+
'type:sub-task',
|
|
705
|
+
`stask:${input.taskId}`,
|
|
706
|
+
`parent:${input.parentTaskId}`,
|
|
707
|
+
`status:${finalStatus}`,
|
|
708
|
+
],
|
|
709
|
+
metadata: {
|
|
710
|
+
projectName: this.projectName,
|
|
711
|
+
taskId: input.taskId,
|
|
712
|
+
parentTaskId: input.parentTaskId,
|
|
713
|
+
status: finalStatus,
|
|
714
|
+
createdAt: existing.createdAt,
|
|
715
|
+
updatedAt: now,
|
|
716
|
+
completedAt,
|
|
717
|
+
completedAtCommit: existing.completedAtCommit || null,
|
|
718
|
+
revertReason: existing.revertReason || null,
|
|
719
|
+
},
|
|
720
|
+
importance: 0.7,
|
|
721
|
+
};
|
|
722
|
+
const id = this.taskStore.put(docInput);
|
|
723
|
+
this.refreshMainTaskCounts(input.parentTaskId);
|
|
724
|
+
this.taskStore.flush();
|
|
725
|
+
return {
|
|
726
|
+
...existing,
|
|
727
|
+
...taskData,
|
|
728
|
+
id,
|
|
729
|
+
status: finalStatus,
|
|
730
|
+
updatedAt: now,
|
|
731
|
+
completedAt,
|
|
732
|
+
};
|
|
733
|
+
}
|
|
734
|
+
/**
|
|
735
|
+
* 获取子任务
|
|
736
|
+
*
|
|
737
|
+
* 取同一 taskId 的最新版本(以 metadata.updatedAt 判定)。
|
|
738
|
+
*/
|
|
739
|
+
getSubTask(taskId) {
|
|
740
|
+
const tag = `stask:${taskId}`;
|
|
741
|
+
const docs = this.taskStore.findByTag(tag)
|
|
742
|
+
.filter((doc) => doc.tags.includes(`plan:${this.projectName}`));
|
|
743
|
+
if (docs.length === 0)
|
|
744
|
+
return null;
|
|
745
|
+
// 取最新版本(以 metadata.updatedAt 为判定依据)
|
|
746
|
+
const latest = docs.sort((a, b) => this.getDocUpdatedAt(b) - this.getDocUpdatedAt(a))[0];
|
|
747
|
+
return this.docToSubTask(latest);
|
|
748
|
+
}
|
|
749
|
+
/**
|
|
750
|
+
* 列出某主任务下的所有子任务
|
|
751
|
+
*
|
|
752
|
+
* 对同一 taskId 的多个历史版本做去重,仅保留最新版。
|
|
753
|
+
*/
|
|
754
|
+
listSubTasks(parentTaskId, filter) {
|
|
755
|
+
const parentTag = `parent:${parentTaskId}`;
|
|
756
|
+
let docs = this.taskStore.findByTag(parentTag)
|
|
757
|
+
.filter((doc) => doc.tags.includes(`plan:${this.projectName}`) &&
|
|
758
|
+
doc.tags.includes('type:sub-task'));
|
|
759
|
+
// 按 taskId 去重,仅保留最新版本
|
|
760
|
+
docs = this.deduplicateByTaskId(docs);
|
|
761
|
+
if (filter?.status) {
|
|
762
|
+
const statusTag = `status:${filter.status}`;
|
|
763
|
+
docs = docs.filter((doc) => doc.tags.includes(statusTag));
|
|
764
|
+
}
|
|
765
|
+
return docs.map((doc) => this.docToSubTask(doc));
|
|
766
|
+
}
|
|
767
|
+
/**
|
|
768
|
+
* 更新子任务状态
|
|
769
|
+
*
|
|
770
|
+
* @param options.completedAtCommit - 完成时的 Git commit hash(仅 status=completed 时有效)
|
|
771
|
+
* @param options.revertReason - 回退原因(仅 status 从 completed 变为 pending 时有效)
|
|
772
|
+
*/
|
|
773
|
+
updateSubTaskStatus(taskId, status, options) {
|
|
774
|
+
const subTask = this.getSubTask(taskId);
|
|
775
|
+
if (!subTask)
|
|
776
|
+
return null;
|
|
777
|
+
// 获取父任务以保留 parentId
|
|
778
|
+
const mainTask = this.getMainTask(subTask.parentTaskId);
|
|
779
|
+
this.deleteAndEnsureTimestampAdvance(this.taskStore, subTask.id);
|
|
780
|
+
const now = Date.now();
|
|
781
|
+
const completedAt = status === 'completed' ? now : (status === 'pending' ? null : subTask.completedAt);
|
|
782
|
+
const completedAtCommit = status === 'completed'
|
|
783
|
+
? (options?.completedAtCommit || subTask.completedAtCommit)
|
|
784
|
+
: (status === 'pending' ? undefined : subTask.completedAtCommit);
|
|
785
|
+
const revertReason = options?.revertReason || (status === 'pending' ? undefined : subTask.revertReason);
|
|
786
|
+
const taskData = {
|
|
787
|
+
taskId: subTask.taskId,
|
|
788
|
+
title: subTask.title,
|
|
789
|
+
estimatedHours: subTask.estimatedHours || 0,
|
|
790
|
+
relatedFiles: subTask.relatedFiles || [],
|
|
791
|
+
description: subTask.description || '',
|
|
792
|
+
};
|
|
793
|
+
const docInput = {
|
|
794
|
+
content: JSON.stringify(taskData),
|
|
795
|
+
contentType: aifastdb_1.ContentType.Text,
|
|
796
|
+
parentId: mainTask?.id || undefined,
|
|
797
|
+
tags: [
|
|
798
|
+
`plan:${this.projectName}`,
|
|
799
|
+
'type:sub-task',
|
|
800
|
+
`stask:${subTask.taskId}`,
|
|
801
|
+
`parent:${subTask.parentTaskId}`,
|
|
802
|
+
`status:${status}`,
|
|
803
|
+
],
|
|
804
|
+
metadata: {
|
|
805
|
+
projectName: this.projectName,
|
|
806
|
+
taskId: subTask.taskId,
|
|
807
|
+
parentTaskId: subTask.parentTaskId,
|
|
808
|
+
status,
|
|
809
|
+
createdAt: subTask.createdAt,
|
|
810
|
+
updatedAt: now,
|
|
811
|
+
completedAt,
|
|
812
|
+
completedAtCommit: completedAtCommit || null,
|
|
813
|
+
revertReason: revertReason || null,
|
|
814
|
+
},
|
|
815
|
+
importance: 0.7,
|
|
816
|
+
};
|
|
817
|
+
const id = this.taskStore.put(docInput);
|
|
818
|
+
this.taskStore.flush();
|
|
819
|
+
return {
|
|
820
|
+
...subTask,
|
|
821
|
+
id,
|
|
822
|
+
status,
|
|
823
|
+
updatedAt: now,
|
|
824
|
+
completedAt,
|
|
825
|
+
completedAtCommit,
|
|
826
|
+
revertReason,
|
|
827
|
+
};
|
|
828
|
+
}
|
|
829
|
+
// ==========================================================================
|
|
830
|
+
// Core: Task Completion Workflow
|
|
831
|
+
// ==========================================================================
|
|
832
|
+
/**
|
|
833
|
+
* 完成子任务 — 核心自动化方法
|
|
834
|
+
*
|
|
835
|
+
* 自动处理:
|
|
836
|
+
* 1. 获取当前 Git HEAD 的 short SHA 用于锚定
|
|
837
|
+
* 2. 更新子任务状态为 completed,写入 completedAt 时间戳和 completedAtCommit
|
|
838
|
+
* 3. 重新计算主任务的 completedSubtasks 计数
|
|
839
|
+
* 4. 如果全部子任务完成,自动标记主任务为 completed
|
|
840
|
+
* 5. 如果主任务完成,更新 milestones 文档
|
|
841
|
+
*/
|
|
842
|
+
completeSubTask(taskId) {
|
|
843
|
+
// 1. 获取当前 Git commit hash
|
|
844
|
+
const commitHash = this.getCurrentGitCommit();
|
|
845
|
+
// 2. 更新子任务(带 Git commit 锚定)
|
|
846
|
+
const updatedSubTask = this.updateSubTaskStatus(taskId, 'completed', {
|
|
847
|
+
completedAtCommit: commitHash,
|
|
848
|
+
});
|
|
849
|
+
if (!updatedSubTask) {
|
|
850
|
+
throw new Error(`Sub task "${taskId}" not found for project "${this.projectName}"`);
|
|
851
|
+
}
|
|
852
|
+
// 3. 刷新主任务计数
|
|
853
|
+
const updatedMainTask = this.refreshMainTaskCounts(updatedSubTask.parentTaskId);
|
|
854
|
+
if (!updatedMainTask) {
|
|
855
|
+
throw new Error(`Parent main task "${updatedSubTask.parentTaskId}" not found`);
|
|
856
|
+
}
|
|
857
|
+
// 4. 检查主任务是否全部完成
|
|
858
|
+
const mainTaskCompleted = updatedMainTask.totalSubtasks > 0 &&
|
|
859
|
+
updatedMainTask.completedSubtasks >= updatedMainTask.totalSubtasks;
|
|
860
|
+
if (mainTaskCompleted && updatedMainTask.status !== 'completed') {
|
|
861
|
+
const completedMain = this.updateMainTaskStatus(updatedSubTask.parentTaskId, 'completed');
|
|
862
|
+
if (completedMain) {
|
|
863
|
+
// 5. 更新 milestones 文档(如果存在)
|
|
864
|
+
this.autoUpdateMilestones(completedMain);
|
|
865
|
+
return {
|
|
866
|
+
subTask: updatedSubTask,
|
|
867
|
+
mainTask: completedMain,
|
|
868
|
+
mainTaskCompleted: true,
|
|
869
|
+
completedAtCommit: commitHash,
|
|
870
|
+
};
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
return {
|
|
874
|
+
subTask: updatedSubTask,
|
|
875
|
+
mainTask: updatedMainTask,
|
|
876
|
+
mainTaskCompleted,
|
|
877
|
+
completedAtCommit: commitHash,
|
|
878
|
+
};
|
|
879
|
+
}
|
|
880
|
+
/**
|
|
881
|
+
* 手动完成主任务(跳过子任务检查)
|
|
882
|
+
*/
|
|
883
|
+
completeMainTask(taskId) {
|
|
884
|
+
const result = this.updateMainTaskStatus(taskId, 'completed');
|
|
885
|
+
if (!result) {
|
|
886
|
+
throw new Error(`Main task "${taskId}" not found for project "${this.projectName}"`);
|
|
887
|
+
}
|
|
888
|
+
this.autoUpdateMilestones(result);
|
|
889
|
+
return result;
|
|
890
|
+
}
|
|
891
|
+
// ==========================================================================
|
|
892
|
+
// Progress & Statistics
|
|
893
|
+
// ==========================================================================
|
|
894
|
+
/**
|
|
895
|
+
* 获取项目整体进度
|
|
896
|
+
*/
|
|
897
|
+
getProgress() {
|
|
898
|
+
const sections = this.listSections();
|
|
899
|
+
const mainTasks = this.listMainTasks();
|
|
900
|
+
let totalSub = 0;
|
|
901
|
+
let completedSub = 0;
|
|
902
|
+
const taskProgressList = [];
|
|
903
|
+
for (const mt of mainTasks) {
|
|
904
|
+
const subs = this.listSubTasks(mt.taskId);
|
|
905
|
+
const subCompleted = subs.filter(s => s.status === 'completed').length;
|
|
906
|
+
totalSub += subs.length;
|
|
907
|
+
completedSub += subCompleted;
|
|
908
|
+
taskProgressList.push({
|
|
909
|
+
taskId: mt.taskId,
|
|
910
|
+
title: mt.title,
|
|
911
|
+
priority: mt.priority,
|
|
912
|
+
status: mt.status,
|
|
913
|
+
total: subs.length,
|
|
914
|
+
completed: subCompleted,
|
|
915
|
+
percent: subs.length > 0 ? Math.round((subCompleted / subs.length) * 100) : 0,
|
|
916
|
+
});
|
|
917
|
+
}
|
|
918
|
+
const completedMainTasks = mainTasks.filter(mt => mt.status === 'completed').length;
|
|
919
|
+
return {
|
|
920
|
+
projectName: this.projectName,
|
|
921
|
+
sectionCount: sections.length,
|
|
922
|
+
mainTaskCount: mainTasks.length,
|
|
923
|
+
completedMainTasks,
|
|
924
|
+
subTaskCount: totalSub,
|
|
925
|
+
completedSubTasks: completedSub,
|
|
926
|
+
overallPercent: totalSub > 0 ? Math.round((completedSub / totalSub) * 100) : 0,
|
|
927
|
+
tasks: taskProgressList,
|
|
928
|
+
};
|
|
929
|
+
}
|
|
930
|
+
// ==========================================================================
|
|
931
|
+
// Markdown Export
|
|
932
|
+
// ==========================================================================
|
|
933
|
+
/**
|
|
934
|
+
* 导出完整的 Markdown 文档
|
|
935
|
+
*/
|
|
936
|
+
exportToMarkdown() {
|
|
937
|
+
const sections = this.listSections();
|
|
938
|
+
const progress = this.getProgress();
|
|
939
|
+
let md = `# ${this.projectName} - 开发计划\n\n`;
|
|
940
|
+
md += `> 生成时间: ${new Date().toISOString()}\n`;
|
|
941
|
+
md += `> 总体进度: ${progress.overallPercent}% (${progress.completedSubTasks}/${progress.subTaskCount})\n\n`;
|
|
942
|
+
// 文档片段
|
|
943
|
+
const sectionOrder = [
|
|
944
|
+
'overview', 'core_concepts', 'api_design', 'file_structure',
|
|
945
|
+
'config', 'examples', 'technical_notes', 'api_endpoints',
|
|
946
|
+
'milestones', 'changelog', 'custom',
|
|
947
|
+
];
|
|
948
|
+
for (const sectionType of sectionOrder) {
|
|
949
|
+
const sectionDocs = sections.filter(s => s.section === sectionType);
|
|
950
|
+
for (const doc of sectionDocs) {
|
|
951
|
+
md += doc.content + '\n\n---\n\n';
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
// 任务进度
|
|
955
|
+
md += '## 开发任务进度\n\n';
|
|
956
|
+
for (const taskProg of progress.tasks) {
|
|
957
|
+
const statusIcon = taskProg.status === 'completed' ? '✅'
|
|
958
|
+
: taskProg.status === 'in_progress' ? '🔄'
|
|
959
|
+
: taskProg.status === 'cancelled' ? '❌' : '⬜';
|
|
960
|
+
md += `### ${statusIcon} ${taskProg.title} (${taskProg.completed}/${taskProg.total})\n\n`;
|
|
961
|
+
const subs = this.listSubTasks(taskProg.taskId);
|
|
962
|
+
if (subs.length > 0) {
|
|
963
|
+
md += '| 任务 | 描述 | 状态 | 完成日期 |\n';
|
|
964
|
+
md += '|-----|------|------|--------|\n';
|
|
965
|
+
for (const sub of subs) {
|
|
966
|
+
const subIcon = sub.status === 'completed' ? '✅ 已完成'
|
|
967
|
+
: sub.status === 'in_progress' ? '🔄 进行中'
|
|
968
|
+
: sub.status === 'cancelled' ? '❌ 已取消' : '⬜ 待开始';
|
|
969
|
+
const date = sub.completedAt
|
|
970
|
+
? new Date(sub.completedAt).toISOString().split('T')[0]
|
|
971
|
+
: '-';
|
|
972
|
+
md += `| ${sub.taskId} | ${sub.title} | ${subIcon} | ${date} |\n`;
|
|
973
|
+
}
|
|
974
|
+
md += '\n';
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
return md;
|
|
978
|
+
}
|
|
979
|
+
/**
|
|
980
|
+
* 导出仅任务进度的简洁 Markdown
|
|
981
|
+
*/
|
|
982
|
+
exportTaskSummary() {
|
|
983
|
+
const progress = this.getProgress();
|
|
984
|
+
let md = `# ${this.projectName} - 任务进度总览\n\n`;
|
|
985
|
+
md += `> 更新时间: ${new Date().toISOString()}\n`;
|
|
986
|
+
md += `> 总体进度: **${progress.overallPercent}%** (${progress.completedSubTasks}/${progress.subTaskCount} 子任务完成)\n`;
|
|
987
|
+
md += `> 主任务完成: ${progress.completedMainTasks}/${progress.mainTaskCount}\n\n`;
|
|
988
|
+
for (const tp of progress.tasks) {
|
|
989
|
+
const bar = this.progressBar(tp.percent);
|
|
990
|
+
const statusIcon = tp.status === 'completed' ? '✅'
|
|
991
|
+
: tp.status === 'in_progress' ? '🔄' : '⬜';
|
|
992
|
+
md += `${statusIcon} **${tp.title}** [${tp.priority}]\n`;
|
|
993
|
+
md += ` ${bar} ${tp.percent}% (${tp.completed}/${tp.total})\n\n`;
|
|
994
|
+
}
|
|
995
|
+
return md;
|
|
996
|
+
}
|
|
997
|
+
// ==========================================================================
|
|
998
|
+
// Module Operations
|
|
999
|
+
// ==========================================================================
|
|
1000
|
+
/**
|
|
1001
|
+
* 创建功能模块
|
|
1002
|
+
*/
|
|
1003
|
+
createModule(input) {
|
|
1004
|
+
const existing = this.getModule(input.moduleId);
|
|
1005
|
+
if (existing) {
|
|
1006
|
+
throw new Error(`Module "${input.moduleId}" already exists for project "${this.projectName}"`);
|
|
1007
|
+
}
|
|
1008
|
+
const now = Date.now();
|
|
1009
|
+
const status = input.status || 'active';
|
|
1010
|
+
const moduleData = {
|
|
1011
|
+
moduleId: input.moduleId,
|
|
1012
|
+
name: input.name,
|
|
1013
|
+
description: input.description || '',
|
|
1014
|
+
};
|
|
1015
|
+
const docInput = {
|
|
1016
|
+
content: JSON.stringify(moduleData),
|
|
1017
|
+
contentType: aifastdb_1.ContentType.Text,
|
|
1018
|
+
tags: [
|
|
1019
|
+
`plan:${this.projectName}`,
|
|
1020
|
+
'type:module',
|
|
1021
|
+
`module:${input.moduleId}`,
|
|
1022
|
+
`status:${status}`,
|
|
1023
|
+
],
|
|
1024
|
+
metadata: {
|
|
1025
|
+
projectName: this.projectName,
|
|
1026
|
+
moduleId: input.moduleId,
|
|
1027
|
+
status,
|
|
1028
|
+
createdAt: now,
|
|
1029
|
+
updatedAt: now,
|
|
1030
|
+
},
|
|
1031
|
+
importance: 0.85,
|
|
1032
|
+
};
|
|
1033
|
+
const id = this.moduleStore.put(docInput);
|
|
1034
|
+
this.moduleStore.flush();
|
|
1035
|
+
return {
|
|
1036
|
+
id,
|
|
1037
|
+
projectName: this.projectName,
|
|
1038
|
+
moduleId: input.moduleId,
|
|
1039
|
+
name: input.name,
|
|
1040
|
+
description: input.description,
|
|
1041
|
+
status,
|
|
1042
|
+
mainTaskCount: 0,
|
|
1043
|
+
subTaskCount: 0,
|
|
1044
|
+
completedSubTaskCount: 0,
|
|
1045
|
+
docCount: 0,
|
|
1046
|
+
createdAt: now,
|
|
1047
|
+
updatedAt: now,
|
|
1048
|
+
};
|
|
1049
|
+
}
|
|
1050
|
+
/**
|
|
1051
|
+
* 获取功能模块(含自动计算的 taskCount/docCount)
|
|
1052
|
+
*/
|
|
1053
|
+
getModule(moduleId) {
|
|
1054
|
+
const tag = `module:${moduleId}`;
|
|
1055
|
+
const docs = this.moduleStore.findByTag(tag)
|
|
1056
|
+
.filter((doc) => doc.tags.includes(`plan:${this.projectName}`));
|
|
1057
|
+
if (docs.length === 0)
|
|
1058
|
+
return null;
|
|
1059
|
+
const latest = docs.sort((a, b) => this.getDocUpdatedAt(b) - this.getDocUpdatedAt(a))[0];
|
|
1060
|
+
return this.docToModule(latest);
|
|
1061
|
+
}
|
|
1062
|
+
/**
|
|
1063
|
+
* 列出所有功能模块
|
|
1064
|
+
*/
|
|
1065
|
+
listModules(filter) {
|
|
1066
|
+
let docs = this.moduleStore.findByTag(`plan:${this.projectName}`)
|
|
1067
|
+
.filter((doc) => doc.tags.includes('type:module'));
|
|
1068
|
+
// 按 moduleId 去重
|
|
1069
|
+
const latestMap = new Map();
|
|
1070
|
+
for (const doc of docs) {
|
|
1071
|
+
const data = JSON.parse(doc.content);
|
|
1072
|
+
const moduleId = data.moduleId;
|
|
1073
|
+
if (!moduleId)
|
|
1074
|
+
continue;
|
|
1075
|
+
const existing = latestMap.get(moduleId);
|
|
1076
|
+
if (!existing || this.getDocUpdatedAt(doc) > this.getDocUpdatedAt(existing)) {
|
|
1077
|
+
latestMap.set(moduleId, doc);
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
docs = Array.from(latestMap.values());
|
|
1081
|
+
if (filter?.status) {
|
|
1082
|
+
const statusTag = `status:${filter.status}`;
|
|
1083
|
+
docs = docs.filter((doc) => doc.tags.includes(statusTag));
|
|
1084
|
+
}
|
|
1085
|
+
return docs.map((doc) => this.docToModule(doc));
|
|
1086
|
+
}
|
|
1087
|
+
/**
|
|
1088
|
+
* 更新功能模块
|
|
1089
|
+
*/
|
|
1090
|
+
updateModule(moduleId, updates) {
|
|
1091
|
+
const existing = this.getModule(moduleId);
|
|
1092
|
+
if (!existing)
|
|
1093
|
+
return null;
|
|
1094
|
+
this.deleteAndEnsureTimestampAdvance(this.moduleStore, existing.id);
|
|
1095
|
+
const now = Date.now();
|
|
1096
|
+
const newName = updates.name || existing.name;
|
|
1097
|
+
const newDescription = updates.description !== undefined ? updates.description : existing.description;
|
|
1098
|
+
const newStatus = updates.status || existing.status;
|
|
1099
|
+
const moduleData = {
|
|
1100
|
+
moduleId,
|
|
1101
|
+
name: newName,
|
|
1102
|
+
description: newDescription || '',
|
|
1103
|
+
};
|
|
1104
|
+
const docInput = {
|
|
1105
|
+
content: JSON.stringify(moduleData),
|
|
1106
|
+
contentType: aifastdb_1.ContentType.Text,
|
|
1107
|
+
tags: [
|
|
1108
|
+
`plan:${this.projectName}`,
|
|
1109
|
+
'type:module',
|
|
1110
|
+
`module:${moduleId}`,
|
|
1111
|
+
`status:${newStatus}`,
|
|
1112
|
+
],
|
|
1113
|
+
metadata: {
|
|
1114
|
+
projectName: this.projectName,
|
|
1115
|
+
moduleId,
|
|
1116
|
+
status: newStatus,
|
|
1117
|
+
createdAt: existing.createdAt,
|
|
1118
|
+
updatedAt: now,
|
|
1119
|
+
},
|
|
1120
|
+
importance: 0.85,
|
|
1121
|
+
};
|
|
1122
|
+
const id = this.moduleStore.put(docInput);
|
|
1123
|
+
this.moduleStore.flush();
|
|
1124
|
+
return {
|
|
1125
|
+
id,
|
|
1126
|
+
projectName: this.projectName,
|
|
1127
|
+
moduleId,
|
|
1128
|
+
name: newName,
|
|
1129
|
+
description: newDescription,
|
|
1130
|
+
status: newStatus,
|
|
1131
|
+
mainTaskCount: existing.mainTaskCount,
|
|
1132
|
+
subTaskCount: existing.subTaskCount,
|
|
1133
|
+
completedSubTaskCount: existing.completedSubTaskCount,
|
|
1134
|
+
docCount: existing.docCount,
|
|
1135
|
+
createdAt: existing.createdAt,
|
|
1136
|
+
updatedAt: now,
|
|
1137
|
+
};
|
|
1138
|
+
}
|
|
1139
|
+
/**
|
|
1140
|
+
* 删除功能模块
|
|
1141
|
+
*/
|
|
1142
|
+
deleteModule(moduleId) {
|
|
1143
|
+
const existing = this.getModule(moduleId);
|
|
1144
|
+
if (!existing)
|
|
1145
|
+
return false;
|
|
1146
|
+
this.moduleStore.delete(existing.id);
|
|
1147
|
+
this.moduleStore.flush();
|
|
1148
|
+
return true;
|
|
1149
|
+
}
|
|
1150
|
+
/**
|
|
1151
|
+
* 获取模块详情 — 包含关联的任务和文档
|
|
1152
|
+
*/
|
|
1153
|
+
getModuleDetail(moduleId) {
|
|
1154
|
+
const mod = this.getModule(moduleId);
|
|
1155
|
+
if (!mod)
|
|
1156
|
+
return null;
|
|
1157
|
+
// 获取关联的主任务
|
|
1158
|
+
const moduleTag = `module:${moduleId}`;
|
|
1159
|
+
let taskDocs = this.taskStore.findByTag(moduleTag)
|
|
1160
|
+
.filter((doc) => doc.tags.includes(`plan:${this.projectName}`) &&
|
|
1161
|
+
doc.tags.includes('type:main-task'));
|
|
1162
|
+
taskDocs = this.deduplicateByTaskId(taskDocs);
|
|
1163
|
+
const mainTasks = taskDocs.map((doc) => this.docToMainTask(doc));
|
|
1164
|
+
// 获取关联的所有子任务(通过主任务间接关联)
|
|
1165
|
+
const subTasks = [];
|
|
1166
|
+
for (const mt of mainTasks) {
|
|
1167
|
+
const subs = this.listSubTasks(mt.taskId);
|
|
1168
|
+
subTasks.push(...subs);
|
|
1169
|
+
}
|
|
1170
|
+
// 获取关联的文档
|
|
1171
|
+
let docDocs = this.docStore.findByTag(moduleTag)
|
|
1172
|
+
.filter((doc) => doc.tags.includes(`plan:${this.projectName}`));
|
|
1173
|
+
// 按 section+subSection 去重
|
|
1174
|
+
const latestDocMap = new Map();
|
|
1175
|
+
for (const doc of docDocs) {
|
|
1176
|
+
const sectionTag = doc.tags.find((t) => t.startsWith('section:'));
|
|
1177
|
+
const subTag = doc.tags.find((t) => t.startsWith('sub:'));
|
|
1178
|
+
const key = `${sectionTag || 'unknown'}|${subTag || ''}`;
|
|
1179
|
+
const ex = latestDocMap.get(key);
|
|
1180
|
+
if (!ex || this.getDocUpdatedAt(doc) > this.getDocUpdatedAt(ex)) {
|
|
1181
|
+
latestDocMap.set(key, doc);
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
const documents = Array.from(latestDocMap.values()).map((doc) => this.docToDevPlanDoc(doc));
|
|
1185
|
+
return { module: mod, mainTasks, subTasks, documents };
|
|
1186
|
+
}
|
|
1187
|
+
// ==========================================================================
|
|
1188
|
+
// Utility
|
|
1189
|
+
// ==========================================================================
|
|
1190
|
+
/**
|
|
1191
|
+
* 将存储的更改刷到磁盘
|
|
1192
|
+
*/
|
|
1193
|
+
sync() {
|
|
1194
|
+
this.docStore.flush();
|
|
1195
|
+
this.taskStore.flush();
|
|
1196
|
+
this.moduleStore.flush();
|
|
1197
|
+
}
|
|
1198
|
+
/**
|
|
1199
|
+
* 获取项目名称
|
|
1200
|
+
*/
|
|
1201
|
+
getProjectName() {
|
|
1202
|
+
return this.projectName;
|
|
1203
|
+
}
|
|
1204
|
+
// ==========================================================================
|
|
1205
|
+
// Private Helpers
|
|
1206
|
+
// ==========================================================================
|
|
1207
|
+
/**
|
|
1208
|
+
* 获取文档的有效 updatedAt 时间戳。
|
|
1209
|
+
*
|
|
1210
|
+
* EnhancedDocumentStore 使用 append-only JSONL 存储,修改文档时实际上
|
|
1211
|
+
* 是 delete 旧文档 + put 新文档。因此同一逻辑文档可能存在多个物理版本。
|
|
1212
|
+
* 必须通过 metadata.updatedAt 来判断哪个是最新的"可用文档",
|
|
1213
|
+
* 其余的都是"历史文档"。
|
|
1214
|
+
*
|
|
1215
|
+
* 优先级:metadata.updatedAt > metadata.createdAt > doc.createdAt
|
|
1216
|
+
*/
|
|
1217
|
+
getDocUpdatedAt(doc) {
|
|
1218
|
+
return doc.metadata?.updatedAt || doc.metadata?.createdAt || doc.createdAt;
|
|
1219
|
+
}
|
|
1220
|
+
/**
|
|
1221
|
+
* 确保当前时间戳严格大于参考时间戳。
|
|
1222
|
+
*
|
|
1223
|
+
* EnhancedDocumentStore 使用 append-only JSONL 存储,保留所有历史版本。
|
|
1224
|
+
* 版本选择通过 metadata.updatedAt 判定最新文档。
|
|
1225
|
+
* 如果 delete+put 发生在同一毫秒内,新旧版本的 updatedAt 相同,
|
|
1226
|
+
* 会导致去重时可能选中旧版本(如 pending 状态),造成状态丢失。
|
|
1227
|
+
*
|
|
1228
|
+
* 本方法在 delete 之后、put 之前调用,自旋等待直到时间戳前进,
|
|
1229
|
+
* 从而保证新版本的 updatedAt 一定大于旧版本。
|
|
1230
|
+
*/
|
|
1231
|
+
ensureTimestampAfter(referenceTimestamp) {
|
|
1232
|
+
while (Date.now() <= referenceTimestamp) {
|
|
1233
|
+
// 自旋等待直到当前时间严格大于参考时间戳
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
/**
|
|
1237
|
+
* 删除文档并确保后续 put 的 updatedAt 严格大于被删文档。
|
|
1238
|
+
*
|
|
1239
|
+
* 使用 metadata.updatedAt 作为参考时间戳(而非 doc.createdAt),
|
|
1240
|
+
* 因为版本选择是基于 metadata.updatedAt 进行的。
|
|
1241
|
+
*/
|
|
1242
|
+
deleteAndEnsureTimestampAdvance(store, id) {
|
|
1243
|
+
const deleted = store.delete(id);
|
|
1244
|
+
if (deleted) {
|
|
1245
|
+
// 以 metadata.updatedAt 为基准,确保新文档的 updatedAt 严格递增
|
|
1246
|
+
const refTimestamp = this.getDocUpdatedAt(deleted);
|
|
1247
|
+
this.ensureTimestampAfter(refTimestamp);
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
/**
|
|
1251
|
+
* 对同一 taskId 的多个历史版本做去重,仅保留最新版(metadata.updatedAt 最大)。
|
|
1252
|
+
*
|
|
1253
|
+
* 由于 EnhancedDocumentStore 使用 append-only JSONL 存储,
|
|
1254
|
+
* delete+put 操作会在文件中保留历史版本。重新加载时所有版本都会出现,
|
|
1255
|
+
* 因此需要在查询层面进行去重。
|
|
1256
|
+
*
|
|
1257
|
+
* 使用 metadata.updatedAt(而非 doc.createdAt)作为版本判定依据,
|
|
1258
|
+
* 确保"最近更新时间"的文档才是可用文档,其余为历史文档。
|
|
1259
|
+
*/
|
|
1260
|
+
deduplicateByTaskId(docs) {
|
|
1261
|
+
const latestMap = new Map();
|
|
1262
|
+
for (const doc of docs) {
|
|
1263
|
+
const data = JSON.parse(doc.content);
|
|
1264
|
+
const taskId = data.taskId;
|
|
1265
|
+
if (!taskId)
|
|
1266
|
+
continue;
|
|
1267
|
+
const existing = latestMap.get(taskId);
|
|
1268
|
+
if (!existing || this.getDocUpdatedAt(doc) > this.getDocUpdatedAt(existing)) {
|
|
1269
|
+
latestMap.set(taskId, doc);
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
return Array.from(latestMap.values());
|
|
1273
|
+
}
|
|
1274
|
+
docToDevPlanDoc(doc) {
|
|
1275
|
+
const sectionTag = doc.tags.find((t) => t.startsWith('section:'));
|
|
1276
|
+
const section = (sectionTag?.replace('section:', '') || 'custom');
|
|
1277
|
+
const subTag = doc.tags.find((t) => t.startsWith('sub:'));
|
|
1278
|
+
const subSection = subTag?.replace('sub:', '');
|
|
1279
|
+
const moduleTag = doc.tags.find((t) => t.startsWith('module:'));
|
|
1280
|
+
const moduleId = moduleTag?.replace('module:', '') || undefined;
|
|
1281
|
+
return {
|
|
1282
|
+
id: doc.id,
|
|
1283
|
+
projectName: this.projectName,
|
|
1284
|
+
section,
|
|
1285
|
+
title: doc.metadata?.title || '',
|
|
1286
|
+
content: doc.content,
|
|
1287
|
+
version: doc.metadata?.version || '1.0.0',
|
|
1288
|
+
subSection,
|
|
1289
|
+
relatedSections: doc.metadata?.relatedSections || [],
|
|
1290
|
+
moduleId,
|
|
1291
|
+
createdAt: doc.metadata?.createdAt || doc.createdAt,
|
|
1292
|
+
updatedAt: doc.metadata?.updatedAt || doc.createdAt,
|
|
1293
|
+
};
|
|
1294
|
+
}
|
|
1295
|
+
docToMainTask(doc) {
|
|
1296
|
+
const data = JSON.parse(doc.content);
|
|
1297
|
+
const statusTag = doc.tags.find((t) => t.startsWith('status:'));
|
|
1298
|
+
const status = (statusTag?.replace('status:', '') || 'pending');
|
|
1299
|
+
const moduleTag = doc.tags.find((t) => t.startsWith('module:'));
|
|
1300
|
+
const moduleId = moduleTag?.replace('module:', '') || undefined;
|
|
1301
|
+
return {
|
|
1302
|
+
id: doc.id,
|
|
1303
|
+
projectName: this.projectName,
|
|
1304
|
+
taskId: data.taskId,
|
|
1305
|
+
title: data.title,
|
|
1306
|
+
priority: data.priority,
|
|
1307
|
+
description: data.description,
|
|
1308
|
+
estimatedHours: data.estimatedHours,
|
|
1309
|
+
relatedSections: data.relatedSections || [],
|
|
1310
|
+
moduleId,
|
|
1311
|
+
totalSubtasks: data.totalSubtasks || 0,
|
|
1312
|
+
completedSubtasks: data.completedSubtasks || 0,
|
|
1313
|
+
status,
|
|
1314
|
+
createdAt: doc.metadata?.createdAt || doc.createdAt,
|
|
1315
|
+
updatedAt: doc.metadata?.updatedAt || doc.createdAt,
|
|
1316
|
+
completedAt: doc.metadata?.completedAt || null,
|
|
1317
|
+
};
|
|
1318
|
+
}
|
|
1319
|
+
docToModule(doc) {
|
|
1320
|
+
const data = JSON.parse(doc.content);
|
|
1321
|
+
const statusTag = doc.tags.find((t) => t.startsWith('status:'));
|
|
1322
|
+
const status = (statusTag?.replace('status:', '') || 'active');
|
|
1323
|
+
const moduleId = data.moduleId;
|
|
1324
|
+
// 计算关联的主任务数(去重)
|
|
1325
|
+
const moduleTag = `module:${moduleId}`;
|
|
1326
|
+
const taskDocs = this.taskStore.findByTag(moduleTag)
|
|
1327
|
+
.filter((d) => d.tags.includes(`plan:${this.projectName}`) &&
|
|
1328
|
+
d.tags.includes('type:main-task'));
|
|
1329
|
+
const uniqueTaskIds = new Set();
|
|
1330
|
+
for (const td of taskDocs) {
|
|
1331
|
+
try {
|
|
1332
|
+
uniqueTaskIds.add(JSON.parse(td.content).taskId);
|
|
1333
|
+
}
|
|
1334
|
+
catch { }
|
|
1335
|
+
}
|
|
1336
|
+
// 计算关联的子任务数(遍历关联主任务下的所有子任务)
|
|
1337
|
+
// 注意:findByTag 在 JSONL 重新加载后可能返回同一子任务的多个历史版本,
|
|
1338
|
+
// 必须按 taskId 去重并取 metadata.updatedAt 最新的版本,才能读到正确的状态。
|
|
1339
|
+
let subTaskCount = 0;
|
|
1340
|
+
let completedSubTaskCount = 0;
|
|
1341
|
+
for (const mainTaskId of uniqueTaskIds) {
|
|
1342
|
+
const subDocs = this.taskStore.findByTag(`parent:${mainTaskId}`)
|
|
1343
|
+
.filter((d) => d.tags.includes(`plan:${this.projectName}`) &&
|
|
1344
|
+
d.tags.includes('type:sub-task'));
|
|
1345
|
+
// 按 taskId 去重,保留 updatedAt 最新的版本
|
|
1346
|
+
const latestSubMap = new Map();
|
|
1347
|
+
for (const sd of subDocs) {
|
|
1348
|
+
try {
|
|
1349
|
+
const subData = JSON.parse(sd.content);
|
|
1350
|
+
const subId = subData.taskId;
|
|
1351
|
+
if (!subId)
|
|
1352
|
+
continue;
|
|
1353
|
+
const existing = latestSubMap.get(subId);
|
|
1354
|
+
if (!existing || this.getDocUpdatedAt(sd) > this.getDocUpdatedAt(existing)) {
|
|
1355
|
+
latestSubMap.set(subId, sd);
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
catch { }
|
|
1359
|
+
}
|
|
1360
|
+
for (const sd of latestSubMap.values()) {
|
|
1361
|
+
subTaskCount++;
|
|
1362
|
+
const subStatusTag = sd.tags.find((t) => t.startsWith('status:'));
|
|
1363
|
+
if (subStatusTag === 'status:completed') {
|
|
1364
|
+
completedSubTaskCount++;
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
// 计算关联的文档数(按 section+subSection 去重)
|
|
1369
|
+
const docDocs = this.docStore.findByTag(moduleTag)
|
|
1370
|
+
.filter((d) => d.tags.includes(`plan:${this.projectName}`));
|
|
1371
|
+
const uniqueDocKeys = new Set();
|
|
1372
|
+
for (const dd of docDocs) {
|
|
1373
|
+
const st = dd.tags.find((t) => t.startsWith('section:'));
|
|
1374
|
+
const sub = dd.tags.find((t) => t.startsWith('sub:'));
|
|
1375
|
+
uniqueDocKeys.add(`${st || ''}|${sub || ''}`);
|
|
1376
|
+
}
|
|
1377
|
+
return {
|
|
1378
|
+
id: doc.id,
|
|
1379
|
+
projectName: this.projectName,
|
|
1380
|
+
moduleId,
|
|
1381
|
+
name: data.name,
|
|
1382
|
+
description: data.description || undefined,
|
|
1383
|
+
status,
|
|
1384
|
+
mainTaskCount: uniqueTaskIds.size,
|
|
1385
|
+
subTaskCount,
|
|
1386
|
+
completedSubTaskCount,
|
|
1387
|
+
docCount: uniqueDocKeys.size,
|
|
1388
|
+
createdAt: doc.metadata?.createdAt || doc.createdAt,
|
|
1389
|
+
updatedAt: doc.metadata?.updatedAt || doc.createdAt,
|
|
1390
|
+
};
|
|
1391
|
+
}
|
|
1392
|
+
docToSubTask(doc) {
|
|
1393
|
+
const data = JSON.parse(doc.content);
|
|
1394
|
+
const statusTag = doc.tags.find((t) => t.startsWith('status:'));
|
|
1395
|
+
const status = (statusTag?.replace('status:', '') || 'pending');
|
|
1396
|
+
const parentTag = doc.tags.find((t) => t.startsWith('parent:'));
|
|
1397
|
+
const parentTaskId = parentTag?.replace('parent:', '') || '';
|
|
1398
|
+
return {
|
|
1399
|
+
id: doc.id,
|
|
1400
|
+
projectName: this.projectName,
|
|
1401
|
+
taskId: data.taskId,
|
|
1402
|
+
parentTaskId,
|
|
1403
|
+
title: data.title,
|
|
1404
|
+
estimatedHours: data.estimatedHours,
|
|
1405
|
+
relatedFiles: data.relatedFiles || [],
|
|
1406
|
+
description: data.description,
|
|
1407
|
+
status,
|
|
1408
|
+
createdAt: doc.metadata?.createdAt || doc.createdAt,
|
|
1409
|
+
updatedAt: doc.metadata?.updatedAt || doc.createdAt,
|
|
1410
|
+
completedAt: doc.metadata?.completedAt || null,
|
|
1411
|
+
completedAtCommit: doc.metadata?.completedAtCommit || undefined,
|
|
1412
|
+
revertReason: doc.metadata?.revertReason || undefined,
|
|
1413
|
+
};
|
|
1414
|
+
}
|
|
1415
|
+
/**
|
|
1416
|
+
* 刷新主任务的子任务计数
|
|
1417
|
+
*/
|
|
1418
|
+
refreshMainTaskCounts(mainTaskId) {
|
|
1419
|
+
const mainTask = this.getMainTask(mainTaskId);
|
|
1420
|
+
if (!mainTask)
|
|
1421
|
+
return null;
|
|
1422
|
+
const subs = this.listSubTasks(mainTaskId);
|
|
1423
|
+
const completedCount = subs.filter(s => s.status === 'completed').length;
|
|
1424
|
+
// 如果计数没变,不需要更新
|
|
1425
|
+
if (mainTask.totalSubtasks === subs.length && mainTask.completedSubtasks === completedCount) {
|
|
1426
|
+
return mainTask;
|
|
1427
|
+
}
|
|
1428
|
+
this.deleteAndEnsureTimestampAdvance(this.taskStore, mainTask.id);
|
|
1429
|
+
const now = Date.now();
|
|
1430
|
+
const taskData = {
|
|
1431
|
+
taskId: mainTask.taskId,
|
|
1432
|
+
title: mainTask.title,
|
|
1433
|
+
priority: mainTask.priority,
|
|
1434
|
+
description: mainTask.description || '',
|
|
1435
|
+
estimatedHours: mainTask.estimatedHours || 0,
|
|
1436
|
+
relatedSections: mainTask.relatedSections || [],
|
|
1437
|
+
totalSubtasks: subs.length,
|
|
1438
|
+
completedSubtasks: completedCount,
|
|
1439
|
+
};
|
|
1440
|
+
const tags = [
|
|
1441
|
+
`plan:${this.projectName}`,
|
|
1442
|
+
'type:main-task',
|
|
1443
|
+
`mtask:${mainTask.taskId}`,
|
|
1444
|
+
`priority:${mainTask.priority}`,
|
|
1445
|
+
`status:${mainTask.status}`,
|
|
1446
|
+
];
|
|
1447
|
+
if (mainTask.moduleId) {
|
|
1448
|
+
tags.push(`module:${mainTask.moduleId}`);
|
|
1449
|
+
}
|
|
1450
|
+
const docInput = {
|
|
1451
|
+
content: JSON.stringify(taskData),
|
|
1452
|
+
contentType: aifastdb_1.ContentType.Text,
|
|
1453
|
+
tags,
|
|
1454
|
+
metadata: {
|
|
1455
|
+
projectName: this.projectName,
|
|
1456
|
+
taskId: mainTask.taskId,
|
|
1457
|
+
status: mainTask.status,
|
|
1458
|
+
moduleId: mainTask.moduleId || null,
|
|
1459
|
+
createdAt: mainTask.createdAt,
|
|
1460
|
+
updatedAt: now,
|
|
1461
|
+
completedAt: mainTask.completedAt,
|
|
1462
|
+
},
|
|
1463
|
+
importance: mainTask.priority === 'P0' ? 0.95 : mainTask.priority === 'P1' ? 0.8 : 0.6,
|
|
1464
|
+
};
|
|
1465
|
+
const id = this.taskStore.put(docInput);
|
|
1466
|
+
this.taskStore.flush();
|
|
1467
|
+
return {
|
|
1468
|
+
...mainTask,
|
|
1469
|
+
id,
|
|
1470
|
+
totalSubtasks: subs.length,
|
|
1471
|
+
completedSubtasks: completedCount,
|
|
1472
|
+
updatedAt: now,
|
|
1473
|
+
};
|
|
1474
|
+
}
|
|
1475
|
+
/**
|
|
1476
|
+
* 当主任务完成时自动更新 milestones 文档
|
|
1477
|
+
*/
|
|
1478
|
+
autoUpdateMilestones(completedMainTask) {
|
|
1479
|
+
const milestonesDoc = this.getSection('milestones');
|
|
1480
|
+
if (!milestonesDoc)
|
|
1481
|
+
return;
|
|
1482
|
+
const dateStr = new Date().toISOString().split('T')[0];
|
|
1483
|
+
const appendLine = `\n| ${completedMainTask.taskId} | ${completedMainTask.title} | ${dateStr} | ✅ 已完成 |`;
|
|
1484
|
+
// 追加到 milestones 内容末尾
|
|
1485
|
+
const updatedContent = milestonesDoc.content + appendLine;
|
|
1486
|
+
this.saveSection({
|
|
1487
|
+
projectName: this.projectName,
|
|
1488
|
+
section: 'milestones',
|
|
1489
|
+
title: milestonesDoc.title,
|
|
1490
|
+
content: updatedContent,
|
|
1491
|
+
version: milestonesDoc.version,
|
|
1492
|
+
relatedSections: milestonesDoc.relatedSections,
|
|
1493
|
+
});
|
|
1494
|
+
}
|
|
1495
|
+
// ==========================================================================
|
|
1496
|
+
// Git Integration (Git Commit 锚定 + 同步检查)
|
|
1497
|
+
// ==========================================================================
|
|
1498
|
+
/**
|
|
1499
|
+
* 🆕 获取当前 Git HEAD 的 short SHA
|
|
1500
|
+
*
|
|
1501
|
+
* 在非 Git 仓库或 Git 不可用时返回 undefined,不阻断正常流程。
|
|
1502
|
+
*/
|
|
1503
|
+
getCurrentGitCommit() {
|
|
1504
|
+
try {
|
|
1505
|
+
const { execSync } = require('child_process');
|
|
1506
|
+
return execSync('git rev-parse --short HEAD', {
|
|
1507
|
+
encoding: 'utf-8',
|
|
1508
|
+
timeout: 5000,
|
|
1509
|
+
stdio: ['pipe', 'pipe', 'pipe'], // 静默 stderr
|
|
1510
|
+
}).trim();
|
|
1511
|
+
}
|
|
1512
|
+
catch {
|
|
1513
|
+
return undefined; // 非 Git 仓库或 Git 不可用
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1516
|
+
/**
|
|
1517
|
+
* 🆕 检查 commit 是否是 target 的祖先
|
|
1518
|
+
*
|
|
1519
|
+
* 使用 `git merge-base --is-ancestor` 命令。
|
|
1520
|
+
* 如果 commit 不存在或不可达,返回 false(视为需要回退)。
|
|
1521
|
+
*/
|
|
1522
|
+
isAncestor(commit, target) {
|
|
1523
|
+
try {
|
|
1524
|
+
const { execSync } = require('child_process');
|
|
1525
|
+
execSync(`git merge-base --is-ancestor ${commit} ${target}`, {
|
|
1526
|
+
timeout: 5000,
|
|
1527
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
1528
|
+
});
|
|
1529
|
+
return true; // exit code 0 = is ancestor
|
|
1530
|
+
}
|
|
1531
|
+
catch {
|
|
1532
|
+
return false; // exit code 1 = not ancestor, or error
|
|
1533
|
+
}
|
|
1534
|
+
}
|
|
1535
|
+
/**
|
|
1536
|
+
* 🆕 回退子任务状态
|
|
1537
|
+
*
|
|
1538
|
+
* 将已完成的子任务回退为 pending,记录回退原因,
|
|
1539
|
+
* 清空 completedAtCommit 和 completedAt。
|
|
1540
|
+
* 同时刷新父主任务的计数。
|
|
1541
|
+
*/
|
|
1542
|
+
revertSubTask(taskId, reason) {
|
|
1543
|
+
const result = this.updateSubTaskStatus(taskId, 'pending', {
|
|
1544
|
+
revertReason: reason,
|
|
1545
|
+
});
|
|
1546
|
+
if (result) {
|
|
1547
|
+
// 刷新父主任务计数
|
|
1548
|
+
this.refreshMainTaskCounts(result.parentTaskId);
|
|
1549
|
+
// 如果父主任务被标记为 completed,也需要回退
|
|
1550
|
+
const mainTask = this.getMainTask(result.parentTaskId);
|
|
1551
|
+
if (mainTask && mainTask.status === 'completed') {
|
|
1552
|
+
this.updateMainTaskStatus(result.parentTaskId, 'in_progress');
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1555
|
+
return result;
|
|
1556
|
+
}
|
|
1557
|
+
/**
|
|
1558
|
+
* 🆕 同步检查所有已完成任务与 Git 历史的一致性
|
|
1559
|
+
*
|
|
1560
|
+
* 对每个 status=completed 且有 completedAtCommit 的子任务:
|
|
1561
|
+
* 1. 检查 completedAtCommit 是否是当前 HEAD 的祖先
|
|
1562
|
+
* 2. 如果不是(说明 Git 发生了回滚),回退任务状态为 pending
|
|
1563
|
+
* 3. 记录 revertReason
|
|
1564
|
+
*
|
|
1565
|
+
* @param dryRun 如果为 true,只返回哪些任务会被回退,不实际修改数据
|
|
1566
|
+
* @returns 同步结果,包含被回退的任务列表
|
|
1567
|
+
*/
|
|
1568
|
+
syncWithGit(dryRun = false) {
|
|
1569
|
+
const currentHead = this.getCurrentGitCommit();
|
|
1570
|
+
if (!currentHead) {
|
|
1571
|
+
return {
|
|
1572
|
+
checked: 0,
|
|
1573
|
+
reverted: [],
|
|
1574
|
+
currentHead: 'unknown',
|
|
1575
|
+
error: 'Git not available or not in a Git repository',
|
|
1576
|
+
};
|
|
1577
|
+
}
|
|
1578
|
+
const mainTasks = this.listMainTasks();
|
|
1579
|
+
const reverted = [];
|
|
1580
|
+
let checked = 0;
|
|
1581
|
+
for (const mt of mainTasks) {
|
|
1582
|
+
const subs = this.listSubTasks(mt.taskId);
|
|
1583
|
+
for (const sub of subs) {
|
|
1584
|
+
if (sub.status !== 'completed' || !sub.completedAtCommit)
|
|
1585
|
+
continue;
|
|
1586
|
+
checked++;
|
|
1587
|
+
if (!this.isAncestor(sub.completedAtCommit, currentHead)) {
|
|
1588
|
+
const reason = `Commit ${sub.completedAtCommit} is not ancestor of HEAD ${currentHead}`;
|
|
1589
|
+
if (!dryRun) {
|
|
1590
|
+
this.revertSubTask(sub.taskId, reason);
|
|
1591
|
+
}
|
|
1592
|
+
reverted.push({
|
|
1593
|
+
taskId: sub.taskId,
|
|
1594
|
+
title: sub.title,
|
|
1595
|
+
parentTaskId: sub.parentTaskId,
|
|
1596
|
+
completedAtCommit: sub.completedAtCommit,
|
|
1597
|
+
reason: `Commit ${sub.completedAtCommit} not found in current branch (HEAD: ${currentHead})`,
|
|
1598
|
+
});
|
|
1599
|
+
}
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1602
|
+
return { checked, reverted, currentHead };
|
|
1603
|
+
}
|
|
1604
|
+
// ==========================================================================
|
|
1605
|
+
// Utilities
|
|
1606
|
+
// ==========================================================================
|
|
1607
|
+
/**
|
|
1608
|
+
* 生成文本进度条
|
|
1609
|
+
*/
|
|
1610
|
+
progressBar(percent) {
|
|
1611
|
+
const total = 20;
|
|
1612
|
+
const filled = Math.round((percent / 100) * total);
|
|
1613
|
+
const empty = total - filled;
|
|
1614
|
+
return `[${'█'.repeat(filled)}${'░'.repeat(empty)}]`;
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1617
|
+
exports.DevPlanStore = DevPlanStore;
|
|
1618
|
+
// ============================================================================
|
|
1619
|
+
// Factory Functions
|
|
1620
|
+
// ============================================================================
|
|
1621
|
+
/**
|
|
1622
|
+
* 为项目创建 DevPlanStore
|
|
1623
|
+
*
|
|
1624
|
+
* @param projectName - 项目名称
|
|
1625
|
+
* @param basePath - 存储基础路径(默认优先使用项目内 .devplan/,回退到 ~/.aifastdb/dev-plans/)
|
|
1626
|
+
*
|
|
1627
|
+
* 存储路径解析优先级:
|
|
1628
|
+
* 1. 显式 basePath 参数
|
|
1629
|
+
* 2. AIFASTDB_DEVPLAN_PATH 环境变量
|
|
1630
|
+
* 3. 项目根目录/.devplan/(通过 .git 或 package.json 定位)
|
|
1631
|
+
* 4. ~/.aifastdb/dev-plans/(兜底)
|
|
1632
|
+
*
|
|
1633
|
+
* 最终路径:{basePath}/{projectName}/documents.jsonl + tasks.jsonl
|
|
1634
|
+
*/
|
|
1635
|
+
function createDevPlan(projectName, basePath) {
|
|
1636
|
+
const base = basePath || getDefaultBasePath();
|
|
1637
|
+
return new DevPlanStore(projectName, {
|
|
1638
|
+
documentPath: path.join(base, projectName, 'documents.jsonl'),
|
|
1639
|
+
taskPath: path.join(base, projectName, 'tasks.jsonl'),
|
|
1640
|
+
modulePath: path.join(base, projectName, 'modules.jsonl'),
|
|
1641
|
+
});
|
|
1642
|
+
}
|
|
1643
|
+
/**
|
|
1644
|
+
* 列出所有已有的 DevPlan 项目
|
|
1645
|
+
*/
|
|
1646
|
+
function listDevPlans(basePath) {
|
|
1647
|
+
const base = basePath || getDefaultBasePath();
|
|
1648
|
+
try {
|
|
1649
|
+
const fs = require('fs');
|
|
1650
|
+
if (!fs.existsSync(base))
|
|
1651
|
+
return [];
|
|
1652
|
+
return fs.readdirSync(base).filter((name) => {
|
|
1653
|
+
const fullPath = path.join(base, name);
|
|
1654
|
+
return fs.statSync(fullPath).isDirectory();
|
|
1655
|
+
});
|
|
1656
|
+
}
|
|
1657
|
+
catch {
|
|
1658
|
+
return [];
|
|
1659
|
+
}
|
|
1660
|
+
}
|
|
1661
|
+
/**
|
|
1662
|
+
* 所有标准章节类型列表
|
|
1663
|
+
*/
|
|
1664
|
+
exports.ALL_SECTIONS = [
|
|
1665
|
+
'overview', 'core_concepts', 'api_design', 'file_structure',
|
|
1666
|
+
'config', 'examples', 'technical_notes', 'api_endpoints',
|
|
1667
|
+
'milestones', 'changelog', 'custom',
|
|
1668
|
+
];
|
|
1669
|
+
/**
|
|
1670
|
+
* 标准章节说明
|
|
1671
|
+
*/
|
|
1672
|
+
exports.SECTION_DESCRIPTIONS = {
|
|
1673
|
+
overview: '概述:项目背景、目标、架构图、版本说明',
|
|
1674
|
+
core_concepts: '核心概念:术语定义、数据模型、关键抽象',
|
|
1675
|
+
api_design: 'API 设计:接口定义、类型系统、使用方式',
|
|
1676
|
+
file_structure: '文件结构:目录树、模块划分、代码组织',
|
|
1677
|
+
config: '配置设计:配置文件格式、环境变量、示例',
|
|
1678
|
+
examples: '使用示例:代码片段、调用演示、最佳实践',
|
|
1679
|
+
technical_notes: '技术笔记:性能考虑、安全设计、错误处理等(支持多个子文档)',
|
|
1680
|
+
api_endpoints: 'API 端点汇总:REST/RPC 端点列表、请求/响应格式',
|
|
1681
|
+
milestones: '里程碑:版本目标、交付节点、时间线',
|
|
1682
|
+
changelog: '变更记录:版本历史、修改内容、作者',
|
|
1683
|
+
custom: '自定义章节:用户自行扩展的任意内容',
|
|
1684
|
+
};
|
|
1685
|
+
//# sourceMappingURL=dev-plan-store.js.map
|