@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
package/dist/task_evidence.js
CHANGED
|
@@ -2,6 +2,7 @@ import { readFileSync } from "node:fs";
|
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { parseTasksMd } from "./format.js";
|
|
4
4
|
import { readEvents, sha256Text } from "./store.js";
|
|
5
|
+
import { GREEN_ONLY_NO_TDD_REASON } from "./types.js";
|
|
5
6
|
export function taskEvidenceReadiness(projectRoot, change, changeRoot, attempt) {
|
|
6
7
|
if (attempt.contract_mode === true) {
|
|
7
8
|
return contractTaskEvidenceReadiness(projectRoot, change, attempt);
|
|
@@ -18,6 +19,9 @@ export function taskEvidenceReadiness(projectRoot, change, changeRoot, attempt)
|
|
|
18
19
|
if (currentDigest !== attempt.task_structure_digest)
|
|
19
20
|
missing.push("任务结构指纹");
|
|
20
21
|
if (!taskInfo.tddRequired) {
|
|
22
|
+
if (taskInfo.noTddReason === GREEN_ONLY_NO_TDD_REASON) {
|
|
23
|
+
missing.push(`${GREEN_ONLY_NO_TDD_REASON} 任务必须使用执行依据模式`);
|
|
24
|
+
}
|
|
21
25
|
if (!taskInfo.noTddReason)
|
|
22
26
|
missing.push("no_tdd_reason");
|
|
23
27
|
return missing.length === 0
|
|
@@ -48,7 +52,76 @@ export function taskEvidenceReadiness(projectRoot, change, changeRoot, attempt)
|
|
|
48
52
|
: { ready: false, missing, reason: missing.join("、") };
|
|
49
53
|
}
|
|
50
54
|
function contractTaskEvidenceReadiness(projectRoot, change, attempt) {
|
|
55
|
+
if (attempt.required_evidence) {
|
|
56
|
+
return effectiveContractTaskEvidenceReadiness(projectRoot, change, attempt);
|
|
57
|
+
}
|
|
58
|
+
return legacyContractTaskEvidenceReadiness(projectRoot, change, attempt);
|
|
59
|
+
}
|
|
60
|
+
/** 新执行依据模式:只消费 task-start 冻结的有效证据计划。 */
|
|
61
|
+
function effectiveContractTaskEvidenceReadiness(projectRoot, change, attempt) {
|
|
51
62
|
const missing = [];
|
|
63
|
+
const required = attempt.required_evidence;
|
|
64
|
+
const declaredTests = required.test_ids;
|
|
65
|
+
if (declaredTests.length === 0) {
|
|
66
|
+
const evidence = attemptLevelEvidence(projectRoot, change, attempt.attempt_id, new Set(required.accepted_green_statuses));
|
|
67
|
+
if (required.red_required && !evidence.hasRed)
|
|
68
|
+
missing.push("RED 证据");
|
|
69
|
+
if (required.green_required && !evidence.hasGreen)
|
|
70
|
+
missing.push("GREEN 证据");
|
|
71
|
+
return missing.length === 0
|
|
72
|
+
? { ready: true, missing: [] }
|
|
73
|
+
: { ready: false, missing, reason: missing.join("、") };
|
|
74
|
+
}
|
|
75
|
+
const perTest = new Map();
|
|
76
|
+
for (const testId of declaredTests) {
|
|
77
|
+
perTest.set(testId, { sawRed: false, sawGreen: false, paired: false });
|
|
78
|
+
}
|
|
79
|
+
for (const ev of readEvents(projectRoot, change)) {
|
|
80
|
+
if (ev.event_type !== "test_run_recorded")
|
|
81
|
+
continue;
|
|
82
|
+
const tr = ev.payload;
|
|
83
|
+
if (tr.attempt_id !== attempt.attempt_id)
|
|
84
|
+
continue;
|
|
85
|
+
if (typeof tr.test_id !== "string")
|
|
86
|
+
continue;
|
|
87
|
+
const state = perTest.get(tr.test_id);
|
|
88
|
+
if (!state)
|
|
89
|
+
continue;
|
|
90
|
+
if (tr.semantic_status === "expected_failure" && typeof tr.exit_code === "number" && tr.exit_code !== 0) {
|
|
91
|
+
state.sawRed = true;
|
|
92
|
+
}
|
|
93
|
+
const isAcceptedGreen = typeof tr.semantic_status === "string" &&
|
|
94
|
+
required.accepted_green_statuses.includes(tr.semantic_status) &&
|
|
95
|
+
tr.exit_code === 0;
|
|
96
|
+
if (isAcceptedGreen) {
|
|
97
|
+
if (state.sawRed)
|
|
98
|
+
state.paired = true;
|
|
99
|
+
state.sawGreen = true;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
if (required.green_required) {
|
|
103
|
+
for (const [testId, state] of perTest) {
|
|
104
|
+
if (!state.sawGreen)
|
|
105
|
+
missing.push(`${testId} GREEN 证据`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
if (required.red_required) {
|
|
109
|
+
const hasPairedTest = [...perTest.values()].some(state => state.paired);
|
|
110
|
+
if (!hasPairedTest)
|
|
111
|
+
missing.push("同一 TEST 的 RED/GREEN 配对");
|
|
112
|
+
}
|
|
113
|
+
return missing.length === 0
|
|
114
|
+
? { ready: true, missing: [] }
|
|
115
|
+
: { ready: false, missing, reason: missing.join("、") };
|
|
116
|
+
}
|
|
117
|
+
/** 历史执行依据模式仍按当时写入 attempt 的标记回放。 */
|
|
118
|
+
function legacyContractTaskEvidenceReadiness(projectRoot, change, attempt) {
|
|
119
|
+
const missing = [];
|
|
120
|
+
const executionPolicy = attempt.execution_policy ?? "tdd";
|
|
121
|
+
const greenOnly = executionPolicy === "green_only" && attempt.no_tdd_reason === GREEN_ONLY_NO_TDD_REASON;
|
|
122
|
+
if (attempt.no_tdd_reason === GREEN_ONLY_NO_TDD_REASON && !greenOnly) {
|
|
123
|
+
missing.push(`${GREEN_ONLY_NO_TDD_REASON} 任务不适用于当前 TDD apply`);
|
|
124
|
+
}
|
|
52
125
|
const tddRequired = attempt.tdd_required !== false;
|
|
53
126
|
const characterization = attempt.tdd_required === false && attempt.no_tdd_reason === "characterization";
|
|
54
127
|
const declaredTests = attempt.contract?.tests ?? [];
|
|
@@ -56,8 +129,11 @@ function contractTaskEvidenceReadiness(projectRoot, change, attempt) {
|
|
|
56
129
|
missing.push("no_tdd_reason");
|
|
57
130
|
}
|
|
58
131
|
if (declaredTests.length === 0) {
|
|
59
|
-
if (
|
|
60
|
-
|
|
132
|
+
if (greenOnly) {
|
|
133
|
+
missing.push(`${GREEN_ONLY_NO_TDD_REASON} 任务缺少声明 TEST`);
|
|
134
|
+
}
|
|
135
|
+
else if (tddRequired) {
|
|
136
|
+
const hasAttemptEvidence = attemptLevelEvidence(projectRoot, change, attempt.attempt_id, new Set(["expected_success"]));
|
|
61
137
|
if (!hasAttemptEvidence.hasRed)
|
|
62
138
|
missing.push("RED 证据");
|
|
63
139
|
if (!hasAttemptEvidence.hasGreen)
|
|
@@ -75,16 +151,13 @@ function contractTaskEvidenceReadiness(projectRoot, change, attempt) {
|
|
|
75
151
|
if (ev.event_type !== "test_run_recorded")
|
|
76
152
|
continue;
|
|
77
153
|
const tr = ev.payload;
|
|
78
|
-
if (tr.attempt_id !== attempt.attempt_id)
|
|
79
|
-
continue;
|
|
80
|
-
if (typeof tr.test_id !== "string")
|
|
154
|
+
if (tr.attempt_id !== attempt.attempt_id || typeof tr.test_id !== "string")
|
|
81
155
|
continue;
|
|
82
156
|
const state = perTest.get(tr.test_id);
|
|
83
157
|
if (!state)
|
|
84
158
|
continue;
|
|
85
|
-
if (tr.semantic_status === "expected_failure" && typeof tr.exit_code === "number" && tr.exit_code !== 0)
|
|
159
|
+
if (tr.semantic_status === "expected_failure" && typeof tr.exit_code === "number" && tr.exit_code !== 0)
|
|
86
160
|
state.sawRed = true;
|
|
87
|
-
}
|
|
88
161
|
const isExpectedSuccess = tr.semantic_status === "expected_success" && tr.exit_code === 0;
|
|
89
162
|
const isCharacterizationPass = characterization && tr.semantic_status === "characterization_pass" && tr.exit_code === 0;
|
|
90
163
|
if (isExpectedSuccess || isCharacterizationPass) {
|
|
@@ -97,7 +170,7 @@ function contractTaskEvidenceReadiness(projectRoot, change, attempt) {
|
|
|
97
170
|
if (!state.sawGreen)
|
|
98
171
|
missing.push(`${testId} GREEN 证据`);
|
|
99
172
|
}
|
|
100
|
-
if (tddRequired) {
|
|
173
|
+
if (tddRequired && !greenOnly) {
|
|
101
174
|
const hasPairedTest = [...perTest.values()].some(state => state.paired);
|
|
102
175
|
if (!hasPairedTest)
|
|
103
176
|
missing.push("同一 TEST 的 RED/GREEN 配对");
|
|
@@ -106,7 +179,7 @@ function contractTaskEvidenceReadiness(projectRoot, change, attempt) {
|
|
|
106
179
|
? { ready: true, missing: [] }
|
|
107
180
|
: { ready: false, missing, reason: missing.join("、") };
|
|
108
181
|
}
|
|
109
|
-
function
|
|
182
|
+
function attemptLevelEvidence(projectRoot, change, attemptId, acceptedGreenStatuses) {
|
|
110
183
|
let hasRed = false;
|
|
111
184
|
let hasGreen = false;
|
|
112
185
|
for (const ev of readEvents(projectRoot, change)) {
|
|
@@ -118,7 +191,9 @@ function attemptLevelRedGreenEvidence(projectRoot, change, attemptId) {
|
|
|
118
191
|
if (tr.semantic_status === "expected_failure" && typeof tr.exit_code === "number" && tr.exit_code !== 0) {
|
|
119
192
|
hasRed = true;
|
|
120
193
|
}
|
|
121
|
-
if (tr.semantic_status === "
|
|
194
|
+
if (typeof tr.semantic_status === "string" &&
|
|
195
|
+
acceptedGreenStatuses.has(tr.semantic_status) &&
|
|
196
|
+
tr.exit_code === 0) {
|
|
122
197
|
hasGreen = true;
|
|
123
198
|
}
|
|
124
199
|
}
|
package/dist/transition.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type ReviewRisk } from "./review.ts";
|
|
1
2
|
import type { Snapshot, State, Job, TransitionResult } from "./types.ts";
|
|
2
3
|
interface Decision {
|
|
3
4
|
fromState: State;
|
|
@@ -31,16 +32,16 @@ export declare function commitTransition(projectRoot: string, change: string, ch
|
|
|
31
32
|
decide: (snapshot: Snapshot) => Decision | SkipDecision | BlockedDecision;
|
|
32
33
|
idempotencyInputs?: Record<string, unknown>;
|
|
33
34
|
}): TransitionResult;
|
|
34
|
-
export declare function proposeReady(projectRoot: string, change: string, changeRoot: string, risk?:
|
|
35
|
+
export declare function proposeReady(projectRoot: string, change: string, changeRoot: string, risk?: ReviewRisk): TransitionResult;
|
|
35
36
|
export declare function transitionInit(projectRoot: string, change: string, changeRoot: string): TransitionResult;
|
|
36
|
-
export declare function transitionExplore(projectRoot: string, change: string, changeRoot: string, risk?:
|
|
37
|
+
export declare function transitionExplore(projectRoot: string, change: string, changeRoot: string, risk?: ReviewRisk): TransitionResult;
|
|
37
38
|
export declare function startApply(projectRoot: string, change: string, changeRoot: string): TransitionResult;
|
|
38
39
|
export declare function taskStart(projectRoot: string, change: string, changeRoot: string, taskId: string): TransitionResult;
|
|
39
40
|
export declare function reopen(projectRoot: string, change: string, changeRoot: string, to: State, reason: string, opts?: {
|
|
40
41
|
reviewFix?: string;
|
|
41
42
|
reviewFinding?: string;
|
|
42
43
|
}): TransitionResult;
|
|
43
|
-
export declare function reviewReady(projectRoot: string, change: string, changeRoot: string, risk?:
|
|
44
|
+
export declare function reviewReady(projectRoot: string, change: string, changeRoot: string, risk?: ReviewRisk): TransitionResult;
|
|
44
45
|
export declare function accept(projectRoot: string, change: string, changeRoot: string): TransitionResult;
|
|
45
46
|
export declare function taskComplete(projectRoot: string, change: string, changeRoot: string, taskId: string, inputContent?: string | null): TransitionResult;
|
|
46
47
|
export {};
|
package/dist/transition.js
CHANGED
|
@@ -5,21 +5,23 @@ import { ensureChangeLayout, readEvents, appendEvent, makeEvent, writeSnapshot,
|
|
|
5
5
|
import { rebuildSnapshot } from "./sync.js";
|
|
6
6
|
import { requiredJobActions } from "./job_action.js";
|
|
7
7
|
import { assertCommitPayloadExtension, isFreshReviewVerifier, isReviewReadyVerifier, latestReviewHistoryForGateRole, readReviewPolicyFromEvents, reviewBoundFiles, reviewEvidenceDigest, reviewPolicyForRisk, REVIEW_DOC_PATHS, } from "./review.js";
|
|
8
|
-
import { REVIEW_CODE_REVIEW_GATE_ID, REVIEW_FINAL_VERIFIER_GATE_ID, } from "./review_job_gates.js";
|
|
8
|
+
import { REVIEW_CODE_REVIEW_GATE_ID, REVIEW_FINAL_VERIFIER_GATE_ID, reviewScopeForGateRole, } from "./review_job_gates.js";
|
|
9
9
|
import { codeReviewBoundFiles, codeReviewDecisionScope, codeReviewJobStaleReason, codeReviewPacketContext, codeReviewPacketDigest, collectCodeReviewGateFacts, computeCodeStateCheck, currentCodeReviewWorkingPaths, dismissedCodeReviewSummary, latestCodeReviewDecision, latestCodeReviewFailedStatus, missingCoverageExemptionTestIds, requiresFinalVerifierForCurrentReview, scanCodeChangesForReview, } from "./code_review.js";
|
|
10
10
|
import { taskEvidenceReadiness } from "./task_evidence.js";
|
|
11
11
|
import { adoptedContractForTask, findTaskInLines, isReviewFixTaskId, parseTasksMd, parseTestContractEntries, } from "./format.js";
|
|
12
|
-
import { applyRequirementModeForCurrentRound, blockingJobsForApplyDone, formatPendingTaskMessage, latestAcceptedProposalBaseline, pendingTaskStatusForApply, planTransition, proposalDocsBaseline, } from "./phase_plan.js";
|
|
12
|
+
import { applyRequirementModeForCurrentRound, executionRequirementVersionForCurrentRound, blockingJobsForApplyDone, executionPolicyForCurrentRound, formatPendingTaskMessage, latestAcceptedProposalBaseline, pendingTaskStatusForApply, planTransition, discoveryDocsBaseline, proposalDocsBaseline, } from "./phase_plan.js";
|
|
13
13
|
import { latestAcceptedPhaseDecision, phaseConfirmationCommitPayload, phaseConfirmationForBoundary, phaseConfirmationMissingMessage, } from "./phase_confirmation.js";
|
|
14
|
-
import { currentGitHead, dirtyCodeFiles } from "./git_state.js";
|
|
14
|
+
import { currentGitHead, dirtyCodeFiles, stageProductionJavaFilesSince } from "./git_state.js";
|
|
15
|
+
import { workflowRiskForProject } from "./workflow_config.js";
|
|
15
16
|
let transitionSeq = 0;
|
|
16
17
|
function newTransitionId() { return `T-${Date.now()}-${++transitionSeq}`; }
|
|
17
18
|
let jobSeq = 0;
|
|
18
19
|
function newJobId(change, role) { return `JOB-${change.slice(0, 8)}-${role.slice(0, 4)}-${Date.now()}-${++jobSeq}`; }
|
|
19
20
|
function createReviewJobsForGate(state, gate, roles, changeRoot, change, reason, events) {
|
|
20
21
|
const newJobs = roles.map(role => {
|
|
21
|
-
const
|
|
22
|
-
//
|
|
22
|
+
const scope = reviewScopeForGateRole(gate, role);
|
|
23
|
+
// 角色职责目标和显式 freshness 路径绑定时点指纹:单文件缺失使用 sha256:missing,目录缺失使用稳定空指纹。
|
|
24
|
+
const boundPaths = [...new Set(scope.boundPaths)];
|
|
23
25
|
const boundFiles = boundPaths
|
|
24
26
|
.map(p => docRef(changeRoot, p));
|
|
25
27
|
const previousRejection = latestReviewHistoryForGateRole(events, gate, role);
|
|
@@ -29,14 +31,14 @@ function createReviewJobsForGate(state, gate, roles, changeRoot, change, reason,
|
|
|
29
31
|
state: "requested",
|
|
30
32
|
gate_id: gate.gate_id,
|
|
31
33
|
boundFiles,
|
|
32
|
-
...(
|
|
33
|
-
...(
|
|
34
|
+
...(scope.reviewTargets.length > 0 ? { review_targets: [...scope.reviewTargets] } : {}),
|
|
35
|
+
...(scope.readOnlyRefs.length > 0 ? { read_only_refs: [...scope.readOnlyRefs] } : {}),
|
|
34
36
|
packet_digest: sha256Text(JSON.stringify({
|
|
35
37
|
role,
|
|
36
38
|
gate_id: gate.gate_id,
|
|
37
39
|
boundFiles,
|
|
38
|
-
review_targets:
|
|
39
|
-
read_only_refs:
|
|
40
|
+
review_targets: scope.reviewTargets,
|
|
41
|
+
read_only_refs: scope.readOnlyRefs,
|
|
40
42
|
created_from_transition: gate.created_from_transition,
|
|
41
43
|
...(previousRejection ? { previous_rejection: previousRejection } : {}),
|
|
42
44
|
})),
|
|
@@ -80,6 +82,17 @@ function boundarySnapshotPayload(projectRoot) {
|
|
|
80
82
|
...(result.reason ? { boundary_snapshot_reason: result.reason } : {}),
|
|
81
83
|
};
|
|
82
84
|
}
|
|
85
|
+
function boundarySnapshotForTaskAttempt(events, attemptId) {
|
|
86
|
+
const start = events.findLast(event => event.event_type === "task_started" &&
|
|
87
|
+
event.payload.attempt_id === attemptId);
|
|
88
|
+
const boundary = start?.payload?.boundary_snapshot;
|
|
89
|
+
if (!boundary || typeof boundary !== "object" || Array.isArray(boundary))
|
|
90
|
+
return null;
|
|
91
|
+
const value = boundary;
|
|
92
|
+
if (!Array.isArray(value.dirty_files))
|
|
93
|
+
return null;
|
|
94
|
+
return value;
|
|
95
|
+
}
|
|
83
96
|
function parseScopeNoteInput(inputContent) {
|
|
84
97
|
if (inputContent == null)
|
|
85
98
|
return { ok: true, value: null, digest: null };
|
|
@@ -129,9 +142,41 @@ function parseScopeNoteInput(inputContent) {
|
|
|
129
142
|
},
|
|
130
143
|
};
|
|
131
144
|
}
|
|
132
|
-
function validateTaskStartContract(changeRoot, taskId,
|
|
133
|
-
if (
|
|
145
|
+
function validateTaskStartContract(changeRoot, taskId, parsedContract) {
|
|
146
|
+
if (parsedContract.errors.length > 0)
|
|
147
|
+
return parsedContract.errors.join(";");
|
|
148
|
+
if (!parsedContract.declaredFields.includes("tests"))
|
|
149
|
+
return `${taskId} 的执行依据缺少测试字段`;
|
|
150
|
+
if (!parsedContract.contract.design)
|
|
151
|
+
return `${taskId} 的执行依据缺少设计`;
|
|
152
|
+
if (parsedContract.contract.source.length === 0)
|
|
153
|
+
return `${taskId} 的执行依据缺少来源`;
|
|
154
|
+
if (!parsedContract.contract.acceptance)
|
|
155
|
+
return `${taskId} 的执行依据缺少验收目标`;
|
|
156
|
+
if (!parsedContract.contract.guard)
|
|
157
|
+
return `${taskId} 的执行依据缺少边界`;
|
|
158
|
+
if (parsedContract.contract.tests.length === 0)
|
|
134
159
|
return null;
|
|
160
|
+
const testContractPath = join(changeRoot, ".superspec", "artifacts", "test-contract.md");
|
|
161
|
+
if (!existsSync(testContractPath))
|
|
162
|
+
return "test-contract.md 不存在,无法校验执行依据测试引用";
|
|
163
|
+
const parsed = parseTestContractEntries(readFileSync(testContractPath, "utf8"));
|
|
164
|
+
if (!parsed.ok)
|
|
165
|
+
return parsed.message;
|
|
166
|
+
const known = new Set(parsed.entries.map(entry => entry.test_id));
|
|
167
|
+
const missing = parsedContract.contract.tests.filter(testId => !known.has(testId));
|
|
168
|
+
return missing.length > 0 ? `执行依据引用了不存在的 TEST ID:${missing.join(", ")}` : null;
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* v1 只回放旧 contract 约束:普通 TDD task 必须保有执行依据和 TEST 引用,
|
|
172
|
+
* 但不要求 v2 新增的五字段,避免阻断历史 documentation-only task。
|
|
173
|
+
*/
|
|
174
|
+
function validateLegacyTaskStartContract(changeRoot, taskId, tddRequired, parsedContract) {
|
|
175
|
+
if (!parsedContract) {
|
|
176
|
+
return tddRequired && !isReviewFixTaskId(taskId)
|
|
177
|
+
? `执行依据模式下,普通 TDD 任务 ${taskId} 缺少执行依据`
|
|
178
|
+
: null;
|
|
179
|
+
}
|
|
135
180
|
if (parsedContract.errors.length > 0)
|
|
136
181
|
return parsedContract.errors.join(";");
|
|
137
182
|
if (tddRequired && !isReviewFixTaskId(taskId) && parsedContract.contract.tests.length === 0) {
|
|
@@ -149,6 +194,21 @@ function validateTaskStartContract(changeRoot, taskId, tddRequired, parsedContra
|
|
|
149
194
|
const missing = parsedContract.contract.tests.filter(testId => !known.has(testId));
|
|
150
195
|
return missing.length > 0 ? `执行依据引用了不存在的 TEST ID:${missing.join(", ")}` : null;
|
|
151
196
|
}
|
|
197
|
+
/**
|
|
198
|
+
* 只在 task-start 编译新模式 task 的有效证据要求。Propose 只声明 TEST,
|
|
199
|
+
* RED 是否要求由这里读取已冻结的 execution_policy 决定,之后不再依赖 tasks.md。
|
|
200
|
+
*/
|
|
201
|
+
function compileRequiredEvidence(executionPolicy, testIds, requiresVerificationWithoutDeclaredTest) {
|
|
202
|
+
// REVIEW-FIX 没有计划阶段声明的 TEST,但仍必须登记一次真实回归验证。
|
|
203
|
+
// 是否需要 RED 始终由已冻结的 execution_policy 决定。
|
|
204
|
+
const requiresVerification = testIds.length > 0 || requiresVerificationWithoutDeclaredTest;
|
|
205
|
+
return {
|
|
206
|
+
test_ids: testIds,
|
|
207
|
+
red_required: executionPolicy === "tdd" && requiresVerification,
|
|
208
|
+
green_required: requiresVerification,
|
|
209
|
+
accepted_green_statuses: ["expected_success"],
|
|
210
|
+
};
|
|
211
|
+
}
|
|
152
212
|
function hasRejectedReviewReadyVerifier(events) {
|
|
153
213
|
const reviewReadyVerifierIds = new Set();
|
|
154
214
|
for (const ev of events) {
|
|
@@ -234,7 +294,7 @@ function appendReviewFixTask(changeRoot, ref, finding) {
|
|
|
234
294
|
const description = typeof finding.description === "string" && finding.description.trim()
|
|
235
295
|
? finding.description.trim().replace(/\s+/g, " ")
|
|
236
296
|
: `修复代码审查问题 ${ref.findingId}`;
|
|
237
|
-
const line = `- [ ] ${reviewFixTaskId(ref)} ${description}
|
|
297
|
+
const line = `- [ ] ${reviewFixTaskId(ref)} ${description} ${marker}`;
|
|
238
298
|
const suffix = content.endsWith("\n") ? "" : "\n";
|
|
239
299
|
writeFileSync(tasksPath, `${content}${suffix}${line}\n`);
|
|
240
300
|
return "created";
|
|
@@ -415,7 +475,7 @@ function evaluateFinalVerifierGate(input) {
|
|
|
415
475
|
return { skip: true, message: "已在 review 状态,最终验证仍然有效" };
|
|
416
476
|
}
|
|
417
477
|
function authorizePhaseAdvance(input) {
|
|
418
|
-
const confirmation = phaseConfirmationForBoundary(input.projectRoot, input.events, input.snapshot, input.boundary);
|
|
478
|
+
const confirmation = phaseConfirmationForBoundary(input.projectRoot, input.events, input.snapshot, input.boundary, input.risk);
|
|
419
479
|
const phaseDecision = confirmation
|
|
420
480
|
? latestAcceptedPhaseDecision(input.events, confirmation)
|
|
421
481
|
: null;
|
|
@@ -440,7 +500,7 @@ function transitionPlanToDecision(snapshot, changeRoot, change, plan, events) {
|
|
|
440
500
|
case "skip":
|
|
441
501
|
return { skip: true, message: plan.message };
|
|
442
502
|
case "blocked":
|
|
443
|
-
return { blocked: true, reason: plan.reason, jobs: plan.jobs };
|
|
503
|
+
return { blocked: true, reason: plan.reason, jobs: plan.jobs, ...(plan.details ? { details: plan.details } : {}) };
|
|
444
504
|
case "create_gate_jobs":
|
|
445
505
|
return createReviewJobsForGate(snapshot.state, plan.gate, plan.roles, changeRoot, change, plan.reason, events);
|
|
446
506
|
case "advance":
|
|
@@ -483,7 +543,7 @@ export function commitTransition(projectRoot, change, changeRoot, opts) {
|
|
|
483
543
|
from_state: snapshot.state,
|
|
484
544
|
to_state: snapshot.state,
|
|
485
545
|
created_jobs: [],
|
|
486
|
-
required_jobs: requiredJobActions(change, decision.jobs),
|
|
546
|
+
...(decision.jobs.length > 0 ? { required_jobs: requiredJobActions(change, decision.jobs) } : {}),
|
|
487
547
|
message: decision.reason,
|
|
488
548
|
events_written: 0,
|
|
489
549
|
...(decision.details ? { details: decision.details } : {}),
|
|
@@ -539,7 +599,7 @@ export function commitTransition(projectRoot, change, changeRoot, opts) {
|
|
|
539
599
|
});
|
|
540
600
|
}
|
|
541
601
|
// ===== propose-ready =====
|
|
542
|
-
export function proposeReady(projectRoot, change, changeRoot, risk =
|
|
602
|
+
export function proposeReady(projectRoot, change, changeRoot, risk = workflowRiskForProject(projectRoot)) {
|
|
543
603
|
return commitTransition(projectRoot, change, changeRoot, {
|
|
544
604
|
name: "propose-ready", idempotencyInputs: { risk },
|
|
545
605
|
decide: (snapshot) => {
|
|
@@ -569,7 +629,7 @@ export function transitionInit(projectRoot, change, changeRoot) {
|
|
|
569
629
|
});
|
|
570
630
|
}
|
|
571
631
|
// ===== explore =====
|
|
572
|
-
export function transitionExplore(projectRoot, change, changeRoot, risk =
|
|
632
|
+
export function transitionExplore(projectRoot, change, changeRoot, risk = workflowRiskForProject(projectRoot)) {
|
|
573
633
|
return commitTransition(projectRoot, change, changeRoot, {
|
|
574
634
|
name: "explore", idempotencyInputs: { phase: "explore", risk },
|
|
575
635
|
decide: (snapshot) => {
|
|
@@ -598,7 +658,7 @@ export function startApply(projectRoot, change, changeRoot) {
|
|
|
598
658
|
changeRoot,
|
|
599
659
|
events,
|
|
600
660
|
snapshot,
|
|
601
|
-
mode: { kind: "risk", risk:
|
|
661
|
+
mode: { kind: "risk", risk: workflowRiskForProject(projectRoot) },
|
|
602
662
|
});
|
|
603
663
|
return transitionPlanToDecision(snapshot, changeRoot, change, plan, events);
|
|
604
664
|
},
|
|
@@ -619,6 +679,8 @@ export function taskStart(projectRoot, change, changeRoot, taskId) {
|
|
|
619
679
|
if (taskLineIdx < 0)
|
|
620
680
|
return { skip: true, message: `任务 ${taskId} 不存在` };
|
|
621
681
|
const contractMode = applyRequirementModeForCurrentRound(events);
|
|
682
|
+
const executionRequirementVersion = executionRequirementVersionForCurrentRound(events);
|
|
683
|
+
const executionPolicy = executionPolicyForCurrentRound(events);
|
|
622
684
|
if (contractMode && pendingTaskStatusForApply(changeRoot, events).completedByEvent.includes(taskId)) {
|
|
623
685
|
return { skip: true, message: `任务 ${taskId} 已通过完成事件完成` };
|
|
624
686
|
}
|
|
@@ -633,23 +695,38 @@ export function taskStart(projectRoot, change, changeRoot, taskId) {
|
|
|
633
695
|
// 统一出口:adopted.contract 非 null 当且仅当契约模式下有绑定块;
|
|
634
696
|
// legacy 轮即使 task 带执行依据文本也输出 null,避免"看似契约、实按 legacy 校验"的误导态
|
|
635
697
|
const adopted = adoptedContractForTask(tasksContent, taskId, contractMode);
|
|
636
|
-
|
|
637
|
-
|
|
698
|
+
const reviewFix = isReviewFixTaskId(taskId);
|
|
699
|
+
if (contractMode && executionRequirementVersion === 2 && !reviewFix && !adopted.parsed) {
|
|
700
|
+
return { skip: true, message: `执行依据模式下,任务 ${taskId} 缺少执行依据` };
|
|
701
|
+
}
|
|
702
|
+
if (contractMode && executionRequirementVersion === 2 && !reviewFix && adopted.parsed) {
|
|
703
|
+
const contractError = validateTaskStartContract(changeRoot, taskId, adopted.parsed);
|
|
704
|
+
if (contractError)
|
|
705
|
+
return { skip: true, message: contractError };
|
|
638
706
|
}
|
|
639
|
-
if (contractMode) {
|
|
640
|
-
const contractError =
|
|
707
|
+
if (contractMode && executionRequirementVersion === 1) {
|
|
708
|
+
const contractError = validateLegacyTaskStartContract(changeRoot, taskId, taskInfo.tddRequired, adopted.parsed);
|
|
641
709
|
if (contractError)
|
|
642
710
|
return { skip: true, message: contractError };
|
|
643
711
|
}
|
|
644
712
|
const structureDigest = sha256Text(tasksContent.replace(/- \[[xX]\]/g, "- [ ]"));
|
|
713
|
+
const effectivePolicy = executionPolicy;
|
|
714
|
+
const requiredEvidence = contractMode && executionRequirementVersion === 2
|
|
715
|
+
? compileRequiredEvidence(effectivePolicy, adopted.contract?.tests ?? [], reviewFix)
|
|
716
|
+
: null;
|
|
645
717
|
const attempt = {
|
|
646
718
|
attempt_id: `ATT-${taskId}-${Date.now()}-${++attemptSeq}`,
|
|
647
719
|
task_id: taskId, state: "active",
|
|
648
720
|
task_structure_digest: structureDigest,
|
|
649
721
|
contract_mode: contractMode,
|
|
650
722
|
contract: adopted.contract,
|
|
651
|
-
|
|
652
|
-
|
|
723
|
+
...(requiredEvidence ? { required_evidence: requiredEvidence } : {
|
|
724
|
+
// 历史模式以及 v1 契约轮继续使用 task 行标记回放;v2 只消费快照。
|
|
725
|
+
tdd_required: taskInfo.tddRequired,
|
|
726
|
+
no_tdd_reason: taskInfo.noTddReason,
|
|
727
|
+
}),
|
|
728
|
+
// REVIEW-FIX 没有计划阶段测试契约,但有效证据要求仍由本轮冻结策略编译。
|
|
729
|
+
execution_policy: effectivePolicy,
|
|
653
730
|
declared_write_scope: [], pre_edit_source_fingerprint: null,
|
|
654
731
|
pre_edit_red_ref: null, executor_packet_digest: null,
|
|
655
732
|
executor_result_ref: null, post_edit_green_ref: null,
|
|
@@ -666,7 +743,9 @@ export function taskStart(projectRoot, change, changeRoot, taskId) {
|
|
|
666
743
|
details: {
|
|
667
744
|
attempt_id: attempt.attempt_id,
|
|
668
745
|
task_id: taskId,
|
|
746
|
+
execution_policy: effectivePolicy,
|
|
669
747
|
contract: adopted.contract,
|
|
748
|
+
...(requiredEvidence ? { required_evidence: requiredEvidence } : {}),
|
|
670
749
|
// legacy 轮统一标注历史模式(无论有无执行依据文本);契约轮无块时标 false(如 REVIEW-FIX)
|
|
671
750
|
...(adopted.contract ? {} : { legacy_contract: !contractMode }),
|
|
672
751
|
},
|
|
@@ -675,6 +754,42 @@ export function taskStart(projectRoot, change, changeRoot, taskId) {
|
|
|
675
754
|
});
|
|
676
755
|
}
|
|
677
756
|
// ===== reopen =====
|
|
757
|
+
function abandonActiveTaskAttempts(snapshot, to, reason) {
|
|
758
|
+
return snapshot.active_task_attempts
|
|
759
|
+
.filter(attempt => attempt.state === "active")
|
|
760
|
+
.map(attempt => ({
|
|
761
|
+
type: "task_abandoned",
|
|
762
|
+
payload: {
|
|
763
|
+
attempt_id: attempt.attempt_id,
|
|
764
|
+
task_id: attempt.task_id,
|
|
765
|
+
reason: `reopen --to ${to}:${reason}`,
|
|
766
|
+
},
|
|
767
|
+
}));
|
|
768
|
+
}
|
|
769
|
+
function invalidateOpenJobs(snapshot, to, reason) {
|
|
770
|
+
return snapshot.open_jobs.map(job => ({
|
|
771
|
+
type: "job_invalidated",
|
|
772
|
+
payload: {
|
|
773
|
+
job_id: job.job_id,
|
|
774
|
+
role: job.role,
|
|
775
|
+
reason: `reopen --to ${to}:${reason}`,
|
|
776
|
+
},
|
|
777
|
+
}));
|
|
778
|
+
}
|
|
779
|
+
function planningReopenExtraEvents(snapshot, to, reason) {
|
|
780
|
+
return [
|
|
781
|
+
...invalidateOpenJobs(snapshot, to, reason),
|
|
782
|
+
...abandonActiveTaskAttempts(snapshot, to, reason),
|
|
783
|
+
];
|
|
784
|
+
}
|
|
785
|
+
function canReopenToExplore(from) {
|
|
786
|
+
return from === "propose" || from === "propose_ready" || from === "apply" ||
|
|
787
|
+
from === "apply_done" || from === "review" || from === "accepted";
|
|
788
|
+
}
|
|
789
|
+
function canReopenToPropose(from) {
|
|
790
|
+
return from === "propose_ready" || from === "apply" || from === "apply_done" ||
|
|
791
|
+
from === "review" || from === "accepted";
|
|
792
|
+
}
|
|
678
793
|
export function reopen(projectRoot, change, changeRoot, to, reason, opts = {}) {
|
|
679
794
|
return commitTransition(projectRoot, change, changeRoot, {
|
|
680
795
|
name: "reopen", idempotencyInputs: { to, reason, reviewFix: opts.reviewFix ?? "", reviewFinding: opts.reviewFinding ?? "" },
|
|
@@ -750,22 +865,36 @@ export function reopen(projectRoot, change, changeRoot, to, reason, opts = {}) {
|
|
|
750
865
|
},
|
|
751
866
|
};
|
|
752
867
|
}
|
|
868
|
+
if (to === "explore") {
|
|
869
|
+
if (!canReopenToExplore(snapshot.state)) {
|
|
870
|
+
return { skip: true, message: `当前状态 ${snapshot.state},不能 reopen 到 explore` };
|
|
871
|
+
}
|
|
872
|
+
return {
|
|
873
|
+
fromState: snapshot.state,
|
|
874
|
+
toState: "explore",
|
|
875
|
+
outcome: "advanced",
|
|
876
|
+
reason: reason.trim(),
|
|
877
|
+
commitPayload: {
|
|
878
|
+
reopen_target: "explore",
|
|
879
|
+
reopen_source: snapshot.state,
|
|
880
|
+
baseline_docs: discoveryDocsBaseline(changeRoot),
|
|
881
|
+
},
|
|
882
|
+
extraEvents: planningReopenExtraEvents(snapshot, "explore", reason.trim()),
|
|
883
|
+
};
|
|
884
|
+
}
|
|
753
885
|
if (to === "propose") {
|
|
754
|
-
if (snapshot.state
|
|
755
|
-
return {
|
|
756
|
-
skip: true,
|
|
757
|
-
message: `当前状态 ${snapshot.state},主动 reopen --to propose 只允许从 accepted 发起;apply_done 的代码审查问题请使用 --review-finding`,
|
|
758
|
-
};
|
|
886
|
+
if (!canReopenToPropose(snapshot.state)) {
|
|
887
|
+
return { skip: true, message: `当前状态 ${snapshot.state},不能 reopen 到 propose` };
|
|
759
888
|
}
|
|
760
|
-
if (snapshot.open_jobs.length > 0) {
|
|
889
|
+
if (snapshot.state === "accepted" && snapshot.open_jobs.length > 0) {
|
|
761
890
|
return {
|
|
762
891
|
blocked: true,
|
|
763
|
-
reason: `状态未推进;accepted
|
|
892
|
+
reason: `状态未推进;accepted 状态仍有 ${snapshot.open_jobs.length} 个待完成工作项`,
|
|
764
893
|
jobs: snapshot.open_jobs,
|
|
765
894
|
};
|
|
766
895
|
}
|
|
767
|
-
const acceptedBaseline = latestAcceptedProposalBaseline(events);
|
|
768
896
|
const currentBaseline = proposalDocsBaseline(changeRoot);
|
|
897
|
+
const acceptedBaseline = snapshot.state === "accepted" ? latestAcceptedProposalBaseline(events) : null;
|
|
769
898
|
// 旧版 accepted 事件的基线可能缺少后来纳入 Propose gate 的材料。保留其已冻结
|
|
770
899
|
// 的摘要,并用 reopen 当刻的摘要补齐缺项,确保本轮之后对任一审查目标的修改都能被检测。
|
|
771
900
|
const baselineNeedsBackfill = acceptedBaseline !== null && Object.keys(currentBaseline)
|
|
@@ -777,20 +906,21 @@ export function reopen(projectRoot, change, changeRoot, to, reason, opts = {}) {
|
|
|
777
906
|
]))
|
|
778
907
|
: currentBaseline;
|
|
779
908
|
return {
|
|
780
|
-
fromState:
|
|
909
|
+
fromState: snapshot.state,
|
|
781
910
|
toState: "propose",
|
|
782
911
|
outcome: "advanced",
|
|
783
912
|
reason: reason.trim(),
|
|
784
913
|
commitPayload: {
|
|
785
914
|
reopen_target: "propose",
|
|
786
|
-
reopen_source:
|
|
915
|
+
reopen_source: snapshot.state,
|
|
787
916
|
baseline_source: acceptedBaseline ? (baselineNeedsBackfill ? "accepted_backfill" : "accepted") : "reopen_fallback",
|
|
788
917
|
baseline_docs: baselineDocs,
|
|
789
918
|
},
|
|
919
|
+
extraEvents: planningReopenExtraEvents(snapshot, "propose", reason.trim()),
|
|
790
920
|
};
|
|
791
921
|
}
|
|
792
922
|
if (to !== "apply")
|
|
793
|
-
return { skip: true, message: `reopen 当前只支持 --to
|
|
923
|
+
return { skip: true, message: `reopen 当前只支持 --to explore、--to propose 或 --to apply,不支持 ${to}` };
|
|
794
924
|
if (snapshot.state !== "apply_done" && snapshot.state !== "review") {
|
|
795
925
|
return { skip: true, message: `当前状态 ${snapshot.state},不能 reopen 到 apply` };
|
|
796
926
|
}
|
|
@@ -807,7 +937,7 @@ export function reopen(projectRoot, change, changeRoot, to, reason, opts = {}) {
|
|
|
807
937
|
});
|
|
808
938
|
}
|
|
809
939
|
// ===== review-ready =====
|
|
810
|
-
export function reviewReady(projectRoot, change, changeRoot, risk =
|
|
940
|
+
export function reviewReady(projectRoot, change, changeRoot, risk = workflowRiskForProject(projectRoot)) {
|
|
811
941
|
return commitTransition(projectRoot, change, changeRoot, {
|
|
812
942
|
name: "review-ready", idempotencyInputs: { phase: "review-ready", risk },
|
|
813
943
|
decide: (snapshot) => {
|
|
@@ -862,6 +992,7 @@ export function reviewReady(projectRoot, change, changeRoot, risk = "strict") {
|
|
|
862
992
|
events,
|
|
863
993
|
snapshot,
|
|
864
994
|
boundary: "apply_to_review",
|
|
995
|
+
risk: policy.review_risk,
|
|
865
996
|
decision: codeReviewDecision,
|
|
866
997
|
});
|
|
867
998
|
}
|
|
@@ -896,7 +1027,7 @@ export function accept(projectRoot, change, changeRoot) {
|
|
|
896
1027
|
changeRoot,
|
|
897
1028
|
events,
|
|
898
1029
|
snapshot,
|
|
899
|
-
mode: { kind: "risk", risk:
|
|
1030
|
+
mode: { kind: "risk", risk: workflowRiskForProject(projectRoot) },
|
|
900
1031
|
});
|
|
901
1032
|
return transitionPlanToDecision(snapshot, changeRoot, change, plan, events);
|
|
902
1033
|
},
|
|
@@ -918,9 +1049,11 @@ export function taskComplete(projectRoot, change, changeRoot, taskId, inputConte
|
|
|
918
1049
|
const readiness = taskEvidenceReadiness(projectRoot, change, changeRoot, attempt);
|
|
919
1050
|
if (!readiness.ready)
|
|
920
1051
|
return { skip: true, message: `任务 ${taskId} 无法完成:${readiness.reason}` };
|
|
1052
|
+
const taskStartBoundary = boundarySnapshotForTaskAttempt(readEvents(projectRoot, change), attempt.attempt_id);
|
|
921
1053
|
const completedPayload = {
|
|
922
1054
|
task_id: taskId,
|
|
923
1055
|
attempt_id: attempt.attempt_id,
|
|
1056
|
+
execution_policy: attempt.execution_policy ?? "tdd",
|
|
924
1057
|
...boundarySnapshotPayload(projectRoot),
|
|
925
1058
|
checkbox_update: { status: "pending" },
|
|
926
1059
|
...(scopeInput.value ? { scope_note: scopeInput.value } : {}),
|
|
@@ -929,7 +1062,8 @@ export function taskComplete(projectRoot, change, changeRoot, taskId, inputConte
|
|
|
929
1062
|
fromState: "apply", toState: "apply", outcome: "advanced",
|
|
930
1063
|
reason: `任务 ${taskId} 完成`,
|
|
931
1064
|
extraEvents: [{ type: "task_completed", payload: completedPayload }],
|
|
932
|
-
postCommit: (
|
|
1065
|
+
postCommit: (pr, _ch, cr) => {
|
|
1066
|
+
completedPayload.java_staging = stageProductionJavaFilesSince(pr, taskStartBoundary);
|
|
933
1067
|
const lines = readFileSync(join(cr, "tasks.md"), "utf8").split("\n");
|
|
934
1068
|
const idx = findTaskLine(lines, taskId);
|
|
935
1069
|
if (idx < 0) {
|