sillyspec 3.20.2 → 3.20.3
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-archive/SKILL.md +1 -5
- package/.claude/skills/sillyspec-auto/SKILL.md +2 -8
- package/.claude/skills/sillyspec-brainstorm/SKILL.md +1 -28
- package/.claude/skills/sillyspec-commit/SKILL.md +3 -4
- package/.claude/skills/sillyspec-continue/SKILL.md +4 -5
- package/.claude/skills/sillyspec-doctor/SKILL.md +3 -12
- package/.claude/skills/sillyspec-execute/SKILL.md +2 -15
- package/.claude/skills/sillyspec-explore/SKILL.md +1 -14
- package/.claude/skills/sillyspec-plan/SKILL.md +36 -5
- package/.claude/skills/sillyspec-propose/SKILL.md +0 -4
- package/.claude/skills/sillyspec-quick/SKILL.md +1 -5
- package/.claude/skills/sillyspec-resume/SKILL.md +66 -23
- package/.claude/skills/sillyspec-scan/SKILL.md +1 -5
- package/.claude/skills/sillyspec-state/SKILL.md +8 -8
- package/.claude/skills/sillyspec-status/SKILL.md +1 -5
- package/.claude/skills/sillyspec-verify/SKILL.md +1 -5
- package/.claude/skills/sillyspec-workspace/SKILL.md +3 -11
- package/.sillyspec/changes/archive/2026-04-08-derive-state/design.md +97 -0
- package/.sillyspec/changes/archive/2026-04-08-derive-state/plan.md +51 -0
- package/.sillyspec/changes/archive/2026-04-08-derive-state/proposal.md +29 -0
- package/.sillyspec/changes/archive/2026-04-08-derive-state/requirements.md +34 -0
- package/.sillyspec/changes/archive/2026-04-08-derive-state/tasks.md +13 -0
- package/.sillyspec/changes/archive/2026-04-08-derive-state/verify-result.md +43 -0
- package/.sillyspec/changes/auto-mode/design.md +50 -0
- package/.sillyspec/changes/auto-mode/proposal.md +19 -0
- package/.sillyspec/changes/auto-mode/requirements.md +21 -0
- package/.sillyspec/changes/auto-mode/tasks.md +7 -0
- package/.sillyspec/changes/brainstorm-archive/2026-04-05-dashboard-design.md +206 -0
- package/.sillyspec/changes/brainstorm-archive/2026-04-05-unified-docs-design.md +199 -0
- package/.sillyspec/changes/dashboard/design.md +219 -0
- package/.sillyspec/changes/dashboard/design.md.braindraft +206 -0
- package/.sillyspec/changes/run-command-design/design.md +1230 -0
- package/.sillyspec/changes/unified-docs-design/design.md +199 -0
- package/.sillyspec/docs/sillyspec/scan/.gitkeep +0 -0
- package/.sillyspec/knowledge/INDEX.md +8 -0
- package/.sillyspec/knowledge/uncategorized.md +3 -0
- package/.sillyspec/plans/2026-04-05-dashboard.md +737 -0
- package/.sillyspec/projects/sillyspec.yaml +3 -0
- package/README.md +11 -13
- package/SKILL.md +40 -44
- package/dist/steps/brainstorm/01-load-context.md +30 -0
- package/dist/steps/brainstorm/02-reuse-check.md +6 -0
- package/dist/steps/brainstorm/03-prototype-analysis.md +11 -0
- package/dist/steps/brainstorm/04-module-split.md +23 -0
- package/dist/steps/brainstorm/05-dialog-explore.md +8 -0
- package/dist/steps/brainstorm/06-propose-approaches.md +3 -0
- package/dist/steps/brainstorm/07-present-design.md +3 -0
- package/dist/steps/brainstorm/08-write-design.md +21 -0
- package/dist/steps/brainstorm/09-self-review.md +15 -0
- package/dist/steps/brainstorm/10-user-confirm.md +3 -0
- package/dist/steps/brainstorm/11-output-spec.md +7 -0
- package/dist/steps/brainstorm/manifest.yaml +26 -0
- package/dist/steps/execute/01-load-context.md +41 -0
- package/dist/steps/execute/02-scan-conventions.md +47 -0
- package/dist/steps/execute/03-skill-mcp.md +19 -0
- package/dist/steps/execute/04-assign-task.md +22 -0
- package/dist/steps/execute/04b-prompt-template.md +54 -0
- package/dist/steps/execute/05-write-test.md +7 -0
- package/dist/steps/execute/06-write-code.md +8 -0
- package/dist/steps/execute/07-run-test.md +26 -0
- package/dist/steps/execute/08-fix-issues.md +28 -0
- package/dist/steps/execute/09-next-task.md +33 -0
- package/dist/steps/execute/manifest.yaml +28 -0
- package/dist/steps/plan/01-load-context.md +22 -0
- package/dist/steps/plan/02-anchor-confirm.md +1 -0
- package/dist/steps/plan/03-expand-tasks.md +33 -0
- package/dist/steps/plan/04-mark-order.md +15 -0
- package/dist/steps/plan/05-e2e-planning.md +17 -0
- package/dist/steps/plan/06-self-check.md +16 -0
- package/dist/steps/plan/07-save.md +1 -0
- package/dist/steps/plan/manifest.yaml +18 -0
- package/dist/steps/scan/01-env-detect.md +51 -0
- package/dist/steps/scan/02-tech-stack.md +16 -0
- package/dist/steps/scan/03-conventions.md +16 -0
- package/dist/steps/scan/04-structure.md +19 -0
- package/dist/steps/scan/05-quality.md +18 -0
- package/dist/steps/scan/06-complete.md +49 -0
- package/dist/steps/scan/manifest.yaml +16 -0
- package/dist/steps/verify/01-load-specs.md +28 -0
- package/dist/steps/verify/02-check-tasks.md +1 -0
- package/dist/steps/verify/03-check-design.md +6 -0
- package/dist/steps/verify/04-run-tests.md +7 -0
- package/dist/steps/verify/05-e2e-tests.md +27 -0
- package/dist/steps/verify/05b-e2e-fix.md +33 -0
- package/dist/steps/verify/06-code-quality.md +25 -0
- package/dist/steps/verify/07-lint-check.md +27 -0
- package/dist/steps/verify/08-output-report.md +14 -0
- package/dist/steps/verify/manifest.yaml +22 -0
- package/package.json +1 -7
- package/packages/dashboard/dist/assets/{index-Bq_Z2hne.js → index-D1EVTLmc.js} +1264 -1264
- package/packages/dashboard/dist/assets/index-DGe8CqeP.css +1 -0
- package/packages/dashboard/dist/index.html +2 -2
- package/packages/dashboard/package-lock.json +6 -6
- package/packages/dashboard/server/executor.js +1 -1
- package/packages/dashboard/server/index.js +18 -98
- package/packages/dashboard/server/parser.js +71 -139
- package/packages/dashboard/server/watcher.js +6 -14
- package/packages/dashboard/src/App.vue +185 -418
- package/packages/dashboard/src/components/ActionBar.vue +1 -10
- package/packages/dashboard/src/components/CommandPalette.vue +1 -5
- package/packages/dashboard/src/components/DocPreview.vue +8 -105
- package/packages/dashboard/src/components/DocTree.vue +19 -75
- package/packages/dashboard/src/components/PipelineStage.vue +2 -22
- package/packages/dashboard/src/components/PipelineView.vue +5 -32
- package/packages/dashboard/src/components/ProjectOverview.vue +139 -113
- package/packages/dashboard/src/components/StageBadge.vue +3 -17
- package/packages/dashboard/src/components/StepCard.vue +2 -7
- package/packages/dashboard/src/composables/useDashboard.js +0 -28
- package/src/derive.js +147 -0
- package/src/index.js +40 -688
- package/src/init.js +63 -119
- package/src/migrate.js +7 -7
- package/src/progress.js +248 -1474
- package/src/run.js +302 -3008
- package/src/setup.js +64 -2
- package/src/stages/archive.js +17 -123
- package/src/stages/brainstorm.js +48 -454
- package/src/stages/doctor.js +46 -99
- package/src/stages/execute.js +59 -420
- package/src/stages/index.js +18 -12
- package/src/stages/plan.js +189 -492
- package/src/stages/propose.js +11 -70
- package/src/stages/quick.js +13 -52
- package/src/stages/scan.js +68 -485
- package/src/stages/status.js +1 -1
- package/src/stages/verify.js +16 -203
- package/src/step.js +543 -0
- package/.claude/skills/sillyspec-knowledge/SKILL.md +0 -270
- package/.husky/pre-push +0 -13
- package/CLAUDE.md +0 -18
- package/docs/brainstorm-plan-contract.md +0 -64
- package/docs/plan-execute-contract.md +0 -123
- package/docs/platform-scan-protocol.md +0 -298
- package/docs/revision-mode.md +0 -115
- package/docs/sillyspec/file-lifecycle/known-implementation-gaps.md +0 -99
- package/docs/sillyspec/file-lifecycle/platform-workflows-sync.md +0 -218
- package/docs/sillyspec/file-lifecycle/stage-artifacts.md +0 -167
- package/docs/sillyspec/file-lifecycle/storage-and-state.md +0 -148
- package/docs/sillyspec/file-lifecycle/worktree-and-guard.md +0 -193
- package/docs/sillyspec/file-lifecycle.md +0 -125
- package/docs/workflow-contract-regression.md +0 -106
- package/docs/worktree-isolation.md +0 -252
- package/packages/dashboard/dist/assets/index-O2W5RV4z.css +0 -1
- package/packages/dashboard/dist/prototype-dashboard.html +0 -836
- package/packages/dashboard/dist/prototype-overview.html +0 -256
- package/packages/dashboard/public/prototype-dashboard.html +0 -836
- package/packages/dashboard/public/prototype-overview.html +0 -256
- package/packages/dashboard/src/components/HResizeHandle.vue +0 -48
- package/packages/dashboard/src/components/ProjectCard.vue +0 -187
- package/packages/dashboard/src/components/VResizeHandle.vue +0 -61
- package/packages/dashboard/src/composables/useLayout.js +0 -131
- package/src/brainstorm-postcheck.js +0 -158
- package/src/change-list.js +0 -52
- package/src/change-risk-profile.js +0 -352
- package/src/classify-change.js +0 -73
- package/src/constants.js +0 -70
- package/src/contract-matrix.js +0 -278
- package/src/db.js +0 -201
- package/src/endpoint-extractor.js +0 -315
- package/src/hooks/claude-pre-tool-use.cjs +0 -125
- package/src/hooks/worktree-guard.js +0 -761
- package/src/knowledge-match.js +0 -130
- package/src/modules.js +0 -482
- package/src/scan-postcheck.js +0 -383
- package/src/stage-contract.js +0 -700
- package/src/stages/brainstorm-auto.js +0 -229
- package/src/stages/explore.js +0 -34
- package/src/stages/knowledge.js +0 -498
- package/src/stages/plan-postcheck.js +0 -513
- package/src/sync.js +0 -497
- package/src/task-review.js +0 -346
- package/src/workflow.js +0 -785
- package/src/worktree-apply.js +0 -549
- package/src/worktree.js +0 -932
- package/templates/workflows/archive-impact.yaml +0 -79
- package/templates/workflows/scan-docs.yaml +0 -132
- package/test/brainstorm-plan-contract.test.mjs +0 -273
- package/test/check-syntax.mjs +0 -26
- package/test/cli-top-level-aliases.test.mjs +0 -174
- package/test/contract-artifacts.test.mjs +0 -323
- package/test/decision-supersede.test.mjs +0 -277
- package/test/knowledge-match.test.mjs +0 -231
- package/test/plan-execute-contract.test.mjs +0 -330
- package/test/plan-optimization.test.mjs +0 -572
- package/test/platform-artifacts.test.mjs +0 -190
- package/test/platform-failure-samples.test.mjs +0 -199
- package/test/platform-recovery-chain.test.mjs +0 -179
- package/test/platform-recovery.test.mjs +0 -167
- package/test/platform-scan-p0.test.mjs +0 -175
- package/test/revision-v1.test.mjs +0 -1145
- package/test/run-sanitize-project-name.test.mjs +0 -51
- package/test/run-scan-postcheck-fail.test.mjs +0 -64
- package/test/run-scan-project-parse.test.mjs +0 -200
- package/test/run-tests.mjs +0 -48
- package/test/scan-docs-yaml-placeholders.test.mjs +0 -84
- package/test/scan-knowledge.test.mjs +0 -175
- package/test/scan-paths.test.mjs +0 -68
- package/test/scan-postcheck-project-priority.test.mjs +0 -85
- package/test/scan-postcheck.test.mjs +0 -197
- package/test/scan-workflow-anyfailed-block.test.mjs +0 -52
- package/test/spec-dir.test.mjs +0 -206
- package/test/stage-contract-failed-post-check.test.mjs +0 -102
- package/test/stage-contract.test.mjs +0 -299
- package/test/stage-definitions.test.mjs +0 -39
- package/test/wait-gates.test.mjs +0 -501
- package/test/workflow-spec-base.test.mjs +0 -142
- package/test/worktree-guard.test.mjs +0 -136
- package/test/worktree-native-overlay.test.mjs +0 -188
|
@@ -1,513 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Plan Postcheck 模块
|
|
3
|
-
*
|
|
4
|
-
* 从 run.js 和 plan.js 拆出的确定性校验逻辑:
|
|
5
|
-
* - 拓扑排序(topoSortWaves)
|
|
6
|
-
* - 蓝图一致性校验(validateBlueprintConsistency)
|
|
7
|
-
* - plan 产物校验(validatePlanArtifacts)
|
|
8
|
-
* - 执行入口(executePlanPostcheck)
|
|
9
|
-
*
|
|
10
|
-
* 这些都是 noAI 步骤,不需要 LLM 参与。
|
|
11
|
-
*/
|
|
12
|
-
import { existsSync, readFileSync, readdirSync } from 'fs'
|
|
13
|
-
import { join as pJoin } from 'path'
|
|
14
|
-
|
|
15
|
-
// ═══════════════════════════════════════════════════════════════
|
|
16
|
-
// 解析工具(从 plan.js 迁移)
|
|
17
|
-
// ═══════════════════════════════════════════════════════════════
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* 从 task-NN.md frontmatter 解析 depends_on 字段
|
|
21
|
-
* @param {string} content - task 文件内容
|
|
22
|
-
* @returns {string[]}
|
|
23
|
-
*/
|
|
24
|
-
function parseDependsOn(content) {
|
|
25
|
-
const fmMatch = content.match(/^---\n([\s\S]*?)\n---/)
|
|
26
|
-
if (!fmMatch) return []
|
|
27
|
-
const fm = fmMatch[1]
|
|
28
|
-
const inlineMatch = fm.match(/depends_on:\s*\[([^\]]*)\]/)
|
|
29
|
-
if (inlineMatch) {
|
|
30
|
-
return inlineMatch[1].split(',').map(s => s.trim().replace(/['"]/g, '')).filter(Boolean)
|
|
31
|
-
}
|
|
32
|
-
const blockMatch = fm.match(/depends_on:\s*\n((?:\s+-\s+.+\n?)+)/)
|
|
33
|
-
if (blockMatch) {
|
|
34
|
-
return blockMatch[1].match(/-\s+(.+)/g)?.map(s => s.replace(/^-\s+/, '').trim().replace(/['"]/g, '')) || []
|
|
35
|
-
}
|
|
36
|
-
return []
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* 解析 task-NN.md 的 task id(从 frontmatter 或文件名)
|
|
41
|
-
* @param {string} content - task 文件内容
|
|
42
|
-
* @param {string} filename - 文件名
|
|
43
|
-
* @returns {string|null}
|
|
44
|
-
*/
|
|
45
|
-
function parseTaskId(content, filename) {
|
|
46
|
-
const fmMatch = content.match(/^---\n([\s\S]*?)\n---/)
|
|
47
|
-
if (fmMatch) {
|
|
48
|
-
const idMatch = fmMatch[1].match(/^id:\s*(.+)/m)
|
|
49
|
-
if (idMatch) return idMatch[1].trim()
|
|
50
|
-
}
|
|
51
|
-
const fileMatch = filename.match(/(task-\d+)/i)
|
|
52
|
-
return fileMatch ? fileMatch[1] : null
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* 解析 task-NN.md 的 allowed_paths
|
|
57
|
-
* @param {string} content
|
|
58
|
-
* @returns {string[]}
|
|
59
|
-
*/
|
|
60
|
-
function parseAllowedPaths(content) {
|
|
61
|
-
const fmMatch = content.match(/^---\n([\s\S]*?)\n---/)
|
|
62
|
-
if (!fmMatch) return []
|
|
63
|
-
const fm = fmMatch[1]
|
|
64
|
-
const inlineMatch = fm.match(/allowed_paths:\s*\[([^\]]*)\]/)
|
|
65
|
-
if (inlineMatch) {
|
|
66
|
-
return inlineMatch[1].split(',').map(s => s.trim().replace(/['"]/g, '')).filter(Boolean)
|
|
67
|
-
}
|
|
68
|
-
const blockMatch = fm.match(/allowed_paths:\s*\n((?:\s+-\s+.+\n?)+)/)
|
|
69
|
-
if (blockMatch) {
|
|
70
|
-
return blockMatch[1].match(/-\s+(.+)/g)?.map(s => s.replace(/^-\s+/, '').trim().replace(/['"]/g, '')) || []
|
|
71
|
-
}
|
|
72
|
-
return []
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* 检查 task-NN.md 是否包含验收标准章节
|
|
77
|
-
* @param {string} content
|
|
78
|
-
* @returns {boolean}
|
|
79
|
-
*/
|
|
80
|
-
function hasAcceptanceCriteria(content) {
|
|
81
|
-
return /##\s*验收标准/.test(content) || /##\s*Acceptance/.test(content)
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* 检查 task-NN.md 是否包含 TDD/验证步骤
|
|
86
|
-
* @param {string} content
|
|
87
|
-
* @returns {boolean}
|
|
88
|
-
*/
|
|
89
|
-
function hasTddOrVerify(content) {
|
|
90
|
-
return /##\s*TDD/.test(content) || /##\s*验证/.test(content) || /##\s*Verify/.test(content)
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
// ═══════════════════════════════════════════════════════════════
|
|
94
|
-
// 核心逻辑
|
|
95
|
-
// ═══════════════════════════════════════════════════════════════
|
|
96
|
-
|
|
97
|
-
/**
|
|
98
|
-
* 拓扑排序:根据 depends_on 计算波次
|
|
99
|
-
* @param {Map<string, string[]>} depMap - taskId → depends_on list
|
|
100
|
-
* @returns {{ waves: string[][], error: string|null }}
|
|
101
|
-
*/
|
|
102
|
-
export function topoSortWaves(depMap) {
|
|
103
|
-
const tasks = [...depMap.keys()]
|
|
104
|
-
const waves = []
|
|
105
|
-
const assigned = new Set()
|
|
106
|
-
const visited = new Set()
|
|
107
|
-
|
|
108
|
-
// 先做循环依赖检测(DFS)
|
|
109
|
-
function hasCycle(task, path) {
|
|
110
|
-
if (path.has(task)) return true
|
|
111
|
-
if (visited.has(task)) return false
|
|
112
|
-
path.add(task)
|
|
113
|
-
const deps = depMap.get(task) || []
|
|
114
|
-
for (const dep of deps) {
|
|
115
|
-
if (!depMap.has(dep)) continue // 依赖不存在(可能是外部引用),跳过
|
|
116
|
-
if (hasCycle(dep, path)) return true
|
|
117
|
-
}
|
|
118
|
-
path.delete(task)
|
|
119
|
-
visited.add(task)
|
|
120
|
-
return false
|
|
121
|
-
}
|
|
122
|
-
for (const task of tasks) {
|
|
123
|
-
if (hasCycle(task, new Set())) {
|
|
124
|
-
return { waves: [], error: `检测到循环依赖,涉及 task: ${task}` }
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
// 逐层分配 Wave
|
|
129
|
-
while (assigned.size < tasks.length) {
|
|
130
|
-
const currentWave = []
|
|
131
|
-
for (const task of tasks) {
|
|
132
|
-
if (assigned.has(task)) continue
|
|
133
|
-
const deps = (depMap.get(task) || []).filter(d => depMap.has(d))
|
|
134
|
-
if (deps.every(d => assigned.has(d))) {
|
|
135
|
-
currentWave.push(task)
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
if (currentWave.length === 0) {
|
|
139
|
-
const remaining = tasks.filter(t => !assigned.has(t))
|
|
140
|
-
return { waves: [], error: `无法解析依赖关系,剩余 task: ${remaining.join(', ')}` }
|
|
141
|
-
}
|
|
142
|
-
for (const task of currentWave) {
|
|
143
|
-
assigned.add(task)
|
|
144
|
-
}
|
|
145
|
-
waves.push(currentWave)
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
return { waves, error: null }
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
/**
|
|
152
|
-
* 本地一致性校验器
|
|
153
|
-
* @param {string} changeDir - 变更目录
|
|
154
|
-
* @returns {{ ok: boolean, errors: string[], warnings: string[] }}
|
|
155
|
-
*/
|
|
156
|
-
export function validateBlueprintConsistency(changeDir) {
|
|
157
|
-
const errors = []
|
|
158
|
-
const warnings = []
|
|
159
|
-
|
|
160
|
-
const tasksDir = pJoin(changeDir, 'tasks')
|
|
161
|
-
if (!existsSync(tasksDir)) {
|
|
162
|
-
return { ok: false, errors: ['tasks/ 目录不存在'], warnings }
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
const taskFiles = readdirSync(tasksDir).filter(f => /^task-\d+\.md$/.test(f))
|
|
166
|
-
if (taskFiles.length === 0) {
|
|
167
|
-
return { ok: false, errors: ['tasks/ 目录下没有 task-NN.md 文件'], warnings }
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
const taskInfo = new Map()
|
|
171
|
-
const pathOwners = new Map()
|
|
172
|
-
|
|
173
|
-
for (const file of taskFiles) {
|
|
174
|
-
const filePath = pJoin(tasksDir, file)
|
|
175
|
-
const content = readFileSync(filePath, 'utf8')
|
|
176
|
-
const taskId = parseTaskId(content, file)
|
|
177
|
-
if (!taskId) {
|
|
178
|
-
errors.push(`${file}: 无法解析 task id`)
|
|
179
|
-
continue
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
const dependsOn = parseDependsOn(content)
|
|
183
|
-
const allowedPaths = parseAllowedPaths(content)
|
|
184
|
-
const hasAcceptance = hasAcceptanceCriteria(content)
|
|
185
|
-
const hasTdd = hasTddOrVerify(content)
|
|
186
|
-
|
|
187
|
-
taskInfo.set(taskId, { dependsOn, allowedPaths, hasAcceptance, hasTdd, file })
|
|
188
|
-
|
|
189
|
-
if (allowedPaths.length === 0) {
|
|
190
|
-
errors.push(`${taskId} (${file}): 缺少 allowed_paths`)
|
|
191
|
-
}
|
|
192
|
-
if (!hasAcceptance) {
|
|
193
|
-
errors.push(`${taskId} (${file}): 缺少「验收标准」章节`)
|
|
194
|
-
}
|
|
195
|
-
if (!hasTdd) {
|
|
196
|
-
warnings.push(`${taskId} (${file}): 缺少 TDD/验证步骤章节`)
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
for (const p of allowedPaths) {
|
|
200
|
-
if (!pathOwners.has(p)) pathOwners.set(p, [])
|
|
201
|
-
pathOwners.get(p).push(taskId)
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
// 路径冲突
|
|
206
|
-
for (const [p, owners] of pathOwners) {
|
|
207
|
-
if (owners.length > 1) {
|
|
208
|
-
warnings.push(`路径 ${p} 被 ${owners.length} 个 task 修改: ${owners.join(', ')}(确认是否为有意共享)`)
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
// 拓扑排序 + 循环依赖
|
|
213
|
-
const depMap = new Map()
|
|
214
|
-
for (const [taskId, info] of taskInfo) {
|
|
215
|
-
depMap.set(taskId, info.dependsOn)
|
|
216
|
-
}
|
|
217
|
-
const { error: topoError } = topoSortWaves(depMap)
|
|
218
|
-
if (topoError) {
|
|
219
|
-
errors.push(topoError)
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
return { ok: errors.length === 0, errors, warnings }
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
/**
|
|
226
|
-
* Plan 可行性校验器(本地代码证明 execute 前置条件)
|
|
227
|
-
* 检查 TaskCard 的完整性和可行性
|
|
228
|
-
* @param {string} changeDir - 变更目录
|
|
229
|
-
* @param {string} projectRoot - 项目根目录(用于检查 allowed_paths 是否存在)
|
|
230
|
-
* @returns {{ ok: boolean, errors: string[], warnings: string[] }}
|
|
231
|
-
*/
|
|
232
|
-
export function validatePlanFeasibility(changeDir, projectRoot = null) {
|
|
233
|
-
const errors = []
|
|
234
|
-
const warnings = []
|
|
235
|
-
|
|
236
|
-
const tasksDir = pJoin(changeDir, 'tasks')
|
|
237
|
-
if (!existsSync(tasksDir)) {
|
|
238
|
-
return { ok: true, errors, warnings } // none/light 可能没有 tasks/
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
const taskFiles = readdirSync(tasksDir).filter(f => /^task-\d+\.md$/.test(f)).sort()
|
|
242
|
-
if (taskFiles.length === 0) {
|
|
243
|
-
return { ok: true, errors, warnings }
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
const allTaskIds = []
|
|
247
|
-
const depMap = new Map()
|
|
248
|
-
|
|
249
|
-
for (const file of taskFiles) {
|
|
250
|
-
const content = readFileSync(pJoin(tasksDir, file), 'utf8')
|
|
251
|
-
const fmMatch = content.match(/^---\n([\s\S]*?)\n---/)
|
|
252
|
-
if (!fmMatch) {
|
|
253
|
-
errors.push(`${file}: 缺少 YAML frontmatter`)
|
|
254
|
-
continue
|
|
255
|
-
}
|
|
256
|
-
const fm = fmMatch[1]
|
|
257
|
-
const body = content.slice(fmMatch[0].length)
|
|
258
|
-
|
|
259
|
-
// 1. 必要字段检查
|
|
260
|
-
const taskId = (fm.match(/^id:\s*(.+)/m)?.[1] || '').trim()
|
|
261
|
-
const title = (fm.match(/^title:\s*(.+)/m)?.[1] || '').trim()
|
|
262
|
-
const allowedPathsRaw = fm.match(/allowed_paths:\s*\n((?:\s+-\s+.+\n?)+)/)?.[1] || ''
|
|
263
|
-
const allowedPaths = allowedPathsRaw.match(/-\s+(.+)/g)?.map(s => s.replace(/^-\s+/, '').trim()) || []
|
|
264
|
-
|
|
265
|
-
if (!taskId) errors.push(`${file}: frontmatter 缺少 id`)
|
|
266
|
-
if (!title) errors.push(`${file}: frontmatter 缺少 title`)
|
|
267
|
-
if (taskId) allTaskIds.push(taskId)
|
|
268
|
-
|
|
269
|
-
// 2. allowed_paths 不为空
|
|
270
|
-
if (allowedPaths.length === 0) {
|
|
271
|
-
errors.push(`${taskId || file}: allowed_paths 为空`)
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
// 3. allowed_paths 文件存在或父目录存在(仅在 projectRoot 提供时检查)
|
|
275
|
-
if (projectRoot && allowedPaths.length > 0) {
|
|
276
|
-
for (const p of allowedPaths) {
|
|
277
|
-
const fullPath = pJoin(projectRoot, p)
|
|
278
|
-
const parentDir = pJoin(fullPath, '..')
|
|
279
|
-
if (!existsSync(fullPath) && !existsSync(parentDir)) {
|
|
280
|
-
warnings.push(`${taskId}: allowed_paths 中的 ${p} 文件和父目录都不存在`)
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
// 4. depends_on 引用存在
|
|
286
|
-
const dependsOn = parseDependsOn(content)
|
|
287
|
-
if (taskId) depMap.set(taskId, dependsOn)
|
|
288
|
-
|
|
289
|
-
// 5. body/frontmatter 包含必要字段(TaskCard 格式中这些字段在 frontmatter 内)
|
|
290
|
-
const hasGoal = /^goal:/m.test(fm)
|
|
291
|
-
const hasImplementation = /implementation:/m.test(fm)
|
|
292
|
-
const hasAcceptance = /acceptance:/m.test(fm)
|
|
293
|
-
const hasVerify = /verify:/m.test(fm)
|
|
294
|
-
const hasConstraints = /constraints:/m.test(fm)
|
|
295
|
-
|
|
296
|
-
if (!hasGoal) errors.push(`${taskId || file}: 缺少 goal 字段`)
|
|
297
|
-
if (!hasImplementation) errors.push(`${taskId || file}: 缺少 implementation 字段`)
|
|
298
|
-
if (!hasAcceptance) errors.push(`${taskId || file}: 缺少 acceptance 字段`)
|
|
299
|
-
if (!hasVerify) errors.push(`${taskId || file}: 缺少 verify 字段`)
|
|
300
|
-
if (!hasConstraints) errors.push(`${taskId || file}: 缺少 constraints 字段`)
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
// 4b. depends_on 引用存在性
|
|
304
|
-
for (const [taskId, deps] of depMap) {
|
|
305
|
-
for (const dep of deps) {
|
|
306
|
-
if (!depMap.has(dep)) {
|
|
307
|
-
errors.push(`${taskId}: depends_on 引用了不存在的 ${dep}`)
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
// 5b. depends_on 无环(topoSortWaves 已含循环检测)
|
|
313
|
-
const { error: topoError } = topoSortWaves(depMap)
|
|
314
|
-
if (topoError) {
|
|
315
|
-
errors.push(topoError)
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
// 7. task id 连续性
|
|
319
|
-
if (allTaskIds.length > 0) {
|
|
320
|
-
const nums = allTaskIds.map(id => {
|
|
321
|
-
const m = id.match(/task-(\d+)/i)
|
|
322
|
-
return m ? parseInt(m[1]) : null
|
|
323
|
-
}).filter(n => n !== null).sort((a, b) => a - b)
|
|
324
|
-
if (nums.length > 0 && nums[0] === 1) {
|
|
325
|
-
for (let i = 0; i < nums.length; i++) {
|
|
326
|
-
if (nums[i] !== i + 1) {
|
|
327
|
-
errors.push(`task id 不连续: 期望 task-${String(i + 1).padStart(2, '0')}, 实际 task-${String(nums[i]).padStart(2, '0')}`)
|
|
328
|
-
break
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
// 9. plan.md 通过 validatePlanForExecute(延迟导入避免循环依赖)
|
|
335
|
-
// 这在 run.js 的 postcheck contract 中已检查,这里不重复
|
|
336
|
-
|
|
337
|
-
return { ok: errors.length === 0, errors, warnings }
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
/**
|
|
341
|
-
* Plan 产物校验:检查 plan.md 和 tasks/ 是否齐全
|
|
342
|
-
* @param {string} changeDir - 变更目录
|
|
343
|
-
* @returns {{ ok: boolean, errors: string[], warnings: string[], planExists: boolean, taskCount: number }}
|
|
344
|
-
*/
|
|
345
|
-
export function validatePlanArtifacts(changeDir) {
|
|
346
|
-
const errors = []
|
|
347
|
-
const warnings = []
|
|
348
|
-
|
|
349
|
-
const planPath = pJoin(changeDir, 'plan.md')
|
|
350
|
-
if (!existsSync(planPath)) {
|
|
351
|
-
return { ok: false, errors: ['plan.md 不存在'], warnings, planExists: false, taskCount: 0 }
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
const tasksDir = pJoin(changeDir, 'tasks')
|
|
355
|
-
let taskCount = 0
|
|
356
|
-
if (existsSync(tasksDir)) {
|
|
357
|
-
taskCount = readdirSync(tasksDir).filter(f => /^task-\d+\.md$/.test(f)).length
|
|
358
|
-
} else {
|
|
359
|
-
warnings.push('tasks/ 目录不存在(plan_level=none 时可接受)')
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
return { ok: true, errors, warnings, planExists: true, taskCount }
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
// ═══════════════════════════════════════════════════════════════
|
|
366
|
-
// 执行入口(从 run.js 迁移)
|
|
367
|
-
// ═══════════════════════════════════════════════════════════════
|
|
368
|
-
|
|
369
|
-
/**
|
|
370
|
-
* Plan postcheck 主函数:Wave 重排 + 一致性校验 + 产物确认
|
|
371
|
-
*
|
|
372
|
-
* @param {{ cwd: string, specRoot?: string, resolveChangeDir: Function }} context
|
|
373
|
-
* @throws {Error} 校验失败时抛出
|
|
374
|
-
*/
|
|
375
|
-
export async function executePlanPostcheck(context) {
|
|
376
|
-
const { cwd, specRoot, resolveChangeDir } = context
|
|
377
|
-
|
|
378
|
-
const specDir = specRoot || pJoin(cwd, '.sillyspec')
|
|
379
|
-
const changesDir = pJoin(specDir, 'changes')
|
|
380
|
-
if (!existsSync(changesDir)) {
|
|
381
|
-
console.warn(' ⚠️ 未找到 changes 目录,跳过 postcheck')
|
|
382
|
-
return
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
// 找到当前变更目录
|
|
386
|
-
const progressPath = pJoin(specDir, '.runtime', 'progress.json')
|
|
387
|
-
let changeDir = null
|
|
388
|
-
if (existsSync(progressPath)) {
|
|
389
|
-
const progress = JSON.parse(readFileSync(progressPath, 'utf8'))
|
|
390
|
-
changeDir = resolveChangeDir(cwd, progress, specDir)
|
|
391
|
-
}
|
|
392
|
-
if (!changeDir) {
|
|
393
|
-
// 回退:找最新的变更目录
|
|
394
|
-
const dirs = readdirSync(changesDir)
|
|
395
|
-
.filter(d => existsSync(pJoin(changesDir, d, 'plan.md')))
|
|
396
|
-
.sort().reverse()
|
|
397
|
-
if (dirs.length > 0) changeDir = pJoin(changesDir, dirs[0])
|
|
398
|
-
}
|
|
399
|
-
if (!changeDir) {
|
|
400
|
-
console.warn(' ⚠️ 未找到当前变更目录,跳过 postcheck')
|
|
401
|
-
return
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
console.log(` 📂 变更目录: ${changeDir}`)
|
|
405
|
-
|
|
406
|
-
// ── 1. 一致性校验 ──
|
|
407
|
-
const consistency = validateBlueprintConsistency(changeDir)
|
|
408
|
-
if (consistency.errors.length > 0) {
|
|
409
|
-
console.error('\n❌ 蓝图一致性校验失败:')
|
|
410
|
-
for (const err of consistency.errors) console.error(` - ${err}`)
|
|
411
|
-
console.error('\n 请修复上述问题后重新完成此步骤。')
|
|
412
|
-
throw new Error('planPostcheck: blueprint consistency check failed')
|
|
413
|
-
}
|
|
414
|
-
if (consistency.warnings.length > 0) {
|
|
415
|
-
console.warn('\n⚠️ 蓝图一致性警告(不阻断):')
|
|
416
|
-
for (const w of consistency.warnings) console.warn(` - ${w}`)
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
// ── 1b. 可行性校验 ──
|
|
420
|
-
const feasibility = validatePlanFeasibility(changeDir, context.cwd)
|
|
421
|
-
if (feasibility.errors.length > 0) {
|
|
422
|
-
console.error('\n❌ Plan 可行性校验失败:')
|
|
423
|
-
for (const err of feasibility.errors) console.error(` - ${err}`)
|
|
424
|
-
throw new Error('planPostcheck: feasibility check failed')
|
|
425
|
-
}
|
|
426
|
-
if (feasibility.warnings.length > 0) {
|
|
427
|
-
console.warn('\n⚠️ Plan 可行性警告(不阻断):')
|
|
428
|
-
for (const w of feasibility.warnings) console.warn(` - ${w}`)
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
// ── 2. Wave 重排 ──
|
|
432
|
-
const tasksDir = pJoin(changeDir, 'tasks')
|
|
433
|
-
if (existsSync(tasksDir)) {
|
|
434
|
-
const taskFiles = readdirSync(tasksDir).filter(f => /^task-\d+\.md$/.test(f))
|
|
435
|
-
const depMap = new Map()
|
|
436
|
-
|
|
437
|
-
for (const file of taskFiles) {
|
|
438
|
-
const content = readFileSync(pJoin(tasksDir, file), 'utf8')
|
|
439
|
-
const fmMatch = content.match(/^---\n([\s\S]*?)\n---/)
|
|
440
|
-
let taskId = file.replace('.md', '')
|
|
441
|
-
if (fmMatch) {
|
|
442
|
-
const idMatch = fmMatch[1].match(/^id:\s*(.+)/m)
|
|
443
|
-
if (idMatch) taskId = idMatch[1].trim()
|
|
444
|
-
}
|
|
445
|
-
depMap.set(taskId, parseDependsOn(content))
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
const { waves, error: topoError } = topoSortWaves(depMap)
|
|
449
|
-
if (topoError) {
|
|
450
|
-
console.error(`\n❌ Wave 重排失败: ${topoError}`)
|
|
451
|
-
throw new Error('planPostcheck: ' + topoError)
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
console.log('\n 📊 Wave 分组(基于 depends_on 拓扑排序):')
|
|
455
|
-
waves.forEach((wave, i) => {
|
|
456
|
-
console.log(` Wave ${i + 1}: ${wave.join(', ')}`)
|
|
457
|
-
})
|
|
458
|
-
|
|
459
|
-
// 比较 plan.md 现有 Wave 分组
|
|
460
|
-
if (waves.length > 1 && taskFiles.length > 1) {
|
|
461
|
-
const planPath = pJoin(changeDir, 'plan.md')
|
|
462
|
-
if (existsSync(planPath)) {
|
|
463
|
-
const planContent = readFileSync(planPath, 'utf8')
|
|
464
|
-
if (/##\s*Wave\s+\d/i.test(planContent)) {
|
|
465
|
-
const existingWaves = []
|
|
466
|
-
const lines = planContent.split('\n')
|
|
467
|
-
let currentWaveTasks = null
|
|
468
|
-
for (const line of lines) {
|
|
469
|
-
const wm = line.match(/^#+\s*Wave\s+(\d+)/i)
|
|
470
|
-
if (wm) {
|
|
471
|
-
if (currentWaveTasks) existingWaves.push(currentWaveTasks)
|
|
472
|
-
currentWaveTasks = []
|
|
473
|
-
continue
|
|
474
|
-
}
|
|
475
|
-
const tm = line.match(/^[-*]\s*\[[ x]\]\s*task-(\d+)/i)
|
|
476
|
-
if (tm && currentWaveTasks) {
|
|
477
|
-
currentWaveTasks.push(`task-${tm[1]}`)
|
|
478
|
-
}
|
|
479
|
-
}
|
|
480
|
-
if (currentWaveTasks) existingWaves.push(currentWaveTasks)
|
|
481
|
-
|
|
482
|
-
const sameStructure = waves.length === existingWaves.length &&
|
|
483
|
-
waves.every((w, i) => {
|
|
484
|
-
const a = [...w].sort().join(',')
|
|
485
|
-
const b = [...(existingWaves[i] || [])].sort().join(',')
|
|
486
|
-
return a === b
|
|
487
|
-
})
|
|
488
|
-
|
|
489
|
-
if (sameStructure) {
|
|
490
|
-
console.log(' ✅ Wave 分组与拓扑排序一致,无需更新 plan.md')
|
|
491
|
-
} else {
|
|
492
|
-
console.log(' ⚠️ Wave 分组与拓扑排序不一致,建议手动调整 plan.md')
|
|
493
|
-
console.log(' 拓扑排序建议的 Wave 分组见上方')
|
|
494
|
-
}
|
|
495
|
-
}
|
|
496
|
-
}
|
|
497
|
-
}
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
// ── 3. 产物确认 ──
|
|
501
|
-
const artifacts = validatePlanArtifacts(changeDir)
|
|
502
|
-
if (!artifacts.ok) {
|
|
503
|
-
for (const err of artifacts.errors) console.error(`❌ ${err}`)
|
|
504
|
-
throw new Error('planPostcheck: artifact validation failed')
|
|
505
|
-
}
|
|
506
|
-
console.log('\n ✅ plan.md 存在')
|
|
507
|
-
|
|
508
|
-
if (artifacts.taskCount > 0) {
|
|
509
|
-
console.log(` ✅ tasks/ 目录有 ${artifacts.taskCount} 个 TaskCard 文件`)
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
console.log('\n ✅ Plan postcheck 完成')
|
|
513
|
-
}
|