@peterxiaoyang/superspec 0.1.44 → 0.1.46
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/README.md +13 -1
- package/dist/cli.js +23 -24
- package/dist/code_review.js +7 -2
- package/dist/format.d.ts +20 -2
- package/dist/format.js +217 -26
- package/dist/git_state.d.ts +12 -1
- package/dist/git_state.js +45 -0
- package/dist/install.d.ts +1 -0
- package/dist/install.js +12 -0
- package/dist/next.d.ts +1 -1
- package/dist/next.js +3 -8
- package/dist/openspec.d.ts +13 -0
- package/dist/openspec.js +28 -0
- package/dist/phase_confirmation.d.ts +6 -0
- package/dist/phase_confirmation.js +22 -6
- package/dist/phase_plan.d.ts +10 -1
- package/dist/phase_plan.js +250 -37
- package/dist/record.d.ts +1 -1
- package/dist/record.js +38 -30
- package/dist/review.js +18 -2
- package/dist/sync.js +13 -4
- package/dist/task.js +15 -2
- package/dist/task_evidence.d.ts +1 -1
- package/dist/task_evidence.js +85 -10
- package/dist/transition.d.ts +4 -3
- package/dist/transition.js +166 -29
- package/dist/types.d.ts +38 -1
- package/dist/types.js +1 -0
- package/dist/workflow_config.d.ts +24 -0
- package/dist/workflow_config.js +127 -0
- package/package.json +1 -1
- package/templates/workflow/AGENTS.md +1 -1
- package/templates/workflow/agents/architect.toml +1 -1
- package/templates/workflow/agents/code-reviewer.toml +1 -1
- package/templates/workflow/agents/critic.toml +1 -1
- package/templates/workflow/agents/executor.toml +1 -1
- package/templates/workflow/agents/explore.toml +1 -1
- package/templates/workflow/agents/test-engineer.toml +1 -1
- package/templates/workflow/agents/test-runner.toml +1 -1
- package/templates/workflow/agents/verifier.toml +1 -1
- package/templates/workflow/prompts/architect.md +25 -33
- package/templates/workflow/prompts/code-reviewer.md +19 -67
- package/templates/workflow/prompts/critic.md +36 -87
- package/templates/workflow/prompts/executor.md +17 -19
- package/templates/workflow/prompts/explore.md +12 -46
- package/templates/workflow/prompts/test-engineer.md +22 -35
- package/templates/workflow/prompts/test-runner.md +11 -21
- package/templates/workflow/prompts/verifier.md +13 -37
- package/templates/workflow/skills/superspec-apply/SKILL.md +17 -85
- package/templates/workflow/skills/superspec-explore/SKILL.md +57 -66
- package/templates/workflow/skills/superspec-propose/SKILL.md +76 -129
- package/templates/workflow/skills/superspec-review/SKILL.md +14 -73
package/dist/phase_plan.js
CHANGED
|
@@ -1,18 +1,22 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
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";
|
|
4
|
+
import { collectProposeOpenQuestions, countDiscoveryOpenQuestions, parseExecutionRequirements, parseTasksMd, pendingTasksInContent, validateDiscovery, validateExecutionRequirements, validateExecutionRequirementDocumentReferences, validateProposalImpact, validateTasksDocument, } from "./format.js";
|
|
5
5
|
import { currentGitHead } from "./git_state.js";
|
|
6
|
+
import { validateOpenSpecChange } from "./openspec.js";
|
|
6
7
|
import { docRef, sha256File } from "./store.js";
|
|
7
8
|
import { isReviewReadyVerifier, isFreshReviewVerifier, historicalProposeReadyRoles, readReviewPolicyFromEvents, reviewGateRoleResolution, reviewRejectionOverrideScope, reviewEvidenceDigest, } from "./review.js";
|
|
8
9
|
import { CODE_REVIEW_DECISION_ANSWER_LABELS, CODE_REVIEW_REPAIR_SCOPE_PREFIX, codeReviewDecisionScope, codeReviewJobStaleReason, collectCodeReviewGateFacts, currentCodeReviewWorkingPaths, latestCodeReviewFailedStatus, requiresFinalVerifierForCurrentReview, scanCodeChangesForReview, } from "./code_review.js";
|
|
9
10
|
import { isPhaseAdvanceAuthorized, latestAcceptedPhaseDecision, phaseConfirmationCommitPayload, phaseConfirmationForBoundary, phaseConfirmationMissingMessage, } from "./phase_confirmation.js";
|
|
10
11
|
import { taskEvidenceReadiness } from "./task_evidence.js";
|
|
12
|
+
import { workflowRiskForProposeRound, workflowRiskForState } from "./workflow_config.js";
|
|
11
13
|
function requiredJobs(state, jobs, reason) {
|
|
12
14
|
return { kind: "required_jobs", state, jobs, reason };
|
|
13
15
|
}
|
|
14
16
|
function phaseConfirmationStep(context, boundary, reason) {
|
|
15
|
-
|
|
17
|
+
// 进入 propose_ready / apply 后,mode 来自本轮快照而非当前配置。
|
|
18
|
+
const risk = workflowRiskForState(context.events, context.snapshot.state, context.mode.risk);
|
|
19
|
+
const confirmation = phaseConfirmationForBoundary(context.projectRoot, context.events, context.snapshot, boundary, risk);
|
|
16
20
|
if (!confirmation || isPhaseAdvanceAuthorized(context.events, confirmation))
|
|
17
21
|
return null;
|
|
18
22
|
return {
|
|
@@ -76,23 +80,41 @@ function reviewGatePlan(snapshot, events, changeRoot, gate, requiredRoles) {
|
|
|
76
80
|
}
|
|
77
81
|
return null;
|
|
78
82
|
}
|
|
79
|
-
function validateTasksPlan(changeRoot) {
|
|
83
|
+
function validateTasksPlan(changeRoot, executionRequirementVersion) {
|
|
80
84
|
const tasksPath = join(changeRoot, "tasks.md");
|
|
81
85
|
if (!existsSync(tasksPath))
|
|
82
86
|
return "tasks.md 不存在";
|
|
83
87
|
const tasksContent = readFileSync(tasksPath, "utf8");
|
|
84
|
-
if (
|
|
85
|
-
return "
|
|
88
|
+
if (executionRequirementVersion === 1) {
|
|
89
|
+
return tasksContent.includes("# Tasks") || tasksContent.includes("- [ ]")
|
|
90
|
+
? null
|
|
91
|
+
: "tasks.md 内容不像任务计划文档";
|
|
92
|
+
}
|
|
93
|
+
const errors = validateTasksDocument(tasksContent);
|
|
94
|
+
if (errors.length > 0)
|
|
95
|
+
return errors.join(";");
|
|
86
96
|
return null;
|
|
87
97
|
}
|
|
88
|
-
function
|
|
98
|
+
export function executionPolicyForRisk(risk) {
|
|
99
|
+
return risk === "strict" ? "tdd" : "green_only";
|
|
100
|
+
}
|
|
101
|
+
export function executionPolicyForCurrentRound(events) {
|
|
102
|
+
const index = latestStartApplyIndex(events);
|
|
103
|
+
if (index < 0)
|
|
104
|
+
return "tdd";
|
|
105
|
+
const payload = events[index].payload;
|
|
106
|
+
return payload.execution_policy === "green_only" || payload.execution_policy === "tdd"
|
|
107
|
+
? payload.execution_policy
|
|
108
|
+
: "tdd";
|
|
109
|
+
}
|
|
110
|
+
function validateExecutionRequirementPlan(changeRoot, executionPolicy, executionRequirementVersion = 2) {
|
|
89
111
|
const tasksPath = join(changeRoot, "tasks.md");
|
|
90
112
|
if (!existsSync(tasksPath))
|
|
91
113
|
return { ok: true, mode: false };
|
|
92
114
|
const tasksContent = readFileSync(tasksPath, "utf8");
|
|
93
115
|
const testContractPath = join(changeRoot, ".superspec", "artifacts", "test-contract.md");
|
|
94
116
|
const testContractContent = existsSync(testContractPath) ? readFileSync(testContractPath, "utf8") : null;
|
|
95
|
-
const validation = validateExecutionRequirements(tasksContent, testContractContent);
|
|
117
|
+
const validation = validateExecutionRequirements(tasksContent, testContractContent, executionPolicy, executionRequirementVersion);
|
|
96
118
|
return validation.ok
|
|
97
119
|
? { ok: true, mode: validation.mode }
|
|
98
120
|
: { ok: false, mode: validation.mode, message: validation.errors.join(";") };
|
|
@@ -107,6 +129,43 @@ function missingBaseArtifact(changeRoot, risk) {
|
|
|
107
129
|
}
|
|
108
130
|
return null;
|
|
109
131
|
}
|
|
132
|
+
/** OpenSpec strict gate 只在当前 planning round 冻结为 strict 时执行。 */
|
|
133
|
+
function validateOpenSpecPlanningDocuments(projectRoot, change, changeRoot, profile) {
|
|
134
|
+
if (profile == null || profile.openspec.mode !== "strict")
|
|
135
|
+
return null;
|
|
136
|
+
const currentConfigDigest = sha256File(join(projectRoot, "openspec", "config.yaml"));
|
|
137
|
+
if (currentConfigDigest !== profile.openspec.config_digest) {
|
|
138
|
+
return "OpenSpec 配置自本 planning round 起已变化;请恢复原配置或 reopen --to propose 创建新的计划轮";
|
|
139
|
+
}
|
|
140
|
+
const tasksPath = join(changeRoot, "tasks.md");
|
|
141
|
+
const referenceErrors = validateExecutionRequirementDocumentReferences(changeRoot, parseExecutionRequirements(readFileSync(tasksPath, "utf8")));
|
|
142
|
+
if (referenceErrors.length > 0)
|
|
143
|
+
return referenceErrors.join(";");
|
|
144
|
+
const proposalPath = join(changeRoot, "proposal.md");
|
|
145
|
+
if (!existsSync(proposalPath))
|
|
146
|
+
return "proposal.md 不存在";
|
|
147
|
+
const impact = validateProposalImpact(readFileSync(proposalPath, "utf8"));
|
|
148
|
+
if (!impact.ok)
|
|
149
|
+
return impact.message;
|
|
150
|
+
const native = validateOpenSpecChange(projectRoot, change);
|
|
151
|
+
return native.ok ? null : native.message;
|
|
152
|
+
}
|
|
153
|
+
function validatePlanningPreflight(projectRoot, change, changeRoot, risk, executionPolicy, profile) {
|
|
154
|
+
const executionRequirementVersion = profile?.version ?? 1;
|
|
155
|
+
const tasksPlanError = validateTasksPlan(changeRoot, executionRequirementVersion);
|
|
156
|
+
if (tasksPlanError)
|
|
157
|
+
return { error: tasksPlanError, contractMode: false };
|
|
158
|
+
const executionRequirementPlan = validateExecutionRequirementPlan(changeRoot, executionPolicy, executionRequirementVersion);
|
|
159
|
+
if (!executionRequirementPlan.ok)
|
|
160
|
+
return { error: executionRequirementPlan.message, contractMode: false };
|
|
161
|
+
const missingArtifact = missingBaseArtifact(changeRoot, risk);
|
|
162
|
+
if (missingArtifact)
|
|
163
|
+
return { error: missingArtifact, contractMode: false };
|
|
164
|
+
return {
|
|
165
|
+
error: validateOpenSpecPlanningDocuments(projectRoot, change, changeRoot, profile),
|
|
166
|
+
contractMode: executionRequirementPlan.mode,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
110
169
|
export function proposalDocsBaseline(changeRoot) {
|
|
111
170
|
// 与 Propose gate 的可修改审查目标保持同一来源:specs/ 用目录聚合指纹,避免 reopen 基线漏掉任一个可修改的计划材料。
|
|
112
171
|
const docs = PROPOSE_FINAL_REVIEW_GATE.reviewTargets;
|
|
@@ -116,6 +175,15 @@ export function proposalDocsBaseline(changeRoot) {
|
|
|
116
175
|
}
|
|
117
176
|
return baseline;
|
|
118
177
|
}
|
|
178
|
+
export function discoveryDocsBaseline(changeRoot) {
|
|
179
|
+
// 与 Explore gate 使用同一组审查目标。回退到 explore 后,至少要更新一项
|
|
180
|
+
// discovery 材料,才允许重新进入 propose,避免把一次纯状态回退误当作新探索轮次。
|
|
181
|
+
const baseline = {};
|
|
182
|
+
for (const doc of EXPLORE_DISCOVERY_REVIEW_GATE.reviewTargets) {
|
|
183
|
+
baseline[doc] = docRef(changeRoot, doc).sha;
|
|
184
|
+
}
|
|
185
|
+
return baseline;
|
|
186
|
+
}
|
|
119
187
|
function isDigestMap(value) {
|
|
120
188
|
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
121
189
|
return false;
|
|
@@ -152,10 +220,28 @@ export function latestReopenProposeBaseline(events) {
|
|
|
152
220
|
}
|
|
153
221
|
return null;
|
|
154
222
|
}
|
|
223
|
+
export function latestReopenExploreBaseline(events) {
|
|
224
|
+
for (let i = events.length - 1; i >= 0; i--) {
|
|
225
|
+
const ev = events[i];
|
|
226
|
+
if (ev.event_type !== "transition_commit")
|
|
227
|
+
continue;
|
|
228
|
+
const payload = ev.payload;
|
|
229
|
+
if (payload.transition !== "reopen" || payload.reopen_target !== "explore")
|
|
230
|
+
continue;
|
|
231
|
+
if (!payload.baseline_docs || typeof payload.baseline_docs !== "object" || Array.isArray(payload.baseline_docs))
|
|
232
|
+
return null;
|
|
233
|
+
return payload.baseline_docs;
|
|
234
|
+
}
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
155
237
|
export function proposalDocsChangedSinceBaseline(changeRoot, baseline) {
|
|
156
238
|
const current = proposalDocsBaseline(changeRoot);
|
|
157
239
|
return Object.entries(baseline).some(([path, digest]) => current[path] !== digest);
|
|
158
240
|
}
|
|
241
|
+
export function discoveryDocsChangedSinceBaseline(changeRoot, baseline) {
|
|
242
|
+
const current = discoveryDocsBaseline(changeRoot);
|
|
243
|
+
return Object.entries(baseline).some(([path, digest]) => current[path] !== digest);
|
|
244
|
+
}
|
|
159
245
|
export function pendingTaskIds(changeRoot) {
|
|
160
246
|
const tasksContent = readFileSync(join(changeRoot, "tasks.md"), "utf8");
|
|
161
247
|
return pendingTasksInContent(tasksContent).map(task => task.taskId);
|
|
@@ -171,6 +257,83 @@ function latestStartApplyIndex(events) {
|
|
|
171
257
|
}
|
|
172
258
|
return -1;
|
|
173
259
|
}
|
|
260
|
+
function executionRequirementVersionFromPayload(payload) {
|
|
261
|
+
return payload.execution_requirement_version === 2 ? 2 : 1;
|
|
262
|
+
}
|
|
263
|
+
/** 当前 Apply round 的规则版本;缺失版本的历史 event 保持 v1 回放。 */
|
|
264
|
+
export function executionRequirementVersionForCurrentRound(events) {
|
|
265
|
+
const index = latestStartApplyIndex(events);
|
|
266
|
+
return index < 0 ? 1 : executionRequirementVersionFromPayload(events[index].payload);
|
|
267
|
+
}
|
|
268
|
+
/** Propose-ready 的规则版本决定随后 start-apply 是否采用新的全任务声明要求。 */
|
|
269
|
+
function executionRequirementVersionForProposeRound(events) {
|
|
270
|
+
for (let i = events.length - 1; i >= 0; i--) {
|
|
271
|
+
const event = events[i];
|
|
272
|
+
if (event.event_type !== "transition_commit")
|
|
273
|
+
continue;
|
|
274
|
+
const payload = event.payload;
|
|
275
|
+
if (payload.transition === "propose-ready" && payload.to_state === "propose_ready") {
|
|
276
|
+
return executionRequirementVersionFromPayload(event.payload);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
return 1;
|
|
280
|
+
}
|
|
281
|
+
function isPlanningValidationProfile(value) {
|
|
282
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
283
|
+
return false;
|
|
284
|
+
const profile = value;
|
|
285
|
+
if (profile.version !== 2 || !profile.openspec || typeof profile.openspec !== "object")
|
|
286
|
+
return false;
|
|
287
|
+
return profile.openspec.mode === "disabled" ||
|
|
288
|
+
profile.openspec.mode === "strict" && typeof profile.openspec.config_digest === "string";
|
|
289
|
+
}
|
|
290
|
+
/** 新 planning round 在进入 propose 时冻结当前 OpenSpec 校验契约。 */
|
|
291
|
+
export function planningValidationProfileForNewRound(projectRoot) {
|
|
292
|
+
const configDigest = sha256File(join(projectRoot, "openspec", "config.yaml"));
|
|
293
|
+
return configDigest == null
|
|
294
|
+
? { version: 2, openspec: { mode: "disabled" } }
|
|
295
|
+
: { version: 2, openspec: { mode: "strict", config_digest: configDigest } };
|
|
296
|
+
}
|
|
297
|
+
/** propose 状态尚未 ready 时,从进入本 planning round 的事件读取冻结 profile。 */
|
|
298
|
+
function planningValidationProfileForPendingProposeRound(events) {
|
|
299
|
+
for (let i = events.length - 1; i >= 0; i--) {
|
|
300
|
+
const event = events[i];
|
|
301
|
+
if (event.event_type !== "transition_commit")
|
|
302
|
+
continue;
|
|
303
|
+
const payload = event.payload;
|
|
304
|
+
// 只读取“进入 propose”的边界事件。propose-ready 创建审查 job 时也会
|
|
305
|
+
// 保持在 propose;若把它误当作新的 planning round,便会覆盖此前冻结的
|
|
306
|
+
// profile 并把当前轮错误降级为 v1。
|
|
307
|
+
const entersPropose = payload.to_state === "propose" && (payload.transition === "explore" ||
|
|
308
|
+
payload.transition === "propose" ||
|
|
309
|
+
payload.transition === "reopen" && payload.reopen_target === "propose");
|
|
310
|
+
if (!entersPropose)
|
|
311
|
+
continue;
|
|
312
|
+
return isPlanningValidationProfile(payload.planning_validation_profile)
|
|
313
|
+
? payload.planning_validation_profile
|
|
314
|
+
: null;
|
|
315
|
+
}
|
|
316
|
+
return null;
|
|
317
|
+
}
|
|
318
|
+
/** 已完成 propose-ready 的 round 只回放当时冻结的 profile。 */
|
|
319
|
+
function planningValidationProfileForReadyProposeRound(events) {
|
|
320
|
+
for (let i = events.length - 1; i >= 0; i--) {
|
|
321
|
+
const event = events[i];
|
|
322
|
+
if (event.event_type !== "transition_commit")
|
|
323
|
+
continue;
|
|
324
|
+
const payload = event.payload;
|
|
325
|
+
if (payload.transition !== "propose-ready" || payload.to_state !== "propose_ready")
|
|
326
|
+
continue;
|
|
327
|
+
if (isPlanningValidationProfile(payload.planning_validation_profile))
|
|
328
|
+
return payload.planning_validation_profile;
|
|
329
|
+
// 过渡期已写 v2 执行依据、但尚未带 profile 的事件保持 v2 tasks 契约,
|
|
330
|
+
// 但不在 start-apply 追溯新增 strict gate。
|
|
331
|
+
return executionRequirementVersionFromPayload(event.payload) === 2
|
|
332
|
+
? { version: 2, openspec: { mode: "disabled" } }
|
|
333
|
+
: null;
|
|
334
|
+
}
|
|
335
|
+
return null;
|
|
336
|
+
}
|
|
174
337
|
export function applyRequirementModeForCurrentRound(events) {
|
|
175
338
|
const index = latestStartApplyIndex(events);
|
|
176
339
|
if (index < 0)
|
|
@@ -238,14 +401,13 @@ export function pendingTaskStatusForApply(changeRoot, events) {
|
|
|
238
401
|
export function formatPendingTaskMessage(ids, action) {
|
|
239
402
|
return `尚有未完成任务:${ids.join(", ")};${action}`;
|
|
240
403
|
}
|
|
241
|
-
function nextArgv(change,
|
|
404
|
+
function nextArgv(change, _risk) {
|
|
242
405
|
return [
|
|
243
406
|
"superspec",
|
|
244
407
|
"transition",
|
|
245
408
|
"next",
|
|
246
409
|
"--change",
|
|
247
410
|
change,
|
|
248
|
-
...(risk === "strict" ? [] : ["--risk", risk]),
|
|
249
411
|
];
|
|
250
412
|
}
|
|
251
413
|
function acceptedMaterialFollowup(change, risk, planDocsChangedSinceAccept) {
|
|
@@ -281,6 +443,15 @@ export function planNextStep(context) {
|
|
|
281
443
|
}
|
|
282
444
|
return { kind: "run_transition", state: "init", transition: "explore", reason: "初始化完成,开始探索" };
|
|
283
445
|
case "explore": {
|
|
446
|
+
const reopenBaseline = latestReopenExploreBaseline(events);
|
|
447
|
+
if (reopenBaseline && !discoveryDocsChangedSinceBaseline(changeRoot, reopenBaseline)) {
|
|
448
|
+
return {
|
|
449
|
+
kind: "run_transition",
|
|
450
|
+
state: "explore",
|
|
451
|
+
transition: "explore",
|
|
452
|
+
reason: "回到 explore 后至少一个 discovery 材料必须变化",
|
|
453
|
+
};
|
|
454
|
+
}
|
|
284
455
|
const exploreReviewJobs = EXPLORE_DISCOVERY_REVIEW_GATE.openJobsForGate(snapshot);
|
|
285
456
|
if (exploreReviewJobs.length > 0) {
|
|
286
457
|
return requiredJobs("explore", exploreReviewJobs, `有 ${exploreReviewJobs.length} 个待完成探索审查工作项`);
|
|
@@ -352,6 +523,16 @@ export function planNextStep(context) {
|
|
|
352
523
|
if (confirmation)
|
|
353
524
|
return confirmation;
|
|
354
525
|
}
|
|
526
|
+
// 尚未获得用户确认时,按本次 next 的候选风险预检失败不能退回默认 strict
|
|
527
|
+
// 的 start-apply 命令;否则 normal/minimal 的策略不匹配会被静默改写为 strict。
|
|
528
|
+
if (startApplyPlan.kind === "skip") {
|
|
529
|
+
const ask = {
|
|
530
|
+
question: `${startApplyPlan.message}。请更新计划材料后重新执行 next。`,
|
|
531
|
+
allowed_answers: ["计划已更新"],
|
|
532
|
+
scope: "propose_apply_preflight",
|
|
533
|
+
};
|
|
534
|
+
return { kind: "ask_user", state: "propose_ready", ask, reason: startApplyPlan.message };
|
|
535
|
+
}
|
|
355
536
|
return { kind: "run_transition", state: "propose_ready", transition: "start-apply", reason: "计划就绪,开始执行" };
|
|
356
537
|
}
|
|
357
538
|
case "apply":
|
|
@@ -628,6 +809,10 @@ function planExploreTransition(context) {
|
|
|
628
809
|
if (snapshot.state !== "explore") {
|
|
629
810
|
return { kind: "skip", message: `当前状态 ${snapshot.state},explore 不适用` };
|
|
630
811
|
}
|
|
812
|
+
const reopenBaseline = latestReopenExploreBaseline(events);
|
|
813
|
+
if (reopenBaseline && !discoveryDocsChangedSinceBaseline(changeRoot, reopenBaseline)) {
|
|
814
|
+
return { kind: "skip", message: "回到 explore 后至少一个 discovery 材料必须变化" };
|
|
815
|
+
}
|
|
631
816
|
const discoveryCheck = validateDiscovery(changeRoot);
|
|
632
817
|
if (!discoveryCheck.ok)
|
|
633
818
|
return { kind: "skip", message: discoveryCheck.message };
|
|
@@ -645,7 +830,11 @@ function planExploreTransition(context) {
|
|
|
645
830
|
fromState: "explore",
|
|
646
831
|
toState: "propose",
|
|
647
832
|
reason: "探索完成",
|
|
648
|
-
payload:
|
|
833
|
+
payload: {
|
|
834
|
+
...phaseConfirmationCommitPayload(confirmation, decision),
|
|
835
|
+
planning_validation_version: 2,
|
|
836
|
+
planning_validation_profile: planningValidationProfileForNewRound(projectRoot),
|
|
837
|
+
},
|
|
649
838
|
};
|
|
650
839
|
}
|
|
651
840
|
function planProposeReadyTransition(context) {
|
|
@@ -653,20 +842,15 @@ function planProposeReadyTransition(context) {
|
|
|
653
842
|
const risk = mode.risk;
|
|
654
843
|
if (snapshot.state !== "propose")
|
|
655
844
|
return { kind: "skip", message: `当前状态 ${snapshot.state},不能 propose-ready` };
|
|
656
|
-
const
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
if (!executionRequirementPlan.ok)
|
|
661
|
-
return { kind: "skip", message: executionRequirementPlan.message };
|
|
845
|
+
const planningProfile = planningValidationProfileForPendingProposeRound(context.events);
|
|
846
|
+
const preflight = validatePlanningPreflight(context.projectRoot, context.change, changeRoot, risk, executionPolicyForRisk(risk), planningProfile);
|
|
847
|
+
if (preflight.error)
|
|
848
|
+
return { kind: "skip", message: preflight.error };
|
|
662
849
|
const openQuestions = collectProposeOpenQuestions(changeRoot);
|
|
663
850
|
if (openQuestions.openCount > 0) {
|
|
664
851
|
const files = openQuestions.files.map(f => `${f.path}(${f.openCount})`).join(", ");
|
|
665
852
|
return { kind: "skip", message: `计划文档有 ${openQuestions.openCount} 个待用户确认问题:${files}` };
|
|
666
853
|
}
|
|
667
|
-
const missingArtifact = missingBaseArtifact(changeRoot, risk);
|
|
668
|
-
if (missingArtifact)
|
|
669
|
-
return { kind: "skip", message: missingArtifact };
|
|
670
854
|
const requiredRoles = PROPOSE_FINAL_REVIEW_GATE.requiredRolesForRisk(risk);
|
|
671
855
|
const gatePlan = reviewGatePlan(snapshot, context.events, changeRoot, PROPOSE_FINAL_REVIEW_GATE, requiredRoles);
|
|
672
856
|
if (gatePlan)
|
|
@@ -676,8 +860,21 @@ function planProposeReadyTransition(context) {
|
|
|
676
860
|
fromState: "propose",
|
|
677
861
|
toState: "propose_ready",
|
|
678
862
|
reason: `risk=${risk},所有需求已满足`,
|
|
863
|
+
payload: {
|
|
864
|
+
workflow_mode: risk,
|
|
865
|
+
...(planningProfile ? {
|
|
866
|
+
execution_requirement_version: 2,
|
|
867
|
+
planning_validation_version: 2,
|
|
868
|
+
planning_validation_profile: planningProfile,
|
|
869
|
+
} : {}),
|
|
870
|
+
},
|
|
679
871
|
};
|
|
680
872
|
}
|
|
873
|
+
function acceptedProposeToApplyConfirmation(context, risk) {
|
|
874
|
+
const confirmation = phaseConfirmationForBoundary(context.projectRoot, context.events, context.snapshot, "propose_to_apply", risk);
|
|
875
|
+
const decision = confirmation ? latestAcceptedPhaseDecision(context.events, confirmation) : null;
|
|
876
|
+
return confirmation && decision?.decision === "advance" ? { confirmation, decision } : null;
|
|
877
|
+
}
|
|
681
878
|
function planStartApplyTransition(context, enforceConfirmation = true) {
|
|
682
879
|
const { changeRoot, events, projectRoot, snapshot } = context;
|
|
683
880
|
if (snapshot.state !== "propose_ready")
|
|
@@ -687,23 +884,32 @@ function planStartApplyTransition(context, enforceConfirmation = true) {
|
|
|
687
884
|
// 按基线实际键名提示:升级前留下的旧基线可能不含 specs/,静态清单会误导
|
|
688
885
|
return { kind: "skip", message: `回到 propose 后至少一个计划文档必须变化(基线绑定:${Object.keys(reopenBaseline).join("、")})` };
|
|
689
886
|
}
|
|
690
|
-
const
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
887
|
+
const risk = workflowRiskForProposeRound(events, context.mode.risk);
|
|
888
|
+
const planningProfile = planningValidationProfileForReadyProposeRound(events);
|
|
889
|
+
const executionRequirementVersion = planningProfile?.version ?? executionRequirementVersionForProposeRound(events);
|
|
890
|
+
const executionPolicy = executionPolicyForRisk(risk);
|
|
891
|
+
const preflight = validatePlanningPreflight(projectRoot, context.change, changeRoot, risk, executionPolicy, planningProfile);
|
|
892
|
+
if (preflight.error)
|
|
893
|
+
return { kind: "skip", message: preflight.error };
|
|
894
|
+
const requiredRoles = executionRequirementVersion === 2
|
|
895
|
+
? PROPOSE_FINAL_REVIEW_GATE.requiredRolesForRisk(risk)
|
|
896
|
+
: historicalProposeReadyRoles(events);
|
|
897
|
+
const gatePlan = reviewGatePlan(snapshot, events, changeRoot, PROPOSE_FINAL_REVIEW_GATE, requiredRoles);
|
|
898
|
+
if (gatePlan) {
|
|
899
|
+
return {
|
|
900
|
+
...gatePlan,
|
|
901
|
+
reason: `进入执行阶段前需要重新完成计划文档审查:${gatePlan.reason}`,
|
|
902
|
+
};
|
|
699
903
|
}
|
|
700
|
-
const
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
904
|
+
const acceptedConfirmation = enforceConfirmation
|
|
905
|
+
? acceptedProposeToApplyConfirmation(context, risk)
|
|
906
|
+
: null;
|
|
907
|
+
if (enforceConfirmation && !acceptedConfirmation) {
|
|
908
|
+
const confirmation = phaseConfirmationForBoundary(projectRoot, events, snapshot, "propose_to_apply", risk);
|
|
909
|
+
return {
|
|
910
|
+
kind: "skip",
|
|
911
|
+
message: confirmation ? phaseConfirmationMissingMessage(confirmation) : "无法建立 Propose 阶段确认范围",
|
|
912
|
+
};
|
|
707
913
|
}
|
|
708
914
|
const gitHead = currentGitHead(projectRoot);
|
|
709
915
|
return {
|
|
@@ -714,8 +920,15 @@ function planStartApplyTransition(context, enforceConfirmation = true) {
|
|
|
714
920
|
payload: {
|
|
715
921
|
apply_start_head: gitHead.head,
|
|
716
922
|
apply_start_head_reason: gitHead.reason,
|
|
717
|
-
apply_contract_mode:
|
|
718
|
-
...(
|
|
923
|
+
apply_contract_mode: preflight.contractMode,
|
|
924
|
+
...(executionRequirementVersion === 2 ? { execution_requirement_version: 2 } : {}),
|
|
925
|
+
execution_policy: executionPolicy,
|
|
926
|
+
workflow_mode: risk,
|
|
927
|
+
review_policy: {
|
|
928
|
+
review_risk: risk,
|
|
929
|
+
requires_verifier: risk !== "minimal",
|
|
930
|
+
},
|
|
931
|
+
...(acceptedConfirmation ? phaseConfirmationCommitPayload(acceptedConfirmation.confirmation, acceptedConfirmation.decision) : {}),
|
|
719
932
|
},
|
|
720
933
|
};
|
|
721
934
|
}
|
package/dist/record.d.ts
CHANGED
|
@@ -7,7 +7,7 @@ export declare function recordJobSubmitContent(projectRoot: string, change: stri
|
|
|
7
7
|
export declare function recordUserDecision(projectRoot: string, change: string, inputFile: string): RecordResult;
|
|
8
8
|
/** record user-decision:从 JSON 内容登记用户决策 */
|
|
9
9
|
export declare function recordUserDecisionContent(projectRoot: string, change: string, content: string): RecordResult;
|
|
10
|
-
/** jobs list(
|
|
10
|
+
/** jobs list(job 从 transition_commit.new_jobs 提取;reopen 可用 job_invalidated 关闭未完成工作项) */
|
|
11
11
|
export declare function jobsList(projectRoot: string, change: string): {
|
|
12
12
|
open: Job[];
|
|
13
13
|
accepted: Job[];
|
package/dist/record.js
CHANGED
|
@@ -4,7 +4,7 @@ import { isAbsolute, join, relative, resolve } from "node:path";
|
|
|
4
4
|
import { ensureChangeLayout, readEvents, appendEvent, makeEvent, sha256File, sha256Text, withLock, appendRawRecord, } from "./store.js";
|
|
5
5
|
import { rebuildSnapshot } from "./sync.js";
|
|
6
6
|
import { changeRoot as openspecChangeRoot } from "./openspec.js";
|
|
7
|
-
import { isPhaseConfirmationScope, phaseActionForAnswer, phaseConfirmationForCurrentState, } from "./phase_confirmation.js";
|
|
7
|
+
import { isPhaseConfirmationScope, phaseActionForAnswer, phaseConfirmationForCurrentState, workflowRiskForPhaseConfirmation, } from "./phase_confirmation.js";
|
|
8
8
|
import { CODE_REVIEW_DECISION_ANSWER_LABELS, CODE_REVIEW_DECISION_SCOPE_PREFIX, codeReviewDecisionAnswerLabel, normalizeCodeReviewDecisionAnswer, } from "./code_review.js";
|
|
9
9
|
import { invalidReasonForSubmittedReport } from "./job_validity.js";
|
|
10
10
|
import { jobSubmitArgv } from "./job_action.js";
|
|
@@ -346,6 +346,10 @@ function jobTerminalState(events, jobId) {
|
|
|
346
346
|
}
|
|
347
347
|
return null;
|
|
348
348
|
}
|
|
349
|
+
function jobInvalidated(events, jobId) {
|
|
350
|
+
return events.some(ev => ev.event_type === "job_invalidated" &&
|
|
351
|
+
ev.payload.job_id === jobId);
|
|
352
|
+
}
|
|
349
353
|
function terminalJobSubmitResult(events, jobId, terminal, reportDigest) {
|
|
350
354
|
const existing = events.find(e => (e.event_type === "job_accepted" || e.event_type === "job_rejected")
|
|
351
355
|
&& e.payload.job_id === jobId
|
|
@@ -547,6 +551,9 @@ export function recordJobSubmit(projectRoot, change, changeRoot, jobId, reportFi
|
|
|
547
551
|
if (!job) {
|
|
548
552
|
return { event_type: "job_rejected", accepted: false, message: `工作项 ${jobId} 不存在` };
|
|
549
553
|
}
|
|
554
|
+
if (jobInvalidated(events, jobId)) {
|
|
555
|
+
return { accepted: false, message: `工作项 ${jobId} 已因回退到更早阶段失效,不接受新报告。` };
|
|
556
|
+
}
|
|
550
557
|
const terminal = jobTerminalState(events, jobId);
|
|
551
558
|
if (terminal) {
|
|
552
559
|
const reportDigest = sha256File(reportFile) ?? "sha256:unknown";
|
|
@@ -584,6 +591,9 @@ export function recordJobSubmitContent(projectRoot, change, changeRoot, jobId, r
|
|
|
584
591
|
if (!job) {
|
|
585
592
|
return { event_type: "job_rejected", accepted: false, message: `工作项 ${jobId} 不存在` };
|
|
586
593
|
}
|
|
594
|
+
if (jobInvalidated(events, jobId)) {
|
|
595
|
+
return { accepted: false, message: `工作项 ${jobId} 已因回退到更早阶段失效,不接受新报告。` };
|
|
596
|
+
}
|
|
587
597
|
const reportDigest = sha256Text(reportContent);
|
|
588
598
|
const terminal = jobTerminalState(events, jobId);
|
|
589
599
|
if (terminal) {
|
|
@@ -639,14 +649,9 @@ function recordUserDecisionLoaded(projectRoot, change, events, content, inputDig
|
|
|
639
649
|
});
|
|
640
650
|
const changeRoot = openspecChangeRoot(projectRoot, change);
|
|
641
651
|
const snapshot = rebuildSnapshot(projectRoot, change, changeRoot);
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
? decision.review_risk
|
|
646
|
-
: null;
|
|
647
|
-
const current = decisionRisk
|
|
648
|
-
? phaseConfirmationForCurrentState(projectRoot, events, snapshot, decisionRisk)
|
|
649
|
-
: null;
|
|
652
|
+
// mode 是状态机从配置/round 快照推导的输入,不接受 user-decision JSON 注入。
|
|
653
|
+
const decisionRisk = workflowRiskForPhaseConfirmation(projectRoot, events, snapshot);
|
|
654
|
+
const current = phaseConfirmationForCurrentState(projectRoot, events, snapshot, decisionRisk);
|
|
650
655
|
if (existingAccepted &&
|
|
651
656
|
current?.scope === decision.scope &&
|
|
652
657
|
latestAcceptedForScope?.event_id === existing.event_id) {
|
|
@@ -657,17 +662,15 @@ function recordUserDecisionLoaded(projectRoot, change, events, content, inputDig
|
|
|
657
662
|
};
|
|
658
663
|
}
|
|
659
664
|
const action = current ? phaseActionForAnswer(current, decision.answer) : null;
|
|
660
|
-
const rejectionReason = !
|
|
661
|
-
? "
|
|
662
|
-
:
|
|
663
|
-
? "
|
|
664
|
-
:
|
|
665
|
-
? "
|
|
666
|
-
: !
|
|
667
|
-
? "
|
|
668
|
-
:
|
|
669
|
-
? "missing_phase_confirmation_reason"
|
|
670
|
-
: null;
|
|
665
|
+
const rejectionReason = !current
|
|
666
|
+
? "phase_confirmation_not_pending"
|
|
667
|
+
: decision.scope !== current.scope
|
|
668
|
+
? "stale_phase_confirmation_scope"
|
|
669
|
+
: !action
|
|
670
|
+
? "invalid_phase_confirmation_answer"
|
|
671
|
+
: action.reason === "required" && !nonEmptyString(decision.reason)
|
|
672
|
+
? "missing_phase_confirmation_reason"
|
|
673
|
+
: null;
|
|
671
674
|
if (rejectionReason) {
|
|
672
675
|
if (existing &&
|
|
673
676
|
existing.payload.accepted === false &&
|
|
@@ -687,11 +690,9 @@ function recordUserDecisionLoaded(projectRoot, change, events, content, inputDig
|
|
|
687
690
|
}));
|
|
688
691
|
const message = rejectionReason === "invalid_phase_confirmation_answer" && current
|
|
689
692
|
? `阶段确认答复必须精确为:${current.ask.allowed_answers.join("、")}`
|
|
690
|
-
: rejectionReason === "
|
|
691
|
-
?
|
|
692
|
-
:
|
|
693
|
-
? action.reason_prompt ?? "当前选择必须写明原因"
|
|
694
|
-
: "阶段确认已失效或当前没有待确认的阶段边界,请重新执行 next";
|
|
693
|
+
: rejectionReason === "missing_phase_confirmation_reason" && action
|
|
694
|
+
? action.reason_prompt ?? "当前选择必须写明原因"
|
|
695
|
+
: "阶段确认已失效或当前没有待确认的阶段边界,请重新执行 next";
|
|
695
696
|
return { event_type: "user_decision_recorded", accepted: false, message };
|
|
696
697
|
}
|
|
697
698
|
phaseConfirmation = current;
|
|
@@ -857,7 +858,7 @@ export function recordUserDecisionContent(projectRoot, change, content) {
|
|
|
857
858
|
return recordUserDecisionLoaded(projectRoot, change, events, content, sha256Text(content));
|
|
858
859
|
});
|
|
859
860
|
}
|
|
860
|
-
/** jobs list(
|
|
861
|
+
/** jobs list(job 从 transition_commit.new_jobs 提取;reopen 可用 job_invalidated 关闭未完成工作项) */
|
|
861
862
|
export function jobsList(projectRoot, change) {
|
|
862
863
|
const events = readEvents(projectRoot, change);
|
|
863
864
|
const open = [];
|
|
@@ -888,6 +889,12 @@ export function jobsList(projectRoot, change) {
|
|
|
888
889
|
open.splice(idx, 1)[0];
|
|
889
890
|
rejected.push({ job_id, role });
|
|
890
891
|
}
|
|
892
|
+
else if (ev.event_type === "job_invalidated") {
|
|
893
|
+
const { job_id } = ev.payload;
|
|
894
|
+
const idx = open.findIndex(j => j.job_id === job_id);
|
|
895
|
+
if (idx >= 0)
|
|
896
|
+
open.splice(idx, 1);
|
|
897
|
+
}
|
|
891
898
|
}
|
|
892
899
|
return { open, accepted, rejected };
|
|
893
900
|
}
|
|
@@ -898,8 +905,9 @@ function packetFieldDescriptions() {
|
|
|
898
905
|
boundFiles: "本工作项绑定的文件清单;审查报告必须说明这些文件是否都看过。",
|
|
899
906
|
review_scope: "报告中的审查覆盖范围;普通 reviewer/verifier 用 checked_paths 回执全部绑定文件,code-reviewer 还需按专用协议说明未检查项。",
|
|
900
907
|
code_review_scope: "代码审查范围:从已审基点到当前 HEAD 的提交改动、工作区改动和未跟踪代码文件。",
|
|
901
|
-
task_execution_index: "按任务汇总的执行证据:每个任务(task
|
|
902
|
-
contract: "任务启动时的执行依据快照:tests/design/source/
|
|
908
|
+
task_execution_index: "按任务汇总的执行证据:每个任务(task)的执行依据、有效证据要求、声明测试、测试证据和改动文件。",
|
|
909
|
+
contract: "任务启动时的执行依据快照:tests/design/source/acceptance/guard 分别对应 测试/设计/来源/验收/边界;null 表示历史任务没有执行依据。",
|
|
910
|
+
required_evidence: "task-start 结合冻结策略编译并写入 attempt 的有效证据要求:test_ids、red_required、green_required 和允许的 GREEN 语义状态;null 表示历史任务按旧记录回放。",
|
|
903
911
|
changed_paths: "与某个任务(task)或代码状态检查相关的改动文件。",
|
|
904
912
|
changed_paths_partial_reason: "该任务(task)的提交段 diff 失败原因;存在时 changed_paths 只包含工作区对比结果,归属可能不完整。",
|
|
905
913
|
unattributed_paths: "代码审查范围中暂时无法归属到某个任务(task)的文件。",
|
|
@@ -974,10 +982,10 @@ export function jobsPacket(projectRoot, change, jobId) {
|
|
|
974
982
|
? `最小格式:{"role":"code-reviewer","verdict":"pass|fail","review_scope":{"job_id":"${job.job_id}","packet_digest":"${job.packet_digest}","checked_paths":${JSON.stringify(job.boundFiles.map(f => f.path))},"checked_docs":${JSON.stringify(REVIEW_DOC_PATHS)},"unchecked":[]},"findings":[],"reviewer":{"kind":"codex-subagent","id":"<thread-or-agent-id>"}};审查覆盖范围(review_scope)用来说明本次审查覆盖了哪些文件和文档,已检查路径(checked_paths)与未检查项(unchecked)必须合起来覆盖全部绑定文件(boundFiles),unchecked 条目格式为 {"path":"<path>","reason":"<reason>"}。`
|
|
975
983
|
+ `报告结论为 fail 时,问题列表(findings)至少包含一个可处理、可追溯的阻塞问题,字段为 {"id":"<stable-id>","blocking":true,"type":"implementation|spec|mixed","description":"<what>","evidence":"<why>","source_refs":["<path:line>"],"impact":"<impact>","suggested_action":"apply|propose"}。问题类型(type)中 implementation 表示纯代码实现问题,spec 表示方案/需求文档问题,mixed 表示需要使用者判断的混合问题。`
|
|
976
984
|
+ (packetContext?.task_execution_index
|
|
977
|
-
? `本工作项带任务执行索引(task_execution_index):按 task 对照其执行依据快照(contract)审查——实现路线对照 design 引用原文、累计 diff 对照 guard 边界、测试断言对照 tests 声明的 scenario;每项的 scope_note 是执行者登记的范围扩大说明,判断其合理性与验证充分性;changed_paths 是归属线索不是结论(null 表示未知);unattributed_paths 中的无主改动逐个判断合理性;coverage_exemption_refs 解释未绑定 task 的 TEST 豁免。`
|
|
985
|
+
? `本工作项带任务执行索引(task_execution_index):按 task 对照其执行依据快照(contract)审查——实现路线对照 design 引用原文、累计 diff 对照 guard 边界、测试断言对照 tests 声明的 scenario;每项的 required_evidence 是 task-start 冻结的证据口径,red_required/green_required 分别说明是否需要 RED/GREEN;每项的 scope_note 是执行者登记的范围扩大说明,判断其合理性与验证充分性;changed_paths 是归属线索不是结论(null 表示未知);unattributed_paths 中的无主改动逐个判断合理性;coverage_exemption_refs 解释未绑定 task 的 TEST 豁免。`
|
|
978
986
|
: "")
|
|
979
987
|
: job.role === "verifier"
|
|
980
|
-
? `最小格式:{"role":"verifier","verdict":"pass|fail","findings":[]${hasReviewScope ? `,"review_scope":{"checked_paths":${JSON.stringify(job.boundFiles.map(file => file.path))}}` : ""}}。核对代码审查记录(code_review_gate):passed 必须能追溯到已接受的代码审查工作项,skipped 必须能证明本次没有代码类改动。核对代码审查问题闭环:实现修复任务必须带审查修复引用(review_fix_of:<job_id>#<problem_id
|
|
988
|
+
? `最小格式:{"role":"verifier","verdict":"pass|fail","findings":[]${hasReviewScope ? `,"review_scope":{"checked_paths":${JSON.stringify(job.boundFiles.map(file => file.path))}}` : ""}}。核对代码审查记录(code_review_gate):passed 必须能追溯到已接受的代码审查工作项,skipped 必须能证明本次没有代码类改动。核对代码审查问题闭环:实现修复任务必须带审查修复引用(review_fix_of:<job_id>#<problem_id>),方案/混合问题必须有用户决策或后续修复证据。按 task_execution_index 的 required_evidence 核对测试证据:red_required 时需要同一 TEST 的 RED(expected_failure)后 GREEN;green_required 时每个声明 TEST 都需要允许的 GREEN 语义状态;测试运行证据应包含测试 ID(test_id)、命令(command)、工作目录(cwd)、退出码(exit_code)、语义状态(semantic_status)。审查修复的回归测试运行可用回归覆盖任务列表(covers_task_ids)说明覆盖了哪些已完成任务;缺少任务尝试 ID(attempt_id)的旧证据只能弱引用。` +
|
|
981
989
|
(packetContext?.code_state_check
|
|
982
990
|
? `本工作项带代码状态检查(code_state_check):head_matches 为 false 或 changed_paths 非空表示代码审查后代码又发生变化,须在报告中列出差异并交主流程与用户裁决,不自行判定无害,也不据此自动否定已接受的代码审查。`
|
|
983
991
|
: "")
|
package/dist/review.js
CHANGED
|
@@ -42,11 +42,27 @@ function isReviewPolicy(value) {
|
|
|
42
42
|
typeof obj.requires_verifier === "boolean");
|
|
43
43
|
}
|
|
44
44
|
export function readReviewPolicyFromEvents(events) {
|
|
45
|
-
|
|
45
|
+
// 新格式在 start-apply 写入,并且只能读取最新 Apply round 的策略。
|
|
46
|
+
// 回退到该 round 内的 review-ready,是为了回放升级前的历史 event。
|
|
47
|
+
let latestStartApplyIndex = -1;
|
|
48
|
+
for (let i = events.length - 1; i >= 0; i--) {
|
|
49
|
+
const event = events[i];
|
|
50
|
+
if (event.event_type !== "transition_commit")
|
|
51
|
+
continue;
|
|
52
|
+
const payload = event.payload;
|
|
53
|
+
if (payload.transition === "start-apply" && payload.to_state === "apply") {
|
|
54
|
+
latestStartApplyIndex = i;
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (latestStartApplyIndex < 0)
|
|
59
|
+
return null;
|
|
60
|
+
for (let i = events.length - 1; i >= latestStartApplyIndex; i--) {
|
|
61
|
+
const ev = events[i];
|
|
46
62
|
if (ev.event_type !== "transition_commit")
|
|
47
63
|
continue;
|
|
48
64
|
const payload = ev.payload;
|
|
49
|
-
if (payload.transition !== "review-ready")
|
|
65
|
+
if (payload.transition !== "start-apply" && payload.transition !== "review-ready")
|
|
50
66
|
continue;
|
|
51
67
|
if (isReviewPolicy(payload.review_policy))
|
|
52
68
|
return payload.review_policy;
|