sillyspec 3.26.0 → 3.26.1
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/.sillyspec-platform.json +1 -1
- package/README.md +3 -5
- package/package.json +2 -3
- package/src/db-engine.js +55 -0
- package/src/db.js +44 -34
- package/src/doctor-diagnostics.js +6 -6
- package/src/hooks/worktree-guard.js +18 -26
- package/src/progress/change-registry.js +28 -0
- package/src/progress/shared.js +1 -1
- package/src/progress.js +7 -5
- package/src/quicklog.js +2 -2
- package/src/run/complete-handlers.js +7 -1
- package/src/run/stage.js +6 -1
package/.sillyspec-platform.json
CHANGED
package/README.md
CHANGED
|
@@ -59,12 +59,10 @@ npx sillyspec init --interactive # 完整引导
|
|
|
59
59
|
|
|
60
60
|
## 平台支持
|
|
61
61
|
|
|
62
|
-
SillySpec 进度库使用 SQLite 持久化(基于
|
|
62
|
+
SillySpec 进度库使用 SQLite 持久化(基于 Node.js 内置的 `node:sqlite` 模块,纯 JS 无原生编译依赖)。
|
|
63
63
|
|
|
64
|
-
-
|
|
65
|
-
-
|
|
66
|
-
|
|
67
|
-
如遇边缘平台编译失败,建议在受支持的平台或容器(如 Debian/Ubuntu 基镜像)中运行。
|
|
64
|
+
- **Node.js 版本要求 >= 22.11.0**:`node:sqlite` 自 v22.11.0 起免 `--experimental-sqlite` flag 直接可用。支持 Linux x64、macOS x64 / arm64、Windows x64 等所有 Node.js 官方支持平台。
|
|
65
|
+
- **零编译安装**:不依赖任何原生绑定 / node-gyp / C++ 工具链,npm install 即装即用,无平台编译失败风险。
|
|
68
66
|
|
|
69
67
|
## 从哪里开始
|
|
70
68
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sillyspec",
|
|
3
|
-
"version": "3.26.
|
|
3
|
+
"version": "3.26.1",
|
|
4
4
|
"description": "SillySpec CLI — 流程状态机,让 AI 严格按步骤来",
|
|
5
5
|
"icon": "logo.jpg",
|
|
6
6
|
"homepage": "https://sillyspec.ppdmq.top/",
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"sillyspec": "bin/sillyspec.js"
|
|
14
14
|
},
|
|
15
15
|
"engines": {
|
|
16
|
-
"node": ">=
|
|
16
|
+
"node": ">=22.11.0"
|
|
17
17
|
},
|
|
18
18
|
"keywords": [
|
|
19
19
|
"sillyspec",
|
|
@@ -29,7 +29,6 @@
|
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
31
|
"@inquirer/prompts": "^7.10.1",
|
|
32
|
-
"better-sqlite3": "^11.0.0",
|
|
33
32
|
"chalk": "^5.6.2",
|
|
34
33
|
"chokidar": "^4.0",
|
|
35
34
|
"js-yaml": "^4.2.0",
|
package/src/db-engine.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// db-engine.js — node:sqlite DatabaseSync 引擎抽象层
|
|
2
|
+
// 封装 node:sqlite 内置模块 + 消解 better-sqlite3 → node:sqlite 三缺口:
|
|
3
|
+
// pragma → exec / transaction → 手写 SAVEPOINT / pluck → Object.values 取首列
|
|
4
|
+
// 供 db.js 与 doctor-diagnostics 共用(方案 B 单一换引擎点,D-002)。
|
|
5
|
+
// node:sqlite 在 node v22.11.0+ 无需 --experimental-sqlite flag(仍发 ExperimentalWarning)。
|
|
6
|
+
// Author: sillyspec execute W1 task-02
|
|
7
|
+
// Created: 2026-08-11
|
|
8
|
+
import { DatabaseSync } from 'node:sqlite';
|
|
9
|
+
|
|
10
|
+
// 打开数据库。opts.readOnly(驼峰)透传 node:sqlite。
|
|
11
|
+
// fileMustExist 语义由调用方 existsSync 前置门实现(node:sqlite 不拒缺失文件,不凭空建库由调用方保证)。
|
|
12
|
+
export function openDatabase(dbPath, opts = {}) {
|
|
13
|
+
return new DatabaseSync(dbPath, { readOnly: !!opts.readOnly });
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// 逐条 exec PRAGMA。entries: [['journal_mode','WAL'], ['busy_timeout','5000'], ...]
|
|
17
|
+
// node:sqlite 无 .pragma(),统一走 .exec('PRAGMA key = value')。
|
|
18
|
+
export function applyPragmas(db, entries) {
|
|
19
|
+
for (const [key, value] of entries) {
|
|
20
|
+
db.exec(`PRAGMA ${key} = ${value}`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// 手写事务(node:sqlite 无 .transaction())。
|
|
25
|
+
// 统一用 SAVEPOINT/RELEASE/ROLLBACK TO —— SQLite 允许顶层 SAVEPOINT(等价事务),
|
|
26
|
+
// 嵌套调用自动形成 savepoint 栈(better-sqlite3 嵌套自动 SAVEPOINT 的兼容实现)。
|
|
27
|
+
// fn 抛错自动 ROLLBACK TO + RELEASE 且原错误上抛不吞。
|
|
28
|
+
// 本函数不含 BUSY 重试(由 db.js wrapper 外层包裹)。
|
|
29
|
+
export function runTransaction(db, fn) {
|
|
30
|
+
const sp = 'tx_sp_' + Math.random().toString(36).slice(2);
|
|
31
|
+
db.exec(`SAVEPOINT ${sp}`);
|
|
32
|
+
try {
|
|
33
|
+
const result = fn();
|
|
34
|
+
db.exec(`RELEASE ${sp}`);
|
|
35
|
+
return result;
|
|
36
|
+
} catch (err) {
|
|
37
|
+
try {
|
|
38
|
+
db.exec(`ROLLBACK TO ${sp}`);
|
|
39
|
+
db.exec(`RELEASE ${sp}`);
|
|
40
|
+
} catch { /* 回滚失败不掩盖原错误 */ }
|
|
41
|
+
throw err;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// pluck 替代:取第一行第一列,无行 undefined。
|
|
46
|
+
// node:sqlite .get() 返回行对象(不像 better-sqlite3 pluck 返标量),故 Object.values 取首列。
|
|
47
|
+
export function pluckGet(db, sql, ...params) {
|
|
48
|
+
const row = db.prepare(sql).get(...params);
|
|
49
|
+
return row === undefined ? undefined : Object.values(row)[0];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// pluck 替代:取所有行第一列成数组。
|
|
53
|
+
export function pluckAll(db, sql, ...params) {
|
|
54
|
+
return db.prepare(sql).all(...params).map((row) => Object.values(row)[0]);
|
|
55
|
+
}
|
package/src/db.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { openDatabase, applyPragmas, runTransaction } from './db-engine.js';
|
|
2
2
|
import { copyFileSync, existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from 'fs';
|
|
3
3
|
import { dirname } from 'path';
|
|
4
4
|
|
|
5
5
|
// DB schema 版本(与 project.schema_version DEFAULT 对齐)。_createSchema 改动(加表/列/migration)时 bump,
|
|
6
6
|
// 触发 .schema-version 戳失效 → 下次 init 重跑 _createSchema(W4-H:高频读路径 gate/derive/status
|
|
7
7
|
// 每次 new ProgressManager 都过 init,靠版本戳跳过建表省开销)。
|
|
8
|
-
//
|
|
8
|
+
// node:sqlite(DatabaseSync)是原生 SQLite 引擎,打开即持久化(不像 sql.js 纯内存需整库 export 落盘),
|
|
9
9
|
// _createSchema 内 DDL 直接落盘,无需额外 _save。
|
|
10
|
-
const DB_SCHEMA_VERSION =
|
|
10
|
+
const DB_SCHEMA_VERSION = 5;
|
|
11
11
|
|
|
12
12
|
// SQLITE_BUSY 应用层有限重试(R-08 / NFR-03):WAL 单写者模型,并发写第二者在
|
|
13
13
|
// busy_timeout=5000(init PRAGMA)后抛 SQLITE_BUSY。busy_timeout 已在引擎层处理大部分等待;
|
|
@@ -20,7 +20,7 @@ const MAX_BUSY_RETRIES = 3;
|
|
|
20
20
|
const BUSY_BACKOFF_MS = [50, 100, 200];
|
|
21
21
|
|
|
22
22
|
/**
|
|
23
|
-
* 同步阻塞 sleep(
|
|
23
|
+
* 同步阻塞 sleep(node:sqlite 同步 API,BUSY 退避不能用 await)。
|
|
24
24
|
* Atomics.wait 阻塞当前线程不占 CPU;SharedArrayBuffer 不可用的环境(受限 CSP / 旧 Node)
|
|
25
25
|
* 退化为 busy-wait(退避时长 50-200ms 短,CPU 占用可接受)。仅在 transaction() 的 BUSY
|
|
26
26
|
* 重试路径调用。
|
|
@@ -41,13 +41,13 @@ export class DB {
|
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
/**
|
|
44
|
-
* 同步初始化(
|
|
45
|
-
* -
|
|
46
|
-
* - PRAGMA:journal_mode=WAL 真生效(
|
|
44
|
+
* 同步初始化(node:sqlite DatabaseSync 是同步 API,无 async/await,与 sql.js 的 initSqlJs() 异步加载不同):
|
|
45
|
+
* - openDatabase(path) 同步打开/创建库文件(node:sqlite 原生绑定,非 WASM)
|
|
46
|
+
* - PRAGMA:journal_mode=WAL 真生效(node:sqlite 直连原生 SQLite,WAL 落地;
|
|
47
47
|
* sql.js 是纯内存库,WAL 对它无意义),busy_timeout=5000 让写锁竞争时自动等待,
|
|
48
48
|
* foreign_keys=ON 启用外键级联,synchronous=NORMAL 在 WAL 下兼顾安全与性能
|
|
49
49
|
* - schema 戳检查:内容匹配 DB_SCHEMA_VERSION 则跳过 _createSchema,省建表开销
|
|
50
|
-
* -
|
|
50
|
+
* - node:sqlite 打开即持久化,DDL/事务提交直接落盘,不需要 sql.js 时代的 _save()
|
|
51
51
|
*
|
|
52
52
|
* .bak 损坏回退由 _openWithFallback() 承担(task-04):主库 → .bak → 全新/报错 逐级回退,
|
|
53
53
|
* 语义对齐原 sql.js 的 _loadDatabase。
|
|
@@ -60,14 +60,16 @@ export class DB {
|
|
|
60
60
|
// 2. 同步打开/创建数据库(主库 → .bak → 全新/报错 逐级回退,归 task-04)
|
|
61
61
|
this.db = this._openWithFallback();
|
|
62
62
|
|
|
63
|
-
// 3. 设置 PRAGMA(
|
|
64
|
-
this.db
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
63
|
+
// 3. 设置 PRAGMA(node:sqlite 走真 SQLite,journal_mode=WAL 真生效;经 applyPragmas 逐条 exec)
|
|
64
|
+
applyPragmas(this.db, [
|
|
65
|
+
['journal_mode', 'WAL'],
|
|
66
|
+
['busy_timeout', '5000'],
|
|
67
|
+
['foreign_keys', 'ON'],
|
|
68
|
+
['synchronous', 'NORMAL'],
|
|
69
|
+
]);
|
|
68
70
|
|
|
69
71
|
// 4. 创建表结构(仅当 schema 版本戳不匹配时——W4-H):
|
|
70
|
-
// schema 已最新则跳过建表,省 DDL 开销。
|
|
72
|
+
// schema 已最新则跳过建表,省 DDL 开销。node:sqlite 打开即持久化,
|
|
71
73
|
// _createSchema 内 DDL 直接落盘,无需 _save。
|
|
72
74
|
// 戳在但 db 缺表属"手动改 db"边角(SillySpec 不支持),不在本层兜底。
|
|
73
75
|
const versionPath = `${this.dbPath}.schema-version`;
|
|
@@ -85,13 +87,13 @@ export class DB {
|
|
|
85
87
|
|
|
86
88
|
/**
|
|
87
89
|
* 打开数据库(主库 → .bak → 全新/报错 逐级回退)。
|
|
88
|
-
* 语义对齐原 sql.js 的 _loadDatabase,但
|
|
90
|
+
* 语义对齐原 sql.js 的 _loadDatabase,但 node:sqlite 绑定文件路径无 _save,
|
|
89
91
|
* .bak 回退时 copyFileSync 把 .bak 内容恢复到主库路径,统一用 dbPath。
|
|
90
92
|
* - 主库存在且有内容且可打开:直接用。
|
|
91
93
|
* - 主库缺失/为空/损坏:尝试 .bak;成功则 warn 原因后 copy 回主库使用。
|
|
92
94
|
* - 都不可用:主库不存在→建全新空库;主库曾存在(空/损坏)→fail-loud 抛错(不静默建空库吞进度)。
|
|
93
|
-
*
|
|
94
|
-
* .bak 作为恢复源只要存在且能打开即用(含 0 字节空库——
|
|
95
|
+
* node:sqlite 打开 0 字节文件不抛错:主库须 statSync 显式判空(截断信号→走回退,防静默吞进度);
|
|
96
|
+
* .bak 作为恢复源只要存在且能打开即用(含 0 字节空库——node:sqlite 视其为合法空库,
|
|
95
97
|
* 生产环境 .bak 由原子写改名上一完整主库得来,永不为 0 字节,0 字节态仅出现在测试构造)。
|
|
96
98
|
*/
|
|
97
99
|
_openWithFallback() {
|
|
@@ -101,10 +103,10 @@ export class DB {
|
|
|
101
103
|
if (!existsSync(p)) return null;
|
|
102
104
|
return statSync(p).size === 0 ? 'empty' : 'ok';
|
|
103
105
|
};
|
|
104
|
-
// tryOpen:
|
|
106
|
+
// tryOpen: openDatabase(path) + prepare 探测双保险(防打开成功但内容非 SQLite)
|
|
105
107
|
const tryOpen = (p) => {
|
|
106
108
|
let db;
|
|
107
|
-
try { db =
|
|
109
|
+
try { db = openDatabase(p); }
|
|
108
110
|
catch { return null; }
|
|
109
111
|
try { db.prepare('SELECT count(*) FROM sqlite_master').get(); return db; }
|
|
110
112
|
catch { try { db.close(); } catch { /* 关闭失败忽略 */ } return null; }
|
|
@@ -117,7 +119,7 @@ export class DB {
|
|
|
117
119
|
}
|
|
118
120
|
|
|
119
121
|
// 2. .bak 回退(.bak 是恢复源:存在且能打开即用,含 0 字节空库;门禁只挡"不存在",
|
|
120
|
-
// 避免 tryOpen 对不存在的路径触发
|
|
122
|
+
// 避免 tryOpen 对不存在的路径触发 openDatabase 的副作用——会凭空创建文件)
|
|
121
123
|
if (readValid(bakPath) !== null) {
|
|
122
124
|
const bakDb = tryOpen(bakPath);
|
|
123
125
|
if (bakDb) {
|
|
@@ -129,23 +131,23 @@ export class DB {
|
|
|
129
131
|
: 'sillyspec.db 损坏';
|
|
130
132
|
console.warn(`⚠️ ${reason},已从 .bak 备份恢复。`);
|
|
131
133
|
bakDb.close();
|
|
132
|
-
//
|
|
134
|
+
// node:sqlite 绑定路径,无 _save:把 .bak 内容 copy 回主库,统一用 dbPath
|
|
133
135
|
copyFileSync(bakPath, this.dbPath);
|
|
134
|
-
return
|
|
136
|
+
return openDatabase(this.dbPath);
|
|
135
137
|
}
|
|
136
138
|
}
|
|
137
139
|
|
|
138
140
|
// 3. 都不可用
|
|
139
141
|
if (readValid(this.dbPath) === null) {
|
|
140
142
|
// 主库与备份都不存在 → 全新项目,建空库
|
|
141
|
-
return
|
|
143
|
+
return openDatabase(this.dbPath);
|
|
142
144
|
}
|
|
143
145
|
// 主库曾存在(空/损坏)但无法恢复 → 数据丢失,必须 fail-loud(不静默建空库吞进度)
|
|
144
146
|
throw new Error('sillyspec.db 损坏且 .bak 备份不可用,无法恢复。请从版本控制或其他备份恢复 .sillyspec/.runtime/ 后重试。');
|
|
145
147
|
}
|
|
146
148
|
|
|
147
149
|
/**
|
|
148
|
-
* 关闭数据库。
|
|
150
|
+
* 关闭数据库。node:sqlite 的 close() 自动做 WAL checkpoint(把 -wal/-shm 日志合并回主库),
|
|
149
151
|
* 不需要 sql.js 时代的 _save() 整库 export 落盘。
|
|
150
152
|
*/
|
|
151
153
|
close() {
|
|
@@ -158,9 +160,10 @@ export class DB {
|
|
|
158
160
|
/**
|
|
159
161
|
* 原生事务 + SQLITE_BUSY 应用层有限重试(R-08 / NFR-03)。
|
|
160
162
|
*
|
|
161
|
-
*
|
|
162
|
-
*
|
|
163
|
-
*
|
|
163
|
+
* node:sqlite 无 .transaction(),runTransaction(fn)(db-engine)用 SAVEPOINT/RELEASE/
|
|
164
|
+
* ROLLBACK TO 手写事务:fn 抛错自动回滚且不吞错,嵌套调用自动形成 savepoint 栈
|
|
165
|
+
* (对齐旧引擎嵌套自动 SAVEPOINT 的兼容实现)。无需手动 BEGIN/COMMIT/ROLLBACK,
|
|
166
|
+
* 也无需 _save(node:sqlite 提交即持久化)。
|
|
164
167
|
*
|
|
165
168
|
* SQLITE_BUSY 重试:WAL 单写者模型,并发写第二者在 busy_timeout=5000 后抛 BUSY。此处对
|
|
166
169
|
* BUSY 加 MAX_BUSY_RETRIES 次递增退避(BUSY_BACKOFF_MS);仅 BUSY 重试(其它异常直接上抛),
|
|
@@ -169,14 +172,13 @@ export class DB {
|
|
|
169
172
|
*/
|
|
170
173
|
transaction(fn) {
|
|
171
174
|
if (!this.db) throw new Error('DB not initialized');
|
|
172
|
-
const tx = this.db.transaction(fn);
|
|
173
175
|
let attempt = 0;
|
|
174
176
|
while (true) {
|
|
175
177
|
try {
|
|
176
|
-
return
|
|
178
|
+
return runTransaction(this.db, fn);
|
|
177
179
|
} catch (err) {
|
|
178
|
-
// 仅 SQLITE_BUSY 重试(
|
|
179
|
-
const isBusy = err && (err.code === 'SQLITE_BUSY' || err.code === 5
|
|
180
|
+
// 仅 SQLITE_BUSY 重试(node:sqlite err.errcode 为 5 / err.code 为 'ERR_SQLITE_ERROR',message 含 'database is locked')
|
|
181
|
+
const isBusy = err && (err.code === 'SQLITE_BUSY' || err.code === 5 || err.errcode === 5
|
|
180
182
|
|| (typeof err.message === 'string' && /database is locked|SQLITE_BUSY/i.test(err.message)));
|
|
181
183
|
if (!isBusy) throw err;
|
|
182
184
|
if (attempt >= MAX_BUSY_RETRIES) throw err; // 达上限 fail-loud(不静默吞,防死循环)
|
|
@@ -186,13 +188,13 @@ export class DB {
|
|
|
186
188
|
}
|
|
187
189
|
}
|
|
188
190
|
|
|
189
|
-
/** 获取底层 db 对象(
|
|
191
|
+
/** 获取底层 db 对象(node:sqlite DatabaseSync 实例,供 progress.js 直接使用) */
|
|
190
192
|
getDb() {
|
|
191
193
|
return this.db;
|
|
192
194
|
}
|
|
193
195
|
|
|
194
196
|
_createSchema() {
|
|
195
|
-
//
|
|
197
|
+
// node:sqlite:exec() 执行多条 SQL/DDL(无返回);prepare().run/get/all 用于 DML。
|
|
196
198
|
// sql.js 的 .run(sql) 在此换为 .exec(sql)。
|
|
197
199
|
|
|
198
200
|
// project 表
|
|
@@ -310,11 +312,19 @@ export class DB {
|
|
|
310
312
|
// last_synced_platform_ts NULL=首次同步(base_ts NULL 平台接受首次 push)。
|
|
311
313
|
this._migrateAddColumn('changes', 'last_synced_platform_ts', 'TEXT');
|
|
312
314
|
this._migrateAddColumn('changes', 'last_local_modified_ts', 'TEXT');
|
|
315
|
+
|
|
316
|
+
// changes 表加 title(人类可读中文标题)+ quicklog_id(关联 QUICKLOG ql-ID)两列:
|
|
317
|
+
// 让 quick-<hex> 这种机器 hash sessionId 命名的 change 行可读,DB↔QUICKLOG 可对账。
|
|
318
|
+
// quick 启动时回填(stage.js:title 从任务描述提取、quicklog_id 用分配的 qlId),
|
|
319
|
+
// --done 时从 step3「需求:」刷新 title(agent 可改)。完整流程 change 的 name 本身
|
|
320
|
+
// 已语义化,title/quicklog_id 留空。不纳入平台同步脏度(本地展示用元信息)。
|
|
321
|
+
this._migrateAddColumn('changes', 'title', 'TEXT');
|
|
322
|
+
this._migrateAddColumn('changes', 'quicklog_id', 'TEXT');
|
|
313
323
|
}
|
|
314
324
|
|
|
315
325
|
/**
|
|
316
326
|
* 幂等地给表添加列(列已存在则跳过)。
|
|
317
|
-
*
|
|
327
|
+
* node:sqlite:exec() 执行 ALTER TABLE DDL;列已存在抛错被 catch 静默跳过。
|
|
318
328
|
* @private
|
|
319
329
|
*/
|
|
320
330
|
_migrateAddColumn(table, column, type) {
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* - safe_actions:只描述建议动作与风险等级,绝不自动执行
|
|
12
12
|
*
|
|
13
13
|
* 安全约束(硬性):
|
|
14
|
-
* - 所有检测只读。DB 以
|
|
14
|
+
* - 所有检测只读。DB 以 node:sqlite 只读连接打开(readOnly + existsSync 前置门),不调用
|
|
15
15
|
* export/writeFileSync,不跑建表/迁移,close 后丢弃——绝不写回原 db 文件。
|
|
16
16
|
* - 不删除/移动任何文件;orphan db 仅报告,处理交给后续 --dump-db / --confirm 流程。
|
|
17
17
|
* - execute-progress-plan-mismatch 维度同样只读:仅读 plan.md checkbox + 只读查 stages 表,
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
* 风格对齐 scan-postcheck.js:checks 用 CHECK_SEVERITY,formatter 产出 schema_version JSON,
|
|
21
21
|
* writer 落盘到 <authoritySpecDir>/.runtime/。
|
|
22
22
|
*/
|
|
23
|
-
import
|
|
23
|
+
import { openDatabase, pluckGet, pluckAll } from './db-engine.js';
|
|
24
24
|
import { existsSync, statSync, readFileSync, readdirSync, mkdirSync, writeFileSync, unlinkSync } from 'fs';
|
|
25
25
|
import { join } from 'path';
|
|
26
26
|
import { CHECK_SEVERITY } from './constants.js';
|
|
@@ -68,16 +68,16 @@ function probeDb(dbPath) {
|
|
|
68
68
|
}
|
|
69
69
|
let db = null;
|
|
70
70
|
try {
|
|
71
|
-
db =
|
|
71
|
+
db = openDatabase(dbPath, { readOnly: true });
|
|
72
72
|
const pick = (sql, fallback = null) => {
|
|
73
73
|
try {
|
|
74
|
-
const r = db
|
|
74
|
+
const r = pluckGet(db, sql);
|
|
75
75
|
return r === undefined ? fallback : r;
|
|
76
76
|
} catch { return fallback; }
|
|
77
77
|
};
|
|
78
78
|
const pickCol = (sql) => {
|
|
79
79
|
try {
|
|
80
|
-
return db
|
|
80
|
+
return pluckAll(db, sql);
|
|
81
81
|
} catch { return []; }
|
|
82
82
|
};
|
|
83
83
|
// 每个 active change 的 execute stage status(只读查询;stages 表的 status 列)。
|
|
@@ -657,7 +657,7 @@ export async function dumpDb({ dbPath, cwd }) {
|
|
|
657
657
|
}
|
|
658
658
|
let db = null;
|
|
659
659
|
try {
|
|
660
|
-
db =
|
|
660
|
+
db = openDatabase(dbPath, { readOnly: true });
|
|
661
661
|
const rows = (sql) => {
|
|
662
662
|
try { return db.prepare(sql).all(); } catch { return []; }
|
|
663
663
|
};
|
|
@@ -12,7 +12,6 @@
|
|
|
12
12
|
import { existsSync, readFileSync, readdirSync } from 'fs'
|
|
13
13
|
import path from 'path'
|
|
14
14
|
import { execFileSync } from 'child_process'
|
|
15
|
-
import { createRequire } from 'module'
|
|
16
15
|
|
|
17
16
|
// ── 常量 ──
|
|
18
17
|
|
|
@@ -225,20 +224,19 @@ function isInsideWorktreeStorage(filePath, cwd) {
|
|
|
225
224
|
/**
|
|
226
225
|
* 同步只读查询 sillyspec.db 第一行第一列(不依赖外部 sqlite3 CLI)。
|
|
227
226
|
*
|
|
228
|
-
* 用子进程跑
|
|
229
|
-
* -
|
|
230
|
-
*
|
|
231
|
-
*
|
|
232
|
-
*
|
|
233
|
-
*
|
|
234
|
-
* 看不见 WAL 的过渡态可见性窗口)。
|
|
227
|
+
* 用子进程跑 node:sqlite 的 DatabaseSync(execFileSync 等待):
|
|
228
|
+
* - node:sqlite 是 Node 内置模块,子进程 require('node:sqlite') 直接可用,无需先 resolve 出
|
|
229
|
+
* 绝对路径再注入(旧方案需手动解析原生绑定路径,node:sqlite 无此步骤)。
|
|
230
|
+
* - readOnly 打开只读连接(替代旧方案 readonly/fileMustExist 语义):绝不写库;WAL 并发读不阻塞主进程写,
|
|
231
|
+
* 且只读连接可看到已 commit 但未 checkpoint 的过渡态(修复旧 WASM 内存库裸读 .db
|
|
232
|
+
* 看不见 WAL 的过渡态可见性窗口)。db 缺失/损坏由 open 异常自然触发 fail-closed(外层 catch)。
|
|
235
233
|
*
|
|
236
234
|
* 环境假设(R-09):WAL 模式下只读连接仍需在 .runtime/ 目录建/更新 -shm 索引文件,故 .runtime
|
|
237
235
|
* 必须可写(与主进程一致)——“只读”指不改 DB 数据,不等于不对 -shm 加索引。
|
|
238
236
|
*
|
|
239
|
-
* fail-closed:
|
|
240
|
-
*
|
|
241
|
-
|
|
237
|
+
* fail-closed:db 文件不存在 / 子进程打开、查询异常 / 子进程超时或崩溃——一律 console.warn
|
|
238
|
+
* (含 e.stderr 详情)并返回 null,调用方(readCurrentStage/isNoWorktreeMode)对 null 走
|
|
239
|
+
* fail-closed,禁止 fail-open。
|
|
242
240
|
*/
|
|
243
241
|
function queryDbFirstCell(cwd, sql) {
|
|
244
242
|
const dbPath = path.join(cwd, '.sillyspec', '.runtime', 'sillyspec.db')
|
|
@@ -248,21 +246,15 @@ function queryDbFirstCell(cwd, sql) {
|
|
|
248
246
|
console.warn('⚠️ sillyspec.db 不存在,hook 降级 fail-closed: ' + dbPath)
|
|
249
247
|
return null
|
|
250
248
|
}
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
libPath = createRequire(import.meta.url).resolve('better-sqlite3')
|
|
254
|
-
} catch (e) {
|
|
255
|
-
console.warn('⚠️ sillyspec.db 查询失败(better-sqlite3 未解析),hook 降级 fail-closed: ' + ((e && e.message) || e))
|
|
256
|
-
return null
|
|
257
|
-
}
|
|
258
|
-
// 子进程同步 require 绝对路径(better-sqlite3 是同步原生 API,无 async/import().then 包装)。
|
|
259
|
-
// pluck().get() 取第一行第一列原值,无行返回 undefined(写空 stdout → 父侧 trim 后 null)。
|
|
249
|
+
// 子进程同步内置 require('node:sqlite')(DatabaseSync 是同步 API,无 async/import().then 包装)。
|
|
250
|
+
// prepare(sql).get() 取第一行整行后取首列值,无行返回 undefined(写空 stdout → 父侧 trim 后 null)。
|
|
260
251
|
const script =
|
|
261
|
-
"const
|
|
252
|
+
"const {DatabaseSync}=require('node:sqlite')," +
|
|
262
253
|
"dbPath=" + JSON.stringify(dbPath) + ",sql=" + JSON.stringify(sql) + ";" +
|
|
263
254
|
"let db;" +
|
|
264
|
-
"try{db=new
|
|
265
|
-
"const
|
|
255
|
+
"try{db=new DatabaseSync(dbPath,{readOnly:true});" +
|
|
256
|
+
"const row=db.prepare(sql).get();" +
|
|
257
|
+
"const v=row===undefined?undefined:Object.values(row)[0];" +
|
|
266
258
|
"if(v!==undefined)process.stdout.write(String(v??''));db.close()}" +
|
|
267
259
|
"catch(e){process.stderr.write('hook db query failed: '+String(e&&e.message||e));process.exit(1)};"
|
|
268
260
|
try {
|
|
@@ -270,7 +262,7 @@ function queryDbFirstCell(cwd, sql) {
|
|
|
270
262
|
encoding: 'utf8', timeout: 5000, stdio: ['ignore', 'pipe', 'pipe'],
|
|
271
263
|
}).trim() || null
|
|
272
264
|
} catch (e) {
|
|
273
|
-
// DB 查询失败(db 损坏 /
|
|
265
|
+
// DB 查询失败(db 损坏 / readOnly 打开失败 / 子进程超时或崩溃):降级返回 null,调用方
|
|
274
266
|
//(readCurrentStage 等)对 null 走 fail-closed。补 warn 让 doctor/agent 能看到根因,
|
|
275
267
|
// 而非完全静默(子进程把诊断写到了 stderr,execFileSync 抛错时挂在 e.stderr 上)。
|
|
276
268
|
const detail = (e && e.stderr ? String(e.stderr).trim() : '') || (e && e.message) || 'timeout/crash'
|
|
@@ -291,7 +283,7 @@ export {
|
|
|
291
283
|
* @returns {string|null} 阶段名,null 表示无法确定
|
|
292
284
|
*/
|
|
293
285
|
function readCurrentStage(cwd) {
|
|
294
|
-
// 直读 sillyspec.db(node +
|
|
286
|
+
// 直读 sillyspec.db(node + node:sqlite 只读子进程,不依赖外部 sqlite3 CLI——Windows 默认没有)
|
|
295
287
|
const v = queryDbFirstCell(cwd, "SELECT current_stage FROM changes WHERE status='active' AND current_stage IN ('execute','quick') ORDER BY last_active DESC LIMIT 1")
|
|
296
288
|
return v || null
|
|
297
289
|
}
|
|
@@ -302,7 +294,7 @@ function readCurrentStage(cwd) {
|
|
|
302
294
|
* @returns {boolean}
|
|
303
295
|
*/
|
|
304
296
|
function isNoWorktreeMode(cwd) {
|
|
305
|
-
// 直读 sillyspec.db(node +
|
|
297
|
+
// 直读 sillyspec.db(node + node:sqlite,同 readCurrentStage)
|
|
306
298
|
return queryDbFirstCell(cwd, "SELECT no_worktree FROM changes WHERE status='active' AND current_stage IN ('execute','quick') LIMIT 1") === '1'
|
|
307
299
|
}
|
|
308
300
|
|
|
@@ -67,6 +67,34 @@ export class ChangeRegistry {
|
|
|
67
67
|
}
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
+
/**
|
|
71
|
+
* 更新变更的人类可读元信息(title / quicklog_id),让 quick-<hex> 行可读、DB↔QUICKLOG 可对账。
|
|
72
|
+
* quick 启动时回填(title 从任务描述、quicklog_id 用分配的 qlId);--done 时从 step3「需求:」刷新 title。
|
|
73
|
+
* 部分更新(只传 title 不动 quicklog_id,反之亦然)。不调 _touchLocalModified:title/quicklog_id 是
|
|
74
|
+
* 本地展示用元信息,纳入脏度会扰动平台同步(平台 changes 表无此列)。
|
|
75
|
+
* @param {string} cwd
|
|
76
|
+
* @param {string} changeName
|
|
77
|
+
* @param {{ title?: string, quicklogId?: string }} meta
|
|
78
|
+
*/
|
|
79
|
+
updateChangeMeta(cwd, changeName, meta) {
|
|
80
|
+
if (!changeName || !meta) return;
|
|
81
|
+
const db = this.pm._ensureDB(cwd);
|
|
82
|
+
try {
|
|
83
|
+
db.transaction(() => {
|
|
84
|
+
const sqlDb = db.getDb();
|
|
85
|
+
const sets = [];
|
|
86
|
+
const params = [];
|
|
87
|
+
if (meta.title !== undefined) { sets.push('title = ?'); params.push(meta.title); }
|
|
88
|
+
if (meta.quicklogId !== undefined) { sets.push('quicklog_id = ?'); params.push(meta.quicklogId); }
|
|
89
|
+
if (sets.length === 0) return;
|
|
90
|
+
params.push(changeName);
|
|
91
|
+
sqlDb.prepare(`UPDATE changes SET ${sets.join(', ')} WHERE name = ?`).run(...params);
|
|
92
|
+
});
|
|
93
|
+
} catch (err) {
|
|
94
|
+
console.warn('⚠️ 更新 change 元信息失败:', err.message);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
70
98
|
/**
|
|
71
99
|
* 读取变更的隔离状态
|
|
72
100
|
* @param {string} cwd - 项目根目录
|
package/src/progress/shared.js
CHANGED
|
@@ -27,7 +27,7 @@ export const STAGE_ORDER = ['scan', 'brainstorm', 'plan', 'execute', 'verify', '
|
|
|
27
27
|
export const MAIN_FLOW_ORDER = ['brainstorm', 'plan', 'execute', 'verify', 'archive'];
|
|
28
28
|
|
|
29
29
|
// progress 数据版本(v3 = SQLite;v4 = changes 加 base_ts / 本地脏度两列,platform-sync §8)
|
|
30
|
-
export const CURRENT_VERSION =
|
|
30
|
+
export const CURRENT_VERSION = 5;
|
|
31
31
|
|
|
32
32
|
// 空阶段骨架
|
|
33
33
|
export function emptyStage() {
|
package/src/progress.js
CHANGED
|
@@ -347,7 +347,7 @@ export class ProgressManager {
|
|
|
347
347
|
}
|
|
348
348
|
|
|
349
349
|
const result = {
|
|
350
|
-
_version:
|
|
350
|
+
_version: 5,
|
|
351
351
|
project: projectName,
|
|
352
352
|
currentChange: cName,
|
|
353
353
|
currentStage: currentStage || '',
|
|
@@ -776,6 +776,8 @@ export class ProgressManager {
|
|
|
776
776
|
|
|
777
777
|
unregisterChange(cwd, changeName) { return this._changeRegistry.unregisterChange(cwd, changeName); }
|
|
778
778
|
|
|
779
|
+
updateChangeMeta(cwd, changeName, meta) { return this._changeRegistry.updateChangeMeta(cwd, changeName, meta); }
|
|
780
|
+
|
|
779
781
|
// ── CLI 命令 ──
|
|
780
782
|
|
|
781
783
|
init(cwd) {
|
|
@@ -815,7 +817,7 @@ export class ProgressManager {
|
|
|
815
817
|
* 初始化指定变更的 progress
|
|
816
818
|
* SQL: INSERT changes + 批量 INSERT stages
|
|
817
819
|
*/
|
|
818
|
-
initChange(cwd, changeName) {
|
|
820
|
+
initChange(cwd, changeName, meta = {}) {
|
|
819
821
|
if (!changeName) {
|
|
820
822
|
console.warn('⚠️ initChange: changeName 为空,跳过');
|
|
821
823
|
return null;
|
|
@@ -836,9 +838,9 @@ export class ProgressManager {
|
|
|
836
838
|
if (existing === undefined) {
|
|
837
839
|
// 插入 changes 行
|
|
838
840
|
sqlDb.prepare(
|
|
839
|
-
`INSERT INTO changes (name, current_stage, status, created_at, last_active)
|
|
840
|
-
VALUES (?, 'scan', 'active', ?, ?)`
|
|
841
|
-
).run(changeName, now, now);
|
|
841
|
+
`INSERT INTO changes (name, current_stage, status, created_at, last_active, title, quicklog_id)
|
|
842
|
+
VALUES (?, 'scan', 'active', ?, ?, ?, ?)`
|
|
843
|
+
).run(changeName, now, now, meta.title || null, meta.quicklogId || null);
|
|
842
844
|
}
|
|
843
845
|
|
|
844
846
|
// 获取 change_id
|
package/src/quicklog.js
CHANGED
|
@@ -75,7 +75,7 @@ function nowDatetime(d = new Date()) {
|
|
|
75
75
|
}
|
|
76
76
|
|
|
77
77
|
// 描述压成一行、限长,避免破坏 QUICKLOG 条目结构
|
|
78
|
-
function sanitizeDesc(description) {
|
|
78
|
+
export function sanitizeDesc(description) {
|
|
79
79
|
const s = String(description || '').replace(/[\r\n]+/g, ' ').trim()
|
|
80
80
|
if (!s) return '(quick 任务)'
|
|
81
81
|
return s.length > 120 ? s.slice(0, 120) + '…' : s
|
|
@@ -101,7 +101,7 @@ export function deriveTitleFromLinkedChange(specBase, change) {
|
|
|
101
101
|
|
|
102
102
|
// 从 quick step3 --output 四字段提取「需求:」摘要,用于翻完成时刷新标题行
|
|
103
103
|
// (覆盖启动时的占位/弱标题)。提取失败返回 ''(不刷新)。
|
|
104
|
-
function extractTitleFromResult(result) {
|
|
104
|
+
export function extractTitleFromResult(result) {
|
|
105
105
|
if (!result) return ''
|
|
106
106
|
const m = String(result).match(/需求:([^\n\r]*?)(?:\s+根因:|$)/)
|
|
107
107
|
if (!m) return ''
|
|
@@ -30,7 +30,7 @@ import { detectConcurrentChanges, formatConcurrentWarning } from './concurrent-d
|
|
|
30
30
|
import { stageRegistry } from '../stages/index.js'
|
|
31
31
|
import { SCAN_STATUS, POINTER_STATUS } from '../constants.js'
|
|
32
32
|
import { printQuickAuditReview } from './quick-audit.js'
|
|
33
|
-
import { validateQuickResult, allocateQuicklogEntry, findQuicklogEntry, completeQuicklogEntry } from '../quicklog.js'
|
|
33
|
+
import { validateQuickResult, allocateQuicklogEntry, findQuicklogEntry, completeQuicklogEntry, extractTitleFromResult } from '../quicklog.js'
|
|
34
34
|
import { getRule } from '../stage-contract-spec.js'
|
|
35
35
|
import { archiveDestDirName } from '../stage-contract.js'
|
|
36
36
|
|
|
@@ -774,6 +774,12 @@ export async function handleQuickStageCompletion({ stageName, steps, currentIdx,
|
|
|
774
774
|
changedFiles: realFiles,
|
|
775
775
|
})
|
|
776
776
|
console.log(`📝 QUICKLOG 条目 ${qlId} 已标记完成`)
|
|
777
|
+
// 刷新 DB title:从 step3「需求:」提取(agent 可改 title 的途径),覆盖启动时的兜底快照。
|
|
778
|
+
// 与 QUICKLOG 标题刷新(flipEntryInContent 内 extractTitleFromResult)同源,保持 DB↔QUICKLOG 一致。
|
|
779
|
+
try {
|
|
780
|
+
const refinedTitle = extractTitleFromResult(outputText || '');
|
|
781
|
+
if (refinedTitle) pm.updateChangeMeta(cwd, changeName, { title: refinedTitle });
|
|
782
|
+
} catch { /* title 刷新失败不阻断完成 */ }
|
|
777
783
|
} catch (e) {
|
|
778
784
|
console.warn(`⚠️ QUICKLOG 完成态写入失败: ${e.message}`)
|
|
779
785
|
}
|
package/src/run/stage.js
CHANGED
|
@@ -22,7 +22,7 @@ import { writeAtomicSync } from '../fs-atomic.js'
|
|
|
22
22
|
import { resolveSpecDir, resolveChangeDir, resolveRuntimeRoot, resolveQuickSessionsDir, triggerSync, safeGit, parsePorcelainPath, formatWaitOptions, checkApproval, getStageSteps } from './shared.js'
|
|
23
23
|
import { computeScanProfile, applyScanProfileSteps, executeScanPreflight, executeScanPostcheck } from './scan-profile.js'
|
|
24
24
|
import { outputStep } from './prompt.js'
|
|
25
|
-
import { allocateQuicklogEntry, deriveTitleFromLinkedChange } from '../quicklog.js'
|
|
25
|
+
import { allocateQuicklogEntry, deriveTitleFromLinkedChange, sanitizeDesc } from '../quicklog.js'
|
|
26
26
|
import { checkTransition } from '../stage-contract.js'
|
|
27
27
|
import { completeStageGates } from './gates.js'
|
|
28
28
|
|
|
@@ -288,6 +288,11 @@ export async function runStage(pm, progress, stageName, cwd, changeName, skipApp
|
|
|
288
288
|
if (allowNew) parts.push('允许新增文件')
|
|
289
289
|
console.log(`🛡️ quick 变更边界已记录: ${parts.join(', ')}`)
|
|
290
290
|
console.log(`📝 QUICKLOG 条目已创建: ${qlId}`)
|
|
291
|
+
// 回填 DB changes 行的 title + quicklog_id,让 quick-<hex> 可读、DB↔QUICKLOG 可对账。
|
|
292
|
+
// title 用 quickDesc(任务描述或关联变更标题)经 sanitizeDesc 压一行限长,与 QUICKLOG 条目标题同源。
|
|
293
|
+
try {
|
|
294
|
+
pm.updateChangeMeta(cwd, changeName, { title: sanitizeDesc(quickDesc), quicklogId: qlId });
|
|
295
|
+
} catch { /* 回填失败不阻断 quick 启动 */ }
|
|
291
296
|
pm._write(cwd, progress, changeName)
|
|
292
297
|
} catch (e) {
|
|
293
298
|
console.warn(`⚠️ baseline 记录失败: ${e.message}`)
|