sillyspec 3.26.0 → 3.26.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.
@@ -3,5 +3,5 @@
3
3
  "runtimeRoot": null,
4
4
  "workspaceId": null,
5
5
  "scanRunId": null,
6
- "savedAt": "2026-08-10T17:33:41.549Z"
6
+ "savedAt": "2026-08-11T08:03:50.313Z"
7
7
  }
package/README.md CHANGED
@@ -59,12 +59,10 @@ npx sillyspec init --interactive # 完整引导
59
59
 
60
60
  ## 平台支持
61
61
 
62
- SillySpec 进度库使用 SQLite 持久化(基于 better-sqlite3 原生绑定)。安装支持情况如下:
62
+ SillySpec 进度库使用 SQLite 持久化(基于 Node.js 内置的 `node:sqlite` 模块,纯 JS 无原生编译依赖)。
63
63
 
64
- - **主流平台(prebuilt 零编译安装)**:Node.js >= 18 的 Linux x64、macOS x64 / macOS arm64、Windows x64。安装时直接下载预编译二进制,无需本地 C++ 工具链。
65
- - **边缘平台不保证**:Alpine(musl libc)、Windows arm64、BSD 等平台可能没有对应的 prebuilt 二进制,安装时可能触发 node-gyp 从源码编译(此时需要本机具备 C++ 编译工具链)。
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.0",
3
+ "version": "3.26.2",
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": ">=18"
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",
@@ -5,6 +5,10 @@
5
5
  * 供 auto 模式决定内部流程深度。
6
6
  */
7
7
 
8
+ import { existsSync, readFileSync } from 'fs'
9
+ import { join } from 'path'
10
+ import jsYaml from 'js-yaml'
11
+
8
12
  /**
9
13
  * 用户显式指定的关键词 → 强制模式
10
14
  */
@@ -24,12 +28,36 @@ const FORCE_QUICK_PATTERNS = [
24
28
  /修复.*\s*\bbug\b.*\bfix\b/i,
25
29
  ]
26
30
 
31
+ /**
32
+ * 读 local.yaml 的 auto_mode 段(best-effort,绝不抛)。
33
+ * 供 runCommand 接线:传给 classifyChange 的 localConfig,让 force_*_patterns 真生效。
34
+ * @param {string} cwd - 主仓库根(local.yaml 在 <cwd>/.sillyspec/local.yaml)
35
+ * @returns {{ force_full_patterns?: string[], force_quick_patterns?: string[] } | null}
36
+ * auto_mode 段含至少一个 string pattern → 归一化数组;文件缺/解析失败/无段/空 → null(降级默认关键词)
37
+ */
38
+ export function readAutoModeFromLocalYaml(cwd) {
39
+ try {
40
+ const p = join(cwd, '.sillyspec', 'local.yaml')
41
+ if (!existsSync(p)) return null
42
+ const doc = jsYaml.load(readFileSync(p, 'utf8'))
43
+ if (!doc || typeof doc !== 'object') return null
44
+ const am = doc.auto_mode
45
+ if (!am || typeof am !== 'object') return null
46
+ const ffp = Array.isArray(am.force_full_patterns) ? am.force_full_patterns.filter(s => typeof s === 'string') : []
47
+ const fqp = Array.isArray(am.force_quick_patterns) ? am.force_quick_patterns.filter(s => typeof s === 'string') : []
48
+ if (ffp.length === 0 && fqp.length === 0) return null
49
+ return { force_full_patterns: ffp, force_quick_patterns: fqp }
50
+ } catch {
51
+ return null
52
+ }
53
+ }
54
+
27
55
  /**
28
56
  * 分类变更规模
29
57
  * @param {object} opts
30
58
  * @param {string} opts.description - 用户需求描述
31
59
  * @param {string} [opts.explicitMode] - 用户显式指定的模式(auto/quick/full)
32
- * @param {object} [opts.localConfig] - local.yaml 中的 auto_mode 配置
60
+ * @param {object} [opts.localConfig] - local.yaml auto_mode 段(由 readAutoModeFromLocalYaml 读出)
33
61
  * @returns {{ mode: 'quick'|'auto'|'full', reason: string }}
34
62
  */
