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/init.js
CHANGED
|
@@ -4,6 +4,10 @@ import { fileURLToPath } from 'url';
|
|
|
4
4
|
import { checkbox, confirm, input } from '@inquirer/prompts';
|
|
5
5
|
import { ProgressManager } from './progress.js';
|
|
6
6
|
import chalk from 'chalk';
|
|
7
|
+
import { getVersion } from './version.js';
|
|
8
|
+
// 向后兼容:getVersion 已抽到轻量 version.js(避免 index.js 为 --version 静态加载 init.js 的 inquirer 税),
|
|
9
|
+
// 此处 re-export 保持 init.js 既有导出 API 不破坏(如 test/init-claude-injection.test.mjs)。
|
|
10
|
+
export { getVersion };
|
|
7
11
|
|
|
8
12
|
const __filename = fileURLToPath(import.meta.url);
|
|
9
13
|
const __dirname = dirname(__filename);
|
|
@@ -12,7 +16,7 @@ const __dirname = dirname(__filename);
|
|
|
12
16
|
// 平台模式(specRoot 指向外部)下,源码目录的 .sillyspec/.runtime/ 是旧残留,
|
|
13
17
|
// 应让 specRoot 成为唯一运行时根。但若源码目录的 .sillyspec/ 本身就是该项目
|
|
14
18
|
// 的 specDir(含真实资产),其 .runtime/ 里的 worktrees/、sillyspec.db、
|
|
15
|
-
// global.json
|
|
19
|
+
// global.json 是真实工作状态,整删会摧毁 worktree meta、
|
|
16
20
|
// 导致 depsStatus 恒为 unknown、branch already exists 死循环。
|
|
17
21
|
// 详见 docs/sillyspec/runtime-cleanup-destroys-worktree-meta.md
|
|
18
22
|
//
|
|
@@ -21,7 +25,6 @@ const RUNTIME_KEEP = new Set([
|
|
|
21
25
|
'worktrees', // worktree 目录 + meta.json(worktree.js:17)
|
|
22
26
|
'sillyspec.db', // SQLite 进度库(权威状态源,progress.js:7)
|
|
23
27
|
'global.json', // 项目名/活跃变更缓存(progress.js:8)
|
|
24
|
-
'gate-status.json', // worktree-guard 门禁状态(progress.js:9)
|
|
25
28
|
'contract-artifacts', // execute endpoint 契约(verify 阶段读取)
|
|
26
29
|
'execute-runs', // task review 结果(task-review.js)
|
|
27
30
|
]);
|
|
@@ -209,7 +212,7 @@ function isTTY() {
|
|
|
209
212
|
|
|
210
213
|
// ── 核心安装逻辑 ──
|
|
211
214
|
|
|
212
|
-
|
|
215
|
+
function doInstall(projectDir, tools, subprojects = [], specDir = null) {
|
|
213
216
|
// specDir: 规范目录(默认 projectDir/.sillyspec)
|
|
214
217
|
// projectDir: 源码项目根目录(用于工具检测、指令注入、.gitignore)
|
|
215
218
|
const spec = specDir || join(projectDir, '.sillyspec');
|
|
@@ -308,7 +311,7 @@ async function doInstall(projectDir, tools, subprojects = [], specDir = null) {
|
|
|
308
311
|
|
|
309
312
|
// 初始化 SQLite 数据库
|
|
310
313
|
const pm = new ProgressManager({ specDir: spec });
|
|
311
|
-
|
|
314
|
+
pm.init(projectDir);
|
|
312
315
|
|
|
313
316
|
// .gitignore 只在 specDir 在项目内时才修改
|
|
314
317
|
const isExternalSpec = specDir && resolve(spec) !== resolve(projectDir, '.sillyspec');
|
|
@@ -389,16 +392,6 @@ function showSummary(version, tools, specDir) {
|
|
|
389
392
|
console.log('');
|
|
390
393
|
}
|
|
391
394
|
|
|
392
|
-
// ── 读取版本号 ──
|
|
393
|
-
|
|
394
|
-
export function getVersion() {
|
|
395
|
-
try {
|
|
396
|
-
const pkg = JSON.parse(readFileSync(resolve(__dirname, '..', 'package.json'), 'utf8'));
|
|
397
|
-
return pkg.version;
|
|
398
|
-
} catch {
|
|
399
|
-
return '?.?.?';
|
|
400
|
-
}
|
|
401
|
-
}
|
|
402
395
|
|
|
403
396
|
// ── 主命令 ──
|
|
404
397
|
|
package/src/local-detect.js
CHANGED
|
@@ -1,110 +1,120 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Local Detect — 纯 fs 项目类型嗅探
|
|
3
|
-
*
|
|
4
|
-
* 把"生成本地配置"里的项目类型判定从 scan(半小时 + 大量 token)抽出来,
|
|
5
|
-
* 变成纯 fs 同步读几秒可完成的独立探测。create/gate 只需 project.type +
|
|
6
|
-
* commands,不该被迫跑完整 scan。
|
|
7
|
-
*
|
|
8
|
-
* 纯函数:不 spawn 任何子进程、不调用任何 AI/LLM、不消耗 token。
|
|
9
|
-
* 不写 local.yaml 到磁盘——只返回数据结构,落盘由调用方(CLI 路由 / scan.js)负责。
|
|
10
|
-
* 不引 yaml 依赖。
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
import { existsSync, readFileSync } from 'fs'
|
|
14
|
-
import { join } from 'path'
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* 从 Makefile 文本提取 test 目标的命令(轻量正则,与 verify-postcheck 同风格)。
|
|
18
|
-
* 支持两种常见写法:
|
|
19
|
-
* - 同行命令:`test: pytest` → 'pytest'
|
|
20
|
-
* - tab 续行:`test:\n\tpytest\n` → 'pytest'(命令在下一 tab 开头行)
|
|
21
|
-
* 边界:
|
|
22
|
-
* - 纯空目标(`test:` 无同行命令、无 tab 续行)→ 回退 'make test'
|
|
23
|
-
* - 命令行内 `#` 注释截断;取第一个非空命令行
|
|
24
|
-
* @param {string} makefileText
|
|
25
|
-
* @returns {string}
|
|
26
|
-
*/
|
|
27
|
-
function parseMakefileTestCommand(makefileText) {
|
|
28
|
-
if (!makefileText) return 'make test'
|
|
29
|
-
// 定位 `test:` 行;捕获 `:` 后同行内容(同行命令或空)
|
|
30
|
-
const head = makefileText.match(/^test:\s*([^\n#]*?)\s*(?:#.*)?$/m)
|
|
31
|
-
if (!head) return 'make test'
|
|
32
|
-
const inline = (head[1] || '').trim()
|
|
33
|
-
if (inline) return inline
|
|
34
|
-
// 同行为空 → 找该行之后第一个 tab 续行命令(`\tcmd` 或 ` cmd`)
|
|
35
|
-
const after = makefileText.slice(head.index + head[0].length)
|
|
36
|
-
const cont = after.match(/^[ \t]+([^\n#]+?)\s*(?:#.*)?$/m)
|
|
37
|
-
if (cont && cont[1]) return cont[1].trim()
|
|
38
|
-
return 'make test'
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* 嗅探 workdir 的项目类型与默认 commands(纯 fs,零 AI / 零 token)。
|
|
43
|
-
*
|
|
44
|
-
* 嗅探规则(按顺序,命中即返回):
|
|
45
|
-
* - package.json → nodejs
|
|
46
|
-
* - pom.xml → maven(mvn compile / mvn test / mvn checkstyle:check)
|
|
47
|
-
* - build.gradle → gradle
|
|
48
|
-
* - Makefile → make(test 命令从 test: 目标解析;build/lint 无则不写)
|
|
49
|
-
* - 都没有 → generic(commands 为空对象)
|
|
50
|
-
*
|
|
51
|
-
* @param {string} workdir - 项目根目录
|
|
52
|
-
* @returns {{
|
|
53
|
-
* project: { type: 'nodejs'|'maven'|'gradle'|'make'|'generic' },
|
|
54
|
-
* commands: { build?: string, test?: string, lint?: string }
|
|
55
|
-
* }}
|
|
56
|
-
*/
|
|
57
|
-
export function detectLocalYaml(workdir) {
|
|
58
|
-
// 1. nodejs
|
|
59
|
-
if (existsSync(join(workdir, 'package.json'))) {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Local Detect — 纯 fs 项目类型嗅探
|
|
3
|
+
*
|
|
4
|
+
* 把"生成本地配置"里的项目类型判定从 scan(半小时 + 大量 token)抽出来,
|
|
5
|
+
* 变成纯 fs 同步读几秒可完成的独立探测。create/gate 只需 project.type +
|
|
6
|
+
* commands,不该被迫跑完整 scan。
|
|
7
|
+
*
|
|
8
|
+
* 纯函数:不 spawn 任何子进程、不调用任何 AI/LLM、不消耗 token。
|
|
9
|
+
* 不写 local.yaml 到磁盘——只返回数据结构,落盘由调用方(CLI 路由 / scan.js)负责。
|
|
10
|
+
* 不引 yaml 依赖。
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { existsSync, readFileSync } from 'fs'
|
|
14
|
+
import { join } from 'path'
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* 从 Makefile 文本提取 test 目标的命令(轻量正则,与 verify-postcheck 同风格)。
|
|
18
|
+
* 支持两种常见写法:
|
|
19
|
+
* - 同行命令:`test: pytest` → 'pytest'
|
|
20
|
+
* - tab 续行:`test:\n\tpytest\n` → 'pytest'(命令在下一 tab 开头行)
|
|
21
|
+
* 边界:
|
|
22
|
+
* - 纯空目标(`test:` 无同行命令、无 tab 续行)→ 回退 'make test'
|
|
23
|
+
* - 命令行内 `#` 注释截断;取第一个非空命令行
|
|
24
|
+
* @param {string} makefileText
|
|
25
|
+
* @returns {string}
|
|
26
|
+
*/
|
|
27
|
+
function parseMakefileTestCommand(makefileText) {
|
|
28
|
+
if (!makefileText) return 'make test'
|
|
29
|
+
// 定位 `test:` 行;捕获 `:` 后同行内容(同行命令或空)
|
|
30
|
+
const head = makefileText.match(/^test:\s*([^\n#]*?)\s*(?:#.*)?$/m)
|
|
31
|
+
if (!head) return 'make test'
|
|
32
|
+
const inline = (head[1] || '').trim()
|
|
33
|
+
if (inline) return inline
|
|
34
|
+
// 同行为空 → 找该行之后第一个 tab 续行命令(`\tcmd` 或 ` cmd`)
|
|
35
|
+
const after = makefileText.slice(head.index + head[0].length)
|
|
36
|
+
const cont = after.match(/^[ \t]+([^\n#]+?)\s*(?:#.*)?$/m)
|
|
37
|
+
if (cont && cont[1]) return cont[1].trim()
|
|
38
|
+
return 'make test'
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* 嗅探 workdir 的项目类型与默认 commands(纯 fs,零 AI / 零 token)。
|
|
43
|
+
*
|
|
44
|
+
* 嗅探规则(按顺序,命中即返回):
|
|
45
|
+
* - package.json → nodejs(核验 scripts.{build,test,lint} 存在才写对应键;缺失不写)
|
|
46
|
+
* - pom.xml → maven(mvn compile / mvn test / mvn checkstyle:check)
|
|
47
|
+
* - build.gradle → gradle(gradlew 存在用 ./gradlew 否则 gradle;build / test / check)
|
|
48
|
+
* - Makefile → make(test 命令从 test: 目标解析;build/lint 无则不写)
|
|
49
|
+
* - 都没有 → generic(commands 为空对象)
|
|
50
|
+
*
|
|
51
|
+
* @param {string} workdir - 项目根目录
|
|
52
|
+
* @returns {{
|
|
53
|
+
* project: { type: 'nodejs'|'maven'|'gradle'|'make'|'generic' },
|
|
54
|
+
* commands: { build?: string, test?: string, lint?: string }
|
|
55
|
+
* }}
|
|
56
|
+
*/
|
|
57
|
+
export function detectLocalYaml(workdir) {
|
|
58
|
+
// 1. nodejs
|
|
59
|
+
if (existsSync(join(workdir, 'package.json'))) {
|
|
60
|
+
const pkgPath = join(workdir, 'package.json')
|
|
61
|
+
const raw = readFileSync(pkgPath, 'utf8')
|
|
62
|
+
let pkg
|
|
63
|
+
try {
|
|
64
|
+
pkg = JSON.parse(raw)
|
|
65
|
+
} catch (e) {
|
|
66
|
+
throw new Error('package.json 解析失败:' + pkgPath)
|
|
67
|
+
}
|
|
68
|
+
const scripts = (pkg && pkg.scripts) || {}
|
|
69
|
+
const commands = {}
|
|
70
|
+
if (scripts.build) commands.build = 'npm run build'
|
|
71
|
+
if (scripts.test) commands.test = 'npm test'
|
|
72
|
+
if (scripts.lint) commands.lint = 'npm run lint'
|
|
73
|
+
return {
|
|
74
|
+
project: { type: 'nodejs' },
|
|
75
|
+
commands,
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// 2. maven
|
|
80
|
+
if (existsSync(join(workdir, 'pom.xml'))) {
|
|
81
|
+
return {
|
|
82
|
+
project: { type: 'maven' },
|
|
83
|
+
commands: {
|
|
84
|
+
build: 'mvn compile',
|
|
85
|
+
test: 'mvn test',
|
|
86
|
+
lint: 'mvn checkstyle:check',
|
|
87
|
+
},
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// 3. gradle
|
|
92
|
+
if (existsSync(join(workdir, 'build.gradle'))) {
|
|
93
|
+
const gradlePrefix = existsSync(join(workdir, 'gradlew')) ? './gradlew' : 'gradle'
|
|
94
|
+
return {
|
|
95
|
+
project: { type: 'gradle' },
|
|
96
|
+
commands: {
|
|
97
|
+
build: `${gradlePrefix} build`,
|
|
98
|
+
test: `${gradlePrefix} test`,
|
|
99
|
+
lint: `${gradlePrefix} check`,
|
|
100
|
+
},
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// 4. make(test 命令从 Makefile 解析;build/lint 无则省略)
|
|
105
|
+
const makefilePath = join(workdir, 'Makefile')
|
|
106
|
+
if (existsSync(makefilePath)) {
|
|
107
|
+
const makefileText = readFileSync(makefilePath, 'utf8')
|
|
108
|
+
const test = parseMakefileTestCommand(makefileText)
|
|
109
|
+
return {
|
|
110
|
+
project: { type: 'make' },
|
|
111
|
+
commands: { test },
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// 5. generic
|
|
116
|
+
return {
|
|
117
|
+
project: { type: 'generic' },
|
|
118
|
+
commands: {},
|
|
119
|
+
}
|
|
120
|
+
}
|
package/src/machine-interface.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* 设计原则(见 change 2026-07-09-machine-interface-v1 design.md §2/§3):
|
|
8
8
|
* - 方案 B:独立模块单点封装,不污染既有 run/progress 输出
|
|
9
9
|
* - 只聚合不新增校验:复用 stage-contract / task-review / verify-postcheck 既有策略引擎
|
|
10
|
-
* - 只读语义边界(D-002@v1):只调 ProgressManager 读路径,不写 sillyspec.db
|
|
10
|
+
* - 只读语义边界(D-002@v1):只调 ProgressManager 读路径,不写 sillyspec.db
|
|
11
11
|
* - stdout 纯 JSON(D-005@v1):--json 模式下由 CLI 层(src/index.js 的 withJsonOutput)
|
|
12
12
|
* 在调用 runGate/runDerive 期间劫持 console.log 到 stderr,防被调模块污染机器输出
|
|
13
13
|
* - 退出码 0/1/2 三段语义(D-004@v1):0=通过,1=事实阻断,2=无法核验
|
|
@@ -18,8 +18,9 @@
|
|
|
18
18
|
import { existsSync, readFileSync } from 'fs';
|
|
19
19
|
import { join } from 'path';
|
|
20
20
|
import { ProgressManager, resolveSpecDir } from './progress.js';
|
|
21
|
+
import { resolveRuntimeRoot } from './run/shared.js';
|
|
21
22
|
import { runValidators, checkTransition, checkExecuteCodeEvidence } from './stage-contract.js';
|
|
22
|
-
import { validateTaskReviews } from './task-review.js';
|
|
23
|
+
import { validateTaskReviews, resolveLatestExecuteRunId } from './task-review.js';
|
|
23
24
|
import { runVerifyTestCheck } from './verify-postcheck.js';
|
|
24
25
|
|
|
25
26
|
// ============ 退出码常量(D-004@v1)============
|
|
@@ -101,16 +102,35 @@ export function buildEnvelope({
|
|
|
101
102
|
* @param {object} opts
|
|
102
103
|
* @param {string} opts.cwd - 项目根目录
|
|
103
104
|
* @param {string} [opts.specBase] - .sillyspec(或平台 specRoot)目录;默认 resolveSpecDir(cwd)
|
|
104
|
-
* @param {string} [opts.runtimeRoot] - .runtime 目录;默认
|
|
105
|
+
* @param {string} [opts.runtimeRoot] - .runtime 目录;默认 resolveRuntimeRoot 三级优先级解析
|
|
106
|
+
* @param {string} [opts.specDriftAnchor] - drift 场景主仓 specBase 锚点(坑 execute-runs-isolation)。
|
|
107
|
+
* drift 守卫命中时由调用方传入,task-reviews 的 execute-run-id marker 落点经
|
|
108
|
+
* resolveRuntimeRoot 锚主仓 .runtime,不读 worktree 副本(与 contract-matrix 调用方同语义)。
|
|
105
109
|
* @returns {Promise<{ envelope: object, exitCode: number }>}
|
|
106
110
|
*/
|
|
107
|
-
export async function runGate(stage, changeName, { cwd, specBase, runtimeRoot } = {}) {
|
|
111
|
+
export async function runGate(stage, changeName, { cwd, specBase, runtimeRoot, specDriftAnchor } = {}) {
|
|
108
112
|
const specRoot = specBase || resolveSpecDir(cwd);
|
|
109
113
|
const pm = new ProgressManager();
|
|
110
114
|
|
|
111
115
|
try {
|
|
116
|
+
// ── 只读契约守卫(D-002@v1):db 不存在时 fail-closed 返回 exit 2,不触发建库落盘 ──
|
|
117
|
+
// pm.read → _ensureDB → db.init() 在 db 不存在/schema 戳落后时会 _createSchema + _save 落盘
|
|
118
|
+
// (db.js:init),这会让一个声明只读的查询命令在 spec 目录凭空建 sillyspec.db,违反只读契约。
|
|
119
|
+
// db 还没建 = 该变更从未跑过流程,无从核验 → 显式返回「无法核验」而非建库。
|
|
120
|
+
const dbPath = join(resolveSpecDir(cwd), '.runtime', 'sillyspec.db');
|
|
121
|
+
if (!existsSync(dbPath)) {
|
|
122
|
+
const envelope = buildEnvelope({
|
|
123
|
+
command: 'gate',
|
|
124
|
+
stage,
|
|
125
|
+
change: changeName,
|
|
126
|
+
ok: false,
|
|
127
|
+
errors: [`无法核验:进度库不存在(${dbPath})——变更 ${changeName} 尚未运行过任何阶段,只读 gate 不会为其建库`],
|
|
128
|
+
});
|
|
129
|
+
return { envelope, exitCode: EXIT_UNKNOWN };
|
|
130
|
+
}
|
|
131
|
+
|
|
112
132
|
// ── 读进度:变更不存在 → exit 2(D-004@v1)──
|
|
113
|
-
const progress =
|
|
133
|
+
const progress = pm.read(cwd, changeName);
|
|
114
134
|
if (!progress) {
|
|
115
135
|
const envelope = buildEnvelope({
|
|
116
136
|
command: 'gate',
|
|
@@ -181,13 +201,14 @@ export async function runGate(stage, changeName, { cwd, specBase, runtimeRoot }
|
|
|
181
201
|
}
|
|
182
202
|
}
|
|
183
203
|
|
|
184
|
-
|
|
185
|
-
|
|
204
|
+
// drift 场景经 resolveRuntimeRoot 三级优先级解析(runtimeRoot > specDriftAnchor > 本地兜底),
|
|
205
|
+
// 锚主仓 .runtime,不读 worktree 副本(坑 execute-runs-isolation,与 contract-matrix 调用方同语义)
|
|
206
|
+
const rtRoot = resolveRuntimeRoot({ runtimeRoot, specDriftAnchor }, specRoot);
|
|
207
|
+
// marker 缺失/为空时先扫描 execute-runs/ 找回真实 runId(与 gates.js #6 同源),
|
|
208
|
+
// 避免拿 '' 进 validateTaskReviews 拼出 execute-runs//tasks/... 产出全量误导 errors。
|
|
186
209
|
let executeRunId = '';
|
|
187
210
|
try {
|
|
188
|
-
|
|
189
|
-
executeRunId = readFileSync(runIdFile, 'utf8').trim();
|
|
190
|
-
}
|
|
211
|
+
executeRunId = resolveLatestExecuteRunId({ runtimeRoot: rtRoot, changeName }) || '';
|
|
191
212
|
} catch {
|
|
192
213
|
executeRunId = '';
|
|
193
214
|
}
|
|
@@ -300,7 +321,7 @@ export const FACETS = ['execute-evidence', 'verify-test', 'task-reviews', 'artif
|
|
|
300
321
|
* 与 gate 的差异:gate 聚合某阶段所有 check 的综合结论;derive 只返回单一 facet 的结构化 data,
|
|
301
322
|
* daemon 用来做细粒度事实采集(如轮询 execute-evidence 判断代码是否变更)。
|
|
302
323
|
*
|
|
303
|
-
* 只读语义边界同 gate(D-002@v1):仅调 ProgressManager.read,不写 sillyspec.db
|
|
324
|
+
* 只读语义边界同 gate(D-002@v1):仅调 ProgressManager.read,不写 sillyspec.db。
|
|
304
325
|
* 唯一例外是 verify-test 会真实执行测试并落盘 test-result.json(产物取证,非状态写入,design §3.3)。
|
|
305
326
|
*
|
|
306
327
|
* 退出码语义(D-004@v1):0=事实通过,1=事实性阻断,2=无法核验(用法错/变更不存在/内部异常)。
|
|
@@ -310,10 +331,12 @@ export const FACETS = ['execute-evidence', 'verify-test', 'task-reviews', 'artif
|
|
|
310
331
|
* @param {object} opts
|
|
311
332
|
* @param {string} opts.cwd - 项目根目录
|
|
312
333
|
* @param {string} [opts.specBase] - .sillyspec(或平台 specRoot)目录;默认 resolveSpecDir(cwd)
|
|
313
|
-
* @param {string} [opts.runtimeRoot] - .runtime 目录;默认
|
|
334
|
+
* @param {string} [opts.runtimeRoot] - .runtime 目录;默认 resolveRuntimeRoot 三级优先级解析
|
|
335
|
+
* @param {string} [opts.specDriftAnchor] - drift 场景主仓 specBase 锚点(坑 execute-runs-isolation),
|
|
336
|
+
* task-reviews facet 的 execute-run-id marker 落点经 resolveRuntimeRoot 锚主仓 .runtime,不读副本。
|
|
314
337
|
* @returns {Promise<{ envelope: object, exitCode: number }>}
|
|
315
338
|
*/
|
|
316
|
-
export async function runDerive(facet, changeName, { cwd, specBase, runtimeRoot } = {}) {
|
|
339
|
+
export async function runDerive(facet, changeName, { cwd, specBase, runtimeRoot, specDriftAnchor } = {}) {
|
|
317
340
|
// ── 非法 facet:用法错 → exit 2(D-004@v1)──
|
|
318
341
|
if (!FACETS.includes(facet)) {
|
|
319
342
|
const envelope = buildEnvelope({
|
|
@@ -330,8 +353,21 @@ export async function runDerive(facet, changeName, { cwd, specBase, runtimeRoot
|
|
|
330
353
|
const pm = new ProgressManager();
|
|
331
354
|
|
|
332
355
|
try {
|
|
356
|
+
// ── 只读契约守卫(D-002@v1,同 runGate):db 不存在时 fail-closed 返回 exit 2,不建库落盘 ──
|
|
357
|
+
const dbPath = join(resolveSpecDir(cwd), '.runtime', 'sillyspec.db');
|
|
358
|
+
if (!existsSync(dbPath)) {
|
|
359
|
+
const envelope = buildEnvelope({
|
|
360
|
+
command: 'derive',
|
|
361
|
+
facet,
|
|
362
|
+
change: changeName,
|
|
363
|
+
ok: false,
|
|
364
|
+
errors: [`无法核验:进度库不存在(${dbPath})——变更 ${changeName} 尚未运行过任何阶段,只读 derive 不会为其建库`],
|
|
365
|
+
});
|
|
366
|
+
return { envelope, exitCode: EXIT_UNKNOWN };
|
|
367
|
+
}
|
|
368
|
+
|
|
333
369
|
// ── 读进度:变更不存在 → exit 2 ──
|
|
334
|
-
const progress =
|
|
370
|
+
const progress = pm.read(cwd, changeName);
|
|
335
371
|
if (!progress) {
|
|
336
372
|
const envelope = buildEnvelope({
|
|
337
373
|
command: 'derive',
|
|
@@ -399,13 +435,12 @@ export async function runDerive(facet, changeName, { cwd, specBase, runtimeRoot
|
|
|
399
435
|
}
|
|
400
436
|
}
|
|
401
437
|
|
|
402
|
-
|
|
403
|
-
const
|
|
438
|
+
// drift 场景经 resolveRuntimeRoot 三级优先级解析(同 runGate execute 段),锚主仓 .runtime 不读副本
|
|
439
|
+
const rtRoot = resolveRuntimeRoot({ runtimeRoot, specDriftAnchor }, specRoot);
|
|
440
|
+
// marker 缺失/为空时先扫描 execute-runs/ 找回真实 runId(同 runGate execute 段),避免拿 '' 误导校验
|
|
404
441
|
let executeRunId = '';
|
|
405
442
|
try {
|
|
406
|
-
|
|
407
|
-
executeRunId = readFileSync(runIdFile, 'utf8').trim();
|
|
408
|
-
}
|
|
443
|
+
executeRunId = resolveLatestExecuteRunId({ runtimeRoot: rtRoot, changeName }) || '';
|
|
409
444
|
} catch {
|
|
410
445
|
executeRunId = '';
|
|
411
446
|
}
|
package/src/migrate.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, readdirSync, readFileSync, writeFileSync, mkdirSync, renameSync, copyFileSync } from 'fs';
|
|
1
|
+
import { existsSync, readdirSync, readFileSync, writeFileSync, mkdirSync, renameSync, copyFileSync, statSync, cpSync } from 'fs';
|
|
2
2
|
import { basename, join, resolve } from 'path';
|
|
3
3
|
import chalk from 'chalk';
|
|
4
4
|
|
|
@@ -60,9 +60,19 @@ export function migrateDocs(projectDir) {
|
|
|
60
60
|
const src = join(archiveDir, entry);
|
|
61
61
|
const dest = join(targetDir, entry);
|
|
62
62
|
if (!existsSync(dest)) {
|
|
63
|
-
copyFileSync
|
|
64
|
-
|
|
65
|
-
|
|
63
|
+
// 归档变更本身是目录(含 design.md 等),copyFileSync 对目录源抛 EISDIR;
|
|
64
|
+
// 按类型分发:目录用 cpSync 递归、文件用 copyFileSync,单条失败 try/catch 不中断整体迁移
|
|
65
|
+
try {
|
|
66
|
+
if (statSync(src).isDirectory()) {
|
|
67
|
+
cpSync(src, dest, { recursive: true });
|
|
68
|
+
} else {
|
|
69
|
+
copyFileSync(src, dest);
|
|
70
|
+
}
|
|
71
|
+
console.log(chalk.green(' ✅') + ` archive/${entry}`);
|
|
72
|
+
migrated++;
|
|
73
|
+
} catch (err) {
|
|
74
|
+
console.log(chalk.yellow(' ⚠️') + ` archive/${entry} 迁移失败: ${err.message}`);
|
|
75
|
+
}
|
|
66
76
|
} else {
|
|
67
77
|
console.log(chalk.yellow(' ⏭️') + ` archive/${entry} (已存在)`);
|
|
68
78
|
}
|
package/src/modules.js
CHANGED
|
@@ -60,7 +60,7 @@ function parseModuleCardFrontmatter(content) {
|
|
|
60
60
|
* - verify_commands: 验证命令(build/test/lint)
|
|
61
61
|
* - related_docs: 关联文档路径
|
|
62
62
|
*/
|
|
63
|
-
export async function rebuildModuleMap(cwd) {
|
|
63
|
+
export async function rebuildModuleMap(cwd, { force = false } = {}) {
|
|
64
64
|
const mapPath = findModuleMapPath(cwd);
|
|
65
65
|
if (!mapPath) {
|
|
66
66
|
console.error('❌ 未找到 .sillyspec/docs/<project>/modules/ 目录');
|
|
@@ -162,6 +162,13 @@ export async function rebuildModuleMap(cwd) {
|
|
|
162
162
|
yaml += `\n`;
|
|
163
163
|
}
|
|
164
164
|
|
|
165
|
+
// 破坏性保护(multi-agent-platform 坑 modules-rebuild-destructive):默认 dry-run 只预览不写,--force 才覆盖。
|
|
166
|
+
if (!force && existingMap) {
|
|
167
|
+
console.log('⚠️ rebuild 默认不写入(破坏性保护):该命令会覆盖 _module-map.yaml,清空 tags/entrypoints/main_symbols/depends_on/used_by 等手动维护字段');
|
|
168
|
+
console.log(' 本次为预览,未写入磁盘。确认覆盖请运行:sillyspec modules rebuild --force');
|
|
169
|
+
return { dryRun: true, path: mapPath }
|
|
170
|
+
}
|
|
171
|
+
|
|
165
172
|
writeFileSync(mapPath, yaml, 'utf8');
|
|
166
173
|
console.log(`✅ _module-map.yaml 已重建:${mapPath}`);
|
|
167
174
|
console.log(` 模块数量:${allModuleIds.size}`);
|
|
@@ -170,6 +177,7 @@ export async function rebuildModuleMap(cwd) {
|
|
|
170
177
|
}
|
|
171
178
|
console.log(`\n⚠️ 注意:rebuild 只重建骨架。tags/entrypoints/main_symbols/depends_on/used_by 需要重新运行 scan 或手动补充。`);
|
|
172
179
|
console.log(`ℹ️ schema 已升级到 v2,支持 role/core_files/test_files/entrypoints/depends_on/risk_level/verify_commands 等字段。`);
|
|
180
|
+
return { dryRun: false, path: mapPath, modules: allModuleIds.size }
|
|
173
181
|
}
|
|
174
182
|
|
|
175
183
|
/**
|
|
@@ -278,7 +286,10 @@ export async function generateDependenciesMd(cwd) {
|
|
|
278
286
|
console.log(`✅ dependencies.md 已生成`);
|
|
279
287
|
}
|
|
280
288
|
|
|
281
|
-
|
|
289
|
+
// _module-map.yaml 简单 YAML 解析(模块上下文索引)。canonical 单一真相源——run/prompt.js
|
|
290
|
+
// 的 loadModuleContextIndex 与本文件 generateDependenciesMd 共用此函数(合并历史两份分叉副本,
|
|
291
|
+
// 2026-08-07;字段集取并集:core_files/test_files/related_docs/verify_commands + role/risk_level)。
|
|
292
|
+
export function parseModuleMapSimple(content) {
|
|
282
293
|
const modules = {};
|
|
283
294
|
let currentModule = null;
|
|
284
295
|
let currentKey = null;
|
|
@@ -299,7 +310,7 @@ function parseModuleMapSimple(content) {
|
|
|
299
310
|
if (!currentModule) continue;
|
|
300
311
|
|
|
301
312
|
// Array field like depends_on:\n - xxx
|
|
302
|
-
const arrayFieldMatch = line.match(/^ (depends_on|used_by|paths|tags|aliases|entrypoints|main_symbols|review_reasons):$/);
|
|
313
|
+
const arrayFieldMatch = line.match(/^ (depends_on|used_by|paths|tags|aliases|entrypoints|main_symbols|review_reasons|core_files|test_files|related_docs|verify_commands):$/);
|
|
303
314
|
if (arrayFieldMatch) {
|
|
304
315
|
if (currentArray && currentKey) modules[currentModule][currentKey] = currentArray;
|
|
305
316
|
currentKey = arrayFieldMatch[1];
|
|
@@ -308,7 +319,7 @@ function parseModuleMapSimple(content) {
|
|
|
308
319
|
}
|
|
309
320
|
|
|
310
321
|
// Inline array like tags: [a, b]
|
|
311
|
-
const inlineArrayMatch = line.match(/^ (depends_on|used_by|paths|tags|aliases|entrypoints|main_symbols|review_reasons): \[(.*)\]$/);
|
|
322
|
+
const inlineArrayMatch = line.match(/^ (depends_on|used_by|paths|tags|aliases|entrypoints|main_symbols|review_reasons|core_files|test_files|related_docs|verify_commands): \[(.*)\]$/);
|
|
312
323
|
if (inlineArrayMatch) {
|
|
313
324
|
if (currentArray && currentKey) modules[currentModule][currentKey] = currentArray;
|
|
314
325
|
const vals = inlineArrayMatch[2].split(',').map(v => v.trim()).filter(Boolean);
|
|
@@ -319,7 +330,7 @@ function parseModuleMapSimple(content) {
|
|
|
319
330
|
}
|
|
320
331
|
|
|
321
332
|
// Scalar field
|
|
322
|
-
const scalarMatch = line.match(/^ (status|doc|needs_review): (.+)$/);
|
|
333
|
+
const scalarMatch = line.match(/^ (status|doc|needs_review|role|risk_level): (.+)$/);
|
|
323
334
|
if (scalarMatch) {
|
|
324
335
|
if (currentArray && currentKey) { modules[currentModule][currentKey] = currentArray; currentArray = null; currentKey = null; }
|
|
325
336
|
modules[currentModule][scalarMatch[1]] = scalarMatch[2];
|