sillyspec 3.25.1 → 3.25.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/.claude/skills/sillyspec-doctor/SKILL.md +9 -0
  2. package/.playwright-mcp/console-2026-07-27T15-45-50-621Z.log +1 -0
  3. package/.playwright-mcp/page-2026-07-27T15-45-51-459Z.yml +144 -0
  4. package/.playwright-mcp/prompt-final.png +0 -0
  5. package/.playwright-mcp/prompt-view.png +0 -0
  6. package/CLAUDE.md +18 -0
  7. package/package.json +1 -1
  8. package/src/change-risk-profile.js +60 -12
  9. package/src/check-primitives.js +65 -0
  10. package/src/index.js +135 -6
  11. package/src/progress/change-registry.js +217 -0
  12. package/src/progress/consistency-doctor.js +259 -0
  13. package/src/progress/shared.js +33 -0
  14. package/src/progress/stage-machine.js +535 -0
  15. package/src/progress/step-store.js +256 -0
  16. package/src/progress.js +59 -1191
  17. package/src/quicklog.js +3 -1
  18. package/src/run/command.js +127 -50
  19. package/src/run/complete-handlers.js +2 -1
  20. package/src/run/complete.js +725 -695
  21. package/src/run/gates.js +326 -321
  22. package/src/run/prompt.js +619 -604
  23. package/src/run/quick-audit.js +107 -102
  24. package/src/run/shared.js +43 -0
  25. package/src/stage-contract-engine.js +150 -0
  26. package/src/stage-contract-spec.js +517 -0
  27. package/src/stage-contract.js +83 -148
  28. package/src/stage-review.js +63 -0
  29. package/src/stages/brainstorm.js +3 -1
  30. package/src/stages/execute.js +5 -2
  31. package/src/stages/plan-postcheck.js +38 -27
  32. package/src/stages/plan.js +12 -49
  33. package/src/stages/propose.js +2 -1
  34. package/src/stages/scan.js +3 -1
  35. package/src/workflow.js +9 -17
  36. package/src/worktree-apply.js +5 -2
  37. package/src/worktree-deps.js +114 -32
  38. package/src/worktree.js +6 -3
  39. package/src/brainstorm-postcheck.js +0 -158