35
63
  export function classifyChange({ description = '', explicitMode, localConfig } = {}) {
@@ -44,13 +72,21 @@ export function classifyChange({ description = '', explicitMode, localConfig } =
44
72
  const forceQuickPatterns = localConfig.force_quick_patterns || []
45
73
 
46
74
  for (const pattern of forceFullPatterns) {
47
- if (new RegExp(pattern, 'i').test(description)) {
48
- return { mode: 'full', reason: `local.yaml force_full_pattern 匹配: ${pattern}` }
75
+ try {
76
+ if (new RegExp(pattern, 'i').test(description)) {
77
+ return { mode: 'full', reason: `local.yaml force_full_pattern 匹配: ${pattern}` }
78
+ }
79
+ } catch {
80
+ // 非法正则跳过,不崩 auto 分类(review-2026-08-09 #30)
49
81
  }
50
82
  }
51
83
  for (const pattern of forceQuickPatterns) {
52
- if (new RegExp(pattern, 'i').test(description)) {
53
- return { mode: 'quick', reason: `local.yaml force_quick_pattern 匹配: ${pattern}` }
84
+ try {
85
+ if (new RegExp(pattern, 'i').test(description)) {
86
+ return { mode: 'quick', reason: `local.yaml force_quick_pattern 匹配: ${pattern}` }
87
+ }
88
+ } catch {
89
+ // 非法正则跳过
54
90
  }
55
91
  }
56
92
  }
@@ -0,0 +1,287 @@
1
+ /**
2
+ * config-schema.js — local.yaml 配置键的单一数据源。
3
+ *
4
+ * 定位:`.sillyspec/local.yaml` 是 gitignored 的机器+人混写配置,键散落在 ~10 个 reader
5
+ * 里(detect / verify-postcheck / sync / dispatch / worktree-guard / classify-change …),
6
+ * 外部项目的 agent 无从得知有哪些键、哪些生效。本模块把「全部已知键 + 生效状态 + 读取点」
7
+ * 集中成一张表,供:
8
+ * - `sillyspec config schema`(人类可读树 / --json 机读)打印;
9
+ * - `sillyspec init` 调 renderExample() 落盘脱敏 local.yaml.example 给人/外部 agent 看。
10
+ *
11
+ * 单一真相 = LOCAL_YAML_SCHEMA。renderExample() 是策展模板(YAML 形态各异:嵌套 / inline
12
+ * flow / 顶层标量,自动生成不如策展可读),由测试「每个 live 键路径必出现于 example 文本」
13
+ * 耦合保证不漂移——加键忘 example 则测试红。
14
+ *
15
+ * reader 引用一律用符号名(readMcpConfig / extractModules),**不用行号**——行号随重构漂移,
16
+ * 符号名稳定可 grep。
17
+ *
18
+ * status 语义:
19
+ * - live 真有 reader 从 local.yaml 读并起效(配了即生效)。
20
+ * - declared 代码/JSDoc/prompt 声明过,但无 reader 真读 local.yaml(或调用方未传),配了不生效。
21
+ * 诚实标出,避免 agent 白配 + 暴露待接线债。
22
+ */
23
+
24
+ /**
25
+ * @typedef {Object} SchemaKey
26
+ * @property {string} path 点分路径(mcp.url / worktree-hook.readonlyCommands)
27
+ * @property {string} type string|enum|integer|object|array|scalar
28
+ * @property {string[]} [values] enum 取值
29
+ * @property {boolean} [required] 凭据类必填(两键齐全才视为有效源)
30
+ * @property {boolean} [secret] 敏感凭据,example 用占位符
31
+ * @property {boolean} [optional] 可选键
32
+ * @property {string} status live|declared
33
+ * @property {string[]} readers 读取点符号名(稳定可 grep),declared 键留空数组
34
+ * @property {string} desc 一句话说明
35
+ * @property {string} example 脱敏示例值(secret 键用占位符)
36
+ *
37
+ * @typedef {Object} SchemaSection
38
+ * @property {string} id
39
+ * @property {string} title
40
+ * @property {string} note 段说明(写入来源 / 注意事项)
41
+ * @property {SchemaKey[]} keys
42
+ */
43
+
44
+ /** @type {{ file: string, note: string, sections: SchemaSection[] }} */
45
+ export const LOCAL_YAML_SCHEMA = {
46
+ file: '.sillyspec/local.yaml',
47
+ note: 'gitignored(含敏感凭据勿提交);脱敏示例 local.yaml.example 由 sillyspec init 生成(可提交)。',
48
+ sections: [
49
+ {
50
+ id: 'project',
51
+ title: '项目类型',
52
+ note: '由 sillyspec local detect 纯 fs 嗅探写入,一般无需手填。',
53
+ keys: [
54
+ {
55
+ path: 'project.type',
56
+ type: 'enum',
57
+ values: ['nodejs', 'maven', 'gradle', 'make', 'generic'],
58
+ status: 'live',
59
+ readers: ['detectLocalYaml (src/local-detect.js)', 'extractProjectType (src/worktree-deps.js)'],
60
+ desc: '项目类型,决定默认 commands 嗅探与 worktree 依赖策略。',
61
+ example: 'nodejs',
62
+ },
63
+ ],
64
+ },
65
+ {
66
+ id: 'commands',
67
+ title: '构建/测试/lint 命令',
68
+ note: 'detect 核验 package.json(或对应构建文件) scripts 存在性后才写对应键;缺失则不写。agent 可改。',
69
+ keys: [
70
+ { path: 'commands.build', type: 'string', optional: true, status: 'live', readers: ['detectLocalYaml (src/local-detect.js)', 'validateCommands (src/scan-postcheck.js)'], desc: '构建命令。', example: 'npm run build' },
71
+ { path: 'commands.test', type: 'string', optional: true, status: 'live', readers: ['extractTestCommand (src/verify-postcheck.js)', 'validateCommands (src/scan-postcheck.js)'], desc: '测试命令——verify 阶段 CLI 亲自执行此命令与 verify-result.md 对账,实测失败即阻断。', example: 'npm test' },
72
+ { path: 'commands.lint', type: 'string', optional: true, status: 'live', readers: ['detectLocalYaml (src/local-detect.js)', 'validateCommands (src/scan-postcheck.js)'], desc: 'lint 命令。', example: 'npm run lint' },
73
+ { path: 'commands.install', type: 'string', optional: true, status: 'live', readers: ['extractInstallCommand (src/worktree-deps.js)'], desc: '依赖安装命令(worktree 依赖 provisioning 用)。', example: 'npm install' },
74
+ ],
75
+ },
76
+ {
77
+ id: 'mcp',
78
+ title: 'SillyHub MCP 客户端凭据(派发用)',
79
+ note: 'agent 手填,或 sillyspec platform connect 在 mcp 段缺失时同源自动填(§7.4)。两键齐全才生效,否则回退 env SILLYHUB_MCP_URL/TOKEN。',
80
+ keys: [
81
+ { path: 'mcp.url', type: 'string', required: true, secret: true, status: 'live', readers: ['readMcpConfig (src/sillyhub-mcp/config.js)'], desc: 'SillyHub MCP 服务地址(尾斜杠自动归一)。', example: 'https://your-sillyhub.example.com' },
82
+ { path: 'mcp.token', type: 'string', required: true, secret: true, status: 'live', readers: ['readMcpConfig (src/sillyhub-mcp/config.js)'], desc: 'SillyHub MCP 认证 token。', example: '<your-mcp-token>' },
83
+ ],
84
+ },
85
+ {
86
+ id: 'platform',
87
+ title: 'SillyHub 平台同步',
88
+ note: 'sillyspec platform connect 写入(含 ping 验活);手填亦可。user/last_connected 可空或自动写。',
89
+ keys: [
90
+ { path: 'platform.url', type: 'string', required: true, secret: true, status: 'live', readers: ['SyncManager._getPlatform (src/sync.js)'], desc: 'SillyHub 平台地址。', example: 'https://your-sillyhub.example.com' },
91
+ { path: 'platform.token', type: 'string', required: true, secret: true, status: 'live', readers: ['SyncManager._getPlatform (src/sync.js)'], desc: '平台认证 token(Bearer)。', example: '<your-platform-token>' },
92
+ { path: 'platform.user', type: 'string', optional: true, status: 'live', readers: ['resolvePlatformUser / SyncManager.connect (src/sync.js)'], desc: '推送者身份;缺则回退 git user.name / env。推送时进 X-SillySpec-User header。', example: 'alice' },
93
+ { path: 'platform.last_connected', type: 'string', optional: true, status: 'live', readers: ['SyncManager.connect (src/sync.js)'], desc: 'connect 成功时间戳(ISO),自动写,勿手填。', example: '2026-08-11T00:00:00.000Z' },
94
+ ],
95
+ },
96
+ {
97
+ id: 'dispatch',
98
+ title: '派发调参',
99
+ note: '仅在确知调优值时填,否则留默认。',
100
+ keys: [
101
+ { path: 'dispatch.probe_ttl_ms', type: 'integer', optional: true, status: 'live', readers: ['readProbeTtlFromLocalYaml (src/dispatch/probe.js)'], desc: '派发探测负面缓存 TTL(毫秒,默认 60000)。daemon 抖动期避免反复探测。', example: '60000' },
102
+ { path: 'dispatch.poll_interval_ms', type: 'integer', optional: true, status: 'declared', readers: [], desc: '【路径A 预留·未落地】renderSillyHubInstruction 注入的轮询文本提及,但 isPathASupported()=false 该指令当前不注入;路径A 落地后接线,配了暂不生效。', example: '15000' },
103
+ { path: 'dispatch.worker_timeout_ms', type: 'integer', optional: true, status: 'declared', readers: [], desc: '【路径A 预留·未落地】同 poll_interval_ms,路径A 落地后接线,配了暂不生效。', example: '60000' },
104
+ ],
105
+ },
106
+ {
107
+ id: 'modules',
108
+ title: 'monorepo 子模块映射',
109
+ note: 'test_strategy: module 时,按 git diff 命中的模块子集收窄测试。只支持 inline flow 形态(extractModules 不解析嵌套展开式)。',
110
+ keys: [
111
+ { path: 'modules.<name>.path', type: 'string', optional: true, status: 'live', readers: ['extractModules (src/verify-postcheck.js)', 'parseLocalYamlModules (src/plan-postcheck.js)', 'extractModulePaths (src/worktree-deps.js)'], desc: '子模块目录路径(相对仓库根)。', example: 'frontend/' },
112
+ { path: 'modules.<name>.test', type: 'string', optional: true, status: 'live', readers: ['extractModules (src/verify-postcheck.js)'], desc: '该子模块的测试命令。', example: 'cd frontend && pnpm test' },
113
+ ],
114
+ },
115
+ {
116
+ id: 'test_strategy',
117
+ title: '测试策略',
118
+ note: 'verify 阶段 CLI 对账的收窄策略。',
119
+ keys: [
120
+ { path: 'test_strategy', type: 'enum', values: ['full', 'module'], optional: true, status: 'live', readers: ['extractTestStrategy (src/verify-postcheck.js)'], desc: 'full=全量 commands.test;module=按 git diff 命中 modules 子集收窄(需配 modules)。缺省=全量。', example: 'full' },
121
+ ],
122
+ },
123
+ {
124
+ id: 'known_failures',
125
+ title: '预存失败豁免清单',
126
+ note: '变更前就失败的测试行,豁免后不计入 verify 阻断。支持块列表或 inline 数组。务必定期复核,避免误豁免本变更引入的真实失败。',
127
+ keys: [
128
+ { path: 'known_failures', type: 'array', optional: true, status: 'live', readers: ['extractKnownFailures (src/verify-postcheck.js)'], desc: '失败行模式列表(匹配测试输出失败行)。', example: 'tests/some-old-test' },
129
+ ],
130
+ },
131
+ {
132
+ id: 'worktree-hook',
133
+ title: 'worktree-guard 扩展',
134
+ note: 'execute worktree 隔离期内放行的额外只读命令。键名用 camelCase(parseSimpleYaml 原样保留)。',
135
+ keys: [
136
+ { path: 'worktree-hook.readonlyCommands', type: 'array', optional: true, status: 'live', readers: ['loadLocalConfig (src/hooks/worktree-guard.js)'], desc: '额外放行的只读命令名列表(如 rg/fd),绕过 worktree-guard 写操作拦截。', example: 'rg' },
137
+ ],
138
+ },
139
+ {
140
+ id: 'auto_mode',
141
+ title: '变更规模自动分类',
142
+ note: 'sillyspec run auto 时,readAutoModeFromLocalYaml 读本段传 classifyChange 的 localConfig,force_*_patterns 匹配需求描述则强制对应模式。',
143
+ keys: [
144
+ { path: 'auto_mode.force_full_patterns', type: 'array', optional: true, status: 'live', readers: ['readAutoModeFromLocalYaml + classifyChange (src/classify-change.js)', 'runCommand (src/run/command.js)'], desc: '需求描述匹配任一正则(i 大小写无关)→ 强制 full 流程。非法正则跳过不崩。', example: '数据库|migration' },
145
+ { path: 'auto_mode.force_quick_patterns', type: 'array', optional: true, status: 'live', readers: ['readAutoModeFromLocalYaml + classifyChange (src/classify-change.js)', 'runCommand (src/run/command.js)'], desc: '需求描述匹配任一正则 → 强制 quick。非法正则跳过不崩。', example: 'fix typo' },
146
+ ],
147
+ },
148
+ ],
149
+ };
150
+
151
+ /** 全部键(拍平),便于 JSON 输出与 example 耦合测试。 */
152
+ export function flatKeys() {
153
+ return LOCAL_YAML_SCHEMA.sections.flatMap((s) => s.keys.map((k) => ({ section: s.id, ...k })));
154
+ }
155
+
156
+ /** 人类可读树形 schema 文本。live 在前、declared 在后,段内逐键列 path/type/约束/读取点/说明。 */
157
+ export function renderSchemaHuman() {
158
+ const L = [];
159
+ L.push('SillySpec local.yaml 配置清单');
160
+ L.push(`文件:${LOCAL_YAML_SCHEMA.file} — ${LOCAL_YAML_SCHEMA.note}`);
161
+ L.push('数据源:src/config-schema.js(本表唯一真相;reader 见各键「读取点」,符号名可 grep)');
162
+ L.push('');
163
+
164
+ const all = flatKeys();
165
+ const live = all.filter((k) => k.status === 'live');
166
+ const declared = all.filter((k) => k.status === 'declared');
167
+
168
+ L.push(`━━━ 生效(${live.length} 键,配了即生效) ━━━`);
169
+ L.push('');
170
+ for (const s of LOCAL_YAML_SCHEMA.sections) {
171
+ const keys = s.keys.filter((k) => k.status === 'live');
172
+ if (keys.length === 0) continue;
173
+ L.push(`[${s.id}] ${s.title}`);
174
+ L.push(` ${s.note}`);
175
+ for (const k of keys) {
176
+ L.push(` ${k.path}${formatConstraint(k)} ${k.desc}`);
177
+ if (k.readers.length) L.push(` 读取点:${k.readers.join(';')}`);
178
+ }
179
+ L.push('');
180
+ }
181
+
182
+ L.push(`━━━ 声明但未接线(${declared.length} 键,配了不生效 / 待接线债) ━━━`);
183
+ L.push('');
184
+ for (const s of LOCAL_YAML_SCHEMA.sections) {
185
+ const keys = s.keys.filter((k) => k.status === 'declared');
186
+ if (keys.length === 0) continue;
187
+ L.push(`[${s.id}] ${s.title}`);
188
+ L.push(` ${s.note}`);
189
+ for (const k of keys) {
190
+ L.push(` ${k.path}${formatConstraint(k)} ${k.desc}`);
191
+ }
192
+ L.push('');
193
+ }
194
+ return L.join('\n');
195
+ }
196
+
197
+ function formatConstraint(k) {
198
+ const tags = [];
199
+ if (k.required) tags.push('必填');
200
+ if (k.optional) tags.push('可选');
201
+ if (k.secret) tags.push('凭据');
202
+ if (k.type === 'enum' && k.values) tags.push(k.values.join('|'));
203
+ else tags.push(k.type);
204
+ return tags.length ? ` [${tags.join(' · ')}]` : '';
205
+ }
206
+
207
+ /** 机读 JSON(程序化消费)。结构 = LOCAL_YAML_SCHEMA 本体 + flatKeys 视图。 */
208
+ export function renderSchemaJson() {
209
+ return JSON.stringify({
210
+ file: LOCAL_YAML_SCHEMA.file,
211
+ note: LOCAL_YAML_SCHEMA.note,
212
+ sections: LOCAL_YAML_SCHEMA.sections,
213
+ keys: flatKeys().map(({ section, path, type, values, required, optional, secret, status, readers, desc }) => ({
214
+ section, path, type, values, required, optional, secret, status, readers, desc,
215
+ })),
216
+ }, null, 2);
217
+ }
218
+
219
+ /**
220
+ * 脱敏 local.yaml 示例文本(sillyspec init 落盘用)。
221
+ * 策展模板(非自动生成):YAML 形态各异(嵌套/inline flow/顶层标量),策展可读性远胜自动拼。
222
+ * 与 SCHEMA 的耦合由测试保证:每个 live 键路径必出现于本文本。
223
+ */
224
+ export function renderExample() {
225
+ return `# SillySpec local.yaml — 脱敏示例(由 sillyspec init 生成,可提交)
226
+ # 真实配置文件 .sillyspec/local.yaml 是 gitignored(含敏感凭据)。
227
+ # 用法:复制本文件为 .sillyspec/local.yaml,填真实值;或直接 sillyspec platform connect 自动写凭据段。
228
+ # 全部键与生效状态:sillyspec config schema
229
+
230
+ # ── 项目类型(sillyspec local detect 自动写入,一般无需手填)──
231
+ project:
232
+ type: nodejs # nodejs | maven | gradle | make | generic
233
+
234
+ # ── 构建/测试/lint 命令(detect 核验 scripts 存在性后才写;缺失键省略)──
235
+ commands:
236
+ build: npm run build
237
+ test: npm test
238
+ lint: npm run lint
239
+ # install: npm install # worktree 依赖安装命令
240
+
241
+ # ── SillyHub MCP 客户端凭据(派发到 worker 用)──
242
+ # agent 手填,或 sillyspec platform connect 在 mcp 段缺失时同源自动填。
243
+ # 两键齐全才生效;缺则回退环境变量 SILLYHUB_MCP_URL / SILLYHUB_MCP_TOKEN。
244
+ mcp:
245
+ url: https://your-sillyhub.example.com
246
+ token: <your-mcp-token>
247
+
248
+ # ── SillyHub 平台同步(sillyspec platform connect 写入,含 ping 验活)──
249
+ platform:
250
+ url: https://your-sillyhub.example.com
251
+ token: <your-platform-token>
252
+ user: alice # 推送者身份(可空,回退 git user.name / env)
253
+ # last_connected: 2026-08-11T00:00:00.000Z # connect 自动写,勿手填
254
+
255
+ # ── 派发调参(仅确知调优值时填,否则留默认)──
256
+ dispatch:
257
+ probe_ttl_ms: 60000 # 派发探测负面缓存 TTL(默认 60000)
258
+ # poll_interval_ms: 15000 # ⚠ 路径A 预留·未落地,配了暂不生效
259
+ # worker_timeout_ms: 60000 # ⚠ 路径A 预留·未落地,配了暂不生效
260
+
261
+ # ── monorepo 子模块映射(test_strategy: module 时按 git diff 命中模块收窄测试)──
262
+ # 只支持 inline flow 形态(嵌套展开式解析不出):
263
+ modules:
264
+ frontend: { path: "frontend/", test: "cd frontend && pnpm test" }
265
+ backend: { path: "backend/", test: "cd backend && npm test" }
266
+
267
+ # ── 测试策略:full=全量 commands.test | module=按命中模块收窄(需配 modules)──
268
+ test_strategy: full
269
+
270
+ # ── 预存失败豁免清单(变更前就失败的测试行;务必定期复核,防误豁免真实失败)──
271
+ known_failures:
272
+ - tests/some-old-stable-failure
273
+
274
+ # ── worktree-guard 只读命令扩展(execute worktree 隔离期内放行额外只读命令)──
275
+ worktree-hook:
276
+ readonlyCommands:
277
+ - rg
278
+ - fd
279
+
280
+ # ── 变更规模自动分类(sillyspec run auto 时按需求描述强制 quick/full;正则数组,i 大小写无关)──
281
+ auto_mode:
282
+ force_full_patterns:
283
+ - 数据库|migration
284
+ force_quick_patterns:
285
+ - fix typo
286
+ `;
287
+ }
@@ -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
+ }