sillyspec 3.18.2 → 3.18.4

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.
Files changed (40) hide show
  1. package/docs/brainstorm-plan-contract.md +64 -0
  2. package/docs/plan-execute-contract.md +123 -0
  3. package/docs/revision-mode.md +115 -0
  4. package/docs/sillyspec/file-lifecycle.md +13 -4
  5. package/docs/workflow-contract-regression.md +106 -0
  6. package/package.json +1 -1
  7. package/packages/dashboard/dist/assets/{index-DpLHK4jv.js → index-Bq_Z2hne.js} +568 -568
  8. package/packages/dashboard/dist/assets/{index-BcM2J-hv.css → index-O2W5RV4z.css} +1 -1
  9. package/packages/dashboard/dist/index.html +16 -16
  10. package/packages/dashboard/src/components/PipelineStage.vue +22 -2
  11. package/packages/dashboard/src/components/PipelineView.vue +10 -2
  12. package/packages/dashboard/src/components/StageBadge.vue +17 -3
  13. package/packages/dashboard/src/components/StepCard.vue +7 -2
  14. package/src/change-risk-profile.js +167 -0
  15. package/src/contract-matrix.js +278 -0
  16. package/src/db.js +6 -0
  17. package/src/endpoint-extractor.js +315 -0
  18. package/src/index.js +53 -6
  19. package/src/init.js +31 -4
  20. package/src/knowledge-match.js +130 -0
  21. package/src/progress.js +464 -11
  22. package/src/run.js +287 -7
  23. package/src/scan-postcheck.js +34 -2
  24. package/src/stage-contract.js +86 -6
  25. package/src/stages/brainstorm.js +23 -0
  26. package/src/stages/execute.js +158 -4
  27. package/src/stages/plan.js +82 -0
  28. package/src/stages/scan.js +40 -0
  29. package/src/stages/verify.js +63 -2
  30. package/src/worktree.js +264 -35
  31. package/test/brainstorm-plan-contract.test.mjs +273 -0
  32. package/test/contract-artifacts.test.mjs +323 -0
  33. package/test/knowledge-match.test.mjs +231 -0
  34. package/test/plan-execute-contract.test.mjs +330 -0
  35. package/test/platform-failure-samples.test.mjs +4 -0
  36. package/test/revision-v1.test.mjs +1145 -0
  37. package/test/scan-knowledge.test.mjs +175 -0
  38. package/test/scan-postcheck.test.mjs +3 -0
  39. package/test/spec-dir.test.mjs +8 -3
  40. package/test/stage-definitions.test.mjs +1 -1
package/src/progress.js CHANGED
@@ -38,7 +38,16 @@ const CHANGES_SUBDIR = 'changes';
38
38
  const GLOBAL_FILE = 'global.json';
39
39
  const CURRENT_VERSION = 3;
40
40
  const VALID_STAGES = ['scan', 'brainstorm', 'plan', 'execute', 'verify', 'archive', 'quick', 'explore'];
41
- const VALID_STATUSES = ['pending', 'in-progress', 'completed', 'failed', 'blocked', 'waiting'];
41
+ const VALID_STATUSES = ['pending', 'in-progress', 'completed', 'failed', 'blocked', 'waiting', 'stale'];
42
+
43
+ // Stage statuses (superset of step statuses)
44
+ const VALID_STAGE_STATUSES = ['pending', 'in-progress', 'completed', 'failed', 'blocked', 'revising', 'stale'];
45
+
46
+ // Main flow stage order (for downstream cascade)
47
+ // 完整主流程顺序(含 scan),用于下游 cascade
48
+ const STAGE_ORDER = ['scan', 'brainstorm', 'plan', 'execute', 'verify', 'archive'];
49
+ // 主流程阶段(不含 scan/quick/explore 等辅助阶段)
50
+ const MAIN_FLOW_ORDER = STAGE_ORDER;
42
51
 
