@peterxiaoyang/superspec 0.1.36 → 0.1.38
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +26 -4
- package/dist/code_review.d.ts +1 -0
- package/dist/code_review.js +2 -1
- package/dist/job_validity.d.ts +15 -0
- package/dist/job_validity.js +34 -0
- package/dist/next.js +8 -50
- package/dist/record.d.ts +2 -2
- package/dist/record.js +11 -24
- package/dist/review.js +2 -1
- package/dist/review_job_gates.d.ts +20 -0
- package/dist/review_job_gates.js +84 -0
- package/dist/sync.js +3 -5
- package/dist/task_evidence.d.ts +10 -0
- package/dist/task_evidence.js +46 -0
- package/dist/transition.js +184 -163
- package/dist/types.d.ts +4 -0
- package/dist/workflow_profile.d.ts +11 -0
- package/dist/workflow_profile.js +39 -0
- package/package.json +1 -1
- package/templates/workflow/AGENTS.md +1 -1
- package/templates/workflow/prompts/code-reviewer.md +1 -0
- package/templates/workflow/prompts/explore.md +4 -1
- package/templates/workflow/prompts/verifier.md +1 -0
- package/templates/workflow/skills/superspec-apply/SKILL.md +3 -3
- package/templates/workflow/skills/superspec-explore/SKILL.md +2 -1
- package/templates/workflow/skills/superspec-propose/SKILL.md +2 -0
package/dist/cli.js
CHANGED
|
@@ -66,6 +66,20 @@ function proposeReadyExitCode(result) {
|
|
|
66
66
|
return 0;
|
|
67
67
|
return result.events_written === 0 && result.message.includes("不能") ? 1 : 0;
|
|
68
68
|
}
|
|
69
|
+
function parseReviewRisk(value) {
|
|
70
|
+
if (value == null)
|
|
71
|
+
return "strict";
|
|
72
|
+
if (value === "minimal" || value === "normal" || value === "strict")
|
|
73
|
+
return value;
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
function readReviewRisk(opts) {
|
|
77
|
+
const risk = parseReviewRisk(opts.risk);
|
|
78
|
+
if (!risk) {
|
|
79
|
+
console.error("--risk 只能是 minimal、normal 或 strict");
|
|
80
|
+
}
|
|
81
|
+
return risk;
|
|
82
|
+
}
|
|
69
83
|
function parseVersion(version) {
|
|
70
84
|
const match = version.trim().match(/^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/);
|
|
71
85
|
if (!match)
|
|
@@ -631,7 +645,9 @@ jobs 子命令:
|
|
|
631
645
|
return 0;
|
|
632
646
|
case "explore":
|
|
633
647
|
{
|
|
634
|
-
const risk = opts
|
|
648
|
+
const risk = readReviewRisk(opts);
|
|
649
|
+
if (!risk)
|
|
650
|
+
return 1;
|
|
635
651
|
console.log(JSON.stringify(transitionExplore(projectRoot, change, cr, risk), null, 2));
|
|
636
652
|
}
|
|
637
653
|
return 0;
|
|
@@ -647,13 +663,17 @@ jobs 子命令:
|
|
|
647
663
|
return 0;
|
|
648
664
|
}
|
|
649
665
|
case "next": {
|
|
650
|
-
const risk = opts
|
|
666
|
+
const risk = readReviewRisk(opts);
|
|
667
|
+
if (!risk)
|
|
668
|
+
return 1;
|
|
651
669
|
const result = nextCmd(projectRoot, change, cr, risk);
|
|
652
670
|
console.log(JSON.stringify(result, null, 2));
|
|
653
671
|
return 0;
|
|
654
672
|
}
|
|
655
673
|
case "propose-ready": {
|
|
656
|
-
const risk = opts
|
|
674
|
+
const risk = readReviewRisk(opts);
|
|
675
|
+
if (!risk)
|
|
676
|
+
return 1;
|
|
657
677
|
const result = proposeReady(projectRoot, change, cr, risk);
|
|
658
678
|
console.log(JSON.stringify(result, null, 2));
|
|
659
679
|
return proposeReadyExitCode(result);
|
|
@@ -702,7 +722,9 @@ jobs 子命令:
|
|
|
702
722
|
return transitionExitCode(result);
|
|
703
723
|
}
|
|
704
724
|
case "review-ready": {
|
|
705
|
-
const risk = opts
|
|
725
|
+
const risk = readReviewRisk(opts);
|
|
726
|
+
if (!risk)
|
|
727
|
+
return 1;
|
|
706
728
|
const result = reviewReady(projectRoot, change, cr, risk);
|
|
707
729
|
console.log(JSON.stringify(result, null, 2));
|
|
708
730
|
return transitionExitCode(result);
|
package/dist/code_review.d.ts
CHANGED
|
@@ -48,6 +48,7 @@ export declare function codeReviewBoundFiles(projectRoot: string, paths: string[
|
|
|
48
48
|
export declare function codeReviewJobStaleReason(projectRoot: string, job: Job, currentPaths?: string[]): string | null;
|
|
49
49
|
export declare function codeReviewPacketDigest(input: {
|
|
50
50
|
role: "code-reviewer";
|
|
51
|
+
gate_id?: "review.code_review";
|
|
51
52
|
boundFiles: Ref[];
|
|
52
53
|
checkedDocs: string[];
|
|
53
54
|
created_from_transition: string;
|
package/dist/code_review.js
CHANGED
|
@@ -3,6 +3,7 @@ import { execFileSync } from "node:child_process";
|
|
|
3
3
|
import { existsSync, readdirSync, statSync } from "node:fs";
|
|
4
4
|
import { join, extname } from "node:path";
|
|
5
5
|
import { findLatestEvent, sha256File, sha256Text } from "./store.js";
|
|
6
|
+
import { REVIEW_CODE_REVIEW_GATE } from "./review_job_gates.js";
|
|
6
7
|
export const CODE_REVIEW_REPAIR_SCOPE_PREFIX = "code_reviewer_report_repair:";
|
|
7
8
|
export const CODE_REVIEW_DECISION_SCOPE_PREFIX = "code_review_decision:";
|
|
8
9
|
export const CODE_REVIEW_DECISION_ANSWER_LABELS = {
|
|
@@ -132,7 +133,7 @@ export function codeReviewPacketDigest(input) {
|
|
|
132
133
|
return sha256Text(JSON.stringify(input));
|
|
133
134
|
}
|
|
134
135
|
function isCodeReviewerJob(job) {
|
|
135
|
-
return job.role === "code-reviewer" && job
|
|
136
|
+
return job.role === "code-reviewer" && REVIEW_CODE_REVIEW_GATE.isJobForGate(job);
|
|
136
137
|
}
|
|
137
138
|
function codeReviewResultKind(value) {
|
|
138
139
|
return value === "invalid_report" || value === "non_actionable_report" || value === "review_failed"
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Event, Job } from "./types.ts";
|
|
2
|
+
export interface SubmittedReportValidityContext {
|
|
3
|
+
projectRoot: string;
|
|
4
|
+
changeRoot: string;
|
|
5
|
+
events: Event[];
|
|
6
|
+
reportPath?: string | null;
|
|
7
|
+
ignoredCodePaths?: string[];
|
|
8
|
+
}
|
|
9
|
+
export declare function invalidReasonForSnapshot(input: {
|
|
10
|
+
job: Job;
|
|
11
|
+
projectRoot: string;
|
|
12
|
+
changeRoot: string;
|
|
13
|
+
currentReviewEvidenceDigest: string;
|
|
14
|
+
}): string | null;
|
|
15
|
+
export declare function invalidReasonForSubmittedReport(job: Job, context: SubmittedReportValidityContext): string | null;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { reviewEvidenceDigest, reviewVerifierStaleReason, } from "./review.js";
|
|
2
|
+
import { codeReviewJobStaleReason, scanCodeChanges, } from "./code_review.js";
|
|
3
|
+
const MISSING_REVIEW_EVIDENCE_REASON = "最终验证工作项缺少执行证据版本";
|
|
4
|
+
function reviewReadyVerifierWithoutEvidenceReason(job) {
|
|
5
|
+
if (job.role === "verifier" &&
|
|
6
|
+
job.created_from_transition === "review-ready" &&
|
|
7
|
+
!job.gate_id &&
|
|
8
|
+
(typeof job.review_evidence_digest !== "string" || job.review_evidence_digest.length === 0)) {
|
|
9
|
+
return MISSING_REVIEW_EVIDENCE_REASON;
|
|
10
|
+
}
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
export function invalidReasonForSnapshot(input) {
|
|
14
|
+
if (input.job.role === "code-reviewer") {
|
|
15
|
+
return codeReviewJobStaleReason(input.projectRoot, input.job);
|
|
16
|
+
}
|
|
17
|
+
const invalidReviewReadyVerifier = reviewReadyVerifierWithoutEvidenceReason(input.job);
|
|
18
|
+
if (invalidReviewReadyVerifier)
|
|
19
|
+
return invalidReviewReadyVerifier;
|
|
20
|
+
return reviewVerifierStaleReason(input.job, input.changeRoot, input.currentReviewEvidenceDigest);
|
|
21
|
+
}
|
|
22
|
+
export function invalidReasonForSubmittedReport(job, context) {
|
|
23
|
+
if (job.role === "code-reviewer") {
|
|
24
|
+
const ignored = new Set(context.ignoredCodePaths ?? []);
|
|
25
|
+
if (context.reportPath)
|
|
26
|
+
ignored.add(context.reportPath);
|
|
27
|
+
const currentPaths = scanCodeChanges(context.projectRoot).paths.filter(path => !ignored.has(path));
|
|
28
|
+
return codeReviewJobStaleReason(context.projectRoot, job, currentPaths);
|
|
29
|
+
}
|
|
30
|
+
const invalidReviewReadyVerifier = reviewReadyVerifierWithoutEvidenceReason(job);
|
|
31
|
+
if (invalidReviewReadyVerifier)
|
|
32
|
+
return invalidReviewReadyVerifier;
|
|
33
|
+
return reviewVerifierStaleReason(job, context.changeRoot, reviewEvidenceDigest(context.events));
|
|
34
|
+
}
|
package/dist/next.js
CHANGED
|
@@ -2,18 +2,13 @@
|
|
|
2
2
|
import { rebuildSnapshot } from "./sync.js";
|
|
3
3
|
import { readFileSync } from "node:fs";
|
|
4
4
|
import { join } from "node:path";
|
|
5
|
-
import { readEvents
|
|
5
|
+
import { readEvents } from "./store.js";
|
|
6
6
|
import { isFreshReviewVerifier, isReviewReadyVerifier, readReviewPolicyFromEvents, reviewEvidenceDigest } from "./review.js";
|
|
7
7
|
import { CODE_REVIEW_DECISION_ANSWER_LABELS, CODE_REVIEW_REPAIR_SCOPE_PREFIX, codeReviewDecisionScope, collectCodeReviewGateFacts, latestCodeReviewFailedStatus, requiresFinalVerifierForCurrentReview, } from "./code_review.js";
|
|
8
8
|
import { requiredJobActions } from "./job_action.js";
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
return job.created_from_transition === "propose-ready" && ACTIVE_PROPOSAL_REVIEW_ROLES.has(job.role);
|
|
13
|
-
}
|
|
14
|
-
function isExploreReviewJob(job) {
|
|
15
|
-
return job.created_from_transition === "explore" && job.role === "critic";
|
|
16
|
-
}
|
|
9
|
+
import { EXPLORE_DISCOVERY_REVIEW_GATE, PROPOSE_FINAL_REVIEW_GATE } from "./review_job_gates.js";
|
|
10
|
+
import { taskEvidenceReadiness } from "./task_evidence.js";
|
|
11
|
+
import { validateDiscovery, countDiscoveryOpenQuestions, collectProposeOpenQuestions, pendingTasksInContent } from "./format.js";
|
|
17
12
|
function requiredJobsOutput(state, change, jobs, reason) {
|
|
18
13
|
return {
|
|
19
14
|
state,
|
|
@@ -35,43 +30,6 @@ function pendingTaskIds(changeRoot) {
|
|
|
35
30
|
function reopenCommand(change, pending) {
|
|
36
31
|
return transitionCommand(change, "reopen", `--to apply --reason "pending tasks: ${pending.join(", ")}"`);
|
|
37
32
|
}
|
|
38
|
-
function taskCompletionReadiness(projectRoot, change, changeRoot, attempt) {
|
|
39
|
-
const tasksContent = readFileSync(join(changeRoot, "tasks.md"), "utf8");
|
|
40
|
-
const tasks = parseTasksMd(tasksContent);
|
|
41
|
-
const taskInfo = tasks.find(task => task.taskId === attempt.task_id);
|
|
42
|
-
const missing = [];
|
|
43
|
-
if (!taskInfo)
|
|
44
|
-
return { ready: false, missing: [`任务 ${attempt.task_id} 不存在`] };
|
|
45
|
-
const currentDigest = sha256Text(tasksContent.replace(/- \[[xX]\]/g, "- [ ]"));
|
|
46
|
-
// Keep the wording aligned with task-complete; no command should be suggested if it would fail this guard.
|
|
47
|
-
if (currentDigest !== attempt.task_structure_digest)
|
|
48
|
-
missing.push("任务结构指纹");
|
|
49
|
-
if (!taskInfo.tddRequired) {
|
|
50
|
-
if (!taskInfo.noTddReason)
|
|
51
|
-
missing.push("no_tdd_reason");
|
|
52
|
-
return { ready: missing.length === 0, missing };
|
|
53
|
-
}
|
|
54
|
-
let hasRed = false;
|
|
55
|
-
let hasGreen = false;
|
|
56
|
-
for (const ev of readEvents(projectRoot, change)) {
|
|
57
|
-
if (ev.event_type !== "test_run_recorded")
|
|
58
|
-
continue;
|
|
59
|
-
const tr = ev.payload;
|
|
60
|
-
const matches = tr.attempt_id === attempt.attempt_id ||
|
|
61
|
-
(!tr.attempt_id && tr.task_structure_digest === attempt.task_structure_digest);
|
|
62
|
-
if (!matches)
|
|
63
|
-
continue;
|
|
64
|
-
if (tr.semantic_status === "expected_failure" || tr.semantic_status === "characterization_pass")
|
|
65
|
-
hasRed = true;
|
|
66
|
-
if (tr.semantic_status === "expected_success")
|
|
67
|
-
hasGreen = true;
|
|
68
|
-
}
|
|
69
|
-
if (!hasRed)
|
|
70
|
-
missing.push("RED 证据");
|
|
71
|
-
if (!hasGreen)
|
|
72
|
-
missing.push("GREEN 证据");
|
|
73
|
-
return { ready: missing.length === 0, missing };
|
|
74
|
-
}
|
|
75
33
|
/** next 命令:读 snapshot,返回唯一可执行路径 */
|
|
76
34
|
export function next(projectRoot, change, changeRoot, defaultRisk = "strict") {
|
|
77
35
|
const snapshot = rebuildSnapshot(projectRoot, change, changeRoot);
|
|
@@ -88,7 +46,7 @@ export function next(projectRoot, change, changeRoot, defaultRisk = "strict") {
|
|
|
88
46
|
missing_inputs: [],
|
|
89
47
|
};
|
|
90
48
|
case "explore": {
|
|
91
|
-
const exploreReviewJobs =
|
|
49
|
+
const exploreReviewJobs = EXPLORE_DISCOVERY_REVIEW_GATE.openJobsForGate(snapshot);
|
|
92
50
|
if (exploreReviewJobs.length > 0) {
|
|
93
51
|
return requiredJobsOutput("explore", change, exploreReviewJobs, `有 ${exploreReviewJobs.length} 个待完成探索审查工作项`);
|
|
94
52
|
}
|
|
@@ -132,7 +90,7 @@ export function next(projectRoot, change, changeRoot, defaultRisk = "strict") {
|
|
|
132
90
|
};
|
|
133
91
|
return { state: "propose", path: "ask_user", ask_user: ask, reason: `有 ${openQuestions.openCount} 个 propose 未确认问题` };
|
|
134
92
|
}
|
|
135
|
-
const proposalReviewJobs =
|
|
93
|
+
const proposalReviewJobs = PROPOSE_FINAL_REVIEW_GATE.openJobsForGate(snapshot);
|
|
136
94
|
if (proposalReviewJobs.length > 0) {
|
|
137
95
|
return requiredJobsOutput("propose", change, proposalReviewJobs, `有 ${proposalReviewJobs.length} 个待完成 proposal 审查工作项`);
|
|
138
96
|
}
|
|
@@ -145,7 +103,7 @@ export function next(projectRoot, change, changeRoot, defaultRisk = "strict") {
|
|
|
145
103
|
};
|
|
146
104
|
}
|
|
147
105
|
case "propose_ready": {
|
|
148
|
-
const proposalReviewJobs =
|
|
106
|
+
const proposalReviewJobs = PROPOSE_FINAL_REVIEW_GATE.openJobsForGate(snapshot);
|
|
149
107
|
if (proposalReviewJobs.length > 0) {
|
|
150
108
|
return requiredJobsOutput("propose_ready", change, proposalReviewJobs, `有 ${proposalReviewJobs.length} 个待完成 proposal 审查工作项`);
|
|
151
109
|
}
|
|
@@ -166,7 +124,7 @@ export function next(projectRoot, change, changeRoot, defaultRisk = "strict") {
|
|
|
166
124
|
if (pending.length > 0) {
|
|
167
125
|
const activePending = snapshot.active_task_attempts.find(attempt => attempt.state === "active" && pending.includes(attempt.task_id));
|
|
168
126
|
if (activePending) {
|
|
169
|
-
const readiness =
|
|
127
|
+
const readiness = taskEvidenceReadiness(projectRoot, change, changeRoot, activePending);
|
|
170
128
|
if (readiness.ready) {
|
|
171
129
|
return {
|
|
172
130
|
state: "apply",
|
package/dist/record.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { RecordResult, Job } from "./types.ts";
|
|
1
|
+
import type { RecordResult, Job, JobPacket } from "./types.ts";
|
|
2
2
|
/** record job-submit:登记工作项结果 */
|
|
3
3
|
export declare function recordJobSubmit(projectRoot: string, change: string, changeRoot: string, jobId: string, reportFile: string): RecordResult;
|
|
4
4
|
/** record job-submit:从 JSON 内容登记工作项结果 */
|
|
@@ -19,6 +19,6 @@ export declare function jobsList(projectRoot: string, change: string): {
|
|
|
19
19
|
/** jobs packet:返回工作项执行说明 */
|
|
20
20
|
export declare function jobsPacket(projectRoot: string, change: string, jobId: string): {
|
|
21
21
|
found: boolean;
|
|
22
|
-
packet?:
|
|
22
|
+
packet?: JobPacket;
|
|
23
23
|
message: string;
|
|
24
24
|
};
|
package/dist/record.js
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
import { readFileSync, existsSync } from "node:fs";
|
|
3
3
|
import { isAbsolute, join, relative, resolve } from "node:path";
|
|
4
4
|
import { ensureChangeLayout, readEvents, appendEvent, makeEvent, sha256File, sha256Text, withLock, appendRawRecord, } from "./store.js";
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
5
|
+
import { CODE_REVIEW_DECISION_ANSWER_LABELS, CODE_REVIEW_DECISION_SCOPE_PREFIX, codeReviewDecisionAnswerLabel, normalizeCodeReviewDecisionAnswer, } from "./code_review.js";
|
|
6
|
+
import { invalidReasonForSubmittedReport } from "./job_validity.js";
|
|
7
7
|
import { jobSubmitArgv } from "./job_action.js";
|
|
8
8
|
const REVIEW_REPORT_REQUIRED_FIELDS = ["role", "verdict", "findings"];
|
|
9
9
|
const REVIEW_REPORT_OPTIONAL_FIELDS = ["summary", "evidence_refs", "risks", "open_questions"];
|
|
@@ -220,22 +220,6 @@ function projectRelativePath(projectRoot, path) {
|
|
|
220
220
|
return null;
|
|
221
221
|
return rel;
|
|
222
222
|
}
|
|
223
|
-
function staleBoundFilesChecks(job, projectRoot, changeRoot, ignoredCodePaths = []) {
|
|
224
|
-
const checks = [];
|
|
225
|
-
if (job.role === "code-reviewer") {
|
|
226
|
-
const ignored = new Set(ignoredCodePaths);
|
|
227
|
-
const currentPaths = scanCodeChanges(projectRoot).paths.filter(path => !ignored.has(path));
|
|
228
|
-
const reason = codeReviewJobStaleReason(projectRoot, job, currentPaths);
|
|
229
|
-
return reason ? [reason] : [];
|
|
230
|
-
}
|
|
231
|
-
for (const bf of job.boundFiles) {
|
|
232
|
-
const currentSha = sha256File(join(changeRoot, bf.path)) ?? "sha256:missing";
|
|
233
|
-
if (currentSha !== bf.sha) {
|
|
234
|
-
checks.push(`绑定文件 ${bf.path} 已变化(${bf.sha} → ${currentSha})`);
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
return checks;
|
|
238
|
-
}
|
|
239
223
|
/** 从 events 中查找 job(H4 修复:job 只在 transition_commit 的 new_jobs payload 里) */
|
|
240
224
|
function findJob(events, jobId) {
|
|
241
225
|
for (const ev of events) {
|
|
@@ -312,12 +296,14 @@ function recordJobSubmitLoaded(projectRoot, change, changeRoot, jobId, job, even
|
|
|
312
296
|
checks.push("报告必须是有效 JSON");
|
|
313
297
|
}
|
|
314
298
|
const reportPath = reportFile ? projectRelativePath(projectRoot, reportFile) : null;
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
299
|
+
const invalidReason = invalidReasonForSubmittedReport(job, {
|
|
300
|
+
projectRoot,
|
|
301
|
+
changeRoot,
|
|
302
|
+
events,
|
|
303
|
+
reportPath,
|
|
304
|
+
});
|
|
305
|
+
if (invalidReason) {
|
|
306
|
+
checks.push(invalidReason);
|
|
321
307
|
}
|
|
322
308
|
if (!reportContent.trim()) {
|
|
323
309
|
checks.push("报告内容为空");
|
|
@@ -633,6 +619,7 @@ export function jobsPacket(projectRoot, change, jobId) {
|
|
|
633
619
|
packet: {
|
|
634
620
|
job_id: job.job_id,
|
|
635
621
|
role: job.role,
|
|
622
|
+
...(job.gate_id ? { gate_id: job.gate_id } : {}),
|
|
636
623
|
recommended_agent: recommendedAgentForRole(job.role),
|
|
637
624
|
boundFiles: job.boundFiles,
|
|
638
625
|
...(job.review_evidence_digest ? { review_evidence_digest: job.review_evidence_digest } : {}),
|
package/dist/review.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { existsSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { sha256File, sha256Text } from "./store.js";
|
|
5
|
+
import { REVIEW_FINAL_VERIFIER_GATE } from "./review_job_gates.js";
|
|
5
6
|
export const REVIEW_DOC_PATHS = [
|
|
6
7
|
"proposal.md",
|
|
7
8
|
"tasks.md",
|
|
@@ -52,7 +53,7 @@ export function readReviewPolicyFromEvents(events) {
|
|
|
52
53
|
return null;
|
|
53
54
|
}
|
|
54
55
|
export function isReviewReadyVerifier(job) {
|
|
55
|
-
return job.role === "verifier" && job
|
|
56
|
+
return job.role === "verifier" && REVIEW_FINAL_VERIFIER_GATE.isJobForGate(job);
|
|
56
57
|
}
|
|
57
58
|
export function reviewBoundFiles(changeRoot) {
|
|
58
59
|
return REVIEW_DOC_PATHS
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { ReviewRisk } from "./review.ts";
|
|
2
|
+
import type { Job, JobRole, ReviewJobGateId, Snapshot } from "./types.ts";
|
|
3
|
+
export interface ReviewGateRule {
|
|
4
|
+
gate_id: ReviewJobGateId;
|
|
5
|
+
created_from_transition: "explore" | "propose-ready" | "review-ready";
|
|
6
|
+
allowedRoles: JobRole[];
|
|
7
|
+
reviewedDocPaths: string[];
|
|
8
|
+
requiredRolesForRisk(risk: ReviewRisk): JobRole[];
|
|
9
|
+
matchesOldJob(job: Job): boolean;
|
|
10
|
+
isJobForGate(job: Job): boolean;
|
|
11
|
+
openJobsForGate(snapshot: Snapshot): Job[];
|
|
12
|
+
}
|
|
13
|
+
export declare const EXPLORE_DISCOVERY_REVIEW_GATE_ID: "explore.discovery_review";
|
|
14
|
+
export declare const PROPOSE_FINAL_REVIEW_GATE_ID: "propose.final_review";
|
|
15
|
+
export declare const REVIEW_CODE_REVIEW_GATE_ID: "review.code_review";
|
|
16
|
+
export declare const REVIEW_FINAL_VERIFIER_GATE_ID: "review.final_verifier";
|
|
17
|
+
export declare const EXPLORE_DISCOVERY_REVIEW_GATE: ReviewGateRule;
|
|
18
|
+
export declare const PROPOSE_FINAL_REVIEW_GATE: ReviewGateRule;
|
|
19
|
+
export declare const REVIEW_CODE_REVIEW_GATE: ReviewGateRule;
|
|
20
|
+
export declare const REVIEW_FINAL_VERIFIER_GATE: ReviewGateRule;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { reviewRolesForGate } from "./workflow_profile.js";
|
|
2
|
+
function makeReviewGateRule(input) {
|
|
3
|
+
const gate = {
|
|
4
|
+
...input,
|
|
5
|
+
isJobForGate(job) {
|
|
6
|
+
if (!input.allowedRoles.includes(job.role))
|
|
7
|
+
return false;
|
|
8
|
+
if (job.gate_id)
|
|
9
|
+
return job.gate_id === input.gate_id;
|
|
10
|
+
return input.matchesOldJob(job);
|
|
11
|
+
},
|
|
12
|
+
openJobsForGate(snapshot) {
|
|
13
|
+
return snapshot.open_jobs.filter(job => gate.isJobForGate(job));
|
|
14
|
+
},
|
|
15
|
+
};
|
|
16
|
+
return gate;
|
|
17
|
+
}
|
|
18
|
+
export const EXPLORE_DISCOVERY_REVIEW_GATE_ID = "explore.discovery_review";
|
|
19
|
+
export const PROPOSE_FINAL_REVIEW_GATE_ID = "propose.final_review";
|
|
20
|
+
export const REVIEW_CODE_REVIEW_GATE_ID = "review.code_review";
|
|
21
|
+
export const REVIEW_FINAL_VERIFIER_GATE_ID = "review.final_verifier";
|
|
22
|
+
const EXPLORE_DISCOVERY_REVIEW_ROLES = ["critic"];
|
|
23
|
+
const PROPOSAL_REVIEW_ROLES = ["critic", "architect", "test-engineer"];
|
|
24
|
+
const PROPOSAL_REVIEW_ROLE_SET = new Set(PROPOSAL_REVIEW_ROLES);
|
|
25
|
+
const REVIEW_CODE_REVIEW_ROLES = ["code-reviewer"];
|
|
26
|
+
const REVIEW_FINAL_VERIFIER_ROLES = ["verifier"];
|
|
27
|
+
export const EXPLORE_DISCOVERY_REVIEW_GATE = makeReviewGateRule({
|
|
28
|
+
gate_id: EXPLORE_DISCOVERY_REVIEW_GATE_ID,
|
|
29
|
+
created_from_transition: "explore",
|
|
30
|
+
allowedRoles: EXPLORE_DISCOVERY_REVIEW_ROLES,
|
|
31
|
+
reviewedDocPaths: [".superspec/artifacts/discovery.md"],
|
|
32
|
+
requiredRolesForRisk(risk) {
|
|
33
|
+
return reviewRolesForGate(EXPLORE_DISCOVERY_REVIEW_GATE_ID, risk);
|
|
34
|
+
},
|
|
35
|
+
matchesOldJob(job) {
|
|
36
|
+
return job.created_from_transition === "explore" && job.role === "critic";
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
export const PROPOSE_FINAL_REVIEW_GATE = makeReviewGateRule({
|
|
40
|
+
gate_id: PROPOSE_FINAL_REVIEW_GATE_ID,
|
|
41
|
+
created_from_transition: "propose-ready",
|
|
42
|
+
allowedRoles: PROPOSAL_REVIEW_ROLES,
|
|
43
|
+
reviewedDocPaths: [
|
|
44
|
+
"proposal.md",
|
|
45
|
+
"tasks.md",
|
|
46
|
+
"design.md",
|
|
47
|
+
".superspec/artifacts/discovery.md",
|
|
48
|
+
".superspec/artifacts/business-invariants.md",
|
|
49
|
+
".superspec/artifacts/test-contract.md",
|
|
50
|
+
],
|
|
51
|
+
requiredRolesForRisk(risk) {
|
|
52
|
+
return reviewRolesForGate(PROPOSE_FINAL_REVIEW_GATE_ID, risk);
|
|
53
|
+
},
|
|
54
|
+
matchesOldJob(job) {
|
|
55
|
+
return job.created_from_transition === "propose-ready" && PROPOSAL_REVIEW_ROLE_SET.has(job.role);
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
export const REVIEW_CODE_REVIEW_GATE = makeReviewGateRule({
|
|
59
|
+
gate_id: REVIEW_CODE_REVIEW_GATE_ID,
|
|
60
|
+
created_from_transition: "review-ready",
|
|
61
|
+
allowedRoles: REVIEW_CODE_REVIEW_ROLES,
|
|
62
|
+
reviewedDocPaths: [],
|
|
63
|
+
requiredRolesForRisk(risk) {
|
|
64
|
+
return reviewRolesForGate(REVIEW_CODE_REVIEW_GATE_ID, risk);
|
|
65
|
+
},
|
|
66
|
+
matchesOldJob(job) {
|
|
67
|
+
return job.created_from_transition === "review-ready" && job.role === "code-reviewer";
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
export const REVIEW_FINAL_VERIFIER_GATE = makeReviewGateRule({
|
|
71
|
+
gate_id: REVIEW_FINAL_VERIFIER_GATE_ID,
|
|
72
|
+
created_from_transition: "review-ready",
|
|
73
|
+
allowedRoles: REVIEW_FINAL_VERIFIER_ROLES,
|
|
74
|
+
reviewedDocPaths: [],
|
|
75
|
+
requiredRolesForRisk(risk) {
|
|
76
|
+
return reviewRolesForGate(REVIEW_FINAL_VERIFIER_GATE_ID, risk);
|
|
77
|
+
},
|
|
78
|
+
matchesOldJob(job) {
|
|
79
|
+
return job.created_from_transition === "review-ready" &&
|
|
80
|
+
job.role === "verifier" &&
|
|
81
|
+
typeof job.review_evidence_digest === "string" &&
|
|
82
|
+
job.review_evidence_digest.length > 0;
|
|
83
|
+
},
|
|
84
|
+
});
|
package/dist/sync.js
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
import { readFileSync, existsSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { readEvents, eventsDigest, computeDocumentDigests, sha256Text, ensureChangeLayout, } from "./store.js";
|
|
5
|
-
import { reviewEvidenceDigest
|
|
6
|
-
import {
|
|
5
|
+
import { reviewEvidenceDigest } from "./review.js";
|
|
6
|
+
import { invalidReasonForSnapshot } from "./job_validity.js";
|
|
7
7
|
const TRACKED_DOCS = [
|
|
8
8
|
"proposal.md", "design.md", "tasks.md",
|
|
9
9
|
".superspec/artifacts/discovery.md",
|
|
@@ -84,9 +84,7 @@ function replayEvents(events) {
|
|
|
84
84
|
function checkStaleJobs(jobs, projectRoot, changeRoot, currentReviewEvidenceDigest) {
|
|
85
85
|
const stale = [];
|
|
86
86
|
for (const job of jobs) {
|
|
87
|
-
const reason = job
|
|
88
|
-
? codeReviewJobStaleReason(projectRoot, job)
|
|
89
|
-
: reviewVerifierStaleReason(job, changeRoot, currentReviewEvidenceDigest);
|
|
87
|
+
const reason = invalidReasonForSnapshot({ job, projectRoot, changeRoot, currentReviewEvidenceDigest });
|
|
90
88
|
if (reason) {
|
|
91
89
|
stale.push({ job_id: job.job_id, reason });
|
|
92
90
|
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { TaskAttempt } from "./types.ts";
|
|
2
|
+
export type TaskEvidenceReadiness = {
|
|
3
|
+
ready: true;
|
|
4
|
+
missing: [];
|
|
5
|
+
} | {
|
|
6
|
+
ready: false;
|
|
7
|
+
missing: string[];
|
|
8
|
+
reason: string;
|
|
9
|
+
};
|
|
10
|
+
export declare function taskEvidenceReadiness(projectRoot: string, change: string, changeRoot: string, attempt: TaskAttempt): TaskEvidenceReadiness;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { parseTasksMd } from "./format.js";
|
|
4
|
+
import { readEvents, sha256Text } from "./store.js";
|
|
5
|
+
export function taskEvidenceReadiness(projectRoot, change, changeRoot, attempt) {
|
|
6
|
+
const tasksContent = readFileSync(join(changeRoot, "tasks.md"), "utf8");
|
|
7
|
+
const tasks = parseTasksMd(tasksContent);
|
|
8
|
+
const taskInfo = tasks.find(task => task.taskId === attempt.task_id);
|
|
9
|
+
const missing = [];
|
|
10
|
+
if (!taskInfo) {
|
|
11
|
+
missing.push(`任务 ${attempt.task_id} 不存在`);
|
|
12
|
+
return { ready: false, missing, reason: missing.join("、") };
|
|
13
|
+
}
|
|
14
|
+
const currentDigest = sha256Text(tasksContent.replace(/- \[[xX]\]/g, "- [ ]"));
|
|
15
|
+
if (currentDigest !== attempt.task_structure_digest)
|
|
16
|
+
missing.push("任务结构指纹");
|
|
17
|
+
if (!taskInfo.tddRequired) {
|
|
18
|
+
if (!taskInfo.noTddReason)
|
|
19
|
+
missing.push("no_tdd_reason");
|
|
20
|
+
return missing.length === 0
|
|
21
|
+
? { ready: true, missing: [] }
|
|
22
|
+
: { ready: false, missing, reason: missing.join("、") };
|
|
23
|
+
}
|
|
24
|
+
let hasRed = false;
|
|
25
|
+
let hasGreen = false;
|
|
26
|
+
for (const ev of readEvents(projectRoot, change)) {
|
|
27
|
+
if (ev.event_type !== "test_run_recorded")
|
|
28
|
+
continue;
|
|
29
|
+
const tr = ev.payload;
|
|
30
|
+
const matches = tr.attempt_id === attempt.attempt_id ||
|
|
31
|
+
(!tr.attempt_id && tr.task_structure_digest === attempt.task_structure_digest);
|
|
32
|
+
if (!matches)
|
|
33
|
+
continue;
|
|
34
|
+
if (tr.semantic_status === "expected_failure" || tr.semantic_status === "characterization_pass")
|
|
35
|
+
hasRed = true;
|
|
36
|
+
if (tr.semantic_status === "expected_success")
|
|
37
|
+
hasGreen = true;
|
|
38
|
+
}
|
|
39
|
+
if (!hasRed)
|
|
40
|
+
missing.push("RED 证据");
|
|
41
|
+
if (!hasGreen)
|
|
42
|
+
missing.push("GREEN 证据");
|
|
43
|
+
return missing.length === 0
|
|
44
|
+
? { ready: true, missing: [] }
|
|
45
|
+
: { ready: false, missing, reason: missing.join("、") };
|
|
46
|
+
}
|
package/dist/transition.js
CHANGED
|
@@ -5,36 +5,26 @@ 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, readReviewPolicyFromEvents, reviewBoundFiles, reviewEvidenceDigest, reviewPolicyForRisk, REVIEW_DOC_PATHS, } from "./review.js";
|
|
8
|
+
import { EXPLORE_DISCOVERY_REVIEW_GATE, PROPOSE_FINAL_REVIEW_GATE, REVIEW_CODE_REVIEW_GATE_ID, REVIEW_FINAL_VERIFIER_GATE_ID, } from "./review_job_gates.js";
|
|
8
9
|
import { codeReviewBoundFiles, codeReviewDecisionScope, codeReviewJobStaleReason, codeReviewPacketDigest, collectCodeReviewGateFacts, dismissedCodeReviewSummary, latestCodeReviewDecision, latestCodeReviewFailedStatus, requiresFinalVerifierForCurrentReview, scanCodeChanges, } from "./code_review.js";
|
|
9
|
-
import {
|
|
10
|
+
import { taskEvidenceReadiness } from "./task_evidence.js";
|
|
11
|
+
import { validateDiscovery, collectProposeOpenQuestions, findTaskInLines, pendingTasksInContent } from "./format.js";
|
|
10
12
|
let transitionSeq = 0;
|
|
11
13
|
function newTransitionId() { return `T-${Date.now()}-${++transitionSeq}`; }
|
|
12
14
|
let jobSeq = 0;
|
|
13
15
|
function newJobId(change, role) { return `JOB-${change.slice(0, 8)}-${role.slice(0, 4)}-${Date.now()}-${++jobSeq}`; }
|
|
14
|
-
const TRANSITION_REQUIREMENTS = {
|
|
15
|
-
"propose-ready": {
|
|
16
|
-
minimal: [],
|
|
17
|
-
normal: ["critic"],
|
|
18
|
-
strict: ["critic", "architect", "test-engineer"],
|
|
19
|
-
},
|
|
20
|
-
"explore": {
|
|
21
|
-
minimal: [],
|
|
22
|
-
normal: [],
|
|
23
|
-
strict: ["critic"],
|
|
24
|
-
},
|
|
25
|
-
};
|
|
26
16
|
/**
|
|
27
17
|
* 通用 job 检查/创建逻辑——任何 transition 都能调用。
|
|
28
18
|
* 检查 requiredRoles 是否有 fresh accepted job;缺则创建新 job。
|
|
29
19
|
* 返回 null = 全部满足;返回 Decision = 需要创建 job(状态不变)。
|
|
30
20
|
*/
|
|
31
|
-
function checkOrCreateReviewJobs(snapshot, requiredRoles, changeRoot, change
|
|
21
|
+
function checkOrCreateReviewJobs(snapshot, gate, requiredRoles, changeRoot, change) {
|
|
32
22
|
const staleRoles = [];
|
|
33
23
|
for (const role of requiredRoles) {
|
|
34
|
-
const openForRole = snapshot.open_jobs.find(j => j
|
|
24
|
+
const openForRole = snapshot.open_jobs.find(j => gate.isJobForGate(j) && j.role === role);
|
|
35
25
|
if (openForRole)
|
|
36
26
|
return { blocked: true, reason: `状态未推进;已有待完成工作项 ${role}(${openForRole.job_id})`, jobs: [openForRole] };
|
|
37
|
-
const fresh = snapshot.accepted_jobs.find(j => j
|
|
27
|
+
const fresh = snapshot.accepted_jobs.find(j => gate.isJobForGate(j) && j.role === role);
|
|
38
28
|
if (!fresh) {
|
|
39
29
|
staleRoles.push({ role, reason: `需求 ${role} 无已接受的工作项` });
|
|
40
30
|
}
|
|
@@ -50,8 +40,22 @@ function checkOrCreateReviewJobs(snapshot, requiredRoles, changeRoot, change, tr
|
|
|
50
40
|
}
|
|
51
41
|
if (staleRoles.length > 0) {
|
|
52
42
|
const newJobs = staleRoles.map(({ role }) => {
|
|
53
|
-
const boundFiles =
|
|
54
|
-
return {
|
|
43
|
+
const boundFiles = gate.reviewedDocPaths.filter(p => existsSync(join(changeRoot, p))).map(p => ({ path: p, sha: sha256File(join(changeRoot, p)) ?? "sha256:missing" }));
|
|
44
|
+
return {
|
|
45
|
+
job_id: newJobId(change, role),
|
|
46
|
+
role,
|
|
47
|
+
state: "requested",
|
|
48
|
+
gate_id: gate.gate_id,
|
|
49
|
+
boundFiles,
|
|
50
|
+
packet_digest: sha256Text(JSON.stringify({
|
|
51
|
+
role,
|
|
52
|
+
gate_id: gate.gate_id,
|
|
53
|
+
boundFiles,
|
|
54
|
+
created_from_transition: gate.created_from_transition,
|
|
55
|
+
})),
|
|
56
|
+
created_from_transition: gate.created_from_transition,
|
|
57
|
+
created_at: new Date().toISOString(),
|
|
58
|
+
};
|
|
55
59
|
});
|
|
56
60
|
return {
|
|
57
61
|
fromState: snapshot.state, toState: snapshot.state, outcome: "job_created",
|
|
@@ -67,7 +71,7 @@ function historicalProposeReadyRoles(projectRoot, change) {
|
|
|
67
71
|
continue;
|
|
68
72
|
const newJobs = ev.payload.new_jobs ?? [];
|
|
69
73
|
for (const job of newJobs) {
|
|
70
|
-
if (job
|
|
74
|
+
if (PROPOSE_FINAL_REVIEW_GATE.isJobForGate(job) &&
|
|
71
75
|
(job.role === "critic" || job.role === "architect" || job.role === "test-engineer"))
|
|
72
76
|
roles.add(job.role);
|
|
73
77
|
}
|
|
@@ -119,6 +123,7 @@ function createCodeReviewerJob(change, projectRoot, events) {
|
|
|
119
123
|
: undefined;
|
|
120
124
|
const packetInput = {
|
|
121
125
|
role: "code-reviewer",
|
|
126
|
+
gate_id: REVIEW_CODE_REVIEW_GATE_ID,
|
|
122
127
|
boundFiles,
|
|
123
128
|
checkedDocs: REVIEW_DOC_PATHS,
|
|
124
129
|
created_from_transition: "review-ready",
|
|
@@ -130,6 +135,7 @@ function createCodeReviewerJob(change, projectRoot, events) {
|
|
|
130
135
|
job_id: newJobId(change, "code-reviewer"),
|
|
131
136
|
role: "code-reviewer",
|
|
132
137
|
state: "requested",
|
|
138
|
+
gate_id: REVIEW_CODE_REVIEW_GATE_ID,
|
|
133
139
|
boundFiles,
|
|
134
140
|
packet_digest: codeReviewPacketDigest(packetInput),
|
|
135
141
|
created_from_transition: "review-ready",
|
|
@@ -176,6 +182,141 @@ function appendReviewFixTask(changeRoot, ref, finding) {
|
|
|
176
182
|
function isFreshOpenCodeReviewerJob(job, projectRoot) {
|
|
177
183
|
return codeReviewJobStaleReason(projectRoot, job) == null;
|
|
178
184
|
}
|
|
185
|
+
function evaluateApplyDoneCodeReviewGate(input) {
|
|
186
|
+
const scan = scanCodeChanges(input.projectRoot);
|
|
187
|
+
const facts = collectCodeReviewGateFacts(input.events);
|
|
188
|
+
if (scan.hasCodeChanges) {
|
|
189
|
+
const freshOpenJobs = facts.openJobs.filter(job => isFreshOpenCodeReviewerJob(job, input.projectRoot));
|
|
190
|
+
if (freshOpenJobs.length > 0) {
|
|
191
|
+
return {
|
|
192
|
+
blocked: true,
|
|
193
|
+
reason: `状态未推进;已有待完成代码审查工作项 ${freshOpenJobs[0].job_id}`,
|
|
194
|
+
jobs: [freshOpenJobs[0]],
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
const latest = facts.latestTerminal;
|
|
198
|
+
if (latest?.state === "accepted") {
|
|
199
|
+
return {
|
|
200
|
+
fromState: "apply_done",
|
|
201
|
+
toState: "review",
|
|
202
|
+
outcome: "advanced",
|
|
203
|
+
reason: "代码审查已通过,进入最终审查阶段",
|
|
204
|
+
commitPayload: {
|
|
205
|
+
...input.policyPayload,
|
|
206
|
+
code_review_gate: { decision: "passed", job_id: latest.job.job_id },
|
|
207
|
+
},
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
if (latest?.state === "rejected" && latest.result_kind === "review_failed") {
|
|
211
|
+
const reviewFailedStatus = latestCodeReviewFailedStatus(input.events);
|
|
212
|
+
if (reviewFailedStatus && reviewFailedStatus.findings.length > 0 && reviewFailedStatus.unresolved.length === 0) {
|
|
213
|
+
const { job, scanReason } = createCodeReviewerJob(input.change, input.projectRoot, input.events);
|
|
214
|
+
return {
|
|
215
|
+
fromState: "apply_done",
|
|
216
|
+
toState: "apply_done",
|
|
217
|
+
outcome: "job_created",
|
|
218
|
+
newJobs: [job],
|
|
219
|
+
reason: `重新创建代码审查工作项;${scanReason};上一次阻塞问题已被主流程复核驳回`,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
return {
|
|
223
|
+
skip: true,
|
|
224
|
+
message: "代码审查发现需要处理的问题,请先执行 next,根据提示回到实现阶段修复或让使用者决定是否回到计划阶段",
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
const { job, scanReason } = createCodeReviewerJob(input.change, input.projectRoot, input.events);
|
|
228
|
+
return {
|
|
229
|
+
fromState: "apply_done",
|
|
230
|
+
toState: "apply_done",
|
|
231
|
+
outcome: "job_created",
|
|
232
|
+
newJobs: [job],
|
|
233
|
+
reason: latest?.state === "rejected"
|
|
234
|
+
? `重新创建代码审查工作项;上一次报告未被接受,原因:${latest.reason ?? "报告不符合要求"}`
|
|
235
|
+
: `创建代码审查工作项;${scanReason}`,
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
return {
|
|
239
|
+
fromState: "apply_done",
|
|
240
|
+
toState: "review",
|
|
241
|
+
outcome: "advanced",
|
|
242
|
+
reason: "没有代码类改动,直接进入最终审查阶段",
|
|
243
|
+
commitPayload: {
|
|
244
|
+
...input.policyPayload,
|
|
245
|
+
code_review_gate: { decision: "skipped", reason: "no_code_changes" },
|
|
246
|
+
},
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
function createFinalVerifierJob(change, changeRoot, currentEvidenceDigest) {
|
|
250
|
+
const boundFiles = reviewBoundFiles(changeRoot);
|
|
251
|
+
return {
|
|
252
|
+
job_id: newJobId(change, "verifier"),
|
|
253
|
+
role: "verifier",
|
|
254
|
+
state: "requested",
|
|
255
|
+
gate_id: REVIEW_FINAL_VERIFIER_GATE_ID,
|
|
256
|
+
boundFiles,
|
|
257
|
+
review_evidence_digest: currentEvidenceDigest,
|
|
258
|
+
packet_digest: sha256Text(JSON.stringify({
|
|
259
|
+
role: "verifier",
|
|
260
|
+
gate_id: REVIEW_FINAL_VERIFIER_GATE_ID,
|
|
261
|
+
boundFiles,
|
|
262
|
+
review_evidence_digest: currentEvidenceDigest,
|
|
263
|
+
created_from_transition: "review-ready",
|
|
264
|
+
})),
|
|
265
|
+
created_from_transition: "review-ready",
|
|
266
|
+
created_at: new Date().toISOString(),
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
function evaluateFinalVerifierGate(input) {
|
|
270
|
+
const verifierOpen = input.snapshot.open_jobs.find(isReviewReadyVerifier);
|
|
271
|
+
if (verifierOpen) {
|
|
272
|
+
return {
|
|
273
|
+
blocked: true,
|
|
274
|
+
reason: `状态未推进;已有待完成最终验证工作项 ${verifierOpen.job_id}`,
|
|
275
|
+
jobs: [verifierOpen],
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
const verifierAccepted = input.snapshot.accepted_jobs.find(job => isFreshReviewVerifier(job, input.changeRoot, input.currentEvidenceDigest));
|
|
279
|
+
const finalVerifierRequired = requiresFinalVerifierForCurrentReview(input.events) || input.policy.requires_verifier;
|
|
280
|
+
if (!finalVerifierRequired) {
|
|
281
|
+
if (!input.hasStoredPolicy) {
|
|
282
|
+
return {
|
|
283
|
+
fromState: "review",
|
|
284
|
+
toState: "review",
|
|
285
|
+
outcome: "advanced",
|
|
286
|
+
reason: `补写审查策略=${input.policy.review_risk}`,
|
|
287
|
+
commitPayload: input.policyPayload,
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
return { skip: true, message: "已在 review 状态,审查策略无需 verifier" };
|
|
291
|
+
}
|
|
292
|
+
if (!verifierAccepted) {
|
|
293
|
+
const previousVerifierRejected = hasRejectedReviewReadyVerifier(input.events);
|
|
294
|
+
const job = createFinalVerifierJob(input.change, input.changeRoot, input.currentEvidenceDigest);
|
|
295
|
+
return {
|
|
296
|
+
fromState: input.snapshot.state,
|
|
297
|
+
toState: input.snapshot.state,
|
|
298
|
+
outcome: "job_created",
|
|
299
|
+
newJobs: [job],
|
|
300
|
+
reason: previousVerifierRejected
|
|
301
|
+
? "此前最终验证未通过;请先根据验证报告修改任务或文档,确认无需修改时再执行新的最终验证工作项"
|
|
302
|
+
: "创建最终验证工作项",
|
|
303
|
+
commitPayload: input.policyPayload,
|
|
304
|
+
...(previousVerifierRejected ? {
|
|
305
|
+
details: { advisory: "此前最终验证未通过;请先根据验证报告修改任务或文档,确认无需修改时再执行新的最终验证工作项" },
|
|
306
|
+
} : {}),
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
if (!input.hasStoredPolicy) {
|
|
310
|
+
return {
|
|
311
|
+
fromState: "review",
|
|
312
|
+
toState: "review",
|
|
313
|
+
outcome: "advanced",
|
|
314
|
+
reason: `补写审查策略=${input.policy.review_risk}`,
|
|
315
|
+
commitPayload: input.policyPayload,
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
return { skip: true, message: "已在 review 状态,最终验证仍然有效" };
|
|
319
|
+
}
|
|
179
320
|
function documentBaseline(changeRoot) {
|
|
180
321
|
const docs = ["proposal.md", "design.md", "tasks.md", ".superspec/artifacts/test-contract.md"];
|
|
181
322
|
const baseline = {};
|
|
@@ -313,8 +454,8 @@ export function proposeReady(projectRoot, change, changeRoot, risk = "strict") {
|
|
|
313
454
|
}
|
|
314
455
|
}
|
|
315
456
|
// 通用 job 检查(用提取的 helper)
|
|
316
|
-
const requiredRoles =
|
|
317
|
-
const reviewResult = checkOrCreateReviewJobs(snapshot, requiredRoles, changeRoot, change
|
|
457
|
+
const requiredRoles = PROPOSE_FINAL_REVIEW_GATE.requiredRolesForRisk(risk);
|
|
458
|
+
const reviewResult = checkOrCreateReviewJobs(snapshot, PROPOSE_FINAL_REVIEW_GATE, requiredRoles, changeRoot, change);
|
|
318
459
|
if (reviewResult)
|
|
319
460
|
return reviewResult;
|
|
320
461
|
return { fromState: "propose", toState: "propose_ready", outcome: "advanced", reason: `risk=${risk},所有需求已满足` };
|
|
@@ -349,8 +490,8 @@ export function transitionExplore(projectRoot, change, changeRoot, risk = "stric
|
|
|
349
490
|
if (!discoveryCheck.ok)
|
|
350
491
|
return { skip: true, message: discoveryCheck.message };
|
|
351
492
|
// 通用 job 审查(和 propose-ready 同一个 helper)
|
|
352
|
-
const requiredRoles =
|
|
353
|
-
const reviewResult = checkOrCreateReviewJobs(snapshot, requiredRoles, changeRoot, change
|
|
493
|
+
const requiredRoles = EXPLORE_DISCOVERY_REVIEW_GATE.requiredRolesForRisk(risk);
|
|
494
|
+
const reviewResult = checkOrCreateReviewJobs(snapshot, EXPLORE_DISCOVERY_REVIEW_GATE, requiredRoles, changeRoot, change);
|
|
354
495
|
if (reviewResult)
|
|
355
496
|
return reviewResult;
|
|
356
497
|
return { fromState: "explore", toState: "propose", outcome: "advanced", reason: "探索完成" };
|
|
@@ -373,7 +514,7 @@ export function startApply(projectRoot, change, changeRoot) {
|
|
|
373
514
|
}
|
|
374
515
|
const reviewedRoles = historicalProposeReadyRoles(projectRoot, change);
|
|
375
516
|
if (reviewedRoles.length > 0) {
|
|
376
|
-
const reviewResult = checkOrCreateReviewJobs(snapshot, reviewedRoles, changeRoot, change
|
|
517
|
+
const reviewResult = checkOrCreateReviewJobs(snapshot, PROPOSE_FINAL_REVIEW_GATE, reviewedRoles, changeRoot, change);
|
|
377
518
|
if (reviewResult) {
|
|
378
519
|
return {
|
|
379
520
|
...reviewResult,
|
|
@@ -538,111 +679,24 @@ export function reviewReady(projectRoot, change, changeRoot, risk = "strict") {
|
|
|
538
679
|
};
|
|
539
680
|
}
|
|
540
681
|
if (snapshot.state === "apply_done") {
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
blocked: true,
|
|
548
|
-
reason: `状态未推进;已有待完成代码审查工作项 ${freshOpenJobs[0].job_id}`,
|
|
549
|
-
jobs: [freshOpenJobs[0]],
|
|
550
|
-
};
|
|
551
|
-
}
|
|
552
|
-
const latest = facts.latestTerminal;
|
|
553
|
-
if (latest?.state === "accepted") {
|
|
554
|
-
return {
|
|
555
|
-
fromState: "apply_done", toState: "review", outcome: "advanced",
|
|
556
|
-
reason: "代码审查已通过,进入最终审查阶段",
|
|
557
|
-
commitPayload: {
|
|
558
|
-
...policyPayload,
|
|
559
|
-
code_review_gate: { decision: "passed", job_id: latest.job.job_id },
|
|
560
|
-
},
|
|
561
|
-
};
|
|
562
|
-
}
|
|
563
|
-
if (latest?.state === "rejected" && latest.result_kind === "review_failed") {
|
|
564
|
-
const reviewFailedStatus = latestCodeReviewFailedStatus(events);
|
|
565
|
-
if (reviewFailedStatus && reviewFailedStatus.findings.length > 0 && reviewFailedStatus.unresolved.length === 0) {
|
|
566
|
-
const { job, scanReason } = createCodeReviewerJob(change, projectRoot, events);
|
|
567
|
-
return {
|
|
568
|
-
fromState: "apply_done", toState: "apply_done", outcome: "job_created",
|
|
569
|
-
newJobs: [job],
|
|
570
|
-
reason: `重新创建代码审查工作项;${scanReason};上一次阻塞问题已被主流程复核驳回`,
|
|
571
|
-
};
|
|
572
|
-
}
|
|
573
|
-
return {
|
|
574
|
-
skip: true,
|
|
575
|
-
message: "代码审查发现需要处理的问题,请先执行 next,根据提示回到实现阶段修复或让使用者决定是否回到计划阶段",
|
|
576
|
-
};
|
|
577
|
-
}
|
|
578
|
-
const { job, scanReason } = createCodeReviewerJob(change, projectRoot, events);
|
|
579
|
-
return {
|
|
580
|
-
fromState: "apply_done", toState: "apply_done", outcome: "job_created",
|
|
581
|
-
newJobs: [job],
|
|
582
|
-
reason: latest?.state === "rejected"
|
|
583
|
-
? `重新创建代码审查工作项;上一次报告未被接受,原因:${latest.reason ?? "报告不符合要求"}`
|
|
584
|
-
: `创建代码审查工作项;${scanReason}`,
|
|
585
|
-
};
|
|
586
|
-
}
|
|
587
|
-
return {
|
|
588
|
-
fromState: "apply_done", toState: "review", outcome: "advanced",
|
|
589
|
-
reason: "没有代码类改动,直接进入最终审查阶段",
|
|
590
|
-
commitPayload: {
|
|
591
|
-
...policyPayload,
|
|
592
|
-
code_review_gate: { decision: "skipped", reason: "no_code_changes" },
|
|
593
|
-
},
|
|
594
|
-
};
|
|
682
|
+
return evaluateApplyDoneCodeReviewGate({
|
|
683
|
+
events,
|
|
684
|
+
projectRoot,
|
|
685
|
+
change,
|
|
686
|
+
policyPayload,
|
|
687
|
+
});
|
|
595
688
|
}
|
|
596
689
|
if (snapshot.state === "review") {
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
commitPayload: policyPayload,
|
|
608
|
-
};
|
|
609
|
-
}
|
|
610
|
-
return { skip: true, message: "已在 review 状态,审查策略无需 verifier" };
|
|
611
|
-
}
|
|
612
|
-
if (!verifierAccepted) {
|
|
613
|
-
const boundFiles = reviewBoundFiles(changeRoot);
|
|
614
|
-
const previousVerifierRejected = hasRejectedReviewReadyVerifier(events);
|
|
615
|
-
const job = {
|
|
616
|
-
job_id: newJobId(change, "verifier"), role: "verifier", state: "requested",
|
|
617
|
-
boundFiles, review_evidence_digest: currentEvidenceDigest,
|
|
618
|
-
packet_digest: sha256Text(JSON.stringify({
|
|
619
|
-
role: "verifier",
|
|
620
|
-
boundFiles,
|
|
621
|
-
review_evidence_digest: currentEvidenceDigest,
|
|
622
|
-
created_from_transition: "review-ready",
|
|
623
|
-
})),
|
|
624
|
-
created_from_transition: "review-ready", created_at: new Date().toISOString(),
|
|
625
|
-
};
|
|
626
|
-
return {
|
|
627
|
-
fromState: snapshot.state, toState: snapshot.state, outcome: "job_created",
|
|
628
|
-
newJobs: [job],
|
|
629
|
-
reason: previousVerifierRejected
|
|
630
|
-
? "此前最终验证未通过;请先根据验证报告修改任务或文档,确认无需修改时再执行新的最终验证工作项"
|
|
631
|
-
: "创建最终验证工作项",
|
|
632
|
-
commitPayload: policyPayload,
|
|
633
|
-
...(previousVerifierRejected ? {
|
|
634
|
-
details: { advisory: "此前最终验证未通过;请先根据验证报告修改任务或文档,确认无需修改时再执行新的最终验证工作项" },
|
|
635
|
-
} : {}),
|
|
636
|
-
};
|
|
637
|
-
}
|
|
638
|
-
if (!storedPolicy) {
|
|
639
|
-
return {
|
|
640
|
-
fromState: "review", toState: "review", outcome: "advanced",
|
|
641
|
-
reason: `补写审查策略=${policy.review_risk}`,
|
|
642
|
-
commitPayload: policyPayload,
|
|
643
|
-
};
|
|
644
|
-
}
|
|
645
|
-
return { skip: true, message: "已在 review 状态,最终验证仍然有效" };
|
|
690
|
+
return evaluateFinalVerifierGate({
|
|
691
|
+
snapshot,
|
|
692
|
+
events,
|
|
693
|
+
change,
|
|
694
|
+
changeRoot,
|
|
695
|
+
currentEvidenceDigest,
|
|
696
|
+
policy,
|
|
697
|
+
policyPayload,
|
|
698
|
+
hasStoredPolicy: Boolean(storedPolicy),
|
|
699
|
+
});
|
|
646
700
|
}
|
|
647
701
|
return { skip: true, message: `当前状态 ${snapshot.state},review-ready 不适用` };
|
|
648
702
|
},
|
|
@@ -711,42 +765,9 @@ export function taskComplete(projectRoot, change, changeRoot, taskId) {
|
|
|
711
765
|
const attempt = snapshot.active_task_attempts?.find(a => a.task_id === taskId && a.state === "active");
|
|
712
766
|
if (!attempt)
|
|
713
767
|
return { skip: true, message: `任务 ${taskId} 无活跃执行尝试` };
|
|
714
|
-
const
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
return { skip: true, message: "任务结构指纹不匹配" };
|
|
718
|
-
// 解析任务属性(委托给 format.ts 的 parseTasksMd)
|
|
719
|
-
const tasks = parseTasksMd(tasksContent);
|
|
720
|
-
const taskInfo = tasks.find(t => t.taskId === taskId);
|
|
721
|
-
const isTdd = taskInfo?.tddRequired ?? true;
|
|
722
|
-
const hasNoTddReason = taskInfo?.noTddReason != null;
|
|
723
|
-
// RED/GREEN 检查(HIGH-2 修复:按 attempt_id 匹配,避免多任务 digest 碰撞)
|
|
724
|
-
const events = readEvents(projectRoot, change);
|
|
725
|
-
let hasRed = false, hasGreen = false;
|
|
726
|
-
for (const ev of events) {
|
|
727
|
-
if (ev.event_type === "test_run_recorded") {
|
|
728
|
-
const tr = ev.payload;
|
|
729
|
-
// 匹配当前 attempt(优先按 attempt_id,回退到 digest)
|
|
730
|
-
const matches = tr.attempt_id === attempt.attempt_id ||
|
|
731
|
-
(!tr.attempt_id && tr.task_structure_digest === attempt.task_structure_digest);
|
|
732
|
-
if (matches) {
|
|
733
|
-
if (tr.semantic_status === "expected_failure" || tr.semantic_status === "characterization_pass")
|
|
734
|
-
hasRed = true;
|
|
735
|
-
if (tr.semantic_status === "expected_success")
|
|
736
|
-
hasGreen = true;
|
|
737
|
-
}
|
|
738
|
-
}
|
|
739
|
-
}
|
|
740
|
-
if (isTdd) {
|
|
741
|
-
if (!hasRed)
|
|
742
|
-
return { skip: true, message: `TDD 任务 ${taskId} 缺少 RED 证据` };
|
|
743
|
-
if (!hasGreen)
|
|
744
|
-
return { skip: true, message: `TDD 任务 ${taskId} 缺少 GREEN 证据` };
|
|
745
|
-
}
|
|
746
|
-
else {
|
|
747
|
-
if (!hasNoTddReason)
|
|
748
|
-
return { skip: true, message: `非 TDD 任务 ${taskId} 缺少 no_tdd_reason` };
|
|
749
|
-
}
|
|
768
|
+
const readiness = taskEvidenceReadiness(projectRoot, change, changeRoot, attempt);
|
|
769
|
+
if (!readiness.ready)
|
|
770
|
+
return { skip: true, message: `任务 ${taskId} 无法完成:${readiness.reason}` };
|
|
750
771
|
// B1 修复:checkbox 写入移到 postCommit(commit 事件写入后执行)
|
|
751
772
|
return {
|
|
752
773
|
fromState: "apply", toState: "apply", outcome: "advanced",
|
package/dist/types.d.ts
CHANGED
|
@@ -6,6 +6,7 @@ export type Ref = {
|
|
|
6
6
|
};
|
|
7
7
|
export type JobState = "requested" | "accepted" | "rejected";
|
|
8
8
|
export type JobRole = "critic" | "architect" | "test-engineer" | "executor" | "test-run" | "verifier" | "code-reviewer";
|
|
9
|
+
export type ReviewJobGateId = "explore.discovery_review" | "propose.final_review" | "review.code_review" | "review.final_verifier";
|
|
9
10
|
export type CodeReviewResultKind = "invalid_report" | "non_actionable_report" | "review_failed";
|
|
10
11
|
export interface CodeReviewPreviousRejection {
|
|
11
12
|
result_kind: CodeReviewResultKind;
|
|
@@ -16,6 +17,7 @@ export interface Job {
|
|
|
16
17
|
job_id: string;
|
|
17
18
|
role: JobRole;
|
|
18
19
|
state: JobState;
|
|
20
|
+
gate_id?: ReviewJobGateId;
|
|
19
21
|
boundFiles: Ref[];
|
|
20
22
|
review_evidence_digest?: string;
|
|
21
23
|
packet_digest: string;
|
|
@@ -26,9 +28,11 @@ export interface Job {
|
|
|
26
28
|
export interface JobPacket {
|
|
27
29
|
job_id: string;
|
|
28
30
|
role: JobRole;
|
|
31
|
+
gate_id?: ReviewJobGateId;
|
|
29
32
|
recommended_agent?: string;
|
|
30
33
|
boundFiles: Ref[];
|
|
31
34
|
review_evidence_digest?: string;
|
|
35
|
+
previous_rejection?: CodeReviewPreviousRejection;
|
|
32
36
|
packet_digest: string;
|
|
33
37
|
required_output_kind: string;
|
|
34
38
|
preferred_input_mode?: "stdin" | "file";
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ReviewRisk } from "./review.ts";
|
|
2
|
+
import type { JobRole, ReviewJobGateId } from "./types.ts";
|
|
3
|
+
export type WorkflowProfile = "light" | "normal" | "strict";
|
|
4
|
+
export interface WorkflowProfileResolution {
|
|
5
|
+
profile: WorkflowProfile;
|
|
6
|
+
risk: ReviewRisk;
|
|
7
|
+
reviewRolesByGate: Partial<Record<ReviewJobGateId, JobRole[]>>;
|
|
8
|
+
}
|
|
9
|
+
export declare function workflowProfileForRisk(risk: ReviewRisk): WorkflowProfile;
|
|
10
|
+
export declare function resolveWorkflowProfile(risk: ReviewRisk): WorkflowProfileResolution;
|
|
11
|
+
export declare function reviewRolesForGate(gateId: ReviewJobGateId, risk: ReviewRisk): JobRole[];
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
const CURRENT_GATE_ROLES_BY_RISK = {
|
|
2
|
+
minimal: {
|
|
3
|
+
"explore.discovery_review": [],
|
|
4
|
+
"propose.final_review": [],
|
|
5
|
+
"review.code_review": ["code-reviewer"],
|
|
6
|
+
"review.final_verifier": ["verifier"],
|
|
7
|
+
},
|
|
8
|
+
normal: {
|
|
9
|
+
"explore.discovery_review": [],
|
|
10
|
+
"propose.final_review": ["critic"],
|
|
11
|
+
"review.code_review": ["code-reviewer"],
|
|
12
|
+
"review.final_verifier": ["verifier"],
|
|
13
|
+
},
|
|
14
|
+
strict: {
|
|
15
|
+
"explore.discovery_review": ["critic"],
|
|
16
|
+
"propose.final_review": ["critic", "architect", "test-engineer"],
|
|
17
|
+
"review.code_review": ["code-reviewer"],
|
|
18
|
+
"review.final_verifier": ["verifier"],
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
export function workflowProfileForRisk(risk) {
|
|
22
|
+
if (risk === "minimal")
|
|
23
|
+
return "light";
|
|
24
|
+
if (risk === "normal")
|
|
25
|
+
return "normal";
|
|
26
|
+
return "strict";
|
|
27
|
+
}
|
|
28
|
+
export function resolveWorkflowProfile(risk) {
|
|
29
|
+
const profile = workflowProfileForRisk(risk);
|
|
30
|
+
const rolesByGate = CURRENT_GATE_ROLES_BY_RISK[risk];
|
|
31
|
+
const reviewRolesByGate = {};
|
|
32
|
+
for (const [gateId, roles] of Object.entries(rolesByGate)) {
|
|
33
|
+
reviewRolesByGate[gateId] = [...roles];
|
|
34
|
+
}
|
|
35
|
+
return { profile, risk, reviewRolesByGate };
|
|
36
|
+
}
|
|
37
|
+
export function reviewRolesForGate(gateId, risk) {
|
|
38
|
+
return resolveWorkflowProfile(risk).reviewRolesByGate[gateId] ?? [];
|
|
39
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<!-- SUPERSPEC:AGENTS:START -->
|
|
2
2
|
本项目启用 SuperSpec。使用 `superspec-*` 工作流时,以 `superspec transition next --change "<change>"` 返回的下一步为准;流程完成前不得跳阶段、不得自称完成。
|
|
3
3
|
|
|
4
|
-
即使用户没有显式调用 `superspec
|
|
4
|
+
即使用户没有显式调用 `superspec-*`,如果新输入像是在改变业务规则、产品口径、验收标准、示例规范、影响范围,或说明 PRD/文档/原型等需求源已更新,编辑代码前先提醒并做只读确认:这是实现偏差,还是需要先回 `superspec-propose` 更新计划文档;不要直接把这类自然语言当作 apply 授权。
|
|
5
5
|
|
|
6
6
|
当用户显式调用 `$superspec-explore` 工作流时,视为已明确授权启动 `explore` subagent 做只读深扫;其他 `$superspec-*` 阶段仅在工作流引擎创建独立工作项时,视为授权启动对应 subagent。
|
|
7
7
|
|
|
@@ -21,6 +21,7 @@ argument-hint: "本次探索说明"
|
|
|
21
21
|
## 本次任务说明
|
|
22
22
|
|
|
23
23
|
如果主流程提供本次任务说明,先读取其中指向的 refs。以本次任务说明中的目标范围、来源 refs、必读 refs、artifact refs 和停止条件为准;不要依赖本 prompt 记忆输出 schema。
|
|
24
|
+
如果本次任务说明表示 PRD、文档、原型或其它需求源已更新,但只提供旧 artifact 或旧 refs,不要把旧内容当作最新事实;在输出中标记需要主流程重新核对需求源,并说明可能影响范围、验收或用户可见行为的点。
|
|
24
25
|
|
|
25
26
|
## 深扫输出要求
|
|
26
27
|
|
|
@@ -28,7 +29,9 @@ argument-hint: "本次探索说明"
|
|
|
28
29
|
|
|
29
30
|
输出至少区分:
|
|
30
31
|
|
|
31
|
-
-
|
|
32
|
+
- 已确认事实(有源码/文档锚点、命令输出或用户确认直接支撑)
|
|
33
|
+
- 基于锚点的推断(写明依据)
|
|
34
|
+
- 未知/证据缺口(说明是否阻塞;影响范围、验收、用户可见行为、数据或安全相关未知交给主流程确认)
|
|
32
35
|
- 影响范围候选
|
|
33
36
|
- 风险和隐性约束
|
|
34
37
|
- 需要主流程确认的问题
|
|
@@ -34,6 +34,7 @@ argument-hint: "本次验证说明"
|
|
|
34
34
|
- 代码审查提出的阻塞问题是否都有合法闭环;审查修复是否有自身验证和必要的回归验证,回归 test-run 优先用 `covers_task_ids` 说明覆盖了哪些已完成任务。
|
|
35
35
|
- 已完成任务、测试记录、审查记录和提交给 verifier 的证据,是否能对应到 proposal、design、tasks、test-contract 的目标。
|
|
36
36
|
- 计划材料是否被绕过工作流改写;合法自动勾选和代码审查追加的修复项以工作项说明和事件记录为准。
|
|
37
|
+
- 工作项、用户输入或引用材料显示需求源已更新时,最终证据必须能对应已处理该变化的最新计划材料;仍引用旧计划且无 `## 需求变化` 处理时,按证据不足失败。
|
|
37
38
|
- RED/GREEN 证据是否同一次任务尝试闭环;退出码、环境错误或构建错误不能单独作为行为证明。
|
|
38
39
|
- 输入数据来源核查是否闭环;不得只用 GREEN 测试或 task 勾选证明输入完整性。
|
|
39
40
|
|
|
@@ -27,10 +27,10 @@ metadata:
|
|
|
27
27
|
|
|
28
28
|
每个 task 的标准循环:
|
|
29
29
|
|
|
30
|
-
1.
|
|
30
|
+
1. **计划核对**:执行 `task-start` 前,确认当前 task 是 `tasks.md` 顶格任务,并能对应 `design.md` 的实现方向和 `proposal.md` 的 `## Impact` 受影响原因。缺少映射、需要新增能力/验收/影响范围时先停止,交回 propose,不写 RED。
|
|
31
31
|
2. **任务开始**:`superspec transition task-start --change "<change>" --task <task_id>`。
|
|
32
32
|
3. **读取 attempt_id**:从 task-start 返回结果或当前活跃 task attempt 中读取。
|
|
33
|
-
4. **RED
|
|
33
|
+
4. **RED**:写测试前确认测试意图能对应 `test-contract.md` 的 `test_id` 或 `business-invariants.md`;缺少对应关系时先停止,交回 propose。运行后确认失败,并用 `superspec record test-run --change "<change>" --input -` 登记。
|
|
34
34
|
5. **实现**:根据任务写代码,保持范围小。`design.md` 不锁死字段名、函数名、SQL 或局部写法。
|
|
35
35
|
6. **GREEN**:运行测试确认通过,并登记 test-run。
|
|
36
36
|
7. **完成 task**:`superspec transition task-complete --change "<change>" --task <task_id>`。
|
|
@@ -74,7 +74,7 @@ no-TDD 任务(`tdd_required:false` + `no_tdd_reason`)跳过 RED/GREEN,但
|
|
|
74
74
|
- 需要判断影响范围或改动原因不自明时,参考 `proposal.md` 的 `## Impact`,但不要把它当作路径白名单。
|
|
75
75
|
- 编码时发现未列入影响范围的文件,如果从 diff 或引用链能直接解释为同一任务下的局部引用、测试辅助或机械连带改动,可以继续。
|
|
76
76
|
- 如果发现新增能力、用户可见行为、明显新增影响范围或原因不自明,停止扩大实现并报告给主流程;不要在 apply 阶段补改 `proposal.md`。
|
|
77
|
-
- 用户在 apply 期间或 apply
|
|
77
|
+
- 用户在 apply 期间或 apply 后补充最新业务规则、产品口径、验收标准、示例规范、兼容策略、影响范围,或说明需求源已更新时,停止实现并交回主流程使用 `superspec-propose` 更新计划文档;交回时说明变化来源、变化内容、影响范围和建议处理方式。
|
|
78
78
|
- 不修改 `proposal.md`、`design.md`、`specs/**` 或 `.superspec/**`。
|
|
79
79
|
- active attempt 期间不要修改 `tasks.md` 中除 `task-complete` 自动勾选目标 checkbox 外的内容。
|
|
80
80
|
- 不跳过 RED 直接写 GREEN。
|
|
@@ -21,7 +21,7 @@ metadata:
|
|
|
21
21
|
|
|
22
22
|
如果下一步提示当前阶段还有用户确认、审查或验证事项,先完成这些事项。完成前不要进入下一阶段,也不要修改业务代码;就绪或审查后只概括关键事实、真实待确认项和下一步。
|
|
23
23
|
|
|
24
|
-
如果下一步说明 discovery 不完整或有未确认问题,先检查并填写 discovery
|
|
24
|
+
如果下一步说明 discovery 不完整或有未确认问题,先检查并填写 discovery 草稿,不要把草稿占位、格式缺口或路径空白直接转问用户;用户明确说 PRD、文档、原型或其它需求源已更新时,先重新核对来源,不要复用旧依据;只有影响范围、验收标准、用户可见行为、数据来源或安全边界存在真实阻塞时才向用户提问,收到回答后优先用 `superspec record user-decision --change "<change>" --input -` 从 stdin 登记 JSON 内容;文件路径模式仍可作为 fallback。
|
|
25
25
|
|
|
26
26
|
本技能默认走完整审查路径。探索完成后,`explore → propose` 会先创建 `critic` 工作项,由 Critic 角色审查需求澄清记录。审查完成后优先通过 `superspec record job-submit --change "<change>" --job <JOB> --report -` 从 stdin 登记 JSON 报告内容;文件路径模式仍可作为 fallback。
|
|
27
27
|
|
|
@@ -81,6 +81,7 @@ metadata:
|
|
|
81
81
|
只有 `## 待确认问题` 段落内的 `- [ ]` 表示阻塞确认项。其他段落列事实、风险或影响范围时使用普通 bullet,不要用 checklist。
|
|
82
82
|
|
|
83
83
|
代码影响型需求的 `当前代码事实`、`影响范围候选`、`风险和边界` 应尽量包含 `path:line` 锚点。纯文档、配置或新文件任务没有代码锚点时,写明 `N/A` 理由并引用相关文档、配置或需求来源。
|
|
84
|
+
有源码/文档锚点、命令输出或用户确认直接支撑的内容,才写成事实;只有间接锚点支撑的,写为推断并说明依据;没有支撑的,写为未知并说明是否阻塞。影响范围、验收、用户可见行为、数据来源或安全边界的未知必须进入待确认问题,或说明非阻塞理由。
|
|
84
85
|
|
|
85
86
|
### 输入数据来源核查
|
|
86
87
|
|
|
@@ -144,6 +144,8 @@ superspec record user-decision --change "<change>" --input -
|
|
|
144
144
|
|
|
145
145
|
然后把用户决定反映到 proposal/design/test-contract,并将对应确认项改为 `[x]` 或移出未确认列表。局部实现细节、命名、普通文件组织和不影响需求/验收/风险的技术微调不要升级为用户确认。
|
|
146
146
|
|
|
147
|
+
进入 propose 后出现新的业务规则、产品口径、验收标准、示例规范或需求源更新时,不要静默覆盖原计划;默认先在 `proposal.md` 记录 `## 需求变化`,说明变化来源、变化内容、影响范围和处理方式(更新当前 change / 新建后续 change / 暂不处理)。只有影响技术路线、测试契约或业务不变量时,才同步更新 `design.md`、`test-contract.md` 或 `business-invariants.md`。
|
|
148
|
+
|
|
147
149
|
## 完成条件
|
|
148
150
|
|
|
149
151
|
tasks.md 作为计划文档就绪(不是复选框全完成)+ 基础职责文档齐全 → next 返回 propose-ready 命令。
|