@peterxiaoyang/superspec 0.1.38 → 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 +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/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,82 +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 { codeReviewBoundFiles, codeReviewDecisionScope, codeReviewJobStaleReason, codeReviewPacketDigest, collectCodeReviewGateFacts, dismissedCodeReviewSummary, latestCodeReviewDecision, latestCodeReviewFailedStatus, requiresFinalVerifierForCurrentReview,
|
|
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
10
|
import { taskEvidenceReadiness } from "./task_evidence.js";
|
|
11
|
-
import {
|
|
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";
|
|
12
14
|
let transitionSeq = 0;
|
|
13
15
|
function newTransitionId() { return `T-${Date.now()}-${++transitionSeq}`; }
|
|
14
16
|
let jobSeq = 0;
|
|
15
17
|
function newJobId(change, role) { return `JOB-${change.slice(0, 8)}-${role.slice(0, 4)}-${Date.now()}-${++jobSeq}`; }
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
staleRoles.push({ role, reason: `需求 ${role} 无已接受的工作项` });
|
|
30
|
-
}
|
|
31
|
-
else {
|
|
32
|
-
for (const bf of fresh.boundFiles) {
|
|
33
|
-
const currentSha = sha256File(join(changeRoot, bf.path)) ?? "sha256:missing";
|
|
34
|
-
if (currentSha !== bf.sha) {
|
|
35
|
-
staleRoles.push({ role, reason: `${role} 绑定文件 ${bf.path} 已变化` });
|
|
36
|
-
break;
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
if (staleRoles.length > 0) {
|
|
42
|
-
const newJobs = staleRoles.map(({ role }) => {
|
|
43
|
-
const boundFiles = gate.reviewedDocPaths.filter(p => existsSync(join(changeRoot, p))).map(p => ({ path: p, sha: sha256File(join(changeRoot, p)) ?? "sha256:missing" }));
|
|
44
|
-
return {
|
|
45
|
-
job_id: newJobId(change, role),
|
|
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));
|
|
24
|
+
return {
|
|
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({
|
|
46
31
|
role,
|
|
47
|
-
state: "requested",
|
|
48
32
|
gate_id: gate.gate_id,
|
|
49
33
|
boundFiles,
|
|
50
|
-
packet_digest: sha256Text(JSON.stringify({
|
|
51
|
-
role,
|
|
52
|
-
gate_id: gate.gate_id,
|
|
53
|
-
boundFiles,
|
|
54
|
-
created_from_transition: gate.created_from_transition,
|
|
55
|
-
})),
|
|
56
34
|
created_from_transition: gate.created_from_transition,
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
return {
|
|
61
|
-
fromState: snapshot.state, toState: snapshot.state, outcome: "job_created",
|
|
62
|
-
newJobs, reason: staleRoles.map(s => s.reason).join("; "),
|
|
35
|
+
})),
|
|
36
|
+
created_from_transition: gate.created_from_transition,
|
|
37
|
+
created_at: new Date().toISOString(),
|
|
63
38
|
};
|
|
64
|
-
}
|
|
65
|
-
return
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
const newJobs = ev.payload.new_jobs ?? [];
|
|
73
|
-
for (const job of newJobs) {
|
|
74
|
-
if (PROPOSE_FINAL_REVIEW_GATE.isJobForGate(job) &&
|
|
75
|
-
(job.role === "critic" || job.role === "architect" || job.role === "test-engineer"))
|
|
76
|
-
roles.add(job.role);
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
return [...roles];
|
|
39
|
+
});
|
|
40
|
+
return {
|
|
41
|
+
fromState: state,
|
|
42
|
+
toState: state,
|
|
43
|
+
outcome: "job_created",
|
|
44
|
+
newJobs,
|
|
45
|
+
reason,
|
|
46
|
+
};
|
|
80
47
|
}
|
|
81
48
|
/**
|
|
82
49
|
* 已迁移到 format.ts:findTaskInLines / parseTasksMd / tasksStructureDigest
|
|
@@ -85,12 +52,94 @@ function historicalProposeReadyRoles(projectRoot, change) {
|
|
|
85
52
|
function findTaskLine(lines, taskId) {
|
|
86
53
|
return findTaskInLines(lines, taskId);
|
|
87
54
|
}
|
|
88
|
-
function
|
|
89
|
-
const
|
|
90
|
-
|
|
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
|
+
};
|
|
91
67
|
}
|
|
92
|
-
function
|
|
93
|
-
|
|
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
|
+
};
|
|
74
|
+
}
|
|
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;
|
|
94
143
|
}
|
|
95
144
|
function hasRejectedReviewReadyVerifier(events) {
|
|
96
145
|
const reviewReadyVerifierIds = new Set();
|
|
@@ -106,9 +155,10 @@ function hasRejectedReviewReadyVerifier(events) {
|
|
|
106
155
|
return events.some(ev => ev.event_type === "job_rejected" &&
|
|
107
156
|
reviewReadyVerifierIds.has(ev.payload.job_id ?? ""));
|
|
108
157
|
}
|
|
109
|
-
function createCodeReviewerJob(change, projectRoot, events) {
|
|
110
|
-
const scan =
|
|
158
|
+
function createCodeReviewerJob(change, projectRoot, changeRoot, events) {
|
|
159
|
+
const scan = scanCodeChangesForReview(projectRoot, events);
|
|
111
160
|
const boundFiles = codeReviewBoundFiles(projectRoot, scan.paths);
|
|
161
|
+
const packetContext = codeReviewPacketContext(changeRoot, projectRoot, scan.scope, events);
|
|
112
162
|
const facts = collectCodeReviewGateFacts(events);
|
|
113
163
|
const latestRejected = facts.latestRejected;
|
|
114
164
|
const reviewFailedStatus = latestCodeReviewFailedStatus(events);
|
|
@@ -126,6 +176,7 @@ function createCodeReviewerJob(change, projectRoot, events) {
|
|
|
126
176
|
gate_id: REVIEW_CODE_REVIEW_GATE_ID,
|
|
127
177
|
boundFiles,
|
|
128
178
|
checkedDocs: REVIEW_DOC_PATHS,
|
|
179
|
+
packet_context: packetContext,
|
|
129
180
|
created_from_transition: "review-ready",
|
|
130
181
|
...(previousRejection ? { previous_rejection: previousRejection } : {}),
|
|
131
182
|
};
|
|
@@ -137,6 +188,7 @@ function createCodeReviewerJob(change, projectRoot, events) {
|
|
|
137
188
|
state: "requested",
|
|
138
189
|
gate_id: REVIEW_CODE_REVIEW_GATE_ID,
|
|
139
190
|
boundFiles,
|
|
191
|
+
packet_context: packetContext,
|
|
140
192
|
packet_digest: codeReviewPacketDigest(packetInput),
|
|
141
193
|
created_from_transition: "review-ready",
|
|
142
194
|
created_at: new Date().toISOString(),
|
|
@@ -179,14 +231,23 @@ function appendReviewFixTask(changeRoot, ref, finding) {
|
|
|
179
231
|
writeFileSync(tasksPath, `${content}${suffix}${line}\n`);
|
|
180
232
|
return "created";
|
|
181
233
|
}
|
|
182
|
-
function isFreshOpenCodeReviewerJob(job, projectRoot) {
|
|
183
|
-
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() !== "");
|
|
184
244
|
}
|
|
185
245
|
function evaluateApplyDoneCodeReviewGate(input) {
|
|
186
|
-
const scan =
|
|
246
|
+
const scan = scanCodeChangesForReview(input.projectRoot, input.events);
|
|
187
247
|
const facts = collectCodeReviewGateFacts(input.events);
|
|
188
248
|
if (scan.hasCodeChanges) {
|
|
189
|
-
const
|
|
249
|
+
const currentWorkingPaths = currentCodeReviewWorkingPaths(input.projectRoot, input.events);
|
|
250
|
+
const freshOpenJobs = facts.openJobs.filter(job => isFreshOpenCodeReviewerJob(job, input.projectRoot, currentWorkingPaths));
|
|
190
251
|
if (freshOpenJobs.length > 0) {
|
|
191
252
|
return {
|
|
192
253
|
blocked: true,
|
|
@@ -196,6 +257,17 @@ function evaluateApplyDoneCodeReviewGate(input) {
|
|
|
196
257
|
}
|
|
197
258
|
const latest = facts.latestTerminal;
|
|
198
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
|
+
}
|
|
199
271
|
return {
|
|
200
272
|
fromState: "apply_done",
|
|
201
273
|
toState: "review",
|
|
@@ -203,14 +275,19 @@ function evaluateApplyDoneCodeReviewGate(input) {
|
|
|
203
275
|
reason: "代码审查已通过,进入最终审查阶段",
|
|
204
276
|
commitPayload: {
|
|
205
277
|
...input.policyPayload,
|
|
206
|
-
code_review_gate: {
|
|
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
|
+
},
|
|
207
284
|
},
|
|
208
285
|
};
|
|
209
286
|
}
|
|
210
287
|
if (latest?.state === "rejected" && latest.result_kind === "review_failed") {
|
|
211
288
|
const reviewFailedStatus = latestCodeReviewFailedStatus(input.events);
|
|
212
289
|
if (reviewFailedStatus && reviewFailedStatus.findings.length > 0 && reviewFailedStatus.unresolved.length === 0) {
|
|
213
|
-
const { job, scanReason } = createCodeReviewerJob(input.change, input.projectRoot, input.events);
|
|
290
|
+
const { job, scanReason } = createCodeReviewerJob(input.change, input.projectRoot, input.changeRoot, input.events);
|
|
214
291
|
return {
|
|
215
292
|
fromState: "apply_done",
|
|
216
293
|
toState: "apply_done",
|
|
@@ -224,7 +301,7 @@ function evaluateApplyDoneCodeReviewGate(input) {
|
|
|
224
301
|
message: "代码审查发现需要处理的问题,请先执行 next,根据提示回到实现阶段修复或让使用者决定是否回到计划阶段",
|
|
225
302
|
};
|
|
226
303
|
}
|
|
227
|
-
const { job, scanReason } = createCodeReviewerJob(input.change, input.projectRoot, input.events);
|
|
304
|
+
const { job, scanReason } = createCodeReviewerJob(input.change, input.projectRoot, input.changeRoot, input.events);
|
|
228
305
|
return {
|
|
229
306
|
fromState: "apply_done",
|
|
230
307
|
toState: "apply_done",
|
|
@@ -242,12 +319,19 @@ function evaluateApplyDoneCodeReviewGate(input) {
|
|
|
242
319
|
reason: "没有代码类改动,直接进入最终审查阶段",
|
|
243
320
|
commitPayload: {
|
|
244
321
|
...input.policyPayload,
|
|
245
|
-
code_review_gate: {
|
|
322
|
+
code_review_gate: {
|
|
323
|
+
decision: "skipped",
|
|
324
|
+
reason: "no_code_changes",
|
|
325
|
+
head: scan.scope?.current_head ?? null,
|
|
326
|
+
},
|
|
246
327
|
},
|
|
247
328
|
};
|
|
248
329
|
}
|
|
249
|
-
function createFinalVerifierJob(change, changeRoot, currentEvidenceDigest) {
|
|
330
|
+
function createFinalVerifierJob(change, projectRoot, changeRoot, events, currentEvidenceDigest) {
|
|
250
331
|
const boundFiles = reviewBoundFiles(changeRoot);
|
|
332
|
+
const packetContext = {
|
|
333
|
+
code_state_check: computeCodeStateCheck(projectRoot, events),
|
|
334
|
+
};
|
|
251
335
|
return {
|
|
252
336
|
job_id: newJobId(change, "verifier"),
|
|
253
337
|
role: "verifier",
|
|
@@ -255,11 +339,13 @@ function createFinalVerifierJob(change, changeRoot, currentEvidenceDigest) {
|
|
|
255
339
|
gate_id: REVIEW_FINAL_VERIFIER_GATE_ID,
|
|
256
340
|
boundFiles,
|
|
257
341
|
review_evidence_digest: currentEvidenceDigest,
|
|
342
|
+
packet_context: packetContext,
|
|
258
343
|
packet_digest: sha256Text(JSON.stringify({
|
|
259
344
|
role: "verifier",
|
|
260
345
|
gate_id: REVIEW_FINAL_VERIFIER_GATE_ID,
|
|
261
346
|
boundFiles,
|
|
262
347
|
review_evidence_digest: currentEvidenceDigest,
|
|
348
|
+
packet_context: packetContext,
|
|
263
349
|
created_from_transition: "review-ready",
|
|
264
350
|
})),
|
|
265
351
|
created_from_transition: "review-ready",
|
|
@@ -275,7 +361,7 @@ function evaluateFinalVerifierGate(input) {
|
|
|
275
361
|
jobs: [verifierOpen],
|
|
276
362
|
};
|
|
277
363
|
}
|
|
278
|
-
const verifierAccepted = input.snapshot.accepted_jobs.find(job => isFreshReviewVerifier(job, input.changeRoot, input.currentEvidenceDigest));
|
|
364
|
+
const verifierAccepted = input.snapshot.accepted_jobs.find(job => isFreshReviewVerifier(job, input.changeRoot, input.currentEvidenceDigest, input.projectRoot, input.events));
|
|
279
365
|
const finalVerifierRequired = requiresFinalVerifierForCurrentReview(input.events) || input.policy.requires_verifier;
|
|
280
366
|
if (!finalVerifierRequired) {
|
|
281
367
|
if (!input.hasStoredPolicy) {
|
|
@@ -291,7 +377,7 @@ function evaluateFinalVerifierGate(input) {
|
|
|
291
377
|
}
|
|
292
378
|
if (!verifierAccepted) {
|
|
293
379
|
const previousVerifierRejected = hasRejectedReviewReadyVerifier(input.events);
|
|
294
|
-
const job = createFinalVerifierJob(input.change, input.changeRoot, input.currentEvidenceDigest);
|
|
380
|
+
const job = createFinalVerifierJob(input.change, input.projectRoot, input.changeRoot, input.events, input.currentEvidenceDigest);
|
|
295
381
|
return {
|
|
296
382
|
fromState: input.snapshot.state,
|
|
297
383
|
toState: input.snapshot.state,
|
|
@@ -317,31 +403,23 @@ function evaluateFinalVerifierGate(input) {
|
|
|
317
403
|
}
|
|
318
404
|
return { skip: true, message: "已在 review 状态,最终验证仍然有效" };
|
|
319
405
|
}
|
|
320
|
-
function
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
if (!payload.baseline_docs || typeof payload.baseline_docs !== "object" || Array.isArray(payload.baseline_docs))
|
|
337
|
-
return null;
|
|
338
|
-
return payload.baseline_docs;
|
|
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
|
+
};
|
|
339
422
|
}
|
|
340
|
-
return null;
|
|
341
|
-
}
|
|
342
|
-
function proposalDocsChangedSinceBaseline(changeRoot, baseline) {
|
|
343
|
-
const current = documentBaseline(changeRoot);
|
|
344
|
-
return Object.entries(baseline).some(([path, digest]) => current[path] !== digest);
|
|
345
423
|
}
|
|
346
424
|
/**
|
|
347
425
|
* 统一 transition 提交协议——所有校验在锁内。
|
|
@@ -433,32 +511,16 @@ export function proposeReady(projectRoot, change, changeRoot, risk = "strict") {
|
|
|
433
511
|
return commitTransition(projectRoot, change, changeRoot, {
|
|
434
512
|
name: "propose-ready", idempotencyInputs: { risk },
|
|
435
513
|
decide: (snapshot) => {
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
const files = openQuestions.files.map(f => `${f.path}(${f.openCount})`).join(", ");
|
|
447
|
-
return { skip: true, message: `计划文档有 ${openQuestions.openCount} 个待用户确认问题:${files}` };
|
|
448
|
-
}
|
|
449
|
-
if (risk !== "minimal") {
|
|
450
|
-
const artifactsDir = join(changeRoot, ".superspec", "artifacts");
|
|
451
|
-
for (const doc of ["discovery.md", "business-invariants.md", "test-contract.md"]) {
|
|
452
|
-
if (!existsSync(join(artifactsDir, doc)))
|
|
453
|
-
return { skip: true, message: `基础职责缺失:${doc} 不存在(risk=${risk} 需要)` };
|
|
454
|
-
}
|
|
455
|
-
}
|
|
456
|
-
// 通用 job 检查(用提取的 helper)
|
|
457
|
-
const requiredRoles = PROPOSE_FINAL_REVIEW_GATE.requiredRolesForRisk(risk);
|
|
458
|
-
const reviewResult = checkOrCreateReviewJobs(snapshot, PROPOSE_FINAL_REVIEW_GATE, requiredRoles, changeRoot, change);
|
|
459
|
-
if (reviewResult)
|
|
460
|
-
return reviewResult;
|
|
461
|
-
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);
|
|
462
524
|
},
|
|
463
525
|
});
|
|
464
526
|
}
|
|
@@ -479,24 +541,16 @@ export function transitionExplore(projectRoot, change, changeRoot, risk = "stric
|
|
|
479
541
|
return commitTransition(projectRoot, change, changeRoot, {
|
|
480
542
|
name: "explore", idempotencyInputs: { phase: "explore", risk },
|
|
481
543
|
decide: (snapshot) => {
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
// 通用 job 审查(和 propose-ready 同一个 helper)
|
|
493
|
-
const requiredRoles = EXPLORE_DISCOVERY_REVIEW_GATE.requiredRolesForRisk(risk);
|
|
494
|
-
const reviewResult = checkOrCreateReviewJobs(snapshot, EXPLORE_DISCOVERY_REVIEW_GATE, requiredRoles, changeRoot, change);
|
|
495
|
-
if (reviewResult)
|
|
496
|
-
return reviewResult;
|
|
497
|
-
return { fromState: "explore", toState: "propose", outcome: "advanced", reason: "探索完成" };
|
|
498
|
-
}
|
|
499
|
-
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);
|
|
500
554
|
},
|
|
501
555
|
});
|
|
502
556
|
}
|
|
@@ -505,24 +559,16 @@ export function startApply(projectRoot, change, changeRoot) {
|
|
|
505
559
|
return commitTransition(projectRoot, change, changeRoot, {
|
|
506
560
|
name: "start-apply", idempotencyInputs: { phase: "start-apply" },
|
|
507
561
|
decide: (snapshot) => {
|
|
508
|
-
if (snapshot.state !== "propose_ready")
|
|
509
|
-
return { skip: true, message: `当前状态 ${snapshot.state},需要 propose_ready` };
|
|
510
562
|
const events = readEvents(projectRoot, change);
|
|
511
|
-
const
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
...reviewResult,
|
|
521
|
-
reason: `进入执行阶段前需要重新完成计划文档审查:${reviewResult.reason}`,
|
|
522
|
-
};
|
|
523
|
-
}
|
|
524
|
-
}
|
|
525
|
-
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);
|
|
526
572
|
},
|
|
527
573
|
});
|
|
528
574
|
}
|
|
@@ -534,31 +580,64 @@ export function taskStart(projectRoot, change, changeRoot, taskId) {
|
|
|
534
580
|
decide: (snapshot) => {
|
|
535
581
|
if (snapshot.state !== "apply")
|
|
536
582
|
return { skip: true, message: `当前状态 ${snapshot.state},需要 apply` };
|
|
583
|
+
const events = readEvents(projectRoot, change);
|
|
537
584
|
const tasksContent = readFileSync(join(changeRoot, "tasks.md"), "utf8");
|
|
538
585
|
const lines = tasksContent.split("\n");
|
|
539
586
|
const taskLineIdx = findTaskLine(lines, taskId);
|
|
540
587
|
if (taskLineIdx < 0)
|
|
541
588
|
return { skip: true, message: `任务 ${taskId} 不存在` };
|
|
542
|
-
|
|
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]\]/))
|
|
543
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} 不存在` };
|
|
544
598
|
const existing = snapshot.active_task_attempts?.find(a => a.task_id === taskId && a.state === "active");
|
|
545
599
|
if (existing)
|
|
546
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
|
+
}
|
|
547
612
|
const structureDigest = sha256Text(tasksContent.replace(/- \[[xX]\]/g, "- [ ]"));
|
|
548
613
|
const attempt = {
|
|
549
614
|
attempt_id: `ATT-${taskId}-${Date.now()}-${++attemptSeq}`,
|
|
550
615
|
task_id: taskId, state: "active",
|
|
551
616
|
task_structure_digest: structureDigest,
|
|
617
|
+
contract_mode: contractMode,
|
|
618
|
+
contract: adopted.contract,
|
|
619
|
+
tdd_required: taskInfo.tddRequired,
|
|
620
|
+
no_tdd_reason: taskInfo.noTddReason,
|
|
552
621
|
declared_write_scope: [], pre_edit_source_fingerprint: null,
|
|
553
622
|
pre_edit_red_ref: null, executor_packet_digest: null,
|
|
554
623
|
executor_result_ref: null, post_edit_green_ref: null,
|
|
555
624
|
created_at: new Date().toISOString(),
|
|
556
625
|
};
|
|
626
|
+
const eventPayload = {
|
|
627
|
+
...attempt,
|
|
628
|
+
...boundarySnapshotPayload(projectRoot),
|
|
629
|
+
};
|
|
557
630
|
return {
|
|
558
631
|
fromState: "apply", toState: "apply", outcome: "advanced",
|
|
559
632
|
reason: `创建任务 ${taskId} 执行尝试`,
|
|
560
|
-
extraEvents: [{ type: "task_started", payload:
|
|
561
|
-
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
|
+
},
|
|
562
641
|
};
|
|
563
642
|
},
|
|
564
643
|
});
|
|
@@ -599,7 +678,7 @@ export function reopen(projectRoot, change, changeRoot, to, reason, opts = {}) {
|
|
|
599
678
|
source_job_id: ref.jobId,
|
|
600
679
|
finding_id: ref.findingId,
|
|
601
680
|
decision_scope: scope,
|
|
602
|
-
baseline_docs:
|
|
681
|
+
baseline_docs: proposalDocsBaseline(changeRoot),
|
|
603
682
|
},
|
|
604
683
|
};
|
|
605
684
|
}
|
|
@@ -644,7 +723,7 @@ export function reopen(projectRoot, change, changeRoot, to, reason, opts = {}) {
|
|
|
644
723
|
if (snapshot.state !== "apply_done" && snapshot.state !== "review") {
|
|
645
724
|
return { skip: true, message: `当前状态 ${snapshot.state},不能 reopen 到 apply` };
|
|
646
725
|
}
|
|
647
|
-
const pending =
|
|
726
|
+
const pending = pendingTaskStatusForApply(changeRoot, events).pending;
|
|
648
727
|
if (pending.length === 0)
|
|
649
728
|
return { skip: true, message: "没有未完成任务,不能 reopen 到 apply" };
|
|
650
729
|
return {
|
|
@@ -667,9 +746,18 @@ export function reviewReady(projectRoot, change, changeRoot, risk = "strict") {
|
|
|
667
746
|
const policyPayload = storedPolicy ? {} : { review_policy: policy };
|
|
668
747
|
const currentEvidenceDigest = reviewEvidenceDigest(events);
|
|
669
748
|
// 检查是否所有任务已完成
|
|
670
|
-
const pending =
|
|
749
|
+
const pending = pendingTaskStatusForApply(changeRoot, events).pending;
|
|
671
750
|
if (pending.length > 0)
|
|
672
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
|
+
}
|
|
673
761
|
// 如果当前是 apply,先推进到 apply_done
|
|
674
762
|
if (snapshot.state === "apply") {
|
|
675
763
|
return {
|
|
@@ -682,6 +770,7 @@ export function reviewReady(projectRoot, change, changeRoot, risk = "strict") {
|
|
|
682
770
|
return evaluateApplyDoneCodeReviewGate({
|
|
683
771
|
events,
|
|
684
772
|
projectRoot,
|
|
773
|
+
changeRoot,
|
|
685
774
|
change,
|
|
686
775
|
policyPayload,
|
|
687
776
|
});
|
|
@@ -691,6 +780,7 @@ export function reviewReady(projectRoot, change, changeRoot, risk = "strict") {
|
|
|
691
780
|
snapshot,
|
|
692
781
|
events,
|
|
693
782
|
change,
|
|
783
|
+
projectRoot,
|
|
694
784
|
changeRoot,
|
|
695
785
|
currentEvidenceDigest,
|
|
696
786
|
policy,
|
|
@@ -707,22 +797,16 @@ export function accept(projectRoot, change, changeRoot) {
|
|
|
707
797
|
return commitTransition(projectRoot, change, changeRoot, {
|
|
708
798
|
name: "accept", idempotencyInputs: { phase: "accept" },
|
|
709
799
|
decide: (snapshot) => {
|
|
710
|
-
if (snapshot.state !== "review")
|
|
711
|
-
return { skip: true, message: `当前状态 ${snapshot.state},需要 review` };
|
|
712
|
-
const pending = pendingTaskIds(changeRoot);
|
|
713
|
-
if (pending.length > 0)
|
|
714
|
-
return { skip: true, message: formatPendingTaskMessage(pending, "请先 reopen --to apply 继续执行") };
|
|
715
800
|
const events = readEvents(projectRoot, change);
|
|
716
|
-
const
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
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);
|
|
726
810
|
},
|
|
727
811
|
});
|
|
728
812
|
}
|
|
@@ -739,13 +823,10 @@ export function archive(projectRoot, change, changeRoot) {
|
|
|
739
823
|
for (const p of docPaths) {
|
|
740
824
|
manifest[p] = sha256File(join(changeRoot, p)) ?? "sha256:missing";
|
|
741
825
|
}
|
|
742
|
-
// specs/
|
|
826
|
+
// specs/ 目录:递归收录 .md(覆盖 specs/<capability>/spec.md 布局)
|
|
743
827
|
const specsDir = join(changeRoot, "specs");
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
if (f.endsWith(".md"))
|
|
747
|
-
manifest[`specs/${f}`] = sha256File(join(specsDir, f)) ?? "sha256:missing";
|
|
748
|
-
}
|
|
828
|
+
for (const rel of listMarkdownFiles(specsDir)) {
|
|
829
|
+
manifest[`specs/${rel}`] = sha256File(join(specsDir, rel)) ?? "sha256:missing";
|
|
749
830
|
}
|
|
750
831
|
return {
|
|
751
832
|
fromState: "accepted", toState: "archive", outcome: "advanced",
|
|
@@ -756,10 +837,13 @@ export function archive(projectRoot, change, changeRoot) {
|
|
|
756
837
|
});
|
|
757
838
|
}
|
|
758
839
|
// ===== task-complete =====
|
|
759
|
-
export function taskComplete(projectRoot, change, changeRoot, taskId) {
|
|
840
|
+
export function taskComplete(projectRoot, change, changeRoot, taskId, inputContent = null) {
|
|
841
|
+
const scopeInput = parseScopeNoteInput(inputContent);
|
|
760
842
|
return commitTransition(projectRoot, change, changeRoot, {
|
|
761
|
-
name: "task-complete", idempotencyInputs: { task: taskId, phase: "complete" },
|
|
843
|
+
name: "task-complete", idempotencyInputs: { task: taskId, phase: "complete", scope_note_digest: scopeInput.digest ?? "" },
|
|
762
844
|
decide: (snapshot) => {
|
|
845
|
+
if (!scopeInput.ok)
|
|
846
|
+
return { skip: true, message: scopeInput.message };
|
|
763
847
|
if (snapshot.state !== "apply")
|
|
764
848
|
return { skip: true, message: `当前状态 ${snapshot.state},需要 apply` };
|
|
765
849
|
const attempt = snapshot.active_task_attempts?.find(a => a.task_id === taskId && a.state === "active");
|
|
@@ -768,28 +852,35 @@ export function taskComplete(projectRoot, change, changeRoot, taskId) {
|
|
|
768
852
|
const readiness = taskEvidenceReadiness(projectRoot, change, changeRoot, attempt);
|
|
769
853
|
if (!readiness.ready)
|
|
770
854
|
return { skip: true, message: `任务 ${taskId} 无法完成:${readiness.reason}` };
|
|
771
|
-
|
|
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
|
+
};
|
|
772
862
|
return {
|
|
773
863
|
fromState: "apply", toState: "apply", outcome: "advanced",
|
|
774
864
|
reason: `任务 ${taskId} 完成`,
|
|
775
|
-
extraEvents: [{ type: "task_completed", payload:
|
|
865
|
+
extraEvents: [{ type: "task_completed", payload: completedPayload }],
|
|
776
866
|
postCommit: (_pr, _ch, cr) => {
|
|
777
867
|
const lines = readFileSync(join(cr, "tasks.md"), "utf8").split("\n");
|
|
778
868
|
const idx = findTaskLine(lines, taskId);
|
|
779
|
-
if (idx < 0
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
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
|
+
}
|
|
783
881
|
lines[idx] = lines[idx].replace(/- \[ \]/, "- [x]");
|
|
784
882
|
writeFileSync(join(cr, "tasks.md"), lines.join("\n"));
|
|
785
|
-
|
|
786
|
-
const afterLines = readFileSync(join(cr, "tasks.md"), "utf8").split("\n");
|
|
787
|
-
const afterDigest = sha256Text(afterLines.join("\n").replace(/- \[[xX]\]/g, "- [ ]"));
|
|
788
|
-
if (afterDigest !== beforeDigest) {
|
|
789
|
-
afterLines[idx] = afterLines[idx].replace(/- \[x\]/, "- [ ]");
|
|
790
|
-
writeFileSync(join(cr, "tasks.md"), afterLines.join("\n"));
|
|
791
|
-
throw new Error(`复选框补丁导致结构变化:${taskId}`);
|
|
792
|
-
}
|
|
883
|
+
completedPayload.checkbox_update = { status: "applied" };
|
|
793
884
|
},
|
|
794
885
|
};
|
|
795
886
|
},
|