@peterxiaoyang/superspec 0.1.40 → 0.1.42
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -14
- package/dist/cli.js +9 -13
- package/dist/code_review.d.ts +2 -6
- package/dist/install.d.ts +1 -1
- package/dist/install.js +5 -2
- package/dist/next.js +6 -9
- package/dist/phase_confirmation.d.ts +31 -0
- package/dist/phase_confirmation.js +218 -0
- package/dist/phase_plan.d.ts +5 -6
- package/dist/phase_plan.js +155 -15
- package/dist/record.js +280 -76
- package/dist/record_input.d.ts +9 -0
- package/dist/record_input.js +34 -0
- package/dist/review.d.ts +3 -1
- package/dist/review.js +103 -1
- package/dist/review_job_gates.d.ts +2 -1
- package/dist/review_job_gates.js +8 -5
- package/dist/task.js +9 -1
- package/dist/transition.d.ts +0 -1
- package/dist/transition.js +108 -42
- package/dist/types.d.ts +60 -4
- package/package.json +1 -1
- package/templates/workflow/AGENTS.md +2 -0
- package/templates/workflow/prompts/architect.md +16 -15
- package/templates/workflow/prompts/code-reviewer.md +1 -1
- package/templates/workflow/prompts/critic.md +22 -16
- package/templates/workflow/prompts/executor.md +1 -1
- package/templates/workflow/prompts/test-engineer.md +11 -7
- package/templates/workflow/prompts/test-runner.md +1 -1
- package/templates/workflow/skills/superspec-propose/SKILL.md +92 -23
- package/templates/workflow/skills/superspec-review/SKILL.md +9 -6
- package/templates/workflow/skills/superspec-archive/SKILL.md +0 -38
package/dist/review.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { existsSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { docRef, sha256File, sha256Text } from "./store.js";
|
|
5
|
-
import { REVIEW_FINAL_VERIFIER_GATE } from "./review_job_gates.js";
|
|
5
|
+
import { EXPLORE_DISCOVERY_REVIEW_GATE_ID, PROPOSE_FINAL_REVIEW_GATE_ID, REVIEW_FINAL_VERIFIER_GATE, } from "./review_job_gates.js";
|
|
6
6
|
import { computeCodeStateCheck, effectiveCoverageExemptionRefsFromEvents } from "./code_review.js";
|
|
7
7
|
export const REVIEW_DOC_PATHS = [
|
|
8
8
|
"proposal.md",
|
|
@@ -54,6 +54,108 @@ export function readReviewPolicyFromEvents(events) {
|
|
|
54
54
|
}
|
|
55
55
|
return null;
|
|
56
56
|
}
|
|
57
|
+
function reviewResultKind(value) {
|
|
58
|
+
return value === "invalid_report" || value === "non_actionable_report" || value === "review_failed"
|
|
59
|
+
? value
|
|
60
|
+
: null;
|
|
61
|
+
}
|
|
62
|
+
function reviewCycleState(gate) {
|
|
63
|
+
if (gate.gate_id === EXPLORE_DISCOVERY_REVIEW_GATE_ID)
|
|
64
|
+
return "explore";
|
|
65
|
+
if (gate.gate_id === PROPOSE_FINAL_REVIEW_GATE_ID)
|
|
66
|
+
return "propose";
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
function currentReviewGateCycleStart(events, gate) {
|
|
70
|
+
const state = reviewCycleState(gate);
|
|
71
|
+
if (!state)
|
|
72
|
+
return null;
|
|
73
|
+
for (let i = events.length - 1; i >= 0; i--) {
|
|
74
|
+
const event = events[i];
|
|
75
|
+
if (event.event_type !== "transition_commit")
|
|
76
|
+
continue;
|
|
77
|
+
const payload = event.payload;
|
|
78
|
+
if (payload.to_state === state && payload.from_state !== state)
|
|
79
|
+
return i;
|
|
80
|
+
}
|
|
81
|
+
return 0;
|
|
82
|
+
}
|
|
83
|
+
function reviewTerminalResultsForGateRole(events, gate, role) {
|
|
84
|
+
const cycleStartIndex = currentReviewGateCycleStart(events, gate);
|
|
85
|
+
if (cycleStartIndex == null)
|
|
86
|
+
return [];
|
|
87
|
+
const jobsById = new Map();
|
|
88
|
+
for (let i = cycleStartIndex; i < events.length; i++) {
|
|
89
|
+
const event = events[i];
|
|
90
|
+
if (event.event_type !== "transition_commit")
|
|
91
|
+
continue;
|
|
92
|
+
const jobs = event.payload.new_jobs ?? [];
|
|
93
|
+
for (const job of jobs) {
|
|
94
|
+
if (job.role === role && gate.isJobForGate(job))
|
|
95
|
+
jobsById.set(job.job_id, job);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
const results = [];
|
|
99
|
+
for (let i = cycleStartIndex; i < events.length; i++) {
|
|
100
|
+
const event = events[i];
|
|
101
|
+
if (event.event_type !== "job_accepted" && event.event_type !== "job_rejected")
|
|
102
|
+
continue;
|
|
103
|
+
const payload = event.payload;
|
|
104
|
+
if (typeof payload.job_id !== "string")
|
|
105
|
+
continue;
|
|
106
|
+
const job = jobsById.get(payload.job_id);
|
|
107
|
+
if (!job)
|
|
108
|
+
continue;
|
|
109
|
+
const findings = Array.isArray(payload.findings) ? payload.findings : undefined;
|
|
110
|
+
results.push({
|
|
111
|
+
job,
|
|
112
|
+
state: event.event_type === "job_accepted" ? "accepted" : "rejected",
|
|
113
|
+
...(event.event_type === "job_rejected"
|
|
114
|
+
? { result_kind: reviewResultKind(payload.result_kind) ?? (findings ? "review_failed" : "invalid_report") }
|
|
115
|
+
: {}),
|
|
116
|
+
...(typeof payload.reason === "string" && payload.reason.trim() !== "" ? { reason: payload.reason } : {}),
|
|
117
|
+
...(findings ? { findings } : {}),
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
return results;
|
|
121
|
+
}
|
|
122
|
+
export function latestReviewHistoryForGateRole(events, gate, role) {
|
|
123
|
+
const terminalResults = reviewTerminalResultsForGateRole(events, gate, role);
|
|
124
|
+
const latest = terminalResults.at(-1);
|
|
125
|
+
if (!latest || latest.state === "accepted")
|
|
126
|
+
return null;
|
|
127
|
+
const resultKind = latest.result_kind ?? "invalid_report";
|
|
128
|
+
const reason = latest.reason ?? (resultKind === "review_failed"
|
|
129
|
+
? "报告结论为 fail,工作项未通过"
|
|
130
|
+
: "审查报告无效,工作项未通过");
|
|
131
|
+
if (resultKind === "review_failed" && latest.findings && latest.findings.length > 0) {
|
|
132
|
+
return {
|
|
133
|
+
result_kind: resultKind,
|
|
134
|
+
reason,
|
|
135
|
+
job_id: latest.job.job_id,
|
|
136
|
+
...(latest.findings ? { findings: latest.findings } : {}),
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
for (let i = terminalResults.length - 2; i >= 0; i--) {
|
|
140
|
+
const previous = terminalResults[i];
|
|
141
|
+
if (previous.state === "accepted")
|
|
142
|
+
break;
|
|
143
|
+
if (previous.result_kind !== "review_failed" || !previous.findings || previous.findings.length === 0)
|
|
144
|
+
continue;
|
|
145
|
+
return {
|
|
146
|
+
result_kind: resultKind,
|
|
147
|
+
reason,
|
|
148
|
+
job_id: latest.job.job_id,
|
|
149
|
+
...(previous.findings ? { findings: previous.findings } : {}),
|
|
150
|
+
findings_job_id: previous.job.job_id,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
return {
|
|
154
|
+
result_kind: resultKind,
|
|
155
|
+
reason,
|
|
156
|
+
job_id: latest.job.job_id,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
57
159
|
export function isReviewReadyVerifier(job) {
|
|
58
160
|
return job.role === "verifier" && REVIEW_FINAL_VERIFIER_GATE.isJobForGate(job);
|
|
59
161
|
}
|
|
@@ -4,7 +4,8 @@ export interface ReviewGateRule {
|
|
|
4
4
|
gate_id: ReviewJobGateId;
|
|
5
5
|
created_from_transition: "explore" | "propose-ready" | "review-ready";
|
|
6
6
|
allowedRoles: JobRole[];
|
|
7
|
-
|
|
7
|
+
reviewTargets: string[];
|
|
8
|
+
readOnlyRefs: string[];
|
|
8
9
|
requiredRolesForRisk(risk: ReviewRisk): JobRole[];
|
|
9
10
|
matchesOldJob(job: Job): boolean;
|
|
10
11
|
isJobForGate(job: Job): boolean;
|
package/dist/review_job_gates.js
CHANGED
|
@@ -28,7 +28,8 @@ export const EXPLORE_DISCOVERY_REVIEW_GATE = makeReviewGateRule({
|
|
|
28
28
|
gate_id: EXPLORE_DISCOVERY_REVIEW_GATE_ID,
|
|
29
29
|
created_from_transition: "explore",
|
|
30
30
|
allowedRoles: EXPLORE_DISCOVERY_REVIEW_ROLES,
|
|
31
|
-
|
|
31
|
+
reviewTargets: [".superspec/artifacts/discovery.md"],
|
|
32
|
+
readOnlyRefs: [],
|
|
32
33
|
requiredRolesForRisk(risk) {
|
|
33
34
|
return reviewRolesForGate(EXPLORE_DISCOVERY_REVIEW_GATE_ID, risk);
|
|
34
35
|
},
|
|
@@ -40,15 +41,15 @@ export const PROPOSE_FINAL_REVIEW_GATE = makeReviewGateRule({
|
|
|
40
41
|
gate_id: PROPOSE_FINAL_REVIEW_GATE_ID,
|
|
41
42
|
created_from_transition: "propose-ready",
|
|
42
43
|
allowedRoles: PROPOSAL_REVIEW_ROLES,
|
|
43
|
-
|
|
44
|
+
reviewTargets: [
|
|
44
45
|
"proposal.md",
|
|
45
46
|
"tasks.md",
|
|
46
47
|
"design.md",
|
|
47
48
|
"specs/",
|
|
48
|
-
".superspec/artifacts/discovery.md",
|
|
49
49
|
".superspec/artifacts/business-invariants.md",
|
|
50
50
|
".superspec/artifacts/test-contract.md",
|
|
51
51
|
],
|
|
52
|
+
readOnlyRefs: [".superspec/artifacts/discovery.md"],
|
|
52
53
|
requiredRolesForRisk(risk) {
|
|
53
54
|
return reviewRolesForGate(PROPOSE_FINAL_REVIEW_GATE_ID, risk);
|
|
54
55
|
},
|
|
@@ -60,7 +61,8 @@ export const REVIEW_CODE_REVIEW_GATE = makeReviewGateRule({
|
|
|
60
61
|
gate_id: REVIEW_CODE_REVIEW_GATE_ID,
|
|
61
62
|
created_from_transition: "review-ready",
|
|
62
63
|
allowedRoles: REVIEW_CODE_REVIEW_ROLES,
|
|
63
|
-
|
|
64
|
+
reviewTargets: [],
|
|
65
|
+
readOnlyRefs: [],
|
|
64
66
|
requiredRolesForRisk(risk) {
|
|
65
67
|
return reviewRolesForGate(REVIEW_CODE_REVIEW_GATE_ID, risk);
|
|
66
68
|
},
|
|
@@ -72,7 +74,8 @@ export const REVIEW_FINAL_VERIFIER_GATE = makeReviewGateRule({
|
|
|
72
74
|
gate_id: REVIEW_FINAL_VERIFIER_GATE_ID,
|
|
73
75
|
created_from_transition: "review-ready",
|
|
74
76
|
allowedRoles: REVIEW_FINAL_VERIFIER_ROLES,
|
|
75
|
-
|
|
77
|
+
reviewTargets: [],
|
|
78
|
+
readOnlyRefs: [],
|
|
76
79
|
requiredRolesForRisk(risk) {
|
|
77
80
|
return reviewRolesForGate(REVIEW_FINAL_VERIFIER_GATE_ID, risk);
|
|
78
81
|
},
|
package/dist/task.js
CHANGED
|
@@ -3,6 +3,7 @@ import { existsSync, readFileSync } from "node:fs";
|
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { sha256Text, ensureChangeLayout, appendEvent, makeEvent, withLock, appendRawRecord, readEvents } from "./store.js";
|
|
5
5
|
import { tasksStructureDigest as formatDigest } from "./format.js";
|
|
6
|
+
import { RecordInputDecodingError, readRecordInputFile } from "./record_input.js";
|
|
6
7
|
/** tasks.md 结构指纹(委托给 format.ts 统一实现) */
|
|
7
8
|
export function tasksStructureDigestOf(changeRoot) {
|
|
8
9
|
const p = join(changeRoot, "tasks.md");
|
|
@@ -144,7 +145,14 @@ export function recordTestRun(projectRoot, change, inputFile) {
|
|
|
144
145
|
ensureChangeLayout(projectRoot, change);
|
|
145
146
|
if (!existsSync(inputFile))
|
|
146
147
|
return { accepted: false, message: `文件不存在:${inputFile}` };
|
|
147
|
-
|
|
148
|
+
try {
|
|
149
|
+
return recordTestRunLoaded(projectRoot, change, readRecordInputFile(inputFile));
|
|
150
|
+
}
|
|
151
|
+
catch (err) {
|
|
152
|
+
if (err instanceof RecordInputDecodingError)
|
|
153
|
+
return { accepted: false, message: err.message };
|
|
154
|
+
throw err;
|
|
155
|
+
}
|
|
148
156
|
});
|
|
149
157
|
}
|
|
150
158
|
/** record test-run:从 JSON 内容登记测试运行记录 */
|
package/dist/transition.d.ts
CHANGED
|
@@ -42,6 +42,5 @@ export declare function reopen(projectRoot: string, change: string, changeRoot:
|
|
|
42
42
|
}): TransitionResult;
|
|
43
43
|
export declare function reviewReady(projectRoot: string, change: string, changeRoot: string, risk?: "minimal" | "normal" | "strict"): TransitionResult;
|
|
44
44
|
export declare function accept(projectRoot: string, change: string, changeRoot: string): TransitionResult;
|
|
45
|
-
export declare function archive(projectRoot: string, change: string, changeRoot: string): TransitionResult;
|
|
46
45
|
export declare function taskComplete(projectRoot: string, change: string, changeRoot: string, taskId: string, inputContent?: string | null): TransitionResult;
|
|
47
46
|
export {};
|
package/dist/transition.js
CHANGED
|
@@ -1,40 +1,48 @@
|
|
|
1
1
|
// SuperSpec 流程引擎 — transition:提交协议 + 所有 transition 处理器
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
4
|
-
import { ensureChangeLayout, readEvents, appendEvent, makeEvent, writeSnapshot, snapshotDigest, withLock, idempotencyKey, docRef,
|
|
4
|
+
import { ensureChangeLayout, readEvents, appendEvent, makeEvent, writeSnapshot, snapshotDigest, withLock, idempotencyKey, docRef, sha256Text, } from "./store.js";
|
|
5
5
|
import { rebuildSnapshot } from "./sync.js";
|
|
6
6
|
import { requiredJobActions } from "./job_action.js";
|
|
7
|
-
import { assertCommitPayloadExtension, isFreshReviewVerifier, isReviewReadyVerifier, readReviewPolicyFromEvents, reviewBoundFiles, reviewEvidenceDigest, reviewPolicyForRisk, REVIEW_DOC_PATHS, } from "./review.js";
|
|
7
|
+
import { assertCommitPayloadExtension, isFreshReviewVerifier, isReviewReadyVerifier, latestReviewHistoryForGateRole, readReviewPolicyFromEvents, reviewBoundFiles, reviewEvidenceDigest, reviewPolicyForRisk, REVIEW_DOC_PATHS, } from "./review.js";
|
|
8
8
|
import { REVIEW_CODE_REVIEW_GATE_ID, REVIEW_FINAL_VERIFIER_GATE_ID, } from "./review_job_gates.js";
|
|
9
9
|
import { codeReviewBoundFiles, codeReviewDecisionScope, codeReviewJobStaleReason, codeReviewPacketContext, codeReviewPacketDigest, collectCodeReviewGateFacts, computeCodeStateCheck, currentCodeReviewWorkingPaths, dismissedCodeReviewSummary, latestCodeReviewDecision, latestCodeReviewFailedStatus, missingCoverageExemptionTestIds, requiresFinalVerifierForCurrentReview, scanCodeChangesForReview, } from "./code_review.js";
|
|
10
10
|
import { taskEvidenceReadiness } from "./task_evidence.js";
|
|
11
11
|
import { adoptedContractForTask, findTaskInLines, isReviewFixTaskId, parseTasksMd, parseTestContractEntries, } from "./format.js";
|
|
12
|
-
import { applyRequirementModeForCurrentRound, formatPendingTaskMessage, pendingTaskStatusForApply, planTransition, proposalDocsBaseline, } from "./phase_plan.js";
|
|
12
|
+
import { applyRequirementModeForCurrentRound, blockingJobsForApplyDone, formatPendingTaskMessage, latestAcceptedProposalBaseline, pendingTaskStatusForApply, planTransition, proposalDocsBaseline, } from "./phase_plan.js";
|
|
13
|
+
import { latestAcceptedPhaseDecision, phaseConfirmationCommitPayload, phaseConfirmationForBoundary, phaseConfirmationMissingMessage, } from "./phase_confirmation.js";
|
|
13
14
|
import { currentGitHead, dirtyCodeFiles } from "./git_state.js";
|
|
14
15
|
let transitionSeq = 0;
|
|
15
16
|
function newTransitionId() { return `T-${Date.now()}-${++transitionSeq}`; }
|
|
16
17
|
let jobSeq = 0;
|
|
17
18
|
function newJobId(change, role) { return `JOB-${change.slice(0, 8)}-${role.slice(0, 4)}-${Date.now()}-${++jobSeq}`; }
|
|
18
|
-
function createReviewJobsForGate(state, gate, roles, changeRoot, change, reason) {
|
|
19
|
+
function createReviewJobsForGate(state, gate, roles, changeRoot, change, reason, events) {
|
|
19
20
|
const newJobs = roles.map(role => {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
const boundPaths = [...new Set([...gate.reviewTargets, ...gate.readOnlyRefs])];
|
|
22
|
+
// 所有审查目标和只读引用都绑定时点指纹:单文件缺失使用 sha256:missing,目录缺失使用稳定空指纹。
|
|
23
|
+
const boundFiles = boundPaths
|
|
23
24
|
.map(p => docRef(changeRoot, p));
|
|
25
|
+
const previousRejection = latestReviewHistoryForGateRole(events, gate, role);
|
|
24
26
|
return {
|
|
25
27
|
job_id: newJobId(change, role),
|
|
26
28
|
role,
|
|
27
29
|
state: "requested",
|
|
28
30
|
gate_id: gate.gate_id,
|
|
29
31
|
boundFiles,
|
|
32
|
+
...(gate.reviewTargets.length > 0 ? { review_targets: [...gate.reviewTargets] } : {}),
|
|
33
|
+
...(gate.readOnlyRefs.length > 0 ? { read_only_refs: [...gate.readOnlyRefs] } : {}),
|
|
30
34
|
packet_digest: sha256Text(JSON.stringify({
|
|
31
35
|
role,
|
|
32
36
|
gate_id: gate.gate_id,
|
|
33
37
|
boundFiles,
|
|
38
|
+
review_targets: gate.reviewTargets,
|
|
39
|
+
read_only_refs: gate.readOnlyRefs,
|
|
34
40
|
created_from_transition: gate.created_from_transition,
|
|
41
|
+
...(previousRejection ? { previous_rejection: previousRejection } : {}),
|
|
35
42
|
})),
|
|
36
43
|
created_from_transition: gate.created_from_transition,
|
|
37
44
|
created_at: new Date().toISOString(),
|
|
45
|
+
...(previousRejection ? { previous_rejection: previousRejection } : {}),
|
|
38
46
|
};
|
|
39
47
|
});
|
|
40
48
|
return {
|
|
@@ -258,14 +266,17 @@ function evaluateApplyDoneCodeReviewGate(input) {
|
|
|
258
266
|
const latest = facts.latestTerminal;
|
|
259
267
|
if (latest?.state === "accepted") {
|
|
260
268
|
const acceptedScope = latest.job.packet_context?.code_review_scope;
|
|
261
|
-
|
|
269
|
+
const staleReason = codeReviewJobStaleReason(input.projectRoot, latest.job, currentWorkingPaths);
|
|
270
|
+
if (staleReason || !hasFrozenCodeReviewCurrentHead(acceptedScope)) {
|
|
262
271
|
const { job, scanReason } = createCodeReviewerJob(input.change, input.projectRoot, input.changeRoot, input.events);
|
|
263
272
|
return {
|
|
264
273
|
fromState: "apply_done",
|
|
265
274
|
toState: "apply_done",
|
|
266
275
|
outcome: "job_created",
|
|
267
276
|
newJobs: [job],
|
|
268
|
-
reason:
|
|
277
|
+
reason: staleReason
|
|
278
|
+
? `已接受代码审查工作项不再匹配当前代码状态,重新创建代码审查工作项;${staleReason};${scanReason}`
|
|
279
|
+
: `已接受代码审查工作项缺少冻结 current_head,重新创建代码审查工作项;${scanReason}`,
|
|
269
280
|
};
|
|
270
281
|
}
|
|
271
282
|
return {
|
|
@@ -403,14 +414,35 @@ function evaluateFinalVerifierGate(input) {
|
|
|
403
414
|
}
|
|
404
415
|
return { skip: true, message: "已在 review 状态,最终验证仍然有效" };
|
|
405
416
|
}
|
|
406
|
-
function
|
|
417
|
+
function authorizePhaseAdvance(input) {
|
|
418
|
+
const confirmation = phaseConfirmationForBoundary(input.projectRoot, input.events, input.snapshot, input.boundary);
|
|
419
|
+
const phaseDecision = confirmation
|
|
420
|
+
? latestAcceptedPhaseDecision(input.events, confirmation)
|
|
421
|
+
: null;
|
|
422
|
+
if (!confirmation || phaseDecision?.decision !== "advance") {
|
|
423
|
+
return {
|
|
424
|
+
skip: true,
|
|
425
|
+
message: confirmation
|
|
426
|
+
? phaseConfirmationMissingMessage(confirmation)
|
|
427
|
+
: `无法建立 ${input.boundary} 阶段确认范围`,
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
return {
|
|
431
|
+
...input.decision,
|
|
432
|
+
commitPayload: {
|
|
433
|
+
...(input.decision.commitPayload ?? {}),
|
|
434
|
+
...phaseConfirmationCommitPayload(confirmation, phaseDecision),
|
|
435
|
+
},
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
function transitionPlanToDecision(snapshot, changeRoot, change, plan, events) {
|
|
407
439
|
switch (plan.kind) {
|
|
408
440
|
case "skip":
|
|
409
441
|
return { skip: true, message: plan.message };
|
|
410
442
|
case "blocked":
|
|
411
443
|
return { blocked: true, reason: plan.reason, jobs: plan.jobs };
|
|
412
444
|
case "create_gate_jobs":
|
|
413
|
-
return createReviewJobsForGate(snapshot.state, plan.gate, plan.roles, changeRoot, change, plan.reason);
|
|
445
|
+
return createReviewJobsForGate(snapshot.state, plan.gate, plan.roles, changeRoot, change, plan.reason, events);
|
|
414
446
|
case "advance":
|
|
415
447
|
return {
|
|
416
448
|
fromState: plan.fromState,
|
|
@@ -520,7 +552,7 @@ export function proposeReady(projectRoot, change, changeRoot, risk = "strict") {
|
|
|
520
552
|
snapshot,
|
|
521
553
|
mode: { kind: "risk", risk },
|
|
522
554
|
});
|
|
523
|
-
return transitionPlanToDecision(snapshot, changeRoot, change, plan);
|
|
555
|
+
return transitionPlanToDecision(snapshot, changeRoot, change, plan, events);
|
|
524
556
|
},
|
|
525
557
|
});
|
|
526
558
|
}
|
|
@@ -550,7 +582,7 @@ export function transitionExplore(projectRoot, change, changeRoot, risk = "stric
|
|
|
550
582
|
snapshot,
|
|
551
583
|
mode: { kind: "risk", risk },
|
|
552
584
|
});
|
|
553
|
-
return transitionPlanToDecision(snapshot, changeRoot, change, plan);
|
|
585
|
+
return transitionPlanToDecision(snapshot, changeRoot, change, plan, events);
|
|
554
586
|
},
|
|
555
587
|
});
|
|
556
588
|
}
|
|
@@ -568,7 +600,7 @@ export function startApply(projectRoot, change, changeRoot) {
|
|
|
568
600
|
snapshot,
|
|
569
601
|
mode: { kind: "risk", risk: "strict" },
|
|
570
602
|
});
|
|
571
|
-
return transitionPlanToDecision(snapshot, changeRoot, change, plan);
|
|
603
|
+
return transitionPlanToDecision(snapshot, changeRoot, change, plan, events);
|
|
572
604
|
},
|
|
573
605
|
});
|
|
574
606
|
}
|
|
@@ -718,6 +750,45 @@ export function reopen(projectRoot, change, changeRoot, to, reason, opts = {}) {
|
|
|
718
750
|
},
|
|
719
751
|
};
|
|
720
752
|
}
|
|
753
|
+
if (to === "propose") {
|
|
754
|
+
if (snapshot.state !== "accepted") {
|
|
755
|
+
return {
|
|
756
|
+
skip: true,
|
|
757
|
+
message: `当前状态 ${snapshot.state},主动 reopen --to propose 只允许从 accepted 发起;apply_done 的代码审查问题请使用 --review-finding`,
|
|
758
|
+
};
|
|
759
|
+
}
|
|
760
|
+
if (snapshot.open_jobs.length > 0) {
|
|
761
|
+
return {
|
|
762
|
+
blocked: true,
|
|
763
|
+
reason: `状态未推进;accepted 仍有 ${snapshot.open_jobs.length} 个待完成工作项`,
|
|
764
|
+
jobs: snapshot.open_jobs,
|
|
765
|
+
};
|
|
766
|
+
}
|
|
767
|
+
const acceptedBaseline = latestAcceptedProposalBaseline(events);
|
|
768
|
+
const currentBaseline = proposalDocsBaseline(changeRoot);
|
|
769
|
+
// 旧版 accepted 事件的基线可能缺少后来纳入 Propose gate 的材料。保留其已冻结
|
|
770
|
+
// 的摘要,并用 reopen 当刻的摘要补齐缺项,确保本轮之后对任一审查目标的修改都能被检测。
|
|
771
|
+
const baselineNeedsBackfill = acceptedBaseline !== null && Object.keys(currentBaseline)
|
|
772
|
+
.some(path => !Object.prototype.hasOwnProperty.call(acceptedBaseline, path));
|
|
773
|
+
const baselineDocs = acceptedBaseline
|
|
774
|
+
? Object.fromEntries(Object.entries(currentBaseline).map(([path, digest]) => [
|
|
775
|
+
path,
|
|
776
|
+
Object.prototype.hasOwnProperty.call(acceptedBaseline, path) ? acceptedBaseline[path] : digest,
|
|
777
|
+
]))
|
|
778
|
+
: currentBaseline;
|
|
779
|
+
return {
|
|
780
|
+
fromState: "accepted",
|
|
781
|
+
toState: "propose",
|
|
782
|
+
outcome: "advanced",
|
|
783
|
+
reason: reason.trim(),
|
|
784
|
+
commitPayload: {
|
|
785
|
+
reopen_target: "propose",
|
|
786
|
+
reopen_source: "accepted",
|
|
787
|
+
baseline_source: acceptedBaseline ? (baselineNeedsBackfill ? "accepted_backfill" : "accepted") : "reopen_fallback",
|
|
788
|
+
baseline_docs: baselineDocs,
|
|
789
|
+
},
|
|
790
|
+
};
|
|
791
|
+
}
|
|
721
792
|
if (to !== "apply")
|
|
722
793
|
return { skip: true, message: `reopen 当前只支持 --to apply 或 --to propose,不支持 ${to}` };
|
|
723
794
|
if (snapshot.state !== "apply_done" && snapshot.state !== "review") {
|
|
@@ -767,13 +838,34 @@ export function reviewReady(projectRoot, change, changeRoot, risk = "strict") {
|
|
|
767
838
|
};
|
|
768
839
|
}
|
|
769
840
|
if (snapshot.state === "apply_done") {
|
|
770
|
-
|
|
841
|
+
const blockingJobs = blockingJobsForApplyDone(projectRoot, events, snapshot);
|
|
842
|
+
if (blockingJobs.length > 0) {
|
|
843
|
+
return {
|
|
844
|
+
blocked: true,
|
|
845
|
+
reason: `状态未推进;有 ${blockingJobs.length} 个待完成工作项`,
|
|
846
|
+
jobs: blockingJobs,
|
|
847
|
+
};
|
|
848
|
+
}
|
|
849
|
+
const codeReviewDecision = evaluateApplyDoneCodeReviewGate({
|
|
771
850
|
events,
|
|
772
851
|
projectRoot,
|
|
773
852
|
changeRoot,
|
|
774
853
|
change,
|
|
775
854
|
policyPayload,
|
|
776
855
|
});
|
|
856
|
+
if (!("skip" in codeReviewDecision) &&
|
|
857
|
+
!("blocked" in codeReviewDecision) &&
|
|
858
|
+
codeReviewDecision.fromState === "apply_done" &&
|
|
859
|
+
codeReviewDecision.toState === "review") {
|
|
860
|
+
return authorizePhaseAdvance({
|
|
861
|
+
projectRoot,
|
|
862
|
+
events,
|
|
863
|
+
snapshot,
|
|
864
|
+
boundary: "apply_to_review",
|
|
865
|
+
decision: codeReviewDecision,
|
|
866
|
+
});
|
|
867
|
+
}
|
|
868
|
+
return codeReviewDecision;
|
|
777
869
|
}
|
|
778
870
|
if (snapshot.state === "review") {
|
|
779
871
|
return evaluateFinalVerifierGate({
|
|
@@ -806,33 +898,7 @@ export function accept(projectRoot, change, changeRoot) {
|
|
|
806
898
|
snapshot,
|
|
807
899
|
mode: { kind: "risk", risk: "strict" },
|
|
808
900
|
});
|
|
809
|
-
return transitionPlanToDecision(snapshot, changeRoot, change, plan);
|
|
810
|
-
},
|
|
811
|
-
});
|
|
812
|
-
}
|
|
813
|
-
// ===== archive =====
|
|
814
|
-
export function archive(projectRoot, change, changeRoot) {
|
|
815
|
-
return commitTransition(projectRoot, change, changeRoot, {
|
|
816
|
-
name: "archive", idempotencyInputs: { phase: "archive" },
|
|
817
|
-
decide: (snapshot) => {
|
|
818
|
-
if (snapshot.state !== "accepted")
|
|
819
|
-
return { skip: true, message: `当前状态 ${snapshot.state},需要 accepted` };
|
|
820
|
-
// 构建保全清单(Phase 4 简化版:记录文档指纹 + specs/)
|
|
821
|
-
const manifest = {};
|
|
822
|
-
const docPaths = ["proposal.md", "tasks.md", "design.md", ".superspec/artifacts/discovery.md", ".superspec/artifacts/business-invariants.md", ".superspec/artifacts/test-contract.md"];
|
|
823
|
-
for (const p of docPaths) {
|
|
824
|
-
manifest[p] = sha256File(join(changeRoot, p)) ?? "sha256:missing";
|
|
825
|
-
}
|
|
826
|
-
// specs/ 目录:递归收录 .md(覆盖 specs/<capability>/spec.md 布局)
|
|
827
|
-
const specsDir = join(changeRoot, "specs");
|
|
828
|
-
for (const rel of listMarkdownFiles(specsDir)) {
|
|
829
|
-
manifest[`specs/${rel}`] = sha256File(join(specsDir, rel)) ?? "sha256:missing";
|
|
830
|
-
}
|
|
831
|
-
return {
|
|
832
|
-
fromState: "accepted", toState: "archive", outcome: "advanced",
|
|
833
|
-
reason: "归档完成",
|
|
834
|
-
extraEvents: [{ type: "artifact_recorded", payload: { kind: "archive_preservation_manifest", manifest } }],
|
|
835
|
-
};
|
|
901
|
+
return transitionPlanToDecision(snapshot, changeRoot, change, plan, events);
|
|
836
902
|
},
|
|
837
903
|
});
|
|
838
904
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -8,23 +8,28 @@ export type JobState = "requested" | "accepted" | "rejected";
|
|
|
8
8
|
export type JobRole = "critic" | "architect" | "test-engineer" | "executor" | "test-run" | "verifier" | "code-reviewer";
|
|
9
9
|
export type ReviewJobGateId = "explore.discovery_review" | "propose.final_review" | "review.code_review" | "review.final_verifier";
|
|
10
10
|
export type CodeReviewResultKind = "invalid_report" | "non_actionable_report" | "review_failed";
|
|
11
|
-
export interface
|
|
11
|
+
export interface ReviewPreviousRejection {
|
|
12
12
|
result_kind: CodeReviewResultKind;
|
|
13
13
|
reason: string;
|
|
14
14
|
job_id: string;
|
|
15
|
+
findings?: unknown[];
|
|
16
|
+
findings_job_id?: string;
|
|
15
17
|
}
|
|
18
|
+
export type CodeReviewPreviousRejection = ReviewPreviousRejection;
|
|
16
19
|
export interface Job {
|
|
17
20
|
job_id: string;
|
|
18
21
|
role: JobRole;
|
|
19
22
|
state: JobState;
|
|
20
23
|
gate_id?: ReviewJobGateId;
|
|
21
24
|
boundFiles: Ref[];
|
|
25
|
+
review_targets?: string[];
|
|
26
|
+
read_only_refs?: string[];
|
|
22
27
|
review_evidence_digest?: string;
|
|
23
28
|
packet_digest: string;
|
|
24
29
|
packet_context?: JobPacketContext;
|
|
25
30
|
created_from_transition: string;
|
|
26
31
|
created_at: string;
|
|
27
|
-
previous_rejection?:
|
|
32
|
+
previous_rejection?: ReviewPreviousRejection;
|
|
28
33
|
}
|
|
29
34
|
export interface ExecutionContract {
|
|
30
35
|
tests: string[];
|
|
@@ -92,8 +97,10 @@ export interface JobPacket {
|
|
|
92
97
|
gate_id?: ReviewJobGateId;
|
|
93
98
|
recommended_agent?: string;
|
|
94
99
|
boundFiles: Ref[];
|
|
100
|
+
review_targets?: string[];
|
|
101
|
+
read_only_refs?: string[];
|
|
95
102
|
review_evidence_digest?: string;
|
|
96
|
-
previous_rejection?:
|
|
103
|
+
previous_rejection?: ReviewPreviousRejection;
|
|
97
104
|
packet_context?: JobPacketContext;
|
|
98
105
|
code_review_scope?: CodeReviewScope;
|
|
99
106
|
coverage_exemption_refs?: CoverageExemptionRef[];
|
|
@@ -155,6 +162,15 @@ export interface TransitionCommitPayload {
|
|
|
155
162
|
head?: string | null;
|
|
156
163
|
reason?: "no_code_changes";
|
|
157
164
|
};
|
|
165
|
+
phase_confirmation?: {
|
|
166
|
+
boundary: "explore_to_propose" | "propose_to_apply" | "apply_to_review" | "accepted_to_archive";
|
|
167
|
+
decision: "advance";
|
|
168
|
+
epoch_event_id: string;
|
|
169
|
+
material_digest: string;
|
|
170
|
+
scope: string;
|
|
171
|
+
decision_event_id: string;
|
|
172
|
+
};
|
|
173
|
+
accepted_baseline_docs?: Record<string, string>;
|
|
158
174
|
}
|
|
159
175
|
export interface Snapshot {
|
|
160
176
|
change_id: string;
|
|
@@ -207,10 +223,47 @@ export interface MissingInput {
|
|
|
207
223
|
expected: string;
|
|
208
224
|
command_to_fix: string;
|
|
209
225
|
}
|
|
226
|
+
export type AskUserActionResume = {
|
|
227
|
+
kind: "next";
|
|
228
|
+
argv: string[];
|
|
229
|
+
} | {
|
|
230
|
+
kind: "continue_current_phase";
|
|
231
|
+
instruction: string;
|
|
232
|
+
next_argv_after_completion: string[];
|
|
233
|
+
} | {
|
|
234
|
+
kind: "stop";
|
|
235
|
+
};
|
|
236
|
+
export interface AskUserAction {
|
|
237
|
+
label: string;
|
|
238
|
+
selection: "exact_label";
|
|
239
|
+
reason: "none" | "required" | "optional";
|
|
240
|
+
reason_prompt?: string;
|
|
241
|
+
record_argv: string[];
|
|
242
|
+
record_input: {
|
|
243
|
+
scope: string;
|
|
244
|
+
question: string;
|
|
245
|
+
answer: string;
|
|
246
|
+
reason?: string;
|
|
247
|
+
};
|
|
248
|
+
resume: AskUserActionResume;
|
|
249
|
+
}
|
|
210
250
|
export interface AskUser {
|
|
211
251
|
question: string;
|
|
212
252
|
allowed_answers: string[];
|
|
213
253
|
scope: string;
|
|
254
|
+
actions?: AskUserAction[];
|
|
255
|
+
}
|
|
256
|
+
export interface AcceptedMaterialFollowupContinuation {
|
|
257
|
+
kind: "accepted_material_followup";
|
|
258
|
+
trigger: "material_user_followup";
|
|
259
|
+
reason_source: "summarize_user_input";
|
|
260
|
+
reopen_argv_template: string[];
|
|
261
|
+
resume: {
|
|
262
|
+
kind: "continue_current_phase";
|
|
263
|
+
instruction: string;
|
|
264
|
+
next_argv_after_completion: string[];
|
|
265
|
+
};
|
|
266
|
+
plan_docs_changed_since_accept: boolean | null;
|
|
214
267
|
}
|
|
215
268
|
export type NextOutput = {
|
|
216
269
|
state: State;
|
|
@@ -230,6 +283,7 @@ export type NextOutput = {
|
|
|
230
283
|
} | {
|
|
231
284
|
path: "done";
|
|
232
285
|
reason: string;
|
|
286
|
+
continuation?: AcceptedMaterialFollowupContinuation;
|
|
233
287
|
});
|
|
234
288
|
export interface TransitionResult {
|
|
235
289
|
transition: string;
|
|
@@ -243,8 +297,10 @@ export interface TransitionResult {
|
|
|
243
297
|
details?: Record<string, unknown>;
|
|
244
298
|
}
|
|
245
299
|
export interface RecordResult {
|
|
246
|
-
|
|
300
|
+
/** Undefined means the input was rejected before an event was written and may be corrected and resubmitted. */
|
|
301
|
+
event_type?: EventType;
|
|
247
302
|
accepted: boolean;
|
|
248
303
|
message: string;
|
|
249
304
|
job_state?: JobState;
|
|
305
|
+
events_written?: number;
|
|
250
306
|
}
|
package/package.json
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
|
|
4
4
|
即使用户没有显式调用 `superspec-*`,如果新输入像是在改变业务规则、产品口径、验收标准、示例规范、影响范围,或说明 PRD/文档/原型等需求源已更新,编辑代码前先提醒并做只读确认:这是实现偏差,还是需要先回 `superspec-propose` 更新计划文档;不要直接把这类自然语言当作 apply 授权。
|
|
5
5
|
|
|
6
|
+
用户补充 SuperSpec 相关内容时,先确定对应 change,再按 next 返回处理;无法确定时只询问归属,不执行流转。内部命令由主流程完成,不交给用户。
|
|
7
|
+
|
|
6
8
|
当用户显式调用 `$superspec-explore` 工作流时,视为已明确授权启动 `explore` subagent 做只读深扫;其他 `$superspec-*` 阶段仅在工作流引擎创建独立工作项时,视为授权启动对应 subagent。
|
|
7
9
|
|
|
8
10
|
SuperSpec 创建的独立审查/验证工作项,视为已授权启动对应 subagent;无需再次询问用户。主会话不得自批这些工作项。
|
|
@@ -18,21 +18,22 @@ argument-hint: "本次架构审查说明"
|
|
|
18
18
|
|
|
19
19
|
## 计划 / 设计审查口径
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
- `
|
|
24
|
-
-
|
|
25
|
-
-
|
|
26
|
-
-
|
|
27
|
-
-
|
|
28
|
-
-
|
|
29
|
-
-
|
|
30
|
-
-
|
|
31
|
-
-
|
|
32
|
-
-
|
|
33
|
-
-
|
|
34
|
-
-
|
|
35
|
-
-
|
|
21
|
+
重点审查技术方案是否可落地、系统责任边界是否合理、关键设计契约是否充分。文档结构、跨文档登记和 task 格式由 Critic 主责;测试可验证性由 Test Engineer 主责。不要重复报告纯标题、空章节、复述、task ID / 顺序或 TEST 映射格式问题,但发现真实技术影响未登记时仍须直接报告。
|
|
22
|
+
|
|
23
|
+
- `design.md` 的代码影响型能力应有技术上可行的实现路线,说明方案落在哪个系统责任边界,以及本次实际涉及的数据、接口、状态或控制流如何变化;缺少到实现者无法落地时必须 `verdict:"fail"`
|
|
24
|
+
- 如果不同实现会产生不同的行为、字段组或数据语义、接口增量或兼容结果、状态转换、优先级、一致性、事务 / 幂等 / 并发、恢复结果或关键算法语义,design 必须明确对应绑定性契约;仍需实现者自行选择关键语义时必须失败。逐行代码、完整 SQL,以及以文件修改、task 执行或测试操作为对象的机械清单不属于 design;算法阶段、数据 / 控制流、状态转换和事务顺序可以有序表达
|
|
25
|
+
- discovery / proposal / specs 中影响实现的事实和约束必须转成具体设计安排;只罗列材料、关键约束未进入方案,或方案建立在与已确认事实不符的假设上时,必须 `verdict:"fail"`
|
|
26
|
+
- 声明复用现有链路时,应能确认复用对象、接入位置、本次差异和保持不变的语义;重复上游已有变形、增加双重兜底或改变持久化语义却没有明确理由时,应失败。改变已确认的规则变形或持久化语义时,不仅要说明理由和边界,还必须明确声明为本次目标,并与 proposal、specs 和 Impact 按适用范围完成对账
|
|
27
|
+
- 多个功能点共享字段组、接口语义、状态机、优先级或一致性规则时,应形成统一契约;不同方案对同一契约给出冲突解释时,应失败
|
|
28
|
+
- 关键路线未定且未进入 `## 待用户确认`,或 `tasks.md` 无法从实现方案和边界约束中技术性推出时,必须失败
|
|
29
|
+
- 不按固定章节判定设计质量,也不要要求虚假替代方案或风险;但存在明显误走路线、非显然风险、共享契约、兼容、迁移、回滚或发布顺序约束却完全未说明时,必须失败。只有不影响方案落地和边界判定、但仍值得关注的问题才列为非阻塞风险
|
|
30
|
+
- discovery 含 `## 输入数据来源核查` 时,数据来源必须追到目标字段或集合最后一次改变形态的位置;输入完整性方案与 consumer 获得完整输入后的算法方案必须分开说明
|
|
31
|
+
- `IDC-xxx` 为 `未知阻塞` 时 design 不得 ready;为 `未知非阻塞` 时,理由必须在技术上成立,并绑定验收口径或反例
|
|
32
|
+
- discovery 含 `## 链路五要素` 时,方案不得违背已确认的来源、规则变形、持久化语义、消费者或视图差异;确需重复防御或二次变形时必须说明原因和边界
|
|
33
|
+
- 发现 Impact 未登记的真实系统边界、消费者、视图差异或用户 / 系统可观察行为影响时,应直接判为 Impact / design 对账缺口;纯测试脆弱性和实现复杂度不要求进入 Impact
|
|
34
|
+
- 输入来源修复不得无说明地扩大相邻规则、查询、缓存或数据形态的语义
|
|
35
|
+
- task 的 `设计` 引用应指向技术上可行的实现方案、共享契约或边界约束;`边界` 应保护具体系统行为、数据语义、外部接口或共享规则。引用存在但方案不可行、边界与 design / Impact 冲突或漏掉明显高风险边界时,应失败
|
|
36
|
+
- task 分组应符合系统责任边界;高风险模块、跨入口行为或无法独立验证的大改动,应拆成可独立审查和验证的 task
|
|
36
37
|
|
|
37
38
|
## 输出风格
|
|
38
39
|
|