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
package/src/progress.js CHANGED
@@ -11,12 +11,18 @@
11
11
  */
12
12
 
13
13
  import { existsSync, mkdirSync, readFileSync, writeFileSync, renameSync, unlinkSync, appendFileSync } from 'fs';
14
- import { join, basename, dirname, resolve } from 'path';
14
+ import { join, basename, dirname, resolve, sep } from 'path';
15
+ import { tmpdir } from 'os';
15
16
  import { DB } from './db.js';
16
17
  import { writeAtomicSync } from './fs-atomic.js';
18
+ import { ConsistencyDoctor } from './progress/consistency-doctor.js';
19
+ import { ChangeRegistry } from './progress/change-registry.js';
20
+ import { StepStore } from './progress/step-store.js';
21
+ import { StageMachine } from './progress/stage-machine.js';
22
+ import { STAGE_ORDER, MAIN_FLOW_ORDER, VALID_STAGES, STAGE_LABELS, SPEC_DIR_NAME, CURRENT_VERSION, emptyStage } from './progress/shared.js';
17
23
 
18
24
  // 默认规范目录名(相对于 cwd)
19
- const SPEC_DIR_NAME = '.sillyspec';
25
+ // SPEC_DIR_NAME → ./progress/shared.js(W6 Step9d)
20
26
  const RUNTIME_SUBDIR = '.runtime';
21
27
 
22
28
  /**
@@ -99,37 +105,33 @@ export function resolvePlatformSpecDir(cwd, explicitSpecDir = null) {
99
105
  hint: `启动 SillyHub daemon;或 sillyspec doctor --json 诊断;或显式 --spec-dir <本地路径> 临时走本地`,
100
106
  });
101
107
  }
108
+ // 驾驭:pointer 指向 OS temp 目录几乎肯定是陈旧测试/CI 残留(合法项目不会把 specRoot 放 temp)。
109
+ // 历史:npm test 等会写 pointer 到 Temp/spec-dir-test-*,不清理则污染真实环境,所有命令静默落错库。
110
+ // 非阻断警告:让 agent 知道当前解析到的 specDir 可疑,而非静默写到死库。
111
+ const resolvedTmp = resolve(tmpdir());
112
+ if (resolve(ptr.specRoot) === resolvedTmp || resolve(ptr.specRoot).startsWith(resolvedTmp + sep)) {
113
+ console.warn(
114
+ `⚠️ 平台 pointer 的 specRoot 指向系统 temp 目录:${ptr.specRoot}\n` +
115
+ ` 这通常是陈旧的测试/CI 残留,命令会写进这个可疑库而非你当前项目。\n` +
116
+ ` 修复:sillyspec platform pointer --cleanup(删除 ${pointerPath}),或删除该 pointer 文件后回到项目目录重跑。`
117
+ );
118
+ }
102
119
  return ptr.specRoot;
103
120
  }
104
121
 
105
122
  const CHANGES_SUBDIR = 'changes';
106
- const CURRENT_VERSION = 3;
107
- const VALID_STAGES = ['scan', 'brainstorm', 'plan', 'execute', 'verify', 'archive', 'quick', 'explore'];
108
- const VALID_STATUSES = ['pending', 'in-progress', 'completed', 'failed', 'blocked', 'waiting', 'stale'];
123
+ // CURRENT_VERSION → ./progress/shared.js(W6 Step9d)
124
+ // VALID_STAGES → ./progress/shared.js(W6 Step9c)
125
+ // VALID_STATUSES → ./progress/step-store.js(updateStep 专用,本地化)
109
126
 
110
127
  // Stage statuses (superset of step statuses)
111
128
  const VALID_STAGE_STATUSES = ['pending', 'in-progress', 'completed', 'failed', 'blocked', 'revising', 'stale'];
112
129
 
113
- // Main flow stage order (for downstream cascade)
114
- // 完整主流程顺序(含 scan),用于下游 cascade
115
- const STAGE_ORDER = ['scan', 'brainstorm', 'plan', 'execute', 'verify', 'archive'];
116
- // 主流程阶段(不含 scan/quick/explore 等辅助阶段)
117
- const MAIN_FLOW_ORDER = STAGE_ORDER;
118
-
119
- const STAGE_LABELS = {
120
- brainstorm: '🧠 需求探索',
121
- plan: '📐 实现计划',
122
- execute: '⚡ 波次执行',
123
- verify: '🔍 验证确认',
124
- scan: '🔍 代码扫描',
125
- quick: '⚡ 快速任务',
126
- explore: '🧭 自由探索',
127
- archive: '📦 归档变更',
128
- };
129
-
130
- function emptyStage() {
131
- return { status: 'pending', steps: [], startedAt: null, completedAt: null };
132
- }
130
+ // STAGE_ORDER / MAIN_FLOW_ORDER → ./progress/shared.js(W6 Step9a:facade + 子模块共用)
131
+
132
+ // STAGE_LABELS → ./progress/shared.js(W6 Step9c)
133
+
134
+ // emptyStage → ./progress/shared.js(W6 Step9d)
133
135
 
134
136
  function makeInitialProgress(project) {
135
137
  const stages = {};
@@ -150,6 +152,10 @@ export class ProgressManager {
150
152
  */
151
153
  constructor(opts = {}) {
152
154
  this._customSpecDir = opts.specDir || null;
155
+ this._consistency = new ConsistencyDoctor(this);
156
+ this._changeRegistry = new ChangeRegistry(this);
157
+ this._stepStore = new StepStore(this);
158
+ this._stageMachine = new StageMachine(this);
153
159
  }
154
160
 
155
161
  // ── 路径工具 ──
