sillyspec 3.25.9 → 3.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/sillyspec-auto/SKILL.md +8 -13
- package/.claude/skills/sillyspec-brainstorm/SKILL.md +8 -6
- package/.claude/skills/sillyspec-continue/SKILL.md +3 -3
- package/.claude/skills/sillyspec-doctor/SKILL.md +1 -1
- package/.claude/skills/sillyspec-execute/SKILL.md +10 -4
- package/.claude/skills/sillyspec-plan/SKILL.md +1 -1
- package/.claude/skills/sillyspec-quick/SKILL.md +3 -1
- package/.claude/skills/sillyspec-verify/SKILL.md +1 -1
- package/.sillyspec-platform-cleaned +1 -0
- package/.sillyspec-platform.json +7 -0
- package/README.md +9 -0
- package/package.json +2 -2
- package/src/change-list.js +11 -8
- package/src/change-risk-profile.js +22 -0
- package/src/db.js +170 -161
- package/src/dispatch/backends/local-agent.js +152 -0
- package/src/dispatch/backends/sillyhub-mcp.js +242 -0
- package/src/dispatch/probe.js +210 -0
- package/src/dispatch/strategy.js +121 -0
- package/src/doctor-diagnostics.js +19 -25
- package/src/fs-atomic.js +69 -69
- package/src/git-helper.js +66 -0
- package/src/hooks/worktree-guard.js +64 -55
- package/src/index.js +326 -36
- package/src/init.js +7 -14
- package/src/local-detect.js +120 -110
- package/src/machine-interface.js +54 -19
- package/src/migrate.js +14 -4
- package/src/modules.js +16 -5
- package/src/progress/change-registry.js +85 -70
- package/src/progress/consistency-doctor.js +71 -14
- package/src/progress/shared.js +8 -5
- package/src/progress/stage-machine.js +67 -65
- package/src/progress/step-store.js +72 -67
- package/src/progress.js +448 -236
- package/src/quicklog.js +59 -12
- package/src/run/command.js +78 -39
- package/src/run/complete-handlers.js +1028 -889
- package/src/run/complete.js +132 -212
- package/src/run/concurrent-detect.js +132 -0
- package/src/run/gates.js +302 -19
- package/src/run/prompt.js +22 -68
- package/src/run/quick-audit.js +10 -9
- package/src/run/shared.js +722 -600
- package/src/run/stage.js +43 -24
- package/src/scan-postcheck.js +9 -31
- package/src/sillyhub-mcp/client.js +394 -0
- package/src/sillyhub-mcp/config.js +62 -0
- package/src/spec-dir-typo.js +71 -0
- package/src/stage-contract-engine.js +22 -1
- package/src/stage-contract-spec.js +11 -4
- package/src/stage-contract.js +26 -3
- package/src/stage-review.js +118 -10
- package/src/stages/archive.js +4 -6
- package/src/stages/brainstorm-auto.js +26 -20
- package/src/stages/brainstorm.js +29 -5
- package/src/stages/cmd-existence.js +145 -0
- package/src/stages/doctor.js +10 -7
- package/src/stages/execute.js +64 -22
- package/src/stages/plan-postcheck.js +152 -0
- package/src/stages/plan.js +16 -31
- package/src/stages/quick.js +25 -9
- package/src/stages/scan.js +53 -8
- package/src/stages/status.js +0 -1
- package/src/stages/verify.js +14 -9
- package/src/sync.js +433 -16
- package/src/task-review.js +59 -1
- package/src/verify-postcheck.js +938 -862
- package/src/version.js +19 -0
- package/src/worktree-apply.js +218 -93
- package/src/worktree-deps.js +89 -5
- package/src/worktree.js +213 -111
- package/templates/prompts/taskcard-rules.md +27 -0
- package/templates/prompts/verify-probes.md +3 -3
- package/.claude/skills/sillyspec-propose/SKILL.md +0 -87
- package/src/stages/propose.js +0 -181
package/src/progress.js
CHANGED
|
@@ -5,16 +5,16 @@
|
|
|
5
5
|
*
|
|
6
6
|
* 存储结构:
|
|
7
7
|
* .sillyspec/.runtime/sillyspec.db — SQLite 数据库(权威状态源,含全局状态/项目名/活跃变更)
|
|
8
|
-
* .sillyspec/.runtime/gate-status.json — worktree-guard 门禁状态缓存
|
|
9
8
|
*
|
|
10
9
|
* 历史迁移:v1/v2 使用 progress.json 文件,v3 已全部迁移至 SQLite。
|
|
10
|
+
* worktree-guard hook 直读 sillyspec.db,不再有 gate-status.json 缓存双源(task-10 废除)。
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync, renameSync, unlinkSync, appendFileSync } from 'fs';
|
|
13
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, renameSync, unlinkSync, appendFileSync, copyFileSync } from 'fs';
|
|
14
14
|
import { join, basename, dirname, resolve, sep } from 'path';
|
|
15
15
|
import { tmpdir } from 'os';
|
|
16
16
|
import { DB } from './db.js';
|
|
17
|
-
import {
|
|
17
|
+
import { checkExecuteCodeEvidence } from './stage-contract.js';
|
|
18
18
|
import { ConsistencyDoctor } from './progress/consistency-doctor.js';
|
|
19
19
|
import { ChangeRegistry } from './progress/change-registry.js';
|
|
20
20
|
import { StepStore } from './progress/step-store.js';
|
|
@@ -191,11 +191,11 @@ export class ProgressManager {
|
|
|
191
191
|
}
|
|
192
192
|
}
|
|
193
193
|
|
|
194
|
-
/** 懒初始化 DB
|
|
195
|
-
|
|
194
|
+
/** 懒初始化 DB 连接,缓存在实例上(better-sqlite3 同步 API,init 无 async) */
|
|
195
|
+
_ensureDB(cwd) {
|
|
196
196
|
if (!this._db) {
|
|
197
197
|
this._db = new DB(this._runtimePath(cwd, 'sillyspec.db'));
|
|
198
|
-
|
|
198
|
+
this._db.init();
|
|
199
199
|
}
|
|
200
200
|
return this._db;
|
|
201
201
|
}
|
|
@@ -208,21 +208,19 @@ export class ProgressManager {
|
|
|
208
208
|
|
|
209
209
|
// ── 全局状态 ──
|
|
210
210
|
|
|
211
|
-
|
|
211
|
+
readGlobal(cwd) {
|
|
212
212
|
// SQL: SELECT FROM project + changes
|
|
213
|
-
const db =
|
|
213
|
+
const db = this._ensureDB(cwd);
|
|
214
214
|
const sqlDb = db.getDb();
|
|
215
215
|
|
|
216
216
|
// 读取 project 行(id=1)
|
|
217
|
-
const
|
|
218
|
-
if (
|
|
219
|
-
const
|
|
217
|
+
const row = sqlDb.prepare('SELECT name, schema_version FROM project WHERE id = 1').get();
|
|
218
|
+
if (row === undefined) return null;
|
|
219
|
+
const { name, schema_version: schemaVersion } = row;
|
|
220
220
|
|
|
221
221
|
// 读取 active 变更列表
|
|
222
|
-
const changeRows = sqlDb.
|
|
223
|
-
const activeChanges = changeRows
|
|
224
|
-
? changeRows[0].values.map(r => r[0])
|
|
225
|
-
: [];
|
|
222
|
+
const changeRows = sqlDb.prepare("SELECT name FROM changes WHERE status = 'active' ORDER BY name").all();
|
|
223
|
+
const activeChanges = changeRows.map(r => r.name);
|
|
226
224
|
|
|
227
225
|
return {
|
|
228
226
|
_version: schemaVersion,
|
|
@@ -237,12 +235,12 @@ export class ProgressManager {
|
|
|
237
235
|
* 读取指定变更的 progress(SQL 版)
|
|
238
236
|
* @param {string} cwd
|
|
239
237
|
* @param {string|null} changeName - 变更名,null 时尝试自动检测
|
|
240
|
-
* @returns {
|
|
238
|
+
* @returns {object|null} 与 SQLite 查询结果一致的 JS 对象
|
|
241
239
|
*/
|
|
242
|
-
|
|
243
|
-
//
|
|
240
|
+
read(cwd, changeName = null) {
|
|
241
|
+
// 自动检测变更名(listChanges 已在 task-08 同步化,此处直接拿到活跃变更数组)
|
|
244
242
|
if (!changeName) {
|
|
245
|
-
const changes =
|
|
243
|
+
const changes = this.listChanges(cwd);
|
|
246
244
|
if (changes.length === 1) {
|
|
247
245
|
changeName = changes[0];
|
|
248
246
|
} else {
|
|
@@ -251,44 +249,39 @@ export class ProgressManager {
|
|
|
251
249
|
}
|
|
252
250
|
}
|
|
253
251
|
|
|
254
|
-
const db =
|
|
252
|
+
const db = this._ensureDB(cwd);
|
|
255
253
|
const sqlDb = db.getDb();
|
|
256
254
|
|
|
257
255
|
// 1. 从 changes 表获取基本信息
|
|
258
|
-
const
|
|
259
|
-
if (
|
|
260
|
-
const
|
|
256
|
+
const changeRow = sqlDb.prepare('SELECT id, name, current_stage, no_worktree, last_active FROM changes WHERE name = ?').get(changeName);
|
|
257
|
+
if (changeRow === undefined) return null;
|
|
258
|
+
const { id: changeId, name: cName, current_stage: currentStage, no_worktree: noWorktree, last_active: lastActive } = changeRow;
|
|
261
259
|
|
|
262
260
|
// 2. 从 stages 表获取所有阶段(含 revision 列)
|
|
263
|
-
const stageRows = sqlDb.
|
|
261
|
+
const stageRows = sqlDb.prepare('SELECT id, stage, status, started_at, completed_at, revision, reopened_from_step, reopened_at, stale_reason FROM stages WHERE change_id = ? ORDER BY id').all(changeId);
|
|
264
262
|
const stageMap = {};
|
|
265
263
|
const stageIds = [];
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
}
|
|
264
|
+
for (const row of stageRows) {
|
|
265
|
+
const { id: sId, stage, status, started_at: startedAt, completed_at: completedAt, revision, reopened_from_step: reopenedFromStep, reopened_at: reopenedAt, stale_reason: staleReason } = row;
|
|
266
|
+
stageMap[stage] = { _dbId: sId, status, startedAt, completedAt,
|
|
267
|
+
...(revision ? { revision } : {}),
|
|
268
|
+
...(reopenedFromStep ? { reopenedFromStep } : {}),
|
|
269
|
+
...(reopenedAt ? { reopenedAt } : {}),
|
|
270
|
+
...(staleReason ? { staleReason } : {}),
|
|
271
|
+
};
|
|
272
|
+
stageIds.push(sId);
|
|
276
273
|
}
|
|
277
274
|
|
|
278
275
|
// 3. 从 steps 表获取所有步骤
|
|
279
|
-
|
|
276
|
+
const stepsByStage = {};
|
|
280
277
|
if (stageIds.length > 0) {
|
|
281
278
|
const placeholders = stageIds.map(() => '?').join(',');
|
|
282
|
-
stepRows = sqlDb.
|
|
283
|
-
`SELECT stage_id, name, status, output, completed_at, ordering, wait_reason, wait_options, wait_answer, waited_at, wait_answers, wait_round, max_wait_rounds FROM steps WHERE stage_id IN (${placeholders}) ORDER BY stage_id, ordering
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
const stepsByStage = {};
|
|
289
|
-
if (stepRows && stepRows.length > 0) {
|
|
290
|
-
for (const row of stepRows[0].values) {
|
|
291
|
-
const [stageId, name, status, output, completedAt, ordering, waitReason, waitOptions, waitAnswer, waitedAt, waitAnswersJson, waitRound, maxWaitRounds] = row;
|
|
279
|
+
const stepRows = sqlDb.prepare(
|
|
280
|
+
`SELECT stage_id, name, status, output, completed_at, ordering, wait_reason, wait_options, wait_answer, waited_at, wait_answers, wait_round, max_wait_rounds FROM steps WHERE stage_id IN (${placeholders}) ORDER BY stage_id, ordering`
|
|
281
|
+
).all(...stageIds);
|
|
282
|
+
// 按阶段分组步骤
|
|
283
|
+
for (const row of stepRows) {
|
|
284
|
+
const { stage_id: stageId, name, status, output, completed_at: completedAt, wait_reason: waitReason, wait_options: waitOptions, wait_answer: waitAnswer, waited_at: waitedAt, wait_answers: waitAnswersJson, wait_round: waitRound, max_wait_rounds: maxWaitRounds } = row;
|
|
292
285
|
if (!stepsByStage[stageId]) stepsByStage[stageId] = [];
|
|
293
286
|
let waitAnswers = null;
|
|
294
287
|
if (waitAnswersJson) {
|
|
@@ -308,18 +301,16 @@ export class ProgressManager {
|
|
|
308
301
|
}
|
|
309
302
|
|
|
310
303
|
// 4. 从 batch_progress 表获取批量进度
|
|
311
|
-
const
|
|
304
|
+
const batchRow = sqlDb.prepare('SELECT total, completed, failed, skipped FROM batch_progress WHERE change_id = ?').get(changeId);
|
|
312
305
|
let batchProgress = undefined;
|
|
313
|
-
if (
|
|
314
|
-
const
|
|
306
|
+
if (batchRow !== undefined) {
|
|
307
|
+
const { total, completed, failed, skipped } = batchRow;
|
|
315
308
|
batchProgress = { total, completed, failed, skipped };
|
|
316
309
|
}
|
|
317
310
|
|
|
318
311
|
// 5. 获取项目名
|
|
319
|
-
const
|
|
320
|
-
const projectName =
|
|
321
|
-
? projectRows[0].values[0][0]
|
|
322
|
-
: '';
|
|
312
|
+
const projectRow = sqlDb.prepare('SELECT name FROM project WHERE id = 1').get();
|
|
313
|
+
const projectName = projectRow !== undefined ? projectRow.name : '';
|
|
323
314
|
|
|
324
315
|
// 6. 组装为兼容对象
|
|
325
316
|
const stages = {};
|
|
@@ -356,7 +347,7 @@ export class ProgressManager {
|
|
|
356
347
|
}
|
|
357
348
|
|
|
358
349
|
const result = {
|
|
359
|
-
_version:
|
|
350
|
+
_version: 4,
|
|
360
351
|
project: projectName,
|
|
361
352
|
currentChange: cName,
|
|
362
353
|
currentStage: currentStage || '',
|
|
@@ -373,39 +364,337 @@ export class ProgressManager {
|
|
|
373
364
|
return result;
|
|
374
365
|
}
|
|
375
366
|
|
|
367
|
+
/**
|
|
368
|
+
* 同步专用六表完整序列化(design §8/B1,task-02 / D-005@v2 / FR-07)。
|
|
369
|
+
* 与 read()(聚合视图,不读 approvals 且 changes 只投影五列)不同,本方法输出真正六表投影,
|
|
370
|
+
* 作为同步载体(ProgressManager.import() 的逆运算,task-03)。
|
|
371
|
+
*
|
|
372
|
+
* 输出契约(字段名 = DB 列名 snake_case,import 直接对列写回,round-trip 无映射损耗):
|
|
373
|
+
* project { name, schema_version } —— 全局单行稳定字段(id/created_at/updated_at 本地元数据不同步)
|
|
374
|
+
* changes [{ name, current_stage, status, last_active,
|
|
375
|
+
* last_synced_platform_ts, last_local_modified_ts }] —— 只投影流程进度列;
|
|
376
|
+
* 排除 isolation_* 系列 与 platform_* 系列(platform_change_id/workspace_id/last_sync/sync_enabled)
|
|
377
|
+
* 以及 created_at(本地强相关,不同步,B2)
|
|
378
|
+
* stages [{ change_name, stage, status, started_at, completed_at,
|
|
379
|
+
* revision, reopened_from_step, reopened_at, stale_reason }] —— 用 change_name+stage 替代 id 外键
|
|
380
|
+
* steps [{ change_name, stage, name, status, output, completed_at, ordering,
|
|
381
|
+
* wait_reason, wait_options, wait_answer, waited_at, wait_answers,
|
|
382
|
+
* wait_round, max_wait_rounds }] —— wait_options/wait_answers 透传 DB JSON 字符串
|
|
383
|
+
* batch_progress [{ change_name, total, completed, failed, skipped }]
|
|
384
|
+
* approvals [{ change_name, status, requested_at, approved_by, approved_at, rejection_reason }]
|
|
385
|
+
* 可空列归一为 null(非 undefined,保证 JSON 往返键稳定);不含 user/base_ts/pushed_at(D-015,sync.js 放 HTTP header)。
|
|
386
|
+
*
|
|
387
|
+
* @param {string} cwd
|
|
388
|
+
* @param {string|null} [changeName] - 空时单活跃变更自动推导(对齐 read());多/零活跃返回 null
|
|
389
|
+
* @returns {object|null} 六键同步 JSON;change 不存在或无法推导返回 null
|
|
390
|
+
*/
|
|
391
|
+
serializeForSync(cwd, changeName = null) {
|
|
392
|
+
// 自动检测变更名(与 read() 同口径):单活跃变更推导,多/零无法确定返回 null
|
|
393
|
+
if (!changeName) {
|
|
394
|
+
const changes = this.listChanges(cwd);
|
|
395
|
+
if (changes.length === 1) {
|
|
396
|
+
changeName = changes[0];
|
|
397
|
+
} else {
|
|
398
|
+
return null;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
const db = this._ensureDB(cwd);
|
|
403
|
+
const sqlDb = db.getDb();
|
|
404
|
+
|
|
405
|
+
// 1. changes 行(投影流程进度列 + name 标识)
|
|
406
|
+
const changeRow = sqlDb.prepare(
|
|
407
|
+
`SELECT id, name, current_stage, status, last_active, last_synced_platform_ts, last_local_modified_ts
|
|
408
|
+
FROM changes WHERE name = ?`
|
|
409
|
+
).get(changeName);
|
|
410
|
+
if (changeRow === undefined) return null;
|
|
411
|
+
const changeId = changeRow.id;
|
|
412
|
+
const changeProj = {
|
|
413
|
+
name: changeRow.name,
|
|
414
|
+
current_stage: changeRow.current_stage,
|
|
415
|
+
status: changeRow.status,
|
|
416
|
+
last_active: changeRow.last_active,
|
|
417
|
+
last_synced_platform_ts: changeRow.last_synced_platform_ts ?? null,
|
|
418
|
+
last_local_modified_ts: changeRow.last_local_modified_ts ?? null,
|
|
419
|
+
};
|
|
420
|
+
|
|
421
|
+
// 2. project 全局单行(只投影稳定字段 name/schema_version;created_at/updated_at 是本地库元数据,
|
|
422
|
+
// 同步无意义且破坏 round-trip 跨库等值——与 changes 排除 created_at 同理,B2)
|
|
423
|
+
const projectRow = sqlDb.prepare(
|
|
424
|
+
'SELECT name, schema_version FROM project WHERE id = 1'
|
|
425
|
+
).get();
|
|
426
|
+
|
|
427
|
+
// 3. stages(该 change 下,用 change_name+stage 表达外键)
|
|
428
|
+
const stageRows = sqlDb.prepare(
|
|
429
|
+
`SELECT id, stage, status, started_at, completed_at, revision, reopened_from_step, reopened_at, stale_reason
|
|
430
|
+
FROM stages WHERE change_id = ? ORDER BY id`
|
|
431
|
+
).all(changeId);
|
|
432
|
+
const stageJson = stageRows.map(r => ({
|
|
433
|
+
change_name: changeRow.name,
|
|
434
|
+
stage: r.stage,
|
|
435
|
+
status: r.status,
|
|
436
|
+
started_at: r.started_at ?? null,
|
|
437
|
+
completed_at: r.completed_at ?? null,
|
|
438
|
+
revision: r.revision ?? null,
|
|
439
|
+
reopened_from_step: r.reopened_from_step ?? null,
|
|
440
|
+
reopened_at: r.reopened_at ?? null,
|
|
441
|
+
stale_reason: r.stale_reason ?? null,
|
|
442
|
+
}));
|
|
443
|
+
|
|
444
|
+
// 4. steps(stage_id IN;用 change_name+stage 关联回 stage)
|
|
445
|
+
const stageIds = stageRows.map(r => r.id);
|
|
446
|
+
const stageIdToName = new Map(stageRows.map(r => [r.id, r.stage]));
|
|
447
|
+
let stepJson = [];
|
|
448
|
+
if (stageIds.length > 0) {
|
|
449
|
+
const placeholders = stageIds.map(() => '?').join(',');
|
|
450
|
+
const stepRows = sqlDb.prepare(
|
|
451
|
+
`SELECT stage_id, name, status, output, completed_at, ordering, wait_reason, wait_options,
|
|
452
|
+
wait_answer, waited_at, wait_answers, wait_round, max_wait_rounds
|
|
453
|
+
FROM steps WHERE stage_id IN (${placeholders}) ORDER BY stage_id, ordering`
|
|
454
|
+
).all(...stageIds);
|
|
455
|
+
stepJson = stepRows.map(r => ({
|
|
456
|
+
change_name: changeRow.name,
|
|
457
|
+
stage: stageIdToName.get(r.stage_id),
|
|
458
|
+
name: r.name,
|
|
459
|
+
status: r.status,
|
|
460
|
+
output: r.output ?? null,
|
|
461
|
+
completed_at: r.completed_at ?? null,
|
|
462
|
+
ordering: r.ordering ?? null,
|
|
463
|
+
wait_reason: r.wait_reason ?? null,
|
|
464
|
+
wait_options: r.wait_options ?? null,
|
|
465
|
+
wait_answer: r.wait_answer ?? null,
|
|
466
|
+
waited_at: r.waited_at ?? null,
|
|
467
|
+
wait_answers: r.wait_answers ?? null,
|
|
468
|
+
wait_round: r.wait_round ?? null,
|
|
469
|
+
max_wait_rounds: r.max_wait_rounds ?? null,
|
|
470
|
+
}));
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// 5. batch_progress(无行则空数组,import 侧幂等)
|
|
474
|
+
const batchRow = sqlDb.prepare(
|
|
475
|
+
'SELECT total, completed, failed, skipped FROM batch_progress WHERE change_id = ?'
|
|
476
|
+
).get(changeId);
|
|
477
|
+
const batchJson = batchRow !== undefined ? [{
|
|
478
|
+
change_name: changeRow.name,
|
|
479
|
+
total: batchRow.total,
|
|
480
|
+
completed: batchRow.completed,
|
|
481
|
+
failed: batchRow.failed,
|
|
482
|
+
skipped: batchRow.skipped,
|
|
483
|
+
}] : [];
|
|
484
|
+
|
|
485
|
+
// 6. approvals(read() 不读此表,serializeForSync 补齐;无行则空数组)
|
|
486
|
+
const approvalRow = sqlDb.prepare(
|
|
487
|
+
'SELECT status, requested_at, approved_by, approved_at, rejection_reason FROM approvals WHERE change_id = ?'
|
|
488
|
+
).get(changeId);
|
|
489
|
+
const approvalJson = approvalRow !== undefined ? [{
|
|
490
|
+
change_name: changeRow.name,
|
|
491
|
+
status: approvalRow.status,
|
|
492
|
+
requested_at: approvalRow.requested_at ?? null,
|
|
493
|
+
approved_by: approvalRow.approved_by ?? null,
|
|
494
|
+
approved_at: approvalRow.approved_at ?? null,
|
|
495
|
+
rejection_reason: approvalRow.rejection_reason ?? null,
|
|
496
|
+
}] : [];
|
|
497
|
+
|
|
498
|
+
return {
|
|
499
|
+
project: projectRow || null,
|
|
500
|
+
changes: [changeProj],
|
|
501
|
+
stages: stageJson,
|
|
502
|
+
steps: stepJson,
|
|
503
|
+
batch_progress: batchJson,
|
|
504
|
+
approvals: approvalJson,
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* serializeForSync 的逆运算:把平台权威 JSON 原子写回本地 DB 的该 change 行(design §8/B1,task-03 / D-005@v2 / D-011 / FR-07)。
|
|
510
|
+
* ⚠️ 必须保护真实资产:import 前对 DB 做【独立】snapshot `.runtime/sillyspec.db.pre-import-<ts>.bak`,
|
|
511
|
+
* 不抢 _openWithFallback 的 `${dbPath}.bak`(gap5/R-08);任何失败本地进度库不损坏。
|
|
512
|
+
*
|
|
513
|
+
* 语义:
|
|
514
|
+
* - 单个 DB.transaction() 包裹,原子重建 stages/steps/batch_progress/approvals 四表(任一失败整体回滚)
|
|
515
|
+
* - changes 行用 UPDATE 选择投影列(current_stage/status/last_active/last_synced_platform_ts/last_local_modified_ts),
|
|
516
|
+
* 保留 isolation_* / platform_* / created_at(本地强相关状态不被覆盖,B2;change 不存在时 INSERT 兜底平台新增)
|
|
517
|
+
* - import 后 last_synced_platform_ts 与 last_local_modified_ts 均置为 progressObj.pushed_at(D-013 例外:
|
|
518
|
+
* 不更新 now()——否则 now()>base_ts 下次 pull 误判冲突;pushed_at 由 sync.js pull() 从响应 header attach)
|
|
519
|
+
* - 本地确定性操作,失败 throw 中文(非 sync 类 Best Effort,CONVENTIONS #4)
|
|
520
|
+
*
|
|
521
|
+
* @param {string} cwd
|
|
522
|
+
* @param {object} progressObj - serializeForSync 输出 + sync.js 附加的 pushed_at
|
|
523
|
+
* @param {string|null} [changeName] - 空则从 progressObj.changes[0].name 推导
|
|
524
|
+
* @returns {ImportResult} { ok:true, imported:changeName, reason?, bakPath }
|
|
525
|
+
*/
|
|
526
|
+
import(cwd, progressObj, changeName = null) {
|
|
527
|
+
if (!progressObj || typeof progressObj !== 'object') {
|
|
528
|
+
throw new Error('import 参数无效:progressObj 必须是 serializeForSync() 输出的六表 JSON 对象');
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
const cn = changeName || (progressObj.changes && progressObj.changes[0] && progressObj.changes[0].name);
|
|
532
|
+
if (!cn) {
|
|
533
|
+
throw new Error('import 缺少 changeName:无法定位目标 change(progressObj.changes[0].name 为空)');
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// ⚠️ 必须保护真实资产:独立 .bak snapshot(前缀 pre-import- 区分于主 .bak 备份机制)
|
|
537
|
+
const dbPath = this._runtimePath(cwd, 'sillyspec.db');
|
|
538
|
+
if (!existsSync(dbPath)) {
|
|
539
|
+
throw new Error(`本地进度库不存在,无法 import:${dbPath}`);
|
|
540
|
+
}
|
|
541
|
+
const bakTs = new Date().toISOString().replace(/[:.]/g, '-');
|
|
542
|
+
const bakPath = this._runtimePath(cwd, `sillyspec.db.pre-import-${bakTs}.bak`);
|
|
543
|
+
copyFileSync(dbPath, bakPath);
|
|
544
|
+
|
|
545
|
+
const pushedAt = progressObj.pushed_at ?? null;
|
|
546
|
+
const now = new Date().toISOString();
|
|
547
|
+
|
|
548
|
+
try {
|
|
549
|
+
const db = this._ensureDB(cwd);
|
|
550
|
+
db.transaction(() => {
|
|
551
|
+
const sqlDb = db.getDb();
|
|
552
|
+
|
|
553
|
+
// 1. changes 行:INSERT OR IGNORE 兜底平台新增 change;UPDATE 只覆盖投影列(保留本地隔离/平台/created_at 列)
|
|
554
|
+
sqlDb.prepare(
|
|
555
|
+
`INSERT OR IGNORE INTO changes (name, current_stage, status, created_at, last_active)
|
|
556
|
+
VALUES (?, '', 'active', ?, ?)`
|
|
557
|
+
).run(cn, now, now);
|
|
558
|
+
const ch = progressObj.changes && progressObj.changes[0];
|
|
559
|
+
sqlDb.prepare(
|
|
560
|
+
`UPDATE changes SET
|
|
561
|
+
current_stage = ?, status = ?, last_active = ?,
|
|
562
|
+
last_synced_platform_ts = ?, last_local_modified_ts = ?
|
|
563
|
+
WHERE name = ?`
|
|
564
|
+
).run(
|
|
565
|
+
ch && ch.current_stage != null ? ch.current_stage : '',
|
|
566
|
+
ch && ch.status != null ? ch.status : 'active',
|
|
567
|
+
ch && ch.last_active != null ? ch.last_active : now,
|
|
568
|
+
pushedAt, pushedAt, cn
|
|
569
|
+
);
|
|
570
|
+
const changeRow = sqlDb.prepare('SELECT id FROM changes WHERE name = ?').get(cn);
|
|
571
|
+
const changeId = changeRow.id;
|
|
572
|
+
|
|
573
|
+
// 2. project 行:幂等确保存在(不覆盖已有全局行,round-trip 等值不受影响;仅补缺失)
|
|
574
|
+
const projExists = sqlDb.prepare('SELECT id FROM project WHERE id = 1').get();
|
|
575
|
+
if (projExists === undefined) {
|
|
576
|
+
const pj = progressObj.project || {};
|
|
577
|
+
sqlDb.prepare(
|
|
578
|
+
`INSERT INTO project (id, name, schema_version, created_at, updated_at)
|
|
579
|
+
VALUES (1, ?, ?, ?, ?)`
|
|
580
|
+
).run(pj.name != null ? pj.name : 'project', pj.schema_version != null ? pj.schema_version : CURRENT_VERSION, now, now);
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// 3. 重建 stages + steps:DELETE stages(ON DELETE CASCADE 级联删 steps),按 JSON 重建
|
|
584
|
+
sqlDb.prepare('DELETE FROM stages WHERE change_id = ?').run(changeId);
|
|
585
|
+
const stageToId = new Map();
|
|
586
|
+
for (const s of (progressObj.stages || [])) {
|
|
587
|
+
if (!s || !s.stage) continue;
|
|
588
|
+
sqlDb.prepare(
|
|
589
|
+
`INSERT INTO stages (change_id, stage, status, started_at, completed_at, revision, reopened_from_step, reopened_at, stale_reason)
|
|
590
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
591
|
+
).run(changeId, s.stage, s.status != null ? s.status : 'pending', s.started_at ?? null, s.completed_at ?? null,
|
|
592
|
+
s.revision != null ? s.revision : 0, s.reopened_from_step ?? null, s.reopened_at ?? null, s.stale_reason ?? null);
|
|
593
|
+
const row = sqlDb.prepare('SELECT id FROM stages WHERE change_id = ? AND stage = ?').get(changeId, s.stage);
|
|
594
|
+
stageToId.set(s.stage, row.id);
|
|
595
|
+
}
|
|
596
|
+
for (const st of (progressObj.steps || [])) {
|
|
597
|
+
if (!st || !st.name) continue;
|
|
598
|
+
const stageId = stageToId.get(st.stage);
|
|
599
|
+
if (stageId === undefined) continue; // 孤儿 step(stage 不在 JSON)跳过,不破坏完整性
|
|
600
|
+
sqlDb.prepare(
|
|
601
|
+
`INSERT INTO steps (stage_id, name, status, output, completed_at, ordering, wait_reason, wait_options, wait_answer, waited_at, wait_answers, wait_round, max_wait_rounds)
|
|
602
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
603
|
+
).run(stageId, st.name, st.status != null ? st.status : 'pending', st.output ?? null, st.completed_at ?? null,
|
|
604
|
+
st.ordering != null ? st.ordering : 0, st.wait_reason ?? null, st.wait_options ?? null, st.wait_answer ?? null,
|
|
605
|
+
st.waited_at ?? null, st.wait_answers ?? null, st.wait_round ?? null, st.max_wait_rounds ?? null);
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// 4. 重建 batch_progress(DELETE 再插,无行则清空)
|
|
609
|
+
sqlDb.prepare('DELETE FROM batch_progress WHERE change_id = ?').run(changeId);
|
|
610
|
+
const bp = progressObj.batch_progress && progressObj.batch_progress[0];
|
|
611
|
+
if (bp) {
|
|
612
|
+
sqlDb.prepare(
|
|
613
|
+
`INSERT INTO batch_progress (change_id, total, completed, failed, skipped)
|
|
614
|
+
VALUES (?, ?, ?, ?, ?)`
|
|
615
|
+
).run(changeId, bp.total != null ? bp.total : 0, bp.completed != null ? bp.completed : 0,
|
|
616
|
+
bp.failed != null ? bp.failed : 0, bp.skipped != null ? bp.skipped : 0);
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
// 5. 重建 approvals(DELETE 再插,无行则清空)
|
|
620
|
+
sqlDb.prepare('DELETE FROM approvals WHERE change_id = ?').run(changeId);
|
|
621
|
+
const ap = progressObj.approvals && progressObj.approvals[0];
|
|
622
|
+
if (ap) {
|
|
623
|
+
sqlDb.prepare(
|
|
624
|
+
`INSERT INTO approvals (change_id, status, requested_at, approved_by, approved_at, rejection_reason)
|
|
625
|
+
VALUES (?, ?, ?, ?, ?, ?)`
|
|
626
|
+
).run(changeId, ap.status != null ? ap.status : 'not_required', ap.requested_at ?? null, ap.approved_by ?? null,
|
|
627
|
+
ap.approved_at ?? null, ap.rejection_reason ?? null);
|
|
628
|
+
}
|
|
629
|
+
});
|
|
630
|
+
} catch (e) {
|
|
631
|
+
// 事务失败自动回滚(DB 未变),.bak 保留可恢复;fail-loud 中文
|
|
632
|
+
throw new Error(`import 失败,本地进度库已回滚(snapshot: ${bakPath}):${e.message}`);
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
return { ok: true, imported: cn, reason: undefined, bakPath };
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
/**
|
|
639
|
+
* 本地脏度标记(D-013 / task-04):写入路径末尾更新 changes.last_local_modified_ts。
|
|
640
|
+
* 在写方法的事务内调用则属于该事务(原子,失败整体回滚);import 是例外不调本方法
|
|
641
|
+
* (import 自己把 last_local_modified_ts 置为 pushed_at,表示本地=平台干净)。
|
|
642
|
+
* @param {string} cwd
|
|
643
|
+
* @param {string} changeName
|
|
644
|
+
* @param {string} [ts] - 复用调用方事务内的时间戳,保证与本次写入同时刻
|
|
645
|
+
*/
|
|
646
|
+
_touchLocalModified(cwd, changeName, ts = null) {
|
|
647
|
+
if (!changeName) return;
|
|
648
|
+
const db = this._ensureDB(cwd);
|
|
649
|
+
const stamp = ts || new Date().toISOString();
|
|
650
|
+
db.getDb().prepare('UPDATE changes SET last_local_modified_ts = ? WHERE name = ?').run(stamp, changeName);
|
|
651
|
+
}
|
|
652
|
+
|
|
376
653
|
/**
|
|
377
654
|
* 写入指定变更的 progress
|
|
378
655
|
* @param {string} cwd
|
|
379
656
|
* @param {object} data
|
|
380
657
|
* @param {string|null} changeName - 从 data.currentChange 推导,或显式传入
|
|
658
|
+
*
|
|
659
|
+
* 持锁窗口评估(task-05 / R-08):本方法采用全量 UPSERT——无条件 UPDATE changes 行 +
|
|
660
|
+
* 遍历所有 stages(每 stage UPSERT stages 行 + 每 step 先 DELETE 再 INSERT 全量 + 末尾
|
|
661
|
+
* DELETE data 中不存在的多余 step)。评估过改"只写 diff(仅写变更过的 change/stage/step 行)"
|
|
662
|
+
* 以缩小事务持锁窗口、降低 BUSY 概率,结论:保持现状(全量 UPSERT)。
|
|
663
|
+
* 理由:
|
|
664
|
+
* 1. steps 表无 UNIQUE 约束(见循环内 DELETE-then-INSERT 注释),diff 写需先读现状再逐条
|
|
665
|
+
* 比较,复杂度高易错,易引入孤儿/缺失步骤破坏 FK 级联语义(design §3/§8);
|
|
666
|
+
* 2. busy_timeout=5000 + DB 层 SQLITE_BUSY 应用层有限重试(见 db.js transaction)已兜底
|
|
667
|
+
* 并发冲突,全量 UPSERT 的持锁窗口在此兜底下不是瓶颈;
|
|
668
|
+
* 3. 单进程内调用串行(design §9),事务窗口短(毫秒级),全量写正确性远高于 diff 的边际收益。
|
|
669
|
+
* 结论:全量 UPSERT 的简单正确性 > 持锁窗口优化的边际收益,不强行改 diff。
|
|
381
670
|
*/
|
|
382
|
-
|
|
671
|
+
_write(cwd, data, changeName = null) {
|
|
383
672
|
const cn = changeName || data.currentChange;
|
|
384
673
|
if (!cn) {
|
|
385
674
|
console.warn('⚠️ _write: 无变更名,跳过写入');
|
|
386
675
|
return;
|
|
387
676
|
}
|
|
388
677
|
|
|
389
|
-
const db =
|
|
390
|
-
db.transaction((
|
|
391
|
-
|
|
678
|
+
const db = this._ensureDB(cwd);
|
|
679
|
+
db.transaction(() => {
|
|
680
|
+
const sqlDb = db.getDb();
|
|
681
|
+
// 1. 更新 changes 表(last_local_modified_ts 脏度:D-013 / task-04)
|
|
392
682
|
const now = new Date().toISOString();
|
|
393
683
|
const noWorktree = data.noWorktree ? 1 : 0;
|
|
394
|
-
sqlDb.
|
|
395
|
-
'UPDATE changes SET current_stage = ?, last_active = ?, no_worktree = ? WHERE name = ?'
|
|
396
|
-
|
|
397
|
-
);
|
|
684
|
+
sqlDb.prepare(
|
|
685
|
+
'UPDATE changes SET current_stage = ?, last_active = ?, no_worktree = ?, last_local_modified_ts = ? WHERE name = ?'
|
|
686
|
+
).run(data.currentStage || '', now, noWorktree, now, cn);
|
|
398
687
|
|
|
399
688
|
// 2. 获取 change_id
|
|
400
|
-
const changeRow = sqlDb.
|
|
401
|
-
if (
|
|
402
|
-
const changeId = changeRow
|
|
689
|
+
const changeRow = sqlDb.prepare('SELECT id FROM changes WHERE name = ?').get(cn);
|
|
690
|
+
if (changeRow === undefined) return;
|
|
691
|
+
const changeId = changeRow.id;
|
|
403
692
|
|
|
404
693
|
// 3. 遍历 stages,UPSERT stages 表和 steps 表
|
|
405
694
|
if (data.stages && typeof data.stages === 'object') {
|
|
406
695
|
for (const [stageName, stageData] of Object.entries(data.stages)) {
|
|
407
696
|
// UPSERT stages 行(含 revision 列)
|
|
408
|
-
sqlDb.
|
|
697
|
+
sqlDb.prepare(
|
|
409
698
|
`INSERT INTO stages (change_id, stage, status, started_at, completed_at, revision, reopened_from_step, reopened_at, stale_reason)
|
|
410
699
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
411
700
|
ON CONFLICT(change_id, stage) DO UPDATE SET
|
|
@@ -415,15 +704,14 @@ export class ProgressManager {
|
|
|
415
704
|
revision = COALESCE(excluded.revision, stages.revision),
|
|
416
705
|
reopened_from_step = excluded.reopened_from_step,
|
|
417
706
|
reopened_at = excluded.reopened_at,
|
|
418
|
-
stale_reason = excluded.stale_reason
|
|
419
|
-
|
|
420
|
-
stageData.revision || 0, stageData.reopenedFromStep || null, stageData.reopenedAt || null, stageData.staleReason || null
|
|
421
|
-
);
|
|
707
|
+
stale_reason = excluded.stale_reason`
|
|
708
|
+
).run(changeId, stageName, stageData.status || 'pending', stageData.startedAt || null, stageData.completedAt || null,
|
|
709
|
+
stageData.revision || 0, stageData.reopenedFromStep || null, stageData.reopenedAt || null, stageData.staleReason || null);
|
|
422
710
|
|
|
423
711
|
// 获取 stage_id
|
|
424
|
-
const stageRow = sqlDb.
|
|
425
|
-
if (
|
|
426
|
-
const stageId = stageRow
|
|
712
|
+
const stageRow = sqlDb.prepare('SELECT id FROM stages WHERE change_id = ? AND stage = ?').get(changeId, stageName);
|
|
713
|
+
if (stageRow === undefined) continue;
|
|
714
|
+
const stageId = stageRow.id;
|
|
427
715
|
|
|
428
716
|
// 收集 data 中的步骤名
|
|
429
717
|
const stepNames = new Set();
|
|
@@ -432,86 +720,81 @@ export class ProgressManager {
|
|
|
432
720
|
const step = stageData.steps[i];
|
|
433
721
|
stepNames.add(step.name);
|
|
434
722
|
// UPSERT 步骤(先删再插,steps 表无 UNIQUE 约束)
|
|
435
|
-
sqlDb.
|
|
436
|
-
sqlDb.
|
|
437
|
-
'INSERT INTO steps (stage_id, name, status, output, completed_at, ordering, wait_reason, wait_options, wait_answer, waited_at, wait_answers, wait_round, max_wait_rounds) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'
|
|
438
|
-
|
|
439
|
-
step.waitReason ?? null, step.waitOptions ?? null, step.waitAnswer ?? null, step.waitedAt ?? null,
|
|
723
|
+
sqlDb.prepare('DELETE FROM steps WHERE stage_id = ? AND name = ?').run(stageId, step.name);
|
|
724
|
+
sqlDb.prepare(
|
|
725
|
+
'INSERT INTO steps (stage_id, name, status, output, completed_at, ordering, wait_reason, wait_options, wait_answer, waited_at, wait_answers, wait_round, max_wait_rounds) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'
|
|
726
|
+
).run(stageId, step.name, step.status || 'pending', step.output || null, step.completedAt || null, i,
|
|
727
|
+
step.waitReason ?? null, Array.isArray(step.waitOptions) ? JSON.stringify(step.waitOptions) : (step.waitOptions ?? null), step.waitAnswer ?? null, step.waitedAt ?? null,
|
|
440
728
|
Array.isArray(step.waitAnswers) ? JSON.stringify(step.waitAnswers) : null,
|
|
441
|
-
step.waitRound ?? null, step.maxWaitRounds ?? null
|
|
442
|
-
);
|
|
729
|
+
step.waitRound ?? null, step.maxWaitRounds ?? null);
|
|
443
730
|
}
|
|
444
731
|
}
|
|
445
732
|
|
|
446
733
|
// 删除 data 中不存在的多余步骤
|
|
447
734
|
if (stepNames.size > 0) {
|
|
448
735
|
const namePlaceholders = [...stepNames].map(() => '?').join(',');
|
|
449
|
-
sqlDb.
|
|
450
|
-
`DELETE FROM steps WHERE stage_id = ? AND name NOT IN (${namePlaceholders})
|
|
451
|
-
|
|
452
|
-
);
|
|
736
|
+
sqlDb.prepare(
|
|
737
|
+
`DELETE FROM steps WHERE stage_id = ? AND name NOT IN (${namePlaceholders})`
|
|
738
|
+
).run(stageId, ...stepNames);
|
|
453
739
|
} else {
|
|
454
740
|
// data 中没有步骤,清空该阶段所有步骤
|
|
455
|
-
sqlDb.
|
|
741
|
+
sqlDb.prepare('DELETE FROM steps WHERE stage_id = ?').run(stageId);
|
|
456
742
|
}
|
|
457
743
|
}
|
|
458
744
|
}
|
|
459
745
|
|
|
460
746
|
// 4. UPSERT batch_progress
|
|
461
747
|
if (data.batchProgress && typeof data.batchProgress === 'object') {
|
|
462
|
-
sqlDb.
|
|
748
|
+
sqlDb.prepare(
|
|
463
749
|
`INSERT INTO batch_progress (change_id, total, completed, failed, skipped)
|
|
464
750
|
VALUES (?, ?, ?, ?, ?)
|
|
465
751
|
ON CONFLICT(change_id) DO UPDATE SET
|
|
466
752
|
total = excluded.total,
|
|
467
753
|
completed = excluded.completed,
|
|
468
754
|
failed = excluded.failed,
|
|
469
|
-
skipped = excluded.skipped
|
|
470
|
-
|
|
471
|
-
);
|
|
755
|
+
skipped = excluded.skipped`
|
|
756
|
+
).run(changeId, data.batchProgress.total || 0, data.batchProgress.completed || 0, data.batchProgress.failed || 0, data.batchProgress.skipped || 0);
|
|
472
757
|
}
|
|
473
758
|
});
|
|
474
|
-
|
|
475
|
-
await this._updateGateStatus(cwd);
|
|
476
759
|
}
|
|
477
760
|
|
|
478
761
|
// ── 变更管理 ──
|
|
479
762
|
|
|
480
|
-
|
|
763
|
+
listChanges(cwd) { return this._changeRegistry.listChanges(cwd); }
|
|
481
764
|
|
|
482
|
-
|
|
765
|
+
registerChange(cwd, changeName) { return this._changeRegistry.registerChange(cwd, changeName); }
|
|
483
766
|
|
|
484
|
-
|
|
767
|
+
updateChangeIsolation(cwd, changeName, isolation) { return this._changeRegistry.updateChangeIsolation(cwd, changeName, isolation); }
|
|
485
768
|
|
|
486
|
-
|
|
769
|
+
readChangeIsolation(cwd, changeName) { return this._changeRegistry.readChangeIsolation(cwd, changeName); }
|
|
487
770
|
|
|
488
|
-
|
|
771
|
+
_updatePlatformLastSync(cwd, changeName) { return this._changeRegistry._updatePlatformLastSync(cwd, changeName); }
|
|
489
772
|
|
|
490
|
-
|
|
773
|
+
_updateApprovalStatus(cwd, changeName, status, reason = null) { return this._changeRegistry._updateApprovalStatus(cwd, changeName, status, reason); }
|
|
491
774
|
|
|
492
|
-
|
|
775
|
+
renameChange(cwd, oldName, newName) { return this._changeRegistry.renameChange(cwd, oldName, newName); }
|
|
493
776
|
|
|
494
|
-
|
|
777
|
+
unregisterChange(cwd, changeName) { return this._changeRegistry.unregisterChange(cwd, changeName); }
|
|
495
778
|
|
|
496
779
|
// ── CLI 命令 ──
|
|
497
780
|
|
|
498
|
-
|
|
781
|
+
init(cwd) {
|
|
499
782
|
this._ensureRuntimeDir(cwd);
|
|
500
783
|
|
|
501
784
|
// 初始化 DB(如不存在则创建文件 + 建表)
|
|
502
|
-
const db =
|
|
503
|
-
db.transaction((
|
|
785
|
+
const db = this._ensureDB(cwd);
|
|
786
|
+
db.transaction(() => {
|
|
787
|
+
const sqlDb = db.getDb();
|
|
504
788
|
const now = new Date().toISOString();
|
|
505
789
|
const projectName = basename(cwd) || 'project';
|
|
506
790
|
|
|
507
791
|
// 检查 project id=1 是否已存在
|
|
508
|
-
const existing = sqlDb.
|
|
509
|
-
if (
|
|
510
|
-
sqlDb.
|
|
792
|
+
const existing = sqlDb.prepare('SELECT id FROM project WHERE id = 1').get();
|
|
793
|
+
if (existing === undefined) {
|
|
794
|
+
sqlDb.prepare(
|
|
511
795
|
`INSERT INTO project (id, name, schema_version, created_at, updated_at)
|
|
512
|
-
VALUES (1, ?, ?, ?, ?)
|
|
513
|
-
|
|
514
|
-
);
|
|
796
|
+
VALUES (1, ?, ?, ?, ?)`
|
|
797
|
+
).run(projectName, CURRENT_VERSION, now, now);
|
|
515
798
|
console.log(`✅ 已创建全局状态文件(SQLite)`);
|
|
516
799
|
} else {
|
|
517
800
|
console.log(`ℹ️ 全局状态文件已存在,跳过`);
|
|
@@ -525,14 +808,14 @@ export class ProgressManager {
|
|
|
525
808
|
}
|
|
526
809
|
|
|
527
810
|
this._ensureGitignore(cwd);
|
|
528
|
-
return
|
|
811
|
+
return this.readGlobal(cwd);
|
|
529
812
|
}
|
|
530
813
|
|
|
531
814
|
/**
|
|
532
815
|
* 初始化指定变更的 progress
|
|
533
816
|
* SQL: INSERT changes + 批量 INSERT stages
|
|
534
817
|
*/
|
|
535
|
-
|
|
818
|
+
initChange(cwd, changeName) {
|
|
536
819
|
if (!changeName) {
|
|
537
820
|
console.warn('⚠️ initChange: changeName 为空,跳过');
|
|
538
821
|
return null;
|
|
@@ -543,103 +826,105 @@ export class ProgressManager {
|
|
|
543
826
|
this._ensureChangeDir(cwd, changeName);
|
|
544
827
|
}
|
|
545
828
|
|
|
546
|
-
const db =
|
|
547
|
-
db.transaction((
|
|
829
|
+
const db = this._ensureDB(cwd);
|
|
830
|
+
db.transaction(() => {
|
|
831
|
+
const sqlDb = db.getDb();
|
|
548
832
|
const now = new Date().toISOString();
|
|
549
833
|
|
|
550
834
|
// 检查变更是否已存在
|
|
551
|
-
const existing = sqlDb.
|
|
552
|
-
if (
|
|
835
|
+
const existing = sqlDb.prepare('SELECT id FROM changes WHERE name = ?').get(changeName);
|
|
836
|
+
if (existing === undefined) {
|
|
553
837
|
// 插入 changes 行
|
|
554
|
-
sqlDb.
|
|
838
|
+
sqlDb.prepare(
|
|
555
839
|
`INSERT INTO changes (name, current_stage, status, created_at, last_active)
|
|
556
|
-
VALUES (?, 'scan', 'active', ?, ?)
|
|
557
|
-
|
|
558
|
-
);
|
|
840
|
+
VALUES (?, 'scan', 'active', ?, ?)`
|
|
841
|
+
).run(changeName, now, now);
|
|
559
842
|
}
|
|
560
843
|
|
|
561
844
|
// 获取 change_id
|
|
562
|
-
const changeRow = sqlDb.
|
|
563
|
-
const changeId = changeRow
|
|
845
|
+
const changeRow = sqlDb.prepare('SELECT id FROM changes WHERE name = ?').get(changeName);
|
|
846
|
+
const changeId = changeRow.id;
|
|
564
847
|
|
|
565
|
-
//
|
|
566
|
-
const
|
|
567
|
-
|
|
568
|
-
sqlDb.run(
|
|
848
|
+
// 批量插入所有合法阶段(INSERT OR IGNORE 跳过已存在的);沿用顶部 import 的 VALID_STAGES 单一源
|
|
849
|
+
for (const stage of VALID_STAGES) {
|
|
850
|
+
sqlDb.prepare(
|
|
569
851
|
`INSERT OR IGNORE INTO stages (change_id, stage, status)
|
|
570
|
-
VALUES (?, ?, 'pending')
|
|
571
|
-
|
|
572
|
-
);
|
|
852
|
+
VALUES (?, ?, 'pending')`
|
|
853
|
+
).run(changeId, stage);
|
|
573
854
|
}
|
|
855
|
+
// 本地脏度(D-013 / task-04):新建 change 视为有未同步数据
|
|
856
|
+
this._touchLocalModified(cwd, changeName, now);
|
|
574
857
|
});
|
|
575
858
|
|
|
576
859
|
// 不再需要写文件:read() 已改为 SQL
|
|
577
|
-
return
|
|
860
|
+
return this.read(cwd, changeName);
|
|
578
861
|
}
|
|
579
862
|
|
|
580
|
-
|
|
863
|
+
setStage(cwd, stage, changeName = null) { return this._stepStore.setStage(cwd, stage, changeName); }
|
|
581
864
|
|
|
582
|
-
|
|
865
|
+
addStep(cwd, stage, stepName, changeName = null) { return this._stepStore.addStep(cwd, stage, stepName, changeName); }
|
|
583
866
|
|
|
584
|
-
|
|
867
|
+
updateStep(cwd, stage, stepName, options = {}, changeName = null) { return this._stepStore.updateStep(cwd, stage, stepName, options, changeName); }
|
|
585
868
|
|
|
586
869
|
_appendAuditLog(cwd, entry) { return this._consistency._appendAuditLog(cwd, entry); }
|
|
587
870
|
|
|
588
|
-
|
|
871
|
+
_validateStageArtifacts(cwd, stage, changeName) { return this._stageMachine._validateStageArtifacts(cwd, stage, changeName); }
|
|
589
872
|
|
|
590
|
-
|
|
873
|
+
completeStage(cwd, stage, changeName = null, opts = {}) { return this._stageMachine.completeStage(cwd, stage, changeName, opts); }
|
|
591
874
|
|
|
592
|
-
|
|
875
|
+
show(cwd, changeName = null) { return this._stageMachine.show(cwd, changeName); }
|
|
593
876
|
|
|
594
|
-
|
|
877
|
+
_showChange(cwd, changeName) { return this._stageMachine._showChange(cwd, changeName); }
|
|
595
878
|
|
|
596
879
|
_getNextSuggestion(data) { return this._stageMachine._getNextSuggestion(data); }
|
|
597
880
|
|
|
598
|
-
|
|
881
|
+
status(cwd, changeName = null) { return this._stageMachine.status(cwd, changeName); }
|
|
599
882
|
|
|
600
|
-
|
|
883
|
+
checkConsistency(cwd, changeName = null) { return this._consistency.checkConsistency(cwd, changeName); }
|
|
601
884
|
|
|
602
|
-
|
|
885
|
+
repairConsistency(cwd, opts = {}) { return this._consistency.repairConsistency(cwd, opts); }
|
|
603
886
|
|
|
604
|
-
|
|
887
|
+
validate(cwd, changeName = null) { return this._stageMachine.validate(cwd, changeName); }
|
|
605
888
|
|
|
606
|
-
|
|
889
|
+
reopenStage(cwd, stage, opts = {}) { return this._stageMachine.reopenStage(cwd, stage, opts); }
|
|
607
890
|
|
|
608
891
|
_getDownstreamStages(stage) { return this._stageMachine._getDownstreamStages(stage); }
|
|
609
892
|
|
|
610
|
-
|
|
893
|
+
reset(cwd, stage, changeName = null) { return this._stageMachine.reset(cwd, stage, changeName); }
|
|
611
894
|
|
|
612
895
|
// ── 内部辅助 ──
|
|
613
896
|
|
|
614
|
-
|
|
615
|
-
let data =
|
|
897
|
+
_readOrInit(cwd, changeName = null) {
|
|
898
|
+
let data = this.read(cwd, changeName);
|
|
616
899
|
if (!data) {
|
|
617
900
|
// 尝试自动检测变更名
|
|
618
901
|
if (!changeName) {
|
|
619
|
-
const changes =
|
|
902
|
+
const changes = this.listChanges(cwd);
|
|
620
903
|
if (changes.length === 1) changeName = changes[0];
|
|
621
904
|
}
|
|
622
905
|
if (changeName) {
|
|
623
906
|
// 确保变更在 DB 中已初始化
|
|
624
|
-
const db =
|
|
625
|
-
db.transaction((
|
|
907
|
+
const db = this._ensureDB(cwd);
|
|
908
|
+
db.transaction(() => {
|
|
909
|
+
const sqlDb = db.getDb();
|
|
626
910
|
const now = new Date().toISOString();
|
|
627
|
-
sqlDb.
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
);
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
const changeId = changeRow[0].values[0][0];
|
|
911
|
+
const ins = sqlDb.prepare(
|
|
912
|
+
`INSERT OR IGNORE INTO changes (name, current_stage, status, created_at, last_active) VALUES (?, 'scan', 'active', ?, ?)`
|
|
913
|
+
).run(changeName, now, now);
|
|
914
|
+
const changeRow = sqlDb.prepare('SELECT id FROM changes WHERE name = ?').get(changeName);
|
|
915
|
+
if (changeRow !== undefined) {
|
|
916
|
+
const changeId = changeRow.id;
|
|
634
917
|
for (const s of VALID_STAGES) {
|
|
635
|
-
sqlDb.
|
|
918
|
+
sqlDb.prepare(`INSERT OR IGNORE INTO stages (change_id, stage, status) VALUES (?, ?, 'pending')`).run(changeId, s);
|
|
636
919
|
}
|
|
637
920
|
}
|
|
921
|
+
// 只在真正创建 change 时标脏(D-013/task-04);读路径(change 已存在)不标,避免 pull 误判本地有未同步改动
|
|
922
|
+
if (ins.changes > 0) this._touchLocalModified(cwd, changeName, now);
|
|
638
923
|
});
|
|
639
|
-
|
|
924
|
+
this.registerChange(cwd, changeName);
|
|
640
925
|
}
|
|
641
926
|
if (!data) {
|
|
642
|
-
data =
|
|
927
|
+
data = this.read(cwd, changeName);
|
|
643
928
|
}
|
|
644
929
|
if (!data) {
|
|
645
930
|
console.log('❌ 无法确定当前变更,请指定 --change <name>');
|
|
@@ -649,12 +934,12 @@ export class ProgressManager {
|
|
|
649
934
|
return data;
|
|
650
935
|
}
|
|
651
936
|
|
|
652
|
-
|
|
937
|
+
_requireStage(cwd, stage, changeName = null) {
|
|
653
938
|
if (!VALID_STAGES.includes(stage)) {
|
|
654
939
|
console.log(`❌ 未知阶段: ${stage},可选: ${VALID_STAGES.join(', ')}`);
|
|
655
940
|
return null;
|
|
656
941
|
}
|
|
657
|
-
const data =
|
|
942
|
+
const data = this._readOrInit(cwd, changeName);
|
|
658
943
|
if (!data) return null;
|
|
659
944
|
if (!data.stages[stage]) data.stages[stage] = emptyStage();
|
|
660
945
|
return data;
|
|
@@ -664,84 +949,12 @@ export class ProgressManager {
|
|
|
664
949
|
|
|
665
950
|
// ── 批量进度 ──
|
|
666
951
|
|
|
667
|
-
|
|
952
|
+
updateBatchProgress(cwd, batchData, changeName = null) { return this._stepStore.updateBatchProgress(cwd, batchData, changeName); }
|
|
668
953
|
|
|
669
|
-
|
|
954
|
+
readBatchProgress(cwd, changeName = null) { return this._stepStore.readBatchProgress(cwd, changeName); }
|
|
670
955
|
|
|
671
956
|
_renderBatchProgress(batchProgress) { return this._stepStore._renderBatchProgress(batchProgress); }
|
|
672
957
|
|
|
673
|
-
/**
|
|
674
|
-
* 更新 gate-status.json,供 worktree-guard hook 读取
|
|
675
|
-
* 从 SQLite 查询所有处于 execute/quick 阶段的活跃变更,生成或删除 gate-status.json
|
|
676
|
-
*/
|
|
677
|
-
async _updateGateStatus(cwd) {
|
|
678
|
-
const db = await this._ensureDB(cwd);
|
|
679
|
-
const sqlDb = db.getDb();
|
|
680
|
-
|
|
681
|
-
// SQL 查询:所有处于 execute/quick 阶段的活跃变更
|
|
682
|
-
const rows = sqlDb.exec(
|
|
683
|
-
`SELECT name, current_stage, no_worktree FROM changes
|
|
684
|
-
WHERE status = 'active' AND current_stage IN ('execute', 'quick')`
|
|
685
|
-
);
|
|
686
|
-
|
|
687
|
-
const gatePath = this._runtimePath(cwd, 'gate-status.json');
|
|
688
|
-
|
|
689
|
-
if (!rows || rows.length === 0 || rows[0].values.length === 0) {
|
|
690
|
-
// 无 execute/quick 阶段的活跃变更:写墓碑而非 unlink。unlink 失败(Windows 杀毒/索引占用)
|
|
691
|
-
// 会被空 catch 吞掉,陈旧 gate-status 残留 → hook 把 brainstorm 误当 execute 放行(fail-open)。
|
|
692
|
-
// 墓碑 {stage:null} 让 readCurrentStage 的 `gateStatus.stage` 为假 → 自动 fallback DB,语义等价删除但原子。
|
|
693
|
-
if (existsSync(gatePath)) {
|
|
694
|
-
try {
|
|
695
|
-
this._ensureRuntimeDir(cwd);
|
|
696
|
-
writeAtomicSync(gatePath, JSON.stringify({ stage: null, tombstone: true, updatedAt: new Date().toISOString() }) + '\n');
|
|
697
|
-
} catch (e) {
|
|
698
|
-
console.warn(`⚠️ gate-status 墓碑写入失败(hook 可能读到陈旧缓存): ${e.message}`);
|
|
699
|
-
}
|
|
700
|
-
}
|
|
701
|
-
return;
|
|
702
|
-
}
|
|
703
|
-
|
|
704
|
-
let gateStage = null;
|
|
705
|
-
let hasNoWorktree = false;
|
|
706
|
-
const activeChanges = [];
|
|
707
|
-
|
|
708
|
-
for (const [name, stage, noWorktree] of rows[0].values) {
|
|
709
|
-
if (!stage) continue;
|
|
710
|
-
// 优先取 execute,其次 quick
|
|
711
|
-
if (gateStage !== 'execute' || stage === 'execute') {
|
|
712
|
-
gateStage = stage;
|
|
713
|
-
}
|
|
714
|
-
activeChanges.push(name);
|
|
715
|
-
if (noWorktree === 1) hasNoWorktree = true;
|
|
716
|
-
}
|
|
717
|
-
|
|
718
|
-
if (!gateStage) {
|
|
719
|
-
// current_stage 为 NULL 的边界情况,等同于无 execute/quick:同样写墓碑(见上 rationale)
|
|
720
|
-
if (existsSync(gatePath)) {
|
|
721
|
-
try {
|
|
722
|
-
this._ensureRuntimeDir(cwd);
|
|
723
|
-
writeAtomicSync(gatePath, JSON.stringify({ stage: null, tombstone: true, updatedAt: new Date().toISOString() }) + '\n');
|
|
724
|
-
} catch (e) {
|
|
725
|
-
console.warn(`⚠️ gate-status 墓碑写入失败(hook 可能读到陈旧缓存): ${e.message}`);
|
|
726
|
-
}
|
|
727
|
-
}
|
|
728
|
-
return;
|
|
729
|
-
}
|
|
730
|
-
|
|
731
|
-
try {
|
|
732
|
-
this._ensureRuntimeDir(cwd);
|
|
733
|
-
const gateData = {
|
|
734
|
-
stage: gateStage,
|
|
735
|
-
changes: activeChanges,
|
|
736
|
-
updatedAt: new Date().toISOString(),
|
|
737
|
-
...(hasNoWorktree ? { noWorktree: true } : {}),
|
|
738
|
-
};
|
|
739
|
-
writeAtomicSync(gatePath, JSON.stringify(gateData, null, 2) + '\n');
|
|
740
|
-
} catch (err) {
|
|
741
|
-
console.warn('⚠️ 写入 gate-status.json 失败:', err.message);
|
|
742
|
-
}
|
|
743
|
-
}
|
|
744
|
-
|
|
745
958
|
_ensureGitignore(cwd) {
|
|
746
959
|
// 外部 specDir 不需要修改项目 .gitignore
|
|
747
960
|
if (this._customSpecDir) return;
|
|
@@ -801,14 +1014,14 @@ export class ProgressManager {
|
|
|
801
1014
|
* @param {string} specBase - platformOpts.specRoot || join(cwd, '.sillyspec'),用于定位 changes 目录
|
|
802
1015
|
* @param {object} [opts]
|
|
803
1016
|
* @param {boolean} [opts.confirm=false] - 默认 dry-run,仅当 confirm=true 才落盘
|
|
804
|
-
* @returns {
|
|
1017
|
+
* @returns {{ ok: boolean, aligned: number, skipped: number, planTotal: number, planChecked: number, reason?: string, dryRun?: boolean }}
|
|
805
1018
|
*/
|
|
806
|
-
|
|
1019
|
+
alignExecuteToPlan(cwd, changeName, specBase, opts = {}) {
|
|
807
1020
|
if (!changeName) throw new Error('changeName 不能为空');
|
|
808
1021
|
const { confirm = false } = opts;
|
|
809
1022
|
|
|
810
1023
|
// 1. 读 progress;无 progress / 无 execute 阶段 → 拒绝
|
|
811
|
-
const progress =
|
|
1024
|
+
const progress = this.read(cwd, changeName);
|
|
812
1025
|
if (!progress || !progress.stages || !progress.stages.execute) {
|
|
813
1026
|
return { ok: false, aligned: 0, skipped: 0, planTotal: 0, planChecked: 0, reason: 'execute 阶段无进度数据' };
|
|
814
1027
|
}
|
|
@@ -840,7 +1053,6 @@ export class ProgressManager {
|
|
|
840
1053
|
// 对齐前用 git 客观核验是否存在真实代码变更(能确证零变更时才拒绝,
|
|
841
1054
|
// unknown 不阻断——worktree 已清理且变更已提交的正常场景无法对账)。
|
|
842
1055
|
try {
|
|
843
|
-
const { checkExecuteCodeEvidence } = await import('./stage-contract.js');
|
|
844
1056
|
const evidence = checkExecuteCodeEvidence(cwd, changeName);
|
|
845
1057
|
if (evidence.status === 'unchanged') {
|
|
846
1058
|
return {
|
|
@@ -882,7 +1094,7 @@ export class ProgressManager {
|
|
|
882
1094
|
executeStage.completedAt = now;
|
|
883
1095
|
progress.currentStage = progress.currentStage || 'execute';
|
|
884
1096
|
progress.lastActive = now;
|
|
885
|
-
|
|
1097
|
+
this._write(cwd, progress, changeName);
|
|
886
1098
|
|
|
887
1099
|
return { ok: true, aligned, skipped, planTotal, planChecked };
|
|
888
1100
|
}
|