sillyspec 3.25.1 → 3.25.3

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.
@@ -0,0 +1,535 @@
1
+ // W6 Step9d: 阶段状态机 —— completeStage/reopen/reset/validate/show/status + 产物校验 + 下游级联 +
2
+ // 显示辅助。从 src/progress.js 单体 ProgressManager 抽出(Step9 最后一步)。持有 pm 引用(构造注入),
3
+ // 调 pm.read/_write/_ensureDB/_getSpecDir/readGlobal/_runtimePath/listChanges/_appendAuditLog/
4
+ // _renderBatchProgress(persistence-core + 其他组 delegate)。组内方法互调保持 this.X(同 class)。
5
+ // completeStage 五层(resolve/validate/force/tx/history/print)整体搬迁,不拆流水线(保行为、最低风险)。
6
+ import { mkdirSync } from 'fs';
7
+ import { join, resolve, basename } from 'path';
8
+ import { writeAtomicSync } from '../fs-atomic.js';
9
+ import { VALID_STAGES, STAGE_LABELS, STAGE_ORDER, MAIN_FLOW_ORDER, SPEC_DIR_NAME, CURRENT_VERSION, emptyStage } from './shared.js';
10
+
11
+ export class StageMachine {
12
+ constructor(pm) {
13
+ this.pm = pm;
14
+ }
15
+
16
+ /**
17
+ * 阶段产物校验门:progress complete-stage / update-step 自动完成时复用
18
+ * stage-contract 的 validator,防止零产物阶段被直接标 completed。
19
+ */
20
+ async _validateStageArtifacts(cwd, stage, changeName) {
21
+ const { runValidators } = await import('../stage-contract.js');
22
+ const specDir = this.pm._getSpecDir(cwd);
23
+ const defaultSpec = join(resolve(cwd), SPEC_DIR_NAME);
24
+ const specRoot = resolve(specDir) === defaultSpec ? null : specDir;
25
+ let projectName = null;
26
+ try {
27
+ const g = await this.pm.readGlobal(cwd);
28
+ projectName = g?.project || null;
29
+ } catch {}
30
+ return runValidators(stage, cwd, changeName, {
31
+ projectName: projectName || basename(resolve(cwd)),
32
+ specRoot,
33
+ });
34
+ }
35
+
36
+ async completeStage(cwd, stage, changeName = null, opts = {}) {
37
+ const { force = false } = opts;
38
+ if (!VALID_STAGES.includes(stage)) {
39
+ console.log(`❌ 未知阶段: ${stage}`);
40
+ return;
41
+ }
42
+
43
+ const db = await this.pm._ensureDB(cwd);
44
+ const now = new Date().toISOString();
45
+
46
+ // 获取变更名
47
+ let cn = changeName;
48
+ if (!cn) {
49
+ const changes = await this.pm.listChanges(cwd);
50
+ if (changes.length === 1) cn = changes[0];
51
+ if (!cn) { console.log('❌ 无法确定当前变更,请指定 --change <name>'); return; }
52
+ }
53
+
54
+ // ── 产物校验门:complete-stage 不再是零校验后门 ──
55
+ // 与 run.js completeStep 的 validator 同源;--force 为显式逃生口(留审计)。
56
+ const validation = await this._validateStageArtifacts(cwd, stage, cn);
57
+ if (!validation.ok) {
58
+ if (!force) {
59
+ console.error(`❌ complete-stage 被拒绝:阶段 ${stage} 产物校验未通过`);
60
+ for (const err of validation.errors) console.error(` - ${err}`);
61
+ console.error(` 请修复产物后重试,或走正常流程 sillyspec run ${stage} --done。`);
62
+ console.error(` 确需强制标记(如 doctor 修复),使用 --force(将记录审计日志)。`);
63
+ return;
64
+ }
65
+ console.warn(`⚠️ --force 强制完成阶段 ${stage}(校验未通过,已记录审计日志)`);
66
+ for (const err of validation.errors) console.warn(` - ${err}`);
67
+ this.pm._appendAuditLog(cwd, {
68
+ action: 'complete-stage --force',
69
+ stage,
70
+ change: cn,
71
+ validationErrors: validation.errors,
72
+ });
73
+ } else if (force) {
74
+ // 校验通过但仍显式 --force:也留一条审计记录,保持行为可追溯
75
+ this.pm._appendAuditLog(cwd, { action: 'complete-stage --force', stage, change: cn, validationErrors: [] });
76
+ }
77
+
78
+ db.transaction((sqlDb) => {
79
+ const changeRow = sqlDb.exec('SELECT id FROM changes WHERE name = ?', [cn]);
80
+ if (!changeRow || changeRow.length === 0 || changeRow[0].values.length === 0) return;
81
+ const changeId = changeRow[0].values[0][0];
82
+
83
+ // 确保 stages 行存在(阶段不存在时自动创建)
84
+ sqlDb.run(
85
+ 'INSERT OR IGNORE INTO stages (change_id, stage, status) VALUES (?, ?, "pending")',
86
+ [changeId, stage]
87
+ );
88
+
89
+ // UPDATE stages.status=completed + completed_at
90
+ sqlDb.run(
91
+ 'UPDATE stages SET status = "completed", completed_at = ? WHERE change_id = ? AND stage = ?',
92
+ [now, changeId, stage]
93
+ );
94
+
95
+ // 将该阶段所有 pending 步骤标记为 completed
96
+ const stageRow = sqlDb.exec('SELECT id FROM stages WHERE change_id = ? AND stage = ?', [changeId, stage]);
97
+ if (stageRow && stageRow.length > 0 && stageRow[0].values.length > 0) {
98
+ const stageId = stageRow[0].values[0][0];
99
+ sqlDb.run(
100
+ 'UPDATE steps SET status = "completed", completed_at = ? WHERE stage_id = ? AND status = "pending"',
101
+ [now, stageId]
102
+ );
103
+ }
104
+
105
+ // UPDATE changes.last_active
106
+ sqlDb.run('UPDATE changes SET last_active = ? WHERE name = ?', [now, cn]);
107
+ });
108
+
109
+ // 写 history 文件(保持文件系统,不变)
110
+ const data = await this.pm.read(cwd, cn);
111
+ if (data && data.stages && data.stages[stage]) {
112
+ const historyDir = this.pm._runtimePath(cwd, 'history');
113
+ mkdirSync(historyDir, { recursive: true });
114
+ const ts = now.replace(/[:.TZ-]/g, '');
115
+ const stageData = data.stages[stage];
116
+ writeAtomicSync(
117
+ join(historyDir, `${cn}-${stage}-${ts}.json`),
118
+ JSON.stringify({ change: cn, stage, data: stageData, completedAt: now }, null, 2) + '\n'
119
+ );
120
+ }
121
+
122
+ console.log(`✅ 阶段 ${stage} 已标记为完成(不自动推进,下一步由你决定)`);
123
+ }
124
+
125
+ async show(cwd, changeName = null) {
126
+ // 如果指定了变更名,只显示该变更
127
+ if (changeName) {
128
+ return await this._showChange(cwd, changeName);
129
+ }
130
+
131
+ // 否则显示所有变更
132
+ const changes = await this.pm.listChanges(cwd);
133
+ if (changes.length === 0) {
134
+ console.log('ℹ️ 没有活跃的变更');
135
+ return;
136
+ }
137
+
138
+ if (changes.length === 1) {
139
+ return await this._showChange(cwd, changes[0]);
140
+ }
141
+
142
+ // 多个变更:汇总显示
143
+ const global = await this.pm.readGlobal(cwd);
144
+ console.log('');
145
+ console.log(' ═══════════════════════════════════════');
146
+ console.log(` 项目: ${(global?.project) || basename(cwd) || '(未命名)'}`);
147
+ console.log(` 活跃变更: ${changes.length} 个`);
148
+ console.log(' ═══════════════════════════════════════');
149
+ console.log('');
150
+
151
+ for (const cn of changes) {
152
+ const data = await this.pm.read(cwd, cn);
153
+ if (!data) {
154
+ console.log(` 📂 ${cn} — (无法读取)`);
155
+ continue;
156
+ }
157
+ const currentStage = data.currentStage || '(无)';
158
+ const stageLabel = STAGE_LABELS[data.currentStage] || currentStage;
159
+ const lastActive = data.lastActive ? this._timeAgo(data.lastActive) : '未知';
160
+
161
+ console.log(` 📂 ${cn}`);
162
+ console.log(` 当前阶段: ${stageLabel} 最近活跃: ${lastActive}`);
163
+ console.log('');
164
+ }
165
+
166
+ console.log(` 💡 查看详情:sillyspec progress show --change <name>`);
167
+ console.log('');
168
+ }
169
+
170
+ async _showChange(cwd, changeName) {
171
+ const data = await this.pm.read(cwd, changeName);
172
+ if (!data) {
173
+ console.log(`❌ 未找到变更 ${changeName}`);
174
+ return;
175
+ }
176
+
177
+ console.log('');
178
+ console.log(' ═══════════════════════════════════════');
179
+ console.log(` 变更: ${changeName}`);
180
+ console.log(` 项目: ${data.project || '(未命名)'}`);
181
+ console.log(` 当前阶段: ${STAGE_LABELS[data.currentStage] || data.currentStage || '(无)'}`);
182
+ console.log(` 最近活跃: ${data.lastActive ? this._timeAgo(data.lastActive) : '未知'}`);
183
+ console.log(' ═══════════════════════════════════════');
184
+ console.log('');
185
+
186
+ const statusIcons = { pending: '⬜', 'in-progress': '🔵', completed: '✅', failed: '❌', blocked: '🚫', waiting: '⏸️', revising: '🔧', stale: '⚠️' };
187
+
188
+ for (const stage of VALID_STAGES) {
189
+ const stageData = data.stages[stage] || emptyStage();
190
+ const label = STAGE_LABELS[stage] || stage;
191
+ const icon = statusIcons[stageData.status] || '⬜';
192
+ const isCurrent = data.currentStage === stage ? ' ◀' : '';
193
+
194
+ console.log(` ${icon} ${label}${isCurrent}`);
195
+
196
+ // Show revision info
197
+ if (stageData.revision && stageData.revision > 0) {
198
+ console.log(` 📋 revision: ${stageData.revision}${stageData.reopenedFromStep ? `, from step: ${stageData.reopenedFromStep}` : ''}`);
199
+ }
200
+ if (stageData.staleReason) {
201
+ console.log(` ⚠️ stale: ${stageData.staleReason}`);
202
+ if (stage === 'archive') {
203
+ console.log(` 📁 已有归档文件仍保留在磁盘上,但不再可信。`);
204
+ }
205
+ }
206
+
207
+ if (stageData.steps && stageData.steps.length > 0) {
208
+ for (const step of stageData.steps) {
209
+ const si = statusIcons[step.status] || '○';
210
+ const out = step.output ? ` — ${step.output.slice(0, 60)}` : '';
211
+ const waitingTag = step.status === 'waiting' ? ' [WAITING]' : ''
212
+ console.log(` ${si} ${step.name}${out}${waitingTag}`);
213
+ if (step.status === 'waiting') {
214
+ if (step.waitReason) console.log(` 原因:${step.waitReason}`);
215
+ if (step.waitOptions) console.log(` 选项:${(() => { try { const p = JSON.parse(step.waitOptions); return Array.isArray(p) ? p.join(', ') : step.waitOptions; } catch { return step.waitOptions; }})()}`);
216
+ if (step.waitedAt) console.log(` 等待时间:${step.waitedAt}`);
217
+ }
218
+ }
219
+ }
220
+
221
+ if (stageData.startedAt) {
222
+ console.log(` 开始: ${new Date(stageData.startedAt).toLocaleString('zh-CN')}`);
223
+ }
224
+ if (stageData.completedAt) {
225
+ console.log(` 完成: ${new Date(stageData.completedAt).toLocaleString('zh-CN')}`);
226
+ }
227
+ }
228
+
229
+ // 批量进度
230
+ if (data.batchProgress) {
231
+ const batchLine = this.pm._renderBatchProgress(data.batchProgress);
232
+ if (batchLine) {
233
+ console.log('');
234
+ console.log(` ${batchLine}`);
235
+ }
236
+ }
237
+
238
+ // ── Next 建议 ──
239
+ const suggestion = this._getNextSuggestion(data);
240
+ if (suggestion) {
241
+ console.log('');
242
+ console.log(` 💡 ${suggestion.text}`);
243
+ if (suggestion.command) console.log(` ${suggestion.command}`);
244
+ }
245
+
246
+ console.log('');
247
+ }
248
+
249
+ /**
250
+ * 根据当前状态给出下一步建议
251
+ * @param {object} data - progress data
252
+ * @returns {{ text: string, command?: string }|null}
253
+ */
254
+ _getNextSuggestion(data) {
255
+ // 找到第一个 revising 阶段
256
+ const revisingStage = STAGE_ORDER.find(s => data.stages[s]?.status === 'revising');
257
+ if (revisingStage) {
258
+ const sd = data.stages[revisingStage];
259
+ return {
260
+ text: `${STAGE_LABELS[revisingStage] || revisingStage} 正在修订中(revision ${sd.revision || 1}),请继续完成修订。`,
261
+ command: `sillyspec run ${revisingStage}`,
262
+ };
263
+ }
264
+
265
+ // 找到第一个 stale 阶段(上游已修,下游需要重建)
266
+ const staleStage = STAGE_ORDER.find(s => data.stages[s]?.status === 'stale');
267
+ if (staleStage) {
268
+ const sd = data.stages[staleStage];
269
+ return {
270
+ text: `${STAGE_LABELS[staleStage] || staleStage} 已失效(${sd.staleReason || '上游修订'}),需要从第一步重建。`,
271
+ command: `sillyspec run ${staleStage} --reopen --from-step 1`,
272
+ };
273
+ }
274
+
275
+ // 找到第一个有 pending/waiting/failed 步骤的 in-progress 阶段
276
+ for (const s of STAGE_ORDER) {
277
+ const sd = data.stages[s];
278
+ if (!sd) continue;
279
+ if (sd.status === 'in-progress' && sd.steps) {
280
+ const hasPending = sd.steps.some(st => ['pending', 'waiting', 'failed'].includes(st.status));
281
+ if (hasPending) {
282
+ return {
283
+ text: `${STAGE_LABELS[s] || s} 进行中,继续执行下一步。`,
284
+ command: `sillyspec run ${s}`,
285
+ };
286
+ }
287
+ }
288
+ }
289
+
290
+ // 找到第一个 pending 主流程阶段
291
+ for (const s of STAGE_ORDER) {
292
+ const sd = data.stages[s];
293
+ if (sd && sd.status === 'pending' && sd.steps && sd.steps.length > 0) {
294
+ // 检查上游是否都 completed
295
+ const idx = STAGE_ORDER.indexOf(s);
296
+ const upstream = STAGE_ORDER.slice(0, idx);
297
+ const upstreamOk = upstream.every(us =>
298
+ data.stages[us]?.status === 'completed' || !data.stages[us] || data.stages[us].status === 'pending'
299
+ );
300
+ if (upstreamOk) {
301
+ return {
302
+ text: `可以开始 ${STAGE_LABELS[s] || s}。`,
303
+ command: `sillyspec run ${s}`,
304
+ };
305
+ }
306
+ }
307
+ }
308
+
309
+ return null;
310
+ }
311
+
312
+ async status(cwd, changeName = null) {
313
+ await this.show(cwd, changeName);
314
+ }
315
+
316
+ async validate(cwd, changeName = null) {
317
+ const data = await this.pm.read(cwd, changeName);
318
+ if (!data) { console.log('❌ 无法读取进度数据'); return false; }
319
+
320
+ const errors = [];
321
+ if (!data._version || !Number.isInteger(data._version) || data._version < 1) {
322
+ errors.push(`_version 缺失或无效(期望正整数,实际为 ${JSON.stringify(data._version)})`);
323
+ }
324
+ if (!data.stages || typeof data.stages !== 'object') errors.push('缺少 stages');
325
+ if (!VALID_STAGES.every(s => data.stages[s])) errors.push('缺少阶段定义');
326
+
327
+ if (errors.length === 0) { console.log('✅ 进度数据格式正确'); return true; }
328
+
329
+ console.log(`⚠️ 发现问题,尝试修复...`);
330
+ let fixed = { ...data, stages: { ...data.stages } };
331
+ let changed = false;
332
+ if (!fixed.project) {
333
+ fixed.project = basename(cwd);
334
+ changed = true;
335
+ }
336
+ if (!fixed._version || !Number.isInteger(fixed._version) || fixed._version < 1) {
337
+ fixed._version = CURRENT_VERSION;
338
+ changed = true;
339
+ }
340
+ for (const s of VALID_STAGES) {
341
+ if (!fixed.stages[s]) { fixed.stages[s] = emptyStage(); changed = true; }
342
+ }
343
+ if (changed) {
344
+ await this.pm._write(cwd, fixed);
345
+ console.log('✅ 已修复');
346
+ }
347
+
348
+ return true;
349
+ }
350
+
351
+ /**
352
+ * 重新打开已完成的阶段进入修订模式
353
+ * - 不带 fromStep:只允许存在 pending/stale/waiting/failed 步骤时继续
354
+ * - 带 fromStep:从该步骤起,当前及后续步骤标记 stale/pending
355
+ * - 自动级联标记下游阶段为 stale
356
+ *
357
+ * @param {string} cwd
358
+ * @param {string} stage - 要重开的阶段
359
+ * @param {object} opts
360
+ * @param {string|number} [opts.fromStep] - 步骤名或序号(1-based)
361
+ * @param {string} [opts.changeName]
362
+ * @returns {{ ok: boolean, error?: string }}
363
+ */
364
+ async reopenStage(cwd, stage, opts = {}) {
365
+ const { fromStep, changeName = null } = opts;
366
+
367
+ const data = await this.pm.read(cwd, changeName);
368
+ if (!data) return { ok: false, error: '无法读取进度数据' };
369
+
370
+ const stageData = data.stages[stage];
371
+ if (!stageData) return { ok: false, error: `未知阶段: ${stage}` };
372
+
373
+ const steps = stageData.steps || [];
374
+
375
+ // 确定 fromStep 对应的 index
376
+ let fromIdx = null;
377
+ if (fromStep != null) {
378
+ if (typeof fromStep === 'number' || /^\d+$/.test(String(fromStep))) {
379
+ fromIdx = parseInt(String(fromStep), 10) - 1; // 1-based → 0-based
380
+ if (fromIdx < 0 || fromIdx >= steps.length) {
381
+ return { ok: false, error: `步骤序号超出范围: ${fromStep}(共 ${steps.length} 步)` };
382
+ }
383
+ } else {
384
+ // 按名称匹配
385
+ fromIdx = steps.findIndex(s => s.name === fromStep);
386
+ if (fromIdx === -1) {
387
+ return { ok: false, error: `步骤不存在: ${fromStep}` };
388
+ }
389
+ }
390
+ }
391
+
392
+ // 如果不带 fromStep,检查是否存在中断步骤
393
+ if (fromIdx === null) {
394
+ const hasInterrupted = steps.some(s =>
395
+ ['pending', 'stale', 'waiting', 'failed'].includes(s.status)
396
+ );
397
+ if (!hasInterrupted) {
398
+ return { ok: false, error: `阶段 ${stage} 所有步骤均已完成,请使用 --from-step 指定从哪一步开始修订` };
399
+ }
400
+ // 找到第一个中断步骤
401
+ fromIdx = steps.findIndex(s =>
402
+ ['pending', 'stale', 'waiting', 'failed'].includes(s.status)
403
+ );
404
+ }
405
+
406
+ // 执行重开操作
407
+ const newRevision = (stageData.revision || 0) + 1;
408
+ const fromStepName = steps[fromIdx].name;
409
+ const now = new Date().toLocaleString('zh-CN', { hour12: false });
410
+
411
+ // 更新步骤状态:fromStep 之前的保持 completed,fromStep 变 pending,之后的变 stale
412
+ for (let i = 0; i < steps.length; i++) {
413
+ if (i === fromIdx) {
414
+ steps[i].status = 'pending';
415
+ steps[i].completedAt = null;
416
+ steps[i].output = null;
417
+ } else if (i > fromIdx) {
418
+ steps[i].status = 'stale';
419
+ steps[i].completedAt = null;
420
+ }
421
+ // i < fromIdx: 保持原状(completed)
422
+ }
423
+
424
+ stageData.status = 'revising';
425
+ stageData.completedAt = null;
426
+ stageData.revision = newRevision;
427
+ stageData.reopenedFromStep = `${fromIdx + 1}: ${fromStepName}`; // 存 "index: name" 格式
428
+ stageData.reopenedAt = now;
429
+ stageData.steps = steps;
430
+
431
+ data.lastActive = now;
432
+ data.currentStage = stage;
433
+
434
+ await this.pm._write(cwd, data, changeName);
435
+
436
+ // 级联标记下游阶段为 stale
437
+ const downstreamStages = this._getDownstreamStages(stage);
438
+ if (downstreamStages.length > 0) {
439
+ const data2 = await this.pm.read(cwd, changeName); // 重新读取以获取最新状态
440
+ if (data2) {
441
+ for (const ds of downstreamStages) {
442
+ if (data2.stages[ds] && data2.stages[ds].status === 'completed') {
443
+ data2.stages[ds].status = 'stale';
444
+ data2.stages[ds].staleReason = `上游阶段 ${stage} 已修订 (revision ${newRevision})`;
445
+ data2.stages[ds].completedAt = null;
446
+ }
447
+ }
448
+ await this.pm._write(cwd, data2, changeName);
449
+ }
450
+ }
451
+
452
+ return { ok: true, revision: newRevision, fromStep: fromStepName };
453
+ }
454
+
455
+ /**
456
+ * 获取指定阶段的下游主流程阶段列表
457
+ * @param {string} stage
458
+ * @returns {string[]}
459
+ */
460
+ _getDownstreamStages(stage) {
461
+ const idx = MAIN_FLOW_ORDER.indexOf(stage);
462
+ if (idx === -1) return [];
463
+ return MAIN_FLOW_ORDER.slice(idx + 1);
464
+ }
465
+
466
+ async reset(cwd, stage, changeName = null) {
467
+ if (stage) {
468
+ const data = await this.pm.read(cwd, changeName);
469
+ if (!data) { console.log('❌ 无法读取进度数据'); return; }
470
+ if (!data.stages[stage]) { console.log(`❌ 未知阶段: ${stage}`); return; }
471
+ // 破坏性预览:reset 不可逆地清空该阶段所有步骤(含已完成),先告诉 agent 丢了什么。
472
+ const sd = data.stages[stage];
473
+ const steps = sd.steps || [];
474
+ const doneCount = steps.filter(s => s.status === 'completed').length;
475
+ console.warn(`⚠️ 即将重置阶段「${stage}」:丢弃 ${steps.length} 个步骤(其中 ${doneCount} 个已完成,revision ${sd.revision || 0})。此操作不可逆。`);
476
+ data.stages[stage] = emptyStage();
477
+ data.lastActive = new Date().toLocaleString('zh-CN',{hour12:false});
478
+ await this.pm._write(cwd, data);
479
+ console.log(`✅ 已重置阶段: ${stage}`);
480
+ } else {
481
+ // 重置所有变更或指定变更
482
+ if (changeName) {
483
+ console.warn(`⚠️ 即将重置变更「${changeName}」的全部进度(所有阶段的 steps + stage 状态,产物文件不动)。此操作不可逆。`);
484
+ // SQL: 删除该变更的所有 stages 和 steps 数据
485
+ const db = await this.pm._ensureDB(cwd);
486
+ db.transaction((sqlDb) => {
487
+ const changeRow = sqlDb.exec('SELECT id FROM changes WHERE name = ?', [changeName]);
488
+ if (changeRow && changeRow.length > 0 && changeRow[0].values.length > 0) {
489
+ const changeId = changeRow[0].values[0][0];
490
+ sqlDb.run('DELETE FROM steps WHERE stage_id IN (SELECT id FROM stages WHERE change_id = ?)', [changeId]);
491
+ sqlDb.run('DELETE FROM stages WHERE change_id = ?', [changeId]);
492
+ // 重新插入所有阶段(注意:上方已 DELETE stages,无需再 UPDATE)
493
+ for (const s of VALID_STAGES) {
494
+ sqlDb.run('INSERT OR IGNORE INTO stages (change_id, stage, status) VALUES (?, ?, "pending")', [changeId, s]);
495
+ }
496
+ }
497
+ });
498
+ console.log(`✅ 已重置变更 ${changeName} 的进度`);
499
+ } else {
500
+ const changes = await this.pm.listChanges(cwd);
501
+ console.warn(`⚠️ 即将重置【所有 ${changes.length} 个变更】的进度(仅 stage 状态,产物文件不动)。此操作不可逆且范围最大。`);
502
+ const db = await this.pm._ensureDB(cwd);
503
+ db.transaction((sqlDb) => {
504
+ for (const cn of changes) {
505
+ const changeRow = sqlDb.exec('SELECT id FROM changes WHERE name = ?', [cn]);
506
+ if (changeRow && changeRow.length > 0 && changeRow[0].values.length > 0) {
507
+ const changeId = changeRow[0].values[0][0];
508
+ sqlDb.run('DELETE FROM steps WHERE stage_id IN (SELECT id FROM stages WHERE change_id = ?)', [changeId]);
509
+ sqlDb.run('UPDATE stages SET status = "pending", started_at = NULL, completed_at = NULL WHERE change_id = ?', [changeId]);
510
+ }
511
+ }
512
+ });
513
+ console.log('✅ 已重置所有变更的进度');
514
+ }
515
+ }
516
+ }
517
+
518
+ _timeAgo(dateStr) {
519
+ if (!dateStr) return '未知';
520
+ let ts = Date.parse(dateStr);
521
+ if (isNaN(ts)) {
522
+ const m = dateStr.match(/(\d{4})[\/-](\d{1,2})[\/-](\d{1,2})[\s,]+(\d{1,2}):(\d{2})(?::(\d{2}))?/);
523
+ if (m) ts = new Date(+m[1], +m[2]-1, +m[3], +m[4], +m[5], +(m[6]||0)).getTime();
524
+ }
525
+ if (isNaN(ts)) return dateStr;
526
+ const diff = Date.now() - ts;
527
+ const minutes = Math.floor(diff / 60000);
528
+ if (minutes < 1) return '刚刚';
529
+ if (minutes < 60) return `${minutes} 分钟前`;
530
+ const hours = Math.floor(minutes / 60);
531
+ if (hours < 24) return `${hours} 小时前`;
532
+ return `${Math.floor(hours / 24)} 天前`;
533
+ }
534
+ }
535
+