@peterxiaoyang/superspec 0.1.43 → 0.1.45
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 +4 -3
- package/dist/format.js +53 -29
- package/dist/git_state.d.ts +12 -1
- package/dist/git_state.js +46 -1
- 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/phase_confirmation.d.ts +6 -0
- package/dist/phase_confirmation.js +51 -7
- package/dist/phase_plan.d.ts +9 -2
- package/dist/phase_plan.js +178 -45
- package/dist/record.d.ts +1 -1
- package/dist/record.js +102 -10
- package/dist/review.d.ts +48 -1
- package/dist/review.js +108 -4
- package/dist/review_job_gates.d.ts +5 -0
- package/dist/review_job_gates.js +52 -1
- package/dist/sync.js +13 -5
- 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 +172 -38
- package/dist/types.d.ts +25 -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/executor.toml +1 -1
- package/templates/workflow/agents/test-runner.toml +1 -1
- package/templates/workflow/prompts/architect.md +1 -1
- package/templates/workflow/prompts/code-reviewer.md +2 -2
- package/templates/workflow/prompts/critic.md +4 -6
- package/templates/workflow/prompts/executor.md +2 -2
- package/templates/workflow/prompts/test-engineer.md +5 -6
- package/templates/workflow/prompts/test-runner.md +1 -1
- package/templates/workflow/prompts/verifier.md +1 -1
- package/templates/workflow/skills/superspec-apply/SKILL.md +13 -71
- package/templates/workflow/skills/superspec-explore/SKILL.md +7 -9
- package/templates/workflow/skills/superspec-propose/SKILL.md +36 -48
- package/templates/workflow/skills/superspec-review/SKILL.md +21 -50
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { currentGitHead, dirtyCodeFiles } from "./git_state.js";
|
|
2
|
-
import { reviewEvidenceDigest } from "./review.js";
|
|
2
|
+
import { historicalProposeReadyRoles, reviewEvidenceDigest, reviewGateRoleResolution } from "./review.js";
|
|
3
|
+
import { EXPLORE_DISCOVERY_REVIEW_GATE, PROPOSE_FINAL_REVIEW_GATE } from "./review_job_gates.js";
|
|
4
|
+
import { changeRoot as openspecChangeRoot } from "./openspec.js";
|
|
3
5
|
import { findLatestEvent, sha256Text } from "./store.js";
|
|
6
|
+
import { hasFrozenWorkflowModeForProposeRound, workflowRiskForProject, workflowRiskForState, } from "./workflow_config.js";
|
|
4
7
|
export const PHASE_CONFIRMATION_SCOPE_PREFIX = "phase_confirmation:";
|
|
5
8
|
function latestTransition(events, predicate) {
|
|
6
9
|
return findLatestEvent(events, "transition_commit", event => predicate(event.payload));
|
|
@@ -23,7 +26,7 @@ const SPECS = {
|
|
|
23
26
|
},
|
|
24
27
|
],
|
|
25
28
|
scopePrefix: `${PHASE_CONFIRMATION_SCOPE_PREFIX}explore_to_propose`,
|
|
26
|
-
epoch: events => latestTransition(events, payload => payload.
|
|
29
|
+
epoch: events => latestTransition(events, payload => payload.to_state === "explore" && payload.from_state !== "explore"),
|
|
27
30
|
},
|
|
28
31
|
propose_to_apply: {
|
|
29
32
|
state: "propose_ready",
|
|
@@ -63,7 +66,21 @@ function boundaryForState(state) {
|
|
|
63
66
|
default: return null;
|
|
64
67
|
}
|
|
65
68
|
}
|
|
66
|
-
function
|
|
69
|
+
function ordinaryReviewRolesForBoundary(events, boundary, gate, risk) {
|
|
70
|
+
// 历史 plan 没有冻结 mode,当时的 gate 只能按实际创建过的角色回放。
|
|
71
|
+
if (boundary === "propose_to_apply" && !hasFrozenWorkflowModeForProposeRound(events)) {
|
|
72
|
+
return historicalProposeReadyRoles(events);
|
|
73
|
+
}
|
|
74
|
+
return gate.requiredRolesForRisk(risk);
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* 阶段确认不是 mode 的输入。它只读取当前 planning/apply round 的冻结快照;
|
|
78
|
+
* 仍处于 Explore/Propose 时才从项目配置获取候选 mode。
|
|
79
|
+
*/
|
|
80
|
+
export function workflowRiskForPhaseConfirmation(projectRoot, events, snapshot) {
|
|
81
|
+
return workflowRiskForState(events, snapshot.state, workflowRiskForProject(projectRoot));
|
|
82
|
+
}
|
|
83
|
+
function materialDigest(projectRoot, events, snapshot, boundary, risk) {
|
|
67
84
|
const head = currentGitHead(projectRoot);
|
|
68
85
|
const dirty = dirtyCodeFiles(projectRoot);
|
|
69
86
|
const acceptedJobs = snapshot.accepted_jobs
|
|
@@ -77,10 +94,32 @@ function materialDigest(projectRoot, events, snapshot) {
|
|
|
77
94
|
.sort((a, b) => a.job_id.localeCompare(b.job_id));
|
|
78
95
|
const documents = Object.entries(snapshot.document_digests)
|
|
79
96
|
.sort(([left], [right]) => left.localeCompare(right));
|
|
97
|
+
const ordinaryReviewGate = boundary === "explore_to_propose"
|
|
98
|
+
? EXPLORE_DISCOVERY_REVIEW_GATE
|
|
99
|
+
: boundary === "propose_to_apply"
|
|
100
|
+
? PROPOSE_FINAL_REVIEW_GATE
|
|
101
|
+
: null;
|
|
102
|
+
const currentChangeRoot = openspecChangeRoot(projectRoot, snapshot.change_id);
|
|
103
|
+
const overriddenReviewJobs = ordinaryReviewGate
|
|
104
|
+
? ordinaryReviewRolesForBoundary(events, boundary, ordinaryReviewGate, risk)
|
|
105
|
+
.map(role => reviewGateRoleResolution(events, currentChangeRoot, ordinaryReviewGate, role))
|
|
106
|
+
.filter(resolution => resolution.kind === "overridden")
|
|
107
|
+
.map(resolution => ({
|
|
108
|
+
job_id: resolution.override.job_id,
|
|
109
|
+
role: resolution.override.role,
|
|
110
|
+
gate_id: resolution.override.gate_id,
|
|
111
|
+
packet_digest: resolution.override.packet_digest,
|
|
112
|
+
decision_event_id: resolution.override.decision_event_id,
|
|
113
|
+
decision_event_digest: resolution.override.decision_event_digest,
|
|
114
|
+
}))
|
|
115
|
+
.sort((left, right) => `${left.gate_id}\u0000${left.role}\u0000${left.job_id}`.localeCompare(`${right.gate_id}\u0000${right.role}\u0000${right.job_id}`))
|
|
116
|
+
: [];
|
|
80
117
|
return sha256Text(JSON.stringify({
|
|
118
|
+
review_risk: risk,
|
|
81
119
|
documents,
|
|
82
120
|
tasks_structure_digest: snapshot.tasks_structure_digest,
|
|
83
121
|
accepted_jobs: acceptedJobs,
|
|
122
|
+
...(overriddenReviewJobs.length > 0 ? { overridden_review_jobs: overriddenReviewJobs } : {}),
|
|
84
123
|
review_evidence_digest: reviewEvidenceDigest(events),
|
|
85
124
|
code_state: {
|
|
86
125
|
head: head.head,
|
|
@@ -93,14 +132,13 @@ function materialDigest(projectRoot, events, snapshot) {
|
|
|
93
132
|
function phaseRecordArgv(change) {
|
|
94
133
|
return ["superspec", "record", "user-decision", "--change", change, "--input", "-"];
|
|
95
134
|
}
|
|
96
|
-
function nextArgv(change,
|
|
135
|
+
function nextArgv(change, _risk) {
|
|
97
136
|
return [
|
|
98
137
|
"superspec",
|
|
99
138
|
"transition",
|
|
100
139
|
"next",
|
|
101
140
|
"--change",
|
|
102
141
|
change,
|
|
103
|
-
...(risk === "strict" ? [] : ["--risk", risk]),
|
|
104
142
|
];
|
|
105
143
|
}
|
|
106
144
|
function buildActions(change, boundary, scope, question, specs, risk) {
|
|
@@ -126,7 +164,7 @@ function buildActions(change, boundary, scope, question, specs, risk) {
|
|
|
126
164
|
reason: spec.reason,
|
|
127
165
|
...(spec.reasonPrompt ? { reason_prompt: spec.reasonPrompt } : {}),
|
|
128
166
|
record_argv: phaseRecordArgv(change),
|
|
129
|
-
record_input: { scope, question, answer: spec.label },
|
|
167
|
+
record_input: { scope, question, answer: spec.label, review_risk: risk },
|
|
130
168
|
resume: spec.resume.kind === "next"
|
|
131
169
|
? { kind: "next", argv: nextArgv(change, risk) }
|
|
132
170
|
: spec.resume.kind === "continue_current_phase"
|
|
@@ -147,7 +185,7 @@ export function phaseConfirmationForBoundary(projectRoot, events, snapshot, boun
|
|
|
147
185
|
return null;
|
|
148
186
|
const epoch = spec.epoch(events);
|
|
149
187
|
const epochEventId = epoch?.event_id ?? `legacy-${spec.state}`;
|
|
150
|
-
const digest = materialDigest(projectRoot, events, snapshot);
|
|
188
|
+
const digest = materialDigest(projectRoot, events, snapshot, boundary, risk);
|
|
151
189
|
const scope = `${spec.scopePrefix}:${epochEventId}:${digest}`;
|
|
152
190
|
const actions = buildActions(snapshot.change_id, boundary, scope, spec.question, spec.actions, risk);
|
|
153
191
|
return {
|
|
@@ -192,6 +230,11 @@ export function latestAcceptedPhaseDecision(events, confirmation) {
|
|
|
192
230
|
scope: confirmation.scope,
|
|
193
231
|
answer: payload.answer,
|
|
194
232
|
decision: payload.phase_confirmation.decision,
|
|
233
|
+
review_risk: payload.phase_confirmation.review_risk === "minimal" ||
|
|
234
|
+
payload.phase_confirmation.review_risk === "normal" ||
|
|
235
|
+
payload.phase_confirmation.review_risk === "strict"
|
|
236
|
+
? payload.phase_confirmation.review_risk
|
|
237
|
+
: "strict",
|
|
195
238
|
};
|
|
196
239
|
}
|
|
197
240
|
export function isPhaseAdvanceAuthorized(events, confirmation) {
|
|
@@ -209,6 +252,7 @@ export function phaseConfirmationCommitPayload(confirmation, decision) {
|
|
|
209
252
|
material_digest: confirmation.material_digest,
|
|
210
253
|
scope: confirmation.scope,
|
|
211
254
|
decision_event_id: decision.event.event_id,
|
|
255
|
+
review_risk: decision.review_risk,
|
|
212
256
|
},
|
|
213
257
|
};
|
|
214
258
|
}
|
package/dist/phase_plan.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ReviewGateRule } from "./review_job_gates.ts";
|
|
2
|
-
import type { AcceptedMaterialFollowupContinuation, AskUser, Event, Job, JobRole, State } from "./types.ts";
|
|
2
|
+
import type { AcceptedMaterialFollowupContinuation, AskUser, Event, ExecutionPolicy, Job, JobRole, State } from "./types.ts";
|
|
3
3
|
import type { Snapshot } from "./types.ts";
|
|
4
4
|
import type { ReviewRisk } from "./review.ts";
|
|
5
5
|
export type TransitionName = "explore" | "propose-ready" | "start-apply" | "task-start" | "task-complete" | "review-ready" | "reopen" | "accept";
|
|
@@ -64,6 +64,7 @@ export type TransitionDecisionPlan = {
|
|
|
64
64
|
kind: "blocked";
|
|
65
65
|
jobs: Job[];
|
|
66
66
|
reason: string;
|
|
67
|
+
details?: Record<string, unknown>;
|
|
67
68
|
} | {
|
|
68
69
|
kind: "create_gate_jobs";
|
|
69
70
|
gate: ReviewGateRule;
|
|
@@ -82,12 +83,18 @@ export interface ApplyPendingTaskStatus {
|
|
|
82
83
|
needsCompletionEvent: string[];
|
|
83
84
|
completedByEvent: string[];
|
|
84
85
|
}
|
|
86
|
+
export declare function executionPolicyForRisk(risk: ReviewRisk): ExecutionPolicy;
|
|
87
|
+
export declare function executionPolicyForCurrentRound(events: Event[]): ExecutionPolicy;
|
|
85
88
|
export declare function proposalDocsBaseline(changeRoot: string): Record<string, string>;
|
|
89
|
+
export declare function discoveryDocsBaseline(changeRoot: string): Record<string, string>;
|
|
86
90
|
export declare function latestAcceptedProposalBaseline(events: Event[]): Record<string, string> | null;
|
|
87
91
|
export declare function latestReopenProposeBaseline(events: Event[]): Record<string, string> | null;
|
|
92
|
+
export declare function latestReopenExploreBaseline(events: Event[]): Record<string, string> | null;
|
|
88
93
|
export declare function proposalDocsChangedSinceBaseline(changeRoot: string, baseline: Record<string, string>): boolean;
|
|
89
|
-
export declare function
|
|
94
|
+
export declare function discoveryDocsChangedSinceBaseline(changeRoot: string, baseline: Record<string, string>): boolean;
|
|
90
95
|
export declare function pendingTaskIds(changeRoot: string): string[];
|
|
96
|
+
/** 当前 Apply round 的规则版本;缺失版本的历史 event 保持 v1 回放。 */
|
|
97
|
+
export declare function executionRequirementVersionForCurrentRound(events: Event[]): 1 | 2;
|
|
91
98
|
export declare function applyRequirementModeForCurrentRound(events: Event[]): boolean;
|
|
92
99
|
export declare function pendingTaskStatusForApply(changeRoot: string, events: Event[]): ApplyPendingTaskStatus;
|
|
93
100
|
export declare function formatPendingTaskMessage(ids: string[], action: string): string;
|
package/dist/phase_plan.js
CHANGED
|
@@ -4,15 +4,18 @@ import { EXPLORE_DISCOVERY_REVIEW_GATE, PROPOSE_FINAL_REVIEW_GATE } from "./revi
|
|
|
4
4
|
import { collectProposeOpenQuestions, countDiscoveryOpenQuestions, parseTasksMd, pendingTasksInContent, validateDiscovery, validateExecutionRequirements, } from "./format.js";
|
|
5
5
|
import { currentGitHead } from "./git_state.js";
|
|
6
6
|
import { docRef, sha256File } from "./store.js";
|
|
7
|
-
import { isReviewReadyVerifier, isFreshReviewVerifier, readReviewPolicyFromEvents, reviewEvidenceDigest, } from "./review.js";
|
|
7
|
+
import { isReviewReadyVerifier, isFreshReviewVerifier, historicalProposeReadyRoles, readReviewPolicyFromEvents, reviewGateRoleResolution, reviewRejectionOverrideScope, reviewEvidenceDigest, } from "./review.js";
|
|
8
8
|
import { CODE_REVIEW_DECISION_ANSWER_LABELS, CODE_REVIEW_REPAIR_SCOPE_PREFIX, codeReviewDecisionScope, codeReviewJobStaleReason, collectCodeReviewGateFacts, currentCodeReviewWorkingPaths, latestCodeReviewFailedStatus, requiresFinalVerifierForCurrentReview, scanCodeChangesForReview, } from "./code_review.js";
|
|
9
9
|
import { isPhaseAdvanceAuthorized, latestAcceptedPhaseDecision, phaseConfirmationCommitPayload, phaseConfirmationForBoundary, phaseConfirmationMissingMessage, } from "./phase_confirmation.js";
|
|
10
10
|
import { taskEvidenceReadiness } from "./task_evidence.js";
|
|
11
|
+
import { workflowRiskForProposeRound, workflowRiskForState } from "./workflow_config.js";
|
|
11
12
|
function requiredJobs(state, jobs, reason) {
|
|
12
13
|
return { kind: "required_jobs", state, jobs, reason };
|
|
13
14
|
}
|
|
14
15
|
function phaseConfirmationStep(context, boundary, reason) {
|
|
15
|
-
|
|
16
|
+
// 进入 propose_ready / apply 后,mode 来自本轮快照而非当前配置。
|
|
17
|
+
const risk = workflowRiskForState(context.events, context.snapshot.state, context.mode.risk);
|
|
18
|
+
const confirmation = phaseConfirmationForBoundary(context.projectRoot, context.events, context.snapshot, boundary, risk);
|
|
16
19
|
if (!confirmation || isPhaseAdvanceAuthorized(context.events, confirmation))
|
|
17
20
|
return null;
|
|
18
21
|
return {
|
|
@@ -22,16 +25,49 @@ function phaseConfirmationStep(context, boundary, reason) {
|
|
|
22
25
|
reason,
|
|
23
26
|
};
|
|
24
27
|
}
|
|
25
|
-
function reviewGatePlan(snapshot, gate, requiredRoles) {
|
|
28
|
+
function reviewGatePlan(snapshot, events, changeRoot, gate, requiredRoles) {
|
|
26
29
|
const missingRoles = [];
|
|
27
30
|
for (const role of requiredRoles) {
|
|
28
31
|
const openForRole = snapshot.open_jobs.find(job => gate.isJobForGate(job) && job.role === role);
|
|
29
32
|
if (openForRole) {
|
|
30
33
|
return { kind: "blocked", reason: `状态未推进;已有待完成工作项 ${role}(${openForRole.job_id})`, jobs: [openForRole] };
|
|
31
34
|
}
|
|
32
|
-
const
|
|
33
|
-
if (
|
|
34
|
-
|
|
35
|
+
const resolution = reviewGateRoleResolution(events, changeRoot, gate, role);
|
|
36
|
+
if (resolution.kind === "accepted" || resolution.kind === "overridden")
|
|
37
|
+
continue;
|
|
38
|
+
if (resolution.kind === "rejected_pending") {
|
|
39
|
+
const terminal = resolution.terminal;
|
|
40
|
+
const overrideScope = reviewRejectionOverrideScope(terminal.job.job_id);
|
|
41
|
+
return {
|
|
42
|
+
kind: "blocked",
|
|
43
|
+
jobs: [],
|
|
44
|
+
reason: `状态未推进;${role} 审查工作项 ${terminal.job.job_id} 已拒绝。请修改绑定材料、在整份报告没有有效 blocker 时登记整体裁决,或在涉及业务决定时询问用户`,
|
|
45
|
+
details: {
|
|
46
|
+
review_rejection: {
|
|
47
|
+
job_id: terminal.job.job_id,
|
|
48
|
+
role,
|
|
49
|
+
gate_id: gate.gate_id,
|
|
50
|
+
packet_digest: terminal.job.packet_digest,
|
|
51
|
+
result_kind: terminal.result_kind,
|
|
52
|
+
reason: terminal.reason ?? "报告结论为 fail,工作项未通过",
|
|
53
|
+
override_scope: overrideScope,
|
|
54
|
+
allowed_actions: ["modify_materials", "record_override", "ask_user"],
|
|
55
|
+
record_input: {
|
|
56
|
+
scope: overrideScope,
|
|
57
|
+
answer: "do_not_block",
|
|
58
|
+
reason: "<说明整份报告为何没有有效 blocker>",
|
|
59
|
+
decision_source: "main_process",
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
const reason = resolution.kind === "stale"
|
|
66
|
+
? `需求 ${role} 的最新审查工作项已过期:${resolution.stale_reason}`
|
|
67
|
+
: resolution.kind === "rejected_invalid"
|
|
68
|
+
? `需求 ${role} 的最新审查报告无效,不能通过整体裁决绕过`
|
|
69
|
+
: `需求 ${role} 无已接受的工作项`;
|
|
70
|
+
missingRoles.push({ role, reason });
|
|
35
71
|
}
|
|
36
72
|
if (missingRoles.length > 0) {
|
|
37
73
|
return {
|
|
@@ -52,14 +88,26 @@ function validateTasksPlan(changeRoot) {
|
|
|
52
88
|
return "tasks.md 内容不像任务计划文档";
|
|
53
89
|
return null;
|
|
54
90
|
}
|
|
55
|
-
function
|
|
91
|
+
export function executionPolicyForRisk(risk) {
|
|
92
|
+
return risk === "strict" ? "tdd" : "green_only";
|
|
93
|
+
}
|
|
94
|
+
export function executionPolicyForCurrentRound(events) {
|
|
95
|
+
const index = latestStartApplyIndex(events);
|
|
96
|
+
if (index < 0)
|
|
97
|
+
return "tdd";
|
|
98
|
+
const payload = events[index].payload;
|
|
99
|
+
return payload.execution_policy === "green_only" || payload.execution_policy === "tdd"
|
|
100
|
+
? payload.execution_policy
|
|
101
|
+
: "tdd";
|
|
102
|
+
}
|
|
103
|
+
function validateExecutionRequirementPlan(changeRoot, executionPolicy, executionRequirementVersion = 2) {
|
|
56
104
|
const tasksPath = join(changeRoot, "tasks.md");
|
|
57
105
|
if (!existsSync(tasksPath))
|
|
58
106
|
return { ok: true, mode: false };
|
|
59
107
|
const tasksContent = readFileSync(tasksPath, "utf8");
|
|
60
108
|
const testContractPath = join(changeRoot, ".superspec", "artifacts", "test-contract.md");
|
|
61
109
|
const testContractContent = existsSync(testContractPath) ? readFileSync(testContractPath, "utf8") : null;
|
|
62
|
-
const validation = validateExecutionRequirements(tasksContent, testContractContent);
|
|
110
|
+
const validation = validateExecutionRequirements(tasksContent, testContractContent, executionPolicy, executionRequirementVersion);
|
|
63
111
|
return validation.ok
|
|
64
112
|
? { ok: true, mode: validation.mode }
|
|
65
113
|
: { ok: false, mode: validation.mode, message: validation.errors.join(";") };
|
|
@@ -68,7 +116,7 @@ function missingBaseArtifact(changeRoot, risk) {
|
|
|
68
116
|
if (risk === "minimal")
|
|
69
117
|
return null;
|
|
70
118
|
const artifactsDir = join(changeRoot, ".superspec", "artifacts");
|
|
71
|
-
for (const doc of ["discovery.md", "
|
|
119
|
+
for (const doc of ["discovery.md", "test-contract.md"]) {
|
|
72
120
|
if (!existsSync(join(artifactsDir, doc)))
|
|
73
121
|
return `基础职责缺失:${doc} 不存在(risk=${risk} 需要)`;
|
|
74
122
|
}
|
|
@@ -83,6 +131,15 @@ export function proposalDocsBaseline(changeRoot) {
|
|
|
83
131
|
}
|
|
84
132
|
return baseline;
|
|
85
133
|
}
|
|
134
|
+
export function discoveryDocsBaseline(changeRoot) {
|
|
135
|
+
// 与 Explore gate 使用同一组审查目标。回退到 explore 后,至少要更新一项
|
|
136
|
+
// discovery 材料,才允许重新进入 propose,避免把一次纯状态回退误当作新探索轮次。
|
|
137
|
+
const baseline = {};
|
|
138
|
+
for (const doc of EXPLORE_DISCOVERY_REVIEW_GATE.reviewTargets) {
|
|
139
|
+
baseline[doc] = docRef(changeRoot, doc).sha;
|
|
140
|
+
}
|
|
141
|
+
return baseline;
|
|
142
|
+
}
|
|
86
143
|
function isDigestMap(value) {
|
|
87
144
|
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
88
145
|
return false;
|
|
@@ -119,23 +176,27 @@ export function latestReopenProposeBaseline(events) {
|
|
|
119
176
|
}
|
|
120
177
|
return null;
|
|
121
178
|
}
|
|
179
|
+
export function latestReopenExploreBaseline(events) {
|
|
180
|
+
for (let i = events.length - 1; i >= 0; i--) {
|
|
181
|
+
const ev = events[i];
|
|
182
|
+
if (ev.event_type !== "transition_commit")
|
|
183
|
+
continue;
|
|
184
|
+
const payload = ev.payload;
|
|
185
|
+
if (payload.transition !== "reopen" || payload.reopen_target !== "explore")
|
|
186
|
+
continue;
|
|
187
|
+
if (!payload.baseline_docs || typeof payload.baseline_docs !== "object" || Array.isArray(payload.baseline_docs))
|
|
188
|
+
return null;
|
|
189
|
+
return payload.baseline_docs;
|
|
190
|
+
}
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
122
193
|
export function proposalDocsChangedSinceBaseline(changeRoot, baseline) {
|
|
123
194
|
const current = proposalDocsBaseline(changeRoot);
|
|
124
195
|
return Object.entries(baseline).some(([path, digest]) => current[path] !== digest);
|
|
125
196
|
}
|
|
126
|
-
export function
|
|
127
|
-
const
|
|
128
|
-
|
|
129
|
-
if (ev.event_type !== "transition_commit")
|
|
130
|
-
continue;
|
|
131
|
-
const newJobs = ev.payload.new_jobs ?? [];
|
|
132
|
-
for (const job of newJobs) {
|
|
133
|
-
if (PROPOSE_FINAL_REVIEW_GATE.isJobForGate(job) &&
|
|
134
|
-
(job.role === "critic" || job.role === "architect" || job.role === "test-engineer"))
|
|
135
|
-
roles.add(job.role);
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
return [...roles];
|
|
197
|
+
export function discoveryDocsChangedSinceBaseline(changeRoot, baseline) {
|
|
198
|
+
const current = discoveryDocsBaseline(changeRoot);
|
|
199
|
+
return Object.entries(baseline).some(([path, digest]) => current[path] !== digest);
|
|
139
200
|
}
|
|
140
201
|
export function pendingTaskIds(changeRoot) {
|
|
141
202
|
const tasksContent = readFileSync(join(changeRoot, "tasks.md"), "utf8");
|
|
@@ -152,6 +213,27 @@ function latestStartApplyIndex(events) {
|
|
|
152
213
|
}
|
|
153
214
|
return -1;
|
|
154
215
|
}
|
|
216
|
+
function executionRequirementVersionFromPayload(payload) {
|
|
217
|
+
return payload.execution_requirement_version === 2 ? 2 : 1;
|
|
218
|
+
}
|
|
219
|
+
/** 当前 Apply round 的规则版本;缺失版本的历史 event 保持 v1 回放。 */
|
|
220
|
+
export function executionRequirementVersionForCurrentRound(events) {
|
|
221
|
+
const index = latestStartApplyIndex(events);
|
|
222
|
+
return index < 0 ? 1 : executionRequirementVersionFromPayload(events[index].payload);
|
|
223
|
+
}
|
|
224
|
+
/** Propose-ready 的规则版本决定随后 start-apply 是否采用新的全任务声明要求。 */
|
|
225
|
+
function executionRequirementVersionForProposeRound(events) {
|
|
226
|
+
for (let i = events.length - 1; i >= 0; i--) {
|
|
227
|
+
const event = events[i];
|
|
228
|
+
if (event.event_type !== "transition_commit")
|
|
229
|
+
continue;
|
|
230
|
+
const payload = event.payload;
|
|
231
|
+
if (payload.transition === "propose-ready" && payload.to_state === "propose_ready") {
|
|
232
|
+
return executionRequirementVersionFromPayload(event.payload);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return 1;
|
|
236
|
+
}
|
|
155
237
|
export function applyRequirementModeForCurrentRound(events) {
|
|
156
238
|
const index = latestStartApplyIndex(events);
|
|
157
239
|
if (index < 0)
|
|
@@ -219,14 +301,13 @@ export function pendingTaskStatusForApply(changeRoot, events) {
|
|
|
219
301
|
export function formatPendingTaskMessage(ids, action) {
|
|
220
302
|
return `尚有未完成任务:${ids.join(", ")};${action}`;
|
|
221
303
|
}
|
|
222
|
-
function nextArgv(change,
|
|
304
|
+
function nextArgv(change, _risk) {
|
|
223
305
|
return [
|
|
224
306
|
"superspec",
|
|
225
307
|
"transition",
|
|
226
308
|
"next",
|
|
227
309
|
"--change",
|
|
228
310
|
change,
|
|
229
|
-
...(risk === "strict" ? [] : ["--risk", risk]),
|
|
230
311
|
];
|
|
231
312
|
}
|
|
232
313
|
function acceptedMaterialFollowup(change, risk, planDocsChangedSinceAccept) {
|
|
@@ -262,6 +343,15 @@ export function planNextStep(context) {
|
|
|
262
343
|
}
|
|
263
344
|
return { kind: "run_transition", state: "init", transition: "explore", reason: "初始化完成,开始探索" };
|
|
264
345
|
case "explore": {
|
|
346
|
+
const reopenBaseline = latestReopenExploreBaseline(events);
|
|
347
|
+
if (reopenBaseline && !discoveryDocsChangedSinceBaseline(changeRoot, reopenBaseline)) {
|
|
348
|
+
return {
|
|
349
|
+
kind: "run_transition",
|
|
350
|
+
state: "explore",
|
|
351
|
+
transition: "explore",
|
|
352
|
+
reason: "回到 explore 后至少一个 discovery 材料必须变化",
|
|
353
|
+
};
|
|
354
|
+
}
|
|
265
355
|
const exploreReviewJobs = EXPLORE_DISCOVERY_REVIEW_GATE.openJobsForGate(snapshot);
|
|
266
356
|
if (exploreReviewJobs.length > 0) {
|
|
267
357
|
return requiredJobs("explore", exploreReviewJobs, `有 ${exploreReviewJobs.length} 个待完成探索审查工作项`);
|
|
@@ -286,7 +376,7 @@ export function planNextStep(context) {
|
|
|
286
376
|
return { kind: "ask_user", state: "explore", ask, reason: `有 ${openQs} 个未确认问题` };
|
|
287
377
|
}
|
|
288
378
|
const requiredRoles = EXPLORE_DISCOVERY_REVIEW_GATE.requiredRolesForRisk(mode.risk);
|
|
289
|
-
if (!reviewGatePlan(snapshot, EXPLORE_DISCOVERY_REVIEW_GATE, requiredRoles)) {
|
|
379
|
+
if (!reviewGatePlan(snapshot, events, changeRoot, EXPLORE_DISCOVERY_REVIEW_GATE, requiredRoles)) {
|
|
290
380
|
const confirmation = phaseConfirmationStep(context, "explore_to_propose", "探索完成,等待用户确认进入计划阶段");
|
|
291
381
|
if (confirmation)
|
|
292
382
|
return confirmation;
|
|
@@ -333,6 +423,16 @@ export function planNextStep(context) {
|
|
|
333
423
|
if (confirmation)
|
|
334
424
|
return confirmation;
|
|
335
425
|
}
|
|
426
|
+
// 尚未获得用户确认时,按本次 next 的候选风险预检失败不能退回默认 strict
|
|
427
|
+
// 的 start-apply 命令;否则 normal/minimal 的策略不匹配会被静默改写为 strict。
|
|
428
|
+
if (startApplyPlan.kind === "skip") {
|
|
429
|
+
const ask = {
|
|
430
|
+
question: `${startApplyPlan.message}。请更新计划材料后重新执行 next。`,
|
|
431
|
+
allowed_answers: ["计划已更新"],
|
|
432
|
+
scope: "propose_apply_preflight",
|
|
433
|
+
};
|
|
434
|
+
return { kind: "ask_user", state: "propose_ready", ask, reason: startApplyPlan.message };
|
|
435
|
+
}
|
|
336
436
|
return { kind: "run_transition", state: "propose_ready", transition: "start-apply", reason: "计划就绪,开始执行" };
|
|
337
437
|
}
|
|
338
438
|
case "apply":
|
|
@@ -609,14 +709,18 @@ function planExploreTransition(context) {
|
|
|
609
709
|
if (snapshot.state !== "explore") {
|
|
610
710
|
return { kind: "skip", message: `当前状态 ${snapshot.state},explore 不适用` };
|
|
611
711
|
}
|
|
712
|
+
const reopenBaseline = latestReopenExploreBaseline(events);
|
|
713
|
+
if (reopenBaseline && !discoveryDocsChangedSinceBaseline(changeRoot, reopenBaseline)) {
|
|
714
|
+
return { kind: "skip", message: "回到 explore 后至少一个 discovery 材料必须变化" };
|
|
715
|
+
}
|
|
612
716
|
const discoveryCheck = validateDiscovery(changeRoot);
|
|
613
717
|
if (!discoveryCheck.ok)
|
|
614
718
|
return { kind: "skip", message: discoveryCheck.message };
|
|
615
719
|
const requiredRoles = EXPLORE_DISCOVERY_REVIEW_GATE.requiredRolesForRisk(mode.risk);
|
|
616
|
-
const gatePlan = reviewGatePlan(snapshot, EXPLORE_DISCOVERY_REVIEW_GATE, requiredRoles);
|
|
720
|
+
const gatePlan = reviewGatePlan(snapshot, events, changeRoot, EXPLORE_DISCOVERY_REVIEW_GATE, requiredRoles);
|
|
617
721
|
if (gatePlan)
|
|
618
722
|
return gatePlan;
|
|
619
|
-
const confirmation = phaseConfirmationForBoundary(projectRoot, events, snapshot, "explore_to_propose");
|
|
723
|
+
const confirmation = phaseConfirmationForBoundary(projectRoot, events, snapshot, "explore_to_propose", mode.risk);
|
|
620
724
|
const decision = confirmation ? latestAcceptedPhaseDecision(events, confirmation) : null;
|
|
621
725
|
if (!confirmation || decision?.decision !== "advance") {
|
|
622
726
|
return { kind: "skip", message: confirmation ? phaseConfirmationMissingMessage(confirmation) : "无法建立 Explore 阶段确认范围" };
|
|
@@ -637,7 +741,7 @@ function planProposeReadyTransition(context) {
|
|
|
637
741
|
const tasksPlanError = validateTasksPlan(changeRoot);
|
|
638
742
|
if (tasksPlanError)
|
|
639
743
|
return { kind: "skip", message: tasksPlanError };
|
|
640
|
-
const executionRequirementPlan = validateExecutionRequirementPlan(changeRoot);
|
|
744
|
+
const executionRequirementPlan = validateExecutionRequirementPlan(changeRoot, executionPolicyForRisk(risk), 2);
|
|
641
745
|
if (!executionRequirementPlan.ok)
|
|
642
746
|
return { kind: "skip", message: executionRequirementPlan.message };
|
|
643
747
|
const openQuestions = collectProposeOpenQuestions(changeRoot);
|
|
@@ -649,10 +753,24 @@ function planProposeReadyTransition(context) {
|
|
|
649
753
|
if (missingArtifact)
|
|
650
754
|
return { kind: "skip", message: missingArtifact };
|
|
651
755
|
const requiredRoles = PROPOSE_FINAL_REVIEW_GATE.requiredRolesForRisk(risk);
|
|
652
|
-
const gatePlan = reviewGatePlan(snapshot, PROPOSE_FINAL_REVIEW_GATE, requiredRoles);
|
|
756
|
+
const gatePlan = reviewGatePlan(snapshot, context.events, changeRoot, PROPOSE_FINAL_REVIEW_GATE, requiredRoles);
|
|
653
757
|
if (gatePlan)
|
|
654
758
|
return gatePlan;
|
|
655
|
-
return {
|
|
759
|
+
return {
|
|
760
|
+
kind: "advance",
|
|
761
|
+
fromState: "propose",
|
|
762
|
+
toState: "propose_ready",
|
|
763
|
+
reason: `risk=${risk},所有需求已满足`,
|
|
764
|
+
payload: {
|
|
765
|
+
workflow_mode: risk,
|
|
766
|
+
execution_requirement_version: 2,
|
|
767
|
+
},
|
|
768
|
+
};
|
|
769
|
+
}
|
|
770
|
+
function acceptedProposeToApplyConfirmation(context, risk) {
|
|
771
|
+
const confirmation = phaseConfirmationForBoundary(context.projectRoot, context.events, context.snapshot, "propose_to_apply", risk);
|
|
772
|
+
const decision = confirmation ? latestAcceptedPhaseDecision(context.events, confirmation) : null;
|
|
773
|
+
return confirmation && decision?.decision === "advance" ? { confirmation, decision } : null;
|
|
656
774
|
}
|
|
657
775
|
function planStartApplyTransition(context, enforceConfirmation = true) {
|
|
658
776
|
const { changeRoot, events, projectRoot, snapshot } = context;
|
|
@@ -663,23 +781,31 @@ function planStartApplyTransition(context, enforceConfirmation = true) {
|
|
|
663
781
|
// 按基线实际键名提示:升级前留下的旧基线可能不含 specs/,静态清单会误导
|
|
664
782
|
return { kind: "skip", message: `回到 propose 后至少一个计划文档必须变化(基线绑定:${Object.keys(reopenBaseline).join("、")})` };
|
|
665
783
|
}
|
|
666
|
-
const
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
784
|
+
const risk = workflowRiskForProposeRound(events, context.mode.risk);
|
|
785
|
+
const executionRequirementVersion = executionRequirementVersionForProposeRound(events);
|
|
786
|
+
const requiredRoles = executionRequirementVersion === 2
|
|
787
|
+
? PROPOSE_FINAL_REVIEW_GATE.requiredRolesForRisk(risk)
|
|
788
|
+
: historicalProposeReadyRoles(events);
|
|
789
|
+
const gatePlan = reviewGatePlan(snapshot, events, changeRoot, PROPOSE_FINAL_REVIEW_GATE, requiredRoles);
|
|
790
|
+
if (gatePlan) {
|
|
791
|
+
return {
|
|
792
|
+
...gatePlan,
|
|
793
|
+
reason: `进入执行阶段前需要重新完成计划文档审查:${gatePlan.reason}`,
|
|
794
|
+
};
|
|
675
795
|
}
|
|
676
|
-
const
|
|
796
|
+
const acceptedConfirmation = enforceConfirmation
|
|
797
|
+
? acceptedProposeToApplyConfirmation(context, risk)
|
|
798
|
+
: null;
|
|
799
|
+
const executionPolicy = executionPolicyForRisk(risk);
|
|
800
|
+
const executionRequirementPlan = validateExecutionRequirementPlan(changeRoot, executionPolicy, executionRequirementVersion);
|
|
677
801
|
if (!executionRequirementPlan.ok)
|
|
678
802
|
return { kind: "skip", message: executionRequirementPlan.message };
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
803
|
+
if (enforceConfirmation && !acceptedConfirmation) {
|
|
804
|
+
const confirmation = phaseConfirmationForBoundary(projectRoot, events, snapshot, "propose_to_apply", risk);
|
|
805
|
+
return {
|
|
806
|
+
kind: "skip",
|
|
807
|
+
message: confirmation ? phaseConfirmationMissingMessage(confirmation) : "无法建立 Propose 阶段确认范围",
|
|
808
|
+
};
|
|
683
809
|
}
|
|
684
810
|
const gitHead = currentGitHead(projectRoot);
|
|
685
811
|
return {
|
|
@@ -691,7 +817,14 @@ function planStartApplyTransition(context, enforceConfirmation = true) {
|
|
|
691
817
|
apply_start_head: gitHead.head,
|
|
692
818
|
apply_start_head_reason: gitHead.reason,
|
|
693
819
|
apply_contract_mode: executionRequirementPlan.mode,
|
|
694
|
-
...(
|
|
820
|
+
...(executionRequirementVersion === 2 ? { execution_requirement_version: 2 } : {}),
|
|
821
|
+
execution_policy: executionPolicy,
|
|
822
|
+
workflow_mode: risk,
|
|
823
|
+
review_policy: {
|
|
824
|
+
review_risk: risk,
|
|
825
|
+
requires_verifier: risk !== "minimal",
|
|
826
|
+
},
|
|
827
|
+
...(acceptedConfirmation ? phaseConfirmationCommitPayload(acceptedConfirmation.confirmation, acceptedConfirmation.decision) : {}),
|
|
695
828
|
},
|
|
696
829
|
};
|
|
697
830
|
}
|
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[];
|