@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/transition.js
CHANGED
|
@@ -1,78 +1,49 @@
|
|
|
1
1
|
// SuperSpec 流程引擎 — transition:提交协议 + 所有 transition 处理器
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
-
import { existsSync, readFileSync, writeFileSync
|
|
4
|
-
import { ensureChangeLayout, readEvents, appendEvent, makeEvent, writeSnapshot, snapshotDigest, withLock, idempotencyKey, sha256File, sha256Text, } from "./store.js";
|
|
3
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { ensureChangeLayout, readEvents, appendEvent, makeEvent, writeSnapshot, snapshotDigest, withLock, idempotencyKey, docRef, listMarkdownFiles, sha256File, sha256Text, } from "./store.js";
|
|
5
5
|
import { rebuildSnapshot } from "./sync.js";
|
|
6
6
|
import { requiredJobActions } from "./job_action.js";
|
|
7
7
|
import { assertCommitPayloadExtension, isFreshReviewVerifier, isReviewReadyVerifier, readReviewPolicyFromEvents, reviewBoundFiles, reviewEvidenceDigest, reviewPolicyForRisk, REVIEW_DOC_PATHS, } from "./review.js";
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
8
|
+
import { REVIEW_CODE_REVIEW_GATE_ID, REVIEW_FINAL_VERIFIER_GATE_ID, } from "./review_job_gates.js";
|
|
9
|
+
import { codeReviewBoundFiles, codeReviewDecisionScope, codeReviewJobStaleReason, codeReviewPacketContext, codeReviewPacketDigest, collectCodeReviewGateFacts, computeCodeStateCheck, currentCodeReviewWorkingPaths, dismissedCodeReviewSummary, latestCodeReviewDecision, latestCodeReviewFailedStatus, missingCoverageExemptionTestIds, requiresFinalVerifierForCurrentReview, scanCodeChangesForReview, } from "./code_review.js";
|
|
10
|
+
import { taskEvidenceReadiness } from "./task_evidence.js";
|
|
11
|
+
import { adoptedContractForTask, findTaskInLines, isReviewFixTaskId, parseTasksMd, parseTestContractEntries, } from "./format.js";
|
|
12
|
+
import { applyRequirementModeForCurrentRound, formatPendingTaskMessage, pendingTaskStatusForApply, planTransition, proposalDocsBaseline, } from "./phase_plan.js";
|
|
13
|
+
import { currentGitHead, dirtyCodeFiles } from "./git_state.js";
|
|
10
14
|
let transitionSeq = 0;
|
|
11
15
|
function newTransitionId() { return `T-${Date.now()}-${++transitionSeq}`; }
|
|
12
16
|
let jobSeq = 0;
|
|
13
17
|
function newJobId(change, role) { return `JOB-${change.slice(0, 8)}-${role.slice(0, 4)}-${Date.now()}-${++jobSeq}`; }
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
"explore": {
|
|
21
|
-
minimal: [],
|
|
22
|
-
normal: [],
|
|
23
|
-
strict: ["critic"],
|
|
24
|
-
},
|
|
25
|
-
};
|
|
26
|
-
/**
|
|
27
|
-
* 通用 job 检查/创建逻辑——任何 transition 都能调用。
|
|
28
|
-
* 检查 requiredRoles 是否有 fresh accepted job;缺则创建新 job。
|
|
29
|
-
* 返回 null = 全部满足;返回 Decision = 需要创建 job(状态不变)。
|
|
30
|
-
*/
|
|
31
|
-
function checkOrCreateReviewJobs(snapshot, requiredRoles, changeRoot, change, transitionName, bindDocPaths) {
|
|
32
|
-
const staleRoles = [];
|
|
33
|
-
for (const role of requiredRoles) {
|
|
34
|
-
const openForRole = snapshot.open_jobs.find(j => j.role === role && j.created_from_transition === transitionName);
|
|
35
|
-
if (openForRole)
|
|
36
|
-
return { blocked: true, reason: `状态未推进;已有待完成工作项 ${role}(${openForRole.job_id})`, jobs: [openForRole] };
|
|
37
|
-
const fresh = snapshot.accepted_jobs.find(j => j.role === role && j.created_from_transition === transitionName);
|
|
38
|
-
if (!fresh) {
|
|
39
|
-
staleRoles.push({ role, reason: `需求 ${role} 无已接受的工作项` });
|
|
40
|
-
}
|
|
41
|
-
else {
|
|
42
|
-
for (const bf of fresh.boundFiles) {
|
|
43
|
-
const currentSha = sha256File(join(changeRoot, bf.path)) ?? "sha256:missing";
|
|
44
|
-
if (currentSha !== bf.sha) {
|
|
45
|
-
staleRoles.push({ role, reason: `${role} 绑定文件 ${bf.path} 已变化` });
|
|
46
|
-
break;
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
if (staleRoles.length > 0) {
|
|
52
|
-
const newJobs = staleRoles.map(({ role }) => {
|
|
53
|
-
const boundFiles = bindDocPaths.filter(p => existsSync(join(changeRoot, p))).map(p => ({ path: p, sha: sha256File(join(changeRoot, p)) ?? "sha256:missing" }));
|
|
54
|
-
return { job_id: newJobId(change, role), role, state: "requested", boundFiles, packet_digest: sha256Text(JSON.stringify({ role, boundFiles })), created_from_transition: transitionName, created_at: new Date().toISOString() };
|
|
55
|
-
});
|
|
18
|
+
function createReviewJobsForGate(state, gate, roles, changeRoot, change, reason) {
|
|
19
|
+
const newJobs = roles.map(role => {
|
|
20
|
+
// 目录路径(以 / 结尾)始终绑定聚合指纹:审查时不存在、审查后新建同样视为变化
|
|
21
|
+
const boundFiles = gate.reviewedDocPaths
|
|
22
|
+
.filter(p => p.endsWith("/") || existsSync(join(changeRoot, p)))
|
|
23
|
+
.map(p => docRef(changeRoot, p));
|
|
56
24
|
return {
|
|
57
|
-
|
|
58
|
-
|
|
25
|
+
job_id: newJobId(change, role),
|
|
26
|
+
role,
|
|
27
|
+
state: "requested",
|
|
28
|
+
gate_id: gate.gate_id,
|
|
29
|
+
boundFiles,
|
|
30
|
+
packet_digest: sha256Text(JSON.stringify({
|
|
31
|
+
role,
|
|
32
|
+
gate_id: gate.gate_id,
|
|
33
|
+
boundFiles,
|
|
34
|
+
created_from_transition: gate.created_from_transition,
|
|
35
|
+
})),
|
|
36
|
+
created_from_transition: gate.created_from_transition,
|
|
37
|
+
created_at: new Date().toISOString(),
|
|
59
38
|
};
|
|
60
|
-
}
|
|
61
|
-
return
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
const newJobs = ev.payload.new_jobs ?? [];
|
|
69
|
-
for (const job of newJobs) {
|
|
70
|
-
if (job.created_from_transition === "propose-ready" &&
|
|
71
|
-
(job.role === "critic" || job.role === "architect" || job.role === "test-engineer"))
|
|
72
|
-
roles.add(job.role);
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
return [...roles];
|
|
39
|
+
});
|
|
40
|
+
return {
|
|
41
|
+
fromState: state,
|
|
42
|
+
toState: state,
|
|
43
|
+
outcome: "job_created",
|
|
44
|
+
newJobs,
|
|
45
|
+
reason,
|
|
46
|
+
};
|
|
76
47
|
}
|
|
77
48
|
/**
|
|
78
49
|
* 已迁移到 format.ts:findTaskInLines / parseTasksMd / tasksStructureDigest
|
|
@@ -81,12 +52,94 @@ function historicalProposeReadyRoles(projectRoot, change) {
|
|
|
81
52
|
function findTaskLine(lines, taskId) {
|
|
82
53
|
return findTaskInLines(lines, taskId);
|
|
83
54
|
}
|
|
84
|
-
function
|
|
85
|
-
const
|
|
86
|
-
|
|
55
|
+
function boundarySnapshotResult(projectRoot) {
|
|
56
|
+
const head = currentGitHead(projectRoot);
|
|
57
|
+
const dirty = dirtyCodeFiles(projectRoot);
|
|
58
|
+
if (!dirty.ok)
|
|
59
|
+
return { snapshot: null, reason: dirty.reason };
|
|
60
|
+
return {
|
|
61
|
+
snapshot: {
|
|
62
|
+
head: head.head,
|
|
63
|
+
...(head.reason !== "ok" ? { head_reason: head.reason } : {}),
|
|
64
|
+
dirty_files: dirty.files,
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
function boundarySnapshotPayload(projectRoot) {
|
|
69
|
+
const result = boundarySnapshotResult(projectRoot);
|
|
70
|
+
return {
|
|
71
|
+
boundary_snapshot: result.snapshot,
|
|
72
|
+
...(result.reason ? { boundary_snapshot_reason: result.reason } : {}),
|
|
73
|
+
};
|
|
87
74
|
}
|
|
88
|
-
function
|
|
89
|
-
|
|
75
|
+
function parseScopeNoteInput(inputContent) {
|
|
76
|
+
if (inputContent == null)
|
|
77
|
+
return { ok: true, value: null, digest: null };
|
|
78
|
+
const digest = sha256Text(inputContent);
|
|
79
|
+
if (inputContent.trim() === "")
|
|
80
|
+
return { ok: false, message: "范围扩大说明(scope_note)输入不能为空", digest };
|
|
81
|
+
let parsed;
|
|
82
|
+
try {
|
|
83
|
+
parsed = JSON.parse(inputContent);
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
return { ok: false, message: "范围扩大说明(scope_note)输入必须是有效 JSON", digest };
|
|
87
|
+
}
|
|
88
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
89
|
+
return { ok: false, message: "范围扩大说明(scope_note)输入顶层必须是 JSON object", digest };
|
|
90
|
+
}
|
|
91
|
+
const obj = parsed;
|
|
92
|
+
const keys = Object.keys(obj);
|
|
93
|
+
if (keys.some(key => key !== "scope_note")) {
|
|
94
|
+
return { ok: false, message: "范围扩大说明(scope_note)输入包含未知顶层字段", digest };
|
|
95
|
+
}
|
|
96
|
+
const note = obj.scope_note;
|
|
97
|
+
if (!note || typeof note !== "object" || Array.isArray(note)) {
|
|
98
|
+
return { ok: false, message: "范围扩大说明(scope_note)必须是对象", digest };
|
|
99
|
+
}
|
|
100
|
+
const noteObj = note;
|
|
101
|
+
const allowed = new Set(["reason", "changed_area", "plan_alignment", "verification"]);
|
|
102
|
+
if (Object.keys(noteObj).some(key => !allowed.has(key))) {
|
|
103
|
+
return { ok: false, message: "范围扩大说明(scope_note)包含未知字段", digest };
|
|
104
|
+
}
|
|
105
|
+
for (const field of ["reason", "changed_area", "plan_alignment"]) {
|
|
106
|
+
if (typeof noteObj[field] !== "string" || noteObj[field].trim() === "") {
|
|
107
|
+
return { ok: false, message: `范围扩大说明里的 ${field} 字段(scope_note.${field})必须是非空字符串`, digest };
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (!Array.isArray(noteObj.verification) || noteObj.verification.length === 0 || !noteObj.verification.every(item => typeof item === "string" && item.trim() !== "")) {
|
|
111
|
+
return { ok: false, message: "范围扩大说明里的验证字段(scope_note.verification)必须是非空字符串数组", digest };
|
|
112
|
+
}
|
|
113
|
+
return {
|
|
114
|
+
ok: true,
|
|
115
|
+
digest,
|
|
116
|
+
value: {
|
|
117
|
+
reason: noteObj.reason,
|
|
118
|
+
changed_area: noteObj.changed_area,
|
|
119
|
+
plan_alignment: noteObj.plan_alignment,
|
|
120
|
+
verification: noteObj.verification,
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
function validateTaskStartContract(changeRoot, taskId, tddRequired, parsedContract) {
|
|
125
|
+
if (!parsedContract)
|
|
126
|
+
return null;
|
|
127
|
+
if (parsedContract.errors.length > 0)
|
|
128
|
+
return parsedContract.errors.join(";");
|
|
129
|
+
if (tddRequired && !isReviewFixTaskId(taskId) && parsedContract.contract.tests.length === 0) {
|
|
130
|
+
return `${taskId} 是普通 TDD 任务,执行依据缺少测试`;
|
|
131
|
+
}
|
|
132
|
+
if (parsedContract.contract.tests.length === 0)
|
|
133
|
+
return null;
|
|
134
|
+
const testContractPath = join(changeRoot, ".superspec", "artifacts", "test-contract.md");
|
|
135
|
+
if (!existsSync(testContractPath))
|
|
136
|
+
return "test-contract.md 不存在,无法校验执行依据测试引用";
|
|
137
|
+
const parsed = parseTestContractEntries(readFileSync(testContractPath, "utf8"));
|
|
138
|
+
if (!parsed.ok)
|
|
139
|
+
return parsed.message;
|
|
140
|
+
const known = new Set(parsed.entries.map(entry => entry.test_id));
|
|
141
|
+
const missing = parsedContract.contract.tests.filter(testId => !known.has(testId));
|
|
142
|
+
return missing.length > 0 ? `执行依据引用了不存在的 TEST ID:${missing.join(", ")}` : null;
|
|
90
143
|
}
|
|
91
144
|
function hasRejectedReviewReadyVerifier(events) {
|
|
92
145
|
const reviewReadyVerifierIds = new Set();
|
|
@@ -102,9 +155,10 @@ function hasRejectedReviewReadyVerifier(events) {
|
|
|
102
155
|
return events.some(ev => ev.event_type === "job_rejected" &&
|
|
103
156
|
reviewReadyVerifierIds.has(ev.payload.job_id ?? ""));
|
|
104
157
|
}
|
|
105
|
-
function createCodeReviewerJob(change, projectRoot, events) {
|
|
106
|
-
const scan =
|
|
158
|
+
function createCodeReviewerJob(change, projectRoot, changeRoot, events) {
|
|
159
|
+
const scan = scanCodeChangesForReview(projectRoot, events);
|
|
107
160
|
const boundFiles = codeReviewBoundFiles(projectRoot, scan.paths);
|
|
161
|
+
const packetContext = codeReviewPacketContext(changeRoot, projectRoot, scan.scope, events);
|
|
108
162
|
const facts = collectCodeReviewGateFacts(events);
|
|
109
163
|
const latestRejected = facts.latestRejected;
|
|
110
164
|
const reviewFailedStatus = latestCodeReviewFailedStatus(events);
|
|
@@ -119,8 +173,10 @@ function createCodeReviewerJob(change, projectRoot, events) {
|
|
|
119
173
|
: undefined;
|
|
120
174
|
const packetInput = {
|
|
121
175
|
role: "code-reviewer",
|
|
176
|
+
gate_id: REVIEW_CODE_REVIEW_GATE_ID,
|
|
122
177
|
boundFiles,
|
|
123
178
|
checkedDocs: REVIEW_DOC_PATHS,
|
|
179
|
+
packet_context: packetContext,
|
|
124
180
|
created_from_transition: "review-ready",
|
|
125
181
|
...(previousRejection ? { previous_rejection: previousRejection } : {}),
|
|
126
182
|
};
|
|
@@ -130,7 +186,9 @@ function createCodeReviewerJob(change, projectRoot, events) {
|
|
|
130
186
|
job_id: newJobId(change, "code-reviewer"),
|
|
131
187
|
role: "code-reviewer",
|
|
132
188
|
state: "requested",
|
|
189
|
+
gate_id: REVIEW_CODE_REVIEW_GATE_ID,
|
|
133
190
|
boundFiles,
|
|
191
|
+
packet_context: packetContext,
|
|
134
192
|
packet_digest: codeReviewPacketDigest(packetInput),
|
|
135
193
|
created_from_transition: "review-ready",
|
|
136
194
|
created_at: new Date().toISOString(),
|
|
@@ -173,34 +231,195 @@ function appendReviewFixTask(changeRoot, ref, finding) {
|
|
|
173
231
|
writeFileSync(tasksPath, `${content}${suffix}${line}\n`);
|
|
174
232
|
return "created";
|
|
175
233
|
}
|
|
176
|
-
function isFreshOpenCodeReviewerJob(job, projectRoot) {
|
|
177
|
-
return codeReviewJobStaleReason(projectRoot, job) == null;
|
|
234
|
+
function isFreshOpenCodeReviewerJob(job, projectRoot, currentWorkingPaths) {
|
|
235
|
+
return codeReviewJobStaleReason(projectRoot, job, currentWorkingPaths) == null;
|
|
236
|
+
}
|
|
237
|
+
function hasFrozenCodeReviewCurrentHead(scope) {
|
|
238
|
+
if (!scope || typeof scope !== "object")
|
|
239
|
+
return false;
|
|
240
|
+
if (!Object.prototype.hasOwnProperty.call(scope, "current_head"))
|
|
241
|
+
return false;
|
|
242
|
+
const currentHead = scope.current_head;
|
|
243
|
+
return currentHead === null || (typeof currentHead === "string" && currentHead.trim() !== "");
|
|
178
244
|
}
|
|
179
|
-
function
|
|
180
|
-
const
|
|
181
|
-
const
|
|
182
|
-
|
|
183
|
-
|
|
245
|
+
function evaluateApplyDoneCodeReviewGate(input) {
|
|
246
|
+
const scan = scanCodeChangesForReview(input.projectRoot, input.events);
|
|
247
|
+
const facts = collectCodeReviewGateFacts(input.events);
|
|
248
|
+
if (scan.hasCodeChanges) {
|
|
249
|
+
const currentWorkingPaths = currentCodeReviewWorkingPaths(input.projectRoot, input.events);
|
|
250
|
+
const freshOpenJobs = facts.openJobs.filter(job => isFreshOpenCodeReviewerJob(job, input.projectRoot, currentWorkingPaths));
|
|
251
|
+
if (freshOpenJobs.length > 0) {
|
|
252
|
+
return {
|
|
253
|
+
blocked: true,
|
|
254
|
+
reason: `状态未推进;已有待完成代码审查工作项 ${freshOpenJobs[0].job_id}`,
|
|
255
|
+
jobs: [freshOpenJobs[0]],
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
const latest = facts.latestTerminal;
|
|
259
|
+
if (latest?.state === "accepted") {
|
|
260
|
+
const acceptedScope = latest.job.packet_context?.code_review_scope;
|
|
261
|
+
if (!hasFrozenCodeReviewCurrentHead(acceptedScope)) {
|
|
262
|
+
const { job, scanReason } = createCodeReviewerJob(input.change, input.projectRoot, input.changeRoot, input.events);
|
|
263
|
+
return {
|
|
264
|
+
fromState: "apply_done",
|
|
265
|
+
toState: "apply_done",
|
|
266
|
+
outcome: "job_created",
|
|
267
|
+
newJobs: [job],
|
|
268
|
+
reason: `已接受代码审查工作项缺少冻结 current_head,重新创建代码审查工作项;${scanReason}`,
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
return {
|
|
272
|
+
fromState: "apply_done",
|
|
273
|
+
toState: "review",
|
|
274
|
+
outcome: "advanced",
|
|
275
|
+
reason: "代码审查已通过,进入最终审查阶段",
|
|
276
|
+
commitPayload: {
|
|
277
|
+
...input.policyPayload,
|
|
278
|
+
code_review_gate: {
|
|
279
|
+
decision: "passed",
|
|
280
|
+
job_id: latest.job.job_id,
|
|
281
|
+
packet_digest: latest.job.packet_digest,
|
|
282
|
+
current_head: acceptedScope.current_head,
|
|
283
|
+
},
|
|
284
|
+
},
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
if (latest?.state === "rejected" && latest.result_kind === "review_failed") {
|
|
288
|
+
const reviewFailedStatus = latestCodeReviewFailedStatus(input.events);
|
|
289
|
+
if (reviewFailedStatus && reviewFailedStatus.findings.length > 0 && reviewFailedStatus.unresolved.length === 0) {
|
|
290
|
+
const { job, scanReason } = createCodeReviewerJob(input.change, input.projectRoot, input.changeRoot, input.events);
|
|
291
|
+
return {
|
|
292
|
+
fromState: "apply_done",
|
|
293
|
+
toState: "apply_done",
|
|
294
|
+
outcome: "job_created",
|
|
295
|
+
newJobs: [job],
|
|
296
|
+
reason: `重新创建代码审查工作项;${scanReason};上一次阻塞问题已被主流程复核驳回`,
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
return {
|
|
300
|
+
skip: true,
|
|
301
|
+
message: "代码审查发现需要处理的问题,请先执行 next,根据提示回到实现阶段修复或让使用者决定是否回到计划阶段",
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
const { job, scanReason } = createCodeReviewerJob(input.change, input.projectRoot, input.changeRoot, input.events);
|
|
305
|
+
return {
|
|
306
|
+
fromState: "apply_done",
|
|
307
|
+
toState: "apply_done",
|
|
308
|
+
outcome: "job_created",
|
|
309
|
+
newJobs: [job],
|
|
310
|
+
reason: latest?.state === "rejected"
|
|
311
|
+
? `重新创建代码审查工作项;上一次报告未被接受,原因:${latest.reason ?? "报告不符合要求"}`
|
|
312
|
+
: `创建代码审查工作项;${scanReason}`,
|
|
313
|
+
};
|
|
184
314
|
}
|
|
185
|
-
return
|
|
315
|
+
return {
|
|
316
|
+
fromState: "apply_done",
|
|
317
|
+
toState: "review",
|
|
318
|
+
outcome: "advanced",
|
|
319
|
+
reason: "没有代码类改动,直接进入最终审查阶段",
|
|
320
|
+
commitPayload: {
|
|
321
|
+
...input.policyPayload,
|
|
322
|
+
code_review_gate: {
|
|
323
|
+
decision: "skipped",
|
|
324
|
+
reason: "no_code_changes",
|
|
325
|
+
head: scan.scope?.current_head ?? null,
|
|
326
|
+
},
|
|
327
|
+
},
|
|
328
|
+
};
|
|
186
329
|
}
|
|
187
|
-
function
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
330
|
+
function createFinalVerifierJob(change, projectRoot, changeRoot, events, currentEvidenceDigest) {
|
|
331
|
+
const boundFiles = reviewBoundFiles(changeRoot);
|
|
332
|
+
const packetContext = {
|
|
333
|
+
code_state_check: computeCodeStateCheck(projectRoot, events),
|
|
334
|
+
};
|
|
335
|
+
return {
|
|
336
|
+
job_id: newJobId(change, "verifier"),
|
|
337
|
+
role: "verifier",
|
|
338
|
+
state: "requested",
|
|
339
|
+
gate_id: REVIEW_FINAL_VERIFIER_GATE_ID,
|
|
340
|
+
boundFiles,
|
|
341
|
+
review_evidence_digest: currentEvidenceDigest,
|
|
342
|
+
packet_context: packetContext,
|
|
343
|
+
packet_digest: sha256Text(JSON.stringify({
|
|
344
|
+
role: "verifier",
|
|
345
|
+
gate_id: REVIEW_FINAL_VERIFIER_GATE_ID,
|
|
346
|
+
boundFiles,
|
|
347
|
+
review_evidence_digest: currentEvidenceDigest,
|
|
348
|
+
packet_context: packetContext,
|
|
349
|
+
created_from_transition: "review-ready",
|
|
350
|
+
})),
|
|
351
|
+
created_from_transition: "review-ready",
|
|
352
|
+
created_at: new Date().toISOString(),
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
function evaluateFinalVerifierGate(input) {
|
|
356
|
+
const verifierOpen = input.snapshot.open_jobs.find(isReviewReadyVerifier);
|
|
357
|
+
if (verifierOpen) {
|
|
358
|
+
return {
|
|
359
|
+
blocked: true,
|
|
360
|
+
reason: `状态未推进;已有待完成最终验证工作项 ${verifierOpen.job_id}`,
|
|
361
|
+
jobs: [verifierOpen],
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
const verifierAccepted = input.snapshot.accepted_jobs.find(job => isFreshReviewVerifier(job, input.changeRoot, input.currentEvidenceDigest, input.projectRoot, input.events));
|
|
365
|
+
const finalVerifierRequired = requiresFinalVerifierForCurrentReview(input.events) || input.policy.requires_verifier;
|
|
366
|
+
if (!finalVerifierRequired) {
|
|
367
|
+
if (!input.hasStoredPolicy) {
|
|
368
|
+
return {
|
|
369
|
+
fromState: "review",
|
|
370
|
+
toState: "review",
|
|
371
|
+
outcome: "advanced",
|
|
372
|
+
reason: `补写审查策略=${input.policy.review_risk}`,
|
|
373
|
+
commitPayload: input.policyPayload,
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
return { skip: true, message: "已在 review 状态,审查策略无需 verifier" };
|
|
198
377
|
}
|
|
199
|
-
|
|
378
|
+
if (!verifierAccepted) {
|
|
379
|
+
const previousVerifierRejected = hasRejectedReviewReadyVerifier(input.events);
|
|
380
|
+
const job = createFinalVerifierJob(input.change, input.projectRoot, input.changeRoot, input.events, input.currentEvidenceDigest);
|
|
381
|
+
return {
|
|
382
|
+
fromState: input.snapshot.state,
|
|
383
|
+
toState: input.snapshot.state,
|
|
384
|
+
outcome: "job_created",
|
|
385
|
+
newJobs: [job],
|
|
386
|
+
reason: previousVerifierRejected
|
|
387
|
+
? "此前最终验证未通过;请先根据验证报告修改任务或文档,确认无需修改时再执行新的最终验证工作项"
|
|
388
|
+
: "创建最终验证工作项",
|
|
389
|
+
commitPayload: input.policyPayload,
|
|
390
|
+
...(previousVerifierRejected ? {
|
|
391
|
+
details: { advisory: "此前最终验证未通过;请先根据验证报告修改任务或文档,确认无需修改时再执行新的最终验证工作项" },
|
|
392
|
+
} : {}),
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
if (!input.hasStoredPolicy) {
|
|
396
|
+
return {
|
|
397
|
+
fromState: "review",
|
|
398
|
+
toState: "review",
|
|
399
|
+
outcome: "advanced",
|
|
400
|
+
reason: `补写审查策略=${input.policy.review_risk}`,
|
|
401
|
+
commitPayload: input.policyPayload,
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
return { skip: true, message: "已在 review 状态,最终验证仍然有效" };
|
|
200
405
|
}
|
|
201
|
-
function
|
|
202
|
-
|
|
203
|
-
|
|
406
|
+
function transitionPlanToDecision(snapshot, changeRoot, change, plan) {
|
|
407
|
+
switch (plan.kind) {
|
|
408
|
+
case "skip":
|
|
409
|
+
return { skip: true, message: plan.message };
|
|
410
|
+
case "blocked":
|
|
411
|
+
return { blocked: true, reason: plan.reason, jobs: plan.jobs };
|
|
412
|
+
case "create_gate_jobs":
|
|
413
|
+
return createReviewJobsForGate(snapshot.state, plan.gate, plan.roles, changeRoot, change, plan.reason);
|
|
414
|
+
case "advance":
|
|
415
|
+
return {
|
|
416
|
+
fromState: plan.fromState,
|
|
417
|
+
toState: plan.toState,
|
|
418
|
+
outcome: "advanced",
|
|
419
|
+
reason: plan.reason,
|
|
420
|
+
commitPayload: plan.payload,
|
|
421
|
+
};
|
|
422
|
+
}
|
|
204
423
|
}
|
|
205
424
|
/**
|
|
206
425
|
* 统一 transition 提交协议——所有校验在锁内。
|
|
@@ -292,32 +511,16 @@ export function proposeReady(projectRoot, change, changeRoot, risk = "strict") {
|
|
|
292
511
|
return commitTransition(projectRoot, change, changeRoot, {
|
|
293
512
|
name: "propose-ready", idempotencyInputs: { risk },
|
|
294
513
|
decide: (snapshot) => {
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
const files = openQuestions.files.map(f => `${f.path}(${f.openCount})`).join(", ");
|
|
306
|
-
return { skip: true, message: `计划文档有 ${openQuestions.openCount} 个待用户确认问题:${files}` };
|
|
307
|
-
}
|
|
308
|
-
if (risk !== "minimal") {
|
|
309
|
-
const artifactsDir = join(changeRoot, ".superspec", "artifacts");
|
|
310
|
-
for (const doc of ["discovery.md", "business-invariants.md", "test-contract.md"]) {
|
|
311
|
-
if (!existsSync(join(artifactsDir, doc)))
|
|
312
|
-
return { skip: true, message: `基础职责缺失:${doc} 不存在(risk=${risk} 需要)` };
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
// 通用 job 检查(用提取的 helper)
|
|
316
|
-
const requiredRoles = TRANSITION_REQUIREMENTS["propose-ready"]?.[risk] ?? [];
|
|
317
|
-
const reviewResult = checkOrCreateReviewJobs(snapshot, requiredRoles, changeRoot, change, "propose-ready", ["proposal.md", "tasks.md", "design.md", ".superspec/artifacts/discovery.md", ".superspec/artifacts/business-invariants.md", ".superspec/artifacts/test-contract.md"]);
|
|
318
|
-
if (reviewResult)
|
|
319
|
-
return reviewResult;
|
|
320
|
-
return { fromState: "propose", toState: "propose_ready", outcome: "advanced", reason: `risk=${risk},所有需求已满足` };
|
|
514
|
+
const events = readEvents(projectRoot, change);
|
|
515
|
+
const plan = planTransition("propose-ready", {
|
|
516
|
+
projectRoot,
|
|
517
|
+
change,
|
|
518
|
+
changeRoot,
|
|
519
|
+
events,
|
|
520
|
+
snapshot,
|
|
521
|
+
mode: { kind: "risk", risk },
|
|
522
|
+
});
|
|
523
|
+
return transitionPlanToDecision(snapshot, changeRoot, change, plan);
|
|
321
524
|
},
|
|
322
525
|
});
|
|
323
526
|
}
|
|
@@ -338,24 +541,16 @@ export function transitionExplore(projectRoot, change, changeRoot, risk = "stric
|
|
|
338
541
|
return commitTransition(projectRoot, change, changeRoot, {
|
|
339
542
|
name: "explore", idempotencyInputs: { phase: "explore", risk },
|
|
340
543
|
decide: (snapshot) => {
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
// 通用 job 审查(和 propose-ready 同一个 helper)
|
|
352
|
-
const requiredRoles = TRANSITION_REQUIREMENTS["explore"]?.[risk] ?? [];
|
|
353
|
-
const reviewResult = checkOrCreateReviewJobs(snapshot, requiredRoles, changeRoot, change, "explore", [".superspec/artifacts/discovery.md"]);
|
|
354
|
-
if (reviewResult)
|
|
355
|
-
return reviewResult;
|
|
356
|
-
return { fromState: "explore", toState: "propose", outcome: "advanced", reason: "探索完成" };
|
|
357
|
-
}
|
|
358
|
-
return { skip: true, message: `当前状态 ${snapshot.state},explore 不适用` };
|
|
544
|
+
const events = readEvents(projectRoot, change);
|
|
545
|
+
const plan = planTransition("explore", {
|
|
546
|
+
projectRoot,
|
|
547
|
+
change,
|
|
548
|
+
changeRoot,
|
|
549
|
+
events,
|
|
550
|
+
snapshot,
|
|
551
|
+
mode: { kind: "risk", risk },
|
|
552
|
+
});
|
|
553
|
+
return transitionPlanToDecision(snapshot, changeRoot, change, plan);
|
|
359
554
|
},
|
|
360
555
|
});
|
|
361
556
|
}
|
|
@@ -364,24 +559,16 @@ export function startApply(projectRoot, change, changeRoot) {
|
|
|
364
559
|
return commitTransition(projectRoot, change, changeRoot, {
|
|
365
560
|
name: "start-apply", idempotencyInputs: { phase: "start-apply" },
|
|
366
561
|
decide: (snapshot) => {
|
|
367
|
-
if (snapshot.state !== "propose_ready")
|
|
368
|
-
return { skip: true, message: `当前状态 ${snapshot.state},需要 propose_ready` };
|
|
369
562
|
const events = readEvents(projectRoot, change);
|
|
370
|
-
const
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
...reviewResult,
|
|
380
|
-
reason: `进入执行阶段前需要重新完成计划文档审查:${reviewResult.reason}`,
|
|
381
|
-
};
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
return { fromState: "propose_ready", toState: "apply", outcome: "advanced", reason: "进入执行阶段" };
|
|
563
|
+
const plan = planTransition("start-apply", {
|
|
564
|
+
projectRoot,
|
|
565
|
+
change,
|
|
566
|
+
changeRoot,
|
|
567
|
+
events,
|
|
568
|
+
snapshot,
|
|
569
|
+
mode: { kind: "risk", risk: "strict" },
|
|
570
|
+
});
|
|
571
|
+
return transitionPlanToDecision(snapshot, changeRoot, change, plan);
|
|
385
572
|
},
|
|
386
573
|
});
|
|
387
574
|
}
|
|
@@ -393,31 +580,64 @@ export function taskStart(projectRoot, change, changeRoot, taskId) {
|
|
|
393
580
|
decide: (snapshot) => {
|
|
394
581
|
if (snapshot.state !== "apply")
|
|
395
582
|
return { skip: true, message: `当前状态 ${snapshot.state},需要 apply` };
|
|
583
|
+
const events = readEvents(projectRoot, change);
|
|
396
584
|
const tasksContent = readFileSync(join(changeRoot, "tasks.md"), "utf8");
|
|
397
585
|
const lines = tasksContent.split("\n");
|
|
398
586
|
const taskLineIdx = findTaskLine(lines, taskId);
|
|
399
587
|
if (taskLineIdx < 0)
|
|
400
588
|
return { skip: true, message: `任务 ${taskId} 不存在` };
|
|
401
|
-
|
|
589
|
+
const contractMode = applyRequirementModeForCurrentRound(events);
|
|
590
|
+
if (contractMode && pendingTaskStatusForApply(changeRoot, events).completedByEvent.includes(taskId)) {
|
|
591
|
+
return { skip: true, message: `任务 ${taskId} 已通过完成事件完成` };
|
|
592
|
+
}
|
|
593
|
+
if (lines[taskLineIdx].match(/- \[[xX]\]/))
|
|
402
594
|
return { skip: true, message: `任务 ${taskId} 已完成` };
|
|
595
|
+
const taskInfo = parseTasksMd(tasksContent).find(task => task.taskId === taskId);
|
|
596
|
+
if (!taskInfo)
|
|
597
|
+
return { skip: true, message: `任务 ${taskId} 不存在` };
|
|
403
598
|
const existing = snapshot.active_task_attempts?.find(a => a.task_id === taskId && a.state === "active");
|
|
404
599
|
if (existing)
|
|
405
600
|
return { skip: true, message: `任务 ${taskId} 已有活跃尝试` };
|
|
601
|
+
// 统一出口:adopted.contract 非 null 当且仅当契约模式下有绑定块;
|
|
602
|
+
// legacy 轮即使 task 带执行依据文本也输出 null,避免"看似契约、实按 legacy 校验"的误导态
|
|
603
|
+
const adopted = adoptedContractForTask(tasksContent, taskId, contractMode);
|
|
604
|
+
if (contractMode && taskInfo.tddRequired && !isReviewFixTaskId(taskId) && !adopted.parsed) {
|
|
605
|
+
return { skip: true, message: `执行依据模式下,普通 TDD 任务 ${taskId} 缺少执行依据` };
|
|
606
|
+
}
|
|
607
|
+
if (contractMode) {
|
|
608
|
+
const contractError = validateTaskStartContract(changeRoot, taskId, taskInfo.tddRequired, adopted.parsed);
|
|
609
|
+
if (contractError)
|
|
610
|
+
return { skip: true, message: contractError };
|
|
611
|
+
}
|
|
406
612
|
const structureDigest = sha256Text(tasksContent.replace(/- \[[xX]\]/g, "- [ ]"));
|
|
407
613
|
const attempt = {
|
|
408
614
|
attempt_id: `ATT-${taskId}-${Date.now()}-${++attemptSeq}`,
|
|
409
615
|
task_id: taskId, state: "active",
|
|
410
616
|
task_structure_digest: structureDigest,
|
|
617
|
+
contract_mode: contractMode,
|
|
618
|
+
contract: adopted.contract,
|
|
619
|
+
tdd_required: taskInfo.tddRequired,
|
|
620
|
+
no_tdd_reason: taskInfo.noTddReason,
|
|
411
621
|
declared_write_scope: [], pre_edit_source_fingerprint: null,
|
|
412
622
|
pre_edit_red_ref: null, executor_packet_digest: null,
|
|
413
623
|
executor_result_ref: null, post_edit_green_ref: null,
|
|
414
624
|
created_at: new Date().toISOString(),
|
|
415
625
|
};
|
|
626
|
+
const eventPayload = {
|
|
627
|
+
...attempt,
|
|
628
|
+
...boundarySnapshotPayload(projectRoot),
|
|
629
|
+
};
|
|
416
630
|
return {
|
|
417
631
|
fromState: "apply", toState: "apply", outcome: "advanced",
|
|
418
632
|
reason: `创建任务 ${taskId} 执行尝试`,
|
|
419
|
-
extraEvents: [{ type: "task_started", payload:
|
|
420
|
-
details: {
|
|
633
|
+
extraEvents: [{ type: "task_started", payload: eventPayload }],
|
|
634
|
+
details: {
|
|
635
|
+
attempt_id: attempt.attempt_id,
|
|
636
|
+
task_id: taskId,
|
|
637
|
+
contract: adopted.contract,
|
|
638
|
+
// legacy 轮统一标注历史模式(无论有无执行依据文本);契约轮无块时标 false(如 REVIEW-FIX)
|
|
639
|
+
...(adopted.contract ? {} : { legacy_contract: !contractMode }),
|
|
640
|
+
},
|
|
421
641
|
};
|
|
422
642
|
},
|
|
423
643
|
});
|
|
@@ -458,7 +678,7 @@ export function reopen(projectRoot, change, changeRoot, to, reason, opts = {}) {
|
|
|
458
678
|
source_job_id: ref.jobId,
|
|
459
679
|
finding_id: ref.findingId,
|
|
460
680
|
decision_scope: scope,
|
|
461
|
-
baseline_docs:
|
|
681
|
+
baseline_docs: proposalDocsBaseline(changeRoot),
|
|
462
682
|
},
|
|
463
683
|
};
|
|
464
684
|
}
|
|
@@ -503,7 +723,7 @@ export function reopen(projectRoot, change, changeRoot, to, reason, opts = {}) {
|
|
|
503
723
|
if (snapshot.state !== "apply_done" && snapshot.state !== "review") {
|
|
504
724
|
return { skip: true, message: `当前状态 ${snapshot.state},不能 reopen 到 apply` };
|
|
505
725
|
}
|
|
506
|
-
const pending =
|
|
726
|
+
const pending = pendingTaskStatusForApply(changeRoot, events).pending;
|
|
507
727
|
if (pending.length === 0)
|
|
508
728
|
return { skip: true, message: "没有未完成任务,不能 reopen 到 apply" };
|
|
509
729
|
return {
|
|
@@ -526,9 +746,18 @@ export function reviewReady(projectRoot, change, changeRoot, risk = "strict") {
|
|
|
526
746
|
const policyPayload = storedPolicy ? {} : { review_policy: policy };
|
|
527
747
|
const currentEvidenceDigest = reviewEvidenceDigest(events);
|
|
528
748
|
// 检查是否所有任务已完成
|
|
529
|
-
const pending =
|
|
749
|
+
const pending = pendingTaskStatusForApply(changeRoot, events).pending;
|
|
530
750
|
if (pending.length > 0)
|
|
531
751
|
return { skip: true, message: formatPendingTaskMessage(pending, "请先通过 next/reopen 继续执行") };
|
|
752
|
+
if (applyRequirementModeForCurrentRound(events)) {
|
|
753
|
+
const missingExemptions = missingCoverageExemptionTestIds(changeRoot, events);
|
|
754
|
+
if (missingExemptions.length > 0) {
|
|
755
|
+
return {
|
|
756
|
+
skip: true,
|
|
757
|
+
message: `test-contract 中存在未绑定任务且缺少覆盖豁免的 TEST:${missingExemptions.join(", ")};请先向用户确认原因(不要代替用户决策),再执行 superspec record user-decision --change "${change}" --input - 登记,stdin 传入 JSON:{"scope":"test_coverage_exemption:${missingExemptions[0]}","question":"<向用户提出的问题>","answer":"<用户给出的豁免原因>"}`,
|
|
758
|
+
};
|
|
759
|
+
}
|
|
760
|
+
}
|
|
532
761
|
// 如果当前是 apply,先推进到 apply_done
|
|
533
762
|
if (snapshot.state === "apply") {
|
|
534
763
|
return {
|
|
@@ -538,111 +767,26 @@ export function reviewReady(projectRoot, change, changeRoot, risk = "strict") {
|
|
|
538
767
|
};
|
|
539
768
|
}
|
|
540
769
|
if (snapshot.state === "apply_done") {
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
reason: `状态未推进;已有待完成代码审查工作项 ${freshOpenJobs[0].job_id}`,
|
|
549
|
-
jobs: [freshOpenJobs[0]],
|
|
550
|
-
};
|
|
551
|
-
}
|
|
552
|
-
const latest = facts.latestTerminal;
|
|
553
|
-
if (latest?.state === "accepted") {
|
|
554
|
-
return {
|
|
555
|
-
fromState: "apply_done", toState: "review", outcome: "advanced",
|
|
556
|
-
reason: "代码审查已通过,进入最终审查阶段",
|
|
557
|
-
commitPayload: {
|
|
558
|
-
...policyPayload,
|
|
559
|
-
code_review_gate: { decision: "passed", job_id: latest.job.job_id },
|
|
560
|
-
},
|
|
561
|
-
};
|
|
562
|
-
}
|
|
563
|
-
if (latest?.state === "rejected" && latest.result_kind === "review_failed") {
|
|
564
|
-
const reviewFailedStatus = latestCodeReviewFailedStatus(events);
|
|
565
|
-
if (reviewFailedStatus && reviewFailedStatus.findings.length > 0 && reviewFailedStatus.unresolved.length === 0) {
|
|
566
|
-
const { job, scanReason } = createCodeReviewerJob(change, projectRoot, events);
|
|
567
|
-
return {
|
|
568
|
-
fromState: "apply_done", toState: "apply_done", outcome: "job_created",
|
|
569
|
-
newJobs: [job],
|
|
570
|
-
reason: `重新创建代码审查工作项;${scanReason};上一次阻塞问题已被主流程复核驳回`,
|
|
571
|
-
};
|
|
572
|
-
}
|
|
573
|
-
return {
|
|
574
|
-
skip: true,
|
|
575
|
-
message: "代码审查发现需要处理的问题,请先执行 next,根据提示回到实现阶段修复或让使用者决定是否回到计划阶段",
|
|
576
|
-
};
|
|
577
|
-
}
|
|
578
|
-
const { job, scanReason } = createCodeReviewerJob(change, projectRoot, events);
|
|
579
|
-
return {
|
|
580
|
-
fromState: "apply_done", toState: "apply_done", outcome: "job_created",
|
|
581
|
-
newJobs: [job],
|
|
582
|
-
reason: latest?.state === "rejected"
|
|
583
|
-
? `重新创建代码审查工作项;上一次报告未被接受,原因:${latest.reason ?? "报告不符合要求"}`
|
|
584
|
-
: `创建代码审查工作项;${scanReason}`,
|
|
585
|
-
};
|
|
586
|
-
}
|
|
587
|
-
return {
|
|
588
|
-
fromState: "apply_done", toState: "review", outcome: "advanced",
|
|
589
|
-
reason: "没有代码类改动,直接进入最终审查阶段",
|
|
590
|
-
commitPayload: {
|
|
591
|
-
...policyPayload,
|
|
592
|
-
code_review_gate: { decision: "skipped", reason: "no_code_changes" },
|
|
593
|
-
},
|
|
594
|
-
};
|
|
770
|
+
return evaluateApplyDoneCodeReviewGate({
|
|
771
|
+
events,
|
|
772
|
+
projectRoot,
|
|
773
|
+
changeRoot,
|
|
774
|
+
change,
|
|
775
|
+
policyPayload,
|
|
776
|
+
});
|
|
595
777
|
}
|
|
596
778
|
if (snapshot.state === "review") {
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
};
|
|
609
|
-
}
|
|
610
|
-
return { skip: true, message: "已在 review 状态,审查策略无需 verifier" };
|
|
611
|
-
}
|
|
612
|
-
if (!verifierAccepted) {
|
|
613
|
-
const boundFiles = reviewBoundFiles(changeRoot);
|
|
614
|
-
const previousVerifierRejected = hasRejectedReviewReadyVerifier(events);
|
|
615
|
-
const job = {
|
|
616
|
-
job_id: newJobId(change, "verifier"), role: "verifier", state: "requested",
|
|
617
|
-
boundFiles, review_evidence_digest: currentEvidenceDigest,
|
|
618
|
-
packet_digest: sha256Text(JSON.stringify({
|
|
619
|
-
role: "verifier",
|
|
620
|
-
boundFiles,
|
|
621
|
-
review_evidence_digest: currentEvidenceDigest,
|
|
622
|
-
created_from_transition: "review-ready",
|
|
623
|
-
})),
|
|
624
|
-
created_from_transition: "review-ready", created_at: new Date().toISOString(),
|
|
625
|
-
};
|
|
626
|
-
return {
|
|
627
|
-
fromState: snapshot.state, toState: snapshot.state, outcome: "job_created",
|
|
628
|
-
newJobs: [job],
|
|
629
|
-
reason: previousVerifierRejected
|
|
630
|
-
? "此前最终验证未通过;请先根据验证报告修改任务或文档,确认无需修改时再执行新的最终验证工作项"
|
|
631
|
-
: "创建最终验证工作项",
|
|
632
|
-
commitPayload: policyPayload,
|
|
633
|
-
...(previousVerifierRejected ? {
|
|
634
|
-
details: { advisory: "此前最终验证未通过;请先根据验证报告修改任务或文档,确认无需修改时再执行新的最终验证工作项" },
|
|
635
|
-
} : {}),
|
|
636
|
-
};
|
|
637
|
-
}
|
|
638
|
-
if (!storedPolicy) {
|
|
639
|
-
return {
|
|
640
|
-
fromState: "review", toState: "review", outcome: "advanced",
|
|
641
|
-
reason: `补写审查策略=${policy.review_risk}`,
|
|
642
|
-
commitPayload: policyPayload,
|
|
643
|
-
};
|
|
644
|
-
}
|
|
645
|
-
return { skip: true, message: "已在 review 状态,最终验证仍然有效" };
|
|
779
|
+
return evaluateFinalVerifierGate({
|
|
780
|
+
snapshot,
|
|
781
|
+
events,
|
|
782
|
+
change,
|
|
783
|
+
projectRoot,
|
|
784
|
+
changeRoot,
|
|
785
|
+
currentEvidenceDigest,
|
|
786
|
+
policy,
|
|
787
|
+
policyPayload,
|
|
788
|
+
hasStoredPolicy: Boolean(storedPolicy),
|
|
789
|
+
});
|
|
646
790
|
}
|
|
647
791
|
return { skip: true, message: `当前状态 ${snapshot.state},review-ready 不适用` };
|
|
648
792
|
},
|
|
@@ -653,22 +797,16 @@ export function accept(projectRoot, change, changeRoot) {
|
|
|
653
797
|
return commitTransition(projectRoot, change, changeRoot, {
|
|
654
798
|
name: "accept", idempotencyInputs: { phase: "accept" },
|
|
655
799
|
decide: (snapshot) => {
|
|
656
|
-
if (snapshot.state !== "review")
|
|
657
|
-
return { skip: true, message: `当前状态 ${snapshot.state},需要 review` };
|
|
658
|
-
const pending = pendingTaskIds(changeRoot);
|
|
659
|
-
if (pending.length > 0)
|
|
660
|
-
return { skip: true, message: formatPendingTaskMessage(pending, "请先 reopen --to apply 继续执行") };
|
|
661
800
|
const events = readEvents(projectRoot, change);
|
|
662
|
-
const
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
return { fromState: "review", toState: "accepted", outcome: "advanced", reason: "审查通过" };
|
|
801
|
+
const plan = planTransition("accept", {
|
|
802
|
+
projectRoot,
|
|
803
|
+
change,
|
|
804
|
+
changeRoot,
|
|
805
|
+
events,
|
|
806
|
+
snapshot,
|
|
807
|
+
mode: { kind: "risk", risk: "strict" },
|
|
808
|
+
});
|
|
809
|
+
return transitionPlanToDecision(snapshot, changeRoot, change, plan);
|
|
672
810
|
},
|
|
673
811
|
});
|
|
674
812
|
}
|
|
@@ -685,13 +823,10 @@ export function archive(projectRoot, change, changeRoot) {
|
|
|
685
823
|
for (const p of docPaths) {
|
|
686
824
|
manifest[p] = sha256File(join(changeRoot, p)) ?? "sha256:missing";
|
|
687
825
|
}
|
|
688
|
-
// specs/
|
|
826
|
+
// specs/ 目录:递归收录 .md(覆盖 specs/<capability>/spec.md 布局)
|
|
689
827
|
const specsDir = join(changeRoot, "specs");
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
if (f.endsWith(".md"))
|
|
693
|
-
manifest[`specs/${f}`] = sha256File(join(specsDir, f)) ?? "sha256:missing";
|
|
694
|
-
}
|
|
828
|
+
for (const rel of listMarkdownFiles(specsDir)) {
|
|
829
|
+
manifest[`specs/${rel}`] = sha256File(join(specsDir, rel)) ?? "sha256:missing";
|
|
695
830
|
}
|
|
696
831
|
return {
|
|
697
832
|
fromState: "accepted", toState: "archive", outcome: "advanced",
|
|
@@ -702,73 +837,50 @@ export function archive(projectRoot, change, changeRoot) {
|
|
|
702
837
|
});
|
|
703
838
|
}
|
|
704
839
|
// ===== task-complete =====
|
|
705
|
-
export function taskComplete(projectRoot, change, changeRoot, taskId) {
|
|
840
|
+
export function taskComplete(projectRoot, change, changeRoot, taskId, inputContent = null) {
|
|
841
|
+
const scopeInput = parseScopeNoteInput(inputContent);
|
|
706
842
|
return commitTransition(projectRoot, change, changeRoot, {
|
|
707
|
-
name: "task-complete", idempotencyInputs: { task: taskId, phase: "complete" },
|
|
843
|
+
name: "task-complete", idempotencyInputs: { task: taskId, phase: "complete", scope_note_digest: scopeInput.digest ?? "" },
|
|
708
844
|
decide: (snapshot) => {
|
|
845
|
+
if (!scopeInput.ok)
|
|
846
|
+
return { skip: true, message: scopeInput.message };
|
|
709
847
|
if (snapshot.state !== "apply")
|
|
710
848
|
return { skip: true, message: `当前状态 ${snapshot.state},需要 apply` };
|
|
711
849
|
const attempt = snapshot.active_task_attempts?.find(a => a.task_id === taskId && a.state === "active");
|
|
712
850
|
if (!attempt)
|
|
713
851
|
return { skip: true, message: `任务 ${taskId} 无活跃执行尝试` };
|
|
714
|
-
const
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
const events = readEvents(projectRoot, change);
|
|
725
|
-
let hasRed = false, hasGreen = false;
|
|
726
|
-
for (const ev of events) {
|
|
727
|
-
if (ev.event_type === "test_run_recorded") {
|
|
728
|
-
const tr = ev.payload;
|
|
729
|
-
// 匹配当前 attempt(优先按 attempt_id,回退到 digest)
|
|
730
|
-
const matches = tr.attempt_id === attempt.attempt_id ||
|
|
731
|
-
(!tr.attempt_id && tr.task_structure_digest === attempt.task_structure_digest);
|
|
732
|
-
if (matches) {
|
|
733
|
-
if (tr.semantic_status === "expected_failure" || tr.semantic_status === "characterization_pass")
|
|
734
|
-
hasRed = true;
|
|
735
|
-
if (tr.semantic_status === "expected_success")
|
|
736
|
-
hasGreen = true;
|
|
737
|
-
}
|
|
738
|
-
}
|
|
739
|
-
}
|
|
740
|
-
if (isTdd) {
|
|
741
|
-
if (!hasRed)
|
|
742
|
-
return { skip: true, message: `TDD 任务 ${taskId} 缺少 RED 证据` };
|
|
743
|
-
if (!hasGreen)
|
|
744
|
-
return { skip: true, message: `TDD 任务 ${taskId} 缺少 GREEN 证据` };
|
|
745
|
-
}
|
|
746
|
-
else {
|
|
747
|
-
if (!hasNoTddReason)
|
|
748
|
-
return { skip: true, message: `非 TDD 任务 ${taskId} 缺少 no_tdd_reason` };
|
|
749
|
-
}
|
|
750
|
-
// B1 修复:checkbox 写入移到 postCommit(commit 事件写入后执行)
|
|
852
|
+
const readiness = taskEvidenceReadiness(projectRoot, change, changeRoot, attempt);
|
|
853
|
+
if (!readiness.ready)
|
|
854
|
+
return { skip: true, message: `任务 ${taskId} 无法完成:${readiness.reason}` };
|
|
855
|
+
const completedPayload = {
|
|
856
|
+
task_id: taskId,
|
|
857
|
+
attempt_id: attempt.attempt_id,
|
|
858
|
+
...boundarySnapshotPayload(projectRoot),
|
|
859
|
+
checkbox_update: { status: "pending" },
|
|
860
|
+
...(scopeInput.value ? { scope_note: scopeInput.value } : {}),
|
|
861
|
+
};
|
|
751
862
|
return {
|
|
752
863
|
fromState: "apply", toState: "apply", outcome: "advanced",
|
|
753
864
|
reason: `任务 ${taskId} 完成`,
|
|
754
|
-
extraEvents: [{ type: "task_completed", payload:
|
|
865
|
+
extraEvents: [{ type: "task_completed", payload: completedPayload }],
|
|
755
866
|
postCommit: (_pr, _ch, cr) => {
|
|
756
867
|
const lines = readFileSync(join(cr, "tasks.md"), "utf8").split("\n");
|
|
757
868
|
const idx = findTaskLine(lines, taskId);
|
|
758
|
-
if (idx < 0
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
869
|
+
if (idx < 0) {
|
|
870
|
+
completedPayload.checkbox_update = { status: "failed", reason: `找不到 ${taskId} 的任务行` };
|
|
871
|
+
return;
|
|
872
|
+
}
|
|
873
|
+
if (lines[idx].match(/- \[[xX]\]/)) {
|
|
874
|
+
completedPayload.checkbox_update = { status: "applied" };
|
|
875
|
+
return;
|
|
876
|
+
}
|
|
877
|
+
if (!lines[idx].match(/- \[ \]/)) {
|
|
878
|
+
completedPayload.checkbox_update = { status: "failed", reason: `找不到 ${taskId} 的未完成复选框` };
|
|
879
|
+
return;
|
|
880
|
+
}
|
|
762
881
|
lines[idx] = lines[idx].replace(/- \[ \]/, "- [x]");
|
|
763
882
|
writeFileSync(join(cr, "tasks.md"), lines.join("\n"));
|
|
764
|
-
|
|
765
|
-
const afterLines = readFileSync(join(cr, "tasks.md"), "utf8").split("\n");
|
|
766
|
-
const afterDigest = sha256Text(afterLines.join("\n").replace(/- \[[xX]\]/g, "- [ ]"));
|
|
767
|
-
if (afterDigest !== beforeDigest) {
|
|
768
|
-
afterLines[idx] = afterLines[idx].replace(/- \[x\]/, "- [ ]");
|
|
769
|
-
writeFileSync(join(cr, "tasks.md"), afterLines.join("\n"));
|
|
770
|
-
throw new Error(`复选框补丁导致结构变化:${taskId}`);
|
|
771
|
-
}
|
|
883
|
+
completedPayload.checkbox_update = { status: "applied" };
|
|
772
884
|
},
|
|
773
885
|
};
|
|
774
886
|
},
|