@peterxiaoyang/superspec 0.1.48 → 0.1.50
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/code_review.d.ts +10 -5
- package/dist/code_review.js +155 -35
- package/dist/job_validity.js +5 -2
- package/dist/phase_plan.d.ts +2 -0
- package/dist/phase_plan.js +47 -12
- package/dist/record.js +6 -3
- package/dist/transition.js +66 -14
- package/dist/types.d.ts +5 -3
- package/package.json +1 -1
- package/templates/workflow/AGENTS.md +6 -0
- package/templates/workflow/prompts/code-reviewer.md +5 -4
- package/templates/workflow/skills/superspec-apply/SKILL.md +11 -2
- package/templates/workflow/skills/superspec-propose/SKILL.md +2 -0
package/dist/code_review.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { CodeReviewGateEvidence, CodeReviewResultKind, CodeReviewScope, CodeStateCheck, CoverageExemptionRef, Event, Job, JobPacketContext, Ref, ReviewPreviousRejection, TaskExecutionIndexEntry } from "./types.ts";
|
|
1
|
+
import type { CodeReviewGateEvidence, CodeReviewResultKind, CodeReviewScope, CodeStateCheck, CoverageExemptionRef, DirtyFileFingerprint, Event, Job, JobPacketContext, Ref, ReviewPreviousRejection, TaskExecutionIndexEntry } from "./types.ts";
|
|
2
2
|
export declare const CODE_REVIEW_REPAIR_SCOPE_PREFIX = "code_reviewer_report_repair:";
|
|
3
3
|
export declare const CODE_REVIEW_DECISION_SCOPE_PREFIX = "code_review_decision:";
|
|
4
4
|
export declare const TEST_COVERAGE_EXEMPTION_SCOPE_PREFIX = "test_coverage_exemption:";
|
|
@@ -47,15 +47,19 @@ export interface CodeReviewGateFacts {
|
|
|
47
47
|
export declare function scanCodeChanges(projectRoot: string): CodeChangeScan;
|
|
48
48
|
export declare function currentCodeReviewWorkingPaths(projectRoot: string, events: Event[], extraIgnoredPaths?: string[]): string[];
|
|
49
49
|
export declare function knownCodeReviewReportPaths(projectRoot: string, events: Event[]): Set<string>;
|
|
50
|
-
|
|
50
|
+
interface CodeReviewBase {
|
|
51
51
|
base_head: string | null;
|
|
52
|
-
kind: "reviewed" | "start_apply" | "empty_tree" | "history_missing";
|
|
52
|
+
kind: "reviewed" | "task_start" | "start_apply" | "empty_tree" | "history_missing";
|
|
53
53
|
reason: string;
|
|
54
|
-
|
|
54
|
+
dirty_files?: DirtyFileFingerprint[];
|
|
55
|
+
reviewed_files?: Ref[];
|
|
56
|
+
required_recheck_paths?: string[];
|
|
57
|
+
}
|
|
58
|
+
export declare function selectCodeReviewBase(events: Event[]): CodeReviewBase;
|
|
55
59
|
export declare function scanCodeReviewScope(projectRoot: string, events: Event[]): CodeReviewScope;
|
|
56
60
|
export declare function scanCodeChangesForReview(projectRoot: string, events: Event[]): CodeChangeScan;
|
|
57
61
|
export declare function codeReviewBoundFiles(projectRoot: string, paths: string[]): Ref[];
|
|
58
|
-
export declare function codeReviewJobStaleReason(projectRoot: string, job: Job, currentPaths?: string[]): string | null;
|
|
62
|
+
export declare function codeReviewJobStaleReason(projectRoot: string, job: Job, currentPaths?: string[], events?: Event[], extraIgnoredPaths?: string[]): string | null;
|
|
59
63
|
export declare function codeReviewPacketDigest(input: {
|
|
60
64
|
role: "code-reviewer";
|
|
61
65
|
gate_id?: "review.code_review";
|
|
@@ -94,3 +98,4 @@ export declare function latestApplyDoneToReviewGate(events: Event[]): {
|
|
|
94
98
|
export declare function latestCodeReviewGateEvidence(events: Event[]): CodeReviewGateEvidence | null;
|
|
95
99
|
export declare function requiresFinalVerifierForCurrentReview(events: Event[]): boolean;
|
|
96
100
|
export declare function computeCodeStateCheck(projectRoot: string, events: Event[], ignoredCodePaths?: string[]): CodeStateCheck;
|
|
101
|
+
export {};
|
package/dist/code_review.js
CHANGED
|
@@ -3,7 +3,7 @@ import { existsSync, readFileSync } from "node:fs";
|
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { findLatestEvent, sha256File, sha256Text } from "./store.js";
|
|
5
5
|
import { REVIEW_CODE_REVIEW_GATE } from "./review_job_gates.js";
|
|
6
|
-
import { codeFileContentSha, currentGitHead, diffFingerprints, dirtyCodePaths, gitLines, isCodeLikePath, projectHasReadableDirectory, walkCodeFiles, } from "./git_state.js";
|
|
6
|
+
import { codeFileContentSha, currentGitHead, diffFingerprints, dirtyCodeFiles, dirtyCodePaths, gitLines, isCodeLikePath, projectHasReadableDirectory, walkCodeFiles, } from "./git_state.js";
|
|
7
7
|
import { parseExecutionRequirements, parseTestContractEntries } from "./format.js";
|
|
8
8
|
export const CODE_REVIEW_REPAIR_SCOPE_PREFIX = "code_reviewer_report_repair:";
|
|
9
9
|
export const CODE_REVIEW_DECISION_SCOPE_PREFIX = "code_review_decision:";
|
|
@@ -88,32 +88,101 @@ function firstStartApplyHead(events) {
|
|
|
88
88
|
}
|
|
89
89
|
return { present: false, head: null, reason: "没有 apply_start_head" };
|
|
90
90
|
}
|
|
91
|
-
function
|
|
91
|
+
function latestReviewAttemptBase(events) {
|
|
92
|
+
const jobs = new Map();
|
|
93
|
+
let latest = null;
|
|
94
|
+
let startIndex = 0;
|
|
92
95
|
for (let i = events.length - 1; i >= 0; i--) {
|
|
93
96
|
const ev = events[i];
|
|
94
|
-
if (ev.event_type
|
|
97
|
+
if (ev.event_type === "transition_commit" && ev.payload.transition === "start-apply") {
|
|
98
|
+
startIndex = i;
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
for (let i = startIndex; i < events.length; i++) {
|
|
103
|
+
const ev = events[i];
|
|
104
|
+
if (ev.event_type === "transition_commit") {
|
|
105
|
+
for (const job of ev.payload.new_jobs ?? []) {
|
|
106
|
+
if (isCodeReviewerJob(job))
|
|
107
|
+
jobs.set(job.job_id, job);
|
|
108
|
+
}
|
|
109
|
+
const gate = ev.payload.code_review_gate;
|
|
110
|
+
if (gate?.decision === "skipped" && typeof gate.head === "string" && gate.head.trim() !== "") {
|
|
111
|
+
latest = { base_head: gate.head, kind: "reviewed", reason: "latest_code_review_gate" };
|
|
112
|
+
}
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
if (ev.event_type !== "job_accepted" && ev.event_type !== "job_rejected")
|
|
95
116
|
continue;
|
|
96
117
|
const payload = ev.payload;
|
|
97
|
-
|
|
98
|
-
if (!gate)
|
|
118
|
+
if (typeof payload.job_id !== "string")
|
|
99
119
|
continue;
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
if (
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
120
|
+
const job = jobs.get(payload.job_id);
|
|
121
|
+
const scope = job?.packet_context?.code_review_scope;
|
|
122
|
+
if (!job || !scope || !("current_head" in scope))
|
|
123
|
+
continue;
|
|
124
|
+
const reusable = ev.event_type === "job_accepted" || payload.result_kind === "review_failed";
|
|
125
|
+
if (!reusable)
|
|
126
|
+
continue;
|
|
127
|
+
const findings = ev.payload.findings;
|
|
128
|
+
const requiredRecheckPaths = Array.isArray(findings)
|
|
129
|
+
? findings.flatMap(raw => {
|
|
130
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw))
|
|
131
|
+
return [];
|
|
132
|
+
const refs = raw.source_refs;
|
|
133
|
+
if (!Array.isArray(refs))
|
|
134
|
+
return [];
|
|
135
|
+
return refs.flatMap(ref => {
|
|
136
|
+
if (typeof ref !== "string")
|
|
137
|
+
return [];
|
|
138
|
+
const match = /^(.+?)(?::\d+(?::\d+)?)?$/.exec(ref.trim());
|
|
139
|
+
return match && isCodeLikePath(match[1]) ? [normalizeKnownPath(match[1])] : [];
|
|
140
|
+
});
|
|
141
|
+
})
|
|
142
|
+
: [];
|
|
143
|
+
latest = {
|
|
144
|
+
base_head: scope.current_head ?? null,
|
|
145
|
+
kind: "reviewed",
|
|
146
|
+
reason: "latest_code_review_attempt",
|
|
147
|
+
reviewed_files: job.boundFiles,
|
|
148
|
+
required_recheck_paths: uniqSorted(requiredRecheckPaths),
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
return latest;
|
|
152
|
+
}
|
|
153
|
+
function firstTaskBoundaryInCurrentApply(events) {
|
|
154
|
+
let startIndex = -1;
|
|
155
|
+
for (let i = events.length - 1; i >= 0; i--) {
|
|
156
|
+
const ev = events[i];
|
|
157
|
+
if (ev.event_type !== "transition_commit")
|
|
158
|
+
continue;
|
|
159
|
+
if (ev.payload.transition === "start-apply") {
|
|
160
|
+
startIndex = i;
|
|
161
|
+
break;
|
|
109
162
|
}
|
|
110
163
|
}
|
|
111
|
-
|
|
164
|
+
for (let i = startIndex + 1; i < events.length; i++) {
|
|
165
|
+
if (events[i].event_type !== "task_started")
|
|
166
|
+
continue;
|
|
167
|
+
const boundary = boundaryFromPayload(events[i].payload);
|
|
168
|
+
if (boundary)
|
|
169
|
+
return boundary;
|
|
170
|
+
}
|
|
171
|
+
return null;
|
|
112
172
|
}
|
|
113
173
|
export function selectCodeReviewBase(events) {
|
|
114
|
-
const reviewed =
|
|
115
|
-
if (reviewed
|
|
116
|
-
return
|
|
174
|
+
const reviewed = latestReviewAttemptBase(events);
|
|
175
|
+
if (reviewed)
|
|
176
|
+
return reviewed;
|
|
177
|
+
const taskBoundary = firstTaskBoundaryInCurrentApply(events);
|
|
178
|
+
if (taskBoundary) {
|
|
179
|
+
return {
|
|
180
|
+
base_head: taskBoundary.head,
|
|
181
|
+
kind: "task_start",
|
|
182
|
+
reason: "first_task_boundary",
|
|
183
|
+
dirty_files: taskBoundary.dirty_files,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
117
186
|
const firstStart = firstStartApplyHead(events);
|
|
118
187
|
if (!firstStart.present)
|
|
119
188
|
return { base_head: null, kind: "history_missing", reason: firstStart.reason };
|
|
@@ -148,17 +217,54 @@ function scanCodeReviewScopeFromBase(projectRoot, base, ignoredPaths = new Set()
|
|
|
148
217
|
}
|
|
149
218
|
}
|
|
150
219
|
}
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
.filter(Boolean)
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
220
|
+
let worktreePaths;
|
|
221
|
+
let untrackedPaths;
|
|
222
|
+
if (base.dirty_files) {
|
|
223
|
+
const dirty = dirtyCodeFiles(projectRoot);
|
|
224
|
+
if (!dirty.ok) {
|
|
225
|
+
scopeReliable = false;
|
|
226
|
+
scopeReason = [scopeReason, dirty.reason].filter(Boolean).join("; ");
|
|
227
|
+
worktreePaths = [];
|
|
228
|
+
}
|
|
229
|
+
else {
|
|
230
|
+
worktreePaths = excludeKnownPaths(uniqSorted(diffFingerprints(base.dirty_files, dirty.files)), ignoredPaths);
|
|
231
|
+
}
|
|
232
|
+
untrackedPaths = [];
|
|
233
|
+
}
|
|
234
|
+
else if (base.reviewed_files) {
|
|
235
|
+
const dirty = dirtyCodePaths(projectRoot);
|
|
236
|
+
if (!dirty.ok) {
|
|
237
|
+
scopeReliable = false;
|
|
238
|
+
scopeReason = [scopeReason, dirty.reason].filter(Boolean).join("; ");
|
|
239
|
+
worktreePaths = [];
|
|
240
|
+
}
|
|
241
|
+
else {
|
|
242
|
+
const reviewed = new Map(base.reviewed_files.map(file => [file.path, file.sha]));
|
|
243
|
+
const changedReviewed = base.reviewed_files
|
|
244
|
+
.filter(file => (codeFileContentSha(projectRoot, file.path) ?? "sha256:missing") !== file.sha)
|
|
245
|
+
.map(file => file.path);
|
|
246
|
+
const newlyChanged = dirty.paths.filter(path => !reviewed.has(path));
|
|
247
|
+
worktreePaths = excludeKnownPaths(uniqSorted([
|
|
248
|
+
...changedReviewed,
|
|
249
|
+
...newlyChanged,
|
|
250
|
+
...(base.required_recheck_paths ?? []),
|
|
251
|
+
]), ignoredPaths);
|
|
252
|
+
}
|
|
253
|
+
untrackedPaths = [];
|
|
254
|
+
}
|
|
255
|
+
else {
|
|
256
|
+
const staged = gitLines(projectRoot, ["diff", "--name-only", "--cached"]);
|
|
257
|
+
const unstaged = gitLines(projectRoot, ["diff", "--name-only"]);
|
|
258
|
+
const untracked = gitLines(projectRoot, ["ls-files", "--others", "--exclude-standard"]);
|
|
259
|
+
if (!staged.ok || !unstaged.ok || !untracked.ok) {
|
|
260
|
+
scopeReliable = false;
|
|
261
|
+
scopeReason = [scopeReason, !staged.ok ? staged.reason : "", !unstaged.ok ? unstaged.reason : "", !untracked.ok ? untracked.reason : ""]
|
|
262
|
+
.filter(Boolean)
|
|
263
|
+
.join("; ");
|
|
264
|
+
}
|
|
265
|
+
worktreePaths = excludeKnownPaths(uniqSorted([...(staged.ok ? staged.lines : []), ...(unstaged.ok ? unstaged.lines : [])]), ignoredPaths);
|
|
266
|
+
untrackedPaths = excludeKnownPaths(uniqSorted(untracked.ok ? untracked.lines : []), ignoredPaths);
|
|
267
|
+
}
|
|
162
268
|
const fallbackPaths = projectHasReadableDirectory(projectRoot)
|
|
163
269
|
? excludeKnownPaths(walkCodeFiles(projectRoot).sort(), ignoredPaths)
|
|
164
270
|
: [];
|
|
@@ -181,7 +287,8 @@ export function scanCodeReviewScope(projectRoot, events) {
|
|
|
181
287
|
}
|
|
182
288
|
export function scanCodeChangesForReview(projectRoot, events) {
|
|
183
289
|
const scope = scanCodeReviewScope(projectRoot, events);
|
|
184
|
-
const
|
|
290
|
+
const hasReviewHistory = collectCodeReviewGateFacts(events).jobs.length > 0;
|
|
291
|
+
const hasCodeChanges = hasReviewHistory || !scope.scope_reliable ||
|
|
185
292
|
scope.committed_paths == null ||
|
|
186
293
|
scope.committed_paths.length > 0 ||
|
|
187
294
|
scope.worktree_paths.length > 0 ||
|
|
@@ -202,7 +309,16 @@ function samePathSet(left, right) {
|
|
|
202
309
|
const b = [...new Set(right)].sort();
|
|
203
310
|
return a.length === b.length && a.every((path, index) => path === b[index]);
|
|
204
311
|
}
|
|
205
|
-
|
|
312
|
+
function currentPathsForFrozenCodeReview(projectRoot, events, job, extraIgnoredPaths = []) {
|
|
313
|
+
const creationIndex = events.findIndex(ev => ev.event_type === "transition_commit" &&
|
|
314
|
+
(ev.payload.new_jobs ?? []).some(candidate => candidate.job_id === job.job_id));
|
|
315
|
+
const priorEvents = creationIndex >= 0 ? events.slice(0, creationIndex) : events;
|
|
316
|
+
const ignored = knownCodeReviewReportPaths(projectRoot, events);
|
|
317
|
+
for (const path of extraIgnoredPaths)
|
|
318
|
+
ignored.add(normalizeKnownPath(path));
|
|
319
|
+
return scanCodeReviewScopeFromBase(projectRoot, selectCodeReviewBase(priorEvents), ignored).review_paths;
|
|
320
|
+
}
|
|
321
|
+
export function codeReviewJobStaleReason(projectRoot, job, currentPaths, events, extraIgnoredPaths = []) {
|
|
206
322
|
if (!isCodeReviewerJob(job))
|
|
207
323
|
return null;
|
|
208
324
|
const frozenScope = job.packet_context?.code_review_scope;
|
|
@@ -211,10 +327,14 @@ export function codeReviewJobStaleReason(projectRoot, job, currentPaths) {
|
|
|
211
327
|
if (frozenScope.current_head !== currentHead.head) {
|
|
212
328
|
return `代码审查创建后的 HEAD 已变化(原记录:${frozenScope.current_head ?? "<none>"};当前:${currentHead.head ?? "<none>"})`;
|
|
213
329
|
}
|
|
214
|
-
const
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
330
|
+
const currentScopePaths = events
|
|
331
|
+
? currentPathsForFrozenCodeReview(projectRoot, events, job, extraIgnoredPaths)
|
|
332
|
+
: currentPaths ?? scanCodeChanges(projectRoot).paths;
|
|
333
|
+
const frozenScopePaths = events
|
|
334
|
+
? frozenScope.review_paths
|
|
335
|
+
: uniqSorted([...frozenScope.worktree_paths, ...frozenScope.untracked_paths]);
|
|
336
|
+
if (!samePathSet(frozenScopePaths, currentScopePaths)) {
|
|
337
|
+
return `代码审查范围已变化:工作区范围已变化(原范围:${frozenScopePaths.join(", ") || "<none>"};当前范围:${currentScopePaths.join(", ") || "<none>"})`;
|
|
218
338
|
}
|
|
219
339
|
for (const bound of job.boundFiles) {
|
|
220
340
|
const currentSha = codeFileContentSha(projectRoot, bound.path) ?? "sha256:missing";
|
package/dist/job_validity.js
CHANGED
|
@@ -12,7 +12,7 @@ function reviewReadyVerifierWithoutEvidenceReason(job) {
|
|
|
12
12
|
}
|
|
13
13
|
export function invalidReasonForSnapshot(input) {
|
|
14
14
|
if (input.job.role === "code-reviewer") {
|
|
15
|
-
return codeReviewJobStaleReason(input.projectRoot, input.job, currentCodeReviewWorkingPaths(input.projectRoot, input.events));
|
|
15
|
+
return codeReviewJobStaleReason(input.projectRoot, input.job, currentCodeReviewWorkingPaths(input.projectRoot, input.events), input.events);
|
|
16
16
|
}
|
|
17
17
|
const invalidReviewReadyVerifier = reviewReadyVerifierWithoutEvidenceReason(input.job);
|
|
18
18
|
if (invalidReviewReadyVerifier)
|
|
@@ -25,7 +25,10 @@ export function invalidReasonForSubmittedReport(job, context) {
|
|
|
25
25
|
...(context.ignoredCodePaths ?? []),
|
|
26
26
|
...(context.reportPath ? [context.reportPath] : []),
|
|
27
27
|
]);
|
|
28
|
-
return codeReviewJobStaleReason(context.projectRoot, job, currentPaths
|
|
28
|
+
return codeReviewJobStaleReason(context.projectRoot, job, currentPaths, context.events, [
|
|
29
|
+
...(context.ignoredCodePaths ?? []),
|
|
30
|
+
...(context.reportPath ? [context.reportPath] : []),
|
|
31
|
+
]);
|
|
29
32
|
}
|
|
30
33
|
const invalidReviewReadyVerifier = reviewReadyVerifierWithoutEvidenceReason(job);
|
|
31
34
|
if (invalidReviewReadyVerifier)
|
package/dist/phase_plan.d.ts
CHANGED
|
@@ -106,6 +106,8 @@ export interface ApplyPendingTaskStatus {
|
|
|
106
106
|
export declare function executionPolicyForRisk(risk: ReviewRisk): ExecutionPolicy;
|
|
107
107
|
export declare function executionPolicyForCurrentRound(events: Event[]): ExecutionPolicy;
|
|
108
108
|
export declare function proposalDocsBaseline(changeRoot: string): Record<string, string>;
|
|
109
|
+
export declare function applyPlanningBaseline(changeRoot: string): Record<string, string>;
|
|
110
|
+
export declare function applyPlanningDocsChangedSinceBaseline(changeRoot: string, baseline: Record<string, string>): boolean;
|
|
109
111
|
export declare function discoveryDocsBaseline(changeRoot: string): Record<string, string>;
|
|
110
112
|
/** 新 Explore 轮次冻结已有已确认事项,避免把历史答复当作本轮遗漏。 */
|
|
111
113
|
export declare function exploreAnswerRegistrationPayloadForChange(changeRoot: string): ReturnType<typeof exploreAnswerRegistrationPayload>;
|
package/dist/phase_plan.js
CHANGED
|
@@ -25,14 +25,14 @@ function freeTextDecisionAsk(change, question, scope) {
|
|
|
25
25
|
required_fields: ["answer"],
|
|
26
26
|
};
|
|
27
27
|
}
|
|
28
|
-
function requiredArtifact(projectRoot, change, changeRoot, state, kind,
|
|
29
|
-
const artifactPath = relative(projectRoot, join(changeRoot,
|
|
28
|
+
function requiredArtifact(projectRoot, change, changeRoot, state, kind, canonicalPath, risk) {
|
|
29
|
+
const artifactPath = relative(projectRoot, join(changeRoot, canonicalPath)).replaceAll("\\", "/");
|
|
30
30
|
return {
|
|
31
31
|
kind: "artifact_required",
|
|
32
32
|
state,
|
|
33
33
|
artifact: { kind, path: artifactPath, operation: "create_or_update" },
|
|
34
34
|
resume: { argv: nextArgv(change, risk) },
|
|
35
|
-
reason: `${
|
|
35
|
+
reason: `${canonicalPath.split("/").at(-1)} 不存在`,
|
|
36
36
|
};
|
|
37
37
|
}
|
|
38
38
|
function phaseConfirmationStep(context, boundary, reason) {
|
|
@@ -244,6 +244,15 @@ export function proposalDocsBaseline(changeRoot) {
|
|
|
244
244
|
}
|
|
245
245
|
return baseline;
|
|
246
246
|
}
|
|
247
|
+
export function applyPlanningBaseline(changeRoot) {
|
|
248
|
+
const baseline = proposalDocsBaseline(changeRoot);
|
|
249
|
+
delete baseline["tasks.md"];
|
|
250
|
+
return baseline;
|
|
251
|
+
}
|
|
252
|
+
export function applyPlanningDocsChangedSinceBaseline(changeRoot, baseline) {
|
|
253
|
+
const current = applyPlanningBaseline(changeRoot);
|
|
254
|
+
return Object.entries(baseline).some(([path, digest]) => current[path] !== digest);
|
|
255
|
+
}
|
|
247
256
|
export function discoveryDocsBaseline(changeRoot) {
|
|
248
257
|
// 与 Explore gate 使用同一组审查目标。回退到 explore 后,至少要更新一项
|
|
249
258
|
// discovery 材料,才允许重新进入 propose,避免把一次纯状态回退误当作新探索轮次。
|
|
@@ -476,6 +485,20 @@ export function pendingTaskStatusForApply(changeRoot, events) {
|
|
|
476
485
|
completedByEvent,
|
|
477
486
|
};
|
|
478
487
|
}
|
|
488
|
+
function isPostApplyPlanOnlyRepair(changeRoot, events) {
|
|
489
|
+
// Propose 返工发生在新 Apply round 之前,旧 round 的 task_completed 不能覆盖
|
|
490
|
+
// 当前计划明确重新打开的 checkbox;这里以当前 tasks.md 为准。
|
|
491
|
+
if (pendingTaskIds(changeRoot).length > 0)
|
|
492
|
+
return false;
|
|
493
|
+
const roundId = currentProposeRoundId(events);
|
|
494
|
+
const roundEvent = events.find(event => event.event_id === roundId);
|
|
495
|
+
if (roundEvent?.event_type !== "transition_commit")
|
|
496
|
+
return false;
|
|
497
|
+
const payload = roundEvent.payload;
|
|
498
|
+
return payload.transition === "reopen"
|
|
499
|
+
&& payload.reopen_target === "propose"
|
|
500
|
+
&& ["apply", "apply_done", "review", "accepted"].includes(String(payload.reopen_source));
|
|
501
|
+
}
|
|
479
502
|
export function formatPendingTaskMessage(ids, action) {
|
|
480
503
|
return `尚有未完成任务:${ids.join(", ")};${action}`;
|
|
481
504
|
}
|
|
@@ -532,7 +555,7 @@ export function planNextStep(context) {
|
|
|
532
555
|
}
|
|
533
556
|
const discoveryPath = join(changeRoot, ".superspec", "artifacts", "discovery.md");
|
|
534
557
|
if (!existsSync(discoveryPath)) {
|
|
535
|
-
return requiredArtifact(projectRoot, change, changeRoot, "explore", "discovery", "discovery.md", mode.risk);
|
|
558
|
+
return requiredArtifact(projectRoot, change, changeRoot, "explore", "discovery", ".superspec/artifacts/discovery.md", mode.risk);
|
|
536
559
|
}
|
|
537
560
|
const discoveryCheck = validateDiscovery(changeRoot);
|
|
538
561
|
if (!discoveryCheck.ok) {
|
|
@@ -616,7 +639,10 @@ export function planNextStep(context) {
|
|
|
616
639
|
}
|
|
617
640
|
const testContractPath = join(changeRoot, ".superspec", "artifacts", "test-contract.md");
|
|
618
641
|
if (!existsSync(testContractPath)) {
|
|
619
|
-
return requiredArtifact(projectRoot, change, changeRoot, "propose", "test_contract", "test-contract.md", mode.risk);
|
|
642
|
+
return requiredArtifact(projectRoot, change, changeRoot, "propose", "test_contract", ".superspec/artifacts/test-contract.md", mode.risk);
|
|
643
|
+
}
|
|
644
|
+
if (!existsSync(join(changeRoot, "tasks.md"))) {
|
|
645
|
+
return requiredArtifact(projectRoot, change, changeRoot, "propose", "tasks", "tasks.md", mode.risk);
|
|
620
646
|
}
|
|
621
647
|
const planningProfile = planningValidationProfileForPendingProposeRound(events);
|
|
622
648
|
const preflight = validatePlanningPreflight(projectRoot, change, changeRoot, mode.risk, executionPolicyForRisk(mode.risk), planningProfile);
|
|
@@ -646,7 +672,8 @@ export function planNextStep(context) {
|
|
|
646
672
|
return requiredJobs("propose_ready", proposalReviewJobs, `有 ${proposalReviewJobs.length} 个待完成 proposal 审查工作项`);
|
|
647
673
|
}
|
|
648
674
|
const startApplyPlan = planStartApplyTransition(context, false);
|
|
649
|
-
|
|
675
|
+
const planOnlyRepair = isPostApplyPlanOnlyRepair(changeRoot, events);
|
|
676
|
+
if (startApplyPlan.kind === "advance" && !planOnlyRepair) {
|
|
650
677
|
const confirmation = phaseConfirmationStep(context, "propose_to_apply", "计划阶段完成,等待用户确认开始实现");
|
|
651
678
|
if (confirmation)
|
|
652
679
|
return confirmation;
|
|
@@ -661,7 +688,12 @@ export function planNextStep(context) {
|
|
|
661
688
|
};
|
|
662
689
|
return { kind: "ask_user", state: "propose_ready", ask, reason: startApplyPlan.message };
|
|
663
690
|
}
|
|
664
|
-
return {
|
|
691
|
+
return {
|
|
692
|
+
kind: "run_transition",
|
|
693
|
+
state: "propose_ready",
|
|
694
|
+
transition: "start-apply",
|
|
695
|
+
reason: planOnlyRepair ? "计划材料已修正且没有待实施任务,跳过重复的 Apply 确认" : "计划就绪,开始执行",
|
|
696
|
+
};
|
|
665
697
|
}
|
|
666
698
|
case "apply":
|
|
667
699
|
return planApplyNext(context);
|
|
@@ -746,7 +778,7 @@ export function blockingJobsForApplyDone(projectRoot, events, snapshot) {
|
|
|
746
778
|
const facts = collectCodeReviewGateFacts(events);
|
|
747
779
|
const currentWorkingPaths = currentCodeReviewWorkingPaths(projectRoot, events);
|
|
748
780
|
const freshCodeReviewJobIds = new Set(facts.openJobs
|
|
749
|
-
.filter(job => codeReviewJobStaleReason(projectRoot, job, currentWorkingPaths) == null)
|
|
781
|
+
.filter(job => codeReviewJobStaleReason(projectRoot, job, currentWorkingPaths, events) == null)
|
|
750
782
|
.map(job => job.job_id));
|
|
751
783
|
return snapshot.open_jobs.filter(job => job.role !== "code-reviewer" || freshCodeReviewJobIds.has(job.job_id));
|
|
752
784
|
}
|
|
@@ -770,7 +802,7 @@ function planApplyDoneNext(context) {
|
|
|
770
802
|
}
|
|
771
803
|
const latest = facts.latestTerminal;
|
|
772
804
|
if (latest?.state === "rejected" && latest.result_kind === "review_failed") {
|
|
773
|
-
const staleReason = codeReviewJobStaleReason(context.projectRoot, latest.job, currentWorkingPaths);
|
|
805
|
+
const staleReason = codeReviewJobStaleReason(context.projectRoot, latest.job, currentWorkingPaths, events);
|
|
774
806
|
if (staleReason) {
|
|
775
807
|
return {
|
|
776
808
|
kind: "run_transition",
|
|
@@ -871,7 +903,7 @@ function planApplyDoneNext(context) {
|
|
|
871
903
|
: undefined;
|
|
872
904
|
const acceptedCurrentHead = acceptedScope?.current_head;
|
|
873
905
|
const acceptedReviewReady = latest?.state === "accepted" &&
|
|
874
|
-
codeReviewJobStaleReason(context.projectRoot, latest.job, currentWorkingPaths) == null && (acceptedCurrentHead === null ||
|
|
906
|
+
codeReviewJobStaleReason(context.projectRoot, latest.job, currentWorkingPaths, events) == null && (acceptedCurrentHead === null ||
|
|
875
907
|
(typeof acceptedCurrentHead === "string" && acceptedCurrentHead.trim() !== ""));
|
|
876
908
|
if (!codeScan.hasCodeChanges || acceptedReviewReady) {
|
|
877
909
|
const confirmation = phaseConfirmationStep(context, "apply_to_review", "Apply 与代码审查完成,等待用户确认进入最终审查");
|
|
@@ -1061,10 +1093,12 @@ function planStartApplyTransition(context, enforceConfirmation = true) {
|
|
|
1061
1093
|
reason: `进入执行阶段前需要重新完成计划文档审查:${gatePlan.reason}`,
|
|
1062
1094
|
};
|
|
1063
1095
|
}
|
|
1096
|
+
const planOnlyRepair = isPostApplyPlanOnlyRepair(changeRoot, events);
|
|
1064
1097
|
const acceptedConfirmation = enforceConfirmation
|
|
1098
|
+
&& !planOnlyRepair
|
|
1065
1099
|
? acceptedProposeToApplyConfirmation(context, risk)
|
|
1066
1100
|
: null;
|
|
1067
|
-
if (enforceConfirmation && !acceptedConfirmation) {
|
|
1101
|
+
if (enforceConfirmation && !acceptedConfirmation && !planOnlyRepair) {
|
|
1068
1102
|
const confirmation = phaseConfirmationForBoundary(projectRoot, events, snapshot, "propose_to_apply", risk);
|
|
1069
1103
|
return {
|
|
1070
1104
|
kind: "skip",
|
|
@@ -1076,7 +1110,7 @@ function planStartApplyTransition(context, enforceConfirmation = true) {
|
|
|
1076
1110
|
kind: "advance",
|
|
1077
1111
|
fromState: "propose_ready",
|
|
1078
1112
|
toState: "apply",
|
|
1079
|
-
reason: "进入执行阶段",
|
|
1113
|
+
reason: planOnlyRepair ? "计划材料修正完成且没有待实施任务" : "进入执行阶段",
|
|
1080
1114
|
payload: {
|
|
1081
1115
|
apply_start_head: gitHead.head,
|
|
1082
1116
|
apply_start_head_reason: gitHead.reason,
|
|
@@ -1088,6 +1122,7 @@ function planStartApplyTransition(context, enforceConfirmation = true) {
|
|
|
1088
1122
|
review_risk: risk,
|
|
1089
1123
|
requires_verifier: risk !== "minimal",
|
|
1090
1124
|
},
|
|
1125
|
+
apply_planning_baseline: applyPlanningBaseline(changeRoot),
|
|
1091
1126
|
...(acceptedConfirmation ? phaseConfirmationCommitPayload(acceptedConfirmation.confirmation, acceptedConfirmation.decision) : {}),
|
|
1092
1127
|
},
|
|
1093
1128
|
};
|
package/dist/record.js
CHANGED
|
@@ -102,7 +102,7 @@ function reviewScopeInstruction(job, reviewTargets, readOnlyRefs) {
|
|
|
102
102
|
function genericReviewCoverageInstruction(job) {
|
|
103
103
|
if (!requiresReviewScope(job))
|
|
104
104
|
return "";
|
|
105
|
-
return "完整审查全部 boundFiles,不因发现第一个 blocker 停止;read_only_refs 只在核对本次问题与上下游一致性时读取。review_scope.checked_paths
|
|
105
|
+
return "完整审查全部 boundFiles,不因发现第一个 blocker 停止;read_only_refs 只在核对本次问题与上下游一致性时读取。review_scope.checked_paths 只填写本次实际浏览并完成语义审查的绑定文件,不能根据 packet 预填;未检查项如实写入 unchecked。覆盖回执不能代替语义审查,也不扩大可报告问题的范围。";
|
|
106
106
|
}
|
|
107
107
|
function proposalIncrementalReviewInstruction(job) {
|
|
108
108
|
if (!PROPOSE_FINAL_REVIEW_GATE.isJobForGate(job))
|
|
@@ -317,6 +317,9 @@ function validateCodeReviewScope(obj, job, checks) {
|
|
|
317
317
|
checks.push(`代码审查报告未说明是否检查了 ${bound.path}`);
|
|
318
318
|
}
|
|
319
319
|
}
|
|
320
|
+
if (obj.verdict === "pass" && uncheckedPaths.size > 0) {
|
|
321
|
+
checks.push("代码审查结论为 pass 时不能包含未检查的绑定文件");
|
|
322
|
+
}
|
|
320
323
|
}
|
|
321
324
|
}
|
|
322
325
|
function validateReviewScope(obj, job, checks) {
|
|
@@ -991,7 +994,7 @@ function recordUserDecisionLoaded(projectRoot, change, events, content, inputDig
|
|
|
991
994
|
? status.findings.find(item => item.id === ref.findingId && (item.type === "spec" || item.type === "mixed"))
|
|
992
995
|
: null;
|
|
993
996
|
const staleReason = status && ref && status.terminal.job.job_id === ref.jobId
|
|
994
|
-
? codeReviewJobStaleReason(projectRoot, status.terminal.job, currentCodeReviewWorkingPaths(projectRoot, events))
|
|
997
|
+
? codeReviewJobStaleReason(projectRoot, status.terminal.job, currentCodeReviewWorkingPaths(projectRoot, events), events)
|
|
995
998
|
: null;
|
|
996
999
|
if (!ref || snapshot.state !== "apply_done" || !status || !finding || staleReason) {
|
|
997
1000
|
const reason = !ref
|
|
@@ -1231,7 +1234,7 @@ export function jobsPacket(projectRoot, change, jobId) {
|
|
|
1231
1234
|
(requiresReviewer(job.role) ? `必须由独立 ${recommendedAgentForRole(job.role)} 审查角色执行,并在审查者来源字段(reviewer.kind/id)中记录来源,` : "") +
|
|
1232
1235
|
`产出 JSON 报告内容并优先通过 --report - 从 stdin 登记;文件路径模式仅作备用。${recordInputInstruction(job)}协议字段含义见 packet 顶层“字段说明”,普通对话不要原样复述 JSON。` +
|
|
1233
1236
|
(isCodeReviewer
|
|
1234
|
-
?
|
|
1237
|
+
? `格式骨架:{"role":"code-reviewer","verdict":"pass","review_scope":{"job_id":"${job.job_id}","packet_digest":"${job.packet_digest}","checked_paths":[],"checked_docs":[],"unchecked":[]},"findings":[],"reviewer":{"kind":"codex-subagent","id":"<thread-or-agent-id>"}}。提交前按真实审查结果填写数组;不得从 boundFiles 自动复制 checked_paths。verdict 只能为 pass 或 fail;审查覆盖范围(review_scope)用来说明本次审查覆盖了哪些文件和文档,已检查路径(checked_paths)与未检查项(unchecked)必须合起来覆盖全部绑定文件(boundFiles),unchecked 条目格式为 {"path":"<path>","reason":"<reason>"};pass 不允许仍有未检查的绑定文件。`
|
|
1235
1238
|
+ `报告结论为 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 表示需要使用者判断的混合问题。`
|
|
1236
1239
|
+ (packetContext?.task_execution_index
|
|
1237
1240
|
? `本工作项带任务执行索引(task_execution_index):按 task 对照其执行依据快照(contract)审查——实现路线对照 design 引用原文、累计 diff 对照 guard 边界、测试断言对照 tests 声明的 scenario;每项的 required_evidence 是 task-start 冻结的证据口径,red_required/green_required 分别说明是否需要 RED/GREEN;fix 非空表示状态机创建的实现修复,source、parent_task_id 和 reason 说明其归属,code_review 来源还需核对 review_finding;每项的 scope_note 是执行者登记的范围扩大说明,判断其合理性与验证充分性;changed_paths 是归属线索不是结论(null 表示未知);unattributed_paths 中的无主改动逐个判断合理性;coverage_exemption_refs 解释未绑定 task 的 TEST 豁免。`
|
package/dist/transition.js
CHANGED
|
@@ -9,7 +9,7 @@ import { REVIEW_CODE_REVIEW_GATE_ID, REVIEW_FINAL_VERIFIER_GATE, REVIEW_FINAL_VE
|
|
|
9
9
|
import { codeReviewBoundFiles, codeReviewDecisionScope, codeReviewJobStaleReason, codeReviewPacketContext, codeReviewPacketDigest, collectCodeReviewGateFacts, computeCodeStateCheck, currentCodeReviewWorkingPaths, dismissedCodeReviewSummary, effectiveCoverageExemptionRefsFromEvents, latestCodeReviewGateEvidence, latestCodeReviewDecision, latestCodeReviewFailedStatus, missingCoverageExemptionTestIds, requiresFinalVerifierForCurrentReview, scanCodeChangesForReview, taskExecutionIndexForReview, } from "./code_review.js";
|
|
10
10
|
import { taskEvidenceReadiness } from "./task_evidence.js";
|
|
11
11
|
import { adoptedContractForTask, findTaskInLines, isFixTaskId, parseTasksMd, parseTestContractEntries, } from "./format.js";
|
|
12
|
-
import { applyRequirementModeForCurrentRound, executionRequirementVersionForCurrentRound, blockingJobsForApplyDone, executionPolicyForCurrentRound, formatPendingTaskMessage, latestAcceptedProposalBaseline, pendingTaskStatusForApply, planningValidationProfileForNewRound, planTransition, discoveryDocsBaseline, exploreAnswerRegistrationPayloadForChange, proposeAnswerRegistrationPayloadForChange, proposalDocsBaseline, } from "./phase_plan.js";
|
|
12
|
+
import { applyRequirementModeForCurrentRound, applyPlanningDocsChangedSinceBaseline, executionRequirementVersionForCurrentRound, blockingJobsForApplyDone, executionPolicyForCurrentRound, formatPendingTaskMessage, latestAcceptedProposalBaseline, pendingTaskStatusForApply, planningValidationProfileForNewRound, planTransition, discoveryDocsBaseline, exploreAnswerRegistrationPayloadForChange, proposeAnswerRegistrationPayloadForChange, proposalDocsBaseline, } from "./phase_plan.js";
|
|
13
13
|
import { latestAcceptedPhaseDecision, phaseConfirmationCommitPayload, phaseConfirmationForBoundary, phaseConfirmationMissingMessage, } from "./phase_confirmation.js";
|
|
14
14
|
import { currentGitHead, dirtyCodeFiles, stageProductionJavaFilesSince } from "./git_state.js";
|
|
15
15
|
import { workflowRiskForProject } from "./workflow_config.js";
|
|
@@ -209,8 +209,8 @@ function compileRequiredEvidence(executionPolicy, testIds, requiresVerificationW
|
|
|
209
209
|
accepted_green_statuses: ["expected_success"],
|
|
210
210
|
};
|
|
211
211
|
}
|
|
212
|
-
function evidenceActionsForAttempt(change, attemptId, required) {
|
|
213
|
-
const testIds = required.test_ids.length > 0 ? required.test_ids : [
|
|
212
|
+
function evidenceActionsForAttempt(change, attemptId, fallbackTestId, required) {
|
|
213
|
+
const testIds = required.test_ids.length > 0 ? required.test_ids : [fallbackTestId];
|
|
214
214
|
const statuses = [];
|
|
215
215
|
if (required.red_required)
|
|
216
216
|
statuses.push("expected_failure");
|
|
@@ -218,10 +218,10 @@ function evidenceActionsForAttempt(change, attemptId, required) {
|
|
|
218
218
|
statuses.push(required.accepted_green_statuses[0] ?? "expected_success");
|
|
219
219
|
return testIds.flatMap(testId => statuses.map(semanticStatus => ({
|
|
220
220
|
kind: "test_run",
|
|
221
|
-
|
|
221
|
+
test_id: testId,
|
|
222
222
|
record_argv: ["superspec", "record", "test-run", "--change", change, "--input", "-"],
|
|
223
223
|
record_input: {
|
|
224
|
-
|
|
224
|
+
test_id: testId,
|
|
225
225
|
attempt_id: attemptId,
|
|
226
226
|
command: null,
|
|
227
227
|
cwd: null,
|
|
@@ -428,8 +428,8 @@ function fixDescriptorForTask(events, taskId) {
|
|
|
428
428
|
}
|
|
429
429
|
: null;
|
|
430
430
|
}
|
|
431
|
-
function isFreshOpenCodeReviewerJob(job, projectRoot, currentWorkingPaths) {
|
|
432
|
-
return codeReviewJobStaleReason(projectRoot, job, currentWorkingPaths) == null;
|
|
431
|
+
function isFreshOpenCodeReviewerJob(job, projectRoot, events, currentWorkingPaths) {
|
|
432
|
+
return codeReviewJobStaleReason(projectRoot, job, currentWorkingPaths, events) == null;
|
|
433
433
|
}
|
|
434
434
|
function hasFrozenCodeReviewCurrentHead(scope) {
|
|
435
435
|
if (!scope || typeof scope !== "object")
|
|
@@ -444,7 +444,7 @@ function evaluateApplyDoneCodeReviewGate(input) {
|
|
|
444
444
|
const facts = collectCodeReviewGateFacts(input.events);
|
|
445
445
|
if (scan.hasCodeChanges) {
|
|
446
446
|
const currentWorkingPaths = currentCodeReviewWorkingPaths(input.projectRoot, input.events);
|
|
447
|
-
const freshOpenJobs = facts.openJobs.filter(job => isFreshOpenCodeReviewerJob(job, input.projectRoot, currentWorkingPaths));
|
|
447
|
+
const freshOpenJobs = facts.openJobs.filter(job => isFreshOpenCodeReviewerJob(job, input.projectRoot, input.events, currentWorkingPaths));
|
|
448
448
|
if (freshOpenJobs.length > 0) {
|
|
449
449
|
return {
|
|
450
450
|
blocked: true,
|
|
@@ -455,7 +455,7 @@ function evaluateApplyDoneCodeReviewGate(input) {
|
|
|
455
455
|
const latest = facts.latestTerminal;
|
|
456
456
|
if (latest?.state === "accepted") {
|
|
457
457
|
const acceptedScope = latest.job.packet_context?.code_review_scope;
|
|
458
|
-
const staleReason = codeReviewJobStaleReason(input.projectRoot, latest.job, currentWorkingPaths);
|
|
458
|
+
const staleReason = codeReviewJobStaleReason(input.projectRoot, latest.job, currentWorkingPaths, input.events);
|
|
459
459
|
if (staleReason || !hasFrozenCodeReviewCurrentHead(acceptedScope)) {
|
|
460
460
|
const { job, scanReason } = createCodeReviewerJob(input.change, input.projectRoot, input.changeRoot, input.events);
|
|
461
461
|
return {
|
|
@@ -485,7 +485,7 @@ function evaluateApplyDoneCodeReviewGate(input) {
|
|
|
485
485
|
};
|
|
486
486
|
}
|
|
487
487
|
if (latest?.state === "rejected" && latest.result_kind === "review_failed") {
|
|
488
|
-
const staleReason = codeReviewJobStaleReason(input.projectRoot, latest.job, currentWorkingPaths);
|
|
488
|
+
const staleReason = codeReviewJobStaleReason(input.projectRoot, latest.job, currentWorkingPaths, input.events);
|
|
489
489
|
if (staleReason) {
|
|
490
490
|
const { job, scanReason } = createCodeReviewerJob(input.change, input.projectRoot, input.changeRoot, input.events);
|
|
491
491
|
return {
|
|
@@ -820,6 +820,9 @@ export function taskStart(projectRoot, change, changeRoot, taskId) {
|
|
|
820
820
|
if (snapshot.state !== "apply")
|
|
821
821
|
return { skip: true, message: `当前状态 ${snapshot.state},需要 apply` };
|
|
822
822
|
const events = readEvents(projectRoot, change);
|
|
823
|
+
if (applyPlanningMaterialsChanged(changeRoot, events)) {
|
|
824
|
+
return { skip: true, message: "Apply 期间计划材料已变化;请回到 Propose 核对并重新批准计划后再继续任务" };
|
|
825
|
+
}
|
|
823
826
|
const tasksContent = readFileSync(join(changeRoot, "tasks.md"), "utf8");
|
|
824
827
|
const lines = tasksContent.split("\n");
|
|
825
828
|
const taskLineIdx = findTaskLine(lines, taskId);
|
|
@@ -890,7 +893,7 @@ export function taskStart(projectRoot, change, changeRoot, taskId) {
|
|
|
890
893
|
...boundarySnapshotPayload(projectRoot),
|
|
891
894
|
};
|
|
892
895
|
const evidenceActions = requiredEvidence
|
|
893
|
-
? evidenceActionsForAttempt(change, attempt.attempt_id, requiredEvidence)
|
|
896
|
+
? evidenceActionsForAttempt(change, attempt.attempt_id, taskId, requiredEvidence)
|
|
894
897
|
: null;
|
|
895
898
|
return {
|
|
896
899
|
fromState: "apply", toState: "apply", outcome: "advanced",
|
|
@@ -934,6 +937,36 @@ function invalidateOpenJobs(snapshot, to, reason) {
|
|
|
934
937
|
},
|
|
935
938
|
}));
|
|
936
939
|
}
|
|
940
|
+
function latestApplyPlanningBaseline(events) {
|
|
941
|
+
for (let index = events.length - 1; index >= 0; index--) {
|
|
942
|
+
const event = events[index];
|
|
943
|
+
if (event.event_type !== "transition_commit")
|
|
944
|
+
continue;
|
|
945
|
+
const payload = event.payload;
|
|
946
|
+
if (payload.transition !== "start-apply")
|
|
947
|
+
continue;
|
|
948
|
+
const baseline = payload.apply_planning_baseline;
|
|
949
|
+
if (!baseline || typeof baseline !== "object" || Array.isArray(baseline))
|
|
950
|
+
return null;
|
|
951
|
+
const entries = Object.entries(baseline);
|
|
952
|
+
return entries.every(([, digest]) => typeof digest === "string")
|
|
953
|
+
? Object.fromEntries(entries)
|
|
954
|
+
: null;
|
|
955
|
+
}
|
|
956
|
+
return null;
|
|
957
|
+
}
|
|
958
|
+
function applyPlanningMaterialsChanged(changeRoot, events) {
|
|
959
|
+
const baseline = latestApplyPlanningBaseline(events);
|
|
960
|
+
return baseline != null && applyPlanningDocsChangedSinceBaseline(changeRoot, baseline);
|
|
961
|
+
}
|
|
962
|
+
function proposalReopenBaseline(changeRoot, events, source) {
|
|
963
|
+
const applyBaseline = ["apply", "apply_done", "review"].includes(source)
|
|
964
|
+
? latestApplyPlanningBaseline(events)
|
|
965
|
+
: null;
|
|
966
|
+
return applyBaseline
|
|
967
|
+
? { baseline: { ...proposalDocsBaseline(changeRoot), ...applyBaseline }, source: "apply" }
|
|
968
|
+
: { baseline: proposalDocsBaseline(changeRoot), source: "reopen_fallback" };
|
|
969
|
+
}
|
|
937
970
|
function planningReopenExtraEvents(snapshot, to, reason) {
|
|
938
971
|
return [
|
|
939
972
|
...invalidateOpenJobs(snapshot, to, reason),
|
|
@@ -1004,6 +1037,9 @@ export function reopen(projectRoot, change, changeRoot, to, reason, opts = {}) {
|
|
|
1004
1037
|
return { skip: true, message: "--review-fix 只能用于回到实现阶段(reopen --to apply)" };
|
|
1005
1038
|
if (snapshot.state !== "apply_done")
|
|
1006
1039
|
return { skip: true, message: `当前状态 ${snapshot.state},不能通过代码审查修复回到实现阶段` };
|
|
1040
|
+
if (applyPlanningMaterialsChanged(changeRoot, events)) {
|
|
1041
|
+
return { skip: true, message: "计划材料已变化,不能作为纯实现问题回到 Apply;请 reopen --to propose" };
|
|
1042
|
+
}
|
|
1007
1043
|
const ref = parseCodeReviewFindingRef(opts.reviewFix);
|
|
1008
1044
|
if (!ref)
|
|
1009
1045
|
return { skip: true, message: "--review-fix 必须是 <job_id>#<finding_id>" };
|
|
@@ -1045,6 +1081,9 @@ export function reopen(projectRoot, change, changeRoot, to, reason, opts = {}) {
|
|
|
1045
1081
|
return { skip: true, message: `当前状态 ${snapshot.state},不能通过自测问题回到实现阶段` };
|
|
1046
1082
|
}
|
|
1047
1083
|
const parentTaskId = opts.selfTestFix.trim();
|
|
1084
|
+
if (applyPlanningMaterialsChanged(changeRoot, events)) {
|
|
1085
|
+
return { skip: true, message: "计划材料已变化,不能作为纯实现问题创建 self-test 修复;请 reopen --to propose" };
|
|
1086
|
+
}
|
|
1048
1087
|
const parentTask = parseTasksMd(readFileSync(join(changeRoot, "tasks.md"), "utf8"))
|
|
1049
1088
|
.find(task => task.taskId === parentTaskId);
|
|
1050
1089
|
if (!parentTask)
|
|
@@ -1118,12 +1157,13 @@ export function reopen(projectRoot, change, changeRoot, to, reason, opts = {}) {
|
|
|
1118
1157
|
// 的摘要,并用 reopen 当刻的摘要补齐缺项,确保本轮之后对任一审查目标的修改都能被检测。
|
|
1119
1158
|
const baselineNeedsBackfill = acceptedBaseline !== null && Object.keys(currentBaseline)
|
|
1120
1159
|
.some(path => !Object.prototype.hasOwnProperty.call(acceptedBaseline, path));
|
|
1160
|
+
const applyReopenBaseline = acceptedBaseline ? null : proposalReopenBaseline(changeRoot, events, snapshot.state);
|
|
1121
1161
|
const baselineDocs = acceptedBaseline
|
|
1122
1162
|
? Object.fromEntries(Object.entries(currentBaseline).map(([path, digest]) => [
|
|
1123
1163
|
path,
|
|
1124
1164
|
Object.prototype.hasOwnProperty.call(acceptedBaseline, path) ? acceptedBaseline[path] : digest,
|
|
1125
1165
|
]))
|
|
1126
|
-
:
|
|
1166
|
+
: applyReopenBaseline.baseline;
|
|
1127
1167
|
return {
|
|
1128
1168
|
fromState: snapshot.state,
|
|
1129
1169
|
toState: "propose",
|
|
@@ -1132,7 +1172,9 @@ export function reopen(projectRoot, change, changeRoot, to, reason, opts = {}) {
|
|
|
1132
1172
|
commitPayload: {
|
|
1133
1173
|
reopen_target: "propose",
|
|
1134
1174
|
reopen_source: snapshot.state,
|
|
1135
|
-
baseline_source: acceptedBaseline
|
|
1175
|
+
baseline_source: acceptedBaseline
|
|
1176
|
+
? (baselineNeedsBackfill ? "accepted_backfill" : "accepted")
|
|
1177
|
+
: applyReopenBaseline.source,
|
|
1136
1178
|
baseline_docs: baselineDocs,
|
|
1137
1179
|
planning_validation_version: 2,
|
|
1138
1180
|
planning_validation_profile: planningValidationProfileForNewRound(projectRoot),
|
|
@@ -1145,6 +1187,9 @@ export function reopen(projectRoot, change, changeRoot, to, reason, opts = {}) {
|
|
|
1145
1187
|
if (snapshot.state !== "apply_done" && snapshot.state !== "review") {
|
|
1146
1188
|
return { skip: true, message: `当前状态 ${snapshot.state},不能 reopen 到 apply` };
|
|
1147
1189
|
}
|
|
1190
|
+
if (applyPlanningMaterialsChanged(changeRoot, events)) {
|
|
1191
|
+
return { skip: true, message: "计划材料已变化,不能直接回到 Apply;请 reopen --to propose" };
|
|
1192
|
+
}
|
|
1148
1193
|
const pending = pendingTaskStatusForApply(changeRoot, events).pending;
|
|
1149
1194
|
if (pending.length === 0)
|
|
1150
1195
|
return { skip: true, message: "没有未完成任务,不能 reopen 到 apply" };
|
|
@@ -1167,6 +1212,9 @@ export function reviewReady(projectRoot, change, changeRoot, risk = workflowRisk
|
|
|
1167
1212
|
const policy = storedPolicy ?? reviewPolicyForRisk(risk);
|
|
1168
1213
|
const policyPayload = storedPolicy ? {} : { review_policy: policy };
|
|
1169
1214
|
const currentEvidenceDigest = reviewEvidenceDigest(events);
|
|
1215
|
+
if (snapshot.state === "apply" && applyPlanningMaterialsChanged(changeRoot, events)) {
|
|
1216
|
+
return { skip: true, message: "Apply 期间计划材料已变化,不能进入 Review;请回到 Propose 核对并重新批准计划" };
|
|
1217
|
+
}
|
|
1170
1218
|
// 检查是否所有任务已完成
|
|
1171
1219
|
const pending = pendingTaskStatusForApply(changeRoot, events).pending;
|
|
1172
1220
|
if (pending.length > 0)
|
|
@@ -1267,10 +1315,14 @@ export function taskComplete(projectRoot, change, changeRoot, taskId, inputConte
|
|
|
1267
1315
|
const attempt = snapshot.active_task_attempts?.find(a => a.task_id === taskId && a.state === "active");
|
|
1268
1316
|
if (!attempt)
|
|
1269
1317
|
return { skip: true, message: `任务 ${taskId} 无活跃执行尝试` };
|
|
1318
|
+
const events = readEvents(projectRoot, change);
|
|
1319
|
+
if (applyPlanningMaterialsChanged(changeRoot, events)) {
|
|
1320
|
+
return { skip: true, message: "Apply 期间计划材料已变化,不能完成当前任务;请回到 Propose 核对并重新批准计划" };
|
|
1321
|
+
}
|
|
1270
1322
|
const readiness = taskEvidenceReadiness(projectRoot, change, changeRoot, attempt);
|
|
1271
1323
|
if (!readiness.ready)
|
|
1272
1324
|
return { skip: true, message: `任务 ${taskId} 无法完成:${readiness.reason}` };
|
|
1273
|
-
const taskStartBoundary = boundarySnapshotForTaskAttempt(
|
|
1325
|
+
const taskStartBoundary = boundarySnapshotForTaskAttempt(events, attempt.attempt_id);
|
|
1274
1326
|
const completedPayload = {
|
|
1275
1327
|
task_id: taskId,
|
|
1276
1328
|
attempt_id: attempt.attempt_id,
|
package/dist/types.d.ts
CHANGED
|
@@ -226,6 +226,8 @@ export interface TransitionCommitPayload {
|
|
|
226
226
|
review_risk?: "minimal" | "normal" | "strict";
|
|
227
227
|
};
|
|
228
228
|
accepted_baseline_docs?: Record<string, string>;
|
|
229
|
+
/** Apply 开始时冻结的计划材料摘要;tasks.md 由状态机维护,不参与冻结。 */
|
|
230
|
+
apply_planning_baseline?: Record<string, string>;
|
|
229
231
|
/** Propose-ready / start-apply 写入的本轮 workflow mode,后续阶段只读该快照。 */
|
|
230
232
|
workflow_mode?: "minimal" | "normal" | "strict";
|
|
231
233
|
/** v2 起所有普通任务必须有五字段执行依据;缺失表示旧 change,沿用旧规则回放。 */
|
|
@@ -342,7 +344,7 @@ export interface AcceptedMaterialFollowupContinuation {
|
|
|
342
344
|
};
|
|
343
345
|
plan_docs_changed_since_accept: boolean | null;
|
|
344
346
|
}
|
|
345
|
-
export type WorkflowArtifactKind = "discovery" | "test_contract";
|
|
347
|
+
export type WorkflowArtifactKind = "discovery" | "test_contract" | "tasks";
|
|
346
348
|
export interface RequiredWorkflowArtifact {
|
|
347
349
|
kind: WorkflowArtifactKind;
|
|
348
350
|
/** Repository-relative canonical path owned by the workflow engine. */
|
|
@@ -357,10 +359,10 @@ export interface MaterialUpdateRequiredResume {
|
|
|
357
359
|
}
|
|
358
360
|
export interface TestEvidenceAction {
|
|
359
361
|
kind: "test_run";
|
|
360
|
-
test_id
|
|
362
|
+
test_id: string;
|
|
361
363
|
record_argv: string[];
|
|
362
364
|
record_input: {
|
|
363
|
-
test_id
|
|
365
|
+
test_id: string;
|
|
364
366
|
attempt_id: string;
|
|
365
367
|
command: null;
|
|
366
368
|
cwd: null;
|
package/package.json
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
<!-- SUPERSPEC:AGENTS:START -->
|
|
2
2
|
只有当用户显式调用 `superspec-*`,或明确要求继续处理某个已有 SuperSpec change 时,才进入或续转 SuperSpec 工作流。普通开发、修复、排查、测试或审查请求,即使项目已安装 SuperSpec,也不得自行启动工作流、创建 change、执行 `transition next`,或切换到某个 `superspec-*` 阶段。
|
|
3
3
|
|
|
4
|
+
以下规则仅适用于已经显式启动或明确指定的 SuperSpec change。没有活跃 SuperSpec change 时,本区块除上述工作流激活边界外均不适用,按项目常规开发指令执行。
|
|
5
|
+
|
|
4
6
|
一旦用户已显式启动工作流或明确指定 change,使用 `superspec-*` 工作流时一律以 `superspec transition next --change "<change>"` 返回的下一步推进;主流程执行内部命令,不要求用户手动运行工作流命令。流程完成前不得跳阶段、不得自称完成。
|
|
5
7
|
|
|
6
8
|
每完成 `next` 返回的当前事项(材料更新、用户答复回写、实现、验证、审查或修复时),立即再次运行 `superspec transition next --change "<change>"` 并继续处理。完成单个事项不等于完成整个 change;只有工作流明确需要用户决定、当前独立工作项尚未返回结果、遇到真实阻塞或整个 change 已完成时才暂停。
|
|
@@ -13,6 +15,10 @@ Explore 中需要用户决定业务、验收、范围或关键取舍时,先简
|
|
|
13
15
|
|
|
14
16
|
当前 change 的自测、联调或用户指出的问题若仍能由既有 task 的批准行为、边界和验收解释,就在同一 change 内处理:
|
|
15
17
|
|
|
18
|
+
计划材料没有枚举某个类、继承关系、方法或局部实现细节,不等于计划遗漏。只要正确修法能够由既有 task、已批准行为和仓库事实唯一推导,仍属于 Apply;只有需要重新决定公共接口、数据归属、迁移兼容、实现路线、验收或 task 边界时才回 Propose。
|
|
19
|
+
|
|
20
|
+
Apply 以满足已批准行为的最小语义影响面为成功标准。需求未要求改变的公共契约、共享行为和兼容语义应保持不变;扩大公共实现边界必须有当前 task 和真实调用链支持。最小改动不能成为遗漏已确认消费者或验收路径的理由。
|
|
21
|
+
|
|
16
22
|
- 当前 task 尚未完成时,在其范围内直接修复;不要为同一实现问题新增 task 或回 propose。
|
|
17
23
|
- 所有 task 已完成后,若问题仍能关联一个已完成 task、且不改变已批准行为和方案,主流程执行 `superspec transition reopen --change "<change>" --to apply --self-test-fix "<task>" --reason "<reason>"`,让工作流创建修复事项;随后继续 `next`,不得手改 tasks。
|
|
18
24
|
- 无法关联既有 task,或需要改变行为、验收、接口、数据语义或实现路线时,才回 propose。
|
|
@@ -7,13 +7,13 @@ argument-hint: "本次代码审查说明"
|
|
|
7
7
|
|
|
8
8
|
## 角色
|
|
9
9
|
|
|
10
|
-
你是 Code Reviewer
|
|
10
|
+
你是 Code Reviewer。你独立、只读地审查本次实现是否以最小语义影响兑现已批准计划,找出真实 bug、范围遗漏、边界条件、安全/性能/兼容问题、关键测试缺口和无关改动。
|
|
11
11
|
|
|
12
12
|
## 工作边界
|
|
13
13
|
|
|
14
14
|
- 先读任务说明、指定代码范围和相关计划材料;范围和停止条件以任务说明为准。不要把未打开的材料当作审查依据。
|
|
15
15
|
- 只读;不实现修复、不修改计划或证据、不自行宣布完成。上下文不足时明确指出缺口。
|
|
16
|
-
-
|
|
16
|
+
- 修复复核优先关闭原问题,并审查本次变化及其直接影响链路。此前漏报的问题只有在当前代码中存在直接证据、影响既定验收或兼容边界时才能成为新 blocker;不要重新打开与本次修复无关、未变化的模块。
|
|
17
17
|
|
|
18
18
|
## 审查判断
|
|
19
19
|
|
|
@@ -23,11 +23,12 @@ argument-hint: "本次代码审查说明"
|
|
|
23
23
|
|
|
24
24
|
- 实现是否兑现当前任务的验收和边界,且与已批准的方案/规格一致。
|
|
25
25
|
- 是否引入功能、数据、一致性、安全、权限、性能或兼容问题,以及直接的边界条件遗漏。
|
|
26
|
-
-
|
|
26
|
+
- 改动是否覆盖计划及真实调用链已经确认的直接影响,并且每项语义变化都有必要性证据。任务勾选和测试通过不能替代对遗漏路径与无关改动的判断。
|
|
27
|
+
- 文件或代码数量本身不是问题。局部需求改变无关生产行为、扩大共享边界或夹带重构时,只有能够证明这些变化并非当前验收所必需,才作为过度实现问题。
|
|
27
28
|
- 测试是否实际证明相关行为和直接回归风险,而非只存在一条通过记录。
|
|
28
29
|
- 需求源已更新时,代码是否仍在执行过期计划;此类问题按方案或需求缺口归因,不把旧材料当作当前依据。
|
|
29
30
|
|
|
30
|
-
|
|
31
|
+
风格偏好、无证据的猜测、历史无关问题和“另一种写法更优雅”不阻塞。不要用“最小改动”要求遗漏批准范围。
|
|
31
32
|
|
|
32
33
|
将问题归因为:纯实现问题(可回 apply 修复)、方案/需求问题(计划不能支持正确实现)或混合问题(需要主流程处理分歧),并说明依据。不要用审查建议创造新的需求或架构。
|
|
33
34
|
|
|
@@ -8,7 +8,7 @@ metadata:
|
|
|
8
8
|
|
|
9
9
|
# SuperSpec Apply
|
|
10
10
|
|
|
11
|
-
按当前 change 的已批准 task 完成小范围实现和真实验证。Apply
|
|
11
|
+
按当前 change 的已批准 task 完成小范围实现和真实验证。Apply 只执行已批准计划,不把发现的新需求悄悄带进代码。实现目标是在满足验收的同时保持最小语义影响面,而不只是让最终功能可用。
|
|
12
12
|
|
|
13
13
|
## 工作方式
|
|
14
14
|
|
|
@@ -17,7 +17,7 @@ metadata:
|
|
|
17
17
|
每个 task 使用同一循环:
|
|
18
18
|
|
|
19
19
|
1. 开始 task 前先检查当前工作区变化,并沿 task 引用链核对发生变化的需求源或计划材料;确认要实现的行为、边界和相关测试仍与当前计划一致。新变化使已批准行为、验收、边界或方案失效时,不按旧计划继续,停止实现并交回 Propose。
|
|
20
|
-
2.
|
|
20
|
+
2. 在授权范围内实现最小改动;不要提前修改计划材料或扩大范围。优先复用现有边界或作局部适配,保持未被需求要求改变的公共接口、共享工具、默认行为和兼容语义不变。
|
|
21
21
|
3. 完成当前 task 要求的验证,如实报告测试、环境或覆盖不足的结果。
|
|
22
22
|
4. 当前 task 完成后立即继续工作流并处理下一事项;不要总结交付或等待用户再次要求继续。
|
|
23
23
|
|
|
@@ -25,6 +25,14 @@ metadata:
|
|
|
25
25
|
|
|
26
26
|
不要伪造完成结果、验证材料或审查结论。
|
|
27
27
|
|
|
28
|
+
### 最小语义影响面
|
|
29
|
+
|
|
30
|
+
最小改动不是追求最少文件或最少行数,而是只引入实现已批准行为所必需的语义变化。需求责任确实跨越多个层次时可以跨文件实现;局部需求不能成为重写共享逻辑或扩大公共行为的理由。
|
|
31
|
+
|
|
32
|
+
- 不为方便当前实现或测试而改变无关生产行为,也不把顺手重构、历史清理或技术偏好带入本次交付。
|
|
33
|
+
- 公共或共享实现的扩展必须由当前 task、现有结构和真实调用链证明必要;存在安全的局部实现时,保持公共边界不变。
|
|
34
|
+
- 完成标准包括整体差异可解释,并覆盖批准范围内的真实消费者。无法对应当前 task 或必要直接影响的变化不应保留,最小改动也不能以遗漏需求为代价。
|
|
35
|
+
|
|
28
36
|
### 保留得住的测试
|
|
29
37
|
|
|
30
38
|
- 测试描述调用方得到的能力,不把内部实现过程当成验收。
|
|
@@ -47,5 +55,6 @@ metadata:
|
|
|
47
55
|
## Guardrails
|
|
48
56
|
|
|
49
57
|
- 只改当前 task 授权范围内的实现和测试文件。
|
|
58
|
+
- 不改变当前 task 未要求变化的共享语义;必要的公共改动必须有调用链证据,并验证直接受影响的既有行为。
|
|
50
59
|
- 不修改计划材料、工作流记录、审查报告或验证材料。
|
|
51
60
|
- 不代替后续审查或验证流程作结论。
|
|
@@ -186,6 +186,8 @@ metadata:
|
|
|
186
186
|
|
|
187
187
|
Propose 以已确认的 Discovery 为需求边界。范围、业务行为、验收、数据语义或安全仍不明确时,回同一 change 的 Explore 澄清,不静默采用默认业务语义。
|
|
188
188
|
|
|
189
|
+
用户补充与已经核实的代码、运行行为、接口契约或外部系统事实冲突时,不通过改写事实材料来消除冲突。若用户明确要求改变真实行为,将相应实现、迁移和验证影响纳入计划;若是否改变真实行为仍不明确,保留冲突并交给用户确认。
|
|
190
|
+
|
|
189
191
|
当需求结果已经明确,但多个可行技术路线会让使用者承担不同的迁移、兼容、数据归属、发布、成本或长期维护边界时,不替使用者静默选择。在 `design.md` 的 `## 待用户确认` 中保留当前高影响设计决定,说明已知事实、候选结果、推荐及依据和会受影响的交付;内部命名、文件组织、局部实现和不改变这些结果的技术选择由模型自主决定。没有这类取舍时不制造问答。
|
|
190
192
|
|
|
191
193
|
每项使用 `DEC-xxx` 标识并只表达一个决定。工作流一次返回当前一项;用户答复后按工作流反馈处理,勾选时保留决定项原文,将最终选择与影响写入相邻正文及相关 design、specs、tasks 和测试契约,并按新方案重新判断后续事项。审查角色只能依据本次目标和直接证据指出缺失的决定,不能把个人偏好或更理想的架构升级为用户义务。
|