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
package/src/stage-contract.js
DELETED
|
@@ -1,700 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* StageContract — 阶段协议
|
|
3
|
-
*
|
|
4
|
-
* 每个阶段声明:允许的前置阶段、必须的产出、校验器、后续阶段。
|
|
5
|
-
* CLI 不再相信 prompt 完成,completeStep 后必须过 validator。
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { existsSync, readdirSync, readFileSync } from 'fs'
|
|
9
|
-
import { join, basename } from 'path'
|
|
10
|
-
import { detectChangeRisk, checkIntegrationEvidence } from './change-risk-profile.js'
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* 校验结果
|
|
14
|
-
* @typedef {{ ok: boolean, errors: string[], warnings: string[] }} ValidationResult
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* 阶段合约
|
|
19
|
-
* @typedef {{
|
|
20
|
-
* stage: string,
|
|
21
|
-
* description: string,
|
|
22
|
-
* allowedFrom: string[],
|
|
23
|
-
* allowedTo: string[],
|
|
24
|
-
* validators: Function[],
|
|
25
|
-
* }} StageContract
|
|
26
|
-
*/
|
|
27
|
-
|
|
28
|
-
// ============ Validators ============
|
|
29
|
-
|
|
30
|
-
function resolveChangeDir(cwd, changeName, specRoot = null) {
|
|
31
|
-
const changesRoot = specRoot ? join(specRoot, 'changes') : join(cwd, '.sillyspec', 'changes')
|
|
32
|
-
return join(changesRoot, changeName)
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function collectIdsFromLine(line, re, ids) {
|
|
36
|
-
for (const match of line.matchAll(re)) {
|
|
37
|
-
ids.add(match[0].toUpperCase())
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function extractIds(content, prefix) {
|
|
42
|
-
if (!content) return []
|
|
43
|
-
const ids = new Set()
|
|
44
|
-
const idRe = new RegExp(`\\b${prefix}-\\d+(?:@v\\d+)?\\b`, 'gi')
|
|
45
|
-
const headingLineRe = /^\s{0,3}#{1,6}\s+/i
|
|
46
|
-
const fieldLineRe = /^\s*(?:[-*]\s*)?(?:id|decision[-_ ]?ids?|requirement[-_ ]?ids?|covers?|coverage|references?|impacts?|覆盖(?:来源|决策|需求)?)\s*[::]/i
|
|
47
|
-
const tableLineRe = /^\s*\|/
|
|
48
|
-
const listStartsWithIdRe = new RegExp(`^\\s*(?:[-*]|\\d+\\.)\\s*(?:\\[[ xX]\\]\\s*)?${prefix}-\\d+(?:@v\\d+)?\\b`, 'i')
|
|
49
|
-
|
|
50
|
-
for (const line of content.split(/\r?\n/)) {
|
|
51
|
-
if (!headingLineRe.test(line) && !fieldLineRe.test(line) && !tableLineRe.test(line) && !listStartsWithIdRe.test(line)) continue
|
|
52
|
-
collectIdsFromLine(line, idRe, ids)
|
|
53
|
-
}
|
|
54
|
-
return [...ids].sort()
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function readDecisionField(body, fieldPattern, fallback = '') {
|
|
58
|
-
const re = new RegExp(`^\\s*(?:[-*]\\s*)?(?:${fieldPattern})\\s*[::]\\s*([^\\n]+)`, 'im')
|
|
59
|
-
return (body.match(re)?.[1] || fallback).trim()
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function buildDecisionRecord(id, body) {
|
|
63
|
-
const status = readDecisionField(body, 'status', 'accepted').toLowerCase()
|
|
64
|
-
const blockerValue = readDecisionField(body, 'blocker', 'false').toLowerCase()
|
|
65
|
-
const blocker = ['true', 'yes', '1'].includes(blockerValue)
|
|
66
|
-
const priorityValue = readDecisionField(body, 'priority|level|severity')
|
|
67
|
-
const priorityMissing = priorityValue.length === 0
|
|
68
|
-
const fallbackPriority = (['unresolved', 'blocking'].includes(status) || blocker) ? 'P1' : 'P2'
|
|
69
|
-
const priority = (priorityValue.match(/P[0-2]/i)?.[0] || fallbackPriority).toUpperCase()
|
|
70
|
-
// 解析 supersedes 字段:记录本条决策取代了哪个旧版本
|
|
71
|
-
const supersedesRaw = readDecisionField(body, 'supersedes', '')
|
|
72
|
-
const supersedes = supersedesRaw
|
|
73
|
-
? supersedesRaw.split(',').map(s => s.trim().toUpperCase().replace(/['"]/g, '')).filter(Boolean)
|
|
74
|
-
: []
|
|
75
|
-
return { id: id.toUpperCase(), body, status, priority, blocker, priorityMissing, supersedes }
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function findNextDecisionBoundary(content, startIndex) {
|
|
79
|
-
const boundaryRe = /^(\s{0,3}#{2,6}\s+D-\d+(?:@v\d+)?\b|\s*(?:[-*]\s*)?(?:id|decision[-_ ]?id|decision)\s*[::]\s*D-\d+(?:@v\d+)?\b)/gmi
|
|
80
|
-
boundaryRe.lastIndex = startIndex
|
|
81
|
-
const next = boundaryRe.exec(content)
|
|
82
|
-
return next ? next.index : content.length
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
function isInsideRange(index, ranges) {
|
|
86
|
-
return ranges.some(range => index >= range.start && index < range.end)
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
function parseDecisionRecords(content) {
|
|
90
|
-
if (!content) return []
|
|
91
|
-
const records = []
|
|
92
|
-
const ranges = []
|
|
93
|
-
const headingRe = /^\s{0,3}#{2,6}\s+(D-\d+(?:@v\d+)?)(?:\b|:)[^\n]*$/gmi
|
|
94
|
-
const headings = []
|
|
95
|
-
let match
|
|
96
|
-
while ((match = headingRe.exec(content)) !== null) {
|
|
97
|
-
headings.push({ id: match[1].toUpperCase(), index: match.index, end: headingRe.lastIndex })
|
|
98
|
-
}
|
|
99
|
-
for (let i = 0; i < headings.length; i++) {
|
|
100
|
-
const current = headings[i]
|
|
101
|
-
const next = headings[i + 1]
|
|
102
|
-
const body = content.slice(current.end, next ? next.index : content.length)
|
|
103
|
-
const end = next ? next.index : content.length
|
|
104
|
-
ranges.push({ start: current.index, end })
|
|
105
|
-
records.push(buildDecisionRecord(current.id, body))
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
const idLineRe = /^\s*(?:[-*]\s*)?(?:id|decision[-_ ]?id|decision)\s*[::]\s*(D-\d+(?:@v\d+)?)(?:\b|$)/gmi
|
|
109
|
-
while ((match = idLineRe.exec(content)) !== null) {
|
|
110
|
-
if (isInsideRange(match.index, ranges)) continue
|
|
111
|
-
const bodyEnd = findNextDecisionBoundary(content, idLineRe.lastIndex)
|
|
112
|
-
const body = content.slice(match.index, bodyEnd)
|
|
113
|
-
records.push(buildDecisionRecord(match[1], body))
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
return records
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
function extractCurrentDecisionIds(content) {
|
|
120
|
-
const records = parseDecisionRecords(content)
|
|
121
|
-
if (records.length === 0) return extractIds(content, 'D')
|
|
122
|
-
// 收集所有被 supersedes 声明取代的旧版本 ID
|
|
123
|
-
const supersededIds = new Set()
|
|
124
|
-
for (const r of records) {
|
|
125
|
-
for (const oldId of r.supersedes) {
|
|
126
|
-
supersededIds.add(oldId)
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
return records
|
|
130
|
-
.filter(r => !['superseded', 'rejected'].includes(r.status))
|
|
131
|
-
.filter(r => !supersededIds.has(r.id)) // 被新版本显式取代的旧版本不再校验
|
|
132
|
-
.map(r => r.id)
|
|
133
|
-
.sort()
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
function findBlockingDecisionIssues(content) {
|
|
137
|
-
return parseDecisionRecords(content)
|
|
138
|
-
.filter(r => (r.blocker || ['unresolved', 'blocking'].includes(r.status)) && ['P0', 'P1'].includes(r.priority))
|
|
139
|
-
.map(r => `${r.id} (${r.priority}${r.priorityMissing ? ', priority=missing->P1' : ''}, status=${r.status})`)
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
function readIfExists(file) {
|
|
143
|
-
return existsSync(file) ? readFileSync(file, 'utf8') : ''
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
function warnMissingIds(warnings, ids, targetContent, targetName, sourceName) {
|
|
147
|
-
for (const id of ids) {
|
|
148
|
-
if (!targetContent.toUpperCase().includes(id)) {
|
|
149
|
-
warnings.push(`${targetName} 未引用 ${sourceName} 中的 ${id}`)
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
/**
|
|
155
|
-
* scan 完成校验:检查 7 份 scan 文档 + manifest
|
|
156
|
-
*/
|
|
157
|
-
function validateScanOutputs(cwd, changeName, context = {}) {
|
|
158
|
-
const { projectName, specRoot } = context
|
|
159
|
-
// 平台模式:specRoot 直接是规范目录(含 docs/)
|
|
160
|
-
// 本地模式:规范目录是 cwd/.sillyspec
|
|
161
|
-
// 不用 isSpecDir 启发式猜测——很多项目根目录有自己的 docs/,会误判
|
|
162
|
-
const specBase = specRoot || join(cwd, '.sillyspec')
|
|
163
|
-
const docsRoot = projectName
|
|
164
|
-
? join(specBase, 'docs', projectName, 'scan')
|
|
165
|
-
: join(specBase, 'docs', 'scan')
|
|
166
|
-
|
|
167
|
-
const requiredDocs = [
|
|
168
|
-
'ARCHITECTURE.md',
|
|
169
|
-
'CONVENTIONS.md',
|
|
170
|
-
'STRUCTURE.md',
|
|
171
|
-
'INTEGRATIONS.md',
|
|
172
|
-
'TESTING.md',
|
|
173
|
-
'CONCERNS.md',
|
|
174
|
-
'PROJECT.md',
|
|
175
|
-
]
|
|
176
|
-
|
|
177
|
-
const errors = []
|
|
178
|
-
const warnings = []
|
|
179
|
-
|
|
180
|
-
for (const doc of requiredDocs) {
|
|
181
|
-
if (!existsSync(join(docsRoot, doc))) {
|
|
182
|
-
errors.push(`scan 文档缺失: ${join(docsRoot, doc)}`)
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
// 检查 modules 目录
|
|
187
|
-
const modulesRoot = projectName
|
|
188
|
-
? join(specBase, 'docs', projectName, 'modules')
|
|
189
|
-
: join(specBase, 'docs', 'modules')
|
|
190
|
-
if (!existsSync(modulesRoot)) {
|
|
191
|
-
warnings.push('modules 目录不存在')
|
|
192
|
-
} else {
|
|
193
|
-
const modules = readdirSync(modulesRoot).filter(f => f.endsWith('.md'))
|
|
194
|
-
if (modules.length === 0) {
|
|
195
|
-
warnings.push('modules 目录为空')
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
return { ok: errors.length === 0, errors, warnings }
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
/**
|
|
203
|
-
* brainstorm 完成校验:检查四件套规范文件是否生成
|
|
204
|
-
*/
|
|
205
|
-
function validateBrainstormOutputs(cwd, changeName, context = {}) {
|
|
206
|
-
const { specRoot } = context
|
|
207
|
-
const changesRoot = specRoot ? join(specRoot, 'changes') : join(cwd, '.sillyspec', 'changes')
|
|
208
|
-
if (specRoot && !existsSync(changesRoot)) {
|
|
209
|
-
return { ok: false, errors: [`平台模式 specRoot 缺少 changes 目录: ${changesRoot}`], warnings: [] }
|
|
210
|
-
}
|
|
211
|
-
const changeDir = resolveChangeDir(cwd, changeName, specRoot)
|
|
212
|
-
const errors = []
|
|
213
|
-
const warnings = []
|
|
214
|
-
|
|
215
|
-
const requiredFiles = ['design.md', 'proposal.md', 'requirements.md', 'tasks.md']
|
|
216
|
-
|
|
217
|
-
for (const file of requiredFiles) {
|
|
218
|
-
if (!existsSync(join(changeDir, file))) {
|
|
219
|
-
errors.push(`brainstorm 产物缺失: ${join(changeDir, file)}`)
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
// 内容校验(文件存在时检查关键章节)
|
|
224
|
-
if (existsSync(join(changeDir, 'proposal.md'))) {
|
|
225
|
-
const content = readFileSync(join(changeDir, 'proposal.md'), 'utf8')
|
|
226
|
-
if (!content.includes('不在范围内') && !content.includes('Non-Goals') && !content.includes('非目标')) {
|
|
227
|
-
warnings.push('proposal.md 缺少「不在范围内/Non-Goals」章节')
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
if (existsSync(join(changeDir, 'requirements.md'))) {
|
|
232
|
-
const content = readFileSync(join(changeDir, 'requirements.md'), 'utf8')
|
|
233
|
-
if (!/FR-\d+/i.test(content)) {
|
|
234
|
-
warnings.push('requirements.md 缺少 FR 编号的需求项')
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
if (existsSync(join(changeDir, 'design.md'))) {
|
|
239
|
-
const content = readFileSync(join(changeDir, 'design.md'), 'utf8')
|
|
240
|
-
if (!content.includes('文件变更清单') && !content.includes('File Changes') && !content.includes('文件清单')) {
|
|
241
|
-
warnings.push('design.md 缺少「文件变更清单」章节')
|
|
242
|
-
}
|
|
243
|
-
if (!content.includes('风险登记') && !content.includes('Risk') && !content.includes('风险')) {
|
|
244
|
-
warnings.push('design.md 缺少「风险登记」章节')
|
|
245
|
-
}
|
|
246
|
-
if (!content.includes('自审') && !content.includes('Self-Review') && !content.includes('Self-review')) {
|
|
247
|
-
warnings.push('design.md 缺少「自审」章节')
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
// P1: 涉及生命周期关键词时,design.md 必须包含生命周期契约表
|
|
251
|
-
const hasLifecycleKeyword = /\b(session|lease|agent[._-]?run|daemon|lifecycle|state[._-]?transition|claim|heartbeat)\b/i.test(content)
|
|
252
|
-
if (hasLifecycleKeyword) {
|
|
253
|
-
const hasLifecycleTable =
|
|
254
|
-
/生命周期契约表|lifecycle[._-]?contract|lifecycle[._-]?matrix|Lifecycle Contract/i.test(content) ||
|
|
255
|
-
/事件.*发起方.*接收方.*必需字段.*状态变化/.test(content)
|
|
256
|
-
if (!hasLifecycleTable) {
|
|
257
|
-
errors.push('design.md 涉及生命周期关键词(session/lease/agent_run/daemon/lifecycle)但缺少「生命周期契约表」— 必须列出完整的事件×状态转换矩阵')
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
if (existsSync(join(changeDir, 'tasks.md'))) {
|
|
263
|
-
const content = readFileSync(join(changeDir, 'tasks.md'), 'utf8')
|
|
264
|
-
const lines = content.split('\n').filter(l => l.trim().startsWith('-') || l.trim().startsWith('*') || /^\d+\./.test(l.trim()))
|
|
265
|
-
if (lines.length === 0) {
|
|
266
|
-
warnings.push('tasks.md 没有任务列表项')
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
const decisionsFile = join(changeDir, 'decisions.md')
|
|
271
|
-
if (existsSync(decisionsFile)) {
|
|
272
|
-
const decisions = readFileSync(decisionsFile, 'utf8')
|
|
273
|
-
const blockers = findBlockingDecisionIssues(decisions)
|
|
274
|
-
for (const issue of blockers) {
|
|
275
|
-
errors.push(`decisions.md 存在 P0/P1 未决阻塞: ${issue}`)
|
|
276
|
-
}
|
|
277
|
-
const decisionIds = extractCurrentDecisionIds(decisions)
|
|
278
|
-
if (decisionIds.length === 0) {
|
|
279
|
-
warnings.push('decisions.md 存在但没有当前版本 D-xxx@vN 决策 ID')
|
|
280
|
-
} else {
|
|
281
|
-
const design = readIfExists(join(changeDir, 'design.md'))
|
|
282
|
-
const requirements = readIfExists(join(changeDir, 'requirements.md'))
|
|
283
|
-
const tasks = readIfExists(join(changeDir, 'tasks.md'))
|
|
284
|
-
warnMissingIds(warnings, decisionIds, design, 'design.md', 'decisions.md')
|
|
285
|
-
warnMissingIds(warnings, decisionIds, requirements, 'requirements.md', 'decisions.md')
|
|
286
|
-
warnMissingIds(warnings, decisionIds, tasks, 'tasks.md', 'decisions.md')
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
return { ok: errors.length === 0, errors, warnings }
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
/**
|
|
294
|
-
* plan 完成校验:检查 plan.md 生成
|
|
295
|
-
*/
|
|
296
|
-
function validatePlanOutputs(cwd, changeName, context = {}) {
|
|
297
|
-
const { specRoot } = context
|
|
298
|
-
const changeDir = resolveChangeDir(cwd, changeName, specRoot)
|
|
299
|
-
const planFile = join(changeDir, 'plan.md')
|
|
300
|
-
const errors = []
|
|
301
|
-
|
|
302
|
-
if (!existsSync(planFile)) {
|
|
303
|
-
errors.push(`plan.md 缺失: ${planFile}`)
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
const warnings = []
|
|
307
|
-
if (existsSync(planFile)) {
|
|
308
|
-
const plan = readFileSync(planFile, 'utf8')
|
|
309
|
-
const requirements = readIfExists(join(changeDir, 'requirements.md'))
|
|
310
|
-
const requirementIds = extractIds(requirements, 'FR')
|
|
311
|
-
warnMissingIds(warnings, requirementIds, plan, 'plan.md', 'requirements.md')
|
|
312
|
-
|
|
313
|
-
const decisions = readIfExists(join(changeDir, 'decisions.md'))
|
|
314
|
-
const blockers = findBlockingDecisionIssues(decisions)
|
|
315
|
-
for (const issue of blockers) {
|
|
316
|
-
errors.push(`decisions.md 存在 P0/P1 未决阻塞: ${issue}`)
|
|
317
|
-
}
|
|
318
|
-
const decisionIds = extractCurrentDecisionIds(decisions)
|
|
319
|
-
warnMissingIds(warnings, decisionIds, plan, 'plan.md', 'decisions.md')
|
|
320
|
-
}
|
|
321
|
-
// ── P0: 生产接线路径检查:design 提到入口但 task 的 allowed_paths 不含入口文件 ──
|
|
322
|
-
const designContent = readIfExists(join(changeDir, 'design.md'))
|
|
323
|
-
if (designContent) {
|
|
324
|
-
const entryPointPatterns = [
|
|
325
|
-
/\b(cli\.ts|main\.ts|server\.(?:js|ts)|index\.(?:js|ts))\b.*\b(?:实例化|instantiate|构造|new\s)/gi,
|
|
326
|
-
/\bnew\s+(Daemon|SessionManager|App|Server|Application)\b/gi,
|
|
327
|
-
/\b(?:在|from)\s+['"]?(cli\.ts|main\.ts|server\.(?:js|ts)|index\.(?:js|ts))['"]?/gi,
|
|
328
|
-
/\b(?:注入|inject)\b.*\b(?:构造|constructor|初始化|init|实例化|instantiate)\b/gi,
|
|
329
|
-
/\b(?:启动路径|startup|entrypoint|bootstrap|daemon[._-]?start|main.*entry)\b/gi,
|
|
330
|
-
]
|
|
331
|
-
const mentionedFiles = new Set()
|
|
332
|
-
for (const pattern of entryPointPatterns) {
|
|
333
|
-
pattern.lastIndex = 0
|
|
334
|
-
for (const match of designContent.matchAll(pattern)) {
|
|
335
|
-
const fileMatch = match[0].match(/\b(cli\.ts|main\.ts|server\.(?:js|ts)|index\.(?:js|ts))\b/i)
|
|
336
|
-
if (fileMatch) mentionedFiles.add(fileMatch[1].toLowerCase())
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
if (mentionedFiles.size > 0) {
|
|
340
|
-
const tasksDir = join(changeDir, 'tasks')
|
|
341
|
-
const allAllowedPaths = new Set()
|
|
342
|
-
if (existsSync(tasksDir)) {
|
|
343
|
-
const taskFiles = readdirSync(tasksDir).filter(f => /^task-\d+\.md$/i.test(f))
|
|
344
|
-
for (const taskFile of taskFiles) {
|
|
345
|
-
const taskContent = readFileSync(join(tasksDir, taskFile), 'utf8')
|
|
346
|
-
const allowedSection = taskContent.match(/allowed_paths:\s*\n((?:\s+-\s+.+\n?)+)/)
|
|
347
|
-
if (allowedSection) {
|
|
348
|
-
const paths = allowedSection[1].match(/-\s+(.+)/g) || []
|
|
349
|
-
for (const p of paths) allAllowedPaths.add(p.replace(/^-\s+/, '').trim().toLowerCase())
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
// 也从 plan.md 文件变更清单中收集
|
|
354
|
-
if (existsSync(planFile)) {
|
|
355
|
-
const planContent = readFileSync(planFile, 'utf8')
|
|
356
|
-
const planFileChanges = planContent.match(/\|\s*(?:新增|修改|new|modify|update)\s*\|\s*`?([^`|]+)`?\s*\|/gi) || []
|
|
357
|
-
for (const line of planFileChanges) {
|
|
358
|
-
const file = line.match(/\|\s*(?:新增|修改|new|modify|update)\s*\|\s*`?([^`|]+)`?\s*\|/i)
|
|
359
|
-
if (file) allAllowedPaths.add(file[1].trim().toLowerCase())
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
for (const mentionedFile of mentionedFiles) {
|
|
363
|
-
const found = [...allAllowedPaths].some(p => p.includes(mentionedFile))
|
|
364
|
-
if (!found) {
|
|
365
|
-
const noChangePattern = new RegExp(`不需要改.*${mentionedFile}|${mentionedFile}.*不需要|不修改.*${mentionedFile}|${mentionedFile}.*不变|${mentionedFile}.*no.?change`, 'i')
|
|
366
|
-
if (!noChangePattern.test(designContent)) {
|
|
367
|
-
errors.push(`生产接线路径矛盾: design.md 提到了入口文件 "${mentionedFile}" 但所有 task 的 allowed_paths 中均不含该文件`)
|
|
368
|
-
warnings.push(`提示: 如果确实不需要修改 ${mentionedFile},请在 design.md 中明确写明理由`)
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
return { ok: errors.length === 0, errors, warnings }
|
|
376
|
-
}
|
|
377
|
-
function validateVerifyOutputs(cwd, changeName, context = {}) {
|
|
378
|
-
const { specRoot } = context
|
|
379
|
-
const changeDir = resolveChangeDir(cwd, changeName, specRoot)
|
|
380
|
-
const errors = []
|
|
381
|
-
const warnings = []
|
|
382
|
-
|
|
383
|
-
if (!existsSync(changeDir)) {
|
|
384
|
-
errors.push(`变更目录缺失: ${changeDir}`)
|
|
385
|
-
return { ok: false, errors, warnings }
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
// verify 阶段应该产出 verify-result.md(或类似报告)
|
|
389
|
-
const verifyResult = join(changeDir, 'verify-result.md')
|
|
390
|
-
if (!existsSync(verifyResult)) {
|
|
391
|
-
warnings.push('verify-result.md 不存在(verify 阶段建议产出验证报告)')
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
// 确保核心规范文件仍然存在
|
|
395
|
-
const requiredDocs = ['design.md', 'plan.md']
|
|
396
|
-
for (const doc of requiredDocs) {
|
|
397
|
-
if (!existsSync(join(changeDir, doc))) {
|
|
398
|
-
errors.push(`核心文档缺失: ${join(changeDir, doc)}`)
|
|
399
|
-
}
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
if (existsSync(verifyResult)) {
|
|
403
|
-
const verify = readFileSync(verifyResult, 'utf8')
|
|
404
|
-
const decisions = readIfExists(join(changeDir, 'decisions.md'))
|
|
405
|
-
const blockers = findBlockingDecisionIssues(decisions)
|
|
406
|
-
for (const issue of blockers) {
|
|
407
|
-
errors.push(`decisions.md 存在 P0/P1 未决阻塞: ${issue}`)
|
|
408
|
-
}
|
|
409
|
-
const decisionIds = extractCurrentDecisionIds(decisions)
|
|
410
|
-
warnMissingIds(warnings, decisionIds, verify, 'verify-result.md', 'decisions.md')
|
|
411
|
-
|
|
412
|
-
// ── P0: Change Risk Gate — 核心功能缺少真实集成验证时 FAIL ──
|
|
413
|
-
const changeRiskProfile = detectChangeRisk({
|
|
414
|
-
designContent: readIfExists(join(changeDir, 'design.md')),
|
|
415
|
-
planContent: readIfExists(join(changeDir, 'plan.md')),
|
|
416
|
-
})
|
|
417
|
-
if (['integration-critical', 'deployment-critical'].includes(changeRiskProfile.level)) {
|
|
418
|
-
const conclusionMatch = verify.match(/^## 结论\s*\n\s*(PASS|PASS WITH NOTES|FAIL)/im)
|
|
419
|
-
const conclusion = conclusionMatch ? conclusionMatch[1] : ''
|
|
420
|
-
if (conclusion === 'PASS WITH NOTES' || conclusion === 'PASS') {
|
|
421
|
-
const evidenceCheck = checkIntegrationEvidence(verify, changeRiskProfile.requiredVerification)
|
|
422
|
-
if (!evidenceCheck.ok) {
|
|
423
|
-
errors.push(`[${changeRiskProfile.level}] 验证结论为 ${conclusion},但缺少真实集成证据:${evidenceCheck.errors.join('; ')}`)
|
|
424
|
-
errors.push(`触发词: ${changeRiskProfile.triggers.join(', ')} — PASS WITH NOTES 不被允许,必须 FAIL 或提供集成证据`)
|
|
425
|
-
}
|
|
426
|
-
warnings.push(...evidenceCheck.warnings)
|
|
427
|
-
}
|
|
428
|
-
}
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
return { ok: errors.length === 0, errors, warnings }
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
/**
|
|
435
|
-
* archive 完成校验:检查归档目录完整性
|
|
436
|
-
*/
|
|
437
|
-
function validateArchiveOutputs(cwd, changeName) {
|
|
438
|
-
const errors = []
|
|
439
|
-
const warnings = []
|
|
440
|
-
const archiveDir = join(cwd, '.sillyspec', 'changes', 'archive')
|
|
441
|
-
const date = new Date().toISOString().slice(0, 10)
|
|
442
|
-
const destDir = join(archiveDir, `${date}-${changeName}`)
|
|
443
|
-
|
|
444
|
-
// 检查归档目录是否存在
|
|
445
|
-
if (!existsSync(destDir)) {
|
|
446
|
-
errors.push(`归档目录缺失: ${destDir}`)
|
|
447
|
-
return { ok: false, errors, warnings }
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
// 检查核心文档
|
|
451
|
-
const requiredDocs = ['plan.md']
|
|
452
|
-
const recommendedDocs = ['design.md', 'module-impact.md']
|
|
453
|
-
|
|
454
|
-
for (const doc of requiredDocs) {
|
|
455
|
-
if (!existsSync(join(destDir, doc))) {
|
|
456
|
-
errors.push(`归档目录缺失核心文档: ${doc}`)
|
|
457
|
-
}
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
for (const doc of recommendedDocs) {
|
|
461
|
-
if (!existsSync(join(destDir, doc))) {
|
|
462
|
-
warnings.push(`归档目录缺少推荐文档: ${doc}`)
|
|
463
|
-
}
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
return { ok: errors.length === 0, errors, warnings }
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
/**
|
|
470
|
-
* archive 前置校验:所有主流程阶段完成
|
|
471
|
-
*/
|
|
472
|
-
function validateChangeClosed(cwd, changeName) {
|
|
473
|
-
const errors = []
|
|
474
|
-
const warnings = []
|
|
475
|
-
|
|
476
|
-
// 检查前置阶段状态
|
|
477
|
-
const progressDir = join(cwd, '.sillyspec', '.runtime')
|
|
478
|
-
// 这里只做文件层面的检查,DB 检查在 run.js 里做
|
|
479
|
-
const changeDir = join(cwd, '.sillyspec', 'changes', changeName)
|
|
480
|
-
if (!existsSync(changeDir)) {
|
|
481
|
-
errors.push(`变更目录不存在: ${changeDir}`)
|
|
482
|
-
return { ok: false, errors, warnings }
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
if (!existsSync(join(changeDir, 'plan.md'))) {
|
|
486
|
-
errors.push(`plan.md 缺失 — 请确保 plan 阶段已完成`)
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
return { ok: errors.length === 0, errors, warnings }
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
// ============ Contract Registry ============
|
|
493
|
-
|
|
494
|
-
/**
|
|
495
|
-
* 主流程阶段(有严格转换顺序)
|
|
496
|
-
*/
|
|
497
|
-
const mainFlowStages = ['brainstorm', 'plan', 'execute', 'verify']
|
|
498
|
-
|
|
499
|
-
/**
|
|
500
|
-
* 辅助阶段(可独立运行,无严格转换顺序)
|
|
501
|
-
*/
|
|
502
|
-
const auxiliaryStages = ['scan', 'quick', 'explore', 'archive', 'status', 'doctor']
|
|
503
|
-
|
|
504
|
-
/**
|
|
505
|
-
* @type {Object<string, StageContract>}
|
|
506
|
-
*/
|
|
507
|
-
const contracts = {
|
|
508
|
-
// === 主流程 ===
|
|
509
|
-
brainstorm: {
|
|
510
|
-
stage: 'brainstorm',
|
|
511
|
-
description: '需求分析与设计',
|
|
512
|
-
allowedFrom: [], // 任何变更的起始阶段
|
|
513
|
-
allowedTo: ['plan'],
|
|
514
|
-
validators: [validateBrainstormOutputs],
|
|
515
|
-
},
|
|
516
|
-
plan: {
|
|
517
|
-
stage: 'plan',
|
|
518
|
-
description: '任务拆解与规划',
|
|
519
|
-
allowedFrom: ['brainstorm'],
|
|
520
|
-
allowedTo: ['execute'],
|
|
521
|
-
validators: [validatePlanOutputs],
|
|
522
|
-
},
|
|
523
|
-
execute: {
|
|
524
|
-
stage: 'execute',
|
|
525
|
-
description: '代码实现',
|
|
526
|
-
allowedFrom: ['plan'],
|
|
527
|
-
allowedTo: ['verify'],
|
|
528
|
-
validators: [],
|
|
529
|
-
},
|
|
530
|
-
verify: {
|
|
531
|
-
stage: 'verify',
|
|
532
|
-
description: '验证与测试',
|
|
533
|
-
allowedFrom: ['execute'],
|
|
534
|
-
allowedTo: ['archive'],
|
|
535
|
-
validators: [validateVerifyOutputs],
|
|
536
|
-
},
|
|
537
|
-
archive: {
|
|
538
|
-
stage: 'archive',
|
|
539
|
-
description: '归档与收口',
|
|
540
|
-
allowedFrom: ['verify'],
|
|
541
|
-
allowedTo: [],
|
|
542
|
-
// 阶段级 validator 全部移除,改为 run.js 中 step 4 完成后的硬编码校验。
|
|
543
|
-
// 理由:两个 validator 的生效窗口互斥 ——
|
|
544
|
-
// validateChangeClosed 要求变更目录存在(step 4 --confirm 后已被移到 archive 目录)
|
|
545
|
-
// validateArchiveOutputs 要求 archive 目录存在(step 4 前还不存在)
|
|
546
|
-
// 注册为阶段级 validator 会导致每步都误报错误。
|
|
547
|
-
// run.js:893-909 已在正确的时机(step 4 完成后)执行相同检查。
|
|
548
|
-
validators: [],
|
|
549
|
-
},
|
|
550
|
-
|
|
551
|
-
// === 辅助阶段 ===
|
|
552
|
-
scan: {
|
|
553
|
-
stage: 'scan',
|
|
554
|
-
description: '项目扫描',
|
|
555
|
-
allowedFrom: [], // 无前置要求
|
|
556
|
-
allowedTo: [], // 不进入主流程
|
|
557
|
-
validators: [validateScanOutputs],
|
|
558
|
-
},
|
|
559
|
-
quick: {
|
|
560
|
-
stage: 'quick',
|
|
561
|
-
description: '快速任务',
|
|
562
|
-
allowedFrom: [], // 无前置要求
|
|
563
|
-
allowedTo: [], // 不进入主流程
|
|
564
|
-
validators: [],
|
|
565
|
-
},
|
|
566
|
-
explore: {
|
|
567
|
-
stage: 'explore',
|
|
568
|
-
description: '代码探索',
|
|
569
|
-
allowedFrom: [],
|
|
570
|
-
allowedTo: [],
|
|
571
|
-
validators: [],
|
|
572
|
-
},
|
|
573
|
-
status: {
|
|
574
|
-
stage: 'status',
|
|
575
|
-
description: '状态查看',
|
|
576
|
-
allowedFrom: [],
|
|
577
|
-
allowedTo: [],
|
|
578
|
-
validators: [],
|
|
579
|
-
},
|
|
580
|
-
doctor: {
|
|
581
|
-
stage: 'doctor',
|
|
582
|
-
description: '环境诊断',
|
|
583
|
-
allowedFrom: [],
|
|
584
|
-
allowedTo: [],
|
|
585
|
-
validators: [],
|
|
586
|
-
},
|
|
587
|
-
}
|
|
588
|
-
|
|
589
|
-
// ============ Public API ============
|
|
590
|
-
|
|
591
|
-
/**
|
|
592
|
-
* 获取阶段合约
|
|
593
|
-
*/
|
|
594
|
-
export function getContract(stageName) {
|
|
595
|
-
return contracts[stageName] || null
|
|
596
|
-
}
|
|
597
|
-
|
|
598
|
-
/**
|
|
599
|
-
* 校验状态转换是否允许
|
|
600
|
-
* @param {string} fromStage - 当前阶段(空字符串表示变更起始)
|
|
601
|
-
* @param {string} toStage - 目标阶段
|
|
602
|
-
* @param {{ fromStageData?: { status?: string } | undefined }} [options] - 可选,从 progress.stages[prevStage] 提取
|
|
603
|
-
* @returns {{ allowed: boolean, reason?: string }}
|
|
604
|
-
*/
|
|
605
|
-
export function checkTransition(fromStage, toStage, options = {}) {
|
|
606
|
-
const { fromStageData } = options // { status?: string } | undefined
|
|
607
|
-
const contract = contracts[toStage]
|
|
608
|
-
if (!contract) {
|
|
609
|
-
return { allowed: false, reason: `未知阶段: ${toStage}` }
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
// 辅助阶段随时可执行(archive 除外:从主流程进入 archive 需要校验)
|
|
613
|
-
if (auxiliaryStages.includes(toStage) && toStage !== 'archive') {
|
|
614
|
-
return { allowed: true }
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
// 同阶段内重复运行:允许(继续执行当前阶段的下一步、或修订模式继续)
|
|
618
|
-
if (fromStage === toStage) {
|
|
619
|
-
return { allowed: true }
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
// task-07: failed_post_check 门控
|
|
623
|
-
// scan post-check 未通过时,禁止进入主流程的下游阶段(brainstorm/plan/execute/verify/archive)
|
|
624
|
-
// 必须先重跑 scan 修复。toStage === 'scan' 的重跑路径已被上方 fromStage === toStage 放行。
|
|
625
|
-
// fromStageData.status 缺失(旧数据)时门控不触发(向后兼容)。
|
|
626
|
-
if (fromStage === 'scan' && fromStageData?.status === 'failed_post_check' && toStage !== 'scan') {
|
|
627
|
-
return {
|
|
628
|
-
allowed: false,
|
|
629
|
-
reason: 'scan post-check 未通过(failed_post_check),需修复后重跑 scan 再进入 ' + toStage,
|
|
630
|
-
}
|
|
631
|
-
}
|
|
632
|
-
|
|
633
|
-
// archive 特殊处理:从 verify 来的允许,从其他主流程阶段来的需要校验
|
|
634
|
-
if (toStage === 'archive') {
|
|
635
|
-
if (fromStage === 'verify') {
|
|
636
|
-
return { allowed: true }
|
|
637
|
-
}
|
|
638
|
-
// 独立运行 archive(无前置)也允许
|
|
639
|
-
if (!fromStage || auxiliaryStages.includes(fromStage)) {
|
|
640
|
-
return { allowed: true }
|
|
641
|
-
}
|
|
642
|
-
return { allowed: false, reason: 'archive 的前置阶段是 verify,不能从 ' + fromStage + ' 跳转' }
|
|
643
|
-
}
|
|
644
|
-
|
|
645
|
-
// 从辅助阶段进入主流程:允许
|
|
646
|
-
if (auxiliaryStages.includes(fromStage)) {
|
|
647
|
-
return { allowed: true }
|
|
648
|
-
}
|
|
649
|
-
|
|
650
|
-
// 无前置阶段(变更起始):只能开始 brainstorm 或辅助阶段
|
|
651
|
-
if (!fromStage) {
|
|
652
|
-
// 主流程必须从 brainstorm 开始
|
|
653
|
-
if (contract.allowedFrom.length === 0) {
|
|
654
|
-
return { allowed: true }
|
|
655
|
-
}
|
|
656
|
-
return { allowed: false, reason: `${toStage} 需要先完成 ${contract.allowedFrom.join(' 或 ')}` }
|
|
657
|
-
}
|
|
658
|
-
|
|
659
|
-
// 主流程内部跳转:检查目标阶段的 allowedFrom 是否包含 fromStage
|
|
660
|
-
if (contract.allowedFrom.includes(fromStage)) {
|
|
661
|
-
return { allowed: true }
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
return { allowed: false, reason: `${toStage} 的前置阶段是 ${contract.allowedFrom.join(' 或 ')},不能从 ${fromStage} 跳转` }
|
|
665
|
-
}
|
|
666
|
-
|
|
667
|
-
/**
|
|
668
|
-
* 执行阶段完成校验
|
|
669
|
-
* @param {string} stageName
|
|
670
|
-
* @param {string} cwd
|
|
671
|
-
* @param {string} changeName
|
|
672
|
-
* @param {object} context - 额外上下文(如 projectName)
|
|
673
|
-
* @returns {ValidationResult}
|
|
674
|
-
*/
|
|
675
|
-
export function runValidators(stageName, cwd, changeName, context = {}) {
|
|
676
|
-
const contract = contracts[stageName]
|
|
677
|
-
if (!contract || contract.validators.length === 0) {
|
|
678
|
-
return { ok: true, errors: [], warnings: [] }
|
|
679
|
-
}
|
|
680
|
-
|
|
681
|
-
const allErrors = []
|
|
682
|
-
const allWarnings = []
|
|
683
|
-
|
|
684
|
-
for (const validator of contract.validators) {
|
|
685
|
-
try {
|
|
686
|
-
const result = validator(cwd, changeName, context)
|
|
687
|
-
allErrors.push(...(result.errors || []))
|
|
688
|
-
allWarnings.push(...(result.warnings || []))
|
|
689
|
-
} catch (e) {
|
|
690
|
-
allErrors.push(`校验器 ${validator.name || 'unknown'} 异常: ${e.message}`)
|
|
691
|
-
}
|
|
692
|
-
}
|
|
693
|
-
|
|
694
|
-
return { ok: allErrors.length === 0, errors: allErrors, warnings: allWarnings }
|
|
695
|
-
}
|
|
696
|
-
|
|
697
|
-
/**
|
|
698
|
-
* 获取所有主流程阶段
|
|
699
|
-
*/
|
|
700
|
-
export { mainFlowStages, auxiliaryStages }
|