@@ -471,211 +477,21 @@ export class ProgressManager {
471
477
 
472
478
  // ── 变更管理 ──
473
479
 
474
- /**
475
- * 列出所有活跃变更名
476
- * SQL: SELECT name FROM changes WHERE status = 'active'
477
- */
478
- async listChanges(cwd) {
479
- const db = await this._ensureDB(cwd);
480
- const sqlDb = db.getDb();
481
- const rows = sqlDb.exec("SELECT name FROM changes WHERE status = 'active' ORDER BY name");
482
- if (!rows || rows.length === 0) return [];
483
- return rows[0].values.map(r => r[0]);
484
- }
480
+ async listChanges(cwd) { return this._changeRegistry.listChanges(cwd); }
485
481
 
486
- /**
487
- * 注册变更到活跃列表
488
- * SQL: INSERT OR IGNORE → 若已 archived 则 UPDATE status='active'
489
- */
490
- async registerChange(cwd, changeName) {
491
- if (!changeName) {
492
- console.warn('⚠️ registerChange: changeName 为空,跳过');
493
- return;
494
- }
495
- const db = await this._ensureDB(cwd);
496
- db.transaction((sqlDb) => {
497
- const now = new Date().toISOString();
498
- // 尝试插入新行
499
- sqlDb.run(
500
- `INSERT OR IGNORE INTO changes (name, created_at, last_active)
501
- VALUES (?, ?, ?)`,
502
- [changeName, now, now]
503
- );
504
- // 注意:不复活已归档的变更——归档是不可逆操作
505
- // 如果变更已存在且为 archived,保持 archived 状态不变
506
- });
507
- }
482
+ async registerChange(cwd, changeName) { return this._changeRegistry.registerChange(cwd, changeName); }
508
483
 
509
- /**
510
- * 更新变更的隔离状态
511
- * @param {string} cwd - 项目根目录
512
- * @param {string} changeName - 变更名
513
- * @param {{ status: string, mode?: string, reason?: string }} isolation
514
- */
515
- async updateChangeIsolation(cwd, changeName, isolation) {
516
- const db = await this._ensureDB(cwd);
517
- const sqlDb = db.getDb();
518
- try {
519
- sqlDb.run(
520
- `UPDATE changes SET isolation_status = ?, isolation_mode = ?, isolation_reason = ?, last_active = ? WHERE name = ?`,
521
- [isolation.status, isolation.mode || null, isolation.reason || null, new Date().toISOString(), changeName]
522
- );
523
- db._save();
524
- } catch (err) {
525
- console.warn('⚠️ 更新 isolation 状态失败:', err.message);
526
- }
527
- }
484
+ async updateChangeIsolation(cwd, changeName, isolation) { return this._changeRegistry.updateChangeIsolation(cwd, changeName, isolation); }
528
485
 
529
- /**
530
- * 读取变更的隔离状态
531
- * @param {string} cwd - 项目根目录
532
- * @param {string} changeName - 变更名
533
- * @returns {{ status: string|null, mode: string|null, reason: string|null }|null}
534
- */
535
- async readChangeIsolation(cwd, changeName) {
536
- const db = await this._ensureDB(cwd);
537
- const sqlDb = db.getDb();
538
- try {
539
- const rows = sqlDb.exec(
540
- `SELECT isolation_status, isolation_mode, isolation_reason FROM changes WHERE name = ?`,
541
- [changeName]
542
- );
543
- if (!rows || rows.length === 0 || rows[0].values.length === 0) return null;
544
- const [status, mode, reason] = rows[0].values[0];
545
- return { status: status || null, mode: mode || null, reason: reason || null };
546
- } catch {
547
- return null;
548
- }
549
- }
486
+ async readChangeIsolation(cwd, changeName) { return this._changeRegistry.readChangeIsolation(cwd, changeName); }
550
487
 
551
- async _updatePlatformLastSync(cwd, changeName) {
552
- if (!changeName) return;
553
- const db = await this._ensureDB(cwd);
554
- db.transaction((sqlDb) => {
555
- sqlDb.run(
556
- 'UPDATE changes SET platform_last_sync = ?, platform_sync_enabled = 1 WHERE name = ?',
557
- [new Date().toISOString(), changeName]
558
- );
559
- });
560
- }
488
+ async _updatePlatformLastSync(cwd, changeName) { return this._changeRegistry._updatePlatformLastSync(cwd, changeName); }
561
489
 
562
- async _updateApprovalStatus(cwd, changeName, status, reason = null) {
563
- if (!changeName || !status) return;
564
- const db = await this._ensureDB(cwd);
565
- db.transaction((sqlDb) => {
566
- const rows = sqlDb.exec('SELECT id FROM changes WHERE name = ?', [changeName]);
567
- if (!rows || rows.length === 0 || rows[0].values.length === 0) return;
568
- const changeId = rows[0].values[0][0];
569
- const now = new Date().toISOString();
570
- sqlDb.run(
571
- `INSERT INTO approvals (change_id, status, requested_at, approved_at, rejection_reason)
572
- VALUES (?, ?, ?, ?, ?)
573
- ON CONFLICT(change_id) DO UPDATE SET
574
- status = excluded.status,
575
- approved_at = excluded.approved_at,
576
- rejection_reason = excluded.rejection_reason`,
577
- [
578
- changeId,
579
- status,
580
- now,
581
- status === 'approved' ? now : null,
582
- status === 'rejected' ? reason : null,
583
- ]
584
- );
585
- });
586
- }
490
+ async _updateApprovalStatus(cwd, changeName, status, reason = null) { return this._changeRegistry._updateApprovalStatus(cwd, changeName, status, reason); }
587
491
 
588
- /**
589
- * 重命名变更:同步更新 DB + 目录
590
- * @param {string} cwd - 项目根目录
591
- * @param {string} oldName - 旧变更名
592
- * @param {string} newName - 新变更名
593
- */
594
- async renameChange(cwd, oldName, newName) {
595
- if (!oldName || !newName) {
596
- console.warn('⚠️ renameChange: 旧名或新名为空,跳过');
597
- return;
598
- }
599
- if (oldName === newName) {
600
- console.warn('⚠️ renameChange: 新旧名称相同,跳过');
601
- return;
602
- }
603
- const db = await this._ensureDB(cwd);
604
- // 检查旧名是否存在
605
- const existing = db.transaction((sqlDb) => {
606
- const row = sqlDb.exec(`SELECT name, status FROM changes WHERE name = ?`, [oldName]);
607
- if (!row || !row[0] || row[0].values.length === 0) return null;
608
- return { name: row[0].values[0][0], status: row[0].values[0][1] };
609
- });
610
- if (!existing) {
611
- console.error(`❌ 变更 ${oldName} 不存在`);
612
- return;
613
- }
614
- // 检查新名是否已存在
615
- const conflict = db.transaction((sqlDb) => {
616
- const row = sqlDb.exec(`SELECT name FROM changes WHERE name = ?`, [newName]);
617
- return row && row[0] && row[0].values.length > 0;
618
- });
619
- if (conflict) {
620
- console.error(`❌ 变更 ${newName} 已存在`);
621
- return;
622
- }
623
- // 先更新 DB,再重命名目录;FS 失败则回滚 DB,避免"目录已改名但 DB 旧名"的孤儿
624
- // (旧实现 FS-first 无补偿:renameSync 成功后 DB transaction 抛 EPERM/EBUSY 会让
625
- // 目录已是 newName、DB 仍是 oldName,read(两名) 都失联且无自动恢复)。
626
- const oldDir = this._changePath(cwd, oldName);
627
- const newDir = this._changePath(cwd, newName);
628
- const now = new Date().toISOString();
629
- try {
630
- db.transaction((sqlDb) => {
631
- sqlDb.run(`UPDATE changes SET name = ?, last_active = ? WHERE name = ?`, [newName, now, oldName]);
632
- });
633
- } catch (e) {
634
- console.error(`❌ 重命名失败:更新数据库时出错(${e.message})`);
635
- return;
636
- }
637
- let renamed = true;
638
- if (existsSync(oldDir)) {
639
- try {
640
- renameSync(oldDir, newDir);
641
- } catch (e) {
642
- renamed = false;
643
- // FS 重命名失败:回滚 DB 恢复 oldName,保持 DB 与目录一致
644
- try {
645
- db.transaction((sqlDb) => {
646
- sqlDb.run(`UPDATE changes SET name = ?, last_active = ? WHERE name = ?`, [oldName, now, newName]);
647
- });
648
- console.error(`❌ 重命名失败:移动目录出错(${e.message}),已回滚数据库`);
649
- } catch (rollbackErr) {
650
- // 回滚本身也失败:DB 是 newName、目录是 oldName,两端分裂且无自动恢复。
651
- // 不能再撒谎"已回滚"——必须显式报错让用户跑 doctor 修复。
652
- console.error(`❌ 重命名失败且数据库回滚也失败(状态分裂,需手动修复):原错=${e.message} 回滚错=${rollbackErr.message}。请跑 sillyspec doctor --json`);
653
- }
654
- }
655
- } else {
656
- mkdirSync(newDir, { recursive: true });
657
- }
658
- if (renamed) console.log(`✅ 变更已重命名:${oldName} → ${newName}`);
659
- }
492
+ async renameChange(cwd, oldName, newName) { return this._changeRegistry.renameChange(cwd, oldName, newName); }
660
493
 
661
- /**
662
- * 从活跃列表移除变更(归档时调用,不物理删除)
663
- * SQL: UPDATE changes SET status = 'archived'
664
- */
665
- async unregisterChange(cwd, changeName) {
666
- if (!changeName) {
667
- console.warn('⚠️ unregisterChange: changeName 为空,跳过');
668
- return;
669
- }
670
- const db = await this._ensureDB(cwd);
671
- db.transaction((sqlDb) => {
672
- const now = new Date().toISOString();
673
- sqlDb.run(
674
- `UPDATE changes SET status = 'archived', last_active = ? WHERE name = ?`,
675
- [now, changeName]
676
- );
677
- });
678
- }
494
+ async unregisterChange(cwd, changeName) { return this._changeRegistry.unregisterChange(cwd, changeName); }
679
495
 
680
496
  // ── CLI 命令 ──
681
497
 
@@ -761,927 +577,37 @@ export class ProgressManager {
761
577
  return await this.read(cwd, changeName);
762
578
  }
763
579
 
764
- async setStage(cwd, stage, changeName = null) {
765
- if (!VALID_STAGES.includes(stage)) {
766
- console.log(`❌ 未知阶段: ${stage},可选: ${VALID_STAGES.join(', ')}`);
767
- return;
768
- }
769
-
770
- const db = await this._ensureDB(cwd);
771
- const now = new Date().toISOString();
772
-
773
- // 获取变更名
774
- let cn = changeName;
775
- if (!cn) {
776
- const changes = await this.listChanges(cwd);
777
- if (changes.length === 1) cn = changes[0];
778
- if (!cn) { console.log('❌ 无法确定当前变更,请指定 --change <name>'); return; }
779
- }
780
-
781
- db.transaction((sqlDb) => {
782
- // 确保 change 存在
783
- const changeRow = sqlDb.exec('SELECT id, current_stage FROM changes WHERE name = ?', [cn]);
784
- if (!changeRow || changeRow.length === 0 || changeRow[0].values.length === 0) return;
785
-
786
- const changeId = changeRow[0].values[0][0];
787
-
788
- // UPDATE changes.current_stage + last_active
789
- sqlDb.run('UPDATE changes SET current_stage = ?, last_active = ? WHERE name = ?', [stage, now, cn]);
790
-
791
- // 确保 stages 行存在(INSERT OR IGNORE)
792
- sqlDb.run(
793
- 'INSERT OR IGNORE INTO stages (change_id, stage, status) VALUES (?, ?, "pending")',
794
- [changeId, stage]
795
- );
796
-
797
- // UPDATE stages.status 为 in-progress(仅当仍为 pending 时)
798
- sqlDb.run(
799
- "UPDATE stages SET status = 'in-progress', started_at = ? WHERE change_id = ? AND stage = ? AND status = 'pending'",
800
- [now, changeId, stage]
801
- );
802
- });
803
-
804
- // read() 已改为 SQL,直接通过 SQL 查询即可,无需 _write
805
- console.log(`✅ 当前阶段已设为: ${STAGE_LABELS[stage] || stage}`);
806
- }
807
-
808
- async addStep(cwd, stage, stepName, changeName = null) {
809
- if (!stepName) { console.log('❌ 请指定步骤名称'); return; }
810
-
811
- const db = await this._ensureDB(cwd);
812
-
813
- // 获取变更名
814
- let cn = changeName;
815
- if (!cn) {
816
- const changes = await this.listChanges(cwd);
817
- if (changes.length === 1) cn = changes[0];
818
- if (!cn) { console.log('❌ 无法确定当前变更,请指定 --change <name>'); return; }
819
- }
820
-
821
- // 查找 stage_id
822
- const sqlDb = db.getDb();
823
- const stageRow = sqlDb.exec(
824
- 'SELECT s.id FROM stages s JOIN changes c ON s.change_id = c.id WHERE c.name = ? AND s.stage = ?',
825
- [cn, stage]
826
- );
827
- if (!stageRow || stageRow.length === 0 || stageRow[0].values.length === 0) {
828
- // stages 行不存在,静默跳过
829
- console.log(`ℹ️ 阶段 ${stage} 不存在`);
830
- return;
831
- }
832
- const stageId = stageRow[0].values[0][0];
833
-
834
- // 重复步骤名检查
835
- const dupRow = sqlDb.exec('SELECT id FROM steps WHERE stage_id = ? AND name = ?', [stageId, stepName]);
836
- if (dupRow && dupRow.length > 0 && dupRow[0].values.length > 0) {
837
- console.log(`ℹ️ 步骤 "${stepName}" 已存在于 ${stage}`);
838
- return;
839
- }
840
-
841
- // INSERT INTO steps(ordering 递增)
842
- db.transaction((tDb) => {
843
- tDb.run(
844
- `INSERT INTO steps (stage_id, name, ordering, status)
845
- VALUES (?, ?, (SELECT COALESCE(MAX(ordering), 0) + 1 FROM steps WHERE stage_id = ?), 'pending')`,
846
- [stageId, stepName, stageId]
847
- );
848
- tDb.run('UPDATE changes SET last_active = ? WHERE name = ?', [new Date().toISOString(), cn]);
849
- });
850
-
851
- console.log(`✅ 已添加步骤: ${stage}/${stepName}`);
852
- }
853
-
854
- async updateStep(cwd, stage, stepName, options = {}, changeName = null) {
855
- const { status, output } = options;
856
- if (!stepName) { console.log('❌ 请指定步骤名称'); return; }
857
-
858
- const db = await this._ensureDB(cwd);
859
-
860
- // 获取变更名
861
- let cn = changeName;
862
- if (!cn) {
863
- const changes = await this.listChanges(cwd);
864
- if (changes.length === 1) cn = changes[0];
865
- if (!cn) { console.log('❌ 无法确定当前变更,请指定 --change <name>'); return; }
866
- }
867
-
868
- // 状态校验
869
- if (status && !VALID_STATUSES.includes(status)) {
870
- console.log(`❌ 无效状态: ${status},可选: ${VALID_STATUSES.join(', ')}`);
871
- return;
872
- }
873
-
874
- // 查找 step_id:通过 changes → stages → steps JOIN 查询
875
- const sqlDb = db.getDb();
876
- const stepRow = sqlDb.exec(
877
- `SELECT st.id, st.status FROM steps st
878
- JOIN stages sg ON st.stage_id = sg.id
879
- JOIN changes c ON sg.change_id = c.id
880
- WHERE c.name = ? AND sg.stage = ? AND st.name = ?`,
881
- [cn, stage, stepName]
882
- );
883
- if (!stepRow || stepRow.length === 0 || stepRow[0].values.length === 0) {
884
- console.log(`❌ 步骤不存在: ${stage}/${stepName}`);
885
- return;
886
- }
887
- const stepId = stepRow[0].values[0][0];
888
-
889
- // UPDATE steps
890
- let stageCompletionCandidateId = null;
891
- db.transaction((tDb) => {
892
- const now = new Date().toISOString();
893
- if (status) {
894
- tDb.run('UPDATE steps SET status = ?, completed_at = ? WHERE id = ? AND name = ?', [status, now, stepId, stepName]);
895
- }
896
- if (output !== undefined) {
897
- tDb.run('UPDATE steps SET output = ? WHERE id = ? AND name = ?', [output, stepId, stepName]);
898
- }
899
-
900
- // 自动完成检测:同 stage_id 下所有 steps 都 completed 时,候选标记 stage completed
901
- // (实际标记延后到事务外,先过产物校验门,防止 update-step 成为绕过 validator 的后门)
902
- if (status === 'completed') {
903
- // 获取 stage_id
904
- const stRow = tDb.exec('SELECT stage_id FROM steps WHERE id = ?', [stepId]);
905
- if (stRow && stRow.length > 0 && stRow[0].values.length > 0) {
906
- const stId = stRow[0].values[0][0];
907
- const pendingRows = tDb.exec('SELECT COUNT(*) FROM steps WHERE stage_id = ? AND status != "completed"', [stId]);
908
- if (pendingRows && pendingRows.length > 0 && pendingRows[0].values[0][0] === 0) {
909
- stageCompletionCandidateId = stId;
910
- }
911
- }
912
- }
913
-
914
- tDb.run('UPDATE changes SET last_active = ? WHERE name = ?', [now, cn]);
915
- });
916
-
917
- if (stageCompletionCandidateId !== null) {
918
- const { force = false } = options;
919
- const validation = await this._validateStageArtifacts(cwd, stage, cn);
920
- if (!validation.ok && !force) {
921
- console.error(`⚠️ 阶段 ${stage} 所有步骤已完成,但产物校验未通过,阶段不标记为 completed:`);
922
- for (const err of validation.errors) console.error(` - ${err}`);
923
- console.error(` 请修复产物后走正常流程 sillyspec run ${stage} --done,或使用 --force(将记录审计日志)。`);
924
- } else {
925
- if (!validation.ok && force) {
926
- console.warn(`⚠️ --force 强制标记阶段 ${stage} completed(校验未通过,已记录审计日志)`);
927
- this._appendAuditLog(cwd, {
928
- action: 'update-step --force (stage auto-complete)',
929
- stage,
930
- change: cn,
931
- validationErrors: validation.errors,
932
- });
933
- }
934
- db.transaction((tDb) => {
935
- tDb.run('UPDATE stages SET status = "completed", completed_at = ? WHERE id = ?', [new Date().toISOString(), stageCompletionCandidateId]);
936
- });
937
- console.log(`✅ 阶段 ${stage} 所有步骤已完成,阶段已标记为 completed`);
938
- }
939
- }
940
-
941
- console.log(`✅ 步骤已更新: ${stage}/${stepName} → ${status || '(仅更新 output)'}`);
942
- }
943
-
944
- /**
945
- * 强制状态变更的审计记录(--force 逃生口专用)。
946
- * 追加到 .runtime/audit.log,供人工/doctor 追溯"谁在什么时候绕过了校验"。
947
- */
948
- _appendAuditLog(cwd, entry) {
949
- try {
950
- this._ensureRuntimeDir(cwd);
951
- const auditPath = this._runtimePath(cwd, 'audit.log');
952
- const line = JSON.stringify({ at: new Date().toISOString(), ...entry });
953
- appendFileSync(auditPath, line + '\n');
954
- } catch (e) {
955
- console.warn(`⚠️ 审计日志写入失败: ${e.message}`);
956
- }
957
- }
958
-
959
- /**
960
- * 阶段产物校验门:progress complete-stage / update-step 自动完成时复用
961
- * stage-contract 的 validator,防止零产物阶段被直接标 completed。
962
- */
963
- async _validateStageArtifacts(cwd, stage, changeName) {
964
- const { runValidators } = await import('./stage-contract.js');
965
- const specDir = this._getSpecDir(cwd);
966
- const defaultSpec = join(resolve(cwd), SPEC_DIR_NAME);
967
- const specRoot = resolve(specDir) === defaultSpec ? null : specDir;
968
- let projectName = null;
969
- try {
970
- const g = await this.readGlobal(cwd);
971
- projectName = g?.project || null;
972
- } catch {}
973
- return runValidators(stage, cwd, changeName, {
974
- projectName: projectName || basename(resolve(cwd)),
975
- specRoot,
976
- });
977
- }
978
-
979
- async completeStage(cwd, stage, changeName = null, opts = {}) {
980
- const { force = false } = opts;
981
- if (!VALID_STAGES.includes(stage)) {
982
- console.log(`❌ 未知阶段: ${stage}`);
983
- return;
984
- }
985
-
986
- const db = await this._ensureDB(cwd);
987
- const now = new Date().toISOString();
988
-
989
- // 获取变更名
990
- let cn = changeName;
991
- if (!cn) {
992
- const changes = await this.listChanges(cwd);
993
- if (changes.length === 1) cn = changes[0];
994
- if (!cn) { console.log('❌ 无法确定当前变更,请指定 --change <name>'); return; }
995
- }
996
-
997
- // ── 产物校验门:complete-stage 不再是零校验后门 ──
998
- // 与 run.js completeStep 的 validator 同源;--force 为显式逃生口(留审计)。
999
- const validation = await this._validateStageArtifacts(cwd, stage, cn);
1000
- if (!validation.ok) {
1001
- if (!force) {
1002
- console.error(`❌ complete-stage 被拒绝:阶段 ${stage} 产物校验未通过`);
1003
- for (const err of validation.errors) console.error(` - ${err}`);
1004
- console.error(` 请修复产物后重试,或走正常流程 sillyspec run ${stage} --done。`);
1005
- console.error(` 确需强制标记(如 doctor 修复),使用 --force(将记录审计日志)。`);
1006
- return;
1007
- }
1008
- console.warn(`⚠️ --force 强制完成阶段 ${stage}(校验未通过,已记录审计日志)`);
1009
- for (const err of validation.errors) console.warn(` - ${err}`);
1010
- this._appendAuditLog(cwd, {
1011
- action: 'complete-stage --force',
1012
- stage,
1013
- change: cn,
1014
- validationErrors: validation.errors,
1015
- });
1016
- } else if (force) {
1017
- // 校验通过但仍显式 --force:也留一条审计记录,保持行为可追溯
1018
- this._appendAuditLog(cwd, { action: 'complete-stage --force', stage, change: cn, validationErrors: [] });
1019
- }
1020
-
1021
- db.transaction((sqlDb) => {
1022
- const changeRow = sqlDb.exec('SELECT id FROM changes WHERE name = ?', [cn]);
1023
- if (!changeRow || changeRow.length === 0 || changeRow[0].values.length === 0) return;
1024
- const changeId = changeRow[0].values[0][0];
1025
-
1026
- // 确保 stages 行存在(阶段不存在时自动创建)
1027
- sqlDb.run(
1028
- 'INSERT OR IGNORE INTO stages (change_id, stage, status) VALUES (?, ?, "pending")',
1029
- [changeId, stage]
1030
- );
1031
-
1032
- // UPDATE stages.status=completed + completed_at
1033
- sqlDb.run(
1034
- 'UPDATE stages SET status = "completed", completed_at = ? WHERE change_id = ? AND stage = ?',
1035
- [now, changeId, stage]
1036
- );
1037
-
1038
- // 将该阶段所有 pending 步骤标记为 completed
1039
- const stageRow = sqlDb.exec('SELECT id FROM stages WHERE change_id = ? AND stage = ?', [changeId, stage]);
1040
- if (stageRow && stageRow.length > 0 && stageRow[0].values.length > 0) {
1041
- const stageId = stageRow[0].values[0][0];
1042
- sqlDb.run(
1043
- 'UPDATE steps SET status = "completed", completed_at = ? WHERE stage_id = ? AND status = "pending"',
1044
- [now, stageId]
1045
- );
1046
- }
1047
-
1048
- // UPDATE changes.last_active
1049
- sqlDb.run('UPDATE changes SET last_active = ? WHERE name = ?', [now, cn]);
1050
- });
1051
-
1052
- // 写 history 文件(保持文件系统,不变)
1053
- const data = await this.read(cwd, cn);
1054
- if (data && data.stages && data.stages[stage]) {
1055
- const historyDir = this._runtimePath(cwd, 'history');
1056
- mkdirSync(historyDir, { recursive: true });
1057
- const ts = now.replace(/[:.TZ-]/g, '');
1058
- const stageData = data.stages[stage];
1059
- writeAtomicSync(
1060
- join(historyDir, `${cn}-${stage}-${ts}.json`),
1061
- JSON.stringify({ change: cn, stage, data: stageData, completedAt: now }, null, 2) + '\n'
1062
- );
1063
- }
1064
-
1065
- console.log(`✅ 阶段 ${stage} 已标记为完成(不自动推进,下一步由你决定)`);
1066
- }
1067
-
1068
- async show(cwd, changeName = null) {
1069
- // 如果指定了变更名,只显示该变更
1070
- if (changeName) {
1071
- return await this._showChange(cwd, changeName);
1072
- }
1073
-
1074
- // 否则显示所有变更
1075
- const changes = await this.listChanges(cwd);
1076
- if (changes.length === 0) {
1077
- console.log('ℹ️ 没有活跃的变更');
1078
- return;
1079
- }
1080
-
1081
- if (changes.length === 1) {
1082
- return await this._showChange(cwd, changes[0]);
1083
- }
1084
-
1085
- // 多个变更:汇总显示
1086
- const global = await this.readGlobal(cwd);
1087
- console.log('');
1088
- console.log(' ═══════════════════════════════════════');
1089
- console.log(` 项目: ${(global?.project) || basename(cwd) || '(未命名)'}`);
1090
- console.log(` 活跃变更: ${changes.length} 个`);
1091
- console.log(' ═══════════════════════════════════════');
1092
- console.log('');
1093
-
1094
- for (const cn of changes) {
1095
- const data = await this.read(cwd, cn);
1096
- if (!data) {
1097
- console.log(` 📂 ${cn} — (无法读取)`);
1098
- continue;
1099
- }
1100
- const currentStage = data.currentStage || '(无)';
1101
- const stageLabel = STAGE_LABELS[data.currentStage] || currentStage;
1102
- const lastActive = data.lastActive ? this._timeAgo(data.lastActive) : '未知';
1103
-
1104
- console.log(` 📂 ${cn}`);
1105
- console.log(` 当前阶段: ${stageLabel} 最近活跃: ${lastActive}`);
1106
- console.log('');
1107
- }
1108
-
1109
- console.log(` 💡 查看详情:sillyspec progress show --change <name>`);
1110
- console.log('');
1111
- }
1112
-
1113
- async _showChange(cwd, changeName) {
1114
- const data = await this.read(cwd, changeName);
1115
- if (!data) {
1116
- console.log(`❌ 未找到变更 ${changeName}`);
1117
- return;
1118
- }
1119
-
1120
- console.log('');
1121
- console.log(' ═══════════════════════════════════════');
1122
- console.log(` 变更: ${changeName}`);
1123
- console.log(` 项目: ${data.project || '(未命名)'}`);
1124
- console.log(` 当前阶段: ${STAGE_LABELS[data.currentStage] || data.currentStage || '(无)'}`);
1125
- console.log(` 最近活跃: ${data.lastActive ? this._timeAgo(data.lastActive) : '未知'}`);
1126
- console.log(' ═══════════════════════════════════════');
1127
- console.log('');
1128
-
1129
- const statusIcons = { pending: '⬜', 'in-progress': '🔵', completed: '✅', failed: '❌', blocked: '🚫', waiting: '⏸️', revising: '🔧', stale: '⚠️' };
1130
-
1131
- for (const stage of VALID_STAGES) {
1132
- const stageData = data.stages[stage] || emptyStage();
1133
- const label = STAGE_LABELS[stage] || stage;
1134
- const icon = statusIcons[stageData.status] || '⬜';
1135
- const isCurrent = data.currentStage === stage ? ' ◀' : '';
1136
-
1137
- console.log(` ${icon} ${label}${isCurrent}`);
1138
-
1139
- // Show revision info
1140
- if (stageData.revision && stageData.revision > 0) {
1141
- console.log(` 📋 revision: ${stageData.revision}${stageData.reopenedFromStep ? `, from step: ${stageData.reopenedFromStep}` : ''}`);
1142
- }
1143
- if (stageData.staleReason) {
1144
- console.log(` ⚠️ stale: ${stageData.staleReason}`);
1145
- if (stage === 'archive') {
1146
- console.log(` 📁 已有归档文件仍保留在磁盘上,但不再可信。`);
1147
- }
1148
- }
1149
-
1150
- if (stageData.steps && stageData.steps.length > 0) {
1151
- for (const step of stageData.steps) {
1152
- const si = statusIcons[step.status] || '○';
1153
- const out = step.output ? ` — ${step.output.slice(0, 60)}` : '';
1154
- const waitingTag = step.status === 'waiting' ? ' [WAITING]' : ''
1155
- console.log(` ${si} ${step.name}${out}${waitingTag}`);
1156
- if (step.status === 'waiting') {
1157
- if (step.waitReason) console.log(` 原因:${step.waitReason}`);
1158
- if (step.waitOptions) console.log(` 选项:${(() => { try { const p = JSON.parse(step.waitOptions); return Array.isArray(p) ? p.join(', ') : step.waitOptions; } catch { return step.waitOptions; }})()}`);
1159
- if (step.waitedAt) console.log(` 等待时间:${step.waitedAt}`);
1160
- }
1161
- }
1162
- }
1163
-
1164
- if (stageData.startedAt) {
1165
- console.log(` 开始: ${new Date(stageData.startedAt).toLocaleString('zh-CN')}`);
1166
- }
1167
- if (stageData.completedAt) {
1168
- console.log(` 完成: ${new Date(stageData.completedAt).toLocaleString('zh-CN')}`);
1169
- }
1170
- }
1171
-
1172
- // 批量进度
1173
- if (data.batchProgress) {
1174
- const batchLine = this._renderBatchProgress(data.batchProgress);
1175
- if (batchLine) {
1176
- console.log('');
1177
- console.log(` ${batchLine}`);
1178
- }
1179
- }
1180
-
1181
- // ── Next 建议 ──
1182
- const suggestion = this._getNextSuggestion(data);
1183
- if (suggestion) {
1184
- console.log('');
1185
- console.log(` 💡 ${suggestion.text}`);
1186
- if (suggestion.command) console.log(` ${suggestion.command}`);
1187
- }
1188
-
1189
- console.log('');
1190
- }
1191
-
1192
- /**
1193
- * 根据当前状态给出下一步建议
1194
- * @param {object} data - progress data
1195
- * @returns {{ text: string, command?: string }|null}
1196
- */
1197
- _getNextSuggestion(data) {
1198
- // 找到第一个 revising 阶段
1199
- const revisingStage = STAGE_ORDER.find(s => data.stages[s]?.status === 'revising');
1200
- if (revisingStage) {
1201
- const sd = data.stages[revisingStage];
1202
- return {
1203
- text: `${STAGE_LABELS[revisingStage] || revisingStage} 正在修订中(revision ${sd.revision || 1}),请继续完成修订。`,
1204
- command: `sillyspec run ${revisingStage}`,
1205
- };
1206
- }
1207
-
1208
- // 找到第一个 stale 阶段(上游已修,下游需要重建)
1209
- const staleStage = STAGE_ORDER.find(s => data.stages[s]?.status === 'stale');
1210
- if (staleStage) {
1211
- const sd = data.stages[staleStage];
1212
- return {
1213
- text: `${STAGE_LABELS[staleStage] || staleStage} 已失效(${sd.staleReason || '上游修订'}),需要从第一步重建。`,
1214
- command: `sillyspec run ${staleStage} --reopen --from-step 1`,
1215
- };
1216
- }
1217
-
1218
- // 找到第一个有 pending/waiting/failed 步骤的 in-progress 阶段
1219
- for (const s of STAGE_ORDER) {
1220
- const sd = data.stages[s];
1221
- if (!sd) continue;
1222
- if (sd.status === 'in-progress' && sd.steps) {
1223
- const hasPending = sd.steps.some(st => ['pending', 'waiting', 'failed'].includes(st.status));
1224
- if (hasPending) {
1225
- return {
1226
- text: `${STAGE_LABELS[s] || s} 进行中,继续执行下一步。`,
1227
- command: `sillyspec run ${s}`,
1228
- };
1229
- }
1230
- }
1231
- }
1232
-
1233
- // 找到第一个 pending 主流程阶段
1234
- for (const s of STAGE_ORDER) {
1235
- const sd = data.stages[s];
1236
- if (sd && sd.status === 'pending' && sd.steps && sd.steps.length > 0) {
1237
- // 检查上游是否都 completed
1238
- const idx = STAGE_ORDER.indexOf(s);
1239
- const upstream = STAGE_ORDER.slice(0, idx);
1240
- const upstreamOk = upstream.every(us =>
1241
- data.stages[us]?.status === 'completed' || !data.stages[us] || data.stages[us].status === 'pending'
1242
- );
1243
- if (upstreamOk) {
1244
- return {
1245
- text: `可以开始 ${STAGE_LABELS[s] || s}。`,
1246
- command: `sillyspec run ${s}`,
1247
- };
1248
- }
1249
- }
1250
- }
1251
-
1252
- return null;
1253
- }
1254
-
1255
- async status(cwd, changeName = null) {
1256
- await this.show(cwd, changeName);
1257
- }
1258
-
1259
- /**
1260
- * Revision v1 状态一致性检查
1261
- * 只报告,不自动修复。
1262
- * @param {string} cwd
1263
- * @param {string|null} changeName
1264
- * @returns {{ ok: boolean, issues: string[], warnings: string[] }}
1265
- */
1266
- async checkConsistency(cwd, changeName = null) {
1267
- const data = await this.read(cwd, changeName);
1268
- if (!data) {
1269
- return { ok: false, issues: ['无法读取进度数据'], warnings: [] };
1270
- }
580
+ async setStage(cwd, stage, changeName = null) { return this._stepStore.setStage(cwd, stage, changeName); }
1271
581
 
1272
- const issues = [];
1273
- const warnings = [];
582
+ async addStep(cwd, stage, stepName, changeName = null) { return this._stepStore.addStep(cwd, stage, stepName, changeName); }
1274
583
 
1275
- for (const stageName of STAGE_ORDER) {
1276
- const sd = data.stages[stageName];
1277
- if (!sd) continue;
584
+ async updateStep(cwd, stage, stepName, options = {}, changeName = null) { return this._stepStore.updateStep(cwd, stage, stepName, options, changeName); }
1278
585
 
1279
- // a. completed stage 不能有 pending/stale steps
1280
- if (sd.status === 'completed' && sd.steps) {
1281
- const badSteps = sd.steps.filter(s => ['pending', 'stale', 'in-progress'].includes(s.status));
1282
- for (const step of badSteps) {
1283
- issues.push(`${stageName}/${step.name}: step 状态为 ${step.status},但 stage 状态为 completed`);
1284
- }
1285
- }
1286
-
1287
- // b. revising stage 应有 revision > 0 或 reopenedFromStep
1288
- if (sd.status === 'revising') {
1289
- if (!sd.revision || sd.revision < 1) {
1290
- issues.push(`${stageName}: 状态为 revising 但 revision 缺失或为 0`);
1291
- }
1292
- if (!sd.reopenedFromStep) {
1293
- warnings.push(`${stageName}: 状态为 revising 但未记录 reopenedFromStep`);
1294
- }
1295
- }
586
+ _appendAuditLog(cwd, entry) { return this._consistency._appendAuditLog(cwd, entry); }
1296
587
 
1297
- // c. stale stage 应有 staleReason
1298
- if (sd.status === 'stale') {
1299
- if (!sd.staleReason) {
1300
- warnings.push(`${stageName}: 状态为 stale 但缺少 staleReason`);
1301
- }
1302
- }
588
+ async _validateStageArtifacts(cwd, stage, changeName) { return this._stageMachine._validateStageArtifacts(cwd, stage, changeName); }
1303
589
 
1304
- // d. 下游 completed 不能出现在上游 stale/revising 之后
1305
- const stageIdx = STAGE_ORDER.indexOf(stageName);
1306
- for (let i = 0; i < stageIdx; i++) {
1307
- const upstream = STAGE_ORDER[i];
1308
- const upData = data.stages[upstream];
1309
- if (upData && (upData.status === 'stale' || upData.status === 'revising')) {
1310
- if (sd.status === 'completed') {
1311
- issues.push(`${stageName}: 状态为 completed,但上游 ${upstream} 状态为 ${upData.status}(下游不应在上游修订/失效时保持 completed)`);
1312
- }
1313
- }
1314
- }
590
+ async completeStage(cwd, stage, changeName = null, opts = {}) { return this._stageMachine.completeStage(cwd, stage, changeName, opts); }
1315
591
 
1316
- // e. step stale 时 stage 不应是 completed
1317
- if (sd.status === 'completed' && sd.steps) {
1318
- const staleSteps = sd.steps.filter(s => s.status === 'stale');
1319
- for (const step of staleSteps) {
1320
- issues.push(`${stageName}/${step.name}: step 状态为 stale,但 stage 状态为 completed`);
1321
- }
1322
- }
1323
- }
592
+ async show(cwd, changeName = null) { return this._stageMachine.show(cwd, changeName); }
1324
593
 
1325
- // 输出报告
1326
- console.log('');
1327
- console.log(' ═══════════════════════════════════════');
1328
- console.log(' 状态一致性检查');
1329
- console.log(' ═══════════════════════════════════════');
594
+ async _showChange(cwd, changeName) { return this._stageMachine._showChange(cwd, changeName); }
1330
595
 
1331
- if (issues.length === 0 && warnings.length === 0) {
1332
- console.log(' ✅ 未发现一致性问题');
1333
- } else {
1334
- if (issues.length > 0) {
1335
- console.log(`\n ❌ 问题 (${issues.length}):`);
1336
- for (const issue of issues) console.log(` - ${issue}`);
1337
- }
1338
- if (warnings.length > 0) {
1339
- console.log(`\n ⚠️ 警告 (${warnings.length}):`);
1340
- for (const w of warnings) console.log(` - ${w}`);
1341
- }
1342
- }
1343
- console.log('');
596
+ _getNextSuggestion(data) { return this._stageMachine._getNextSuggestion(data); }
1344
597
 
1345
- return { ok: issues.length === 0, issues, warnings };
1346
- }
598
+ async status(cwd, changeName = null) { return this._stageMachine.status(cwd, changeName); }
1347
599
 
1348
- /**
1349
- * Revision v1.2 状态修复
1350
- * 默认 dry-run,--apply 才真正修改 DB。
1351
- * 只修安全项,不碰产物文件、不 reset/reopen stage。
1352
- *
1353
- * @param {string} cwd
1354
- * @param {object} opts
1355
- * @param {boolean} [opts.apply=false]
1356
- * @param {string|null} [opts.changeName]
1357
- * @returns {{ fixable: object[], manual: string[], applied: object[] }}
1358
- */
1359
- async repairConsistency(cwd, opts = {}) {
1360
- const { apply = false, changeName = null } = opts;
600
+ async checkConsistency(cwd, changeName = null) { return this._consistency.checkConsistency(cwd, changeName); }
1361
601
 
1362
- const data = await this.read(cwd, changeName);
1363
- if (!data) {
1364
- console.log('❌ 无法读取进度数据');
1365
- return { fixable: [], manual: ['无法读取进度数据'], applied: [] };
1366
- }
602
+ async repairConsistency(cwd, opts = {}) { return this._consistency.repairConsistency(cwd, opts); }
1367
603
 
1368
- const fixable = []; // { stage, action, description, apply: (data) => void }
1369
- const manual = []; // string
1370
-
1371
- const now = new Date().toLocaleString('zh-CN', { hour12: false });
1372
-
1373
- for (const stageName of STAGE_ORDER) {
1374
- const sd = data.stages[stageName];
1375
- if (!sd) continue;
1376
-
1377
- // Fix a: stale stage 缺 staleReason → 补默认原因
1378
- if (sd.status === 'stale' && !sd.staleReason) {
1379
- const reason = stageName === 'archive'
1380
- ? 'upstream stage revised; existing archive artifacts are preserved but no longer trusted'
1381
- : 'unknown upstream revision';
1382
- fixable.push({
1383
- stage: stageName,
1384
- action: 'set_stale_reason',
1385
- description: `${stageName}: stale 缺 staleReason → 补 "${reason}"`,
1386
- apply: (d) => { d.stages[stageName].staleReason = reason; },
1387
- });
1388
- }
604
+ async validate(cwd, changeName = null) { return this._stageMachine.validate(cwd, changeName); }
1389
605
 
1390
- // Fix b: 上游 stale/revising,下游仍 completed → cascade stale
1391
- const stageIdx = STAGE_ORDER.indexOf(stageName);
1392
- for (let i = 0; i < stageIdx; i++) {
1393
- const upstream = STAGE_ORDER[i];
1394
- const upData = data.stages[upstream];
1395
- if (upData && (upData.status === 'stale' || upData.status === 'revising')) {
1396
- if (sd.status === 'completed') {
1397
- const upStatus = upData.status;
1398
- const reason = `upstream ${upstream} is ${upStatus}`;
1399
- fixable.push({
1400
- stage: stageName,
1401
- action: 'cascade_stale',
1402
- description: `${stageName}: completed → stale(上游 ${upstream} 为 ${upStatus})`,
1403
- apply: (d) => {
1404
- d.stages[stageName].status = 'stale';
1405
- d.stages[stageName].staleReason = reason;
1406
- d.stages[stageName].completedAt = null;
1407
- },
1408
- });
1409
- }
1410
- }
1411
- }
606
+ async reopenStage(cwd, stage, opts = {}) { return this._stageMachine.reopenStage(cwd, stage, opts); }
1412
607
 
1413
- // Fix c: archive stale 缺 staleReason(专用文案)
1414
- if (stageName === 'archive' && sd.status === 'stale' && !sd.staleReason) {
1415
- // 已在 Fix a 中处理,这里不重复
1416
- }
608
+ _getDownstreamStages(stage) { return this._stageMachine._getDownstreamStages(stage); }
1417
609
 
1418
- // Fix d: revising stage 缺 reopenedAt → 补当前时间
1419
- if (sd.status === 'revising' && !sd.reopenedAt) {
1420
- fixable.push({
1421
- stage: stageName,
1422
- action: 'set_reopened_at',
1423
- description: `${stageName}: revising 缺 reopenedAt → 补当前时间`,
1424
- apply: (d) => { d.stages[stageName].reopenedAt = now; },
1425
- });
1426
- }
1427
-
1428
- // Manual a: completed stage 里有 pending/stale/in-progress steps
1429
- if (sd.status === 'completed' && sd.steps) {
1430
- const badSteps = sd.steps.filter(s => ['pending', 'stale', 'in-progress'].includes(s.status));
1431
- for (const step of badSteps) {
1432
- manual.push(`${stageName}/${step.name}: step 状态为 ${step.status},但 stage 状态为 completed(需手动确认)`);
1433
- }
1434
- }
1435
-
1436
- // Manual b: revising stage 缺 reopenedFromStep
1437
- if (sd.status === 'revising' && !sd.reopenedFromStep) {
1438
- manual.push(`${stageName}: revising 缺 reopenedFromStep(需手动确认修订起始步骤)`);
1439
- }
1440
-
1441
- // Manual c: steps 为空但 stage completed
1442
- if (sd.status === 'completed' && (!sd.steps || sd.steps.length === 0)) {
1443
- manual.push(`${stageName}: completed 但 steps 为空(需手动确认)`);
1444
- }
1445
- }
1446
-
1447
- // 输出报告
1448
- console.log('');
1449
- console.log(' ═══════════════════════════════════════');
1450
- console.log(` 状态修复 ${apply ? '(--apply 模式)' : '(dry-run 模式)'}`);
1451
- console.log(' ═══════════════════════════════════════');
1452
-
1453
- if (fixable.length === 0 && manual.length === 0) {
1454
- console.log(' ✅ 未发现问题,无需修复');
1455
- console.log('');
1456
- return { fixable: [], manual: [], applied: [] };
1457
- }
1458
-
1459
- const applied = [];
1460
-
1461
- if (fixable.length > 0) {
1462
- console.log(`\n 🔧 可自动修复 (${fixable.length}):`);
1463
- for (const item of fixable) {
1464
- console.log(` - ${item.description}`);
1465
- if (apply) {
1466
- item.apply(data);
1467
- applied.push({ stage: item.stage, action: item.action });
1468
- }
1469
- }
1470
- if (!apply) {
1471
- console.log('\n 💡 使用 --apply 执行修复');
1472
- }
1473
- }
1474
-
1475
- if (manual.length > 0) {
1476
- console.log(`\n 👆 需手动处理 (${manual.length}):`);
1477
- for (const m of manual) console.log(` - ${m}`);
1478
- }
1479
-
1480
- if (apply && applied.length > 0) {
1481
- data.lastActive = now;
1482
- await this._write(cwd, data, changeName);
1483
- console.log(`\n ✅ 已修复 ${applied.length} 项`);
1484
- }
1485
-
1486
- console.log('');
1487
-
1488
- return { fixable, manual, applied };
1489
- }
1490
-
1491
- async validate(cwd, changeName = null) {
1492
- const data = await this.read(cwd, changeName);
1493
- if (!data) { console.log('❌ 无法读取进度数据'); return false; }
1494
-
1495
- const errors = [];
1496
- if (!data._version || !Number.isInteger(data._version) || data._version < 1) {
1497
- errors.push(`_version 缺失或无效(期望正整数,实际为 ${JSON.stringify(data._version)})`);
1498
- }
1499
- if (!data.stages || typeof data.stages !== 'object') errors.push('缺少 stages');
1500
- if (!VALID_STAGES.every(s => data.stages[s])) errors.push('缺少阶段定义');
1501
-
1502
- if (errors.length === 0) { console.log('✅ 进度数据格式正确'); return true; }
1503
-
1504
- console.log(`⚠️ 发现问题,尝试修复...`);
1505
- let fixed = { ...data, stages: { ...data.stages } };
1506
- let changed = false;
1507
- if (!fixed.project) {
1508
- fixed.project = basename(cwd);
1509
- changed = true;
1510
- }
1511
- if (!fixed._version || !Number.isInteger(fixed._version) || fixed._version < 1) {
1512
- fixed._version = CURRENT_VERSION;
1513
- changed = true;
1514
- }
1515
- for (const s of VALID_STAGES) {
1516
- if (!fixed.stages[s]) { fixed.stages[s] = emptyStage(); changed = true; }
1517
- }
1518
- if (changed) {
1519
- await this._write(cwd, fixed);
1520
- console.log('✅ 已修复');
1521
- }
1522
-
1523
- return true;
1524
- }
1525
-
1526
- /**
1527
- * 重新打开已完成的阶段进入修订模式
1528
- * - 不带 fromStep:只允许存在 pending/stale/waiting/failed 步骤时继续
1529
- * - 带 fromStep:从该步骤起,当前及后续步骤标记 stale/pending
1530
- * - 自动级联标记下游阶段为 stale
1531
- *
1532
- * @param {string} cwd
1533
- * @param {string} stage - 要重开的阶段
1534
- * @param {object} opts
1535
- * @param {string|number} [opts.fromStep] - 步骤名或序号(1-based)
1536
- * @param {string} [opts.changeName]
1537
- * @returns {{ ok: boolean, error?: string }}
1538
- */
1539
- async reopenStage(cwd, stage, opts = {}) {
1540
- const { fromStep, changeName = null } = opts;
1541
-
1542
- const data = await this.read(cwd, changeName);
1543
- if (!data) return { ok: false, error: '无法读取进度数据' };
1544
-
1545
- const stageData = data.stages[stage];
1546
- if (!stageData) return { ok: false, error: `未知阶段: ${stage}` };
1547
-
1548
- const steps = stageData.steps || [];
1549
-
1550
- // 确定 fromStep 对应的 index
1551
- let fromIdx = null;
1552
- if (fromStep != null) {
1553
- if (typeof fromStep === 'number' || /^\d+$/.test(String(fromStep))) {
1554
- fromIdx = parseInt(String(fromStep), 10) - 1; // 1-based → 0-based
1555
- if (fromIdx < 0 || fromIdx >= steps.length) {
1556
- return { ok: false, error: `步骤序号超出范围: ${fromStep}(共 ${steps.length} 步)` };
1557
- }
1558
- } else {
1559
- // 按名称匹配
1560
- fromIdx = steps.findIndex(s => s.name === fromStep);
1561
- if (fromIdx === -1) {
1562
- return { ok: false, error: `步骤不存在: ${fromStep}` };
1563
- }
1564
- }
1565
- }
1566
-
1567
- // 如果不带 fromStep,检查是否存在中断步骤
1568
- if (fromIdx === null) {
1569
- const hasInterrupted = steps.some(s =>
1570
- ['pending', 'stale', 'waiting', 'failed'].includes(s.status)
1571
- );
1572
- if (!hasInterrupted) {
1573
- return { ok: false, error: `阶段 ${stage} 所有步骤均已完成,请使用 --from-step 指定从哪一步开始修订` };
1574
- }
1575
- // 找到第一个中断步骤
1576
- fromIdx = steps.findIndex(s =>
1577
- ['pending', 'stale', 'waiting', 'failed'].includes(s.status)
1578
- );
1579
- }
1580
-
1581
- // 执行重开操作
1582
- const newRevision = (stageData.revision || 0) + 1;
1583
- const fromStepName = steps[fromIdx].name;
1584
- const now = new Date().toLocaleString('zh-CN', { hour12: false });
1585
-
1586
- // 更新步骤状态:fromStep 之前的保持 completed,fromStep 变 pending,之后的变 stale
1587
- for (let i = 0; i < steps.length; i++) {
1588
- if (i === fromIdx) {
1589
- steps[i].status = 'pending';
1590
- steps[i].completedAt = null;
1591
- steps[i].output = null;
1592
- } else if (i > fromIdx) {
1593
- steps[i].status = 'stale';
1594
- steps[i].completedAt = null;
1595
- }
1596
- // i < fromIdx: 保持原状(completed)
1597
- }
1598
-
1599
- stageData.status = 'revising';
1600
- stageData.completedAt = null;
1601
- stageData.revision = newRevision;
1602
- stageData.reopenedFromStep = `${fromIdx + 1}: ${fromStepName}`; // 存 "index: name" 格式
1603
- stageData.reopenedAt = now;
1604
- stageData.steps = steps;
1605
-
1606
- data.lastActive = now;
1607
- data.currentStage = stage;
1608
-
1609
- await this._write(cwd, data, changeName);
1610
-
1611
- // 级联标记下游阶段为 stale
1612
- const downstreamStages = this._getDownstreamStages(stage);
1613
- if (downstreamStages.length > 0) {
1614
- const data2 = await this.read(cwd, changeName); // 重新读取以获取最新状态
1615
- if (data2) {
1616
- for (const ds of downstreamStages) {
1617
- if (data2.stages[ds] && data2.stages[ds].status === 'completed') {
1618
- data2.stages[ds].status = 'stale';
1619
- data2.stages[ds].staleReason = `上游阶段 ${stage} 已修订 (revision ${newRevision})`;
1620
- data2.stages[ds].completedAt = null;
1621
- }
1622
- }
1623
- await this._write(cwd, data2, changeName);
1624
- }
1625
- }
1626
-
1627
- return { ok: true, revision: newRevision, fromStep: fromStepName };
1628
- }
1629
-
1630
- /**
1631
- * 获取指定阶段的下游主流程阶段列表
1632
- * @param {string} stage
1633
- * @returns {string[]}
1634
- */
1635
- _getDownstreamStages(stage) {
1636
- const idx = MAIN_FLOW_ORDER.indexOf(stage);
1637
- if (idx === -1) return [];
1638
- return MAIN_FLOW_ORDER.slice(idx + 1);
1639
- }
1640
-
1641
- async reset(cwd, stage, changeName = null) {
1642
- if (stage) {
1643
- const data = await this.read(cwd, changeName);
1644
- if (!data) { console.log('❌ 无法读取进度数据'); return; }
1645
- if (!data.stages[stage]) { console.log(`❌ 未知阶段: ${stage}`); return; }
1646
- data.stages[stage] = emptyStage();
1647
- data.lastActive = new Date().toLocaleString('zh-CN',{hour12:false});
1648
- await this._write(cwd, data);
1649
- console.log(`✅ 已重置阶段: ${stage}`);
1650
- } else {
1651
- // 重置所有变更或指定变更
1652
- if (changeName) {
1653
- // SQL: 删除该变更的所有 stages 和 steps 数据
1654
- const db = await this._ensureDB(cwd);
1655
- db.transaction((sqlDb) => {
1656
- const changeRow = sqlDb.exec('SELECT id FROM changes WHERE name = ?', [changeName]);
1657
- if (changeRow && changeRow.length > 0 && changeRow[0].values.length > 0) {
1658
- const changeId = changeRow[0].values[0][0];
1659
- sqlDb.run('DELETE FROM steps WHERE stage_id IN (SELECT id FROM stages WHERE change_id = ?)', [changeId]);
1660
- sqlDb.run('DELETE FROM stages WHERE change_id = ?', [changeId]);
1661
- // 重新插入所有阶段(注意:上方已 DELETE stages,无需再 UPDATE)
1662
- for (const s of VALID_STAGES) {
1663
- sqlDb.run('INSERT OR IGNORE INTO stages (change_id, stage, status) VALUES (?, ?, "pending")', [changeId, s]);
1664
- }
1665
- }
1666
- });
1667
- console.log(`✅ 已重置变更 ${changeName} 的进度`);
1668
- } else {
1669
- const changes = await this.listChanges(cwd);
1670
- const db = await this._ensureDB(cwd);
1671
- db.transaction((sqlDb) => {
1672
- for (const cn of changes) {
1673
- const changeRow = sqlDb.exec('SELECT id FROM changes WHERE name = ?', [cn]);
1674
- if (changeRow && changeRow.length > 0 && changeRow[0].values.length > 0) {
1675
- const changeId = changeRow[0].values[0][0];
1676
- sqlDb.run('DELETE FROM steps WHERE stage_id IN (SELECT id FROM stages WHERE change_id = ?)', [changeId]);
1677
- sqlDb.run('UPDATE stages SET status = "pending", started_at = NULL, completed_at = NULL WHERE change_id = ?', [changeId]);
1678
- }
1679
- }
1680
- });
1681
- console.log('✅ 已重置所有变更的进度');
1682
- }
1683
- }
1684
- }
610
+ async reset(cwd, stage, changeName = null) { return this._stageMachine.reset(cwd, stage, changeName); }
1685
611
 
1686
612
  // ── 内部辅助 ──
1687
613
 
@@ -1734,73 +660,15 @@ export class ProgressManager {
1734
660
  return data;
1735
661
  }
1736
662
 
1737
- _timeAgo(dateStr) {
1738
- if (!dateStr) return '未知';
1739
- let ts = Date.parse(dateStr);
1740
- if (isNaN(ts)) {
1741
- const m = dateStr.match(/(\d{4})[\/-](\d{1,2})[\/-](\d{1,2})[\s,]+(\d{1,2}):(\d{2})(?::(\d{2}))?/);
1742
- if (m) ts = new Date(+m[1], +m[2]-1, +m[3], +m[4], +m[5], +(m[6]||0)).getTime();
1743
- }
1744
- if (isNaN(ts)) return dateStr;
1745
- const diff = Date.now() - ts;
1746
- const minutes = Math.floor(diff / 60000);
1747
- if (minutes < 1) return '刚刚';
1748
- if (minutes < 60) return `${minutes} 分钟前`;
1749
- const hours = Math.floor(minutes / 60);
1750
- if (hours < 24) return `${hours} 小时前`;
1751
- return `${Math.floor(hours / 24)} 天前`;
1752
- }
663
+ _timeAgo(dateStr) { return this._stageMachine._timeAgo(dateStr); }
1753
664
 
1754
665
  // ── 批量进度 ──
1755
666
 
1756
- async updateBatchProgress(cwd, batchData, changeName = null) {
1757
- const cn = changeName || null;
667
+ async updateBatchProgress(cwd, batchData, changeName = null) { return this._stepStore.updateBatchProgress(cwd, batchData, changeName); }
1758
668
 
1759
- const db = await this._ensureDB(cwd);
1760
- db.transaction((sqlDb) => {
1761
- // 获取 change_id
1762
- let changeId = null;
1763
- if (cn) {
1764
- const row = sqlDb.exec('SELECT id FROM changes WHERE name = ?', [cn]);
1765
- if (row && row.length > 0 && row[0].values.length > 0) changeId = row[0].values[0][0];
1766
- }
1767
- if (!changeId) {
1768
- // 尝试从唯一活跃变更获取
1769
- const rows = sqlDb.exec("SELECT id FROM changes WHERE status = 'active'");
1770
- if (rows && rows.length > 0 && rows[0].values.length === 1) changeId = rows[0].values[0][0];
1771
- }
1772
- if (!changeId) return;
669
+ async readBatchProgress(cwd, changeName = null) { return this._stepStore.readBatchProgress(cwd, changeName); }
1773
670
 
1774
- sqlDb.run(
1775
- `INSERT INTO batch_progress (change_id, total, completed, failed, skipped)
1776
- VALUES (?, ?, ?, ?, ?)
1777
- ON CONFLICT(change_id) DO UPDATE SET
1778
- total = excluded.total,
1779
- completed = excluded.completed,
1780
- failed = excluded.failed,
1781
- skipped = excluded.skipped`,
1782
- [changeId, batchData.total || 0, batchData.completed || 0, batchData.failed || 0, batchData.skipped || 0]
1783
- );
1784
- });
1785
- }
1786
-
1787
- async readBatchProgress(cwd, changeName = null) {
1788
- const data = await this.read(cwd, changeName);
1789
- return data?.batchProgress || null;
1790
- }
1791
-
1792
- _renderBatchProgress(batchProgress) {
1793
- if (!batchProgress || !batchProgress.total) return null;
1794
- const { total, completed = 0, failed = 0, skipped = 0 } = batchProgress;
1795
- const barLen = 20;
1796
- const filled = Math.round((completed / total) * barLen);
1797
- const bar = '█'.repeat(filled) + '░'.repeat(barLen - filled);
1798
- const parts = [];
1799
- if (failed > 0) parts.push(`${failed} 失败`);
1800
- if (skipped > 0) parts.push(`${skipped} 跳过`);
1801
- const suffix = parts.length ? ` (${parts.join(', ')})` : '';
1802
- return `📊 批量进度: ${bar} ${completed}/${total}${suffix}`;
1803
- }
671
+ _renderBatchProgress(batchProgress) { return this._stepStore._renderBatchProgress(batchProgress); }
1804
672
 
1805
673
  /**
1806
674
  * 更新 gate-status.json,供 worktree-guard hook 读取