@peterxiaoyang/superspec 0.1.45 → 0.1.47
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 +2 -1
- package/dist/code_review.d.ts +11 -1
- package/dist/code_review.js +40 -0
- package/dist/explore_round.d.ts +23 -0
- package/dist/explore_round.js +94 -0
- package/dist/format.d.ts +67 -2
- package/dist/format.js +273 -22
- package/dist/openspec.d.ts +13 -0
- package/dist/openspec.js +53 -4
- package/dist/phase_confirmation.js +71 -2
- package/dist/phase_plan.d.ts +6 -1
- package/dist/phase_plan.js +180 -32
- package/dist/record.js +191 -57
- package/dist/review.js +2 -0
- package/dist/task_evidence.js +5 -3
- package/dist/transition.d.ts +1 -0
- package/dist/transition.js +222 -28
- package/dist/types.d.ts +42 -0
- package/package.json +1 -1
- package/templates/workflow/AGENTS.md +15 -5
- 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 -86
- package/templates/workflow/prompts/executor.md +17 -19
- package/templates/workflow/prompts/explore.md +12 -46
- package/templates/workflow/prompts/test-engineer.md +22 -34
- 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 +26 -26
- package/templates/workflow/skills/superspec-explore/SKILL.md +69 -60
- package/templates/workflow/skills/superspec-propose/SKILL.md +85 -133
- package/templates/workflow/skills/superspec-review/SKILL.md +14 -44
package/dist/phase_plan.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
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 {
|
|
4
|
+
import { currentExploreRoundId, exploreAnswerRegistrationPayload, unregisteredClosedExploreQuestions, } from "./explore_round.js";
|
|
5
|
+
import { collectProposeOpenQuestions, discoveryOpenQuestionDisplayText, discoveryOpenQuestionScope, parseExecutionRequirements, parseDiscoveryOpenQuestions, parseTasksMd, pendingTasksInContent, validateDiscovery, validateExecutionRequirements, validateExecutionRequirementDocumentReferences, validateProposalImpact, validateTasksDocument, } from "./format.js";
|
|
5
6
|
import { currentGitHead } from "./git_state.js";
|
|
7
|
+
import { validateOpenSpecChange } from "./openspec.js";
|
|
6
8
|
import { docRef, sha256File } from "./store.js";
|
|
7
9
|
import { isReviewReadyVerifier, isFreshReviewVerifier, historicalProposeReadyRoles, readReviewPolicyFromEvents, reviewGateRoleResolution, reviewRejectionOverrideScope, reviewEvidenceDigest, } from "./review.js";
|
|
8
10
|
import { CODE_REVIEW_DECISION_ANSWER_LABELS, CODE_REVIEW_REPAIR_SCOPE_PREFIX, codeReviewDecisionScope, codeReviewJobStaleReason, collectCodeReviewGateFacts, currentCodeReviewWorkingPaths, latestCodeReviewFailedStatus, requiresFinalVerifierForCurrentReview, scanCodeChangesForReview, } from "./code_review.js";
|
|
@@ -79,13 +81,19 @@ function reviewGatePlan(snapshot, events, changeRoot, gate, requiredRoles) {
|
|
|
79
81
|
}
|
|
80
82
|
return null;
|
|
81
83
|
}
|
|
82
|
-
function validateTasksPlan(changeRoot) {
|
|
84
|
+
function validateTasksPlan(changeRoot, executionRequirementVersion) {
|
|
83
85
|
const tasksPath = join(changeRoot, "tasks.md");
|
|
84
86
|
if (!existsSync(tasksPath))
|
|
85
87
|
return "tasks.md 不存在";
|
|
86
88
|
const tasksContent = readFileSync(tasksPath, "utf8");
|
|
87
|
-
if (
|
|
88
|
-
return "
|
|
89
|
+
if (executionRequirementVersion === 1) {
|
|
90
|
+
return tasksContent.includes("# Tasks") || tasksContent.includes("- [ ]")
|
|
91
|
+
? null
|
|
92
|
+
: "tasks.md 内容不像任务计划文档";
|
|
93
|
+
}
|
|
94
|
+
const errors = validateTasksDocument(tasksContent);
|
|
95
|
+
if (errors.length > 0)
|
|
96
|
+
return errors.join(";");
|
|
89
97
|
return null;
|
|
90
98
|
}
|
|
91
99
|
export function executionPolicyForRisk(risk) {
|
|
@@ -122,6 +130,43 @@ function missingBaseArtifact(changeRoot, risk) {
|
|
|
122
130
|
}
|
|
123
131
|
return null;
|
|
124
132
|
}
|
|
133
|
+
/** OpenSpec strict gate 只在当前 planning round 冻结为 strict 时执行。 */
|
|
134
|
+
function validateOpenSpecPlanningDocuments(projectRoot, change, changeRoot, profile) {
|
|
135
|
+
if (profile == null || profile.openspec.mode !== "strict")
|
|
136
|
+
return null;
|
|
137
|
+
const currentConfigDigest = sha256File(join(projectRoot, "openspec", "config.yaml"));
|
|
138
|
+
if (currentConfigDigest !== profile.openspec.config_digest) {
|
|
139
|
+
return "OpenSpec 配置自本 planning round 起已变化;请恢复原配置或 reopen --to propose 创建新的计划轮";
|
|
140
|
+
}
|
|
141
|
+
const tasksPath = join(changeRoot, "tasks.md");
|
|
142
|
+
const referenceErrors = validateExecutionRequirementDocumentReferences(changeRoot, parseExecutionRequirements(readFileSync(tasksPath, "utf8")));
|
|
143
|
+
if (referenceErrors.length > 0)
|
|
144
|
+
return referenceErrors.join(";");
|
|
145
|
+
const proposalPath = join(changeRoot, "proposal.md");
|
|
146
|
+
if (!existsSync(proposalPath))
|
|
147
|
+
return "proposal.md 不存在";
|
|
148
|
+
const impact = validateProposalImpact(readFileSync(proposalPath, "utf8"));
|
|
149
|
+
if (!impact.ok)
|
|
150
|
+
return impact.message;
|
|
151
|
+
const native = validateOpenSpecChange(projectRoot, change);
|
|
152
|
+
return native.ok ? null : native.message;
|
|
153
|
+
}
|
|
154
|
+
function validatePlanningPreflight(projectRoot, change, changeRoot, risk, executionPolicy, profile) {
|
|
155
|
+
const executionRequirementVersion = profile?.version ?? 1;
|
|
156
|
+
const tasksPlanError = validateTasksPlan(changeRoot, executionRequirementVersion);
|
|
157
|
+
if (tasksPlanError)
|
|
158
|
+
return { error: tasksPlanError, contractMode: false };
|
|
159
|
+
const executionRequirementPlan = validateExecutionRequirementPlan(changeRoot, executionPolicy, executionRequirementVersion);
|
|
160
|
+
if (!executionRequirementPlan.ok)
|
|
161
|
+
return { error: executionRequirementPlan.message, contractMode: false };
|
|
162
|
+
const missingArtifact = missingBaseArtifact(changeRoot, risk);
|
|
163
|
+
if (missingArtifact)
|
|
164
|
+
return { error: missingArtifact, contractMode: false };
|
|
165
|
+
return {
|
|
166
|
+
error: validateOpenSpecPlanningDocuments(projectRoot, change, changeRoot, profile),
|
|
167
|
+
contractMode: executionRequirementPlan.mode,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
125
170
|
export function proposalDocsBaseline(changeRoot) {
|
|
126
171
|
// 与 Propose gate 的可修改审查目标保持同一来源:specs/ 用目录聚合指纹,避免 reopen 基线漏掉任一个可修改的计划材料。
|
|
127
172
|
const docs = PROPOSE_FINAL_REVIEW_GATE.reviewTargets;
|
|
@@ -140,6 +185,11 @@ export function discoveryDocsBaseline(changeRoot) {
|
|
|
140
185
|
}
|
|
141
186
|
return baseline;
|
|
142
187
|
}
|
|
188
|
+
/** 新 Explore 轮次冻结已有已确认事项,避免把历史答复当作本轮遗漏。 */
|
|
189
|
+
export function exploreAnswerRegistrationPayloadForChange(changeRoot) {
|
|
190
|
+
const discoveryPath = join(changeRoot, ".superspec", "artifacts", "discovery.md");
|
|
191
|
+
return exploreAnswerRegistrationPayload(existsSync(discoveryPath) ? readFileSync(discoveryPath, "utf8") : null);
|
|
192
|
+
}
|
|
143
193
|
function isDigestMap(value) {
|
|
144
194
|
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
145
195
|
return false;
|
|
@@ -234,6 +284,62 @@ function executionRequirementVersionForProposeRound(events) {
|
|
|
234
284
|
}
|
|
235
285
|
return 1;
|
|
236
286
|
}
|
|
287
|
+
function isPlanningValidationProfile(value) {
|
|
288
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
289
|
+
return false;
|
|
290
|
+
const profile = value;
|
|
291
|
+
if (profile.version !== 2 || !profile.openspec || typeof profile.openspec !== "object")
|
|
292
|
+
return false;
|
|
293
|
+
return profile.openspec.mode === "disabled" ||
|
|
294
|
+
profile.openspec.mode === "strict" && typeof profile.openspec.config_digest === "string";
|
|
295
|
+
}
|
|
296
|
+
/** 新 planning round 在进入 propose 时冻结当前 OpenSpec 校验契约。 */
|
|
297
|
+
export function planningValidationProfileForNewRound(projectRoot) {
|
|
298
|
+
const configDigest = sha256File(join(projectRoot, "openspec", "config.yaml"));
|
|
299
|
+
return configDigest == null
|
|
300
|
+
? { version: 2, openspec: { mode: "disabled" } }
|
|
301
|
+
: { version: 2, openspec: { mode: "strict", config_digest: configDigest } };
|
|
302
|
+
}
|
|
303
|
+
/** propose 状态尚未 ready 时,从进入本 planning round 的事件读取冻结 profile。 */
|
|
304
|
+
function planningValidationProfileForPendingProposeRound(events) {
|
|
305
|
+
for (let i = events.length - 1; i >= 0; i--) {
|
|
306
|
+
const event = events[i];
|
|
307
|
+
if (event.event_type !== "transition_commit")
|
|
308
|
+
continue;
|
|
309
|
+
const payload = event.payload;
|
|
310
|
+
// 只读取“进入 propose”的边界事件。propose-ready 创建审查 job 时也会
|
|
311
|
+
// 保持在 propose;若把它误当作新的 planning round,便会覆盖此前冻结的
|
|
312
|
+
// profile 并把当前轮错误降级为 v1。
|
|
313
|
+
const entersPropose = payload.to_state === "propose" && (payload.transition === "explore" ||
|
|
314
|
+
payload.transition === "propose" ||
|
|
315
|
+
payload.transition === "reopen" && payload.reopen_target === "propose");
|
|
316
|
+
if (!entersPropose)
|
|
317
|
+
continue;
|
|
318
|
+
return isPlanningValidationProfile(payload.planning_validation_profile)
|
|
319
|
+
? payload.planning_validation_profile
|
|
320
|
+
: null;
|
|
321
|
+
}
|
|
322
|
+
return null;
|
|
323
|
+
}
|
|
324
|
+
/** 已完成 propose-ready 的 round 只回放当时冻结的 profile。 */
|
|
325
|
+
function planningValidationProfileForReadyProposeRound(events) {
|
|
326
|
+
for (let i = events.length - 1; i >= 0; i--) {
|
|
327
|
+
const event = events[i];
|
|
328
|
+
if (event.event_type !== "transition_commit")
|
|
329
|
+
continue;
|
|
330
|
+
const payload = event.payload;
|
|
331
|
+
if (payload.transition !== "propose-ready" || payload.to_state !== "propose_ready")
|
|
332
|
+
continue;
|
|
333
|
+
if (isPlanningValidationProfile(payload.planning_validation_profile))
|
|
334
|
+
return payload.planning_validation_profile;
|
|
335
|
+
// 过渡期已写 v2 执行依据、但尚未带 profile 的事件保持 v2 tasks 契约,
|
|
336
|
+
// 但不在 start-apply 追溯新增 strict gate。
|
|
337
|
+
return executionRequirementVersionFromPayload(event.payload) === 2
|
|
338
|
+
? { version: 2, openspec: { mode: "disabled" } }
|
|
339
|
+
: null;
|
|
340
|
+
}
|
|
341
|
+
return null;
|
|
342
|
+
}
|
|
237
343
|
export function applyRequirementModeForCurrentRound(events) {
|
|
238
344
|
const index = latestStartApplyIndex(events);
|
|
239
345
|
if (index < 0)
|
|
@@ -352,10 +458,6 @@ export function planNextStep(context) {
|
|
|
352
458
|
reason: "回到 explore 后至少一个 discovery 材料必须变化",
|
|
353
459
|
};
|
|
354
460
|
}
|
|
355
|
-
const exploreReviewJobs = EXPLORE_DISCOVERY_REVIEW_GATE.openJobsForGate(snapshot);
|
|
356
|
-
if (exploreReviewJobs.length > 0) {
|
|
357
|
-
return requiredJobs("explore", exploreReviewJobs, `有 ${exploreReviewJobs.length} 个待完成探索审查工作项`);
|
|
358
|
-
}
|
|
359
461
|
const discoveryCheck = validateDiscovery(changeRoot);
|
|
360
462
|
if (!discoveryCheck.ok) {
|
|
361
463
|
const ask = {
|
|
@@ -366,14 +468,32 @@ export function planNextStep(context) {
|
|
|
366
468
|
return { kind: "ask_user", state: "explore", ask, reason: discoveryCheck.message };
|
|
367
469
|
}
|
|
368
470
|
const content = readFileSync(join(changeRoot, ".superspec", "artifacts", "discovery.md"), "utf8");
|
|
369
|
-
const
|
|
370
|
-
if (
|
|
471
|
+
const currentQuestion = parseDiscoveryOpenQuestions(content)[0];
|
|
472
|
+
if (currentQuestion) {
|
|
473
|
+
const questionText = discoveryOpenQuestionDisplayText(currentQuestion);
|
|
371
474
|
const ask = {
|
|
372
|
-
question:
|
|
373
|
-
|
|
374
|
-
|
|
475
|
+
question: `现在有一件事需要你确认:${questionText}\n\n请只回答这一件事。主流程会先登记答复,再将结论回写 discovery.md;回写完成前会继续询问这一件事。`,
|
|
476
|
+
// 用户可以接受建议、选择其他方向或补充事实;状态机不解释答案语义。
|
|
477
|
+
allowed_answers: [],
|
|
478
|
+
scope: discoveryOpenQuestionScope(currentQuestion, currentExploreRoundId(events)),
|
|
479
|
+
};
|
|
480
|
+
return { kind: "ask_user", state: "explore", ask, reason: "等待用户确认" };
|
|
481
|
+
}
|
|
482
|
+
const unregisteredClosedQuestions = unregisteredClosedExploreQuestions(events, content);
|
|
483
|
+
if (unregisteredClosedQuestions.length > 0) {
|
|
484
|
+
const ask = {
|
|
485
|
+
question: "发现一项已标记为已确认的事项没有对应的答复登记。请先将该项恢复为待确认,按主流程重新登记用户答复并回写 discovery.md 后继续。",
|
|
486
|
+
allowed_answers: ["已处理"],
|
|
487
|
+
scope: "explore_answer_registration",
|
|
375
488
|
};
|
|
376
|
-
return { kind: "ask_user", state: "explore", ask, reason:
|
|
489
|
+
return { kind: "ask_user", state: "explore", ask, reason: "存在未登记答复的已确认事项" };
|
|
490
|
+
}
|
|
491
|
+
// 先让用户澄清当前 Discovery,再审查材料;否则 critic 会审查一份仍有
|
|
492
|
+
// 关键业务未知的文档。正常创建的 job 已绑定 discovery 指纹,材料变化后
|
|
493
|
+
// 会由 snapshot freshness 自动失效。
|
|
494
|
+
const exploreReviewJobs = EXPLORE_DISCOVERY_REVIEW_GATE.openJobsForGate(snapshot);
|
|
495
|
+
if (exploreReviewJobs.length > 0) {
|
|
496
|
+
return requiredJobs("explore", exploreReviewJobs, `有 ${exploreReviewJobs.length} 个待完成探索审查工作项`);
|
|
377
497
|
}
|
|
378
498
|
const requiredRoles = EXPLORE_DISCOVERY_REVIEW_GATE.requiredRolesForRisk(mode.risk);
|
|
379
499
|
if (!reviewGatePlan(snapshot, events, changeRoot, EXPLORE_DISCOVERY_REVIEW_GATE, requiredRoles)) {
|
|
@@ -542,6 +662,16 @@ function planApplyDoneNext(context) {
|
|
|
542
662
|
}
|
|
543
663
|
const latest = facts.latestTerminal;
|
|
544
664
|
if (latest?.state === "rejected" && latest.result_kind === "review_failed") {
|
|
665
|
+
const staleReason = codeReviewJobStaleReason(context.projectRoot, latest.job, currentWorkingPaths);
|
|
666
|
+
if (staleReason) {
|
|
667
|
+
return {
|
|
668
|
+
kind: "run_transition",
|
|
669
|
+
state: "apply_done",
|
|
670
|
+
transition: "review-ready",
|
|
671
|
+
risk: mode.risk,
|
|
672
|
+
reason: `代码审查结论已过期,重新发起审查:${staleReason}`,
|
|
673
|
+
};
|
|
674
|
+
}
|
|
545
675
|
const status = latestCodeReviewFailedStatus(events);
|
|
546
676
|
const pendingFinding = status?.unresolved[0] ?? null;
|
|
547
677
|
if (status && status.findings.length > 0 && !pendingFinding) {
|
|
@@ -704,7 +834,13 @@ export function planTransition(name, context) {
|
|
|
704
834
|
function planExploreTransition(context) {
|
|
705
835
|
const { changeRoot, events, mode, projectRoot, snapshot } = context;
|
|
706
836
|
if (snapshot.state === "init") {
|
|
707
|
-
return {
|
|
837
|
+
return {
|
|
838
|
+
kind: "advance",
|
|
839
|
+
fromState: "init",
|
|
840
|
+
toState: "explore",
|
|
841
|
+
reason: "进入探索阶段",
|
|
842
|
+
payload: exploreAnswerRegistrationPayloadForChange(changeRoot),
|
|
843
|
+
};
|
|
708
844
|
}
|
|
709
845
|
if (snapshot.state !== "explore") {
|
|
710
846
|
return { kind: "skip", message: `当前状态 ${snapshot.state},explore 不适用` };
|
|
@@ -716,6 +852,14 @@ function planExploreTransition(context) {
|
|
|
716
852
|
const discoveryCheck = validateDiscovery(changeRoot);
|
|
717
853
|
if (!discoveryCheck.ok)
|
|
718
854
|
return { kind: "skip", message: discoveryCheck.message };
|
|
855
|
+
const discoveryContent = readFileSync(join(changeRoot, ".superspec", "artifacts", "discovery.md"), "utf8");
|
|
856
|
+
const currentQuestion = parseDiscoveryOpenQuestions(discoveryContent)[0];
|
|
857
|
+
if (currentQuestion) {
|
|
858
|
+
return { kind: "skip", message: "discovery.md 仍有需要确认的事项,请先完成确认并回写 discovery.md" };
|
|
859
|
+
}
|
|
860
|
+
if (unregisteredClosedExploreQuestions(events, discoveryContent).length > 0) {
|
|
861
|
+
return { kind: "skip", message: "discovery.md 有已确认事项缺少对应答复登记,请先恢复为待确认并按主流程登记答复" };
|
|
862
|
+
}
|
|
719
863
|
const requiredRoles = EXPLORE_DISCOVERY_REVIEW_GATE.requiredRolesForRisk(mode.risk);
|
|
720
864
|
const gatePlan = reviewGatePlan(snapshot, events, changeRoot, EXPLORE_DISCOVERY_REVIEW_GATE, requiredRoles);
|
|
721
865
|
if (gatePlan)
|
|
@@ -730,7 +874,11 @@ function planExploreTransition(context) {
|
|
|
730
874
|
fromState: "explore",
|
|
731
875
|
toState: "propose",
|
|
732
876
|
reason: "探索完成",
|
|
733
|
-
payload:
|
|
877
|
+
payload: {
|
|
878
|
+
...phaseConfirmationCommitPayload(confirmation, decision),
|
|
879
|
+
planning_validation_version: 2,
|
|
880
|
+
planning_validation_profile: planningValidationProfileForNewRound(projectRoot),
|
|
881
|
+
},
|
|
734
882
|
};
|
|
735
883
|
}
|
|
736
884
|
function planProposeReadyTransition(context) {
|
|
@@ -738,20 +886,15 @@ function planProposeReadyTransition(context) {
|
|
|
738
886
|
const risk = mode.risk;
|
|
739
887
|
if (snapshot.state !== "propose")
|
|
740
888
|
return { kind: "skip", message: `当前状态 ${snapshot.state},不能 propose-ready` };
|
|
741
|
-
const
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
if (!executionRequirementPlan.ok)
|
|
746
|
-
return { kind: "skip", message: executionRequirementPlan.message };
|
|
889
|
+
const planningProfile = planningValidationProfileForPendingProposeRound(context.events);
|
|
890
|
+
const preflight = validatePlanningPreflight(context.projectRoot, context.change, changeRoot, risk, executionPolicyForRisk(risk), planningProfile);
|
|
891
|
+
if (preflight.error)
|
|
892
|
+
return { kind: "skip", message: preflight.error };
|
|
747
893
|
const openQuestions = collectProposeOpenQuestions(changeRoot);
|
|
748
894
|
if (openQuestions.openCount > 0) {
|
|
749
895
|
const files = openQuestions.files.map(f => `${f.path}(${f.openCount})`).join(", ");
|
|
750
896
|
return { kind: "skip", message: `计划文档有 ${openQuestions.openCount} 个待用户确认问题:${files}` };
|
|
751
897
|
}
|
|
752
|
-
const missingArtifact = missingBaseArtifact(changeRoot, risk);
|
|
753
|
-
if (missingArtifact)
|
|
754
|
-
return { kind: "skip", message: missingArtifact };
|
|
755
898
|
const requiredRoles = PROPOSE_FINAL_REVIEW_GATE.requiredRolesForRisk(risk);
|
|
756
899
|
const gatePlan = reviewGatePlan(snapshot, context.events, changeRoot, PROPOSE_FINAL_REVIEW_GATE, requiredRoles);
|
|
757
900
|
if (gatePlan)
|
|
@@ -763,7 +906,11 @@ function planProposeReadyTransition(context) {
|
|
|
763
906
|
reason: `risk=${risk},所有需求已满足`,
|
|
764
907
|
payload: {
|
|
765
908
|
workflow_mode: risk,
|
|
766
|
-
|
|
909
|
+
...(planningProfile ? {
|
|
910
|
+
execution_requirement_version: 2,
|
|
911
|
+
planning_validation_version: 2,
|
|
912
|
+
planning_validation_profile: planningProfile,
|
|
913
|
+
} : {}),
|
|
767
914
|
},
|
|
768
915
|
};
|
|
769
916
|
}
|
|
@@ -782,7 +929,12 @@ function planStartApplyTransition(context, enforceConfirmation = true) {
|
|
|
782
929
|
return { kind: "skip", message: `回到 propose 后至少一个计划文档必须变化(基线绑定:${Object.keys(reopenBaseline).join("、")})` };
|
|
783
930
|
}
|
|
784
931
|
const risk = workflowRiskForProposeRound(events, context.mode.risk);
|
|
785
|
-
const
|
|
932
|
+
const planningProfile = planningValidationProfileForReadyProposeRound(events);
|
|
933
|
+
const executionRequirementVersion = planningProfile?.version ?? executionRequirementVersionForProposeRound(events);
|
|
934
|
+
const executionPolicy = executionPolicyForRisk(risk);
|
|
935
|
+
const preflight = validatePlanningPreflight(projectRoot, context.change, changeRoot, risk, executionPolicy, planningProfile);
|
|
936
|
+
if (preflight.error)
|
|
937
|
+
return { kind: "skip", message: preflight.error };
|
|
786
938
|
const requiredRoles = executionRequirementVersion === 2
|
|
787
939
|
? PROPOSE_FINAL_REVIEW_GATE.requiredRolesForRisk(risk)
|
|
788
940
|
: historicalProposeReadyRoles(events);
|
|
@@ -796,10 +948,6 @@ function planStartApplyTransition(context, enforceConfirmation = true) {
|
|
|
796
948
|
const acceptedConfirmation = enforceConfirmation
|
|
797
949
|
? acceptedProposeToApplyConfirmation(context, risk)
|
|
798
950
|
: null;
|
|
799
|
-
const executionPolicy = executionPolicyForRisk(risk);
|
|
800
|
-
const executionRequirementPlan = validateExecutionRequirementPlan(changeRoot, executionPolicy, executionRequirementVersion);
|
|
801
|
-
if (!executionRequirementPlan.ok)
|
|
802
|
-
return { kind: "skip", message: executionRequirementPlan.message };
|
|
803
951
|
if (enforceConfirmation && !acceptedConfirmation) {
|
|
804
952
|
const confirmation = phaseConfirmationForBoundary(projectRoot, events, snapshot, "propose_to_apply", risk);
|
|
805
953
|
return {
|
|
@@ -816,7 +964,7 @@ function planStartApplyTransition(context, enforceConfirmation = true) {
|
|
|
816
964
|
payload: {
|
|
817
965
|
apply_start_head: gitHead.head,
|
|
818
966
|
apply_start_head_reason: gitHead.reason,
|
|
819
|
-
apply_contract_mode:
|
|
967
|
+
apply_contract_mode: preflight.contractMode,
|
|
820
968
|
...(executionRequirementVersion === 2 ? { execution_requirement_version: 2 } : {}),
|
|
821
969
|
execution_policy: executionPolicy,
|
|
822
970
|
workflow_mode: risk,
|