@peterxiaoyang/superspec 0.1.38 → 0.1.40
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 +19 -2
- package/dist/code_review.d.ts +17 -2
- package/dist/code_review.js +471 -80
- 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 +1 -0
- package/dist/job_validity.js +11 -8
- package/dist/next.js +46 -314
- package/dist/phase_plan.d.ts +97 -0
- package/dist/phase_plan.js +582 -0
- package/dist/record.js +56 -7
- package/dist/review.d.ts +3 -2
- package/dist/review.js +64 -32
- package/dist/review_job_gates.js +1 -0
- package/dist/store.d.ts +10 -0
- package/dist/store.js +53 -3
- package/dist/sync.js +5 -5
- package/dist/task.js +87 -9
- package/dist/task_evidence.js +80 -0
- package/dist/transition.d.ts +1 -1
- package/dist/transition.js +301 -210
- package/dist/types.d.ts +77 -1
- package/package.json +1 -1
- package/templates/workflow/agents/explore.toml +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 +66 -64
- package/templates/workflow/skills/superspec-propose/SKILL.md +67 -44
- package/templates/workflow/skills/superspec-review/SKILL.md +1 -1
|
@@ -0,0 +1,582 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { EXPLORE_DISCOVERY_REVIEW_GATE, PROPOSE_FINAL_REVIEW_GATE } from "./review_job_gates.js";
|
|
4
|
+
import { collectProposeOpenQuestions, countDiscoveryOpenQuestions, parseTasksMd, pendingTasksInContent, validateDiscovery, validateExecutionRequirements, } from "./format.js";
|
|
5
|
+
import { currentGitHead } from "./git_state.js";
|
|
6
|
+
import { docRef, sha256File } from "./store.js";
|
|
7
|
+
import { isReviewReadyVerifier, isFreshReviewVerifier, readReviewPolicyFromEvents, reviewEvidenceDigest, } from "./review.js";
|
|
8
|
+
import { CODE_REVIEW_DECISION_ANSWER_LABELS, CODE_REVIEW_REPAIR_SCOPE_PREFIX, codeReviewDecisionScope, collectCodeReviewGateFacts, latestCodeReviewFailedStatus, requiresFinalVerifierForCurrentReview, } from "./code_review.js";
|
|
9
|
+
import { taskEvidenceReadiness } from "./task_evidence.js";
|
|
10
|
+
function requiredJobs(state, jobs, reason) {
|
|
11
|
+
return { kind: "required_jobs", state, jobs, reason };
|
|
12
|
+
}
|
|
13
|
+
function reviewGatePlan(snapshot, gate, requiredRoles) {
|
|
14
|
+
const missingRoles = [];
|
|
15
|
+
for (const role of requiredRoles) {
|
|
16
|
+
const openForRole = snapshot.open_jobs.find(job => gate.isJobForGate(job) && job.role === role);
|
|
17
|
+
if (openForRole) {
|
|
18
|
+
return { kind: "blocked", reason: `状态未推进;已有待完成工作项 ${role}(${openForRole.job_id})`, jobs: [openForRole] };
|
|
19
|
+
}
|
|
20
|
+
const acceptedForRole = snapshot.accepted_jobs.find(job => gate.isJobForGate(job) && job.role === role);
|
|
21
|
+
if (!acceptedForRole)
|
|
22
|
+
missingRoles.push({ role, reason: `需求 ${role} 无已接受的工作项` });
|
|
23
|
+
}
|
|
24
|
+
if (missingRoles.length > 0) {
|
|
25
|
+
return {
|
|
26
|
+
kind: "create_gate_jobs",
|
|
27
|
+
gate,
|
|
28
|
+
roles: missingRoles.map(item => item.role),
|
|
29
|
+
reason: missingRoles.map(item => item.reason).join("; "),
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
function validateTasksPlan(changeRoot) {
|
|
35
|
+
const tasksPath = join(changeRoot, "tasks.md");
|
|
36
|
+
if (!existsSync(tasksPath))
|
|
37
|
+
return "tasks.md 不存在";
|
|
38
|
+
const tasksContent = readFileSync(tasksPath, "utf8");
|
|
39
|
+
if (!tasksContent.includes("# Tasks") && !tasksContent.includes("- [ ]"))
|
|
40
|
+
return "tasks.md 内容不像任务计划文档";
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
function validateExecutionRequirementPlan(changeRoot) {
|
|
44
|
+
const tasksPath = join(changeRoot, "tasks.md");
|
|
45
|
+
if (!existsSync(tasksPath))
|
|
46
|
+
return { ok: true, mode: false };
|
|
47
|
+
const tasksContent = readFileSync(tasksPath, "utf8");
|
|
48
|
+
const testContractPath = join(changeRoot, ".superspec", "artifacts", "test-contract.md");
|
|
49
|
+
const testContractContent = existsSync(testContractPath) ? readFileSync(testContractPath, "utf8") : null;
|
|
50
|
+
const validation = validateExecutionRequirements(tasksContent, testContractContent);
|
|
51
|
+
return validation.ok
|
|
52
|
+
? { ok: true, mode: validation.mode }
|
|
53
|
+
: { ok: false, mode: validation.mode, message: validation.errors.join(";") };
|
|
54
|
+
}
|
|
55
|
+
function missingBaseArtifact(changeRoot, risk) {
|
|
56
|
+
if (risk === "minimal")
|
|
57
|
+
return null;
|
|
58
|
+
const artifactsDir = join(changeRoot, ".superspec", "artifacts");
|
|
59
|
+
for (const doc of ["discovery.md", "business-invariants.md", "test-contract.md"]) {
|
|
60
|
+
if (!existsSync(join(artifactsDir, doc)))
|
|
61
|
+
return `基础职责缺失:${doc} 不存在(risk=${risk} 需要)`;
|
|
62
|
+
}
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
export function proposalDocsBaseline(changeRoot) {
|
|
66
|
+
// specs/ 用目录聚合指纹:审查可能只对 specs 提出修复,reopen 后仅改 specs 也算文档变化
|
|
67
|
+
const docs = ["proposal.md", "design.md", "tasks.md", "specs/", ".superspec/artifacts/test-contract.md"];
|
|
68
|
+
const baseline = {};
|
|
69
|
+
for (const doc of docs) {
|
|
70
|
+
baseline[doc] = docRef(changeRoot, doc).sha;
|
|
71
|
+
}
|
|
72
|
+
return baseline;
|
|
73
|
+
}
|
|
74
|
+
export function latestReopenProposeBaseline(events) {
|
|
75
|
+
for (let i = events.length - 1; i >= 0; i--) {
|
|
76
|
+
const ev = events[i];
|
|
77
|
+
if (ev.event_type !== "transition_commit")
|
|
78
|
+
continue;
|
|
79
|
+
const payload = ev.payload;
|
|
80
|
+
if (payload.transition !== "reopen" || payload.reopen_target !== "propose")
|
|
81
|
+
continue;
|
|
82
|
+
if (!payload.baseline_docs || typeof payload.baseline_docs !== "object" || Array.isArray(payload.baseline_docs))
|
|
83
|
+
return null;
|
|
84
|
+
return payload.baseline_docs;
|
|
85
|
+
}
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
export function proposalDocsChangedSinceBaseline(changeRoot, baseline) {
|
|
89
|
+
const current = proposalDocsBaseline(changeRoot);
|
|
90
|
+
return Object.entries(baseline).some(([path, digest]) => current[path] !== digest);
|
|
91
|
+
}
|
|
92
|
+
export function historicalProposeReadyRoles(events) {
|
|
93
|
+
const roles = new Set();
|
|
94
|
+
for (const ev of events) {
|
|
95
|
+
if (ev.event_type !== "transition_commit")
|
|
96
|
+
continue;
|
|
97
|
+
const newJobs = ev.payload.new_jobs ?? [];
|
|
98
|
+
for (const job of newJobs) {
|
|
99
|
+
if (PROPOSE_FINAL_REVIEW_GATE.isJobForGate(job) &&
|
|
100
|
+
(job.role === "critic" || job.role === "architect" || job.role === "test-engineer"))
|
|
101
|
+
roles.add(job.role);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return [...roles];
|
|
105
|
+
}
|
|
106
|
+
export function pendingTaskIds(changeRoot) {
|
|
107
|
+
const tasksContent = readFileSync(join(changeRoot, "tasks.md"), "utf8");
|
|
108
|
+
return pendingTasksInContent(tasksContent).map(task => task.taskId);
|
|
109
|
+
}
|
|
110
|
+
function latestStartApplyIndex(events) {
|
|
111
|
+
for (let i = events.length - 1; i >= 0; i--) {
|
|
112
|
+
const ev = events[i];
|
|
113
|
+
if (ev.event_type !== "transition_commit")
|
|
114
|
+
continue;
|
|
115
|
+
const payload = ev.payload;
|
|
116
|
+
if (payload.transition === "start-apply" && payload.to_state === "apply")
|
|
117
|
+
return i;
|
|
118
|
+
}
|
|
119
|
+
return -1;
|
|
120
|
+
}
|
|
121
|
+
export function applyRequirementModeForCurrentRound(events) {
|
|
122
|
+
const index = latestStartApplyIndex(events);
|
|
123
|
+
if (index < 0)
|
|
124
|
+
return false;
|
|
125
|
+
const payload = events[index].payload;
|
|
126
|
+
return payload.apply_contract_mode === true || payload.execution_requirement_mode === true;
|
|
127
|
+
}
|
|
128
|
+
export function pendingTaskStatusForApply(changeRoot, events) {
|
|
129
|
+
const tasksContent = readFileSync(join(changeRoot, "tasks.md"), "utf8");
|
|
130
|
+
const tasks = parseTasksMd(tasksContent);
|
|
131
|
+
if (!applyRequirementModeForCurrentRound(events)) {
|
|
132
|
+
return {
|
|
133
|
+
mode: "legacy",
|
|
134
|
+
pending: tasks.filter(task => !task.done).map(task => task.taskId),
|
|
135
|
+
needsCompletionEvent: [],
|
|
136
|
+
completedByEvent: [],
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
const startIndex = latestStartApplyIndex(events);
|
|
140
|
+
const completedAttemptKeys = new Set();
|
|
141
|
+
const completedByTask = new Set();
|
|
142
|
+
const startedByTask = new Map();
|
|
143
|
+
for (const ev of events.slice(startIndex + 1)) {
|
|
144
|
+
if (ev.event_type === "task_started") {
|
|
145
|
+
const payload = ev.payload;
|
|
146
|
+
if (typeof payload.task_id !== "string" || typeof payload.attempt_id !== "string")
|
|
147
|
+
continue;
|
|
148
|
+
const attempts = startedByTask.get(payload.task_id) ?? [];
|
|
149
|
+
attempts.push(payload.attempt_id);
|
|
150
|
+
startedByTask.set(payload.task_id, attempts);
|
|
151
|
+
}
|
|
152
|
+
else if (ev.event_type === "task_completed") {
|
|
153
|
+
const payload = ev.payload;
|
|
154
|
+
if (typeof payload.task_id !== "string" || typeof payload.attempt_id !== "string")
|
|
155
|
+
continue;
|
|
156
|
+
completedAttemptKeys.add(`${payload.task_id}\u0000${payload.attempt_id}`);
|
|
157
|
+
completedByTask.add(payload.task_id);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
const pending = [];
|
|
161
|
+
const needsCompletionEvent = [];
|
|
162
|
+
const completedByEvent = [];
|
|
163
|
+
for (const task of tasks) {
|
|
164
|
+
const startedAttempts = startedByTask.get(task.taskId) ?? [];
|
|
165
|
+
const hasActiveCurrentAttempt = startedAttempts.some(attemptId => !completedAttemptKeys.has(`${task.taskId}\u0000${attemptId}`));
|
|
166
|
+
if (hasActiveCurrentAttempt) {
|
|
167
|
+
pending.push(task.taskId);
|
|
168
|
+
needsCompletionEvent.push(task.taskId);
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
if (completedByTask.has(task.taskId)) {
|
|
172
|
+
completedByEvent.push(task.taskId);
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
if (!task.done)
|
|
176
|
+
pending.push(task.taskId);
|
|
177
|
+
}
|
|
178
|
+
return {
|
|
179
|
+
mode: "contract",
|
|
180
|
+
pending,
|
|
181
|
+
needsCompletionEvent,
|
|
182
|
+
completedByEvent,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
export function formatPendingTaskMessage(ids, action) {
|
|
186
|
+
return `尚有未完成任务:${ids.join(", ")};${action}`;
|
|
187
|
+
}
|
|
188
|
+
export function planNextStep(context) {
|
|
189
|
+
const { changeRoot, mode, snapshot } = context;
|
|
190
|
+
switch (snapshot.state) {
|
|
191
|
+
case "init":
|
|
192
|
+
if (snapshot.open_jobs.length > 0) {
|
|
193
|
+
return requiredJobs("init", snapshot.open_jobs, `有 ${snapshot.open_jobs.length} 个待完成工作项`);
|
|
194
|
+
}
|
|
195
|
+
return { kind: "run_transition", state: "init", transition: "explore", reason: "初始化完成,开始探索" };
|
|
196
|
+
case "explore": {
|
|
197
|
+
const exploreReviewJobs = EXPLORE_DISCOVERY_REVIEW_GATE.openJobsForGate(snapshot);
|
|
198
|
+
if (exploreReviewJobs.length > 0) {
|
|
199
|
+
return requiredJobs("explore", exploreReviewJobs, `有 ${exploreReviewJobs.length} 个待完成探索审查工作项`);
|
|
200
|
+
}
|
|
201
|
+
const discoveryCheck = validateDiscovery(changeRoot);
|
|
202
|
+
if (!discoveryCheck.ok) {
|
|
203
|
+
const ask = {
|
|
204
|
+
question: discoveryCheck.message + ",请处理后继续",
|
|
205
|
+
allowed_answers: ["已处理"],
|
|
206
|
+
scope: "explore_discovery",
|
|
207
|
+
};
|
|
208
|
+
return { kind: "ask_user", state: "explore", ask, reason: discoveryCheck.message };
|
|
209
|
+
}
|
|
210
|
+
const content = readFileSync(join(changeRoot, ".superspec", "artifacts", "discovery.md"), "utf8");
|
|
211
|
+
const openQs = countDiscoveryOpenQuestions(content);
|
|
212
|
+
if (openQs > 0) {
|
|
213
|
+
const ask = {
|
|
214
|
+
question: `discovery.md 有 ${openQs} 个未解决的待确认问题,请逐个确认`,
|
|
215
|
+
allowed_answers: ["所有问题已确认"],
|
|
216
|
+
scope: "explore_open_questions",
|
|
217
|
+
};
|
|
218
|
+
return { kind: "ask_user", state: "explore", ask, reason: `有 ${openQs} 个未确认问题` };
|
|
219
|
+
}
|
|
220
|
+
return {
|
|
221
|
+
kind: "run_transition",
|
|
222
|
+
state: "explore",
|
|
223
|
+
transition: "explore",
|
|
224
|
+
reason: "探索完成,推进到计划阶段",
|
|
225
|
+
risk: mode.risk,
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
case "propose": {
|
|
229
|
+
const openQuestions = collectProposeOpenQuestions(changeRoot);
|
|
230
|
+
if (openQuestions.openCount > 0) {
|
|
231
|
+
const files = openQuestions.files.map(f => `${f.path}(${f.openCount})`).join(", ");
|
|
232
|
+
const ask = {
|
|
233
|
+
question: `计划文档有 ${openQuestions.openCount} 个待用户确认问题:${files}。请确认并更新计划文档后继续`,
|
|
234
|
+
allowed_answers: ["所有问题已确认"],
|
|
235
|
+
scope: "propose_open_questions",
|
|
236
|
+
};
|
|
237
|
+
return { kind: "ask_user", state: "propose", ask, reason: `有 ${openQuestions.openCount} 个 propose 未确认问题` };
|
|
238
|
+
}
|
|
239
|
+
const proposalReviewJobs = PROPOSE_FINAL_REVIEW_GATE.openJobsForGate(snapshot);
|
|
240
|
+
if (proposalReviewJobs.length > 0) {
|
|
241
|
+
return requiredJobs("propose", proposalReviewJobs, `有 ${proposalReviewJobs.length} 个待完成 proposal 审查工作项`);
|
|
242
|
+
}
|
|
243
|
+
return {
|
|
244
|
+
kind: "run_transition",
|
|
245
|
+
state: "propose",
|
|
246
|
+
transition: "propose-ready",
|
|
247
|
+
reason: "计划文档就绪,提交 propose-ready",
|
|
248
|
+
risk: mode.risk,
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
case "propose_ready": {
|
|
252
|
+
const proposalReviewJobs = PROPOSE_FINAL_REVIEW_GATE.openJobsForGate(snapshot);
|
|
253
|
+
if (proposalReviewJobs.length > 0) {
|
|
254
|
+
return requiredJobs("propose_ready", proposalReviewJobs, `有 ${proposalReviewJobs.length} 个待完成 proposal 审查工作项`);
|
|
255
|
+
}
|
|
256
|
+
return { kind: "run_transition", state: "propose_ready", transition: "start-apply", reason: "计划就绪,开始执行" };
|
|
257
|
+
}
|
|
258
|
+
case "apply":
|
|
259
|
+
return planApplyNext(context);
|
|
260
|
+
case "apply_done":
|
|
261
|
+
return planApplyDoneNext(context);
|
|
262
|
+
case "review":
|
|
263
|
+
return planReviewNext(context);
|
|
264
|
+
case "accepted":
|
|
265
|
+
if (snapshot.open_jobs.length > 0) {
|
|
266
|
+
return requiredJobs("accepted", snapshot.open_jobs, `有 ${snapshot.open_jobs.length} 个待完成工作项,暂不归档`);
|
|
267
|
+
}
|
|
268
|
+
return { kind: "ask_archive_confirmation", state: "accepted", reason: "审查通过,等待用户确认归档" };
|
|
269
|
+
case "archive":
|
|
270
|
+
if (snapshot.open_jobs.length > 0) {
|
|
271
|
+
return requiredJobs("archive", snapshot.open_jobs, `有 ${snapshot.open_jobs.length} 个待完成工作项,暂不结束`);
|
|
272
|
+
}
|
|
273
|
+
return { kind: "done", state: "archive", reason: "已归档,流程完成。" };
|
|
274
|
+
case "abandoned":
|
|
275
|
+
return { kind: "done", state: "abandoned", reason: "变更已放弃,流程终止。" };
|
|
276
|
+
default:
|
|
277
|
+
return null;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
function planApplyNext(context) {
|
|
281
|
+
const { change, changeRoot, events, mode, projectRoot, snapshot } = context;
|
|
282
|
+
if (snapshot.open_jobs.length > 0) {
|
|
283
|
+
return requiredJobs("apply", snapshot.open_jobs, `有 ${snapshot.open_jobs.length} 个待完成工作项`);
|
|
284
|
+
}
|
|
285
|
+
const pendingStatus = pendingTaskStatusForApply(changeRoot, events);
|
|
286
|
+
const pending = pendingStatus.pending;
|
|
287
|
+
if (pending.length > 0) {
|
|
288
|
+
const activePending = snapshot.active_task_attempts.find(attempt => attempt.state === "active" && pending.includes(attempt.task_id));
|
|
289
|
+
if (activePending) {
|
|
290
|
+
const readiness = taskEvidenceReadiness(projectRoot, change, changeRoot, activePending);
|
|
291
|
+
if (readiness.ready) {
|
|
292
|
+
return {
|
|
293
|
+
kind: "run_transition",
|
|
294
|
+
state: "apply",
|
|
295
|
+
transition: "task-complete",
|
|
296
|
+
taskId: activePending.task_id,
|
|
297
|
+
reason: `任务 ${activePending.task_id} 证据已登记,可以完成`,
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
const ask = {
|
|
301
|
+
question: `任务 ${activePending.task_id} 已开始,请先登记 ${readiness.missing.join("、")} 后继续`,
|
|
302
|
+
allowed_answers: ["证据已登记"],
|
|
303
|
+
scope: `apply_active_task_${activePending.task_id}`,
|
|
304
|
+
};
|
|
305
|
+
return { kind: "ask_user", state: "apply", ask, reason: `任务 ${activePending.task_id} 缺少完成证据` };
|
|
306
|
+
}
|
|
307
|
+
return {
|
|
308
|
+
kind: "run_transition",
|
|
309
|
+
state: "apply",
|
|
310
|
+
transition: "task-start",
|
|
311
|
+
taskId: pending[0],
|
|
312
|
+
reason: `执行中:下一个未完成任务 ${pending[0]}`,
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
return {
|
|
316
|
+
kind: "run_transition",
|
|
317
|
+
state: "apply",
|
|
318
|
+
transition: "review-ready",
|
|
319
|
+
risk: mode.risk,
|
|
320
|
+
reason: "所有任务完成,进入审查",
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
function planApplyDoneNext(context) {
|
|
324
|
+
const { change, changeRoot, events, mode, snapshot } = context;
|
|
325
|
+
const pendingTasks = pendingTaskStatusForApply(changeRoot, events).pending;
|
|
326
|
+
if (pendingTasks.length > 0) {
|
|
327
|
+
return {
|
|
328
|
+
kind: "run_transition",
|
|
329
|
+
state: "apply_done",
|
|
330
|
+
transition: "reopen",
|
|
331
|
+
reopen: { to: "apply", reason: "pending_tasks", taskIds: pendingTasks },
|
|
332
|
+
reason: `发现未完成任务 ${pendingTasks[0]},回到执行阶段`,
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
if (snapshot.open_jobs.length > 0) {
|
|
336
|
+
return requiredJobs("apply_done", snapshot.open_jobs, `有 ${snapshot.open_jobs.length} 个待完成工作项`);
|
|
337
|
+
}
|
|
338
|
+
const facts = collectCodeReviewGateFacts(events);
|
|
339
|
+
const latest = facts.latestTerminal;
|
|
340
|
+
if (latest?.state === "rejected" && latest.result_kind === "review_failed") {
|
|
341
|
+
const status = latestCodeReviewFailedStatus(events);
|
|
342
|
+
const pendingFinding = status?.unresolved[0] ?? null;
|
|
343
|
+
if (status && status.findings.length > 0 && !pendingFinding) {
|
|
344
|
+
return {
|
|
345
|
+
kind: "run_transition",
|
|
346
|
+
state: "apply_done",
|
|
347
|
+
transition: "review-ready",
|
|
348
|
+
risk: mode.risk,
|
|
349
|
+
reason: "代码审查问题已被主流程复核驳回,重新发起代码审查",
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
const findingId = pendingFinding?.id ?? "";
|
|
353
|
+
const type = pendingFinding?.type;
|
|
354
|
+
const decision = pendingFinding?.decision;
|
|
355
|
+
if (findingId && type === "implementation") {
|
|
356
|
+
return {
|
|
357
|
+
kind: "run_transition",
|
|
358
|
+
state: "apply_done",
|
|
359
|
+
transition: "reopen",
|
|
360
|
+
reopen: {
|
|
361
|
+
to: "apply",
|
|
362
|
+
reason: "review_fix",
|
|
363
|
+
jobId: latest.job.job_id,
|
|
364
|
+
findingId,
|
|
365
|
+
reopenReason: `修复代码审查问题 ${findingId}`,
|
|
366
|
+
},
|
|
367
|
+
reason: `代码审查发现纯代码实现问题 ${findingId},回到实现阶段修复`,
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
if (findingId && (type === "spec" || type === "mixed")) {
|
|
371
|
+
if (decision?.answer === "reopen_propose") {
|
|
372
|
+
return {
|
|
373
|
+
kind: "run_transition",
|
|
374
|
+
state: "apply_done",
|
|
375
|
+
transition: "reopen",
|
|
376
|
+
reopen: { to: "propose", reason: "review_finding", jobId: latest.job.job_id, findingId },
|
|
377
|
+
reason: `使用者已确认问题 ${findingId} 需要回到计划阶段`,
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
if (decision?.answer === "reopen_apply") {
|
|
381
|
+
return {
|
|
382
|
+
kind: "run_transition",
|
|
383
|
+
state: "apply_done",
|
|
384
|
+
transition: "reopen",
|
|
385
|
+
reopen: {
|
|
386
|
+
to: "apply",
|
|
387
|
+
reason: "review_fix",
|
|
388
|
+
jobId: latest.job.job_id,
|
|
389
|
+
findingId,
|
|
390
|
+
reopenReason: `根据代码审查问题 ${findingId} 回到实现阶段修复`,
|
|
391
|
+
},
|
|
392
|
+
reason: `使用者已确认问题 ${findingId} 直接回到实现阶段修复`,
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
const problemKind = type === "spec"
|
|
396
|
+
? "方案或需求文档可能需要调整"
|
|
397
|
+
: "代码实现和方案文档都可能有关";
|
|
398
|
+
const ask = {
|
|
399
|
+
question: `代码审查发现问题 ${findingId}:${problemKind}。请选择回到计划阶段修改文档、确认现有文档方向不变并回到实现阶段修代码,或驳回该问题;无论选择哪一项都必须写明原因。`,
|
|
400
|
+
allowed_answers: [
|
|
401
|
+
CODE_REVIEW_DECISION_ANSWER_LABELS.reopen_propose,
|
|
402
|
+
CODE_REVIEW_DECISION_ANSWER_LABELS.reopen_apply,
|
|
403
|
+
CODE_REVIEW_DECISION_ANSWER_LABELS.dismiss,
|
|
404
|
+
],
|
|
405
|
+
scope: codeReviewDecisionScope(latest.job.job_id, findingId),
|
|
406
|
+
};
|
|
407
|
+
return { kind: "ask_user", state: "apply_done", ask, reason: `代码审查发现需要使用者判断的问题 ${findingId}` };
|
|
408
|
+
}
|
|
409
|
+
const ask = {
|
|
410
|
+
question: "代码审查报告里缺少可用于处理问题的编号或分类。请修正审查报告后重新执行 review-ready。",
|
|
411
|
+
allowed_answers: ["报告已修正"],
|
|
412
|
+
scope: `${CODE_REVIEW_REPAIR_SCOPE_PREFIX}${change}`,
|
|
413
|
+
};
|
|
414
|
+
return { kind: "ask_user", state: "apply_done", ask, reason: "代码审查报告中的阻塞问题无法处理" };
|
|
415
|
+
}
|
|
416
|
+
if (latest?.state === "rejected" &&
|
|
417
|
+
(latest.result_kind === "invalid_report" || latest.result_kind === "non_actionable_report") &&
|
|
418
|
+
facts.consecutiveRejected >= 2) {
|
|
419
|
+
const ask = {
|
|
420
|
+
question: "代码审查报告连续两次不符合要求,或者没有给出可处理的问题。请先修正报告生成方式、模板或审查口径;修正后仍可显式执行 review-ready。",
|
|
421
|
+
allowed_answers: ["已修正"],
|
|
422
|
+
scope: `${CODE_REVIEW_REPAIR_SCOPE_PREFIX}${change}`,
|
|
423
|
+
};
|
|
424
|
+
return { kind: "ask_user", state: "apply_done", ask, reason: "代码审查报告连续不符合要求或没有可处理问题" };
|
|
425
|
+
}
|
|
426
|
+
return {
|
|
427
|
+
kind: "run_transition",
|
|
428
|
+
state: "apply_done",
|
|
429
|
+
transition: "review-ready",
|
|
430
|
+
risk: mode.risk,
|
|
431
|
+
reason: "所有任务完成,进入审查",
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
function planReviewNext(context) {
|
|
435
|
+
const { changeRoot, events, mode, projectRoot, snapshot } = context;
|
|
436
|
+
const pending = pendingTaskStatusForApply(changeRoot, events).pending;
|
|
437
|
+
if (pending.length > 0) {
|
|
438
|
+
return {
|
|
439
|
+
kind: "run_transition",
|
|
440
|
+
state: "review",
|
|
441
|
+
transition: "reopen",
|
|
442
|
+
reopen: { to: "apply", reason: "pending_tasks", taskIds: pending },
|
|
443
|
+
reason: `发现未完成任务 ${pending[0]},回到执行阶段`,
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
const reviewVerifierJobs = snapshot.open_jobs.filter(isReviewReadyVerifier);
|
|
447
|
+
if (reviewVerifierJobs.length > 0) {
|
|
448
|
+
return requiredJobs("review", reviewVerifierJobs, `有 ${reviewVerifierJobs.length} 个待完成最终验证工作项`);
|
|
449
|
+
}
|
|
450
|
+
const policy = readReviewPolicyFromEvents(events);
|
|
451
|
+
if (!policy) {
|
|
452
|
+
return {
|
|
453
|
+
kind: "run_transition",
|
|
454
|
+
state: "review",
|
|
455
|
+
transition: "review-ready",
|
|
456
|
+
risk: mode.risk,
|
|
457
|
+
reason: "缺少审查策略,先补 review-ready",
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
if (requiresFinalVerifierForCurrentReview(events) || policy.requires_verifier) {
|
|
461
|
+
const currentEvidenceDigest = reviewEvidenceDigest(events);
|
|
462
|
+
const verifierAccepted = snapshot.accepted_jobs.find(job => isFreshReviewVerifier(job, changeRoot, currentEvidenceDigest, projectRoot, events));
|
|
463
|
+
if (!verifierAccepted) {
|
|
464
|
+
return {
|
|
465
|
+
kind: "run_transition",
|
|
466
|
+
state: "review",
|
|
467
|
+
transition: "review-ready",
|
|
468
|
+
risk: mode.risk,
|
|
469
|
+
reason: "最终验证已经缺失或不再匹配当前证据,先补最终验证",
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
return { kind: "run_transition", state: "review", transition: "accept", reason: "审查完成,提交接受" };
|
|
474
|
+
}
|
|
475
|
+
export function planTransition(name, context) {
|
|
476
|
+
switch (name) {
|
|
477
|
+
case "explore":
|
|
478
|
+
return planExploreTransition(context);
|
|
479
|
+
case "propose-ready":
|
|
480
|
+
return planProposeReadyTransition(context);
|
|
481
|
+
case "start-apply":
|
|
482
|
+
return planStartApplyTransition(context);
|
|
483
|
+
case "accept":
|
|
484
|
+
return planAcceptTransition(context);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
function planExploreTransition(context) {
|
|
488
|
+
const { changeRoot, mode, snapshot } = context;
|
|
489
|
+
if (snapshot.state === "init") {
|
|
490
|
+
return { kind: "advance", fromState: "init", toState: "explore", reason: "进入探索阶段" };
|
|
491
|
+
}
|
|
492
|
+
if (snapshot.state !== "explore") {
|
|
493
|
+
return { kind: "skip", message: `当前状态 ${snapshot.state},explore 不适用` };
|
|
494
|
+
}
|
|
495
|
+
const discoveryCheck = validateDiscovery(changeRoot);
|
|
496
|
+
if (!discoveryCheck.ok)
|
|
497
|
+
return { kind: "skip", message: discoveryCheck.message };
|
|
498
|
+
const requiredRoles = EXPLORE_DISCOVERY_REVIEW_GATE.requiredRolesForRisk(mode.risk);
|
|
499
|
+
const gatePlan = reviewGatePlan(snapshot, EXPLORE_DISCOVERY_REVIEW_GATE, requiredRoles);
|
|
500
|
+
if (gatePlan)
|
|
501
|
+
return gatePlan;
|
|
502
|
+
return { kind: "advance", fromState: "explore", toState: "propose", reason: "探索完成" };
|
|
503
|
+
}
|
|
504
|
+
function planProposeReadyTransition(context) {
|
|
505
|
+
const { changeRoot, mode, snapshot } = context;
|
|
506
|
+
const risk = mode.risk;
|
|
507
|
+
if (snapshot.state !== "propose")
|
|
508
|
+
return { kind: "skip", message: `当前状态 ${snapshot.state},不能 propose-ready` };
|
|
509
|
+
const tasksPlanError = validateTasksPlan(changeRoot);
|
|
510
|
+
if (tasksPlanError)
|
|
511
|
+
return { kind: "skip", message: tasksPlanError };
|
|
512
|
+
const executionRequirementPlan = validateExecutionRequirementPlan(changeRoot);
|
|
513
|
+
if (!executionRequirementPlan.ok)
|
|
514
|
+
return { kind: "skip", message: executionRequirementPlan.message };
|
|
515
|
+
const openQuestions = collectProposeOpenQuestions(changeRoot);
|
|
516
|
+
if (openQuestions.openCount > 0) {
|
|
517
|
+
const files = openQuestions.files.map(f => `${f.path}(${f.openCount})`).join(", ");
|
|
518
|
+
return { kind: "skip", message: `计划文档有 ${openQuestions.openCount} 个待用户确认问题:${files}` };
|
|
519
|
+
}
|
|
520
|
+
const missingArtifact = missingBaseArtifact(changeRoot, risk);
|
|
521
|
+
if (missingArtifact)
|
|
522
|
+
return { kind: "skip", message: missingArtifact };
|
|
523
|
+
const requiredRoles = PROPOSE_FINAL_REVIEW_GATE.requiredRolesForRisk(risk);
|
|
524
|
+
const gatePlan = reviewGatePlan(snapshot, PROPOSE_FINAL_REVIEW_GATE, requiredRoles);
|
|
525
|
+
if (gatePlan)
|
|
526
|
+
return gatePlan;
|
|
527
|
+
return { kind: "advance", fromState: "propose", toState: "propose_ready", reason: `risk=${risk},所有需求已满足` };
|
|
528
|
+
}
|
|
529
|
+
function planStartApplyTransition(context) {
|
|
530
|
+
const { changeRoot, events, projectRoot, snapshot } = context;
|
|
531
|
+
if (snapshot.state !== "propose_ready")
|
|
532
|
+
return { kind: "skip", message: `当前状态 ${snapshot.state},需要 propose_ready` };
|
|
533
|
+
const reopenBaseline = latestReopenProposeBaseline(events);
|
|
534
|
+
if (reopenBaseline && !proposalDocsChangedSinceBaseline(changeRoot, reopenBaseline)) {
|
|
535
|
+
// 按基线实际键名提示:升级前留下的旧基线可能不含 specs/,静态清单会误导
|
|
536
|
+
return { kind: "skip", message: `回到 propose 后至少一个计划文档必须变化(基线绑定:${Object.keys(reopenBaseline).join("、")})` };
|
|
537
|
+
}
|
|
538
|
+
const reviewedRoles = historicalProposeReadyRoles(events);
|
|
539
|
+
if (reviewedRoles.length > 0) {
|
|
540
|
+
const gatePlan = reviewGatePlan(snapshot, PROPOSE_FINAL_REVIEW_GATE, reviewedRoles);
|
|
541
|
+
if (gatePlan) {
|
|
542
|
+
return {
|
|
543
|
+
...gatePlan,
|
|
544
|
+
reason: `进入执行阶段前需要重新完成计划文档审查:${gatePlan.reason}`,
|
|
545
|
+
};
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
const executionRequirementPlan = validateExecutionRequirementPlan(changeRoot);
|
|
549
|
+
if (!executionRequirementPlan.ok)
|
|
550
|
+
return { kind: "skip", message: executionRequirementPlan.message };
|
|
551
|
+
const gitHead = currentGitHead(projectRoot);
|
|
552
|
+
return {
|
|
553
|
+
kind: "advance",
|
|
554
|
+
fromState: "propose_ready",
|
|
555
|
+
toState: "apply",
|
|
556
|
+
reason: "进入执行阶段",
|
|
557
|
+
payload: {
|
|
558
|
+
apply_start_head: gitHead.head,
|
|
559
|
+
apply_start_head_reason: gitHead.reason,
|
|
560
|
+
apply_contract_mode: executionRequirementPlan.mode,
|
|
561
|
+
},
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
function planAcceptTransition(context) {
|
|
565
|
+
const { changeRoot, events, projectRoot, snapshot } = context;
|
|
566
|
+
if (snapshot.state !== "review")
|
|
567
|
+
return { kind: "skip", message: `当前状态 ${snapshot.state},需要 review` };
|
|
568
|
+
const pending = pendingTaskStatusForApply(changeRoot, events).pending;
|
|
569
|
+
if (pending.length > 0) {
|
|
570
|
+
return { kind: "skip", message: formatPendingTaskMessage(pending, "请先 reopen --to apply 继续执行") };
|
|
571
|
+
}
|
|
572
|
+
const policy = readReviewPolicyFromEvents(events);
|
|
573
|
+
if (!policy)
|
|
574
|
+
return { kind: "skip", message: "缺少审查策略,请先运行 review-ready" };
|
|
575
|
+
if (requiresFinalVerifierForCurrentReview(events) || policy.requires_verifier) {
|
|
576
|
+
const currentEvidenceDigest = reviewEvidenceDigest(events);
|
|
577
|
+
const verifierAccepted = snapshot.accepted_jobs.find(job => isFreshReviewVerifier(job, changeRoot, currentEvidenceDigest, projectRoot, events));
|
|
578
|
+
if (!verifierAccepted)
|
|
579
|
+
return { kind: "skip", message: "缺少仍然匹配当前证据的最终验证,请先运行 review-ready" };
|
|
580
|
+
}
|
|
581
|
+
return { kind: "advance", fromState: "review", toState: "accepted", reason: "审查通过" };
|
|
582
|
+
}
|