@@ -0,0 +1,217 @@
1
+ // W6 Step9b: 变更注册表(changes 表生命周期)—— 注册/注销/重命名/隔离状态/平台同步戳/审批状态。
2
+ // 从 src/progress.js 单体 ProgressManager 抽出。持有 pm 引用(构造注入),调 pm._ensureDB /
3
+ // pm._changePath(persistence-core 留 facade 本体)。无共享常量依赖。
4
+ import { existsSync, mkdirSync, renameSync } from 'fs';
5
+
6
+ export class ChangeRegistry {
7
+ constructor(pm) {
8
+ this.pm = pm;
9
+ }
10
+
11
+ /**
12
+ * 列出所有活跃变更名
13
+ * SQL: SELECT name FROM changes WHERE status = 'active'
14
+ */
15
+ async listChanges(cwd) {
16
+ const db = await this.pm._ensureDB(cwd);
17
+ const sqlDb = db.getDb();
18
+ const rows = sqlDb.exec("SELECT name FROM changes WHERE status = 'active' ORDER BY name");
19
+ if (!rows || rows.length === 0) return [];
20
+ return rows[0].values.map(r => r[0]);
21
+ }
22
+
23
+ /**
24
+ * 注册变更到活跃列表
25
+ * SQL: INSERT OR IGNORE → 若已 archived 则 UPDATE status='active'
26
+ */
27
+ async registerChange(cwd, changeName) {
28
+ if (!changeName) {
29
+ console.warn('⚠️ registerChange: changeName 为空,跳过');
30
+ return;
31
+ }
32
+ const db = await this.pm._ensureDB(cwd);
33
+ db.transaction((sqlDb) => {
34
+ const now = new Date().toISOString();
35
+ // 尝试插入新行
36
+ sqlDb.run(
37
+ `INSERT OR IGNORE INTO changes (name, created_at, last_active)
38
+ VALUES (?, ?, ?)`,
39
+ [changeName, now, now]
40
+ );
41
+ // 注意:不复活已归档的变更——归档是不可逆操作
42
+ // 如果变更已存在且为 archived,保持 archived 状态不变
43
+ });
44
+ }
45
+
46
+ /**
47
+ * 更新变更的隔离状态
48
+ * @param {string} cwd - 项目根目录
49
+ * @param {string} changeName - 变更名
50
+ * @param {{ status: string, mode?: string, reason?: string }} isolation
51
+ */
52
+ async updateChangeIsolation(cwd, changeName, isolation) {
53
+ const db = await this.pm._ensureDB(cwd);
54
+ const sqlDb = db.getDb();
55
+ try {
56
+ sqlDb.run(
57
+ `UPDATE changes SET isolation_status = ?, isolation_mode = ?, isolation_reason = ?, last_active = ? WHERE name = ?`,
58
+ [isolation.status, isolation.mode || null, isolation.reason || null, new Date().toISOString(), changeName]
59
+ );
60
+ db._save();
61
+ } catch (err) {
62
+ console.warn('⚠️ 更新 isolation 状态失败:', err.message);
63
+ }
64
+ }
65
+
66
+ /**
67
+ * 读取变更的隔离状态
68
+ * @param {string} cwd - 项目根目录
69
+ * @param {string} changeName - 变更名
70
+ * @returns {{ status: string|null, mode: string|null, reason: string|null }|null}
71
+ */
72
+ async readChangeIsolation(cwd, changeName) {
73
+ const db = await this.pm._ensureDB(cwd);
74
+ const sqlDb = db.getDb();
75
+ try {
76
+ const rows = sqlDb.exec(
77
+ `SELECT isolation_status, isolation_mode, isolation_reason FROM changes WHERE name = ?`,
78
+ [changeName]
79
+ );
80
+ if (!rows || rows.length === 0 || rows[0].values.length === 0) return null;
81
+ const [status, mode, reason] = rows[0].values[0];
82
+ return { status: status || null, mode: mode || null, reason: reason || null };
83
+ } catch {
84
+ return null;
85
+ }
86
+ }
87
+
88
+ async _updatePlatformLastSync(cwd, changeName) {
89
+ if (!changeName) return;
90
+ const db = await this.pm._ensureDB(cwd);
91
+ db.transaction((sqlDb) => {
92
+ sqlDb.run(
93
+ 'UPDATE changes SET platform_last_sync = ?, platform_sync_enabled = 1 WHERE name = ?',
94
+ [new Date().toISOString(), changeName]
95
+ );
96
+ });
97
+ }
98
+
99
+ async _updateApprovalStatus(cwd, changeName, status, reason = null) {
100
+ if (!changeName || !status) return;
101
+ const db = await this.pm._ensureDB(cwd);
102
+ db.transaction((sqlDb) => {
103
+ const rows = sqlDb.exec('SELECT id FROM changes WHERE name = ?', [changeName]);
104
+ if (!rows || rows.length === 0 || rows[0].values.length === 0) return;
105
+ const changeId = rows[0].values[0][0];
106
+ const now = new Date().toISOString();
107
+ sqlDb.run(
108
+ `INSERT INTO approvals (change_id, status, requested_at, approved_at, rejection_reason)
109
+ VALUES (?, ?, ?, ?, ?)
110
+ ON CONFLICT(change_id) DO UPDATE SET
111
+ status = excluded.status,
112
+ approved_at = excluded.approved_at,
113
+ rejection_reason = excluded.rejection_reason`,
114
+ [
115
+ changeId,
116
+ status,
117
+ now,
118
+ status === 'approved' ? now : null,
119
+ status === 'rejected' ? reason : null,
120
+ ]
121
+ );
122
+ });
123
+ }
124
+
125
+ /**
126
+ * 重命名变更:同步更新 DB + 目录
127
+ * @param {string} cwd - 项目根目录
128
+ * @param {string} oldName - 旧变更名
129
+ * @param {string} newName - 新变更名
130
+ */
131
+ async renameChange(cwd, oldName, newName) {
132
+ if (!oldName || !newName) {
133
+ console.warn('⚠️ renameChange: 旧名或新名为空,跳过');
134
+ return;
135
+ }
136
+ if (oldName === newName) {
137
+ console.warn('⚠️ renameChange: 新旧名称相同,跳过');
138
+ return;
139
+ }
140
+ const db = await this.pm._ensureDB(cwd);
141
+ // 检查旧名是否存在
142
+ const existing = db.transaction((sqlDb) => {
143
+ const row = sqlDb.exec(`SELECT name, status FROM changes WHERE name = ?`, [oldName]);
144
+ if (!row || !row[0] || row[0].values.length === 0) return null;
145
+ return { name: row[0].values[0][0], status: row[0].values[0][1] };
146
+ });
147
+ if (!existing) {
148
+ console.error(`❌ 变更 ${oldName} 不存在`);
149
+ return;
150
+ }
151
+ // 检查新名是否已存在
152
+ const conflict = db.transaction((sqlDb) => {
153
+ const row = sqlDb.exec(`SELECT name FROM changes WHERE name = ?`, [newName]);
154
+ return row && row[0] && row[0].values.length > 0;
155
+ });
156
+ if (conflict) {
157
+ console.error(`❌ 变更 ${newName} 已存在`);
158
+ return;
159
+ }
160
+ // 先更新 DB,再重命名目录;FS 失败则回滚 DB,避免"目录已改名但 DB 旧名"的孤儿
161
+ // (旧实现 FS-first 无补偿:renameSync 成功后 DB transaction 抛 EPERM/EBUSY 会让
162
+ // 目录已是 newName、DB 仍是 oldName,read(两名) 都失联且无自动恢复)。
163
+ const oldDir = this.pm._changePath(cwd, oldName);
164
+ const newDir = this.pm._changePath(cwd, newName);
165
+ const now = new Date().toISOString();
166
+ try {
167
+ db.transaction((sqlDb) => {
168
+ sqlDb.run(`UPDATE changes SET name = ?, last_active = ? WHERE name = ?`, [newName, now, oldName]);
169
+ });
170
+ } catch (e) {
171
+ console.error(`❌ 重命名失败:更新数据库时出错(${e.message})`);
172
+ return;
173
+ }
174
+ let renamed = true;
175
+ if (existsSync(oldDir)) {
176
+ try {
177
+ renameSync(oldDir, newDir);
178
+ } catch (e) {
179
+ renamed = false;
180
+ // FS 重命名失败:回滚 DB 恢复 oldName,保持 DB 与目录一致
181
+ try {
182
+ db.transaction((sqlDb) => {
183
+ sqlDb.run(`UPDATE changes SET name = ?, last_active = ? WHERE name = ?`, [oldName, now, newName]);
184
+ });
185
+ console.error(`❌ 重命名失败:移动目录出错(${e.message}),已回滚数据库`);
186
+ } catch (rollbackErr) {
187
+ // 回滚本身也失败:DB 是 newName、目录是 oldName,两端分裂且无自动恢复。
188
+ // 不能再撒谎"已回滚"——必须显式报错让用户跑 doctor 修复。
189
+ console.error(`❌ 重命名失败且数据库回滚也失败(状态分裂,需手动修复):原错=${e.message} 回滚错=${rollbackErr.message}。请跑 sillyspec doctor --json`);
190
+ }
191
+ }
192
+ } else {
193
+ mkdirSync(newDir, { recursive: true });
194
+ }
195
+ if (renamed) console.log(`✅ 变更已重命名:${oldName} → ${newName}`);
196
+ }
197
+
198
+ /**
199
+ * 从活跃列表移除变更(归档时调用,不物理删除)
200
+ * SQL: UPDATE changes SET status = 'archived'
201
+ */
202
+ async unregisterChange(cwd, changeName) {
203
+ if (!changeName) {
204
+ console.warn('⚠️ unregisterChange: changeName 为空,跳过');
205
+ return;
206
+ }
207
+ const db = await this.pm._ensureDB(cwd);
208
+ db.transaction((sqlDb) => {
209
+ const now = new Date().toISOString();
210
+ sqlDb.run(
211
+ `UPDATE changes SET status = 'archived', last_active = ? WHERE name = ?`,
212
+ [now, changeName]
213
+ );
214
+ });
215
+ }
216
+ }
217
+
@@ -0,0 +1,259 @@
1
+ // W6 Step9a: Revision v1 状态一致性检查 + 修复 + --force 审计日志。
2
+ // 从 src/progress.js 单体 ProgressManager 抽出。持有 pm 引用(构造注入),调 pm.read / pm._write /
3
+ // pm._ensureRuntimeDir / pm._runtimePath(persistence-core 留 facade 本体)。
4
+ import { appendFileSync } from 'fs';
5
+ import { STAGE_ORDER } from './shared.js';
6
+
7
+ export class ConsistencyDoctor {
8
+ constructor(pm) {
9
+ this.pm = pm;
10
+ }
11
+
12
+ /**
13
+ * 强制状态变更的审计记录(--force 逃生口专用)。
14
+ * 追加到 .runtime/audit.log,供人工/doctor 追溯"谁在什么时候绕过了校验"。
15
+ */
16
+ _appendAuditLog(cwd, entry) {
17
+ try {
18
+ this.pm._ensureRuntimeDir(cwd);
19
+ const auditPath = this.pm._runtimePath(cwd, 'audit.log');
20
+ const line = JSON.stringify({ at: new Date().toISOString(), ...entry });
21
+ appendFileSync(auditPath, line + '\n');
22
+ } catch (e) {
23
+ console.warn(`⚠️ 审计日志写入失败: ${e.message}`);
24
+ }
25
+ }
26
+
27
+ /**
28
+ * Revision v1 状态一致性检查
29
+ * 只报告,不自动修复。
30
+ * @param {string} cwd
31
+ * @param {string|null} changeName
32
+ * @returns {{ ok: boolean, issues: string[], warnings: string[] }}
33
+ */
34
+ async checkConsistency(cwd, changeName = null) {
35
+ const data = await this.pm.read(cwd, changeName);
36
+ if (!data) {
37
+ return { ok: false, issues: ['无法读取进度数据'], warnings: [] };
38
+ }
39
+
40
+ const issues = [];
41
+ const warnings = [];
42
+
43
+ for (const stageName of STAGE_ORDER) {
44
+ const sd = data.stages[stageName];
45
+ if (!sd) continue;
46
+
47
+ // a. completed stage 不能有 pending/stale steps
48
+ if (sd.status === 'completed' && sd.steps) {
49
+ const badSteps = sd.steps.filter(s => ['pending', 'stale', 'in-progress'].includes(s.status));
50
+ for (const step of badSteps) {
51
+ issues.push(`${stageName}/${step.name}: step 状态为 ${step.status},但 stage 状态为 completed`);
52
+ }
53
+ }
54
+
55
+ // b. revising stage 应有 revision > 0 或 reopenedFromStep
56
+ if (sd.status === 'revising') {
57
+ if (!sd.revision || sd.revision < 1) {
58
+ issues.push(`${stageName}: 状态为 revising 但 revision 缺失或为 0`);
59
+ }
60
+ if (!sd.reopenedFromStep) {
61
+ warnings.push(`${stageName}: 状态为 revising 但未记录 reopenedFromStep`);
62
+ }
63
+ }
64
+
65
+ // c. stale stage 应有 staleReason
66
+ if (sd.status === 'stale') {
67
+ if (!sd.staleReason) {
68
+ warnings.push(`${stageName}: 状态为 stale 但缺少 staleReason`);
69
+ }
70
+ }
71
+
72
+ // d. 下游 completed 不能出现在上游 stale/revising 之后
73
+ const stageIdx = STAGE_ORDER.indexOf(stageName);
74
+ for (let i = 0; i < stageIdx; i++) {
75
+ const upstream = STAGE_ORDER[i];
76
+ const upData = data.stages[upstream];
77
+ if (upData && (upData.status === 'stale' || upData.status === 'revising')) {
78
+ if (sd.status === 'completed') {
79
+ issues.push(`${stageName}: 状态为 completed,但上游 ${upstream} 状态为 ${upData.status}(下游不应在上游修订/失效时保持 completed)`);
80
+ }
81
+ }
82
+ }
83
+
84
+ // e. step stale 时 stage 不应是 completed
85
+ if (sd.status === 'completed' && sd.steps) {
86
+ const staleSteps = sd.steps.filter(s => s.status === 'stale');
87
+ for (const step of staleSteps) {
88
+ issues.push(`${stageName}/${step.name}: step 状态为 stale,但 stage 状态为 completed`);
89
+ }
90
+ }
91
+ }
92
+
93
+ // 输出报告
94
+ console.log('');
95
+ console.log(' ═══════════════════════════════════════');
96
+ console.log(' 状态一致性检查');
97
+ console.log(' ═══════════════════════════════════════');
98
+
99
+ if (issues.length === 0 && warnings.length === 0) {
100
+ console.log(' ✅ 未发现一致性问题');
101
+ } else {
102
+ if (issues.length > 0) {
103
+ console.log(`\n ❌ 问题 (${issues.length}):`);
104
+ for (const issue of issues) console.log(` - ${issue}`);
105
+ }
106
+ if (warnings.length > 0) {
107
+ console.log(`\n ⚠️ 警告 (${warnings.length}):`);
108
+ for (const w of warnings) console.log(` - ${w}`);
109
+ }
110
+ }
111
+ console.log('');
112
+
113
+ return { ok: issues.length === 0, issues, warnings };
114
+ }
115
+
116
+ /**
117
+ * Revision v1.2 状态修复
118
+ * 默认 dry-run,--apply 才真正修改 DB。
119
+ * 只修安全项,不碰产物文件、不 reset/reopen stage。
120
+ *
121
+ * @param {string} cwd
122
+ * @param {object} opts
123
+ * @param {boolean} [opts.apply=false]
124
+ * @param {string|null} [opts.changeName]
125
+ * @returns {{ fixable: object[], manual: string[], applied: object[] }}
126
+ */
127
+ async repairConsistency(cwd, opts = {}) {
128
+ const { apply = false, changeName = null } = opts;
129
+
130
+ const data = await this.pm.read(cwd, changeName);
131
+ if (!data) {
132
+ console.log('❌ 无法读取进度数据');
133
+ return { fixable: [], manual: ['无法读取进度数据'], applied: [] };
134
+ }
135
+
136
+ const fixable = []; // { stage, action, description, apply: (data) => void }
137
+ const manual = []; // string
138
+
139
+ const now = new Date().toLocaleString('zh-CN', { hour12: false });
140
+
141
+ for (const stageName of STAGE_ORDER) {
142
+ const sd = data.stages[stageName];
143
+ if (!sd) continue;
144
+
145
+ // Fix a: stale stage 缺 staleReason → 补默认原因
146
+ if (sd.status === 'stale' && !sd.staleReason) {
147
+ const reason = stageName === 'archive'
148
+ ? 'upstream stage revised; existing archive artifacts are preserved but no longer trusted'
149
+ : 'unknown upstream revision';
150
+ fixable.push({
151
+ stage: stageName,
152
+ action: 'set_stale_reason',
153
+ description: `${stageName}: stale 缺 staleReason → 补 "${reason}"`,
154
+ apply: (d) => { d.stages[stageName].staleReason = reason; },
155
+ });
156
+ }
157
+
158
+ // Fix b: 上游 stale/revising,下游仍 completed → cascade stale
159
+ const stageIdx = STAGE_ORDER.indexOf(stageName);
160
+ for (let i = 0; i < stageIdx; i++) {
161
+ const upstream = STAGE_ORDER[i];
162
+ const upData = data.stages[upstream];
163
+ if (upData && (upData.status === 'stale' || upData.status === 'revising')) {
164
+ if (sd.status === 'completed') {
165
+ const upStatus = upData.status;
166
+ const reason = `upstream ${upstream} is ${upStatus}`;
167
+ fixable.push({
168
+ stage: stageName,
169
+ action: 'cascade_stale',
170
+ description: `${stageName}: completed → stale(上游 ${upstream} 为 ${upStatus})`,
171
+ apply: (d) => {
172
+ d.stages[stageName].status = 'stale';
173
+ d.stages[stageName].staleReason = reason;
174
+ d.stages[stageName].completedAt = null;
175
+ },
176
+ });
177
+ }
178
+ }
179
+ }
180
+
181
+ // Fix c: archive stale 缺 staleReason(专用文案)
182
+ if (stageName === 'archive' && sd.status === 'stale' && !sd.staleReason) {
183
+ // 已在 Fix a 中处理,这里不重复
184
+ }
185
+
186
+ // Fix d: revising stage 缺 reopenedAt → 补当前时间
187
+ if (sd.status === 'revising' && !sd.reopenedAt) {
188
+ fixable.push({
189
+ stage: stageName,
190
+ action: 'set_reopened_at',
191
+ description: `${stageName}: revising 缺 reopenedAt → 补当前时间`,
192
+ apply: (d) => { d.stages[stageName].reopenedAt = now; },
193
+ });
194
+ }
195
+
196
+ // Manual a: completed stage 里有 pending/stale/in-progress steps
197
+ if (sd.status === 'completed' && sd.steps) {
198
+ const badSteps = sd.steps.filter(s => ['pending', 'stale', 'in-progress'].includes(s.status));
199
+ for (const step of badSteps) {
200
+ manual.push(`${stageName}/${step.name}: step 状态为 ${step.status},但 stage 状态为 completed(需手动确认)`);
201
+ }
202
+ }
203
+
204
+ // Manual b: revising stage 缺 reopenedFromStep
205
+ if (sd.status === 'revising' && !sd.reopenedFromStep) {
206
+ manual.push(`${stageName}: revising 缺 reopenedFromStep(需手动确认修订起始步骤)`);
207
+ }
208
+
209
+ // Manual c: steps 为空但 stage completed
210
+ if (sd.status === 'completed' && (!sd.steps || sd.steps.length === 0)) {
211
+ manual.push(`${stageName}: completed 但 steps 为空(需手动确认)`);
212
+ }
213
+ }
214
+
215
+ // 输出报告
216
+ console.log('');
217
+ console.log(' ═══════════════════════════════════════');
218
+ console.log(` 状态修复 ${apply ? '(--apply 模式)' : '(dry-run 模式)'}`);
219
+ console.log(' ═══════════════════════════════════════');
220
+
221
+ if (fixable.length === 0 && manual.length === 0) {
222
+ console.log(' ✅ 未发现问题,无需修复');
223
+ console.log('');
224
+ return { fixable: [], manual: [], applied: [] };
225
+ }
226
+
227
+ const applied = [];
228
+
229
+ if (fixable.length > 0) {
230
+ console.log(`\n 🔧 可自动修复 (${fixable.length}):`);
231
+ for (const item of fixable) {
232
+ console.log(` - ${item.description}`);
233
+ if (apply) {
234
+ item.apply(data);
235
+ applied.push({ stage: item.stage, action: item.action });
236
+ }
237
+ }
238
+ if (!apply) {
239
+ console.log('\n 💡 使用 --apply 执行修复');
240
+ }
241
+ }
242
+
243
+ if (manual.length > 0) {
244
+ console.log(`\n 👆 需手动处理 (${manual.length}):`);
245
+ for (const m of manual) console.log(` - ${m}`);
246
+ }
247
+
248
+ if (apply && applied.length > 0) {
249
+ data.lastActive = now;
250
+ await this.pm._write(cwd, data, changeName);
251
+ console.log(`\n ✅ 已修复 ${applied.length} 项`);
252
+ }
253
+
254
+ console.log('');
255
+
256
+ return { fixable, manual, applied };
257
+ }
258
+ }
259
+
@@ -0,0 +1,33 @@
1
+ // W6 Step9 共享常量:progress.js facade 与 src/progress/* 子模块共用,避免 facade↔子模块循环引用。
2
+ // 9a: STAGE_ORDER/MAIN_FLOW_ORDER;9c: VALID_STAGES/STAGE_LABELS;9d: SPEC_DIR_NAME/CURRENT_VERSION/emptyStage。
3
+
4
+ export const SPEC_DIR_NAME = '.sillyspec';
5
+
6
+ // 合法阶段名(含辅助阶段 scan/quick/explore)
7
+ export const VALID_STAGES = ['scan', 'brainstorm', 'plan', 'execute', 'verify', 'archive', 'quick', 'explore'];
8
+
9
+ // 阶段展示文案
10
+ export const STAGE_LABELS = {
11
+ brainstorm: '🧠 需求探索',
12
+ plan: '📐 实现计划',
13
+ execute: '⚡ 波次执行',
14
+ verify: '🔍 验证确认',
15
+ scan: '🔍 代码扫描',
16
+ quick: '⚡ 快速任务',
17
+ explore: '🧭 自由探索',
18
+ archive: '📦 归档变更',
19
+ };
20
+
21
+ // 完整主流程顺序(含 scan),用于下游 cascade / 一致性检查
22
+ export const STAGE_ORDER = ['scan', 'brainstorm', 'plan', 'execute', 'verify', 'archive'];
23
+ // 主流程阶段(不含 scan/quick/explore 等辅助阶段)—— 当前与 STAGE_ORDER 同值
24
+ export const MAIN_FLOW_ORDER = STAGE_ORDER;
25
+
26
+ // progress 数据版本(v3 = SQLite)
27
+ export const CURRENT_VERSION = 3;
28
+
29
+ // 空阶段骨架
30
+ export function emptyStage() {
31
+ return { status: 'pending', steps: [], startedAt: null, completedAt: null };
32
+ }
33
+