@peterxiaoyang/superspec 0.1.22 → 0.1.24
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/next.js +38 -0
- package/dist/record.js +31 -4
- package/dist/review.d.ts +17 -0
- package/dist/review.js +159 -0
- package/dist/store.d.ts +15 -0
- package/dist/store.js +50 -0
- package/dist/sync.js +9 -13
- package/dist/task.js +7 -2
- package/dist/transition.d.ts +1 -0
- package/dist/transition.js +93 -15
- package/dist/types.d.ts +7 -0
- package/package.json +16 -5
- package/templates/workflow/prompts/architect.md +11 -0
- package/templates/workflow/prompts/critic.md +37 -1
- package/templates/workflow/prompts/explore.md +13 -0
- package/templates/workflow/prompts/test-engineer.md +14 -4
- package/templates/workflow/prompts/verifier.md +13 -1
- package/templates/workflow/skills/superspec-apply/SKILL.md +14 -2
- package/templates/workflow/skills/superspec-explore/SKILL.md +30 -5
- package/templates/workflow/skills/superspec-propose/SKILL.md +41 -6
- package/templates/workflow/skills/superspec-review/SKILL.md +11 -0
package/dist/next.js
CHANGED
|
@@ -3,6 +3,7 @@ import { rebuildSnapshot } from "./sync.js";
|
|
|
3
3
|
import { readFileSync } from "node:fs";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { readEvents, sha256Text } from "./store.js";
|
|
6
|
+
import { isFreshReviewVerifier, isReviewReadyVerifier, readReviewPolicyFromEvents, reviewEvidenceDigest } from "./review.js";
|
|
6
7
|
import { validateDiscovery, countDiscoveryOpenQuestions, collectProposeOpenQuestions, parseTasksMd, pendingTasksInContent } from "./format.js";
|
|
7
8
|
const ACTIVE_PROPOSAL_REVIEW_ROLES = new Set(["critic", "architect", "test-engineer"]);
|
|
8
9
|
function isActiveProposalReviewJob(job) {
|
|
@@ -244,6 +245,43 @@ export function next(projectRoot, change, changeRoot, defaultRisk = "strict") {
|
|
|
244
245
|
missing_inputs: [],
|
|
245
246
|
};
|
|
246
247
|
}
|
|
248
|
+
const reviewVerifierJobs = snapshot.open_jobs.filter(isReviewReadyVerifier);
|
|
249
|
+
if (reviewVerifierJobs.length > 0) {
|
|
250
|
+
return {
|
|
251
|
+
state: "review",
|
|
252
|
+
path: "required_job",
|
|
253
|
+
required_jobs: reviewVerifierJobs.map(j => ({
|
|
254
|
+
job_id: j.job_id,
|
|
255
|
+
role: j.role,
|
|
256
|
+
packet_command: packetCommand(change, j.job_id),
|
|
257
|
+
})),
|
|
258
|
+
reason: `有 ${reviewVerifierJobs.length} 个待完成最终验证工作项`,
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
const events = readEvents(projectRoot, change);
|
|
262
|
+
const policy = readReviewPolicyFromEvents(events);
|
|
263
|
+
if (!policy) {
|
|
264
|
+
return {
|
|
265
|
+
state: "review",
|
|
266
|
+
path: "next_command",
|
|
267
|
+
next_command: transitionCommand(change, "review-ready", riskFlag(defaultRisk)),
|
|
268
|
+
reason: "缺少审查策略,先补 review-ready",
|
|
269
|
+
missing_inputs: [],
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
if (policy.requires_verifier) {
|
|
273
|
+
const currentEvidenceDigest = reviewEvidenceDigest(events);
|
|
274
|
+
const verifierAccepted = snapshot.accepted_jobs.find(job => isFreshReviewVerifier(job, changeRoot, currentEvidenceDigest));
|
|
275
|
+
if (!verifierAccepted) {
|
|
276
|
+
return {
|
|
277
|
+
state: "review",
|
|
278
|
+
path: "next_command",
|
|
279
|
+
next_command: transitionCommand(change, "review-ready", riskFlag(defaultRisk)),
|
|
280
|
+
reason: "缺少 fresh verifier,先补最终验证",
|
|
281
|
+
missing_inputs: [],
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
}
|
|
247
285
|
return {
|
|
248
286
|
state: "review",
|
|
249
287
|
path: "next_command",
|
package/dist/record.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
// SuperSpec 流程引擎 — record:工作项结果登记
|
|
2
2
|
import { readFileSync, existsSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
-
import { ensureChangeLayout, readEvents, appendEvent, makeEvent, sha256File, withLock, } from "./store.js";
|
|
4
|
+
import { ensureChangeLayout, readEvents, appendEvent, makeEvent, sha256File, withLock, appendRawRecord, } from "./store.js";
|
|
5
|
+
import { reviewEvidenceDigest, reviewVerifierStaleReason } from "./review.js";
|
|
5
6
|
const REVIEW_REPORT_REQUIRED_FIELDS = ["role", "verdict", "findings"];
|
|
6
7
|
const REVIEW_REPORT_OPTIONAL_FIELDS = ["summary", "evidence_refs", "risks", "open_questions"];
|
|
7
8
|
const REVIEWER_KINDS = new Set(["codex-subagent", "human", "external-agent"]);
|
|
@@ -97,6 +98,7 @@ export function recordJobSubmit(projectRoot, change, changeRoot, jobId, reportFi
|
|
|
97
98
|
const reportDigest = sha256File(reportFile) ?? "sha256:unknown";
|
|
98
99
|
// acceptance checks
|
|
99
100
|
const checks = [];
|
|
101
|
+
let parsedReport = null;
|
|
100
102
|
// 0. 报告格式和角色匹配(最小 JSON contract)
|
|
101
103
|
try {
|
|
102
104
|
const report = JSON.parse(reportContent);
|
|
@@ -104,7 +106,8 @@ export function recordJobSubmit(projectRoot, change, changeRoot, jobId, reportFi
|
|
|
104
106
|
checks.push("报告必须是 JSON object");
|
|
105
107
|
}
|
|
106
108
|
else {
|
|
107
|
-
|
|
109
|
+
parsedReport = report;
|
|
110
|
+
const obj = parsedReport;
|
|
108
111
|
for (const field of REVIEW_REPORT_REQUIRED_FIELDS) {
|
|
109
112
|
if (!(field in obj))
|
|
110
113
|
checks.push(`报告缺少必填字段 ${field}`);
|
|
@@ -149,6 +152,10 @@ export function recordJobSubmit(projectRoot, change, changeRoot, jobId, reportFi
|
|
|
149
152
|
checks.push(`绑定文件 ${bf.path} 已变化(${bf.sha} → ${currentSha})`);
|
|
150
153
|
}
|
|
151
154
|
}
|
|
155
|
+
const reviewStaleReason = reviewVerifierStaleReason(job, changeRoot, reviewEvidenceDigest(events));
|
|
156
|
+
if (reviewStaleReason && !checks.includes(reviewStaleReason)) {
|
|
157
|
+
checks.push(reviewStaleReason);
|
|
158
|
+
}
|
|
152
159
|
// 2. 报告格式基本校验(非空 JSON 或文本)
|
|
153
160
|
if (!reportContent.trim()) {
|
|
154
161
|
checks.push("报告内容为空");
|
|
@@ -170,11 +177,13 @@ export function recordJobSubmit(projectRoot, change, changeRoot, jobId, reportFi
|
|
|
170
177
|
};
|
|
171
178
|
}
|
|
172
179
|
// 接受
|
|
180
|
+
const rawRef = appendRawRecord(projectRoot, change, "review-reports", parsedReport);
|
|
173
181
|
const acceptEvent = makeEvent(change, "job_accepted", {
|
|
174
182
|
job_id: jobId,
|
|
175
183
|
role: job.role,
|
|
176
184
|
report_digest: reportDigest,
|
|
177
185
|
accepted_at: new Date().toISOString(),
|
|
186
|
+
...rawRef,
|
|
178
187
|
});
|
|
179
188
|
appendEvent(projectRoot, change, acceptEvent);
|
|
180
189
|
return {
|
|
@@ -189,6 +198,7 @@ export function recordJobSubmit(projectRoot, change, changeRoot, jobId, reportFi
|
|
|
189
198
|
export function recordUserDecision(projectRoot, change, inputFile) {
|
|
190
199
|
return withLock(projectRoot, change, () => {
|
|
191
200
|
ensureChangeLayout(projectRoot, change);
|
|
201
|
+
const events = readEvents(projectRoot, change);
|
|
192
202
|
if (!existsSync(inputFile)) {
|
|
193
203
|
appendEvent(projectRoot, change, makeEvent(change, "user_decision_recorded", { accepted: false, reason: "file_not_found", path: inputFile }));
|
|
194
204
|
return { event_type: "user_decision_recorded", accepted: false, message: `决策文件不存在:${inputFile}` };
|
|
@@ -206,11 +216,26 @@ export function recordUserDecision(projectRoot, change, inputFile) {
|
|
|
206
216
|
appendEvent(projectRoot, change, makeEvent(change, "user_decision_recorded", { accepted: false, reason: "missing_scope_or_answer" }));
|
|
207
217
|
return { event_type: "user_decision_recorded", accepted: false, message: "决策文件缺少 scope 或 answer" };
|
|
208
218
|
}
|
|
209
|
-
const
|
|
219
|
+
const inputDigest = sha256File(inputFile) ?? "sha256:unknown";
|
|
220
|
+
const existing = events.find(e => e.event_type === "user_decision_recorded"
|
|
221
|
+
&& e.payload.input_digest === inputDigest);
|
|
222
|
+
if (existing) {
|
|
223
|
+
return {
|
|
224
|
+
event_type: "user_decision_recorded",
|
|
225
|
+
accepted: true,
|
|
226
|
+
message: "幂等返回:同 user decision 已登记",
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
const normalizedDecision = {
|
|
210
230
|
scope: decision.scope,
|
|
211
231
|
question: decision.question ?? "",
|
|
212
232
|
answer: decision.answer,
|
|
213
|
-
|
|
233
|
+
};
|
|
234
|
+
const rawRef = appendRawRecord(projectRoot, change, "user-decisions", normalizedDecision);
|
|
235
|
+
const event = makeEvent(change, "user_decision_recorded", {
|
|
236
|
+
...normalizedDecision,
|
|
237
|
+
input_digest: inputDigest,
|
|
238
|
+
...rawRef,
|
|
214
239
|
});
|
|
215
240
|
appendEvent(projectRoot, change, event);
|
|
216
241
|
return {
|
|
@@ -268,11 +293,13 @@ export function jobsPacket(projectRoot, change, jobId) {
|
|
|
268
293
|
role: job.role,
|
|
269
294
|
recommended_agent: recommendedAgentForRole(job.role),
|
|
270
295
|
boundFiles: job.boundFiles,
|
|
296
|
+
...(job.review_evidence_digest ? { review_evidence_digest: job.review_evidence_digest } : {}),
|
|
271
297
|
packet_digest: job.packet_digest,
|
|
272
298
|
required_output_kind: "job_report_json",
|
|
273
299
|
output_contract_fields: requiresReviewer(job.role) ? [...REVIEW_REPORT_REQUIRED_FIELDS, "reviewer"] : [...REVIEW_REPORT_REQUIRED_FIELDS],
|
|
274
300
|
output_contract_optional_fields: [...REVIEW_REPORT_OPTIONAL_FIELDS],
|
|
275
301
|
output_instructions: `${roleDescription(job.role)}。请审查 ${job.boundFiles.map(f => f.path).join(", ")},` +
|
|
302
|
+
(job.review_evidence_digest ? `本工作项绑定的执行证据版本为 ${job.review_evidence_digest},` : "") +
|
|
276
303
|
(requiresReviewer(job.role) ? `必须由独立 ${recommendedAgentForRole(job.role)} reviewer 执行并在 reviewer.kind/id 中记录来源,` : "") +
|
|
277
304
|
`产出 JSON 报告文件并通过 superspec record job-submit 登记。` +
|
|
278
305
|
(requiresReviewer(job.role)
|
package/dist/review.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { Event, Job, Ref } from "./types.ts";
|
|
2
|
+
export type ReviewRisk = "minimal" | "normal" | "strict";
|
|
3
|
+
export interface ReviewPolicy {
|
|
4
|
+
review_risk: ReviewRisk;
|
|
5
|
+
requires_verifier: boolean;
|
|
6
|
+
}
|
|
7
|
+
export declare const REVIEW_DOC_PATHS: string[];
|
|
8
|
+
export declare function assertCommitPayloadExtension(payload: Record<string, unknown>): void;
|
|
9
|
+
export declare function reviewPolicyForRisk(risk: ReviewRisk): ReviewPolicy;
|
|
10
|
+
export declare function readReviewPolicyFromEvents(events: Event[]): ReviewPolicy | null;
|
|
11
|
+
export declare function isReviewReadyVerifier(job: Job): boolean;
|
|
12
|
+
export declare function reviewBoundFiles(changeRoot: string): Ref[];
|
|
13
|
+
export declare function reviewEvidenceDigest(events: Event[]): string;
|
|
14
|
+
export declare function boundFilesStaleReason(job: Job, changeRoot: string): string | null;
|
|
15
|
+
export declare function reviewEvidenceStaleReason(job: Job, currentDigest: string): string | null;
|
|
16
|
+
export declare function reviewVerifierStaleReason(job: Job, changeRoot: string, currentEvidenceDigest: string): string | null;
|
|
17
|
+
export declare function isFreshReviewVerifier(job: Job, changeRoot: string, currentEvidenceDigest: string): boolean;
|
package/dist/review.js
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
// SuperSpec review helpers: policy, verifier freshness, evidence digest
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { sha256File, sha256Text } from "./store.js";
|
|
5
|
+
export const REVIEW_DOC_PATHS = [
|
|
6
|
+
"proposal.md",
|
|
7
|
+
"tasks.md",
|
|
8
|
+
"design.md",
|
|
9
|
+
".superspec/artifacts/discovery.md",
|
|
10
|
+
".superspec/artifacts/business-invariants.md",
|
|
11
|
+
".superspec/artifacts/test-contract.md",
|
|
12
|
+
];
|
|
13
|
+
const CORE_COMMIT_FIELDS = new Set([
|
|
14
|
+
"transition",
|
|
15
|
+
"from_state",
|
|
16
|
+
"to_state",
|
|
17
|
+
"outcome",
|
|
18
|
+
"created_job_ids",
|
|
19
|
+
"new_jobs",
|
|
20
|
+
"reason",
|
|
21
|
+
]);
|
|
22
|
+
export function assertCommitPayloadExtension(payload) {
|
|
23
|
+
for (const key of Object.keys(payload)) {
|
|
24
|
+
if (CORE_COMMIT_FIELDS.has(key)) {
|
|
25
|
+
throw new Error(`commitPayload 不能覆盖核心字段:${key}`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
export function reviewPolicyForRisk(risk) {
|
|
30
|
+
return {
|
|
31
|
+
review_risk: risk,
|
|
32
|
+
requires_verifier: risk !== "minimal",
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
function isReviewPolicy(value) {
|
|
36
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
37
|
+
return false;
|
|
38
|
+
const obj = value;
|
|
39
|
+
return ((obj.review_risk === "minimal" || obj.review_risk === "normal" || obj.review_risk === "strict") &&
|
|
40
|
+
typeof obj.requires_verifier === "boolean");
|
|
41
|
+
}
|
|
42
|
+
export function readReviewPolicyFromEvents(events) {
|
|
43
|
+
for (const ev of events) {
|
|
44
|
+
if (ev.event_type !== "transition_commit")
|
|
45
|
+
continue;
|
|
46
|
+
const payload = ev.payload;
|
|
47
|
+
if (payload.transition !== "review-ready")
|
|
48
|
+
continue;
|
|
49
|
+
if (isReviewPolicy(payload.review_policy))
|
|
50
|
+
return payload.review_policy;
|
|
51
|
+
}
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
export function isReviewReadyVerifier(job) {
|
|
55
|
+
return job.role === "verifier" && job.created_from_transition === "review-ready";
|
|
56
|
+
}
|
|
57
|
+
export function reviewBoundFiles(changeRoot) {
|
|
58
|
+
return REVIEW_DOC_PATHS
|
|
59
|
+
.filter(path => existsSync(join(changeRoot, path)))
|
|
60
|
+
.map(path => ({ path, sha: sha256File(join(changeRoot, path)) ?? "sha256:missing" }));
|
|
61
|
+
}
|
|
62
|
+
function evidenceSortKey(record) {
|
|
63
|
+
return [
|
|
64
|
+
record.kind,
|
|
65
|
+
record.task_id ?? "",
|
|
66
|
+
record.attempt_id ?? "",
|
|
67
|
+
record.task_structure_digest ?? "",
|
|
68
|
+
record.test_id ?? "",
|
|
69
|
+
record.semantic_status ?? "",
|
|
70
|
+
record.command ?? "",
|
|
71
|
+
record.cwd ?? "",
|
|
72
|
+
record.exit_code == null ? "" : String(record.exit_code),
|
|
73
|
+
record.target_fingerprint ?? "",
|
|
74
|
+
record.event_digest,
|
|
75
|
+
].join("\u0000");
|
|
76
|
+
}
|
|
77
|
+
export function reviewEvidenceDigest(events) {
|
|
78
|
+
const attemptsById = new Map();
|
|
79
|
+
const completed = [];
|
|
80
|
+
for (const ev of events) {
|
|
81
|
+
if (ev.event_type === "task_started") {
|
|
82
|
+
const attempt = ev.payload;
|
|
83
|
+
attemptsById.set(attempt.attempt_id, attempt);
|
|
84
|
+
}
|
|
85
|
+
else if (ev.event_type === "task_completed") {
|
|
86
|
+
const payload = ev.payload;
|
|
87
|
+
if (typeof payload.task_id !== "string" || typeof payload.attempt_id !== "string")
|
|
88
|
+
continue;
|
|
89
|
+
const attempt = attemptsById.get(payload.attempt_id);
|
|
90
|
+
completed.push({
|
|
91
|
+
task_id: payload.task_id,
|
|
92
|
+
attempt_id: payload.attempt_id,
|
|
93
|
+
task_structure_digest: attempt?.task_structure_digest ?? null,
|
|
94
|
+
event_digest: ev.event_digest,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
const completedAttemptIds = new Set(completed.map(item => item.attempt_id));
|
|
99
|
+
const completedStructureDigests = new Set(completed
|
|
100
|
+
.map(item => item.task_structure_digest)
|
|
101
|
+
.filter((digest) => typeof digest === "string" && digest.length > 0));
|
|
102
|
+
const records = completed.map(item => ({
|
|
103
|
+
kind: "task_completed",
|
|
104
|
+
task_id: item.task_id,
|
|
105
|
+
attempt_id: item.attempt_id,
|
|
106
|
+
task_structure_digest: item.task_structure_digest,
|
|
107
|
+
event_digest: item.event_digest,
|
|
108
|
+
}));
|
|
109
|
+
for (const ev of events) {
|
|
110
|
+
if (ev.event_type !== "test_run_recorded")
|
|
111
|
+
continue;
|
|
112
|
+
const payload = ev.payload;
|
|
113
|
+
const attemptId = typeof payload.attempt_id === "string" ? payload.attempt_id : null;
|
|
114
|
+
const structureDigest = typeof payload.task_structure_digest === "string" ? payload.task_structure_digest : null;
|
|
115
|
+
const matchesCompletedAttempt = attemptId != null && completedAttemptIds.has(attemptId);
|
|
116
|
+
const matchesLegacyDigest = attemptId == null && structureDigest != null && completedStructureDigests.has(structureDigest);
|
|
117
|
+
if (!matchesCompletedAttempt && !matchesLegacyDigest)
|
|
118
|
+
continue;
|
|
119
|
+
records.push({
|
|
120
|
+
kind: "test_run_recorded",
|
|
121
|
+
test_id: typeof payload.test_id === "string" ? payload.test_id : null,
|
|
122
|
+
attempt_id: attemptId,
|
|
123
|
+
task_structure_digest: structureDigest,
|
|
124
|
+
semantic_status: typeof payload.semantic_status === "string" ? payload.semantic_status : null,
|
|
125
|
+
command: typeof payload.command === "string" ? payload.command : "",
|
|
126
|
+
cwd: typeof payload.cwd === "string" ? payload.cwd : "",
|
|
127
|
+
exit_code: typeof payload.exit_code === "number" ? payload.exit_code : null,
|
|
128
|
+
target_fingerprint: typeof payload.target_fingerprint === "string" ? payload.target_fingerprint : null,
|
|
129
|
+
event_digest: ev.event_digest,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
records.sort((a, b) => evidenceSortKey(a).localeCompare(evidenceSortKey(b)));
|
|
133
|
+
return sha256Text(JSON.stringify(records));
|
|
134
|
+
}
|
|
135
|
+
export function boundFilesStaleReason(job, changeRoot) {
|
|
136
|
+
for (const bf of job.boundFiles) {
|
|
137
|
+
const current = sha256File(join(changeRoot, bf.path)) ?? "sha256:missing";
|
|
138
|
+
if (current !== bf.sha) {
|
|
139
|
+
return `绑定文件 ${bf.path} 已变化(${bf.sha} → ${current})`;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
export function reviewEvidenceStaleReason(job, currentDigest) {
|
|
145
|
+
if (!isReviewReadyVerifier(job))
|
|
146
|
+
return null;
|
|
147
|
+
if (!job.review_evidence_digest)
|
|
148
|
+
return "最终验证工作项缺少执行证据版本";
|
|
149
|
+
if (job.review_evidence_digest !== currentDigest) {
|
|
150
|
+
return `最终验证执行证据版本已变化(${job.review_evidence_digest} → ${currentDigest})`;
|
|
151
|
+
}
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
export function reviewVerifierStaleReason(job, changeRoot, currentEvidenceDigest) {
|
|
155
|
+
return boundFilesStaleReason(job, changeRoot) ?? reviewEvidenceStaleReason(job, currentEvidenceDigest);
|
|
156
|
+
}
|
|
157
|
+
export function isFreshReviewVerifier(job, changeRoot, currentEvidenceDigest) {
|
|
158
|
+
return isReviewReadyVerifier(job) && reviewVerifierStaleReason(job, changeRoot, currentEvidenceDigest) == null;
|
|
159
|
+
}
|
package/dist/store.d.ts
CHANGED
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
import type { Event, Snapshot, Ref } from "./types.ts";
|
|
2
|
+
export declare const RAW_RECORD_KINDS: readonly ["test-runs", "review-reports", "user-decisions"];
|
|
3
|
+
export type RawRecordKind = typeof RAW_RECORD_KINDS[number];
|
|
4
|
+
export interface RawRecordRef {
|
|
5
|
+
raw_kind: RawRecordKind;
|
|
6
|
+
raw_index: number;
|
|
7
|
+
raw_digest: string;
|
|
8
|
+
}
|
|
2
9
|
export declare function engineRoot(projectRoot: string): string;
|
|
3
10
|
export declare function changeDir(projectRoot: string, change: string): string;
|
|
4
11
|
export declare function eventsFile(projectRoot: string, change: string): string;
|
|
12
|
+
export declare function rawFile(projectRoot: string, change: string, kind: RawRecordKind): string;
|
|
5
13
|
export declare function snapshotFile(projectRoot: string, change: string): string;
|
|
6
14
|
export declare function lockFile(projectRoot: string, change: string): string;
|
|
7
15
|
export declare function stagingDir(projectRoot: string, change: string, transitionId: string): string;
|
|
@@ -21,6 +29,13 @@ export declare function makeEvent(change: string, eventType: Event["event_type"]
|
|
|
21
29
|
outputRefs?: Ref[];
|
|
22
30
|
}): Event;
|
|
23
31
|
export declare function eventsDigest(events: Event[]): string;
|
|
32
|
+
/**
|
|
33
|
+
* Append an accepted record input to the raw archive.
|
|
34
|
+
*
|
|
35
|
+
* Contract: call only while holding the per-change lock. The returned raw_index
|
|
36
|
+
* is the zero-based valid-record index, not necessarily the physical line.
|
|
37
|
+
*/
|
|
38
|
+
export declare function appendRawRecord(projectRoot: string, change: string, kind: RawRecordKind, record: unknown): RawRecordRef;
|
|
24
39
|
export declare function writeSnapshot(projectRoot: string, change: string, snapshot: Snapshot): void;
|
|
25
40
|
export declare function readSnapshot(projectRoot: string, change: string): Snapshot | null;
|
|
26
41
|
export declare function snapshotDigest(snapshot: Snapshot): string;
|
package/dist/store.js
CHANGED
|
@@ -4,6 +4,7 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync, openSync, closeSync
|
|
|
4
4
|
import { join, dirname } from "node:path";
|
|
5
5
|
import { hostname } from "node:os";
|
|
6
6
|
// ===== 路径 =====
|
|
7
|
+
export const RAW_RECORD_KINDS = ["test-runs", "review-reports", "user-decisions"];
|
|
7
8
|
export function engineRoot(projectRoot) {
|
|
8
9
|
return join(projectRoot, ".superspec");
|
|
9
10
|
}
|
|
@@ -13,6 +14,9 @@ export function changeDir(projectRoot, change) {
|
|
|
13
14
|
export function eventsFile(projectRoot, change) {
|
|
14
15
|
return join(changeDir(projectRoot, change), "events.jsonl");
|
|
15
16
|
}
|
|
17
|
+
export function rawFile(projectRoot, change, kind) {
|
|
18
|
+
return join(changeDir(projectRoot, change), "raw", `${kind}.jsonl`);
|
|
19
|
+
}
|
|
16
20
|
export function snapshotFile(projectRoot, change) {
|
|
17
21
|
return join(changeDir(projectRoot, change), "snapshot.json");
|
|
18
22
|
}
|
|
@@ -109,6 +113,52 @@ export function makeEvent(change, eventType, payload, opts = {}) {
|
|
|
109
113
|
export function eventsDigest(events) {
|
|
110
114
|
return sha256Text(events.map(e => e.event_digest).join("\n"));
|
|
111
115
|
}
|
|
116
|
+
// ===== Raw 归档 =====
|
|
117
|
+
function countValidJsonlRecords(filePath) {
|
|
118
|
+
if (!existsSync(filePath))
|
|
119
|
+
return 0;
|
|
120
|
+
let count = 0;
|
|
121
|
+
for (const line of readFileSync(filePath, "utf8").split("\n")) {
|
|
122
|
+
if (!line.trim())
|
|
123
|
+
continue;
|
|
124
|
+
try {
|
|
125
|
+
JSON.parse(line);
|
|
126
|
+
count++;
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
// raw JSONL 与 events.jsonl 一样容忍截断/损坏行;raw_index 按有效记录计数。
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return count;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Append an accepted record input to the raw archive.
|
|
136
|
+
*
|
|
137
|
+
* Contract: call only while holding the per-change lock. The returned raw_index
|
|
138
|
+
* is the zero-based valid-record index, not necessarily the physical line.
|
|
139
|
+
*/
|
|
140
|
+
export function appendRawRecord(projectRoot, change, kind, record) {
|
|
141
|
+
if (!RAW_RECORD_KINDS.includes(kind)) {
|
|
142
|
+
throw new Error(`Unsupported raw record kind: ${kind}`);
|
|
143
|
+
}
|
|
144
|
+
const rf = rawFile(projectRoot, change, kind);
|
|
145
|
+
mkdirSync(dirname(rf), { recursive: true });
|
|
146
|
+
const rawIndex = countValidJsonlRecords(rf);
|
|
147
|
+
const line = JSON.stringify(record);
|
|
148
|
+
const rawDigest = sha256Text(line);
|
|
149
|
+
const fd = openSync(rf, "a");
|
|
150
|
+
try {
|
|
151
|
+
writeFileSync(fd, line + "\n", "utf8");
|
|
152
|
+
}
|
|
153
|
+
finally {
|
|
154
|
+
closeSync(fd);
|
|
155
|
+
}
|
|
156
|
+
return {
|
|
157
|
+
raw_kind: kind,
|
|
158
|
+
raw_index: rawIndex,
|
|
159
|
+
raw_digest: rawDigest,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
112
162
|
// ===== 快照 =====
|
|
113
163
|
export function writeSnapshot(projectRoot, change, snapshot) {
|
|
114
164
|
writeFileSync(snapshotFile(projectRoot, change), JSON.stringify(snapshot, null, 2) + "\n", "utf8");
|
package/dist/sync.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
// SuperSpec 流程引擎 — sync:从当前文档 + events.jsonl 重建 snapshot
|
|
2
2
|
import { readFileSync, existsSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
-
import { readEvents, eventsDigest, computeDocumentDigests,
|
|
4
|
+
import { readEvents, eventsDigest, computeDocumentDigests, sha256Text, ensureChangeLayout, } from "./store.js";
|
|
5
|
+
import { reviewEvidenceDigest, reviewVerifierStaleReason } from "./review.js";
|
|
5
6
|
const TRACKED_DOCS = [
|
|
6
7
|
"proposal.md", "design.md", "tasks.md",
|
|
7
8
|
".superspec/artifacts/discovery.md",
|
|
@@ -79,18 +80,12 @@ function replayEvents(events) {
|
|
|
79
80
|
return { state, openJobs, acceptedJobs, activeAttempts, taskStatuses, lastTransition };
|
|
80
81
|
}
|
|
81
82
|
/** 粗粒度失效:检查 job 的 boundFiles 是否仍匹配当前文档 */
|
|
82
|
-
function checkStaleJobs(jobs, changeRoot) {
|
|
83
|
+
function checkStaleJobs(jobs, changeRoot, currentReviewEvidenceDigest) {
|
|
83
84
|
const stale = [];
|
|
84
85
|
for (const job of jobs) {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
stale.push({
|
|
89
|
-
job_id: job.job_id,
|
|
90
|
-
reason: `绑定文件 ${bf.path} 已变化(${bf.sha} → ${current})`,
|
|
91
|
-
});
|
|
92
|
-
break; // 一个文件变就够了
|
|
93
|
-
}
|
|
86
|
+
const reason = reviewVerifierStaleReason(job, changeRoot, currentReviewEvidenceDigest);
|
|
87
|
+
if (reason) {
|
|
88
|
+
stale.push({ job_id: job.job_id, reason });
|
|
94
89
|
}
|
|
95
90
|
}
|
|
96
91
|
return stale;
|
|
@@ -113,9 +108,10 @@ export function rebuildSnapshot(projectRoot, change, changeRoot, openspecStatusD
|
|
|
113
108
|
const documentDigests = computeDocumentDigests(changeRoot, TRACKED_DOCS);
|
|
114
109
|
const tsDigest = tasksStructureDigest(changeRoot);
|
|
115
110
|
const { state, openJobs, acceptedJobs, activeAttempts, taskStatuses, lastTransition } = replayEvents(events);
|
|
111
|
+
const currentReviewEvidenceDigest = reviewEvidenceDigest(events);
|
|
116
112
|
// 粗粒度失效检查(只读,不写事件):snapshot 只暴露当前可执行/可复用 job。
|
|
117
|
-
const staleOpenInfo = checkStaleJobs(openJobs, changeRoot);
|
|
118
|
-
const staleAcceptedInfo = checkStaleJobs(acceptedJobs, changeRoot);
|
|
113
|
+
const staleOpenInfo = checkStaleJobs(openJobs, changeRoot, currentReviewEvidenceDigest);
|
|
114
|
+
const staleAcceptedInfo = checkStaleJobs(acceptedJobs, changeRoot, currentReviewEvidenceDigest);
|
|
119
115
|
const freshOpen = openJobs.filter(j => !staleOpenInfo.some(s => s.job_id === j.job_id));
|
|
120
116
|
const freshAccepted = acceptedJobs.filter(j => !staleAcceptedInfo.some(s => s.job_id === j.job_id));
|
|
121
117
|
return {
|
package/dist/task.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// SuperSpec 流程引擎 — task:测试运行记录 + 任务结构指纹工具
|
|
2
2
|
import { existsSync, readFileSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
-
import { sha256Text, ensureChangeLayout, appendEvent, makeEvent, withLock } from "./store.js";
|
|
4
|
+
import { sha256Text, ensureChangeLayout, appendEvent, makeEvent, withLock, appendRawRecord } from "./store.js";
|
|
5
5
|
import { tasksStructureDigest as formatDigest } from "./format.js";
|
|
6
6
|
/** tasks.md 结构指纹(委托给 format.ts 统一实现) */
|
|
7
7
|
export function tasksStructureDigestOf(changeRoot) {
|
|
@@ -27,7 +27,7 @@ export function recordTestRun(projectRoot, change, inputFile) {
|
|
|
27
27
|
if (!tr.test_id || !tr.task_structure_digest) {
|
|
28
28
|
return { accepted: false, message: "缺少 test_id 或 task_structure_digest" };
|
|
29
29
|
}
|
|
30
|
-
const
|
|
30
|
+
const normalizedTestRun = {
|
|
31
31
|
test_id: tr.test_id,
|
|
32
32
|
task_structure_digest: tr.task_structure_digest,
|
|
33
33
|
attempt_id: tr.attempt_id ?? null,
|
|
@@ -37,6 +37,11 @@ export function recordTestRun(projectRoot, change, inputFile) {
|
|
|
37
37
|
semantic_status: tr.semantic_status ?? "unknown",
|
|
38
38
|
target_fingerprint: tr.target_fingerprint ?? null,
|
|
39
39
|
raw_log_ref: tr.raw_log_ref ?? null,
|
|
40
|
+
};
|
|
41
|
+
const rawRef = appendRawRecord(projectRoot, change, "test-runs", normalizedTestRun);
|
|
42
|
+
const event = makeEvent(change, "test_run_recorded", {
|
|
43
|
+
...normalizedTestRun,
|
|
44
|
+
...rawRef,
|
|
40
45
|
});
|
|
41
46
|
appendEvent(projectRoot, change, event);
|
|
42
47
|
return { accepted: true, message: `测试运行已登记:test_id=${tr.test_id}` };
|
package/dist/transition.d.ts
CHANGED
package/dist/transition.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
// SuperSpec 流程引擎 — transition:提交协议 + 所有 transition 处理器
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
-
import { existsSync, readFileSync, writeFileSync,
|
|
4
|
-
import { ensureChangeLayout, readEvents, appendEvent, makeEvent, writeSnapshot, snapshotDigest, withLock, idempotencyKey,
|
|
3
|
+
import { existsSync, readFileSync, writeFileSync, readdirSync } from "node:fs";
|
|
4
|
+
import { ensureChangeLayout, readEvents, appendEvent, makeEvent, writeSnapshot, snapshotDigest, withLock, idempotencyKey, sha256File, sha256Text, } from "./store.js";
|
|
5
5
|
import { rebuildSnapshot } from "./sync.js";
|
|
6
|
+
import { assertCommitPayloadExtension, isFreshReviewVerifier, isReviewReadyVerifier, readReviewPolicyFromEvents, reviewBoundFiles, reviewEvidenceDigest, reviewPolicyForRisk, } from "./review.js";
|
|
6
7
|
import { validateDiscovery, collectProposeOpenQuestions, findTaskInLines, parseTasksMd, pendingTasksInContent, tasksStructureDigest } from "./format.js";
|
|
7
8
|
let transitionSeq = 0;
|
|
8
9
|
function newTransitionId() { return `T-${Date.now()}-${++transitionSeq}`; }
|
|
@@ -85,6 +86,20 @@ function pendingTaskIds(changeRoot) {
|
|
|
85
86
|
function formatPendingTaskMessage(ids, action) {
|
|
86
87
|
return `尚有未完成任务:${ids.join(", ")};${action}`;
|
|
87
88
|
}
|
|
89
|
+
function hasRejectedReviewReadyVerifier(events) {
|
|
90
|
+
const reviewReadyVerifierIds = new Set();
|
|
91
|
+
for (const ev of events) {
|
|
92
|
+
if (ev.event_type !== "transition_commit")
|
|
93
|
+
continue;
|
|
94
|
+
const newJobs = ev.payload.new_jobs ?? [];
|
|
95
|
+
for (const job of newJobs) {
|
|
96
|
+
if (isReviewReadyVerifier(job))
|
|
97
|
+
reviewReadyVerifierIds.add(job.job_id);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return events.some(ev => ev.event_type === "job_rejected" &&
|
|
101
|
+
reviewReadyVerifierIds.has(ev.payload.job_id ?? ""));
|
|
102
|
+
}
|
|
88
103
|
/**
|
|
89
104
|
* 统一 transition 提交协议——所有校验在锁内。
|
|
90
105
|
*/
|
|
@@ -113,7 +128,8 @@ export function commitTransition(projectRoot, change, changeRoot, opts) {
|
|
|
113
128
|
created_jobs: [], message: decision.message, events_written: 0,
|
|
114
129
|
};
|
|
115
130
|
}
|
|
116
|
-
const { fromState, toState, outcome, newJobs = [], reason, extraEvents = [], details } = decision;
|
|
131
|
+
const { fromState, toState, outcome, newJobs = [], reason, commitPayload = {}, extraEvents = [], details } = decision;
|
|
132
|
+
assertCommitPayloadExtension(commitPayload);
|
|
117
133
|
if (fromState !== snapshot.state) {
|
|
118
134
|
return {
|
|
119
135
|
transition: name, outcome: "advanced",
|
|
@@ -137,6 +153,7 @@ export function commitTransition(projectRoot, change, changeRoot, opts) {
|
|
|
137
153
|
appendEvent(projectRoot, change, makeEvent(change, "transition_commit", {
|
|
138
154
|
transition: name, from_state: fromState, to_state: toState,
|
|
139
155
|
outcome, created_job_ids: newJobs.map(j => j.job_id), new_jobs: newJobs, reason,
|
|
156
|
+
...commitPayload,
|
|
140
157
|
}, { transitionId, idempotencyKey: idemKey, prevSnapshotDigest: worldDigest }));
|
|
141
158
|
// extra events (task_started, task_completed, etc.)
|
|
142
159
|
for (const ex of extraEvents) {
|
|
@@ -311,35 +328,86 @@ export function reviewReady(projectRoot, change, changeRoot, risk = "strict") {
|
|
|
311
328
|
return commitTransition(projectRoot, change, changeRoot, {
|
|
312
329
|
name: "review-ready", idempotencyInputs: { phase: "review-ready", risk },
|
|
313
330
|
decide: (snapshot) => {
|
|
331
|
+
const events = readEvents(projectRoot, change);
|
|
332
|
+
const storedPolicy = readReviewPolicyFromEvents(events);
|
|
333
|
+
const policy = storedPolicy ?? reviewPolicyForRisk(risk);
|
|
334
|
+
const policyPayload = storedPolicy ? {} : { review_policy: policy };
|
|
335
|
+
const currentEvidenceDigest = reviewEvidenceDigest(events);
|
|
314
336
|
// 检查是否所有任务已完成
|
|
315
337
|
const pending = pendingTaskIds(changeRoot);
|
|
316
338
|
if (pending.length > 0)
|
|
317
339
|
return { skip: true, message: formatPendingTaskMessage(pending, "请先通过 next/reopen 继续执行") };
|
|
318
340
|
// 如果当前是 apply,先推进到 apply_done
|
|
319
341
|
if (snapshot.state === "apply") {
|
|
320
|
-
return {
|
|
342
|
+
return {
|
|
343
|
+
fromState: "apply", toState: "apply_done", outcome: "advanced",
|
|
344
|
+
reason: `所有任务完成;审查策略=${policy.review_risk}`,
|
|
345
|
+
commitPayload: policyPayload,
|
|
346
|
+
};
|
|
321
347
|
}
|
|
322
|
-
if (snapshot.state === "apply_done") {
|
|
323
|
-
const isReviewReadyVerifier = (job) => job.role === "verifier" && job.created_from_transition === "review-ready";
|
|
348
|
+
if (snapshot.state === "apply_done" || snapshot.state === "review") {
|
|
324
349
|
const verifierOpen = snapshot.open_jobs.find(isReviewReadyVerifier);
|
|
325
350
|
if (verifierOpen)
|
|
326
351
|
return { skip: true, message: `有待完成的最终验证工作项 ${verifierOpen.job_id}` };
|
|
327
|
-
const verifierAccepted = snapshot.accepted_jobs.find(
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
352
|
+
const verifierAccepted = snapshot.accepted_jobs.find(job => isFreshReviewVerifier(job, changeRoot, currentEvidenceDigest));
|
|
353
|
+
if (!policy.requires_verifier) {
|
|
354
|
+
if (snapshot.state === "apply_done") {
|
|
355
|
+
return {
|
|
356
|
+
fromState: "apply_done", toState: "review", outcome: "advanced",
|
|
357
|
+
reason: `审查策略=${policy.review_risk},无需最终验证,进入审查阶段`,
|
|
358
|
+
commitPayload: policyPayload,
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
if (!storedPolicy) {
|
|
362
|
+
return {
|
|
363
|
+
fromState: "review", toState: "review", outcome: "advanced",
|
|
364
|
+
reason: `补写审查策略=${policy.review_risk}`,
|
|
365
|
+
commitPayload: policyPayload,
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
return { skip: true, message: "已在 review 状态,审查策略无需 verifier" };
|
|
369
|
+
}
|
|
370
|
+
if (!verifierAccepted) {
|
|
371
|
+
const boundFiles = reviewBoundFiles(changeRoot);
|
|
372
|
+
const previousVerifierRejected = hasRejectedReviewReadyVerifier(events);
|
|
332
373
|
const job = {
|
|
333
374
|
job_id: newJobId(change, "verifier"), role: "verifier", state: "requested",
|
|
334
|
-
boundFiles,
|
|
375
|
+
boundFiles, review_evidence_digest: currentEvidenceDigest,
|
|
376
|
+
packet_digest: sha256Text(JSON.stringify({
|
|
377
|
+
role: "verifier",
|
|
378
|
+
boundFiles,
|
|
379
|
+
review_evidence_digest: currentEvidenceDigest,
|
|
380
|
+
created_from_transition: "review-ready",
|
|
381
|
+
})),
|
|
335
382
|
created_from_transition: "review-ready", created_at: new Date().toISOString(),
|
|
336
383
|
};
|
|
337
384
|
return {
|
|
338
|
-
fromState:
|
|
339
|
-
newJobs: [job],
|
|
385
|
+
fromState: snapshot.state, toState: snapshot.state, outcome: "job_created",
|
|
386
|
+
newJobs: [job],
|
|
387
|
+
reason: previousVerifierRejected
|
|
388
|
+
? "此前 verifier 未通过;请先根据 findings 修改任务或文档,确认无需修改时再执行新的最终验证工作项"
|
|
389
|
+
: "创建最终验证工作项",
|
|
390
|
+
commitPayload: policyPayload,
|
|
391
|
+
...(previousVerifierRejected ? {
|
|
392
|
+
details: { advisory: "此前 verifier 未通过;请先根据 findings 修改任务或文档,确认无需修改时再执行新的最终验证工作项" },
|
|
393
|
+
} : {}),
|
|
340
394
|
};
|
|
341
395
|
}
|
|
342
|
-
|
|
396
|
+
if (snapshot.state === "apply_done") {
|
|
397
|
+
return {
|
|
398
|
+
fromState: "apply_done", toState: "review", outcome: "advanced",
|
|
399
|
+
reason: "最终验证已接受,进入审查阶段",
|
|
400
|
+
commitPayload: policyPayload,
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
if (!storedPolicy) {
|
|
404
|
+
return {
|
|
405
|
+
fromState: "review", toState: "review", outcome: "advanced",
|
|
406
|
+
reason: `补写审查策略=${policy.review_risk}`,
|
|
407
|
+
commitPayload: policyPayload,
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
return { skip: true, message: "已在 review 状态,最终验证仍然有效" };
|
|
343
411
|
}
|
|
344
412
|
return { skip: true, message: `当前状态 ${snapshot.state},review-ready 不适用` };
|
|
345
413
|
},
|
|
@@ -355,6 +423,16 @@ export function accept(projectRoot, change, changeRoot) {
|
|
|
355
423
|
const pending = pendingTaskIds(changeRoot);
|
|
356
424
|
if (pending.length > 0)
|
|
357
425
|
return { skip: true, message: formatPendingTaskMessage(pending, "请先 reopen --to apply 继续执行") };
|
|
426
|
+
const events = readEvents(projectRoot, change);
|
|
427
|
+
const policy = readReviewPolicyFromEvents(events);
|
|
428
|
+
if (!policy)
|
|
429
|
+
return { skip: true, message: "缺少审查策略,请先运行 review-ready" };
|
|
430
|
+
if (policy.requires_verifier) {
|
|
431
|
+
const currentEvidenceDigest = reviewEvidenceDigest(events);
|
|
432
|
+
const verifierAccepted = snapshot.accepted_jobs.find(job => isFreshReviewVerifier(job, changeRoot, currentEvidenceDigest));
|
|
433
|
+
if (!verifierAccepted)
|
|
434
|
+
return { skip: true, message: "缺少 fresh verifier,请先运行 review-ready" };
|
|
435
|
+
}
|
|
358
436
|
return { fromState: "review", toState: "accepted", outcome: "advanced", reason: "审查通过" };
|
|
359
437
|
},
|
|
360
438
|
});
|
package/dist/types.d.ts
CHANGED
|
@@ -11,6 +11,7 @@ export interface Job {
|
|
|
11
11
|
role: JobRole;
|
|
12
12
|
state: JobState;
|
|
13
13
|
boundFiles: Ref[];
|
|
14
|
+
review_evidence_digest?: string;
|
|
14
15
|
packet_digest: string;
|
|
15
16
|
created_from_transition: string;
|
|
16
17
|
created_at: string;
|
|
@@ -20,6 +21,7 @@ export interface JobPacket {
|
|
|
20
21
|
role: JobRole;
|
|
21
22
|
recommended_agent?: string;
|
|
22
23
|
boundFiles: Ref[];
|
|
24
|
+
review_evidence_digest?: string;
|
|
23
25
|
packet_digest: string;
|
|
24
26
|
required_output_kind: string;
|
|
25
27
|
output_contract_fields?: string[];
|
|
@@ -48,7 +50,12 @@ export interface TransitionCommitPayload {
|
|
|
48
50
|
to_state: State;
|
|
49
51
|
outcome: "advanced" | "job_created";
|
|
50
52
|
created_job_ids: string[];
|
|
53
|
+
new_jobs?: Job[];
|
|
51
54
|
reason: string;
|
|
55
|
+
review_policy?: {
|
|
56
|
+
review_risk: "minimal" | "normal" | "strict";
|
|
57
|
+
requires_verifier: boolean;
|
|
58
|
+
};
|
|
52
59
|
}
|
|
53
60
|
export interface Snapshot {
|
|
54
61
|
change_id: string;
|
package/package.json
CHANGED
|
@@ -1,14 +1,25 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@peterxiaoyang/superspec",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.24",
|
|
4
4
|
"description": "SuperSpec 流程引擎 — transition engine with lightweight fact-sync",
|
|
5
5
|
"type": "module",
|
|
6
|
-
"engines": {
|
|
7
|
-
|
|
6
|
+
"engines": {
|
|
7
|
+
"node": ">=20.19.0"
|
|
8
|
+
},
|
|
9
|
+
"bin": {
|
|
10
|
+
"superspec": "dist/cli.js"
|
|
11
|
+
},
|
|
8
12
|
"exports": {
|
|
9
|
-
".": {
|
|
13
|
+
".": {
|
|
14
|
+
"types": "./dist/types.d.ts",
|
|
15
|
+
"default": "./dist/cli.js"
|
|
16
|
+
}
|
|
10
17
|
},
|
|
11
|
-
"files": [
|
|
18
|
+
"files": [
|
|
19
|
+
"README.md",
|
|
20
|
+
"dist",
|
|
21
|
+
"templates"
|
|
22
|
+
],
|
|
12
23
|
"scripts": {
|
|
13
24
|
"build": "node build.js",
|
|
14
25
|
"typecheck": "tsc --noEmit",
|
|
@@ -36,6 +36,17 @@ argument-hint: "本次架构审查说明"
|
|
|
36
36
|
|
|
37
37
|
`role`、`verdict`、`findings`、`reviewer` 是必填字段。`reviewer.kind` 必须是 `codex-subagent`、`human` 或 `external-agent`,`reviewer.id` 必须能指向实际审查来源。发现阻塞架构问题时必须使用 `verdict:"fail"`。
|
|
38
38
|
|
|
39
|
+
## 计划 / 设计审查口径
|
|
40
|
+
|
|
41
|
+
审查计划文档时,重点判断影响范围、技术决策和任务拆分是否能支撑后续实现,不要把文档格式本身当成目标。
|
|
42
|
+
|
|
43
|
+
- `proposal.md` 的 `## Impact` 应通过 `Area` / `Reason` 说明受影响区域和原因;如果只有泛目录、没有原因或把 `Area` 当路径白名单,应提出阻塞或风险
|
|
44
|
+
- `design.md` 应记录关键决策、替代方案和风险取舍;如果只是复制影响范围、任务清单或实现步骤,说明设计边界不清
|
|
45
|
+
- `tasks.md` 可以用 Markdown 标题分组,但可执行边界必须落到顶格 checkbox 叶子 task
|
|
46
|
+
- 任务分组应贴合系统边界;高风险模块、跨入口行为或难以 review 的大改动,应要求拆成可独立验证的 task
|
|
47
|
+
- 如果分组标题、task id 或任务文本会让执行者容易启动错任务,应使用 `verdict:"fail"`
|
|
48
|
+
- 不要为了弥补拆分不清而要求新增父子任务状态、额外设计字段或 tasks 反向引用 design;先要求更清楚的分组和叶子 task
|
|
49
|
+
|
|
39
50
|
## 输出风格
|
|
40
51
|
|
|
41
52
|
- 所有用户可见输出必须使用简体中文。
|
|
@@ -37,7 +37,43 @@ argument-hint: "本次反方审查说明"
|
|
|
37
37
|
|
|
38
38
|
`role`、`verdict`、`findings`、`reviewer` 是必填字段。`reviewer.kind` 必须是 `codex-subagent`、`human` 或 `external-agent`,`reviewer.id` 必须能指向实际审查来源。发现阻塞问题时必须使用 `verdict:"fail"`,并在 `findings` 中给出证据和修复建议。
|
|
39
39
|
|
|
40
|
-
当你在 `review_complete`
|
|
40
|
+
当你在 `review_complete` 中承担验证职责时,必须确认本次任务说明要求输出验证意见;否则只输出 source guidance。
|
|
41
|
+
|
|
42
|
+
## Discovery 审查口径
|
|
43
|
+
|
|
44
|
+
当审查 explore 阶段的 discovery 时,判断它是否足以支撑进入 propose。不要接管设计,不要替主流程选方案。
|
|
45
|
+
|
|
46
|
+
最小通过条件:
|
|
47
|
+
|
|
48
|
+
- 代码影响型需求必须包含 repo source anchors;纯文档、配置或新文件任务没有代码锚点时,必须说明 `N/A` 理由并引用相关文档、配置或需求来源。
|
|
49
|
+
- `当前代码事实` 必须能说明当前实现怎么工作,而不是泛泛复述需求。
|
|
50
|
+
- `需求理解` 必须说明用户目标和当前实现之间的差异。
|
|
51
|
+
- `影响范围候选` 中每个主要候选应有至少一个 `path:line` 或等价文档锚点;无法验证时必须标明不确定性。
|
|
52
|
+
- 风险必须绑定具体代码、行为、数据或文档事实。
|
|
53
|
+
- 未验证假设、会影响范围或验收的问题必须进入 `## 待确认问题`,或明确说明为什么非阻塞。
|
|
54
|
+
|
|
55
|
+
代码影响型 discovery 缺少事实锚点、需求理解与当前实现脱节、或把未验证假设当成事实时,使用 `verdict:"fail"`。
|
|
56
|
+
|
|
57
|
+
## Propose 审查口径
|
|
58
|
+
|
|
59
|
+
审查 propose 阶段计划时,重点挑战影响范围、原因和任务计划是否会让 apply 跑偏。
|
|
60
|
+
|
|
61
|
+
阻塞条件:
|
|
62
|
+
|
|
63
|
+
- `proposal.md` 缺少 `## Impact`
|
|
64
|
+
- `## Impact` 没有说明 `Area` / `Reason`
|
|
65
|
+
- `Area` 只有泛目录,且没有原因或不确定性说明
|
|
66
|
+
- `Reason` 只写“要改这里”,没有解释为什么受影响
|
|
67
|
+
- `## Impact` 写成任务清单或路径白名单
|
|
68
|
+
- `design.md` 把影响范围表、任务拆分或实现清单复制进去,导致技术决策不清
|
|
69
|
+
- `tasks.md` 的任务拆分过粗,把多个独立行为放进同一个执行单元,导致 apply 难以用一组清晰的 RED/GREEN 证据验收
|
|
70
|
+
- task id 重复、不稳定,或分组标题混入 task id,导致后续执行命令容易指错任务
|
|
71
|
+
- 普通说明或缩进 checkbox 承载了实际未完成工作,导致工作流无法自然推进
|
|
72
|
+
- task 中写入 RED/GREEN 命令、断言或预期输出,导致任务计划和实际执行证据混在一起
|
|
73
|
+
|
|
74
|
+
发现这些问题时使用 `verdict:"fail"`,并给出最小拆分或补充建议。
|
|
75
|
+
|
|
76
|
+
负例:一个 task 同时要求修改运行时行为、发布流程和文档,并且这些改动不能由同一组测试证据验收,应要求拆分;普通说明里出现 `TODO` / `follow-up` / “后续补”,但没有对应顶格 task,应使用 `verdict:"fail"`。
|
|
41
77
|
|
|
42
78
|
## 输出风格
|
|
43
79
|
|
|
@@ -15,11 +15,24 @@ argument-hint: "本次探索说明"
|
|
|
15
15
|
- 优先使用 repo search 和文件读取验证事实,结论必须绑定可读源码或文档锚点。
|
|
16
16
|
- 不要写 `proposal.md`/`design.md`/`tasks.md`/`specs/**`/`.superspec/**`。
|
|
17
17
|
- 不能作为 `explore_complete` 的 role evidence;strict 风险模式需要门禁审查时交给 `critic`。
|
|
18
|
+
- 当作为 explore subagent 深扫时,只输出事实、文件行号锚点、隐性约束、影响范围候选、风险和需要主流程确认的问题;不要输出实现方案,不要替主流程做取舍。
|
|
19
|
+
- “影响范围候选”只描述现有代码表面、相邻模块和潜在风险,不写具体实现步骤。
|
|
18
20
|
|
|
19
21
|
## 本次任务说明
|
|
20
22
|
|
|
21
23
|
如果主流程提供本次任务说明,先读取其中指向的 refs。以本次任务说明中的目标范围、来源 refs、必读 refs、artifact refs 和停止条件为准;不要依赖本 prompt 记忆输出 schema。
|
|
22
24
|
|
|
25
|
+
## 深扫输出要求
|
|
26
|
+
|
|
27
|
+
代码影响型需求必须尽量提供 `path:line` 形式的 repo source anchors。纯文档、配置或新文件任务没有代码锚点时,明确写出 `N/A` 理由,并引用相关文档、配置或需求来源。
|
|
28
|
+
|
|
29
|
+
输出至少区分:
|
|
30
|
+
|
|
31
|
+
- 已确认事实
|
|
32
|
+
- 影响范围候选
|
|
33
|
+
- 风险和隐性约束
|
|
34
|
+
- 需要主流程确认的问题
|
|
35
|
+
|
|
23
36
|
## 输出风格
|
|
24
37
|
|
|
25
38
|
- 所有用户可见输出必须使用简体中文。
|
|
@@ -7,18 +7,18 @@ argument-hint: "本次测试审查说明"
|
|
|
7
7
|
|
|
8
8
|
## 角色身份
|
|
9
9
|
|
|
10
|
-
你是 Test Engineer。你审查测试策略、覆盖充分性、RED/GREEN 可信度、脆弱测试风险和验收场景映射。普通测试任务中可以编写测试;在 SuperSpec review/propose
|
|
10
|
+
你是 Test Engineer。你审查测试策略、覆盖充分性、RED/GREEN 可信度、脆弱测试风险和验收场景映射。普通测试任务中可以编写测试;在 SuperSpec review/propose 阶段中只提供 guidance,不直接改 artifact。
|
|
11
11
|
|
|
12
12
|
## 读写边界
|
|
13
13
|
|
|
14
|
-
- SuperSpec review/propose
|
|
14
|
+
- SuperSpec review/propose 阶段默认只读;不要修改方案、测试契约或实现。
|
|
15
15
|
- 普通测试实现任务中,只写测试,不写业务实现;需要实现改动时向主流程说明。
|
|
16
|
-
- Apply 阶段如需新增或修改 RED/characterization
|
|
16
|
+
- Apply 阶段如需新增或修改 RED/characterization 测试文件,只在主流程明确交付的有界测试任务内写测试;正式 RED/characterization/GREEN 运行证据仍由 test-runner 的本次测试说明生成。
|
|
17
17
|
- 必须核对现有测试模式和目标 acceptance,不用臆测替代证据。
|
|
18
18
|
|
|
19
19
|
## 本次任务说明
|
|
20
20
|
|
|
21
|
-
在 SuperSpec review/propose
|
|
21
|
+
在 SuperSpec review/propose 阶段中,先读取主流程提供的本次任务说明。以本次任务说明中的审查范围、绑定文件、输出格式、字段要求和停止条件为准;不要依赖本 prompt 记忆输出 schema。
|
|
22
22
|
|
|
23
23
|
当本次任务说明要求提交 `job_report_json` 报告时,提交给 `superspec record job-submit` 的报告文件必须是 JSON:
|
|
24
24
|
|
|
@@ -37,6 +37,16 @@ argument-hint: "本次测试审查说明"
|
|
|
37
37
|
|
|
38
38
|
`role`、`verdict`、`findings`、`reviewer` 是必填字段。`reviewer.kind` 必须是 `codex-subagent`、`human` 或 `external-agent`,`reviewer.id` 必须能指向实际审查来源。测试契约、覆盖策略或验证路径不足时必须使用 `verdict:"fail"`。
|
|
39
39
|
|
|
40
|
+
## 任务拆分与 RED/GREEN 审查口径
|
|
41
|
+
|
|
42
|
+
在 propose 或 review 阶段审查 `tasks.md` 时:
|
|
43
|
+
|
|
44
|
+
- TDD task 应能形成清晰 RED/GREEN 闭环,但 RED/GREEN 命令、断言或预期输出不应写进 `tasks.md`
|
|
45
|
+
- `tasks.md` 只声明任务边界和 `tdd_required:true/false`;实际 RED/GREEN 细节属于 apply 阶段的 `record test-run` 证据
|
|
46
|
+
- 无法定义目标测试身份、RED 失败信号、GREEN 覆盖映射,或只靠退出码/笼统命令证明的测试方案,应使用 `verdict:"fail"`
|
|
47
|
+
- `tdd_required:false` 必须有明确 `no_tdd_reason`
|
|
48
|
+
- 不要求建立新的 test-contract 关联,也不要求把 RED/GREEN 细节塞回 task 行
|
|
49
|
+
|
|
40
50
|
## 输出风格
|
|
41
51
|
|
|
42
52
|
- 所有用户可见输出必须使用简体中文。
|
|
@@ -35,7 +35,7 @@ argument-hint: "本次验证说明"
|
|
|
35
35
|
|
|
36
36
|
`role`、`verdict`、`findings` 是必填字段。`verdict` 只能是 `pass` 或 `fail`。任务未完成、测试证据缺失、文档与实现状态不一致、绑定文件无法核对时输出 `verdict:"fail"`。
|
|
37
37
|
|
|
38
|
-
`superspec-review`
|
|
38
|
+
`superspec-review` 验证环节先读主流程提供的本次验证说明;以本次任务说明中的引用范围、输出格式、字段要求和停止条件为准;不要依赖本 prompt 记忆输出 schema。
|
|
39
39
|
|
|
40
40
|
确认本次任务说明要求输出 verification review 后,再输出 verification review。
|
|
41
41
|
|
|
@@ -45,6 +45,18 @@ apply worker report 字段以本次任务说明中的 `verifier_report_required_
|
|
|
45
45
|
|
|
46
46
|
遵守本次任务说明中的报告策略:长日志、完整 diff、编译输出和大段生成内容用 artifact refs,不内联。
|
|
47
47
|
|
|
48
|
+
## 计划 / 设计验证口径
|
|
49
|
+
|
|
50
|
+
核对最终实现和计划文档时:
|
|
51
|
+
|
|
52
|
+
- 实际代码改动应能从 `proposal.md` 的 `## Impact`、`design.md` 的关键决策或已完成 task 找到合理解释;无法解释的用户可见行为、新能力或大范围改动应使用 `verdict:"fail"`
|
|
53
|
+
- `tasks.md` 在执行期间不应被改写计划内容;除目标 checkbox 被完成命令勾选外,新增任务、改任务含义或把未完成工作藏进普通说明,都应视为证明缺口
|
|
54
|
+
- 已完成 TDD task 的 RED/GREEN 以 `record test-run` 证据为准,不以 `tasks.md` 的文字描述为准
|
|
55
|
+
- 对每个已完成 TDD task,核对同一个 `task_completed.attempt_id` 下是否同时存在 RED/characterization 和 GREEN;新证据必须带同一 `attempt_id`
|
|
56
|
+
- 缺少 `attempt_id`、只靠 `task_structure_digest` 匹配的 test-run 只能视为旧数据兼容,不作为新流程“确实跑了红绿验证”的强证明
|
|
57
|
+
- test-run 证据应说明目标测试身份、`test_id`、`command`、`cwd`、`exit_code` 和 `semantic_status`;退出码本身不等于证明,环境错误 / 构建错误不算 RED/GREEN
|
|
58
|
+
- 可追溯性以引擎记录的 test-run 事件、`raw_index` 和 `raw_digest` 为准;额外日志或 test-runner report 只作为补充引用
|
|
59
|
+
|
|
48
60
|
## 输出风格
|
|
49
61
|
|
|
50
62
|
- 所有用户可见输出必须使用简体中文。
|
|
@@ -23,15 +23,19 @@ metadata:
|
|
|
23
23
|
|
|
24
24
|
每个任务的循环:
|
|
25
25
|
|
|
26
|
-
1. **任务开始**:`superspec transition task-start --change "<change>" --task
|
|
26
|
+
1. **任务开始**:`superspec transition task-start --change "<change>" --task <task_id>`
|
|
27
27
|
2. **拿到执行尝试 ID**:从 task-start 的返回结果或 `superspec status` 中读取当前活跃 attempt 的 `attempt_id`
|
|
28
28
|
3. **红灯验证**:写测试,跑测试确认失败,`superspec record test-run --change "<change>" --input <FILE>`
|
|
29
29
|
4. **代码实现**:根据任务写代码实现,保证代码不出现过渡设计以及代码质量
|
|
30
30
|
5. **绿灯验证**:跑测试确认通过,`superspec record test-run --change "<change>" --input <FILE>`
|
|
31
|
-
6. **任务结束标记完成**:`superspec transition task-complete --change "<change>" --task
|
|
31
|
+
6. **任务结束标记完成**:`superspec transition task-complete --change "<change>" --task <task_id>`
|
|
32
32
|
|
|
33
33
|
no-TDD 任务(tdd_required:false + no_tdd_reason)跳过 RED/GREEN。
|
|
34
34
|
|
|
35
|
+
只执行 `tasks.md` 中顶格 checkbox 行里的 `<task_id>`,例如 `1.1` 或 `TASK-001.1`。Markdown 标题只是分组,不传给 `task-start` / `task-complete`;普通 bullet 只是说明,不单独成为工作流执行单元。
|
|
36
|
+
|
|
37
|
+
`tasks.md` 不写 RED/GREEN 命令、断言或预期输出。RED/GREEN 的真实证明来自 apply 阶段实际执行后登记的 `record test-run`。
|
|
38
|
+
|
|
35
39
|
## test-run 输入格式
|
|
36
40
|
|
|
37
41
|
```json
|
|
@@ -50,10 +54,18 @@ no-TDD 任务(tdd_required:false + no_tdd_reason)跳过 RED/GREEN。
|
|
|
50
54
|
- `attempt_id`:从 task-start 结果获取,确保 RED/GREEN 绑定到正确的执行尝试
|
|
51
55
|
- `semantic_status`:`expected_failure`(RED)/ `expected_success`(GREEN)/ `characterization_pass`
|
|
52
56
|
- `task_structure_digest`:tasks.md 复选框归一化后的 sha256(引擎计算,你不需要手动算)
|
|
57
|
+
- 新产生的 TDD 证据必须带当前 `attempt_id`;缺少 `attempt_id`、只靠 `task_structure_digest` 匹配的 test-run 仅用于旧数据兼容,不作为新流程强证明
|
|
58
|
+
- `test_id`、`command`、`cwd`、`exit_code`、`semantic_status` 和目标测试身份必须能说明目标测试确实运行;退出码本身不等于证明
|
|
59
|
+
- 可追溯证据以引擎记录的 test-run 事件为准;如有额外日志或 test-runner report,可作为补充引用,不作为必填字段
|
|
53
60
|
|
|
54
61
|
## Guardrails
|
|
55
62
|
|
|
56
63
|
- 只改 tasks.md 里本任务范围相关的文件
|
|
64
|
+
- 需要判断影响范围或改动原因不自明时,参考 `proposal.md` 的 `## Impact`,但不要把它当作路径白名单
|
|
65
|
+
- 编码时发现未列入影响范围的文件,如果从 diff 或引用链能直接解释为同一任务下的局部引用、测试辅助或机械连带改动,可以继续
|
|
66
|
+
- 如果发现新增能力、用户可见行为、明显新增影响范围或原因不自明,停止扩大实现并报告给主流程;不要在 apply 阶段补改 `proposal.md`
|
|
67
|
+
- 不修改 `proposal.md`、`design.md`、`specs/**` 或 `.superspec/**`
|
|
68
|
+
- active attempt 期间不要修改 `tasks.md` 中除 `task-complete` 自动勾选目标 checkbox 外的内容
|
|
57
69
|
- 不跳过 RED 直接写 GREEN
|
|
58
70
|
- 退出码 0 ≠ 测试通过——semantic_status 才是证据
|
|
59
71
|
- 环境错误 / 构建失败不算 RED 或 GREEN
|
|
@@ -29,6 +29,25 @@ next 返回 `ask_user` 说明 discovery 不完整或有未确认问题——向
|
|
|
29
29
|
2. **写 discovery.md**:
|
|
30
30
|
3. **澄清歧义**:有阻塞歧义时向用户提问
|
|
31
31
|
|
|
32
|
+
## 探索分工
|
|
33
|
+
|
|
34
|
+
主会话负责广度:理解用户需求、提出探索问题、汇总 discovery、判断哪些问题必须问用户。
|
|
35
|
+
|
|
36
|
+
涉及多个文件、模块、入口或文件类型时,使用 `explore` subagent 做只读深扫。以下情况也应使用:
|
|
37
|
+
|
|
38
|
+
- 当前行为不清楚
|
|
39
|
+
- 涉及状态机、公共 API、数据格式、测试策略、权限、迁移或发布流程
|
|
40
|
+
- 影响范围可能大于用户表述
|
|
41
|
+
|
|
42
|
+
可跳过 subagent 的场景:
|
|
43
|
+
|
|
44
|
+
- 纯文档
|
|
45
|
+
- 明显 typo
|
|
46
|
+
- 单文件机械小修
|
|
47
|
+
- 明确无代码影响的需求
|
|
48
|
+
|
|
49
|
+
跳过时在 discovery 中说明原因。`explore` subagent 只输出代码/文档事实、文件行号锚点、隐性约束、影响范围候选、风险和需要主流程确认的问题;不写方案、不写业务代码、不替主流程做决策。
|
|
50
|
+
|
|
32
51
|
## discovery.md 格式
|
|
33
52
|
|
|
34
53
|
写入 `openspec/changes/<change>/.superspec/artifacts/discovery.md`:
|
|
@@ -36,14 +55,17 @@ next 返回 `ask_user` 说明 discovery 不完整或有未确认问题——向
|
|
|
36
55
|
```markdown
|
|
37
56
|
# Discovery
|
|
38
57
|
|
|
39
|
-
##
|
|
40
|
-
|
|
58
|
+
## 当前代码事实
|
|
59
|
+
- src/path.ts:10 当前系统怎么工作
|
|
41
60
|
|
|
42
|
-
##
|
|
43
|
-
|
|
61
|
+
## 需求理解
|
|
62
|
+
(用户目标和当前实现之间的差异)
|
|
63
|
+
|
|
64
|
+
## 影响范围候选
|
|
65
|
+
- src/path.ts:10 可能受影响的代码表面和相邻风险
|
|
44
66
|
|
|
45
67
|
## 风险和边界
|
|
46
|
-
|
|
68
|
+
(技术风险、依赖、兼容性;尽量绑定代码或文档锚点)
|
|
47
69
|
|
|
48
70
|
## 待确认问题
|
|
49
71
|
- [ ] 问题1的描述
|
|
@@ -51,6 +73,9 @@ next 返回 `ask_user` 说明 discovery 不完整或有未确认问题——向
|
|
|
51
73
|
```
|
|
52
74
|
|
|
53
75
|
**重要**:`- [ ]` 标记的待确认问题必须全部解决(用户确认后改为 `- [x]` 或删除),否则工作流引擎会阻止推进到 propose。
|
|
76
|
+
只有 `## 待确认问题` 段落内的 `- [ ]` 表示阻塞确认项。其他段落列事实、风险或影响范围时使用普通 bullet,不要用 checklist。
|
|
77
|
+
|
|
78
|
+
代码影响型需求的 `当前代码事实`、`影响范围候选`、`风险和边界` 应尽量包含 `path:line` 锚点。纯文档、配置或新文件任务没有代码锚点时,写明 `N/A` 理由并引用相关文档、配置或需求来源。
|
|
54
79
|
|
|
55
80
|
## Guardrails
|
|
56
81
|
|
|
@@ -31,28 +31,63 @@ next 返回需要审查时,先按返回的审查说明完成对应审查,再
|
|
|
31
31
|
## 本阶段做什么
|
|
32
32
|
|
|
33
33
|
### proposal.md
|
|
34
|
-
|
|
34
|
+
使用 OpenSpec proposal 原生结构。正文使用简体中文。
|
|
35
|
+
|
|
36
|
+
SuperSpec 只增加一个轻量要求:在 OpenSpec 原生 `## Impact` 段落中,必须能看出受影响范围和原因。推荐写成:
|
|
37
|
+
|
|
38
|
+
```markdown
|
|
39
|
+
| Area | Reason |
|
|
40
|
+
|---|---|
|
|
41
|
+
| src/review.ts | 需要核对 review verifier 如何绑定文档和执行证据 |
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
规则:
|
|
45
|
+
- `proposal.md` 说明为什么要做、做什么、能力变化和影响范围
|
|
46
|
+
- `Area` 可以写代码区域、API、依赖、系统、配置或文档
|
|
47
|
+
- `Reason` 只解释为什么该范围受影响,不写详细实现方案
|
|
48
|
+
- `Area` 不作为路径白名单
|
|
49
|
+
- 不写任务拆分
|
|
50
|
+
- 只有存在阻塞确认项时才增加 `## 待用户确认`
|
|
35
51
|
|
|
36
52
|
### specs/
|
|
37
53
|
OpenSpec 能力规范增量(`openspec instructions specs` 格式)。
|
|
38
54
|
|
|
39
55
|
### design.md
|
|
40
|
-
|
|
56
|
+
使用 OpenSpec design 原生结构。正文使用简体中文。
|
|
57
|
+
|
|
58
|
+
规则:
|
|
59
|
+
- `design.md` 写技术方案、关键决策、替代方案和风险取舍
|
|
60
|
+
- 不复制 `proposal.md` 的影响范围表
|
|
61
|
+
- 不写任务拆分
|
|
62
|
+
- 只有存在阻塞确认项时才增加 `## 待用户确认`
|
|
41
63
|
|
|
42
64
|
### tasks.md
|
|
43
|
-
|
|
65
|
+
使用 OpenSpec tasks 原生分组结构。每个顶格 checkbox 行是一个 SuperSpec 可执行 task,Markdown 标题只用于分组。
|
|
44
66
|
|
|
45
67
|
```markdown
|
|
46
68
|
# Tasks
|
|
47
69
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
- [ ]
|
|
70
|
+
## Review verifier
|
|
71
|
+
|
|
72
|
+
- [ ] 1.1 检查 verifier 绑定文档 tdd_required:true
|
|
73
|
+
- [ ] 1.2 检查 verifier 绑定执行证据 tdd_required:true
|
|
74
|
+
|
|
75
|
+
## Documentation
|
|
76
|
+
|
|
77
|
+
- [ ] 2.1 更新文档 tdd_required:false no_tdd_reason:documentation-only
|
|
51
78
|
```
|
|
52
79
|
|
|
53
80
|
规则:
|
|
81
|
+
- 标题只分组,不是可执行 task;标题不要包含可执行 task id token,例如不要写 `## 1.1 Review verifier`
|
|
82
|
+
- 顶格 `- [ ] <task_id> ...` 才是可执行 task,`<task_id>` 可以是 `1.1` 或 `TASK-001.1`
|
|
83
|
+
- 每个可执行 task id 必须唯一、稳定
|
|
84
|
+
- 不展示、不推荐缩进 checkbox;task 内部步骤用普通 bullet,不用 checkbox
|
|
54
85
|
- `tdd_required:true`(默认)——改运行时代码/业务逻辑/数据迁移/权限/外部接口
|
|
55
86
|
- `tdd_required:false` + `no_tdd_reason:xxx`——纯文档/配置/机械改名/生成物
|
|
87
|
+
- task 行只标记是否需要 TDD,不写 RED/GREEN 命令、断言或预期输出;实际 RED/GREEN 由 apply 阶段执行,并通过 `record test-run` 绑定到 attempt
|
|
88
|
+
- 一个 task 对应一个可独立验证的行为变化,或一个明确的非行为改动
|
|
89
|
+
- 多个行为变化、多个入口、多个运行时模块混在一起,且不能形成同一个 RED/GREEN 闭环时,应拆开
|
|
90
|
+
- 如果一个 task 需要“顺便”改很多不相邻模块,应在 propose 阶段重新拆分或补充任务,不留到 apply 阶段扩大范围
|
|
56
91
|
|
|
57
92
|
### business-invariants.md
|
|
58
93
|
格式:
|
|
@@ -21,12 +21,23 @@ metadata:
|
|
|
21
21
|
|
|
22
22
|
next 返回需要 verifier 工作项时,先按返回的验证说明执行核对,再用 `superspec record job-submit --change "<change>" --job <JOB> --report <FILE>` 提交验证报告。
|
|
23
23
|
|
|
24
|
+
`record job-submit` 沿用现有 raw 归档:报告追加到 `raw/review-reports.jsonl`,不会为 review gate 新增 raw 文件类型。
|
|
25
|
+
|
|
24
26
|
## 本阶段做什么
|
|
25
27
|
|
|
26
28
|
1. **确认所有任务完成**:review-ready 会检查 tasks.md 无未完成项
|
|
27
29
|
2. **处理 verifier**:核对 proposal + 实现 + 测试契约一致性
|
|
28
30
|
3. **accept**:`superspec transition accept --change "<change>"`
|
|
29
31
|
|
|
32
|
+
## verifier gate 规则
|
|
33
|
+
|
|
34
|
+
- `review-ready` 首次运行会持久化审查策略;后续 risk 参数不会覆盖首次策略。
|
|
35
|
+
- `minimal` 不要求 verifier;`normal` 和 `strict` 要求 verifier。
|
|
36
|
+
- 当前 MVP 中 `strict` 等同 `normal`,不启用额外检查。
|
|
37
|
+
- 如果 review 状态缺少审查策略,先运行 `review-ready` 补策略,不直接 accept。
|
|
38
|
+
- verifier 工作项 packet 会包含绑定文档和 `review_evidence_digest`,用于确认审查对应的任务完成与 RED/GREEN 证据版本。
|
|
39
|
+
- verifier 通过后,如果绑定文档或已登记执行证据版本变化,需要重新运行 `review-ready` 创建新的 verifier。
|
|
40
|
+
|
|
30
41
|
## Guardrails
|
|
31
42
|
|
|
32
43
|
- 不改业务代码(审查阶段只读)
|