@peterxiaoyang/superspec 0.1.37 → 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/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
|
+
}
|