@peterxiaoyang/superspec 0.1.37 → 0.1.39
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/dist/cli.js +45 -6
- package/dist/code_review.d.ts +18 -2
- package/dist/code_review.js +473 -81
- package/dist/format.d.ts +46 -4
- package/dist/format.js +311 -26
- package/dist/git_state.d.ts +36 -0
- package/dist/git_state.js +174 -0
- package/dist/job_validity.d.ts +16 -0
- package/dist/job_validity.js +37 -0
- package/dist/next.js +45 -355
- package/dist/phase_plan.d.ts +97 -0
- package/dist/phase_plan.js +582 -0
- package/dist/record.d.ts +2 -2
- package/dist/record.js +67 -31
- package/dist/review.d.ts +3 -2
- package/dist/review.js +66 -33
- package/dist/review_job_gates.d.ts +20 -0
- package/dist/review_job_gates.js +85 -0
- package/dist/store.d.ts +10 -0
- package/dist/store.js +53 -3
- package/dist/sync.js +7 -9
- package/dist/task.js +87 -9
- package/dist/task_evidence.d.ts +10 -0
- package/dist/task_evidence.js +126 -0
- package/dist/transition.d.ts +1 -1
- package/dist/transition.js +449 -337
- package/dist/types.d.ts +81 -1
- package/dist/workflow_profile.d.ts +11 -0
- package/dist/workflow_profile.js +39 -0
- package/package.json +1 -1
- package/templates/workflow/prompts/architect.md +17 -27
- package/templates/workflow/prompts/code-reviewer.md +13 -2
- package/templates/workflow/prompts/critic.md +62 -61
- package/templates/workflow/prompts/executor.md +1 -1
- package/templates/workflow/prompts/explore.md +38 -26
- package/templates/workflow/prompts/test-engineer.md +18 -32
- package/templates/workflow/prompts/verifier.md +5 -3
- package/templates/workflow/skills/superspec-apply/SKILL.md +34 -11
- package/templates/workflow/skills/superspec-explore/SKILL.md +65 -64
- package/templates/workflow/skills/superspec-propose/SKILL.md +64 -43
- package/templates/workflow/skills/superspec-review/SKILL.md +1 -1
package/dist/next.js
CHANGED
|
@@ -1,19 +1,8 @@
|
|
|
1
1
|
// SuperSpec 流程引擎 — next:返回可执行路径
|
|
2
2
|
import { rebuildSnapshot } from "./sync.js";
|
|
3
|
-
import {
|
|
4
|
-
import { join } from "node:path";
|
|
5
|
-
import { readEvents, sha256Text } from "./store.js";
|
|
6
|
-
import { isFreshReviewVerifier, isReviewReadyVerifier, readReviewPolicyFromEvents, reviewEvidenceDigest } from "./review.js";
|
|
7
|
-
import { CODE_REVIEW_DECISION_ANSWER_LABELS, CODE_REVIEW_REPAIR_SCOPE_PREFIX, codeReviewDecisionScope, collectCodeReviewGateFacts, latestCodeReviewFailedStatus, requiresFinalVerifierForCurrentReview, } from "./code_review.js";
|
|
3
|
+
import { readEvents } from "./store.js";
|
|
8
4
|
import { requiredJobActions } from "./job_action.js";
|
|
9
|
-
import {
|
|
10
|
-
const ACTIVE_PROPOSAL_REVIEW_ROLES = new Set(["critic", "architect", "test-engineer"]);
|
|
11
|
-
function isActiveProposalReviewJob(job) {
|
|
12
|
-
return job.created_from_transition === "propose-ready" && ACTIVE_PROPOSAL_REVIEW_ROLES.has(job.role);
|
|
13
|
-
}
|
|
14
|
-
function isExploreReviewJob(job) {
|
|
15
|
-
return job.created_from_transition === "explore" && job.role === "critic";
|
|
16
|
-
}
|
|
5
|
+
import { planNextStep } from "./phase_plan.js";
|
|
17
6
|
function requiredJobsOutput(state, change, jobs, reason) {
|
|
18
7
|
return {
|
|
19
8
|
state,
|
|
@@ -28,358 +17,59 @@ function transitionCommand(change, name, extra = "") {
|
|
|
28
17
|
function riskFlag(risk) {
|
|
29
18
|
return risk === "strict" ? "" : `--risk ${risk}`;
|
|
30
19
|
}
|
|
31
|
-
function
|
|
32
|
-
|
|
33
|
-
return pendingTasksInContent(tasksContent).map(task => task.taskId);
|
|
20
|
+
function riskArg(risk) {
|
|
21
|
+
return risk ? riskFlag(risk) : "";
|
|
34
22
|
}
|
|
35
|
-
function
|
|
36
|
-
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
if (!taskInfo)
|
|
44
|
-
return { ready: false, missing: [`任务 ${attempt.task_id} 不存在`] };
|
|
45
|
-
const currentDigest = sha256Text(tasksContent.replace(/- \[[xX]\]/g, "- [ ]"));
|
|
46
|
-
// Keep the wording aligned with task-complete; no command should be suggested if it would fail this guard.
|
|
47
|
-
if (currentDigest !== attempt.task_structure_digest)
|
|
48
|
-
missing.push("任务结构指纹");
|
|
49
|
-
if (!taskInfo.tddRequired) {
|
|
50
|
-
if (!taskInfo.noTddReason)
|
|
51
|
-
missing.push("no_tdd_reason");
|
|
52
|
-
return { ready: missing.length === 0, missing };
|
|
23
|
+
function formatTransitionArgs(plan) {
|
|
24
|
+
if (plan.taskId)
|
|
25
|
+
return `--task ${plan.taskId}`;
|
|
26
|
+
if (plan.reopen?.reason === "pending_tasks") {
|
|
27
|
+
return `--to apply --reason "pending tasks: ${plan.reopen.taskIds.join(", ")}"`;
|
|
28
|
+
}
|
|
29
|
+
if (plan.reopen?.reason === "review_fix") {
|
|
30
|
+
return `--to apply --review-fix ${plan.reopen.jobId}#${plan.reopen.findingId} --reason "${plan.reopen.reopenReason}"`;
|
|
53
31
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
for (const ev of readEvents(projectRoot, change)) {
|
|
57
|
-
if (ev.event_type !== "test_run_recorded")
|
|
58
|
-
continue;
|
|
59
|
-
const tr = ev.payload;
|
|
60
|
-
const matches = tr.attempt_id === attempt.attempt_id ||
|
|
61
|
-
(!tr.attempt_id && tr.task_structure_digest === attempt.task_structure_digest);
|
|
62
|
-
if (!matches)
|
|
63
|
-
continue;
|
|
64
|
-
if (tr.semantic_status === "expected_failure" || tr.semantic_status === "characterization_pass")
|
|
65
|
-
hasRed = true;
|
|
66
|
-
if (tr.semantic_status === "expected_success")
|
|
67
|
-
hasGreen = true;
|
|
32
|
+
if (plan.reopen?.reason === "review_finding") {
|
|
33
|
+
return `--to propose --review-finding ${plan.reopen.jobId}#${plan.reopen.findingId} --reason "根据代码审查问题 ${plan.reopen.findingId} 回到计划阶段"`;
|
|
68
34
|
}
|
|
69
|
-
|
|
70
|
-
missing.push("RED 证据");
|
|
71
|
-
if (!hasGreen)
|
|
72
|
-
missing.push("GREEN 证据");
|
|
73
|
-
return { ready: missing.length === 0, missing };
|
|
35
|
+
return riskArg(plan.risk);
|
|
74
36
|
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
case "
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
}
|
|
83
|
-
return {
|
|
84
|
-
state: "init",
|
|
85
|
-
path: "next_command",
|
|
86
|
-
next_command: transitionCommand(change, "explore"),
|
|
87
|
-
reason: "初始化完成,开始探索",
|
|
88
|
-
missing_inputs: [],
|
|
89
|
-
};
|
|
90
|
-
case "explore": {
|
|
91
|
-
const exploreReviewJobs = snapshot.open_jobs.filter(isExploreReviewJob);
|
|
92
|
-
if (exploreReviewJobs.length > 0) {
|
|
93
|
-
return requiredJobsOutput("explore", change, exploreReviewJobs, `有 ${exploreReviewJobs.length} 个待完成探索审查工作项`);
|
|
94
|
-
}
|
|
95
|
-
// Phase 2:检查 discovery.md
|
|
96
|
-
const discoveryCheck = validateDiscovery(changeRoot);
|
|
97
|
-
if (!discoveryCheck.ok) {
|
|
98
|
-
const ask = {
|
|
99
|
-
question: discoveryCheck.message + ",请处理后继续",
|
|
100
|
-
allowed_answers: ["已处理"],
|
|
101
|
-
scope: "explore_discovery",
|
|
102
|
-
};
|
|
103
|
-
return { state: "explore", path: "ask_user", ask_user: ask, reason: discoveryCheck.message };
|
|
104
|
-
}
|
|
105
|
-
// 检查"待确认问题"段(用 format.ts 的段感知解析)
|
|
106
|
-
const content = readFileSync(join(changeRoot, ".superspec", "artifacts", "discovery.md"), "utf8");
|
|
107
|
-
const openQs = countDiscoveryOpenQuestions(content);
|
|
108
|
-
if (openQs > 0) {
|
|
109
|
-
const ask = {
|
|
110
|
-
question: `discovery.md 有 ${openQs} 个未解决的待确认问题,请逐个确认`,
|
|
111
|
-
allowed_answers: ["所有问题已确认"],
|
|
112
|
-
scope: "explore_open_questions",
|
|
113
|
-
};
|
|
114
|
-
return { state: "explore", path: "ask_user", ask_user: ask, reason: `有 ${openQs} 个未确认问题` };
|
|
115
|
-
}
|
|
116
|
-
return {
|
|
117
|
-
state: "explore",
|
|
118
|
-
path: "next_command",
|
|
119
|
-
next_command: transitionCommand(change, "explore", riskFlag(defaultRisk)),
|
|
120
|
-
reason: "探索完成,推进到计划阶段",
|
|
121
|
-
missing_inputs: [],
|
|
122
|
-
};
|
|
123
|
-
}
|
|
124
|
-
case "propose": {
|
|
125
|
-
const openQuestions = collectProposeOpenQuestions(changeRoot);
|
|
126
|
-
if (openQuestions.openCount > 0) {
|
|
127
|
-
const files = openQuestions.files.map(f => `${f.path}(${f.openCount})`).join(", ");
|
|
128
|
-
const ask = {
|
|
129
|
-
question: `计划文档有 ${openQuestions.openCount} 个待用户确认问题:${files}。请确认并更新计划文档后继续`,
|
|
130
|
-
allowed_answers: ["所有问题已确认"],
|
|
131
|
-
scope: "propose_open_questions",
|
|
132
|
-
};
|
|
133
|
-
return { state: "propose", path: "ask_user", ask_user: ask, reason: `有 ${openQuestions.openCount} 个 propose 未确认问题` };
|
|
134
|
-
}
|
|
135
|
-
const proposalReviewJobs = snapshot.open_jobs.filter(isActiveProposalReviewJob);
|
|
136
|
-
if (proposalReviewJobs.length > 0) {
|
|
137
|
-
return requiredJobsOutput("propose", change, proposalReviewJobs, `有 ${proposalReviewJobs.length} 个待完成 proposal 审查工作项`);
|
|
138
|
-
}
|
|
139
|
-
return {
|
|
140
|
-
state: "propose",
|
|
141
|
-
path: "next_command",
|
|
142
|
-
next_command: transitionCommand(change, "propose-ready", riskFlag(defaultRisk)),
|
|
143
|
-
reason: "计划文档就绪,提交 propose-ready",
|
|
144
|
-
missing_inputs: [],
|
|
145
|
-
};
|
|
146
|
-
}
|
|
147
|
-
case "propose_ready": {
|
|
148
|
-
const proposalReviewJobs = snapshot.open_jobs.filter(isActiveProposalReviewJob);
|
|
149
|
-
if (proposalReviewJobs.length > 0) {
|
|
150
|
-
return requiredJobsOutput("propose_ready", change, proposalReviewJobs, `有 ${proposalReviewJobs.length} 个待完成 proposal 审查工作项`);
|
|
151
|
-
}
|
|
152
|
-
return {
|
|
153
|
-
state: "propose_ready",
|
|
154
|
-
path: "next_command",
|
|
155
|
-
next_command: transitionCommand(change, "start-apply"),
|
|
156
|
-
reason: "计划就绪,开始执行",
|
|
157
|
-
missing_inputs: [],
|
|
158
|
-
};
|
|
159
|
-
}
|
|
160
|
-
case "apply": {
|
|
161
|
-
if (snapshot.open_jobs.length > 0) {
|
|
162
|
-
return requiredJobsOutput("apply", change, snapshot.open_jobs, `有 ${snapshot.open_jobs.length} 个待完成工作项`);
|
|
163
|
-
}
|
|
164
|
-
// 检查是否所有任务已完成
|
|
165
|
-
const pending = pendingTaskIds(changeRoot);
|
|
166
|
-
if (pending.length > 0) {
|
|
167
|
-
const activePending = snapshot.active_task_attempts.find(attempt => attempt.state === "active" && pending.includes(attempt.task_id));
|
|
168
|
-
if (activePending) {
|
|
169
|
-
const readiness = taskCompletionReadiness(projectRoot, change, changeRoot, activePending);
|
|
170
|
-
if (readiness.ready) {
|
|
171
|
-
return {
|
|
172
|
-
state: "apply",
|
|
173
|
-
path: "next_command",
|
|
174
|
-
next_command: transitionCommand(change, "task-complete", `--task ${activePending.task_id}`),
|
|
175
|
-
reason: `任务 ${activePending.task_id} 证据已登记,可以完成`,
|
|
176
|
-
missing_inputs: [],
|
|
177
|
-
};
|
|
178
|
-
}
|
|
179
|
-
const ask = {
|
|
180
|
-
question: `任务 ${activePending.task_id} 已开始,请先登记 ${readiness.missing.join("、")} 后继续`,
|
|
181
|
-
allowed_answers: ["证据已登记"],
|
|
182
|
-
scope: `apply_active_task_${activePending.task_id}`,
|
|
183
|
-
};
|
|
184
|
-
return { state: "apply", path: "ask_user", ask_user: ask, reason: `任务 ${activePending.task_id} 缺少完成证据` };
|
|
185
|
-
}
|
|
186
|
-
return {
|
|
187
|
-
state: "apply",
|
|
188
|
-
path: "next_command",
|
|
189
|
-
next_command: transitionCommand(change, "task-start", `--task ${pending[0]}`),
|
|
190
|
-
reason: `执行中:下一个未完成任务 ${pending[0]}`,
|
|
191
|
-
missing_inputs: [],
|
|
192
|
-
};
|
|
193
|
-
}
|
|
194
|
-
return {
|
|
195
|
-
state: "apply",
|
|
196
|
-
path: "next_command",
|
|
197
|
-
next_command: transitionCommand(change, "review-ready", riskFlag(defaultRisk)),
|
|
198
|
-
reason: "所有任务完成,进入审查",
|
|
199
|
-
missing_inputs: [],
|
|
200
|
-
};
|
|
201
|
-
}
|
|
202
|
-
case "apply_done": {
|
|
203
|
-
const pending = pendingTaskIds(changeRoot);
|
|
204
|
-
if (pending.length > 0) {
|
|
205
|
-
return {
|
|
206
|
-
state: "apply_done",
|
|
207
|
-
path: "next_command",
|
|
208
|
-
next_command: reopenCommand(change, pending),
|
|
209
|
-
reason: `发现未完成任务 ${pending[0]},回到执行阶段`,
|
|
210
|
-
missing_inputs: [],
|
|
211
|
-
};
|
|
212
|
-
}
|
|
213
|
-
if (snapshot.open_jobs.length > 0) {
|
|
214
|
-
return requiredJobsOutput("apply_done", change, snapshot.open_jobs, `有 ${snapshot.open_jobs.length} 个待完成工作项`);
|
|
215
|
-
}
|
|
216
|
-
const events = readEvents(projectRoot, change);
|
|
217
|
-
const facts = collectCodeReviewGateFacts(events);
|
|
218
|
-
const latest = facts.latestTerminal;
|
|
219
|
-
if (latest?.state === "rejected" && latest.result_kind === "review_failed") {
|
|
220
|
-
const status = latestCodeReviewFailedStatus(events);
|
|
221
|
-
const pending = status?.unresolved[0] ?? null;
|
|
222
|
-
if (status && status.findings.length > 0 && !pending) {
|
|
223
|
-
return {
|
|
224
|
-
state: "apply_done",
|
|
225
|
-
path: "next_command",
|
|
226
|
-
next_command: transitionCommand(change, "review-ready", riskFlag(defaultRisk)),
|
|
227
|
-
reason: "代码审查问题已被主流程复核驳回,重新发起代码审查",
|
|
228
|
-
missing_inputs: [],
|
|
229
|
-
};
|
|
230
|
-
}
|
|
231
|
-
const findingId = pending?.id ?? "";
|
|
232
|
-
const type = pending?.type;
|
|
233
|
-
const decision = pending?.decision;
|
|
234
|
-
if (findingId && type === "implementation") {
|
|
235
|
-
return {
|
|
236
|
-
state: "apply_done",
|
|
237
|
-
path: "next_command",
|
|
238
|
-
next_command: transitionCommand(change, "reopen", `--to apply --review-fix ${latest.job.job_id}#${findingId} --reason "修复代码审查问题 ${findingId}"`),
|
|
239
|
-
reason: `代码审查发现纯代码实现问题 ${findingId},回到实现阶段修复`,
|
|
240
|
-
missing_inputs: [],
|
|
241
|
-
};
|
|
242
|
-
}
|
|
243
|
-
if (findingId && (type === "spec" || type === "mixed")) {
|
|
244
|
-
if (decision?.answer === "reopen_propose") {
|
|
245
|
-
return {
|
|
246
|
-
state: "apply_done",
|
|
247
|
-
path: "next_command",
|
|
248
|
-
next_command: transitionCommand(change, "reopen", `--to propose --review-finding ${latest.job.job_id}#${findingId} --reason "根据代码审查问题 ${findingId} 回到计划阶段"`),
|
|
249
|
-
reason: `使用者已确认问题 ${findingId} 需要回到计划阶段`,
|
|
250
|
-
missing_inputs: [],
|
|
251
|
-
};
|
|
252
|
-
}
|
|
253
|
-
if (decision?.answer === "reopen_apply") {
|
|
254
|
-
return {
|
|
255
|
-
state: "apply_done",
|
|
256
|
-
path: "next_command",
|
|
257
|
-
next_command: transitionCommand(change, "reopen", `--to apply --review-fix ${latest.job.job_id}#${findingId} --reason "根据代码审查问题 ${findingId} 回到实现阶段修复"`),
|
|
258
|
-
reason: `使用者已确认问题 ${findingId} 直接回到实现阶段修复`,
|
|
259
|
-
missing_inputs: [],
|
|
260
|
-
};
|
|
261
|
-
}
|
|
262
|
-
const problemKind = type === "spec"
|
|
263
|
-
? "方案或需求文档可能需要调整"
|
|
264
|
-
: "代码实现和方案文档都可能有关";
|
|
265
|
-
const ask = {
|
|
266
|
-
question: `代码审查发现问题 ${findingId}:${problemKind}。请选择回到计划阶段修改文档、确认现有文档方向不变并回到实现阶段修代码,或驳回该问题;无论选择哪一项都必须写明原因。`,
|
|
267
|
-
allowed_answers: [
|
|
268
|
-
CODE_REVIEW_DECISION_ANSWER_LABELS.reopen_propose,
|
|
269
|
-
CODE_REVIEW_DECISION_ANSWER_LABELS.reopen_apply,
|
|
270
|
-
CODE_REVIEW_DECISION_ANSWER_LABELS.dismiss,
|
|
271
|
-
],
|
|
272
|
-
scope: codeReviewDecisionScope(latest.job.job_id, findingId),
|
|
273
|
-
};
|
|
274
|
-
return { state: "apply_done", path: "ask_user", ask_user: ask, reason: `代码审查发现需要使用者判断的问题 ${findingId}` };
|
|
275
|
-
}
|
|
276
|
-
const ask = {
|
|
277
|
-
question: "代码审查报告里缺少可用于处理问题的编号或分类。请修正审查报告后重新执行 review-ready。",
|
|
278
|
-
allowed_answers: ["报告已修正"],
|
|
279
|
-
scope: `${CODE_REVIEW_REPAIR_SCOPE_PREFIX}${change}`,
|
|
280
|
-
};
|
|
281
|
-
return { state: "apply_done", path: "ask_user", ask_user: ask, reason: "代码审查报告中的阻塞问题无法处理" };
|
|
282
|
-
}
|
|
283
|
-
if (latest?.state === "rejected" &&
|
|
284
|
-
(latest.result_kind === "invalid_report" || latest.result_kind === "non_actionable_report") &&
|
|
285
|
-
facts.consecutiveRejected >= 2) {
|
|
286
|
-
const ask = {
|
|
287
|
-
question: "代码审查报告连续两次不符合要求,或者没有给出可处理的问题。请先修正报告生成方式、模板或审查口径;修正后仍可显式执行 review-ready。",
|
|
288
|
-
allowed_answers: ["已修正"],
|
|
289
|
-
scope: `${CODE_REVIEW_REPAIR_SCOPE_PREFIX}${change}`,
|
|
290
|
-
};
|
|
291
|
-
return { state: "apply_done", path: "ask_user", ask_user: ask, reason: "代码审查报告连续不符合要求或没有可处理问题" };
|
|
292
|
-
}
|
|
293
|
-
return {
|
|
294
|
-
state: "apply_done",
|
|
295
|
-
path: "next_command",
|
|
296
|
-
next_command: transitionCommand(change, "review-ready", riskFlag(defaultRisk)),
|
|
297
|
-
reason: "所有任务完成,进入审查",
|
|
298
|
-
missing_inputs: [],
|
|
299
|
-
};
|
|
300
|
-
}
|
|
301
|
-
case "review": {
|
|
302
|
-
const pending = pendingTaskIds(changeRoot);
|
|
303
|
-
if (pending.length > 0) {
|
|
304
|
-
return {
|
|
305
|
-
state: "review",
|
|
306
|
-
path: "next_command",
|
|
307
|
-
next_command: reopenCommand(change, pending),
|
|
308
|
-
reason: `发现未完成任务 ${pending[0]},回到执行阶段`,
|
|
309
|
-
missing_inputs: [],
|
|
310
|
-
};
|
|
311
|
-
}
|
|
312
|
-
const reviewVerifierJobs = snapshot.open_jobs.filter(isReviewReadyVerifier);
|
|
313
|
-
if (reviewVerifierJobs.length > 0) {
|
|
314
|
-
return requiredJobsOutput("review", change, reviewVerifierJobs, `有 ${reviewVerifierJobs.length} 个待完成最终验证工作项`);
|
|
315
|
-
}
|
|
316
|
-
const events = readEvents(projectRoot, change);
|
|
317
|
-
const policy = readReviewPolicyFromEvents(events);
|
|
318
|
-
if (!policy) {
|
|
319
|
-
return {
|
|
320
|
-
state: "review",
|
|
321
|
-
path: "next_command",
|
|
322
|
-
next_command: transitionCommand(change, "review-ready", riskFlag(defaultRisk)),
|
|
323
|
-
reason: "缺少审查策略,先补 review-ready",
|
|
324
|
-
missing_inputs: [],
|
|
325
|
-
};
|
|
326
|
-
}
|
|
327
|
-
if (requiresFinalVerifierForCurrentReview(events) || policy.requires_verifier) {
|
|
328
|
-
const currentEvidenceDigest = reviewEvidenceDigest(events);
|
|
329
|
-
const verifierAccepted = snapshot.accepted_jobs.find(job => isFreshReviewVerifier(job, changeRoot, currentEvidenceDigest));
|
|
330
|
-
if (!verifierAccepted) {
|
|
331
|
-
return {
|
|
332
|
-
state: "review",
|
|
333
|
-
path: "next_command",
|
|
334
|
-
next_command: transitionCommand(change, "review-ready", riskFlag(defaultRisk)),
|
|
335
|
-
reason: "最终验证已经缺失或不再匹配当前证据,先补最终验证",
|
|
336
|
-
missing_inputs: [],
|
|
337
|
-
};
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
return {
|
|
341
|
-
state: "review",
|
|
342
|
-
path: "next_command",
|
|
343
|
-
next_command: transitionCommand(change, "accept"),
|
|
344
|
-
reason: "审查完成,提交接受",
|
|
345
|
-
missing_inputs: [],
|
|
346
|
-
};
|
|
347
|
-
}
|
|
348
|
-
case "accepted":
|
|
349
|
-
if (snapshot.open_jobs.length > 0) {
|
|
350
|
-
return requiredJobsOutput("accepted", change, snapshot.open_jobs, `有 ${snapshot.open_jobs.length} 个待完成工作项,暂不归档`);
|
|
351
|
-
}
|
|
37
|
+
function toNextOutput(change, plan) {
|
|
38
|
+
switch (plan.kind) {
|
|
39
|
+
case "required_jobs":
|
|
40
|
+
return requiredJobsOutput(plan.state, change, plan.jobs, plan.reason);
|
|
41
|
+
case "ask_user":
|
|
42
|
+
return { state: plan.state, path: "ask_user", ask_user: plan.ask, reason: plan.reason };
|
|
43
|
+
case "ask_archive_confirmation": {
|
|
352
44
|
const ask = {
|
|
353
45
|
question: `审查已通过,流程停在 accepted。确认归档时请执行 ${transitionCommand(change, "archive")}`,
|
|
354
46
|
allowed_answers: ["确认归档"],
|
|
355
47
|
scope: "archive_confirmation",
|
|
356
48
|
};
|
|
49
|
+
return { state: "accepted", path: "ask_user", ask_user: ask, reason: plan.reason };
|
|
50
|
+
}
|
|
51
|
+
case "run_transition":
|
|
357
52
|
return {
|
|
358
|
-
state:
|
|
359
|
-
path: "
|
|
360
|
-
|
|
361
|
-
reason:
|
|
362
|
-
|
|
363
|
-
case "archive":
|
|
364
|
-
if (snapshot.open_jobs.length > 0) {
|
|
365
|
-
return requiredJobsOutput("archive", change, snapshot.open_jobs, `有 ${snapshot.open_jobs.length} 个待完成工作项,暂不结束`);
|
|
366
|
-
}
|
|
367
|
-
return {
|
|
368
|
-
state: "archive",
|
|
369
|
-
path: "done",
|
|
370
|
-
reason: "已归档,流程完成。",
|
|
371
|
-
};
|
|
372
|
-
case "abandoned":
|
|
373
|
-
return {
|
|
374
|
-
state: "abandoned",
|
|
375
|
-
path: "done",
|
|
376
|
-
reason: "变更已放弃,流程终止。",
|
|
377
|
-
};
|
|
378
|
-
default:
|
|
379
|
-
return {
|
|
380
|
-
state: snapshot.state,
|
|
381
|
-
path: "done",
|
|
382
|
-
reason: `状态 ${snapshot.state} 超出 Phase 1 范围`,
|
|
53
|
+
state: plan.state,
|
|
54
|
+
path: "next_command",
|
|
55
|
+
next_command: transitionCommand(change, plan.transition, formatTransitionArgs(plan)),
|
|
56
|
+
reason: plan.reason,
|
|
57
|
+
missing_inputs: [],
|
|
383
58
|
};
|
|
59
|
+
case "done":
|
|
60
|
+
return { state: plan.state, path: "done", reason: plan.reason };
|
|
384
61
|
}
|
|
385
62
|
}
|
|
63
|
+
/** next 命令:读 snapshot,返回唯一可执行路径 */
|
|
64
|
+
export function next(projectRoot, change, changeRoot, defaultRisk = "strict") {
|
|
65
|
+
const snapshot = rebuildSnapshot(projectRoot, change, changeRoot);
|
|
66
|
+
const events = readEvents(projectRoot, change);
|
|
67
|
+
const plannedNextStep = planNextStep({ projectRoot, change, changeRoot, events, snapshot, mode: { kind: "risk", risk: defaultRisk } });
|
|
68
|
+
if (plannedNextStep)
|
|
69
|
+
return toNextOutput(change, plannedNextStep);
|
|
70
|
+
return {
|
|
71
|
+
state: snapshot.state,
|
|
72
|
+
path: "done",
|
|
73
|
+
reason: `状态 ${snapshot.state} 没有可执行下一步`,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import type { ReviewGateRule } from "./review_job_gates.ts";
|
|
2
|
+
import type { AskUser, Event, Job, JobRole, State } from "./types.ts";
|
|
3
|
+
import type { Snapshot } from "./types.ts";
|
|
4
|
+
import type { ReviewRisk } from "./review.ts";
|
|
5
|
+
export type TransitionName = "explore" | "propose-ready" | "start-apply" | "task-start" | "task-complete" | "review-ready" | "reopen" | "accept" | "archive";
|
|
6
|
+
export type WorkflowMode = {
|
|
7
|
+
kind: "risk";
|
|
8
|
+
risk: ReviewRisk;
|
|
9
|
+
};
|
|
10
|
+
export interface PhasePlanContext {
|
|
11
|
+
projectRoot: string;
|
|
12
|
+
change: string;
|
|
13
|
+
changeRoot: string;
|
|
14
|
+
events: Event[];
|
|
15
|
+
snapshot: Snapshot;
|
|
16
|
+
mode: WorkflowMode;
|
|
17
|
+
}
|
|
18
|
+
export interface TransitionPlanContext extends PhasePlanContext {
|
|
19
|
+
}
|
|
20
|
+
export type ReopenNextStep = {
|
|
21
|
+
to: "apply";
|
|
22
|
+
reason: "pending_tasks";
|
|
23
|
+
taskIds: string[];
|
|
24
|
+
} | {
|
|
25
|
+
to: "apply";
|
|
26
|
+
reason: "review_fix";
|
|
27
|
+
jobId: string;
|
|
28
|
+
findingId: string;
|
|
29
|
+
reopenReason: string;
|
|
30
|
+
} | {
|
|
31
|
+
to: "propose";
|
|
32
|
+
reason: "review_finding";
|
|
33
|
+
jobId: string;
|
|
34
|
+
findingId: string;
|
|
35
|
+
};
|
|
36
|
+
export type NextStepPlan = {
|
|
37
|
+
kind: "required_jobs";
|
|
38
|
+
state: State;
|
|
39
|
+
jobs: Job[];
|
|
40
|
+
reason: string;
|
|
41
|
+
} | {
|
|
42
|
+
kind: "ask_user";
|
|
43
|
+
state: State;
|
|
44
|
+
ask: AskUser;
|
|
45
|
+
reason: string;
|
|
46
|
+
} | {
|
|
47
|
+
kind: "ask_archive_confirmation";
|
|
48
|
+
state: "accepted";
|
|
49
|
+
reason: string;
|
|
50
|
+
} | {
|
|
51
|
+
kind: "run_transition";
|
|
52
|
+
state: State;
|
|
53
|
+
transition: TransitionName;
|
|
54
|
+
reason: string;
|
|
55
|
+
risk?: ReviewRisk;
|
|
56
|
+
taskId?: string;
|
|
57
|
+
reopen?: ReopenNextStep;
|
|
58
|
+
} | {
|
|
59
|
+
kind: "done";
|
|
60
|
+
state: State;
|
|
61
|
+
reason: string;
|
|
62
|
+
};
|
|
63
|
+
export type TransitionDecisionPlan = {
|
|
64
|
+
kind: "skip";
|
|
65
|
+
message: string;
|
|
66
|
+
} | {
|
|
67
|
+
kind: "blocked";
|
|
68
|
+
jobs: Job[];
|
|
69
|
+
reason: string;
|
|
70
|
+
} | {
|
|
71
|
+
kind: "create_gate_jobs";
|
|
72
|
+
gate: ReviewGateRule;
|
|
73
|
+
roles: JobRole[];
|
|
74
|
+
reason: string;
|
|
75
|
+
} | {
|
|
76
|
+
kind: "advance";
|
|
77
|
+
fromState: State;
|
|
78
|
+
toState: State;
|
|
79
|
+
reason: string;
|
|
80
|
+
payload?: Record<string, unknown>;
|
|
81
|
+
};
|
|
82
|
+
export interface ApplyPendingTaskStatus {
|
|
83
|
+
mode: "legacy" | "contract";
|
|
84
|
+
pending: string[];
|
|
85
|
+
needsCompletionEvent: string[];
|
|
86
|
+
completedByEvent: string[];
|
|
87
|
+
}
|
|
88
|
+
export declare function proposalDocsBaseline(changeRoot: string): Record<string, string>;
|
|
89
|
+
export declare function latestReopenProposeBaseline(events: Event[]): Record<string, string> | null;
|
|
90
|
+
export declare function proposalDocsChangedSinceBaseline(changeRoot: string, baseline: Record<string, string>): boolean;
|
|
91
|
+
export declare function historicalProposeReadyRoles(events: Event[]): JobRole[];
|
|
92
|
+
export declare function pendingTaskIds(changeRoot: string): string[];
|
|
93
|
+
export declare function applyRequirementModeForCurrentRound(events: Event[]): boolean;
|
|
94
|
+
export declare function pendingTaskStatusForApply(changeRoot: string, events: Event[]): ApplyPendingTaskStatus;
|
|
95
|
+
export declare function formatPendingTaskMessage(ids: string[], action: string): string;
|
|
96
|
+
export declare function planNextStep(context: PhasePlanContext): NextStepPlan | null;
|
|
97
|
+
export declare function planTransition(name: "explore" | "propose-ready" | "start-apply" | "accept", context: TransitionPlanContext): TransitionDecisionPlan;
|