43
52
  const STAGE_LABELS = {
44
53
  brainstorm: '🧠 需求探索',
@@ -208,13 +217,18 @@ export class ProgressManager {
208
217
  if (!changeRows || changeRows.length === 0 || changeRows[0].values.length === 0) return null;
209
218
  const [changeId, cName, currentStage, noWorktree, lastActive] = changeRows[0].values[0];
210
219
 
211
- // 2. 从 stages 表获取所有阶段
212
- const stageRows = sqlDb.exec('SELECT id, stage, status, started_at, completed_at FROM stages WHERE change_id = ? ORDER BY id', [changeId]);
220
+ // 2. 从 stages 表获取所有阶段(含 revision 列)
221
+ const stageRows = sqlDb.exec('SELECT id, stage, status, started_at, completed_at, revision, reopened_from_step, reopened_at, stale_reason FROM stages WHERE change_id = ? ORDER BY id', [changeId]);
213
222
  const stageMap = {};
214
223
  const stageIds = [];
215
224
  if (stageRows && stageRows.length > 0) {
216
- for (const [sId, stage, status, startedAt, completedAt] of stageRows[0].values) {
217
- stageMap[stage] = { _dbId: sId, status, startedAt, completedAt };
225
+ for (const [sId, stage, status, startedAt, completedAt, revision, reopenedFromStep, reopenedAt, staleReason] of stageRows[0].values) {
226
+ stageMap[stage] = { _dbId: sId, status, startedAt, completedAt,
227
+ ...(revision ? { revision } : {}),
228
+ ...(reopenedFromStep ? { reopenedFromStep } : {}),
229
+ ...(reopenedAt ? { reopenedAt } : {}),
230
+ ...(staleReason ? { staleReason } : {}),
231
+ };
218
232
  stageIds.push(sId);
219
233
  }
220
234
  }
@@ -291,6 +305,11 @@ export class ProgressManager {
291
305
  steps,
292
306
  startedAt: info.startedAt,
293
307
  completedAt: info.completedAt,
308
+ // Revision v1 fields
309
+ ...(info.revision ? { revision: info.revision } : {}),
310
+ ...(info.reopenedFromStep ? { reopenedFromStep: info.reopenedFromStep } : {}),
311
+ ...(info.reopenedAt ? { reopenedAt: info.reopenedAt } : {}),
312
+ ...(info.staleReason ? { staleReason: info.staleReason } : {}),
294
313
  };
295
314
  }
296
315
 
@@ -343,15 +362,20 @@ export class ProgressManager {
343
362
  // 3. 遍历 stages,UPSERT stages 表和 steps 表
344
363
  if (data.stages && typeof data.stages === 'object') {
345
364
  for (const [stageName, stageData] of Object.entries(data.stages)) {
346
- // UPSERT stages
365
+ // UPSERT stages 行(含 revision 列)
347
366
  sqlDb.run(
348
- `INSERT INTO stages (change_id, stage, status, started_at, completed_at)
349
- VALUES (?, ?, ?, ?, ?)
367
+ `INSERT INTO stages (change_id, stage, status, started_at, completed_at, revision, reopened_from_step, reopened_at, stale_reason)
368
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
350
369
  ON CONFLICT(change_id, stage) DO UPDATE SET
351
370
  status = excluded.status,
352
371
  started_at = excluded.started_at,
353
- completed_at = excluded.completed_at`,
354
- [changeId, stageName, stageData.status || 'pending', stageData.startedAt || null, stageData.completedAt || null]
372
+ completed_at = excluded.completed_at,
373
+ revision = COALESCE(excluded.revision, stages.revision),
374
+ reopened_from_step = excluded.reopened_from_step,
375
+ reopened_at = excluded.reopened_at,
376
+ stale_reason = excluded.stale_reason`,
377
+ [changeId, stageName, stageData.status || 'pending', stageData.startedAt || null, stageData.completedAt || null,
378
+ stageData.revision || 0, stageData.reopenedFromStep || null, stageData.reopenedAt || null, stageData.staleReason || null]
355
379
  );
356
380
 
357
381
  // 获取 stage_id
@@ -955,7 +979,7 @@ export class ProgressManager {
955
979
  console.log(' ═══════════════════════════════════════');
956
980
  console.log('');
957
981
 
958
- const statusIcons = { pending: '⬜', 'in-progress': '🔵', completed: '✅', failed: '❌', blocked: '🚫', waiting: '⏸️' };
982
+ const statusIcons = { pending: '⬜', 'in-progress': '🔵', completed: '✅', failed: '❌', blocked: '🚫', waiting: '⏸️', revising: '🔧', stale: '⚠️' };
959
983
 
960
984
  for (const stage of VALID_STAGES) {
961
985
  const stageData = data.stages[stage] || emptyStage();
@@ -965,6 +989,17 @@ export class ProgressManager {
965
989
 
966
990
  console.log(` ${icon} ${label}${isCurrent}`);
967
991
 
992
+ // Show revision info
993
+ if (stageData.revision && stageData.revision > 0) {
994
+ console.log(` 📋 revision: ${stageData.revision}${stageData.reopenedFromStep ? `, from step: ${stageData.reopenedFromStep}` : ''}`);
995
+ }
996
+ if (stageData.staleReason) {
997
+ console.log(` ⚠️ stale: ${stageData.staleReason}`);
998
+ if (stage === 'archive') {
999
+ console.log(` 📁 已有归档文件仍保留在磁盘上,但不再可信。`);
1000
+ }
1001
+ }
1002
+
968
1003
  if (stageData.steps && stageData.steps.length > 0) {
969
1004
  for (const step of stageData.steps) {
970
1005
  const si = statusIcons[step.status] || '○';
@@ -996,13 +1031,316 @@ export class ProgressManager {
996
1031
  }
997
1032
  }
998
1033
 
1034
+ // ── Next 建议 ──
1035
+ const suggestion = this._getNextSuggestion(data);
1036
+ if (suggestion) {
1037
+ console.log('');
1038
+ console.log(` 💡 ${suggestion.text}`);
1039
+ if (suggestion.command) console.log(` ${suggestion.command}`);
1040
+ }
1041
+
999
1042
  console.log('');
1000
1043
  }
1001
1044
 
1045
+ /**
1046
+ * 根据当前状态给出下一步建议
1047
+ * @param {object} data - progress data
1048
+ * @returns {{ text: string, command?: string }|null}
1049
+ */
1050
+ _getNextSuggestion(data) {
1051
+ // 找到第一个 revising 阶段
1052
+ const revisingStage = STAGE_ORDER.find(s => data.stages[s]?.status === 'revising');
1053
+ if (revisingStage) {
1054
+ const sd = data.stages[revisingStage];
1055
+ return {
1056
+ text: `${STAGE_LABELS[revisingStage] || revisingStage} 正在修订中(revision ${sd.revision || 1}),请继续完成修订。`,
1057
+ command: `sillyspec run ${revisingStage}`,
1058
+ };
1059
+ }
1060
+
1061
+ // 找到第一个 stale 阶段(上游已修,下游需要重建)
1062
+ const staleStage = STAGE_ORDER.find(s => data.stages[s]?.status === 'stale');
1063
+ if (staleStage) {
1064
+ const sd = data.stages[staleStage];
1065
+ return {
1066
+ text: `${STAGE_LABELS[staleStage] || staleStage} 已失效(${sd.staleReason || '上游修订'}),需要从第一步重建。`,
1067
+ command: `sillyspec run ${staleStage} --reopen --from-step 1`,
1068
+ };
1069
+ }
1070
+
1071
+ // 找到第一个有 pending/waiting/failed 步骤的 in-progress 阶段
1072
+ for (const s of STAGE_ORDER) {
1073
+ const sd = data.stages[s];
1074
+ if (!sd) continue;
1075
+ if (sd.status === 'in-progress' && sd.steps) {
1076
+ const hasPending = sd.steps.some(st => ['pending', 'waiting', 'failed'].includes(st.status));
1077
+ if (hasPending) {
1078
+ return {
1079
+ text: `${STAGE_LABELS[s] || s} 进行中,继续执行下一步。`,
1080
+ command: `sillyspec run ${s}`,
1081
+ };
1082
+ }
1083
+ }
1084
+ }
1085
+
1086
+ // 找到第一个 pending 主流程阶段
1087
+ for (const s of STAGE_ORDER) {
1088
+ const sd = data.stages[s];
1089
+ if (sd && sd.status === 'pending' && sd.steps && sd.steps.length > 0) {
1090
+ // 检查上游是否都 completed
1091
+ const idx = STAGE_ORDER.indexOf(s);
1092
+ const upstream = STAGE_ORDER.slice(0, idx);
1093
+ const upstreamOk = upstream.every(us =>
1094
+ data.stages[us]?.status === 'completed' || !data.stages[us] || data.stages[us].status === 'pending'
1095
+ );
1096
+ if (upstreamOk) {
1097
+ return {
1098
+ text: `可以开始 ${STAGE_LABELS[s] || s}。`,
1099
+ command: `sillyspec run ${s}`,
1100
+ };
1101
+ }
1102
+ }
1103
+ }
1104
+
1105
+ return null;
1106
+ }
1107
+
1002
1108
  async status(cwd, changeName = null) {
1003
1109
  await this.show(cwd, changeName);
1004
1110
  }
1005
1111
 
1112
+ /**
1113
+ * Revision v1 状态一致性检查
1114
+ * 只报告,不自动修复。
1115
+ * @param {string} cwd
1116
+ * @param {string|null} changeName
1117
+ * @returns {{ ok: boolean, issues: string[], warnings: string[] }}
1118
+ */
1119
+ async checkConsistency(cwd, changeName = null) {
1120
+ const data = await this.read(cwd, changeName);
1121
+ if (!data) {
1122
+ return { ok: false, issues: ['无法读取进度数据'], warnings: [] };
1123
+ }
1124
+
1125
+ const issues = [];
1126
+ const warnings = [];
1127
+
1128
+ for (const stageName of STAGE_ORDER) {
1129
+ const sd = data.stages[stageName];
1130
+ if (!sd) continue;
1131
+
1132
+ // a. completed stage 不能有 pending/stale steps
1133
+ if (sd.status === 'completed' && sd.steps) {
1134
+ const badSteps = sd.steps.filter(s => ['pending', 'stale', 'in-progress'].includes(s.status));
1135
+ for (const step of badSteps) {
1136
+ issues.push(`${stageName}/${step.name}: step 状态为 ${step.status},但 stage 状态为 completed`);
1137
+ }
1138
+ }
1139
+
1140
+ // b. revising stage 应有 revision > 0 或 reopenedFromStep
1141
+ if (sd.status === 'revising') {
1142
+ if (!sd.revision || sd.revision < 1) {
1143
+ issues.push(`${stageName}: 状态为 revising 但 revision 缺失或为 0`);
1144
+ }
1145
+ if (!sd.reopenedFromStep) {
1146
+ warnings.push(`${stageName}: 状态为 revising 但未记录 reopenedFromStep`);
1147
+ }
1148
+ }
1149
+
1150
+ // c. stale stage 应有 staleReason
1151
+ if (sd.status === 'stale') {
1152
+ if (!sd.staleReason) {
1153
+ warnings.push(`${stageName}: 状态为 stale 但缺少 staleReason`);
1154
+ }
1155
+ }
1156
+
1157
+ // d. 下游 completed 不能出现在上游 stale/revising 之后
1158
+ const stageIdx = STAGE_ORDER.indexOf(stageName);
1159
+ for (let i = 0; i < stageIdx; i++) {
1160
+ const upstream = STAGE_ORDER[i];
1161
+ const upData = data.stages[upstream];
1162
+ if (upData && (upData.status === 'stale' || upData.status === 'revising')) {
1163
+ if (sd.status === 'completed') {
1164
+ issues.push(`${stageName}: 状态为 completed,但上游 ${upstream} 状态为 ${upData.status}(下游不应在上游修订/失效时保持 completed)`);
1165
+ }
1166
+ }
1167
+ }
1168
+
1169
+ // e. step stale 时 stage 不应是 completed
1170
+ if (sd.status === 'completed' && sd.steps) {
1171
+ const staleSteps = sd.steps.filter(s => s.status === 'stale');
1172
+ for (const step of staleSteps) {
1173
+ issues.push(`${stageName}/${step.name}: step 状态为 stale,但 stage 状态为 completed`);
1174
+ }
1175
+ }
1176
+ }
1177
+
1178
+ // 输出报告
1179
+ console.log('');
1180
+ console.log(' ═══════════════════════════════════════');
1181
+ console.log(' 状态一致性检查');
1182
+ console.log(' ═══════════════════════════════════════');
1183
+
1184
+ if (issues.length === 0 && warnings.length === 0) {
1185
+ console.log(' ✅ 未发现一致性问题');
1186
+ } else {
1187
+ if (issues.length > 0) {
1188
+ console.log(`\n ❌ 问题 (${issues.length}):`);
1189
+ for (const issue of issues) console.log(` - ${issue}`);
1190
+ }
1191
+ if (warnings.length > 0) {
1192
+ console.log(`\n ⚠️ 警告 (${warnings.length}):`);
1193
+ for (const w of warnings) console.log(` - ${w}`);
1194
+ }
1195
+ }
1196
+ console.log('');
1197
+
1198
+ return { ok: issues.length === 0, issues, warnings };
1199
+ }
1200
+
1201
+ /**
1202
+ * Revision v1.2 状态修复
1203
+ * 默认 dry-run,--apply 才真正修改 DB。
1204
+ * 只修安全项,不碰产物文件、不 reset/reopen stage。
1205
+ *
1206
+ * @param {string} cwd
1207
+ * @param {object} opts
1208
+ * @param {boolean} [opts.apply=false]
1209
+ * @param {string|null} [opts.changeName]
1210
+ * @returns {{ fixable: object[], manual: string[], applied: object[] }}
1211
+ */
1212
+ async repairConsistency(cwd, opts = {}) {
1213
+ const { apply = false, changeName = null } = opts;
1214
+
1215
+ const data = await this.read(cwd, changeName);
1216
+ if (!data) {
1217
+ console.log('❌ 无法读取进度数据');
1218
+ return { fixable: [], manual: ['无法读取进度数据'], applied: [] };
1219
+ }
1220
+
1221
+ const fixable = []; // { stage, action, description, apply: (data) => void }
1222
+ const manual = []; // string
1223
+
1224
+ const now = new Date().toLocaleString('zh-CN', { hour12: false });
1225
+
1226
+ for (const stageName of STAGE_ORDER) {
1227
+ const sd = data.stages[stageName];
1228
+ if (!sd) continue;
1229
+
1230
+ // Fix a: stale stage 缺 staleReason → 补默认原因
1231
+ if (sd.status === 'stale' && !sd.staleReason) {
1232
+ const reason = stageName === 'archive'
1233
+ ? 'upstream stage revised; existing archive artifacts are preserved but no longer trusted'
1234
+ : 'unknown upstream revision';
1235
+ fixable.push({
1236
+ stage: stageName,
1237
+ action: 'set_stale_reason',
1238
+ description: `${stageName}: stale 缺 staleReason → 补 "${reason}"`,
1239
+ apply: (d) => { d.stages[stageName].staleReason = reason; },
1240
+ });
1241
+ }
1242
+
1243
+ // Fix b: 上游 stale/revising,下游仍 completed → cascade stale
1244
+ const stageIdx = STAGE_ORDER.indexOf(stageName);
1245
+ for (let i = 0; i < stageIdx; i++) {
1246
+ const upstream = STAGE_ORDER[i];
1247
+ const upData = data.stages[upstream];
1248
+ if (upData && (upData.status === 'stale' || upData.status === 'revising')) {
1249
+ if (sd.status === 'completed') {
1250
+ const upStatus = upData.status;
1251
+ const reason = `upstream ${upstream} is ${upStatus}`;
1252
+ fixable.push({
1253
+ stage: stageName,
1254
+ action: 'cascade_stale',
1255
+ description: `${stageName}: completed → stale(上游 ${upstream} 为 ${upStatus})`,
1256
+ apply: (d) => {
1257
+ d.stages[stageName].status = 'stale';
1258
+ d.stages[stageName].staleReason = reason;
1259
+ d.stages[stageName].completedAt = null;
1260
+ },
1261
+ });
1262
+ }
1263
+ }
1264
+ }
1265
+
1266
+ // Fix c: archive stale 缺 staleReason(专用文案)
1267
+ if (stageName === 'archive' && sd.status === 'stale' && !sd.staleReason) {
1268
+ // 已在 Fix a 中处理,这里不重复
1269
+ }
1270
+
1271
+ // Fix d: revising stage 缺 reopenedAt → 补当前时间
1272
+ if (sd.status === 'revising' && !sd.reopenedAt) {
1273
+ fixable.push({
1274
+ stage: stageName,
1275
+ action: 'set_reopened_at',
1276
+ description: `${stageName}: revising 缺 reopenedAt → 补当前时间`,
1277
+ apply: (d) => { d.stages[stageName].reopenedAt = now; },
1278
+ });
1279
+ }
1280
+
1281
+ // Manual a: completed stage 里有 pending/stale/in-progress steps
1282
+ if (sd.status === 'completed' && sd.steps) {
1283
+ const badSteps = sd.steps.filter(s => ['pending', 'stale', 'in-progress'].includes(s.status));
1284
+ for (const step of badSteps) {
1285
+ manual.push(`${stageName}/${step.name}: step 状态为 ${step.status},但 stage 状态为 completed(需手动确认)`);
1286
+ }
1287
+ }
1288
+
1289
+ // Manual b: revising stage 缺 reopenedFromStep
1290
+ if (sd.status === 'revising' && !sd.reopenedFromStep) {
1291
+ manual.push(`${stageName}: revising 缺 reopenedFromStep(需手动确认修订起始步骤)`);
1292
+ }
1293
+
1294
+ // Manual c: steps 为空但 stage completed
1295
+ if (sd.status === 'completed' && (!sd.steps || sd.steps.length === 0)) {
1296
+ manual.push(`${stageName}: completed 但 steps 为空(需手动确认)`);
1297
+ }
1298
+ }
1299
+
1300
+ // 输出报告
1301
+ console.log('');
1302
+ console.log(' ═══════════════════════════════════════');
1303
+ console.log(` 状态修复 ${apply ? '(--apply 模式)' : '(dry-run 模式)'}`);
1304
+ console.log(' ═══════════════════════════════════════');
1305
+
1306
+ if (fixable.length === 0 && manual.length === 0) {
1307
+ console.log(' ✅ 未发现问题,无需修复');
1308
+ console.log('');
1309
+ return { fixable: [], manual: [], applied: [] };
1310
+ }
1311
+
1312
+ const applied = [];
1313
+
1314
+ if (fixable.length > 0) {
1315
+ console.log(`\n 🔧 可自动修复 (${fixable.length}):`);
1316
+ for (const item of fixable) {
1317
+ console.log(` - ${item.description}`);
1318
+ if (apply) {
1319
+ item.apply(data);
1320
+ applied.push({ stage: item.stage, action: item.action });
1321
+ }
1322
+ }
1323
+ if (!apply) {
1324
+ console.log('\n 💡 使用 --apply 执行修复');
1325
+ }
1326
+ }
1327
+
1328
+ if (manual.length > 0) {
1329
+ console.log(`\n 👆 需手动处理 (${manual.length}):`);
1330
+ for (const m of manual) console.log(` - ${m}`);
1331
+ }
1332
+
1333
+ if (apply && applied.length > 0) {
1334
+ data.lastActive = now;
1335
+ await this._write(cwd, data, changeName);
1336
+ console.log(`\n ✅ 已修复 ${applied.length} 项`);
1337
+ }
1338
+
1339
+ console.log('');
1340
+
1341
+ return { fixable, manual, applied };
1342
+ }
1343
+
1006
1344
  async validate(cwd, changeName = null) {
1007
1345
  const data = await this.read(cwd, changeName);
1008
1346
  if (!data) { console.log('❌ 无法读取进度数据'); return false; }
@@ -1038,6 +1376,121 @@ export class ProgressManager {
1038
1376
  return true;
1039
1377
  }
1040
1378
 
1379
+ /**
1380
+ * 重新打开已完成的阶段进入修订模式
1381
+ * - 不带 fromStep:只允许存在 pending/stale/waiting/failed 步骤时继续
1382
+ * - 带 fromStep:从该步骤起,当前及后续步骤标记 stale/pending
1383
+ * - 自动级联标记下游阶段为 stale
1384
+ *
1385
+ * @param {string} cwd
1386
+ * @param {string} stage - 要重开的阶段
1387
+ * @param {object} opts
1388
+ * @param {string|number} [opts.fromStep] - 步骤名或序号(1-based)
1389
+ * @param {string} [opts.changeName]
1390
+ * @returns {{ ok: boolean, error?: string }}
1391
+ */
1392
+ async reopenStage(cwd, stage, opts = {}) {
1393
+ const { fromStep, changeName = null } = opts;
1394
+
1395
+ const data = await this.read(cwd, changeName);
1396
+ if (!data) return { ok: false, error: '无法读取进度数据' };
1397
+
1398
+ const stageData = data.stages[stage];
1399
+ if (!stageData) return { ok: false, error: `未知阶段: ${stage}` };
1400
+
1401
+ const steps = stageData.steps || [];
1402
+
1403
+ // 确定 fromStep 对应的 index
1404
+ let fromIdx = null;
1405
+ if (fromStep != null) {
1406
+ if (typeof fromStep === 'number' || /^\d+$/.test(String(fromStep))) {
1407
+ fromIdx = parseInt(String(fromStep), 10) - 1; // 1-based → 0-based
1408
+ if (fromIdx < 0 || fromIdx >= steps.length) {
1409
+ return { ok: false, error: `步骤序号超出范围: ${fromStep}(共 ${steps.length} 步)` };
1410
+ }
1411
+ } else {
1412
+ // 按名称匹配
1413
+ fromIdx = steps.findIndex(s => s.name === fromStep);
1414
+ if (fromIdx === -1) {
1415
+ return { ok: false, error: `步骤不存在: ${fromStep}` };
1416
+ }
1417
+ }
1418
+ }
1419
+
1420
+ // 如果不带 fromStep,检查是否存在中断步骤
1421
+ if (fromIdx === null) {
1422
+ const hasInterrupted = steps.some(s =>
1423
+ ['pending', 'stale', 'waiting', 'failed'].includes(s.status)
1424
+ );
1425
+ if (!hasInterrupted) {
1426
+ return { ok: false, error: `阶段 ${stage} 所有步骤均已完成,请使用 --from-step 指定从哪一步开始修订` };
1427
+ }
1428
+ // 找到第一个中断步骤
1429
+ fromIdx = steps.findIndex(s =>
1430
+ ['pending', 'stale', 'waiting', 'failed'].includes(s.status)
1431
+ );
1432
+ }
1433
+
1434
+ // 执行重开操作
1435
+ const newRevision = (stageData.revision || 0) + 1;
1436
+ const fromStepName = steps[fromIdx].name;
1437
+ const now = new Date().toLocaleString('zh-CN', { hour12: false });
1438
+
1439
+ // 更新步骤状态:fromStep 之前的保持 completed,fromStep 变 pending,之后的变 stale
1440
+ for (let i = 0; i < steps.length; i++) {
1441
+ if (i === fromIdx) {
1442
+ steps[i].status = 'pending';
1443
+ steps[i].completedAt = null;
1444
+ steps[i].output = null;
1445
+ } else if (i > fromIdx) {
1446
+ steps[i].status = 'stale';
1447
+ steps[i].completedAt = null;
1448
+ }
1449
+ // i < fromIdx: 保持原状(completed)
1450
+ }
1451
+
1452
+ stageData.status = 'revising';
1453
+ stageData.completedAt = null;
1454
+ stageData.revision = newRevision;
1455
+ stageData.reopenedFromStep = `${fromIdx + 1}: ${fromStepName}`; // 存 "index: name" 格式
1456
+ stageData.reopenedAt = now;
1457
+ stageData.steps = steps;
1458
+
1459
+ data.lastActive = now;
1460
+ data.currentStage = stage;
1461
+
1462
+ await this._write(cwd, data, changeName);
1463
+
1464
+ // 级联标记下游阶段为 stale
1465
+ const downstreamStages = this._getDownstreamStages(stage);
1466
+ if (downstreamStages.length > 0) {
1467
+ const data2 = await this.read(cwd, changeName); // 重新读取以获取最新状态
1468
+ if (data2) {
1469
+ for (const ds of downstreamStages) {
1470
+ if (data2.stages[ds] && data2.stages[ds].status === 'completed') {
1471
+ data2.stages[ds].status = 'stale';
1472
+ data2.stages[ds].staleReason = `上游阶段 ${stage} 已修订 (revision ${newRevision})`;
1473
+ data2.stages[ds].completedAt = null;
1474
+ }
1475
+ }
1476
+ await this._write(cwd, data2, changeName);
1477
+ }
1478
+ }
1479
+
1480
+ return { ok: true, revision: newRevision, fromStep: fromStepName };
1481
+ }
1482
+
1483
+ /**
1484
+ * 获取指定阶段的下游主流程阶段列表
1485
+ * @param {string} stage
1486
+ * @returns {string[]}
1487
+ */
1488
+ _getDownstreamStages(stage) {
1489
+ const idx = MAIN_FLOW_ORDER.indexOf(stage);
1490
+ if (idx === -1) return [];
1491
+ return MAIN_FLOW_ORDER.slice(idx + 1);
1492
+ }
1493
+
1041
1494
  async reset(cwd, stage, changeName = null) {
1042
1495
  if (stage) {
1043
1496
  const data = await this.read(cwd, changeName);