@peterxiaoyang/superspec 0.1.22 → 0.1.23
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/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.23",
|
|
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",
|
|
@@ -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
|
- 不改业务代码(审查阶段只读)
|