aifastdb 2.2.1 → 2.2.6
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/aifastdb.win32-x64-msvc.node +0 -0
- package/dist/dev-plan-store.d.ts +245 -14
- package/dist/dev-plan-store.d.ts.map +1 -1
- package/dist/dev-plan-store.js +765 -59
- package/dist/dev-plan-store.js.map +1 -1
- package/dist/federation/FederatedDb.d.ts +138 -1
- package/dist/federation/FederatedDb.d.ts.map +1 -1
- package/dist/federation/FederatedDb.js +158 -0
- package/dist/federation/FederatedDb.js.map +1 -1
- package/dist/federation/index.d.ts +1 -1
- package/dist/federation/index.d.ts.map +1 -1
- package/dist/federation/index.js.map +1 -1
- package/dist/federation/types.d.ts +136 -4
- package/dist/federation/types.d.ts.map +1 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -9
- package/dist/index.js.map +1 -1
- package/dist/mcp-server/index.d.ts +5 -14
- package/dist/mcp-server/index.d.ts.map +1 -1
- package/dist/mcp-server/index.js +6 -523
- package/dist/mcp-server/index.js.map +1 -1
- package/dist/security/server/routes.d.ts +46 -0
- package/dist/security/server/routes.d.ts.map +1 -1
- package/dist/security/server/routes.js +568 -1
- package/dist/security/server/routes.js.map +1 -1
- package/dist/social-graph-v2.d.ts +55 -0
- package/dist/social-graph-v2.d.ts.map +1 -1
- package/dist/social-graph-v2.js +61 -0
- package/dist/social-graph-v2.js.map +1 -1
- package/package.json +1 -1
package/dist/dev-plan-store.js
CHANGED
|
@@ -89,13 +89,40 @@ function sectionImportance(section) {
|
|
|
89
89
|
}
|
|
90
90
|
/**
|
|
91
91
|
* 获取默认的 DevPlan 存储基础路径
|
|
92
|
+
*
|
|
93
|
+
* 优先级:
|
|
94
|
+
* 1. AIFASTDB_DEVPLAN_PATH 环境变量(显式指定)
|
|
95
|
+
* 2. 项目内 .devplan/ 目录(天然跟随 Git 版本管理)
|
|
96
|
+
* 3. 回退到用户目录 ~/.aifastdb/dev-plans/(兜底)
|
|
92
97
|
*/
|
|
93
98
|
function getDefaultBasePath() {
|
|
94
99
|
if (process.env.AIFASTDB_DEVPLAN_PATH) {
|
|
95
100
|
return process.env.AIFASTDB_DEVPLAN_PATH;
|
|
96
101
|
}
|
|
102
|
+
// 尝试定位项目根目录(查找 .git 或 package.json 所在目录)
|
|
103
|
+
const projectRoot = findProjectRoot();
|
|
104
|
+
if (projectRoot) {
|
|
105
|
+
return path.join(projectRoot, '.devplan');
|
|
106
|
+
}
|
|
107
|
+
// 兜底:用户目录
|
|
97
108
|
return path.join(os.homedir(), '.aifastdb', 'dev-plans');
|
|
98
109
|
}
|
|
110
|
+
/**
|
|
111
|
+
* 从当前工作目录向上查找项目根目录
|
|
112
|
+
* 通过 .git 目录或 package.json 文件来判断
|
|
113
|
+
*/
|
|
114
|
+
function findProjectRoot() {
|
|
115
|
+
const fs = require('fs');
|
|
116
|
+
let dir = process.cwd();
|
|
117
|
+
const root = path.parse(dir).root;
|
|
118
|
+
while (dir !== root) {
|
|
119
|
+
if (fs.existsSync(path.join(dir, '.git')) || fs.existsSync(path.join(dir, 'package.json'))) {
|
|
120
|
+
return dir;
|
|
121
|
+
}
|
|
122
|
+
dir = path.dirname(dir);
|
|
123
|
+
}
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
99
126
|
// ============================================================================
|
|
100
127
|
// DevPlanStore Implementation
|
|
101
128
|
// ============================================================================
|
|
@@ -111,6 +138,7 @@ class DevPlanStore {
|
|
|
111
138
|
this.projectName = projectName;
|
|
112
139
|
this.docStore = new native_1.EnhancedDocumentStore(config.documentPath, (0, document_store_1.documentStoreProductionConfig)());
|
|
113
140
|
this.taskStore = new native_1.EnhancedDocumentStore(config.taskPath, (0, document_store_1.documentStoreProductionConfig)());
|
|
141
|
+
this.moduleStore = new native_1.EnhancedDocumentStore(config.modulePath, (0, document_store_1.documentStoreProductionConfig)());
|
|
114
142
|
}
|
|
115
143
|
// ==========================================================================
|
|
116
144
|
// Document Section Operations
|
|
@@ -128,15 +156,20 @@ class DevPlanStore {
|
|
|
128
156
|
}
|
|
129
157
|
const version = input.version || '1.0.0';
|
|
130
158
|
const now = Date.now();
|
|
159
|
+
const finalModuleId = input.moduleId || existing?.moduleId;
|
|
160
|
+
const tags = [
|
|
161
|
+
`plan:${this.projectName}`,
|
|
162
|
+
`section:${input.section}`,
|
|
163
|
+
...(input.subSection ? [`sub:${input.subSection}`] : []),
|
|
164
|
+
`ver:${version}`,
|
|
165
|
+
];
|
|
166
|
+
if (finalModuleId) {
|
|
167
|
+
tags.push(`module:${finalModuleId}`);
|
|
168
|
+
}
|
|
131
169
|
const docInput = {
|
|
132
170
|
content: input.content,
|
|
133
171
|
contentType: native_2.ContentType.Text,
|
|
134
|
-
tags
|
|
135
|
-
`plan:${this.projectName}`,
|
|
136
|
-
`section:${input.section}`,
|
|
137
|
-
...(input.subSection ? [`sub:${input.subSection}`] : []),
|
|
138
|
-
`ver:${version}`,
|
|
139
|
-
],
|
|
172
|
+
tags,
|
|
140
173
|
metadata: {
|
|
141
174
|
projectName: this.projectName,
|
|
142
175
|
section: input.section,
|
|
@@ -144,6 +177,7 @@ class DevPlanStore {
|
|
|
144
177
|
version,
|
|
145
178
|
subSection: input.subSection || null,
|
|
146
179
|
relatedSections: input.relatedSections || [],
|
|
180
|
+
moduleId: finalModuleId || null,
|
|
147
181
|
createdAt: existing?.createdAt || now,
|
|
148
182
|
updatedAt: now,
|
|
149
183
|
},
|
|
@@ -172,8 +206,8 @@ class DevPlanStore {
|
|
|
172
206
|
}
|
|
173
207
|
if (filtered.length === 0)
|
|
174
208
|
return null;
|
|
175
|
-
//
|
|
176
|
-
const latest = filtered.sort((a, b) => b
|
|
209
|
+
// 返回最新版本(以 metadata.updatedAt 为判定依据)
|
|
210
|
+
const latest = filtered.sort((a, b) => this.getDocUpdatedAt(b) - this.getDocUpdatedAt(a))[0];
|
|
177
211
|
return this.docToDevPlanDoc(latest);
|
|
178
212
|
}
|
|
179
213
|
/**
|
|
@@ -184,14 +218,14 @@ class DevPlanStore {
|
|
|
184
218
|
listSections() {
|
|
185
219
|
const planTag = `plan:${this.projectName}`;
|
|
186
220
|
const docs = this.docStore.findByTag(planTag);
|
|
187
|
-
// 按 section+subSection
|
|
221
|
+
// 按 section+subSection 去重,保留最新版本(以 metadata.updatedAt 判定)
|
|
188
222
|
const latestMap = new Map();
|
|
189
223
|
for (const doc of docs) {
|
|
190
224
|
const sectionTag = doc.tags.find((t) => t.startsWith('section:'));
|
|
191
225
|
const subTag = doc.tags.find((t) => t.startsWith('sub:'));
|
|
192
226
|
const key = `${sectionTag || 'unknown'}|${subTag || ''}`;
|
|
193
227
|
const existing = latestMap.get(key);
|
|
194
|
-
if (!existing || doc
|
|
228
|
+
if (!existing || this.getDocUpdatedAt(doc) > this.getDocUpdatedAt(existing)) {
|
|
195
229
|
latestMap.set(key, doc);
|
|
196
230
|
}
|
|
197
231
|
}
|
|
@@ -223,14 +257,14 @@ class DevPlanStore {
|
|
|
223
257
|
searchSections(query, limit = 10) {
|
|
224
258
|
const planTag = `plan:${this.projectName}`;
|
|
225
259
|
const allDocs = this.docStore.findByTag(planTag);
|
|
226
|
-
// 按 section+subSection
|
|
260
|
+
// 按 section+subSection 去重,保留最新版本(以 metadata.updatedAt 判定)
|
|
227
261
|
const latestMap = new Map();
|
|
228
262
|
for (const doc of allDocs) {
|
|
229
263
|
const sectionTag = doc.tags.find((t) => t.startsWith('section:'));
|
|
230
264
|
const subTag = doc.tags.find((t) => t.startsWith('sub:'));
|
|
231
265
|
const key = `${sectionTag || 'unknown'}|${subTag || ''}`;
|
|
232
266
|
const existing = latestMap.get(key);
|
|
233
|
-
if (!existing || doc
|
|
267
|
+
if (!existing || this.getDocUpdatedAt(doc) > this.getDocUpdatedAt(existing)) {
|
|
234
268
|
latestMap.set(key, doc);
|
|
235
269
|
}
|
|
236
270
|
}
|
|
@@ -275,20 +309,25 @@ class DevPlanStore {
|
|
|
275
309
|
totalSubtasks: 0,
|
|
276
310
|
completedSubtasks: 0,
|
|
277
311
|
};
|
|
312
|
+
const tags = [
|
|
313
|
+
`plan:${this.projectName}`,
|
|
314
|
+
'type:main-task',
|
|
315
|
+
`mtask:${input.taskId}`,
|
|
316
|
+
`priority:${input.priority}`,
|
|
317
|
+
'status:pending',
|
|
318
|
+
];
|
|
319
|
+
if (input.moduleId) {
|
|
320
|
+
tags.push(`module:${input.moduleId}`);
|
|
321
|
+
}
|
|
278
322
|
const docInput = {
|
|
279
323
|
content: JSON.stringify(taskData),
|
|
280
324
|
contentType: native_2.ContentType.Text,
|
|
281
|
-
tags
|
|
282
|
-
`plan:${this.projectName}`,
|
|
283
|
-
'type:main-task',
|
|
284
|
-
`mtask:${input.taskId}`,
|
|
285
|
-
`priority:${input.priority}`,
|
|
286
|
-
'status:pending',
|
|
287
|
-
],
|
|
325
|
+
tags,
|
|
288
326
|
metadata: {
|
|
289
327
|
projectName: this.projectName,
|
|
290
328
|
taskId: input.taskId,
|
|
291
329
|
status: 'pending',
|
|
330
|
+
moduleId: input.moduleId || null,
|
|
292
331
|
createdAt: now,
|
|
293
332
|
updatedAt: now,
|
|
294
333
|
completedAt: null,
|
|
@@ -301,17 +340,111 @@ class DevPlanStore {
|
|
|
301
340
|
id,
|
|
302
341
|
projectName: this.projectName,
|
|
303
342
|
...taskData,
|
|
343
|
+
moduleId: input.moduleId,
|
|
304
344
|
status: 'pending',
|
|
305
345
|
createdAt: now,
|
|
306
346
|
updatedAt: now,
|
|
307
347
|
completedAt: null,
|
|
308
348
|
};
|
|
309
349
|
}
|
|
350
|
+
/**
|
|
351
|
+
* 幂等导入主任务(Upsert)
|
|
352
|
+
*
|
|
353
|
+
* - 如果主任务不存在 → 创建新任务
|
|
354
|
+
* - 如果主任务已存在 → 更新标题/描述/优先级等字段,但保留已有的更高级状态
|
|
355
|
+
* (例如已完成的任务不会被重置为 pending)
|
|
356
|
+
* - updatedAt 保证严格递增,不会与历史版本重复
|
|
357
|
+
*
|
|
358
|
+
* @param input 主任务输入
|
|
359
|
+
* @param options.preserveStatus 若为 true(默认),则不覆盖已完成的状态
|
|
360
|
+
* @param options.status 导入时的目标状态(默认 pending)
|
|
361
|
+
* @returns 创建或更新后的主任务
|
|
362
|
+
*/
|
|
363
|
+
upsertMainTask(input, options) {
|
|
364
|
+
const preserveStatus = options?.preserveStatus !== false; // 默认 true
|
|
365
|
+
const targetStatus = options?.status || 'pending';
|
|
366
|
+
const existing = this.getMainTask(input.taskId);
|
|
367
|
+
if (!existing) {
|
|
368
|
+
// 新建
|
|
369
|
+
const task = this.createMainTask(input);
|
|
370
|
+
// 如果目标状态不是 pending,更新状态
|
|
371
|
+
if (targetStatus !== 'pending') {
|
|
372
|
+
return this.updateMainTaskStatus(task.taskId, targetStatus) || task;
|
|
373
|
+
}
|
|
374
|
+
return task;
|
|
375
|
+
}
|
|
376
|
+
// 已存在 — 决定最终状态
|
|
377
|
+
let finalStatus = targetStatus;
|
|
378
|
+
if (preserveStatus) {
|
|
379
|
+
// 状态优先级: completed > in_progress > pending > cancelled
|
|
380
|
+
const statusPriority = {
|
|
381
|
+
cancelled: 0,
|
|
382
|
+
pending: 1,
|
|
383
|
+
in_progress: 2,
|
|
384
|
+
completed: 3,
|
|
385
|
+
};
|
|
386
|
+
if (statusPriority[existing.status] >= statusPriority[targetStatus]) {
|
|
387
|
+
finalStatus = existing.status; // 保留更高级状态
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
// 删除旧版本并确保时间戳递增
|
|
391
|
+
this.deleteAndEnsureTimestampAdvance(this.taskStore, existing.id);
|
|
392
|
+
const now = Date.now();
|
|
393
|
+
const completedAt = finalStatus === 'completed' ? (existing.completedAt || now) : null;
|
|
394
|
+
const finalModuleId = input.moduleId || existing.moduleId;
|
|
395
|
+
const taskData = {
|
|
396
|
+
taskId: input.taskId,
|
|
397
|
+
title: input.title,
|
|
398
|
+
priority: input.priority,
|
|
399
|
+
description: input.description || existing.description || '',
|
|
400
|
+
estimatedHours: input.estimatedHours || existing.estimatedHours || 0,
|
|
401
|
+
relatedSections: input.relatedSections || existing.relatedSections || [],
|
|
402
|
+
totalSubtasks: existing.totalSubtasks,
|
|
403
|
+
completedSubtasks: existing.completedSubtasks,
|
|
404
|
+
};
|
|
405
|
+
const tags = [
|
|
406
|
+
`plan:${this.projectName}`,
|
|
407
|
+
'type:main-task',
|
|
408
|
+
`mtask:${input.taskId}`,
|
|
409
|
+
`priority:${input.priority}`,
|
|
410
|
+
`status:${finalStatus}`,
|
|
411
|
+
];
|
|
412
|
+
if (finalModuleId) {
|
|
413
|
+
tags.push(`module:${finalModuleId}`);
|
|
414
|
+
}
|
|
415
|
+
const docInput = {
|
|
416
|
+
content: JSON.stringify(taskData),
|
|
417
|
+
contentType: native_2.ContentType.Text,
|
|
418
|
+
tags,
|
|
419
|
+
metadata: {
|
|
420
|
+
projectName: this.projectName,
|
|
421
|
+
taskId: input.taskId,
|
|
422
|
+
status: finalStatus,
|
|
423
|
+
moduleId: finalModuleId || null,
|
|
424
|
+
createdAt: existing.createdAt,
|
|
425
|
+
updatedAt: now,
|
|
426
|
+
completedAt,
|
|
427
|
+
},
|
|
428
|
+
importance: input.priority === 'P0' ? 0.95 : input.priority === 'P1' ? 0.8 : 0.6,
|
|
429
|
+
};
|
|
430
|
+
const id = this.taskStore.put(docInput);
|
|
431
|
+
this.taskStore.flush();
|
|
432
|
+
return {
|
|
433
|
+
...taskData,
|
|
434
|
+
id,
|
|
435
|
+
projectName: this.projectName,
|
|
436
|
+
moduleId: finalModuleId,
|
|
437
|
+
status: finalStatus,
|
|
438
|
+
createdAt: existing.createdAt,
|
|
439
|
+
updatedAt: now,
|
|
440
|
+
completedAt,
|
|
441
|
+
};
|
|
442
|
+
}
|
|
310
443
|
/**
|
|
311
444
|
* 获取主任务
|
|
312
445
|
*
|
|
313
446
|
* 由于 JSONL append-only 存储会保留历史版本,
|
|
314
|
-
* 需要按
|
|
447
|
+
* 需要按 metadata.updatedAt 降序取最新版本。
|
|
315
448
|
*/
|
|
316
449
|
getMainTask(taskId) {
|
|
317
450
|
const tag = `mtask:${taskId}`;
|
|
@@ -319,8 +452,8 @@ class DevPlanStore {
|
|
|
319
452
|
.filter((doc) => doc.tags.includes(`plan:${this.projectName}`));
|
|
320
453
|
if (docs.length === 0)
|
|
321
454
|
return null;
|
|
322
|
-
//
|
|
323
|
-
const latest = docs.sort((a, b) => b
|
|
455
|
+
// 取最新版本(以 metadata.updatedAt 为判定依据)
|
|
456
|
+
const latest = docs.sort((a, b) => this.getDocUpdatedAt(b) - this.getDocUpdatedAt(a))[0];
|
|
324
457
|
return this.docToMainTask(latest);
|
|
325
458
|
}
|
|
326
459
|
/**
|
|
@@ -341,6 +474,10 @@ class DevPlanStore {
|
|
|
341
474
|
const priorityTag = `priority:${filter.priority}`;
|
|
342
475
|
docs = docs.filter((doc) => doc.tags.includes(priorityTag));
|
|
343
476
|
}
|
|
477
|
+
if (filter?.moduleId) {
|
|
478
|
+
const moduleTag = `module:${filter.moduleId}`;
|
|
479
|
+
docs = docs.filter((doc) => doc.tags.includes(moduleTag));
|
|
480
|
+
}
|
|
344
481
|
return docs.map((doc) => this.docToMainTask(doc));
|
|
345
482
|
}
|
|
346
483
|
/**
|
|
@@ -363,20 +500,25 @@ class DevPlanStore {
|
|
|
363
500
|
totalSubtasks: mainTask.totalSubtasks,
|
|
364
501
|
completedSubtasks: mainTask.completedSubtasks,
|
|
365
502
|
};
|
|
503
|
+
const tags = [
|
|
504
|
+
`plan:${this.projectName}`,
|
|
505
|
+
'type:main-task',
|
|
506
|
+
`mtask:${mainTask.taskId}`,
|
|
507
|
+
`priority:${mainTask.priority}`,
|
|
508
|
+
`status:${status}`,
|
|
509
|
+
];
|
|
510
|
+
if (mainTask.moduleId) {
|
|
511
|
+
tags.push(`module:${mainTask.moduleId}`);
|
|
512
|
+
}
|
|
366
513
|
const docInput = {
|
|
367
514
|
content: JSON.stringify(taskData),
|
|
368
515
|
contentType: native_2.ContentType.Text,
|
|
369
|
-
tags
|
|
370
|
-
`plan:${this.projectName}`,
|
|
371
|
-
'type:main-task',
|
|
372
|
-
`mtask:${mainTask.taskId}`,
|
|
373
|
-
`priority:${mainTask.priority}`,
|
|
374
|
-
`status:${status}`,
|
|
375
|
-
],
|
|
516
|
+
tags,
|
|
376
517
|
metadata: {
|
|
377
518
|
projectName: this.projectName,
|
|
378
519
|
taskId: mainTask.taskId,
|
|
379
520
|
status,
|
|
521
|
+
moduleId: mainTask.moduleId || null,
|
|
380
522
|
createdAt: mainTask.createdAt,
|
|
381
523
|
updatedAt: now,
|
|
382
524
|
completedAt,
|
|
@@ -455,10 +597,146 @@ class DevPlanStore {
|
|
|
455
597
|
completedAt: null,
|
|
456
598
|
};
|
|
457
599
|
}
|
|
600
|
+
/**
|
|
601
|
+
* 幂等导入子任务(Upsert)
|
|
602
|
+
*
|
|
603
|
+
* - 如果子任务不存在 → 创建新子任务
|
|
604
|
+
* - 如果子任务已存在 → 更新标题/描述等字段,但保留已有的更高级状态
|
|
605
|
+
* (例如已完成的任务不会被重置为 pending)
|
|
606
|
+
* - updatedAt 保证严格递增,不会与历史版本重复
|
|
607
|
+
*
|
|
608
|
+
* @param input 子任务输入
|
|
609
|
+
* @param options.preserveStatus 若为 true(默认),则不覆盖已完成的状态
|
|
610
|
+
* @param options.status 导入时的目标状态(默认 pending)
|
|
611
|
+
* @returns 创建或更新后的子任务
|
|
612
|
+
*/
|
|
613
|
+
upsertSubTask(input, options) {
|
|
614
|
+
const preserveStatus = options?.preserveStatus !== false; // 默认 true
|
|
615
|
+
const targetStatus = options?.status || 'pending';
|
|
616
|
+
const existing = this.getSubTask(input.taskId);
|
|
617
|
+
if (!existing) {
|
|
618
|
+
// 新建(验证父任务存在)
|
|
619
|
+
const mainTask = this.getMainTask(input.parentTaskId);
|
|
620
|
+
if (!mainTask) {
|
|
621
|
+
throw new Error(`Parent main task "${input.parentTaskId}" not found for project "${this.projectName}"`);
|
|
622
|
+
}
|
|
623
|
+
const now = Date.now();
|
|
624
|
+
const taskData = {
|
|
625
|
+
taskId: input.taskId,
|
|
626
|
+
title: input.title,
|
|
627
|
+
estimatedHours: input.estimatedHours || 0,
|
|
628
|
+
relatedFiles: input.relatedFiles || [],
|
|
629
|
+
description: input.description || '',
|
|
630
|
+
};
|
|
631
|
+
const docInput = {
|
|
632
|
+
content: JSON.stringify(taskData),
|
|
633
|
+
contentType: native_2.ContentType.Text,
|
|
634
|
+
parentId: mainTask.id,
|
|
635
|
+
tags: [
|
|
636
|
+
`plan:${this.projectName}`,
|
|
637
|
+
'type:sub-task',
|
|
638
|
+
`stask:${input.taskId}`,
|
|
639
|
+
`parent:${input.parentTaskId}`,
|
|
640
|
+
`status:${targetStatus}`,
|
|
641
|
+
],
|
|
642
|
+
metadata: {
|
|
643
|
+
projectName: this.projectName,
|
|
644
|
+
taskId: input.taskId,
|
|
645
|
+
parentTaskId: input.parentTaskId,
|
|
646
|
+
status: targetStatus,
|
|
647
|
+
createdAt: now,
|
|
648
|
+
updatedAt: now,
|
|
649
|
+
completedAt: targetStatus === 'completed' ? now : null,
|
|
650
|
+
},
|
|
651
|
+
importance: 0.7,
|
|
652
|
+
};
|
|
653
|
+
const id = this.taskStore.put(docInput);
|
|
654
|
+
this.refreshMainTaskCounts(input.parentTaskId);
|
|
655
|
+
this.taskStore.flush();
|
|
656
|
+
return {
|
|
657
|
+
id,
|
|
658
|
+
projectName: this.projectName,
|
|
659
|
+
...taskData,
|
|
660
|
+
parentTaskId: input.parentTaskId,
|
|
661
|
+
status: targetStatus,
|
|
662
|
+
createdAt: now,
|
|
663
|
+
updatedAt: now,
|
|
664
|
+
completedAt: targetStatus === 'completed' ? now : null,
|
|
665
|
+
};
|
|
666
|
+
}
|
|
667
|
+
// 已存在 — 决定最终状态
|
|
668
|
+
let finalStatus = targetStatus;
|
|
669
|
+
if (preserveStatus) {
|
|
670
|
+
const statusPriority = {
|
|
671
|
+
cancelled: 0,
|
|
672
|
+
pending: 1,
|
|
673
|
+
in_progress: 2,
|
|
674
|
+
completed: 3,
|
|
675
|
+
};
|
|
676
|
+
if (statusPriority[existing.status] >= statusPriority[targetStatus]) {
|
|
677
|
+
finalStatus = existing.status;
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
// 检查是否有实质性变化(避免无意义的更新,减少历史版本膨胀)
|
|
681
|
+
if (existing.title === input.title &&
|
|
682
|
+
existing.description === (input.description || '') &&
|
|
683
|
+
existing.status === finalStatus &&
|
|
684
|
+
existing.estimatedHours === (input.estimatedHours || 0)) {
|
|
685
|
+
// 无变化,直接返回
|
|
686
|
+
return existing;
|
|
687
|
+
}
|
|
688
|
+
// 删除旧版本并确保时间戳递增
|
|
689
|
+
this.deleteAndEnsureTimestampAdvance(this.taskStore, existing.id);
|
|
690
|
+
const mainTask = this.getMainTask(input.parentTaskId);
|
|
691
|
+
const now = Date.now();
|
|
692
|
+
const completedAt = finalStatus === 'completed' ? (existing.completedAt || now) : null;
|
|
693
|
+
const taskData = {
|
|
694
|
+
taskId: input.taskId,
|
|
695
|
+
title: input.title,
|
|
696
|
+
estimatedHours: input.estimatedHours || existing.estimatedHours || 0,
|
|
697
|
+
relatedFiles: input.relatedFiles || existing.relatedFiles || [],
|
|
698
|
+
description: input.description || existing.description || '',
|
|
699
|
+
};
|
|
700
|
+
const docInput = {
|
|
701
|
+
content: JSON.stringify(taskData),
|
|
702
|
+
contentType: native_2.ContentType.Text,
|
|
703
|
+
parentId: mainTask?.id || undefined,
|
|
704
|
+
tags: [
|
|
705
|
+
`plan:${this.projectName}`,
|
|
706
|
+
'type:sub-task',
|
|
707
|
+
`stask:${input.taskId}`,
|
|
708
|
+
`parent:${input.parentTaskId}`,
|
|
709
|
+
`status:${finalStatus}`,
|
|
710
|
+
],
|
|
711
|
+
metadata: {
|
|
712
|
+
projectName: this.projectName,
|
|
713
|
+
taskId: input.taskId,
|
|
714
|
+
parentTaskId: input.parentTaskId,
|
|
715
|
+
status: finalStatus,
|
|
716
|
+
createdAt: existing.createdAt,
|
|
717
|
+
updatedAt: now,
|
|
718
|
+
completedAt,
|
|
719
|
+
completedAtCommit: existing.completedAtCommit || null,
|
|
720
|
+
revertReason: existing.revertReason || null,
|
|
721
|
+
},
|
|
722
|
+
importance: 0.7,
|
|
723
|
+
};
|
|
724
|
+
const id = this.taskStore.put(docInput);
|
|
725
|
+
this.refreshMainTaskCounts(input.parentTaskId);
|
|
726
|
+
this.taskStore.flush();
|
|
727
|
+
return {
|
|
728
|
+
...existing,
|
|
729
|
+
...taskData,
|
|
730
|
+
id,
|
|
731
|
+
status: finalStatus,
|
|
732
|
+
updatedAt: now,
|
|
733
|
+
completedAt,
|
|
734
|
+
};
|
|
735
|
+
}
|
|
458
736
|
/**
|
|
459
737
|
* 获取子任务
|
|
460
738
|
*
|
|
461
|
-
* 取同一 taskId
|
|
739
|
+
* 取同一 taskId 的最新版本(以 metadata.updatedAt 判定)。
|
|
462
740
|
*/
|
|
463
741
|
getSubTask(taskId) {
|
|
464
742
|
const tag = `stask:${taskId}`;
|
|
@@ -466,8 +744,8 @@ class DevPlanStore {
|
|
|
466
744
|
.filter((doc) => doc.tags.includes(`plan:${this.projectName}`));
|
|
467
745
|
if (docs.length === 0)
|
|
468
746
|
return null;
|
|
469
|
-
//
|
|
470
|
-
const latest = docs.sort((a, b) => b
|
|
747
|
+
// 取最新版本(以 metadata.updatedAt 为判定依据)
|
|
748
|
+
const latest = docs.sort((a, b) => this.getDocUpdatedAt(b) - this.getDocUpdatedAt(a))[0];
|
|
471
749
|
return this.docToSubTask(latest);
|
|
472
750
|
}
|
|
473
751
|
/**
|
|
@@ -490,8 +768,11 @@ class DevPlanStore {
|
|
|
490
768
|
}
|
|
491
769
|
/**
|
|
492
770
|
* 更新子任务状态
|
|
771
|
+
*
|
|
772
|
+
* @param options.completedAtCommit - 完成时的 Git commit hash(仅 status=completed 时有效)
|
|
773
|
+
* @param options.revertReason - 回退原因(仅 status 从 completed 变为 pending 时有效)
|
|
493
774
|
*/
|
|
494
|
-
updateSubTaskStatus(taskId, status) {
|
|
775
|
+
updateSubTaskStatus(taskId, status, options) {
|
|
495
776
|
const subTask = this.getSubTask(taskId);
|
|
496
777
|
if (!subTask)
|
|
497
778
|
return null;
|
|
@@ -499,7 +780,11 @@ class DevPlanStore {
|
|
|
499
780
|
const mainTask = this.getMainTask(subTask.parentTaskId);
|
|
500
781
|
this.deleteAndEnsureTimestampAdvance(this.taskStore, subTask.id);
|
|
501
782
|
const now = Date.now();
|
|
502
|
-
const completedAt = status === 'completed' ? now : subTask.completedAt;
|
|
783
|
+
const completedAt = status === 'completed' ? now : (status === 'pending' ? null : subTask.completedAt);
|
|
784
|
+
const completedAtCommit = status === 'completed'
|
|
785
|
+
? (options?.completedAtCommit || subTask.completedAtCommit)
|
|
786
|
+
: (status === 'pending' ? undefined : subTask.completedAtCommit);
|
|
787
|
+
const revertReason = options?.revertReason || (status === 'pending' ? undefined : subTask.revertReason);
|
|
503
788
|
const taskData = {
|
|
504
789
|
taskId: subTask.taskId,
|
|
505
790
|
title: subTask.title,
|
|
@@ -526,6 +811,8 @@ class DevPlanStore {
|
|
|
526
811
|
createdAt: subTask.createdAt,
|
|
527
812
|
updatedAt: now,
|
|
528
813
|
completedAt,
|
|
814
|
+
completedAtCommit: completedAtCommit || null,
|
|
815
|
+
revertReason: revertReason || null,
|
|
529
816
|
},
|
|
530
817
|
importance: 0.7,
|
|
531
818
|
};
|
|
@@ -537,6 +824,8 @@ class DevPlanStore {
|
|
|
537
824
|
status,
|
|
538
825
|
updatedAt: now,
|
|
539
826
|
completedAt,
|
|
827
|
+
completedAtCommit,
|
|
828
|
+
revertReason,
|
|
540
829
|
};
|
|
541
830
|
}
|
|
542
831
|
// ==========================================================================
|
|
@@ -546,34 +835,40 @@ class DevPlanStore {
|
|
|
546
835
|
* 完成子任务 — 核心自动化方法
|
|
547
836
|
*
|
|
548
837
|
* 自动处理:
|
|
549
|
-
* 1.
|
|
550
|
-
* 2.
|
|
551
|
-
* 3.
|
|
552
|
-
* 4.
|
|
838
|
+
* 1. 获取当前 Git HEAD 的 short SHA 用于锚定
|
|
839
|
+
* 2. 更新子任务状态为 completed,写入 completedAt 时间戳和 completedAtCommit
|
|
840
|
+
* 3. 重新计算主任务的 completedSubtasks 计数
|
|
841
|
+
* 4. 如果全部子任务完成,自动标记主任务为 completed
|
|
842
|
+
* 5. 如果主任务完成,更新 milestones 文档
|
|
553
843
|
*/
|
|
554
844
|
completeSubTask(taskId) {
|
|
555
|
-
// 1.
|
|
556
|
-
const
|
|
845
|
+
// 1. 获取当前 Git commit hash
|
|
846
|
+
const commitHash = this.getCurrentGitCommit();
|
|
847
|
+
// 2. 更新子任务(带 Git commit 锚定)
|
|
848
|
+
const updatedSubTask = this.updateSubTaskStatus(taskId, 'completed', {
|
|
849
|
+
completedAtCommit: commitHash,
|
|
850
|
+
});
|
|
557
851
|
if (!updatedSubTask) {
|
|
558
852
|
throw new Error(`Sub task "${taskId}" not found for project "${this.projectName}"`);
|
|
559
853
|
}
|
|
560
|
-
//
|
|
854
|
+
// 3. 刷新主任务计数
|
|
561
855
|
const updatedMainTask = this.refreshMainTaskCounts(updatedSubTask.parentTaskId);
|
|
562
856
|
if (!updatedMainTask) {
|
|
563
857
|
throw new Error(`Parent main task "${updatedSubTask.parentTaskId}" not found`);
|
|
564
858
|
}
|
|
565
|
-
//
|
|
859
|
+
// 4. 检查主任务是否全部完成
|
|
566
860
|
const mainTaskCompleted = updatedMainTask.totalSubtasks > 0 &&
|
|
567
861
|
updatedMainTask.completedSubtasks >= updatedMainTask.totalSubtasks;
|
|
568
862
|
if (mainTaskCompleted && updatedMainTask.status !== 'completed') {
|
|
569
863
|
const completedMain = this.updateMainTaskStatus(updatedSubTask.parentTaskId, 'completed');
|
|
570
864
|
if (completedMain) {
|
|
571
|
-
//
|
|
865
|
+
// 5. 更新 milestones 文档(如果存在)
|
|
572
866
|
this.autoUpdateMilestones(completedMain);
|
|
573
867
|
return {
|
|
574
868
|
subTask: updatedSubTask,
|
|
575
869
|
mainTask: completedMain,
|
|
576
870
|
mainTaskCompleted: true,
|
|
871
|
+
completedAtCommit: commitHash,
|
|
577
872
|
};
|
|
578
873
|
}
|
|
579
874
|
}
|
|
@@ -581,6 +876,7 @@ class DevPlanStore {
|
|
|
581
876
|
subTask: updatedSubTask,
|
|
582
877
|
mainTask: updatedMainTask,
|
|
583
878
|
mainTaskCompleted,
|
|
879
|
+
completedAtCommit: commitHash,
|
|
584
880
|
};
|
|
585
881
|
}
|
|
586
882
|
/**
|
|
@@ -701,6 +997,196 @@ class DevPlanStore {
|
|
|
701
997
|
return md;
|
|
702
998
|
}
|
|
703
999
|
// ==========================================================================
|
|
1000
|
+
// Module Operations
|
|
1001
|
+
// ==========================================================================
|
|
1002
|
+
/**
|
|
1003
|
+
* 创建功能模块
|
|
1004
|
+
*/
|
|
1005
|
+
createModule(input) {
|
|
1006
|
+
const existing = this.getModule(input.moduleId);
|
|
1007
|
+
if (existing) {
|
|
1008
|
+
throw new Error(`Module "${input.moduleId}" already exists for project "${this.projectName}"`);
|
|
1009
|
+
}
|
|
1010
|
+
const now = Date.now();
|
|
1011
|
+
const status = input.status || 'active';
|
|
1012
|
+
const moduleData = {
|
|
1013
|
+
moduleId: input.moduleId,
|
|
1014
|
+
name: input.name,
|
|
1015
|
+
description: input.description || '',
|
|
1016
|
+
};
|
|
1017
|
+
const docInput = {
|
|
1018
|
+
content: JSON.stringify(moduleData),
|
|
1019
|
+
contentType: native_2.ContentType.Text,
|
|
1020
|
+
tags: [
|
|
1021
|
+
`plan:${this.projectName}`,
|
|
1022
|
+
'type:module',
|
|
1023
|
+
`module:${input.moduleId}`,
|
|
1024
|
+
`status:${status}`,
|
|
1025
|
+
],
|
|
1026
|
+
metadata: {
|
|
1027
|
+
projectName: this.projectName,
|
|
1028
|
+
moduleId: input.moduleId,
|
|
1029
|
+
status,
|
|
1030
|
+
createdAt: now,
|
|
1031
|
+
updatedAt: now,
|
|
1032
|
+
},
|
|
1033
|
+
importance: 0.85,
|
|
1034
|
+
};
|
|
1035
|
+
const id = this.moduleStore.put(docInput);
|
|
1036
|
+
this.moduleStore.flush();
|
|
1037
|
+
return {
|
|
1038
|
+
id,
|
|
1039
|
+
projectName: this.projectName,
|
|
1040
|
+
moduleId: input.moduleId,
|
|
1041
|
+
name: input.name,
|
|
1042
|
+
description: input.description,
|
|
1043
|
+
status,
|
|
1044
|
+
mainTaskCount: 0,
|
|
1045
|
+
subTaskCount: 0,
|
|
1046
|
+
completedSubTaskCount: 0,
|
|
1047
|
+
docCount: 0,
|
|
1048
|
+
createdAt: now,
|
|
1049
|
+
updatedAt: now,
|
|
1050
|
+
};
|
|
1051
|
+
}
|
|
1052
|
+
/**
|
|
1053
|
+
* 获取功能模块(含自动计算的 taskCount/docCount)
|
|
1054
|
+
*/
|
|
1055
|
+
getModule(moduleId) {
|
|
1056
|
+
const tag = `module:${moduleId}`;
|
|
1057
|
+
const docs = this.moduleStore.findByTag(tag)
|
|
1058
|
+
.filter((doc) => doc.tags.includes(`plan:${this.projectName}`));
|
|
1059
|
+
if (docs.length === 0)
|
|
1060
|
+
return null;
|
|
1061
|
+
const latest = docs.sort((a, b) => this.getDocUpdatedAt(b) - this.getDocUpdatedAt(a))[0];
|
|
1062
|
+
return this.docToModule(latest);
|
|
1063
|
+
}
|
|
1064
|
+
/**
|
|
1065
|
+
* 列出所有功能模块
|
|
1066
|
+
*/
|
|
1067
|
+
listModules(filter) {
|
|
1068
|
+
let docs = this.moduleStore.findByTag(`plan:${this.projectName}`)
|
|
1069
|
+
.filter((doc) => doc.tags.includes('type:module'));
|
|
1070
|
+
// 按 moduleId 去重
|
|
1071
|
+
const latestMap = new Map();
|
|
1072
|
+
for (const doc of docs) {
|
|
1073
|
+
const data = JSON.parse(doc.content);
|
|
1074
|
+
const moduleId = data.moduleId;
|
|
1075
|
+
if (!moduleId)
|
|
1076
|
+
continue;
|
|
1077
|
+
const existing = latestMap.get(moduleId);
|
|
1078
|
+
if (!existing || this.getDocUpdatedAt(doc) > this.getDocUpdatedAt(existing)) {
|
|
1079
|
+
latestMap.set(moduleId, doc);
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
docs = Array.from(latestMap.values());
|
|
1083
|
+
if (filter?.status) {
|
|
1084
|
+
const statusTag = `status:${filter.status}`;
|
|
1085
|
+
docs = docs.filter((doc) => doc.tags.includes(statusTag));
|
|
1086
|
+
}
|
|
1087
|
+
return docs.map((doc) => this.docToModule(doc));
|
|
1088
|
+
}
|
|
1089
|
+
/**
|
|
1090
|
+
* 更新功能模块
|
|
1091
|
+
*/
|
|
1092
|
+
updateModule(moduleId, updates) {
|
|
1093
|
+
const existing = this.getModule(moduleId);
|
|
1094
|
+
if (!existing)
|
|
1095
|
+
return null;
|
|
1096
|
+
this.deleteAndEnsureTimestampAdvance(this.moduleStore, existing.id);
|
|
1097
|
+
const now = Date.now();
|
|
1098
|
+
const newName = updates.name || existing.name;
|
|
1099
|
+
const newDescription = updates.description !== undefined ? updates.description : existing.description;
|
|
1100
|
+
const newStatus = updates.status || existing.status;
|
|
1101
|
+
const moduleData = {
|
|
1102
|
+
moduleId,
|
|
1103
|
+
name: newName,
|
|
1104
|
+
description: newDescription || '',
|
|
1105
|
+
};
|
|
1106
|
+
const docInput = {
|
|
1107
|
+
content: JSON.stringify(moduleData),
|
|
1108
|
+
contentType: native_2.ContentType.Text,
|
|
1109
|
+
tags: [
|
|
1110
|
+
`plan:${this.projectName}`,
|
|
1111
|
+
'type:module',
|
|
1112
|
+
`module:${moduleId}`,
|
|
1113
|
+
`status:${newStatus}`,
|
|
1114
|
+
],
|
|
1115
|
+
metadata: {
|
|
1116
|
+
projectName: this.projectName,
|
|
1117
|
+
moduleId,
|
|
1118
|
+
status: newStatus,
|
|
1119
|
+
createdAt: existing.createdAt,
|
|
1120
|
+
updatedAt: now,
|
|
1121
|
+
},
|
|
1122
|
+
importance: 0.85,
|
|
1123
|
+
};
|
|
1124
|
+
const id = this.moduleStore.put(docInput);
|
|
1125
|
+
this.moduleStore.flush();
|
|
1126
|
+
return {
|
|
1127
|
+
id,
|
|
1128
|
+
projectName: this.projectName,
|
|
1129
|
+
moduleId,
|
|
1130
|
+
name: newName,
|
|
1131
|
+
description: newDescription,
|
|
1132
|
+
status: newStatus,
|
|
1133
|
+
mainTaskCount: existing.mainTaskCount,
|
|
1134
|
+
subTaskCount: existing.subTaskCount,
|
|
1135
|
+
completedSubTaskCount: existing.completedSubTaskCount,
|
|
1136
|
+
docCount: existing.docCount,
|
|
1137
|
+
createdAt: existing.createdAt,
|
|
1138
|
+
updatedAt: now,
|
|
1139
|
+
};
|
|
1140
|
+
}
|
|
1141
|
+
/**
|
|
1142
|
+
* 删除功能模块
|
|
1143
|
+
*/
|
|
1144
|
+
deleteModule(moduleId) {
|
|
1145
|
+
const existing = this.getModule(moduleId);
|
|
1146
|
+
if (!existing)
|
|
1147
|
+
return false;
|
|
1148
|
+
this.moduleStore.delete(existing.id);
|
|
1149
|
+
this.moduleStore.flush();
|
|
1150
|
+
return true;
|
|
1151
|
+
}
|
|
1152
|
+
/**
|
|
1153
|
+
* 获取模块详情 — 包含关联的任务和文档
|
|
1154
|
+
*/
|
|
1155
|
+
getModuleDetail(moduleId) {
|
|
1156
|
+
const mod = this.getModule(moduleId);
|
|
1157
|
+
if (!mod)
|
|
1158
|
+
return null;
|
|
1159
|
+
// 获取关联的主任务
|
|
1160
|
+
const moduleTag = `module:${moduleId}`;
|
|
1161
|
+
let taskDocs = this.taskStore.findByTag(moduleTag)
|
|
1162
|
+
.filter((doc) => doc.tags.includes(`plan:${this.projectName}`) &&
|
|
1163
|
+
doc.tags.includes('type:main-task'));
|
|
1164
|
+
taskDocs = this.deduplicateByTaskId(taskDocs);
|
|
1165
|
+
const mainTasks = taskDocs.map((doc) => this.docToMainTask(doc));
|
|
1166
|
+
// 获取关联的所有子任务(通过主任务间接关联)
|
|
1167
|
+
const subTasks = [];
|
|
1168
|
+
for (const mt of mainTasks) {
|
|
1169
|
+
const subs = this.listSubTasks(mt.taskId);
|
|
1170
|
+
subTasks.push(...subs);
|
|
1171
|
+
}
|
|
1172
|
+
// 获取关联的文档
|
|
1173
|
+
let docDocs = this.docStore.findByTag(moduleTag)
|
|
1174
|
+
.filter((doc) => doc.tags.includes(`plan:${this.projectName}`));
|
|
1175
|
+
// 按 section+subSection 去重
|
|
1176
|
+
const latestDocMap = new Map();
|
|
1177
|
+
for (const doc of docDocs) {
|
|
1178
|
+
const sectionTag = doc.tags.find((t) => t.startsWith('section:'));
|
|
1179
|
+
const subTag = doc.tags.find((t) => t.startsWith('sub:'));
|
|
1180
|
+
const key = `${sectionTag || 'unknown'}|${subTag || ''}`;
|
|
1181
|
+
const ex = latestDocMap.get(key);
|
|
1182
|
+
if (!ex || this.getDocUpdatedAt(doc) > this.getDocUpdatedAt(ex)) {
|
|
1183
|
+
latestDocMap.set(key, doc);
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
const documents = Array.from(latestDocMap.values()).map((doc) => this.docToDevPlanDoc(doc));
|
|
1187
|
+
return { module: mod, mainTasks, subTasks, documents };
|
|
1188
|
+
}
|
|
1189
|
+
// ==========================================================================
|
|
704
1190
|
// Utility
|
|
705
1191
|
// ==========================================================================
|
|
706
1192
|
/**
|
|
@@ -709,6 +1195,7 @@ class DevPlanStore {
|
|
|
709
1195
|
sync() {
|
|
710
1196
|
this.docStore.flush();
|
|
711
1197
|
this.taskStore.flush();
|
|
1198
|
+
this.moduleStore.flush();
|
|
712
1199
|
}
|
|
713
1200
|
/**
|
|
714
1201
|
* 获取项目名称
|
|
@@ -719,16 +1206,29 @@ class DevPlanStore {
|
|
|
719
1206
|
// ==========================================================================
|
|
720
1207
|
// Private Helpers
|
|
721
1208
|
// ==========================================================================
|
|
1209
|
+
/**
|
|
1210
|
+
* 获取文档的有效 updatedAt 时间戳。
|
|
1211
|
+
*
|
|
1212
|
+
* EnhancedDocumentStore 使用 append-only JSONL 存储,修改文档时实际上
|
|
1213
|
+
* 是 delete 旧文档 + put 新文档。因此同一逻辑文档可能存在多个物理版本。
|
|
1214
|
+
* 必须通过 metadata.updatedAt 来判断哪个是最新的"可用文档",
|
|
1215
|
+
* 其余的都是"历史文档"。
|
|
1216
|
+
*
|
|
1217
|
+
* 优先级:metadata.updatedAt > metadata.createdAt > doc.createdAt
|
|
1218
|
+
*/
|
|
1219
|
+
getDocUpdatedAt(doc) {
|
|
1220
|
+
return doc.metadata?.updatedAt || doc.metadata?.createdAt || doc.createdAt;
|
|
1221
|
+
}
|
|
722
1222
|
/**
|
|
723
1223
|
* 确保当前时间戳严格大于参考时间戳。
|
|
724
1224
|
*
|
|
725
1225
|
* EnhancedDocumentStore 使用 append-only JSONL 存储,保留所有历史版本。
|
|
726
|
-
*
|
|
727
|
-
* 如果 delete+put 发生在同一毫秒内,新旧版本的
|
|
1226
|
+
* 版本选择通过 metadata.updatedAt 判定最新文档。
|
|
1227
|
+
* 如果 delete+put 发生在同一毫秒内,新旧版本的 updatedAt 相同,
|
|
728
1228
|
* 会导致去重时可能选中旧版本(如 pending 状态),造成状态丢失。
|
|
729
1229
|
*
|
|
730
1230
|
* 本方法在 delete 之后、put 之前调用,自旋等待直到时间戳前进,
|
|
731
|
-
* 从而保证新版本的
|
|
1231
|
+
* 从而保证新版本的 updatedAt 一定大于旧版本。
|
|
732
1232
|
*/
|
|
733
1233
|
ensureTimestampAfter(referenceTimestamp) {
|
|
734
1234
|
while (Date.now() <= referenceTimestamp) {
|
|
@@ -736,21 +1236,28 @@ class DevPlanStore {
|
|
|
736
1236
|
}
|
|
737
1237
|
}
|
|
738
1238
|
/**
|
|
739
|
-
* 删除文档并确保后续 put
|
|
740
|
-
*
|
|
1239
|
+
* 删除文档并确保后续 put 的 updatedAt 严格大于被删文档。
|
|
1240
|
+
*
|
|
1241
|
+
* 使用 metadata.updatedAt 作为参考时间戳(而非 doc.createdAt),
|
|
1242
|
+
* 因为版本选择是基于 metadata.updatedAt 进行的。
|
|
741
1243
|
*/
|
|
742
1244
|
deleteAndEnsureTimestampAdvance(store, id) {
|
|
743
1245
|
const deleted = store.delete(id);
|
|
744
1246
|
if (deleted) {
|
|
745
|
-
|
|
1247
|
+
// 以 metadata.updatedAt 为基准,确保新文档的 updatedAt 严格递增
|
|
1248
|
+
const refTimestamp = this.getDocUpdatedAt(deleted);
|
|
1249
|
+
this.ensureTimestampAfter(refTimestamp);
|
|
746
1250
|
}
|
|
747
1251
|
}
|
|
748
1252
|
/**
|
|
749
|
-
* 对同一 taskId 的多个历史版本做去重,仅保留最新版(
|
|
1253
|
+
* 对同一 taskId 的多个历史版本做去重,仅保留最新版(metadata.updatedAt 最大)。
|
|
750
1254
|
*
|
|
751
1255
|
* 由于 EnhancedDocumentStore 使用 append-only JSONL 存储,
|
|
752
1256
|
* delete+put 操作会在文件中保留历史版本。重新加载时所有版本都会出现,
|
|
753
1257
|
* 因此需要在查询层面进行去重。
|
|
1258
|
+
*
|
|
1259
|
+
* 使用 metadata.updatedAt(而非 doc.createdAt)作为版本判定依据,
|
|
1260
|
+
* 确保"最近更新时间"的文档才是可用文档,其余为历史文档。
|
|
754
1261
|
*/
|
|
755
1262
|
deduplicateByTaskId(docs) {
|
|
756
1263
|
const latestMap = new Map();
|
|
@@ -760,7 +1267,7 @@ class DevPlanStore {
|
|
|
760
1267
|
if (!taskId)
|
|
761
1268
|
continue;
|
|
762
1269
|
const existing = latestMap.get(taskId);
|
|
763
|
-
if (!existing || doc
|
|
1270
|
+
if (!existing || this.getDocUpdatedAt(doc) > this.getDocUpdatedAt(existing)) {
|
|
764
1271
|
latestMap.set(taskId, doc);
|
|
765
1272
|
}
|
|
766
1273
|
}
|
|
@@ -771,6 +1278,8 @@ class DevPlanStore {
|
|
|
771
1278
|
const section = (sectionTag?.replace('section:', '') || 'custom');
|
|
772
1279
|
const subTag = doc.tags.find((t) => t.startsWith('sub:'));
|
|
773
1280
|
const subSection = subTag?.replace('sub:', '');
|
|
1281
|
+
const moduleTag = doc.tags.find((t) => t.startsWith('module:'));
|
|
1282
|
+
const moduleId = moduleTag?.replace('module:', '') || undefined;
|
|
774
1283
|
return {
|
|
775
1284
|
id: doc.id,
|
|
776
1285
|
projectName: this.projectName,
|
|
@@ -780,6 +1289,7 @@ class DevPlanStore {
|
|
|
780
1289
|
version: doc.metadata?.version || '1.0.0',
|
|
781
1290
|
subSection,
|
|
782
1291
|
relatedSections: doc.metadata?.relatedSections || [],
|
|
1292
|
+
moduleId,
|
|
783
1293
|
createdAt: doc.metadata?.createdAt || doc.createdAt,
|
|
784
1294
|
updatedAt: doc.metadata?.updatedAt || doc.createdAt,
|
|
785
1295
|
};
|
|
@@ -788,6 +1298,8 @@ class DevPlanStore {
|
|
|
788
1298
|
const data = JSON.parse(doc.content);
|
|
789
1299
|
const statusTag = doc.tags.find((t) => t.startsWith('status:'));
|
|
790
1300
|
const status = (statusTag?.replace('status:', '') || 'pending');
|
|
1301
|
+
const moduleTag = doc.tags.find((t) => t.startsWith('module:'));
|
|
1302
|
+
const moduleId = moduleTag?.replace('module:', '') || undefined;
|
|
791
1303
|
return {
|
|
792
1304
|
id: doc.id,
|
|
793
1305
|
projectName: this.projectName,
|
|
@@ -797,6 +1309,7 @@ class DevPlanStore {
|
|
|
797
1309
|
description: data.description,
|
|
798
1310
|
estimatedHours: data.estimatedHours,
|
|
799
1311
|
relatedSections: data.relatedSections || [],
|
|
1312
|
+
moduleId,
|
|
800
1313
|
totalSubtasks: data.totalSubtasks || 0,
|
|
801
1314
|
completedSubtasks: data.completedSubtasks || 0,
|
|
802
1315
|
status,
|
|
@@ -805,6 +1318,71 @@ class DevPlanStore {
|
|
|
805
1318
|
completedAt: doc.metadata?.completedAt || null,
|
|
806
1319
|
};
|
|
807
1320
|
}
|
|
1321
|
+
docToModule(doc) {
|
|
1322
|
+
const data = JSON.parse(doc.content);
|
|
1323
|
+
const statusTag = doc.tags.find((t) => t.startsWith('status:'));
|
|
1324
|
+
const status = (statusTag?.replace('status:', '') || 'active');
|
|
1325
|
+
const moduleId = data.moduleId;
|
|
1326
|
+
// 计算关联的主任务数(去重)
|
|
1327
|
+
const moduleTag = `module:${moduleId}`;
|
|
1328
|
+
const taskDocs = this.taskStore.findByTag(moduleTag)
|
|
1329
|
+
.filter((d) => d.tags.includes(`plan:${this.projectName}`) &&
|
|
1330
|
+
d.tags.includes('type:main-task'));
|
|
1331
|
+
const uniqueTaskIds = new Set();
|
|
1332
|
+
for (const td of taskDocs) {
|
|
1333
|
+
try {
|
|
1334
|
+
uniqueTaskIds.add(JSON.parse(td.content).taskId);
|
|
1335
|
+
}
|
|
1336
|
+
catch { }
|
|
1337
|
+
}
|
|
1338
|
+
// 计算关联的子任务数(遍历关联主任务下的所有子任务)
|
|
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 去重
|
|
1346
|
+
const seenSubIds = new Set();
|
|
1347
|
+
for (const sd of subDocs) {
|
|
1348
|
+
try {
|
|
1349
|
+
const subData = JSON.parse(sd.content);
|
|
1350
|
+
if (seenSubIds.has(subData.taskId))
|
|
1351
|
+
continue;
|
|
1352
|
+
seenSubIds.add(subData.taskId);
|
|
1353
|
+
subTaskCount++;
|
|
1354
|
+
const subStatusTag = sd.tags.find((t) => t.startsWith('status:'));
|
|
1355
|
+
if (subStatusTag === 'status:completed') {
|
|
1356
|
+
completedSubTaskCount++;
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
catch { }
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
// 计算关联的文档数(按 section+subSection 去重)
|
|
1363
|
+
const docDocs = this.docStore.findByTag(moduleTag)
|
|
1364
|
+
.filter((d) => d.tags.includes(`plan:${this.projectName}`));
|
|
1365
|
+
const uniqueDocKeys = new Set();
|
|
1366
|
+
for (const dd of docDocs) {
|
|
1367
|
+
const st = dd.tags.find((t) => t.startsWith('section:'));
|
|
1368
|
+
const sub = dd.tags.find((t) => t.startsWith('sub:'));
|
|
1369
|
+
uniqueDocKeys.add(`${st || ''}|${sub || ''}`);
|
|
1370
|
+
}
|
|
1371
|
+
return {
|
|
1372
|
+
id: doc.id,
|
|
1373
|
+
projectName: this.projectName,
|
|
1374
|
+
moduleId,
|
|
1375
|
+
name: data.name,
|
|
1376
|
+
description: data.description || undefined,
|
|
1377
|
+
status,
|
|
1378
|
+
mainTaskCount: uniqueTaskIds.size,
|
|
1379
|
+
subTaskCount,
|
|
1380
|
+
completedSubTaskCount,
|
|
1381
|
+
docCount: uniqueDocKeys.size,
|
|
1382
|
+
createdAt: doc.metadata?.createdAt || doc.createdAt,
|
|
1383
|
+
updatedAt: doc.metadata?.updatedAt || doc.createdAt,
|
|
1384
|
+
};
|
|
1385
|
+
}
|
|
808
1386
|
docToSubTask(doc) {
|
|
809
1387
|
const data = JSON.parse(doc.content);
|
|
810
1388
|
const statusTag = doc.tags.find((t) => t.startsWith('status:'));
|
|
@@ -824,6 +1402,8 @@ class DevPlanStore {
|
|
|
824
1402
|
createdAt: doc.metadata?.createdAt || doc.createdAt,
|
|
825
1403
|
updatedAt: doc.metadata?.updatedAt || doc.createdAt,
|
|
826
1404
|
completedAt: doc.metadata?.completedAt || null,
|
|
1405
|
+
completedAtCommit: doc.metadata?.completedAtCommit || undefined,
|
|
1406
|
+
revertReason: doc.metadata?.revertReason || undefined,
|
|
827
1407
|
};
|
|
828
1408
|
}
|
|
829
1409
|
/**
|
|
@@ -851,20 +1431,25 @@ class DevPlanStore {
|
|
|
851
1431
|
totalSubtasks: subs.length,
|
|
852
1432
|
completedSubtasks: completedCount,
|
|
853
1433
|
};
|
|
1434
|
+
const tags = [
|
|
1435
|
+
`plan:${this.projectName}`,
|
|
1436
|
+
'type:main-task',
|
|
1437
|
+
`mtask:${mainTask.taskId}`,
|
|
1438
|
+
`priority:${mainTask.priority}`,
|
|
1439
|
+
`status:${mainTask.status}`,
|
|
1440
|
+
];
|
|
1441
|
+
if (mainTask.moduleId) {
|
|
1442
|
+
tags.push(`module:${mainTask.moduleId}`);
|
|
1443
|
+
}
|
|
854
1444
|
const docInput = {
|
|
855
1445
|
content: JSON.stringify(taskData),
|
|
856
1446
|
contentType: native_2.ContentType.Text,
|
|
857
|
-
tags
|
|
858
|
-
`plan:${this.projectName}`,
|
|
859
|
-
'type:main-task',
|
|
860
|
-
`mtask:${mainTask.taskId}`,
|
|
861
|
-
`priority:${mainTask.priority}`,
|
|
862
|
-
`status:${mainTask.status}`,
|
|
863
|
-
],
|
|
1447
|
+
tags,
|
|
864
1448
|
metadata: {
|
|
865
1449
|
projectName: this.projectName,
|
|
866
1450
|
taskId: mainTask.taskId,
|
|
867
1451
|
status: mainTask.status,
|
|
1452
|
+
moduleId: mainTask.moduleId || null,
|
|
868
1453
|
createdAt: mainTask.createdAt,
|
|
869
1454
|
updatedAt: now,
|
|
870
1455
|
completedAt: mainTask.completedAt,
|
|
@@ -901,6 +1486,118 @@ class DevPlanStore {
|
|
|
901
1486
|
relatedSections: milestonesDoc.relatedSections,
|
|
902
1487
|
});
|
|
903
1488
|
}
|
|
1489
|
+
// ==========================================================================
|
|
1490
|
+
// Git Integration (Git Commit 锚定 + 同步检查)
|
|
1491
|
+
// ==========================================================================
|
|
1492
|
+
/**
|
|
1493
|
+
* 🆕 获取当前 Git HEAD 的 short SHA
|
|
1494
|
+
*
|
|
1495
|
+
* 在非 Git 仓库或 Git 不可用时返回 undefined,不阻断正常流程。
|
|
1496
|
+
*/
|
|
1497
|
+
getCurrentGitCommit() {
|
|
1498
|
+
try {
|
|
1499
|
+
const { execSync } = require('child_process');
|
|
1500
|
+
return execSync('git rev-parse --short HEAD', {
|
|
1501
|
+
encoding: 'utf-8',
|
|
1502
|
+
timeout: 5000,
|
|
1503
|
+
stdio: ['pipe', 'pipe', 'pipe'], // 静默 stderr
|
|
1504
|
+
}).trim();
|
|
1505
|
+
}
|
|
1506
|
+
catch {
|
|
1507
|
+
return undefined; // 非 Git 仓库或 Git 不可用
|
|
1508
|
+
}
|
|
1509
|
+
}
|
|
1510
|
+
/**
|
|
1511
|
+
* 🆕 检查 commit 是否是 target 的祖先
|
|
1512
|
+
*
|
|
1513
|
+
* 使用 `git merge-base --is-ancestor` 命令。
|
|
1514
|
+
* 如果 commit 不存在或不可达,返回 false(视为需要回退)。
|
|
1515
|
+
*/
|
|
1516
|
+
isAncestor(commit, target) {
|
|
1517
|
+
try {
|
|
1518
|
+
const { execSync } = require('child_process');
|
|
1519
|
+
execSync(`git merge-base --is-ancestor ${commit} ${target}`, {
|
|
1520
|
+
timeout: 5000,
|
|
1521
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
1522
|
+
});
|
|
1523
|
+
return true; // exit code 0 = is ancestor
|
|
1524
|
+
}
|
|
1525
|
+
catch {
|
|
1526
|
+
return false; // exit code 1 = not ancestor, or error
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1529
|
+
/**
|
|
1530
|
+
* 🆕 回退子任务状态
|
|
1531
|
+
*
|
|
1532
|
+
* 将已完成的子任务回退为 pending,记录回退原因,
|
|
1533
|
+
* 清空 completedAtCommit 和 completedAt。
|
|
1534
|
+
* 同时刷新父主任务的计数。
|
|
1535
|
+
*/
|
|
1536
|
+
revertSubTask(taskId, reason) {
|
|
1537
|
+
const result = this.updateSubTaskStatus(taskId, 'pending', {
|
|
1538
|
+
revertReason: reason,
|
|
1539
|
+
});
|
|
1540
|
+
if (result) {
|
|
1541
|
+
// 刷新父主任务计数
|
|
1542
|
+
this.refreshMainTaskCounts(result.parentTaskId);
|
|
1543
|
+
// 如果父主任务被标记为 completed,也需要回退
|
|
1544
|
+
const mainTask = this.getMainTask(result.parentTaskId);
|
|
1545
|
+
if (mainTask && mainTask.status === 'completed') {
|
|
1546
|
+
this.updateMainTaskStatus(result.parentTaskId, 'in_progress');
|
|
1547
|
+
}
|
|
1548
|
+
}
|
|
1549
|
+
return result;
|
|
1550
|
+
}
|
|
1551
|
+
/**
|
|
1552
|
+
* 🆕 同步检查所有已完成任务与 Git 历史的一致性
|
|
1553
|
+
*
|
|
1554
|
+
* 对每个 status=completed 且有 completedAtCommit 的子任务:
|
|
1555
|
+
* 1. 检查 completedAtCommit 是否是当前 HEAD 的祖先
|
|
1556
|
+
* 2. 如果不是(说明 Git 发生了回滚),回退任务状态为 pending
|
|
1557
|
+
* 3. 记录 revertReason
|
|
1558
|
+
*
|
|
1559
|
+
* @param dryRun 如果为 true,只返回哪些任务会被回退,不实际修改数据
|
|
1560
|
+
* @returns 同步结果,包含被回退的任务列表
|
|
1561
|
+
*/
|
|
1562
|
+
syncWithGit(dryRun = false) {
|
|
1563
|
+
const currentHead = this.getCurrentGitCommit();
|
|
1564
|
+
if (!currentHead) {
|
|
1565
|
+
return {
|
|
1566
|
+
checked: 0,
|
|
1567
|
+
reverted: [],
|
|
1568
|
+
currentHead: 'unknown',
|
|
1569
|
+
error: 'Git not available or not in a Git repository',
|
|
1570
|
+
};
|
|
1571
|
+
}
|
|
1572
|
+
const mainTasks = this.listMainTasks();
|
|
1573
|
+
const reverted = [];
|
|
1574
|
+
let checked = 0;
|
|
1575
|
+
for (const mt of mainTasks) {
|
|
1576
|
+
const subs = this.listSubTasks(mt.taskId);
|
|
1577
|
+
for (const sub of subs) {
|
|
1578
|
+
if (sub.status !== 'completed' || !sub.completedAtCommit)
|
|
1579
|
+
continue;
|
|
1580
|
+
checked++;
|
|
1581
|
+
if (!this.isAncestor(sub.completedAtCommit, currentHead)) {
|
|
1582
|
+
const reason = `Commit ${sub.completedAtCommit} is not ancestor of HEAD ${currentHead}`;
|
|
1583
|
+
if (!dryRun) {
|
|
1584
|
+
this.revertSubTask(sub.taskId, reason);
|
|
1585
|
+
}
|
|
1586
|
+
reverted.push({
|
|
1587
|
+
taskId: sub.taskId,
|
|
1588
|
+
title: sub.title,
|
|
1589
|
+
parentTaskId: sub.parentTaskId,
|
|
1590
|
+
completedAtCommit: sub.completedAtCommit,
|
|
1591
|
+
reason: `Commit ${sub.completedAtCommit} not found in current branch (HEAD: ${currentHead})`,
|
|
1592
|
+
});
|
|
1593
|
+
}
|
|
1594
|
+
}
|
|
1595
|
+
}
|
|
1596
|
+
return { checked, reverted, currentHead };
|
|
1597
|
+
}
|
|
1598
|
+
// ==========================================================================
|
|
1599
|
+
// Utilities
|
|
1600
|
+
// ==========================================================================
|
|
904
1601
|
/**
|
|
905
1602
|
* 生成文本进度条
|
|
906
1603
|
*/
|
|
@@ -919,13 +1616,22 @@ exports.DevPlanStore = DevPlanStore;
|
|
|
919
1616
|
* 为项目创建 DevPlanStore
|
|
920
1617
|
*
|
|
921
1618
|
* @param projectName - 项目名称
|
|
922
|
-
* @param basePath -
|
|
1619
|
+
* @param basePath - 存储基础路径(默认优先使用项目内 .devplan/,回退到 ~/.aifastdb/dev-plans/)
|
|
1620
|
+
*
|
|
1621
|
+
* 存储路径解析优先级:
|
|
1622
|
+
* 1. 显式 basePath 参数
|
|
1623
|
+
* 2. AIFASTDB_DEVPLAN_PATH 环境变量
|
|
1624
|
+
* 3. 项目根目录/.devplan/(通过 .git 或 package.json 定位)
|
|
1625
|
+
* 4. ~/.aifastdb/dev-plans/(兜底)
|
|
1626
|
+
*
|
|
1627
|
+
* 最终路径:{basePath}/{projectName}/documents.jsonl + tasks.jsonl
|
|
923
1628
|
*/
|
|
924
1629
|
function createDevPlan(projectName, basePath) {
|
|
925
1630
|
const base = basePath || getDefaultBasePath();
|
|
926
1631
|
return new DevPlanStore(projectName, {
|
|
927
1632
|
documentPath: path.join(base, projectName, 'documents.jsonl'),
|
|
928
1633
|
taskPath: path.join(base, projectName, 'tasks.jsonl'),
|
|
1634
|
+
modulePath: path.join(base, projectName, 'modules.jsonl'),
|
|
929
1635
|
});
|
|
930
1636
|
}
|
|
931
1637
|
/**
|