sillyspec 3.24.7 → 3.25.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/package.json +1 -1
- package/src/constants.js +25 -0
- package/src/contract-matrix.js +48 -20
- package/src/endpoint-extractor.js +92 -6
- package/src/fs-atomic.js +69 -0
- package/src/hooks/worktree-guard.js +16 -4
- package/src/index.js +6 -7
- package/src/modules.js +2 -2
- package/src/progress.js +38 -53
- package/src/run.js +578 -459
- package/src/scan-postcheck.js +2 -10
- package/src/stage-contract.js +8 -17
- package/src/stages/brainstorm-auto.js +1 -1
- package/src/stages/brainstorm.js +571 -571
- package/src/stages/doctor.js +5 -5
- package/src/stages/execute.js +1 -1
- package/src/stages/index.js +1 -1
- package/src/stages/plan.js +2 -2
- package/src/stages/propose.js +1 -1
- package/src/stages/status.js +1 -1
- package/src/stages/verify.js +2 -0
- package/src/verify-postcheck.js +41 -0
- package/src/worktree-apply.js +45 -18
- package/src/worktree.js +13 -5
package/src/stages/doctor.js
CHANGED
|
@@ -77,19 +77,19 @@ subs.forEach(s => {
|
|
|
77
77
|
|
|
78
78
|
### 5. 配置文件检查
|
|
79
79
|
\`\`\`bash
|
|
80
|
-
# 检查 local.yaml 和
|
|
80
|
+
# 检查 local.yaml 和 scan 总览文档
|
|
81
81
|
for f in .sillyspec/projects/*.yaml; do
|
|
82
82
|
[ -f "$f" ] || continue
|
|
83
83
|
name=$(grep '^name:' "$f" | head -1 | sed 's/^name:[[:space:]]*//')
|
|
84
84
|
p=$(grep '^path:' "$f" | head -1 | sed 's/^path:[[:space:]]*//')
|
|
85
85
|
[ -z "$p" ] && continue
|
|
86
86
|
local_yaml="$p/.sillyspec/local.yaml"
|
|
87
|
-
|
|
87
|
+
arch_md=".sillyspec/docs/$name/scan/ARCHITECTURE.md"
|
|
88
88
|
[ -f "$local_yaml" ] && echo "✅ local.yaml ($name)" || echo "⚠️ local.yaml ($name) — 不存在"
|
|
89
89
|
if [ -f "$local_yaml" ]; then
|
|
90
90
|
grep -q 'test:' "$local_yaml" && echo " ✅ test 命令已配置" || echo " ⚠️ 缺少 test 命令"
|
|
91
91
|
fi
|
|
92
|
-
[ -f "$
|
|
92
|
+
[ -f "$arch_md" ] && echo "✅ scan/ARCHITECTURE.md ($name)" || echo "⚠️ scan/ARCHITECTURE.md ($name) — 不存在(运行 sillyspec run scan)"
|
|
93
93
|
done
|
|
94
94
|
\`\`\`
|
|
95
95
|
|
|
@@ -167,7 +167,7 @@ echo "项目目录: $PROJECT_DIR"
|
|
|
167
167
|
for f in pom.xml build.gradle package.json requirements.txt pyproject.toml go.mod Cargo.toml; do
|
|
168
168
|
[ -f "$PROJECT_DIR/$f" ] && echo "检测到: $f"
|
|
169
169
|
done
|
|
170
|
-
|
|
170
|
+
cat .sillyspec/docs/*/scan/ARCHITECTURE.md 2>/dev/null | head -30 || echo "(无 scan/ARCHITECTURE.md)"
|
|
171
171
|
\`\`\`
|
|
172
172
|
|
|
173
173
|
### 2. 构建工具可用性
|
|
@@ -349,7 +349,7 @@ timeout 5 which docker 2>/dev/null && echo "✅ Docker 可用" || echo "ℹ️ D
|
|
|
349
349
|
- CLI 未安装 → \`npm install -g sillyspec\`
|
|
350
350
|
- 缺少 local.yaml → \`sillyspec init\` 重新生成,或手动创建
|
|
351
351
|
- local.yaml 缺少 test 命令 → 补充对应命令
|
|
352
|
-
- 缺少
|
|
352
|
+
- 缺少 scan 文档(ARCHITECTURE.md 等)→ \`sillyspec run scan\` 重新扫描
|
|
353
353
|
- sillyspec.db 状态不一致 → \`sillyspec run <阶段> --reset\` 重置对应阶段
|
|
354
354
|
- 孤儿目录 → 确认后 \`rm -rf .sillyspec/changes/<目录名>\`
|
|
355
355
|
- Maven 私服不可达 → 检查 VPN、settings.xml 配置、私服状态
|
package/src/stages/execute.js
CHANGED
|
@@ -115,7 +115,7 @@ const fixedPrefix = [
|
|
|
115
115
|
2. 读取 design.md(技术方案)
|
|
116
116
|
3. 读取 CONVENTIONS.md、ARCHITECTURE.md
|
|
117
117
|
4. 读取 local.yaml(构建命令)
|
|
118
|
-
5.
|
|
118
|
+
5. 加载项目总览 \`.sillyspec/docs/<project>/scan/PROJECT.md\`(如存在)
|
|
119
119
|
|
|
120
120
|
### 模块文档加载
|
|
121
121
|
6. 读取 \`.sillyspec/docs/<project>/modules/_module-map.yaml\`(不存在则跳过以下步骤)
|
package/src/stages/index.js
CHANGED
package/src/stages/plan.js
CHANGED
|
@@ -87,12 +87,12 @@ const stepClassify = {
|
|
|
87
87
|
|
|
88
88
|
### 操作
|
|
89
89
|
1. 运行 \`sillyspec progress show\`,确认 currentStage 为 "plan"
|
|
90
|
-
2.
|
|
90
|
+
2. 读取项目总览 \`{SPEC_ROOT}/docs/<project>/scan/PROJECT.md\` + 各子项目上下文
|
|
91
91
|
3. 读取 proposal.md、design.md、requirements.md、tasks.md
|
|
92
92
|
4. 如果存在 decisions.md,必须读取并提取所有当前版本 D-xxx@vN 决策 ID
|
|
93
93
|
- 如果发现 priority=P0/P1 且 status=unresolved/blocking 的决策,停止生成计划,要求先回到 brainstorm 的 Design Grill 修正
|
|
94
94
|
- 如果发现 superseded 决策,只引用最新版本,不引用旧版本
|
|
95
|
-
5. 读取 CONVENTIONS.md、ARCHITECTURE.md
|
|
95
|
+
5. 读取 CONVENTIONS.md、ARCHITECTURE.md(技术栈含在 ARCHITECTURE.md)
|
|
96
96
|
6. 读取 local.yaml 获取构建/测试命令
|
|
97
97
|
7. 读取 \`{SPEC_ROOT}/docs/<project>/modules/_module-map.yaml\`(不存在则跳过)
|
|
98
98
|
- 根据 design.md 的文件变更清单匹配模块
|
package/src/stages/propose.js
CHANGED
package/src/stages/status.js
CHANGED
|
@@ -9,7 +9,7 @@ export const definition = {
|
|
|
9
9
|
prompt: `收集项目基础信息。
|
|
10
10
|
|
|
11
11
|
### 操作
|
|
12
|
-
1. \`cat .sillyspec/PROJECT.md 2>/dev/null || echo "未初始化"\`
|
|
12
|
+
1. \`cat .sillyspec/docs/*/scan/PROJECT.md 2>/dev/null | head -20 || echo "未初始化"\`
|
|
13
13
|
2. 获取 project 名
|
|
14
14
|
3. \`ls .sillyspec/docs/<project>/scan/ 2>/dev/null | head -10\`
|
|
15
15
|
4. \`cat .sillyspec/REQUIREMENTS.md 2>/dev/null | head -20\`
|
package/src/stages/verify.js
CHANGED
|
@@ -60,6 +60,8 @@ export const definition = {
|
|
|
60
60
|
3. 加载项目信息:\`cat .sillyspec/projects/*.yaml 2>/dev/null\`
|
|
61
61
|
4. 加载本地配置:\`cat .sillyspec/local.yaml 2>/dev/null\`(构建命令、测试命令、lint 命令等)
|
|
62
62
|
5. 加载代码规范:\`cat .sillyspec/docs/<project>/scan/CONVENTIONS.md 2>/dev/null\`
|
|
63
|
+
- 测试现状:\`cat .sillyspec/docs/<project>/scan/TESTING.md 2>/dev/null\`(了解既有测试约定与覆盖范围,验收时对照)
|
|
64
|
+
- 技术债清单:\`cat .sillyspec/docs/<project>/scan/CONCERNS.md 2>/dev/null\`(🔴/🟡 区域;本次变更若触碰须在 verify-result.md 标注)
|
|
63
65
|
6. 标注每个文件的存在/不存在状态
|
|
64
66
|
|
|
65
67
|
### Execute Evidence 传递检查
|
package/src/verify-postcheck.js
CHANGED
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
import { execSync } from 'child_process'
|
|
19
19
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs'
|
|
20
20
|
import { join } from 'path'
|
|
21
|
+
import { verifyApiParity } from './contract-matrix.js'
|
|
21
22
|
|
|
22
23
|
// 测试命令最长执行时间;超时视为失败(防止 CLI 被挂起的测试卡死)
|
|
23
24
|
const TEST_TIMEOUT_MS = Number(process.env.SILLYSPEC_TEST_TIMEOUT_MS) || 10 * 60 * 1000
|
|
@@ -504,3 +505,43 @@ export function printVerifyTestCheck(result) {
|
|
|
504
505
|
console.log(`📄 实测结果已写入: ${result.resultPath}`)
|
|
505
506
|
}
|
|
506
507
|
}
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* API parity 对账(advisory):前端调用 vs execute 提取的后端 endpoint artifact。
|
|
511
|
+
* 三态:skipped(无 provider artifact → 非全栈项目不打扰)/ warning(missingBackend>0)/ passed。
|
|
512
|
+
* 启发式正则、多假阳源 → advisory 不阻断 verify 完成(与 SillySpec 确定性校验定位一致)。
|
|
513
|
+
*
|
|
514
|
+
* @param {object} opts
|
|
515
|
+
* @param {string} opts.cwd - 扫描前端调用的根(主工作区,verify 时代码在此)
|
|
516
|
+
* @param {string} opts.specBase
|
|
517
|
+
* @param {string|null} [opts.changeName]
|
|
518
|
+
* @param {string|null} [opts.runtimeRoot]
|
|
519
|
+
* @returns {{ status: 'skipped'|'warning'|'passed', missingBackend: Array, unusedBackend: Array, summary: string, reason: string|null }}
|
|
520
|
+
*/
|
|
521
|
+
export function runVerifyParityCheck({ cwd, specBase, changeName = null, runtimeRoot = null }) {
|
|
522
|
+
const r = verifyApiParity(specBase, cwd, runtimeRoot, changeName)
|
|
523
|
+
// 无 provider artifact(execute 未提取 / 非后端项目)→ 不打扰
|
|
524
|
+
if (r.backendCount === 0) {
|
|
525
|
+
return { status: 'skipped', missingBackend: [], unusedBackend: [], summary: r.summary, reason: '无后端契约 artifact(非全栈项目或 execute 未提取端点)' }
|
|
526
|
+
}
|
|
527
|
+
if (r.missingBackend.length > 0) {
|
|
528
|
+
return { status: 'warning', missingBackend: r.missingBackend, unusedBackend: r.unusedBackend, summary: r.summary, reason: null }
|
|
529
|
+
}
|
|
530
|
+
return { status: 'passed', missingBackend: [], unusedBackend: r.unusedBackend, summary: r.summary, reason: null }
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/** 打印 parity 对账结果(advisory,不阻断) */
|
|
534
|
+
export function printVerifyParityCheck(result) {
|
|
535
|
+
if (result.status === 'skipped') return // 静默,不打扰非全栈项目
|
|
536
|
+
if (result.status === 'passed') {
|
|
537
|
+
console.log(`\n✅ API parity 对账通过:${result.summary}`)
|
|
538
|
+
return
|
|
539
|
+
}
|
|
540
|
+
// warning
|
|
541
|
+
console.warn(`\n⚠️ API parity 对账发现 ${result.missingBackend.length} 个前端调用无对应后端端点(advisory,不阻断归档):`)
|
|
542
|
+
for (const m of result.missingBackend.slice(0, 20)) {
|
|
543
|
+
console.warn(` - ${m.method} ${m.path} ← ${m.consumerFile}:${m.consumerLine}`)
|
|
544
|
+
}
|
|
545
|
+
if (result.missingBackend.length > 20) console.warn(` …还有 ${result.missingBackend.length - 20} 个`)
|
|
546
|
+
console.warn(' 提示:检查是否后端漏实现,或前端调用了尚未实现的端点。确认无误可在 design.md 标注豁免。')
|
|
547
|
+
}
|
package/src/worktree-apply.js
CHANGED
|
@@ -49,14 +49,34 @@ export function filterDeliverableFiles(files) {
|
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
/**
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
52
|
+
* 批量获取多个文件在某个 treeish 中的 blob hash(一次 git ls-tree 替代 N 次 rev-parse)。
|
|
53
|
+
*
|
|
54
|
+
* 语义等价于对每个文件调 `git rev-parse <treeish>:<path>`:
|
|
55
|
+
* - 文件在 tree 中 → Map<f, hash>(与 rev-parse 返回的同一 blob hash)
|
|
56
|
+
* - 文件不在 tree 中 → 不在 Map 中(调用方 map.get(f) ?? null 得 null,等同 rev-parse 失败→null)
|
|
57
|
+
* ls-tree 输出 "<mode> <type> <hash>\t<path>"。path 恒为文件路径(来自 git diff/ls-files),
|
|
58
|
+
* 非目录,故不带 -r 也正确——文件在 tree 时 rev-parse treeish:path 与 ls-tree 都给同一 hash,
|
|
59
|
+
* 不在时都不给,等价。(沿用 git() 字符串拼接模式,不引入新的引号/空格破法。)
|
|
60
|
+
*
|
|
61
|
+
* @param {string} cwd
|
|
62
|
+
* @param {string} treeish
|
|
63
|
+
* @param {string[]} files
|
|
64
|
+
* @returns {Map<string, string>} path → blob hash(仅含存在于 tree 中的文件)
|
|
57
65
|
*/
|
|
58
|
-
function
|
|
59
|
-
|
|
66
|
+
function getBlobHashMap(cwd, treeish, files) {
|
|
67
|
+
const map = new Map();
|
|
68
|
+
if (files.length === 0) return map; // 空 pathspec 会让 ls-tree 列出整棵树,必须拦截
|
|
69
|
+
const raw = gitQuiet(cwd, `ls-tree ${treeish} -- ${files.join(' ')}`);
|
|
70
|
+
if (!raw) return map;
|
|
71
|
+
for (const line of raw.split('\n')) {
|
|
72
|
+
if (!line) continue;
|
|
73
|
+
const tabIdx = line.indexOf('\t');
|
|
74
|
+
if (tabIdx === -1) continue;
|
|
75
|
+
const filePath = line.slice(tabIdx + 1);
|
|
76
|
+
const hash = line.slice(0, tabIdx).split(' ')[2]; // "<mode> <type> <hash>"
|
|
77
|
+
if (hash) map.set(filePath, hash);
|
|
78
|
+
}
|
|
79
|
+
return map;
|
|
60
80
|
}
|
|
61
81
|
|
|
62
82
|
/**
|
|
@@ -203,10 +223,14 @@ export function applyWorktree(changeName, { cwd, checkOnly = false, merge = fals
|
|
|
203
223
|
}
|
|
204
224
|
|
|
205
225
|
// 5b. 对比 worktree 的 baseHash 和主工作区 HEAD 中每个清单文件的 blob hash
|
|
226
|
+
// 批量化:两次 ls-tree(worktree 的 baseHash + 主仓库 HEAD)各建一张 path→hash Map,
|
|
227
|
+
// 替代 per-file getFileBlobHash × 2(原 2N spawn → 固定 2)。语义等价见 getBlobHashMap。
|
|
206
228
|
const targetFiles = hasAllowList ? [...allowSet] : changedFiles;
|
|
229
|
+
const wtHashMap = getBlobHashMap(worktreePath, baseHash, targetFiles);
|
|
230
|
+
const mainHashMap = getBlobHashMap(projectRoot, 'HEAD', targetFiles);
|
|
207
231
|
for (const f of targetFiles) {
|
|
208
|
-
const wtBlob =
|
|
209
|
-
const mainBlob =
|
|
232
|
+
const wtBlob = wtHashMap.get(f) ?? null;
|
|
233
|
+
const mainBlob = mainHashMap.get(f) ?? null;
|
|
210
234
|
|
|
211
235
|
// 两者都为 null(文件在 base 时不存在)→ OK
|
|
212
236
|
if (wtBlob === null && mainBlob === null) continue;
|
|
@@ -234,7 +258,6 @@ export function applyWorktree(changeName, { cwd, checkOnly = false, merge = fals
|
|
|
234
258
|
const patchFiles = hasAllowList
|
|
235
259
|
? [...allowSet].filter(f => changedFiles.includes(f))
|
|
236
260
|
: changedFiles;
|
|
237
|
-
const fileArgs = patchFiles.length > 0 ? `-- ${patchFiles.join(' ')}` : '';
|
|
238
261
|
|
|
239
262
|
// 创建临时文件
|
|
240
263
|
const tmpDir = mkdtempSync(join(tmpdir(), 'sillyspec-patch-'));
|
|
@@ -245,14 +268,18 @@ export function applyWorktree(changeName, { cwd, checkOnly = false, merge = fals
|
|
|
245
268
|
let patchContent = '';
|
|
246
269
|
|
|
247
270
|
// 分 tracked 变更和 untracked 新文件生成 patch
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
271
|
+
// 批量化:一次 ls-tree(diffBase tree 中存在的文件)+ 一次 ls-files(index 中存在的文件)
|
|
272
|
+
// 建集合,替代 per-file cat-file -e / ls-files --error-unmatch(原至多 2N spawn → 固定 2)。
|
|
273
|
+
// 语义等价:cat-file -e diffBase:f 成功 ⟺ f 在 ls-tree diffBase 输出(getBlobHashMap key);
|
|
274
|
+
// ls-files --error-unmatch f 成功 ⟺ f 在 ls-files -- 输出。
|
|
275
|
+
const inTree = getBlobHashMap(worktreePath, diffBase, patchFiles);
|
|
276
|
+
const inIndexList = patchFiles.length > 0
|
|
277
|
+
? (gitQuiet(worktreePath, `ls-files -- ${patchFiles.join(' ')}`) || '').split('\n').filter(Boolean)
|
|
278
|
+
: [];
|
|
279
|
+
const inIndex = new Set(inIndexList);
|
|
280
|
+
const trackedFiles = patchFiles.filter(f => inTree.has(f) || inIndex.has(f));
|
|
281
|
+
const trackedSet = new Set(trackedFiles);
|
|
282
|
+
const untrackedPatchFiles = patchFiles.filter(f => !trackedSet.has(f));
|
|
256
283
|
|
|
257
284
|
// tracked 文件:git diff baseHash
|
|
258
285
|
if (trackedFiles.length > 0) {
|
package/src/worktree.js
CHANGED
|
@@ -18,6 +18,11 @@ const WORKTREES_REL = '.sillyspec/.runtime/worktrees';
|
|
|
18
18
|
const BRANCH_PREFIX = 'sillyspec/';
|
|
19
19
|
const META_FILE = 'meta.json';
|
|
20
20
|
|
|
21
|
+
// 进程级缓存:cwd → 主仓库根目录。避免每次 new WorktreeManager 都 spawn 一次
|
|
22
|
+
// `git rev-parse --git-common-dir`(Windows 上每次 spawn 约 30-100ms,execute 一次命令
|
|
23
|
+
// 会 new 多个 WorktreeManager)。git-common-dir 在进程内对同一 cwd 稳定,缓存安全。
|
|
24
|
+
const _mainRepoRootByCwd = new Map();
|
|
25
|
+
|
|
21
26
|
/**
|
|
22
27
|
* 检测当前目录的隔离状态
|
|
23
28
|
* 返回 { inWorktree: boolean, inSubmodule: boolean }
|
|
@@ -224,24 +229,27 @@ export class WorktreeManager {
|
|
|
224
229
|
* @private
|
|
225
230
|
*/
|
|
226
231
|
_resolveMainRepoRoot() {
|
|
232
|
+
const cached = _mainRepoRootByCwd.get(this.cwd);
|
|
233
|
+
if (cached !== undefined) return cached;
|
|
234
|
+
|
|
235
|
+
let root = this.cwd;
|
|
227
236
|
try {
|
|
228
237
|
// git-common-dir 在主仓库内 = <main>/.git
|
|
229
238
|
// 在 linked worktree 内 = <main>/.git(git 共享 .git 目录)
|
|
230
239
|
const commonDir = gitQuiet(this.cwd, 'rev-parse --git-common-dir');
|
|
231
|
-
if (!commonDir) return this.cwd;
|
|
232
|
-
|
|
233
240
|
// commonDir 应该是 <main-repo>/.git
|
|
234
241
|
// dirname(commonDir) = <main-repo>
|
|
235
|
-
if (existsSync(commonDir)) {
|
|
242
|
+
if (commonDir && existsSync(commonDir)) {
|
|
236
243
|
const st = statSync(commonDir);
|
|
237
244
|
if (st.isDirectory()) {
|
|
238
|
-
|
|
245
|
+
root = dirname(commonDir);
|
|
239
246
|
}
|
|
240
247
|
}
|
|
241
248
|
} catch (e) {
|
|
242
249
|
// 静默 fallback:主仓库内执行或 git 异常
|
|
243
250
|
}
|
|
244
|
-
|
|
251
|
+
_mainRepoRootByCwd.set(this.cwd, root);
|
|
252
|
+
return root;
|
|
245
253
|
}
|
|
246
254
|
|
|
247
255
|
/**
|